@needmoretruth/nmts-cli 0.17.2 → 0.34.2

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 (419) hide show
  1. package/AGENTS.md +125 -517
  2. package/CHANGELOG.md +104 -0
  3. package/README.ko.md +377 -469
  4. package/README.md +394 -474
  5. package/dist/account-proof.d.ts +29 -0
  6. package/dist/account-proof.js +6 -6
  7. package/dist/account.d.ts +18 -0
  8. package/dist/account.js +5 -5
  9. package/dist/agent-host.d.ts +71 -0
  10. package/dist/agent-host.js +169 -0
  11. package/dist/api-advice.d.ts +2 -0
  12. package/dist/api-advice.js +210 -0
  13. package/dist/api-key.d.ts +79 -0
  14. package/dist/api-key.js +7 -7
  15. package/dist/api-refusal.d.ts +14 -0
  16. package/dist/api-refusal.js +8 -0
  17. package/dist/api.d.ts +114 -0
  18. package/dist/api.js +59 -162
  19. package/dist/args.d.ts +160 -0
  20. package/dist/args.js +118 -30
  21. package/dist/artifact-about.d.ts +49 -0
  22. package/dist/autonomy.d.ts +39 -0
  23. package/dist/autonomy.js +110 -32
  24. package/dist/code-access.d.ts +29 -0
  25. package/dist/code-access.js +3 -3
  26. package/dist/code-vault.d.ts +38 -0
  27. package/dist/code-vault.js +8 -8
  28. package/dist/collision.d.ts +67 -0
  29. package/dist/collision.js +1 -1
  30. package/dist/commands/accept-terms.d.ts +11 -0
  31. package/dist/commands/accept-terms.js +106 -0
  32. package/dist/commands/balance.d.ts +7 -0
  33. package/dist/commands/balance.js +54 -1
  34. package/dist/commands/create-code-file.d.ts +24 -0
  35. package/dist/commands/create-code-file.js +93 -0
  36. package/dist/commands/create-link.d.ts +10 -0
  37. package/dist/commands/create-link.js +232 -0
  38. package/dist/commands/create.d.ts +20 -0
  39. package/dist/commands/create.js +43 -102
  40. package/dist/commands/credits.d.ts +9 -0
  41. package/dist/commands/credits.js +110 -0
  42. package/dist/commands/delete-account.d.ts +11 -0
  43. package/dist/commands/delete-account.js +63 -0
  44. package/dist/commands/deposit.d.ts +7 -0
  45. package/dist/commands/deposit.js +85 -0
  46. package/dist/commands/devices.d.ts +12 -0
  47. package/dist/commands/devices.js +144 -0
  48. package/dist/commands/documents.d.ts +35 -0
  49. package/dist/commands/documents.js +212 -0
  50. package/dist/commands/env.d.ts +5 -0
  51. package/dist/commands/env.js +10 -2
  52. package/dist/commands/erase.d.ts +13 -0
  53. package/dist/commands/erase.js +193 -0
  54. package/dist/commands/expiring.d.ts +19 -0
  55. package/dist/commands/extend.d.ts +33 -0
  56. package/dist/commands/extend.js +54 -15
  57. package/dist/commands/get.d.ts +18 -0
  58. package/dist/commands/key-manage.d.ts +11 -0
  59. package/dist/commands/key-manage.js +162 -0
  60. package/dist/commands/key.d.ts +28 -0
  61. package/dist/commands/key.js +249 -0
  62. package/dist/commands/kit.d.ts +11 -0
  63. package/dist/commands/kit.js +7 -7
  64. package/dist/commands/listfile.d.ts +13 -0
  65. package/dist/commands/listfile.js +12 -5
  66. package/dist/commands/login.d.ts +18 -0
  67. package/dist/commands/login.js +19 -19
  68. package/dist/commands/logout.d.ts +1 -0
  69. package/dist/commands/logout.js +1 -1
  70. package/dist/commands/losses.d.ts +12 -0
  71. package/dist/commands/losses.js +180 -0
  72. package/dist/commands/ls.d.ts +23 -0
  73. package/dist/commands/marks-dispatch.d.ts +2 -0
  74. package/dist/commands/marks-dispatch.js +51 -0
  75. package/dist/commands/marks.d.ts +46 -0
  76. package/dist/commands/marks.js +80 -0
  77. package/dist/commands/mcp.d.ts +24 -0
  78. package/dist/commands/mcp.js +70 -5
  79. package/dist/commands/mode.d.ts +8 -0
  80. package/dist/commands/mode.js +60 -24
  81. package/dist/commands/on-collision.d.ts +6 -0
  82. package/dist/commands/organise.d.ts +46 -0
  83. package/dist/commands/padding.d.ts +7 -0
  84. package/dist/commands/padding.js +114 -0
  85. package/dist/commands/public-code.d.ts +9 -0
  86. package/dist/commands/public-code.js +8 -8
  87. package/dist/commands/pull.d.ts +27 -0
  88. package/dist/commands/push-wallet.d.ts +19 -0
  89. package/dist/commands/push-wallet.js +69 -0
  90. package/dist/commands/push.d.ts +47 -0
  91. package/dist/commands/push.js +37 -7
  92. package/dist/commands/put-wallet.d.ts +77 -0
  93. package/dist/commands/put-wallet.js +316 -0
  94. package/dist/commands/put.d.ts +56 -0
  95. package/dist/commands/put.js +53 -6
  96. package/dist/commands/rebuild.d.ts +18 -0
  97. package/dist/commands/rebuild.js +108 -10
  98. package/dist/commands/receive.d.ts +24 -0
  99. package/dist/commands/recovery-dispatch.d.ts +2 -0
  100. package/dist/commands/recovery-dispatch.js +34 -0
  101. package/dist/commands/recovery-list.d.ts +11 -0
  102. package/dist/commands/recovery-list.js +6 -6
  103. package/dist/commands/recovery.d.ts +25 -0
  104. package/dist/commands/recovery.js +2 -2
  105. package/dist/commands/rollback.d.ts +9 -0
  106. package/dist/commands/rollback.js +118 -0
  107. package/dist/commands/s3.d.ts +23 -0
  108. package/dist/commands/s3.js +6 -6
  109. package/dist/commands/settings.d.ts +5 -0
  110. package/dist/commands/settings.js +21 -6
  111. package/dist/commands/share-dispatch.d.ts +2 -0
  112. package/dist/commands/share-dispatch.js +46 -0
  113. package/dist/commands/share.d.ts +14 -0
  114. package/dist/commands/share.js +13 -3
  115. package/dist/commands/shares-sent.d.ts +18 -0
  116. package/dist/commands/shares-sent.js +95 -0
  117. package/dist/commands/support.d.ts +24 -0
  118. package/dist/commands/support.js +321 -0
  119. package/dist/commands/sweep.d.ts +11 -0
  120. package/dist/commands/tip.d.ts +11 -0
  121. package/dist/commands/tip.js +86 -0
  122. package/dist/commands/trash.d.ts +17 -0
  123. package/dist/commands/trash.js +1 -1
  124. package/dist/commands/trial.d.ts +7 -0
  125. package/dist/commands/trial.js +16 -18
  126. package/dist/commands/unlock.d.ts +16 -0
  127. package/dist/commands/unlock.js +159 -0
  128. package/dist/commands/update.d.ts +42 -0
  129. package/dist/commands/update.js +1 -1
  130. package/dist/commands/usage.d.ts +8 -0
  131. package/dist/commands/verify.d.ts +18 -0
  132. package/dist/commands/verify.js +10 -6
  133. package/dist/commands/wallet-activity.d.ts +26 -0
  134. package/dist/commands/wallet-activity.js +117 -0
  135. package/dist/commands/wallet-donate.d.ts +26 -0
  136. package/dist/commands/wallet-donate.js +163 -0
  137. package/dist/commands/wallet-hall.d.ts +35 -0
  138. package/dist/commands/wallet-hall.js +214 -0
  139. package/dist/commands/wallet-send.d.ts +22 -0
  140. package/dist/commands/wallet-send.js +153 -0
  141. package/dist/commands/wallet-storage-ops.d.ts +24 -0
  142. package/dist/commands/wallet-storage-ops.js +215 -0
  143. package/dist/commands/wallet-storage.d.ts +18 -0
  144. package/dist/commands/wallet-storage.js +95 -0
  145. package/dist/commands/wallet-swap.d.ts +32 -0
  146. package/dist/commands/wallet-swap.js +287 -0
  147. package/dist/commands/wallet.d.ts +36 -0
  148. package/dist/commands/wallet.js +57 -15
  149. package/dist/commands/whoami.d.ts +10 -0
  150. package/dist/commands/whoami.js +24 -6
  151. package/dist/consent.d.ts +140 -0
  152. package/dist/consent.js +89 -30
  153. package/dist/credentials.d.ts +146 -0
  154. package/dist/credentials.js +10 -10
  155. package/dist/crypto-surface.d.ts +197 -0
  156. package/dist/crypto.d.ts +81 -0
  157. package/dist/crypto.js +13 -0
  158. package/dist/deposit.d.ts +25 -0
  159. package/dist/deposit.js +78 -0
  160. package/dist/download-part.d.ts +40 -0
  161. package/dist/download-sink.d.ts +76 -0
  162. package/dist/download.d.ts +79 -0
  163. package/dist/download.js +3 -3
  164. package/dist/drive-paths.d.ts +73 -0
  165. package/dist/environment.d.ts +62 -0
  166. package/dist/environment.js +29 -9
  167. package/dist/errors.d.ts +22 -0
  168. package/dist/errors.js +3 -3
  169. package/dist/exit.d.ts +44 -0
  170. package/dist/expiry.d.ts +58 -0
  171. package/dist/extend-budget.d.ts +35 -0
  172. package/dist/extend-budget.js +81 -0
  173. package/dist/extend-chain.d.ts +43 -0
  174. package/dist/extend-chain.js +40 -3
  175. package/dist/extend-plan.d.ts +148 -0
  176. package/dist/extend-plan.js +1 -1
  177. package/dist/gate.d.ts +18 -0
  178. package/dist/gate.js +100 -0
  179. package/dist/guards.d.ts +2 -0
  180. package/dist/help.d.ts +20 -0
  181. package/dist/help.js +199 -33
  182. package/dist/human-check.d.ts +23 -0
  183. package/dist/index.d.ts +42 -0
  184. package/dist/index.js +49 -0
  185. package/dist/item-trash.d.ts +1 -0
  186. package/dist/kit-file.d.ts +56 -0
  187. package/dist/kit-file.js +10 -10
  188. package/dist/list-file.d.ts +88 -0
  189. package/dist/list-file.js +90 -7
  190. package/dist/list-view-find.d.ts +15 -0
  191. package/dist/list-view-order.d.ts +32 -0
  192. package/dist/main.d.ts +12 -0
  193. package/dist/main.js +112 -113
  194. package/dist/manifest-chunk-cache.d.ts +14 -0
  195. package/dist/manifest-chunk-cache.js +103 -0
  196. package/dist/manifest-chunk-flow.d.ts +81 -0
  197. package/dist/manifest-chunk-flow.js +307 -0
  198. package/dist/manifest-create.d.ts +23 -0
  199. package/dist/manifest-create.js +26 -30
  200. package/dist/manifest-write.d.ts +161 -0
  201. package/dist/manifest-write.js +42 -33
  202. package/dist/manifest.d.ts +82 -0
  203. package/dist/manifest.js +69 -21
  204. package/dist/mark-render.d.ts +26 -0
  205. package/dist/mcp-args.d.ts +17 -0
  206. package/dist/mcp-ask.d.ts +43 -0
  207. package/dist/mcp-ask.js +100 -0
  208. package/dist/mcp-tools/context.d.ts +46 -0
  209. package/dist/mcp-tools/credits.d.ts +3 -0
  210. package/dist/mcp-tools/credits.js +61 -0
  211. package/dist/mcp-tools/files.d.ts +3 -0
  212. package/dist/mcp-tools/files.js +34 -0
  213. package/dist/mcp-tools/organise.d.ts +3 -0
  214. package/dist/mcp-tools/organise.js +82 -2
  215. package/dist/mcp-tools/reads.d.ts +3 -0
  216. package/dist/mcp-tools/reads.js +132 -4
  217. package/dist/mcp-tools/share.d.ts +3 -0
  218. package/dist/mcp-tools/share.js +26 -7
  219. package/dist/mcp-tools/support.d.ts +3 -0
  220. package/dist/mcp-tools/support.js +140 -0
  221. package/dist/mcp-tools/tiers.d.ts +31 -0
  222. package/dist/mcp-tools/tiers.js +147 -0
  223. package/dist/mcp.d.ts +80 -0
  224. package/dist/mcp.js +110 -9
  225. package/dist/net-retry.d.ts +38 -0
  226. package/dist/network.d.ts +10 -0
  227. package/dist/notice.d.ts +7 -0
  228. package/dist/notice.js +7 -7
  229. package/dist/product.d.ts +29 -0
  230. package/dist/product.js +1 -1
  231. package/dist/progress.d.ts +37 -0
  232. package/dist/prompt.d.ts +31 -0
  233. package/dist/prompt.js +1 -1
  234. package/dist/rebuild-key-check.d.ts +26 -0
  235. package/dist/rebuild-key-check.js +90 -0
  236. package/dist/rebuild.d.ts +87 -0
  237. package/dist/rebuild.js +26 -4
  238. package/dist/recovery-assemble.d.ts +40 -0
  239. package/dist/recovery-assemble.js +1 -1
  240. package/dist/recovery-build.d.ts +40 -0
  241. package/dist/recovery-map-file.d.ts +53 -0
  242. package/dist/recovery-map-file.js +4 -4
  243. package/dist/recovery-map.d.ts +197 -0
  244. package/dist/recovery-release.d.ts +104 -0
  245. package/dist/recovery-seq.d.ts +10 -0
  246. package/dist/recovery-source.d.ts +50 -0
  247. package/dist/redact.d.ts +70 -0
  248. package/dist/redact.js +212 -0
  249. package/dist/registration.d.ts +30 -0
  250. package/dist/registration.js +5 -5
  251. package/dist/risk.d.ts +380 -0
  252. package/dist/risk.js +206 -0
  253. package/dist/run-log.d.ts +78 -0
  254. package/dist/run-log.js +301 -0
  255. package/dist/s3/listing.d.ts +47 -0
  256. package/dist/s3/multipart.d.ts +21 -0
  257. package/dist/s3/response-sink.d.ts +8 -0
  258. package/dist/s3/same-file.d.ts +54 -0
  259. package/dist/s3/server.d.ts +58 -0
  260. package/dist/s3/server.js +2 -2
  261. package/dist/s3/sigv4.d.ts +46 -0
  262. package/dist/s3/sigv4.js +1 -1
  263. package/dist/s3/staging.d.ts +10 -0
  264. package/dist/s3/xml.d.ts +28 -0
  265. package/dist/safe-path.d.ts +41 -0
  266. package/dist/seal.d.ts +120 -0
  267. package/dist/secret-reader.d.ts +28 -0
  268. package/dist/server.d.ts +9 -0
  269. package/dist/session.d.ts +32 -0
  270. package/dist/session.js +1 -1
  271. package/dist/setup-questions.d.ts +7 -0
  272. package/dist/share.d.ts +108 -0
  273. package/dist/share.js +1 -1
  274. package/dist/shared/lib/crypto/size-padding.d.ts +55 -0
  275. package/dist/shared/lib/crypto/size-padding.js +5 -0
  276. package/dist/shared/lib/drive/manifest-chunks.d.ts +95 -0
  277. package/dist/shared/lib/drive/manifest-chunks.js +252 -0
  278. package/dist/shared/lib/drive/manifest-codec.d.ts +214 -0
  279. package/dist/shared/lib/drive/manifest-codec.js +7 -7
  280. package/dist/shared/lib/drive/manifest-index.d.ts +98 -0
  281. package/dist/shared/lib/drive/manifest-ops.d.ts +167 -0
  282. package/dist/shared/lib/drive/manifest-ops.js +9 -4
  283. package/dist/shared/lib/drive/manifest-pack.d.ts +65 -0
  284. package/dist/shared/lib/drive/manifest-pack.js +287 -0
  285. package/dist/shared/lib/drive/manifest-settings.d.ts +113 -0
  286. package/dist/shared/lib/drive/manifest-settings.js +87 -6
  287. package/dist/shared/lib/drive/name-conflict.d.ts +76 -0
  288. package/dist/shared/lib/drive/rebuild-verify.d.ts +73 -0
  289. package/dist/shared/lib/drive/rebuild-verify.js +112 -0
  290. package/dist/shared/lib/drive/unique-name.d.ts +9 -0
  291. package/dist/shared/lib/drive/zstd.d.ts +27 -0
  292. package/dist/shared/lib/drive/zstd.js +74 -0
  293. package/dist/shared/lib/extend/epochs.d.ts +205 -0
  294. package/dist/shared/lib/net/retry-budget.d.ts +79 -0
  295. package/dist/shared/lib/share/shared-file-info.d.ts +21 -0
  296. package/dist/shared/lib/storage-control/chain.d.ts +57 -0
  297. package/dist/shared/lib/storage-control/chain.js +82 -0
  298. package/dist/shared/lib/storage-control/plan.d.ts +40 -0
  299. package/dist/shared/lib/storage-control/plan.js +72 -0
  300. package/dist/shared/lib/storage-control/reuse.d.ts +72 -0
  301. package/dist/shared/lib/storage-control/reuse.js +95 -0
  302. package/dist/shared/lib/storage-network.d.ts +25 -0
  303. package/dist/shared/lib/upload/part-plan.d.ts +24 -0
  304. package/dist/shared/lib/wallet/activity.d.ts +138 -0
  305. package/dist/shared/lib/wallet/activity.js +208 -0
  306. package/dist/shared/lib/wallet/send-rules.d.ts +68 -0
  307. package/dist/shared/lib/wallet/send-rules.js +116 -0
  308. package/dist/shared/lib/wallet/swap-rules.d.ts +115 -0
  309. package/dist/shared/lib/wallet/swap-rules.js +199 -0
  310. package/dist/shared/lib/wallet/tip.d.ts +14 -0
  311. package/dist/shared/lib/wallet/tip.js +34 -0
  312. package/dist/shared/lib/wallet/venue-ids.d.ts +23 -0
  313. package/dist/shared/lib/wallet/venue-ids.js +53 -0
  314. package/dist/standing-tip.d.ts +24 -0
  315. package/dist/standing-tip.js +52 -0
  316. package/dist/stdout.d.ts +38 -0
  317. package/dist/storage-control-chain.d.ts +38 -0
  318. package/dist/storage-control-chain.js +78 -0
  319. package/dist/sui-rpc.d.ts +15 -0
  320. package/dist/sui-rpc.js +59 -0
  321. package/dist/support-attach.d.ts +24 -0
  322. package/dist/support-attach.js +72 -0
  323. package/dist/support-copy.d.ts +42 -0
  324. package/dist/support-copy.js +109 -0
  325. package/dist/support-wire.d.ts +49 -0
  326. package/dist/support-wire.js +194 -0
  327. package/dist/trash-sweep.d.ts +55 -0
  328. package/dist/units.d.ts +2 -0
  329. package/dist/update-check.d.ts +50 -0
  330. package/dist/update-source.d.ts +63 -0
  331. package/dist/upload-api.d.ts +3 -0
  332. package/dist/upload-file.d.ts +97 -0
  333. package/dist/upload-file.js +3 -2
  334. package/dist/upload-price.d.ts +50 -0
  335. package/dist/upload-steps.d.ts +26 -0
  336. package/dist/upload-steps.js +29 -9
  337. package/dist/upload-store.d.ts +182 -0
  338. package/dist/upload-wallet-chain.d.ts +54 -0
  339. package/dist/upload-wallet-chain.js +203 -0
  340. package/dist/upload-wallet-plan.d.ts +128 -0
  341. package/dist/upload-wallet-plan.js +184 -0
  342. package/dist/upload-wallet.d.ts +24 -0
  343. package/dist/upload-wallet.js +208 -0
  344. package/dist/upload-wire.d.ts +244 -0
  345. package/dist/upload.d.ts +18 -0
  346. package/dist/upload.js +10 -0
  347. package/dist/usage-report.d.ts +35 -0
  348. package/dist/wallet-activity-chain.d.ts +5 -0
  349. package/dist/wallet-activity-chain.js +31 -0
  350. package/dist/wallet-chain.d.ts +19 -0
  351. package/dist/wallet-chain.js +2 -2
  352. package/dist/wallet-grant.d.ts +50 -0
  353. package/dist/wallet-grant.js +218 -0
  354. package/dist/wallet-send-chain.d.ts +24 -0
  355. package/dist/wallet-send-chain.js +49 -0
  356. package/dist/wallet-sign.d.ts +133 -0
  357. package/dist/wallet-sign.js +317 -0
  358. package/dist/wallet-storage-chain.d.ts +3 -0
  359. package/dist/wallet-storage-chain.js +20 -0
  360. package/dist/wallet-swap-chain.d.ts +52 -0
  361. package/dist/wallet-swap-chain.js +186 -0
  362. package/dist/wallet-swap-quote.d.ts +22 -0
  363. package/dist/wallet-swap-quote.js +126 -0
  364. package/dist/wallet.d.ts +135 -0
  365. package/dist/wallet.js +5 -5
  366. package/dist/walrus-write.d.ts +56 -0
  367. package/dist/walrus-write.js +5 -4
  368. package/dist/walrus.d.ts +86 -0
  369. package/dist/walrus.js +48 -10
  370. package/dist/zstd-node.d.ts +11 -0
  371. package/dist/zstd-node.js +71 -0
  372. package/docs/commands/accept-terms.md +15 -0
  373. package/docs/commands/create.md +17 -0
  374. package/docs/commands/credits.md +34 -0
  375. package/docs/commands/delete-account.md +14 -0
  376. package/docs/commands/deposit.md +27 -0
  377. package/docs/commands/devices.md +9 -0
  378. package/docs/commands/documents.md +10 -0
  379. package/docs/commands/env.md +21 -0
  380. package/docs/commands/extend.md +27 -0
  381. package/docs/commands/folders.md +10 -0
  382. package/docs/commands/get.md +14 -0
  383. package/docs/commands/key.md +14 -0
  384. package/docs/commands/login.md +23 -0
  385. package/docs/commands/logout.md +11 -0
  386. package/docs/commands/losses.md +13 -0
  387. package/docs/commands/ls.md +16 -0
  388. package/docs/commands/marks.md +9 -0
  389. package/docs/commands/mcp.md +33 -0
  390. package/docs/commands/mode.md +29 -0
  391. package/docs/commands/on-collision.md +13 -0
  392. package/docs/commands/padding.md +13 -0
  393. package/docs/commands/public-code.md +9 -0
  394. package/docs/commands/pull.md +9 -0
  395. package/docs/commands/push.md +12 -0
  396. package/docs/commands/put.md +36 -0
  397. package/docs/commands/rebuild.md +13 -0
  398. package/docs/commands/recovery.md +18 -0
  399. package/docs/commands/s3.md +17 -0
  400. package/docs/commands/share.md +15 -0
  401. package/docs/commands/support.md +16 -0
  402. package/docs/commands/tip.md +21 -0
  403. package/docs/commands/trash.md +25 -0
  404. package/docs/commands/trial.md +12 -0
  405. package/docs/commands/unlock.md +25 -0
  406. package/docs/commands/update.md +14 -0
  407. package/docs/commands/usage.md +14 -0
  408. package/docs/commands/verify.md +18 -0
  409. package/docs/commands/wallet.md +67 -0
  410. package/docs/commands/whoami.md +12 -0
  411. package/package.json +67 -8
  412. package/server.json +28 -0
  413. package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +23 -2
  414. package/vendor/nmts-crypto/nmts_crypto_wasm.js +44 -2
  415. package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
  416. package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +1 -0
  417. package/vendor/nmts-crypto/package.json +1 -1
  418. package/dist/commands/consent.js +0 -77
  419. package/dist/extend-sign.js +0 -130
