@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/download-sink.js
CHANGED
|
@@ -12,202 +12,9 @@
|
|
|
12
12
|
// ⛔ A SINK MUST NOT KEEP THE ARRAY IT IS HANDED. The caller zeroes each run of plaintext as soon
|
|
13
13
|
// as `write` resolves, so a sink that stored the reference would hold a buffer full of zeroes
|
|
14
14
|
// and write them out. Copy, or finish with the bytes before resolving.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/** The one refusal text for a destination that is already occupied. Shared by every command. */
|
|
22
|
-
function alreadyThere(destination) {
|
|
23
|
-
return new NmtsError(`${destination} already exists.`, {
|
|
24
|
-
exitCode: 4,
|
|
25
|
-
nextStep: "Nothing was written. Pass --out to choose another name, or --force to replace it.",
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Write to a file, through a temporary name beside it, renamed into place only when the whole
|
|
30
|
-
* file has been proved.
|
|
31
|
-
*
|
|
32
|
-
* ⛔ THE TEMPORARY FILE IS IN THE SAME DIRECTORY AS THE DESTINATION, not in a system temporary
|
|
33
|
-
* directory, and that is the whole mechanism: `rename` is atomic only within one filesystem,
|
|
34
|
-
* and only an atomic rename guarantees that a reader either sees nothing under the real name or
|
|
35
|
-
* sees the finished file. A temporary directory can be — and on the machines this runs on
|
|
36
|
-
* usually is — a different filesystem, where the same call becomes copy-then-delete and a
|
|
37
|
-
* reader can catch the copy half done.
|
|
38
|
-
*
|
|
39
|
-
* ⛔ IT IS CREATED 0600 AND THE RENAME KEEPS THAT. The file is somebody's plaintext; it must not
|
|
40
|
-
* be readable by other accounts on the machine for the seconds it takes to download, any more
|
|
41
|
-
* than it may be afterwards.
|
|
42
|
-
*/
|
|
43
|
-
export function fileSink(destination, options) {
|
|
44
|
-
// ⛔ Asked BEFORE the download rather than at the end, which is where the old code found out.
|
|
45
|
-
// Fetching a file to discover it had nowhere to go spent somebody's time and the storage
|
|
46
|
-
// network's bandwidth for a refusal that was knowable up front.
|
|
47
|
-
if (!options.force && existsSync(destination))
|
|
48
|
-
throw alreadyThere(destination);
|
|
49
|
-
const temporary = join(dirname(destination), `.${basename(destination)}.nmts-${randomBytes(6).toString("hex")}.part`);
|
|
50
|
-
let stream = null;
|
|
51
|
-
let failure = null;
|
|
52
|
-
let renamed = false;
|
|
53
|
-
// ⚠ Created on the first write rather than up front: a download that fails before it produces a
|
|
54
|
-
// byte — a wrong key, a part on a network this build cannot read — then leaves no file at all,
|
|
55
|
-
// not even one that appears and disappears.
|
|
56
|
-
const opened = () => {
|
|
57
|
-
if (stream !== null)
|
|
58
|
-
return stream;
|
|
59
|
-
const fresh = createWriteStream(temporary, { flags: "wx", mode: 0o600 });
|
|
60
|
-
// ⛔ An 'error' with no listener is thrown out of the event loop and kills the process, which
|
|
61
|
-
// would turn "the disk is full" into a crash with no next step. Kept, and reported by the
|
|
62
|
-
// next call that can report it.
|
|
63
|
-
fresh.on("error", (error) => {
|
|
64
|
-
failure = error;
|
|
65
|
-
});
|
|
66
|
-
stream = fresh;
|
|
67
|
-
return fresh;
|
|
68
|
-
};
|
|
69
|
-
const closed = async () => {
|
|
70
|
-
const open = stream;
|
|
71
|
-
if (open === null)
|
|
72
|
-
return;
|
|
73
|
-
stream = null;
|
|
74
|
-
// ⚠ Waits for 'close', not 'finish': a stream that failed to open never finishes, and waiting
|
|
75
|
-
// for the wrong event there is a hang rather than an error.
|
|
76
|
-
await new Promise((resolve) => {
|
|
77
|
-
if (open.closed) {
|
|
78
|
-
resolve();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
open.once("close", () => resolve());
|
|
82
|
-
open.end();
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
const abandon = async () => {
|
|
86
|
-
if (renamed)
|
|
87
|
-
return;
|
|
88
|
-
try {
|
|
89
|
-
await closed();
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// Nothing to do about it: the file below is going away either way.
|
|
93
|
-
}
|
|
94
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
95
|
-
};
|
|
96
|
-
return {
|
|
97
|
-
// Nothing to refuse: what a disk cannot take, the write itself reports, and the temporary
|
|
98
|
-
// file means a disk that fills up leaves no file under the name somebody asked for.
|
|
99
|
-
expect: () => undefined,
|
|
100
|
-
write: async (bytes) => {
|
|
101
|
-
const target = opened();
|
|
102
|
-
await new Promise((resolve, reject) => {
|
|
103
|
-
const known = failure;
|
|
104
|
-
if (known !== null) {
|
|
105
|
-
reject(known);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
target.write(bytes, (error) => {
|
|
109
|
-
if (error === undefined || error === null)
|
|
110
|
-
resolve();
|
|
111
|
-
else
|
|
112
|
-
reject(error);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
},
|
|
116
|
-
commit: async () => {
|
|
117
|
-
// A file of zero bytes is still a file, so the stream is opened even if nothing was written.
|
|
118
|
-
opened();
|
|
119
|
-
await closed();
|
|
120
|
-
const known = failure;
|
|
121
|
-
if (known !== null) {
|
|
122
|
-
await abandon();
|
|
123
|
-
throw new NmtsError(`${destination} could not be written: ${known.message}`, {
|
|
124
|
-
exitCode: 1,
|
|
125
|
-
nextStep: "Nothing was left under that name. The download itself was fine — this is the disk.",
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
// ⛔ Asked a second time, as late as possible. The first check was before the download; a
|
|
129
|
-
// file that appeared in the meantime is somebody else's, and rename would silently
|
|
130
|
-
// replace it. The window is now the width of one system call rather than a download.
|
|
131
|
-
if (!options.force && existsSync(destination)) {
|
|
132
|
-
await abandon();
|
|
133
|
-
throw alreadyThere(destination);
|
|
134
|
-
}
|
|
135
|
-
await rename(temporary, destination);
|
|
136
|
-
renamed = true;
|
|
137
|
-
return true;
|
|
138
|
-
},
|
|
139
|
-
abandon,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* How much plaintext `--out -` will hold before it hands anything over. One part's worth.
|
|
144
|
-
*
|
|
145
|
-
* ⛔ NOT AN ARBITRARY NUMBER: it is the upload path's default part size, which is the most memory
|
|
146
|
-
* this tool has ever asked for. Keeping the stdout ceiling there means the whole tool's bound
|
|
147
|
-
* is one part plus one chunk whichever direction the bytes are going.
|
|
148
|
-
*/
|
|
149
|
-
export const STDOUT_HOLD_LIMIT = 64 * 2 ** 20;
|
|
150
|
-
/**
|
|
151
|
-
* Hand the whole file to whatever is reading stdout — after it has been proved, not during.
|
|
152
|
-
*
|
|
153
|
-
* ⛔ THERE IS NO RENAME ON A PIPE, so the trick the file path uses is not available: a byte handed
|
|
154
|
-
* to a reader cannot be taken back, and a reader that has already consumed half a file cannot
|
|
155
|
-
* be told afterwards that the half was wrong. An exit code does not fix that — `nmts get x
|
|
156
|
-
* --out - > y` leaves the truncated bytes in `y` whatever the exit code says, and an agent
|
|
157
|
-
* reading a pipe usually acts on what it read. So this branch keeps the ORIGINAL guarantee
|
|
158
|
-
* rather than the original size: the file is held, checked, and only then handed over, exactly
|
|
159
|
-
* as before streaming existed.
|
|
160
|
-
*
|
|
161
|
-
* ⛔ WHICH MEANS IT HAS TO REFUSE SOMEWHERE, AND IT REFUSES OUT LOUD AND EARLY. Above the ceiling
|
|
162
|
-
* the answer is a refusal naming `--out <name>`, which streams and is checked before the file
|
|
163
|
-
* appears — not a silent trade of the guarantee for the size, and not the out-of-memory crash
|
|
164
|
-
* that this used to be. `put` already refuses this way rather than discovering a limit halfway.
|
|
165
|
-
*
|
|
166
|
-
* ⚠ Every protection this branch already had is still here and still applies to the whole file:
|
|
167
|
-
* bytes a terminal would act on are refused (`readableOnATerminal`), a reader that closed the
|
|
168
|
-
* pipe first is an ordinary end and not a failure, and any other write failure throws.
|
|
169
|
-
*/
|
|
170
|
-
export function stdoutSink(to, limit = STDOUT_HOLD_LIMIT) {
|
|
171
|
-
const held = [];
|
|
172
|
-
let total = 0;
|
|
173
|
-
const tooLarge = (size) => new NmtsError(`This file is ${size} bytes, and \`--out -\` hands over at most ${limit}.`, {
|
|
174
|
-
exitCode: 4,
|
|
175
|
-
nextStep: "Nothing was written and nothing was sent. A pipe cannot be taken back, so this mode " +
|
|
176
|
-
"proves the whole file before it sends a byte, which means holding it. Use `--out <name>` " +
|
|
177
|
-
"— that writes any size, and the file only appears under that name once it has been checked.",
|
|
178
|
-
});
|
|
179
|
-
const forget = () => {
|
|
180
|
-
for (const run of held)
|
|
181
|
-
run.fill(0);
|
|
182
|
-
held.length = 0;
|
|
183
|
-
};
|
|
184
|
-
return {
|
|
185
|
-
expect: (size) => {
|
|
186
|
-
if (size > limit)
|
|
187
|
-
throw tooLarge(size);
|
|
188
|
-
},
|
|
189
|
-
write: async (bytes) => {
|
|
190
|
-
// ⚠ Copied, because the caller zeroes what it handed over as soon as this resolves.
|
|
191
|
-
total += bytes.length;
|
|
192
|
-
if (total > limit) {
|
|
193
|
-
forget();
|
|
194
|
-
throw tooLarge(total);
|
|
195
|
-
}
|
|
196
|
-
held.push(new Uint8Array(bytes));
|
|
197
|
-
},
|
|
198
|
-
commit: async () => {
|
|
199
|
-
const whole = new Uint8Array(total);
|
|
200
|
-
let at = 0;
|
|
201
|
-
for (const run of held) {
|
|
202
|
-
whole.set(run, at);
|
|
203
|
-
at += run.length;
|
|
204
|
-
}
|
|
205
|
-
forget();
|
|
206
|
-
// ⚠ NOT zeroed afterwards, deliberately: `ByteDestination` promises only that the bytes have
|
|
207
|
-
// been handed over, and a destination is entitled to keep the array it was given — the
|
|
208
|
-
// tests use one that does. Wiping it here would hand a reader a buffer of zeroes.
|
|
209
|
-
return await handOver(whole, to);
|
|
210
|
-
},
|
|
211
|
-
abandon: async () => forget(),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
15
|
+
//
|
|
16
|
+
// ⛔ ONLY THE SHAPE IS HERE. The two destinations this tool ships — a file on a disk, and whatever
|
|
17
|
+
// is reading its stdout — are both Node's, and they live in `download-sink-node.ts`. What is
|
|
18
|
+
// left is the contract, which is what a caller in a browser implements to hand a download to a
|
|
19
|
+
// `Blob`, an element or a stream of its own.
|
|
20
|
+
export {};
|
package/dist/download.js
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
// the engine's own buffer for the chunk it is assembling, and the one chunk of plaintext it
|
|
27
27
|
// hands back — 4 MiB each in NCF-3. A hundred-gigabyte file costs the same as a hundred-megabyte
|
|
28
28
|
// one. The exception is `--out -`, which is bounded by `STDOUT_HOLD_LIMIT` and refuses above it.
|
|
29
|
-
import {
|
|
29
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
30
|
+
import { fromBase64Url, utf8 } from "./bytes.js";
|
|
30
31
|
import { request } from "./api.js";
|
|
31
32
|
import { AAD, DERIVED, loadCrypto } from "./crypto.js";
|
|
32
33
|
import { asParts, fetchPart, openPart } from "./download-part.js";
|
|
@@ -69,7 +70,7 @@ export async function fetchFile(input) {
|
|
|
69
70
|
derived.fill(0);
|
|
70
71
|
let dek;
|
|
71
72
|
try {
|
|
72
|
-
dek = crypt.envelope_open(dataKey,
|
|
73
|
+
dek = crypt.envelope_open(dataKey, utf8(AAD.dekWrap), fromBase64Url(input.dekWrapped));
|
|
73
74
|
}
|
|
74
75
|
catch {
|
|
75
76
|
dataKey.fill(0);
|
|
@@ -80,7 +81,7 @@ export async function fetchFile(input) {
|
|
|
80
81
|
let expected = null;
|
|
81
82
|
if (input.contentHashCt !== undefined && input.contentHashCt !== "") {
|
|
82
83
|
try {
|
|
83
|
-
expected = crypt.envelope_open(dataKey,
|
|
84
|
+
expected = crypt.envelope_open(dataKey, utf8(AAD.contentHash), fromBase64Url(input.contentHashCt));
|
|
84
85
|
}
|
|
85
86
|
catch {
|
|
86
87
|
dataKey.fill(0);
|
|
@@ -115,7 +116,7 @@ export async function fetchFile(input) {
|
|
|
115
116
|
* sees the finished file or sees nothing under that name.
|
|
116
117
|
*/
|
|
117
118
|
async function collect(crypt, ordered, dek, expected, size, chain, read, sink) {
|
|
118
|
-
const hasher =
|
|
119
|
+
const hasher = sha256.create();
|
|
119
120
|
let remaining = size;
|
|
120
121
|
try {
|
|
121
122
|
// ⛔ Asked before a byte is fetched. A destination that cannot take a file this size says so
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CryptoGlue } from "./crypto-surface.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Where the engine's WebAssembly sits, in the three places an installation can put it.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ FOUND RATHER THAN CONFIGURED. The package carries it in `vendor/`, a checkout runs from
|
|
6
|
+
* `src/` with the same folder one level further up, and the repository's own web tree holds a
|
|
7
|
+
* build of the same crate. A path in a setting would be a fourth answer nobody keeps right.
|
|
8
|
+
*/
|
|
9
|
+
export declare function engineDir(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Load the engine once per process.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ NO TYPE ASSERTION. A dynamically imported module is `unknown`, and staying honest about that
|
|
14
|
+
* matters here more than anywhere: a rebuild that renamed an export would otherwise become
|
|
15
|
+
* "undefined is not a function" deep inside a derivation.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadEngine(): Promise<CryptoGlue>;
|
|
18
|
+
/** For tests that need a fresh load. */
|
|
19
|
+
export declare function forgetEngine(): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Finding and loading the engine on a machine with a disk.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ NOTHING IS RE-IMPLEMENTED HERE. The bytes that derive an account's keys are the same
|
|
4
|
+
// WebAssembly the browser runs, built from the same Rust crate — this file only finds the build
|
|
5
|
+
// and hands it to the shape check every load passes.
|
|
6
|
+
//
|
|
7
|
+
// ⛔ SEPARATE FROM `host-node.ts` SO THAT REGISTERING A HOST COSTS NOTHING. `nmts --help` puts the
|
|
8
|
+
// host in place and never derives anything; loading this file, and the four `node:` modules it
|
|
9
|
+
// needs, on every run would be a fixed cost an agent pays thousands of times.
|
|
10
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
11
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
12
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
13
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return path;
|
|
17
|
+
};
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
|
+
import { isCryptoGlue, missingExports } from "./crypto-surface.js";
|
|
23
|
+
import { NmtsError } from "./errors.js";
|
|
24
|
+
/**
|
|
25
|
+
* Where the engine's WebAssembly sits, in the three places an installation can put it.
|
|
26
|
+
*
|
|
27
|
+
* ⛔ FOUND RATHER THAN CONFIGURED. The package carries it in `vendor/`, a checkout runs from
|
|
28
|
+
* `src/` with the same folder one level further up, and the repository's own web tree holds a
|
|
29
|
+
* build of the same crate. A path in a setting would be a fourth answer nobody keeps right.
|
|
30
|
+
*/
|
|
31
|
+
export function engineDir() {
|
|
32
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const candidates = [
|
|
34
|
+
join(here, "..", "vendor", "nmts-crypto"),
|
|
35
|
+
join(here, "..", "..", "vendor", "nmts-crypto"),
|
|
36
|
+
join(here, "..", "..", "web", "vendor", "nmts-crypto"),
|
|
37
|
+
];
|
|
38
|
+
for (const dir of candidates) {
|
|
39
|
+
if (existsSync(join(dir, "nmts_crypto_wasm_bg.wasm")))
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
throw new NmtsError("The NMTS crypto engine is missing from this installation.", {
|
|
43
|
+
exitCode: 1,
|
|
44
|
+
nextStep: "Reinstall the package. Nothing can be encrypted or decrypted without it.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
let engine = null;
|
|
48
|
+
/**
|
|
49
|
+
* Load the engine once per process.
|
|
50
|
+
*
|
|
51
|
+
* ⛔ NO TYPE ASSERTION. A dynamically imported module is `unknown`, and staying honest about that
|
|
52
|
+
* matters here more than anywhere: a rebuild that renamed an export would otherwise become
|
|
53
|
+
* "undefined is not a function" deep inside a derivation.
|
|
54
|
+
*/
|
|
55
|
+
export async function loadEngine() {
|
|
56
|
+
if (engine !== null)
|
|
57
|
+
return engine;
|
|
58
|
+
const dir = engineDir();
|
|
59
|
+
const module = await import(__rewriteRelativeImportExtension(pathToFileURL(join(dir, "nmts_crypto_wasm.js")).href));
|
|
60
|
+
if (typeof module !== "object" || module === null || !("default" in module)) {
|
|
61
|
+
throw new NmtsError("The NMTS crypto engine did not load (no initialiser).", { exitCode: 1 });
|
|
62
|
+
}
|
|
63
|
+
const init = Reflect.get(module, "default");
|
|
64
|
+
if (typeof init !== "function") {
|
|
65
|
+
throw new NmtsError("The NMTS crypto engine did not load (initialiser is not callable).", { exitCode: 1 });
|
|
66
|
+
}
|
|
67
|
+
await init({ module_or_path: await readFile(join(dir, "nmts_crypto_wasm_bg.wasm")) });
|
|
68
|
+
if (!isCryptoGlue(module)) {
|
|
69
|
+
throw new NmtsError(`The NMTS crypto engine is missing: ${missingExports(module).join(", ")}. This build does not match this tool.`, { exitCode: 1, nextStep: "Reinstall the package." });
|
|
70
|
+
}
|
|
71
|
+
engine = module;
|
|
72
|
+
return module;
|
|
73
|
+
}
|
|
74
|
+
/** For tests that need a fresh load. */
|
|
75
|
+
export function forgetEngine() {
|
|
76
|
+
engine = null;
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Which NMTS server this run talks to. */
|
|
2
|
+
export declare const SERVER_ENV_VAR = "NMTS_SERVER";
|
|
3
|
+
/** Which storage network that server uses. */
|
|
4
|
+
export declare const NETWORK_ENV_VAR = "NMTS_NETWORK";
|
|
5
|
+
/**
|
|
6
|
+
* Point reads at somebody else's aggregator, or at a development stack.
|
|
7
|
+
*
|
|
8
|
+
* ⚠ It replaces the list rather than adding to it, and that is deliberate: a run should read from
|
|
9
|
+
* where it was told to read, not from there AND the public hosts. Comma-separated for more
|
|
10
|
+
* than one, tried in the order given.
|
|
11
|
+
*/
|
|
12
|
+
export declare const AGGREGATOR_ENV_VAR = "NMTS_AGGREGATOR";
|
|
13
|
+
/**
|
|
14
|
+
* Push writes through somebody else's relay, or through a development stack.
|
|
15
|
+
*
|
|
16
|
+
* ⚠ ONE host, not a list. Unlike reads there is nothing to fail over to — see `RELAY_HOSTS`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const RELAY_ENV_VAR = "NMTS_RELAY";
|
|
19
|
+
/** Ask a different Sui JSON-RPC node the shard-count question. */
|
|
20
|
+
export declare const SUI_RPC_ENV_VAR = "NMTS_SUI_RPC";
|
package/dist/env-vars.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The names of the environment variables this tool reads.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ NAMES ONLY, AND NO IMPORTS. `nmts --help` prints several of these, and a help text that had to
|
|
4
|
+
// load the module that USES a variable would drag that module's whole chain into every run —
|
|
5
|
+
// `check:cli-startup` measures exactly that, and it is the leak it has caught most often. The
|
|
6
|
+
// modules that read the variables re-export their own name from here, so every caller still
|
|
7
|
+
// finds it where it belongs and there is still only one spelling of each.
|
|
8
|
+
/** Which NMTS server this run talks to. */
|
|
9
|
+
export const SERVER_ENV_VAR = "NMTS_SERVER";
|
|
10
|
+
/** Which storage network that server uses. */
|
|
11
|
+
export const NETWORK_ENV_VAR = "NMTS_NETWORK";
|
|
12
|
+
/**
|
|
13
|
+
* Point reads at somebody else's aggregator, or at a development stack.
|
|
14
|
+
*
|
|
15
|
+
* ⚠ It replaces the list rather than adding to it, and that is deliberate: a run should read from
|
|
16
|
+
* where it was told to read, not from there AND the public hosts. Comma-separated for more
|
|
17
|
+
* than one, tried in the order given.
|
|
18
|
+
*/
|
|
19
|
+
export const AGGREGATOR_ENV_VAR = "NMTS_AGGREGATOR";
|
|
20
|
+
/**
|
|
21
|
+
* Push writes through somebody else's relay, or through a development stack.
|
|
22
|
+
*
|
|
23
|
+
* ⚠ ONE host, not a list. Unlike reads there is nothing to fail over to — see `RELAY_HOSTS`.
|
|
24
|
+
*/
|
|
25
|
+
export const RELAY_ENV_VAR = "NMTS_RELAY";
|
|
26
|
+
/** Ask a different Sui JSON-RPC node the shard-count question. */
|
|
27
|
+
export const SUI_RPC_ENV_VAR = "NMTS_SUI_RPC";
|
package/dist/gate.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function unlocked(key, now) {
|
|
|
34
34
|
}
|
|
35
35
|
export async function gate(act, args, io = {}) {
|
|
36
36
|
const a = ACTS[act];
|
|
37
|
-
const mode = currentMode();
|
|
37
|
+
const mode = await currentMode();
|
|
38
38
|
const now = io.now ?? (() => new Date());
|
|
39
39
|
if (a.tier === "none")
|
|
40
40
|
return { ask: false, mode };
|
package/dist/help.js
CHANGED
|
@@ -10,10 +10,11 @@ import { AGENTS_DOC, BINARY_NAME, HOME_URL, PRODUCT_NAME, SOURCE_URL, VERSION }
|
|
|
10
10
|
// reading it here costs `nmts --help` nothing (`check:cli-startup`).
|
|
11
11
|
import { ATTACH_LOG_TEXT, SUPPORT_LONG, supportHelpText } from "./support-copy.js";
|
|
12
12
|
import { API_KEY_ENV_VAR, API_KEY_FILE_ENV_VAR, CODE_ENV_VAR, CODE_FILE_ENV_VAR, PASSPHRASE_ENV_VAR, } from "./credentials.js";
|
|
13
|
-
|
|
13
|
+
// ⛔ THE NAMES COME FROM THE MODULE THAT HAS NO IMPORTS, not from the modules that USE them. Those
|
|
14
|
+
// three reach the host register, and loading them to print a variable's name would put that
|
|
15
|
+
// chain in front of every `nmts --help` (`check:cli-startup`).
|
|
16
|
+
import { AGGREGATOR_ENV_VAR, NETWORK_ENV_VAR, SERVER_ENV_VAR } from "./env-vars.js";
|
|
14
17
|
import { LATEST_RELEASE_URL, NO_CHECK_ENV_VAR } from "./update-source.js";
|
|
15
|
-
import { SERVER_ENV_VAR } from "./server.js";
|
|
16
|
-
import { AGGREGATOR_ENV_VAR } from "./walrus.js";
|
|
17
18
|
import { CHECK_DOES_NOT_PROVE, CHECK_PROVES, RECOVERY_TOOL_URL, wrapText, } from "./recovery-release.js";
|
|
18
19
|
export function helpText(version) {
|
|
19
20
|
return [
|
|
@@ -123,6 +124,9 @@ export function helpText(version) {
|
|
|
123
124
|
` privacy The Privacy Policy in force`,
|
|
124
125
|
` mcp Serve this account's commands as tools, for an agent that speaks MCP`,
|
|
125
126
|
` s3 Serve the drive to any S3 program, on this machine only`,
|
|
127
|
+
` platform keygen Make the key pair a business signs its Platform requests with — writes`,
|
|
128
|
+
` both halves to a file only you can read, prints only the public half`,
|
|
129
|
+
` platform register Where that public key is registered: a browser session, not this tool`,
|
|
126
130
|
``,
|
|
127
131
|
`OPTIONS`,
|
|
128
132
|
` --server <url> NMTS server (default ${SERVER_ENV_VAR} or the live one)`,
|
|
@@ -171,7 +175,8 @@ export function helpText(version) {
|
|
|
171
175
|
` --desc Reverse whichever order is in effect (ls)`,
|
|
172
176
|
` --hidden Include entries whose name begins with a dot (push)`,
|
|
173
177
|
` --out <path> Where to write files (get, pull, mcp, listfile, recovery,`,
|
|
174
|
-
` recovery-list, kit, create, notices, terms, privacy
|
|
178
|
+
` recovery-list, kit, create, notices, terms, privacy, platform`,
|
|
179
|
+
` keygen). Default: here`,
|
|
175
180
|
` --out - Send a fetched file to stdout instead of writing it (get, listfile)`,
|
|
176
181
|
` --lang <en|ko> Which language of a document (terms, privacy). Default: English`,
|
|
177
182
|
` --board The message board's terms rather than the service's (terms)`,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The one test both hosts sit: what "keeping state" has to mean, whoever is keeping it.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ ONE FUNCTION, RUN FROM TWO TEST FILES. `test/host-node.test.ts` runs it over the Node host and
|
|
4
|
+
// the SDK's `test/host-browser.test.ts` runs it over the browser one. Two tests written
|
|
5
|
+
// separately would drift, and the way that drift shows up is a half-finished upload that
|
|
6
|
+
// resumes on a laptop and starts again in a browser — the same account, the same file, two
|
|
7
|
+
// answers.
|
|
8
|
+
//
|
|
9
|
+
// ⛔ IT REPORTS EVERY VIOLATION RATHER THAN THROWING AT THE FIRST. A host that is wrong is usually
|
|
10
|
+
// wrong about one thing; a list says which, and a caller that wanted an assertion gets one by
|
|
11
|
+
// asserting the list is empty.
|
|
12
|
+
//
|
|
13
|
+
// ⚠ IT WRITES. The keys all sit under `contract/`, which nothing else uses, and each one is
|
|
14
|
+
// removed before the function returns — including when a check above it failed.
|
|
15
|
+
/** Where this check keeps its scratch. Nothing else in the package writes under it. */
|
|
16
|
+
const AREA = "contract";
|
|
17
|
+
function sample(length, seed) {
|
|
18
|
+
const out = new Uint8Array(length);
|
|
19
|
+
for (let i = 0; i < length; i += 1)
|
|
20
|
+
out[i] = (i * 31 + seed) & 0xff;
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function same(left, right) {
|
|
24
|
+
if (left === undefined || left.length !== right.length)
|
|
25
|
+
return false;
|
|
26
|
+
for (let i = 0; i < left.length; i += 1)
|
|
27
|
+
if (left[i] !== right[i])
|
|
28
|
+
return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Put a host's state through everything the package relies on, and say what it got wrong.
|
|
33
|
+
*
|
|
34
|
+
* An empty array is a pass.
|
|
35
|
+
*/
|
|
36
|
+
export async function hostContract(host) {
|
|
37
|
+
const state = host.state;
|
|
38
|
+
const problems = [];
|
|
39
|
+
const one = `${AREA}/one`;
|
|
40
|
+
const two = `${AREA}/two`;
|
|
41
|
+
const big = `${AREA}/big`;
|
|
42
|
+
const say = (what) => void problems.push(what);
|
|
43
|
+
try {
|
|
44
|
+
// A key nothing has written is not an error and not empty bytes: it is nothing.
|
|
45
|
+
if ((await state.read(`${AREA}/never-written`)) !== undefined) {
|
|
46
|
+
say("read() of a key that was never written answered bytes instead of undefined");
|
|
47
|
+
}
|
|
48
|
+
const first = sample(64, 1);
|
|
49
|
+
await state.write(one, first);
|
|
50
|
+
if (!same(await state.read(one), first))
|
|
51
|
+
say("read() did not hand back the bytes write() was given");
|
|
52
|
+
// ⛔ THE SECOND WRITE REPLACES. A store that appended, or that kept the first value, would make
|
|
53
|
+
// every record in the package grow a second version nobody reads.
|
|
54
|
+
const second = sample(96, 2);
|
|
55
|
+
await state.write(one, second);
|
|
56
|
+
if (!same(await state.read(one), second))
|
|
57
|
+
say("writing a key a second time did not replace what was there");
|
|
58
|
+
await state.write(two, sample(8, 3));
|
|
59
|
+
const listed = await state.keys(`${AREA}/`);
|
|
60
|
+
for (const key of [one, two]) {
|
|
61
|
+
if (!listed.includes(key))
|
|
62
|
+
say(`keys("${AREA}/") did not include ${key}`);
|
|
63
|
+
}
|
|
64
|
+
if (listed.includes(`${AREA}/never-written`))
|
|
65
|
+
say("keys() listed a key that was never written");
|
|
66
|
+
await state.remove(one);
|
|
67
|
+
if ((await state.read(one)) !== undefined)
|
|
68
|
+
say("remove() left the bytes readable");
|
|
69
|
+
if ((await state.keys(`${AREA}/`)).includes(one))
|
|
70
|
+
say("remove() left the key in keys()");
|
|
71
|
+
// Removing what is not there is how every caller here clears a record it may never have made.
|
|
72
|
+
await state.remove(one);
|
|
73
|
+
// ⛔ ONE MEBIBYTE, BECAUSE THAT IS WHAT AN UPLOAD KEEPS. A reservation holds a sealed part, and
|
|
74
|
+
// a store that quietly truncated or refused at some smaller size would lose exactly the
|
|
75
|
+
// records that let a large upload resume — and nothing else would notice.
|
|
76
|
+
const large = sample(1024 * 1024, 4);
|
|
77
|
+
await state.write(big, large);
|
|
78
|
+
if (!same(await state.read(big), large))
|
|
79
|
+
say("a 1 MiB value did not come back as it was written");
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
say(`a call threw instead of answering: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
for (const key of [one, two, big]) {
|
|
86
|
+
try {
|
|
87
|
+
await state.remove(key);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// The store is somebody else's; a scratch key left behind is not this check's to report.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return problems;
|
|
95
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Host, type StateHost } from "./host.ts";
|
|
2
|
+
/** Files under the person's own config directory, named by `placeOf`. */
|
|
3
|
+
export declare function nodeState(): StateHost;
|
|
4
|
+
/** Where a state key lands on this machine. For tests, and for messages that name a file. */
|
|
5
|
+
export declare function statePath(key: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Every `NMTS_*` variable this process is holding.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ THE ONE PLACE THE WHOLE ENVIRONMENT IS READ. `redact.ts` needs the values to label them out of
|
|
10
|
+
* a report, and a browser has none — so it is a host call rather than a `process.env` walk in a
|
|
11
|
+
* module that has to run in both.
|
|
12
|
+
*/
|
|
13
|
+
export declare function nodeEnvEntries(): {
|
|
14
|
+
name: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}[];
|
|
17
|
+
/** This machine, as the package's host. */
|
|
18
|
+
export declare function nodeHost(): Host;
|
|
19
|
+
/**
|
|
20
|
+
* Put this machine in the register: what the two Node doors — `index.ts` and the command's own
|
|
21
|
+
* entry point — call before anything that loads the engine, keeps state or reads the environment.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ THE COMMAND LOADS THIS FILE LATE, not at the top of `main.ts`. The chain below reaches the
|
|
24
|
+
* credentials module, and a fixed cost at the head of `nmts --help` is one an agent running
|
|
25
|
+
* this tool in a loop pays thousands of times (`check:cli-startup`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function registerNodeHost(): void;
|