@aigne/afs-did-space 1.12.0-beta.5

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 (510) hide show
  1. package/LICENSE.md +26 -0
  2. package/README.md +129 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
  5. package/dist/_virtual/rolldown_runtime.cjs +29 -0
  6. package/dist/afs-metadata-context.cjs +20 -0
  7. package/dist/afs-metadata-context.d.cts +21 -0
  8. package/dist/afs-metadata-context.d.cts.map +1 -0
  9. package/dist/afs-metadata-context.d.mts +21 -0
  10. package/dist/afs-metadata-context.d.mts.map +1 -0
  11. package/dist/afs-metadata-context.mjs +18 -0
  12. package/dist/afs-metadata-context.mjs.map +1 -0
  13. package/dist/blocklet-hook.cjs +180 -0
  14. package/dist/blocklet-hook.d.cts +29 -0
  15. package/dist/blocklet-hook.d.cts.map +1 -0
  16. package/dist/blocklet-hook.d.mts +29 -0
  17. package/dist/blocklet-hook.d.mts.map +1 -0
  18. package/dist/blocklet-hook.mjs +178 -0
  19. package/dist/blocklet-hook.mjs.map +1 -0
  20. package/dist/changelog-schema.cjs +603 -0
  21. package/dist/changelog-schema.d.cts +395 -0
  22. package/dist/changelog-schema.d.cts.map +1 -0
  23. package/dist/changelog-schema.d.mts +395 -0
  24. package/dist/changelog-schema.d.mts.map +1 -0
  25. package/dist/changelog-schema.mjs +570 -0
  26. package/dist/changelog-schema.mjs.map +1 -0
  27. package/dist/changelog-write.cjs +80 -0
  28. package/dist/changelog-write.d.cts +70 -0
  29. package/dist/changelog-write.d.cts.map +1 -0
  30. package/dist/changelog-write.d.mts +70 -0
  31. package/dist/changelog-write.d.mts.map +1 -0
  32. package/dist/changelog-write.mjs +77 -0
  33. package/dist/changelog-write.mjs.map +1 -0
  34. package/dist/cid-byte-cache.cjs +126 -0
  35. package/dist/cid-byte-cache.d.cts +46 -0
  36. package/dist/cid-byte-cache.d.cts.map +1 -0
  37. package/dist/cid-byte-cache.d.mts +47 -0
  38. package/dist/cid-byte-cache.d.mts.map +1 -0
  39. package/dist/cid-byte-cache.mjs +125 -0
  40. package/dist/cid-byte-cache.mjs.map +1 -0
  41. package/dist/cid.cjs +68 -0
  42. package/dist/cid.d.cts +22 -0
  43. package/dist/cid.d.cts.map +1 -0
  44. package/dist/cid.d.mts +22 -0
  45. package/dist/cid.d.mts.map +1 -0
  46. package/dist/cid.mjs +65 -0
  47. package/dist/cid.mjs.map +1 -0
  48. package/dist/client.cjs +221 -0
  49. package/dist/client.d.cts +72 -0
  50. package/dist/client.d.cts.map +1 -0
  51. package/dist/client.d.mts +72 -0
  52. package/dist/client.d.mts.map +1 -0
  53. package/dist/client.mjs +220 -0
  54. package/dist/client.mjs.map +1 -0
  55. package/dist/cloudflare.cjs +510 -0
  56. package/dist/cloudflare.d.cts +307 -0
  57. package/dist/cloudflare.d.cts.map +1 -0
  58. package/dist/cloudflare.d.mts +308 -0
  59. package/dist/cloudflare.d.mts.map +1 -0
  60. package/dist/cloudflare.mjs +507 -0
  61. package/dist/cloudflare.mjs.map +1 -0
  62. package/dist/ctime-backfill.cjs +272 -0
  63. package/dist/ctime-backfill.d.cts +169 -0
  64. package/dist/ctime-backfill.d.cts.map +1 -0
  65. package/dist/ctime-backfill.d.mts +169 -0
  66. package/dist/ctime-backfill.d.mts.map +1 -0
  67. package/dist/ctime-backfill.mjs +267 -0
  68. package/dist/ctime-backfill.mjs.map +1 -0
  69. package/dist/d1-gc-queue.cjs +80 -0
  70. package/dist/d1-gc-queue.d.cts +59 -0
  71. package/dist/d1-gc-queue.d.cts.map +1 -0
  72. package/dist/d1-gc-queue.d.mts +59 -0
  73. package/dist/d1-gc-queue.d.mts.map +1 -0
  74. package/dist/d1-gc-queue.mjs +81 -0
  75. package/dist/d1-gc-queue.mjs.map +1 -0
  76. package/dist/d1-resolve-store.cjs +189 -0
  77. package/dist/d1-resolve-store.d.cts +93 -0
  78. package/dist/d1-resolve-store.d.cts.map +1 -0
  79. package/dist/d1-resolve-store.d.mts +93 -0
  80. package/dist/d1-resolve-store.d.mts.map +1 -0
  81. package/dist/d1-resolve-store.mjs +190 -0
  82. package/dist/d1-resolve-store.mjs.map +1 -0
  83. package/dist/d1-schema.cjs +361 -0
  84. package/dist/d1-schema.d.cts +237 -0
  85. package/dist/d1-schema.d.cts.map +1 -0
  86. package/dist/d1-schema.d.mts +237 -0
  87. package/dist/d1-schema.d.mts.map +1 -0
  88. package/dist/d1-schema.mjs +341 -0
  89. package/dist/d1-schema.mjs.map +1 -0
  90. package/dist/d1-share-store.cjs +0 -0
  91. package/dist/d1-share-store.d.cts +53 -0
  92. package/dist/d1-share-store.d.cts.map +1 -0
  93. package/dist/d1-share-store.d.mts +53 -0
  94. package/dist/d1-share-store.d.mts.map +1 -0
  95. package/dist/d1-share-store.mjs +0 -0
  96. package/dist/d1-share-store.mjs.map +1 -0
  97. package/dist/d1-tree-index.cjs +1098 -0
  98. package/dist/d1-tree-index.d.cts +203 -0
  99. package/dist/d1-tree-index.d.cts.map +1 -0
  100. package/dist/d1-tree-index.d.mts +203 -0
  101. package/dist/d1-tree-index.d.mts.map +1 -0
  102. package/dist/d1-tree-index.mjs +1094 -0
  103. package/dist/d1-tree-index.mjs.map +1 -0
  104. package/dist/data-backend.cjs +147 -0
  105. package/dist/data-backend.d.cts +30 -0
  106. package/dist/data-backend.d.cts.map +1 -0
  107. package/dist/data-backend.d.mts +30 -0
  108. package/dist/data-backend.d.mts.map +1 -0
  109. package/dist/data-backend.mjs +148 -0
  110. package/dist/data-backend.mjs.map +1 -0
  111. package/dist/dedup-backfill.cjs +278 -0
  112. package/dist/dedup-backfill.d.cts +165 -0
  113. package/dist/dedup-backfill.d.cts.map +1 -0
  114. package/dist/dedup-backfill.d.mts +165 -0
  115. package/dist/dedup-backfill.d.mts.map +1 -0
  116. package/dist/dedup-backfill.mjs +273 -0
  117. package/dist/dedup-backfill.mjs.map +1 -0
  118. package/dist/did-space-like.d.cts +2 -0
  119. package/dist/did-space-like.d.mts +2 -0
  120. package/dist/did-utils.cjs +44 -0
  121. package/dist/did-utils.d.cts +25 -0
  122. package/dist/did-utils.d.cts.map +1 -0
  123. package/dist/did-utils.d.mts +25 -0
  124. package/dist/did-utils.d.mts.map +1 -0
  125. package/dist/did-utils.mjs +43 -0
  126. package/dist/did-utils.mjs.map +1 -0
  127. package/dist/dirty-path-queue.cjs +31 -0
  128. package/dist/dirty-path-queue.d.cts +104 -0
  129. package/dist/dirty-path-queue.d.cts.map +1 -0
  130. package/dist/dirty-path-queue.d.mts +104 -0
  131. package/dist/dirty-path-queue.d.mts.map +1 -0
  132. package/dist/dirty-path-queue.mjs +31 -0
  133. package/dist/dirty-path-queue.mjs.map +1 -0
  134. package/dist/dirty-path-schema.cjs +142 -0
  135. package/dist/dirty-path-schema.d.cts +90 -0
  136. package/dist/dirty-path-schema.d.cts.map +1 -0
  137. package/dist/dirty-path-schema.d.mts +90 -0
  138. package/dist/dirty-path-schema.d.mts.map +1 -0
  139. package/dist/dirty-path-schema.mjs +136 -0
  140. package/dist/dirty-path-schema.mjs.map +1 -0
  141. package/dist/download-actions.cjs +43 -0
  142. package/dist/download-actions.mjs +40 -0
  143. package/dist/download-actions.mjs.map +1 -0
  144. package/dist/fs-object-store.cjs +153 -0
  145. package/dist/fs-object-store.d.cts +21 -0
  146. package/dist/fs-object-store.d.cts.map +1 -0
  147. package/dist/fs-object-store.d.mts +22 -0
  148. package/dist/fs-object-store.d.mts.map +1 -0
  149. package/dist/fs-object-store.mjs +153 -0
  150. package/dist/fs-object-store.mjs.map +1 -0
  151. package/dist/gc-queue.d.cts +43 -0
  152. package/dist/gc-queue.d.cts.map +1 -0
  153. package/dist/gc-queue.d.mts +43 -0
  154. package/dist/gc-queue.d.mts.map +1 -0
  155. package/dist/index.cjs +2712 -0
  156. package/dist/index.d.cts +504 -0
  157. package/dist/index.d.cts.map +1 -0
  158. package/dist/index.d.mts +505 -0
  159. package/dist/index.d.mts.map +1 -0
  160. package/dist/index.mjs +2537 -0
  161. package/dist/index.mjs.map +1 -0
  162. package/dist/inline-content-migration.cjs +212 -0
  163. package/dist/inline-content-migration.d.cts +110 -0
  164. package/dist/inline-content-migration.d.cts.map +1 -0
  165. package/dist/inline-content-migration.d.mts +111 -0
  166. package/dist/inline-content-migration.d.mts.map +1 -0
  167. package/dist/inline-content-migration.mjs +207 -0
  168. package/dist/inline-content-migration.mjs.map +1 -0
  169. package/dist/inline-content.cjs +145 -0
  170. package/dist/inline-content.d.cts +98 -0
  171. package/dist/inline-content.d.cts.map +1 -0
  172. package/dist/inline-content.d.mts +98 -0
  173. package/dist/inline-content.d.mts.map +1 -0
  174. package/dist/inline-content.mjs +137 -0
  175. package/dist/inline-content.mjs.map +1 -0
  176. package/dist/local-changelog.cjs +241 -0
  177. package/dist/local-changelog.d.cts +119 -0
  178. package/dist/local-changelog.d.cts.map +1 -0
  179. package/dist/local-changelog.d.mts +119 -0
  180. package/dist/local-changelog.d.mts.map +1 -0
  181. package/dist/local-changelog.mjs +240 -0
  182. package/dist/local-changelog.mjs.map +1 -0
  183. package/dist/local.cjs +435 -0
  184. package/dist/local.d.cts +254 -0
  185. package/dist/local.d.cts.map +1 -0
  186. package/dist/local.d.mts +254 -0
  187. package/dist/local.d.mts.map +1 -0
  188. package/dist/local.mjs +427 -0
  189. package/dist/local.mjs.map +1 -0
  190. package/dist/memory-gc-queue.cjs +36 -0
  191. package/dist/memory-gc-queue.d.cts +22 -0
  192. package/dist/memory-gc-queue.d.cts.map +1 -0
  193. package/dist/memory-gc-queue.d.mts +22 -0
  194. package/dist/memory-gc-queue.d.mts.map +1 -0
  195. package/dist/memory-gc-queue.mjs +36 -0
  196. package/dist/memory-gc-queue.mjs.map +1 -0
  197. package/dist/memory-tree-index.cjs +734 -0
  198. package/dist/memory-tree-index.d.cts +159 -0
  199. package/dist/memory-tree-index.d.cts.map +1 -0
  200. package/dist/memory-tree-index.d.mts +159 -0
  201. package/dist/memory-tree-index.d.mts.map +1 -0
  202. package/dist/memory-tree-index.mjs +735 -0
  203. package/dist/memory-tree-index.mjs.map +1 -0
  204. package/dist/metadata-db.cjs +562 -0
  205. package/dist/metadata-db.d.cts +102 -0
  206. package/dist/metadata-db.d.cts.map +1 -0
  207. package/dist/metadata-db.d.mts +102 -0
  208. package/dist/metadata-db.d.mts.map +1 -0
  209. package/dist/metadata-db.mjs +562 -0
  210. package/dist/metadata-db.mjs.map +1 -0
  211. package/dist/mime-utils.cjs +2 -0
  212. package/dist/mime-utils.d.cts +2 -0
  213. package/dist/mime-utils.d.mts +2 -0
  214. package/dist/mime-utils.mjs +3 -0
  215. package/dist/object-key.cjs +41 -0
  216. package/dist/object-key.d.cts +25 -0
  217. package/dist/object-key.d.cts.map +1 -0
  218. package/dist/object-key.d.mts +25 -0
  219. package/dist/object-key.d.mts.map +1 -0
  220. package/dist/object-key.mjs +40 -0
  221. package/dist/object-key.mjs.map +1 -0
  222. package/dist/packages/session/dist/index.cjs +788 -0
  223. package/dist/packages/session/dist/index.mjs +782 -0
  224. package/dist/packages/session/dist/index.mjs.map +1 -0
  225. package/dist/platform/r2/dist/types.d.cts +49 -0
  226. package/dist/platform/r2/dist/types.d.cts.map +1 -0
  227. package/dist/prefix-range.cjs +33 -0
  228. package/dist/prefix-range.d.cts +29 -0
  229. package/dist/prefix-range.d.cts.map +1 -0
  230. package/dist/prefix-range.d.mts +29 -0
  231. package/dist/prefix-range.d.mts.map +1 -0
  232. package/dist/prefix-range.mjs +33 -0
  233. package/dist/prefix-range.mjs.map +1 -0
  234. package/dist/prefixed-tree-index.cjs +238 -0
  235. package/dist/prefixed-tree-index.d.cts +85 -0
  236. package/dist/prefixed-tree-index.d.cts.map +1 -0
  237. package/dist/prefixed-tree-index.d.mts +85 -0
  238. package/dist/prefixed-tree-index.d.mts.map +1 -0
  239. package/dist/prefixed-tree-index.mjs +239 -0
  240. package/dist/prefixed-tree-index.mjs.map +1 -0
  241. package/dist/presign-signer.cjs +7 -0
  242. package/dist/presign-signer.d.cts +99 -0
  243. package/dist/presign-signer.d.cts.map +1 -0
  244. package/dist/presign-signer.d.mts +99 -0
  245. package/dist/presign-signer.d.mts.map +1 -0
  246. package/dist/presign-signer.mjs +7 -0
  247. package/dist/presign-signer.mjs.map +1 -0
  248. package/dist/providers/platform/r2/dist/index.d.mts +3 -0
  249. package/dist/providers/platform/r2/dist/types.d.mts +49 -0
  250. package/dist/providers/platform/r2/dist/types.d.mts.map +1 -0
  251. package/dist/reconcile.cjs +184 -0
  252. package/dist/reconcile.d.cts +26 -0
  253. package/dist/reconcile.d.cts.map +1 -0
  254. package/dist/reconcile.d.mts +26 -0
  255. package/dist/reconcile.d.mts.map +1 -0
  256. package/dist/reconcile.mjs +184 -0
  257. package/dist/reconcile.mjs.map +1 -0
  258. package/dist/reference-check.cjs +49 -0
  259. package/dist/reference-check.d.cts +10 -0
  260. package/dist/reference-check.d.cts.map +1 -0
  261. package/dist/reference-check.d.mts +10 -0
  262. package/dist/reference-check.d.mts.map +1 -0
  263. package/dist/reference-check.mjs +48 -0
  264. package/dist/reference-check.mjs.map +1 -0
  265. package/dist/remote-afs.cjs +681 -0
  266. package/dist/remote-afs.d.cts +236 -0
  267. package/dist/remote-afs.d.cts.map +1 -0
  268. package/dist/remote-afs.d.mts +236 -0
  269. package/dist/remote-afs.d.mts.map +1 -0
  270. package/dist/remote-afs.mjs +676 -0
  271. package/dist/remote-afs.mjs.map +1 -0
  272. package/dist/rename-guard.cjs +18 -0
  273. package/dist/rename-guard.mjs +18 -0
  274. package/dist/rename-guard.mjs.map +1 -0
  275. package/dist/resolve.cjs +260 -0
  276. package/dist/resolve.d.cts +113 -0
  277. package/dist/resolve.d.cts.map +1 -0
  278. package/dist/resolve.d.mts +113 -0
  279. package/dist/resolve.d.mts.map +1 -0
  280. package/dist/resolve.mjs +260 -0
  281. package/dist/resolve.mjs.map +1 -0
  282. package/dist/rsync-content.cjs +33 -0
  283. package/dist/rsync-content.mjs +33 -0
  284. package/dist/rsync-content.mjs.map +1 -0
  285. package/dist/rsync.cjs +400 -0
  286. package/dist/rsync.d.cts +157 -0
  287. package/dist/rsync.d.cts.map +1 -0
  288. package/dist/rsync.d.mts +157 -0
  289. package/dist/rsync.d.mts.map +1 -0
  290. package/dist/rsync.mjs +397 -0
  291. package/dist/rsync.mjs.map +1 -0
  292. package/dist/run-gc.cjs +57 -0
  293. package/dist/run-gc.d.cts +40 -0
  294. package/dist/run-gc.d.cts.map +1 -0
  295. package/dist/run-gc.d.mts +41 -0
  296. package/dist/run-gc.d.mts.map +1 -0
  297. package/dist/run-gc.mjs +57 -0
  298. package/dist/run-gc.mjs.map +1 -0
  299. package/dist/scope-id.cjs +70 -0
  300. package/dist/scope-id.d.cts +35 -0
  301. package/dist/scope-id.d.cts.map +1 -0
  302. package/dist/scope-id.d.mts +35 -0
  303. package/dist/scope-id.d.mts.map +1 -0
  304. package/dist/scope-id.mjs +68 -0
  305. package/dist/scope-id.mjs.map +1 -0
  306. package/dist/scope-migration-node.cjs +172 -0
  307. package/dist/scope-migration-node.d.cts +44 -0
  308. package/dist/scope-migration-node.d.cts.map +1 -0
  309. package/dist/scope-migration-node.d.mts +44 -0
  310. package/dist/scope-migration-node.d.mts.map +1 -0
  311. package/dist/scope-migration-node.mjs +171 -0
  312. package/dist/scope-migration-node.mjs.map +1 -0
  313. package/dist/scope-migration.cjs +155 -0
  314. package/dist/scope-migration.d.cts +65 -0
  315. package/dist/scope-migration.d.cts.map +1 -0
  316. package/dist/scope-migration.d.mts +65 -0
  317. package/dist/scope-migration.d.mts.map +1 -0
  318. package/dist/scope-migration.mjs +154 -0
  319. package/dist/scope-migration.mjs.map +1 -0
  320. package/dist/share-declaration.cjs +225 -0
  321. package/dist/share-declaration.d.cts +30 -0
  322. package/dist/share-declaration.d.cts.map +1 -0
  323. package/dist/share-declaration.d.mts +32 -0
  324. package/dist/share-declaration.d.mts.map +1 -0
  325. package/dist/share-declaration.mjs +221 -0
  326. package/dist/share-declaration.mjs.map +1 -0
  327. package/dist/share-hook.cjs +63 -0
  328. package/dist/share-hook.mjs +62 -0
  329. package/dist/share-hook.mjs.map +1 -0
  330. package/dist/share-resolve.cjs +176 -0
  331. package/dist/share-resolve.d.cts +36 -0
  332. package/dist/share-resolve.d.cts.map +1 -0
  333. package/dist/share-resolve.d.mts +36 -0
  334. package/dist/share-resolve.d.mts.map +1 -0
  335. package/dist/share-resolve.mjs +177 -0
  336. package/dist/share-resolve.mjs.map +1 -0
  337. package/dist/share-serve.cjs +145 -0
  338. package/dist/share-serve.d.cts +30 -0
  339. package/dist/share-serve.d.cts.map +1 -0
  340. package/dist/share-serve.d.mts +30 -0
  341. package/dist/share-serve.d.mts.map +1 -0
  342. package/dist/share-serve.mjs +144 -0
  343. package/dist/share-serve.mjs.map +1 -0
  344. package/dist/share-store.cjs +25 -0
  345. package/dist/share-store.d.cts +65 -0
  346. package/dist/share-store.d.cts.map +1 -0
  347. package/dist/share-store.d.mts +65 -0
  348. package/dist/share-store.d.mts.map +1 -0
  349. package/dist/share-store.mjs +25 -0
  350. package/dist/share-store.mjs.map +1 -0
  351. package/dist/share-virtual-path.cjs +102 -0
  352. package/dist/share-virtual-path.mjs +101 -0
  353. package/dist/share-virtual-path.mjs.map +1 -0
  354. package/dist/space-resolver.cjs +54 -0
  355. package/dist/space-resolver.d.cts +34 -0
  356. package/dist/space-resolver.d.cts.map +1 -0
  357. package/dist/space-resolver.d.mts +34 -0
  358. package/dist/space-resolver.d.mts.map +1 -0
  359. package/dist/space-resolver.mjs +55 -0
  360. package/dist/space-resolver.mjs.map +1 -0
  361. package/dist/sqlite-gc-queue.cjs +51 -0
  362. package/dist/sqlite-gc-queue.d.cts +22 -0
  363. package/dist/sqlite-gc-queue.d.cts.map +1 -0
  364. package/dist/sqlite-gc-queue.d.mts +22 -0
  365. package/dist/sqlite-gc-queue.d.mts.map +1 -0
  366. package/dist/sqlite-gc-queue.mjs +52 -0
  367. package/dist/sqlite-gc-queue.mjs.map +1 -0
  368. package/dist/sqlite-resolve-store.cjs +163 -0
  369. package/dist/sqlite-resolve-store.d.cts +50 -0
  370. package/dist/sqlite-resolve-store.d.cts.map +1 -0
  371. package/dist/sqlite-resolve-store.d.mts +50 -0
  372. package/dist/sqlite-resolve-store.d.mts.map +1 -0
  373. package/dist/sqlite-resolve-store.mjs +164 -0
  374. package/dist/sqlite-resolve-store.mjs.map +1 -0
  375. package/dist/sqlite-share-store.cjs +157 -0
  376. package/dist/sqlite-share-store.d.cts +31 -0
  377. package/dist/sqlite-share-store.d.cts.map +1 -0
  378. package/dist/sqlite-share-store.d.mts +31 -0
  379. package/dist/sqlite-share-store.d.mts.map +1 -0
  380. package/dist/sqlite-share-store.mjs +157 -0
  381. package/dist/sqlite-share-store.mjs.map +1 -0
  382. package/dist/sqlite-tree-index.cjs +901 -0
  383. package/dist/sqlite-tree-index.d.cts +107 -0
  384. package/dist/sqlite-tree-index.d.cts.map +1 -0
  385. package/dist/sqlite-tree-index.d.mts +107 -0
  386. package/dist/sqlite-tree-index.d.mts.map +1 -0
  387. package/dist/sqlite-tree-index.mjs +902 -0
  388. package/dist/sqlite-tree-index.mjs.map +1 -0
  389. package/dist/sync-engine.cjs +1324 -0
  390. package/dist/sync-engine.d.cts +473 -0
  391. package/dist/sync-engine.d.cts.map +1 -0
  392. package/dist/sync-engine.d.mts +473 -0
  393. package/dist/sync-engine.d.mts.map +1 -0
  394. package/dist/sync-engine.mjs +1315 -0
  395. package/dist/sync-engine.mjs.map +1 -0
  396. package/dist/sync-state.cjs +303 -0
  397. package/dist/sync-state.d.cts +224 -0
  398. package/dist/sync-state.d.cts.map +1 -0
  399. package/dist/sync-state.d.mts +224 -0
  400. package/dist/sync-state.d.mts.map +1 -0
  401. package/dist/sync-state.mjs +297 -0
  402. package/dist/sync-state.mjs.map +1 -0
  403. package/dist/timestamp.cjs +92 -0
  404. package/dist/timestamp.mjs +91 -0
  405. package/dist/timestamp.mjs.map +1 -0
  406. package/dist/transports/http.cjs +54 -0
  407. package/dist/transports/http.d.cts +26 -0
  408. package/dist/transports/http.d.cts.map +1 -0
  409. package/dist/transports/http.d.mts +26 -0
  410. package/dist/transports/http.d.mts.map +1 -0
  411. package/dist/transports/http.mjs +54 -0
  412. package/dist/transports/http.mjs.map +1 -0
  413. package/dist/transports/static.cjs +55 -0
  414. package/dist/transports/static.d.cts +28 -0
  415. package/dist/transports/static.d.cts.map +1 -0
  416. package/dist/transports/static.d.mts +28 -0
  417. package/dist/transports/static.d.mts.map +1 -0
  418. package/dist/transports/static.mjs +55 -0
  419. package/dist/transports/static.mjs.map +1 -0
  420. package/dist/transports/types.cjs +13 -0
  421. package/dist/transports/types.d.cts +72 -0
  422. package/dist/transports/types.d.cts.map +1 -0
  423. package/dist/transports/types.d.mts +72 -0
  424. package/dist/transports/types.d.mts.map +1 -0
  425. package/dist/transports/types.mjs +13 -0
  426. package/dist/transports/types.mjs.map +1 -0
  427. package/dist/transports/ws.cjs +207 -0
  428. package/dist/transports/ws.d.cts +47 -0
  429. package/dist/transports/ws.d.cts.map +1 -0
  430. package/dist/transports/ws.d.mts +47 -0
  431. package/dist/transports/ws.d.mts.map +1 -0
  432. package/dist/transports/ws.mjs +206 -0
  433. package/dist/transports/ws.mjs.map +1 -0
  434. package/dist/tree-index.cjs +333 -0
  435. package/dist/tree-index.d.cts +524 -0
  436. package/dist/tree-index.d.cts.map +1 -0
  437. package/dist/tree-index.d.mts +524 -0
  438. package/dist/tree-index.d.mts.map +1 -0
  439. package/dist/tree-index.mjs +322 -0
  440. package/dist/tree-index.mjs.map +1 -0
  441. package/dist/tree-query.cjs +655 -0
  442. package/dist/tree-query.d.cts +70 -0
  443. package/dist/tree-query.d.cts.map +1 -0
  444. package/dist/tree-query.d.mts +70 -0
  445. package/dist/tree-query.d.mts.map +1 -0
  446. package/dist/tree-query.mjs +648 -0
  447. package/dist/tree-query.mjs.map +1 -0
  448. package/dist/upload-actions.cjs +97 -0
  449. package/dist/upload-actions.d.cts +19 -0
  450. package/dist/upload-actions.d.cts.map +1 -0
  451. package/dist/upload-actions.d.mts +21 -0
  452. package/dist/upload-actions.d.mts.map +1 -0
  453. package/dist/upload-actions.mjs +90 -0
  454. package/dist/upload-actions.mjs.map +1 -0
  455. package/dist/upload-session-store.cjs +185 -0
  456. package/dist/upload-session-store.d.cts +99 -0
  457. package/dist/upload-session-store.d.cts.map +1 -0
  458. package/dist/upload-session-store.d.mts +99 -0
  459. package/dist/upload-session-store.d.mts.map +1 -0
  460. package/dist/upload-session-store.mjs +184 -0
  461. package/dist/upload-session-store.mjs.map +1 -0
  462. package/dist/url-normalize.cjs +61 -0
  463. package/dist/url-normalize.d.cts +33 -0
  464. package/dist/url-normalize.d.cts.map +1 -0
  465. package/dist/url-normalize.d.mts +33 -0
  466. package/dist/url-normalize.d.mts.map +1 -0
  467. package/dist/url-normalize.mjs +61 -0
  468. package/dist/url-normalize.mjs.map +1 -0
  469. package/dist/versioned-content-backends.cjs +155 -0
  470. package/dist/versioned-content-backends.d.cts +49 -0
  471. package/dist/versioned-content-backends.d.cts.map +1 -0
  472. package/dist/versioned-content-backends.d.mts +49 -0
  473. package/dist/versioned-content-backends.d.mts.map +1 -0
  474. package/dist/versioned-content-backends.mjs +154 -0
  475. package/dist/versioned-content-backends.mjs.map +1 -0
  476. package/dist/versioned-content-index.cjs +102 -0
  477. package/dist/versioned-content-index.d.cts +46 -0
  478. package/dist/versioned-content-index.d.cts.map +1 -0
  479. package/dist/versioned-content-index.d.mts +46 -0
  480. package/dist/versioned-content-index.d.mts.map +1 -0
  481. package/dist/versioned-content-index.mjs +103 -0
  482. package/dist/versioned-content-index.mjs.map +1 -0
  483. package/dist/versioned-content-store.cjs +121 -0
  484. package/dist/versioned-content-store.d.cts +90 -0
  485. package/dist/versioned-content-store.d.cts.map +1 -0
  486. package/dist/versioned-content-store.d.mts +90 -0
  487. package/dist/versioned-content-store.d.mts.map +1 -0
  488. package/dist/versioned-content-store.mjs +122 -0
  489. package/dist/versioned-content-store.mjs.map +1 -0
  490. package/dist/versioned-scope.cjs +39 -0
  491. package/dist/versioned-scope.d.cts +57 -0
  492. package/dist/versioned-scope.d.cts.map +1 -0
  493. package/dist/versioned-scope.d.mts +57 -0
  494. package/dist/versioned-scope.d.mts.map +1 -0
  495. package/dist/versioned-scope.mjs +37 -0
  496. package/dist/versioned-scope.mjs.map +1 -0
  497. package/migrations/0001_tree_entries.sql +17 -0
  498. package/migrations/0002_resolve_entries.sql +7 -0
  499. package/migrations/0003_entries_v2.sql +38 -0
  500. package/migrations/0004_gc_queue.sql +5 -0
  501. package/migrations/0006_resolve_enhance.sql +14 -0
  502. package/migrations/0007_resolve_created_at.sql +10 -0
  503. package/migrations/0008_changelog.sql +34 -0
  504. package/migrations/0009_device_sync_state.sql +29 -0
  505. package/migrations/0010_scope_meta.sql +33 -0
  506. package/migrations/0011_shares.sql +25 -0
  507. package/migrations/0012_scope_meta_file_count.sql +41 -0
  508. package/migrations/0013_scope_meta_total_bytes.sql +47 -0
  509. package/migrations/0014_rename_tables.sql +62 -0
  510. package/package.json +102 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,2537 @@
