@needmoretruth/nmts-cli 0.34.4 → 0.36.1

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 (156) hide show
  1. package/AGENTS.md +11 -12
  2. package/CHANGELOG.md +43 -0
  3. package/README.ko.md +33 -35
  4. package/README.md +21 -22
  5. package/dist/account.js +2 -1
  6. package/dist/arg-options.d.ts +169 -0
  7. package/dist/arg-options.js +10 -0
  8. package/dist/args.d.ts +2 -157
  9. package/dist/args.js +2 -0
  10. package/dist/artifact-about.d.ts +1 -1
  11. package/dist/autonomy.d.ts +3 -10
  12. package/dist/autonomy.js +15 -17
  13. package/dist/bytes.d.ts +15 -0
  14. package/dist/bytes.js +63 -0
  15. package/dist/collision.d.ts +5 -5
  16. package/dist/collision.js +19 -22
  17. package/dist/commands/delete-account.js +1 -1
  18. package/dist/commands/erase.js +1 -1
  19. package/dist/commands/extend.d.ts +2 -0
  20. package/dist/commands/extend.js +8 -1
  21. package/dist/commands/get.js +1 -1
  22. package/dist/commands/listfile.js +1 -1
  23. package/dist/commands/mcp.js +5 -1
  24. package/dist/commands/mode.js +2 -2
  25. package/dist/commands/on-collision.d.ts +1 -1
  26. package/dist/commands/on-collision.js +3 -3
  27. package/dist/commands/platform.d.ts +9 -0
  28. package/dist/commands/platform.js +124 -0
  29. package/dist/commands/pull.js +1 -1
  30. package/dist/commands/push-wallet.js +8 -1
  31. package/dist/commands/push.d.ts +2 -0
  32. package/dist/commands/push.js +6 -4
  33. package/dist/commands/put-payer.d.ts +8 -0
  34. package/dist/commands/put-payer.js +41 -0
  35. package/dist/commands/put-wallet.d.ts +9 -3
  36. package/dist/commands/put-wallet.js +108 -186
  37. package/dist/commands/put.d.ts +5 -7
  38. package/dist/commands/put.js +11 -31
  39. package/dist/commands/receive.js +1 -1
  40. package/dist/commands/settings.js +1 -1
  41. package/dist/commands/support.js +3 -3
  42. package/dist/commands/unlock.js +2 -2
  43. package/dist/commands/wallet-donate.d.ts +3 -0
  44. package/dist/commands/wallet-donate.js +6 -2
  45. package/dist/commands/wallet-hall.d.ts +4 -1
  46. package/dist/commands/wallet-hall.js +16 -8
  47. package/dist/commands/wallet-list.d.ts +18 -0
  48. package/dist/commands/wallet-list.js +116 -0
  49. package/dist/commands/wallet-send.d.ts +4 -0
  50. package/dist/commands/wallet-send.js +6 -2
  51. package/dist/commands/wallet-storage-ops.d.ts +4 -1
  52. package/dist/commands/wallet-storage-ops.js +7 -2
  53. package/dist/commands/wallet-swap.d.ts +3 -0
  54. package/dist/commands/wallet-swap.js +8 -4
  55. package/dist/commands/wallet-use.d.ts +7 -0
  56. package/dist/commands/wallet-use.js +55 -0
  57. package/dist/commands/wallet.d.ts +4 -0
  58. package/dist/commands/wallet.js +20 -8
  59. package/dist/crypto.d.ts +8 -5
  60. package/dist/crypto.js +15 -61
  61. package/dist/download-sink-node.d.ts +49 -0
  62. package/dist/download-sink-node.js +204 -0
  63. package/dist/download-sink.d.ts +0 -48
  64. package/dist/download-sink.js +6 -199
  65. package/dist/download.js +5 -4
  66. package/dist/engine-node.d.ts +19 -0
  67. package/dist/engine-node.js +77 -0
  68. package/dist/env-vars.d.ts +20 -0
  69. package/dist/env-vars.js +27 -0
  70. package/dist/extend-plan.d.ts +2 -0
  71. package/dist/gate.js +1 -1
  72. package/dist/help.js +12 -5
  73. package/dist/host-contract.d.ts +7 -0
  74. package/dist/host-contract.js +95 -0
  75. package/dist/host-node.d.ts +27 -0
  76. package/dist/host-node.js +192 -0
  77. package/dist/host.d.ts +99 -0
  78. package/dist/host.js +59 -0
  79. package/dist/index.d.ts +6 -40
  80. package/dist/index.js +20 -32
  81. package/dist/main.js +11 -10
  82. package/dist/manifest-chunk-cache.d.ts +3 -3
  83. package/dist/manifest-chunk-cache.js +39 -44
  84. package/dist/manifest-chunk-flow.js +15 -14
  85. package/dist/manifest-write.js +1 -1
  86. package/dist/manifest.d.ts +2 -2
  87. package/dist/manifest.js +37 -34
  88. package/dist/mcp-tools/tiers.js +1 -1
  89. package/dist/network.d.ts +1 -1
  90. package/dist/network.js +6 -2
  91. package/dist/platform-sign.d.ts +143 -0
  92. package/dist/platform-sign.js +220 -0
  93. package/dist/portable.d.ts +59 -0
  94. package/dist/portable.js +82 -0
  95. package/dist/product.d.ts +1 -1
  96. package/dist/product.js +1 -1
  97. package/dist/progress-node.d.ts +2 -0
  98. package/dist/progress-node.js +14 -0
  99. package/dist/progress.d.ts +0 -1
  100. package/dist/progress.js +4 -8
  101. package/dist/redact.js +3 -2
  102. package/dist/registration.js +5 -2
  103. package/dist/risk.d.ts +11 -0
  104. package/dist/risk.js +16 -0
  105. package/dist/run-log.d.ts +4 -4
  106. package/dist/run-log.js +29 -36
  107. package/dist/seal.js +5 -4
  108. package/dist/server.d.ts +1 -1
  109. package/dist/server.js +6 -2
  110. package/dist/setup-questions.js +2 -2
  111. package/dist/shared/lib/drive/manifest-ops.d.ts +8 -30
  112. package/dist/shared/lib/drive/manifest-ops.js +7 -39
  113. package/dist/shared/lib/drive/manifest-settings-patch.d.ts +52 -0
  114. package/dist/shared/lib/drive/manifest-settings-patch.js +110 -0
  115. package/dist/shared/lib/drive/manifest-settings.d.ts +55 -4
  116. package/dist/shared/lib/drive/manifest-settings.js +66 -28
  117. package/dist/shared/lib/wallet/discover.d.ts +54 -0
  118. package/dist/shared/lib/wallet/discover.js +66 -0
  119. package/dist/standing-tip.d.ts +3 -0
  120. package/dist/standing-tip.js +1 -0
  121. package/dist/upload-file-node.d.ts +3 -0
  122. package/dist/upload-file-node.js +38 -0
  123. package/dist/upload-file.d.ts +2 -2
  124. package/dist/upload-file.js +15 -45
  125. package/dist/upload-price-node.d.ts +8 -0
  126. package/dist/upload-price-node.js +37 -0
  127. package/dist/upload-price.d.ts +0 -8
  128. package/dist/upload-price.js +4 -33
  129. package/dist/upload-steps.js +3 -3
  130. package/dist/upload-store.d.ts +14 -14
  131. package/dist/upload-store.js +68 -75
  132. package/dist/upload-wallet-chain.js +3 -2
  133. package/dist/upload-wallet-put.d.ts +118 -0
  134. package/dist/upload-wallet-put.js +221 -0
  135. package/dist/upload-wallet.d.ts +3 -0
  136. package/dist/upload-wallet.js +18 -15
  137. package/dist/upload.js +16 -15
  138. package/dist/wallet-list-chain.d.ts +4 -0
  139. package/dist/wallet-list-chain.js +21 -0
  140. package/dist/wallet-pay-index.d.ts +21 -0
  141. package/dist/wallet-pay-index.js +68 -0
  142. package/dist/wallet-sign-seams.d.ts +69 -0
  143. package/dist/wallet-sign-seams.js +16 -0
  144. package/dist/wallet-sign.d.ts +3 -53
  145. package/dist/wallet-sign.js +22 -18
  146. package/dist/wallet.d.ts +13 -13
  147. package/dist/wallet.js +15 -15
  148. package/dist/walrus-write.js +3 -2
  149. package/dist/walrus.d.ts +1 -16
  150. package/dist/walrus.js +11 -22
  151. package/docs/commands/credits.md +1 -1
  152. package/docs/commands/platform.md +14 -0
  153. package/docs/commands/support.md +1 -1
  154. package/docs/commands/trash.md +1 -1
  155. package/docs/commands/wallet.md +26 -8
  156. package/package.json +13 -2
