@needmoretruth/nmts-cli 0.17.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 (151) hide show
  1. package/AGENTS.md +591 -0
  2. package/LICENSE +202 -0
  3. package/LICENSING.md +49 -0
  4. package/README.ko.md +641 -0
  5. package/README.md +657 -0
  6. package/dist/account-proof.js +78 -0
  7. package/dist/account.js +75 -0
  8. package/dist/api-key.js +310 -0
  9. package/dist/api.js +271 -0
  10. package/dist/args.js +137 -0
  11. package/dist/artifact-about.js +70 -0
  12. package/dist/autonomy.js +98 -0
  13. package/dist/code-access.js +76 -0
  14. package/dist/code-vault.js +233 -0
  15. package/dist/collision.js +133 -0
  16. package/dist/commands/balance.js +93 -0
  17. package/dist/commands/consent.js +77 -0
  18. package/dist/commands/create.js +356 -0
  19. package/dist/commands/env.js +136 -0
  20. package/dist/commands/expiring.js +167 -0
  21. package/dist/commands/extend.js +236 -0
  22. package/dist/commands/get.js +130 -0
  23. package/dist/commands/kit.js +136 -0
  24. package/dist/commands/listfile.js +105 -0
  25. package/dist/commands/login.js +335 -0
  26. package/dist/commands/logout.js +33 -0
  27. package/dist/commands/ls.js +202 -0
  28. package/dist/commands/marks.js +176 -0
  29. package/dist/commands/mcp.js +139 -0
  30. package/dist/commands/mode.js +54 -0
  31. package/dist/commands/on-collision.js +45 -0
  32. package/dist/commands/organise.js +296 -0
  33. package/dist/commands/public-code.js +89 -0
  34. package/dist/commands/pull.js +185 -0
  35. package/dist/commands/push.js +278 -0
  36. package/dist/commands/put.js +282 -0
  37. package/dist/commands/rebuild.js +160 -0
  38. package/dist/commands/receive.js +125 -0
  39. package/dist/commands/recovery-list.js +104 -0
  40. package/dist/commands/recovery.js +291 -0
  41. package/dist/commands/s3.js +241 -0
  42. package/dist/commands/settings.js +28 -0
  43. package/dist/commands/share.js +276 -0
  44. package/dist/commands/sweep.js +240 -0
  45. package/dist/commands/trash.js +288 -0
  46. package/dist/commands/trial.js +264 -0
  47. package/dist/commands/update.js +169 -0
  48. package/dist/commands/usage.js +84 -0
  49. package/dist/commands/verify.js +331 -0
  50. package/dist/commands/wallet.js +122 -0
  51. package/dist/commands/whoami.js +33 -0
  52. package/dist/consent.js +200 -0
  53. package/dist/credentials.js +311 -0
  54. package/dist/crypto-surface.js +54 -0
  55. package/dist/crypto.js +137 -0
  56. package/dist/download-part.js +144 -0
  57. package/dist/download-sink.js +213 -0
  58. package/dist/download.js +163 -0
  59. package/dist/drive-paths.js +170 -0
  60. package/dist/environment.js +190 -0
  61. package/dist/errors.js +71 -0
  62. package/dist/exit.js +107 -0
  63. package/dist/expiry.js +115 -0
  64. package/dist/extend-chain.js +137 -0
  65. package/dist/extend-plan.js +149 -0
  66. package/dist/extend-sign.js +130 -0
  67. package/dist/guards.js +15 -0
  68. package/dist/help.js +192 -0
  69. package/dist/human-check.js +54 -0
  70. package/dist/item-trash.js +25 -0
  71. package/dist/kit-file.js +133 -0
  72. package/dist/list-file.js +71 -0
  73. package/dist/list-view-find.js +71 -0
  74. package/dist/list-view-order.js +96 -0
  75. package/dist/main.js +394 -0
  76. package/dist/manifest-create.js +81 -0
  77. package/dist/manifest-write.js +244 -0
  78. package/dist/manifest.js +213 -0
  79. package/dist/mark-render.js +45 -0
  80. package/dist/mcp-args.js +154 -0
  81. package/dist/mcp-tools/context.js +50 -0
  82. package/dist/mcp-tools/files.js +133 -0
  83. package/dist/mcp-tools/organise.js +128 -0
  84. package/dist/mcp-tools/reads.js +89 -0
  85. package/dist/mcp-tools/share.js +61 -0
  86. package/dist/mcp.js +142 -0
  87. package/dist/net-retry.js +84 -0
  88. package/dist/network.js +43 -0
  89. package/dist/notice.js +45 -0
  90. package/dist/product.js +38 -0
  91. package/dist/progress.js +110 -0
  92. package/dist/prompt.js +145 -0
  93. package/dist/rebuild.js +255 -0
  94. package/dist/recovery-assemble.js +148 -0
  95. package/dist/recovery-build.js +260 -0
  96. package/dist/recovery-map-file.js +114 -0
  97. package/dist/recovery-map.js +191 -0
  98. package/dist/recovery-release.js +229 -0
  99. package/dist/recovery-seq.js +63 -0
  100. package/dist/recovery-source.js +143 -0
  101. package/dist/registration.js +83 -0
  102. package/dist/s3/listing.js +134 -0
  103. package/dist/s3/multipart.js +104 -0
  104. package/dist/s3/response-sink.js +41 -0
  105. package/dist/s3/same-file.js +117 -0
  106. package/dist/s3/server.js +252 -0
  107. package/dist/s3/sigv4.js +168 -0
  108. package/dist/s3/staging.js +87 -0
  109. package/dist/s3/xml.js +80 -0
  110. package/dist/safe-path.js +89 -0
  111. package/dist/seal.js +206 -0
  112. package/dist/secret-reader.js +112 -0
  113. package/dist/server.js +33 -0
  114. package/dist/session.js +49 -0
  115. package/dist/setup-questions.js +31 -0
  116. package/dist/share.js +195 -0
  117. package/dist/shared/lib/crypto/size-padding.js +126 -0
  118. package/dist/shared/lib/drive/manifest-codec.js +287 -0
  119. package/dist/shared/lib/drive/manifest-index.js +235 -0
  120. package/dist/shared/lib/drive/manifest-ops.js +267 -0
  121. package/dist/shared/lib/drive/manifest-settings.js +65 -0
  122. package/dist/shared/lib/drive/name-conflict.js +121 -0
  123. package/dist/shared/lib/drive/unique-name.js +48 -0
  124. package/dist/shared/lib/extend/epochs.js +256 -0
  125. package/dist/shared/lib/net/retry-budget.js +95 -0
  126. package/dist/shared/lib/share/shared-file-info.js +65 -0
  127. package/dist/shared/lib/storage-network.js +64 -0
  128. package/dist/shared/lib/upload/part-plan.js +28 -0
  129. package/dist/stdout.js +109 -0
  130. package/dist/trash-sweep.js +123 -0
  131. package/dist/units.js +19 -0
  132. package/dist/update-check.js +164 -0
  133. package/dist/update-source.js +143 -0
  134. package/dist/upload-api.js +98 -0
  135. package/dist/upload-file.js +242 -0
  136. package/dist/upload-price.js +117 -0
  137. package/dist/upload-steps.js +111 -0
  138. package/dist/upload-store.js +252 -0
  139. package/dist/upload-wire.js +25 -0
  140. package/dist/upload.js +294 -0
  141. package/dist/usage-report.js +54 -0
  142. package/dist/wallet-chain.js +76 -0
  143. package/dist/wallet.js +209 -0
  144. package/dist/walrus-write.js +188 -0
  145. package/dist/walrus.js +156 -0
  146. package/package.json +56 -0
  147. package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +469 -0
  148. package/vendor/nmts-crypto/nmts_crypto_wasm.js +1300 -0
  149. package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
  150. package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +55 -0
  151. package/vendor/nmts-crypto/package.json +17 -0
