@opendatalabs/vana-sdk 3.17.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/direct/personal-server-read.cjs +20 -1
  2. package/dist/direct/personal-server-read.cjs.map +1 -1
  3. package/dist/direct/personal-server-read.js +23 -1
  4. package/dist/direct/personal-server-read.js.map +1 -1
  5. package/dist/errors.cjs +27 -0
  6. package/dist/errors.cjs.map +1 -1
  7. package/dist/errors.d.ts +67 -0
  8. package/dist/errors.js +24 -0
  9. package/dist/errors.js.map +1 -1
  10. package/dist/index.browser.d.ts +2 -1
  11. package/dist/index.browser.js +724 -309
  12. package/dist/index.browser.js.map +4 -4
  13. package/dist/index.node.cjs +736 -311
  14. package/dist/index.node.cjs.map +4 -4
  15. package/dist/index.node.d.ts +2 -1
  16. package/dist/index.node.js +724 -309
  17. package/dist/index.node.js.map +4 -4
  18. package/dist/protocol/data-point-deletion.cjs +220 -0
  19. package/dist/protocol/data-point-deletion.cjs.map +1 -0
  20. package/dist/protocol/data-point-deletion.d.ts +180 -0
  21. package/dist/protocol/data-point-deletion.js +193 -0
  22. package/dist/protocol/data-point-deletion.js.map +1 -0
  23. package/dist/protocol/data-point-deletion.test.d.ts +1 -0
  24. package/dist/protocol/gateway.cjs +145 -32
  25. package/dist/protocol/gateway.cjs.map +1 -1
  26. package/dist/protocol/gateway.d.ts +42 -3
  27. package/dist/protocol/gateway.js +156 -32
  28. package/dist/protocol/gateway.js.map +1 -1
  29. package/dist/protocol/personal-server-data.cjs +20 -1
  30. package/dist/protocol/personal-server-data.cjs.map +1 -1
  31. package/dist/protocol/personal-server-data.js +23 -1
  32. package/dist/protocol/personal-server-data.js.map +1 -1
  33. package/dist/storage/index.cjs.map +1 -1
  34. package/dist/storage/index.d.ts +1 -1
  35. package/dist/storage/index.js.map +1 -1
  36. package/dist/storage/providers/vana-storage.cjs +66 -0
  37. package/dist/storage/providers/vana-storage.cjs.map +1 -1
  38. package/dist/storage/providers/vana-storage.d.ts +26 -0
  39. package/dist/storage/providers/vana-storage.js +66 -0
  40. package/dist/storage/providers/vana-storage.js.map +1 -1
  41. package/dist/utils/response-body.cjs +46 -0
  42. package/dist/utils/response-body.cjs.map +1 -0
  43. package/dist/utils/response-body.d.ts +26 -0
  44. package/dist/utils/response-body.js +20 -0
  45. package/dist/utils/response-body.js.map +1 -0
  46. package/dist/utils/response-body.test.d.ts +1 -0
  47. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/protocol/personal-server-data.ts"],"sourcesContent":["import {\n buildWeb3SignedHeader,\n type Web3SignedSignFn,\n} from \"../auth/web3-signed-builder\";\nimport { DataFileEnvelopeSchema, type DataFileEnvelope } from \"./data-file\";\n\nexport interface BuildPersonalServerDataReadRequestParams {\n personalServerUrl: string;\n scope: string;\n grantId: string;\n signMessage: Web3SignedSignFn;\n audience?: string;\n headers?: HeadersInit;\n}\n\nexport interface ReadPersonalServerDataParams extends BuildPersonalServerDataReadRequestParams {\n fetch?: typeof fetch;\n}\n\nexport function personalServerDataReadPath(scope: string): string {\n return `/v1/data/${encodeURIComponent(scope)}`;\n}\n\nexport async function buildPersonalServerDataReadRequest(\n params: BuildPersonalServerDataReadRequestParams,\n): Promise<Request> {\n const path = personalServerDataReadPath(params.scope);\n const baseUrl = params.personalServerUrl.replace(/\\/+$/, \"\");\n const audience = params.audience ?? baseUrl;\n const headers = new Headers(params.headers);\n\n headers.set(\n \"Authorization\",\n await buildWeb3SignedHeader({\n aud: audience,\n grantId: params.grantId,\n method: \"GET\",\n signMessage: params.signMessage,\n uri: path,\n }),\n );\n\n return new Request(`${baseUrl}${path}`, {\n headers,\n method: \"GET\",\n });\n}\n\nexport async function readPersonalServerData(\n params: ReadPersonalServerDataParams,\n): Promise<DataFileEnvelope> {\n const fetchFn = params.fetch ?? globalThis.fetch;\n if (fetchFn === undefined) {\n throw new Error(\"No fetch implementation available\");\n }\n\n const request = await buildPersonalServerDataReadRequest(params);\n const response = await fetchFn(request);\n\n if (!response.ok) {\n throw new Error(\n `Personal Server data read failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return DataFileEnvelopeSchema.parse(await response.json());\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAGO;AACP,uBAA8D;AAevD,SAAS,2BAA2B,OAAuB;AAChE,SAAO,YAAY,mBAAmB,KAAK,CAAC;AAC9C;AAEA,eAAsB,mCACpB,QACkB;AAClB,QAAM,OAAO,2BAA2B,OAAO,KAAK;AACpD,QAAM,UAAU,OAAO,kBAAkB,QAAQ,QAAQ,EAAE;AAC3D,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAE1C,UAAQ;AAAA,IACN;AAAA,IACA,UAAM,kDAAsB;AAAA,MAC1B,KAAK;AAAA,MACL,SAAS,OAAO;AAAA,MAChB,QAAQ;AAAA,MACR,aAAa,OAAO;AAAA,MACpB,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,QAAQ,GAAG,OAAO,GAAG,IAAI,IAAI;AAAA,IACtC;AAAA,IACA,QAAQ;AAAA,EACV,CAAC;AACH;AAEA,eAAsB,uBACpB,QAC2B;AAC3B,QAAM,UAAU,OAAO,SAAS,WAAW;AAC3C,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAEA,QAAM,UAAU,MAAM,mCAAmC,MAAM;AAC/D,QAAM,WAAW,MAAM,QAAQ,OAAO;AAEtC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,qCAAqC,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IAC7E;AAAA,EACF;AAEA,SAAO,wCAAuB,MAAM,MAAM,SAAS,KAAK,CAAC;AAC3D;","names":[]}
