@opendatalabs/vana-sdk
Version:
A TypeScript library for interacting with Vana Network smart contracts.
1 lines • 15.2 kB
Source Map (JSON)
{"version":3,"sources":["../../../src/storage/providers/vana-storage.ts"],"sourcesContent":["import {\n StorageError,\n type StorageProvider,\n type StorageUploadResult,\n type StorageFile,\n type StorageListOptions,\n type StorageProviderConfig,\n} from \"../index\";\nimport {\n buildWeb3SignedHeader,\n type Web3SignedSignFn,\n} from \"../../auth/web3-signed-builder\";\n\nconst DEFAULT_ENDPOINT = \"https://storage.vana.org\";\nconst BLOB_PATH_PREFIX = \"/v1/blobs\";\nconst DEFAULT_TOKEN_TTL_SECONDS = 300;\n\n/**\n * Wallet-style signer used by {@link VanaStorage} to authenticate every\n * request. For Personal Server flows this can be a registered server wallet\n * signing requests for the owner's storage namespace.\n *\n * @category Storage\n */\nexport interface VanaStorageSigner {\n /** EIP-191 address (`0x...`). */\n address: `0x${string}`;\n /** EIP-191 personal_sign callback (e.g. viem `account.signMessage`). */\n signMessage: Web3SignedSignFn;\n}\n\n/**\n * Configuration for {@link VanaStorage}.\n *\n * @category Storage\n */\nexport interface VanaStorageConfig {\n /**\n * Base URL of the vana-storage Worker. Defaults to `https://storage.vana.org`.\n */\n endpoint?: string;\n /**\n * Wallet signer used to authenticate writes and reads.\n */\n signer: VanaStorageSigner;\n /**\n * Owner namespace under which blobs are stored. Defaults to the signer address.\n */\n ownerAddress?: `0x${string}`;\n /**\n * Optional `fetch` implementation. Defaults to the global `fetch`.\n * Useful for tests and for environments that need a custom HTTP client.\n */\n fetchImpl?: typeof fetch;\n}\n\ninterface VanaStorageUploadResponse {\n key: string;\n url: string;\n etag: string;\n size: number;\n}\n\n/**\n * Storage provider that talks to the vana-storage Worker\n * (`https://storage.vana.org` by default). All requests are authenticated\n * with Web3Signed headers signed by the configured wallet.\n *\n * @remarks\n * Filenames passed to {@link VanaStorage.upload} must be of the form\n * `\"{scope}/{collectedAt}\"` (e.g. `\"instagram.profile/2026-05-08T20:00:00.000Z\"`).\n * The owner address is prepended automatically to produce the canonical\n * blob path `/v1/blobs/{owner}/{scope}/{collectedAt}`.\n *\n * @category Storage\n *\n * @example\n * ```typescript\n * import { privateKeyToAccount } from \"viem/accounts\";\n * import { VanaStorage } from \"@opendatalabs/vana-sdk/node\";\n *\n * const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);\n * const storage = new VanaStorage({\n * signer: {\n * address: account.address,\n * signMessage: (msg) => account.signMessage({ message: msg }),\n * },\n * });\n *\n * const result = await storage.upload(\n * new Blob([ciphertext]),\n * \"instagram.profile/2026-05-08T20:00:00.000Z\",\n * );\n * ```\n */\nexport class VanaStorage implements StorageProvider {\n private readonly endpoint: string;\n private readonly signer: VanaStorageSigner;\n private readonly ownerAddress: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(config: VanaStorageConfig) {\n if (!config?.signer?.address || !config?.signer?.signMessage) {\n throw new StorageError(\n \"VanaStorage requires a signer with address and signMessage\",\n \"MISSING_SIGNER\",\n \"vana-storage\",\n );\n }\n this.endpoint = (config.endpoint ?? DEFAULT_ENDPOINT).replace(/\\/+$/, \"\");\n this.signer = config.signer;\n this.ownerAddress = (\n config.ownerAddress ?? config.signer.address\n ).toLowerCase();\n this.fetchImpl = config.fetchImpl ?? globalThis.fetch.bind(globalThis);\n }\n\n /**\n * Upload an encrypted blob to vana-storage.\n *\n * @param file - The blob to upload.\n * @param filename - Required relative key in the form `\"{scope}/{collectedAt}\"`.\n * The owner address is prepended automatically.\n */\n async upload(file: Blob, filename?: string): Promise<StorageUploadResult> {\n if (!filename) {\n throw new StorageError(\n \"VanaStorage.upload requires a filename of the form '{scope}/{collectedAt}'\",\n \"MISSING_FILENAME\",\n \"vana-storage\",\n );\n }\n\n const subpath = encodeRelativePath(filename);\n const path = `${BLOB_PATH_PREFIX}/${this.ownerAddress}/${subpath}`;\n const body = new Uint8Array(await file.arrayBuffer());\n const contentType =\n file.type !== \"\" ? file.type : \"application/octet-stream\";\n\n const header = await this.signRequest(\"PUT\", path, body);\n\n let response: Response;\n try {\n response = await this.fetchImpl(`${this.endpoint}${path}`, {\n method: \"PUT\",\n headers: {\n authorization: header,\n \"content-type\": contentType,\n },\n body,\n });\n } catch (cause) {\n throw new StorageError(\n `vana-storage upload network error: ${describe(cause)}`,\n \"UPLOAD_ERROR\",\n \"vana-storage\",\n { cause: cause instanceof Error ? cause : undefined },\n );\n }\n\n if (!response.ok) {\n throw new StorageError(\n `vana-storage upload failed: ${response.status} ${response.statusText} - ${await safeText(response)}`,\n \"UPLOAD_FAILED\",\n \"vana-storage\",\n );\n }\n\n const result = (await response.json()) as VanaStorageUploadResponse;\n return {\n url: result.url,\n size: result.size,\n contentType,\n metadata: { key: result.key, etag: result.etag },\n };\n }\n\n /**\n * Download a blob by URL. The URL must point at a path under this\n * provider's endpoint.\n */\n async download(url: string): Promise<Blob> {\n const path = this.pathFromUrl(url);\n const header = await this.signRequest(\"GET\", path);\n\n let response: Response;\n try {\n response = await this.fetchImpl(`${this.endpoint}${path}`, {\n method: \"GET\",\n headers: { authorization: header },\n });\n } catch (cause) {\n throw new StorageError(\n `vana-storage download network error: ${describe(cause)}`,\n \"DOWNLOAD_ERROR\",\n \"vana-storage\",\n { cause: cause instanceof Error ? cause : undefined },\n );\n }\n\n if (!response.ok) {\n throw new StorageError(\n `vana-storage download failed: ${response.status} ${response.statusText}`,\n \"DOWNLOAD_FAILED\",\n \"vana-storage\",\n );\n }\n\n return await response.blob();\n }\n\n /**\n * Listing is not supported by vana-storage — file discovery is handled by\n * the Gateway DataRegistry, not the storage layer.\n */\n async list(_options?: StorageListOptions): Promise<StorageFile[]> {\n throw new StorageError(\n \"list is not supported by vana-storage; query the Gateway DataRegistry instead\",\n \"NOT_IMPLEMENTED\",\n \"vana-storage\",\n );\n }\n\n async delete(url: string): Promise<boolean> {\n const path = this.pathFromUrl(url);\n const header = await this.signRequest(\"DELETE\", path);\n\n let response: Response;\n try {\n response = await this.fetchImpl(`${this.endpoint}${path}`, {\n method: \"DELETE\",\n headers: { authorization: header },\n });\n } catch (cause) {\n throw new StorageError(\n `vana-storage delete network error: ${describe(cause)}`,\n \"DELETE_ERROR\",\n \"vana-storage\",\n { cause: cause instanceof Error ? cause : undefined },\n );\n }\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new StorageError(\n `vana-storage delete failed: ${response.status} ${response.statusText}`,\n \"DELETE_FAILED\",\n \"vana-storage\",\n );\n }\n return true;\n }\n\n getConfig(): StorageProviderConfig {\n return {\n name: \"vana-storage\",\n type: \"vana-storage\",\n requiresAuth: true,\n features: {\n upload: true,\n download: true,\n list: false,\n delete: true,\n },\n };\n }\n\n private async signRequest(\n method: \"GET\" | \"PUT\" | \"DELETE\",\n path: string,\n body?: Uint8Array,\n ): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n return buildWeb3SignedHeader({\n signMessage: this.signer.signMessage,\n aud: this.endpoint,\n method,\n uri: path,\n iat: now,\n exp: now + DEFAULT_TOKEN_TTL_SECONDS,\n ...(body !== undefined && body.length > 0 && { body }),\n });\n }\n\n private pathFromUrl(url: string): string {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new StorageError(\n `Invalid URL: ${url}`,\n \"INVALID_URL\",\n \"vana-storage\",\n );\n }\n const expectedHost = new URL(this.endpoint).host;\n if (parsed.host !== expectedHost) {\n throw new StorageError(\n `URL host '${parsed.host}' does not match storage endpoint '${expectedHost}'`,\n \"INVALID_URL\",\n \"vana-storage\",\n );\n }\n // Restrict to /v1/blobs/{owner}/{scope}/{collectedAt} so a caller\n // cannot induce this wallet to sign arbitrary same-host paths.\n const segments = parsed.pathname.split(\"/\").filter((s) => s.length > 0);\n const isTraversal = (s: string): boolean => s === \".\" || s === \"..\";\n const valid =\n segments.length === 5 &&\n segments[0] === \"v1\" &&\n segments[1] === \"blobs\" &&\n segments[2]?.toLowerCase() === this.ownerAddress &&\n segments[3] !== undefined &&\n !isTraversal(segments[3]) &&\n segments[4] !== undefined &&\n !isTraversal(segments[4]);\n if (!valid) {\n throw new StorageError(\n `URL path '${parsed.pathname}' must be /v1/blobs/${this.ownerAddress}/{scope}/{collectedAt}`,\n \"INVALID_URL\",\n \"vana-storage\",\n );\n }\n return parsed.pathname;\n }\n}\n\nfunction encodeRelativePath(filename: string): string {\n const parts = filename.split(\"/\");\n if (\n parts.length !== 2 ||\n parts.some((p) => p.length === 0 || p === \".\" || p === \"..\")\n ) {\n throw new StorageError(\n `filename must be exactly '{scope}/{collectedAt}' with non-empty segments, got '${filename}'`,\n \"INVALID_FILENAME\",\n \"vana-storage\",\n );\n }\n return parts.map((p) => encodeURIComponent(p)).join(\"/\");\n}\n\nfunction describe(value: unknown): string {\n if (value instanceof Error) return value.message;\n return String(value);\n}\n\nasync function safeText(response: Response): Promise<string> {\n try {\n return await response.text();\n } catch {\n return \"\";\n }\n}\n"],"mappings":"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","names":[]}