{"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":[]}