1
+ {"version":3,"sources":["../../src/protocol/personal-server-data.ts"],"sourcesContent":["import {\n buildWeb3SignedHeader,\n type Web3SignedSignFn,\n} from \"../auth/web3-signed-builder\";\nimport { DataPointDeletedError } from \"../errors\";\nimport { DataFileEnvelopeSchema, type DataFileEnvelope } from \"./data-file\";\nimport {\n isDataPointTombstone,\n tombstoneDeletedAt,\n} from \"./data-point-deletion\";\nimport { readJsonValue } from \"../utils/response-body\";\n\nexport interface BuildPersonalServerDataReadRequestParams {\n personalServerUrl: string;\n scope: string;\n grantId: string;\n signMessage: Web3SignedSignFn;\n audience?: string;\n headers?: HeadersInit;\n}\n\nexport interface ReadPersonalServerDataParams extends BuildPersonalServerDataReadRequestParams {\n fetch?: typeof fetch;\n}\n\nexport function personalServerDataReadPath(scope: string): string {\n return `/v1/data/${encodeURIComponent(scope)}`;\n}\n\nexport async function buildPersonalServerDataReadRequest(\n params: BuildPersonalServerDataReadRequestParams,\n): Promise<Request> {\n const path = personalServerDataReadPath(params.scope);\n const baseUrl = params.personalServerUrl.replace(/\\/+$/, \"\");\n const audience = params.audience ?? baseUrl;\n const headers = new Headers(params.headers);\n\n headers.set(\n \"Authorization\",\n await buildWeb3SignedHeader({\n aud: audience,\n grantId: params.grantId,\n method: \"GET\",\n signMessage: params.signMessage,\n uri: path,\n }),\n );\n\n return new Request(`${baseUrl}${path}`, {\n headers,\n method: \"GET\",\n });\n}\n\nexport async function readPersonalServerData(\n params: ReadPersonalServerDataParams,\n): Promise<DataFileEnvelope> {\n const fetchFn = params.fetch ?? globalThis.fetch;\n if (fetchFn === undefined) {\n throw new Error(\"No fetch implementation available\");\n }\n\n const request = await buildPersonalServerDataReadRequest(params);\n const response = await fetchFn(request);\n\n // 410 = the scope was tombstoned. Typed so callers can branch on it\n // instead of pattern-matching a generic read failure.\n if (response.status === 410) {\n throw new DataPointDeletedError(\n `Personal Server scope '${params.scope}' has been deleted`,\n {\n scope: params.scope,\n deletedAt: tombstoneDeletedAt(await readJsonValue(response)),\n },\n );\n }\n\n if (!response.ok) {\n throw new Error(\n `Personal Server data read failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const body: unknown = await response.json();\n // A tombstone must never be returned as data, even if the server answered\n // 200 with the deleted row (deletedAt / tombstone hash pair).\n if (isDataPointTombstone(body)) {\n throw new DataPointDeletedError(\n `Personal Server scope '${params.scope}' has been deleted`,\n { scope: params.scope, deletedAt: tombstoneDeletedAt(body) },\n );\n }\n\n return DataFileEnvelopeSchema.parse(body);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAGO;AACP,oBAAsC;AACtC,uBAA8D;AAC9D,iCAGO;AACP,2BAA8B;AAevB,SAAS,2BAA2B,OAAuB;AAChE,SAAO,YAAY,mBAAmB,KAAK,CAAC;AAC9C;AAEA,eAAsB,mCACpB,QACkB;AAClB,QAAM,OAAO,2BAA2B,OAAO,KAAK;AACpD,QAAM,UAAU,OAAO,kBAAkB,QAAQ,QAAQ,EAAE;AAC3D,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAE1C,UAAQ;AAAA,IACN;AAAA,IACA,UAAM,kDAAsB;AAAA,MAC1B,KAAK;AAAA,MACL,SAAS,OAAO;AAAA,MAChB,QAAQ;AAAA,MACR,aAAa,OAAO;AAAA,MACpB,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,QAAQ,GAAG,OAAO,GAAG,IAAI,IAAI;AAAA,IACtC;AAAA,IACA,QAAQ;AAAA,EACV,CAAC;AACH;AAEA,eAAsB,uBACpB,QAC2B;AAC3B,QAAM,UAAU,OAAO,SAAS,WAAW;AAC3C,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAEA,QAAM,UAAU,MAAM,mCAAmC,MAAM;AAC/D,QAAM,WAAW,MAAM,QAAQ,OAAO;AAItC,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI;AAAA,MACR,0BAA0B,OAAO,KAAK;AAAA,MACtC;AAAA,QACE,OAAO,OAAO;AAAA,QACd,eAAW,+CAAmB,UAAM,oCAAc,QAAQ,CAAC;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,qCAAqC,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IAC7E;AAAA,EACF;AAEA,QAAM,OAAgB,MAAM,SAAS,KAAK;AAG1C,UAAI,iDAAqB,IAAI,GAAG;AAC9B,UAAM,IAAI;AAAA,MACR,0BAA0B,OAAO,KAAK;AAAA,MACtC,EAAE,OAAO,OAAO,OAAO,eAAW,+CAAmB,IAAI,EAAE;AAAA,IAC7D;AAAA,EACF;AAEA,SAAO,wCAAuB,MAAM,IAAI;AAC1C;","names":[]}
@@ -1,7 +1,13 @@
1
1
  import {
2
2
  buildWeb3SignedHeader
3
3
  } from "../auth/web3-signed-builder.js";
4
+ import { DataPointDeletedError } from "../errors.js";
4
5
  import { DataFileEnvelopeSchema } from "./data-file.js";
6
+ import {
7
+ isDataPointTombstone,
8
+ tombstoneDeletedAt
9
+ } from "./data-point-deletion.js";
10
+ import { readJsonValue } from "../utils/response-body.js";
5
11
  function personalServerDataReadPath(scope) {
6
12
  return `/v1/data/${encodeURIComponent(scope)}`;
7
13
  }
@@ -32,12 +38,28 @@ async function readPersonalServerData(params) {
32
38
  }
33
39
  const request = await buildPersonalServerDataReadRequest(params);
34
40
  const response = await fetchFn(request);
41
+ if (response.status === 410) {
42
+ throw new DataPointDeletedError(
43
+ `Personal Server scope '${params.scope}' has been deleted`,
44
+ {
45
+ scope: params.scope,
46
+ deletedAt: tombstoneDeletedAt(await readJsonValue(response))
47
+ }
48
+ );
49
+ }
35
50
  if (!response.ok) {
36
51
  throw new Error(
37
52
  `Personal Server data read failed: ${response.status} ${response.statusText}`
38
53
  );
39
54
  }
40
- return DataFileEnvelopeSchema.parse(await response.json());
55
+ const body = await response.json();
56
+ if (isDataPointTombstone(body)) {
57
+ throw new DataPointDeletedError(
58
+ `Personal Server scope '${params.scope}' has been deleted`,
59
+ { scope: params.scope, deletedAt: tombstoneDeletedAt(body) }
60
+ );
61
+ }
62
+ return DataFileEnvelopeSchema.parse(body);
41
63
  }
42
64
  export {
43
65
  buildPersonalServerDataReadRequest,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/protocol/personal-server-data.ts"],"sourcesContent":["import {\n buildWeb3SignedHeader,\n type Web3SignedSignFn,\n} from \"../auth/web3-signed-builder\";\nimport { DataFileEnvelopeSchema, type DataFileEnvelope } from \"./data-file\";\n\nexport interface BuildPersonalServerDataReadRequestParams {\n personalServerUrl: string;\n scope: string;\n grantId: string;\n signMessage: Web3SignedSignFn;\n audience?: string;\n headers?: HeadersInit;\n}\n\nexport interface ReadPersonalServerDataParams extends BuildPersonalServerDataReadRequestParams {\n fetch?: typeof fetch;\n}\n\nexport function personalServerDataReadPath(scope: string): string {\n return `/v1/data/${encodeURIComponent(scope)}`;\n}\n\nexport async function buildPersonalServerDataReadRequest(\n params: BuildPersonalServerDataReadRequestParams,\n): Promise<Request> {\n const path = personalServerDataReadPath(params.scope);\n const baseUrl = params.personalServerUrl.replace(/\\/+$/, \"\");\n const audience = params.audience ?? baseUrl;\n const headers = new Headers(params.headers);\n\n headers.set(\n \"Authorization\",\n await buildWeb3SignedHeader({\n aud: audience,\n grantId: params.grantId,\n method: \"GET\",\n signMessage: params.signMessage,\n uri: path,\n }),\n );\n\n return new Request(`${baseUrl}${path}`, {\n headers,\n method: \"GET\",\n });\n}\n\nexport async function readPersonalServerData(\n params: ReadPersonalServerDataParams,\n): Promise<DataFileEnvelope> {\n const fetchFn = params.fetch ?? globalThis.fetch;\n if (fetchFn === undefined) {\n throw new Error(\"No fetch implementation available\");\n }\n\n const request = await buildPersonalServerDataReadRequest(params);\n const response = await fetchFn(request);\n\n if (!response.ok) {\n throw new Error(\n `Personal Server data read failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return DataFileEnvelopeSchema.parse(await response.json());\n}\n"],"mappings":"AAAA;AAAA,EACE;AAAA,OAEK;AACP,SAAS,8BAAqD;AAevD,SAAS,2BAA2B,OAAuB;AAChE,SAAO,YAAY,mBAAmB,KAAK,CAAC;AAC9C;AAEA,eAAsB,mCACpB,QACkB;AAClB,QAAM,OAAO,2BAA2B,OAAO,KAAK;AACpD,QAAM,UAAU,OAAO,kBAAkB,QAAQ,QAAQ,EAAE;AAC3D,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAE1C,UAAQ;AAAA,IACN;AAAA,IACA,MAAM,sBAAsB;AAAA,MAC1B,KAAK;AAAA,MACL,SAAS,OAAO;AAAA,MAChB,QAAQ;AAAA,MACR,aAAa,OAAO;AAAA,MACpB,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,QAAQ,GAAG,OAAO,GAAG,IAAI,IAAI;AAAA,IACtC;AAAA,IACA,QAAQ;AAAA,EACV,CAAC;AACH;AAEA,eAAsB,uBACpB,QAC2B;AAC3B,QAAM,UAAU,OAAO,SAAS,WAAW;AAC3C,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAEA,QAAM,UAAU,MAAM,mCAAmC,MAAM;AAC/D,QAAM,WAAW,MAAM,QAAQ,OAAO;AAEtC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,qCAAqC,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IAC7E;AAAA,EACF;AAEA,SAAO,uBAAuB,MAAM,MAAM,SAAS,KAAK,CAAC;AAC3D;","names":[]}
