@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.
- package/AGENTS.md +591 -0
- package/LICENSE +202 -0
- package/LICENSING.md +49 -0
- package/README.ko.md +641 -0
- package/README.md +657 -0
- package/dist/account-proof.js +78 -0
- package/dist/account.js +75 -0
- package/dist/api-key.js +310 -0
- package/dist/api.js +271 -0
- package/dist/args.js +137 -0
- package/dist/artifact-about.js +70 -0
- package/dist/autonomy.js +98 -0
- package/dist/code-access.js +76 -0
- package/dist/code-vault.js +233 -0
- package/dist/collision.js +133 -0
- package/dist/commands/balance.js +93 -0
- package/dist/commands/consent.js +77 -0
- package/dist/commands/create.js +356 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/expiring.js +167 -0
- package/dist/commands/extend.js +236 -0
- package/dist/commands/get.js +130 -0
- package/dist/commands/kit.js +136 -0
- package/dist/commands/listfile.js +105 -0
- package/dist/commands/login.js +335 -0
- package/dist/commands/logout.js +33 -0
- package/dist/commands/ls.js +202 -0
- package/dist/commands/marks.js +176 -0
- package/dist/commands/mcp.js +139 -0
- package/dist/commands/mode.js +54 -0
- package/dist/commands/on-collision.js +45 -0
- package/dist/commands/organise.js +296 -0
- package/dist/commands/public-code.js +89 -0
- package/dist/commands/pull.js +185 -0
- package/dist/commands/push.js +278 -0
- package/dist/commands/put.js +282 -0
- package/dist/commands/rebuild.js +160 -0
- package/dist/commands/receive.js +125 -0
- package/dist/commands/recovery-list.js +104 -0
- package/dist/commands/recovery.js +291 -0
- package/dist/commands/s3.js +241 -0
- package/dist/commands/settings.js +28 -0
- package/dist/commands/share.js +276 -0
- package/dist/commands/sweep.js +240 -0
- package/dist/commands/trash.js +288 -0
- package/dist/commands/trial.js +264 -0
- package/dist/commands/update.js +169 -0
- package/dist/commands/usage.js +84 -0
- package/dist/commands/verify.js +331 -0
- package/dist/commands/wallet.js +122 -0
- package/dist/commands/whoami.js +33 -0
- package/dist/consent.js +200 -0
- package/dist/credentials.js +311 -0
- package/dist/crypto-surface.js +54 -0
- package/dist/crypto.js +137 -0
- package/dist/download-part.js +144 -0
- package/dist/download-sink.js +213 -0
- package/dist/download.js +163 -0
- package/dist/drive-paths.js +170 -0
- package/dist/environment.js +190 -0
- package/dist/errors.js +71 -0
- package/dist/exit.js +107 -0
- package/dist/expiry.js +115 -0
- package/dist/extend-chain.js +137 -0
- package/dist/extend-plan.js +149 -0
- package/dist/extend-sign.js +130 -0
- package/dist/guards.js +15 -0
- package/dist/help.js +192 -0
- package/dist/human-check.js +54 -0
- package/dist/item-trash.js +25 -0
- package/dist/kit-file.js +133 -0
- package/dist/list-file.js +71 -0
- package/dist/list-view-find.js +71 -0
- package/dist/list-view-order.js +96 -0
- package/dist/main.js +394 -0
- package/dist/manifest-create.js +81 -0
- package/dist/manifest-write.js +244 -0
- package/dist/manifest.js +213 -0
- package/dist/mark-render.js +45 -0
- package/dist/mcp-args.js +154 -0
- package/dist/mcp-tools/context.js +50 -0
- package/dist/mcp-tools/files.js +133 -0
- package/dist/mcp-tools/organise.js +128 -0
- package/dist/mcp-tools/reads.js +89 -0
- package/dist/mcp-tools/share.js +61 -0
- package/dist/mcp.js +142 -0
- package/dist/net-retry.js +84 -0
- package/dist/network.js +43 -0
- package/dist/notice.js +45 -0
- package/dist/product.js +38 -0
- package/dist/progress.js +110 -0
- package/dist/prompt.js +145 -0
- package/dist/rebuild.js +255 -0
- package/dist/recovery-assemble.js +148 -0
- package/dist/recovery-build.js +260 -0
- package/dist/recovery-map-file.js +114 -0
- package/dist/recovery-map.js +191 -0
- package/dist/recovery-release.js +229 -0
- package/dist/recovery-seq.js +63 -0
- package/dist/recovery-source.js +143 -0
- package/dist/registration.js +83 -0
- package/dist/s3/listing.js +134 -0
- package/dist/s3/multipart.js +104 -0
- package/dist/s3/response-sink.js +41 -0
- package/dist/s3/same-file.js +117 -0
- package/dist/s3/server.js +252 -0
- package/dist/s3/sigv4.js +168 -0
- package/dist/s3/staging.js +87 -0
- package/dist/s3/xml.js +80 -0
- package/dist/safe-path.js +89 -0
- package/dist/seal.js +206 -0
- package/dist/secret-reader.js +112 -0
- package/dist/server.js +33 -0
- package/dist/session.js +49 -0
- package/dist/setup-questions.js +31 -0
- package/dist/share.js +195 -0
- package/dist/shared/lib/crypto/size-padding.js +126 -0
- package/dist/shared/lib/drive/manifest-codec.js +287 -0
- package/dist/shared/lib/drive/manifest-index.js +235 -0
- package/dist/shared/lib/drive/manifest-ops.js +267 -0
- package/dist/shared/lib/drive/manifest-settings.js +65 -0
- package/dist/shared/lib/drive/name-conflict.js +121 -0
- package/dist/shared/lib/drive/unique-name.js +48 -0
- package/dist/shared/lib/extend/epochs.js +256 -0
- package/dist/shared/lib/net/retry-budget.js +95 -0
- package/dist/shared/lib/share/shared-file-info.js +65 -0
- package/dist/shared/lib/storage-network.js +64 -0
- package/dist/shared/lib/upload/part-plan.js +28 -0
- package/dist/stdout.js +109 -0
- package/dist/trash-sweep.js +123 -0
- package/dist/units.js +19 -0
- package/dist/update-check.js +164 -0
- package/dist/update-source.js +143 -0
- package/dist/upload-api.js +98 -0
- package/dist/upload-file.js +242 -0
- package/dist/upload-price.js +117 -0
- package/dist/upload-steps.js +111 -0
- package/dist/upload-store.js +252 -0
- package/dist/upload-wire.js +25 -0
- package/dist/upload.js +294 -0
- package/dist/usage-report.js +54 -0
- package/dist/wallet-chain.js +76 -0
- package/dist/wallet.js +209 -0
- package/dist/walrus-write.js +188 -0
- package/dist/walrus.js +156 -0
- package/package.json +56 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +469 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm.js +1300 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +55 -0
- 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
|
+
}
|