@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.
- package/AGENTS.md +11 -12
- package/CHANGELOG.md +35 -0
- package/README.ko.md +19 -21
- package/README.md +21 -22
- package/dist/account.js +2 -1
- package/dist/arg-options.d.ts +3 -1
- package/dist/artifact-about.d.ts +1 -1
- package/dist/autonomy.d.ts +3 -10
- package/dist/autonomy.js +15 -17
- package/dist/bytes.d.ts +15 -0
- package/dist/bytes.js +63 -0
- package/dist/collision.d.ts +5 -5
- package/dist/collision.js +19 -22
- package/dist/commands/delete-account.js +1 -1
- package/dist/commands/erase.js +1 -1
- package/dist/commands/get.js +1 -1
- package/dist/commands/listfile.js +1 -1
- package/dist/commands/mcp.js +5 -1
- package/dist/commands/mode.js +2 -2
- package/dist/commands/on-collision.d.ts +1 -1
- package/dist/commands/on-collision.js +3 -3
- package/dist/commands/platform.d.ts +9 -0
- package/dist/commands/platform.js +153 -0
- package/dist/commands/pull.js +1 -1
- package/dist/commands/push-wallet.js +2 -1
- package/dist/commands/push.js +6 -4
- package/dist/commands/put-wallet.js +5 -3
- package/dist/commands/put.js +8 -5
- package/dist/commands/receive.js +1 -1
- package/dist/commands/settings.js +1 -1
- package/dist/commands/share.js +1 -1
- package/dist/commands/support.js +3 -3
- package/dist/commands/unlock.js +2 -2
- package/dist/commands/wallet-hall.d.ts +4 -1
- package/dist/commands/wallet-hall.js +16 -8
- package/dist/commands/wallet-storage-ops.d.ts +4 -1
- package/dist/commands/wallet-storage-ops.js +7 -2
- package/dist/commands/wallet-swap.d.ts +3 -0
- package/dist/commands/wallet-swap.js +8 -4
- package/dist/commands/wallet.d.ts +1 -1
- package/dist/crypto.d.ts +8 -5
- package/dist/crypto.js +15 -61
- package/dist/download-sink-node.d.ts +49 -0
- package/dist/download-sink-node.js +204 -0
- package/dist/download-sink.d.ts +0 -48
- package/dist/download-sink.js +6 -199
- package/dist/download.js +5 -4
- package/dist/engine-node.d.ts +19 -0
- package/dist/engine-node.js +77 -0
- package/dist/env-vars.d.ts +20 -0
- package/dist/env-vars.js +27 -0
- package/dist/gate.js +1 -1
- package/dist/help.js +9 -4
- package/dist/host-contract.d.ts +7 -0
- package/dist/host-contract.js +95 -0
- package/dist/host-node.d.ts +27 -0
- package/dist/host-node.js +192 -0
- package/dist/host.d.ts +99 -0
- package/dist/host.js +59 -0
- package/dist/index.d.ts +6 -49
- package/dist/index.js +20 -45
- package/dist/main.js +7 -6
- package/dist/manifest-chunk-cache.d.ts +3 -3
- package/dist/manifest-chunk-cache.js +39 -44
- package/dist/manifest-chunk-flow.js +15 -14
- package/dist/manifest-write.js +1 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/manifest.js +37 -34
- package/dist/mcp-tools/tiers.js +1 -1
- package/dist/network.d.ts +1 -1
- package/dist/network.js +6 -2
- package/dist/platform-sign.d.ts +143 -0
- package/dist/platform-sign.js +220 -0
- package/dist/portable.d.ts +59 -0
- package/dist/portable.js +82 -0
- package/dist/product.d.ts +1 -1
- package/dist/product.js +1 -1
- package/dist/progress-node.d.ts +2 -0
- package/dist/progress-node.js +14 -0
- package/dist/progress.d.ts +0 -1
- package/dist/progress.js +4 -8
- package/dist/redact.js +3 -2
- package/dist/registration.js +5 -2
- package/dist/risk.d.ts +7 -0
- package/dist/risk.js +10 -0
- package/dist/run-log.d.ts +4 -4
- package/dist/run-log.js +29 -36
- package/dist/seal.js +5 -4
- package/dist/server.d.ts +1 -1
- package/dist/server.js +6 -2
- package/dist/setup-questions.js +2 -2
- package/dist/upload-file-node.d.ts +3 -0
- package/dist/upload-file-node.js +38 -0
- package/dist/upload-file.d.ts +2 -2
- package/dist/upload-file.js +15 -45
- package/dist/upload-price-node.d.ts +8 -0
- package/dist/upload-price-node.js +37 -0
- package/dist/upload-price.d.ts +0 -8
- package/dist/upload-price.js +4 -33
- package/dist/upload-steps.js +4 -4
- package/dist/upload-store.d.ts +14 -14
- package/dist/upload-store.js +68 -75
- package/dist/upload-wallet-chain.js +3 -2
- package/dist/upload-wallet-put.js +2 -2
- package/dist/upload-wallet.js +16 -15
- package/dist/upload.js +16 -15
- package/dist/wallet-grant.js +1 -1
- package/dist/wallet-sign-seams.d.ts +12 -0
- package/dist/wallet-sign.js +10 -13
- package/dist/walrus-write.js +3 -2
- package/dist/walrus.d.ts +1 -16
- package/dist/walrus.js +11 -22
- package/docs/commands/credits.md +1 -1
- package/docs/commands/platform.md +15 -0
- package/docs/commands/support.md +1 -1
- package/docs/commands/trash.md +1 -1
- package/docs/commands/wallet.md +11 -7
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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).", {
|
package/dist/commands/erase.js
CHANGED
|
@@ -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).", {
|
package/dist/commands/get.js
CHANGED
|
@@ -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,
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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 (
|
|
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."
|
package/dist/commands/mode.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/pull.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/commands/push.js
CHANGED
|
@@ -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
|
|
30
|
+
import { Progress, silentSink } from "../progress.js";
|
|
31
|
+
import { stderrSink } from "../progress-node.js";
|
|
31
32
|
import { openSession } from "../session.js";
|
|
32
|
-
import {
|
|
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
|
|
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 {
|
|
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";
|
package/dist/commands/put.js
CHANGED
|
@@ -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
|
|
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 {
|
|
32
|
-
import {
|
|
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
|
package/dist/commands/receive.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/share.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/commands/support.js
CHANGED
|
@@ -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);
|
package/dist/commands/unlock.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
|
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
|
-
// ⛔
|
|
184
|
-
//
|
|
185
|
-
|
|
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
|
-
/** ⚠
|
|
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;
|