UNPKG

@opendatalabs/vana-sdk

Version:

A TypeScript library for interacting with Vana Network smart contracts.

231 lines 6.98 kB
import { StorageError } from "../index.js"; import { buildWeb3SignedHeader } from "../../auth/web3-signed-builder.js"; const DEFAULT_ENDPOINT = "https://storage.vana.org"; const BLOB_PATH_PREFIX = "/v1/blobs"; const DEFAULT_TOKEN_TTL_SECONDS = 300; class VanaStorage { endpoint; signer; ownerAddress; fetchImpl; constructor(config) { if (!config?.signer?.address || !config?.signer?.signMessage) { throw new StorageError( "VanaStorage requires a signer with address and signMessage", "MISSING_SIGNER", "vana-storage" ); } this.endpoint = (config.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, ""); this.signer = config.signer; this.ownerAddress = (config.ownerAddress ?? config.signer.address).toLowerCase(); this.fetchImpl = config.fetchImpl ?? globalThis.fetch.bind(globalThis); } /** * Upload an encrypted blob to vana-storage. * * @param file - The blob to upload. * @param filename - Required relative key in the form `"{scope}/{collectedAt}"`. * The owner address is prepended automatically. */ async upload(file, filename) { if (!filename) { throw new StorageError( "VanaStorage.upload requires a filename of the form '{scope}/{collectedAt}'", "MISSING_FILENAME", "vana-storage" ); } const subpath = encodeRelativePath(filename); const path = `${BLOB_PATH_PREFIX}/${this.ownerAddress}/${subpath}`; const body = new Uint8Array(await file.arrayBuffer()); const contentType = file.type !== "" ? file.type : "application/octet-stream"; const header = await this.signRequest("PUT", path, body); let response; try { response = await this.fetchImpl(`${this.endpoint}${path}`, { method: "PUT", headers: { authorization: header, "content-type": contentType }, body }); } catch (cause) { throw new StorageError( `vana-storage upload network error: ${describe(cause)}`, "UPLOAD_ERROR", "vana-storage", { cause: cause instanceof Error ? cause : void 0 } ); } if (!response.ok) { throw new StorageError( `vana-storage upload failed: ${response.status} ${response.statusText} - ${await safeText(response)}`, "UPLOAD_FAILED", "vana-storage" ); } const result = await response.json(); return { url: result.url, size: result.size, contentType, metadata: { key: result.key, etag: result.etag } }; } /** * Download a blob by URL. The URL must point at a path under this * provider's endpoint. */ async download(url) { const path = this.pathFromUrl(url); const header = await this.signRequest("GET", path); let response; try { response = await this.fetchImpl(`${this.endpoint}${path}`, { method: "GET", headers: { authorization: header } }); } catch (cause) { throw new StorageError( `vana-storage download network error: ${describe(cause)}`, "DOWNLOAD_ERROR", "vana-storage", { cause: cause instanceof Error ? cause : void 0 } ); } if (!response.ok) { throw new StorageError( `vana-storage download failed: ${response.status} ${response.statusText}`, "DOWNLOAD_FAILED", "vana-storage" ); } return await response.blob(); } /** * Listing is not supported by vana-storage — file discovery is handled by * the Gateway DataRegistry, not the storage layer. */ async list(_options) { throw new StorageError( "list is not supported by vana-storage; query the Gateway DataRegistry instead", "NOT_IMPLEMENTED", "vana-storage" ); } async delete(url) { const path = this.pathFromUrl(url); const header = await this.signRequest("DELETE", path); let response; try { response = await this.fetchImpl(`${this.endpoint}${path}`, { method: "DELETE", headers: { authorization: header } }); } catch (cause) { throw new StorageError( `vana-storage delete network error: ${describe(cause)}`, "DELETE_ERROR", "vana-storage", { cause: cause instanceof Error ? cause : void 0 } ); } if (response.status === 404) return false; if (!response.ok) { throw new StorageError( `vana-storage delete failed: ${response.status} ${response.statusText}`, "DELETE_FAILED", "vana-storage" ); } return true; } getConfig() { return { name: "vana-storage", type: "vana-storage", requiresAuth: true, features: { upload: true, download: true, list: false, delete: true } }; } async signRequest(method, path, body) { const now = Math.floor(Date.now() / 1e3); return buildWeb3SignedHeader({ signMessage: this.signer.signMessage, aud: this.endpoint, method, uri: path, iat: now, exp: now + DEFAULT_TOKEN_TTL_SECONDS, ...body !== void 0 && body.length > 0 && { body } }); } pathFromUrl(url) { let parsed; try { parsed = new URL(url); } catch { throw new StorageError( `Invalid URL: ${url}`, "INVALID_URL", "vana-storage" ); } const expectedHost = new URL(this.endpoint).host; if (parsed.host !== expectedHost) { throw new StorageError( `URL host '${parsed.host}' does not match storage endpoint '${expectedHost}'`, "INVALID_URL", "vana-storage" ); } const segments = parsed.pathname.split("/").filter((s) => s.length > 0); const isTraversal = (s) => s === "." || s === ".."; const valid = segments.length === 5 && segments[0] === "v1" && segments[1] === "blobs" && segments[2]?.toLowerCase() === this.ownerAddress && segments[3] !== void 0 && !isTraversal(segments[3]) && segments[4] !== void 0 && !isTraversal(segments[4]); if (!valid) { throw new StorageError( `URL path '${parsed.pathname}' must be /v1/blobs/${this.ownerAddress}/{scope}/{collectedAt}`, "INVALID_URL", "vana-storage" ); } return parsed.pathname; } } function encodeRelativePath(filename) { const parts = filename.split("/"); if (parts.length !== 2 || parts.some((p) => p.length === 0 || p === "." || p === "..")) { throw new StorageError( `filename must be exactly '{scope}/{collectedAt}' with non-empty segments, got '${filename}'`, "INVALID_FILENAME", "vana-storage" ); } return parts.map((p) => encodeURIComponent(p)).join("/"); } function describe(value) { if (value instanceof Error) return value.message; return String(value); } async function safeText(response) { try { return await response.text(); } catch { return ""; } } export { VanaStorage }; //# sourceMappingURL=vana-storage.js.map