@opendatalabs/personal-server-ts-core 0.0.1-canary.01ca694 → 0.0.1-canary.052d47e

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 (322) hide show
  1. package/dist/api/index.d.ts +282 -9
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +867 -163
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/auth/request.d.ts.map +1 -1
  6. package/dist/auth/request.js.map +1 -1
  7. package/dist/client.d.ts +8 -1
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +21 -3
  10. package/dist/client.js.map +1 -1
  11. package/dist/contracts/binary.d.ts +12 -0
  12. package/dist/contracts/binary.d.ts.map +1 -1
  13. package/dist/contracts/binary.js +31 -0
  14. package/dist/contracts/binary.js.map +1 -1
  15. package/dist/contracts/data.d.ts +51 -5
  16. package/dist/contracts/data.d.ts.map +1 -1
  17. package/dist/contracts/data.js +166 -30
  18. package/dist/contracts/data.js.map +1 -1
  19. package/dist/contracts/grants.d.ts +7 -5
  20. package/dist/contracts/grants.d.ts.map +1 -1
  21. package/dist/contracts/grants.js +60 -37
  22. package/dist/contracts/grants.js.map +1 -1
  23. package/dist/contracts/index.d.ts +1 -1
  24. package/dist/contracts/index.d.ts.map +1 -1
  25. package/dist/contracts/index.js +1 -1
  26. package/dist/contracts/index.js.map +1 -1
  27. package/dist/derivatives/api.d.ts +48 -0
  28. package/dist/derivatives/api.d.ts.map +1 -0
  29. package/dist/derivatives/api.js +366 -0
  30. package/dist/derivatives/api.js.map +1 -0
  31. package/dist/derivatives/compute.d.ts +106 -0
  32. package/dist/derivatives/compute.d.ts.map +1 -0
  33. package/dist/derivatives/compute.js +414 -0
  34. package/dist/derivatives/compute.js.map +1 -0
  35. package/dist/derivatives/e2ee/aad.d.ts +22 -0
  36. package/dist/derivatives/e2ee/aad.d.ts.map +1 -0
  37. package/dist/derivatives/e2ee/aad.js +32 -0
  38. package/dist/derivatives/e2ee/aad.js.map +1 -0
  39. package/dist/derivatives/e2ee/attestation.d.ts +134 -0
  40. package/dist/derivatives/e2ee/attestation.d.ts.map +1 -0
  41. package/dist/derivatives/e2ee/attestation.js +248 -0
  42. package/dist/derivatives/e2ee/attestation.js.map +1 -0
  43. package/dist/derivatives/e2ee/index.d.ts +6 -0
  44. package/dist/derivatives/e2ee/index.d.ts.map +1 -0
  45. package/dist/derivatives/e2ee/index.js +6 -0
  46. package/dist/derivatives/e2ee/index.js.map +1 -0
  47. package/dist/derivatives/e2ee/jcs.d.ts +17 -0
  48. package/dist/derivatives/e2ee/jcs.d.ts.map +1 -0
  49. package/dist/derivatives/e2ee/jcs.js +55 -0
  50. package/dist/derivatives/e2ee/jcs.js.map +1 -0
  51. package/dist/derivatives/e2ee/phala.d.ts +62 -0
  52. package/dist/derivatives/e2ee/phala.d.ts.map +1 -0
  53. package/dist/derivatives/e2ee/phala.js +168 -0
  54. package/dist/derivatives/e2ee/phala.js.map +1 -0
  55. package/dist/derivatives/e2ee/suite.d.ts +56 -0
  56. package/dist/derivatives/e2ee/suite.d.ts.map +1 -0
  57. package/dist/derivatives/e2ee/suite.js +164 -0
  58. package/dist/derivatives/e2ee/suite.js.map +1 -0
  59. package/dist/derivatives/index.d.ts +10 -0
  60. package/dist/derivatives/index.d.ts.map +1 -0
  61. package/dist/derivatives/index.js +10 -0
  62. package/dist/derivatives/index.js.map +1 -0
  63. package/dist/derivatives/inference.d.ts +151 -0
  64. package/dist/derivatives/inference.d.ts.map +1 -0
  65. package/dist/derivatives/inference.js +262 -0
  66. package/dist/derivatives/inference.js.map +1 -0
  67. package/dist/derivatives/prompt.d.ts +64 -0
  68. package/dist/derivatives/prompt.d.ts.map +1 -0
  69. package/dist/derivatives/prompt.js +193 -0
  70. package/dist/derivatives/prompt.js.map +1 -0
  71. package/dist/derivatives/registration.d.ts +54 -0
  72. package/dist/derivatives/registration.d.ts.map +1 -0
  73. package/dist/derivatives/registration.js +180 -0
  74. package/dist/derivatives/registration.js.map +1 -0
  75. package/dist/derivatives/scheduler.d.ts +84 -0
  76. package/dist/derivatives/scheduler.d.ts.map +1 -0
  77. package/dist/derivatives/scheduler.js +257 -0
  78. package/dist/derivatives/scheduler.js.map +1 -0
  79. package/dist/derivatives/store.d.ts +14 -0
  80. package/dist/derivatives/store.d.ts.map +1 -0
  81. package/dist/derivatives/store.js +77 -0
  82. package/dist/derivatives/store.js.map +1 -0
  83. package/dist/derivatives/types.d.ts +92 -0
  84. package/dist/derivatives/types.d.ts.map +1 -0
  85. package/dist/derivatives/types.js +27 -0
  86. package/dist/derivatives/types.js.map +1 -0
  87. package/dist/errors/catalog.d.ts +75 -0
  88. package/dist/errors/catalog.d.ts.map +1 -1
  89. package/dist/errors/catalog.js +104 -0
  90. package/dist/errors/catalog.js.map +1 -1
  91. package/dist/grants/index.d.ts +2 -1
  92. package/dist/grants/index.d.ts.map +1 -1
  93. package/dist/grants/index.js +1 -1
  94. package/dist/grants/index.js.map +1 -1
  95. package/dist/keys/index.d.ts +1 -0
  96. package/dist/keys/index.d.ts.map +1 -1
  97. package/dist/keys/index.js +1 -1
  98. package/dist/keys/index.js.map +1 -1
  99. package/dist/keys/public-only-account.d.ts +8 -0
  100. package/dist/keys/public-only-account.d.ts.map +1 -0
  101. package/dist/keys/public-only-account.js +21 -0
  102. package/dist/keys/public-only-account.js.map +1 -0
  103. package/dist/keys/server-account.d.ts +2 -2
  104. package/dist/keys/server-account.d.ts.map +1 -1
  105. package/dist/lineage/attestation.d.ts +42 -0
  106. package/dist/lineage/attestation.d.ts.map +1 -0
  107. package/dist/lineage/attestation.js +25 -0
  108. package/dist/lineage/attestation.js.map +1 -0
  109. package/dist/lineage/gateway.d.ts +105 -0
  110. package/dist/lineage/gateway.d.ts.map +1 -0
  111. package/dist/lineage/gateway.js +242 -0
  112. package/dist/lineage/gateway.js.map +1 -0
  113. package/dist/lineage/index.d.ts +5 -0
  114. package/dist/lineage/index.d.ts.map +1 -0
  115. package/dist/lineage/index.js +5 -0
  116. package/dist/lineage/index.js.map +1 -0
  117. package/dist/lineage/lineage.d.ts +141 -0
  118. package/dist/lineage/lineage.d.ts.map +1 -0
  119. package/dist/lineage/lineage.js +255 -0
  120. package/dist/lineage/lineage.js.map +1 -0
  121. package/dist/logging/access-log.d.ts +10 -1
  122. package/dist/logging/access-log.d.ts.map +1 -1
  123. package/dist/mcp/activity.d.ts.map +1 -1
  124. package/dist/mcp/activity.js.map +1 -1
  125. package/dist/mcp/connection-api.d.ts.map +1 -1
  126. package/dist/mcp/connection-api.js +0 -1
  127. package/dist/mcp/connection-api.js.map +1 -1
  128. package/dist/mcp/index.d.ts +1 -0
  129. package/dist/mcp/index.d.ts.map +1 -1
  130. package/dist/mcp/index.js +1 -0
  131. package/dist/mcp/index.js.map +1 -1
  132. package/dist/mcp/read-client.d.ts +61 -1
  133. package/dist/mcp/read-client.d.ts.map +1 -1
  134. package/dist/mcp/read-client.js +397 -42
  135. package/dist/mcp/read-client.js.map +1 -1
  136. package/dist/mcp/resources.d.ts +37 -0
  137. package/dist/mcp/resources.d.ts.map +1 -0
  138. package/dist/mcp/resources.js +114 -0
  139. package/dist/mcp/resources.js.map +1 -0
  140. package/dist/mcp/search/index.d.ts.map +1 -1
  141. package/dist/mcp/search/index.js +108 -21
  142. package/dist/mcp/search/index.js.map +1 -1
  143. package/dist/mcp/server.d.ts.map +1 -1
  144. package/dist/mcp/server.js +27 -9
  145. package/dist/mcp/server.js.map +1 -1
  146. package/dist/mcp/session.d.ts +160 -0
  147. package/dist/mcp/session.d.ts.map +1 -0
  148. package/dist/mcp/session.js +243 -0
  149. package/dist/mcp/session.js.map +1 -0
  150. package/dist/mcp/tools.d.ts +29 -3
  151. package/dist/mcp/tools.d.ts.map +1 -1
  152. package/dist/mcp/tools.js +515 -48
  153. package/dist/mcp/tools.js.map +1 -1
  154. package/dist/mcp/types.d.ts.map +1 -1
  155. package/dist/payment/index.d.ts +2 -0
  156. package/dist/payment/index.d.ts.map +1 -0
  157. package/dist/payment/index.js +2 -0
  158. package/dist/payment/index.js.map +1 -0
  159. package/dist/payment/x402.d.ts +155 -0
  160. package/dist/payment/x402.d.ts.map +1 -0
  161. package/dist/payment/x402.js +402 -0
  162. package/dist/payment/x402.js.map +1 -0
  163. package/dist/policy/data-read.d.ts +18 -2
  164. package/dist/policy/data-read.d.ts.map +1 -1
  165. package/dist/policy/data-read.js +80 -49
  166. package/dist/policy/data-read.js.map +1 -1
  167. package/dist/policy/data-write.d.ts +69 -0
  168. package/dist/policy/data-write.d.ts.map +1 -0
  169. package/dist/policy/data-write.js +131 -0
  170. package/dist/policy/data-write.js.map +1 -0
  171. package/dist/policy/index.d.ts +3 -1
  172. package/dist/policy/index.d.ts.map +1 -1
  173. package/dist/policy/index.js +3 -1
  174. package/dist/policy/index.js.map +1 -1
  175. package/dist/policy/signed-artifacts.d.ts +17 -0
  176. package/dist/policy/signed-artifacts.d.ts.map +1 -0
  177. package/dist/policy/signed-artifacts.js +110 -0
  178. package/dist/policy/signed-artifacts.js.map +1 -0
  179. package/dist/ports/index.d.ts +159 -32
  180. package/dist/ports/index.d.ts.map +1 -1
  181. package/dist/ports/index.js +6 -5
  182. package/dist/ports/index.js.map +1 -1
  183. package/dist/schemas/server-config.d.ts +49 -4
  184. package/dist/schemas/server-config.d.ts.map +1 -1
  185. package/dist/schemas/server-config.js +95 -13
  186. package/dist/schemas/server-config.js.map +1 -1
  187. package/dist/signing/index.d.ts +2 -2
  188. package/dist/signing/index.d.ts.map +1 -1
  189. package/dist/signing/index.js +1 -1
  190. package/dist/signing/index.js.map +1 -1
  191. package/dist/signing/request-signer.d.ts +2 -0
  192. package/dist/signing/request-signer.d.ts.map +1 -1
  193. package/dist/signing/request-signer.js +1 -0
  194. package/dist/signing/request-signer.js.map +1 -1
  195. package/dist/signing/signer.d.ts +27 -25
  196. package/dist/signing/signer.d.ts.map +1 -1
  197. package/dist/signing/signer.js +27 -51
  198. package/dist/signing/signer.js.map +1 -1
  199. package/dist/storage/adapters/index.d.ts +1 -1
  200. package/dist/storage/adapters/index.d.ts.map +1 -1
  201. package/dist/storage/adapters/index.js +1 -1
  202. package/dist/storage/adapters/index.js.map +1 -1
  203. package/dist/storage/adapters/interface.d.ts +10 -0
  204. package/dist/storage/adapters/interface.d.ts.map +1 -1
  205. package/dist/storage/adapters/sdk.d.ts +11 -1
  206. package/dist/storage/adapters/sdk.d.ts.map +1 -1
  207. package/dist/storage/adapters/sdk.js +38 -3
  208. package/dist/storage/adapters/sdk.js.map +1 -1
  209. package/dist/storage/adapters/vana.d.ts +3 -0
  210. package/dist/storage/adapters/vana.d.ts.map +1 -1
  211. package/dist/storage/adapters/vana.js +95 -2
  212. package/dist/storage/adapters/vana.js.map +1 -1
  213. package/dist/storage/blocks/errors.d.ts.map +1 -1
  214. package/dist/storage/blocks/errors.js.map +1 -1
  215. package/dist/storage/blocks/index.d.ts +2 -0
  216. package/dist/storage/blocks/index.d.ts.map +1 -1
  217. package/dist/storage/blocks/index.js +2 -0
  218. package/dist/storage/blocks/index.js.map +1 -1
  219. package/dist/storage/blocks/select.d.ts +29 -0
  220. package/dist/storage/blocks/select.d.ts.map +1 -0
  221. package/dist/storage/blocks/select.js +120 -0
  222. package/dist/storage/blocks/select.js.map +1 -0
  223. package/dist/storage/blocks/types.d.ts +7 -0
  224. package/dist/storage/blocks/types.d.ts.map +1 -1
  225. package/dist/storage/index/manager.d.ts +30 -2
  226. package/dist/storage/index/manager.d.ts.map +1 -1
  227. package/dist/storage/index/schema.d.ts +1 -1
  228. package/dist/storage/index/schema.js +1 -1
  229. package/dist/storage/index/types.d.ts +6 -1
  230. package/dist/storage/index/types.d.ts.map +1 -1
  231. package/dist/sync/data-point-feed.d.ts +35 -0
  232. package/dist/sync/data-point-feed.d.ts.map +1 -0
  233. package/dist/sync/data-point-feed.js +122 -0
  234. package/dist/sync/data-point-feed.js.map +1 -0
  235. package/dist/sync/data-point-id.d.ts +7 -0
  236. package/dist/sync/data-point-id.d.ts.map +1 -0
  237. package/dist/sync/data-point-id.js +16 -0
  238. package/dist/sync/data-point-id.js.map +1 -0
  239. package/dist/sync/delete-data-port.d.ts +50 -0
  240. package/dist/sync/delete-data-port.d.ts.map +1 -0
  241. package/dist/sync/delete-data-port.js +244 -0
  242. package/dist/sync/delete-data-port.js.map +1 -0
  243. package/dist/sync/engine/sync-manager.d.ts +16 -4
  244. package/dist/sync/engine/sync-manager.d.ts.map +1 -1
  245. package/dist/sync/engine/sync-manager.js +93 -32
  246. package/dist/sync/engine/sync-manager.js.map +1 -1
  247. package/dist/sync/index.d.ts +7 -0
  248. package/dist/sync/index.d.ts.map +1 -1
  249. package/dist/sync/index.js +7 -0
  250. package/dist/sync/index.js.map +1 -1
  251. package/dist/sync/issues.d.ts.map +1 -1
  252. package/dist/sync/issues.js +21 -3
  253. package/dist/sync/issues.js.map +1 -1
  254. package/dist/sync/pending-blob-deletions.d.ts +29 -0
  255. package/dist/sync/pending-blob-deletions.d.ts.map +1 -0
  256. package/dist/sync/pending-blob-deletions.js +116 -0
  257. package/dist/sync/pending-blob-deletions.js.map +1 -0
  258. package/dist/sync/retry-memory.d.ts +33 -0
  259. package/dist/sync/retry-memory.d.ts.map +1 -0
  260. package/dist/sync/retry-memory.js +68 -0
  261. package/dist/sync/retry-memory.js.map +1 -0
  262. package/dist/sync/scope-deletions.d.ts +155 -0
  263. package/dist/sync/scope-deletions.d.ts.map +1 -0
  264. package/dist/sync/scope-deletions.js +225 -0
  265. package/dist/sync/scope-deletions.js.map +1 -0
  266. package/dist/sync/tombstone.d.ts +27 -0
  267. package/dist/sync/tombstone.d.ts.map +1 -0
  268. package/dist/sync/tombstone.js +28 -0
  269. package/dist/sync/tombstone.js.map +1 -0
  270. package/dist/sync/workers/delete.d.ts +152 -31
  271. package/dist/sync/workers/delete.d.ts.map +1 -1
  272. package/dist/sync/workers/delete.js +457 -55
  273. package/dist/sync/workers/delete.js.map +1 -1
  274. package/dist/sync/workers/download.d.ts +108 -27
  275. package/dist/sync/workers/download.d.ts.map +1 -1
  276. package/dist/sync/workers/download.js +321 -126
  277. package/dist/sync/workers/download.js.map +1 -1
  278. package/dist/sync/workers/upload.d.ts +65 -10
  279. package/dist/sync/workers/upload.d.ts.map +1 -1
  280. package/dist/sync/workers/upload.js +371 -42
  281. package/dist/sync/workers/upload.js.map +1 -1
  282. package/dist/test-utils/e2ee-fixtures.d.ts +94 -0
  283. package/dist/test-utils/e2ee-fixtures.d.ts.map +1 -0
  284. package/dist/test-utils/e2ee-fixtures.js +116 -0
  285. package/dist/test-utils/e2ee-fixtures.js.map +1 -0
  286. package/dist/test-utils/e2ee-gateway.d.ts +61 -0
  287. package/dist/test-utils/e2ee-gateway.d.ts.map +1 -0
  288. package/dist/test-utils/e2ee-gateway.js +248 -0
  289. package/dist/test-utils/e2ee-gateway.js.map +1 -0
  290. package/dist/test-utils/index.d.ts +3 -0
  291. package/dist/test-utils/index.d.ts.map +1 -1
  292. package/dist/test-utils/index.js +3 -0
  293. package/dist/test-utils/index.js.map +1 -1
  294. package/dist/test-utils/memory-storage.d.ts +12 -0
  295. package/dist/test-utils/memory-storage.d.ts.map +1 -0
  296. package/dist/test-utils/memory-storage.js +146 -0
  297. package/dist/test-utils/memory-storage.js.map +1 -0
  298. package/dist/test-utils/wallet.d.ts +6 -0
  299. package/dist/test-utils/wallet.d.ts.map +1 -1
  300. package/dist/test-utils/wallet.js +46 -0
  301. package/dist/test-utils/wallet.js.map +1 -1
  302. package/dist/write/attribution.d.ts +185 -0
  303. package/dist/write/attribution.d.ts.map +1 -0
  304. package/dist/write/attribution.js +450 -0
  305. package/dist/write/attribution.js.map +1 -0
  306. package/dist/write/authorize.d.ts +77 -0
  307. package/dist/write/authorize.d.ts.map +1 -0
  308. package/dist/write/authorize.js +109 -0
  309. package/dist/write/authorize.js.map +1 -0
  310. package/dist/write/index.d.ts +5 -0
  311. package/dist/write/index.d.ts.map +1 -0
  312. package/dist/write/index.js +5 -0
  313. package/dist/write/index.js.map +1 -0
  314. package/dist/write/route.d.ts +61 -0
  315. package/dist/write/route.d.ts.map +1 -0
  316. package/dist/write/route.js +132 -0
  317. package/dist/write/route.js.map +1 -0
  318. package/dist/write/session.d.ts +96 -0
  319. package/dist/write/session.d.ts.map +1 -0
  320. package/dist/write/session.js +136 -0
  321. package/dist/write/session.js.map +1 -0
  322. package/package.json +19 -3
