@needmoretruth/nmts-cli 0.35.0 → 0.36.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/AGENTS.md +11 -12
  2. package/CHANGELOG.md +35 -0
  3. package/README.ko.md +19 -21
  4. package/README.md +21 -22
  5. package/dist/account.js +2 -1
  6. package/dist/arg-options.d.ts +3 -1
  7. package/dist/artifact-about.d.ts +1 -1
  8. package/dist/autonomy.d.ts +3 -10
  9. package/dist/autonomy.js +15 -17
  10. package/dist/bytes.d.ts +15 -0
  11. package/dist/bytes.js +63 -0
  12. package/dist/collision.d.ts +5 -5
  13. package/dist/collision.js +19 -22
  14. package/dist/commands/delete-account.js +1 -1
  15. package/dist/commands/erase.js +1 -1
  16. package/dist/commands/get.js +1 -1
  17. package/dist/commands/listfile.js +1 -1
  18. package/dist/commands/mcp.js +5 -1
  19. package/dist/commands/mode.js +2 -2
  20. package/dist/commands/on-collision.d.ts +1 -1
  21. package/dist/commands/on-collision.js +3 -3
  22. package/dist/commands/platform.d.ts +9 -0
  23. package/dist/commands/platform.js +153 -0
  24. package/dist/commands/pull.js +1 -1
  25. package/dist/commands/push-wallet.js +2 -1
  26. package/dist/commands/push.js +6 -4
  27. package/dist/commands/put-wallet.js +5 -3
  28. package/dist/commands/put.js +8 -5
  29. package/dist/commands/receive.js +1 -1
  30. package/dist/commands/settings.js +1 -1
  31. package/dist/commands/share.js +1 -1
  32. package/dist/commands/support.js +3 -3
  33. package/dist/commands/unlock.js +2 -2
  34. package/dist/commands/wallet-hall.d.ts +4 -1
  35. package/dist/commands/wallet-hall.js +16 -8
  36. package/dist/commands/wallet-storage-ops.d.ts +4 -1
  37. package/dist/commands/wallet-storage-ops.js +7 -2
  38. package/dist/commands/wallet-swap.d.ts +3 -0
  39. package/dist/commands/wallet-swap.js +8 -4
  40. package/dist/commands/wallet.d.ts +1 -1
  41. package/dist/crypto.d.ts +8 -5
  42. package/dist/crypto.js +15 -61
  43. package/dist/download-sink-node.d.ts +49 -0
  44. package/dist/download-sink-node.js +204 -0
  45. package/dist/download-sink.d.ts +0 -48
  46. package/dist/download-sink.js +6 -199
  47. package/dist/download.js +5 -4
  48. package/dist/engine-node.d.ts +19 -0
  49. package/dist/engine-node.js +77 -0
  50. package/dist/env-vars.d.ts +20 -0
  51. package/dist/env-vars.js +27 -0
  52. package/dist/gate.js +1 -1
  53. package/dist/help.js +9 -4
  54. package/dist/host-contract.d.ts +7 -0
  55. package/dist/host-contract.js +95 -0
  56. package/dist/host-node.d.ts +27 -0
  57. package/dist/host-node.js +192 -0
  58. package/dist/host.d.ts +99 -0
  59. package/dist/host.js +59 -0
  60. package/dist/index.d.ts +6 -49
  61. package/dist/index.js +20 -45
  62. package/dist/main.js +7 -6
  63. package/dist/manifest-chunk-cache.d.ts +3 -3
  64. package/dist/manifest-chunk-cache.js +39 -44
  65. package/dist/manifest-chunk-flow.js +15 -14
  66. package/dist/manifest-write.js +1 -1
  67. package/dist/manifest.d.ts +2 -2
  68. package/dist/manifest.js +37 -34
  69. package/dist/mcp-tools/tiers.js +1 -1
  70. package/dist/network.d.ts +1 -1
  71. package/dist/network.js +6 -2
  72. package/dist/platform-sign.d.ts +143 -0
  73. package/dist/platform-sign.js +220 -0
  74. package/dist/portable.d.ts +59 -0
  75. package/dist/portable.js +82 -0
  76. package/dist/product.d.ts +1 -1
  77. package/dist/product.js +1 -1
  78. package/dist/progress-node.d.ts +2 -0
  79. package/dist/progress-node.js +14 -0
  80. package/dist/progress.d.ts +0 -1
  81. package/dist/progress.js +4 -8
  82. package/dist/redact.js +3 -2
  83. package/dist/registration.js +5 -2
  84. package/dist/risk.d.ts +7 -0
  85. package/dist/risk.js +10 -0
  86. package/dist/run-log.d.ts +4 -4
  87. package/dist/run-log.js +29 -36
  88. package/dist/seal.js +5 -4
  89. package/dist/server.d.ts +1 -1
  90. package/dist/server.js +6 -2
  91. package/dist/setup-questions.js +2 -2
  92. package/dist/upload-file-node.d.ts +3 -0
  93. package/dist/upload-file-node.js +38 -0
  94. package/dist/upload-file.d.ts +2 -2
  95. package/dist/upload-file.js +15 -45
  96. package/dist/upload-price-node.d.ts +8 -0
  97. package/dist/upload-price-node.js +37 -0
  98. package/dist/upload-price.d.ts +0 -8
  99. package/dist/upload-price.js +4 -33
  100. package/dist/upload-steps.js +4 -4
  101. package/dist/upload-store.d.ts +14 -14
  102. package/dist/upload-store.js +68 -75
  103. package/dist/upload-wallet-chain.js +3 -2
  104. package/dist/upload-wallet-put.js +2 -2
  105. package/dist/upload-wallet.js +16 -15
  106. package/dist/upload.js +16 -15
  107. package/dist/wallet-grant.js +1 -1
  108. package/dist/wallet-sign-seams.d.ts +12 -0
  109. package/dist/wallet-sign.js +10 -13
  110. package/dist/walrus-write.js +3 -2
  111. package/dist/walrus.d.ts +1 -16
  112. package/dist/walrus.js +11 -22
  113. package/docs/commands/credits.md +1 -1
  114. package/docs/commands/platform.md +15 -0
  115. package/docs/commands/support.md +1 -1
  116. package/docs/commands/trash.md +1 -1
  117. package/docs/commands/wallet.md +11 -7
  118. package/package.json +13 -2
