@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
@@ -30,7 +30,7 @@ export async function deleteAccount(options = {}) {
30
30
  // The tier gate has already refused the auto modes and, under skip-permissions, taken a
31
31
  // --reason and the --yes. ⛔ THE SENTENCE IS STILL TYPED IN EVERY OTHER MODE: `--yes` alone does
32
32
  // not stand for it — only the mode that turned every question off does.
33
- const typedFor = options.yes === true && currentMode() === "skip-permissions";
33
+ const typedFor = options.yes === true && (await currentMode()) === "skip-permissions";
34
34
  const ask = options.readLine ?? promptLine;
35
35
  if (!typedFor && options.readLine === undefined && !stdinIsATerminal()) {
36
36
  throw new NmtsError("There is no terminal to type into (stdin is not a TTY).", {
@@ -39,7 +39,7 @@ export async function erase(paths, options = {}) {
39
39
  nextStep: `\`${BINARY_NAME} ls --all\` prints the paths as this expects them.`,
40
40
  });
41
41
  }
42
- const typedFor = options.yes === true && currentMode() === "skip-permissions";
42
+ const typedFor = options.yes === true && (await currentMode()) === "skip-permissions";
43
43
  const ask = options.readLine ?? promptLine;
44
44
  if (!typedFor && options.readLine === undefined && !stdinIsATerminal()) {
45
45
  throw new NmtsError("There is no terminal to type into (stdin is not a TTY).", {
@@ -29,5 +29,7 @@ export interface ExtendOptions {
29
29
  now?: number;
30
30
  /** ⚠ SEAMS, NOT OPTIONS — the standing tip's own read and signature. No flag reaches them. */
31
31
  tip?: Pick<StandingTipInput, "readDonation" | "sign">;
32
+ /** `--wallet N`: which wallet pays, this run only. Absent = the account's own number. */
33
+ wallet?: string | undefined;
32
34
  }
33
35
  export declare function extend(target: string | undefined, options?: ExtendOptions): Promise<number>;
@@ -31,6 +31,8 @@ import { resolveNetwork } from "../network.js";
31
31
  import { BINARY_NAME } from "../product.js";
32
32
  import { openSession } from "../session.js";
33
33
  import { coinAmount, walletAddress } from "../wallet.js";
34
+ import { payingWalletIndex } from "../wallet-pay-index.js";
35
+ import { activeWalletOf } from "../shared/lib/drive/manifest-settings.js";
34
36
  import { budgetFacts, describeBudget, readBudget, shortfallNextStep } from "../extend-budget.js";
35
37
  export async function extend(target, options = {}) {
36
38
  const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
@@ -110,7 +112,10 @@ export async function extend(target, options = {}) {
110
112
  const cohort = Math.max(0, ...preview.targets.map((t) => t.sharedItems));
111
113
  const unreachable = preview.treasuryParts + preview.untrackedParts;
112
114
  // What the wallet holds and what the chain would charge — read, never assumed (`extend-budget.ts`).
113
- const address = await walletAddress(session.code);
115
+ // WHICH WALLET PAYS comes out of the list this run already read, before the price is measured
116
+ // against a balance: the address below is the one that will sign (`wallet-pay-index.ts`).
117
+ const wallet = await payingWalletIndex({ ...options, readActiveWallet: async () => activeWalletOf(settings) });
118
+ const address = await walletAddress(session.code, wallet);
114
119
  const budget = await readBudget(reads, {
115
120
  address,
116
121
  objectIds: preview.targets.map((t) => t.objectId),
@@ -182,6 +187,7 @@ export async function extend(target, options = {}) {
182
187
  const digest = await sign({
183
188
  network: session.network,
184
189
  code: session.code,
190
+ wallet,
185
191
  objectIds: preview.targets.map((t) => t.objectId),
186
192
  epochs,
187
193
  });
@@ -227,6 +233,7 @@ export async function extend(target, options = {}) {
227
233
  network: resolveNetwork(session.server, session.network),
228
234
  code: session.code,
229
235
  settings,
236
+ wallet,
230
237
  paidWalFrost: frost,
231
238
  say: options.json === true ? (line) => void process.stderr.write(`${line}\n`) : say,
232
239
  ...(options.tip ?? {}),
@@ -28,7 +28,7 @@
28
28
  import { basename, resolve } from "node:path";
29
29
  import { CODE_ENV_VAR } from "../credentials.js";
30
30
  import { fetchFile } from "../download.js";
31
- import { fileSink, stdoutSink } from "../download-sink.js";
31
+ import { fileSink, stdoutSink } from "../download-sink-node.js";
32
32
  import { buildIndex, entryAt, fullPathOf, KIND_FILE, normalisePath } from "../drive-paths.js";
33
33
  import { NmtsError } from "../errors.js";
34
34
  import { readFileList } from "../manifest.js";
@@ -35,7 +35,7 @@ export async function listfile(options = {}) {
35
35
  const emit = options.writeDocument ?? ((text) => process.stdout.write(text));
36
36
  const resolved = await requireAccountCode();
37
37
  const identity = await identityOf(resolved.code);
38
- const kept = readKeptList(identity.accountId);
38
+ const kept = await readKeptList(identity.accountId);
39
39
  if (kept === null) {
40
40
  throw new NmtsError(`This machine has no copy of this account's file list.`, {
41
41
  exitCode: 4,
@@ -171,6 +171,10 @@ export async function mcp(options = {}) {
171
171
  // reading stdout must see nothing but protocol.
172
172
  note(`${PRODUCT_NAME} · ${identity.accountId} · ${network} · files land in ${outDir}`);
173
173
  note(`Tools: ${tools.map((t) => t.name).join(" · ")}`);
174
+ // ⚠ READ ONCE, BEFORE THE SERVER STARTS. The line below is written from a callback the
175
+ // protocol drives, and a callback that had to await would report the setting after the
176
+ // sentence it belongs to.
177
+ const announcedMode = await currentMode();
174
178
  await serve({
175
179
  input: options.input ?? process.stdin,
176
180
  output: options.output ?? ((line) => process.stdout.write(`${line}\n`)),
@@ -193,7 +197,7 @@ export async function mcp(options = {}) {
193
197
  // ⚠ A MODE THAT IS ON DECIDES THIS, so it is read here rather than assumed. Saying "every
194
198
  // share will ask you" to somebody who turned a mode on would be telling them the opposite
195
199
  // of what their own setting does.
196
- if (currentMode() !== "default")
200
+ if (announcedMode !== "default")
197
201
  return;
198
202
  note(built === null
199
203
  ? "This client cannot show you a question, so sharing is refused here. Share from a terminal."
@@ -22,9 +22,9 @@ export async function mode(wanted, target, options = {}) {
22
22
  const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
23
23
  const now = options.now ?? (() => new Date());
24
24
  if (wanted === undefined || wanted === "") {
25
- const at = currentMode();
25
+ const at = await currentMode();
26
26
  if (options.json === true) {
27
- say(JSON.stringify({ mode: at, setAt: setAt(), means: MODE_MEANS[at] }));
27
+ say(JSON.stringify({ mode: at, setAt: await setAt(), means: MODE_MEANS[at] }));
28
28
  return 0;
29
29
  }
30
30
  say(`${at} — ${MODE_MEANS[at]}`);
@@ -3,4 +3,4 @@ export interface OnCollisionOptions {
3
3
  write?: (line: string) => void;
4
4
  now?: () => Date;
5
5
  }
6
- export declare function onCollision(wanted: string | undefined, options?: OnCollisionOptions): number;
6
+ export declare function onCollision(wanted: string | undefined, options?: OnCollisionOptions): Promise<number>;
@@ -14,11 +14,11 @@ import { BINARY_NAME, VERSION } from "../product.js";
14
14
  function isChoice(value) {
15
15
  return COLLISION_CHOICES.includes(value);
16
16
  }
17
- export function onCollision(wanted, options = {}) {
17
+ export async function onCollision(wanted, options = {}) {
18
18
  const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
19
19
  const now = options.now ?? (() => new Date());
20
20
  if (wanted === undefined || wanted === "") {
21
- const at = currentChoice();
21
+ const at = await currentChoice();
22
22
  if (options.json === true) {
23
23
  say(JSON.stringify({ onCollision: at, means: COLLISION_MEANS[at] }));
24
24
  return 0;
@@ -39,7 +39,7 @@ export function onCollision(wanted, options = {}) {
39
39
  nextStep: `One of: ${COLLISION_CHOICES.join(" · ")}`,
40
40
  });
41
41
  }
42
- setChoice(wanted, VERSION, now());
42
+ await setChoice(wanted, VERSION, now());
43
43
  say(`${wanted} — ${COLLISION_MEANS[wanted]}`);
44
44
  return 0;
45
45
  }
@@ -0,0 +1,9 @@
1
+ /** Where the pair goes when `--out` names nowhere. Beside whatever the business is running. */
2
+ export declare const DEFAULT_KEY_FILE = "nmts-business-key.json";
3
+ /** Where a person registers the public half. The one path this command exists to name. */
4
+ export declare const REGISTER_PATH = "Settings \u203A Developer \u203A Platform";
5
+ export interface PlatformOptions {
6
+ out?: string | undefined;
7
+ write?: ((line: string) => void) | undefined;
8
+ }
9
+ export declare function platform(sub: string | undefined, options?: PlatformOptions): number;
@@ -0,0 +1,124 @@
1
+ // `nmts platform` — the two things a business does from a terminal.
2
+ //
3
+ // ⛔ THE KEY PAIR IS MADE HERE AND REGISTERED SOMEWHERE ELSE. Making it needs nothing but a random
4
+ // source; registering it needs a signed-in person, because attaching a standing credential to
5
+ // an account is a decision a session alone may not make. So `keygen` does the half a terminal
6
+ // can do and `register` says, in one sentence, where the other half is.
7
+ //
8
+ // ⛔ THE PRIVATE HALF IS NEVER PRINTED. It goes into a file only its owner can read and nowhere
9
+ // else — not to stdout, not into a message, not into the run log. What this command puts on the
10
+ // screen is the PUBLIC half, which is what gets pasted into the browser.
11
+ //
12
+ // ⛔ AND THE FILE IS NEVER REPLACED. `--force` does not open it: the file it would overwrite may
13
+ // hold the only copy of a key a business is already registered under, and replacing it locks
14
+ // that business out of its own door with nothing able to bring the key back.
15
+ import { mkdirSync, statSync, writeFileSync } from "node:fs";
16
+ import { isAbsolute, resolve } from "node:path";
17
+ import { modesAreEnforced } from "../credentials.js";
18
+ import { NmtsError } from "../errors.js";
19
+ import { generateBusinessKeys } from "../platform-sign.js";
20
+ import { BINARY_NAME, HOME_URL } from "../product.js";
21
+ import { STDOUT_TARGET } from "../stdout.js";
22
+ /** Where the pair goes when `--out` names nowhere. Beside whatever the business is running. */
23
+ export const DEFAULT_KEY_FILE = "nmts-business-key.json";
24
+ /** Where a person registers the public half. The one path this command exists to name. */
25
+ export const REGISTER_PATH = "Settings › Developer › Platform";
26
+ export function platform(sub, options = {}) {
27
+ const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
28
+ switch (sub) {
29
+ case "keygen":
30
+ return keygen(options.out, say);
31
+ case "register":
32
+ return register(say);
33
+ default:
34
+ throw new NmtsError(sub === undefined ? "`platform` needs a subcommand." : `Unknown subcommand: ${sub}`, {
35
+ exitCode: 2,
36
+ nextStep: `\`${BINARY_NAME} platform keygen\` makes the key pair a business signs with; ` +
37
+ `\`${BINARY_NAME} platform register\` says where the public half is registered.`,
38
+ });
39
+ }
40
+ }
41
+ /** Make the pair, write it where only its owner can read it, and print the half that is not secret. */
42
+ function keygen(out, say) {
43
+ const path = targetFor(out);
44
+ const keys = generateBusinessKeys();
45
+ writeKeyFile(path, keys);
46
+ say(`Public key: ${keys.publicKey}`);
47
+ say(`Written to: ${path}`);
48
+ // The same sentence `nmts doctor` uses for a stored key: where no mode applies, say so.
49
+ if (!modesAreEnforced()) {
50
+ say(`Windows applies no POSIX file mode, so this file inherits its folder's permissions ` +
51
+ `rather than being restricted to one user.`);
52
+ }
53
+ say(`Register the public key at ${HOME_URL} — ${REGISTER_PATH}. The private half stays in that file.`);
54
+ return 0;
55
+ }
56
+ /**
57
+ * Say where registering happens, and stop.
58
+ *
59
+ * ⛔ EXIT 2, WHICH IS "THE COMMAND LINE WAS WRONG" AND NOT "IT FAILED". An agent that read this as
60
+ * a transient failure would try it again; the code says the request cannot be made this way at
61
+ * all, whatever the arguments are.
62
+ */
63
+ function register(say) {
64
+ say(`Registering a business needs a signed-in browser session, so this tool cannot do it.`);
65
+ say(`Open ${HOME_URL} and go to ${REGISTER_PATH}.`);
66
+ say(`Paste the public key from \`${BINARY_NAME} platform keygen\` there.`);
67
+ return 2;
68
+ }
69
+ /** Where the file goes, refusing a name that is already taken and refusing stdout outright. */
70
+ function targetFor(out) {
71
+ const named = out === undefined || out === "" ? DEFAULT_KEY_FILE : out;
72
+ if (named === STDOUT_TARGET) {
73
+ throw new NmtsError("The business key pair will not be sent to stdout.", {
74
+ exitCode: 2,
75
+ nextStep: "Nothing was made. stdout is what a program reads and a log keeps, and this file holds " +
76
+ "the private half. Name a file instead.",
77
+ });
78
+ }
79
+ const path = isAbsolute(named) ? named : resolve(process.cwd(), named);
80
+ let existing = null;
81
+ try {
82
+ existing = statSync(path);
83
+ }
84
+ catch {
85
+ // Not there is exactly what this wants.
86
+ }
87
+ if (existing !== null) {
88
+ throw new NmtsError(`${path} is already there.`, {
89
+ exitCode: 4,
90
+ nextStep: existing.isDirectory()
91
+ ? "--out names the FILE the key pair goes into, not a directory."
92
+ : "Nothing was made. That file is not replaced, whatever --force says: it may hold the " +
93
+ "key a business is already registered under. Name one that does not exist.",
94
+ });
95
+ }
96
+ return path;
97
+ }
98
+ /**
99
+ * Write the pair, readable by nobody else.
100
+ *
101
+ * ⚠ `wx` FAILS IF THE NAME APPEARED SINCE THE CHECK ABOVE, which is the point of using it rather
102
+ * than trusting that check: between the two, something else may have written there.
103
+ *
104
+ * ⚠ ON WINDOWS THE MODE IS IGNORED and the file inherits the folder's permissions — the same limit
105
+ * every other file this tool writes has, and claiming otherwise would claim a guarantee the
106
+ * platform does not give.
107
+ */
108
+ function writeKeyFile(path, keys) {
109
+ mkdirSync(resolve(path, ".."), { recursive: true, mode: 0o700 });
110
+ // ⚠ The field names are the ones the library takes, so the parsed file goes straight into a
111
+ // client without a program in the middle renaming anything.
112
+ const text = `${JSON.stringify({ publicKey: keys.publicKey, privateKey: keys.privateKey }, null, 2)}\n`;
113
+ try {
114
+ writeFileSync(path, text, { mode: 0o600, flag: "wx" });
115
+ }
116
+ catch (error) {
117
+ // ⛔ THE CAUSE IS NAMED AND THE KEY IS NOT. The errno line carries the path and never the
118
+ // contents, so it is safe to pass on.
119
+ throw new NmtsError(`The key pair could not be written to ${path}.`, {
120
+ exitCode: 1,
121
+ nextStep: `Nothing was made. Cause: ${error instanceof Error ? error.message : String(error)}`,
122
+ });
123
+ }
124
+ }
@@ -21,7 +21,7 @@
21
21
  import { mkdirSync, existsSync } from "node:fs";
22
22
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
23
23
  import { fetchFile } from "../download.js";
24
- import { fileSink } from "../download-sink.js";
24
+ import { fileSink } from "../download-sink-node.js";
25
25
  import { buildIndex, entryAt, fullPathOf, isLive, KIND_FILE, KIND_FOLDER, normalisePath, underPrefix, } from "../drive-paths.js";
26
26
  import { NmtsError } from "../errors.js";
27
27
  import { readFileList } from "../manifest.js";
@@ -3,10 +3,13 @@
3
3
  //
4
4
  // ⛔ SPLIT OUT OF `push.ts` FOR THE LENGTH GATE, and along the one seam that was already there:
5
5
  // `push.ts` decides WHICH files go and where; this decides how the wallet pays for each.
6
+ import { payingWalletIndex } from "../wallet-pay-index.js";
7
+ import { activeWalletOf } from "../shared/lib/drive/manifest-settings.js";
6
8
  import { loadCrypto } from "../crypto.js";
7
9
  import { NmtsError } from "../errors.js";
8
10
  import { resolveNetwork } from "../network.js";
9
- import { Progress, silentSink, stderrSink } from "../progress.js";
11
+ import { Progress, silentSink } from "../progress.js";
12
+ import { stderrSink } from "../progress-node.js";
10
13
  import { folderFor } from "./push.js";
11
14
  /** The wallet rail, one file at a time, stopping at the first failure as the credit rail does. */
12
15
  export async function pushWithWallet(session, options, run) {
@@ -24,6 +27,10 @@ export async function pushWithWallet(session, options, run) {
24
27
  rule: run.rule,
25
28
  onCollision: run.asked,
26
29
  settings: run.settings,
30
+ wallet: await payingWalletIndex({
31
+ ...options,
32
+ readActiveWallet: async () => activeWalletOf(run.settings),
33
+ }),
27
34
  progress: new Progress(options.json === true ? silentSink() : stderrSink(), "uploading"),
28
35
  say,
29
36
  json: options.json === true,
@@ -19,6 +19,8 @@ export interface PushOptions {
19
19
  epochs?: string | number | undefined;
20
20
  /** ⛔ Refused here: a held resource holds ONE blob, and a directory is many. */
21
21
  storage?: string | undefined;
22
+ /** `--pay wallet`: which of this key's wallets pays, this run only. Absent = the account's own. */
23
+ wallet?: string | undefined;
22
24
  json?: boolean;
23
25
  write?: (line: string) => void;
24
26
  /**
@@ -27,9 +27,11 @@ import { depositDefaultOf, depositLines, parseDeposit, refuseDepositWithWallet }
27
27
  import { setTrashed } from "../item-trash.js";
28
28
  import { paddingRuleOf, readFileList } from "../manifest.js";
29
29
  import { BINARY_NAME } from "../product.js";
30
- import { Progress, silentSink, stderrSink } from "../progress.js";
30
+ import { Progress, silentSink } from "../progress.js";
31
+ import { stderrSink } from "../progress-node.js";
31
32
  import { openSession } from "../session.js";
32
- import { fileSource, partKeysOf, uploadFile } from "../upload-file.js";
33
+ import { partKeysOf, uploadFile } from "../upload-file.js";
34
+ import { fileSource } from "../upload-file-node.js";
33
35
  import { createUploadApi } from "../upload-api.js";
34
36
  import { clearItemRecord, clearReservation } from "../upload-store.js";
35
37
  import { CREDIT_BYTES, partSizeFor, planAndPrice, UPLOAD_EPOCHS } from "../upload-price.js";
@@ -242,9 +244,9 @@ async function sendOne(session, crypt, one, ctx) {
242
244
  },
243
245
  });
244
246
  // ⛔ ONLY NOW. Until the entry is in the list the file is paid for and invisible.
245
- clearItemRecord(result.fileKey);
247
+ await clearItemRecord(result.fileKey);
246
248
  for (const record of partKeysOf(result.fileKey, result.parts))
247
- clearReservation(record);
249
+ await clearReservation(record);
248
250
  // The file this one displaced goes to the server's trash last — see the same note in `put.ts`.
249
251
  if (added.replaced)
250
252
  await setTrashed(session.server, session.apiKey, added.replaced.id, true);
@@ -0,0 +1,8 @@
1
+ /** Who pays, or a refusal for a payer this tool does not know. */
2
+ export declare function payerOf(pay: string | undefined): "credits" | "wallet";
3
+ /** The two options that only mean something when the wallet pays, refused when it does not. */
4
+ export declare function refuseWalletOnlyOptions(options: {
5
+ epochs?: string | number | undefined;
6
+ storage?: string | undefined;
7
+ wallet?: string | undefined;
8
+ }): void;
@@ -0,0 +1,41 @@
1
+ // WHO PAYS FOR AN UPLOAD, and which options mean anything once that is answered.
2
+ //
3
+ // ⛔ THE OPTIONS ARE REFUSED, NOT IGNORED. `--epochs 12 --pay credits` is somebody asking for a
4
+ // term credits do not sell; accepting it silently would upload for a different length than the
5
+ // person typed and charge them for it. Every refusal here happens before a file is measured.
6
+ //
7
+ // ⚠ They moved out of `put.ts` on 2026-09-16 (`--wallet` joined them and that file has a ceiling).
8
+ // `put.ts` re-exports them, so `push.ts` and the tests spell the same path they always did.
9
+ import { NmtsError } from "../errors.js";
10
+ /** Who pays, or a refusal for a payer this tool does not know. */
11
+ export function payerOf(pay) {
12
+ if (pay === undefined || pay === "credits")
13
+ return "credits";
14
+ if (pay === "wallet")
15
+ return "wallet";
16
+ throw new NmtsError(`--pay takes credits or wallet, not "${pay}".`, {
17
+ exitCode: 2,
18
+ nextStep: `Nothing was sent. --pay wallet buys the storage from the wallet this NMTS key derives; without it credits pay.`,
19
+ });
20
+ }
21
+ /** The two options that only mean something when the wallet pays, refused when it does not. */
22
+ export function refuseWalletOnlyOptions(options) {
23
+ if (options.wallet !== undefined) {
24
+ throw new NmtsError("--wallet only applies with --pay wallet: credits are not held in a wallet.", {
25
+ exitCode: 2,
26
+ nextStep: `Nothing was sent. Add --pay wallet to pay from one of this key's wallets.`,
27
+ });
28
+ }
29
+ if (options.epochs !== undefined) {
30
+ throw new NmtsError("--epochs only applies with --pay wallet: one credit buys a fixed term.", {
31
+ exitCode: 2,
32
+ nextStep: `Nothing was sent. Add --pay wallet to choose the term, or leave --epochs off to pay with credits.`,
33
+ });
34
+ }
35
+ if (options.storage !== undefined) {
36
+ throw new NmtsError("--storage only applies with --pay wallet: credits buy storage from the treasury.", {
37
+ exitCode: 2,
38
+ nextStep: `Nothing was sent. Add --pay wallet to use a storage resource this wallet holds.`,
39
+ });
40
+ }
41
+ }
@@ -15,6 +15,8 @@ export interface WalletPayOptions {
15
15
  epochs?: string | number | undefined;
16
16
  /** `fit`, `whole`, or a held resource's object id. Absent = buy new storage. */
17
17
  storage?: string | undefined;
18
+ /** `--wallet N`: which wallet pays, this run only. Absent = the account's own number. */
19
+ wallet?: string | undefined;
18
20
  /** The instant the wallet agreement is measured against. */
19
21
  now?: number;
20
22
  /** ⚠ A SEAM, NOT AN OPTION — no flag reaches it. */
@@ -54,16 +56,20 @@ export interface WalletUploadContext {
54
56
  onCollision?: OnCollision | undefined;
55
57
  /** The sealed list's account settings, as read for this run — the standing tip lives in them. */
56
58
  settings?: AccountSettings | undefined;
59
+ /** Which of this key's wallets pays (`wallet-pay-index.ts`). Read from the same list as above. */
60
+ wallet: number;
57
61
  progress: Progress;
58
62
  say: (line: string) => void;
59
63
  json: boolean;
60
64
  }
61
65
  export declare function putWithWallet(target: string | undefined, options: PutWalletOptions): Promise<number>;
62
66
  /**
63
- * Review, agree, upload and record ONE file. Null when `--dry-run` stopped at the review.
67
+ * Review, agree, upload and record ONE file for a person who is watching. Null when `--dry-run`
68
+ * stopped at the review.
64
69
  *
65
- * ⛔ THE SIGNING MODULE IS LOADED AFTER THE AGREEMENT, and only then a dry run and a refusal
66
- * never bring the code that can spend into memory.
70
+ * ⛔ THE AGREEMENT HANDED IN BELOW IS THIS MACHINE'S GRANT, held against the total the review
71
+ * names. It is the one gate between a program and somebody's wallet, and the reason it is
72
+ * handed to the library rather than living inside it: a library call has nobody to ask.
67
73
  */
68
74
  export declare function uploadOneWithWallet(ctx: WalletUploadContext, file: WalletUploadFile, options: WalletPayOptions & {
69
75
  dryRun?: boolean | undefined;