@thirdweb-dev/storage
Advanced tools
Comparing version 0.0.0-dev-ec891b4-20230807224410 to 0.0.0-dev-ed17781-20240229185724
import { GatewayUrls } from "../types"; | ||
/** | ||
* @internal | ||
* @param url | ||
* @param url - the url to check | ||
* @returns | ||
@@ -27,3 +27,3 @@ */ | ||
*/ | ||
export declare function getGatewayUrlForCid(gatewayUrl: string, cid: string): string; | ||
export declare function getGatewayUrlForCid(gatewayUrl: string, cid: string, clientId?: string): string; | ||
/** | ||
@@ -30,0 +30,0 @@ * @internal |
@@ -31,3 +31,3 @@ /// <reference types="node" /> | ||
*/ | ||
export declare function replaceSchemeWithGatewayUrl(uri: string, gatewayUrls: GatewayUrls, index?: number): string | undefined; | ||
export declare function replaceSchemeWithGatewayUrl(uri: string, gatewayUrls: GatewayUrls, index?: number, clientId?: string): string | undefined; | ||
/** | ||
@@ -40,3 +40,3 @@ * @internal | ||
*/ | ||
export declare function replaceObjectSchemesWithGatewayUrls<TData = unknown>(data: TData, gatewayUrls: GatewayUrls): TData; | ||
export declare function replaceObjectSchemesWithGatewayUrls<TData = unknown>(data: TData, gatewayUrls: GatewayUrls, clientId?: string): TData; | ||
/** | ||
@@ -43,0 +43,0 @@ * @internal |
@@ -1,2 +0,2 @@ | ||
import { GatewayUrls, IStorageDownloader, IpfsDownloaderOptions } from "../../types"; | ||
import { GatewayUrls, IStorageDownloader, IpfsDownloaderOptions, SingleDownloadOptions } from "../../types"; | ||
/** | ||
@@ -22,7 +22,10 @@ * Default downloader used - handles downloading from all schemes specified in the gateway URLs configuration. | ||
export declare class StorageDownloader implements IStorageDownloader { | ||
DEFAULT_TIMEOUT_IN_SECONDS: number; | ||
DEFAULT_MAX_RETRIES: number; | ||
private secretKey?; | ||
private clientId?; | ||
private defaultTimeout; | ||
constructor(options: IpfsDownloaderOptions); | ||
download(uri: string, gatewayUrls: GatewayUrls, attempts?: number): Promise<Response>; | ||
download(uri: string, gatewayUrls: GatewayUrls, options?: SingleDownloadOptions, attempts?: number): Promise<Response>; | ||
} | ||
//# sourceMappingURL=storage-downloader.d.ts.map |
@@ -1,2 +0,2 @@ | ||
import { GatewayUrls, IThirdwebStorage, IpfsUploadBatchOptions, ThirdwebStorageOptions, UploadOptions } from "../types"; | ||
import { GatewayUrls, IThirdwebStorage, IpfsUploadBatchOptions, SingleDownloadOptions, ThirdwebStorageOptions, UploadOptions } from "../types"; | ||
/** | ||
@@ -40,2 +40,3 @@ * Upload and download files from decentralized storage systems. | ||
private gatewayUrls; | ||
private clientId?; | ||
constructor(options?: ThirdwebStorageOptions<T>); | ||
@@ -68,3 +69,3 @@ /** | ||
*/ | ||
download(url: string): Promise<Response>; | ||
download(url: string, options?: SingleDownloadOptions): Promise<Response>; | ||
/** | ||
@@ -83,3 +84,3 @@ * Downloads JSON data from any URL scheme. | ||
*/ | ||
downloadJSON<TJSON = any>(url: string): Promise<TJSON>; | ||
downloadJSON<TJSON = any>(url: string, options?: SingleDownloadOptions): Promise<TJSON>; | ||
/** | ||
@@ -91,3 +92,3 @@ * Upload arbitrary file or JSON data using the configured decentralized storage system. | ||
* @param options - Options to pass through to the storage uploader class | ||
* @returns - The URI of the uploaded data | ||
* @returns The URI of the uploaded data | ||
* | ||
@@ -112,3 +113,3 @@ * @example | ||
* @param options - Options to pass through to the storage uploader class | ||
* @returns - The URIs of the uploaded data | ||
* @returns The URIs of the uploaded data | ||
* | ||
@@ -115,0 +116,0 @@ * @example |
@@ -12,3 +12,3 @@ /** | ||
*/ | ||
download(url: string, gatewayUrls?: GatewayUrls): Promise<Response>; | ||
download(url: string, gatewayUrls?: GatewayUrls, options?: SingleDownloadOptions): Promise<Response>; | ||
} | ||
@@ -29,3 +29,17 @@ /** | ||
clientId?: string; | ||
/** | ||
* Optional timeout in seconds for the download request, overrides the default timeout | ||
*/ | ||
timeoutInSeconds?: number; | ||
}; | ||
export type SingleDownloadOptions = { | ||
/** | ||
* Optional timeout in seconds for the download request, overrides the default timeout | ||
*/ | ||
timeoutInSeconds?: number; | ||
/** | ||
* Number of different gateways to attempt on failure | ||
*/ | ||
maxRetries?: number; | ||
}; | ||
/** | ||
@@ -32,0 +46,0 @@ * @public |
@@ -1,2 +0,2 @@ | ||
import { GatewayUrls, IStorageDownloader } from "./download"; | ||
import { GatewayUrls, IStorageDownloader, SingleDownloadOptions } from "./download"; | ||
import { IStorageUploader, UploadOptions } from "./upload"; | ||
@@ -13,3 +13,3 @@ export type ThirdwebStorageOptions<T extends UploadOptions> = { | ||
resolveScheme(url: string): string; | ||
download(url: string): Promise<Response>; | ||
download(url: string, options?: SingleDownloadOptions): Promise<Response>; | ||
downloadJSON<TJSON = any>(url: string): Promise<TJSON>; | ||
@@ -16,0 +16,0 @@ upload(data: any, options?: { |
@@ -6,3 +6,3 @@ 'use strict'; | ||
var CIDTool = require('cid-tool'); | ||
var fetch = require('cross-fetch'); | ||
var crypto = require('@thirdweb-dev/crypto'); | ||
var FormData = require('form-data'); | ||
@@ -14,3 +14,2 @@ var uuid = require('uuid'); | ||
var CIDTool__default = /*#__PURE__*/_interopDefault(CIDTool); | ||
var fetch__default = /*#__PURE__*/_interopDefault(fetch); | ||
var FormData__default = /*#__PURE__*/_interopDefault(FormData); | ||
@@ -29,2 +28,3 @@ | ||
const TW_HOSTNAME_SUFFIX = ".ipfscdn.io"; | ||
const TW_STAGINGHOSTNAME_SUFFIX = ".thirdwebstorage-staging.com"; | ||
const TW_GATEWAY_URLS = [`https://{clientId}${TW_HOSTNAME_SUFFIX}/ipfs/{cid}/{path}`]; | ||
@@ -34,7 +34,13 @@ | ||
* @internal | ||
* @param url | ||
* @param url - the url to check | ||
* @returns | ||
*/ | ||
function isTwGatewayUrl(url) { | ||
return new URL(url).hostname.endsWith(TW_HOSTNAME_SUFFIX); | ||
const hostname = new URL(url).hostname; | ||
const isProd = hostname.endsWith(TW_HOSTNAME_SUFFIX); | ||
if (isProd) { | ||
return true; | ||
} | ||
// fall back to also handle staging urls | ||
return hostname.endsWith(TW_STAGINGHOSTNAME_SUFFIX); | ||
} | ||
@@ -76,3 +82,3 @@ const PUBLIC_GATEWAY_URLS = ["https://{cid}.ipfs.cf-ipfs.com/{path}", "https://{cid}.ipfs.dweb.link/{path}", "https://ipfs.io/ipfs/{cid}/{path}", "https://cloudflare-ipfs.com/ipfs/{cid}/{path}", "https://{cid}.ipfs.w3s.link/{path}", "https://w3s.link/ipfs/{cid}/{path}", "https://nftstorage.link/ipfs/{cid}/{path}", "https://gateway.pinata.cloud/ipfs/{cid}/{path}"]; | ||
*/ | ||
function getGatewayUrlForCid(gatewayUrl, cid) { | ||
function getGatewayUrlForCid(gatewayUrl, cid, clientId) { | ||
const parts = cid.split("/"); | ||
@@ -96,2 +102,9 @@ const hash = convertCidToV1(parts[0]); | ||
} | ||
// if the URL contains the {clientId} token, replace it with the client ID | ||
if (gatewayUrl.includes("{clientId}")) { | ||
if (!clientId) { | ||
throw new Error("Cannot use {clientId} in gateway URL without providing a client ID"); | ||
} | ||
url = url.replace("{clientId}", clientId); | ||
} | ||
return url; | ||
@@ -118,6 +131,3 @@ } | ||
} | ||
// this is on purpose because we're using the crypto module only in node | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const crypto = require("crypto"); | ||
const hashedSecretKey = crypto.createHash("sha256").update(secretKey).digest("hex"); | ||
const hashedSecretKey = crypto.sha256HexSync(secretKey); | ||
const derivedClientId = hashedSecretKey.slice(0, 32); | ||
@@ -141,3 +151,3 @@ return url.replace("{clientId}", derivedClientId); | ||
function convertCidToV1(cid) { | ||
let normalized; | ||
let normalized = ''; | ||
try { | ||
@@ -262,2 +272,3 @@ const hash = cid.split("/")[0]; | ||
let index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
let clientId = arguments.length > 3 ? arguments[3] : undefined; | ||
const scheme = Object.keys(gatewayUrls).find(s => uri.startsWith(s)); | ||
@@ -272,3 +283,9 @@ const schemeGatewayUrls = scheme ? gatewayUrls[scheme] : []; | ||
const path = uri.replace(scheme, ""); | ||
return getGatewayUrlForCid(schemeGatewayUrls[index], path); | ||
try { | ||
const gatewayUrl = getGatewayUrlForCid(schemeGatewayUrls[index], path, clientId); | ||
return gatewayUrl; | ||
} catch (err) { | ||
console.warn(`The IPFS uri: ${path} is not valid.`); | ||
return undefined; | ||
} | ||
} | ||
@@ -304,5 +321,5 @@ | ||
*/ | ||
function replaceObjectSchemesWithGatewayUrls(data, gatewayUrls) { | ||
function replaceObjectSchemesWithGatewayUrls(data, gatewayUrls, clientId) { | ||
if (typeof data === "string") { | ||
return replaceSchemeWithGatewayUrl(data, gatewayUrls); | ||
return replaceSchemeWithGatewayUrl(data, gatewayUrls, 0, clientId); | ||
} | ||
@@ -317,7 +334,7 @@ if (typeof data === "object") { | ||
if (Array.isArray(data)) { | ||
return data.map(entry => replaceObjectSchemesWithGatewayUrls(entry, gatewayUrls)); | ||
return data.map(entry => replaceObjectSchemesWithGatewayUrls(entry, gatewayUrls, clientId)); | ||
} | ||
return Object.fromEntries(Object.entries(data).map(_ref2 => { | ||
let [key, value] = _ref2; | ||
return [key, replaceObjectSchemesWithGatewayUrls(value, gatewayUrls)]; | ||
return [key, replaceObjectSchemesWithGatewayUrls(value, gatewayUrls, clientId)]; | ||
})); | ||
@@ -379,3 +396,148 @@ } | ||
var pkg = { | ||
name: "@thirdweb-dev/storage", | ||
version: "2.0.9", | ||
main: "dist/thirdweb-dev-storage.cjs.js", | ||
module: "dist/thirdweb-dev-storage.esm.js", | ||
exports: { | ||
".": { | ||
module: "./dist/thirdweb-dev-storage.esm.js", | ||
"default": "./dist/thirdweb-dev-storage.cjs.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
repository: "https://github.com/thirdweb-dev/js/tree/main/packages/storage", | ||
author: "thirdweb eng <eng@thirdweb.com>", | ||
license: "Apache-2.0", | ||
sideEffects: false, | ||
scripts: { | ||
format: "prettier --write 'src/**/*'", | ||
lint: "eslint src/ && bunx publint --strict --level warning", | ||
fix: "eslint src/ --fix", | ||
clean: "rm -rf dist/", | ||
build: "tsc && preconstruct build", | ||
"test:all": "NODE_ENV=test SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000 --parallel './test/**/*.test.ts'", | ||
test: "pnpm test:all", | ||
"test:single": "NODE_ENV=test SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000", | ||
push: "yalc push", | ||
typedoc: "node scripts/typedoc.mjs" | ||
}, | ||
files: [ | ||
"dist/" | ||
], | ||
preconstruct: { | ||
exports: true | ||
}, | ||
devDependencies: { | ||
"@babel/preset-env": "^7.23.8", | ||
"@babel/preset-typescript": "^7.23.3", | ||
"@microsoft/api-documenter": "^7.22.30", | ||
"@microsoft/api-extractor": "^7.36.3", | ||
"@microsoft/tsdoc": "^0.14.1", | ||
"@preconstruct/cli": "2.7.0", | ||
"@swc-node/register": "^1.6.8", | ||
"@thirdweb-dev/tsconfig": "workspace:*", | ||
"@types/chai": "^4.3.5", | ||
"@types/mocha": "^10.0.0", | ||
"@types/uuid": "^9.0.5", | ||
"@typescript-eslint/eslint-plugin": "^6.2.0", | ||
"@typescript-eslint/parser": "^6.19.1", | ||
chai: "^4.3.6", | ||
eslint: "^8.56.0", | ||
"eslint-config-thirdweb": "workspace:*", | ||
"eslint-plugin-tsdoc": "^0.2.16", | ||
esm: "^3.2.25", | ||
mocha: "^10.2.0", | ||
rimraf: "^3.0.2", | ||
"typedoc-gen": "workspace:*", | ||
typescript: "^5.3.3" | ||
}, | ||
dependencies: { | ||
"@thirdweb-dev/crypto": "workspace:*", | ||
"cid-tool": "^3.0.0", | ||
"form-data": "^4.0.0", | ||
uuid: "^9.0.1" | ||
}, | ||
engines: { | ||
node: ">=18" | ||
} | ||
}; | ||
/** | ||
* @internal | ||
* | ||
* The code below comes from the package https://github.com/DamonOehlman/detect-browser | ||
*/ | ||
const operatingSystemRules = [["iOS", /iP(hone|od|ad)/], ["Android OS", /Android/], ["BlackBerry OS", /BlackBerry|BB10/], ["Windows Mobile", /IEMobile/], ["Amazon OS", /Kindle/], ["Windows 3.11", /Win16/], ["Windows 95", /(Windows 95)|(Win95)|(Windows_95)/], ["Windows 98", /(Windows 98)|(Win98)/], ["Windows 2000", /(Windows NT 5.0)|(Windows 2000)/], ["Windows XP", /(Windows NT 5.1)|(Windows XP)/], ["Windows Server 2003", /(Windows NT 5.2)/], ["Windows Vista", /(Windows NT 6.0)/], ["Windows 7", /(Windows NT 6.1)/], ["Windows 8", /(Windows NT 6.2)/], ["Windows 8.1", /(Windows NT 6.3)/], ["Windows 10", /(Windows NT 10.0)/], ["Windows ME", /Windows ME/], ["Windows CE", /Windows CE|WinCE|Microsoft Pocket Internet Explorer/], ["Open BSD", /OpenBSD/], ["Sun OS", /SunOS/], ["Chrome OS", /CrOS/], ["Linux", /(Linux)|(X11)/], ["Mac OS", /(Mac_PowerPC)|(Macintosh)/], ["QNX", /QNX/], ["BeOS", /BeOS/], ["OS/2", /OS\/2/]]; | ||
function detectOS(ua) { | ||
for (let ii = 0, count = operatingSystemRules.length; ii < count; ii++) { | ||
const result = operatingSystemRules[ii]; | ||
if (!result) { | ||
continue; | ||
} | ||
const [os, regex] = result; | ||
const match = regex.exec(ua); | ||
if (match) { | ||
return os; | ||
} | ||
} | ||
return null; | ||
} | ||
function getOperatingSystem() { | ||
if (typeof navigator !== "undefined" && navigator.product === "ReactNative") { | ||
return ""; | ||
} else if (typeof window !== "undefined") { | ||
const userAgent = navigator.userAgent; | ||
return detectOS(userAgent) || ""; | ||
} else { | ||
return process.platform; | ||
} | ||
} | ||
function setAnalyticsHeaders(headers) { | ||
const globals = getAnalyticsGlobals(); | ||
headers["x-sdk-version"] = globals.x_sdk_version; | ||
headers["x-sdk-name"] = globals.x_sdk_name; | ||
headers["x-sdk-platform"] = globals.x_sdk_platform; | ||
headers["x-sdk-os"] = globals.x_sdk_os; | ||
} | ||
function setAnalyticsHeadersForXhr(xhr) { | ||
const globals = getAnalyticsGlobals(); | ||
xhr.setRequestHeader("x-sdk-version", globals.x_sdk_version); | ||
xhr.setRequestHeader("x-sdk-os", globals.x_sdk_os); | ||
xhr.setRequestHeader("x-sdk-name", globals.x_sdk_name); | ||
xhr.setRequestHeader("x-sdk-platform", globals.x_sdk_platform); | ||
xhr.setRequestHeader("x-bundle-id", globals.app_bundle_id); | ||
} | ||
function getAnalyticsGlobals() { | ||
if (typeof globalThis === "undefined") { | ||
return { | ||
x_sdk_name: pkg.name, | ||
x_sdk_platform: getPlatform(), | ||
x_sdk_version: pkg.version, | ||
x_sdk_os: getOperatingSystem(), | ||
app_bundle_id: undefined | ||
}; | ||
} | ||
if (globalThis.X_SDK_NAME === undefined) { | ||
globalThis.X_SDK_NAME = pkg.name; | ||
globalThis.X_SDK_PLATFORM = getPlatform(); | ||
globalThis.X_SDK_VERSION = pkg.version; | ||
globalThis.X_SDK_OS = getOperatingSystem(); | ||
globalThis.APP_BUNDLE_ID = undefined; | ||
} | ||
return { | ||
x_sdk_name: globalThis.X_SDK_NAME, | ||
x_sdk_platform: globalThis.X_SDK_PLATFORM, | ||
x_sdk_version: globalThis.X_SDK_VERSION, | ||
x_sdk_os: globalThis.X_SDK_OS, | ||
app_bundle_id: globalThis.APP_BUNDLE_ID || "" // if react, this will be empty | ||
}; | ||
} | ||
function getPlatform() { | ||
return typeof navigator !== "undefined" && navigator.product === "ReactNative" ? "mobile" : typeof window !== "undefined" ? "browser" : "node"; | ||
} | ||
/** | ||
* Default downloader used - handles downloading from all schemes specified in the gateway URLs configuration. | ||
@@ -400,12 +562,16 @@ * | ||
class StorageDownloader { | ||
DEFAULT_TIMEOUT_IN_SECONDS = 60; | ||
DEFAULT_MAX_RETRIES = 3; | ||
constructor(options) { | ||
this.secretKey = options.secretKey; | ||
this.clientId = options.clientId; | ||
this.defaultTimeout = options.timeoutInSeconds || this.DEFAULT_TIMEOUT_IN_SECONDS; | ||
} | ||
async download(uri, gatewayUrls) { | ||
let attempts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
if (attempts > 3) { | ||
async download(uri, gatewayUrls, options) { | ||
let attempts = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; | ||
const maxRetries = options?.maxRetries || this.DEFAULT_MAX_RETRIES; | ||
if (attempts > maxRetries) { | ||
console.error("[FAILED_TO_DOWNLOAD_ERROR] Failed to download from URI - too many attempts failed."); | ||
// return a 404 response to avoid retrying | ||
return new fetch.Response(JSON.stringify({ | ||
return new Response(JSON.stringify({ | ||
error: "Not Found" | ||
@@ -421,7 +587,7 @@ }), { | ||
// Replace recognized scheme with the highest priority gateway URL that hasn't already been attempted | ||
let resolvedUri = replaceSchemeWithGatewayUrl(uri, gatewayUrls, attempts); | ||
let resolvedUri = replaceSchemeWithGatewayUrl(uri, gatewayUrls, attempts, this.clientId); | ||
// If every gateway URL we know about for the designated scheme has been tried (via recursion) and failed, throw an error | ||
if (!resolvedUri) { | ||
console.error("[FAILED_TO_DOWNLOAD_ERROR] Unable to download from URI - all gateway URLs failed to respond."); | ||
return new fetch.Response(JSON.stringify({ | ||
return new Response(JSON.stringify({ | ||
error: "Not Found" | ||
@@ -439,2 +605,3 @@ }), { | ||
if (isTwGatewayUrl(resolvedUri)) { | ||
const bundleId = getAnalyticsGlobals().app_bundle_id; | ||
if (this.secretKey) { | ||
@@ -445,11 +612,9 @@ headers = { | ||
} else if (this.clientId) { | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
resolvedUri = resolvedUri + `?bundleId=${globalThis.APP_BUNDLE_ID}`; | ||
if (!resolvedUri.includes("bundleId")) { | ||
resolvedUri = resolvedUri + (bundleId ? `?bundleId=${bundleId}` : ""); | ||
} | ||
headers = { | ||
"x-client-Id": this.clientId | ||
}; | ||
headers["x-client-Id"] = this.clientId; | ||
} | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
headers = { | ||
@@ -460,10 +625,19 @@ ...headers, | ||
} | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
headers = { | ||
...headers, | ||
authorization: `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}` | ||
}; | ||
headers["x-authorize-wallet"] = "true"; | ||
} | ||
setAnalyticsHeaders(headers); | ||
} | ||
if (isTooManyRequests(resolvedUri)) { | ||
// skip the request if we're getting too many request error from the gateway | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
const controller = new AbortController(); | ||
const timeout = setTimeout(() => controller.abort(), 5000); | ||
const resOrErr = await fetch__default["default"](resolvedUri, { | ||
const timeoutInSeconds = options?.timeoutInSeconds || this.defaultTimeout; | ||
const timeout = setTimeout(() => controller.abort(), timeoutInSeconds * 1000); | ||
const resOrErr = await fetch(resolvedUri, { | ||
headers, | ||
@@ -478,3 +652,3 @@ signal: controller.signal | ||
// early exit if we don't have a status code | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
throw new Error(`Request timed out after ${timeoutInSeconds} seconds. ${isTwGatewayUrl(resolvedUri) ? "You can update the timeoutInSeconds option to increase the timeout." : "You're using a public IPFS gateway, pass in a clientId or secretKey for a reliable IPFS gateway."}`); | ||
} | ||
@@ -490,3 +664,3 @@ | ||
// Since the current gateway failed, recursively try the next one we know about | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
@@ -509,6 +683,8 @@ if (resOrErr.status === 410) { | ||
// 5xx - Server Errors | ||
if (resOrErr.status !== 408 && resOrErr.status !== 429 && resOrErr.status < 500) ; | ||
if (resOrErr.status !== 408 && resOrErr.status !== 429 && resOrErr.status < 500) { | ||
return resOrErr; | ||
} | ||
// Since the current gateway failed, recursively try the next one we know about | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
@@ -715,3 +891,3 @@ } | ||
} else { | ||
return resolve(fileNames.map(name => `ipfs://${cid}/${name}`)); | ||
return resolve(fileNames.map(n => `ipfs://${cid}/${n}`)); | ||
} | ||
@@ -725,3 +901,3 @@ } | ||
if (xhr.readyState !== 0 && xhr.readyState !== 4 || xhr.status === 0) { | ||
return reject(new Error("This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.")); | ||
return reject(new Error("Upload failed due to a network error.")); | ||
} | ||
@@ -736,10 +912,14 @@ return reject(new Error("Unknown upload error occured")); | ||
} | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
xhr.setRequestHeader("x-bundle-id", globalThis.APP_BUNDLE_ID); | ||
} | ||
setAnalyticsHeadersForXhr(xhr); | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
// if we have a authorization token on global context then add that to the headers, this is for the dashboard. | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
xhr.setRequestHeader("authorization", `Bearer ${globalThis.TW_AUTH_TOKEN}`); | ||
} | ||
// CLI auth token | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
xhr.setRequestHeader("authorization", `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}`); | ||
xhr.setRequestHeader("x-authorize-wallet", `true`); | ||
} | ||
xhr.send(form); | ||
@@ -759,12 +939,14 @@ }); | ||
// if we have a bundle id on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
headers["x-bundle-id"] = globalThis.APP_BUNDLE_ID; | ||
// if we have a authorization token on global context then add that to the headers, this is for the dashboard. | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
headers["authorization"] = `Bearer ${globalThis.TW_AUTH_TOKEN}`; | ||
} | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
headers["authorization"] = `Bearer ${globalThis.TW_AUTH_TOKEN}`; | ||
// CLI auth token | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
headers["authorization"] = `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}`; | ||
headers["x-authorize-wallet"] = "true"; | ||
} | ||
const res = await fetch__default["default"](`${this.uploadServerUrl}/ipfs/upload`, { | ||
setAnalyticsHeaders(headers); | ||
const res = await fetch(`${this.uploadServerUrl}/ipfs/upload`, { | ||
method: "POST", | ||
@@ -777,10 +959,12 @@ headers: { | ||
}); | ||
const body = await res.json(); | ||
if (!res.ok) { | ||
console.warn(body); | ||
throw new Error("Failed to upload files to IPFS"); | ||
if (res.status === 401) { | ||
throw new Error("Unauthorized - You don't have permission to use this service."); | ||
} | ||
throw new Error(`Failed to upload files to IPFS - ${res.status} - ${res.statusText} - ${await res.text()}`); | ||
} | ||
const body = await res.json(); | ||
const cid = body.IpfsHash; | ||
if (!cid) { | ||
throw new Error("Failed to upload files to IPFS"); | ||
throw new Error("Failed to upload files to IPFS - Bad CID"); | ||
} | ||
@@ -841,2 +1025,3 @@ if (options?.uploadWithoutDirectory) { | ||
this.gatewayUrls = prepareGatewayUrls(parseGatewayUrls(options?.gatewayUrls), options?.clientId, options?.secretKey); | ||
this.clientId = options?.clientId; | ||
} | ||
@@ -858,3 +1043,3 @@ | ||
resolveScheme(url) { | ||
return replaceSchemeWithGatewayUrl(url, this.gatewayUrls); | ||
return replaceSchemeWithGatewayUrl(url, this.gatewayUrls, 0, this.clientId); | ||
} | ||
@@ -874,4 +1059,4 @@ | ||
*/ | ||
async download(url) { | ||
return this.downloader.download(url, this.gatewayUrls); | ||
async download(url, options) { | ||
return this.downloader.download(url, this.gatewayUrls, options); | ||
} | ||
@@ -892,8 +1077,8 @@ | ||
*/ | ||
async downloadJSON(url) { | ||
const res = await this.download(url); | ||
async downloadJSON(url, options) { | ||
const res = await this.download(url, options); | ||
// If we get a JSON object, recursively replace any schemes with gatewayUrls | ||
const json = await res.json(); | ||
return replaceObjectSchemesWithGatewayUrls(json, this.gatewayUrls); | ||
return replaceObjectSchemesWithGatewayUrls(json, this.gatewayUrls, this.clientId); | ||
} | ||
@@ -907,3 +1092,3 @@ | ||
* @param options - Options to pass through to the storage uploader class | ||
* @returns - The URI of the uploaded data | ||
* @returns The URI of the uploaded data | ||
* | ||
@@ -932,3 +1117,3 @@ * @example | ||
* @param options - Options to pass through to the storage uploader class | ||
* @returns - The URIs of the uploaded data | ||
* @returns The URIs of the uploaded data | ||
* | ||
@@ -999,3 +1184,3 @@ * @example | ||
// Ex: used for Solana, where services don't resolve schemes for you, so URLs must be usable by default | ||
cleaned = replaceObjectSchemesWithGatewayUrls(cleaned, this.gatewayUrls); | ||
cleaned = replaceObjectSchemesWithGatewayUrls(cleaned, this.gatewayUrls, this.clientId); | ||
} | ||
@@ -1002,0 +1187,0 @@ return cleaned; |
@@ -6,3 +6,3 @@ 'use strict'; | ||
var CIDTool = require('cid-tool'); | ||
var fetch = require('cross-fetch'); | ||
var crypto = require('@thirdweb-dev/crypto'); | ||
var FormData = require('form-data'); | ||
@@ -14,3 +14,2 @@ var uuid = require('uuid'); | ||
var CIDTool__default = /*#__PURE__*/_interopDefault(CIDTool); | ||
var fetch__default = /*#__PURE__*/_interopDefault(fetch); | ||
var FormData__default = /*#__PURE__*/_interopDefault(FormData); | ||
@@ -29,2 +28,3 @@ | ||
const TW_HOSTNAME_SUFFIX = ".ipfscdn.io"; | ||
const TW_STAGINGHOSTNAME_SUFFIX = ".thirdwebstorage-staging.com"; | ||
const TW_GATEWAY_URLS = [`https://{clientId}${TW_HOSTNAME_SUFFIX}/ipfs/{cid}/{path}`]; | ||
@@ -34,7 +34,13 @@ | ||
* @internal | ||
* @param url | ||
* @param url - the url to check | ||
* @returns | ||
*/ | ||
function isTwGatewayUrl(url) { | ||
return new URL(url).hostname.endsWith(TW_HOSTNAME_SUFFIX); | ||
const hostname = new URL(url).hostname; | ||
const isProd = hostname.endsWith(TW_HOSTNAME_SUFFIX); | ||
if (isProd) { | ||
return true; | ||
} | ||
// fall back to also handle staging urls | ||
return hostname.endsWith(TW_STAGINGHOSTNAME_SUFFIX); | ||
} | ||
@@ -76,3 +82,3 @@ const PUBLIC_GATEWAY_URLS = ["https://{cid}.ipfs.cf-ipfs.com/{path}", "https://{cid}.ipfs.dweb.link/{path}", "https://ipfs.io/ipfs/{cid}/{path}", "https://cloudflare-ipfs.com/ipfs/{cid}/{path}", "https://{cid}.ipfs.w3s.link/{path}", "https://w3s.link/ipfs/{cid}/{path}", "https://nftstorage.link/ipfs/{cid}/{path}", "https://gateway.pinata.cloud/ipfs/{cid}/{path}"]; | ||
*/ | ||
function getGatewayUrlForCid(gatewayUrl, cid) { | ||
function getGatewayUrlForCid(gatewayUrl, cid, clientId) { | ||
const parts = cid.split("/"); | ||
@@ -96,2 +102,9 @@ const hash = convertCidToV1(parts[0]); | ||
} | ||
// if the URL contains the {clientId} token, replace it with the client ID | ||
if (gatewayUrl.includes("{clientId}")) { | ||
if (!clientId) { | ||
throw new Error("Cannot use {clientId} in gateway URL without providing a client ID"); | ||
} | ||
url = url.replace("{clientId}", clientId); | ||
} | ||
return url; | ||
@@ -118,6 +131,3 @@ } | ||
} | ||
// this is on purpose because we're using the crypto module only in node | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const crypto = require("crypto"); | ||
const hashedSecretKey = crypto.createHash("sha256").update(secretKey).digest("hex"); | ||
const hashedSecretKey = crypto.sha256HexSync(secretKey); | ||
const derivedClientId = hashedSecretKey.slice(0, 32); | ||
@@ -141,3 +151,3 @@ return url.replace("{clientId}", derivedClientId); | ||
function convertCidToV1(cid) { | ||
let normalized; | ||
let normalized = ''; | ||
try { | ||
@@ -262,2 +272,3 @@ const hash = cid.split("/")[0]; | ||
let index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
let clientId = arguments.length > 3 ? arguments[3] : undefined; | ||
const scheme = Object.keys(gatewayUrls).find(s => uri.startsWith(s)); | ||
@@ -272,3 +283,9 @@ const schemeGatewayUrls = scheme ? gatewayUrls[scheme] : []; | ||
const path = uri.replace(scheme, ""); | ||
return getGatewayUrlForCid(schemeGatewayUrls[index], path); | ||
try { | ||
const gatewayUrl = getGatewayUrlForCid(schemeGatewayUrls[index], path, clientId); | ||
return gatewayUrl; | ||
} catch (err) { | ||
console.warn(`The IPFS uri: ${path} is not valid.`); | ||
return undefined; | ||
} | ||
} | ||
@@ -304,5 +321,5 @@ | ||
*/ | ||
function replaceObjectSchemesWithGatewayUrls(data, gatewayUrls) { | ||
function replaceObjectSchemesWithGatewayUrls(data, gatewayUrls, clientId) { | ||
if (typeof data === "string") { | ||
return replaceSchemeWithGatewayUrl(data, gatewayUrls); | ||
return replaceSchemeWithGatewayUrl(data, gatewayUrls, 0, clientId); | ||
} | ||
@@ -317,7 +334,7 @@ if (typeof data === "object") { | ||
if (Array.isArray(data)) { | ||
return data.map(entry => replaceObjectSchemesWithGatewayUrls(entry, gatewayUrls)); | ||
return data.map(entry => replaceObjectSchemesWithGatewayUrls(entry, gatewayUrls, clientId)); | ||
} | ||
return Object.fromEntries(Object.entries(data).map(_ref2 => { | ||
let [key, value] = _ref2; | ||
return [key, replaceObjectSchemesWithGatewayUrls(value, gatewayUrls)]; | ||
return [key, replaceObjectSchemesWithGatewayUrls(value, gatewayUrls, clientId)]; | ||
})); | ||
@@ -379,3 +396,148 @@ } | ||
var pkg = { | ||
name: "@thirdweb-dev/storage", | ||
version: "2.0.9", | ||
main: "dist/thirdweb-dev-storage.cjs.js", | ||
module: "dist/thirdweb-dev-storage.esm.js", | ||
exports: { | ||
".": { | ||
module: "./dist/thirdweb-dev-storage.esm.js", | ||
"default": "./dist/thirdweb-dev-storage.cjs.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
repository: "https://github.com/thirdweb-dev/js/tree/main/packages/storage", | ||
author: "thirdweb eng <eng@thirdweb.com>", | ||
license: "Apache-2.0", | ||
sideEffects: false, | ||
scripts: { | ||
format: "prettier --write 'src/**/*'", | ||
lint: "eslint src/ && bunx publint --strict --level warning", | ||
fix: "eslint src/ --fix", | ||
clean: "rm -rf dist/", | ||
build: "tsc && preconstruct build", | ||
"test:all": "NODE_ENV=test SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000 --parallel './test/**/*.test.ts'", | ||
test: "pnpm test:all", | ||
"test:single": "NODE_ENV=test SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000", | ||
push: "yalc push", | ||
typedoc: "node scripts/typedoc.mjs" | ||
}, | ||
files: [ | ||
"dist/" | ||
], | ||
preconstruct: { | ||
exports: true | ||
}, | ||
devDependencies: { | ||
"@babel/preset-env": "^7.23.8", | ||
"@babel/preset-typescript": "^7.23.3", | ||
"@microsoft/api-documenter": "^7.22.30", | ||
"@microsoft/api-extractor": "^7.36.3", | ||
"@microsoft/tsdoc": "^0.14.1", | ||
"@preconstruct/cli": "2.7.0", | ||
"@swc-node/register": "^1.6.8", | ||
"@thirdweb-dev/tsconfig": "workspace:*", | ||
"@types/chai": "^4.3.5", | ||
"@types/mocha": "^10.0.0", | ||
"@types/uuid": "^9.0.5", | ||
"@typescript-eslint/eslint-plugin": "^6.2.0", | ||
"@typescript-eslint/parser": "^6.19.1", | ||
chai: "^4.3.6", | ||
eslint: "^8.56.0", | ||
"eslint-config-thirdweb": "workspace:*", | ||
"eslint-plugin-tsdoc": "^0.2.16", | ||
esm: "^3.2.25", | ||
mocha: "^10.2.0", | ||
rimraf: "^3.0.2", | ||
"typedoc-gen": "workspace:*", | ||
typescript: "^5.3.3" | ||
}, | ||
dependencies: { | ||
"@thirdweb-dev/crypto": "workspace:*", | ||
"cid-tool": "^3.0.0", | ||
"form-data": "^4.0.0", | ||
uuid: "^9.0.1" | ||
}, | ||
engines: { | ||
node: ">=18" | ||
} | ||
}; | ||
/** | ||
* @internal | ||
* | ||
* The code below comes from the package https://github.com/DamonOehlman/detect-browser | ||
*/ | ||
const operatingSystemRules = [["iOS", /iP(hone|od|ad)/], ["Android OS", /Android/], ["BlackBerry OS", /BlackBerry|BB10/], ["Windows Mobile", /IEMobile/], ["Amazon OS", /Kindle/], ["Windows 3.11", /Win16/], ["Windows 95", /(Windows 95)|(Win95)|(Windows_95)/], ["Windows 98", /(Windows 98)|(Win98)/], ["Windows 2000", /(Windows NT 5.0)|(Windows 2000)/], ["Windows XP", /(Windows NT 5.1)|(Windows XP)/], ["Windows Server 2003", /(Windows NT 5.2)/], ["Windows Vista", /(Windows NT 6.0)/], ["Windows 7", /(Windows NT 6.1)/], ["Windows 8", /(Windows NT 6.2)/], ["Windows 8.1", /(Windows NT 6.3)/], ["Windows 10", /(Windows NT 10.0)/], ["Windows ME", /Windows ME/], ["Windows CE", /Windows CE|WinCE|Microsoft Pocket Internet Explorer/], ["Open BSD", /OpenBSD/], ["Sun OS", /SunOS/], ["Chrome OS", /CrOS/], ["Linux", /(Linux)|(X11)/], ["Mac OS", /(Mac_PowerPC)|(Macintosh)/], ["QNX", /QNX/], ["BeOS", /BeOS/], ["OS/2", /OS\/2/]]; | ||
function detectOS(ua) { | ||
for (let ii = 0, count = operatingSystemRules.length; ii < count; ii++) { | ||
const result = operatingSystemRules[ii]; | ||
if (!result) { | ||
continue; | ||
} | ||
const [os, regex] = result; | ||
const match = regex.exec(ua); | ||
if (match) { | ||
return os; | ||
} | ||
} | ||
return null; | ||
} | ||
function getOperatingSystem() { | ||
if (typeof navigator !== "undefined" && navigator.product === "ReactNative") { | ||
return ""; | ||
} else if (typeof window !== "undefined") { | ||
const userAgent = navigator.userAgent; | ||
return detectOS(userAgent) || ""; | ||
} else { | ||
return process.platform; | ||
} | ||
} | ||
function setAnalyticsHeaders(headers) { | ||
const globals = getAnalyticsGlobals(); | ||
headers["x-sdk-version"] = globals.x_sdk_version; | ||
headers["x-sdk-name"] = globals.x_sdk_name; | ||
headers["x-sdk-platform"] = globals.x_sdk_platform; | ||
headers["x-sdk-os"] = globals.x_sdk_os; | ||
} | ||
function setAnalyticsHeadersForXhr(xhr) { | ||
const globals = getAnalyticsGlobals(); | ||
xhr.setRequestHeader("x-sdk-version", globals.x_sdk_version); | ||
xhr.setRequestHeader("x-sdk-os", globals.x_sdk_os); | ||
xhr.setRequestHeader("x-sdk-name", globals.x_sdk_name); | ||
xhr.setRequestHeader("x-sdk-platform", globals.x_sdk_platform); | ||
xhr.setRequestHeader("x-bundle-id", globals.app_bundle_id); | ||
} | ||
function getAnalyticsGlobals() { | ||
if (typeof globalThis === "undefined") { | ||
return { | ||
x_sdk_name: pkg.name, | ||
x_sdk_platform: getPlatform(), | ||
x_sdk_version: pkg.version, | ||
x_sdk_os: getOperatingSystem(), | ||
app_bundle_id: undefined | ||
}; | ||
} | ||
if (globalThis.X_SDK_NAME === undefined) { | ||
globalThis.X_SDK_NAME = pkg.name; | ||
globalThis.X_SDK_PLATFORM = getPlatform(); | ||
globalThis.X_SDK_VERSION = pkg.version; | ||
globalThis.X_SDK_OS = getOperatingSystem(); | ||
globalThis.APP_BUNDLE_ID = undefined; | ||
} | ||
return { | ||
x_sdk_name: globalThis.X_SDK_NAME, | ||
x_sdk_platform: globalThis.X_SDK_PLATFORM, | ||
x_sdk_version: globalThis.X_SDK_VERSION, | ||
x_sdk_os: globalThis.X_SDK_OS, | ||
app_bundle_id: globalThis.APP_BUNDLE_ID || "" // if react, this will be empty | ||
}; | ||
} | ||
function getPlatform() { | ||
return typeof navigator !== "undefined" && navigator.product === "ReactNative" ? "mobile" : typeof window !== "undefined" ? "browser" : "node"; | ||
} | ||
/** | ||
* Default downloader used - handles downloading from all schemes specified in the gateway URLs configuration. | ||
@@ -400,12 +562,16 @@ * | ||
class StorageDownloader { | ||
DEFAULT_TIMEOUT_IN_SECONDS = 60; | ||
DEFAULT_MAX_RETRIES = 3; | ||
constructor(options) { | ||
this.secretKey = options.secretKey; | ||
this.clientId = options.clientId; | ||
this.defaultTimeout = options.timeoutInSeconds || this.DEFAULT_TIMEOUT_IN_SECONDS; | ||
} | ||
async download(uri, gatewayUrls) { | ||
let attempts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
if (attempts > 3) { | ||
async download(uri, gatewayUrls, options) { | ||
let attempts = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; | ||
const maxRetries = options?.maxRetries || this.DEFAULT_MAX_RETRIES; | ||
if (attempts > maxRetries) { | ||
console.error("[FAILED_TO_DOWNLOAD_ERROR] Failed to download from URI - too many attempts failed."); | ||
// return a 404 response to avoid retrying | ||
return new fetch.Response(JSON.stringify({ | ||
return new Response(JSON.stringify({ | ||
error: "Not Found" | ||
@@ -421,7 +587,7 @@ }), { | ||
// Replace recognized scheme with the highest priority gateway URL that hasn't already been attempted | ||
let resolvedUri = replaceSchemeWithGatewayUrl(uri, gatewayUrls, attempts); | ||
let resolvedUri = replaceSchemeWithGatewayUrl(uri, gatewayUrls, attempts, this.clientId); | ||
// If every gateway URL we know about for the designated scheme has been tried (via recursion) and failed, throw an error | ||
if (!resolvedUri) { | ||
console.error("[FAILED_TO_DOWNLOAD_ERROR] Unable to download from URI - all gateway URLs failed to respond."); | ||
return new fetch.Response(JSON.stringify({ | ||
return new Response(JSON.stringify({ | ||
error: "Not Found" | ||
@@ -439,2 +605,3 @@ }), { | ||
if (isTwGatewayUrl(resolvedUri)) { | ||
const bundleId = getAnalyticsGlobals().app_bundle_id; | ||
if (this.secretKey) { | ||
@@ -445,11 +612,9 @@ headers = { | ||
} else if (this.clientId) { | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
resolvedUri = resolvedUri + `?bundleId=${globalThis.APP_BUNDLE_ID}`; | ||
if (!resolvedUri.includes("bundleId")) { | ||
resolvedUri = resolvedUri + (bundleId ? `?bundleId=${bundleId}` : ""); | ||
} | ||
headers = { | ||
"x-client-Id": this.clientId | ||
}; | ||
headers["x-client-Id"] = this.clientId; | ||
} | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
headers = { | ||
@@ -460,10 +625,19 @@ ...headers, | ||
} | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
headers = { | ||
...headers, | ||
authorization: `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}` | ||
}; | ||
headers["x-authorize-wallet"] = "true"; | ||
} | ||
setAnalyticsHeaders(headers); | ||
} | ||
if (isTooManyRequests(resolvedUri)) { | ||
// skip the request if we're getting too many request error from the gateway | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
const controller = new AbortController(); | ||
const timeout = setTimeout(() => controller.abort(), 5000); | ||
const resOrErr = await fetch__default["default"](resolvedUri, { | ||
const timeoutInSeconds = options?.timeoutInSeconds || this.defaultTimeout; | ||
const timeout = setTimeout(() => controller.abort(), timeoutInSeconds * 1000); | ||
const resOrErr = await fetch(resolvedUri, { | ||
headers, | ||
@@ -478,3 +652,3 @@ signal: controller.signal | ||
// early exit if we don't have a status code | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
throw new Error(`Request timed out after ${timeoutInSeconds} seconds. ${isTwGatewayUrl(resolvedUri) ? "You can update the timeoutInSeconds option to increase the timeout." : "You're using a public IPFS gateway, pass in a clientId or secretKey for a reliable IPFS gateway."}`); | ||
} | ||
@@ -490,3 +664,3 @@ | ||
// Since the current gateway failed, recursively try the next one we know about | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
@@ -509,6 +683,8 @@ if (resOrErr.status === 410) { | ||
// 5xx - Server Errors | ||
if (resOrErr.status !== 408 && resOrErr.status !== 429 && resOrErr.status < 500) ; | ||
if (resOrErr.status !== 408 && resOrErr.status !== 429 && resOrErr.status < 500) { | ||
return resOrErr; | ||
} | ||
// Since the current gateway failed, recursively try the next one we know about | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
@@ -715,3 +891,3 @@ } | ||
} else { | ||
return resolve(fileNames.map(name => `ipfs://${cid}/${name}`)); | ||
return resolve(fileNames.map(n => `ipfs://${cid}/${n}`)); | ||
} | ||
@@ -725,3 +901,3 @@ } | ||
if (xhr.readyState !== 0 && xhr.readyState !== 4 || xhr.status === 0) { | ||
return reject(new Error("This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.")); | ||
return reject(new Error("Upload failed due to a network error.")); | ||
} | ||
@@ -736,10 +912,14 @@ return reject(new Error("Unknown upload error occured")); | ||
} | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
xhr.setRequestHeader("x-bundle-id", globalThis.APP_BUNDLE_ID); | ||
} | ||
setAnalyticsHeadersForXhr(xhr); | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
// if we have a authorization token on global context then add that to the headers, this is for the dashboard. | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
xhr.setRequestHeader("authorization", `Bearer ${globalThis.TW_AUTH_TOKEN}`); | ||
} | ||
// CLI auth token | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
xhr.setRequestHeader("authorization", `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}`); | ||
xhr.setRequestHeader("x-authorize-wallet", `true`); | ||
} | ||
xhr.send(form); | ||
@@ -759,12 +939,14 @@ }); | ||
// if we have a bundle id on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
headers["x-bundle-id"] = globalThis.APP_BUNDLE_ID; | ||
// if we have a authorization token on global context then add that to the headers, this is for the dashboard. | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
headers["authorization"] = `Bearer ${globalThis.TW_AUTH_TOKEN}`; | ||
} | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
headers["authorization"] = `Bearer ${globalThis.TW_AUTH_TOKEN}`; | ||
// CLI auth token | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
headers["authorization"] = `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}`; | ||
headers["x-authorize-wallet"] = "true"; | ||
} | ||
const res = await fetch__default["default"](`${this.uploadServerUrl}/ipfs/upload`, { | ||
setAnalyticsHeaders(headers); | ||
const res = await fetch(`${this.uploadServerUrl}/ipfs/upload`, { | ||
method: "POST", | ||
@@ -777,10 +959,12 @@ headers: { | ||
}); | ||
const body = await res.json(); | ||
if (!res.ok) { | ||
console.warn(body); | ||
throw new Error("Failed to upload files to IPFS"); | ||
if (res.status === 401) { | ||
throw new Error("Unauthorized - You don't have permission to use this service."); | ||
} | ||
throw new Error(`Failed to upload files to IPFS - ${res.status} - ${res.statusText} - ${await res.text()}`); | ||
} | ||
const body = await res.json(); | ||
const cid = body.IpfsHash; | ||
if (!cid) { | ||
throw new Error("Failed to upload files to IPFS"); | ||
throw new Error("Failed to upload files to IPFS - Bad CID"); | ||
} | ||
@@ -841,2 +1025,3 @@ if (options?.uploadWithoutDirectory) { | ||
this.gatewayUrls = prepareGatewayUrls(parseGatewayUrls(options?.gatewayUrls), options?.clientId, options?.secretKey); | ||
this.clientId = options?.clientId; | ||
} | ||
@@ -858,3 +1043,3 @@ | ||
resolveScheme(url) { | ||
return replaceSchemeWithGatewayUrl(url, this.gatewayUrls); | ||
return replaceSchemeWithGatewayUrl(url, this.gatewayUrls, 0, this.clientId); | ||
} | ||
@@ -874,4 +1059,4 @@ | ||
*/ | ||
async download(url) { | ||
return this.downloader.download(url, this.gatewayUrls); | ||
async download(url, options) { | ||
return this.downloader.download(url, this.gatewayUrls, options); | ||
} | ||
@@ -892,8 +1077,8 @@ | ||
*/ | ||
async downloadJSON(url) { | ||
const res = await this.download(url); | ||
async downloadJSON(url, options) { | ||
const res = await this.download(url, options); | ||
// If we get a JSON object, recursively replace any schemes with gatewayUrls | ||
const json = await res.json(); | ||
return replaceObjectSchemesWithGatewayUrls(json, this.gatewayUrls); | ||
return replaceObjectSchemesWithGatewayUrls(json, this.gatewayUrls, this.clientId); | ||
} | ||
@@ -907,3 +1092,3 @@ | ||
* @param options - Options to pass through to the storage uploader class | ||
* @returns - The URI of the uploaded data | ||
* @returns The URI of the uploaded data | ||
* | ||
@@ -932,3 +1117,3 @@ * @example | ||
* @param options - Options to pass through to the storage uploader class | ||
* @returns - The URIs of the uploaded data | ||
* @returns The URIs of the uploaded data | ||
* | ||
@@ -999,3 +1184,3 @@ * @example | ||
// Ex: used for Solana, where services don't resolve schemes for you, so URLs must be usable by default | ||
cleaned = replaceObjectSchemesWithGatewayUrls(cleaned, this.gatewayUrls); | ||
cleaned = replaceObjectSchemesWithGatewayUrls(cleaned, this.gatewayUrls, this.clientId); | ||
} | ||
@@ -1002,0 +1187,0 @@ return cleaned; |
import CIDTool from 'cid-tool'; | ||
import fetch, { Response } from 'cross-fetch'; | ||
import { sha256HexSync } from '@thirdweb-dev/crypto'; | ||
import FormData from 'form-data'; | ||
@@ -17,2 +17,3 @@ import { v4 } from 'uuid'; | ||
const TW_HOSTNAME_SUFFIX = ".ipfscdn.io"; | ||
const TW_STAGINGHOSTNAME_SUFFIX = ".thirdwebstorage-staging.com"; | ||
const TW_GATEWAY_URLS = [`https://{clientId}${TW_HOSTNAME_SUFFIX}/ipfs/{cid}/{path}`]; | ||
@@ -22,7 +23,13 @@ | ||
* @internal | ||
* @param url | ||
* @param url - the url to check | ||
* @returns | ||
*/ | ||
function isTwGatewayUrl(url) { | ||
return new URL(url).hostname.endsWith(TW_HOSTNAME_SUFFIX); | ||
const hostname = new URL(url).hostname; | ||
const isProd = hostname.endsWith(TW_HOSTNAME_SUFFIX); | ||
if (isProd) { | ||
return true; | ||
} | ||
// fall back to also handle staging urls | ||
return hostname.endsWith(TW_STAGINGHOSTNAME_SUFFIX); | ||
} | ||
@@ -64,3 +71,3 @@ const PUBLIC_GATEWAY_URLS = ["https://{cid}.ipfs.cf-ipfs.com/{path}", "https://{cid}.ipfs.dweb.link/{path}", "https://ipfs.io/ipfs/{cid}/{path}", "https://cloudflare-ipfs.com/ipfs/{cid}/{path}", "https://{cid}.ipfs.w3s.link/{path}", "https://w3s.link/ipfs/{cid}/{path}", "https://nftstorage.link/ipfs/{cid}/{path}", "https://gateway.pinata.cloud/ipfs/{cid}/{path}"]; | ||
*/ | ||
function getGatewayUrlForCid(gatewayUrl, cid) { | ||
function getGatewayUrlForCid(gatewayUrl, cid, clientId) { | ||
const parts = cid.split("/"); | ||
@@ -84,2 +91,9 @@ const hash = convertCidToV1(parts[0]); | ||
} | ||
// if the URL contains the {clientId} token, replace it with the client ID | ||
if (gatewayUrl.includes("{clientId}")) { | ||
if (!clientId) { | ||
throw new Error("Cannot use {clientId} in gateway URL without providing a client ID"); | ||
} | ||
url = url.replace("{clientId}", clientId); | ||
} | ||
return url; | ||
@@ -106,6 +120,3 @@ } | ||
} | ||
// this is on purpose because we're using the crypto module only in node | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const crypto = require("crypto"); | ||
const hashedSecretKey = crypto.createHash("sha256").update(secretKey).digest("hex"); | ||
const hashedSecretKey = sha256HexSync(secretKey); | ||
const derivedClientId = hashedSecretKey.slice(0, 32); | ||
@@ -129,3 +140,3 @@ return url.replace("{clientId}", derivedClientId); | ||
function convertCidToV1(cid) { | ||
let normalized; | ||
let normalized = ''; | ||
try { | ||
@@ -250,2 +261,3 @@ const hash = cid.split("/")[0]; | ||
let index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
let clientId = arguments.length > 3 ? arguments[3] : undefined; | ||
const scheme = Object.keys(gatewayUrls).find(s => uri.startsWith(s)); | ||
@@ -260,3 +272,9 @@ const schemeGatewayUrls = scheme ? gatewayUrls[scheme] : []; | ||
const path = uri.replace(scheme, ""); | ||
return getGatewayUrlForCid(schemeGatewayUrls[index], path); | ||
try { | ||
const gatewayUrl = getGatewayUrlForCid(schemeGatewayUrls[index], path, clientId); | ||
return gatewayUrl; | ||
} catch (err) { | ||
console.warn(`The IPFS uri: ${path} is not valid.`); | ||
return undefined; | ||
} | ||
} | ||
@@ -292,5 +310,5 @@ | ||
*/ | ||
function replaceObjectSchemesWithGatewayUrls(data, gatewayUrls) { | ||
function replaceObjectSchemesWithGatewayUrls(data, gatewayUrls, clientId) { | ||
if (typeof data === "string") { | ||
return replaceSchemeWithGatewayUrl(data, gatewayUrls); | ||
return replaceSchemeWithGatewayUrl(data, gatewayUrls, 0, clientId); | ||
} | ||
@@ -305,7 +323,7 @@ if (typeof data === "object") { | ||
if (Array.isArray(data)) { | ||
return data.map(entry => replaceObjectSchemesWithGatewayUrls(entry, gatewayUrls)); | ||
return data.map(entry => replaceObjectSchemesWithGatewayUrls(entry, gatewayUrls, clientId)); | ||
} | ||
return Object.fromEntries(Object.entries(data).map(_ref2 => { | ||
let [key, value] = _ref2; | ||
return [key, replaceObjectSchemesWithGatewayUrls(value, gatewayUrls)]; | ||
return [key, replaceObjectSchemesWithGatewayUrls(value, gatewayUrls, clientId)]; | ||
})); | ||
@@ -367,3 +385,148 @@ } | ||
var pkg = { | ||
name: "@thirdweb-dev/storage", | ||
version: "2.0.9", | ||
main: "dist/thirdweb-dev-storage.cjs.js", | ||
module: "dist/thirdweb-dev-storage.esm.js", | ||
exports: { | ||
".": { | ||
module: "./dist/thirdweb-dev-storage.esm.js", | ||
"default": "./dist/thirdweb-dev-storage.cjs.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
repository: "https://github.com/thirdweb-dev/js/tree/main/packages/storage", | ||
author: "thirdweb eng <eng@thirdweb.com>", | ||
license: "Apache-2.0", | ||
sideEffects: false, | ||
scripts: { | ||
format: "prettier --write 'src/**/*'", | ||
lint: "eslint src/ && bunx publint --strict --level warning", | ||
fix: "eslint src/ --fix", | ||
clean: "rm -rf dist/", | ||
build: "tsc && preconstruct build", | ||
"test:all": "NODE_ENV=test SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000 --parallel './test/**/*.test.ts'", | ||
test: "pnpm test:all", | ||
"test:single": "NODE_ENV=test SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000", | ||
push: "yalc push", | ||
typedoc: "node scripts/typedoc.mjs" | ||
}, | ||
files: [ | ||
"dist/" | ||
], | ||
preconstruct: { | ||
exports: true | ||
}, | ||
devDependencies: { | ||
"@babel/preset-env": "^7.23.8", | ||
"@babel/preset-typescript": "^7.23.3", | ||
"@microsoft/api-documenter": "^7.22.30", | ||
"@microsoft/api-extractor": "^7.36.3", | ||
"@microsoft/tsdoc": "^0.14.1", | ||
"@preconstruct/cli": "2.7.0", | ||
"@swc-node/register": "^1.6.8", | ||
"@thirdweb-dev/tsconfig": "workspace:*", | ||
"@types/chai": "^4.3.5", | ||
"@types/mocha": "^10.0.0", | ||
"@types/uuid": "^9.0.5", | ||
"@typescript-eslint/eslint-plugin": "^6.2.0", | ||
"@typescript-eslint/parser": "^6.19.1", | ||
chai: "^4.3.6", | ||
eslint: "^8.56.0", | ||
"eslint-config-thirdweb": "workspace:*", | ||
"eslint-plugin-tsdoc": "^0.2.16", | ||
esm: "^3.2.25", | ||
mocha: "^10.2.0", | ||
rimraf: "^3.0.2", | ||
"typedoc-gen": "workspace:*", | ||
typescript: "^5.3.3" | ||
}, | ||
dependencies: { | ||
"@thirdweb-dev/crypto": "workspace:*", | ||
"cid-tool": "^3.0.0", | ||
"form-data": "^4.0.0", | ||
uuid: "^9.0.1" | ||
}, | ||
engines: { | ||
node: ">=18" | ||
} | ||
}; | ||
/** | ||
* @internal | ||
* | ||
* The code below comes from the package https://github.com/DamonOehlman/detect-browser | ||
*/ | ||
const operatingSystemRules = [["iOS", /iP(hone|od|ad)/], ["Android OS", /Android/], ["BlackBerry OS", /BlackBerry|BB10/], ["Windows Mobile", /IEMobile/], ["Amazon OS", /Kindle/], ["Windows 3.11", /Win16/], ["Windows 95", /(Windows 95)|(Win95)|(Windows_95)/], ["Windows 98", /(Windows 98)|(Win98)/], ["Windows 2000", /(Windows NT 5.0)|(Windows 2000)/], ["Windows XP", /(Windows NT 5.1)|(Windows XP)/], ["Windows Server 2003", /(Windows NT 5.2)/], ["Windows Vista", /(Windows NT 6.0)/], ["Windows 7", /(Windows NT 6.1)/], ["Windows 8", /(Windows NT 6.2)/], ["Windows 8.1", /(Windows NT 6.3)/], ["Windows 10", /(Windows NT 10.0)/], ["Windows ME", /Windows ME/], ["Windows CE", /Windows CE|WinCE|Microsoft Pocket Internet Explorer/], ["Open BSD", /OpenBSD/], ["Sun OS", /SunOS/], ["Chrome OS", /CrOS/], ["Linux", /(Linux)|(X11)/], ["Mac OS", /(Mac_PowerPC)|(Macintosh)/], ["QNX", /QNX/], ["BeOS", /BeOS/], ["OS/2", /OS\/2/]]; | ||
function detectOS(ua) { | ||
for (let ii = 0, count = operatingSystemRules.length; ii < count; ii++) { | ||
const result = operatingSystemRules[ii]; | ||
if (!result) { | ||
continue; | ||
} | ||
const [os, regex] = result; | ||
const match = regex.exec(ua); | ||
if (match) { | ||
return os; | ||
} | ||
} | ||
return null; | ||
} | ||
function getOperatingSystem() { | ||
if (typeof navigator !== "undefined" && navigator.product === "ReactNative") { | ||
return ""; | ||
} else if (typeof window !== "undefined") { | ||
const userAgent = navigator.userAgent; | ||
return detectOS(userAgent) || ""; | ||
} else { | ||
return process.platform; | ||
} | ||
} | ||
function setAnalyticsHeaders(headers) { | ||
const globals = getAnalyticsGlobals(); | ||
headers["x-sdk-version"] = globals.x_sdk_version; | ||
headers["x-sdk-name"] = globals.x_sdk_name; | ||
headers["x-sdk-platform"] = globals.x_sdk_platform; | ||
headers["x-sdk-os"] = globals.x_sdk_os; | ||
} | ||
function setAnalyticsHeadersForXhr(xhr) { | ||
const globals = getAnalyticsGlobals(); | ||
xhr.setRequestHeader("x-sdk-version", globals.x_sdk_version); | ||
xhr.setRequestHeader("x-sdk-os", globals.x_sdk_os); | ||
xhr.setRequestHeader("x-sdk-name", globals.x_sdk_name); | ||
xhr.setRequestHeader("x-sdk-platform", globals.x_sdk_platform); | ||
xhr.setRequestHeader("x-bundle-id", globals.app_bundle_id); | ||
} | ||
function getAnalyticsGlobals() { | ||
if (typeof globalThis === "undefined") { | ||
return { | ||
x_sdk_name: pkg.name, | ||
x_sdk_platform: getPlatform(), | ||
x_sdk_version: pkg.version, | ||
x_sdk_os: getOperatingSystem(), | ||
app_bundle_id: undefined | ||
}; | ||
} | ||
if (globalThis.X_SDK_NAME === undefined) { | ||
globalThis.X_SDK_NAME = pkg.name; | ||
globalThis.X_SDK_PLATFORM = getPlatform(); | ||
globalThis.X_SDK_VERSION = pkg.version; | ||
globalThis.X_SDK_OS = getOperatingSystem(); | ||
globalThis.APP_BUNDLE_ID = undefined; | ||
} | ||
return { | ||
x_sdk_name: globalThis.X_SDK_NAME, | ||
x_sdk_platform: globalThis.X_SDK_PLATFORM, | ||
x_sdk_version: globalThis.X_SDK_VERSION, | ||
x_sdk_os: globalThis.X_SDK_OS, | ||
app_bundle_id: globalThis.APP_BUNDLE_ID || "" // if react, this will be empty | ||
}; | ||
} | ||
function getPlatform() { | ||
return typeof navigator !== "undefined" && navigator.product === "ReactNative" ? "mobile" : typeof window !== "undefined" ? "browser" : "node"; | ||
} | ||
/** | ||
* Default downloader used - handles downloading from all schemes specified in the gateway URLs configuration. | ||
@@ -388,9 +551,13 @@ * | ||
class StorageDownloader { | ||
DEFAULT_TIMEOUT_IN_SECONDS = 60; | ||
DEFAULT_MAX_RETRIES = 3; | ||
constructor(options) { | ||
this.secretKey = options.secretKey; | ||
this.clientId = options.clientId; | ||
this.defaultTimeout = options.timeoutInSeconds || this.DEFAULT_TIMEOUT_IN_SECONDS; | ||
} | ||
async download(uri, gatewayUrls) { | ||
let attempts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
if (attempts > 3) { | ||
async download(uri, gatewayUrls, options) { | ||
let attempts = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; | ||
const maxRetries = options?.maxRetries || this.DEFAULT_MAX_RETRIES; | ||
if (attempts > maxRetries) { | ||
console.error("[FAILED_TO_DOWNLOAD_ERROR] Failed to download from URI - too many attempts failed."); | ||
@@ -409,3 +576,3 @@ // return a 404 response to avoid retrying | ||
// Replace recognized scheme with the highest priority gateway URL that hasn't already been attempted | ||
let resolvedUri = replaceSchemeWithGatewayUrl(uri, gatewayUrls, attempts); | ||
let resolvedUri = replaceSchemeWithGatewayUrl(uri, gatewayUrls, attempts, this.clientId); | ||
// If every gateway URL we know about for the designated scheme has been tried (via recursion) and failed, throw an error | ||
@@ -427,2 +594,3 @@ if (!resolvedUri) { | ||
if (isTwGatewayUrl(resolvedUri)) { | ||
const bundleId = getAnalyticsGlobals().app_bundle_id; | ||
if (this.secretKey) { | ||
@@ -433,11 +601,9 @@ headers = { | ||
} else if (this.clientId) { | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
resolvedUri = resolvedUri + `?bundleId=${globalThis.APP_BUNDLE_ID}`; | ||
if (!resolvedUri.includes("bundleId")) { | ||
resolvedUri = resolvedUri + (bundleId ? `?bundleId=${bundleId}` : ""); | ||
} | ||
headers = { | ||
"x-client-Id": this.clientId | ||
}; | ||
headers["x-client-Id"] = this.clientId; | ||
} | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
headers = { | ||
@@ -448,9 +614,18 @@ ...headers, | ||
} | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
headers = { | ||
...headers, | ||
authorization: `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}` | ||
}; | ||
headers["x-authorize-wallet"] = "true"; | ||
} | ||
setAnalyticsHeaders(headers); | ||
} | ||
if (isTooManyRequests(resolvedUri)) { | ||
// skip the request if we're getting too many request error from the gateway | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
const controller = new AbortController(); | ||
const timeout = setTimeout(() => controller.abort(), 5000); | ||
const timeoutInSeconds = options?.timeoutInSeconds || this.defaultTimeout; | ||
const timeout = setTimeout(() => controller.abort(), timeoutInSeconds * 1000); | ||
const resOrErr = await fetch(resolvedUri, { | ||
@@ -466,3 +641,3 @@ headers, | ||
// early exit if we don't have a status code | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
throw new Error(`Request timed out after ${timeoutInSeconds} seconds. ${isTwGatewayUrl(resolvedUri) ? "You can update the timeoutInSeconds option to increase the timeout." : "You're using a public IPFS gateway, pass in a clientId or secretKey for a reliable IPFS gateway."}`); | ||
} | ||
@@ -478,3 +653,3 @@ | ||
// Since the current gateway failed, recursively try the next one we know about | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
@@ -497,6 +672,8 @@ if (resOrErr.status === 410) { | ||
// 5xx - Server Errors | ||
if (resOrErr.status !== 408 && resOrErr.status !== 429 && resOrErr.status < 500) ; | ||
if (resOrErr.status !== 408 && resOrErr.status !== 429 && resOrErr.status < 500) { | ||
return resOrErr; | ||
} | ||
// Since the current gateway failed, recursively try the next one we know about | ||
return this.download(uri, gatewayUrls, attempts + 1); | ||
return this.download(uri, gatewayUrls, options, attempts + 1); | ||
} | ||
@@ -703,3 +880,3 @@ } | ||
} else { | ||
return resolve(fileNames.map(name => `ipfs://${cid}/${name}`)); | ||
return resolve(fileNames.map(n => `ipfs://${cid}/${n}`)); | ||
} | ||
@@ -713,3 +890,3 @@ } | ||
if (xhr.readyState !== 0 && xhr.readyState !== 4 || xhr.status === 0) { | ||
return reject(new Error("This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.")); | ||
return reject(new Error("Upload failed due to a network error.")); | ||
} | ||
@@ -724,10 +901,14 @@ return reject(new Error("Unknown upload error occured")); | ||
} | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
xhr.setRequestHeader("x-bundle-id", globalThis.APP_BUNDLE_ID); | ||
} | ||
setAnalyticsHeadersForXhr(xhr); | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
// if we have a authorization token on global context then add that to the headers, this is for the dashboard. | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
xhr.setRequestHeader("authorization", `Bearer ${globalThis.TW_AUTH_TOKEN}`); | ||
} | ||
// CLI auth token | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
xhr.setRequestHeader("authorization", `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}`); | ||
xhr.setRequestHeader("x-authorize-wallet", `true`); | ||
} | ||
xhr.send(form); | ||
@@ -747,11 +928,13 @@ }); | ||
// if we have a bundle id on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "APP_BUNDLE_ID" in globalThis) { | ||
headers["x-bundle-id"] = globalThis.APP_BUNDLE_ID; | ||
// if we have a authorization token on global context then add that to the headers, this is for the dashboard. | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis && typeof globalThis.TW_AUTH_TOKEN === "string") { | ||
headers["authorization"] = `Bearer ${globalThis.TW_AUTH_TOKEN}`; | ||
} | ||
// if we have a authorization token on global context then add that to the headers | ||
if (typeof globalThis !== "undefined" && "TW_AUTH_TOKEN" in globalThis) { | ||
headers["authorization"] = `Bearer ${globalThis.TW_AUTH_TOKEN}`; | ||
// CLI auth token | ||
if (typeof globalThis !== "undefined" && "TW_CLI_AUTH_TOKEN" in globalThis && typeof globalThis.TW_CLI_AUTH_TOKEN === "string") { | ||
headers["authorization"] = `Bearer ${globalThis.TW_CLI_AUTH_TOKEN}`; | ||
headers["x-authorize-wallet"] = "true"; | ||
} | ||
setAnalyticsHeaders(headers); | ||
const res = await fetch(`${this.uploadServerUrl}/ipfs/upload`, { | ||
@@ -765,10 +948,12 @@ method: "POST", | ||
}); | ||
const body = await res.json(); | ||
if (!res.ok) { | ||
console.warn(body); | ||
throw new Error("Failed to upload files to IPFS"); | ||
if (res.status === 401) { | ||
throw new Error("Unauthorized - You don't have permission to use this service."); | ||
} | ||
throw new Error(`Failed to upload files to IPFS - ${res.status} - ${res.statusText} - ${await res.text()}`); | ||
} | ||
const body = await res.json(); | ||
const cid = body.IpfsHash; | ||
if (!cid) { | ||
throw new Error("Failed to upload files to IPFS"); | ||
throw new Error("Failed to upload files to IPFS - Bad CID"); | ||
} | ||
@@ -829,2 +1014,3 @@ if (options?.uploadWithoutDirectory) { | ||
this.gatewayUrls = prepareGatewayUrls(parseGatewayUrls(options?.gatewayUrls), options?.clientId, options?.secretKey); | ||
this.clientId = options?.clientId; | ||
} | ||
@@ -846,3 +1032,3 @@ | ||
resolveScheme(url) { | ||
return replaceSchemeWithGatewayUrl(url, this.gatewayUrls); | ||
return replaceSchemeWithGatewayUrl(url, this.gatewayUrls, 0, this.clientId); | ||
} | ||
@@ -862,4 +1048,4 @@ | ||
*/ | ||
async download(url) { | ||
return this.downloader.download(url, this.gatewayUrls); | ||
async download(url, options) { | ||
return this.downloader.download(url, this.gatewayUrls, options); | ||
} | ||
@@ -880,8 +1066,8 @@ | ||
*/ | ||
async downloadJSON(url) { | ||
const res = await this.download(url); | ||
async downloadJSON(url, options) { | ||
const res = await this.download(url, options); | ||
// If we get a JSON object, recursively replace any schemes with gatewayUrls | ||
const json = await res.json(); | ||
return replaceObjectSchemesWithGatewayUrls(json, this.gatewayUrls); | ||
return replaceObjectSchemesWithGatewayUrls(json, this.gatewayUrls, this.clientId); | ||
} | ||
@@ -895,3 +1081,3 @@ | ||
* @param options - Options to pass through to the storage uploader class | ||
* @returns - The URI of the uploaded data | ||
* @returns The URI of the uploaded data | ||
* | ||
@@ -920,3 +1106,3 @@ * @example | ||
* @param options - Options to pass through to the storage uploader class | ||
* @returns - The URIs of the uploaded data | ||
* @returns The URIs of the uploaded data | ||
* | ||
@@ -987,3 +1173,3 @@ * @example | ||
// Ex: used for Solana, where services don't resolve schemes for you, so URLs must be usable by default | ||
cleaned = replaceObjectSchemesWithGatewayUrls(cleaned, this.gatewayUrls); | ||
cleaned = replaceObjectSchemesWithGatewayUrls(cleaned, this.gatewayUrls, this.clientId); | ||
} | ||
@@ -990,0 +1176,0 @@ return cleaned; |
{ | ||
"name": "@thirdweb-dev/storage", | ||
"version": "0.0.0-dev-ec891b4-20230807224410", | ||
"version": "0.0.0-dev-ed17781-20240229185724", | ||
"main": "dist/thirdweb-dev-storage.cjs.js", | ||
"module": "dist/thirdweb-dev-storage.esm.js", | ||
"browser": { | ||
"./dist/thirdweb-dev-storage.esm.js": "./dist/thirdweb-dev-storage.browser.esm.js" | ||
}, | ||
"exports": { | ||
".": { | ||
"module": { | ||
"browser": "./dist/thirdweb-dev-storage.browser.esm.js", | ||
"default": "./dist/thirdweb-dev-storage.esm.js" | ||
}, | ||
"module": "./dist/thirdweb-dev-storage.esm.js", | ||
"default": "./dist/thirdweb-dev-storage.cjs.js" | ||
@@ -27,11 +21,7 @@ }, | ||
"preconstruct": { | ||
"exports": { | ||
"envConditions": [ | ||
"browser" | ||
] | ||
} | ||
"exports": true | ||
}, | ||
"devDependencies": { | ||
"@babel/preset-env": "^7.22.9", | ||
"@babel/preset-typescript": "^7.22.5", | ||
"@babel/preset-env": "^7.23.8", | ||
"@babel/preset-typescript": "^7.23.3", | ||
"@microsoft/api-documenter": "^7.22.30", | ||
@@ -41,27 +31,32 @@ "@microsoft/api-extractor": "^7.36.3", | ||
"@preconstruct/cli": "2.7.0", | ||
"@swc-node/register": "^1.6.6", | ||
"@swc-node/register": "^1.6.8", | ||
"@thirdweb-dev/tsconfig": "^0.1.7", | ||
"@types/chai": "^4.3.5", | ||
"@types/mocha": "^10.0.0", | ||
"@types/uuid": "^9.0.2", | ||
"@types/uuid": "^9.0.5", | ||
"@typescript-eslint/eslint-plugin": "^6.2.0", | ||
"@typescript-eslint/parser": "^6.2.0", | ||
"@typescript-eslint/parser": "^6.19.1", | ||
"chai": "^4.3.6", | ||
"eslint": "^8.45.0", | ||
"eslint-config-thirdweb": "^0.1.6", | ||
"eslint": "^8.56.0", | ||
"eslint-config-thirdweb": "^0.1.7", | ||
"eslint-plugin-tsdoc": "^0.2.16", | ||
"esm": "^3.2.25", | ||
"mocha": "^10.2.0", | ||
"typescript": "^5.1.6" | ||
"rimraf": "^3.0.2", | ||
"typedoc-gen": "^1.0.1", | ||
"typescript": "^5.3.3" | ||
}, | ||
"dependencies": { | ||
"@thirdweb-dev/crypto": "0.0.0-dev-ed17781-20240229185724", | ||
"cid-tool": "^3.0.0", | ||
"cross-fetch": "^3.1.8", | ||
"form-data": "^4.0.0", | ||
"uuid": "^9.0.0" | ||
"uuid": "^9.0.1" | ||
}, | ||
"engines": { | ||
"node": ">=18" | ||
}, | ||
"scripts": { | ||
"format": "prettier --write 'src/**/*'", | ||
"lint": "eslint src/", | ||
"lint": "eslint src/ && bunx publint --strict --level warning", | ||
"fix": "eslint src/ --fix", | ||
"generate-docs": "api-extractor run --local && api-documenter markdown -i ./temp -o ./docs", | ||
"clean": "rm -rf dist/", | ||
@@ -72,4 +67,5 @@ "build": "tsc && preconstruct build", | ||
"test:single": "NODE_ENV=test SWC_NODE_PROJECT=./tsconfig.test.json mocha --timeout 30000", | ||
"push": "yalc push" | ||
"push": "yalc push", | ||
"typedoc": "node scripts/typedoc.mjs" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
49
8
6
177769
22
4112
+ Added@thirdweb-dev/crypto@0.0.0-dev-ed17781-20240229185724
+ Added@noble/hashes@1.7.1(transitive)
+ Added@thirdweb-dev/crypto@0.0.0-dev-ed17781-20240229185724(transitive)
+ Addedjs-sha3@0.9.3(transitive)
- Removedcross-fetch@^3.1.8
- Removedcross-fetch@3.2.0(transitive)
- Removednode-fetch@2.7.0(transitive)
- Removedtr46@0.0.3(transitive)
- Removedwebidl-conversions@3.0.1(transitive)
- Removedwhatwg-url@5.0.0(transitive)
Updateduuid@^9.0.1