package/dist/api/index.js CHANGED
@@ -1,10 +1,34 @@
1
- import { ProtocolError } from "../errors/catalog.js";
2
- import { deleteDataScopeContract, ingestDataContract, ingestBinaryDataContract, listAccessLogsContract, listDataScopesContract, listDataVersionsContract, parseDataScopeContract, parseJsonObjectBody, readDataContract, syncFileContract, triggerSyncContract, getSyncStatusContract, configReadErrorContract, configWriteErrorContract, createGrantContract, listGrantsContract, oauthTokenContract, revokeGrantContract, validateServerConfigContract, verifyGrantContract, decodeBinaryEnvelope, isBinaryEnvelope, parseMetadataHeader, stringifyMetadataHeader, } from "../contracts/index.js";
1
+ import { DataDeletedError, DeleteTombstoneFailedError, GrantRequiredError, InvalidCascadeError, LineageCascadeUnavailableError, LineageGatewayError, LineageUnavailableError, ProtocolError, ServerNotConfiguredError, } from "../errors/catalog.js";
2
+ import { computeDataPointId } from "../sync/data-point-id.js";
3
+ import { isEntryCoveredByTombstone, } from "../sync/scope-deletions.js";
4
+ import { deleteScope as deleteScopeLocally, } from "../sync/workers/delete.js";
5
+ import { ingestDataContract, ingestBinaryDataContract, listAccessLogsContract, listDataScopesContract, listDataVersionsContract, parseDataScopeContract, parseJsonObjectBody, readDataContract, syncFileContract, triggerSyncContract, getSyncStatusContract, configReadErrorContract, configWriteErrorContract, createGrantContract, listGrantsContract, oauthTokenContract, revokeGrantContract, validateServerConfigContract, verifyGrantContract, decodeBinaryEnvelope, isBinaryEnvelope, parseMetadataHeader, stringifyMetadataHeader, IngestPersistedError, } from "../contracts/index.js";
6
+ import { WRITER_ATTRIBUTION_KEY, } from "../write/attribution.js";
7
+ import { extractLineageField, LINEAGE_KEY, prepareLineage, } from "../lineage/lineage.js";
8
+ import { binaryFilename, binaryMimeType, isJsonContentType, } from "../contracts/binary.js";
9
+ import { buildChallenge, parsePaymentHeader, verifyPayment, } from "../payment/index.js";
3
10
  function jsonResponse(body, init) {
4
11
  const headers = new Headers(init?.headers);
5
12
  headers.set("Content-Type", "application/json");
6
13
  return new Response(JSON.stringify(body), { ...init, headers });
7
14
  }