@@ -0,0 +1,48 @@
1
+ // Give an uploaded file a name that is not already in use in its destination folder —
2
+ // `report.pdf` → `report (2).pdf`. ⚠ PUBLISHED — copied byte-for-byte into the `nmts` command-line
3
+ // package; keep comments self-contained English.
4
+ //
5
+ // WHY THIS INSTEAD OF OVERWRITING: the alternative on the table was
6
+ // "same name replaces the old file". Replacing destroys data that cannot be got back — NMTS
7
+ // keeps no previous versions, so an accidental same-name upload would be permanent loss — and
8
+ // keeping previous versions would mean paying Walrus storage for every generation, out of the
9
+ // user's own wallet. Suffixing costs nothing, loses nothing, and is what every desktop does.
10
+ //
11
+ // EXACT-MATCH COMPARISON, deliberately: the drive treats `A.txt` and `a.txt` as two files, so this
12
+ // must too, or uploading `a.txt` next to an existing `A.txt` would rename a file the user can
13
+ // see is differently named. Case-insensitive filesystems are handled at the other end — the bulk
14
+ // download's `freeName()` already suffixes on collision when writing to disk.
15
+ //
16
+ // WHERE THE TAKEN SET COMES FROM: the account's sealed file list, in memory (`namesIn`). It used
17
+ // to be one server listing per destination folder, decrypted name by name — which could FAIL,
18
+ // and a failed name check had to refuse the upload outright. That whole failure mode is gone.
19
+ //
20
+ // Pure: no imports at all, so `node --test` covers it directly.
21
+ /** Where the extension starts, or -1. A leading dot is part of the name, not a separator. */
22
+ function extIndex(filename) {
23
+ const dot = filename.lastIndexOf(".");
24
+ return dot > 0 ? dot : -1;
25
+ }
26
+ /**
27
+ * A name not present in `taken`.
28
+ *
29
+ * Returns `desired` untouched when it is free. Otherwise inserts ` (n)` before the extension,
30
+ * starting at 2, until it finds a free one — matching desktop behaviour so nobody has to learn a
31
+ * new convention. `taken` is not mutated; callers uploading several files at once must add each
32
+ * returned name themselves, or a batch of identical names would all resolve to the same `(2)`.
33
+ */
34
+ export function uniqueFileName(desired, taken) {
35
+ if (!taken.has(desired))
36
+ return desired;
37
+ const dot = extIndex(desired);
38
+ const stem = dot === -1 ? desired : desired.slice(0, dot);
39
+ const ext = dot === -1 ? "" : desired.slice(dot);
40
+ // Bounded: an unbounded loop here would hang the upload rather than fail it. 10k duplicates of
41
+ // one name in one folder is far past any real use, and the timestamp fallback is still unique.
42
+ for (let n = 2; n < 10_000; n += 1) {
43
+ const candidate = `${stem} (${n})${ext}`;
44
+ if (!taken.has(candidate))
45
+ return candidate;
46
+ }
47
+ return `${stem} (${new Date().toISOString().replace(/[:.]/g, "-")})${ext}`;
48
+ }
@@ -0,0 +1,256 @@
1
+ // The half of storage-lease reasoning that never speaks to a network: when a lease runs out, when
2
+ // to start warning, and how much more time can still be bought. Numbers in, numbers out.
3
+ //
4
+ // ⛔ WHY IT IS NOT IN `chain.ts` ANY MORE. The command-line tool has to answer the same questions,
5
+ // and that file also imports the wallet client, which a command-line package cannot follow. So
6
+ // the tool had a SECOND IMPLEMENTATION of this arithmetic, and the only thing a machine compared
7
+ // between them was four threshold numbers. ⚠ COMPARING VALUES DOES NOT STOP TWO PROGRAMS
8
+ // COMPUTING DIFFERENT ANSWERS FROM THEM — and the answer here is printed on a screen where
9
+ // somebody decides whether to spend money. This file is now copied byte for byte into that
10
+ // package, so both programs run the same functions.
11
+ //
12
+ // ⛔ DO NOT ADD AN IMPORT HERE. The moment this file imports anything the copy cannot resolve, the
13
+ // copy breaks and the tool goes back to writing its own. Everything that talks to the chain
14
+ // belongs in `chain.ts`, which re-exports these names so callers need not know the split exists.
15
+ //
16
+ // ⛔ AND IT IS ENGLISH, unlike most notes in this tree: these exact bytes are published in a
17
+ // separate public repository, whose checks refuse Korean and refuse pointers into documents
18
+ // nobody outside can read.
19
+ //
20
+ // PURE: not even a clock — `nowMs` is always passed in. That is what lets a plain test drive the
21
+ // branches a real network only reaches while it is changing epochs.
22
+ /**
23
+ * The wall-clock moment an epoch is reached, from the network's own epoch clock. Null when there
24
+ * is no anchor to measure from.
25
+ *
26
+ * ⚠ An ESTIMATE, and labelled as one wherever it is shown: epoch changes can run late, and the
27
+ * measured history proves it (epoch 471 began 9 days later than `first_epoch_start` plus 470 × the
28
+ * epoch length would put it). Never present this as the exact deletion time.
29
+ */
30
+ export function epochDate(window, epoch) {
31
+ // ⚠ `== null` catches undefined as well as null. `LeaseClock.startedMs` is optional, so a
32
+ // `=== null` test lets undefined through and the arithmetic produces an Invalid Date.
33
+ if (window.startedMs == null || !Number.isFinite(window.durationMs))
34
+ return null;
35
+ return new Date(window.startedMs + (epoch - window.current) * window.durationMs);
36
+ }
37
+ /**
38
+ * ⭐ THE ONE PIECE OF ARITHMETIC EVERY EXPIRY SURFACE STANDS ON: how long until `epoch` arrives,
39
+ * in ms from `nowMs`, as a RANGE.
40
+ *
41
+ * `epochDate` refuses to answer without an exact anchor (`startedMs`), and the network lives in
42
+ * that state most of the time — so the extend sheet said "cannot be determined" over a file whose
43
+ * end epoch and epoch length were both known. Not knowing exactly is not the same as knowing
44
+ * nothing, and drawing one as the other loses a real answer. What IS known pins the moment to one
45
+ * epoch's width: we are somewhere
46
+ * inside epoch `current` and cannot tell how far in, so `epoch` arrives between (epoch−current−1)
47
+ * and (epoch−current) epoch-lengths from now.
48
+ *
49
+ * ⛔ `max` — (epoch−current) whole epochs — IS THE DEFECT. It is the OPTIMISTIC edge:
50
+ * it silently assumes the current epoch has not started yet. On mainnet, where an epoch is
51
+ * fourteen days, a file was told it had 28 days left when the chain said 23 (measured 2026-08-02,
52
+ * 2026-08-02). Anything shown to a person about deletion takes `min` — it can only warn early,
53
+ * never late — and the words beside it then say "or more" / "as early as", because a floor is not
54
+ * a measurement.
55
+ *
56
+ * With an exact anchor both ends collapse onto one moment (`exact: true`). Null only when the
57
+ * epoch length itself is missing — a window `readEpochWindow` never produces.
58
+ */
59
+ function epochArrival(clock, epoch, nowMs) {
60
+ if (!Number.isFinite(clock.durationMs) || clock.durationMs <= 0)
61
+ return null;
62
+ const ahead = epoch - clock.current;
63
+ const started = clock.startedMs;
64
+ if (started != null && Number.isFinite(started)) {
65
+ const at = started + ahead * clock.durationMs - nowMs;
66
+ return { minMs: at, maxMs: at, exact: true };
67
+ }
68
+ return {
69
+ minMs: (ahead - 1) * clock.durationMs,
70
+ maxMs: ahead * clock.durationMs,
71
+ exact: false,
72
+ };
73
+ }
74
+ /**
75
+ * The window an epoch can arrive in — an answer even when the clock is only APPROXIMATE.
76
+ *
77
+ * ⚠ The two ends are clamped at "now" only on the approximate branch, where a negative bound is an
78
+ * artefact of not knowing how far into the current epoch we are. An anchored date that lands in the
79
+ * past is a real answer (the lease ended on that day) and is returned as it is.
80
+ */
81
+ export function epochDateBounds(clock, epoch, nowMs) {
82
+ const a = epochArrival(clock, epoch, nowMs);
83
+ if (a === null)
84
+ return null;
85
+ if (a.exact) {
86
+ const at = new Date(nowMs + a.minMs);
87
+ return { earliest: at, latest: at, exact: true };
88
+ }
89
+ return {
90
+ earliest: new Date(nowMs + Math.max(0, a.minMs)),
91
+ latest: new Date(nowMs + Math.max(0, a.maxMs)),
92
+ exact: false,
93
+ };
94
+ }
95
+ /**
96
+ * Warn from here on. Two stages: a plain note, then a red one.
97
+ *
98
+ * ⭐ THEY LIVE HERE, NOT IN THE COMPONENT THAT DRAWS THE BANNER (moved 2026-07-30). Two surfaces
99
+ * now read them — the drive-wide banner (`ExpiryNotice`) and every file row's own status
100
+ * (`lib/drive/file-status.ts`). A copy in each is how a banner ends up shouting "extend now" over
101
+ * a list of rows that all say "sealed", with neither file looking wrong on its own.
102
+ * ⚠ A `.tsx` cannot hold them either: `node --test` reads plain `.ts` and not `.tsx`, so a constant
103
+ * parked in a component is a constant no pure test can check.
104
+ *
105
+ * Both stages are a floor in DAYS and a floor in EPOCHS, and the wider one wins (see
106
+ * `warningEpochs`). The epoch figures are what mainnet is really sized by: 3 epochs is six weeks of
107
+ * runway there, and 1 epoch — the urgent stage — is the last two weeks in which extending is still
108
+ * possible at all.
109
+ */
110
+ export const NOTICE_DAYS = 14;
111
+ export const NOTICE_EPOCHS = 3;
112
+ export const URGENT_DAYS = 3;
113
+ export const URGENT_EPOCHS = 1;
114
+ /**
115
+ * How far ahead the expiry warning starts, in epochs: a floor in DAYS and a floor in EPOCHS, wider
116
+ * one wins.
117
+ *
118
+ * Counted in days alone, mainnet's 14-day epoch made the whole warning exactly ONE epoch wide, so
119
+ * someone who opens the app monthly missed it entirely. Counted in epochs alone, testnet's 1-day
120
+ * epoch would have shrunk the window from 14 days to 3. Taking whichever is longer never narrows
121
+ * either network's warning — and the day-count rounds UP, because arriving a tick late is the
122
+ * quiet failure this whole surface exists to prevent.
123
+ */
124
+ export function warningEpochs(days, floorEpochs, durationMs) {
125
+ const dayMs = 24 * 60 * 60 * 1000;
126
+ return Math.max(Math.ceil((days * dayMs) / durationMs), floorEpochs);
127
+ }
128
+ /**
129
+ * Whole days from now until `epoch` is reached, by the network's own epoch clock.
130
+ *
131
+ * ⛔ THE SAFE EDGE, NOT THE OPTIMISTIC ONE (fixed 2026-08-16). This used to be
132
+ * `(epoch − current) × epochLength`, which ignores the time already spent inside the current epoch
133
+ * — on mainnet that overstates the runway by up to fourteen days, and it did: 28 shown where the
134
+ * chain said 23. Without an anchor the answer is now the floor of `epochArrival`'s `minMs`, and
135
+ * `exact: false` tells the surface to say "N days or more" instead of making a flat claim.
136
+ *
137
+ * ⛔ Never substitute `NETWORK.epochDays` for the clock here. That constant is the MAINNET basis
138
+ * (14); using it on testnet, where an epoch is one day, would report 196 days over a file with
139
+ * fourteen days to live.
140
+ *
141
+ * Null when the clock carries no usable epoch length — the caller says the period is unread rather
142
+ * than counting days from a constant.
143
+ */
144
+ export function daysLeftUntilEpoch(epoch, clock, nowMs) {
145
+ const a = epochArrival(clock, epoch, nowMs);
146
+ if (a === null)
147
+ return null;
148
+ const dayMs = 24 * 60 * 60 * 1000;
149
+ return { days: Math.floor(a.minMs / dayMs), exact: a.exact };
150
+ }
151
+ /**
152
+ * The same answer `daysLeftUntilEpoch` gives, down to the second.
153
+ *
154
+ * ⭐ WHY THIS EXISTS: a day count is the right thing to glance at and the wrong thing to plan by
155
+ * on the last day. "1 day left" covers everything from twenty-four hours to one minute, and by
156
+ * then the only question left is whether there is time to deal with it tonight.
157
+ *
158
+ * ⛔ IT IS THE SAME EDGE AS THE DAY COUNT, on purpose. Both take `epochArrival`'s `minMs`, so the
159
+ * two can never disagree: a countdown reading three hours under a label reading "2 days left"
160
+ * would be two numbers about one fact, and a person believes the reassuring one.
161
+ *
162
+ * ⚠ EVEN EXACT IS AN ESTIMATE, and the direction is known: an epoch change can run LATE, never
163
+ * early, so this can only be early. Measured on mainnet 2026-08-25 — 36 epochs after the first,
164
+ * the accumulated drift was nine minutes.
165
+ *
166
+ * Null on the same input as `daysLeftUntilEpoch`: no usable epoch length.
167
+ */
168
+ export function timeLeftUntilEpoch(epoch, clock, nowMs) {
169
+ const a = epochArrival(clock, epoch, nowMs);
170
+ if (a === null)
171
+ return null;
172
+ const totalMs = Math.max(0, a.minMs);
173
+ const seconds = Math.floor(totalMs / 1000);
174
+ return {
175
+ days: Math.floor(seconds / 86_400),
176
+ hours: Math.floor(seconds / 3_600) % 24,
177
+ minutes: Math.floor(seconds / 60) % 60,
178
+ seconds: seconds % 60,
179
+ totalMs,
180
+ exact: a.exact,
181
+ };
182
+ }
183
+ /**
184
+ * The largest number of epochs these leases can ALL be extended by.
185
+ *
186
+ * The ceiling is per-blob: a lease may not end more than `maxAhead` epochs past the current one,
187
+ * so the blob that already reaches furthest into the future is the binding one. Returns 0 when
188
+ * nothing more can be bought yet — which is a real answer ("already paid as far ahead as the
189
+ * network allows"), not an error.
190
+ */
191
+ export function headroom(leases, window) {
192
+ if (leases.length === 0)
193
+ return 0;
194
+ const furthest = Math.max(...leases.map((l) => l.endEpoch));
195
+ return Math.max(0, window.current + window.maxAhead - furthest);
196
+ }
197
+ /**
198
+ * The enum cases of the network's `epoch_state` that carry WHEN THE CURRENT EPOCH BEGAN.
199
+ *
200
+ * ⛔ THERE ARE TWO OF THEM, AND READING ONLY THE FIRST COST US A FORTNIGHT OF PRECISION. The Move
201
+ * enum has three cases: `EpochChangeSync` (a node count — a small integer, NOT a moment),
202
+ * `EpochChangeDone` (the moment this epoch's change happened), and `NextParamsSelected` (the
203
+ * same moment, kept after the next epoch's parameters are chosen). A network settles into the
204
+ * LAST of those and stays there, so an implementation that accepted `EpochChangeDone` alone
205
+ * threw the anchor away nearly always. Measured on mainnet 2026-08-25: epoch 37 reported
206
+ * `NextParamsSelected` holding 2026-08-11T15:08:58Z — exactly one epoch before its end. Without
207
+ * it `epochArrival` falls back to a range ONE WHOLE EPOCH wide (fourteen days on mainnet), and
208
+ * every surface that counts down to a deletion then says "as early as N days" — honest, and
209
+ * useless to somebody deciding whether to pay for more time.
210
+ *
211
+ * ⛔ THIS IS AN ALLOW-LIST, NOT A DENY-LIST. `EpochChangeSync` carries a `u16`, which is finite and
212
+ * would happily become a date in 1970; and a case this code has not been taught must report NO
213
+ * anchor rather than guess. Losing precision costs a warning that comes early. Guessing costs a
214
+ * deleted file.
215
+ */
216
+ export const EPOCH_START_VARIANTS = ["EpochChangeDone", "NextParamsSelected"];
217
+ /**
218
+ * When the current epoch began, in ms — or null when this reading cannot say.
219
+ *
220
+ * ⭐ ONE OF IT, ON PURPOSE. Both the browser and the command-line tool read the same enum for the
221
+ * same reason, and a second narrowing elsewhere would be a second answer to "has this epoch
222
+ * settled" — the two would drift the day the protocol renames a case, and drift here is a wrong
223
+ * deletion date on a screen where somebody spends money.
224
+ *
225
+ * Read by name rather than cast: a shape change in the protocol has to surface as "no anchor",
226
+ * which costs precision, instead of as a NaN that becomes a date.
227
+ */
228
+ export function epochStartedMs(epochState) {
229
+ if (typeof epochState !== "object" || epochState === null)
230
+ return null;
231
+ const kind = Reflect.get(epochState, "$kind");
232
+ if (typeof kind !== "string" || !EPOCH_START_VARIANTS.includes(kind))
233
+ return null;
234
+ const at = Number(Reflect.get(epochState, kind));
235
+ // 0 is not a moment any epoch began at, and a negative one is a corrupted reading.
236
+ return Number.isFinite(at) && at > 0 ? at : null;
237
+ }
238
+ /**
239
+ * When a file actually runs out: the SOONEST end epoch across the blobs it rides on. Null when
240
+ * there is no lease to read.
241
+ *
242
+ * ⚠ NULL IS NOT A ZERO AND NOT "NOW" (2026-07-28). The sheet that shows this used to
243
+ * substitute the CURRENT epoch when the list was empty, and so printed today's date under "expires
244
+ * on" above the sentence "after this date the file is deleted and cannot be recovered" —
245
+ * telling someone their file dies today. The empty case is reached by real files: every part on
246
+ * treasury-paid storage, or parts old enough to carry no on-chain object id, produce no targets
247
+ * and therefore no leases. There is no epoch to report for those, so this reports none and the
248
+ * caller shows nothing rather than a number it invented.
249
+ *
250
+ * SOONEST, not furthest: one expired blob is enough to make the file unreadable.
251
+ */
252
+ export function soonestEnd(leases) {
253
+ if (leases.length === 0)
254
+ return null;
255
+ return Math.min(...leases.map((l) => l.endEpoch));
256
+ }
@@ -0,0 +1,95 @@
1
+ // Waiting for a network to come back and repeating a failure are different things, and this is the
2
+ // arithmetic that tells them apart. Numbers in, a decision out.
3
+ //
4
+ // ⛔ AND IT IS ENGLISH, unlike most notes in this tree: these exact bytes are copied into the
5
+ // published command-line package, so that both programs answer "have we tried hard enough" the
6
+ // same way. Two policies is two answers, and the one nobody looks at is the one that gives up
7
+ // early.
8
+ //
9
+ // ⛔ WHY IT EXISTS. What this product did about a dropped connection was three attempts, two and
10
+ // four seconds apart, on one step of one upload path; a download had two attempts a quarter of
11
+ // a second apart. Moving between a phone's data and a wifi network takes five to thirty
12
+ // seconds. So the retries were spent before the network came back, every time, and a person was
13
+ // handed a choice they had no way to make: resume, or give up.
14
+ //
15
+ // ⛔ TIME IS THE BUDGET, NOT A COUNT OF ATTEMPTS. "We tried three times" says nothing; three
16
+ // attempts six seconds apart is not trying. What a person means by "keep trying" is a length of
17
+ // time, so that is what is counted.
18
+ //
19
+ // ⛔ BEING OFFLINE DOES NOT SPEND THE BUDGET. There is nothing to retry while there is no network,
20
+ // and charging that time to the budget is how a long tunnel becomes a failed upload. Waiting
21
+ // for the network has its own, longer, bound — it exists so a forgotten process does not hold a
22
+ // file open forever, not to give up on somebody who is briefly out of range.
23
+ //
24
+ // ⚠ WHATEVER SAYS "ONLINE" IS A HINT AND NOTHING MORE. A browser's flag is false when there is
25
+ // certainly no network and true when there might be one, so it is read for the FALSE case only:
26
+ // the caller keeps asking the network itself, and this decides how long to wait between askings.
27
+ //
28
+ // PURE: no DOM, no clock of its own, no randomness of its own — every input is passed in. That is
29
+ // what lets a test drive a tunnel, a flapping link and an exhausted budget on demand.
30
+ /** First wait after a failure, before jitter. */
31
+ export const RETRY_BASE_MS = 1_000;
32
+ /**
33
+ * The longest single wait. Without a ceiling the ninth backoff is eight minutes, and a person
34
+ * watching a progress bar cannot tell that from a program that has stopped.
35
+ */
36
+ export const RETRY_MAX_WAIT_MS = 30_000;
37
+ /**
38
+ * How long to go on retrying while the network is reachable and the far side keeps failing.
39
+ *
40
+ * ⚠ THIS IS THE UPLOAD-SHAPED DEFAULT: money is already spent, the work resumes where it stopped,
41
+ * and nobody is staring at it. A surface where none of that is true passes its own (`budgetMs`).
42
+ */
43
+ export const RETRY_BUDGET_MS = 10 * 60_000;
44
+ /**
45
+ * The budget for something a person is watching and can simply ask for again — a download.
46
+ *
47
+ * ⚠ It is not small because failing is fine; it is small because ten minutes of "still trying" is
48
+ * indistinguishable from a program that has stopped, and pressing again costs nothing here.
49
+ */
50
+ export const WATCHED_RETRY_BUDGET_MS = 60_000;
51
+ /** The matching offline bound for a watched surface. */
52
+ export const WATCHED_OFFLINE_BUDGET_MS = 5 * 60_000;
53
+ /** How long to go on waiting while there is no network at all. Longer, because waiting is free. */
54
+ export const OFFLINE_BUDGET_MS = 30 * 60_000;
55
+ /** How often to look again while offline. The `online` event is the real signal; this is the net. */
56
+ export const OFFLINE_POLL_MS = 2_000;
57
+ /**
58
+ * What to do after one failed attempt.
59
+ *
60
+ * ⛔ THE ORDER OF THE TWO CHECKS MATTERS. Offline is decided first, because a failure that
61
+ * happened because there is no network must not spend the online budget — otherwise a tunnel
62
+ * long enough to exhaust it turns into a failed upload the moment the network returns.
63
+ */
64
+ export function nextAttempt(input) {
65
+ if (!input.online) {
66
+ return {
67
+ again: input.elapsedOfflineMs < (input.offlineBudgetMs ?? OFFLINE_BUDGET_MS),
68
+ waitMs: OFFLINE_POLL_MS,
69
+ waitingForNetwork: true,
70
+ };
71
+ }
72
+ const step = Math.min(RETRY_BASE_MS * 2 ** Math.max(0, input.attempt - 1), RETRY_MAX_WAIT_MS);
73
+ // Jitter spreads parts that failed together: without it every part of one file wakes at the same
74
+ // instant and hits the same host again, which is the load that made them fail.
75
+ const jitter = Math.floor(clampRandom(input.random) * RETRY_BASE_MS);
76
+ return {
77
+ again: input.elapsedOnlineMs < (input.budgetMs ?? RETRY_BUDGET_MS),
78
+ waitMs: step + jitter,
79
+ waitingForNetwork: false,
80
+ };
81
+ }
82
+ /** A caller that hands over something that is not a number must not silently get a fixed wait. */
83
+ function clampRandom(value) {
84
+ if (!Number.isFinite(value) || value < 0)
85
+ return 0;
86
+ return value >= 1 ? 0.999_999 : value;
87
+ }
88
+ /**
89
+ * How much of the budget is left, as a fraction — for a screen that wants to say how long it will
90
+ * go on trying. Null when nothing has been spent on this kind of waiting yet.
91
+ */
92
+ export function budgetLeft(elapsedMs, waitingForNetwork) {
93
+ const total = waitingForNetwork ? OFFLINE_BUDGET_MS : RETRY_BUDGET_MS;
94
+ return Math.max(0, 1 - elapsedMs / total);
95
+ }
@@ -0,0 +1,65 @@
1
+ // What the SENDER seals beside a shared file — the plaintext the recipient opens to learn what
2
+ // they were sent.
3
+ //
4
+ // It used to be the file's name and nothing else: one sealed string, opened and shown. That was
5
+ // enough while the number the server holds was the file's own length. It stopped being enough
6
+ // twice over:
7
+ // 1. The stored size became the SEALED total, so the number a recipient is handed is
8
+ // bytes-on-the-network rather than bytes-of-the-file — larger by a fixed amount per part.
9
+ // A length check against it fails on any file stored in more than one piece.
10
+ // 2. Size padding means a stored stream may be sealed from MORE bytes than the file has, so the
11
+ // padding can only be taken back off by somebody who knows the real length. The owner has it
12
+ // in their own sealed list. A recipient has nothing but what the sender sealed.
13
+ //
14
+ // So this is a small, EXTENSIBLE document rather than a bare string. Adding a field later costs a
15
+ // key here and a fallback there; it does not cost a database column, a migration, or a byte the
16
+ // server can read — which is the whole reason the size lives in here.
17
+ //
18
+ // ⛔ BACKWARD COMPATIBLE IN ONE DIRECTION, ON PURPOSE. Shares sealed before this document existed
19
+ // hold a bare name, and `decode` reads them as exactly that. A recipient running this code
20
+ // against an older share therefore learns the name and NOT the size — which is correct, because
21
+ // a file shared before padding existed has none to strip. If a padded file ever were shared by
22
+ // an old client still sealing bare names, the recipient would write the padding, the whole-file
23
+ // content hash would not match, and the download would be DISCARDED rather than saved wrong.
24
+ // That is the safe direction, and it is why this needed no flag day.
25
+ //
26
+ // ⚠ IT IS COPIED VERBATIM INTO OTHER PROGRAMS, so it depends on nothing. Two programs that seal
27
+ // this document must produce the same bytes: what is sealed here is hashed into the key that
28
+ // wraps the file's own key, so a document that differs by one character makes a share the
29
+ // recipient cannot open.
30
+ /** The marker that tells a document apart from a file that happens to be named like JSON. */
31
+ const FORMAT = "nmts-share-file/1";
32
+ /** The string to seal as `name_share_ct`. */
33
+ export function encodeSharedFileInfo(info) {
34
+ const doc = { f: FORMAT, name: info.name };
35
+ if (info.size !== undefined && Number.isSafeInteger(info.size) && info.size >= 0) {
36
+ doc.size = info.size;
37
+ }
38
+ return JSON.stringify(doc);
39
+ }
40
+ /**
41
+ * Read what the sender sealed. Never throws: a name that cannot be parsed IS the name.
42
+ *
43
+ * The refusal to throw is deliberate. This runs while painting a list of everything shared with a
44
+ * person, and one row whose document is malformed must not take the other rows' names down with it.
45
+ */
46
+ export function decodeSharedFileInfo(sealed) {
47
+ if (!sealed.startsWith("{"))
48
+ return { name: sealed };
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(sealed);
52
+ }
53
+ catch {
54
+ return { name: sealed };
55
+ }
56
+ if (typeof parsed !== "object" || parsed === null)
57
+ return { name: sealed };
58
+ const doc = parsed;
59
+ if (doc.f !== FORMAT || typeof doc.name !== "string")
60
+ return { name: sealed };
61
+ const size = typeof doc.size === "number" && Number.isSafeInteger(doc.size) && doc.size >= 0
62
+ ? doc.size
63
+ : undefined;
64
+ return size === undefined ? { name: doc.name } : { name: doc.name, size };
65
+ }
@@ -0,0 +1,64 @@
1
+ // WHICH STORAGE NETWORK a stored piece lives on — the client half of the registry. The server
2
+ // keeps the same codes in its own item rows. NCF-3 dropped the storage-network section the old
3
+ // format document carried, so these modules ARE the registry now.
4
+ //
5
+ // ⚠ THIS FILE IS PUBLISHED. It is copied byte-for-byte into the `nmts` command-line package.
6
+ // Keep the comments self-contained English, with no pointer only we can follow.
7
+ //
8
+ // WHY THIS EXISTS AT ALL: every placement field NMTS has ever written is Walrus vocabulary —
9
+ // `blob_id`, `patch_id`, `sui_object_id`, `expiry_epoch`. Nothing said so, because until the
10
+ // tiers were decided (BACKLOG §2.2) "the storage network" and "Walrus" were the same thing. A
11
+ // blob id is only meaningful on the network that issued it, so a recovery tool holding one and
12
+ // no network name has to guess which aggregator to ask.
13
+ //
14
+ // TWO SPELLINGS, ONE MEANING — and the split is deliberate:
15
+ // · NUMBER (0/1/2) in the database and in the sealed FILE LIST (NMF-1), where every byte is
16
+ // rewritten on each save and repeated across thousands of entries.
17
+ // · NAME ("walrus") in the RECOVERY MAP (NRM-2), which a standalone tool parses years from now
18
+ // with none of our code beside it. A magic number in that document is a trap; a word is not.
19
+ // This mirrors the existing `kind` field, which is `1` in the file list and `"file"` in the map.
20
+ //
21
+ // ⚠ NOT A TIER MAPPING. 「NMTS Fast」/「NMTS Heavy」 are product names for what the person buys;
22
+ // these are the networks underneath. The screen keeps its own mapping (components/drive/
23
+ // StorageTier.tsx) so a rename of either never silently redefines the other.
24
+ //
25
+ // FAILURE MODES: none at runtime — pure functions over constants. An UNKNOWN code answers `null`
26
+ // rather than falling back to Walrus: a wrong network recorded in a recovery map is unfixable
27
+ // years later, so callers are made to decide what to do about it.
28
+ /** Network codes, as stored. Fixed forever once a row carries one. */
29
+ export const NETWORK_WALRUS = 0;
30
+ /** Filecoin — product tier 「NMTS Heavy」. Reserved; no upload path exists yet. */
31
+ export const NETWORK_FILECOIN = 1;
32
+ /** Arweave — reserved and deliberately last: it cannot be deleted, ever (BACKLOG §2.2). */
33
+ export const NETWORK_ARWEAVE = 2;
34
+ const BY_CODE = {
35
+ [NETWORK_WALRUS]: "walrus",
36
+ [NETWORK_FILECOIN]: "filecoin",
37
+ [NETWORK_ARWEAVE]: "arweave",
38
+ };
39
+ const BY_NAME = {
40
+ walrus: NETWORK_WALRUS,
41
+ filecoin: NETWORK_FILECOIN,
42
+ arweave: NETWORK_ARWEAVE,
43
+ };
44
+ /**
45
+ * Code → the name a recovery map stores. `null` for a code this build has never heard of.
46
+ *
47
+ * A newer client could store a network this one predates. Returning null makes that visible to
48
+ * the caller instead of mislabelling someone's bytes.
49
+ */
50
+ export function networkName(code) {
51
+ return BY_CODE[code] ?? null;
52
+ }
53
+ /** Name → code. `null` for anything not in the registry. */
54
+ export function networkCode(name) {
55
+ return BY_NAME[name] ?? null;
56
+ }
57
+ /**
58
+ * What an ABSENT network field means: Walrus.
59
+ *
60
+ * This is a fact, not a fallback. No other network has ever had an upload path, so every part,
61
+ * entry and map written before the field existed is on Walrus by construction. Stated as a named
62
+ * constant so the reasoning sits next to every use of it rather than being re-derived.
63
+ */
64
+ export const NETWORK_WHEN_UNRECORDED = NETWORK_WALRUS;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Split a plaintext length into ordered, contiguous, non-overlapping ranges of at most
3
+ * `partSizeBytes` each; the last one carries the remainder.
4
+ *
5
+ * The ranges tile `[0, plaintextLen)` exactly — the lengths sum to `plaintextLen` — and a file that
6
+ * fits in one part is simply the one-range case, so a caller never needs a separate path for it.
7
+ *
8
+ * Throws on a non-positive length or part size rather than coercing: both are caller mistakes, and
9
+ * a silently corrected one would produce a plan that does not describe the file.
10
+ */
11
+ export function planParts(plaintextLen, partSizeBytes) {
12
+ if (!Number.isFinite(plaintextLen) || plaintextLen <= 0) {
13
+ throw new RangeError(`planParts needs a positive plaintextLen, got ${String(plaintextLen)}`);
14
+ }
15
+ if (!Number.isFinite(partSizeBytes) || partSizeBytes <= 0) {
16
+ throw new RangeError(`planParts needs a positive partSizeBytes, got ${String(partSizeBytes)}`);
17
+ }
18
+ const ranges = [];
19
+ let offset = 0;
20
+ let partIndex = 0;
21
+ while (offset < plaintextLen) {
22
+ const length = Math.min(partSizeBytes, plaintextLen - offset);
23
+ ranges.push({ partIndex, offset, length });
24
+ offset += length;
25
+ partIndex += 1;
26
+ }
27
+ return ranges;
28
+ }