1
+ {"version":3,"sources":["../../src/protocol/personal-server-data.ts"],"sourcesContent":["import {\n buildWeb3SignedHeader,\n type Web3SignedSignFn,\n} from \"../auth/web3-signed-builder\";\nimport { DataPointDeletedError } from \"../errors\";\nimport { DataFileEnvelopeSchema, type DataFileEnvelope } from \"./data-file\";\nimport {\n isDataPointTombstone,\n tombstoneDeletedAt,\n} from \"./data-point-deletion\";\nimport { readJsonValue } from \"../utils/response-body\";\n\nexport interface BuildPersonalServerDataReadRequestParams {\n personalServerUrl: string;\n scope: string;\n grantId: string;\n signMessage: Web3SignedSignFn;\n audience?: string;\n headers?: HeadersInit;\n}\n\nexport interface ReadPersonalServerDataParams extends BuildPersonalServerDataReadRequestParams {\n fetch?: typeof fetch;\n}\n\nexport function personalServerDataReadPath(scope: string): string {\n return `/v1/data/${encodeURIComponent(scope)}`;\n}\n\nexport async function buildPersonalServerDataReadRequest(\n params: BuildPersonalServerDataReadRequestParams,\n): Promise<Request> {\n const path = personalServerDataReadPath(params.scope);\n const baseUrl = params.personalServerUrl.replace(/\\/+$/, \"\");\n const audience = params.audience ?? baseUrl;\n const headers = new Headers(params.headers);\n\n headers.set(\n \"Authorization\",\n await buildWeb3SignedHeader({\n aud: audience,\n grantId: params.grantId,\n method: \"GET\",\n signMessage: params.signMessage,\n uri: path,\n }),\n );\n\n return new Request(`${baseUrl}${path}`, {\n headers,\n method: \"GET\",\n });\n}\n\nexport async function readPersonalServerData(\n params: ReadPersonalServerDataParams,\n): Promise<DataFileEnvelope> {\n const fetchFn = params.fetch ?? globalThis.fetch;\n if (fetchFn === undefined) {\n throw new Error(\"No fetch implementation available\");\n }\n\n const request = await buildPersonalServerDataReadRequest(params);\n const response = await fetchFn(request);\n\n // 410 = the scope was tombstoned. Typed so callers can branch on it\n // instead of pattern-matching a generic read failure.\n if (response.status === 410) {\n throw new DataPointDeletedError(\n `Personal Server scope '${params.scope}' has been deleted`,\n {\n scope: params.scope,\n deletedAt: tombstoneDeletedAt(await readJsonValue(response)),\n },\n );\n }\n\n if (!response.ok) {\n throw new Error(\n `Personal Server data read failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const body: unknown = await response.json();\n // A tombstone must never be returned as data, even if the server answered\n // 200 with the deleted row (deletedAt / tombstone hash pair).\n if (isDataPointTombstone(body)) {\n throw new DataPointDeletedError(\n `Personal Server scope '${params.scope}' has been deleted`,\n { scope: params.scope, deletedAt: tombstoneDeletedAt(body) },\n );\n }\n\n return DataFileEnvelopeSchema.parse(body);\n}\n"],"mappings":"AAAA;AAAA,EACE;AAAA,OAEK;AACP,SAAS,6BAA6B;AACtC,SAAS,8BAAqD;AAC9D;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAevB,SAAS,2BAA2B,OAAuB;AAChE,SAAO,YAAY,mBAAmB,KAAK,CAAC;AAC9C;AAEA,eAAsB,mCACpB,QACkB;AAClB,QAAM,OAAO,2BAA2B,OAAO,KAAK;AACpD,QAAM,UAAU,OAAO,kBAAkB,QAAQ,QAAQ,EAAE;AAC3D,QAAM,WAAW,OAAO,YAAY;AACpC,QAAM,UAAU,IAAI,QAAQ,OAAO,OAAO;AAE1C,UAAQ;AAAA,IACN;AAAA,IACA,MAAM,sBAAsB;AAAA,MAC1B,KAAK;AAAA,MACL,SAAS,OAAO;AAAA,MAChB,QAAQ;AAAA,MACR,aAAa,OAAO;AAAA,MACpB,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,QAAQ,GAAG,OAAO,GAAG,IAAI,IAAI;AAAA,IACtC;AAAA,IACA,QAAQ;AAAA,EACV,CAAC;AACH;AAEA,eAAsB,uBACpB,QAC2B;AAC3B,QAAM,UAAU,OAAO,SAAS,WAAW;AAC3C,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAEA,QAAM,UAAU,MAAM,mCAAmC,MAAM;AAC/D,QAAM,WAAW,MAAM,QAAQ,OAAO;AAItC,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI;AAAA,MACR,0BAA0B,OAAO,KAAK;AAAA,MACtC;AAAA,QACE,OAAO,OAAO;AAAA,QACd,WAAW,mBAAmB,MAAM,cAAc,QAAQ,CAAC;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,qCAAqC,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IAC7E;AAAA,EACF;AAEA,QAAM,OAAgB,MAAM,SAAS,KAAK;AAG1C,MAAI,qBAAqB,IAAI,GAAG;AAC9B,UAAM,IAAI;AAAA,MACR,0BAA0B,OAAO,KAAK;AAAA,MACtC,EAAE,OAAO,OAAO,OAAO,WAAW,mBAAmB,IAAI,EAAE;AAAA,IAC7D;AAAA,EACF;AAEA,SAAO,uBAAuB,MAAM,IAAI;AAC1C;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/storage/index.ts"],"sourcesContent":["/**\n * Storage API for Vana SDK\n *\n * Provides unified interface for different storage providers\n * to upload, download, and manage user data files.\n *\n * ## Storage Provider Decision Tree\n *\n * Choose your storage provider based on your needs:\n *\n * **Default backend hosted by ODL?**\n * - ✅ Use `VanaStorage` - storage.vana.org with Web3Signed auth.\n *\n * **Need full CRUD operations and metadata?**\n * - ✅ Use `PinataStorage` - Managed IPFS with listing, deletion, and rich metadata\n *\n * **Want to use your own IPFS infrastructure?**\n * - ✅ Use `IpfsStorage.forInfura()` - Connect to Infura IPFS service\n * - ✅ Use `IpfsStorage.forLocalNode()` - Connect to local IPFS node\n * - ✅ Use `new IpfsStorage()` - Connect to any IPFS-compatible service\n *\n * **Want flexible callback-based storage?**\n * - ✅ Use `CallbackStorage` - Implement storage via custom callbacks (HTTP, WebSocket, etc.)\n *\n * **Need Google Drive integration?**\n * - ✅ Use `GoogleDriveStorage` - Direct Google Drive API with folder management\n *\n * @example\n * ```typescript\n * // Managed IPFS with full features\n * const pinata = new PinataStorage({ jwt: \"your-jwt\" });\n *\n * // Standard IPFS with Infura\n * const ipfs = IpfsStorage.forInfura({ projectId: \"...\", projectSecret: \"...\" });\n *\n * // Callback-based storage (flexible)\n * const storage = new CallbackStorage({\n * async upload(blob, filename) {\n * // Your custom upload logic\n * const response = await fetch('/api/upload', { method: 'POST', body: blob });\n * const data = await response.json();\n * return { url: data.url, size: blob.size, contentType: blob.type };\n * },\n * async download(identifier) {\n * // Your custom download logic\n * const response = await fetch(`/api/download/${identifier}`);\n * return response.blob();\n * }\n * });\n * ```\n */\n\n// Re-export storage types from types module to avoid circular dependencies\nexport type {\n StorageProvider,\n StorageUploadResult,\n StorageFile,\n StorageListOptions,\n StorageProviderConfig,\n} from \"../types/storage\";\n\nexport { StorageError } from \"../types/storage\";\n\n// Export default Vana storage factory\nexport { createVanaStorageProvider } from \"./default\";\nexport type { VanaStorageProviderOptions } from \"./default\";\n\n// Export storage providers\nexport { R2Storage } from \"./providers/r2\";\nexport type { R2Config } from \"./providers/r2\";\nexport { VanaStorage } from \"./providers/vana-storage\";\nexport type {\n VanaStorageConfig,\n VanaStorageSigner,\n} from \"./providers/vana-storage\";\nexport type { ProtocolNetwork } from \"../protocol/networks\";\nexport { GoogleDriveStorage } from \"./providers/google-drive\";\nexport { DropboxStorage } from \"./providers/dropbox\";\nexport { IpfsStorage } from \"./providers/ipfs\";\nexport { PinataStorage } from \"./providers/pinata\";\nexport { CallbackStorage } from \"./providers/callback-storage\";\n\n// Export storage manager\nexport { StorageManager } from \"./manager\";\n\n// Export storage callback types\nexport type {\n StorageCallbacks,\n StorageDownloadOptions,\n StorageListResult,\n} from \"../types/config\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DA,qBAA6B;AAG7B,qBAA0C;AAI1C,gBAA0B;AAE1B,0BAA4B;AAM5B,0BAAmC;AACnC,qBAA+B;AAC/B,kBAA4B;AAC5B,oBAA8B;AAC9B,8BAAgC;AAGhC,qBAA+B;","names":[]}
1
+ {"version":3,"sources":["../../src/storage/index.ts"],"sourcesContent":["/**\n * Storage API for Vana SDK\n *\n * Provides unified interface for different storage providers\n * to upload, download, and manage user data files.\n *\n * ## Storage Provider Decision Tree\n *\n * Choose your storage provider based on your needs:\n *\n * **Default backend hosted by ODL?**\n * - ✅ Use `VanaStorage` - storage.vana.org with Web3Signed auth.\n *\n * **Need full CRUD operations and metadata?**\n * - ✅ Use `PinataStorage` - Managed IPFS with listing, deletion, and rich metadata\n *\n * **Want to use your own IPFS infrastructure?**\n * - ✅ Use `IpfsStorage.forInfura()` - Connect to Infura IPFS service\n * - ✅ Use `IpfsStorage.forLocalNode()` - Connect to local IPFS node\n * - ✅ Use `new IpfsStorage()` - Connect to any IPFS-compatible service\n *\n * **Want flexible callback-based storage?**\n * - ✅ Use `CallbackStorage` - Implement storage via custom callbacks (HTTP, WebSocket, etc.)\n *\n * **Need Google Drive integration?**\n * - ✅ Use `GoogleDriveStorage` - Direct Google Drive API with folder management\n *\n * @example\n * ```typescript\n * // Managed IPFS with full features\n * const pinata = new PinataStorage({ jwt: \"your-jwt\" });\n *\n * // Standard IPFS with Infura\n * const ipfs = IpfsStorage.forInfura({ projectId: \"...\", projectSecret: \"...\" });\n *\n * // Callback-based storage (flexible)\n * const storage = new CallbackStorage({\n * async upload(blob, filename) {\n * // Your custom upload logic\n * const response = await fetch('/api/upload', { method: 'POST', body: blob });\n * const data = await response.json();\n * return { url: data.url, size: blob.size, contentType: blob.type };\n * },\n * async download(identifier) {\n * // Your custom download logic\n * const response = await fetch(`/api/download/${identifier}`);\n * return response.blob();\n * }\n * });\n * ```\n */\n\n// Re-export storage types from types module to avoid circular dependencies\nexport type {\n StorageProvider,\n StorageUploadResult,\n StorageFile,\n StorageListOptions,\n StorageProviderConfig,\n} from \"../types/storage\";\n\nexport { StorageError } from \"../types/storage\";\n\n// Export default Vana storage factory\nexport { createVanaStorageProvider } from \"./default\";\nexport type { VanaStorageProviderOptions } from \"./default\";\n\n// Export storage providers\nexport { R2Storage } from \"./providers/r2\";\nexport type { R2Config } from \"./providers/r2\";\nexport { VanaStorage } from \"./providers/vana-storage\";\nexport type {\n VanaStorageConfig,\n VanaStorageSigner,\n VanaStorageScopeDeleteResult,\n} from \"./providers/vana-storage\";\nexport type { ProtocolNetwork } from \"../protocol/networks\";\nexport { GoogleDriveStorage } from \"./providers/google-drive\";\nexport { DropboxStorage } from \"./providers/dropbox\";\nexport { IpfsStorage } from \"./providers/ipfs\";\nexport { PinataStorage } from \"./providers/pinata\";\nexport { CallbackStorage } from \"./providers/callback-storage\";\n\n// Export storage manager\nexport { StorageManager } from \"./manager\";\n\n// Export storage callback types\nexport type {\n StorageCallbacks,\n StorageDownloadOptions,\n StorageListResult,\n} from \"../types/config\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DA,qBAA6B;AAG7B,qBAA0C;AAI1C,gBAA0B;AAE1B,0BAA4B;AAO5B,0BAAmC;AACnC,qBAA+B;AAC/B,kBAA4B;AAC5B,oBAA8B;AAC9B,8BAAgC;AAGhC,qBAA+B;","names":[]}
@@ -56,7 +56,7 @@ export type { VanaStorageProviderOptions } from "./default.js";
56
56
  export { R2Storage } from "./providers/r2.js";
57
57
  export type { R2Config } from "./providers/r2.js";
58
58
  export { VanaStorage } from "./providers/vana-storage.js";
59
- export type { VanaStorageConfig, VanaStorageSigner, } from "./providers/vana-storage.js";
59
+ export type { VanaStorageConfig, VanaStorageSigner, VanaStorageScopeDeleteResult, } from "./providers/vana-storage.js";
60
60
  export type { ProtocolNetwork } from "../protocol/networks.js";
61
61
  export { GoogleDriveStorage } from "./providers/google-drive.js";
62
62
  export { DropboxStorage } from "./providers/dropbox.js";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/storage/index.ts"],"sourcesContent":["/**\n * Storage API for Vana SDK\n *\n * Provides unified interface for different storage providers\n * to upload, download, and manage user data files.\n *\n * ## Storage Provider Decision Tree\n *\n * Choose your storage provider based on your needs:\n *\n * **Default backend hosted by ODL?**\n * - ✅ Use `VanaStorage` - storage.vana.org with Web3Signed auth.\n *\n * **Need full CRUD operations and metadata?**\n * - ✅ Use `PinataStorage` - Managed IPFS with listing, deletion, and rich metadata\n *\n * **Want to use your own IPFS infrastructure?**\n * - ✅ Use `IpfsStorage.forInfura()` - Connect to Infura IPFS service\n * - ✅ Use `IpfsStorage.forLocalNode()` - Connect to local IPFS node\n * - ✅ Use `new IpfsStorage()` - Connect to any IPFS-compatible service\n *\n * **Want flexible callback-based storage?**\n * - ✅ Use `CallbackStorage` - Implement storage via custom callbacks (HTTP, WebSocket, etc.)\n *\n * **Need Google Drive integration?**\n * - ✅ Use `GoogleDriveStorage` - Direct Google Drive API with folder management\n *\n * @example\n * ```typescript\n * // Managed IPFS with full features\n * const pinata = new PinataStorage({ jwt: \"your-jwt\" });\n *\n * // Standard IPFS with Infura\n * const ipfs = IpfsStorage.forInfura({ projectId: \"...\", projectSecret: \"...\" });\n *\n * // Callback-based storage (flexible)\n * const storage = new CallbackStorage({\n * async upload(blob, filename) {\n * // Your custom upload logic\n * const response = await fetch('/api/upload', { method: 'POST', body: blob });\n * const data = await response.json();\n * return { url: data.url, size: blob.size, contentType: blob.type };\n * },\n * async download(identifier) {\n * // Your custom download logic\n * const response = await fetch(`/api/download/${identifier}`);\n * return response.blob();\n * }\n * });\n * ```\n */\n\n// Re-export storage types from types module to avoid circular dependencies\nexport type {\n StorageProvider,\n StorageUploadResult,\n StorageFile,\n StorageListOptions,\n StorageProviderConfig,\n} from \"../types/storage\";\n\nexport { StorageError } from \"../types/storage\";\n\n// Export default Vana storage factory\nexport { createVanaStorageProvider } from \"./default\";\nexport type { VanaStorageProviderOptions } from \"./default\";\n\n// Export storage providers\nexport { R2Storage } from \"./providers/r2\";\nexport type { R2Config } from \"./providers/r2\";\nexport { VanaStorage } from \"./providers/vana-storage\";\nexport type {\n VanaStorageConfig,\n VanaStorageSigner,\n} from \"./providers/vana-storage\";\nexport type { ProtocolNetwork } from \"../protocol/networks\";\nexport { GoogleDriveStorage } from \"./providers/google-drive\";\nexport { DropboxStorage } from \"./providers/dropbox\";\nexport { IpfsStorage } from \"./providers/ipfs\";\nexport { PinataStorage } from \"./providers/pinata\";\nexport { CallbackStorage } from \"./providers/callback-storage\";\n\n// Export storage manager\nexport { StorageManager } from \"./manager\";\n\n// Export storage callback types\nexport type {\n StorageCallbacks,\n StorageDownloadOptions,\n StorageListResult,\n} from \"../types/config\";\n"],"mappings":"AA6DA,SAAS,oBAAoB;AAG7B,SAAS,iCAAiC;AAI1C,SAAS,iBAAiB;AAE1B,SAAS,mBAAmB;AAM5B,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAGhC,SAAS,sBAAsB;","names":[]}
1
+ {"version":3,"sources":["../../src/storage/index.ts"],"sourcesContent":["/**\n * Storage API for Vana SDK\n *\n * Provides unified interface for different storage providers\n * to upload, download, and manage user data files.\n *\n * ## Storage Provider Decision Tree\n *\n * Choose your storage provider based on your needs:\n *\n * **Default backend hosted by ODL?**\n * - ✅ Use `VanaStorage` - storage.vana.org with Web3Signed auth.\n *\n * **Need full CRUD operations and metadata?**\n * - ✅ Use `PinataStorage` - Managed IPFS with listing, deletion, and rich metadata\n *\n * **Want to use your own IPFS infrastructure?**\n * - ✅ Use `IpfsStorage.forInfura()` - Connect to Infura IPFS service\n * - ✅ Use `IpfsStorage.forLocalNode()` - Connect to local IPFS node\n * - ✅ Use `new IpfsStorage()` - Connect to any IPFS-compatible service\n *\n * **Want flexible callback-based storage?**\n * - ✅ Use `CallbackStorage` - Implement storage via custom callbacks (HTTP, WebSocket, etc.)\n *\n * **Need Google Drive integration?**\n * - ✅ Use `GoogleDriveStorage` - Direct Google Drive API with folder management\n *\n * @example\n * ```typescript\n * // Managed IPFS with full features\n * const pinata = new PinataStorage({ jwt: \"your-jwt\" });\n *\n * // Standard IPFS with Infura\n * const ipfs = IpfsStorage.forInfura({ projectId: \"...\", projectSecret: \"...\" });\n *\n * // Callback-based storage (flexible)\n * const storage = new CallbackStorage({\n * async upload(blob, filename) {\n * // Your custom upload logic\n * const response = await fetch('/api/upload', { method: 'POST', body: blob });\n * const data = await response.json();\n * return { url: data.url, size: blob.size, contentType: blob.type };\n * },\n * async download(identifier) {\n * // Your custom download logic\n * const response = await fetch(`/api/download/${identifier}`);\n * return response.blob();\n * }\n * });\n * ```\n */\n\n// Re-export storage types from types module to avoid circular dependencies\nexport type {\n StorageProvider,\n StorageUploadResult,\n StorageFile,\n StorageListOptions,\n StorageProviderConfig,\n} from \"../types/storage\";\n\nexport { StorageError } from \"../types/storage\";\n\n// Export default Vana storage factory\nexport { createVanaStorageProvider } from \"./default\";\nexport type { VanaStorageProviderOptions } from \"./default\";\n\n// Export storage providers\nexport { R2Storage } from \"./providers/r2\";\nexport type { R2Config } from \"./providers/r2\";\nexport { VanaStorage } from \"./providers/vana-storage\";\nexport type {\n VanaStorageConfig,\n VanaStorageSigner,\n VanaStorageScopeDeleteResult,\n} from \"./providers/vana-storage\";\nexport type { ProtocolNetwork } from \"../protocol/networks\";\nexport { GoogleDriveStorage } from \"./providers/google-drive\";\nexport { DropboxStorage } from \"./providers/dropbox\";\nexport { IpfsStorage } from \"./providers/ipfs\";\nexport { PinataStorage } from \"./providers/pinata\";\nexport { CallbackStorage } from \"./providers/callback-storage\";\n\n// Export storage manager\nexport { StorageManager } from \"./manager\";\n\n// Export storage callback types\nexport type {\n StorageCallbacks,\n StorageDownloadOptions,\n StorageListResult,\n} from \"../types/config\";\n"],"mappings":"AA6DA,SAAS,oBAAoB;AAG7B,SAAS,iCAAiC;AAI1C,SAAS,iBAAiB;AAE1B,SAAS,mBAAmB;AAO5B,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAGhC,SAAS,sBAAsB;","names":[]}
@@ -24,6 +24,7 @@ module.exports = __toCommonJS(vana_storage_exports);
24
24
  var import__ = require("../index");
25
25
  var import_web3_signed_builder = require("../../auth/web3-signed-builder");
26
26
  var import_networks = require("../../protocol/networks");
27
+ var import_response_body = require("../../utils/response-body");
27
28
  const DEFAULT_ENDPOINT = "https://storage.vana.org";
28
29
  const LEGACY_BLOB_PATH_PREFIX = "/v1/blobs";
29
30
  const DEFAULT_TOKEN_TTL_SECONDS = 300;
@@ -194,6 +195,65 @@ class VanaStorage {
194
195
  }
195
196
  return true;
196
197
  }