package/dist/collision.js CHANGED
@@ -23,12 +23,13 @@
23
23
  // ⚠ WHAT THIS CANNOT DO is tell an agent from a person. Nothing on a command line can. What it can
24
24
  // do is make the destructive answer require a setting that was turned on deliberately, and say
25
25
  // which setting decided.
26
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync } from "node:fs";
27
- import { join } from "node:path";
28
26
  import { currentMode } from "./autonomy.js";
27
+ import { fromUtf8, utf8 } from "./bytes.js";
29
28
  import { NmtsError } from "./errors.js";
29
+ import { host } from "./host.js";
30
30
  import { BINARY_NAME } from "./product.js";
31
- import { configDir, modesAreEnforced } from "./credentials.js";
31
+ /** The one key this answer lives under. On this machine that is `collision.json`. */
32
+ const KEY = "collision";
32
33
  export const COLLISION_CHOICES = ["rename", "overwrite"];
33
34
  /** What each choice does, in the words the tool prints. One line each. */
34
35
  export const COLLISION_MEANS = {
@@ -53,9 +54,6 @@ export function parseAsked(typed) {
53
54
  }
54
55
  /** What is written down when nobody has chosen. */
55
56
  export const DEFAULT_COLLISION = "rename";
56
- function path() {
57
- return join(configDir(), "collision.json");
58
- }
59
57
  function isChoice(value) {
60
58
  return typeof value === "string" && COLLISION_CHOICES.includes(value);
61
59
  }
@@ -65,9 +63,12 @@ function isChoice(value) {
65
63
  * ⛔ Unreadable counts as `rename`, for the same reason autonomy unreadable counts as off: the
66
64
  * fail-safe direction for "I do not know" is the one that destroys nothing.
67
65
  */
68
- export function currentChoice() {
66
+ export async function currentChoice() {
69
67
  try {
70
- const parsed = JSON.parse(readFileSync(path(), "utf8"));
68
+ const held = await host().state.read(KEY);
69
+ if (held === undefined)
70
+ return DEFAULT_COLLISION;
71
+ const parsed = JSON.parse(fromUtf8(held));
71
72
  if (typeof parsed !== "object" || parsed === null)
72
73
  return DEFAULT_COLLISION;
73
74
  const choice = Reflect.get(parsed, "onCollision");
@@ -78,21 +79,17 @@ export function currentChoice() {
78
79
  }
79
80
  }
80
81
  /** Has anybody answered on this machine? Used to know whether setup still has to ask. */
81
- export function hasChosen() {
82
- return existsSync(path());
82
+ export async function hasChosen() {
83
+ return (await host().state.read(KEY)) !== undefined;
83
84
  }
84
85
  /** Write the choice down, with the date and the version that asked. */
85
- export function setChoice(choice, version, now) {
86
- mkdirSync(configDir(), { recursive: true, mode: 0o700 });
86
+ export async function setChoice(choice, version, now) {
87
87
  const body = { onCollision: choice, setAt: now.toISOString(), byVersion: version };
88
- writeFileSync(path(), `${JSON.stringify(body, null, 2)}\n`, { mode: 0o600 });
89
- if (modesAreEnforced())
90
- chmodSync(path(), 0o600);
88
+ await host().state.write(KEY, utf8(`${JSON.stringify(body, null, 2)}\n`));
91
89
  }
92
90
  /** Forget the answer, so setup asks again. */
93
- export function forgetChoice() {
94
- if (existsSync(path()))
95
- rmSync(path(), { force: true });
91
+ export async function forgetChoice() {
92
+ await host().state.remove(KEY);
96
93
  }
97
94
  /**
98
95
  * What to do with this collision, and what settled it.
@@ -108,14 +105,14 @@ export function forgetChoice() {
108
105
  * ⛔ THE OVERRIDE IS ONE-WAY. A mode can let `overwrite` through; nothing here turns a `rename`
109
106
  * into an `overwrite`.
110
107
  */
111
- export function decide(
108
+ export async function decide(
112
109
  /** What this run asked for, if anything. `undefined` means "use what this machine is set to". */
113
- askedFor, setting = currentChoice(), mode = currentMode()) {
110
+ askedFor, setting, mode) {
114
111
  if (askedFor === undefined)
115
- return { choice: setting, by: "setting" };
112
+ return { choice: setting ?? (await currentChoice()), by: "setting" };
116
113
  if (askedFor === "rename")
117
114
  return { choice: "rename", by: "asked-for" };
118
- if (mode === "default")
115
+ if ((mode ?? (await currentMode())) === "default")
119
116
  return { choice: "rename", by: "agent-refused" };
120
117
  return { choice: "overwrite", by: "asked-for" };
121
118
  }
@@ -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).", {
@@ -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,153 @@
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 { spawnSync } from "node:child_process";
16
+ import { mkdirSync, statSync, writeFileSync } from "node:fs";
17
+ import { userInfo } from "node:os";
18
+ import { isAbsolute, resolve } from "node:path";
19
+ import { modesAreEnforced } from "../credentials.js";
20
+ import { NmtsError } from "../errors.js";
21
+ import { generateBusinessKeys } from "../platform-sign.js";
22
+ import { BINARY_NAME, HOME_URL } from "../product.js";
23
+ import { STDOUT_TARGET } from "../stdout.js";
24
+ /** Where the pair goes when `--out` names nowhere. Beside whatever the business is running. */
25
+ export const DEFAULT_KEY_FILE = "nmts-business-key.json";
26
+ /** Where a person registers the public half. The one path this command exists to name. */
27
+ export const REGISTER_PATH = "Settings › Developer › Platform";
28
+ export function platform(sub, options = {}) {
29
+ const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
30
+ switch (sub) {
31
+ case "keygen":
32
+ return keygen(options.out, say);
33
+ case "register":
34
+ return register(say);
35
+ default:
36
+ throw new NmtsError(sub === undefined ? "`platform` needs a subcommand." : `Unknown subcommand: ${sub}`, {
37
+ exitCode: 2,
38
+ nextStep: `\`${BINARY_NAME} platform keygen\` makes the key pair a business signs with; ` +
39
+ `\`${BINARY_NAME} platform register\` says where the public half is registered.`,
40
+ });
41
+ }
42
+ }
43
+ /** Make the pair, write it where only its owner can read it, and print the half that is not secret. */
44
+ function keygen(out, say) {
45
+ const path = targetFor(out);
46
+ const keys = generateBusinessKeys();
47
+ writeKeyFile(path, keys);
48
+ say(`Public key: ${keys.publicKey}`);
49
+ say(`Written to: ${path}`);
50
+ if (!modesAreEnforced()) {
51
+ // ⚠ THIS FILE LANDS WHEREVER THE COMMAND WAS RUN, which may be a shared folder — unlike the
52
+ // stored NMTS key, which lives under the user's own profile. So on Windows the mode is not
53
+ // merely reported as absent: the file is cut off from the folder's permissions.
54
+ say(restrictToCurrentUser(path)
55
+ ? `Inherited permissions were removed; only your Windows account can open this file.`
56
+ : `Windows applies no POSIX file mode, and restricting the file with icacls failed, so it ` +
57
+ `inherits its folder's permissions. Move it somewhere only you can read.`);
58
+ }
59
+ say(`Register the public key at ${HOME_URL} — ${REGISTER_PATH}. The private half stays in that file.`);
60
+ return 0;
61
+ }
62
+ /**
63
+ * Windows only: drop the inherited entries and grant the current account alone.
64
+ *
65
+ * ⛔ `icacls`, NOT A HAND-ROLLED ACL. It ships with every supported Windows, and this is the shape
66
+ * OpenSSH asks of a private key there. One invocation does both halves, so there is no moment
67
+ * with the inheritance gone and nobody granted.
68
+ * ⚠ A FAILURE IS REPORTED, NOT THROWN: the pair is already written and is the only copy, so the
69
+ * command must still print the public half and say plainly what protects the file.
70
+ */
71
+ function restrictToCurrentUser(path) {
72
+ try {
73
+ const name = userInfo().username;
74
+ const domain = process.env.USERDOMAIN;
75
+ const account = domain === undefined || domain === "" ? name : `${domain}\\${name}`;
76
+ const done = spawnSync("icacls", [path, "/inheritance:r", "/grant:r", `${account}:F`], {
77
+ windowsHide: true,
78
+ stdio: "ignore",
79
+ });
80
+ return done.status === 0;
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ /**
87
+ * Say where registering happens, and stop.
88
+ *
89
+ * ⛔ EXIT 2, WHICH IS "THE COMMAND LINE WAS WRONG" AND NOT "IT FAILED". An agent that read this as
90
+ * a transient failure would try it again; the code says the request cannot be made this way at
91
+ * all, whatever the arguments are.
92
+ */
93
+ function register(say) {
94
+ say(`Registering a business needs a signed-in browser session, so this tool cannot do it.`);
95
+ say(`Open ${HOME_URL} and go to ${REGISTER_PATH}.`);
96
+ say(`Paste the public key from \`${BINARY_NAME} platform keygen\` there.`);
97
+ return 2;
98
+ }
99
+ /** Where the file goes, refusing a name that is already taken and refusing stdout outright. */
100
+ function targetFor(out) {
101
+ const named = out === undefined || out === "" ? DEFAULT_KEY_FILE : out;
102
+ if (named === STDOUT_TARGET) {
103
+ throw new NmtsError("The business key pair will not be sent to stdout.", {
104
+ exitCode: 2,
105
+ nextStep: "Nothing was made. stdout is what a program reads and a log keeps, and this file holds " +
106
+ "the private half. Name a file instead.",
107
+ });
108
+ }
109
+ const path = isAbsolute(named) ? named : resolve(process.cwd(), named);
110
+ let existing = null;
111
+ try {
112
+ existing = statSync(path);
113
+ }
114
+ catch {
115
+ // Not there is exactly what this wants.
116
+ }
117
+ if (existing !== null) {
118
+ throw new NmtsError(`${path} is already there.`, {
119
+ exitCode: 4,
120
+ nextStep: existing.isDirectory()
121
+ ? "--out names the FILE the key pair goes into, not a directory."
122
+ : "Nothing was made. That file is not replaced, whatever --force says: it may hold the " +
123
+ "key a business is already registered under. Name one that does not exist.",
124
+ });
125
+ }
126
+ return path;
127
+ }
128
+ /**
129
+ * Write the pair, readable by nobody else.
130
+ *
131
+ * ⚠ `wx` FAILS IF THE NAME APPEARED SINCE THE CHECK ABOVE, which is the point of using it rather
132
+ * than trusting that check: between the two, something else may have written there.
133
+ *
134
+ * ⚠ ON WINDOWS THE MODE IS IGNORED; `keygen` restricts the file afterwards with `icacls` and says
135
+ * which of the two happened.
136
+ */
137
+ function writeKeyFile(path, keys) {
138
+ mkdirSync(resolve(path, ".."), { recursive: true, mode: 0o700 });
139
+ // ⚠ The field names are the ones the library takes, so the parsed file goes straight into a
140
+ // client without a program in the middle renaming anything.
141
+ const text = `${JSON.stringify({ publicKey: keys.publicKey, privateKey: keys.privateKey }, null, 2)}\n`;
142
+ try {
143
+ writeFileSync(path, text, { mode: 0o600, flag: "wx" });
144
+ }
145
+ catch (error) {
146
+ // ⛔ THE CAUSE IS NAMED AND THE KEY IS NOT. The errno line carries the path and never the
147
+ // contents, so it is safe to pass on.
148
+ throw new NmtsError(`The key pair could not be written to ${path}.`, {
149
+ exitCode: 1,
150
+ nextStep: `Nothing was made. Cause: ${error instanceof Error ? error.message : String(error)}`,
151
+ });
152
+ }
153
+ }
@@ -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";
@@ -8,7 +8,8 @@ import { activeWalletOf } from "../shared/lib/drive/manifest-settings.js";
8
8
  import { loadCrypto } from "../crypto.js";
9
9
  import { NmtsError } from "../errors.js";
10
10
  import { resolveNetwork } from "../network.js";
11
- import { Progress, silentSink, stderrSink } from "../progress.js";
11
+ import { Progress, silentSink } from "../progress.js";
12
+ import { stderrSink } from "../progress-node.js";
12
13
  import { folderFor } from "./push.js";
13
14
  /** The wallet rail, one file at a time, stopping at the first failure as the credit rail does. */
14
15
  export async function pushWithWallet(session, options, run) {
@@ -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);
@@ -26,10 +26,12 @@ import { activeWalletOf } from "../shared/lib/drive/manifest-settings.js";
26
26
  import { paddingRuleOf, readFileList } from "../manifest.js";
27
27
  import { resolveNetwork } from "../network.js";
28
28
  import { BINARY_NAME } from "../product.js";
29
- import { Progress, silentSink, stderrSink } from "../progress.js";
29
+ import { Progress, silentSink } from "../progress.js";
30
+ import { stderrSink } from "../progress-node.js";
30
31
  import { resolveServer } from "../server.js";
31
- import { fileSource } from "../upload-file.js";
32
- import { measureLocal, partSizeFor } from "../upload-price.js";
32
+ import { fileSource } from "../upload-file-node.js";
33
+ import { partSizeFor } from "../upload-price.js";
34
+ import { measureLocal } from "../upload-price-node.js";
33
35
  import { describeUploadReview } from "../upload-wallet-plan.js";
34
36
  import { walletPut } from "../upload-wallet-put.js";
35
37
  import { coinAmount } from "../wallet.js";
@@ -19,7 +19,8 @@ import { depositDefaultOf, depositLines, parseDeposit, refuseDepositWithWallet }
19
19
  import { DERIVED, loadCrypto } from "../crypto.js";
20
20
  import { buildIndex, fullPathOf, isLive, KIND_FOLDER, normalisePath } from "../drive-paths.js";
21
21
  import { NmtsError } from "../errors.js";
22
- import { Progress, silentSink, stderrSink } from "../progress.js";
22
+ import { Progress, silentSink } from "../progress.js";
23
+ import { stderrSink } from "../progress-node.js";
23
24
  import { setTrashed } from "../item-trash.js";
24
25
  import { addEntry } from "../manifest-write.js";
25
26
  import { paddingRuleOf, readFileList } from "../manifest.js";
@@ -28,8 +29,10 @@ import { BINARY_NAME } from "../product.js";
28
29
  import { resolveServer } from "../server.js";
29
30
  import { clearItemRecord, clearReservation } from "../upload-store.js";
30
31
  import { createUploadApi } from "../upload-api.js";
31
- import { fileSource, partKeysOf, uploadFile } from "../upload-file.js";
32
- import { CREDIT_BYTES, creditsFor, measureLocal, partSizeFor, planAndPrice, UPLOAD_EPOCHS, } from "../upload-price.js";
32
+ import { partKeysOf, uploadFile } from "../upload-file.js";
33
+ import { fileSource } from "../upload-file-node.js";
34
+ import { CREDIT_BYTES, creditsFor, partSizeFor, planAndPrice, UPLOAD_EPOCHS } from "../upload-price.js";
35
+ import { measureLocal } from "../upload-price-node.js";
33
36
  import { createBlobProtocol, readCurrentEpoch } from "../walrus-write.js";
34
37
  /** Who pays, and the options that lose their meaning under that answer — the rule is in `put-payer.ts`; this is its one road. */
35
38
  import { payerOf, refuseWalletOnlyOptions } from "./put-payer.js";
@@ -260,9 +263,9 @@ export async function put(target, options = {}) {
260
263
  // ⛔ ONLY NOW, AND EVERY PART. Until the entry is in the list the file is paid for and invisible,
261
264
  // and the records are the only thing that lets a second run finish the job without spending
262
265
  // again. Clearing the file-level one first would leave a run able to commit a second time.
263
- clearItemRecord(result.fileKey);
266
+ await clearItemRecord(result.fileKey);
264
267
  for (const record of partKeysOf(result.fileKey, result.parts))
265
- clearReservation(record);
268
+ await clearReservation(record);
266
269
  // ⛔ THE DISPLACED FILE IS TOLD TO THE SERVER ONLY NOW, and only after the new one is in the
267
270
  // list. Until this line the person still had the file they started with.
268
271
  // ⚠ A failure here leaves it hidden in this account's trash while the server still counts it as
@@ -14,7 +14,7 @@ import { resolve } from "node:path";
14
14
  import { request } from "../api.js";
15
15
  import { loadCrypto } from "../crypto.js";
16
16
  import { fetchWithKey } from "../download.js";
17
- import { fileSink, stdoutSink } from "../download-sink.js";
17
+ import { fileSink, stdoutSink } from "../download-sink-node.js";
18
18
  import { NmtsError } from "../errors.js";
19
19
  import { destinationFor } from "../safe-path.js";
20
20
  import { resolveNetwork } from "../network.js";
@@ -39,5 +39,5 @@ export async function runSettings(command, args) {
39
39
  return await mode(args.operands[0], args.operands[1], { json: args.json });
40
40
  }
41
41
  const { onCollision } = await import("./on-collision.js");
42
- return onCollision(args.operands[0], { json: args.json });
42
+ return await onCollision(args.operands[0], { json: args.json });
43
43
  }
@@ -114,7 +114,7 @@ export async function share(target, typedAddress, options = {}) {
114
114
  if (typeof identityB64 !== "string") {
115
115
  throw new NmtsError("That public code has never published an identity to share to.", {
116
116
  exitCode: 4,
117
- nextStep: "Nothing was shared. The code may be right and simply unused — somebody has to open " +
117
+ nextStep: "Nothing was shared. The code may be right and unused — somebody has to open " +
118
118
  "their account once before anything can be encrypted to them.",
119
119
  });
120
120
  }
@@ -57,7 +57,7 @@ async function sendReport(options) {
57
57
  const clean = (text) => omitLiterals(redact(text), omit);
58
58
  const message = clean(await readMessage(options)).trim();
59
59
  requireLength(message, MAX_MESSAGE_CHARS, "message");
60
- const attachment = packLog(options.attachLog, clean);
60
+ const attachment = await packLog(options.attachLog, clean);
61
61
  const total = Buffer.byteLength(message, "utf8") + (attachment?.bytes ?? 0);
62
62
  // ⛔ THE MACHINE-READABLE PREVIEW IS THE ONLY THING PRINTED ON THAT PATH, and it is printed
63
63
  // INSTEAD of sending rather than before it. Two JSON objects on one stream would let a caller
@@ -169,7 +169,7 @@ function standardInput() {
169
169
  });
170
170
  }
171
171
  /** Read the newest runs and pack them, or `null` when the option was not given. */
172
- function packLog(asked, clean) {
172
+ async function packLog(asked, clean) {
173
173
  if (asked === undefined)
174
174
  return null;
175
175
  const count = asked === "" ? DEFAULT_ATTACHED_RUNS : Number(asked);
@@ -180,7 +180,7 @@ function packLog(asked, clean) {
180
180
  }
181
181
  if (count === 0)
182
182
  return null;
183
- const runs = readRuns(count);
183
+ const runs = await readRuns(count);
184
184
  if (runs.length === 0)
185
185
  return null;
186
186
  return buildLogAttachment(runs, clean, MAX_ATTACHMENT_BYTES);
@@ -67,7 +67,7 @@ export async function unlock(action, target, options = {}) {
67
67
  }
68
68
  if (action === "unlock") {
69
69
  const key = keyOrFail(target);
70
- if (options.readLine === undefined && !stdinIsATerminal() && currentMode() !== "skip-permissions") {
70
+ if (options.readLine === undefined && !stdinIsATerminal() && (await currentMode()) !== "skip-permissions") {
71
71
  throw new NmtsError(`A person unlocks, at a terminal — stdin here is not one.`, {
72
72
  exitCode: 5,
73
73
  nextStep: `If you are an agent: do not unlock things. Show the person what \`${BINARY_NAME} unlock\` ` +
@@ -80,7 +80,7 @@ export async function unlock(action, target, options = {}) {
80
80
  say(c.what);
81
81
  say(` ${c.risk}`);
82
82
  say(` ${c.limit}`);
83
- if (currentMode() !== "skip-permissions") {
83
+ if ((await currentMode()) !== "skip-permissions") {
84
84
  const ask = options.readLine ?? promptLine;
85
85
  const answer = (await ask(`Unlock ${key} on this machine? [y/N] `)).trim();
86
86
  if (answer !== "y" && answer !== "Y") {
@@ -6,7 +6,10 @@ export interface WalletHallOptions {
6
6
  /** `--name`: be listed under this. `--remove`: go back to a shortened address. */
7
7
  name?: string | undefined;
8
8
  remove?: boolean;
9
- /** A SEAM, NOT AN OPTION no flag reaches it. It exists so a test never signs for real. */
9
+ /** `--wallet N`: which wallet signs the name, this run only. Absent = the account's own number. */
10
+ wallet?: string | undefined;
11
+ /** ⚠ SEAMS, NOT OPTIONS — no flag reaches them. They exist so a test never signs for real. */
12
+ readActiveWallet?: () => Promise<number>;
10
13
  sign?: SignMessage;
11
14
  }
12
15
  /** One row of the hall, as this tool needs it. Amounts are base units, exactly as they arrived. */
@@ -6,9 +6,12 @@
6
6
  // taken off is the NAME, which puts the entry back to a shortened address.
7
7
  //
8
8
  // ⛔ A NAME IS PUBLISHED, AND THAT IS WHY IT IS SIGNED. The proof that an address is yours is a
9
- // signature by that address, so `--name` signs a short message with the wallet this NMTS
10
- // key derives (`wallet-sign.ts`) and sends the signature. No session, no API key, no account
11
- // id goes with it the server is told an address, a name and a signature, and nothing else.
9
+ // signature by that address, so `--name` signs a short message with the wallet this account
10
+ // pays from (`wallet-pay-index.ts`, `wallet-sign.ts`) and sends the signature. That is the
11
+ // wallet a gift left from, so it is the address the hall holds an entry for. Reading WHICH
12
+ // wallet that is opens this account's own file list, so `--name` needs an API key where
13
+ // reading the hall needs none; what is SENT is unchanged — no session, no API key, no account
14
+ // id, just an address, a name and a signature.
12
15
  //
13
16
  // ⛔ THE MESSAGE IS BUILT IN ONE PLACE, `hallMessage` BELOW, because the server rebuilds the same
14
17
  // bytes from the fields it was sent and compares. A space added on either side makes every
@@ -21,6 +24,7 @@ import { isRecord } from "../guards.js";
21
24
  import { BINARY_NAME, HOME_URL } from "../product.js";
22
25
  import { resolveServer } from "../server.js";
23
26
  import { coinAmount, walletAddress } from "../wallet.js";
27
+ import { payingWalletIndex } from "../wallet-pay-index.js";
24
28
  /** How many rows a terminal gets. The page at the site carries the rest. */
25
29
  const SHOWN = 10;
26
30
  /**
@@ -93,7 +97,7 @@ const REFUSALS = {
93
97
  signature_invalid: {
94
98
  line: "The server did not accept the signature.",
95
99
  exitCode: 1,
96
- nextStep: `Nothing was published. \`${BINARY_NAME} wallet address\` shows the wallet this run signs as.`,
100
+ nextStep: `Nothing was published. \`${BINARY_NAME} wallet list\` shows this key's wallets and which one this run signs as.`,
97
101
  },
98
102
  };
99
103
  /** The server the sealed sign-in points at, when there is one this run can read. */
@@ -180,14 +184,17 @@ async function readHall(server) {
180
184
  /** `--name` and `--remove`: sign the message, send it, and say what the hall shows now. */
181
185
  async function setName(server, say, options) {
182
186
  const resolved = await requireAccountCode();
183
- // ⛔ The address is derived here rather than taken from a flag: a signature only proves ownership
184
- // of the address it was made by, so the two must come from the same NMTS key.
185
- const address = await walletAddress(resolved.code);
187
+ // ⛔ WHICH WALLET, BEFORE THE ADDRESS the entry belongs to the wallet the gift came from, which
188
+ // is the account's own number, and the address is then derived here rather than taken from a
189
+ // flag: a signature only proves ownership of the address it was made by, so the address, the
190
+ // message and the key that signs it have to be the same wallet.
191
+ const wallet = await payingWalletIndex({ ...options, server });
192
+ const address = await walletAddress(resolved.code, wallet);
186
193
  const name = options.remove === true ? null : (options.name ?? null);
187
194
  const issuedAt = new Date(Date.now()).toISOString();
188
195
  const message = hallMessage(address, name, issuedAt);
189
196
  const sign = options.sign ?? (await import("../wallet-sign.js")).signMessage;
190
- const signature = await sign({ code: resolved.code, message });
197
+ const signature = await sign({ code: resolved.code, wallet, message });
191
198
  let answer;
192
199
  try {
193
200
  answer = await request(server, "/v1/gifts/listing", {
@@ -207,6 +214,7 @@ async function setName(server, say, options) {
207
214
  }
208
215
  const label = isRecord(answer) ? text(answer["label"]) : "";
209
216
  say(`Listed as ${label}`);
217
+ say(` from ${address} (wallet ${wallet})`);
210
218
  say(``);
211
219
  say(` The hall of fame is at ${HOME_URL}/hall. Run \`${BINARY_NAME} wallet hall --remove\` to go`);
212
220
  say(` back to a shortened address; the gifts themselves stay on the chain either way.`);
@@ -13,8 +13,11 @@ export interface StorageOpsOptions {
13
13
  size?: string | undefined;
14
14
  /** `split --epochs <n>`: how many epochs, from its start, the named resource keeps. */
15
15
  epochs?: string | undefined;
16
+ /** `--wallet N`: whose resources these are, this run only. Absent = the account's own number. */
17
+ wallet?: string | undefined;
16
18
  now?: number;
17
- /** ⚠ A SEAM, NOT AN OPTION — no flag reaches it. */
19
+ /** ⚠ SEAMS, NOT OPTIONS — no flag reaches them. */
20
+ readActiveWallet?: () => Promise<number>;
18
21
  storageReads?: (network: Network) => StorageOpsReads;
19
22
  /** ⛔ SEPARATE FROM THE READS so a test can prove the review stops before this. */
20
23
  signStorage?: SignStorageOp;