@@ -0,0 +1,14 @@
1
+ /** The sealed bytes this machine holds under that name, or null when it holds none. */
2
+ export declare function readCachedChunk(accountId: string, hash: string): string | null;
3
+ /** Keep these sealed bytes under that name. Silent when the machine will not take them. */
4
+ export declare function writeCachedChunk(accountId: string, hash: string, ct: string): void;
5
+ /**
6
+ * Drop every copy this account holds that the given list does not name.
7
+ *
8
+ * ⛔ THIS IS THE BOUND ON THE CACHE, and it is a set rather than a size or an age. A chunk the
9
+ * current list does not name is a version of the list nobody will ask for again — no timer can
10
+ * say that, and a size limit would evict a chunk the list still needs while keeping one it
11
+ * abandoned. Called after every complete read and every successful write, which is exactly when
12
+ * "what the list names" is known.
13
+ */
14
+ export declare function pruneChunkCache(accountId: string, keep: ReadonlySet<string>): void;
@@ -0,0 +1,103 @@
1
+ // This machine's copies of the file list's chunks, kept by name (NCF-3 §6.3).
2
+ //
3
+ // ⛔ A CHUNK IS NAMED BY THE HASH OF ITS OWN BYTES, so a copy under that name can never be stale:
4
+ // when the bytes change the name changes, and the index names the new one. That is the whole
5
+ // reason this exists — a reload fetches only what the index newly names, and an edit that
6
+ // rewrote one chunk costs one download instead of the whole list.
7
+ //
8
+ // ⛔ AND IT IS STILL CHECKED ON THE WAY OUT. The reader re-hashes what it reads from here before
9
+ // opening it (`manifest-chunk-flow.ts`). A directory on this machine is not a trusted store:
10
+ // whoever holds the machine can edit it, and the index is the only thing that says which bytes
11
+ // belong to which version.
12
+ //
13
+ // ⛔ WHAT IS STORED IS SEALED. These are the account's names, folders and file keys, sealed with
14
+ // the NMTS key — the same bytes the server holds and cannot read. They are written 0600 in
15
+ // a 0700 directory, beside the kept copy of the index and for the same reason.
16
+ //
17
+ // ⚠ NOTHING HERE THROWS. A cache that cannot be read or written is a slower command, never a
18
+ // broken one: every function answers "no copy" and the network path behind it does the work.
19
+ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { configDir } from "./credentials.js";
22
+ /** A chunk's name: base64url SHA-256, unpadded — 43 characters, and it becomes a file name. */
23
+ const NAME = /^[A-Za-z0-9_-]{43}$/;
24
+ /**
25
+ * ⛔ AN ACCOUNT ID BECOMES PART OF A PATH HERE, so it is CHECKED rather than trusted — the same
26
+ * check `manifest.ts` makes on the kept list, and for the same reason: a value that reaches a
27
+ * path join unchecked is how `..` becomes a write somewhere else.
28
+ */
29
+ const ACCOUNT = /^[A-Za-z0-9_-]{1,64}$/;
30
+ /** Where one account's chunks live, or null when the id is not one this tool derived. */
31
+ function dirFor(accountId) {
32
+ if (!ACCOUNT.test(accountId))
33
+ return null;
34
+ return join(configDir(), "file-list-chunks", accountId);
35
+ }
36
+ function pathFor(accountId, hash) {
37
+ const dir = dirFor(accountId);
38
+ if (dir === null || !NAME.test(hash))
39
+ return null;
40
+ return join(dir, `${hash}.ct`);
41
+ }
42
+ /** The sealed bytes this machine holds under that name, or null when it holds none. */
43
+ export function readCachedChunk(accountId, hash) {
44
+ const path = pathFor(accountId, hash);
45
+ if (path === null)
46
+ return null;
47
+ try {
48
+ const text = readFileSync(path, "utf8").trim();
49
+ return text === "" ? null : text;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /** Keep these sealed bytes under that name. Silent when the machine will not take them. */
56
+ export function writeCachedChunk(accountId, hash, ct) {
57
+ const dir = dirFor(accountId);
58
+ const path = pathFor(accountId, hash);
59
+ if (dir === null || path === null)
60
+ return;
61
+ try {
62
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
63
+ writeFileSync(path, `${ct}\n`, { mode: 0o600 });
64
+ }
65
+ catch {
66
+ // Out of space, read-only home, a directory somebody removed underneath: the next read of the
67
+ // list fetches from the server instead, which is what this cache is an optimisation of.
68
+ }
69
+ }
70
+ /**
71
+ * Drop every copy this account holds that the given list does not name.
72
+ *
73
+ * ⛔ THIS IS THE BOUND ON THE CACHE, and it is a set rather than a size or an age. A chunk the
74
+ * current list does not name is a version of the list nobody will ask for again — no timer can
75
+ * say that, and a size limit would evict a chunk the list still needs while keeping one it
76
+ * abandoned. Called after every complete read and every successful write, which is exactly when
77
+ * "what the list names" is known.
78
+ */
79
+ export function pruneChunkCache(accountId, keep) {
80
+ const dir = dirFor(accountId);
81
+ if (dir === null)
82
+ return;
83
+ let names;
84
+ try {
85
+ names = readdirSync(dir);
86
+ }
87
+ catch {
88
+ return;
89
+ }
90
+ for (const name of names) {
91
+ if (!name.endsWith(".ct"))
92
+ continue;
93
+ if (keep.has(name.slice(0, -3)))
94
+ continue;
95
+ try {
96
+ rmSync(join(dir, name), { force: true });
97
+ }
98
+ catch {
99
+ // A copy that will not delete costs disk and nothing else; it is named by a hash, so it can
100
+ // never be handed back as some other version.
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,81 @@
1
+ import { type CryptoGlue } from "./crypto.ts";
2
+ import { NmtsError } from "./errors.ts";
3
+ import { type ManifestIndexV2 } from "./shared/lib/drive/manifest-chunks.ts";
4
+ import type { ManifestEntry } from "./shared/lib/drive/manifest-codec.ts";
5
+ import { type HeldChunk } from "./shared/lib/drive/manifest-pack.ts";
6
+ import type { AccountSettings } from "./shared/lib/drive/manifest-settings.ts";
7
+ /** A list this machine could not read completely. Never rendered as an empty or shorter drive. */
8
+ export declare class ManifestChunkError extends NmtsError {
9
+ constructor(message: string);
10
+ }
11
+ /** Everything the chunk half of one account's list needs. The key is the caller's to zero. */
12
+ export interface ChunkIO {
13
+ server: string;
14
+ apiKey: string;
15
+ accountId: string;
16
+ crypt: CryptoGlue;
17
+ /** The file-list key (NCF-3 §1.2). Derived, used and zeroed by the caller. */
18
+ key: Uint8Array;
19
+ }
20
+ /**
21
+ * Fetch, verify and open every chunk the index names, four at a time.
22
+ *
23
+ * The result is in INDEX order, which is placement order (§6.3.3), so the list arrives sorted for
24
+ * free. Afterwards the machine's own copies are pruned to what this index names: everything else
25
+ * is a version nobody will ask for again.
26
+ */
27
+ export declare function openChunks(io: ChunkIO, index: ManifestIndexV2): Promise<HeldChunk[]>;
28
+ /** What one save of the list is made of. `prev` is absent only for the very first version. */
29
+ export interface ListWritePlan {
30
+ /** The chunks behind the version this save was built on. Empty converts a version-1 list. */
31
+ previous: readonly HeldChunk[];
32
+ entries: readonly ManifestEntry[];
33
+ /** The version being written — one past the one that was read. */
34
+ seq: number;
35
+ /** Hash of the sealed blob this one continues, version 1's included (§6.3.6). */
36
+ prev?: string;
37
+ settings: AccountSettings;
38
+ /** What the compare-and-swap is made against. `null` means "I believe none exists yet". */
39
+ baseSeq: number | null;
40
+ }
41
+ export interface ListWriteResult {
42
+ /** The version the server says is now current. */
43
+ seq: number;
44
+ /** The sealed INDEX, base64url — the bytes this machine keeps as its copy. */
45
+ ct: string;
46
+ /** The chunks behind that index, for a caller that goes on to write again. */
47
+ held: HeldChunk[];
48
+ }
49
+ /**
50
+ * Write these entries as version 2: the chunks that changed, then the index that names them all.
51
+ *
52
+ * ⛔ ONLY WHAT CHANGED IS SENT. The packer compares the new entries against the chunks the read
53
+ * handed over and keeps every chunk whose contents came through untouched, so a rename uploads
54
+ * one chunk instead of the whole list. That is the entire point of this format version.
55
+ *
56
+ * ⚠ A LOST COMPARE-AND-SWAP COMES BACK AS ITSELF. The caller re-reads and re-applies its intent;
57
+ * the chunks written by the losing attempt are named by no index and the server sweeps them.
58
+ */
59
+ export declare function writeChunkedList(io: ChunkIO, plan: ListWritePlan): Promise<ListWriteResult>;
60
+ /**
61
+ * The chunk names a sealed index carries, or an empty list when these bytes are not one.
62
+ *
63
+ * ⛔ A ROLLBACK NEEDS THIS AND NOTHING ELSE FROM INSIDE THE LIST. Writing an index back as the
64
+ * current version without naming its chunks would let the server free them as unreferenced, and
65
+ * the restored list would open into a drive missing files. So the bytes are opened far enough
66
+ * to read the names, and no further.
67
+ *
68
+ * ⚠ BYTES THAT WILL NOT OPEN ANSWER "no chunks", which is the request `rollback` has always made.
69
+ * A list that does not open cannot be told apart from a version-1 one without opening it, and
70
+ * refusing here would refuse in a case the command used to handle.
71
+ */
72
+ export declare function namedChunks(code: string, ct: string): Promise<string[]>;
73
+ /**
74
+ * The sealed chunks a kept index names, from this machine's own copies alone.
75
+ *
76
+ * Null means the kept bytes are a version-1 list, which carries its entries itself and needs
77
+ * nothing beside it. A refusal means this machine holds the index but not everything it names, so
78
+ * what could be written out would be an incomplete list — worse than none, because somebody would
79
+ * keep it for years believing they were covered.
80
+ */
81
+ export declare function keptChunks(code: string, accountId: string, indexCt: string): Promise<string[] | null>;
@@ -0,0 +1,307 @@
1
+ // Reading and writing the file list as an INDEX plus CHUNKS — NCF-3 §6.3, format version 2.
2
+ //
3
+ // ⛔ THE ONE RULE THIS FILE CARRIES. A chunk that will not arrive, or that arrives with bytes whose
4
+ // hash is not the one the index named, is a list this machine COULD NOT READ COMPLETELY. It is
5
+ // never a shorter list. Every path below refuses by name, because a drive silently missing a
6
+ // chunk's worth of files invites its owner to upload them again — at their own cost, onto a
7
+ // storage network.
8
+ //
9
+ // ⛔ WHAT THE INDEX PINS, RESTATED AS CODE. The index is authenticated (the envelope), continued
10
+ // (`p`), and names every chunk by the hash of its transport string plus how many entries it
11
+ // holds. So the reader re-hashes what it was handed and counts what it opened: a swapped,
12
+ // dropped, duplicated or rolled-back chunk fails one of those two before an entry is used.
13
+ //
14
+ // ⛔ THE CHUNKS ARE WRITTEN BEFORE THE INDEX, always. An index naming a chunk the server does not
15
+ // hold would be a version nobody — including this machine — could open afterwards. The server
16
+ // refuses exactly that, and this ordering is why it never has to.
17
+ //
18
+ // PURE OF COMMANDS: it takes a derived key and a server, and knows nothing about what an edit
19
+ // means. Deciding WHICH entries the list should hold is `manifest-write.ts`'s job; deciding which
20
+ // entry goes in which chunk is the shared packer's.
21
+ import { request, ServerError } from "./api.js";
22
+ import { AAD, DERIVED, loadCrypto } from "./crypto.js";
23
+ import { NmtsError } from "./errors.js";
24
+ import { pruneChunkCache, readCachedChunk, writeCachedChunk } from "./manifest-chunk-cache.js";
25
+ import { keepTrying } from "./net-retry.js";
26
+ import { registerNodeZstd } from "./zstd-node.js";
27
+ import { AAD_FILE_LIST_CHUNK, chunkFingerprint, decodeChunk, decodeFileList, encodeChunk, encodeIndex, FILE_LIST_VERSION_CHUNKED, } from "./shared/lib/drive/manifest-chunks.js";
28
+ import { packAll, repack } from "./shared/lib/drive/manifest-pack.js";
29
+ /** How many chunk requests are in flight at once. Enough to fill a link, few enough to stay fair. */
30
+ const CONCURRENCY = 4;
31
+ const utf8 = new TextEncoder();
32
+ /** A list this machine could not read completely. Never rendered as an empty or shorter drive. */
33
+ export class ManifestChunkError extends NmtsError {
34
+ constructor(message) {
35
+ super(message, {
36
+ exitCode: 1,
37
+ nextStep: "Nothing was changed. The list is stored in pieces and one of them did not arrive whole, " +
38
+ "so what this command could show would be missing files. Try again; if it repeats, open " +
39
+ "the account in a browser and compare before writing anything.",
40
+ });
41
+ this.name = "ManifestChunkError";
42
+ }
43
+ }
44
+ function sealUnder(io, aad, body) {
45
+ return Buffer.from(io.crypt.envelope_seal(io.key, utf8.encode(aad), body)).toString("base64url");
46
+ }
47
+ function openUnder(io, aad, ct) {
48
+ return io.crypt.envelope_open(io.key, utf8.encode(aad), Buffer.from(ct, "base64url"));
49
+ }
50
+ /** What the two chunk routes answer. Narrowed here rather than trusted. */
51
+ function ctOf(answer) {
52
+ const ct = typeof answer === "object" && answer !== null ? Reflect.get(answer, "ct") : null;
53
+ if (typeof ct !== "string" || ct === "") {
54
+ throw new ManifestChunkError("The server answered a part of the file list without its bytes.");
55
+ }
56
+ return ct;
57
+ }
58
+ /**
59
+ * Fetch, verify and open every chunk the index names, four at a time.
60
+ *
61
+ * The result is in INDEX order, which is placement order (§6.3.3), so the list arrives sorted for
62
+ * free. Afterwards the machine's own copies are pruned to what this index names: everything else
63
+ * is a version nobody will ask for again.
64
+ */
65
+ export async function openChunks(io, index) {
66
+ registerNodeZstd();
67
+ const refs = index.chunks;
68
+ const out = new Array(refs.length);
69
+ let next = 0;
70
+ const worker = async () => {
71
+ for (;;) {
72
+ const at = next;
73
+ next += 1;
74
+ const ref = refs[at];
75
+ if (ref === undefined)
76
+ return;
77
+ out[at] = { h: ref.h, f: ref.f, l: ref.l, items: await openOne(io, ref, index.seq) };
78
+ }
79
+ };
80
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, refs.length) }, worker));
81
+ pruneChunkCache(io.accountId, new Set(refs.map((c) => c.h)));
82
+ return out;
83
+ }
84
+ /** One chunk: from this machine if it has it, else from the server — verified either way. */
85
+ async function openOne(io, ref, indexSeq) {
86
+ const cached = readCachedChunk(io.accountId, ref.h);
87
+ let ct = cached;
88
+ if (ct === null) {
89
+ try {
90
+ ct = ctOf(await request(io.server, `/v1/manifest/chunks/${ref.h}`, { token: io.apiKey }));
91
+ }
92
+ catch (error) {
93
+ // ⛔ A chunk the server will not hand over is an INCOMPLETE list, and the command must say
94
+ // so. Carrying on with "the chunks that did arrive" would draw a drive missing files.
95
+ throw new ManifestChunkError(`Part of the file list could not be fetched: ${error instanceof Error ? error.message : String(error)}`);
96
+ }
97
+ }
98
+ // ⛔ RE-HASHED EVEN WHEN IT CAME FROM THIS MACHINE'S OWN COPY. The name is the only thing tying
99
+ // these bytes to the index the account sealed, and a directory on this machine is storage
100
+ // anybody holding the machine can edit.
101
+ if ((await chunkFingerprint(ct)) !== ref.h) {
102
+ throw new ManifestChunkError("Part of the file list does not match the name the index gave it.");
103
+ }
104
+ let body;
105
+ try {
106
+ body = openUnder(io, AAD_FILE_LIST_CHUNK, ct);
107
+ }
108
+ catch {
109
+ throw new ManifestChunkError("Part of the file list did not open with this account's key.");
110
+ }
111
+ const doc = await decodeChunk(body);
112
+ body.fill(0);
113
+ // A chunk keeps the version that MADE it (§6.3.2), so this is an upper bound and not equality:
114
+ // a chunk claiming a version the index has not reached did not come from this history.
115
+ if (doc.seq > indexSeq) {
116
+ throw new ManifestChunkError(`Part of the file list claims version ${doc.seq}, after the index's ${indexSeq}.`);
117
+ }
118
+ if (doc.items.length !== ref.n) {
119
+ throw new ManifestChunkError(`Part of the file list holds ${doc.items.length} entries where the index says ${ref.n}.`);
120
+ }
121
+ if (cached === null)
122
+ writeCachedChunk(io.accountId, ref.h, ct);
123
+ return doc.items;
124
+ }
125
+ /**
126
+ * Write these entries as version 2: the chunks that changed, then the index that names them all.
127
+ *
128
+ * ⛔ ONLY WHAT CHANGED IS SENT. The packer compares the new entries against the chunks the read
129
+ * handed over and keeps every chunk whose contents came through untouched, so a rename uploads
130
+ * one chunk instead of the whole list. That is the entire point of this format version.
131
+ *
132
+ * ⚠ A LOST COMPARE-AND-SWAP COMES BACK AS ITSELF. The caller re-reads and re-applies its intent;
133
+ * the chunks written by the losing attempt are named by no index and the server sweeps them.
134
+ */
135
+ export async function writeChunkedList(io, plan) {
136
+ registerNodeZstd();
137
+ const packed = plan.previous.length > 0 ? repack(plan.previous, plan.entries) : packAll(plan.entries);
138
+ const rows = [];
139
+ const held = [];
140
+ const fresh = new Map();
141
+ for (const chunk of packed) {
142
+ let name = chunk.reuse;
143
+ if (name === undefined) {
144
+ const body = await encodeChunk({
145
+ v: FILE_LIST_VERSION_CHUNKED,
146
+ seq: plan.seq,
147
+ items: chunk.items,
148
+ });
149
+ const ct = sealUnder(io, AAD_FILE_LIST_CHUNK, body);
150
+ body.fill(0);
151
+ name = await chunkFingerprint(ct);
152
+ fresh.set(name, ct);
153
+ }
154
+ rows.push({ h: name, n: chunk.items.length, f: chunk.f, l: chunk.l });
155
+ held.push({ h: name, f: chunk.f, l: chunk.l, items: chunk.items });
156
+ }
157
+ await putChunks(io, fresh);
158
+ const body = await encodeIndex({
159
+ v: FILE_LIST_VERSION_CHUNKED,
160
+ seq: plan.seq,
161
+ ...(plan.prev !== undefined ? { p: plan.prev } : {}),
162
+ settings: plan.settings,
163
+ chunks: rows,
164
+ });
165
+ // The index is sealed under the file-list label the single blob always used, so a build that
166
+ // does not know version 2 opens it and refuses it by version — the designed outcome of §6.1.
167
+ const ct = sealUnder(io, AAD.fileList, body);
168
+ body.fill(0);
169
+ const refs = rows.map((r) => r.h);
170
+ const seq = await putIndex(io, plan.baseSeq, ct, refs, fresh);
171
+ pruneChunkCache(io.accountId, new Set(refs));
172
+ return { seq, ct, held };
173
+ }
174
+ /**
175
+ * Write the index, and answer the one refusal that has a mechanical remedy.
176
+ *
177
+ * `MANIFEST_CHUNK_MISSING` says the server does not hold a chunk this index names. The remedy is
178
+ * to write this save's chunks again and send the same index once more: a chunk is stored under its
179
+ * own hash, so re-writing one the server already has stores nothing and answers "existed". Chunks
180
+ * this save REUSED are sent from this machine's own copies, which is the only place their exact
181
+ * bytes still are — re-sealing would give a fresh nonce and therefore a different name.
182
+ */
183
+ async function putIndex(io, baseSeq, ct, refs, fresh) {
184
+ for (let attempt = 0;; attempt += 1) {
185
+ try {
186
+ return seqOf(await request(io.server, "/v1/manifest", {
187
+ method: "PUT",
188
+ token: io.apiKey,
189
+ body: { base_seq: baseSeq, ct, refs },
190
+ }));
191
+ }
192
+ catch (error) {
193
+ const missing = error instanceof ServerError && error.code === "MANIFEST_CHUNK_MISSING";
194
+ if (!missing || attempt > 0)
195
+ throw error;
196
+ const again = new Map(fresh);
197
+ for (const name of refs) {
198
+ if (again.has(name))
199
+ continue;
200
+ const kept = readCachedChunk(io.accountId, name);
201
+ if (kept !== null)
202
+ again.set(name, kept);
203
+ }
204
+ await putChunks(io, again);
205
+ }
206
+ }
207
+ }
208
+ /**
209
+ * Write these sealed chunks, four at a time.
210
+ *
211
+ * ⚠ REPEATED ON THE SERVER'S "NOT RIGHT NOW". The chunk budget is a burst and then one write a
212
+ * second, so a save that converts a large list meets it. Repeating this particular write is
213
+ * safe in a way an ordinary write is not: the name IS the hash of the bytes, so a second copy of
214
+ * the request is the same request and the server answers "existed" rather than storing anything.
215
+ */
216
+ async function putChunks(io, chunks) {
217
+ const names = [...chunks.keys()];
218
+ let next = 0;
219
+ const worker = async () => {
220
+ for (;;) {
221
+ const at = next;
222
+ next += 1;
223
+ const name = names[at];
224
+ if (name === undefined)
225
+ return;
226
+ const ct = chunks.get(name);
227
+ if (ct === undefined)
228
+ continue;
229
+ await keepTrying(() => request(io.server, `/v1/manifest/chunks/${name}`, {
230
+ method: "PUT",
231
+ token: io.apiKey,
232
+ body: { ct },
233
+ }), { retryable: (error) => error instanceof ServerError && error.status === 429 });
234
+ writeCachedChunk(io.accountId, name, ct);
235
+ }
236
+ };
237
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, names.length) }, worker));
238
+ }
239
+ function seqOf(answer) {
240
+ if (typeof answer === "object" && answer !== null) {
241
+ const seq = Reflect.get(answer, "seq");
242
+ if (typeof seq === "number" && Number.isSafeInteger(seq) && seq >= 1)
243
+ return seq;
244
+ }
245
+ throw new NmtsError("The file list was written but the server did not say which version it is now.", {
246
+ nextStep: "The change is saved. Run `nmts ls` to see it.",
247
+ });
248
+ }
249
+ /**
250
+ * The chunk names a sealed index carries, or an empty list when these bytes are not one.
251
+ *
252
+ * ⛔ A ROLLBACK NEEDS THIS AND NOTHING ELSE FROM INSIDE THE LIST. Writing an index back as the
253
+ * current version without naming its chunks would let the server free them as unreferenced, and
254
+ * the restored list would open into a drive missing files. So the bytes are opened far enough
255
+ * to read the names, and no further.
256
+ *
257
+ * ⚠ BYTES THAT WILL NOT OPEN ANSWER "no chunks", which is the request `rollback` has always made.
258
+ * A list that does not open cannot be told apart from a version-1 one without opening it, and
259
+ * refusing here would refuse in a case the command used to handle.
260
+ */
261
+ export async function namedChunks(code, ct) {
262
+ registerNodeZstd();
263
+ const crypt = await loadCrypto();
264
+ const [from, to] = DERIVED.fileListKey;
265
+ const derived = crypt.kdf_derive(crypt.account_code_parse(code));
266
+ const key = derived.slice(from, to);
267
+ derived.fill(0);
268
+ try {
269
+ const body = crypt.envelope_open(key, utf8.encode(AAD.fileList), Buffer.from(ct, "base64url"));
270
+ const doc = await decodeFileList(body);
271
+ body.fill(0);
272
+ return doc.v === FILE_LIST_VERSION_CHUNKED ? doc.index.chunks.map((c) => c.h) : [];
273
+ }
274
+ catch {
275
+ return [];
276
+ }
277
+ finally {
278
+ key.fill(0);
279
+ }
280
+ }
281
+ /**
282
+ * The sealed chunks a kept index names, from this machine's own copies alone.
283
+ *
284
+ * Null means the kept bytes are a version-1 list, which carries its entries itself and needs
285
+ * nothing beside it. A refusal means this machine holds the index but not everything it names, so
286
+ * what could be written out would be an incomplete list — worse than none, because somebody would
287
+ * keep it for years believing they were covered.
288
+ */
289
+ export async function keptChunks(code, accountId, indexCt) {
290
+ const names = await namedChunks(code, indexCt);
291
+ if (names.length === 0)
292
+ return null;
293
+ const out = [];
294
+ for (const name of names) {
295
+ const ct = readCachedChunk(accountId, name);
296
+ if (ct === null) {
297
+ throw new NmtsError("This machine holds the file list's index but not all of its parts.", {
298
+ exitCode: 4,
299
+ nextStep: "Nothing was written. Run `nmts ls` once while this machine can reach the server: a " +
300
+ "read of the list fetches every part and keeps it here, and this command writes them " +
301
+ "out together with the index.",
302
+ });
303
+ }
304
+ out.push(ct);
305
+ }
306
+ return out;
307
+ }
@@ -0,0 +1,23 @@
1
+ import type { ManifestEntry } from "./shared/lib/drive/manifest-codec.ts";
2
+ export interface CreateListInput {
3
+ server: string;
4
+ apiKey: string;
5
+ /** The NMTS key. Used to derive the file-list key, and not kept. */
6
+ code: string;
7
+ accountId: string;
8
+ }
9
+ export interface CreateListResult {
10
+ /** The version the server says is now current. 1 for a list that had nothing before it. */
11
+ seq: number;
12
+ }
13
+ /**
14
+ * Seal these entries as store version 1 and write them, or refuse because a list already exists.
15
+ *
16
+ * ⛔ NO `prev` LINK, because there is nothing before this. The first version is the one version
17
+ * that is allowed not to name what it continued from; every version after it must, or the fork
18
+ * check has a hole exactly where a fork would be introduced.
19
+ *
20
+ * ⛔ NO SETTINGS EITHER. Account settings live in this blob or nowhere, and a rebuild has none to
21
+ * carry: they were in the list that was lost. Writing an empty set is not a loss caused here.
22
+ */
23
+ export declare function createFirstList(input: CreateListInput, entries: readonly ManifestEntry[]): Promise<CreateListResult>;
@@ -1,4 +1,6 @@
1
- // Writing VERSION 1 of an account's sealed file list — the only write that builds on nothing.
1
+ // Writing THE FIRST VERSION of an account's sealed file list — the only write that builds on
2
+ // nothing. ("Version 1" here is the store version — `seq` — and not the sealed format version:
3
+ // what this writes is format version 2, an index plus its chunks, like every other write.)
2
4
  //