1
+ import { BLOCKLET_PUBLISH_MARKER, applyBlockletDeleteHook, applyBlockletWriteHook } from "./blocklet-hook.mjs";
2
+ import { CHANGELOG_DDL, CHANGELOG_INDEX_SQL, CHANGELOG_ROW_KIND, CHANGELOG_SCOPE_CID_MIGRATION_SQL, CHANGELOG_TABLE_SQL, CHANGELOG_WIRE_EXTRA_FIELDS, DEVICE_SYNC_STATE_INDEX_SQL, DEVICE_SYNC_STATE_TABLE_SQL, MANIFEST_FILE_KIND, MANIFEST_WIRE_EXTRA_FIELDS, PREFIX_WATERMARK_TABLE_SQL, SCOPE_META_FILE_COUNT_BACKFILL_SQL, SCOPE_META_FILE_COUNT_DELETE_ONE_SQL, SCOPE_META_FILE_COUNT_DELETE_RANGE_SQL, SCOPE_META_FILE_COUNT_TRIGGER_SQL, SCOPE_META_INDEX_SQL, SCOPE_META_TABLE_SQL, SCOPE_META_TOTAL_BYTES_BACKFILL_SQL, SCOPE_META_TOTAL_BYTES_COLUMN_SQL, SCOPE_META_TOTAL_BYTES_TRIGGER_SQL, SYNC_STATE_DDL, SYNC_STATE_INDEX_SQL, SYNC_STATE_TABLE_SQL, normalizeDdl, parseChangelogEntry, parseManifestEntry, serializeChangelogEntry, serializeManifestEntry, watermarkWritePrefix } from "./changelog-schema.mjs";
3
+ import { checksumSha256OfContent, cidToChecksumSha256, computeCID } from "./cid.mjs";
4
+ import { normalizeId, validateId } from "./did-utils.mjs";
5
+ import { DOWNLOAD_ACTIONS, DOWNLOAD_GRANT_TTL_SEC, PrepareDownloadArgsSchema } from "./download-actions.mjs";
6
+ import { detectMimeType, isTextMime } from "./mime-utils.mjs";
7
+ import { DEFAULT_INLINE_THRESHOLD, INGEST_DEVICE_ID, INLINE_CONTENT_HARD_CAP, assertInlineThreshold, decodeInlineText } from "./inline-content.mjs";
8
+ import { isShareDeclarationPath, isShareReservedPath } from "./share-declaration.mjs";
9
+ import { applyShareDeleteHook, applyShareWriteHook } from "./share-hook.mjs";
10
+ import { ShareVirtualHandler, matchSharesVirtualPath } from "./share-virtual-path.mjs";
11
+ import { resolveCtime } from "./timestamp.mjs";
12
+ import { buildKeysetCursor, buildOrderedQueryParts, compareTreeSortValues, keysetAfter, loopBatchDeleteEntries, loopBatchPutEntries, parseTreeCursor } from "./tree-index.mjs";
13
+ import { assertTreeQuerySpec, parseRecordText, projectRecordFields } from "./tree-query.mjs";
14
+ import { D1_DID_SPACE_SCHEMA_SQL, ENTRIES_CONTENT_TYPE_COLUMN_SQL, ENTRIES_CONTENT_TYPE_INDEX_DROP_SQL, ENTRIES_CONTENT_TYPE_INDEX_SQL, ENTRIES_DEDUP_INDEX_SQL, ENTRIES_DEDUP_KEY_COLUMN_SQL, ENTRIES_DEDUP_SCOPE_COLUMN_SQL, ENTRIES_INDEX_SQL, ENTRIES_INLINE_CONTENT_COLUMN_SQL, ENTRIES_METADATA_COLUMN_SQL, ENTRIES_TABLE_SQL, GC_QUEUE_INDEX_SQL, GC_QUEUE_TABLE_SQL, RESOLVE_ENTRIES_INDEX_SQL, RESOLVE_ENTRIES_TABLE_SQL, SHARES_INDEX_SQL, SHARES_TABLE_SQL, UPLOAD_SESSIONS_INDEX_SQL, UPLOAD_SESSIONS_TABLE_SQL, UPLOAD_SESSION_ENTRIES_TABLE_SQL, VERSIONED_SCOPES_INDEX_SQL, VERSIONED_SCOPES_TABLE_SQL } from "./d1-schema.mjs";
15
+ import { D1UploadSessionStore, SqliteUploadSessionStore, UPLOAD_SESSION_TTL_MS } from "./upload-session-store.mjs";
16
+ import { AFS_IFMATCH_ABSENT, AbortUploadArgsSchema, CommitUploadArgsSchema, GRANT_EXPIRES_SEC, PrepareUploadArgsSchema, UPLOAD_ACTIONS, ifMatchPreconditionFailed, zodMessage } from "./upload-actions.mjs";
17
+ import { normalizeItemUrl } from "./url-normalize.mjs";
18
+ import { __decorate } from "./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs";
19
+ import { attachMetadataContext, detachMetadataContext, getMetadataContext } from "./afs-metadata-context.mjs";
20
+ import { CHANGELOG_INSERT_SQL, MAX_DEVICE_ID_BYTES, buildChangelogBinds, validateDeviceId } from "./changelog-write.mjs";
21
+ import { CachingObjectStore, CidByteCache } from "./cid-byte-cache.mjs";
22
+ import { BatchOverBudgetError, BatchUnsupportedError, RpcUserSpaceAFS } from "./remote-afs.mjs";
23
+ import { HttpTransport } from "./transports/http.mjs";
24
+ import { StaticReadonlyTransport } from "./transports/static.mjs";
25
+ import { WsTransport, createWsTransport } from "./transports/ws.mjs";
26
+ import { DIDSpaceClient, connectDIDSpace } from "./client.mjs";
27
+ import { D1GCQueue } from "./d1-gc-queue.mjs";
28
+ import { prefixRange } from "./prefix-range.mjs";
29
+ import { D1ResolveStore } from "./d1-resolve-store.mjs";
30
+ import { assertValidCid, objectKeyForCid } from "./object-key.mjs";
31
+ import { ShareResolver } from "./share-resolve.mjs";
32
+ import { rowToEntry } from "./share-store.mjs";
33
+ import { D1ShareReconciler, D1ShareStore } from "./d1-share-store.mjs";
34
+ import { DIRTY_PATH_DDL, DIRTY_PATH_INDEX_SQL, DIRTY_PATH_TABLE_SQL, DIRTY_PATH_TRIGGER_SQL } from "./dirty-path-schema.mjs";
35
+ import { D1TreeIndex, MAX_ENTRIES_PER_SCOPE, ancestorDirs, nameOf, parentOf } from "./d1-tree-index.mjs";
36
+ import { DIDSpaceBlockletDataBackend } from "./data-backend.mjs";
37
+ import { applyDirtyUpsert } from "./dirty-path-queue.mjs";
38
+ import { LOCAL_CHANGELOG_DEFAULT_LIMIT, LOCAL_CHANGELOG_MAX_LIMIT, LocalChangelog } from "./local-changelog.mjs";
39
+ import { MemoryGcQueue } from "./memory-gc-queue.mjs";
40
+ import { MemoryTreeIndex } from "./memory-tree-index.mjs";
41
+ import { PrefixedTreeIndex } from "./prefixed-tree-index.mjs";
42
+ import { PRESIGN_MAX_EXPIRES_SEC } from "./presign-signer.mjs";
43
+ import { referencedAnywhereD1, referencedAnywhereLibsql } from "./reference-check.mjs";
44
+ import { DIDSpaceResolver } from "./resolve.mjs";
45
+ import { buildManifest, computeDiff, fetchRemoteManifest, runWithConcurrency, syncDIDSpace } from "./rsync.mjs";
46
+ import { DEFAULT_GC_GRACE_MS, runGc } from "./run-gc.mjs";
47
+ import { decodeScopeHmacSecret, deriveScope, isPlainDidScope } from "./scope-id.mjs";
48
+ import { buildD1MigrationSql, classifyScopes, migrateD1Scopes } from "./scope-migration.mjs";
49
+ import { serveShare } from "./share-serve.mjs";
50
+ import { SqliteGcQueue } from "./sqlite-gc-queue.mjs";
51
+ import { SqliteResolveStore } from "./sqlite-resolve-store.mjs";
52
+ import { SqliteShareStore, openDaemonShareStore } from "./sqlite-share-store.mjs";
53
+ import { ConflictResolutionError, CurrentObjectMissingError, PULL_BATCH_LIMIT, PULL_FETCH_CONCURRENCY, PUSH_BATCH_LIMIT, PUSH_PROGRESS_FLUSH_INTERVAL, SYNC_WALL_CLOCK_BUDGET_MS, SyncEngine, classifyMissingObject } from "./sync-engine.mjs";
54
+ import { DEVICE_ID_FORMAT_RE, SYNC_STATE_KEYS, SyncStateStore, defaultAppState, generateDesktopDeviceId, isValidDeviceId, parseSyncStatus, serializeSyncStatus } from "./sync-state.mjs";
55
+ import { D1VersionedScopeBackend, LibsqlVersionedScopeBackend, MemoryVersionedScopeBackend } from "./versioned-content-backends.mjs";
56
+ import { VersionedContentIndex } from "./versioned-content-index.mjs";
57
+ import { VersionedContentStore } from "./versioned-content-store.mjs";
58
+ import { resolveVersionToken, versionedScope, versionedScopeGroup } from "./versioned-scope.mjs";
59
+ import { AFSCompactedError, AFSConflictError, AFSForbiddenError, AFSNotFoundError, AFSUnsupportedError, AFSValidationError, SCRIPT_EVENT_TYPES, collectionQueryStandardAction, isValidOrderBy, makeNsLog, orderByRequested, orderEntries, resolveFieldPath } from "@aigne/afs";
60
+ import { AFSBaseProvider, Actions, Delete, Explain, List, Meta, Read, Search, Stat, Write } from "@aigne/afs/provider";
61
+ import { time } from "@aigne/afs-timing";
62
+ import { joinURL } from "ufo";
63
+ import { z } from "zod";
64
+
65
+ //#region src/index.ts
66
+ const log = makeNsLog("provider:did-space");
67
+ const LIST_MAX_LIMIT = 1e3;
68
+ /**
69
+ * AFS entry `id` is the basename (last path segment), matching AFSFS/AFSJSON —
70
+ * NOT the full path. `AFSBaseProvider.buildEntry` defaults `id` to the full
71
+ * normalized path, so the list handlers below MUST pass an explicit basename id;
72
+ * otherwise consumers that labelize `entry.id` (e.g. the auto-surface settings
73
+ * tabs) render the whole path instead of "Input"/"Sort"/"View". The stat handler
74
+ * already does this (`afsPath.split("/").pop()`); the list handlers were the gap.
75
+ */
76
+ function basenameId(path) {
77
+ return path.split("/").filter(Boolean).pop() ?? "/";
78
+ }
79
+ /**
80
+ * Provider-reserved meta keys (planning/agent-hub/metadata-foundation.md §
81
+ * 白名单). These are computed by DID Space at write time and overwrite
82
+ * anything the application tries to put under the same name. The application
83
+ * may write any OTHER key into `payload.meta` and it'll be persisted to the
84
+ * `entries.metadata` column.
85
+ *
86
+ * Note: `ctime / mtime` are NOT in `entry.meta` (they live at AFSEntry top
87
+ * level as `createdAt / updatedAt`), so they're not in the reserved set —
88
+ * the application can write `meta.ctime` and it'll be persisted as just
89
+ * another application field, NOT conflated with the entry's timestamp.
90
+ */
91
+ const PROVIDER_RESERVED_META_KEYS = new Set([
92
+ "kind",
93
+ "size",
94
+ "cid",
95
+ "mimeType"
96
+ ]);
97
+ /**
98
+ * Strip provider-reserved keys from a write payload's meta, leaving only the
99
+ * application-owned fields that should go to `entries.metadata`.
100
+ * Returns undefined when nothing application-owned remains (avoids storing
101
+ * `{}` blobs).
102
+ */
103
+ function extractApplicationMeta(meta) {
104
+ if (!meta) return void 0;
105
+ const out = {};
106
+ let any = false;
107
+ for (const [k, v] of Object.entries(meta)) {
108
+ if (PROVIDER_RESERVED_META_KEYS.has(k)) continue;
109
+ out[k] = v;
110
+ any = true;
111
+ }
112
+ return any ? out : void 0;
113
+ }
114
+ /**
115
+ * Loose upper bound on serialized application metadata (did-space-metadata
116
+ * NEEDS DECISION 3). `metadata` rides `z.record(z.unknown())` and is NOT
117
+ * digest-checked (it's application data, not bytes — design §5), so the wire
118
+ * imposes no size limit of its own. Aside's real metadata is tiny (a few ISO
119
+ * timestamps, ~50B), but a malicious / buggy client could otherwise stuff a
120
+ * huge JSON blob straight into the D1 `metadata` column (row-size / write-
121
+ * amplification risk, cf. the D1 burn-through lesson). 64KB is far above any
122
+ * legitimate use yet blocks abuse. Shared by the two new server ingress points
123
+ * (batchWrite + upload-plane commit) so the threshold can't drift.
124
+ */
125
+ const METADATA_MAX_BYTES = 64 * 1024;
126
+ /**
127
+ * Reject abusively large application metadata gracefully (an `AFSValidationError`,
128
+ * which afs-rpc maps to 400 — never an uncaught 500). Call AFTER
129
+ * `extractApplicationMeta` so the reserved-key strip can't be gamed to dodge
130
+ * the bound. No-op for absent metadata.
131
+ */
132
+ function assertMetadataSize(meta, path) {
133
+ if (!meta) return;
134
+ const bytes = new TextEncoder().encode(JSON.stringify(meta)).length;
135
+ if (bytes > METADATA_MAX_BYTES) throw new AFSValidationError(`Application metadata for ${path} is ${bytes} bytes, over the ${METADATA_MAX_BYTES}-byte limit`);
136
+ }
137
+ /** Plain JSON object (not array, not null) — the deep-merge branch. */
138
+ function isPlainObject(v) {
139
+ return typeof v === "object" && v !== null && !Array.isArray(v);
140
+ }
141
+ /** An incoming scalar that carries no information and must never overwrite. */
142
+ function isEmptyScalar(v) {
143
+ return v === void 0 || v === null || v === "";
144
+ }
145
+ /** Union two arrays, dedup by structural identity, existing entries first. */
146
+ function unionArrays(existing, incoming) {
147
+ const seen = new Set(existing.map((x) => JSON.stringify(x)));
148
+ const out = [...existing];
149
+ for (const item of incoming) {
150
+ const key = JSON.stringify(item);
151
+ if (!seen.has(key)) {
152
+ out.push(item);
153
+ seen.add(key);
154
+ }
155
+ }
156
+ return out;
157
+ }
158
+ /**
159
+ * Merge incoming item content into the existing item on a duplicate-key write
160
+ * (#492). A re-save is ADDITIVE and must never DEGRADE the stored item:
161
+ *
162
+ * - keys in `immutable` (always `createdAt`, plus caller-supplied lifecycle
163
+ * fields like `status`) keep the existing value — a re-clip never resets an
164
+ * archived item or rewrites its creation time;
165
+ * - arrays union-dedup (existing first) so tags / collections accrete;
166
+ * - plain objects deep-merge recursively, so a partial re-clip of `og` /
167
+ * `content` keeps fields the new capture omitted;
168
+ * - scalars take the incoming value EXCEPT empty ones (`undefined` / `null` /
169
+ * `""`), which keep the existing value — a bookmarklet that failed to grab a
170
+ * title must not wipe the good title already saved;
171
+ * - type conflicts (array↔scalar, object↔scalar) take the incoming value.
172
+ */
173
+ function mergeItemContent(existing, incoming, immutable = new Set(["createdAt"])) {
174
+ const merged = { ...existing };
175
+ for (const [k, v] of Object.entries(incoming)) {
176
+ if (immutable.has(k)) continue;
177
+ const cur = merged[k];
178
+ if (Array.isArray(v) && Array.isArray(cur)) merged[k] = unionArrays(cur, v);
179
+ else if (isPlainObject(v) && isPlainObject(cur)) merged[k] = mergeItemContent(cur, v, immutable);
180
+ else if (isEmptyScalar(v)) {} else merged[k] = v;
181
+ }
182
+ return merged;
183
+ }
184
+ var DIDSpaceProvider = class extends AFSBaseProvider {
185
+ static manifest() {
186
+ return {
187
+ name: "did-space",
188
+ description: "DID-scoped persistent storage with CID-based content deduplication.\n- Per-user data isolation via DID prefix\n- Content-addressable deduplication (CID v1, raw codec + SHA-256)\n- R2 for objects, D1 for tree index",
189
+ uriTemplate: "did-space://{did}/{path*}",
190
+ category: "storage",
191
+ schema: z.object({
192
+ did: z.string(),
193
+ path: z.string().optional()
194
+ }),
195
+ tags: [
196
+ "did",
197
+ "storage",
198
+ "cid",
199
+ "dedup",
200
+ "cloud"
201
+ ],
202
+ capabilityTags: [
203
+ "read-write",
204
+ "crud",
205
+ "search",
206
+ "auth:token"
207
+ ],
208
+ security: {
209
+ riskLevel: "sandboxed",
210
+ resourceAccess: []
211
+ },
212
+ capabilities: { filesystem: {
213
+ read: true,
214
+ write: true
215
+ } },
216
+ noPerEntryActions: true
217
+ };
218
+ }
219
+ static treeSchema() {
220
+ return {
221
+ operations: [
222
+ "list",
223
+ "read",
224
+ "write",
225
+ "delete",
226
+ "search",
227
+ "stat",
228
+ "explain"
229
+ ],
230
+ tree: {
231
+ "/": { kind: "did-space:root" },
232
+ "/{path+}": { kind: "did-space:file" }
233
+ },
234
+ auth: { type: "custom" },
235
+ bestFor: [
236
+ "user files",
237
+ "persistent storage",
238
+ "content-addressed data"
239
+ ],
240
+ notFor: ["large binary blobs", "real-time streaming"]
241
+ };
242
+ }
243
+ name;
244
+ description;
245
+ accessMode;
246
+ did;
247
+ objectStore;
248
+ treeIndex;
249
+ gcQueue;
250
+ /** See `DIDSpaceProviderOptions.deviceId`. Read by writeHandler/deleteHandler. */
251
+ deviceId;
252
+ /** Optional ShareResolver for .share reconcile hook (Phase 0.4) */
253
+ shareResolver;
254
+ /** See `DIDSpaceProviderOptions.shareOwnerDid` / `shareTreePrefix`. */
255
+ shareOwnerDid;
256
+ shareTreePrefix;
257
+ /** See `DIDSpaceProviderOptions.blockletResolver` / `blockletOwnerDid` (#674). */
258
+ blockletResolver;
259
+ blockletOwnerDid;
260
+ /** Upload plane deps (design §7). See `DIDSpaceProviderOptions`. */
261
+ signer;
262
+ uploadSessionStore;
263
+ uploadScope;
264
+ constructor(options) {
265
+ super();
266
+ validateId(options.did);
267
+ this.name = options.name || "did-space";
268
+ this.description = options.description;
269
+ this.accessMode = options.accessMode ?? "readwrite";
270
+ this.did = normalizeId(options.did);
271
+ this.objectStore = options.objectStore;
272
+ this.treeIndex = options.treeIndex;
273
+ this.gcQueue = options.gcQueue;
274
+ this.deviceId = options.deviceId ?? "server:default";
275
+ this.shareResolver = options.shareResolver;
276
+ this.shareOwnerDid = options.shareOwnerDid;
277
+ this.shareTreePrefix = options.shareTreePrefix;
278
+ this.blockletResolver = options.blockletResolver;
279
+ this.blockletOwnerDid = options.blockletOwnerDid;
280
+ this.signer = options.signer;
281
+ this.uploadSessionStore = options.uploadSessionStore;
282
+ this.uploadScope = options.uploadScope;
283
+ }
284
+ /** Convert AFS path to tree-index path (strip leading /) */
285
+ treePath(afsPath) {
286
+ if (afsPath.includes("\0")) throw new Error("Null byte in path");
287
+ if (/(?:^|\/)\.\.(?:\/|$)/.test(afsPath)) throw new Error("Path traversal in path");
288
+ return afsPath.replace(/^\/+/, "");
289
+ }
290
+ /** Convert AFS path to tree-index prefix for directory listing */
291
+ treeListPrefix(afsPath) {
292
+ if (afsPath === "/") return "";
293
+ return `${this.treePath(afsPath)}/`;
294
+ }
295
+ /**
296
+ * Remap a write/delete into the arguments the .share reconcile hook expects.
297
+ * On a whole-space provider this is identity. On a prefixed instance-fragment
298
+ * provider (`shareTreePrefix` set) it restores the WHOLE-TREE decl path and
299
+ * keys the share to the space owner (`shareOwnerDid`), so reconcile stores the
300
+ * same `owner_did` + full `decl_path` the public serving path resolves by.
301
+ */
302
+ shareHookArgs(afsPath, treePath) {
303
+ const fullTreePath = this.shareTreePrefix ? joinURL(this.shareTreePrefix, treePath) : treePath;
304
+ return {
305
+ afsPath: this.shareTreePrefix ? `/${fullTreePath}` : afsPath,
306
+ treePath: fullTreePath,
307
+ ownerDid: this.shareOwnerDid ?? this.did
308
+ };
309
+ }
310
+ /**
311
+ * Remap a write/delete into the arguments the blocklet hook expects. Same
312
+ * whole-tree path restoration as {@link shareHookArgs}, but the recorded
313
+ * `ownerDid` is the PLAINTEXT space owner (`blockletOwnerDid`), not the de-id
314
+ * scope — the serve path re-derives the storage scope from the plaintext DID.
315
+ */
316
+ blockletHookArgs(afsPath, treePath) {
317
+ const fullTreePath = this.shareTreePrefix ? joinURL(this.shareTreePrefix, treePath) : treePath;
318
+ return {
319
+ afsPath: this.shareTreePrefix ? `/${fullTreePath}` : afsPath,
320
+ treePath: fullTreePath,
321
+ ownerDid: this.blockletOwnerDid ?? this.did
322
+ };
323
+ }
324
+ objectKey(cid) {
325
+ if (!/^[bz][a-zA-Z0-9]{20,100}$/.test(cid)) throw new Error(`Invalid CID format: ${cid.slice(0, 30)}`);
326
+ return `objects/${cid}`;
327
+ }
328
+ async isDirectory(path) {
329
+ if (path === "/") return true;
330
+ return this.treeIndex.hasChildren(this.treeListPrefix(path));
331
+ }
332
+ /** Collect all CIDs under a prefix for GC queuing before bulk delete. */
333
+ async collectCIDsByPrefix(prefix) {
334
+ const cids = [];
335
+ let cursor;
336
+ do {
337
+ const result = await this.treeIndex.listEntries(prefix, {
338
+ limit: 1e3,
339
+ cursor
340
+ });
341
+ for (const e of result.entries) if (e.cid) cids.push(e.cid);
342
+ cursor = result.hasMore ? result.cursor : void 0;
343
+ } while (cursor);
344
+ return cids;
345
+ }
346
+ async readMeta(ctx) {
347
+ const afsPath = joinURL("/", ctx.params.path ?? "");
348
+ if (afsPath === "/") {
349
+ const childrenCount = await this.treeIndex.countChildren(this.treeListPrefix("/"));
350
+ return this.buildEntry(joinURL(afsPath, ".meta"), {
351
+ content: {
352
+ type: "directory",
353
+ path: "/"
354
+ },
355
+ meta: {
356
+ kind: "did-space:root",
357
+ childrenCount
358
+ }
359
+ });
360
+ }
361
+ const entry = await this.treeIndex.get(this.treePath(afsPath), { includeContent: false });
362
+ if (entry) return this.buildEntry(joinURL(afsPath, ".meta"), {
363
+ content: {
364
+ type: "file",
365
+ path: afsPath,
366
+ size: entry.size,
367
+ cid: entry.cid
368
+ },
369
+ meta: {
370
+ kind: "did-space:file",
371
+ size: entry.size,
372
+ cid: entry.cid,
373
+ mimeType: entry.mimeType
374
+ },
375
+ createdAt: new Date(entry.ctime),
376
+ updatedAt: new Date(entry.mtime)
377
+ });
378
+ if (await this.isDirectory(afsPath)) {
379
+ const childrenCount = await this.treeIndex.countChildren(this.treeListPrefix(afsPath));
380
+ return this.buildEntry(joinURL(afsPath, ".meta"), {
381
+ content: {
382
+ type: "directory",
383
+ path: afsPath
384
+ },
385
+ meta: {
386
+ kind: "did-space:directory",
387
+ childrenCount
388
+ }
389
+ });
390
+ }
391
+ throw new AFSNotFoundError(afsPath);
392
+ }
393
+ /**
394
+ * Override to implement the `maxDepth: -1` snapshot contract.
395
+ *
396
+ * When called with `maxDepth: -1`, collects ALL file entries under `path`
397
+ * using cursor-based pagination through the treeIndex and returns them in
398
+ * a single `AFSListResult` with `meta.cursor = String(manifestSeq)` so
399
+ * `SyncEngine.fullResync()` can advance `cloudSeq` correctly.
400
+ *
401
+ * > 10_000 entries → sets `meta.truncated: true` (HTTP 200, SyncEngine
402
+ * throws `ManifestTooLargeError`).
403
+ *
404
+ * All other options fall through to `super.list()` → `listHandler`.
405
+ */
406
+ async list(path, options) {
407
+ if (options?.maxDepth !== -1) return super.list(path, options);
408
+ const normPath = path.startsWith("/") ? path.slice(1) : path;
409
+ if (normPath === ".changelog" || normPath.startsWith(".changelog/")) throw new AFSValidationError(".changelog path does not support maxDepth=-1 (changelog uses cursor-based incremental access)");
410
+ if (options.cursor !== void 0 || options.limit !== void 0 && Number.isFinite(options.limit) || options.offset !== void 0 && Number.isFinite(options.offset)) throw new AFSValidationError("list with maxDepth=-1 cannot be combined with cursor, limit, or offset");
411
+ const SNAPSHOT_MAX_ENTRIES = 1e4;
412
+ const prefix = this.treeListPrefix(path);
413
+ const allEntries = [];
414
+ let cursor;
415
+ do {
416
+ const result = await this.treeIndex.listEntries(prefix, {
417
+ limit: LIST_MAX_LIMIT,
418
+ cursor
419
+ });
420
+ for (const treeEntry of result.entries) {
421
+ if (allEntries.length >= SNAPSHOT_MAX_ENTRIES) return {
422
+ data: allEntries,
423
+ meta: {
424
+ truncated: true,
425
+ ...orderByRequested(options.orderBy) ? { orderByIgnored: true } : {}
426
+ }
427
+ };
428
+ allEntries.push(this.buildEntry(`/${treeEntry.path}`, {
429
+ id: basenameId(treeEntry.path),
430
+ meta: serializeManifestEntry({
431
+ cid: treeEntry.cid,
432
+ size: treeEntry.size,
433
+ mtime: treeEntry.mtime,
434
+ mimeType: treeEntry.mimeType
435
+ }),
436
+ createdAt: new Date(treeEntry.ctime),
437
+ updatedAt: new Date(treeEntry.mtime)
438
+ }));
439
+ }
440
+ cursor = result.hasMore ? result.cursor : void 0;
441
+ } while (cursor);
442
+ const manifestSeq = await this.treeIndex.getMaxSeq();
443
+ const snapshotMeta = { cursor: String(manifestSeq) };
444
+ if (orderByRequested(options.orderBy)) snapshotMeta.orderByIgnored = true;
445
+ return {
446
+ data: allEntries,
447
+ meta: snapshotMeta
448
+ };
449
+ }
450
+ async listHandler(ctx) {
451
+ const afsPath = ctx.params.path ? `/${ctx.params.path}` : "/";
452
+ const options = ctx.options;
453
+ const includeContent = options?.includeContent === true;
454
+ const includeChildrenCounts = options?.includeChildrenCounts === true;
455
+ const orderRequested = orderByRequested(options?.orderBy);
456
+ const orderBy = orderRequested && isValidOrderBy(options?.orderBy) ? options.orderBy : void 0;
457
+ /** Merge the no-silent-drop flag into a result when ordering was requested but not applied. */
458
+ const flagIgnored = (result) => orderRequested ? {
459
+ ...result,
460
+ meta: {
461
+ ...result.meta,
462
+ orderByIgnored: true
463
+ }
464
+ } : result;
465
+ const sharesMatch = matchSharesVirtualPath(afsPath);
466
+ if (sharesMatch && this.shareResolver && !this.shareTreePrefix) {
467
+ const { list } = await new ShareVirtualHandler(this.shareResolver, this.did).list(sharesMatch);
468
+ return flagIgnored({ data: list });
469
+ }
470
+ const normPath = afsPath.startsWith("/") ? afsPath.slice(1) : afsPath;
471
+ const isChangelog = normPath === ".changelog" || normPath.startsWith(".changelog/");
472
+ if (isChangelog && options?.cursor !== void 0) return flagIgnored(await this.listChangelog(options.cursor, options.limit));
473
+ const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
474
+ const offset = Math.max(0, Number(options?.offset) || 0);
475
+ const maxDepth = options?.maxDepth ?? 1;
476
+ const collectLimit = offset + limit;
477
+ if (maxDepth === 0) return { data: [] };
478
+ const prefix = this.treeListPrefix(afsPath);
479
+ if (afsPath !== "/") {
480
+ if (!await this.treeIndex.get(this.treePath(afsPath), { includeContent: false })) {
481
+ if (!await this.treeIndex.hasChildren(prefix)) throw new AFSNotFoundError(afsPath);
482
+ }
483
+ }
484
+ const entries = [];
485
+ if (maxDepth === 1) {
486
+ if (orderBy) {
487
+ const scan$1 = await this.listShallowOrdered(prefix, collectLimit, entries, orderBy, includeContent);
488
+ if (includeChildrenCounts) await this.fillChildrenCounts(entries);
489
+ return this.withLiveSeq({
490
+ data: entries,
491
+ total: await this.shallowTotal(prefix, scan$1)
492
+ }, prefix, isChangelog);
493
+ }
494
+ const scan = await this.listShallow(prefix, collectLimit, entries, void 0, includeContent);
495
+ if (includeChildrenCounts) await this.fillChildrenCounts(entries);
496
+ return this.withLiveSeq(flagIgnored({
497
+ data: entries,
498
+ total: await this.shallowTotal(prefix, scan)
499
+ }), prefix, isChangelog);
500
+ }
501
+ await this.listDeep(afsPath, prefix, collectLimit, maxDepth, entries, includeContent);
502
+ return flagIgnored({ data: entries });
503
+ }
504
+ /**
505
+ * True direct-children total for a shallow listing.
506
+ *
507
+ * AFSListResult contract (`packages/core/src/type.ts`): absent `total`
508
+ * means "data IS the complete result" — so a handler that truncates its
509
+ * collection MUST report `total`, or every consumer downstream (base.ts
510
+ * pagination, afs-list pager) mistakes the page for the whole set.
511
+ *
512
+ * Cost model (D1: per-query round-trips dominate): `listShallow` scans
513
+ * the index in LIST_MAX_LIMIT batches and keeps counting rows past the
514
+ * build cap, so when the cursor drained (directories ≤ LIST_MAX_LIMIT
515
+ * children — the common case) the scan itself yields the exact total:
516
+ * ZERO extra queries. Only a directory truncated mid-cursor (> 1000
517
+ * children) pays ONE COUNT round-trip (same `countChildren(prefix)`
518
+ * call and parent-key format `readMeta` already relies on).
519
+ */
520
+ async shallowTotal(prefix, scan) {
521
+ if (scan.exhausted) return scan.seen;
522
+ return this.treeIndex.countChildren(prefix);
523
+ }
524
+ /**
525
+ * Attach the per-prefix watermark `version` as the result's top-level
526
+ * `liveSeq` (Live Update Resilience §4 / 0a'). Read ONE row via the
527
+ * TreeIndex — O(1), independent of changelog history/global write volume.
528
+ *
529
+ * - Skipped for `.changelog` listings (sync-internal, no live-update scope).
530
+ * - Skipped when the index has no `getPrefixWatermark` (rollout) or reports
531
+ * no watermark key for the path (`null` — no collection root): the
532
+ * consumer then falls back to full re-list on the next edge/poll, still
533
+ * correct.
534
+ * - A valid-but-never-written collection root reports `"0"` so consumers
535
+ * baseline at 0 instead of conservatively refetching every poll tick on a
536
+ * read-only collection (the first write bumps to 1 > 0 and is detected).
537
+ * - The value is prefix-LOCAL (only this collection root's write count), so
538
+ * it never leaks another blocklet's timing (invariant I3).
539
+ */
540
+ async withLiveSeq(result, prefix, isChangelog) {
541
+ if (isChangelog || !this.treeIndex.getPrefixWatermark) return result;
542
+ const version = await this.treeIndex.getPrefixWatermark(prefix);
543
+ if (version != null) result.liveSeq = String(version);
544
+ return result;
545
+ }
546
+ /**
547
+ * Shallow listing with a VALID orderBy (did-space-sorting Phase 3).
548
+ *
549
+ * - Single tuple → forwarded to the TreeIndex (`listChildren` orders
550
+ * natively over the (scope,parent,col,name) indexes; keyset cursor
551
+ * pages stay globally ordered, so collecting offset+limit entries is
552
+ * the correct order-before-pagination slice).
553
+ * - 2-3 tuples → the index can order by one column only, so collect the
554
+ * FULL child set and apply the canonical core sort before truncation
555
+ * (order → offset → limit still holds).
556
+ */
557
+ async listShallowOrdered(prefix, collectLimit, entries, orderBy, includeContent = false) {
558
+ if (orderBy.length === 1) {
559
+ const [field, order] = orderBy[0];
560
+ return this.listShallow(prefix, collectLimit, entries, {
561
+ orderBy: field,
562
+ order
563
+ }, includeContent);
564
+ }
565
+ const all = [];
566
+ await this.listShallow(prefix, Number.MAX_SAFE_INTEGER, all, void 0, includeContent);
567
+ const { entries: sorted } = orderEntries(all, orderBy);
568
+ entries.push(...sorted.slice(0, collectLimit));
569
+ return {
570
+ exhausted: true,
571
+ seen: all.length
572
+ };
573
+ }
574
+ async listShallow(prefix, limit, entries, order, includeContent = false) {
575
+ let cursor;
576
+ let seen = 0;
577
+ do {
578
+ const result = await this.treeIndex.listChildren(prefix, {
579
+ limit: LIST_MAX_LIMIT,
580
+ cursor,
581
+ includeContent,
582
+ ...order ?? {}
583
+ });
584
+ seen += result.entries.length;
585
+ for (const child of result.entries) {
586
+ if (entries.length >= limit) continue;
587
+ if (child.path.endsWith("/")) {
588
+ const displayPath = `/${child.path.slice(0, -1)}`;
589
+ entries.push(this.buildEntry(displayPath, {
590
+ id: basenameId(displayPath),
591
+ meta: {
592
+ kind: "did-space:directory",
593
+ childrenCount: -1,
594
+ size: child.size
595
+ },
596
+ createdAt: new Date(child.ctime),
597
+ updatedAt: new Date(child.mtime)
598
+ }));
599
+ } else entries.push(this.buildEntry(`/${child.path}`, {
600
+ id: basenameId(child.path),
601
+ ...includeContent && await this.verifiedInline(child) !== null ? { content: child.content } : {},
602
+ meta: {
603
+ kind: "did-space:file",
604
+ cid: child.cid,
605
+ size: child.size,
606
+ mimeType: child.mimeType
607
+ },
608
+ createdAt: new Date(child.ctime),
609
+ updatedAt: new Date(child.mtime)
610
+ }));
611
+ }
612
+ cursor = result.hasMore ? result.cursor : void 0;
613
+ } while (cursor && entries.length < limit);
614
+ return {
615
+ exhausted: cursor === void 0,
616
+ seen
617
+ };
618
+ }
619
+ /**
620
+ * Patch EXACT `meta.childrenCount` onto the directory entries of a
621
+ * collected shallow page — the opt-in half of the two-tier childrenCount
622
+ * contract (`AFSListOptions.includeChildrenCounts`; default tier is the
623
+ * `-1` sentinel stamped by listShallow). Page-bounded by construction:
624
+ * only the dirs actually collected are counted, ONE `countChildrenMany`
625
+ * round-trip (per-dir `countChildren` fallback for indexes without it),
626
+ * so the cost is Σ direct children of the page's dirs — never the
627
+ * whole-subtree scan the retired `countChildrenGrouped` performed.
628
+ */
629
+ async fillChildrenCounts(entries) {
630
+ const dirs = entries.filter((e) => e.meta?.kind === "did-space:directory");
631
+ if (dirs.length === 0) return;
632
+ const keyOf = (e) => `${this.treePath(e.path)}/`;
633
+ if (this.treeIndex.countChildrenMany) {
634
+ const counts = await this.treeIndex.countChildrenMany(dirs.map(keyOf));
635
+ for (const e of dirs) e.meta.childrenCount = counts.get(keyOf(e)) ?? 0;
636
+ return;
637
+ }
638
+ for (const e of dirs) e.meta.childrenCount = await this.treeIndex.countChildren(keyOf(e));
639
+ }
640
+ async listDeep(afsBasePath, prefix, limit, maxDepth, entries, includeContent = false) {
641
+ const prefixDepth = afsBasePath === "/" ? 0 : afsBasePath.split("/").filter(Boolean).length;
642
+ const seenDirs = /* @__PURE__ */ new Set();
643
+ let cursor;
644
+ do {
645
+ const result = await this.treeIndex.listEntries(prefix, {
646
+ limit: LIST_MAX_LIMIT,
647
+ cursor,
648
+ includeContent
649
+ });
650
+ for (const treeEntry of result.entries) {
651
+ if (entries.length >= limit) return;
652
+ const parts = treeEntry.path.slice(prefix.length).split("/").filter(Boolean);
653
+ for (let i = 0; i < parts.length - 1; i++) {
654
+ if (i + 1 > maxDepth) break;
655
+ const dirKey = prefix + parts.slice(0, i + 1).join("/");
656
+ if (!seenDirs.has(dirKey)) {
657
+ seenDirs.add(dirKey);
658
+ entries.push(this.buildEntry(`/${dirKey}`, {
659
+ id: basenameId(dirKey),
660
+ meta: {
661
+ kind: "did-space:directory",
662
+ childrenCount: -1
663
+ }
664
+ }));
665
+ }
666
+ }
667
+ const path = `/${treeEntry.path}`;
668
+ if (path.split("/").filter(Boolean).length - prefixDepth <= maxDepth) entries.push(this.buildEntry(path, {
669
+ id: basenameId(path),
670
+ ...includeContent && await this.verifiedInline(treeEntry) !== null ? { content: treeEntry.content } : {},
671
+ meta: {
672
+ kind: "did-space:file",
673
+ cid: treeEntry.cid,
674
+ size: treeEntry.size,
675
+ mimeType: treeEntry.mimeType
676
+ },
677
+ createdAt: new Date(treeEntry.ctime),
678
+ updatedAt: new Date(treeEntry.mtime)
679
+ }));
680
+ }
681
+ cursor = result.hasMore ? result.cursor : void 0;
682
+ } while (cursor && entries.length < limit);
683
+ }
684
+ /**
685
+ * Task 1.9: list changelog rows starting after `cursor` (seq number).
686
+ * Returns rows as AFSEntry objects + meta.cursor pointing to the last row's seq.
687
+ */
688
+ async listChangelog(cursor, limit) {
689
+ const sinceSeq = Number(cursor) || 0;
690
+ const effectiveLimit = Math.min(limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
691
+ const watermark = await this.treeIndex.getCompactedBelowSeq?.() ?? 0;
692
+ if (watermark > 0 && sinceSeq < watermark) throw new AFSCompactedError("/.changelog", String(watermark));
693
+ const changelog = await this.treeIndex.getChangelogSince(sinceSeq, effectiveLimit);
694
+ return {
695
+ data: changelog.entries.map((row) => this.buildEntry(`/.changelog/${row.seq}`, {
696
+ content: null,
697
+ meta: {
698
+ kind: CHANGELOG_ROW_KIND,
699
+ extra: serializeChangelogEntry(row)
700
+ }
701
+ })),
702
+ meta: {
703
+ cursor: String(changelog.cursor),
704
+ hasMore: changelog.hasMore
705
+ }
706
+ };
707
+ }
708
+ /**
709
+ * Promote write + delete to the object form so callers can discover the
710
+ * `ifMatch` sub-capability (CLAUDE.md "Sub-capability schema"). The base impl
711
+ * (base.ts) returns plain booleans; without this override the if-match
712
+ * conformance suite's `getCapabilityFeature("write","ifMatch")` reads nothing
713
+ * and the suite skips. Mirrors AFSFS's override (fs/src/index.ts).
714
+ */
715
+ getOperationsDeclaration() {
716
+ const ops = super.getOperationsDeclaration();
717
+ if (typeof ops.write === "boolean" ? ops.write : ops.write?.supported === true) ops.write = {
718
+ supported: true,
719
+ features: {
720
+ ifMatch: true,
721
+ treeMerge: true,
722
+ treeToggle: true,
723
+ treeIncrement: true,
724
+ treeListAdd: true,
725
+ treeListRemove: true,
726
+ batchPushdown: true
727
+ }
728
+ };
729
+ if (typeof ops.delete === "boolean" ? ops.delete : ops.delete?.supported === true) ops.delete = {
730
+ supported: true,
731
+ features: { ifMatch: true }
732
+ };
733
+ if (typeof ops.list === "boolean" ? ops.list : ops.list?.supported === true) ops.list = {
734
+ supported: true,
735
+ features: {
736
+ orderBy: true,
737
+ includeContent: true
738
+ }
739
+ };
740
+ return ops;
741
+ }
742
+ async readCapabilities(_ctx) {
743
+ const operations = this.getOperationsDeclaration();
744
+ const queryable = typeof this.treeIndex.query === "function";
745
+ if (queryable) operations.query = true;
746
+ return {
747
+ id: "/.meta/.capabilities",
748
+ path: "/.meta/.capabilities",
749
+ content: {
750
+ schemaVersion: 1,
751
+ provider: this.name,
752
+ description: this.description || "DID-scoped persistent storage with CID-based deduplication",
753
+ tools: [],
754
+ actions: [],
755
+ operations,
756
+ ...queryable ? { standardActions: [collectionQueryStandardAction({
757
+ doc: true,
758
+ groupBy: true,
759
+ annotate: true,
760
+ aggregate: true
761
+ })] } : {}
762
+ },
763
+ meta: {
764
+ kind: "afs:capabilities",
765
+ operations
766
+ }
767
+ };
768
+ }
769
+ /**
770
+ * Typed collection query over a directory of records (design §3.5).
771
+ *
772
+ * - dir-per-record (`spec.doc`): direct child dirs joined with their
773
+ * `<dir>/<doc>` document; file-per-record (no doc): direct child files'
774
+ * inline content is the record.
775
+ * - where/orderBy/pagination/total/groupBy execute inside the TreeIndex
776
+ * engine (SQL or in-memory — lockstep-tested). select + annotate.memberOf
777
+ * are page-level post-processing here, identical across backends.
778
+ * - Strict contract: spec arrives core-validated; provider constraints
779
+ * (single orderBy key, no text) throw AFSValidationError. An index
780
+ * without the engine (e.g. not-yet-migrated backends) throws
781
+ * AFSUnsupportedError — and is never declared in capabilities.
782
+ */
783
+ async query(path, spec, _options) {
784
+ if (!this.treeIndex.query) throw new AFSUnsupportedError("query", "Collection query is not supported by this index backend");
785
+ const afsPath = joinURL("/", path);
786
+ if (this.isVirtualPath(afsPath) || matchSharesVirtualPath(afsPath)) throw new AFSValidationError(`Invalid query spec: path: not a queryable collection: ${afsPath}`);
787
+ assertTreeQuerySpec(spec);
788
+ const prefix = this.treeListPrefix(afsPath);
789
+ if (afsPath !== "/") {
790
+ if (!await this.treeIndex.get(this.treePath(afsPath), { includeContent: false }) && !await this.treeIndex.hasChildren(prefix)) throw new AFSNotFoundError(afsPath);
791
+ }
792
+ const result = await this.treeIndex.query(prefix, spec);
793
+ let memberValues;
794
+ if (spec.annotate) {
795
+ memberValues = /* @__PURE__ */ new Set();
796
+ try {
797
+ const ref = await this.read(joinURL("/", spec.annotate.memberOf.doc));
798
+ const raw = resolveFieldPath(parseRecordText(typeof ref.data?.content === "string" ? ref.data.content : void 0), spec.annotate.memberOf.field);
799
+ if (Array.isArray(raw)) for (const v of raw) memberValues.add(typeof v === "string" ? v.toLowerCase() : String(v));
800
+ } catch {}
801
+ }
802
+ const entries = [];
803
+ for (const row of result.rows) entries.push(await this.queryRowToEntry(row, spec, memberValues));
804
+ const out = {
805
+ entries,
806
+ total: result.total
807
+ };
808
+ if (result.cursor) out.cursor = result.cursor;
809
+ if (result.groups) out.groups = result.groups;
810
+ if (result.aggregateGroups) out.aggregateGroups = result.aggregateGroups;
811
+ if (result.seekOffset !== void 0) out.seekOffset = result.seekOffset;
812
+ if (this.treeIndex.getPrefixWatermark) {
813
+ const version = await this.treeIndex.getPrefixWatermark(prefix);
814
+ if (version != null) out.liveSeq = String(version);
815
+ }
816
+ return out;
817
+ }
818
+ /** Map an engine row to an AFSEntry (mirrors the list handlers' shapes). */
819
+ async queryRowToEntry(row, spec, memberValues) {
820
+ const isDir = row.kind === "dir";
821
+ const displayPath = isDir ? `/${row.path.slice(0, -1)}` : `/${row.path}`;
822
+ const verifyCid = isDir ? row.contentCid : row.cid;
823
+ const verified = row.content !== void 0 && verifyCid ? await this.verifiedInline({
824
+ cid: verifyCid,
825
+ content: row.content
826
+ }) : null;
827
+ let content;
828
+ if (spec.select) content = projectRecordFields(parseRecordText(verified ?? void 0), spec.select);
829
+ else if (verified !== null) content = verified;
830
+ const entry = this.buildEntry(displayPath, {
831
+ id: basenameId(displayPath),
832
+ ...content !== void 0 ? { content } : {},
833
+ meta: isDir ? {
834
+ ...row.metadata ?? {},
835
+ kind: "did-space:directory",
836
+ childrenCount: -1,
837
+ size: row.size
838
+ } : {
839
+ ...row.metadata ?? {},
840
+ kind: "did-space:file",
841
+ cid: row.cid,
842
+ size: row.size,
843
+ mimeType: row.mimeType
844
+ },
845
+ createdAt: new Date(row.ctime),
846
+ updatedAt: new Date(row.mtime)
847
+ });
848
+ if (spec.annotate && memberValues) {
849
+ const { by, as } = spec.annotate.memberOf;
850
+ const byValue = by === "$id" ? row.name : resolveFieldPath(parseRecordText(verified ?? row.content), by);
851
+ const member = byValue != null && memberValues.has(typeof byValue === "string" ? byValue.toLowerCase() : String(byValue));
852
+ entry._derived = { [as]: member ? "1" : "" };
853
+ }
854
+ return entry;
855
+ }
856
+ async readHandler(ctx) {
857
+ const afsPath = joinURL("/", ctx.params.path ?? "");
858
+ const sharesMatch = matchSharesVirtualPath(afsPath);
859
+ if (sharesMatch && this.shareResolver && !this.shareTreePrefix) {
860
+ const result = await new ShareVirtualHandler(this.shareResolver, this.did).read(sharesMatch);
861
+ if (!result) throw new AFSNotFoundError(afsPath);
862
+ return result.data;
863
+ }
864
+ const normPath = afsPath.startsWith("/") ? afsPath.slice(1) : afsPath;
865
+ if (normPath.startsWith(".objects/")) {
866
+ const cid = normPath.slice(9);
867
+ if (!await time("ds.ref", () => this.treeIndex.isReferenced(cid))) throw new AFSNotFoundError(afsPath);
868
+ const obj = await time("ds.obj", () => this.objectStore.get(this.objectKey(cid)));
869
+ if (!obj) throw new AFSNotFoundError(afsPath);
870
+ const bytes = new Uint8Array(await time("ds.body", () => obj.arrayBuffer()));
871
+ return this.buildEntry(afsPath, {
872
+ content: bytes,
873
+ meta: {
874
+ kind: "did-space:object",
875
+ cid,
876
+ size: bytes.length
877
+ }
878
+ });
879
+ }
880
+ if (afsPath === "/") {
881
+ const childrenCount = await this.treeIndex.countChildren(this.treeListPrefix("/"));
882
+ return this.buildEntry(afsPath, {
883
+ content: `DID Space root for ${this.did} (${childrenCount} entries)`,
884
+ meta: {
885
+ kind: "did-space:root",
886
+ childrenCount
887
+ }
888
+ });
889
+ }
890
+ const entry = await time("ds.idx", () => this.treeIndex.get(this.treePath(afsPath)));
891
+ if (entry) {
892
+ if (entry.content !== void 0 && isTextMime(entry.mimeType)) {
893
+ if (await time("ds.inl", async () => {
894
+ return await computeCID(entry.content) === entry.cid;
895
+ })) return this.buildEntry(afsPath, {
896
+ content: entry.content,
897
+ meta: {
898
+ ...entry.metadata ?? {},
899
+ kind: "did-space:file",
900
+ size: entry.size,
901
+ cid: entry.cid,
902
+ mimeType: entry.mimeType,
903
+ version: entry.cid
904
+ },
905
+ createdAt: new Date(entry.ctime),
906
+ updatedAt: new Date(entry.mtime)
907
+ });
908
+ log.warn(`[did-space] inline content failed cid verification at ${afsPath} (cid=${entry.cid}) — serving from objects; row will self-repair on next write`);
909
+ }
910
+ const obj = await time("ds.obj", () => this.objectStore.get(this.objectKey(entry.cid)));
911
+ const content = await time("ds.body", async () => obj ? isTextMime(entry.mimeType) ? await obj.text() : await obj.arrayBuffer() : isTextMime(entry.mimeType) ? "" : /* @__PURE__ */ new ArrayBuffer(0));
912
+ return this.buildEntry(afsPath, {
913
+ content,
914
+ meta: {
915
+ ...entry.metadata ?? {},
916
+ kind: "did-space:file",
917
+ size: entry.size,
918
+ cid: entry.cid,
919
+ mimeType: entry.mimeType,
920
+ version: entry.cid
921
+ },
922
+ createdAt: new Date(entry.ctime),
923
+ updatedAt: new Date(entry.mtime)
924
+ });
925
+ }
926
+ if (await this.isDirectory(afsPath)) {
927
+ const childrenCount = await this.treeIndex.countChildren(this.treeListPrefix(afsPath));
928
+ return this.buildEntry(afsPath, {
929
+ content: `Directory: ${afsPath} (${childrenCount} entries)`,
930
+ meta: {
931
+ kind: "did-space:directory",
932
+ childrenCount
933
+ }
934
+ });
935
+ }
936
+ throw new AFSNotFoundError(afsPath);
937
+ }
938
+ /**
939
+ * presignRead — path-keyed read-grant capability (preview-r2-direct §3).
940
+ *
941
+ * Resolves `path` in THIS provider's (prefix-isolated) view exactly like
942
+ * `read`/`stat` (`treeIndex.get(treePath(path))`, incl. the same null-byte /
943
+ * `..`-segment guards). iff the resolved entry has a cid AND a signer with
944
+ * `presignGet` is present → mint a short-TTL presigned GET with the
945
+ * Content-Type baked in, and return `{ url, expiresAt }`.
946
+ *
947
+ * The path resolution IS the authorization: a caller can only presign what
948
+ * `read` would already return. Prefix isolation is inherited from the view's
949
+ * `PrefixedTreeIndex` — no separate object-level `isReferenced(cid)` is needed
950
+ * (that primitive is for the cid-keyed `.objects/{cid}` path, which has no
951
+ * tree path). NEVER throws to the caller: no signer (Node) / non-CAS / no cid /
952
+ * unresolved / any internal error → `null`, and the raw handler then falls back
953
+ * to the worker serve path. TTL = `DOWNLOAD_GRANT_TTL_SEC` (2h) — never the 7d
954
+ * signer cap (§8.3).
955
+ */
956
+ async presignRead(path, opts) {
957
+ try {
958
+ const entry = await this.treeIndex.get(this.treePath(path), { includeContent: false });
959
+ if (!entry?.cid) return null;
960
+ if (this.signer == null || typeof this.signer.presignGet !== "function") return null;
961
+ const g = await this.signer.presignGet({
962
+ objectKey: this.objectKey(entry.cid),
963
+ expiresInSec: DOWNLOAD_GRANT_TTL_SEC,
964
+ responseContentType: opts?.responseContentType
965
+ });
966
+ return {
967
+ url: g.url,
968
+ expiresAt: g.expiresAt
969
+ };
970
+ } catch {
971
+ return null;
972
+ }
973
+ }
974
+ /**
975
+ * Re-verify an entry's inline copy against its cid (§2.I list contract).
976
+ * Returns the content when verified, null otherwise (best-effort miss).
977
+ */
978
+ async verifiedInline(entry) {
979
+ if (entry.content === void 0) return null;
980
+ return await computeCID(entry.content) === entry.cid ? entry.content : null;
981
+ }
982
+ /**
983
+ * Returns true if the given AFS path is a virtual D4-protected path
984
+ * (.objects/* or .changelog/*).
985
+ */
986
+ isVirtualPath(afsPath) {
987
+ const norm = afsPath.startsWith("/") ? afsPath.slice(1) : afsPath;
988
+ return norm === ".objects" || norm.startsWith(".objects/") || norm === ".changelog" || norm.startsWith(".changelog/");
989
+ }
990
+ async writeHandler(ctx, payload) {
991
+ const afsPath = joinURL("/", ctx.params.path ?? "");
992
+ if (this.isVirtualPath(afsPath)) throw new AFSForbiddenError(afsPath, "virtual_path");
993
+ if (isShareReservedPath(afsPath)) throw new AFSForbiddenError(afsPath, "share-path-reserved");
994
+ const writeIfMatch = ctx.options?.ifMatch;
995
+ if (writeIfMatch !== void 0) {
996
+ const current = await this.treeIndex.get(this.treePath(afsPath), { includeContent: false });
997
+ if (ifMatchPreconditionFailed(writeIfMatch, current)) throw new AFSConflictError(afsPath, {
998
+ cid: current?.cid ?? null,
999
+ mtime: current?.mtime ?? null
1000
+ });
1001
+ }
1002
+ if (payload.content === void 0 && payload.meta !== void 0) return { data: this.buildEntry(afsPath, { meta: {
1003
+ kind: "did-space:file",
1004
+ ...payload.meta
1005
+ } }) };
1006
+ let bytes;
1007
+ let storeValue;
1008
+ if (payload.content instanceof Uint8Array) {
1009
+ bytes = payload.content;
1010
+ storeValue = bytes.buffer;
1011
+ } else if (payload.content instanceof ArrayBuffer) {
1012
+ bytes = new Uint8Array(payload.content);
1013
+ storeValue = payload.content;
1014
+ } else {
1015
+ const text = typeof payload.content === "string" ? payload.content : JSON.stringify(payload.content);
1016
+ bytes = new TextEncoder().encode(text);
1017
+ storeValue = text;
1018
+ }
1019
+ const cid = await computeCID(bytes);
1020
+ const treePath = this.treePath(afsPath);
1021
+ await applyShareWriteHook({
1022
+ ...this.shareHookArgs(afsPath, treePath),
1023
+ bytes,
1024
+ cid,
1025
+ resolver: this.shareResolver,
1026
+ phase: "pre-io"
1027
+ });
1028
+ if (!await this.objectStore.head(this.objectKey(cid))) await this.objectStore.put(this.objectKey(cid), storeValue);
1029
+ const writeMeta = ctx.options?.meta;
1030
+ const writeDeviceId = typeof writeMeta?.deviceId === "string" && writeMeta.deviceId ? writeMeta.deviceId : this.deviceId;
1031
+ const now = typeof writeMeta?.mtime === "number" ? writeMeta.mtime : Date.now();
1032
+ let mimeType = payload.meta?.mimeType ?? detectMimeType(afsPath) ?? void 0;
1033
+ if (!mimeType && (payload.content instanceof Uint8Array || payload.content instanceof ArrayBuffer)) mimeType = "application/octet-stream";
1034
+ const ctime = resolveCtime(payload.meta, now);
1035
+ const treeEntry = {
1036
+ cid,
1037
+ size: bytes.length,
1038
+ mtime: now,
1039
+ ctime,
1040
+ mimeType
1041
+ };
1042
+ const appMeta = extractApplicationMeta(payload.meta);
1043
+ if (appMeta) {
1044
+ if (typeof appMeta.dedupScope === "string") {
1045
+ treeEntry.dedupScope = appMeta.dedupScope;
1046
+ delete appMeta.dedupScope;
1047
+ }
1048
+ if (typeof appMeta.dedupKey === "string") treeEntry.dedupKey = appMeta.dedupKey;
1049
+ if (Object.keys(appMeta).length > 0) treeEntry.metadata = appMeta;
1050
+ }
1051
+ if (typeof storeValue === "string") treeEntry.content = storeValue;
1052
+ else {
1053
+ const inlineText = decodeInlineText(bytes, mimeType);
1054
+ if (inlineText !== null) treeEntry.content = inlineText;
1055
+ }
1056
+ const { oldCid } = await this.treeIndex.put(treePath, treeEntry, { deviceId: writeDeviceId });
1057
+ if (this.gcQueue) {
1058
+ await this.gcQueue.remove(cid);
1059
+ if (oldCid && oldCid !== cid) await this.gcQueue.enqueue([oldCid]);
1060
+ }
1061
+ await applyShareWriteHook({
1062
+ ...this.shareHookArgs(afsPath, treePath),
1063
+ bytes,
1064
+ cid,
1065
+ resolver: this.shareResolver,
1066
+ phase: "post-io"
1067
+ });
1068
+ await applyBlockletWriteHook({
1069
+ ...this.blockletHookArgs(afsPath, treePath),
1070
+ bytes,
1071
+ resolver: this.blockletResolver
1072
+ });
1073
+ if (afsPath.endsWith(".ash")) this.emit({
1074
+ type: SCRIPT_EVENT_TYPES.REGISTERED,
1075
+ path: afsPath,
1076
+ data: { runtime: "ash" }
1077
+ });
1078
+ const meta = {
1079
+ ...appMeta ?? {},
1080
+ kind: "did-space:file",
1081
+ size: treeEntry.size,
1082
+ cid,
1083
+ mimeType: treeEntry.mimeType
1084
+ };
1085
+ if (this.treeIndex.getPrefixWatermark) {
1086
+ const version = await this.treeIndex.getPrefixWatermark(this.treePath(afsPath));
1087
+ if (version != null) meta.liveSeq = String(version);
1088
+ }
1089
+ return { data: this.buildEntry(afsPath, {
1090
+ content: storeValue,
1091
+ meta,
1092
+ createdAt: new Date(treeEntry.ctime),
1093
+ updatedAt: new Date(treeEntry.mtime)
1094
+ }) };
1095
+ }
1096
+ async deleteHandler(ctx) {
1097
+ const afsPath = joinURL("/", ctx.params.path ?? "");
1098
+ if (this.isVirtualPath(afsPath)) throw new AFSForbiddenError(afsPath, "virtual_path");
1099
+ if (isShareReservedPath(afsPath)) throw new AFSForbiddenError(afsPath, "share-path-reserved");
1100
+ const treePath = this.treePath(afsPath);
1101
+ const deleteMeta = ctx.options?.meta;
1102
+ const deleteIfMatch = deleteMeta?.ifMatch;
1103
+ if (deleteIfMatch !== void 0) {
1104
+ const current = await this.treeIndex.get(treePath, { includeContent: false });
1105
+ if (!current || current.cid !== deleteIfMatch) throw new AFSConflictError(afsPath, {
1106
+ cid: current?.cid ?? null,
1107
+ mtime: current?.mtime ?? null
1108
+ });
1109
+ }
1110
+ const deleteOpts = {
1111
+ deviceId: typeof deleteMeta?.deviceId === "string" && deleteMeta.deviceId ? deleteMeta.deviceId : this.deviceId,
1112
+ ...typeof deleteMeta?.mtime === "number" ? { mtime: deleteMeta.mtime } : {}
1113
+ };
1114
+ const entry = await this.treeIndex.get(treePath, { includeContent: false });
1115
+ if (entry) {
1116
+ await this.gcQueue?.enqueue([entry.cid]);
1117
+ await this.treeIndex.delete(treePath, deleteOpts);
1118
+ await applyShareDeleteHook({
1119
+ ...this.shareHookArgs(afsPath, treePath),
1120
+ resolver: this.shareResolver
1121
+ });
1122
+ await applyBlockletDeleteHook({
1123
+ ...this.blockletHookArgs(afsPath, treePath),
1124
+ resolver: this.blockletResolver
1125
+ });
1126
+ if (afsPath.endsWith(".ash")) this.emit({
1127
+ type: SCRIPT_EVENT_TYPES.UNREGISTERED,
1128
+ path: afsPath,
1129
+ data: { runtime: "ash" }
1130
+ });
1131
+ return {
1132
+ path: afsPath,
1133
+ deleted: true
1134
+ };
1135
+ }
1136
+ const prefix = this.treeListPrefix(afsPath);
1137
+ if (this.gcQueue) {
1138
+ const cids = await this.collectCIDsByPrefix(prefix);
1139
+ if (cids.length > 0) await this.gcQueue.enqueue(cids);
1140
+ }
1141
+ if (await this.treeIndex.deleteByPrefix(prefix, deleteOpts) === 0) throw new AFSNotFoundError(afsPath);
1142
+ return {
1143
+ path: afsPath,
1144
+ deleted: true
1145
+ };
1146
+ }
1147
+ /** Conflict failure shaped exactly like the single-op AFSConflictError surface. */
1148
+ batchConflictFailure(afsPath, conflict) {
1149
+ return {
1150
+ path: afsPath,
1151
+ success: false,
1152
+ error: `Write conflict at ${afsPath}`,
1153
+ code: "AFS_CONFLICT",
1154
+ conflict
1155
+ };
1156
+ }
1157
+ /** ONE preflight read for a batch — getMany when the index has it, per-path get otherwise. */
1158
+ async batchPreread(treePaths) {
1159
+ if (this.treeIndex.getMany) return this.treeIndex.getMany(treePaths);
1160
+ return new Map(await Promise.all(treePaths.map(async (tp) => [tp, await this.treeIndex.get(tp)])));
1161
+ }
1162
+ /** Run `fn` over `items` with bounded concurrency (Workers cap: 6 sockets). */
1163
+ async boundedAll(items, limit, fn) {
1164
+ let next = 0;
1165
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
1166
+ while (next < items.length) {
1167
+ const item = items[next++];
1168
+ await fn(item);
1169
+ }
1170
+ });
1171
+ await Promise.all(workers);
1172
+ }
1173
+ /**
1174
+ * Native batch read (issue #808 follow-up) — the render fast path. Resolves the
1175
+ * whole path set with ONE `treeIndex.getMany` (chunked `IN(…)`, which carries
1176
+ * inline content when the index has it), so small cid-verified inline files are
1177
+ * served with ZERO R2 gets; only the (few, large) non-inline entries fall back
1178
+ * to bounded-concurrent `objectStore.get`. Mirrors the single-read handler's
1179
+ * inline-verify → objects cascade per entry. File-oriented: getMany matches
1180
+ * `kind='file'`, so directory / absent paths come back AFS_NOT_FOUND (callers
1181
+ * reading directories use `list`/`read`).
1182
+ */
1183
+ async batchRead(entries, _options) {
1184
+ const afsPaths = entries.map((e) => joinURL("/", e.path));
1185
+ const treePaths = afsPaths.map((p) => this.treePath(p));
1186
+ const preread = await this.batchPreread(treePaths);
1187
+ const results = new Array(entries.length);
1188
+ const fileEntry = (afsPath, entry, content) => this.buildEntry(afsPath, {
1189
+ content,
1190
+ meta: {
1191
+ ...entry.metadata ?? {},
1192
+ kind: "did-space:file",
1193
+ size: entry.size,
1194
+ cid: entry.cid,
1195
+ mimeType: entry.mimeType,
1196
+ version: entry.cid
1197
+ },
1198
+ createdAt: new Date(entry.ctime),
1199
+ updatedAt: new Date(entry.mtime)
1200
+ });
1201
+ const needObject = [];
1202
+ for (let i = 0; i < entries.length; i++) {
1203
+ const entry = preread.get(treePaths[i]);
1204
+ if (!entry) {
1205
+ results[i] = {
1206
+ path: afsPaths[i],
1207
+ success: false,
1208
+ error: `Path not found: ${afsPaths[i]}`,
1209
+ code: "AFS_NOT_FOUND"
1210
+ };
1211
+ continue;
1212
+ }
1213
+ if (entry.content !== void 0 && isTextMime(entry.mimeType)) {
1214
+ if (await computeCID(entry.content) === entry.cid) {
1215
+ results[i] = {
1216
+ path: afsPaths[i],
1217
+ success: true,
1218
+ data: fileEntry(afsPaths[i], entry, entry.content)
1219
+ };
1220
+ continue;
1221
+ }
1222
+ log.warn(`[did-space] inline content failed cid verification at ${afsPaths[i]} (cid=${entry.cid}) — serving from objects; row will self-repair on next write`);
1223
+ }
1224
+ needObject.push(i);
1225
+ }
1226
+ await this.boundedAll(needObject, 6, async (i) => {
1227
+ const entry = preread.get(treePaths[i]);
1228
+ const obj = await this.objectStore.get(this.objectKey(entry.cid));
1229
+ const isText = isTextMime(entry.mimeType);
1230
+ const content = obj ? isText ? await obj.text() : await obj.arrayBuffer() : isText ? "" : /* @__PURE__ */ new ArrayBuffer(0);
1231
+ results[i] = {
1232
+ path: afsPaths[i],
1233
+ success: true,
1234
+ data: fileEntry(afsPaths[i], entry, content)
1235
+ };
1236
+ });
1237
+ const succeeded = results.filter((r) => r.success).length;
1238
+ return {
1239
+ results,
1240
+ succeeded,
1241
+ failed: entries.length - succeeded
1242
+ };
1243
+ }
1244
+ /**
1245
+ * Native batch write — the CF-subrequest-budget merge (§4.3): eight-step
1246
+ * order aligned point-for-point with the single write path (preread BEFORE
1247
+ * share pre-io, so a stale ifMatch on a `.share` declaration yields 409,
1248
+ * never a share error):
1249
+ * 1 shape/decode/computeCID → 2 ONE preread (conflict/skip exit here —
1250
+ * no share hook, no R2 upload) → 3 share pre-io (survivors) → 4 in-batch
1251
+ * CID dedup → 5 unconditional R2 put (unique cids, bounded ≤6 —
1252
+ * P0-measured wall-time) → 6 treeIndex.batchPutEntries (single db.batch)
1253
+ * → 7 gcQueue removeMany/enqueue → 8 share post-io + per-entry results.
1254
+ */
1255
+ async batchWrite(entries, _options) {
1256
+ if (entries.some((e) => e.options?.dedupBy || e.options?.dedupKey)) {
1257
+ const seq = [];
1258
+ let succeeded$1 = 0;
1259
+ for (const entry of entries) {
1260
+ const afsPath = joinURL("/", entry.path);
1261
+ try {
1262
+ const payload = entry.content;
1263
+ if (!payload || payload.content === void 0) throw new AFSValidationError("Batch write requires entry content");
1264
+ const opts = {
1265
+ ...entry.options ?? {},
1266
+ ...entry.mode ? { mode: entry.mode } : {}
1267
+ };
1268
+ const res = await this.write(entry.path, payload, opts);
1269
+ seq.push({
1270
+ path: res.data.path,
1271
+ success: true,
1272
+ data: res.data
1273
+ });
1274
+ succeeded$1++;
1275
+ } catch (err) {
1276
+ seq.push({
1277
+ path: afsPath,
1278
+ success: false,
1279
+ error: err instanceof Error ? err.message : String(err),
1280
+ code: err instanceof AFSConflictError ? "AFS_CONFLICT" : err?.code ?? "AFS_WRITE_ERROR"
1281
+ });
1282
+ }
1283
+ }
1284
+ return {
1285
+ results: seq,
1286
+ succeeded: succeeded$1,
1287
+ failed: entries.length - succeeded$1
1288
+ };
1289
+ }
1290
+ const results = new Array(entries.length);
1291
+ const prepared = [];
1292
+ for (let i = 0; i < entries.length; i++) {
1293
+ const entry = entries[i];
1294
+ const afsPath = joinURL("/", entry.path);
1295
+ if (entry.patches || entry.mode && entry.mode !== "replace") {
1296
+ results[i] = {
1297
+ path: afsPath,
1298
+ success: false,
1299
+ error: `Batch write supports replace-mode content writes only (got mode '${entry.mode ?? "patches"}')`,
1300
+ code: "AFS_UNSUPPORTED"
1301
+ };
1302
+ continue;
1303
+ }
1304
+ if (this.isVirtualPath(afsPath) || isShareReservedPath(afsPath)) {
1305
+ results[i] = {
1306
+ path: afsPath,
1307
+ success: false,
1308
+ error: `Forbidden at ${afsPath}: virtual_path`,
1309
+ code: "AFS_FORBIDDEN"
1310
+ };
1311
+ continue;
1312
+ }
1313
+ const payload = entry.content ?? {};
1314
+ if (payload.content === void 0) {
1315
+ results[i] = {
1316
+ path: afsPath,
1317
+ success: false,
1318
+ error: "Batch write requires entry content (meta-only writes are single-op)",
1319
+ code: "AFS_VALIDATION_ERROR"
1320
+ };
1321
+ continue;
1322
+ }
1323
+ let bytes;
1324
+ let storeValue;
1325
+ if (payload.content instanceof Uint8Array) {
1326
+ bytes = payload.content;
1327
+ storeValue = bytes.buffer;
1328
+ } else if (payload.content instanceof ArrayBuffer) {
1329
+ bytes = new Uint8Array(payload.content);
1330
+ storeValue = payload.content;
1331
+ } else {
1332
+ const text = typeof payload.content === "string" ? payload.content : JSON.stringify(payload.content);
1333
+ bytes = new TextEncoder().encode(text);
1334
+ storeValue = text;
1335
+ }
1336
+ const cid = await computeCID(bytes);
1337
+ const writeOptions = entry.options;
1338
+ const writeMeta = writeOptions?.meta;
1339
+ const deviceId = typeof writeMeta?.deviceId === "string" && writeMeta.deviceId ? writeMeta.deviceId : this.deviceId;
1340
+ const now = typeof writeMeta?.mtime === "number" ? writeMeta.mtime : Date.now();
1341
+ let mimeType = payload.meta?.mimeType ?? detectMimeType(afsPath) ?? void 0;
1342
+ if (!mimeType && (payload.content instanceof Uint8Array || payload.content instanceof ArrayBuffer)) mimeType = "application/octet-stream";
1343
+ const ctime = resolveCtime(payload.meta, now);
1344
+ const treeEntry = {
1345
+ cid,
1346
+ size: bytes.length,
1347
+ mtime: now,
1348
+ ctime,
1349
+ mimeType
1350
+ };
1351
+ const appMeta = extractApplicationMeta(payload.meta);
1352
+ if (appMeta) {
1353
+ try {
1354
+ assertMetadataSize(appMeta, afsPath);
1355
+ } catch (err) {
1356
+ results[i] = {
1357
+ path: afsPath,
1358
+ success: false,
1359
+ error: err instanceof Error ? err.message : String(err),
1360
+ code: err.code ?? "AFS_VALIDATION_ERROR"
1361
+ };
1362
+ continue;
1363
+ }
1364
+ treeEntry.metadata = appMeta;
1365
+ }
1366
+ if (typeof storeValue === "string") treeEntry.content = storeValue;
1367
+ else {
1368
+ const inlineText = decodeInlineText(bytes, mimeType);
1369
+ if (inlineText !== null) treeEntry.content = inlineText;
1370
+ }
1371
+ prepared.push({
1372
+ i,
1373
+ afsPath,
1374
+ treePath: this.treePath(afsPath),
1375
+ bytes,
1376
+ storeValue,
1377
+ cid,
1378
+ treeEntry,
1379
+ deviceId,
1380
+ ifMatch: writeOptions?.ifMatch
1381
+ });
1382
+ }
1383
+ const preread = await this.batchPreread(prepared.map((p) => p.treePath));
1384
+ const skipped = [];
1385
+ let live = [];
1386
+ for (const p of prepared) {
1387
+ const existing = preread.get(p.treePath) ?? null;
1388
+ if (ifMatchPreconditionFailed(p.ifMatch, existing)) {
1389
+ results[p.i] = this.batchConflictFailure(p.afsPath, {
1390
+ currentCid: existing?.cid ?? null,
1391
+ currentMtime: existing?.mtime ?? null,
1392
+ currentDeviceId: null
1393
+ });
1394
+ continue;
1395
+ }
1396
+ live.push(p);
1397
+ }
1398
+ const preIoOk = [];
1399
+ for (const p of live) try {
1400
+ await applyShareWriteHook({
1401
+ ...this.shareHookArgs(p.afsPath, p.treePath),
1402
+ bytes: p.bytes,
1403
+ cid: p.cid,
1404
+ resolver: this.shareResolver,
1405
+ phase: "pre-io"
1406
+ });
1407
+ preIoOk.push(p);
1408
+ } catch (err) {
1409
+ results[p.i] = {
1410
+ path: p.afsPath,
1411
+ success: false,
1412
+ error: err instanceof Error ? err.message : String(err),
1413
+ code: err.code
1414
+ };
1415
+ }
1416
+ live = preIoOk;
1417
+ const uniqueCids = /* @__PURE__ */ new Map();
1418
+ for (const p of live) if (!uniqueCids.has(p.cid)) uniqueCids.set(p.cid, p);
1419
+ const uploadFailed = /* @__PURE__ */ new Map();
1420
+ const uploaded = [];
1421
+ await this.boundedAll([...uniqueCids.values()], 6, async (p) => {
1422
+ try {
1423
+ await this.objectStore.put(this.objectKey(p.cid), p.storeValue);
1424
+ uploaded.push(p.cid);
1425
+ } catch (err) {
1426
+ uploadFailed.set(p.cid, err instanceof Error ? err.message : String(err));
1427
+ }
1428
+ });
1429
+ if (uploadFailed.size > 0) {
1430
+ const stillLive = [];
1431
+ for (const p of live) {
1432
+ const failure = uploadFailed.get(p.cid);
1433
+ if (failure !== void 0) results[p.i] = {
1434
+ path: p.afsPath,
1435
+ success: false,
1436
+ error: `R2 put failed: ${failure}`
1437
+ };
1438
+ else stillLive.push(p);
1439
+ }
1440
+ live = stillLive;
1441
+ }
1442
+ let indexResults = [];
1443
+ if (live.length > 0) {
1444
+ const batchEntries = live.map((p) => ({
1445
+ path: p.treePath,
1446
+ entry: p.treeEntry,
1447
+ opts: { deviceId: p.deviceId }
1448
+ }));
1449
+ try {
1450
+ indexResults = this.treeIndex.batchPutEntries ? await this.treeIndex.batchPutEntries(batchEntries, { preread }) : await loopBatchPutEntries(this.treeIndex, batchEntries, { preread });
1451
+ } catch (err) {
1452
+ await this.gcQueue?.enqueue(uploaded);
1453
+ const message = err instanceof Error ? err.message : String(err);
1454
+ for (const p of live) results[p.i] = {
1455
+ path: p.afsPath,
1456
+ success: false,
1457
+ error: message
1458
+ };
1459
+ live = [];
1460
+ }
1461
+ }
1462
+ const written = [];
1463
+ live.forEach((p, j) => {
1464
+ const r = indexResults[j];
1465
+ if (!r) {
1466
+ results[p.i] = {
1467
+ path: p.afsPath,
1468
+ success: false,
1469
+ error: "index batch returned no result"
1470
+ };
1471
+ return;
1472
+ }
1473
+ if (r.conflict) {
1474
+ results[p.i] = this.batchConflictFailure(p.afsPath, r.conflict);
1475
+ return;
1476
+ }
1477
+ if (r.error) {
1478
+ results[p.i] = {
1479
+ path: p.afsPath,
1480
+ success: false,
1481
+ error: r.error
1482
+ };
1483
+ return;
1484
+ }
1485
+ if (r.skipped) {
1486
+ skipped.push(p);
1487
+ return;
1488
+ }
1489
+ written.push({
1490
+ p,
1491
+ oldCid: r.oldCid,
1492
+ liveSeq: r.liveSeq
1493
+ });
1494
+ });
1495
+ if (this.gcQueue) {
1496
+ const reReferenced = [...new Set([...written.map((w) => w.p.cid), ...skipped.map((s) => s.cid)])];
1497
+ if (reReferenced.length > 0) if (this.gcQueue.removeMany) await this.gcQueue.removeMany(reReferenced);
1498
+ else for (const cid of reReferenced) await this.gcQueue.remove(cid);
1499
+ const replaced = [...new Set(written.filter((w) => w.oldCid && w.oldCid !== w.p.cid).map((w) => w.oldCid))];
1500
+ if (replaced.length > 0) await this.gcQueue.enqueue(replaced);
1501
+ }
1502
+ for (const w of written) {
1503
+ try {
1504
+ await applyShareWriteHook({
1505
+ ...this.shareHookArgs(w.p.afsPath, w.p.treePath),
1506
+ bytes: w.p.bytes,
1507
+ cid: w.p.cid,
1508
+ resolver: this.shareResolver,
1509
+ phase: "post-io"
1510
+ });
1511
+ } catch (err) {
1512
+ w.error = err instanceof Error ? err.message : String(err);
1513
+ }
1514
+ if (w.p.afsPath.endsWith(".ash")) this.emit({
1515
+ type: SCRIPT_EVENT_TYPES.REGISTERED,
1516
+ path: w.p.afsPath,
1517
+ data: { runtime: "ash" }
1518
+ });
1519
+ }
1520
+ const maxSeqByPrefix = /* @__PURE__ */ new Map();
1521
+ for (const w of written) {
1522
+ if (typeof w.liveSeq !== "number") continue;
1523
+ const wm = watermarkWritePrefix(w.p.treePath);
1524
+ if (wm === null) continue;
1525
+ maxSeqByPrefix.set(wm, Math.max(maxSeqByPrefix.get(wm) ?? 0, w.liveSeq));
1526
+ }
1527
+ for (const s of skipped) {
1528
+ const wm = watermarkWritePrefix(s.treePath);
1529
+ let liveSeq = wm !== null ? maxSeqByPrefix.get(wm) : void 0;
1530
+ if (liveSeq === void 0 && this.treeIndex.getPrefixWatermark) liveSeq = await this.treeIndex.getPrefixWatermark(s.treePath) ?? void 0;
1531
+ results[s.i] = {
1532
+ path: s.afsPath,
1533
+ success: true,
1534
+ data: this.buildBatchWriteData(s, liveSeq)
1535
+ };
1536
+ }
1537
+ for (const w of written) {
1538
+ if (w.error) {
1539
+ results[w.p.i] = {
1540
+ path: w.p.afsPath,
1541
+ success: false,
1542
+ error: w.error
1543
+ };
1544
+ continue;
1545
+ }
1546
+ results[w.p.i] = {
1547
+ path: w.p.afsPath,
1548
+ success: true,
1549
+ data: this.buildBatchWriteData(w.p, w.liveSeq)
1550
+ };
1551
+ }
1552
+ const succeeded = results.filter((r) => r?.success).length;
1553
+ return {
1554
+ results,
1555
+ succeeded,
1556
+ failed: results.length - succeeded
1557
+ };
1558
+ }
1559
+ /** Write-echo entry — field-for-field the single-op writeHandler result. */
1560
+ buildBatchWriteData(p, liveSeq) {
1561
+ const meta = {
1562
+ kind: "did-space:file",
1563
+ size: p.treeEntry.size,
1564
+ cid: p.cid,
1565
+ mimeType: p.treeEntry.mimeType
1566
+ };
1567
+ if (liveSeq != null) meta.liveSeq = String(liveSeq);
1568
+ return this.buildEntry(p.afsPath, {
1569
+ content: p.storeValue,
1570
+ meta,
1571
+ createdAt: new Date(p.treeEntry.ctime),
1572
+ updatedAt: new Date(p.treeEntry.mtime)
1573
+ });
1574
+ }
1575
+ /** Build a failed AFSExecResult (shared across the three action handlers). */
1576
+ uploadErr(code, message) {
1577
+ return {
1578
+ success: false,
1579
+ error: {
1580
+ code,
1581
+ message
1582
+ }
1583
+ };
1584
+ }
1585
+ /**
1586
+ * Session ownership isolation (§9 / §10): a session belongs to THIS view only
1587
+ * when its scope equals this provider's storage scope. A different-scope (or
1588
+ * unknown) session is treated as not-found — never confirm another user's
1589
+ * session exists, and never let one view commit into another.
1590
+ */
1591
+ ownsSession(session) {
1592
+ return !!session && session.scope === (this.uploadScope ?? this.did);
1593
+ }
1594
+ /**
1595
+ * Validate an action-supplied `path`. Unlike `@Write` handlers, action args
1596
+ * carry NO router-layer traversal protection — `args.entries[].path` is raw
1597
+ * client input, so we MUST normalize + reject traversal / reserved virtual
1598
+ * prefixes explicitly (design §10). Returns the tree path on success.
1599
+ */
1600
+ validateActionPath(afsPath) {
1601
+ const norm = afsPath.startsWith("/") ? afsPath.slice(1) : afsPath;
1602
+ if (this.isVirtualPath(afsPath) || isShareReservedPath(afsPath) || norm === ".actions" || norm.startsWith(".actions/")) return {
1603
+ ok: false,
1604
+ reason: `reserved path: ${afsPath}`
1605
+ };
1606
+ let treePath;
1607
+ try {
1608
+ treePath = this.treePath(afsPath);
1609
+ } catch (err) {
1610
+ return {
1611
+ ok: false,
1612
+ reason: err instanceof Error ? err.message : String(err)
1613
+ };
1614
+ }
1615
+ return {
1616
+ ok: true,
1617
+ treePath
1618
+ };
1619
+ }
1620
+ /**
1621
+ * Action catalogue — the upload-plane + download-plane actions, always listed
1622
+ * (protocol parity, T2.5). Each plane advertises its OWN capability flag:
1623
+ * `grantCapability` for prepare-upload, the INDEPENDENT `downloadGrantCapability`
1624
+ * for prepare-download (design §7.3 / Finding 4 — never reuse the upload flag,
1625
+ * so the two planes can be advertised separately).
1626
+ */
1627
+ listObjectPlaneActions(ctx) {
1628
+ const grantCapable = this.signer != null;
1629
+ const downloadGrantCapable = this.signer != null && typeof this.signer.presignGet === "function";
1630
+ return { data: [...UPLOAD_ACTIONS, ...DOWNLOAD_ACTIONS].map((name) => this.buildEntry(joinURL(ctx.path, name), {
1631
+ id: name,
1632
+ meta: {
1633
+ kind: "afs:executable",
1634
+ ...name === "prepare-upload" ? { grantCapability: grantCapable } : {},
1635
+ ...name === "prepare-download" ? { downloadGrantCapability: downloadGrantCapable } : {}
1636
+ }
1637
+ })) };
1638
+ }
1639
+ async execPrepareUpload(_ctx, args) {
1640
+ if (!this.uploadSessionStore) return this.uploadErr("AFS_UNSUPPORTED", "upload plane not available");
1641
+ const parsed = PrepareUploadArgsSchema.safeParse(args);
1642
+ if (!parsed.success) return this.uploadErr("AFS_VALIDATION_ERROR", zodMessage(parsed.error));
1643
+ const outEntries = [];
1644
+ const storeEntries = [];
1645
+ const seenPaths = /* @__PURE__ */ new Set();
1646
+ for (const e of parsed.data.entries) {
1647
+ const afsPath = joinURL("/", e.path);
1648
+ const pv = this.validateActionPath(afsPath);
1649
+ if (!pv.ok) return this.uploadErr("AFS_VALIDATION_ERROR", pv.reason);
1650
+ if (seenPaths.has(afsPath)) return this.uploadErr("AFS_VALIDATION_ERROR", `duplicate path: ${afsPath}`);
1651
+ seenPaths.add(afsPath);
1652
+ let checksum;
1653
+ try {
1654
+ checksum = cidToChecksumSha256(e.cid);
1655
+ } catch (err) {
1656
+ return this.uploadErr("AFS_VALIDATION_ERROR", `bad cid for ${afsPath}: ${err instanceof Error ? err.message : err}`);
1657
+ }
1658
+ const objectKey = this.objectKey(e.cid);
1659
+ let grant = null;
1660
+ if (this.signer) {
1661
+ const g = await this.signer.presignPut({
1662
+ objectKey,
1663
+ checksumSha256: checksum,
1664
+ expiresInSec: GRANT_EXPIRES_SEC,
1665
+ contentLength: e.size
1666
+ });
1667
+ grant = {
1668
+ kind: "presigned-put",
1669
+ url: g.url,
1670
+ method: g.method,
1671
+ signedHeaders: g.signedHeaders,
1672
+ expiresAt: g.expiresAt
1673
+ };
1674
+ }
1675
+ outEntries.push({
1676
+ path: afsPath,
1677
+ cid: e.cid,
1678
+ status: "needs-upload",
1679
+ objectKey,
1680
+ checksumSha256: checksum,
1681
+ grant
1682
+ });
1683
+ storeEntries.push({
1684
+ path: afsPath,
1685
+ cid: e.cid,
1686
+ objectKey,
1687
+ checksumSha256: checksum,
1688
+ size: e.size,
1689
+ mimeType: e.mimeType ?? null,
1690
+ ifMatch: e.ifMatch ?? null,
1691
+ deviceId: e.deviceId ?? null,
1692
+ mtime: e.mtime ?? null,
1693
+ status: "needs_upload"
1694
+ });
1695
+ }
1696
+ const sessionId = crypto.randomUUID();
1697
+ const expiresAt = Date.now() + UPLOAD_SESSION_TTL_MS;
1698
+ await this.uploadSessionStore.createSession({
1699
+ id: sessionId,
1700
+ scope: this.uploadScope ?? this.did,
1701
+ caller: null,
1702
+ expiresAt,
1703
+ entries: storeEntries
1704
+ });
1705
+ return {
1706
+ success: true,
1707
+ data: {
1708
+ sessionId,
1709
+ expiresAt: new Date(expiresAt).toISOString(),
1710
+ entries: outEntries
1711
+ }
1712
+ };
1713
+ }
1714
+ async execPrepareDownload(_ctx, args) {
1715
+ const parsed = PrepareDownloadArgsSchema.safeParse(args);
1716
+ if (!parsed.success) return this.uploadErr("AFS_VALIDATION_ERROR", zodMessage(parsed.error));
1717
+ const inputEntries = parsed.data.entries;
1718
+ const cidValid = /* @__PURE__ */ new Map();
1719
+ const validCids = [];
1720
+ for (const e of inputEntries) {
1721
+ if (cidValid.has(e.cid)) continue;
1722
+ let ok = true;
1723
+ try {
1724
+ cidToChecksumSha256(e.cid);
1725
+ } catch {
1726
+ ok = false;
1727
+ }
1728
+ cidValid.set(e.cid, ok);
1729
+ if (ok) validCids.push(e.cid);
1730
+ }
1731
+ let referenced = /* @__PURE__ */ new Set();
1732
+ if (validCids.length > 0) try {
1733
+ referenced = await this.treeIndex.isReferencedMany(validCids);
1734
+ } catch {
1735
+ return this.uploadErr("AFS_INTERNAL_ERROR", "prepare-download authorization unavailable");
1736
+ }
1737
+ const canSign = this.signer != null && typeof this.signer.presignGet === "function";
1738
+ const grantByCid = /* @__PURE__ */ new Map();
1739
+ for (const cid of referenced) if (canSign) {
1740
+ const g = await this.signer.presignGet({
1741
+ objectKey: this.objectKey(cid),
1742
+ expiresInSec: DOWNLOAD_GRANT_TTL_SEC
1743
+ });
1744
+ grantByCid.set(cid, {
1745
+ kind: "presigned-get",
1746
+ url: g.url,
1747
+ method: g.method,
1748
+ signedHeaders: g.signedHeaders,
1749
+ expiresAt: g.expiresAt
1750
+ });
1751
+ } else grantByCid.set(cid, null);
1752
+ return {
1753
+ success: true,
1754
+ data: { entries: inputEntries.map((e) => {
1755
+ if (!cidValid.get(e.cid)) return {
1756
+ cid: e.cid,
1757
+ status: "invalid"
1758
+ };
1759
+ if (!referenced.has(e.cid)) return {
1760
+ cid: e.cid,
1761
+ status: "forbidden"
1762
+ };
1763
+ return {
1764
+ cid: e.cid,
1765
+ status: "ok",
1766
+ objectKey: this.objectKey(e.cid),
1767
+ grant: grantByCid.get(e.cid) ?? null
1768
+ };
1769
+ }) }
1770
+ };
1771
+ }
1772
+ async execCommitUpload(_ctx, args) {
1773
+ if (!this.uploadSessionStore) return this.uploadErr("AFS_UNSUPPORTED", "upload plane not available");
1774
+ const parsed = CommitUploadArgsSchema.safeParse(args);
1775
+ if (!parsed.success) return this.uploadErr("AFS_VALIDATION_ERROR", zodMessage(parsed.error));
1776
+ const { sessionId, entries } = parsed.data;
1777
+ const session = await this.uploadSessionStore.getSession(sessionId);
1778
+ if (!this.ownsSession(session)) return this.uploadErr("AFS_NOT_FOUND", "unknown upload session");
1779
+ if (session.status === "aborted") return this.uploadErr("AFS_UNSUPPORTED", "session aborted");
1780
+ if (session.status === "prepared" && session.expiresAt <= Date.now()) return this.uploadErr("AFS_UNSUPPORTED", "session expired");
1781
+ const result = await this.commitSessionEntries(session.id, session.entries, entries);
1782
+ await this.uploadSessionStore.markCommitted(session.id);
1783
+ return {
1784
+ success: true,
1785
+ data: result
1786
+ };
1787
+ }
1788
+ async execAbortUpload(_ctx, args) {
1789
+ if (!this.uploadSessionStore) return this.uploadErr("AFS_UNSUPPORTED", "upload plane not available");
1790
+ const parsed = AbortUploadArgsSchema.safeParse(args);
1791
+ if (!parsed.success) return this.uploadErr("AFS_VALIDATION_ERROR", zodMessage(parsed.error));
1792
+ const session = await this.uploadSessionStore.getSession(parsed.data.sessionId);
1793
+ if (!this.ownsSession(session)) return this.uploadErr("AFS_NOT_FOUND", "unknown upload session");
1794
+ await this.uploadSessionStore.markAborted(session.id);
1795
+ if (this.gcQueue) {
1796
+ const orphanCids = [...new Set(session.entries.filter((e) => e.status !== "committed").map((e) => e.cid))];
1797
+ if (orphanCids.length > 0) await this.gcQueue.enqueue(orphanCids);
1798
+ }
1799
+ return {
1800
+ success: true,
1801
+ data: {
1802
+ sessionId: session.id,
1803
+ status: "aborted"
1804
+ }
1805
+ };
1806
+ }
1807
+ /**
1808
+ * Resolve the immutable-merge-key set: `createdAt` is always preserved, plus
1809
+ * any caller-supplied lifecycle fields (e.g. `status`).
1810
+ */
1811
+ resolveImmutableMergeKeys(extra) {
1812
+ const keys = new Set(["createdAt"]);
1813
+ for (const k of extra ?? []) keys.add(k);
1814
+ return keys;
1815
+ }
1816
+ /** Extract the JSON record object from a write payload's `content`. */
1817
+ parseRecordBody(content) {
1818
+ if (content !== null && typeof content === "object" && !Array.isArray(content) && !(content instanceof Uint8Array) && !(content instanceof ArrayBuffer)) return content;
1819
+ if (typeof content === "string") try {
1820
+ const v = JSON.parse(content);
1821
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
1822
+ } catch {
1823
+ return null;
1824
+ }
1825
+ return null;
1826
+ }
1827
+ /**
1828
+ * Stamp the dedup identity into a write's meta so the writeHandler can project
1829
+ * it onto the entry's `dedup_scope` / `dedup_key` columns (#1159). `dedupKey`
1830
+ * stays in the metadata blob (observability / collection-query); on a merge
1831
+ * we drop `createdAt` so the index keeps the original file ctime (R1,
1832
+ * feed-sort §5.1).
1833
+ */
1834
+ stampDedupMeta(base, dedupScope, dedupKey, isDuplicate) {
1835
+ const meta = { ...base ?? {} };
1836
+ meta.dedupKey = dedupKey;
1837
+ meta.dedupScope = dedupScope;
1838
+ if (isDuplicate) delete meta.createdAt;
1839
+ return meta;
1840
+ }
1841
+ /**
1842
+ * Dedup-on-write core (#492 / #1159) — the `options.dedupBy` upsert behind the
1843
+ * standard `write` / `batchWrite`. The record's `dedupBy` field (URL-valued)
1844
+ * is normalized into a `dedupKey`; the record lands at a STABLE readable path
1845
+ * (`{collection}/{uuid}.json`) and dedup resolves through the tree-index's
1846
+ * `(scope, dedup_scope, dedup_key)` secondary UNIQUE index — NOT a
1847
+ * cid-in-path filename (which silently orphaned records written under any
1848
+ * other layout, #1159). On a hit the incoming record is MERGED
1849
+ * (non-degrading) into the existing entry AT ITS REAL PATH. Concurrency-safe:
1850
+ * a racing create collides on the UNIQUE index, and the loser re-queries the
1851
+ * index and merges (bounded retry). Records missing the field are written
1852
+ * normally under the collection (no dedup).
1853
+ */
1854
+ async upsertByDedupKey(path, content, options, dedupBy) {
1855
+ if (!this.treeIndex.findByDedupKey) throw new AFSValidationError("dedup-on-write (dedupBy) is not supported by this DID Space index");
1856
+ const { dedupBy: _by, dedupKey: _dk, mergeImmutableFields, ...baseOpts } = options;
1857
+ const body = this.parseRecordBody(content.content);
1858
+ const rawVal = body && typeof body[dedupBy] === "string" ? body[dedupBy] : null;
1859
+ if (!body || !rawVal) {
1860
+ const fresh = joinURL("/", path, `${crypto.randomUUID()}.json`);
1861
+ return {
1862
+ ...await super.write(fresh, content, baseOpts),
1863
+ deduped: false
1864
+ };
1865
+ }
1866
+ const dedupKey = normalizeItemUrl(rawVal);
1867
+ const dedupScope = this.treePath(joinURL("/", path));
1868
+ const immutable = this.resolveImmutableMergeKeys(mergeImmutableFields);
1869
+ const MAX_ATTEMPTS = 4;
1870
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
1871
+ const existing = await this.treeIndex.findByDedupKey(dedupScope, dedupKey);
1872
+ if (existing) {
1873
+ const target = `/${existing.path}`;
1874
+ let existingBody = null;
1875
+ let version;
1876
+ try {
1877
+ const readResult = await this.read(target);
1878
+ existingBody = this.parseRecordBody(readResult.data?.content);
1879
+ version = (readResult.data?.meta)?.version;
1880
+ } catch (err) {
1881
+ if (err instanceof AFSNotFoundError) continue;
1882
+ throw err;
1883
+ }
1884
+ const mergedBody = existingBody !== null ? mergeItemContent(existingBody, body, immutable) : { ...body };
1885
+ const meta$1 = this.stampDedupMeta(content.meta, dedupScope, dedupKey, true);
1886
+ const callerIfMatch = baseOpts.ifMatch;
1887
+ const ifMatch = callerIfMatch ?? version;
1888
+ try {
1889
+ return {
1890
+ ...await super.write(target, {
1891
+ ...content,
1892
+ content: mergedBody,
1893
+ meta: meta$1
1894
+ }, ifMatch ? {
1895
+ ...baseOpts,
1896
+ ifMatch
1897
+ } : baseOpts),
1898
+ deduped: true
1899
+ };
1900
+ } catch (err) {
1901
+ if (err instanceof AFSConflictError && callerIfMatch === void 0) continue;
1902
+ throw err;
1903
+ }
1904
+ }
1905
+ const fresh = joinURL("/", path, `${crypto.randomUUID()}.json`);
1906
+ const meta = this.stampDedupMeta(content.meta, dedupScope, dedupKey, false);
1907
+ try {
1908
+ return {
1909
+ ...await super.write(fresh, {
1910
+ ...content,
1911
+ content: body,
1912
+ meta
1913
+ }, baseOpts),
1914
+ deduped: false
1915
+ };
1916
+ } catch (err) {
1917
+ if (await this.treeIndex.findByDedupKey(dedupScope, dedupKey)) continue;
1918
+ throw err;
1919
+ }
1920
+ }
1921
+ throw new AFSConflictError(joinURL("/", path), {});
1922
+ }
1923
+ /**
1924
+ * One-time reindex/backfill seam (#1159 — run from a deferred one-shot
1925
+ * script, NOT on the write hot path). Populates the dedup columns for
1926
+ * EXISTING records under `collection` so pre-feature entries (old
1927
+ * `{slug}.json` / `{uuid}/item.json` / `{cid}.json` layouts) become
1928
+ * dedup-able — fixing the "an already-saved URL's first re-save duplicates"
1929
+ * bug. Reads each record, extracts + normalizes `record[dedupBy]`, and writes
1930
+ * the dedup columns INDEX-ONLY (no changelog / no cid change) via
1931
+ * `TreeIndex.setDedupKey`. Records missing the field, and genuine
1932
+ * pre-existing duplicates (which would violate the UNIQUE index), are
1933
+ * skipped. Idempotent.
1934
+ */
1935
+ async reindexDedupKeys(collection, dedupBy) {
1936
+ if (!this.treeIndex.setDedupKey) throw new AFSValidationError("reindexDedupKeys is not supported by this DID Space index");
1937
+ const dedupScope = this.treePath(joinURL("/", collection));
1938
+ const listPrefix = dedupScope.endsWith("/") ? dedupScope : `${dedupScope}/`;
1939
+ let scanned = 0;
1940
+ let indexed = 0;
1941
+ let skipped = 0;
1942
+ let cursor;
1943
+ do {
1944
+ const page = await this.treeIndex.listEntries(listPrefix, {
1945
+ limit: 500,
1946
+ cursor
1947
+ });
1948
+ for (const e of page.entries) {
1949
+ scanned++;
1950
+ let body = null;
1951
+ try {
1952
+ const r = await this.read(`/${e.path}`);
1953
+ body = this.parseRecordBody(r.data?.content);
1954
+ } catch {
1955
+ skipped++;
1956
+ continue;
1957
+ }
1958
+ const raw = body && typeof body[dedupBy] === "string" ? body[dedupBy] : null;
1959
+ if (!raw) {
1960
+ skipped++;
1961
+ continue;
1962
+ }
1963
+ try {
1964
+ await this.treeIndex.setDedupKey(e.path, dedupScope, normalizeItemUrl(raw), { deviceId: this.deviceId });
1965
+ indexed++;
1966
+ } catch {
1967
+ skipped++;
1968
+ }
1969
+ }
1970
+ cursor = page.hasMore ? page.cursor : void 0;
1971
+ } while (cursor);
1972
+ return {
1973
+ scanned,
1974
+ indexed,
1975
+ skipped
1976
+ };
1977
+ }
1978
+ /**
1979
+ * Standard write, extended with the dedup-on-write upsert policy (#492 /
1980
+ * #1159). When `options.dedupBy` (canonical) or the deprecated `dedupKey`
1981
+ * alias is set the write becomes a secondary-index upsert (see
1982
+ * {@link upsertByDedupKey}); otherwise it is the base provider's write.
1983
+ */
1984
+ async write(path, content, options) {
1985
+ const dedupBy = options?.dedupBy ?? options?.dedupKey;
1986
+ if (dedupBy) return this.upsertByDedupKey(path, content, options, dedupBy);
1987
+ return super.write(path, content, options);
1988
+ }
1989
+ /**
1990
+ * The commit write — mirrors `batchWrite` steps 2/6/7/8 WITHOUT the R2 byte
1991
+ * leg (existence is trusted, §8): preread → ifMatch/skip → batchPutEntries →
1992
+ * GC bookkeeping → share hooks → per-entry results. `cid`/`size`/`mimeType`
1993
+ * come from the SESSION record (the prepared contract, re-validated against
1994
+ * the client's entries); `deviceId`/`mtime` are client provenance threaded to
1995
+ * the changelog (echo防回环, §6.1).
1996
+ */
1997
+ async commitSessionEntries(sessionId, sessionEntries, clientEntries) {
1998
+ const byPath = new Map(sessionEntries.map((e) => [joinURL("/", e.path), e]));
1999
+ const results = new Array(clientEntries.length);
2000
+ const prepared = [];
2001
+ for (let i = 0; i < clientEntries.length; i++) {
2002
+ const c = clientEntries[i];
2003
+ const afsPath = joinURL("/", c.path);
2004
+ const pv = this.validateActionPath(afsPath);
2005
+ if (!pv.ok) {
2006
+ results[i] = {
2007
+ path: afsPath,
2008
+ success: false,
2009
+ error: pv.reason,
2010
+ code: "AFS_VALIDATION_ERROR"
2011
+ };
2012
+ continue;
2013
+ }
2014
+ const se = byPath.get(afsPath);
2015
+ if (!se || se.cid !== c.cid) {
2016
+ results[i] = {
2017
+ path: afsPath,
2018
+ success: false,
2019
+ error: "entry not in session, or cid mismatch with prepared contract",
2020
+ code: "AFS_VALIDATION_ERROR"
2021
+ };
2022
+ continue;
2023
+ }
2024
+ const deviceId = typeof c.deviceId === "string" && c.deviceId ? c.deviceId : se.deviceId ?? this.deviceId;
2025
+ const mtime = typeof c.mtime === "number" ? c.mtime : se.mtime ?? Date.now();
2026
+ const ctime = resolveCtime(c.metadata, mtime);
2027
+ const treeEntry = {
2028
+ cid: se.cid,
2029
+ size: se.size,
2030
+ mtime,
2031
+ ctime,
2032
+ mimeType: se.mimeType ?? void 0
2033
+ };
2034
+ if (c.content !== void 0 && isTextMime(se.mimeType ?? void 0)) {
2035
+ if (await checksumSha256OfContent(c.content) === se.checksumSha256) treeEntry.content = c.content;
2036
+ }
2037
+ if (c.metadata) {
2038
+ const appMeta = extractApplicationMeta(c.metadata);
2039
+ if (appMeta) {
2040
+ try {
2041
+ assertMetadataSize(appMeta, afsPath);
2042
+ } catch (err) {
2043
+ results[i] = {
2044
+ path: afsPath,
2045
+ success: false,
2046
+ error: err instanceof Error ? err.message : String(err),
2047
+ code: err.code ?? "AFS_VALIDATION_ERROR"
2048
+ };
2049
+ continue;
2050
+ }
2051
+ treeEntry.metadata = appMeta;
2052
+ }
2053
+ }
2054
+ prepared.push({
2055
+ i,
2056
+ afsPath,
2057
+ treePath: pv.treePath,
2058
+ cid: se.cid,
2059
+ treeEntry,
2060
+ deviceId,
2061
+ ...se.ifMatch ? { ifMatch: se.ifMatch } : {}
2062
+ });
2063
+ }
2064
+ const preread = await this.batchPreread(prepared.map((p) => p.treePath));
2065
+ const skipped = [];
2066
+ let live = [];
2067
+ for (const p of prepared) {
2068
+ const existing = preread.get(p.treePath) ?? null;
2069
+ if (ifMatchPreconditionFailed(p.ifMatch, existing)) {
2070
+ results[p.i] = this.batchConflictFailure(p.afsPath, {
2071
+ currentCid: existing?.cid ?? null,
2072
+ currentMtime: existing?.mtime ?? null,
2073
+ currentDeviceId: null
2074
+ });
2075
+ continue;
2076
+ }
2077
+ live.push(p);
2078
+ }
2079
+ const preIoOk = [];
2080
+ for (const p of live) try {
2081
+ await applyShareWriteHook({
2082
+ ...this.shareHookArgs(p.afsPath, p.treePath),
2083
+ bytes: new Uint8Array(0),
2084
+ cid: p.cid,
2085
+ resolver: this.shareResolver,
2086
+ phase: "pre-io"
2087
+ });
2088
+ preIoOk.push(p);
2089
+ } catch (err) {
2090
+ results[p.i] = {
2091
+ path: p.afsPath,
2092
+ success: false,
2093
+ error: err instanceof Error ? err.message : String(err)
2094
+ };
2095
+ }
2096
+ live = preIoOk;
2097
+ let indexResults = [];
2098
+ if (live.length > 0) {
2099
+ const batchEntries = live.map((p) => ({
2100
+ path: p.treePath,
2101
+ entry: p.treeEntry,
2102
+ opts: { deviceId: p.deviceId }
2103
+ }));
2104
+ indexResults = this.treeIndex.batchPutEntries ? await this.treeIndex.batchPutEntries(batchEntries, { preread }) : await loopBatchPutEntries(this.treeIndex, batchEntries, { preread });
2105
+ }
2106
+ const written = [];
2107
+ live.forEach((p, j) => {
2108
+ const r = indexResults[j];
2109
+ if (!r) {
2110
+ results[p.i] = {
2111
+ path: p.afsPath,
2112
+ success: false,
2113
+ error: "index batch returned no result"
2114
+ };
2115
+ return;
2116
+ }
2117
+ if (r.conflict) {
2118
+ results[p.i] = this.batchConflictFailure(p.afsPath, r.conflict);
2119
+ return;
2120
+ }
2121
+ if (r.error) {
2122
+ results[p.i] = {
2123
+ path: p.afsPath,
2124
+ success: false,
2125
+ error: r.error
2126
+ };
2127
+ return;
2128
+ }
2129
+ if (r.skipped) {
2130
+ skipped.push(p);
2131
+ return;
2132
+ }
2133
+ written.push({
2134
+ p,
2135
+ oldCid: r.oldCid,
2136
+ liveSeq: r.liveSeq
2137
+ });
2138
+ });
2139
+ if (this.gcQueue) {
2140
+ const reReferenced = [...new Set([...written.map((w) => w.p.cid), ...skipped.map((s) => s.cid)])];
2141
+ if (reReferenced.length > 0) if (this.gcQueue.removeMany) await this.gcQueue.removeMany(reReferenced);
2142
+ else for (const cid of reReferenced) await this.gcQueue.remove(cid);
2143
+ const replaced = [...new Set(written.filter((w) => w.oldCid && w.oldCid !== w.p.cid).map((w) => w.oldCid))];
2144
+ if (replaced.length > 0) await this.gcQueue.enqueue(replaced);
2145
+ }
2146
+ for (const w of written) try {
2147
+ await applyShareWriteHook({
2148
+ ...this.shareHookArgs(w.p.afsPath, w.p.treePath),
2149
+ bytes: new Uint8Array(0),
2150
+ cid: w.p.cid,
2151
+ resolver: this.shareResolver,
2152
+ phase: "post-io"
2153
+ });
2154
+ } catch {}
2155
+ const commitOk = (p, liveSeq) => ({
2156
+ status: 200,
2157
+ ok: true,
2158
+ data: {
2159
+ path: p.afsPath,
2160
+ meta: {
2161
+ cid: p.cid,
2162
+ liveSeq: liveSeq != null ? String(liveSeq) : void 0
2163
+ }
2164
+ }
2165
+ });
2166
+ for (const w of written) results[w.p.i] = commitOk(w.p, w.liveSeq);
2167
+ for (const s of skipped) {
2168
+ let liveSeq;
2169
+ if (this.treeIndex.getPrefixWatermark) liveSeq = await this.treeIndex.getPrefixWatermark(s.treePath) ?? void 0;
2170
+ results[s.i] = commitOk(s, liveSeq);
2171
+ }
2172
+ const committedPaths = [...written, ...skipped.map((s) => ({ p: s }))].map((w) => ({
2173
+ path: w.p.afsPath,
2174
+ status: "committed"
2175
+ }));
2176
+ if (committedPaths.length > 0) await this.uploadSessionStore?.markEntries(sessionId, committedPaths);
2177
+ const succeeded = results.filter((r) => r?.ok === true).length;
2178
+ return {
2179
+ ok: true,
2180
+ results,
2181
+ succeeded,
2182
+ failed: results.length - succeeded
2183
+ };
2184
+ }
2185
+ /**
2186
+ * Native batch delete — same merge without the R2 leg: ONE preread → GC
2187
+ * enqueue (before delete, single-op ordering) → ONE atomic index batch →
2188
+ * share delete hooks. FILE entries only (no directory fallback — sync
2189
+ * never dir-deletes; a dir path fails per entry with 404).
2190
+ */
2191
+ async batchDelete(entries, _options) {
2192
+ const results = new Array(entries.length);
2193
+ const prepared = [];
2194
+ for (let i = 0; i < entries.length; i++) {
2195
+ const entry = entries[i];
2196
+ const afsPath = joinURL("/", entry.path);
2197
+ if (this.isVirtualPath(afsPath) || isShareReservedPath(afsPath)) {
2198
+ results[i] = {
2199
+ path: afsPath,
2200
+ success: false,
2201
+ error: `Forbidden at ${afsPath}: virtual_path`,
2202
+ code: "AFS_FORBIDDEN"
2203
+ };
2204
+ continue;
2205
+ }
2206
+ const deleteMeta = entry.options?.meta;
2207
+ prepared.push({
2208
+ i,
2209
+ afsPath,
2210
+ treePath: this.treePath(afsPath),
2211
+ deviceId: typeof deleteMeta?.deviceId === "string" && deleteMeta.deviceId ? deleteMeta.deviceId : this.deviceId,
2212
+ ...typeof deleteMeta?.mtime === "number" ? { mtime: deleteMeta.mtime } : {},
2213
+ ...typeof deleteMeta?.ifMatch === "string" ? { ifMatch: deleteMeta.ifMatch } : {}
2214
+ });
2215
+ }
2216
+ const preread = await this.batchPreread(prepared.map((p) => p.treePath));
2217
+ const live = [];
2218
+ for (const p of prepared) {
2219
+ const existing = preread.get(p.treePath) ?? null;
2220
+ if (p.ifMatch !== void 0 && (!existing || existing.cid !== p.ifMatch)) {
2221
+ results[p.i] = this.batchConflictFailure(p.afsPath, {
2222
+ currentCid: existing?.cid ?? null,
2223
+ currentMtime: existing?.mtime ?? null,
2224
+ currentDeviceId: null
2225
+ });
2226
+ continue;
2227
+ }
2228
+ if (!existing) {
2229
+ results[p.i] = {
2230
+ path: p.afsPath,
2231
+ success: false,
2232
+ error: `Path not found: ${p.afsPath}`,
2233
+ code: "AFS_NOT_FOUND"
2234
+ };
2235
+ continue;
2236
+ }
2237
+ live.push(p);
2238
+ }
2239
+ if (this.gcQueue && live.length > 0) {
2240
+ const cids = [...new Set(live.map((p) => (preread.get(p.treePath) ?? null)?.cid).filter(Boolean))];
2241
+ if (cids.length > 0) await this.gcQueue.enqueue(cids);
2242
+ }
2243
+ if (live.length > 0) {
2244
+ const batchEntries = live.map((p) => ({
2245
+ path: p.treePath,
2246
+ opts: {
2247
+ deviceId: p.deviceId,
2248
+ ...p.mtime !== void 0 ? { mtime: p.mtime } : {}
2249
+ }
2250
+ }));
2251
+ let indexResults;
2252
+ try {
2253
+ indexResults = this.treeIndex.batchDeleteEntries ? await this.treeIndex.batchDeleteEntries(batchEntries, { preread }) : await loopBatchDeleteEntries(this.treeIndex, batchEntries, { preread });
2254
+ } catch (err) {
2255
+ const message = err instanceof Error ? err.message : String(err);
2256
+ for (const p of live) results[p.i] = {
2257
+ path: p.afsPath,
2258
+ success: false,
2259
+ error: message
2260
+ };
2261
+ const succeededEarly = results.filter((r) => r?.success).length;
2262
+ return {
2263
+ results,
2264
+ succeeded: succeededEarly,
2265
+ failed: results.length - succeededEarly
2266
+ };
2267
+ }
2268
+ for (let j = 0; j < live.length; j++) {
2269
+ const p = live[j];
2270
+ const r = indexResults[j];
2271
+ if (!r || r.error || r.conflict || r.notFound) {
2272
+ results[p.i] = r?.conflict ? this.batchConflictFailure(p.afsPath, r.conflict) : {
2273
+ path: p.afsPath,
2274
+ success: false,
2275
+ error: r?.error ?? `Path not found: ${p.afsPath}`,
2276
+ ...r?.notFound ? { code: "AFS_NOT_FOUND" } : {}
2277
+ };
2278
+ continue;
2279
+ }
2280
+ await applyShareDeleteHook({
2281
+ ...this.shareHookArgs(p.afsPath, p.treePath),
2282
+ resolver: this.shareResolver
2283
+ });
2284
+ if (p.afsPath.endsWith(".ash")) this.emit({
2285
+ type: SCRIPT_EVENT_TYPES.UNREGISTERED,
2286
+ path: p.afsPath,
2287
+ data: { runtime: "ash" }
2288
+ });
2289
+ results[p.i] = {
2290
+ path: p.afsPath,
2291
+ success: true
2292
+ };
2293
+ }
2294
+ }
2295
+ const succeeded = results.filter((r) => r?.success).length;
2296
+ return {
2297
+ results,
2298
+ succeeded,
2299
+ failed: results.length - succeeded
2300
+ };
2301
+ }
2302
+ async statHandler(ctx) {
2303
+ const afsPath = joinURL("/", ctx.params.path ?? "");
2304
+ if (afsPath === "/") return { data: {
2305
+ id: "/",
2306
+ path: "/",
2307
+ meta: {
2308
+ kind: "did-space:root",
2309
+ childrenCount: await this.treeIndex.countChildren(this.treeListPrefix("/"))
2310
+ }
2311
+ } };
2312
+ const entry = await this.treeIndex.get(this.treePath(afsPath), { includeContent: false });
2313
+ if (entry) return { data: {
2314
+ id: afsPath.split("/").pop() || afsPath,
2315
+ path: afsPath,
2316
+ meta: {
2317
+ kind: "did-space:file",
2318
+ size: entry.size,
2319
+ cid: entry.cid,
2320
+ mimeType: entry.mimeType,
2321
+ version: entry.cid
2322
+ },
2323
+ createdAt: new Date(entry.ctime),
2324
+ updatedAt: new Date(entry.mtime)
2325
+ } };
2326
+ if (await this.isDirectory(afsPath)) {
2327
+ const prefix = this.treeListPrefix(afsPath);
2328
+ const meta = {
2329
+ kind: "did-space:directory",
2330
+ childrenCount: await this.treeIndex.countChildren(prefix)
2331
+ };
2332
+ if (this.treeIndex.getPrefixWatermark) {
2333
+ const version = await this.treeIndex.getPrefixWatermark(prefix);
2334
+ if (version != null) meta.liveSeq = String(version);
2335
+ }
2336
+ const stat = {
2337
+ id: afsPath.split("/").pop() || afsPath,
2338
+ path: afsPath,
2339
+ meta
2340
+ };
2341
+ const dirTimes = await this.treeIndex.getDirTimes?.(prefix);
2342
+ if (dirTimes) {
2343
+ stat.createdAt = new Date(dirTimes.ctime);
2344
+ stat.updatedAt = new Date(dirTimes.mtime);
2345
+ }
2346
+ return { data: stat };
2347
+ }
2348
+ throw new AFSNotFoundError(afsPath);
2349
+ }
2350
+ async searchHandler(ctx, query, options) {
2351
+ const afsPath = ctx.params.path ? `/${ctx.params.path}` : "/";
2352
+ const pattern = query || "";
2353
+ const limit = options?.limit || 50;
2354
+ if (!pattern) return {
2355
+ data: [],
2356
+ total: 0
2357
+ };
2358
+ const prefix = this.treeListPrefix(afsPath);
2359
+ const results = [];
2360
+ let cursor;
2361
+ const searchQuery = pattern.toLowerCase();
2362
+ do {
2363
+ const listResult = await this.treeIndex.list(prefix, {
2364
+ limit: LIST_MAX_LIMIT,
2365
+ cursor
2366
+ });
2367
+ for (const treeListEntry of listResult.entries) {
2368
+ if (results.length >= limit) break;
2369
+ const path = `/${treeListEntry.path}`;
2370
+ if ((path.split("/").pop() || "").toLowerCase().includes(searchQuery)) {
2371
+ results.push(this.buildEntry(path, {}));
2372
+ continue;
2373
+ }
2374
+ const treeEntry = await this.treeIndex.get(treeListEntry.path, { includeContent: false });
2375
+ if (treeEntry) {
2376
+ if (!isTextMime(treeEntry.mimeType)) continue;
2377
+ const obj = await this.objectStore.get(this.objectKey(treeEntry.cid));
2378
+ if (obj) {
2379
+ const content = await obj.text();
2380
+ if (content.toLowerCase().includes(searchQuery)) results.push(this.buildEntry(path, { content }));
2381
+ }
2382
+ }
2383
+ }
2384
+ cursor = listResult.hasMore ? listResult.cursor : void 0;
2385
+ } while (cursor && results.length < limit);
2386
+ return {
2387
+ data: results,
2388
+ total: results.length
2389
+ };
2390
+ }
2391
+ async explainHandler(ctx) {
2392
+ const afsPath = joinURL("/", ctx.params.path ?? "");
2393
+ const lines = [];
2394
+ lines.push(`# DID Space: ${afsPath}`);
2395
+ lines.push("");
2396
+ if (afsPath === "/") {
2397
+ lines.push(`DID: ${this.did}`);
2398
+ lines.push("");
2399
+ lines.push(this.description || "DID-scoped persistent storage with CID-based deduplication");
2400
+ lines.push("");
2401
+ lines.push("## Operations");
2402
+ lines.push("- `list /` — List root files and directories");
2403
+ lines.push("- `read /{path}` — Read file content");
2404
+ lines.push("- `write /{path}` — Write file (content-addressed dedup)");
2405
+ lines.push("- `delete /{path}` — Delete file entry (lazy GC)");
2406
+ } else {
2407
+ const entry = await this.treeIndex.get(this.treePath(afsPath), { includeContent: false });
2408
+ if (entry) {
2409
+ lines.push(`DID: ${this.did}`);
2410
+ lines.push(`Path: ${afsPath}`);
2411
+ lines.push(`Size: ${entry.size} bytes`);
2412
+ lines.push(`CID: ${entry.cid}`);
2413
+ lines.push(`MIME: ${entry.mimeType ?? "unknown"}`);
2414
+ lines.push(`Created: ${new Date(entry.ctime).toISOString()}`);
2415
+ lines.push(`Modified: ${new Date(entry.mtime).toISOString()}`);
2416
+ } else if (await this.isDirectory(afsPath)) {
2417
+ lines.push(`DID: ${this.did}`);
2418
+ lines.push(`Virtual directory: ${afsPath}`);
2419
+ lines.push("Use `list` to see contents.");
2420
+ } else lines.push("Path not found.");
2421
+ }
2422
+ return {
2423
+ format: "markdown",
2424
+ content: lines.join("\n")
2425
+ };
2426
+ }
2427
+ /**
2428
+ * Write content to the provider, bypassing D4 virtual path protection.
2429
+ * Stores content to objectStore and treeIndex with the given deviceId.
2430
+ */
2431
+ async internalWrite(afsPath, opts) {
2432
+ if (isShareReservedPath(afsPath)) throw new AFSForbiddenError(afsPath, "share-path-reserved");
2433
+ const { content, deviceId, mtime, mimeType: explicitMimeType, metadata } = opts;
2434
+ let bytes;
2435
+ let storeValue;
2436
+ if (content instanceof Uint8Array) {
2437
+ bytes = content;
2438
+ storeValue = bytes.buffer;
2439
+ } else {
2440
+ bytes = new Uint8Array(content);
2441
+ storeValue = bytes.buffer;
2442
+ }
2443
+ const cid = await computeCID(bytes);
2444
+ const objKey = this.objectKey(cid);
2445
+ const treePath = this.treePath(afsPath);
2446
+ await applyShareWriteHook({
2447
+ ...this.shareHookArgs(afsPath, treePath),
2448
+ bytes,
2449
+ cid,
2450
+ resolver: this.shareResolver,
2451
+ phase: "pre-io"
2452
+ });
2453
+ if (!await this.objectStore.head(objKey)) await this.objectStore.put(objKey, storeValue);
2454
+ const now = mtime ?? Date.now();
2455
+ const detectedMime = explicitMimeType ?? detectMimeType(afsPath) ?? void 0;
2456
+ const ctime = resolveCtime(metadata, now);
2457
+ const treeEntry = {
2458
+ cid,
2459
+ size: bytes.length,
2460
+ mtime: now,
2461
+ ctime,
2462
+ mimeType: detectedMime
2463
+ };
2464
+ const appMeta = extractApplicationMeta(metadata);
2465
+ if (appMeta) treeEntry.metadata = appMeta;
2466
+ const inlineText = decodeInlineText(bytes, detectedMime);
2467
+ if (inlineText !== null) treeEntry.content = inlineText;
2468
+ const { oldCid } = await this.treeIndex.put(treePath, treeEntry, { deviceId });
2469
+ if (this.gcQueue) {
2470
+ await this.gcQueue.remove(cid);
2471
+ if (oldCid && oldCid !== cid) await this.gcQueue.enqueue([oldCid]);
2472
+ }
2473
+ await applyShareWriteHook({
2474
+ ...this.shareHookArgs(afsPath, treePath),
2475
+ bytes,
2476
+ cid,
2477
+ resolver: this.shareResolver,
2478
+ phase: "post-io"
2479
+ });
2480
+ await applyBlockletWriteHook({
2481
+ ...this.blockletHookArgs(afsPath, treePath),
2482
+ bytes,
2483
+ resolver: this.blockletResolver
2484
+ });
2485
+ return oldCid === void 0 ? { cid } : {
2486
+ cid,
2487
+ oldCid
2488
+ };
2489
+ }
2490
+ /**
2491
+ * Delete a file from the provider, bypassing D4 virtual path protection.
2492
+ */
2493
+ async internalDelete(afsPath, opts) {
2494
+ if (isShareReservedPath(afsPath)) throw new AFSForbiddenError(afsPath, "share-path-reserved");
2495
+ const treePath = this.treePath(afsPath);
2496
+ const entry = await this.treeIndex.get(treePath, { includeContent: false });
2497
+ if (!entry) return { deleted: false };
2498
+ await this.gcQueue?.enqueue([entry.cid]);
2499
+ await this.treeIndex.delete(treePath, { deviceId: opts.deviceId });
2500
+ await applyShareDeleteHook({
2501
+ ...this.shareHookArgs(afsPath, treePath),
2502
+ resolver: this.shareResolver
2503
+ });
2504
+ await applyBlockletDeleteHook({
2505
+ ...this.blockletHookArgs(afsPath, treePath),
2506
+ resolver: this.blockletResolver
2507
+ });
2508
+ return { deleted: true };
2509
+ }
2510
+ capabilities() {
2511
+ return {
2512
+ schemaVersion: 1,
2513
+ provider: this.name,
2514
+ tools: [],
2515
+ actions: [],
2516
+ operations: this.getOperationsDeclaration()
2517
+ };
2518
+ }
2519
+ };
2520
+ __decorate([Meta("/:path*")], DIDSpaceProvider.prototype, "readMeta", null);
2521
+ __decorate([List("/:path*", { handleDepth: true })], DIDSpaceProvider.prototype, "listHandler", null);
2522
+ __decorate([Read("/.meta/.capabilities")], DIDSpaceProvider.prototype, "readCapabilities", null);
2523
+ __decorate([Read("/:path*")], DIDSpaceProvider.prototype, "readHandler", null);
2524
+ __decorate([Write("/:path*")], DIDSpaceProvider.prototype, "writeHandler", null);
2525
+ __decorate([Delete("/:path*")], DIDSpaceProvider.prototype, "deleteHandler", null);
2526
+ __decorate([Actions("/")], DIDSpaceProvider.prototype, "listObjectPlaneActions", null);
2527
+ __decorate([Actions.Exec("/", "prepare-upload", "Reserve an upload session + (CF) mint exact-key checksum-pinned PUT grants", { effect: "write" })], DIDSpaceProvider.prototype, "execPrepareUpload", null);
2528
+ __decorate([Actions.Exec("/", "prepare-download", "Mint exact-key short-TTL presigned GET grants for view-referenced objects", { effect: "write" })], DIDSpaceProvider.prototype, "execPrepareDownload", null);
2529
+ __decorate([Actions.Exec("/", "commit-upload", "Commit uploaded objects' metadata (trust existence, 0 R2 subrequests)", { effect: "write" })], DIDSpaceProvider.prototype, "execCommitUpload", null);
2530
+ __decorate([Actions.Exec("/", "abort-upload", "Abort an upload session; staged objects become GC candidates", { effect: "write" })], DIDSpaceProvider.prototype, "execAbortUpload", null);
2531
+ __decorate([Stat("/:path*")], DIDSpaceProvider.prototype, "statHandler", null);
2532
+ __decorate([Search("/:path*")], DIDSpaceProvider.prototype, "searchHandler", null);
2533
+ __decorate([Explain("/:path*")], DIDSpaceProvider.prototype, "explainHandler", null);
2534
+
2535
+ //#endregion
2536
+ export { AFS_IFMATCH_ABSENT, BLOCKLET_PUBLISH_MARKER, BatchOverBudgetError, BatchUnsupportedError, CHANGELOG_DDL, CHANGELOG_INDEX_SQL, CHANGELOG_INSERT_SQL, CHANGELOG_ROW_KIND, CHANGELOG_SCOPE_CID_MIGRATION_SQL, CHANGELOG_TABLE_SQL, CHANGELOG_WIRE_EXTRA_FIELDS, CachingObjectStore, CidByteCache, ConflictResolutionError, CurrentObjectMissingError, D1GCQueue, D1ResolveStore, D1ShareReconciler, D1ShareStore, D1TreeIndex, D1UploadSessionStore, D1VersionedScopeBackend, D1_DID_SPACE_SCHEMA_SQL, DEFAULT_GC_GRACE_MS, DEFAULT_INLINE_THRESHOLD, DEVICE_ID_FORMAT_RE, DEVICE_SYNC_STATE_INDEX_SQL, DEVICE_SYNC_STATE_TABLE_SQL, DIDSpaceBlockletDataBackend, DIDSpaceClient, DIDSpaceProvider, DIDSpaceResolver, DIRTY_PATH_DDL, DIRTY_PATH_INDEX_SQL, DIRTY_PATH_TABLE_SQL, DIRTY_PATH_TRIGGER_SQL, ENTRIES_CONTENT_TYPE_COLUMN_SQL, ENTRIES_CONTENT_TYPE_INDEX_DROP_SQL, ENTRIES_CONTENT_TYPE_INDEX_SQL, ENTRIES_DEDUP_INDEX_SQL, ENTRIES_DEDUP_KEY_COLUMN_SQL, ENTRIES_DEDUP_SCOPE_COLUMN_SQL, ENTRIES_INDEX_SQL, ENTRIES_INLINE_CONTENT_COLUMN_SQL, ENTRIES_METADATA_COLUMN_SQL, ENTRIES_TABLE_SQL, GC_QUEUE_INDEX_SQL, GC_QUEUE_TABLE_SQL, HttpTransport, INGEST_DEVICE_ID, INLINE_CONTENT_HARD_CAP, LOCAL_CHANGELOG_DEFAULT_LIMIT, LOCAL_CHANGELOG_MAX_LIMIT, LibsqlVersionedScopeBackend, LocalChangelog, MANIFEST_FILE_KIND, MANIFEST_WIRE_EXTRA_FIELDS, MAX_DEVICE_ID_BYTES, MAX_ENTRIES_PER_SCOPE, MemoryGcQueue, MemoryTreeIndex, MemoryVersionedScopeBackend, PREFIX_WATERMARK_TABLE_SQL, PRESIGN_MAX_EXPIRES_SEC, PULL_BATCH_LIMIT, PULL_FETCH_CONCURRENCY, PUSH_BATCH_LIMIT, PUSH_PROGRESS_FLUSH_INTERVAL, PrefixedTreeIndex, RESOLVE_ENTRIES_INDEX_SQL, RESOLVE_ENTRIES_TABLE_SQL, RpcUserSpaceAFS, SCOPE_META_FILE_COUNT_BACKFILL_SQL, SCOPE_META_FILE_COUNT_DELETE_ONE_SQL, SCOPE_META_FILE_COUNT_DELETE_RANGE_SQL, SCOPE_META_FILE_COUNT_TRIGGER_SQL, SCOPE_META_INDEX_SQL, SCOPE_META_TABLE_SQL, SCOPE_META_TOTAL_BYTES_BACKFILL_SQL, SCOPE_META_TOTAL_BYTES_COLUMN_SQL, SCOPE_META_TOTAL_BYTES_TRIGGER_SQL, SHARES_INDEX_SQL, SHARES_TABLE_SQL, SYNC_STATE_DDL, SYNC_STATE_INDEX_SQL, SYNC_STATE_KEYS, SYNC_STATE_TABLE_SQL, SYNC_WALL_CLOCK_BUDGET_MS, ShareResolver, SqliteGcQueue, SqliteResolveStore, SqliteShareStore, SqliteUploadSessionStore, StaticReadonlyTransport, SyncEngine, SyncStateStore, UPLOAD_SESSIONS_INDEX_SQL, UPLOAD_SESSIONS_TABLE_SQL, UPLOAD_SESSION_ENTRIES_TABLE_SQL, UPLOAD_SESSION_TTL_MS, VERSIONED_SCOPES_INDEX_SQL, VERSIONED_SCOPES_TABLE_SQL, VersionedContentIndex, VersionedContentStore, WsTransport, ancestorDirs, applyDirtyUpsert, assertInlineThreshold, assertValidCid, attachMetadataContext, buildChangelogBinds, buildD1MigrationSql, buildKeysetCursor, buildManifest, buildOrderedQueryParts, cidToChecksumSha256, classifyMissingObject, classifyScopes, compareTreeSortValues, computeCID, computeDiff, connectDIDSpace, createWsTransport, decodeInlineText, decodeScopeHmacSecret, defaultAppState, deriveScope, detachMetadataContext, detectMimeType, fetchRemoteManifest, generateDesktopDeviceId, getMetadataContext, isPlainDidScope, isShareDeclarationPath, isTextMime, isValidDeviceId, keysetAfter, migrateD1Scopes, nameOf, normalizeDdl, normalizeId, normalizeItemUrl, objectKeyForCid, openDaemonShareStore, parentOf, parseChangelogEntry, parseManifestEntry, parseSyncStatus, parseTreeCursor, prefixRange, referencedAnywhereD1, referencedAnywhereLibsql, resolveVersionToken, rowToEntry, runGc, runWithConcurrency, serializeChangelogEntry, serializeManifestEntry, serializeSyncStatus, serveShare, syncDIDSpace, validateDeviceId, validateId, versionedScope, versionedScopeGroup };
2537
+ //# sourceMappingURL=index.mjs.map