198
+ /**
199
+ * Delete every version's blob under `(owner, scope)` --
200
+ * `DELETE {prefix}/{owner}/{scope}` on vana-storage, signed with the same
201
+ * Web3Signed header as uploads (aud = endpoint origin, empty bodyHash).
202
+ * The worker accepts the owner's own signature or a personal server the
203
+ * owner registered with the gateway.
204
+ *
205
+ * @param ownerAddress - Must equal the provider's configured owner; a
206
+ * mismatch throws before anything is signed so this wallet can never be
207
+ * induced to sign a delete for another namespace.
208
+ * @param scope - The scope segment, e.g. `"instagram.profile"`.
209
+ */
210
+ async deleteScope(ownerAddress, scope) {
211
+ if (ownerAddress.toLowerCase() !== this.ownerAddress) {
212
+ throw new import__.StorageError(
213
+ `deleteScope owner '${ownerAddress}' does not match the configured owner '${this.ownerAddress}'`,
214
+ "INVALID_OWNER",
215
+ "vana-storage"
216
+ );
217
+ }
218
+ if (scope.length === 0 || scope.includes("/") || isTraversalSegment(scope)) {
219
+ throw new import__.StorageError(
220
+ `scope must be a single non-empty path segment, got '${scope}'`,
221
+ "INVALID_SCOPE",
222
+ "vana-storage"
223
+ );
224
+ }
225
+ const path = `${this.blobPathPrefix}/${this.ownerAddress}/${encodeURIComponent(scope)}`;
226
+ const header = await this.signRequest("DELETE", path);
227
+ let response;
228
+ try {
229
+ response = await this.fetchImpl(`${this.endpoint}${path}`, {
230
+ method: "DELETE",
231
+ headers: { authorization: header }
232
+ });
233
+ } catch (cause) {
234
+ throw new import__.StorageError(
235
+ `vana-storage scope delete network error: ${describe(cause)}`,
236
+ "DELETE_ERROR",
237
+ "vana-storage",
238
+ { cause: cause instanceof Error ? cause : void 0 }
239
+ );
240
+ }
241
+ if (!response.ok) {
242
+ const responseText = await safeText(response);
243
+ throw new import__.StorageError(
244
+ `vana-storage scope delete failed: ${response.status} ${response.statusText} - ${responseText}`,
245
+ "DELETE_FAILED",
246
+ "vana-storage"
247
+ );
248
+ }
249
+ const body = await (0, import_response_body.readJsonObject)(response);
250
+ return {
251
+ deleted: typeof body["deleted"] === "boolean" ? body["deleted"] : true,
252
+ scope: typeof body["scope"] === "string" ? body["scope"] : scope,
253
+ count: nonNegativeInteger(body["count"]) ?? 0,
254
+ totalBytes: nonNegativeInteger(body["totalBytes"]) ?? 0
255
+ };
256
+ }
197
257
  getConfig() {
198
258
  return {
199
259
  name: "vana-storage",
@@ -322,6 +382,12 @@ function encodeRelativePath(filename) {
322
382
  }
323
383
  return parts.map((p) => encodeURIComponent(p)).join("/");
324
384
  }
385
+ function isTraversalSegment(segment) {
386
+ return segment === "." || segment === "..";
387
+ }
388
+ function nonNegativeInteger(value) {
389
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
390
+ }
325
391
  function describe(value) {
326
392
  if (value instanceof Error) return value.message;
327
393
  return String(value);
@@ -1 +1 @@
1
- {"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\";\nimport {\n getProtocolNetworkChainId,\n isProtocolNetwork,\n type ProtocolNetwork,\n} from \"../../protocol/networks\";\n\nconst DEFAULT_ENDPOINT = \"https://storage.vana.org\";\nconst LEGACY_BLOB_PATH_PREFIX = \"/v1/blobs\";\nconst DEFAULT_TOKEN_TTL_SECONDS = 300;\nconst MAX_UPLOAD_ATTEMPTS = 4;\nconst MAX_RATE_LIMIT_DELAY_MS = 30_000;\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 * This selects the storage endpoint and is independent of {@link network} /\n * {@link chainId}.\n */\n endpoint?: string;\n /**\n * Named Vana protocol network that scopes blob paths. Prefer this for known\n * Vana networks because it keeps callers on the SDK's typed network registry.\n *\n * When set, uploads use chain-scoped routes derived from the network's chain\n * ID (`/v1/chains/1480/...` for mainnet, `/v1/chains/14800/...` for Moksha).\n */\n network?: ProtocolNetwork;\n /**\n * Explicit numeric chain ID for custom or future networks not yet represented\n * by {@link ProtocolNetwork}.\n *\n * When set, uploads use chain-scoped routes\n * (`/v1/chains/{chainId}/blobs/...`) so data for different chains under the\n * same owner/scope/timestamp never collides. Reads and deletes must use the\n * same chain-scoped namespace; legacy blobs should be migrated or re-collected\n * rather than read through an ambiguous fallback. When omitted, the provider\n * preserves the legacy `/v1/blobs/...` routes and behavior.\n *\n * `chainId` and {@link network} are mutually consistent ways to select the\n * namespace. If both are provided, they must resolve to the same chain ID.\n * `endpoint` remains orthogonal: it picks the storage host, not the protocol\n * namespace.\n */\n chainId?: number;\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 * When {@link VanaStorageConfig.network} or {@link VanaStorageConfig.chainId}\n * is set, paths are chain-scoped as\n * `/v1/chains/{chainId}/blobs/{owner}/{scope}/{collectedAt}` so different chains\n * never collide on the same host. The Web3Signed audience remains the endpoint\n * origin regardless of network.\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 network?: ProtocolNetwork;\n private readonly chainId?: number;\n private readonly blobPathPrefix: 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 const resolved = resolveStorageNamespace(config);\n this.network = resolved.network;\n this.chainId = resolved.chainId;\n this.blobPathPrefix =\n this.chainId !== undefined\n ? `/v1/chains/${this.chainId}/blobs`\n : LEGACY_BLOB_PATH_PREFIX;\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 = `${this.blobPathPrefix}/${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 | null = null;\n let responseText = \"\";\n for (let attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt++) {\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 break;\n }\n\n responseText = await safeText(response);\n if (response.status === 429 && attempt < MAX_UPLOAD_ATTEMPTS) {\n const delayMs = retryDelayMs(response, responseText);\n if (delayMs > 0) {\n await sleep(delayMs);\n }\n continue;\n }\n\n throw new StorageError(\n `vana-storage upload failed: ${response.status} ${response.statusText} - ${responseText}`,\n \"UPLOAD_FAILED\",\n \"vana-storage\",\n );\n }\n\n if (!response?.ok) {\n throw new StorageError(\n `vana-storage upload failed after ${MAX_UPLOAD_ATTEMPTS} attempts - ${responseText}`,\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 a blob path for this owner so a caller cannot induce this\n // wallet to sign arbitrary same-host paths.\n const route = parseBlobPath(parsed.pathname);\n if (!route || route.owner.toLowerCase() !== this.ownerAddress) {\n throw new StorageError(\n `URL path '${parsed.pathname}' must be ${this.blobPathPrefix}/${this.ownerAddress}/{scope}/{collectedAt}`,\n \"INVALID_URL\",\n \"vana-storage\",\n );\n }\n // A provider must only sign requests for its configured namespace. Chain\n // mode is strict: old legacy blobs should be migrated or re-collected\n // rather than read through a second ambiguous path.\n if (route.chainId !== this.chainId) {\n throw new StorageError(\n `URL chainId '${route.chainId ?? \"legacy\"}' does not match provider namespace '${this.namespaceDescription()}'`,\n \"INVALID_URL\",\n \"vana-storage\",\n );\n }\n return parsed.pathname;\n }\n\n private namespaceDescription(): string {\n if (this.network !== undefined) {\n return `${this.network} (${this.chainId})`;\n }\n return this.chainId === undefined ? \"legacy\" : String(this.chainId);\n }\n}\n\ninterface ParsedBlobRoute {\n chainId?: number;\n owner: string;\n scope: string;\n collectedAt: string;\n}\n\nfunction isValidChainId(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0;\n}\n\nfunction resolveStorageNamespace(\n config: Pick<VanaStorageConfig, \"network\" | \"chainId\">,\n): {\n chainId?: number;\n network?: ProtocolNetwork;\n} {\n if (config.network !== undefined && !isProtocolNetwork(config.network)) {\n throw new StorageError(\n `Unsupported vana-storage network '${String(config.network)}'`,\n \"INVALID_NETWORK\",\n \"vana-storage\",\n );\n }\n\n if (config.chainId !== undefined && !isValidChainId(config.chainId)) {\n throw new StorageError(\n `Unsupported vana-storage chainId '${String(config.chainId)}'`,\n \"INVALID_CHAIN_ID\",\n \"vana-storage\",\n );\n }\n\n if (config.network === undefined) {\n return { chainId: config.chainId };\n }\n\n const networkChainId = getProtocolNetworkChainId(config.network);\n if (config.chainId !== undefined && config.chainId !== networkChainId) {\n throw new StorageError(\n `vana-storage network '${config.network}' resolves to chainId '${networkChainId}', not '${config.chainId}'`,\n \"INVALID_CHAIN_ID\",\n \"vana-storage\",\n );\n }\n\n return { chainId: networkChainId, network: config.network };\n}\n\n/**\n * Parse a storage blob pathname into structured route info, or `null` when the\n * path is neither the legacy nor the chain-scoped blob shape. Traversal\n * segments (`.` / `..`) are rejected as invalid.\n */\nfunction parseBlobPath(pathname: string): ParsedBlobRoute | null {\n const segments = pathname.split(\"/\").filter((s) => s.length > 0);\n const isTraversal = (s: string): boolean => s === \".\" || s === \"..\";\n\n // Legacy: /v1/blobs/{owner}/{scope}/{collectedAt}\n if (\n segments.length === 5 &&\n segments[0] === \"v1\" &&\n segments[1] === \"blobs\"\n ) {\n const [, , owner, scope, collectedAt] = segments as [\n string,\n string,\n string,\n string,\n string,\n ];\n if (isTraversal(scope) || isTraversal(collectedAt)) return null;\n return { owner, scope, collectedAt };\n }\n\n // Chain-scoped: /v1/chains/{chainId}/blobs/{owner}/{scope}/{collectedAt}\n if (\n segments.length === 7 &&\n segments[0] === \"v1\" &&\n segments[1] === \"chains\" &&\n segments[3] === \"blobs\"\n ) {\n const [, , chainIdSegment, , owner, scope, collectedAt] = segments as [\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n ];\n if (!/^[0-9]+$/.test(chainIdSegment)) return null;\n const chainId = Number(chainIdSegment);\n if (!isValidChainId(chainId)) return null;\n if (isTraversal(scope) || isTraversal(collectedAt)) return null;\n return { chainId, owner, scope, collectedAt };\n }\n\n return null;\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\nfunction retryDelayMs(response: Response, responseText: string): number {\n const headerDelayMs = parseRetryAfterHeaderMs(\n response.headers.get(\"retry-after\"),\n );\n if (headerDelayMs !== null) {\n return clampRateLimitDelay(headerDelayMs);\n }\n\n return clampRateLimitDelay(parseRetryAfterBodyMs(responseText) ?? 0);\n}\n\nfunction parseRetryAfterHeaderMs(value: string | null): number | null {\n if (!value) return null;\n\n const seconds = Number(value);\n if (Number.isFinite(seconds)) {\n return seconds * 1000;\n }\n\n const dateMs = Date.parse(value);\n if (Number.isFinite(dateMs)) {\n return Math.max(0, dateMs - Date.now());\n }\n\n return null;\n}\n\nfunction parseRetryAfterBodyMs(responseText: string): number | null {\n if (!responseText) return null;\n\n try {\n const parsed = JSON.parse(responseText) as { retryAfter?: unknown };\n const seconds = Number(parsed.retryAfter);\n return Number.isFinite(seconds) ? seconds * 1000 : null;\n } catch {\n return null;\n }\n}\n\nfunction clampRateLimitDelay(delayMs: number): number {\n return Math.min(Math.max(0, delayMs), MAX_RATE_LIMIT_DELAY_MS);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\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":[]}
1
+ {"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\";\nimport {\n getProtocolNetworkChainId,\n isProtocolNetwork,\n type ProtocolNetwork,\n} from \"../../protocol/networks\";\nimport { readJsonObject } from \"../../utils/response-body\";\n\nconst DEFAULT_ENDPOINT = \"https://storage.vana.org\";\nconst LEGACY_BLOB_PATH_PREFIX = \"/v1/blobs\";\nconst DEFAULT_TOKEN_TTL_SECONDS = 300;\nconst MAX_UPLOAD_ATTEMPTS = 4;\nconst MAX_RATE_LIMIT_DELAY_MS = 30_000;\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 * This selects the storage endpoint and is independent of {@link network} /\n * {@link chainId}.\n */\n endpoint?: string;\n /**\n * Named Vana protocol network that scopes blob paths. Prefer this for known\n * Vana networks because it keeps callers on the SDK's typed network registry.\n *\n * When set, uploads use chain-scoped routes derived from the network's chain\n * ID (`/v1/chains/1480/...` for mainnet, `/v1/chains/14800/...` for Moksha).\n */\n network?: ProtocolNetwork;\n /**\n * Explicit numeric chain ID for custom or future networks not yet represented\n * by {@link ProtocolNetwork}.\n *\n * When set, uploads use chain-scoped routes\n * (`/v1/chains/{chainId}/blobs/...`) so data for different chains under the\n * same owner/scope/timestamp never collides. Reads and deletes must use the\n * same chain-scoped namespace; legacy blobs should be migrated or re-collected\n * rather than read through an ambiguous fallback. When omitted, the provider\n * preserves the legacy `/v1/blobs/...` routes and behavior.\n *\n * `chainId` and {@link network} are mutually consistent ways to select the\n * namespace. If both are provided, they must resolve to the same chain ID.\n * `endpoint` remains orthogonal: it picks the storage host, not the protocol\n * namespace.\n */\n chainId?: number;\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 * Response of {@link VanaStorage.deleteScope} -- the worker's\n * `DELETE /:owner/:scope` body. `count` is the number of blobs removed (0 is\n * a success: nothing was stored under that scope).\n *\n * @category Storage\n */\nexport interface VanaStorageScopeDeleteResult {\n deleted: boolean;\n scope: string;\n count: number;\n totalBytes: 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 * When {@link VanaStorageConfig.network} or {@link VanaStorageConfig.chainId}\n * is set, paths are chain-scoped as\n * `/v1/chains/{chainId}/blobs/{owner}/{scope}/{collectedAt}` so different chains\n * never collide on the same host. The Web3Signed audience remains the endpoint\n * origin regardless of network.\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 network?: ProtocolNetwork;\n private readonly chainId?: number;\n private readonly blobPathPrefix: 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 const resolved = resolveStorageNamespace(config);\n this.network = resolved.network;\n this.chainId = resolved.chainId;\n this.blobPathPrefix =\n this.chainId !== undefined\n ? `/v1/chains/${this.chainId}/blobs`\n : LEGACY_BLOB_PATH_PREFIX;\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 = `${this.blobPathPrefix}/${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 | null = null;\n let responseText = \"\";\n for (let attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt++) {\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 break;\n }\n\n responseText = await safeText(response);\n if (response.status === 429 && attempt < MAX_UPLOAD_ATTEMPTS) {\n const delayMs = retryDelayMs(response, responseText);\n if (delayMs > 0) {\n await sleep(delayMs);\n }\n continue;\n }\n\n throw new StorageError(\n `vana-storage upload failed: ${response.status} ${response.statusText} - ${responseText}`,\n \"UPLOAD_FAILED\",\n \"vana-storage\",\n );\n }\n\n if (!response?.ok) {\n throw new StorageError(\n `vana-storage upload failed after ${MAX_UPLOAD_ATTEMPTS} attempts - ${responseText}`,\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 /**\n * Delete every version's blob under `(owner, scope)` --\n * `DELETE {prefix}/{owner}/{scope}` on vana-storage, signed with the same\n * Web3Signed header as uploads (aud = endpoint origin, empty bodyHash).\n * The worker accepts the owner's own signature or a personal server the\n * owner registered with the gateway.\n *\n * @param ownerAddress - Must equal the provider's configured owner; a\n * mismatch throws before anything is signed so this wallet can never be\n * induced to sign a delete for another namespace.\n * @param scope - The scope segment, e.g. `\"instagram.profile\"`.\n */\n async deleteScope(\n ownerAddress: `0x${string}`,\n scope: string,\n ): Promise<VanaStorageScopeDeleteResult> {\n if (ownerAddress.toLowerCase() !== this.ownerAddress) {\n throw new StorageError(\n `deleteScope owner '${ownerAddress}' does not match the configured owner '${this.ownerAddress}'`,\n \"INVALID_OWNER\",\n \"vana-storage\",\n );\n }\n if (\n scope.length === 0 ||\n scope.includes(\"/\") ||\n isTraversalSegment(scope)\n ) {\n throw new StorageError(\n `scope must be a single non-empty path segment, got '${scope}'`,\n \"INVALID_SCOPE\",\n \"vana-storage\",\n );\n }\n\n const path = `${this.blobPathPrefix}/${this.ownerAddress}/${encodeURIComponent(scope)}`;\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 scope delete network error: ${describe(cause)}`,\n \"DELETE_ERROR\",\n \"vana-storage\",\n { cause: cause instanceof Error ? cause : undefined },\n );\n }\n\n if (!response.ok) {\n const responseText = await safeText(response);\n throw new StorageError(\n `vana-storage scope delete failed: ${response.status} ${response.statusText} - ${responseText}`,\n \"DELETE_FAILED\",\n \"vana-storage\",\n );\n }\n\n // A 2xx is the success signal; the body is informational. Tolerate an\n // empty 204, a non-JSON body, or a JSON `null`/array, and only trust\n // fields that carry the documented type.\n const body = await readJsonObject(response);\n return {\n deleted: typeof body[\"deleted\"] === \"boolean\" ? body[\"deleted\"] : true,\n scope: typeof body[\"scope\"] === \"string\" ? body[\"scope\"] : scope,\n count: nonNegativeInteger(body[\"count\"]) ?? 0,\n totalBytes: nonNegativeInteger(body[\"totalBytes\"]) ?? 0,\n };\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 a blob path for this owner so a caller cannot induce this\n // wallet to sign arbitrary same-host paths.\n const route = parseBlobPath(parsed.pathname);\n if (!route || route.owner.toLowerCase() !== this.ownerAddress) {\n throw new StorageError(\n `URL path '${parsed.pathname}' must be ${this.blobPathPrefix}/${this.ownerAddress}/{scope}/{collectedAt}`,\n \"INVALID_URL\",\n \"vana-storage\",\n );\n }\n // A provider must only sign requests for its configured namespace. Chain\n // mode is strict: old legacy blobs should be migrated or re-collected\n // rather than read through a second ambiguous path.\n if (route.chainId !== this.chainId) {\n throw new StorageError(\n `URL chainId '${route.chainId ?? \"legacy\"}' does not match provider namespace '${this.namespaceDescription()}'`,\n \"INVALID_URL\",\n \"vana-storage\",\n );\n }\n return parsed.pathname;\n }\n\n private namespaceDescription(): string {\n if (this.network !== undefined) {\n return `${this.network} (${this.chainId})`;\n }\n return this.chainId === undefined ? \"legacy\" : String(this.chainId);\n }\n}\n\ninterface ParsedBlobRoute {\n chainId?: number;\n owner: string;\n scope: string;\n collectedAt: string;\n}\n\nfunction isValidChainId(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0;\n}\n\nfunction resolveStorageNamespace(\n config: Pick<VanaStorageConfig, \"network\" | \"chainId\">,\n): {\n chainId?: number;\n network?: ProtocolNetwork;\n} {\n if (config.network !== undefined && !isProtocolNetwork(config.network)) {\n throw new StorageError(\n `Unsupported vana-storage network '${String(config.network)}'`,\n \"INVALID_NETWORK\",\n \"vana-storage\",\n );\n }\n\n if (config.chainId !== undefined && !isValidChainId(config.chainId)) {\n throw new StorageError(\n `Unsupported vana-storage chainId '${String(config.chainId)}'`,\n \"INVALID_CHAIN_ID\",\n \"vana-storage\",\n );\n }\n\n if (config.network === undefined) {\n return { chainId: config.chainId };\n }\n\n const networkChainId = getProtocolNetworkChainId(config.network);\n if (config.chainId !== undefined && config.chainId !== networkChainId) {\n throw new StorageError(\n `vana-storage network '${config.network}' resolves to chainId '${networkChainId}', not '${config.chainId}'`,\n \"INVALID_CHAIN_ID\",\n \"vana-storage\",\n );\n }\n\n return { chainId: networkChainId, network: config.network };\n}\n\n/**\n * Parse a storage blob pathname into structured route info, or `null` when the\n * path is neither the legacy nor the chain-scoped blob shape. Traversal\n * segments (`.` / `..`) are rejected as invalid.\n */\nfunction parseBlobPath(pathname: string): ParsedBlobRoute | null {\n const segments = pathname.split(\"/\").filter((s) => s.length > 0);\n const isTraversal = (s: string): boolean => s === \".\" || s === \"..\";\n\n // Legacy: /v1/blobs/{owner}/{scope}/{collectedAt}\n if (\n segments.length === 5 &&\n segments[0] === \"v1\" &&\n segments[1] === \"blobs\"\n ) {\n const [, , owner, scope, collectedAt] = segments as [\n string,\n string,\n string,\n string,\n string,\n ];\n if (isTraversal(scope) || isTraversal(collectedAt)) return null;\n return { owner, scope, collectedAt };\n }\n\n // Chain-scoped: /v1/chains/{chainId}/blobs/{owner}/{scope}/{collectedAt}\n if (\n segments.length === 7 &&\n segments[0] === \"v1\" &&\n segments[1] === \"chains\" &&\n segments[3] === \"blobs\"\n ) {\n const [, , chainIdSegment, , owner, scope, collectedAt] = segments as [\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n ];\n if (!/^[0-9]+$/.test(chainIdSegment)) return null;\n const chainId = Number(chainIdSegment);\n if (!isValidChainId(chainId)) return null;\n if (isTraversal(scope) || isTraversal(collectedAt)) return null;\n return { chainId, owner, scope, collectedAt };\n }\n\n return null;\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 isTraversalSegment(segment: string): boolean {\n return segment === \".\" || segment === \"..\";\n}\n\nfunction nonNegativeInteger(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0\n ? value\n : undefined;\n}\n\nfunction describe(value: unknown): string {\n if (value instanceof Error) return value.message;\n return String(value);\n}\n\nfunction retryDelayMs(response: Response, responseText: string): number {\n const headerDelayMs = parseRetryAfterHeaderMs(\n response.headers.get(\"retry-after\"),\n );\n if (headerDelayMs !== null) {\n return clampRateLimitDelay(headerDelayMs);\n }\n\n return clampRateLimitDelay(parseRetryAfterBodyMs(responseText) ?? 0);\n}\n\nfunction parseRetryAfterHeaderMs(value: string | null): number | null {\n if (!value) return null;\n\n const seconds = Number(value);\n if (Number.isFinite(seconds)) {\n return seconds * 1000;\n }\n\n const dateMs = Date.parse(value);\n if (Number.isFinite(dateMs)) {\n return Math.max(0, dateMs - Date.now());\n }\n\n return null;\n}\n\nfunction parseRetryAfterBodyMs(responseText: string): number | null {\n if (!responseText) return null;\n\n try {\n const parsed = JSON.parse(responseText) as { retryAfter?: unknown };\n const seconds = Number(parsed.retryAfter);\n return Number.isFinite(seconds) ? seconds * 1000 : null;\n } catch {\n return null;\n }\n}\n\nfunction clampRateLimitDelay(delayMs: number): number {\n return Math.min(Math.max(0, delayMs), MAX_RATE_LIMIT_DELAY_MS);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\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":[]}
@@ -66,6 +66,19 @@ export interface VanaStorageConfig {
66
66
  */
67
67
  fetchImpl?: typeof fetch;
68
68
  }
69
+ /**
70
+ * Response of {@link VanaStorage.deleteScope} -- the worker's
71
+ * `DELETE /:owner/:scope` body. `count` is the number of blobs removed (0 is
72
+ * a success: nothing was stored under that scope).
73
+ *
74
+ * @category Storage
75
+ */
76
+ export interface VanaStorageScopeDeleteResult {
77
+ deleted: boolean;
78
+ scope: string;
79
+ count: number;
80
+ totalBytes: number;
81
+ }
69
82
  /**
70
83
  * Storage provider that talks to the vana-storage Worker
71
84
  * (`https://storage.vana.org` by default). All requests are authenticated
@@ -132,6 +145,19 @@ export declare class VanaStorage implements StorageProvider {
132
145
  */
133
146
  list(_options?: StorageListOptions): Promise<StorageFile[]>;
134
147
  delete(url: string): Promise<boolean>;
148
+ /**
149
+ * Delete every version's blob under `(owner, scope)` --
150
+ * `DELETE {prefix}/{owner}/{scope}` on vana-storage, signed with the same
151
+ * Web3Signed header as uploads (aud = endpoint origin, empty bodyHash).
152
+ * The worker accepts the owner's own signature or a personal server the
153
+ * owner registered with the gateway.
154
+ *
155
+ * @param ownerAddress - Must equal the provider's configured owner; a
156
+ * mismatch throws before anything is signed so this wallet can never be
157
+ * induced to sign a delete for another namespace.
158
+ * @param scope - The scope segment, e.g. `"instagram.profile"`.
159
+ */
160
+ deleteScope(ownerAddress: `0x${string}`, scope: string): Promise<VanaStorageScopeDeleteResult>;
135
161
  getConfig(): StorageProviderConfig;
136
162
  private signRequest;
137
163
  private pathFromUrl;
@@ -8,6 +8,7 @@ import {
8
8
  getProtocolNetworkChainId,
9
9
  isProtocolNetwork
10
10
  } from "../../protocol/networks.js";
11
+ import { readJsonObject } from "../../utils/response-body.js";
11
12
  const DEFAULT_ENDPOINT = "https://storage.vana.org";
12
13
  const LEGACY_BLOB_PATH_PREFIX = "/v1/blobs";
13
14
  const DEFAULT_TOKEN_TTL_SECONDS = 300;
@@ -178,6 +179,65 @@ class VanaStorage {
178
179
  }
179
180
  return true;
180
181
  }
182
+ /**
183
+ * Delete every version's blob under `(owner, scope)` --
184
+ * `DELETE {prefix}/{owner}/{scope}` on vana-storage, signed with the same
185
+ * Web3Signed header as uploads (aud = endpoint origin, empty bodyHash).
186
+ * The worker accepts the owner's own signature or a personal server the
187
+ * owner registered with the gateway.
188
+ *
189
+ * @param ownerAddress - Must equal the provider's configured owner; a
190
+ * mismatch throws before anything is signed so this wallet can never be
191
+ * induced to sign a delete for another namespace.
192
+ * @param scope - The scope segment, e.g. `"instagram.profile"`.
193
+ */
194
+ async deleteScope(ownerAddress, scope) {
195
+ if (ownerAddress.toLowerCase() !== this.ownerAddress) {
196
+ throw new StorageError(
197
+ `deleteScope owner '${ownerAddress}' does not match the configured owner '${this.ownerAddress}'`,
198
+ "INVALID_OWNER",
199
+ "vana-storage"
200
+ );
201
+ }
202
+ if (scope.length === 0 || scope.includes("/") || isTraversalSegment(scope)) {
203
+ throw new StorageError(
204
+ `scope must be a single non-empty path segment, got '${scope}'`,
205
+ "INVALID_SCOPE",
206
+ "vana-storage"
207
+ );
208
+ }
209
+ const path = `${this.blobPathPrefix}/${this.ownerAddress}/${encodeURIComponent(scope)}`;
210
+ const header = await this.signRequest("DELETE", path);
211
+ let response;
212
+ try {
213
+ response = await this.fetchImpl(`${this.endpoint}${path}`, {
214
+ method: "DELETE",
215
+ headers: { authorization: header }
216
+ });
217
+ } catch (cause) {
218
+ throw new StorageError(
219
+ `vana-storage scope delete network error: ${describe(cause)}`,
220
+ "DELETE_ERROR",
221
+ "vana-storage",
222
+ { cause: cause instanceof Error ? cause : void 0 }
223
+ );
224
+ }
225
+ if (!response.ok) {
226
+ const responseText = await safeText(response);
227
+ throw new StorageError(
228
+ `vana-storage scope delete failed: ${response.status} ${response.statusText} - ${responseText}`,
229
+ "DELETE_FAILED",
230
+ "vana-storage"
231
+ );
232
+ }
233
+ const body = await readJsonObject(response);
234
+ return {
235
+ deleted: typeof body["deleted"] === "boolean" ? body["deleted"] : true,
236
+ scope: typeof body["scope"] === "string" ? body["scope"] : scope,
237
+ count: nonNegativeInteger(body["count"]) ?? 0,
238
+ totalBytes: nonNegativeInteger(body["totalBytes"]) ?? 0
239
+ };
240
+ }
181
241
  getConfig() {
182
242
  return {
183
243
  name: "vana-storage",
@@ -306,6 +366,12 @@ function encodeRelativePath(filename) {
306
366
  }
307
367
  return parts.map((p) => encodeURIComponent(p)).join("/");
308
368
  }
369
+ function isTraversalSegment(segment) {
370
+ return segment === "." || segment === "..";
371
+ }
372
+ function nonNegativeInteger(value) {
373
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
374
+ }
309
375
  function describe(value) {
310
376
  if (value instanceof Error) return value.message;
311
377
  return String(value);