3
5
  // ⛔ IT IS A SEPARATE DOOR FROM EVERY OTHER WRITE, AND THAT IS THE POINT. Ordinary edits read the
4
6
  // current list, apply an intent to it and hand the server the version they built on; there is
@@ -12,17 +14,17 @@
12
14
  // ⛔ AND THE CALLER MUST STILL LOOK FIRST. The server's refusal is the last line, not the first:
13
15
  // reading the list before building one is what lets this tool say "this account already has a
14
16
  // file list" without spending a listing of the whole account first.
15
- import { request, ServerError } from "./api.js";
16
- import { AAD, DERIVED, loadCrypto } from "./crypto.js";
17
+ import { ServerError } from "./api.js";
18
+ import { DERIVED, loadCrypto } from "./crypto.js";
17
19
  import { NmtsError } from "./errors.js";
20
+ import { writeChunkedList } from "./manifest-chunk-flow.js";
18
21
  import { recordWrittenList } from "./manifest.js";
19
- import { encodeManifest } from "./shared/lib/drive/manifest-codec.js";
20
22
  /**
21
- * Seal these entries as version 1 and write them, or refuse because a list already exists.
23
+ * Seal these entries as store version 1 and write them, or refuse because a list already exists.
22
24
  *
23
- * ⛔ NO `prev` LINK, because there is nothing before this. Version 1 is the one version that is
24
- * allowed not to name what it continued from; every version after it must, or the fork check
25
- * has a hole exactly where a fork would be introduced.
25
+ * ⛔ NO `prev` LINK, because there is nothing before this. The first version is the one version
26
+ * that is allowed not to name what it continued from; every version after it must, or the fork
27
+ * check has a hole exactly where a fork would be introduced.
26
28
  *
27
29
  * ⛔ NO SETTINGS EITHER. Account settings live in this blob or nowhere, and a rebuild has none to
28
30
  * carry: they were in the list that was lost. Writing an empty set is not a loss caused here.
@@ -33,19 +35,24 @@ export async function createFirstList(input, entries) {
33
35
  const derived = crypt.kdf_derive(crypt.account_code_parse(input.code));
34
36
  const key = derived.slice(from, to);
35
37
  derived.fill(0);
38
+ const io = {
39
+ server: input.server,
40
+ apiKey: input.apiKey,
41
+ accountId: input.accountId,
42
+ crypt,
43
+ key,
44
+ };
36
45
  try {
37
- const body = await encodeManifest(entries, 1);
38
- const sealed = crypt.envelope_seal(key, new TextEncoder().encode(AAD.fileList), body);
39
- body.fill(0);
40
- const ct = Buffer.from(sealed).toString("base64url");
41
- let answer;
46
+ let written;
42
47
  try {
43
- answer = await request(input.server, "/v1/manifest", {
44
- method: "PUT",
45
- token: input.apiKey,
48
+ written = await writeChunkedList(io, {
49
+ previous: [],
50
+ entries,
51
+ seq: 1,
52
+ settings: {},
46
53
  // ⛔ `null` IS THE WHOLE SAFETY DEVICE. Any number here would mean "replace the version I
47
54
  // read", which is exactly what a rebuild must never do.
48
- body: { base_seq: null, ct },
55
+ baseSeq: null,
49
56
  });
50
57
  }
51
58
  catch (error) {
@@ -59,23 +66,12 @@ export async function createFirstList(input, entries) {
59
66
  }
60
67
  throw error;
61
68
  }
62
- const seq = seqOf(answer);
63
69
  // ⛔ ONLY NOW. Recording a version the server did not accept would leave this machine believing
64
70
  // in a list that never existed, and then refusing the real one as a rollback.
65
- await recordWrittenList(input.accountId, seq, ct);
66
- return { seq };
71
+ await recordWrittenList(input.accountId, written.seq, written.ct);
72
+ return { seq: written.seq };
67
73
  }
68
74
  finally {
69
75
  key.fill(0);
70
76
  }
71
77
  }
72
- function seqOf(answer) {
73
- if (typeof answer === "object" && answer !== null) {
74
- const seq = Reflect.get(answer, "seq");
75
- if (typeof seq === "number" && Number.isSafeInteger(seq) && seq >= 1)
76
- return seq;
77
- }
78
- throw new NmtsError("The file list was written but the server did not say which version it is now.", {
79
- nextStep: "The list is saved. Run `nmts ls` to see it.",
80
- });
81
- }