15
+ // X402 spec convention: payment responses are JSON, base64-encoded into a
16
+ // single header value. Returns undefined on encode failure so callers can
17
+ // just omit the header rather than fail the read. Uses btoa + TextEncoder
18
+ // to stay portable across Node and web runtimes.
19
+ function encodePaymentResponseHeader(body) {
20
+ try {
21
+ const json = JSON.stringify(body ?? null);
22
+ const bytes = new TextEncoder().encode(json);
23
+ let binary = "";
24
+ for (const b of bytes)
25
+ binary += String.fromCharCode(b);
26
+ return btoa(binary);
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
8
32
  function contractResponse(result) {
9
33
  return jsonResponse(result.body, { status: result.status });
10
34
  }
@@ -59,97 +83,277 @@ function stripBasePath(pathname, basePath) {
59
83
  function decodePathPart(value) {
60
84
  return decodeURIComponent(value ?? "");
61
85
  }
62
- function selectedGrantId(request, url) {
86
+ /**
87
+ * The grant the caller nominates for a read: `?grantId=` or the
88
+ * `x-ps-grant-id` header, the SDK's shape on every read route. Exported so
89
+ * the derivatives status route authorizes with exactly the same inputs as
90
+ * a data read — a credential that reads /v1/data/<scope> must not be
91
+ * refused by /v1/derivatives/status.
92
+ */
93
+ export function selectedGrantId(request, url) {
63
94
  return (url.searchParams.get("grantId") ??
64
95
  request.headers.get("x-ps-grant-id") ??
65
96
  undefined);
66
97
  }
67
- function collectedAt(now) {
68
- return now()
69
- .toISOString()
70
- .replace(/\.\d{3}Z$/, "Z");
71
- }
72
- async function resolveSchema(deps, scope) {
73
- if (!deps.schemaResolver) {
98
+ /**
99
+ * Run the X402 dispatch for one read:
100
+ *
101
+ * X-PAYMENT absent / malformed / fails local validation
102
+ * → challenge: fresh 402 with current fee + accessRecord
103
+ * X-PAYMENT valid, forward to gateway succeeds
104
+ * → ok: caller proceeds to read the data
105
+ * X-PAYMENT valid, gateway returns 4xx/5xx
106
+ * → gateway-error: relay the gateway's body verbatim so the builder
107
+ * can dispatch (insufficient balance vs. replay vs. race etc.)
108
+ *
109
+ * The gateway POST goes through `fetch` directly (not `gateway.payForOperation`)
110
+ * because the SDK client throws plain Errors and loses the body — which is
111
+ * exactly what we need to distinguish gateway 402 (insufficient balance) from
112
+ * 409 (replay) from 400 (amount mismatch).
113
+ */
114
+ export async function handleX402Cycle(input) {
115
+ const { deps, gateway, gatewayConfig, gatewayUrl, builder, scope } = input;
116
+ // The gateway lowercases opId before EIP-712 recovery — we must do the
117
+ // same in the challenge so the builder signs over the canonical form.
118
+ const opIdLower = input.grantId.toLowerCase();
119
+ // Live grant — re-fetched every cycle. The fee.totalDue is a snapshot
120
+ // (SDK comment: "clients shouldn't cache"); paymentStatus may have just
121
+ // flipped if a concurrent payer paid the registration fee.
122
+ const grant = await gateway.getGrant(opIdLower);
123
+ if (!grant) {
74
124
  return {
75
- ok: false,
76
- response: errorResponse(500, "SERVER_NOT_CONFIGURED", "Gateway is not configured"),
125
+ kind: "challenge",
126
+ body: {
127
+ x402Version: 1,
128
+ error: "PAYMENT_REQUIRED",
129
+ accepts: [],
130
+ },
77
131
  };
78
132
  }
79
- try {
80
- const schema = await deps.schemaResolver.getSchemaForScope(scope);
81
- if (!schema) {
82
- return {
83
- ok: false,
84
- response: jsonResponse({
85
- error: "NO_SCHEMA",
86
- message: `No schema registered for scope: ${scope}`,
87
- }, { status: 400 }),
88
- };
133
+ // Bind the accessRecord to the entry being served, if it exists and has
134
+ // been registered with DPv2 yet.
135
+ const entryRow = deps.storage.findEntry({
136
+ scope,
137
+ fileId: input.fileIdParam,
138
+ at: input.atParam,
139
+ });
140
+ const entryForChallenge = entryRow
141
+ ? {
142
+ dataPointId: entryRow.dataPointId,
143
+ scope,
144
+ version: entryRow.version,
89
145
  }
90
- return {
91
- ok: true,
92
- schemaUrl: schema.definitionUrl,
93
- schemaId: schema.id,
94
- };
146
+ : undefined;
147
+ // `grant` is narrowed to non-null above, but that narrowing is not carried
148
+ // into this closure (vana-sdk 3.14.0 made getGrant return `... | null`), so
149
+ // capture the guaranteed-present value via a typed alias for the closure.
150
+ const liveGrant = grant;
151
+ async function buildFreshChallenge() {
152
+ return buildChallenge({
153
+ builder,
154
+ grantId: opIdLower,
155
+ grant: liveGrant,
156
+ network: deps.network ?? `vana:${gatewayConfig.chainId}`,
157
+ gatewayConfig,
158
+ serverSigner: deps.serverSigner,
159
+ serverOwner: deps.serverOwner,
160
+ entry: entryForChallenge,
161
+ generateNonce: deps.generatePaymentNonce,
162
+ generateRecordIdFn: deps.generateRecordId,
163
+ now: deps.now,
164
+ });
165
+ }
166
+ const headerValue = input.request.headers.get("x-payment");
167
+ const parsed = parsePaymentHeader(headerValue);
168
+ if (!parsed) {
169
+ return { kind: "challenge", body: await buildFreshChallenge() };
170
+ }
171
+ if (!deps.serverAddress) {
172
+ // Can't validate accessRecord recovery without our own address; fail
173
+ // closed by reissuing a challenge.
174
+ deps.logger?.error?.({ scope, grantId: opIdLower }, "X402 enabled but serverAddress is not configured — cannot verify accessRecord");
175
+ return { kind: "challenge", body: await buildFreshChallenge() };
176
+ }
177
+ const verify = await verifyPayment({
178
+ builder,
179
+ grantId: opIdLower,
180
+ grant,
181
+ entry: entryForChallenge,
182
+ serverAddress: deps.serverAddress,
183
+ gatewayConfig,
184
+ serverOwner: deps.serverOwner,
185
+ payment: parsed,
186
+ });
187
+ if (!verify.ok) {
188
+ deps.logger?.info?.({ scope, grantId: opIdLower, reason: verify.reason }, "X402 payment verification failed; reissuing challenge");
189
+ return { kind: "challenge", body: await buildFreshChallenge() };
190
+ }
191
+ // Forward to gateway via direct fetch (preserves error body). The SDK
192
+ // client wraps the same endpoint but throws away the structured response
193
+ // on non-2xx, which is exactly the info we need to distinguish gateway
194
+ // 402 (insufficient balance) from 409 (replay / race) from 400 (mismatch).
195
+ const doFetch = deps.paymentFetch ?? fetch;
196
+ const body = {
197
+ payerAddress: verify.payment.payload.message.payerAddress,
198
+ opType: verify.payment.payload.message.opType,
199
+ opId: verify.payment.payload.message.opId,
200
+ asset: verify.payment.payload.message.asset,
201
+ amount: verify.payment.payload.message.amount,
202
+ paymentNonce: verify.payment.payload.message.paymentNonce,
203
+ };
204
+ if (verify.payment.payload.accessRecord) {
205
+ body["accessRecord"] = verify.payment.payload.accessRecord;
206
+ }
207
+ let gatewayRes;
208
+ try {
209
+ gatewayRes = await doFetch(`${gatewayUrl.replace(/\/+$/, "")}/v1/escrow/pay`, {
210
+ method: "POST",
211
+ headers: {
212
+ "Content-Type": "application/json",
213
+ Authorization: `Web3Signed ${verify.payment.payload.signature}`,
214
+ },
215
+ body: JSON.stringify(body),
216
+ });
95
217
  }
96
218
  catch (err) {
97
- deps.logger?.error?.({ err, scope }, "Gateway schema lookup failed");
98
- return {
99
- ok: false,
100
- response: jsonResponse({
101
- error: "GATEWAY_ERROR",
102
- message: "Failed to look up schema for scope",
103
- }, { status: 502 }),
104
- };
219
+ deps.logger?.error?.({
220
+ scope,
221
+ grantId: opIdLower,
222
+ error: err.message,
223
+ }, "X402 gateway forward threw; reissuing challenge");
224
+ return { kind: "challenge", body: await buildFreshChallenge() };
225
+ }
226
+ if (gatewayRes.ok) {
227
+ // Preserve the gateway's structured success body so the request
228
+ // handler can echo it back via the X-PAYMENT-RESPONSE header (canonical
229
+ // X402 convention). Empty/malformed body → propagate as null; the
230
+ // header still gets set so builders can tell payment succeeded.
231
+ let payResponseBody = null;
232
+ try {
233
+ payResponseBody = await gatewayRes.json();
234
+ }
235
+ catch {
236
+ // Body unreadable but status was 2xx — treat as success with no payload.
237
+ }
238
+ return { kind: "ok", payResponse: payResponseBody };
105
239
  }
240
+ // Read the gateway's structured error body if there is one.
241
+ let errorBody;
242
+ try {
243
+ errorBody = await gatewayRes.json();
244
+ }
245
+ catch {
246
+ errorBody = { error: gatewayRes.statusText };
247
+ }
248
+ // 409 (replay or race) and 400 (amount mismatch / shape error) are
249
+ // recoverable with a fresh challenge — the builder re-signs with the
250
+ // new state and retries. 402 (insufficient balance) is relayed verbatim
251
+ // because the builder needs to fix their escrow, not their signature.
252
+ // 5xx is treated like 402-relayed since the personal server can't
253
+ // distinguish "transient" from "permanent" without more info.
254
+ if (gatewayRes.status === 409 || gatewayRes.status === 400) {
255
+ deps.logger?.info?.({ scope, grantId: opIdLower, status: gatewayRes.status, errorBody }, "Gateway rejected X-PAYMENT; reissuing X402 challenge");
256
+ return { kind: "challenge", body: await buildFreshChallenge() };
257
+ }
258
+ return { kind: "gateway-error", status: gatewayRes.status, body: errorBody };
106
259
  }
107
- /** True when the request body should be treated as a JSON object (the legacy
108
- * path). Missing/blank Content-Type is treated as JSON for backward compat. */
109
- function isJsonContentType(request) {
110
- const ct = request.headers.get("content-type");
111
- if (!ct)
112
- return true;
113
- return ct.toLowerCase().includes("application/json");
114
- }
115
- function binaryMimeType(request) {
116
- const ct = request.headers.get("content-type");
117
- if (!ct)
118
- return "application/octet-stream";
119
- // Strip any "; charset=..." / boundary parameters.
120
- return ct.split(";")[0].trim() || "application/octet-stream";
121
- }
122
- /** Extract a filename from X-Filename or a Content-Disposition header. */
123
- function binaryFilename(request) {
124
- const explicit = request.headers.get("x-filename");
125
- if (explicit)
126
- return explicit;
127
- const disposition = request.headers.get("content-disposition");
128
- const match = disposition?.match(/filename\*?=(?:UTF-8'')?"?([^";]+)"?/i);
129
- return match ? decodeURIComponent(match[1]) : undefined;
260
+ function collectedAt(now) {
261
+ return now()
262
+ .toISOString()
263
+ .replace(/\.\d{3}Z$/, "Z");
130
264
  }
131
265
  /**
132
- * Resolve a schema for a binary scope, registering a permissive "no-schema"
133
- * schema when none exists. Unlike the JSON path this never hard-fails on a
134
- * missing schema: if there is no registrar, ingestion proceeds with no schemaId
135
- * (the entry is registered against the gateway's resolved schema later).
266
+ * The deletion that applies to a read of `scope`, or null when the read may
267
+ * be served. `entry` is the local version the read would serve (undefined
268
+ * on a local miss).
269
+ *
270
+ * - A local copy is refused when the gateway holds a tombstone that covers
271
+ * it (see `isEntryCoveredByTombstone`: registry versions and the ingest
272
+ * marker, never clocks). A local re-add on top of the tombstone is served.
273
+ * - A local miss asks the tracker to consult the gateway (bounded by its
274
+ * per-scope verdict cache and failure back-off): the local index cannot
275
+ * tell "deleted long ago" from "never had it".
276
+ * Gateway unreachability never fails the read: the tracker answers from its
277
+ * last known state, which is the documented offline behaviour.
136
278
  */
137
- async function resolveBinarySchema(deps, scope) {
138
- if (deps.schemaResolver) {
139
- const schema = await deps.schemaResolver.getSchemaForScope(scope);
140
- if (schema) {
141
- return { schemaId: schema.id, schemaUrl: schema.definitionUrl };
142
- }
279
+ export async function resolveReadDeletion(deps, scope, entry) {
280
+ if (!deps.scopeDeletions)
281
+ return null;
282
+ const verdict = await deps.scopeDeletions.resolve(scope, {
283
+ consultGateway: entry ? "if-stale" : "always",
284
+ });
285
+ if (!verdict.deleted)
286
+ return null;
287
+ if (entry && !isEntryCoveredByTombstone(entry, verdict)) {
288
+ return null;
143
289
  }
144
- if (deps.schemaRegistrar) {
145
- const registered = await deps.schemaRegistrar.registerNoSchema(scope);
146
- deps.logger?.info?.({ scope, schemaId: registered.schemaId }, "Registered no-schema schema for binary scope");
147
- return {
148
- schemaId: registered.schemaId,
149
- schemaUrl: registered.definitionUrl,
150
- };
290
+ return {
291
+ scope,
292
+ dataPointId: deps.serverOwner
293
+ ? computeDataPointId(deps.serverOwner, scope)
294
+ : null,
295
+ deletedAt: verdict.deletedAt,
296
+ };
297
+ }
298
+ /**
299
+ * Throw 410 DATA_DELETED when `resolveReadDeletion` says the read must not be
300
+ * served. Call it before any payment step: a builder must never be charged
301
+ * for a scope the owner deleted.
302
+ */
303
+ export async function assertScopeNotDeleted(deps, scope, entry) {
304
+ const deletion = await resolveReadDeletion(deps, scope, entry);
305
+ if (deletion)
306
+ throw new DataDeletedError(deletion);
307
+ }
308
+ /**
309
+ * Discovery filter for the scope and version listings: an entry a read would
310
+ * refuse with 410 is not listed either. Undefined (no filtering) when no
311
+ * deletion tracker is wired.
312
+ */
313
+ function discoveryVisibility(deps) {
314
+ if (!deps.scopeDeletions)
315
+ return undefined;
316
+ return async (scope, entry) => (await resolveReadDeletion(deps, scope, entry)) === null;
317
+ }
318
+ /**
319
+ * The causal marker a fresh ingest carries: the tombstone version this
320
+ * replica currently knows for the scope, if any, so the new entry is a
321
+ * deliberate re-add that survives that deletion (and only that one). Uses
322
+ * the same verdict path as reads, so it costs a gateway lookup only when the
323
+ * tracker's view is stale. An unknown or unsafe version leaves the marker
324
+ * null: the entry is then covered, never resurrected on a guess.
325
+ */
326
+ async function ingestTombstoneMarker(deps, scope) {
327
+ if (!deps.scopeDeletions)
328
+ return null;
329
+ const verdict = await deps.scopeDeletions.resolve(scope);
330
+ if (!verdict.deleted || verdict.version === null)
331
+ return null;
332
+ const version = Number(verdict.version);
333
+ return Number.isSafeInteger(version) ? version : null;
334
+ }
335
+ // The delete worker wants a full Logger; the API logger is all-optional.
336
+ function apiLoggerAsLogger(logger) {
337
+ const noop = () => undefined;
338
+ return {
339
+ debug: (payload, message) => (logger?.debug ?? noop)(payload, message ?? ""),
340
+ info: (payload, message) => (logger?.info ?? noop)(payload, message ?? ""),
341
+ warn: (payload, message) => (logger?.warn ?? noop)(payload, message ?? ""),
342
+ error: (payload, message) => (logger?.error ?? noop)(payload, message ?? ""),
343
+ };
344
+ }
345
+ function notifyDataWritten(deps, event) {
346
+ if (!deps.onDataWritten)
347
+ return;
348
+ try {
349
+ deps.onDataWritten(event);
350
+ }
351
+ catch (err) {
352
+ deps.logger?.warn?.({
353
+ scope: event.scope,
354
+ error: err instanceof Error ? err.message : String(err),
355
+ }, "onDataWritten hook failed; record already stored");
151
356
  }
152
- return {};
153
357
  }
154
358
  function notifyNewData(syncManager) {
155
359
  if (!syncManager)
@@ -160,6 +364,125 @@ function notifyNewData(syncManager) {
160
364
  }
161
365
  void syncManager.trigger().catch(() => undefined);
162
366
  }
367
+ function shouldReportReadFulfillment(grantId) {
368
+ return (grantId !== "unknown" && grantId !== "owner" && grantId !== "policy-bypass");
369
+ }
370
+ function warnReadFulfillmentReporterFailed(deps, event, err) {
371
+ deps.logger?.warn?.({
372
+ builder: event.builder,
373
+ error: err instanceof Error ? err.message : String(err),
374
+ grantId: event.grantId,
375
+ logId: event.logId,
376
+ scope: event.scope,
377
+ }, "Read fulfillment reporter failed");
378
+ }
379
+ export function reportPersonalServerReadFulfillment(deps, event) {
380
+ if (!deps.readFulfillmentReporter ||
381
+ !shouldReportReadFulfillment(event.grantId)) {
382
+ return;
383
+ }
384
+ try {
385
+ void Promise.resolve(deps.readFulfillmentReporter.report(event)).catch((err) => warnReadFulfillmentReporterFailed(deps, event, err));
386
+ }
387
+ catch (err) {
388
+ warnReadFulfillmentReporterFailed(deps, event, err);
389
+ }
390
+ }
391
+ /**
392
+ * Validate a write's caller-supplied `lineage` (JSON body top level, or the
393
+ * binary metadata object) into the `$lineage` record to stamp. `undefined`
394
+ * (or null) means a root record. Throws ProtocolErrors; the write handler
395
+ * lets them propagate before anything is stored.
396
+ */
397
+ async function prepareWriteLineage(deps, scope, field) {
398
+ if (field === undefined || field === null)
399
+ return undefined;
400
+ return prepareLineage({
401
+ scope,
402
+ field,
403
+ serverOwner: deps.serverOwner,
404
+ storage: deps.storage,
405
+ gateway: deps.lineageGateway,
406
+ now: deps.now ?? (() => new Date()),
407
+ });
408
+ }
409
+ /**
410
+ * The lineage view a read auth result entitles the caller to. The auth port
411
+ * answers an owner read with no result or with the "owner" / "policy-bypass"
412
+ * sentinels (see api-auth): those get the full view. A builder read carries
413
+ * the grant the read policy resolved, and that grant names the view. A
414
+ * builder result with no resolved grant (or an "unknown" one) proves
415
+ * nothing and is refused: a missing grantId must never widen to the full
416
+ * view.
417
+ */
418
+ function resolveLineageGrantView(authResult) {
419
+ if (authResult === undefined ||
420
+ authResult.grantId === "owner" ||
421
+ authResult.grantId === "policy-bypass") {
422
+ return { grantId: undefined };
423
+ }
424
+ const grantId = authResult.grantId;
425
+ if (typeof grantId !== "string" || grantId === "" || grantId === "unknown") {
426
+ throw new GrantRequiredError({
427
+ reason: "a lineage read by a builder needs a resolved grant",
428
+ });
429
+ }
430
+ return { grantId };
431
+ }
432
+ /**
433
+ * True when a read auth result entitles the caller to the owner's view of a
434
+ * record — the same convention as resolveLineageGrantView: no result, or the
435
+ * "owner" / "policy-bypass" sentinels. Everything else is a grantee.
436
+ */
437
+ export function isOwnerView(authResult) {
438
+ return (authResult === undefined ||
439
+ authResult.grantId === "owner" ||
440
+ authResult.grantId === "policy-bypass");
441
+ }
442
+ /**
443
+ * The envelope view a grantee read is entitled to. The server-stamped
444
+ * `$writtenBy` (writing builder's address, grantId, signature) and
445
+ * `$lineage` (the owner's source data-point ids, dictionary-attackable back
446
+ * to scopes since dataPointId = keccak(owner, scope)) never leave the server
447
+ * on a grantee read: the owner reads them from storage, and grantees get the
448
+ * graph only through the grant-redacted lineage endpoint. When `$lineage` is
449
+ * present, the caller-supplied lineage field it mirrors (top-level `lineage`
450
+ * on a JSON record, `metadata.lineage` on a binary one) carries the same ids
451
+ * and is dropped with it; without the stamp such a field is ordinary user
452
+ * data and stays. Redaction is at serve time only — the stored envelope, its
453
+ * dataHash commitment, and the owner's view are unchanged.
454
+ */
455
+ export function redactEnvelopeForGrantee(envelope) {
456
+ const { [WRITER_ATTRIBUTION_KEY]: attribution, [LINEAGE_KEY]: storedLineage, ...rest } = envelope.data;
457
+ if (attribution === undefined && storedLineage === undefined) {
458
+ return envelope;
459
+ }
460
+ let data = rest;
461
+ if (storedLineage !== undefined) {
462
+ if (isBinaryEnvelope(envelope)) {
463
+ const metadata = data["metadata"];
464
+ if (metadata !== null &&
465
+ typeof metadata === "object" &&
466
+ !Array.isArray(metadata)) {
467
+ const { lineage: _consumed, ...metadataRest } = metadata;
468
+ if (Object.keys(metadataRest).length === 0) {
469
+ // The lineage was the whole metadata object; leaving `{}` behind
470
+ // would still disclose that a metadata object existed.
471
+ const { metadata: _emptied, ...dataRest } = data;
472
+ data = dataRest;
473
+ }
474
+ else {
475
+ data = { ...data, metadata: metadataRest };
476
+ }
477
+ }
478
+ }
479
+ else {
480
+ const { lineage: _consumed, ...dataRest } = data;
481
+ data = dataRest;
482
+ }
483
+ }
484
+ return { ...envelope, data };
485
+ }
163
486
  export async function handlePersonalServerDataRequest(request, deps, options = {}) {
164
487
  return withApiErrors(async () => {
165
488
  const url = new URL(request.url);
@@ -173,6 +496,7 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
173
496
  scopePrefix: url.searchParams.get("scopePrefix") ?? undefined,
174
497
  limit: normalizeLimit(url.searchParams.get("limit"), 20),
175
498
  offset: normalizeLimit(url.searchParams.get("offset"), 0),
499
+ isVisible: discoveryVisibility(deps),
176
500
  });
177
501
  return jsonResponse(result.response);
178
502
  }
@@ -181,16 +505,107 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
181
505
  if (request.method !== "GET")
182
506
  return methodNotAllowed();
183
507
  await deps.auth.authorizeBuilderList(request);
184
- const result = listDataVersionsContract({
508
+ const result = await listDataVersionsContract({
185
509
  storage: deps.storage,
186
510
  scopeParam: decodePathPart(parts[0]),
187
511
  limit: normalizeLimit(url.searchParams.get("limit"), 20),
188
512
  offset: normalizeLimit(url.searchParams.get("offset"), 0),
513
+ isVisible: discoveryVisibility(deps),
189
514
  });
190
515
  if (!result.ok)
191
516
  return contractErrorResponse(result);
517
+ if (result.response.total === 0) {
518
+ // Nothing visible: answer 410 rather than an empty list when that is
519
+ // because the scope is tombstoned, exactly as a read would.
520
+ await assertScopeNotDeleted(deps, result.scope, undefined);
521
+ }
192
522
  return jsonResponse(result.response);
193
523
  }
524
+ if ((parts.length === 2 || parts.length === 3) && parts[1] === "lineage") {
525
+ if (request.method !== "GET")
526
+ return methodNotAllowed();
527
+ const scopeResult = parseDataScopeContract(decodePathPart(parts[0]));
528
+ if (!scopeResult.ok)
529
+ return contractErrorResponse(scopeResult);
530
+ // The version is a PATH segment (/v1/data/:scope/lineage/:version), so
531
+ // it is inside the signed request uri: a builder's signature for one
532
+ // version cannot be replayed to read another. The grant view is the
533
+ // signed grantId claim the read auth already honours. Query
534
+ // parameters would be unsigned inputs to the view and are refused.
535
+ if (url.searchParams.has("version")) {
536
+ return errorResponse(400, "INVALID_VERSION", "version is a path segment (/v1/data/:scope/lineage/:version), not a query parameter");
537
+ }
538
+ if (url.search.length > 0) {
539
+ return errorResponse(400, "INVALID_QUERY", "lineage reads take no query parameters; the version is a path segment and the grant view is the signed grantId claim");
540
+ }
541
+ const version = parts.length === 3 ? decodePathPart(parts[2]) : undefined;
542
+ if (version !== undefined && !/^[1-9]\d*$/.test(version)) {
543
+ return errorResponse(400, "INVALID_VERSION", "version must be a positive decimal integer");
544
+ }
545
+ // Same gate as a read of the scope: the owner, or a builder whose grant
546
+ // covers it. The gateway then attests the view that grant sees, so a
547
+ // builder only learns the scopes of nodes its grant covers.
548
+ const authResult = await deps.auth.authorizeBuilderRead({
549
+ request,
550
+ scope: scopeResult.scope,
551
+ grantId: selectedGrantId(request, url),
552
+ });
553
+ if (!deps.serverOwner) {
554
+ throw new ServerNotConfiguredError({
555
+ reason: "serverOwner is required to resolve the data point id",
556
+ });
557
+ }
558
+ if (!deps.lineageGateway)
559
+ throw new LineageUnavailableError();
560
+ const { grantId } = resolveLineageGrantView(authResult);
561
+ let result;
562
+ try {
563
+ result = await deps.lineageGateway.getLineage({
564
+ dataPointId: computeDataPointId(deps.serverOwner, scopeResult.scope),
565
+ version,
566
+ grantId,
567
+ });
568
+ }
569
+ catch (err) {
570
+ // Transport failures (DNS, refused, timeout) are gateway errors to
571
+ // the caller, not internal ones: same 502 as a gateway error body.
572
+ if (err instanceof ProtocolError)
573
+ throw err;
574
+ throw new LineageGatewayError({
575
+ status: 0,
576
+ body: { error: err instanceof Error ? err.message : String(err) },
577
+ });
578
+ }
579
+ if (!result.ok) {
580
+ if (result.status === 404) {
581
+ return errorResponse(404, "NOT_FOUND", version
582
+ ? `Scope "${scopeResult.scope}" has no registered version ${version}`
583
+ : `Scope "${scopeResult.scope}" is not registered at the gateway`);
584
+ }
585
+ if (result.status === 403) {
586
+ throw new ProtocolError(403, "LINEAGE_FORBIDDEN", "The gateway refused the lineage view for this grant", { gateway: result.body });
587
+ }
588
+ throw new LineageGatewayError({
589
+ status: result.status,
590
+ body: result.body,
591
+ });
592
+ }
593
+ // A lineage view discloses the derivation graph, so a served view is
594
+ // logged like a data read (refusals above return without an entry).
595
+ await deps.accessLogWriter.write({
596
+ logId: deps.createLogId?.() ?? crypto.randomUUID(),
597
+ grantId: authResult?.grantId ?? "owner",
598
+ builder: authResult?.builder ?? "unknown",
599
+ action: "lineage",
600
+ scope: scopeResult.scope,
601
+ timestamp: (deps.now ?? (() => new Date()))().toISOString(),
602
+ ipAddress: request.headers.get("x-forwarded-for") ??
603
+ request.headers.get("x-real-ip") ??
604
+ "unknown",
605
+ userAgent: request.headers.get("user-agent") ?? "unknown",
606
+ });
607
+ return jsonResponse({ data: result.data, proof: result.proof });
608
+ }
194
609
  if (parts.length !== 1)
195
610
  return notFound();
196
611
  const scopeParam = decodePathPart(parts[0]);
@@ -203,42 +618,180 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
203
618
  fileId: url.searchParams.get("fileId") ?? undefined,
204
619
  at: url.searchParams.get("at") ?? undefined,
205
620
  });
621
+ // Holds the base64-encoded gateway payForOperation response body when
622
+ // X402 succeeds — emitted on the final read response as X-PAYMENT-RESPONSE.
623
+ let paymentResponseHeader;
206
624
  const grantId = selectedGrantId(request, url);
207
625
  const authResult = await deps.auth.authorizeBuilderRead({
208
626
  request,
209
627
  scope: scopeResult.scope,
210
628
  grantId,
211
629
  fileId: url.searchParams.get("fileId") ?? selectedEntry?.fileId ?? undefined,
630
+ at: url.searchParams.get("at") ?? undefined,
212
631
  });
632
+ // Deletion gate, after auth (so unauthenticated callers learn nothing)
633
+ // and before payment (so nobody is charged for deleted data). Covers
634
+ // both a stale local copy of a tombstoned scope and a local miss.
635
+ await assertScopeNotDeleted(deps, scopeResult.scope, selectedEntry);
636
+ // X402 payment dance for builder reads. Owner-exempt reads (the
637
+ // grantId sentinels "owner" / "policy-bypass") skip payment entirely
638
+ // since there's no payable op to attach the payment to.
639
+ //
640
+ // The grantId we pay against is the one verifyDataReadPolicy resolved
641
+ // from the Web3Signed payload (authoritative source) — not the URL /
642
+ // header hint, which may be absent when the payload carries it.
643
+ const isOwnerSignal = authResult?.grantId === "owner" ||
644
+ authResult?.grantId === "policy-bypass";
645
+ const builder = authResult?.builder;
646
+ const resolvedGrantId = !isOwnerSignal && authResult?.grantId ? authResult.grantId : undefined;
647
+ const willCharge = Boolean(deps.paymentEnabled &&
648
+ !isOwnerSignal &&
649
+ typeof builder === "string" &&
650
+ builder.startsWith("0x") &&
651
+ resolvedGrantId &&
652
+ deps.gateway &&
653
+ deps.gatewayConfig &&
654
+ deps.gatewayUrl);
655
+ // Diagnostic: makes it explicit why a read is/isn't charged. x402 has
656
+ // many "served free" branches (owner read, payment disabled, missing
657
+ // gateway deps); this surfaces the deciding inputs.
658
+ //
659
+ // Routing: when a logger is wired (the Node server always provides one),
660
+ // log the full detail through it at debug level so the operator's level
661
+ // controls + redaction apply and access metadata (scope/grantId/builder)
662
+ // doesn't unconditionally hit stdout. The browser PS-Lite runtime wires
663
+ // no logger; there we fall back to console but log ONLY the non-identifying
664
+ // decision booleans — never scope/grantId/builder.
665
+ if (deps.logger?.debug) {
666
+ deps.logger.debug({
667
+ scope: scopeResult.scope,
668
+ grantId: authResult?.grantId,
669
+ isOwnerSignal,
670
+ builder,
671
+ resolvedGrantId,
672
+ paymentEnabled: Boolean(deps.paymentEnabled),
673
+ hasGateway: Boolean(deps.gateway),
674
+ hasGatewayConfig: Boolean(deps.gatewayConfig),
675
+ hasGatewayUrl: Boolean(deps.gatewayUrl),
676
+ willCharge,
677
+ }, "[x402-gate] read");
678
+ }
679
+ else {
680
+ console.info("[x402-gate] read", {
681
+ isOwnerSignal,
682
+ paymentEnabled: Boolean(deps.paymentEnabled),
683
+ hasGateway: Boolean(deps.gateway),
684
+ hasGatewayConfig: Boolean(deps.gatewayConfig),
685
+ hasGatewayUrl: Boolean(deps.gatewayUrl),
686
+ willCharge,
687
+ });
688
+ }
689
+ // Keep the full condition inline (not `if (willCharge)`) so TS narrows
690
+ // deps.gateway/gatewayConfig/gatewayUrl to non-undefined in the block.
691
+ if (deps.paymentEnabled &&
692
+ !isOwnerSignal &&
693
+ typeof builder === "string" &&
694
+ builder.startsWith("0x") &&
695
+ resolvedGrantId &&
696
+ deps.gateway &&
697
+ deps.gatewayConfig &&
698
+ deps.gatewayUrl) {
699
+ const x402Result = await handleX402Cycle({
700
+ deps,
701
+ request,
702
+ scope: scopeResult.scope,
703
+ fileIdParam: url.searchParams.get("fileId") ?? undefined,
704
+ atParam: url.searchParams.get("at") ?? undefined,
705
+ grantId: resolvedGrantId,
706
+ builder: builder,
707
+ gateway: deps.gateway,
708
+ gatewayConfig: deps.gatewayConfig,
709
+ gatewayUrl: deps.gatewayUrl,
710
+ });
711
+ if (x402Result.kind === "challenge") {
712
+ return jsonResponse(x402Result.body, { status: 402 });
713
+ }
714
+ if (x402Result.kind === "gateway-error") {
715
+ // Relay the gateway's structured body verbatim so the builder can
716
+ // dispatch on it (insufficient balance vs. replay vs. race etc.).
717
+ return jsonResponse(x402Result.body, { status: x402Result.status });
718
+ }
719
+ // x402Result.kind === "ok" — payment accepted, proceed to read.
720
+ // The gateway's success body is forwarded back to the builder via
721
+ // X-PAYMENT-RESPONSE (canonical X402 convention) so callers can see
722
+ // breakdown / paidAt / paymentNonce without a second gateway round-trip.
723
+ paymentResponseHeader = encodePaymentResponseHeader(x402Result.payResponse);
724
+ }
213
725
  const result = await readDataContract({
214
726
  storage: deps.storage,
215
727
  scopeParam: scopeResult.scope,
216
728
  fileId: url.searchParams.get("fileId") ?? undefined,
217
729
  at: url.searchParams.get("at") ?? undefined,
218
730
  });
219
- if (!result.ok)
731
+ if (!result.ok) {
732
+ if (result.status === 404) {
733
+ // The entry selected above can vanish before the read (a sync
734
+ // reconcile applying a tombstone concurrently); answer 410, not
735
+ // 404, when that is why.
736
+ await assertScopeNotDeleted(deps, scopeResult.scope, undefined);
737
+ }
220
738
  return contractErrorResponse(result);
739
+ }
740
+ const servedEnvelope = isOwnerView(authResult)
741
+ ? result.envelope
742
+ : redactEnvelopeForGrantee(result.envelope);
743
+ const logId = deps.createLogId?.() ?? crypto.randomUUID();
744
+ const timestamp = (deps.now ?? (() => new Date()))().toISOString();
745
+ const ipAddress = request.headers.get("x-forwarded-for") ??
746
+ request.headers.get("x-real-ip") ??
747
+ "unknown";
748
+ const userAgent = request.headers.get("user-agent") ?? "unknown";
749
+ const loggedGrantId = authResult?.grantId ?? grantId ?? "unknown";
750
+ const loggedBuilder = authResult?.builder ?? "unknown";
221
751
  await deps.accessLogWriter.write({
222
- logId: deps.createLogId?.() ?? crypto.randomUUID(),
223
- grantId: authResult?.grantId ?? grantId ?? "unknown",
224
- builder: authResult?.builder ?? "unknown",
752
+ logId,
753
+ grantId: loggedGrantId,
754
+ builder: loggedBuilder,
225
755
  action: "read",
226
756
  scope: scopeResult.scope,
227
- timestamp: (deps.now ?? (() => new Date()))().toISOString(),
228
- ipAddress: request.headers.get("x-forwarded-for") ??
229
- request.headers.get("x-real-ip") ??
230
- "unknown",
231
- userAgent: request.headers.get("user-agent") ?? "unknown",
757
+ timestamp,
758
+ ipAddress,
759
+ userAgent,
232
760
  });
761
+ const reportReadFulfillment = () => {
762
+ if (!authResult?.grantId || !authResult.builder)
763
+ return;
764
+ reportPersonalServerReadFulfillment(deps, {
765
+ builder: authResult.builder,
766
+ fileId: url.searchParams.get("fileId") ??
767
+ selectedEntry?.fileId ??
768
+ undefined,
769
+ grantId: authResult.grantId,
770
+ ipAddress,
771
+ logId,
772
+ scope: scopeResult.scope,
773
+ servedAt: timestamp,
774
+ userAgent,
775
+ });
776
+ };
777
+ const headers = {};
778
+ if (paymentResponseHeader) {
779
+ headers["X-PAYMENT-RESPONSE"] = paymentResponseHeader;
780
+ }
781
+ const wantsRawContent = url.searchParams.get("content") === "raw";
233
782
  // `?content=raw` streams the decoded bytes of a binary envelope with its
234
- // original media type, so a builder can download the file directly.
235
- if (url.searchParams.get("content") === "raw" &&
236
- isBinaryEnvelope(result.envelope)) {
237
- const decoded = decodeBinaryEnvelope(result.envelope);
238
- const headers = {
239
- "Content-Type": decoded.mimeType,
240
- "Content-Length": String(decoded.bytes.length),
241
- };
783
+ // original media type, so a builder can download the file directly. The
784
+ // X-PAYMENT-RESPONSE header (if any) rides along on the raw response too.
785
+ if (wantsRawContent) {
786
+ if (!isBinaryEnvelope(servedEnvelope)) {
787
+ return jsonResponse({
788
+ error: "NOT_BINARY_SCOPE",
789
+ message: `Scope "${scopeResult.scope}" does not expose raw binary content.`,
790
+ }, { status: 400, headers });
791
+ }
792
+ const decoded = decodeBinaryEnvelope(servedEnvelope);
793
+ headers["Content-Type"] = decoded.mimeType;
794
+ headers["Content-Length"] = String(decoded.bytes.length);
242
795
  if (decoded.filename) {
243
796
  headers["Content-Disposition"] =
244
797
  `attachment; filename="${decoded.filename}"`;
@@ -246,98 +799,248 @@ export async function handlePersonalServerDataRequest(request, deps, options = {
246
799
  if (decoded.metadata !== undefined) {
247
800
  headers["X-Vana-Metadata"] = stringifyMetadataHeader(decoded.metadata);
248
801
  }
249
- return new Response(decoded.bytes, {
802
+ const response = new Response(decoded.bytes, {
250
803
  status: 200,
251
804
  headers,
252
805
  });
806
+ return response;
253
807
  }
254
- return jsonResponse(result.envelope);
808
+ const response = jsonResponse(servedEnvelope, { headers });
809
+ reportReadFulfillment();
810
+ return response;
255
811
  }
256
812
  if (request.method === "POST") {
257
- await deps.auth.authorizeOwner(request);
258
- const scopeResult = parseDataScopeContract(scopeParam);
259
- if (!scopeResult.ok)
260
- return contractErrorResponse(scopeResult);
261
- const collectedAtValue = collectedAt(deps.now ?? (() => new Date()));
262
- const status = deps.syncManager ? "syncing" : "stored";
263
- // Binary / unstructured data (e.g. a PDF): the body is raw bytes and the
264
- // scope may not have a registered schema — auto-register a no-schema one.
265
- if (!isJsonContentType(request)) {
266
- const { schemaId, schemaUrl } = await resolveBinarySchema(deps, scopeResult.scope);
267
- const bytes = new Uint8Array(await request.arrayBuffer());
268
- const result = await ingestBinaryDataContract({
813
+ // Delegated writes: an auth port that supports write sessions handles
814
+ // both paths in authorizeWrite (builder session token -> write policy +
815
+ // attribution; anything else -> owner path). Ports without it keep
816
+ // today's owner-only gate.
817
+ let writeAuth;
818
+ if (deps.auth.authorizeWrite) {
819
+ writeAuth =
820
+ (await deps.auth.authorizeWrite({ request, scope: scopeParam })) ??
821
+ undefined;
822
+ }
823
+ else {
824
+ await deps.auth.authorizeOwner(request);
825
+ }
826
+ // A write that fails before commit hands the proof back (replay guard)
827
+ // so the builder's retry with the same still-valid proof is accepted;
828
+ // after commit the proof stays consumed (a retry would be a duplicate).
829
+ let committed = false;
830
+ const failWrite = async (response) => {
831
+ await writeAuth?.releaseProof?.();
832
+ return response;
833
+ };
834
+ try {
835
+ const scopeResult = parseDataScopeContract(scopeParam);
836
+ if (!scopeResult.ok)
837
+ return failWrite(contractErrorResponse(scopeResult));
838
+ const collectedAtValue = collectedAt(deps.now ?? (() => new Date()));
839
+ const status = deps.syncManager ? "syncing" : "stored";
840
+ const afterTombstoneVersion = await ingestTombstoneMarker(deps, scopeResult.scope);
841
+ // Builder writes land in the same access log as builder reads, so the
842
+ // owner sees who wrote what under which grant.
843
+ // Best-effort: it runs AFTER the record is committed, so a log failure
844
+ // must not turn a successful write into a 500 (the builder would retry
845
+ // and store a duplicate). The ingest log line above still names the
846
+ // builder; the failure itself is logged for the owner.
847
+ const logBuilderWrite = async () => {
848
+ if (!writeAuth)
849
+ return;
850
+ try {
851
+ await deps.accessLogWriter.write({
852
+ logId: deps.createLogId?.() ?? crypto.randomUUID(),
853
+ grantId: writeAuth.grantId,
854
+ builder: writeAuth.builder,
855
+ action: "write",
856
+ scope: scopeResult.scope,
857
+ timestamp: (deps.now ?? (() => new Date()))().toISOString(),
858
+ ipAddress: request.headers.get("x-forwarded-for") ??
859
+ request.headers.get("x-real-ip") ??
860
+ "unknown",
861
+ userAgent: request.headers.get("user-agent") ?? "unknown",
862
+ });
863
+ }
864
+ catch (err) {
865
+ deps.logger?.warn?.({
866
+ scope: scopeResult.scope,
867
+ builder: writeAuth.builder,
868
+ grantId: writeAuth.grantId,
869
+ error: err instanceof Error ? err.message : String(err),
870
+ }, "Builder write access-log entry failed; record already stored");
871
+ }
872
+ };
873
+ // Binary / unstructured data (e.g. a PDF): the body is raw bytes. DPv2
874
+ // data points are scope-addressed and carry no schemaId, so unstructured
875
+ // data needs no schema at all — we ingest it schemaless. (Structured JSON
876
+ // below still resolves a schema for validation/metadata.)
877
+ if (!isJsonContentType(request)) {
878
+ const bytes = new Uint8Array(await request.arrayBuffer());
879
+ const metadata = parseMetadataHeader(request.headers.get("x-vana-metadata"));
880
+ // A binary derivative names its sources inside the metadata
881
+ // object; the validated mirror lands at the top of the record.
882
+ const lineage = await prepareWriteLineage(deps, scopeResult.scope, extractLineageField(metadata));
883
+ const result = await ingestBinaryDataContract({
884
+ storage: deps.storage,
885
+ scopeParam: scopeResult.scope,
886
+ bytes,
887
+ mimeType: binaryMimeType(request),
888
+ filename: binaryFilename(request),
889
+ metadata,
890
+ collectedAt: collectedAtValue,
891
+ status,
892
+ attribution: writeAuth?.attribution,
893
+ lineage,
894
+ afterTombstoneVersion,
895
+ });
896
+ if (!result.ok)
897
+ return failWrite(contractErrorResponse(result));
898
+ committed = true;
899
+ deps.logger?.info?.({
900
+ scope: scopeResult.scope,
901
+ collectedAt: collectedAtValue,
902
+ path: result.writeResult.relativePath,
903
+ mimeType: binaryMimeType(request),
904
+ sizeBytes: bytes.length,
905
+ ...(writeAuth ? { builder: writeAuth.builder } : {}),
906
+ }, "Binary data file ingested");
907
+ await logBuilderWrite();
908
+ notifyNewData(deps.syncManager);
909
+ notifyDataWritten(deps, {
910
+ scope: scopeResult.scope,
911
+ collectedAt: collectedAtValue,
912
+ lineageSources: lineage?.sources,
913
+ });
914
+ return jsonResponse(result.response, { status: 201 });
915
+ }
916
+ const parsed = await parseJsonObjectBody(request, "Request body must be valid JSON");
917
+ if (!parsed.ok)
918
+ return failWrite(contractResponse(parsed.result));
919
+ // Derivative writes: validate the caller's `lineage` before anything
920
+ // is stored (a failure here throws, releasing the builder's proof).
921
+ const lineage = await prepareWriteLineage(deps, scopeResult.scope, extractLineageField(parsed.body));
922
+ // DPv2 is scope-addressed and the gateway records no schemas, so JSON
923
+ // ingest is schemaless too — no lookup, no schemaId/$schema stamped.
924
+ const result = await ingestDataContract({
269
925
  storage: deps.storage,
270
926
  scopeParam: scopeResult.scope,
271
- bytes,
272
- mimeType: binaryMimeType(request),
273
- filename: binaryFilename(request),
274
- metadata: parseMetadataHeader(request.headers.get("x-vana-metadata")),
927
+ body: parsed.body,
275
928
  collectedAt: collectedAtValue,
276
929
  status,
277
- schemaUrl,
278
- schemaId,
930
+ attribution: writeAuth?.attribution,
931
+ lineage,
932
+ afterTombstoneVersion,
279
933
  });
280
934
  if (!result.ok)
281
- return contractErrorResponse(result);
935
+ return failWrite(contractErrorResponse(result));
936
+ committed = true;
282
937
  deps.logger?.info?.({
283
938
  scope: scopeResult.scope,
284
939
  collectedAt: collectedAtValue,
285
940
  path: result.writeResult.relativePath,
286
- mimeType: binaryMimeType(request),
287
- sizeBytes: bytes.length,
288
- }, "Binary data file ingested");
941
+ ...(writeAuth ? { builder: writeAuth.builder } : {}),
942
+ }, "Data file ingested");
943
+ await logBuilderWrite();
289
944
  notifyNewData(deps.syncManager);
945
+ notifyDataWritten(deps, {
946
+ scope: scopeResult.scope,
947
+ collectedAt: collectedAtValue,
948
+ lineageSources: lineage?.sources,
949
+ });
290
950
  return jsonResponse(result.response, { status: 201 });
291
951
  }
292
- const parsed = await parseJsonObjectBody(request, "Request body must be valid JSON");
293
- if (!parsed.ok)
294
- return contractResponse(parsed.result);
295
- const schema = await resolveSchema(deps, scopeResult.scope);
296
- if (!schema.ok)
297
- return schema.response;
298
- const result = await ingestDataContract({
299
- storage: deps.storage,
300
- scopeParam: scopeResult.scope,
301
- body: parsed.body,
302
- collectedAt: collectedAtValue,
303
- status,
304
- schemaUrl: schema.schemaUrl,
305
- schemaId: schema.schemaId,
306
- });
307
- if (!result.ok)
308
- return contractErrorResponse(result);
309
- deps.logger?.info?.({
310
- scope: scopeResult.scope,
311
- collectedAt: collectedAtValue,
312
- path: result.writeResult.relativePath,
313
- }, "Data file ingested");
314
- notifyNewData(deps.syncManager);
315
- return jsonResponse(result.response, { status: 201 });
952
+ catch (err) {
953
+ // An envelope that reached storage before indexing failed is
954
+ // persisted (a re-index surfaces it): keep the proof consumed so a
955
+ // retry cannot store it twice; the builder needs a fresh proof.
956
+ if (err instanceof IngestPersistedError) {
957
+ deps.logger?.error?.({
958
+ scope: scopeParam,
959
+ path: err.relativePath,
960
+ error: err.cause instanceof Error
961
+ ? err.cause.message
962
+ : String(err.cause),
963
+ }, "Envelope written but indexing failed; record persisted unindexed");
964
+ }
965
+ else if (!committed) {
966
+ await writeAuth?.releaseProof?.();
967
+ }
968
+ throw err;
969
+ }
316
970
  }
317
971
  if (request.method === "DELETE") {
318
972
  await deps.auth.authorizeOwner(request);
319
- // Propagate the deletion to the authoritative stores (R2 blobs + gateway records) BEFORE the
320
- // local delete it reads the local index to find what to remove. Best-effort: a remote
321
- // failure must not block the owner's local delete (and sync would otherwise resurrect it; the
322
- // download-worker reconciliation is the backstop). Parse the scope first so an invalid scope
323
- // still 400s without a wasted remote call.
973
+ const cascade = url.searchParams.get("cascade");
974
+ if (cascade !== null && cascade !== "lineage") {
975
+ throw new InvalidCascadeError({ cascade });
976
+ }
977
+ // Parse the scope first so an invalid scope still 400s without a
978
+ // wasted remote call.
324
979
  const parsed = parseDataScopeContract(scopeParam);
325
- if (parsed.ok && deps.syncManager?.deleteScopeRemote) {
326
- try {
327
- await deps.syncManager.deleteScopeRemote(parsed.scope);
328
- }
329
- catch (err) {
330
- deps.logger?.info?.({ scope: scopeParam, error: err.message }, "Remote scope deletion failed; proceeding with local delete");
331
- }
980
+ if (!parsed.ok)
981
+ return contractErrorResponse(parsed);
982
+ if (cascade === "lineage") {
983
+ // Specified (docs/derivative-data-api.md, "Delete") but not
984
+ // implemented here. The durable per-scope delete below is what a
985
+ // cascade node needs; the missing part is the lineage walk itself
986
+ // (owner view of the gateway graph, deepest derivatives first,
987
+ // truncation and cross-owner handling, all-or-nothing before the
988
+ // first tombstone). Until that lands the only honest answer is 501.
989
+ throw new LineageCascadeUnavailableError({ scope: parsed.scope });
332
990
  }
333
- const result = await deleteDataScopeContract({
334
- storage: deps.storage,
335
- scopeParam,
336
- });
337
- if (!result.ok)
338
- return contractErrorResponse(result);
339
- deps.logger?.info?.({ scope: scopeParam, deletedCount: result.deletedCount }, "Scope deleted");
340
- return new Response(null, { status: 204 });
991
+ // Durable order: gateway tombstone -> storage blobs -> local copy. The
992
+ // sync manager owns the remote ports; without one (sync disabled or
993
+ // no owner key material) only the local copy goes and the result says
994
+ // so (`durable: false`) rather than pretending.
995
+ const result = deps.syncManager?.deleteScope
996
+ ? await deps.syncManager.deleteScope(parsed.scope)
997
+ : await deleteScopeLocally({
998
+ storage: deps.storage,
999
+ serverOwner: deps.serverOwner,
1000
+ deleteData: null,
1001
+ logger: apiLoggerAsLogger(deps.logger),
1002
+ }, parsed.scope);
1003
+ if (result.steps.gateway.status === "failed") {
1004
+ // Nothing was deleted: the local copy is kept on purpose so sync
1005
+ // cannot resurrect a half-deleted scope. Surface it as an upstream
1006
+ // failure with the per-step result attached.
1007
+ throw new DeleteTombstoneFailedError({
1008
+ scope: parsed.scope,
1009
+ result,
1010
+ });
1011
+ }
1012
+ // Audit entry, best-effort: the deletion above is irreversible and
1013
+ // already done, so a failed log write must not turn it into an error
1014
+ // response that invites a misleading retry. The failure is logged.
1015
+ try {
1016
+ await deps.accessLogWriter.write({
1017
+ logId: deps.createLogId?.() ?? crypto.randomUUID(),
1018
+ grantId: "owner",
1019
+ builder: deps.serverOwner ?? "owner",
1020
+ action: "delete",
1021
+ scope: parsed.scope,
1022
+ timestamp: (deps.now ?? (() => new Date()))().toISOString(),
1023
+ ipAddress: request.headers.get("x-forwarded-for") ??
1024
+ request.headers.get("x-real-ip") ??
1025
+ "unknown",
1026
+ userAgent: request.headers.get("user-agent") ?? "unknown",
1027
+ });
1028
+ }
1029
+ catch (err) {
1030
+ deps.logger?.warn?.({
1031
+ scope: parsed.scope,
1032
+ error: err instanceof Error ? err.message : String(err),
1033
+ }, "Delete access-log entry failed; scope already deleted");
1034
+ }
1035
+ deps.logger?.info?.({
1036
+ scope: parsed.scope,
1037
+ durable: result.durable,
1038
+ gateway: result.steps.gateway.status,
1039
+ storage: result.steps.storage.status,
1040
+ local: result.steps.local.status,
1041
+ deletedCount: result.steps.local.deletedCount,
1042
+ }, "Scope deleted");
1043
+ return jsonResponse(result, { status: 200 });
341
1044
  }
342
1045
  return methodNotAllowed();
343
1046
  });
@@ -407,12 +1110,13 @@ export async function handlePersonalServerGrantsRequest(request, deps, options =
407
1110
  const parsed = await parseJsonObjectBody(request);
408
1111
  if (!parsed.ok)
409
1112
  return contractResponse(parsed.result);
1113
+ // Canary createGrant doesn't need a clock — grantVersion defaults
1114
+ // to "1" and callers re-registering pass a strictly higher value.
410
1115
  return contractResponse(await createGrantContract({
411
1116
  gateway: deps.gateway,
412
1117
  serverOwner: deps.serverOwner,
413
1118
  serverSigner: deps.serverSigner,
414
1119
  body: parsed.body,
415
- now: () => deps.now?.().getTime() ?? Date.now(),
416
1120
  }));
417
1121
  }
418
1122
  return methodNotAllowed();