package/dist/gate.js CHANGED
@@ -34,7 +34,7 @@ export async function unlocked(key, now) {
34
34
  }
35
35
  export async function gate(act, args, io = {}) {
36
36
  const a = ACTS[act];
37
- const mode = currentMode();
37
+ const mode = await currentMode();
38
38
  const now = io.now ?? (() => new Date());
39
39
  if (a.tier === "none")
40
40
  return { ask: false, mode };
package/dist/help.js CHANGED
@@ -10,10 +10,11 @@ import { AGENTS_DOC, BINARY_NAME, HOME_URL, PRODUCT_NAME, SOURCE_URL, VERSION }
10
10
  // reading it here costs `nmts --help` nothing (`check:cli-startup`).
11
11
  import { ATTACH_LOG_TEXT, SUPPORT_LONG, supportHelpText } from "./support-copy.js";
12
12
  import { API_KEY_ENV_VAR, API_KEY_FILE_ENV_VAR, CODE_ENV_VAR, CODE_FILE_ENV_VAR, PASSPHRASE_ENV_VAR, } from "./credentials.js";
13
- import { NETWORK_ENV_VAR } from "./network.js";
13
+ // THE NAMES COME FROM THE MODULE THAT HAS NO IMPORTS, not from the modules that USE them. Those
14
+ // three reach the host register, and loading them to print a variable's name would put that
15
+ // chain in front of every `nmts --help` (`check:cli-startup`).
16
+ import { AGGREGATOR_ENV_VAR, NETWORK_ENV_VAR, SERVER_ENV_VAR } from "./env-vars.js";
14
17
  import { LATEST_RELEASE_URL, NO_CHECK_ENV_VAR } from "./update-source.js";
15
- import { SERVER_ENV_VAR } from "./server.js";
16
- import { AGGREGATOR_ENV_VAR } from "./walrus.js";
17
18
  import { CHECK_DOES_NOT_PROVE, CHECK_PROVES, RECOVERY_TOOL_URL, wrapText, } from "./recovery-release.js";
18
19
  export function helpText(version) {
19
20
  return [
@@ -52,7 +53,9 @@ export function helpText(version) {
52
53
  ` refused in mode auto. Read first: terms · privacy · notices`,
53
54
  ` wallet Show the account's wallet address, and its SUI and WAL balances`,
54
55
  ` wallet address Just the address, derived on this machine — no network call.`,
55
- ` \`--qr\` draws it as a code a phone can scan`,
56
+ ` \`--qr\` draws it as a code a phone can scan; \`--index N\` another wallet`,
57
+ ` wallet list Every wallet this key has, with balances, and which one pays`,
58
+ ` wallet use <number> Pay from that wallet from now on — on the account, not this machine`,
56
59
  ` wallet activity The wallet's recent transactions, named where the chain proves it`,
57
60
  ` wallet storage The storage resources (size × time) the wallet holds, unbound`,
58
61
  ` wallet storage split <id> --size <n>|--epochs <n> · merge <id> <id> · transfer <id> <address>`,
@@ -121,6 +124,9 @@ export function helpText(version) {
121
124
  ` privacy The Privacy Policy in force`,
122
125
  ` mcp Serve this account's commands as tools, for an agent that speaks MCP`,
123
126
  ` s3 Serve the drive to any S3 program, on this machine only`,
127
+ ` platform keygen Make the key pair a business signs its Platform requests with — writes`,
128
+ ` both halves to a file only you can read, prints only the public half`,
129
+ ` platform register Where that public key is registered: a browser session, not this tool`,
124
130
  ``,
125
131
  `OPTIONS`,
126
132
  ` --server <url> NMTS server (default ${SERVER_ENV_VAR} or the live one)`,
@@ -169,7 +175,8 @@ export function helpText(version) {
169
175
  ` --desc Reverse whichever order is in effect (ls)`,
170
176
  ` --hidden Include entries whose name begins with a dot (push)`,
171
177
  ` --out <path> Where to write files (get, pull, mcp, listfile, recovery,`,
172
- ` recovery-list, kit, create, notices, terms, privacy). Default: here`,
178
+ ` recovery-list, kit, create, notices, terms, privacy, platform`,
179
+ ` keygen). Default: here`,
173
180
  ` --out - Send a fetched file to stdout instead of writing it (get, listfile)`,
174
181
  ` --lang <en|ko> Which language of a document (terms, privacy). Default: English`,
175
182
  ` --board The message board's terms rather than the service's (terms)`,
@@ -0,0 +1,7 @@
1
+ import type { Host } from "./host.ts";
2
+ /**
3
+ * Put a host's state through everything the package relies on, and say what it got wrong.
4
+ *
5
+ * An empty array is a pass.
6
+ */
7
+ export declare function hostContract(host: Host): Promise<string[]>;
@@ -0,0 +1,95 @@
1
+ // The one test both hosts sit: what "keeping state" has to mean, whoever is keeping it.
2
+ //
3
+ // ⛔ ONE FUNCTION, RUN FROM TWO TEST FILES. `test/host-node.test.ts` runs it over the Node host and
4
+ // the SDK's `test/host-browser.test.ts` runs it over the browser one. Two tests written
5
+ // separately would drift, and the way that drift shows up is a half-finished upload that
6
+ // resumes on a laptop and starts again in a browser — the same account, the same file, two
7
+ // answers.
8
+ //
9
+ // ⛔ IT REPORTS EVERY VIOLATION RATHER THAN THROWING AT THE FIRST. A host that is wrong is usually
10
+ // wrong about one thing; a list says which, and a caller that wanted an assertion gets one by
11
+ // asserting the list is empty.
12
+ //
13
+ // ⚠ IT WRITES. The keys all sit under `contract/`, which nothing else uses, and each one is
14
+ // removed before the function returns — including when a check above it failed.
15
+ /** Where this check keeps its scratch. Nothing else in the package writes under it. */
16
+ const AREA = "contract";
17
+ function sample(length, seed) {
18
+ const out = new Uint8Array(length);
19
+ for (let i = 0; i < length; i += 1)
20
+ out[i] = (i * 31 + seed) & 0xff;
21
+ return out;
22
+ }
23
+ function same(left, right) {
24
+ if (left === undefined || left.length !== right.length)
25
+ return false;
26
+ for (let i = 0; i < left.length; i += 1)
27
+ if (left[i] !== right[i])
28
+ return false;
29
+ return true;
30
+ }
31
+ /**
32
+ * Put a host's state through everything the package relies on, and say what it got wrong.
33
+ *
34
+ * An empty array is a pass.
35
+ */
36
+ export async function hostContract(host) {
37
+ const state = host.state;
38
+ const problems = [];
39
+ const one = `${AREA}/one`;
40
+ const two = `${AREA}/two`;
41
+ const big = `${AREA}/big`;
42
+ const say = (what) => void problems.push(what);
43
+ try {
44
+ // A key nothing has written is not an error and not empty bytes: it is nothing.
45
+ if ((await state.read(`${AREA}/never-written`)) !== undefined) {
46
+ say("read() of a key that was never written answered bytes instead of undefined");
47
+ }
48
+ const first = sample(64, 1);
49
+ await state.write(one, first);
50
+ if (!same(await state.read(one), first))
51
+ say("read() did not hand back the bytes write() was given");
52
+ // ⛔ THE SECOND WRITE REPLACES. A store that appended, or that kept the first value, would make
53
+ // every record in the package grow a second version nobody reads.
54
+ const second = sample(96, 2);
55
+ await state.write(one, second);
56
+ if (!same(await state.read(one), second))
57
+ say("writing a key a second time did not replace what was there");
58
+ await state.write(two, sample(8, 3));
59
+ const listed = await state.keys(`${AREA}/`);
60
+ for (const key of [one, two]) {
61
+ if (!listed.includes(key))
62
+ say(`keys("${AREA}/") did not include ${key}`);
63
+ }
64
+ if (listed.includes(`${AREA}/never-written`))
65
+ say("keys() listed a key that was never written");
66
+ await state.remove(one);
67
+ if ((await state.read(one)) !== undefined)
68
+ say("remove() left the bytes readable");
69
+ if ((await state.keys(`${AREA}/`)).includes(one))
70
+ say("remove() left the key in keys()");
71
+ // Removing what is not there is how every caller here clears a record it may never have made.
72
+ await state.remove(one);
73
+ // ⛔ ONE MEBIBYTE, BECAUSE THAT IS WHAT AN UPLOAD KEEPS. A reservation holds a sealed part, and
74
+ // a store that quietly truncated or refused at some smaller size would lose exactly the
75
+ // records that let a large upload resume — and nothing else would notice.
76
+ const large = sample(1024 * 1024, 4);
77
+ await state.write(big, large);
78
+ if (!same(await state.read(big), large))
79
+ say("a 1 MiB value did not come back as it was written");
80
+ }
81
+ catch (error) {
82
+ say(`a call threw instead of answering: ${error instanceof Error ? error.message : String(error)}`);
83
+ }
84
+ finally {
85
+ for (const key of [one, two, big]) {
86
+ try {
87
+ await state.remove(key);
88
+ }
89
+ catch {
90
+ // The store is somebody else's; a scratch key left behind is not this check's to report.
91
+ }
92
+ }
93
+ }
94
+ return problems;
95
+ }
@@ -0,0 +1,27 @@
1
+ import { type Host, type StateHost } from "./host.ts";
2
+ /** Files under the person's own config directory, named by `placeOf`. */
3
+ export declare function nodeState(): StateHost;
4
+ /** Where a state key lands on this machine. For tests, and for messages that name a file. */
5
+ export declare function statePath(key: string): string;
6
+ /**
7
+ * Every `NMTS_*` variable this process is holding.
8
+ *
9
+ * ⛔ THE ONE PLACE THE WHOLE ENVIRONMENT IS READ. `redact.ts` needs the values to label them out of
10
+ * a report, and a browser has none — so it is a host call rather than a `process.env` walk in a
11
+ * module that has to run in both.
12
+ */
13
+ export declare function nodeEnvEntries(): {
14
+ name: string;
15
+ value: string;
16
+ }[];
17
+ /** This machine, as the package's host. */
18
+ export declare function nodeHost(): Host;
19
+ /**
20
+ * Put this machine in the register: what the two Node doors — `index.ts` and the command's own
21
+ * entry point — call before anything that loads the engine, keeps state or reads the environment.
22
+ *
23
+ * ⛔ THE COMMAND LOADS THIS FILE LATE, not at the top of `main.ts`. The chain below reaches the
24
+ * credentials module, and a fixed cost at the head of `nmts --help` is one an agent running
25
+ * this tool in a loop pays thousands of times (`check:cli-startup`).
26
+ */
27
+ export declare function registerNodeHost(): void;
@@ -0,0 +1,192 @@
1
+ // The Node host: the five things `host.ts` names, done the way this tool has always done them.
2
+ //
3
+ // ⛔ THIS FILE IS WHERE THE `node:` IMPORTS WENT. They used to be spread over a dozen modules, and
4
+ // that is what stopped the package running anywhere else. Nothing moved in behaviour: the
5
+ // engine is found on disk the same way, the state is the same files under the same names, and
6
+ // an installation made by an earlier version is read without noticing the change.
7
+ //
8
+ // ⛔ THE FILE NAMES ARE A COMPATIBILITY PROMISE, not a mapping somebody is free to tidy. A kept
9
+ // file list, a chunk of one and an unfinished upload all sit in a home directory that this
10
+ // version did not create. `placeOf` is the whole of that promise, and it is a table so a reader
11
+ // can check it against a real `~/.nmts` in one glance.
12
+ //
13
+ // ⛔ EVERY WRITE IS ASIDE-AND-RENAME, 0600, under a 0700 directory. A rename within one directory
14
+ // is atomic, so a reader meets the old bytes or the new ones and never half of either — which
15
+ // matters most for the one record that says an upload was already paid for.
16
+ //
17
+ // ⚠ NOTHING HERE THROWS ON A FAILED WRITE. Every caller in the package treats missing state as
18
+ // "do the work again", so a full disk, a read-only home directory or a directory owned by
19
+ // somebody else costs a slower command rather than a failed one.
20
+ //
21
+ // ⛔ THE ENGINE AND THE ZSTD ENCODER ARE LOADED WHEN THEY ARE ASKED FOR, not when the host is made.
22
+ // `nmts --help` registers a host and uses none of it, and an agent runs this tool in a loop —
23
+ // `check:cli-startup` measures exactly this and refuses a build that widened it.
24
+ import { chmodSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
25
+ import { join, sep } from "node:path";
26
+ import { configDir, modesAreEnforced } from "./credentials.js";
27
+ import { registerHost } from "./host.js";
28
+ /**
29
+ * The file one state key names — the table an earlier installation's layout is read through.
30
+ *
31
+ * Anything not named here is spelled out as it reads: the slashes become directories and the last
32
+ * segment is the file name, which is what `uploads/…` and the host-contract check already are.
33
+ */
34
+ function placeOf(key) {
35
+ if (key === "runlog")
36
+ return { dir: "", file: "runs.jsonl" };
37
+ if (key === "collision")
38
+ return { dir: "", file: "collision.json" };
39
+ if (key === "autonomy")
40
+ return { dir: "", file: "autonomy.json" };
41
+ if (key === "manifest/state")
42
+ return { dir: "", file: "file-list-state.json" };
43
+ const manifest = after(key, "manifest/");
44
+ if (manifest !== null)
45
+ return { dir: "", file: `file-list-${manifest}.json` };
46
+ const chunk = after(key, "chunks/");
47
+ if (chunk !== null) {
48
+ const cut = chunk.lastIndexOf("/");
49
+ return { dir: join("file-list-chunks", chunk.slice(0, cut)), file: `${chunk.slice(cut + 1)}.ct` };
50
+ }
51
+ const cut = key.lastIndexOf("/");
52
+ return cut < 0 ? { dir: "", file: key } : { dir: key.slice(0, cut), file: key.slice(cut + 1) };
53
+ }
54
+ function after(key, prefix) {
55
+ return key.startsWith(prefix) ? key.slice(prefix.length) : null;
56
+ }
57
+ /** The key a file stands for, or null when this tool keeps no key for it. The inverse of `placeOf`. */
58
+ function keyOf(place) {
59
+ if (place.dir === "") {
60
+ if (place.file === "runs.jsonl")
61
+ return "runlog";
62
+ if (place.file === "collision.json")
63
+ return "collision";
64
+ if (place.file === "autonomy.json")
65
+ return "autonomy";
66
+ if (place.file === "file-list-state.json")
67
+ return "manifest/state";
68
+ const listed = place.file.match(/^file-list-(.+)\.json$/);
69
+ return listed?.[1] === undefined ? null : `manifest/${listed[1]}`;
70
+ }
71
+ const parts = place.dir.split(sep);
72
+ if (parts[0] === "file-list-chunks") {
73
+ if (!place.file.endsWith(".ct"))
74
+ return null;
75
+ return ["chunks", ...parts.slice(1), place.file.slice(0, -".ct".length)].join("/");
76
+ }
77
+ return [...parts, place.file].join("/");
78
+ }
79
+ /** Every file under the config directory, as `Place`s. A missing directory is no files. */
80
+ function walk(root, dir, out) {
81
+ let names;
82
+ try {
83
+ names = readdirSync(join(root, dir));
84
+ }
85
+ catch {
86
+ return;
87
+ }
88
+ for (const name of names) {
89
+ const full = join(root, dir, name);
90
+ let isDirectory = false;
91
+ try {
92
+ isDirectory = statSync(full).isDirectory();
93
+ }
94
+ catch {
95
+ continue;
96
+ }
97
+ if (isDirectory)
98
+ walk(root, join(dir, name), out);
99
+ else
100
+ out.push({ dir, file: name });
101
+ }
102
+ }
103
+ function atomically(target, bytes) {
104
+ const scratch = `${target}.${process.pid}.tmp`;
105
+ writeFileSync(scratch, bytes, { mode: 0o600 });
106
+ if (modesAreEnforced())
107
+ chmodSync(scratch, 0o600);
108
+ renameSync(scratch, target);
109
+ }
110
+ /** Files under the person's own config directory, named by `placeOf`. */
111
+ export function nodeState() {
112
+ return {
113
+ durable: true,
114
+ async read(key) {
115
+ const place = placeOf(key);
116
+ try {
117
+ return new Uint8Array(readFileSync(join(configDir(), place.dir, place.file)));
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ },
123
+ async write(key, bytes) {
124
+ const place = placeOf(key);
125
+ const dir = join(configDir(), place.dir);
126
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
127
+ if (modesAreEnforced())
128
+ chmodSync(configDir(), 0o700);
129
+ atomically(join(dir, place.file), bytes);
130
+ },
131
+ async remove(key) {
132
+ const place = placeOf(key);
133
+ rmSync(join(configDir(), place.dir, place.file), { force: true });
134
+ },
135
+ async keys(prefix) {
136
+ const found = [];
137
+ walk(configDir(), "", found);
138
+ const keys = [];
139
+ for (const place of found) {
140
+ const key = keyOf(place);
141
+ if (key !== null && key.startsWith(prefix))
142
+ keys.push(key);
143
+ }
144
+ return keys;
145
+ },
146
+ };
147
+ }
148
+ /** Where a state key lands on this machine. For tests, and for messages that name a file. */
149
+ export function statePath(key) {
150
+ const place = placeOf(key);
151
+ return join(configDir(), place.dir, place.file);
152
+ }
153
+ /**
154
+ * Every `NMTS_*` variable this process is holding.
155
+ *
156
+ * ⛔ THE ONE PLACE THE WHOLE ENVIRONMENT IS READ. `redact.ts` needs the values to label them out of
157
+ * a report, and a browser has none — so it is a host call rather than a `process.env` walk in a
158
+ * module that has to run in both.
159
+ */
160
+ export function nodeEnvEntries() {
161
+ const out = [];
162
+ for (const [name, value] of Object.entries(process.env)) {
163
+ if (value !== undefined)
164
+ out.push({ name, value });
165
+ }
166
+ return out;
167
+ }
168
+ /** This machine, as the package's host. */
169
+ export function nodeHost() {
170
+ return {
171
+ name: "node",
172
+ engine: { load: async () => (await import("./engine-node.js")).loadEngine() },
173
+ state: nodeState(),
174
+ env: (name) => process.env[name],
175
+ envEntries: nodeEnvEntries,
176
+ // ⛔ STDERR, NOT STDOUT. Progress is not the answer, and a caller redirecting the answer to a
177
+ // file must not find it interleaved with percentages.
178
+ log: (line) => void process.stderr.write(line),
179
+ zstd: { register: async () => (await import("./zstd-node.js")).registerNodeZstd() },
180
+ };
181
+ }
182
+ /**
183
+ * Put this machine in the register: what the two Node doors — `index.ts` and the command's own
184
+ * entry point — call before anything that loads the engine, keeps state or reads the environment.
185
+ *
186
+ * ⛔ THE COMMAND LOADS THIS FILE LATE, not at the top of `main.ts`. The chain below reaches the
187
+ * credentials module, and a fixed cost at the head of `nmts --help` is one an agent running
188
+ * this tool in a loop pays thousands of times (`check:cli-startup`).
189
+ */
190
+ export function registerNodeHost() {
191
+ registerHost(nodeHost());
192
+ }
package/dist/host.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { CryptoGlue } from "./crypto-surface.ts";
2
+ /** Which runtime put the host in the register. Reported; nothing branches on it. */
3
+ export type HostName = "node" | "browser";
4
+ /** Loading the WebAssembly engine that derives this account's keys. */
5
+ export interface EngineHost {
6
+ /**
7
+ * The engine, made once and kept.
8
+ *
9
+ * ⛔ THE SHAPE CHECK IS NOT THE HOST'S. `crypto.ts` runs `isCryptoGlue` over whatever comes
10
+ * back, so both hosts pass the same gate and a build that renamed an export is named at load
11
+ * time whichever runtime met it.
12
+ */
13
+ load(): Promise<CryptoGlue>;
14
+ }
15
+ /**
16
+ * What this installation keeps between runs, as bytes under a key.
17
+ *
18
+ * ⛔ KEYS ARE `<area>/<name>`, and the area is what a reader of the key can see at a glance:
19
+ * `manifest/…` is a kept file list, `chunks/…` a copy of one of its chunks, `uploads/…` a
20
+ * reservation a half-finished upload can be resumed from, `runlog` this machine's record of
21
+ * what it did, `collision` and `autonomy` the two answers a person gave at setup.
22
+ *
23
+ * ⛔ NOTHING HERE IS A CACHE THE ACCOUNT DEPENDS ON. Every reader treats "no bytes" as "fetch it
24
+ * again", so a host whose store is empty, full or unavailable is a slower program and never a
25
+ * broken one.
26
+ */
27
+ export interface StateHost {
28
+ /**
29
+ * False when what is written will not survive the page or the process — a browser with no
30
+ * IndexedDB, or one that would not open it. Reported so a caller can say so rather than
31
+ * promising a resumable upload it cannot resume.
32
+ */
33
+ readonly durable: boolean;
34
+ read(key: string): Promise<Uint8Array | undefined>;
35
+ write(key: string, bytes: Uint8Array): Promise<void>;
36
+ remove(key: string): Promise<void>;
37
+ /** Every key that starts with `prefix`, in no particular order. */
38
+ keys(prefix: string): Promise<string[]>;
39
+ }
40
+ /**
41
+ * Filling the file-list codec's zstd register (NCF-3 §6.3.4). Safe to call more than once.
42
+ *
43
+ * ⛔ IT IS AWAITED. Both hosts fetch their encoder rather than carrying it — Node's `zlib` and a
44
+ * WebAssembly build respectively — and a list read that did not wait for it would fall back to
45
+ * gzip for a frame the browser wrote in zstd.
46
+ */
47
+ export interface ZstdHost {
48
+ register(): Promise<void>;
49
+ }
50
+ /** Everything this package needs from the runtime it was loaded into. */
51
+ export interface Host {
52
+ readonly name: HostName;
53
+ readonly engine: EngineHost;
54
+ readonly state: StateHost;
55
+ /**
56
+ * A named value from the environment, or undefined.
57
+ *
58
+ * ⚠ A PAGE HAS NO ENVIRONMENT, so the addresses a variable would have carried — the relay, the
59
+ * Sui endpoint, the aggregators — arrive as options on the client, and the browser host
60
+ * answers those three under the same variable names and undefined to everything else. Asking
61
+ * through here rather than `process.env` is what lets one module do both without knowing which
62
+ * runtime it is in.
63
+ */
64
+ env(name: string): string | undefined;
65
+ /**
66
+ * Every variable this process is holding, for the one caller that needs the VALUES rather than
67
+ * one of them: `redact.ts` labels them out of a report before it is written down.
68
+ *
69
+ * ⚠ A BROWSER ANSWERS AN EMPTY LIST, which is the truth rather than a stub: a page holds no
70
+ * environment, so there is nothing of that kind to take out of a report.
71
+ */
72
+ envEntries(): {
73
+ name: string;
74
+ value: string;
75
+ }[];
76
+ /** One line about how far along something is. Where it goes is the host's business. */
77
+ log(line: string): void;
78
+ readonly zstd: ZstdHost;
79
+ }
80
+ /**
81
+ * Put the host for this runtime in the register.
82
+ *
83
+ * Called once, from the entry point a program imported, before anything else runs. Calling it
84
+ * again replaces what is there, which is what a test that swaps a host needs and what nothing else
85
+ * should do.
86
+ */
87
+ export declare function registerHost(next: Host): void;
88
+ /** Empty the register. For tests that need to see what an unhosted module does. */
89
+ export declare function forgetHost(): void;
90
+ /** True when something has registered one. Lets a caller ask instead of catching. */
91
+ export declare function hostIsRegistered(): boolean;
92
+ /**
93
+ * The host for this runtime.
94
+ *
95
+ * ⛔ IT REFUSES RATHER THAN GUESSING. A program that imported `@needmoretruth/nmts-cli/portable`
96
+ * and registered nothing has one thing wrong with it, and naming that is worth more than a
97
+ * default host that half-works and fails somewhere further in.
98
+ */
99
+ export declare function host(): Host;
package/dist/host.js ADDED
@@ -0,0 +1,59 @@
1
+ // The five things this package cannot do by itself, in one register — so that everything else in
2
+ // it is code that runs anywhere a `fetch` does.
3
+ //
4
+ // ⛔ A REGISTER, NOT A BRANCH. Nothing below asks which runtime it is in. The entry point that a
5
+ // program imports puts a host in here — `index.ts` registers the Node one, the SDK's `/browser`
6
+ // entry registers a browser one — and every portable module afterwards asks for the one that is
7
+ // there. A `typeof window === "undefined"` test in each of the modules instead would be the
8
+ // same decision made twenty times, and the twenty-first is the one that gets it wrong on a
9
+ // runtime nobody was thinking about.
10
+ //
11
+ // ⛔ FIVE, AND THEY ARE THE WHOLE LIST. Loading the engine, keeping state, reading the
12
+ // environment, saying how far along something is, and having a zstd encoder: those are what a
13
+ // browser cannot do the way Node does. Anything a sixth entry would be asked to carry is a sign
14
+ // that a module is doing something it should be handed instead.
15
+ //
16
+ // ⛔ STATE IS ASYNCHRONOUS. A browser's store is a database with transactions and there is no
17
+ // honest way to read one without waiting. Node's is a file and could have stayed synchronous;
18
+ // making it look synchronous in a browser would take a worker and a shared buffer to fake, and
19
+ // a fake that blocks the page is worse than the `await` it saves.
20
+ //
21
+ // ⛔ NO `node:` IMPORT MAY EVER APPEAR IN THIS FILE, or in anything it reaches. `portable.ts` is
22
+ // the entry that promises it and `test/portable-closure.test.ts` is what holds the promise.
23
+ import { NmtsError } from "./errors.js";
24
+ let registered = null;
25
+ /**
26
+ * Put the host for this runtime in the register.
27
+ *
28
+ * Called once, from the entry point a program imported, before anything else runs. Calling it
29
+ * again replaces what is there, which is what a test that swaps a host needs and what nothing else
30
+ * should do.
31
+ */
32
+ export function registerHost(next) {
33
+ registered = next;
34
+ }
35
+ /** Empty the register. For tests that need to see what an unhosted module does. */
36
+ export function forgetHost() {
37
+ registered = null;
38
+ }
39
+ /** True when something has registered one. Lets a caller ask instead of catching. */
40
+ export function hostIsRegistered() {
41
+ return registered !== null;
42
+ }
43
+ /**
44
+ * The host for this runtime.
45
+ *
46
+ * ⛔ IT REFUSES RATHER THAN GUESSING. A program that imported `@needmoretruth/nmts-cli/portable`
47
+ * and registered nothing has one thing wrong with it, and naming that is worth more than a
48
+ * default host that half-works and fails somewhere further in.
49
+ */
50
+ export function host() {
51
+ if (registered === null) {
52
+ throw new NmtsError("HOST_MISSING: no runtime host has been registered.", {
53
+ exitCode: 1,
54
+ nextStep: "Import `@needmoretruth/nmts-cli` (which registers the Node host) or the SDK's `/browser` " +
55
+ "entry (which registers a browser one) before calling anything that keeps state.",
56
+ });
57
+ }
58
+ return registered;
59
+ }
package/dist/index.d.ts CHANGED
@@ -1,42 +1,8 @@
1
- export { NmtsError, NotLoggedInError, renderError } from "./errors.ts";
2
- export { request, ServerError, DEFAULT_TIMEOUT_MS } from "./api.ts";
3
- export type { RequestOptions, ServerRefusal } from "./api.ts";
4
- export { DEFAULT_SERVER, SERVER_ENV_VAR, resolveServer } from "./server.ts";
5
- export { NETWORKS, NETWORK_ENV_VAR, resolveNetwork } from "./network.ts";
6
- export type { Network } from "./network.ts";
1
+ export * from "./portable.ts";
7
2
  export { API_KEY_ENV_VAR, API_KEY_FILE_ENV_VAR, CODE_ENV_VAR, CODE_FILE_ENV_VAR, configDir, readSecretFile, resolveAccountCode, resolveApiKey, } from "./credentials.ts";
8
3
  export type { CredentialSource, ResolvedCode } from "./credentials.ts";
9
- export { assertUsableCode, identityOf } from "./account.ts";
10
- export type { AccountIdentity } from "./account.ts";
11
- export { AAD, DERIVED, loadCrypto } from "./crypto.ts";
12
- export type { CryptoGlue } from "./crypto.ts";
13
- export { readFileList } from "./manifest.ts";
14
- export type { FileList } from "./manifest.ts";
15
- export { addEntry, applyManyToList, applyToList, planAddition } from "./manifest-write.ts";
16
- export type { AddEntryInput, AddEntryResult, ListEditInput, ListEditResult } from "./manifest-write.ts";
17
- export type { Manifest, ManifestEntry } from "./shared/lib/drive/manifest-codec.ts";
18
- export { buildIndex, entryAt, folderIdFor, fullPathOf, isLive, KIND_FILE, KIND_FOLDER, namesIn, normalisePath, trashedAt, } from "./drive-paths.ts";
19
- export type { FindOptions, ManifestIndex } from "./drive-paths.ts";
20
- export { setTrashed } from "./item-trash.ts";
21
- export { fileSource, partKeysOf, uploadFile } from "./upload-file.ts";
22
- export type { FileUploadInput, FileUploadStep, PlaintextSource } from "./upload-file.ts";
23
- export { createUploadApi } from "./upload-api.ts";
24
- export { CREDIT_BYTES, creditsFor, measureLocal, partSizeFor, planAndPrice, UPLOAD_EPOCHS, } from "./upload-price.ts";
25
- export { clearItemRecord, clearReservation } from "./upload-store.ts";
26
- export { UploadError } from "./upload-wire.ts";
27
- export type { BlobProtocol, UploadApi, UploadResult, UploadStep } from "./upload-wire.ts";
28
- export type { PaddingRule } from "./shared/lib/crypto/size-padding.ts";
29
- export { DEFAULT_PART_BYTES } from "./seal.ts";
30
- export { createBlobProtocol, readCurrentEpoch } from "./walrus-write.ts";
31
- export { fetchFile, fetchWithKey } from "./download.ts";
32
- export type { FetchedFile, FetchInput } from "./download.ts";
33
- export { fileSink } from "./download-sink.ts";
34
- export type { PlaintextSink } from "./download-sink.ts";
35
- export { AGGREGATOR_ENV_VAR, readBlob, RELAY_ENV_VAR, SUI_RPC_ENV_VAR } from "./walrus.ts";
36
- export type { ReadOptions } from "./walrus.ts";
37
- export { coinAmount, readBalances, walletAddress } from "./wallet.ts";
38
- export type { ChainReader, CoinBalance, WalletBalances } from "./wallet.ts";
39
- export { chainReader } from "./wallet-chain.ts";
40
- export { signerAddress, signExtension, signTransfer } from "./wallet-sign.ts";
41
- export type { SignTransfer } from "./wallet-sign.ts";
42
- export { HOME_URL, PRODUCT_NAME, SOURCE_URL, SUPPORT_EMAIL, VERSION } from "./product.ts";
4
+ export { nodeHost, registerNodeHost, statePath } from "./host-node.ts";
5
+ export { engineDir } from "./engine-node.ts";
6
+ export { fileSource } from "./upload-file-node.ts";
7
+ export { measureLocal } from "./upload-price-node.ts";
8
+ export { fileSink } from "./download-sink-node.ts";
package/dist/index.js CHANGED
@@ -10,40 +10,28 @@
10
10
  // that stops to ask on somebody's server is a library that hangs. A program that wants the
11
11
  // NMTS key from the environment reads `credentials.ts` and decides for itself.
12
12
  //
13
+ // ⛔ THIS ENTRY POINT IS NODE'S, AND IT SAYS SO BY REGISTERING THE NODE HOST ON THE WAY IN. Every
14
+ // program that imports this package is unchanged by the split: the engine is still found on
15
+ // disk, state is still the same files under the same names, the environment is still read. What
16
+ // is new is that the same modules also run in a page, through `portable.ts` and a host
17
+ // registered there.
18
+ //
13
19
  // ⚠ THE PACKAGE'S `exports` MAP NAMES THIS FILE AND A FEW OF THE MODULES BELOW BY SUBPATH. A
14
20
  // module not named there is reachable through this file only; adding a subpath is a promise
15
21
  // that its shape stays put, so it is done on purpose and one at a time.
16
- // Errors the one shape every failure below arrives in.
17
- export { NmtsError, NotLoggedInError, renderError } from "./errors.js";
18
- // The server: one request function, and what a refusal looks like.
19
- export { request, ServerError, DEFAULT_TIMEOUT_MS } from "./api.js";
20
- export { DEFAULT_SERVER, SERVER_ENV_VAR, resolveServer } from "./server.js";
21
- export { NETWORKS, NETWORK_ENV_VAR, resolveNetwork } from "./network.js";
22
+ import { registerNodeHost } from "./host-node.js";
23
+ // BEFORE ANYTHING BELOW CAN BE CALLED, AND THAT IS WHY IT IS A STATEMENT RATHER THAN SOMETHING A
24
+ // CALLER DOES. A module's imports run before its body, so by the time a program holds anything
25
+ // from this file the host is in place — and no existing caller had to learn that there is one.
26
+ registerNodeHost();
27
+ // Everything that runs anywhere, with the reasons: `portable.ts`.
28
+ export * from "./portable.js";
22
29
  // Where a credential can come from on this machine. Reading is here; ASKING is not.
23
30
  export { API_KEY_ENV_VAR, API_KEY_FILE_ENV_VAR, CODE_ENV_VAR, CODE_FILE_ENV_VAR, configDir, readSecretFile, resolveAccountCode, resolveApiKey, } from "./credentials.js";
24
- // The account: what a code derives, and the engine that derives it.
25
- export { assertUsableCode, identityOf } from "./account.js";
26
- export { AAD, DERIVED, loadCrypto } from "./crypto.js";
27
- // The sealed file list: reading it, editing it, and walking it by path.
28
- export { readFileList } from "./manifest.js";
29
- export { addEntry, applyManyToList, applyToList, planAddition } from "./manifest-write.js";
30
- export { buildIndex, entryAt, folderIdFor, fullPathOf, isLive, KIND_FILE, KIND_FOLDER, namesIn, normalisePath, trashedAt, } from "./drive-paths.js";
31
- export { setTrashed } from "./item-trash.js";
32
- // Uploading: sealing here, buying storage with credits, pushing the sealed bytes.
33
- export { fileSource, partKeysOf, uploadFile } from "./upload-file.js";
34
- export { createUploadApi } from "./upload-api.js";
35
- export { CREDIT_BYTES, creditsFor, measureLocal, partSizeFor, planAndPrice, UPLOAD_EPOCHS, } from "./upload-price.js";
36
- export { clearItemRecord, clearReservation } from "./upload-store.js";
37
- export { UploadError } from "./upload-wire.js";
38
- export { DEFAULT_PART_BYTES } from "./seal.js";
39
- export { createBlobProtocol, readCurrentEpoch } from "./walrus-write.js";
40
- // Downloading: fetching sealed parts from the storage network and opening them here.
41
- export { fetchFile, fetchWithKey } from "./download.js";
42
- export { fileSink } from "./download-sink.js";
43
- export { AGGREGATOR_ENV_VAR, readBlob, RELAY_ENV_VAR, SUI_RPC_ENV_VAR } from "./walrus.js";
44
- // The wallet the NMTS key derives: reading it, and signing with it.
45
- export { coinAmount, readBalances, walletAddress } from "./wallet.js";
46
- export { chainReader } from "./wallet-chain.js";
47
- export { signerAddress, signExtension, signTransfer } from "./wallet-sign.js";
48
- // What this package is.
49
- export { HOME_URL, PRODUCT_NAME, SOURCE_URL, SUPPORT_EMAIL, VERSION } from "./product.js";
31
+ // This machine as a host: the engine on disk, state in the config directory, the real environment.
32
+ export { nodeHost, registerNodeHost, statePath } from "./host-node.js";
33
+ export { engineDir } from "./engine-node.js";
34
+ // Bytes in and out of the places only Node has: a file on a disk, and this process's own stdout.
35
+ export { fileSource } from "./upload-file-node.js";
36
+ export { measureLocal } from "./upload-price-node.js";
37
+ export { fileSink } from "./download-sink-node.js";