@needmoretruth/nmts-cli 0.34.4 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -12
- package/CHANGELOG.md +43 -0
- package/README.ko.md +33 -35
- package/README.md +21 -22
- package/dist/account.js +2 -1
- package/dist/arg-options.d.ts +169 -0
- package/dist/arg-options.js +10 -0
- package/dist/args.d.ts +2 -157
- package/dist/args.js +2 -0
- 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/extend.d.ts +2 -0
- package/dist/commands/extend.js +8 -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 +124 -0
- package/dist/commands/pull.js +1 -1
- package/dist/commands/push-wallet.js +8 -1
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +6 -4
- package/dist/commands/put-payer.d.ts +8 -0
- package/dist/commands/put-payer.js +41 -0
- package/dist/commands/put-wallet.d.ts +9 -3
- package/dist/commands/put-wallet.js +108 -186
- package/dist/commands/put.d.ts +5 -7
- package/dist/commands/put.js +11 -31
- package/dist/commands/receive.js +1 -1
- package/dist/commands/settings.js +1 -1
- package/dist/commands/support.js +3 -3
- package/dist/commands/unlock.js +2 -2
- package/dist/commands/wallet-donate.d.ts +3 -0
- package/dist/commands/wallet-donate.js +6 -2
- package/dist/commands/wallet-hall.d.ts +4 -1
- package/dist/commands/wallet-hall.js +16 -8
- package/dist/commands/wallet-list.d.ts +18 -0
- package/dist/commands/wallet-list.js +116 -0
- package/dist/commands/wallet-send.d.ts +4 -0
- package/dist/commands/wallet-send.js +6 -2
- 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-use.d.ts +7 -0
- package/dist/commands/wallet-use.js +55 -0
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +20 -8
- 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/extend-plan.d.ts +2 -0
- package/dist/gate.js +1 -1
- package/dist/help.js +12 -5
- 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 -40
- package/dist/index.js +20 -32
- package/dist/main.js +11 -10
- 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 +11 -0
- package/dist/risk.js +16 -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/shared/lib/drive/manifest-ops.d.ts +8 -30
- package/dist/shared/lib/drive/manifest-ops.js +7 -39
- package/dist/shared/lib/drive/manifest-settings-patch.d.ts +52 -0
- package/dist/shared/lib/drive/manifest-settings-patch.js +110 -0
- package/dist/shared/lib/drive/manifest-settings.d.ts +55 -4
- package/dist/shared/lib/drive/manifest-settings.js +66 -28
- package/dist/shared/lib/wallet/discover.d.ts +54 -0
- package/dist/shared/lib/wallet/discover.js +66 -0
- package/dist/standing-tip.d.ts +3 -0
- package/dist/standing-tip.js +1 -0
- 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 +3 -3
- 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.d.ts +118 -0
- package/dist/upload-wallet-put.js +221 -0
- package/dist/upload-wallet.d.ts +3 -0
- package/dist/upload-wallet.js +18 -15
- package/dist/upload.js +16 -15
- package/dist/wallet-list-chain.d.ts +4 -0
- package/dist/wallet-list-chain.js +21 -0
- package/dist/wallet-pay-index.d.ts +21 -0
- package/dist/wallet-pay-index.js +68 -0
- package/dist/wallet-sign-seams.d.ts +69 -0
- package/dist/wallet-sign-seams.js +16 -0
- package/dist/wallet-sign.d.ts +3 -53
- package/dist/wallet-sign.js +22 -18
- package/dist/wallet.d.ts +13 -13
- package/dist/wallet.js +15 -15
- 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 +14 -0
- package/docs/commands/support.md +1 -1
- package/docs/commands/trash.md +1 -1
- package/docs/commands/wallet.md +26 -8
- package/package.json +13 -2
package/dist/main.js
CHANGED
|
@@ -40,6 +40,7 @@ import { BINARY_NAME, VERSION } from "./product.js";
|
|
|
40
40
|
*/
|
|
41
41
|
export const NOT_BUILT_YET = [];
|
|
42
42
|
export async function run(argv) {
|
|
43
|
+
(await import("./host-node.js")).registerNodeHost();
|
|
43
44
|
const args = parseArgs(argv);
|
|
44
45
|
if (args.version) {
|
|
45
46
|
process.stdout.write(`${VERSION}\n`);
|
|
@@ -54,7 +55,7 @@ export async function run(argv) {
|
|
|
54
55
|
// ⚠ `mode` itself is exempt: the command that prints the setting does not need it twice.
|
|
55
56
|
if (args.command !== "mode") {
|
|
56
57
|
const { announcement, currentMode } = await import("./autonomy.js");
|
|
57
|
-
const line = announcement(currentMode());
|
|
58
|
+
const line = announcement(await currentMode());
|
|
58
59
|
if (line !== null)
|
|
59
60
|
process.stderr.write(`${line}\n`);
|
|
60
61
|
}
|
|
@@ -76,10 +77,8 @@ export async function run(argv) {
|
|
|
76
77
|
env: args.env,
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
|
-
case "logout":
|
|
80
|
-
|
|
81
|
-
return logout();
|
|
82
|
-
}
|
|
80
|
+
case "logout":
|
|
81
|
+
return (await import("./commands/logout.js")).logout();
|
|
83
82
|
case "whoami": {
|
|
84
83
|
const { whoami } = await import("./commands/whoami.js");
|
|
85
84
|
return await whoami({
|
|
@@ -124,7 +123,7 @@ export async function run(argv) {
|
|
|
124
123
|
return await wallet(args.operands[0], {
|
|
125
124
|
server: args.server, network: args.network, json: args.json, qr: args.qr,
|
|
126
125
|
rest: args.operands.slice(1), yes: args.yes, dryRun: args.dryRun, feeCap: args.feeCap,
|
|
127
|
-
to: args.to, venue: args.venue, slippageBps: args.slippageBps, acceptExtremes: args.acceptExtremes, size: args.size, epochs: args.epochs, name: args.name, remove: args.remove,
|
|
126
|
+
to: args.to, venue: args.venue, slippageBps: args.slippageBps, acceptExtremes: args.acceptExtremes, size: args.size, epochs: args.epochs, name: args.name, remove: args.remove, index: args.index, wallet: args.wallet,
|
|
128
127
|
});
|
|
129
128
|
}
|
|
130
129
|
case "expiring": {
|
|
@@ -149,7 +148,7 @@ export async function run(argv) {
|
|
|
149
148
|
epochs: args.epochs,
|
|
150
149
|
dryRun: args.dryRun,
|
|
151
150
|
yes: args.yes,
|
|
152
|
-
json: args.json,
|
|
151
|
+
json: args.json, wallet: args.wallet,
|
|
153
152
|
});
|
|
154
153
|
}
|
|
155
154
|
case "create": {
|
|
@@ -191,7 +190,7 @@ export async function run(argv) {
|
|
|
191
190
|
pay: args.pay,
|
|
192
191
|
epochs: args.epochs,
|
|
193
192
|
storage: args.storage,
|
|
194
|
-
json: args.json,
|
|
193
|
+
json: args.json, wallet: args.wallet,
|
|
195
194
|
});
|
|
196
195
|
}
|
|
197
196
|
case "push": {
|
|
@@ -208,7 +207,7 @@ export async function run(argv) {
|
|
|
208
207
|
pay: args.pay,
|
|
209
208
|
epochs: args.epochs,
|
|
210
209
|
storage: args.storage,
|
|
211
|
-
json: args.json,
|
|
210
|
+
json: args.json, wallet: args.wallet,
|
|
212
211
|
});
|
|
213
212
|
}
|
|
214
213
|
case "rm": {
|
|
@@ -352,6 +351,8 @@ export async function run(argv) {
|
|
|
352
351
|
const { mcp } = await import("./commands/mcp.js");
|
|
353
352
|
return await mcp({ server: args.server, network: args.network, out: args.out });
|
|
354
353
|
}
|
|
354
|
+
case "platform":
|
|
355
|
+
return (await import("./commands/platform.js")).platform(args.operands[0], { out: args.out });
|
|
355
356
|
case "s3": {
|
|
356
357
|
const { s3 } = await import("./commands/s3.js");
|
|
357
358
|
return await s3({ server: args.server, network: args.network, port: args.port, json: args.json });
|
|
@@ -385,7 +386,7 @@ async function main() {
|
|
|
385
386
|
const { noteFailure, recordRun } = await import("./run-log.js");
|
|
386
387
|
if (failed !== null)
|
|
387
388
|
noteFailure(failed);
|
|
388
|
-
recordRun(argv, typeof process.exitCode === "number" ? process.exitCode : 0, Date.now() - started);
|
|
389
|
+
await recordRun(argv, typeof process.exitCode === "number" ? process.exitCode : 0, Date.now() - started);
|
|
389
390
|
await noteUpdateAfter(argv, VERSION);
|
|
390
391
|
}
|
|
391
392
|
if (invokedDirectly(import.meta.filename)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The sealed bytes this machine holds under that name, or null when it holds none. */
|
|
2
|
-
export declare function readCachedChunk(accountId: string, hash: string): string | null
|
|
2
|
+
export declare function readCachedChunk(accountId: string, hash: string): Promise<string | null>;
|
|
3
3
|
/** Keep these sealed bytes under that name. Silent when the machine will not take them. */
|
|
4
|
-
export declare function writeCachedChunk(accountId: string, hash: string, ct: string): void
|
|
4
|
+
export declare function writeCachedChunk(accountId: string, hash: string, ct: string): Promise<void>;
|
|
5
5
|
/**
|
|
6
6
|
* Drop every copy this account holds that the given list does not name.
|
|
7
7
|
*
|
|
@@ -11,4 +11,4 @@ export declare function writeCachedChunk(accountId: string, hash: string, ct: st
|
|
|
11
11
|
* abandoned. Called after every complete read and every successful write, which is exactly when
|
|
12
12
|
* "what the list names" is known.
|
|
13
13
|
*/
|
|
14
|
-
export declare function pruneChunkCache(accountId: string, keep: ReadonlySet<string>): void
|
|
14
|
+
export declare function pruneChunkCache(accountId: string, keep: ReadonlySet<string>): Promise<void>;
|
|
@@ -6,46 +6,45 @@
|
|
|
6
6
|
// rewrote one chunk costs one download instead of the whole list.
|
|
7
7
|
//
|
|
8
8
|
// ⛔ AND IT IS STILL CHECKED ON THE WAY OUT. The reader re-hashes what it reads from here before
|
|
9
|
-
// opening it (`manifest-chunk-flow.ts`). A
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// opening it (`manifest-chunk-flow.ts`). A store on this machine is not a trusted one: whoever
|
|
10
|
+
// holds the machine can edit it, and the index is the only thing that says which bytes belong
|
|
11
|
+
// to which version.
|
|
12
12
|
//
|
|
13
13
|
// ⛔ WHAT IS STORED IS SEALED. These are the account's names, folders and file keys, sealed with
|
|
14
|
-
// the NMTS key — the same bytes the server holds and cannot read.
|
|
15
|
-
//
|
|
14
|
+
// the NMTS key — the same bytes the server holds and cannot read. Where they land, and how
|
|
15
|
+
// private that place is, is the host's business: on this machine it is a 0600 file in a 0700
|
|
16
|
+
// directory, beside the kept copy of the index and for the same reason.
|
|
16
17
|
//
|
|
17
18
|
// ⚠ NOTHING HERE THROWS. A cache that cannot be read or written is a slower command, never a
|
|
18
19
|
// broken one: every function answers "no copy" and the network path behind it does the work.
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
/** A chunk's name: base64url SHA-256, unpadded — 43 characters, and it becomes a file name. */
|
|
20
|
+
import { fromUtf8, utf8 } from "./bytes.js";
|
|
21
|
+
import { host } from "./host.js";
|
|
22
|
+
/** A chunk's name: base64url SHA-256, unpadded — 43 characters, and it becomes part of a key. */
|
|
23
23
|
const NAME = /^[A-Za-z0-9_-]{43}$/;
|
|
24
24
|
/**
|
|
25
|
-
* ⛔ AN ACCOUNT ID BECOMES PART OF A
|
|
25
|
+
* ⛔ AN ACCOUNT ID BECOMES PART OF A KEY HERE, so it is CHECKED rather than trusted — the same
|
|
26
26
|
* check `manifest.ts` makes on the kept list, and for the same reason: a value that reaches a
|
|
27
|
-
*
|
|
27
|
+
* key unchecked is how `..` becomes a write somewhere else on a host that spells keys as paths.
|
|
28
28
|
*/
|
|
29
29
|
const ACCOUNT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
30
|
-
/**
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
return null;
|
|
34
|
-
return join(configDir(), "file-list-chunks", accountId);
|
|
30
|
+
/** The key prefix one account's chunks live under, or null when the id is not one this tool derived. */
|
|
31
|
+
function areaFor(accountId) {
|
|
32
|
+
return ACCOUNT.test(accountId) ? `chunks/${accountId}/` : null;
|
|
35
33
|
}
|
|
36
|
-
function
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
return null;
|
|
40
|
-
return join(dir, `${hash}.ct`);
|
|
34
|
+
function keyFor(accountId, hash) {
|
|
35
|
+
const area = areaFor(accountId);
|
|
36
|
+
return area === null || !NAME.test(hash) ? null : `${area}${hash}`;
|
|
41
37
|
}
|
|
42
38
|
/** The sealed bytes this machine holds under that name, or null when it holds none. */
|
|
43
|
-
export function readCachedChunk(accountId, hash) {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
39
|
+
export async function readCachedChunk(accountId, hash) {
|
|
40
|
+
const key = keyFor(accountId, hash);
|
|
41
|
+
if (key === null)
|
|
46
42
|
return null;
|
|
47
43
|
try {
|
|
48
|
-
const
|
|
44
|
+
const held = await host().state.read(key);
|
|
45
|
+
if (held === undefined)
|
|
46
|
+
return null;
|
|
47
|
+
const text = fromUtf8(held).trim();
|
|
49
48
|
return text === "" ? null : text;
|
|
50
49
|
}
|
|
51
50
|
catch {
|
|
@@ -53,18 +52,16 @@ export function readCachedChunk(accountId, hash) {
|
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
/** Keep these sealed bytes under that name. Silent when the machine will not take them. */
|
|
56
|
-
export function writeCachedChunk(accountId, hash, ct) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
if (dir === null || path === null)
|
|
55
|
+
export async function writeCachedChunk(accountId, hash, ct) {
|
|
56
|
+
const key = keyFor(accountId, hash);
|
|
57
|
+
if (key === null)
|
|
60
58
|
return;
|
|
61
59
|
try {
|
|
62
|
-
|
|
63
|
-
writeFileSync(path, `${ct}\n`, { mode: 0o600 });
|
|
60
|
+
await host().state.write(key, utf8(`${ct}\n`));
|
|
64
61
|
}
|
|
65
62
|
catch {
|
|
66
|
-
// Out of space, read-only home, a
|
|
67
|
-
// list fetches from the server instead, which is what this cache is an optimisation of.
|
|
63
|
+
// Out of space, a read-only home, a browser that would not open its database: the next read of
|
|
64
|
+
// the list fetches from the server instead, which is what this cache is an optimisation of.
|
|
68
65
|
}
|
|
69
66
|
}
|
|
70
67
|
/**
|
|
@@ -76,27 +73,25 @@ export function writeCachedChunk(accountId, hash, ct) {
|
|
|
76
73
|
* abandoned. Called after every complete read and every successful write, which is exactly when
|
|
77
74
|
* "what the list names" is known.
|
|
78
75
|
*/
|
|
79
|
-
export function pruneChunkCache(accountId, keep) {
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
76
|
+
export async function pruneChunkCache(accountId, keep) {
|
|
77
|
+
const area = areaFor(accountId);
|
|
78
|
+
if (area === null)
|
|
82
79
|
return;
|
|
83
|
-
let
|
|
80
|
+
let held;
|
|
84
81
|
try {
|
|
85
|
-
|
|
82
|
+
held = await host().state.keys(area);
|
|
86
83
|
}
|
|
87
84
|
catch {
|
|
88
85
|
return;
|
|
89
86
|
}
|
|
90
|
-
for (const
|
|
91
|
-
if (
|
|
92
|
-
continue;
|
|
93
|
-
if (keep.has(name.slice(0, -3)))
|
|
87
|
+
for (const key of held) {
|
|
88
|
+
if (keep.has(key.slice(area.length)))
|
|
94
89
|
continue;
|
|
95
90
|
try {
|
|
96
|
-
|
|
91
|
+
await host().state.remove(key);
|
|
97
92
|
}
|
|
98
93
|
catch {
|
|
99
|
-
// A copy that will not delete costs
|
|
94
|
+
// A copy that will not delete costs space and nothing else; it is named by a hash, so it can
|
|
100
95
|
// never be handed back as some other version.
|
|
101
96
|
}
|
|
102
97
|
}
|
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
// PURE OF COMMANDS: it takes a derived key and a server, and knows nothing about what an edit
|
|
19
19
|
// means. Deciding WHICH entries the list should hold is `manifest-write.ts`'s job; deciding which
|
|
20
20
|
// entry goes in which chunk is the shared packer's.
|
|
21
|
+
import { fromBase64Url, toBase64Url } from "./bytes.js";
|
|
21
22
|
import { request, ServerError } from "./api.js";
|
|
22
23
|
import { AAD, DERIVED, loadCrypto } from "./crypto.js";
|
|
23
24
|
import { NmtsError } from "./errors.js";
|
|
24
25
|
import { pruneChunkCache, readCachedChunk, writeCachedChunk } from "./manifest-chunk-cache.js";
|
|
25
26
|
import { keepTrying } from "./net-retry.js";
|
|
26
|
-
import {
|
|
27
|
+
import { host } from "./host.js";
|
|
27
28
|
import { AAD_FILE_LIST_CHUNK, chunkFingerprint, decodeChunk, decodeFileList, encodeChunk, encodeIndex, FILE_LIST_VERSION_CHUNKED, } from "./shared/lib/drive/manifest-chunks.js";
|
|
28
29
|
import { packAll, repack } from "./shared/lib/drive/manifest-pack.js";
|
|
29
30
|
/** How many chunk requests are in flight at once. Enough to fill a link, few enough to stay fair. */
|
|
@@ -42,10 +43,10 @@ export class ManifestChunkError extends NmtsError {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
function sealUnder(io, aad, body) {
|
|
45
|
-
return
|
|
46
|
+
return toBase64Url(io.crypt.envelope_seal(io.key, utf8.encode(aad), body));
|
|
46
47
|
}
|
|
47
48
|
function openUnder(io, aad, ct) {
|
|
48
|
-
return io.crypt.envelope_open(io.key, utf8.encode(aad),
|
|
49
|
+
return io.crypt.envelope_open(io.key, utf8.encode(aad), fromBase64Url(ct));
|
|
49
50
|
}
|
|
50
51
|
/** What the two chunk routes answer. Narrowed here rather than trusted. */
|
|
51
52
|
function ctOf(answer) {
|
|
@@ -63,7 +64,7 @@ function ctOf(answer) {
|
|
|
63
64
|
* is a version nobody will ask for again.
|
|
64
65
|
*/
|
|
65
66
|
export async function openChunks(io, index) {
|
|
66
|
-
|
|
67
|
+
await host().zstd.register();
|
|
67
68
|
const refs = index.chunks;
|
|
68
69
|
const out = new Array(refs.length);
|
|
69
70
|
let next = 0;
|
|
@@ -78,12 +79,12 @@ export async function openChunks(io, index) {
|
|
|
78
79
|
}
|
|
79
80
|
};
|
|
80
81
|
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, refs.length) }, worker));
|
|
81
|
-
pruneChunkCache(io.accountId, new Set(refs.map((c) => c.h)));
|
|
82
|
+
await pruneChunkCache(io.accountId, new Set(refs.map((c) => c.h)));
|
|
82
83
|
return out;
|
|
83
84
|
}
|
|
84
85
|
/** One chunk: from this machine if it has it, else from the server — verified either way. */
|
|
85
86
|
async function openOne(io, ref, indexSeq) {
|
|
86
|
-
const cached = readCachedChunk(io.accountId, ref.h);
|
|
87
|
+
const cached = await readCachedChunk(io.accountId, ref.h);
|
|
87
88
|
let ct = cached;
|
|
88
89
|
if (ct === null) {
|
|
89
90
|
try {
|
|
@@ -119,7 +120,7 @@ async function openOne(io, ref, indexSeq) {
|
|
|
119
120
|
throw new ManifestChunkError(`Part of the file list holds ${doc.items.length} entries where the index says ${ref.n}.`);
|
|
120
121
|
}
|
|
121
122
|
if (cached === null)
|
|
122
|
-
writeCachedChunk(io.accountId, ref.h, ct);
|
|
123
|
+
await writeCachedChunk(io.accountId, ref.h, ct);
|
|
123
124
|
return doc.items;
|
|
124
125
|
}
|
|
125
126
|
/**
|
|
@@ -133,7 +134,7 @@ async function openOne(io, ref, indexSeq) {
|
|
|
133
134
|
* the chunks written by the losing attempt are named by no index and the server sweeps them.
|
|
134
135
|
*/
|
|
135
136
|
export async function writeChunkedList(io, plan) {
|
|
136
|
-
|
|
137
|
+
await host().zstd.register();
|
|
137
138
|
const packed = plan.previous.length > 0 ? repack(plan.previous, plan.entries) : packAll(plan.entries);
|
|
138
139
|
const rows = [];
|
|
139
140
|
const held = [];
|
|
@@ -168,7 +169,7 @@ export async function writeChunkedList(io, plan) {
|
|
|
168
169
|
body.fill(0);
|
|
169
170
|
const refs = rows.map((r) => r.h);
|
|
170
171
|
const seq = await putIndex(io, plan.baseSeq, ct, refs, fresh);
|
|
171
|
-
pruneChunkCache(io.accountId, new Set(refs));
|
|
172
|
+
await pruneChunkCache(io.accountId, new Set(refs));
|
|
172
173
|
return { seq, ct, held };
|
|
173
174
|
}
|
|
174
175
|
/**
|
|
@@ -197,7 +198,7 @@ async function putIndex(io, baseSeq, ct, refs, fresh) {
|
|
|
197
198
|
for (const name of refs) {
|
|
198
199
|
if (again.has(name))
|
|
199
200
|
continue;
|
|
200
|
-
const kept = readCachedChunk(io.accountId, name);
|
|
201
|
+
const kept = await readCachedChunk(io.accountId, name);
|
|
201
202
|
if (kept !== null)
|
|
202
203
|
again.set(name, kept);
|
|
203
204
|
}
|
|
@@ -231,7 +232,7 @@ async function putChunks(io, chunks) {
|
|
|
231
232
|
token: io.apiKey,
|
|
232
233
|
body: { ct },
|
|
233
234
|
}), { retryable: (error) => error instanceof ServerError && error.status === 429 });
|
|
234
|
-
writeCachedChunk(io.accountId, name, ct);
|
|
235
|
+
await writeCachedChunk(io.accountId, name, ct);
|
|
235
236
|
}
|
|
236
237
|
};
|
|
237
238
|
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, names.length) }, worker));
|
|
@@ -259,14 +260,14 @@ function seqOf(answer) {
|
|
|
259
260
|
* refusing here would refuse in a case the command used to handle.
|
|
260
261
|
*/
|
|
261
262
|
export async function namedChunks(code, ct) {
|
|
262
|
-
|
|
263
|
+
await host().zstd.register();
|
|
263
264
|
const crypt = await loadCrypto();
|
|
264
265
|
const [from, to] = DERIVED.fileListKey;
|
|
265
266
|
const derived = crypt.kdf_derive(crypt.account_code_parse(code));
|
|
266
267
|
const key = derived.slice(from, to);
|
|
267
268
|
derived.fill(0);
|
|
268
269
|
try {
|
|
269
|
-
const body = crypt.envelope_open(key, utf8.encode(AAD.fileList),
|
|
270
|
+
const body = crypt.envelope_open(key, utf8.encode(AAD.fileList), fromBase64Url(ct));
|
|
270
271
|
const doc = await decodeFileList(body);
|
|
271
272
|
body.fill(0);
|
|
272
273
|
return doc.v === FILE_LIST_VERSION_CHUNKED ? doc.index.chunks.map((c) => c.h) : [];
|
|
@@ -292,7 +293,7 @@ export async function keptChunks(code, accountId, indexCt) {
|
|
|
292
293
|
return null;
|
|
293
294
|
const out = [];
|
|
294
295
|
for (const name of names) {
|
|
295
|
-
const ct = readCachedChunk(accountId, name);
|
|
296
|
+
const ct = await readCachedChunk(accountId, name);
|
|
296
297
|
if (ct === null) {
|
|
297
298
|
throw new NmtsError("This machine holds the file list's index but not all of its parts.", {
|
|
298
299
|
exitCode: 4,
|
package/dist/manifest-write.js
CHANGED
|
@@ -236,7 +236,7 @@ export async function addEntry(input) {
|
|
|
236
236
|
let replaced;
|
|
237
237
|
// ⛔ SETTLED ONCE, OUTSIDE THE RETRY LOOP. What the machine is set to and whether a mode is on
|
|
238
238
|
// are facts about this run, not about the list version a compare-and-swap happened to read.
|
|
239
|
-
const choice = decide(input.onCollision).choice;
|
|
239
|
+
const choice = (await decide(input.onCollision)).choice;
|
|
240
240
|
const result = await applyManyToList(input, (entries) => {
|
|
241
241
|
const plan = planAddition(entries, input.entry, choice);
|
|
242
242
|
alreadyThere = plan.alreadyThere ?? null;
|
package/dist/manifest.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface KeptList {
|
|
|
30
30
|
* write out a good copy because an old one is unreadable would be refusing the very thing it is
|
|
31
31
|
* for.
|
|
32
32
|
*/
|
|
33
|
-
export declare function readKeptList(accountId: string): KeptList | null
|
|
33
|
+
export declare function readKeptList(accountId: string): Promise<KeptList | null>;
|
|
34
34
|
/**
|
|
35
35
|
* Record a version this machine WROTE, so the server cannot serve an older one back afterwards.
|
|
36
36
|
*
|
|
@@ -40,7 +40,7 @@ export declare function readKeptList(accountId: string): KeptList | null;
|
|
|
40
40
|
*/
|
|
41
41
|
export declare function recordWrittenList(accountId: string, seq: number, ct: string): Promise<void>;
|
|
42
42
|
/** True when this machine has a record for the account — i.e. a rollback would be visible. */
|
|
43
|
-
export declare function hasSeenBefore(accountId: string): boolean
|
|
43
|
+
export declare function hasSeenBefore(accountId: string): Promise<boolean>;
|
|
44
44
|
export interface FileList {
|
|
45
45
|
/** null when the account has no list yet — a new account, not an error. */
|
|
46
46
|
manifest: Manifest | null;
|
package/dist/manifest.js
CHANGED
|
@@ -27,12 +27,11 @@
|
|
|
27
27
|
// exactly as the single blob was, and the chunks are kept by name in the chunk store
|
|
28
28
|
// (`manifest-chunk-cache.ts`), which is pruned to what the list just read names. So the copy is
|
|
29
29
|
// still complete — it is simply in two places, and `nmts listfile` writes them out as one file.
|
|
30
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
31
|
-
import { join } from "node:path";
|
|
32
30
|
import { request } from "./api.js";
|
|
31
|
+
import { fromBase64Url, fromUtf8, toBase64Url, utf8 } from "./bytes.js";
|
|
33
32
|
import { AAD, DERIVED, loadCrypto } from "./crypto.js";
|
|
34
|
-
import { configDir } from "./credentials.js";
|
|
35
33
|
import { NmtsError } from "./errors.js";
|
|
34
|
+
import { host } from "./host.js";
|
|
36
35
|
import { openChunks } from "./manifest-chunk-flow.js";
|
|
37
36
|
import { decodeFileList, FILE_LIST_VERSION_CHUNKED, } from "./shared/lib/drive/manifest-chunks.js";
|
|
38
37
|
function asResponse(value) {
|
|
@@ -53,26 +52,26 @@ function asResponse(value) {
|
|
|
53
52
|
}
|
|
54
53
|
/** base64url SHA-256 of a sealed blob — the value a later list carries as its `prev`. */
|
|
55
54
|
async function fingerprint(ct) {
|
|
56
|
-
const digest = await crypto.subtle.digest("SHA-256",
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
function statePath() {
|
|
60
|
-
return join(configDir(), "file-list-state.json");
|
|
55
|
+
const digest = await crypto.subtle.digest("SHA-256", utf8(ct));
|
|
56
|
+
return toBase64Url(new Uint8Array(digest));
|
|
61
57
|
}
|
|
58
|
+
/** The one key this machine's record of what it has seen lives under. */
|
|
59
|
+
const STATE_KEY = "manifest/state";
|
|
62
60
|
/**
|
|
63
|
-
*
|
|
61
|
+
* The key this machine keeps one account's sealed file list under.
|
|
64
62
|
*
|
|
65
|
-
* ⛔ AN ACCOUNT ID BECOMES PART OF A
|
|
66
|
-
* tool has comes from its own derivation and is base64url, but a value that reaches a
|
|
67
|
-
* unchecked is how `..` becomes a write somewhere else
|
|
63
|
+
* ⛔ AN ACCOUNT ID BECOMES PART OF A KEY HERE, so it is CHECKED rather than trusted. Every id this
|
|
64
|
+
* tool has comes from its own derivation and is base64url, but a value that reaches a key
|
|
65
|
+
* unchecked is how `..` becomes a write somewhere else on a host that spells keys as paths, and
|
|
66
|
+
* the check costs one line.
|
|
68
67
|
*/
|
|
69
|
-
function
|
|
68
|
+
function keptListKey(accountId) {
|
|
70
69
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(accountId)) {
|
|
71
70
|
throw new NmtsError("That is not an account id this tool derived.", {
|
|
72
71
|
nextStep: "Nothing was written. This is a fault in the tool rather than in the account.",
|
|
73
72
|
});
|
|
74
73
|
}
|
|
75
|
-
return
|
|
74
|
+
return `manifest/${accountId}`;
|
|
76
75
|
}
|
|
77
76
|
/** This machine's copy of one account's sealed file list. */
|
|
78
77
|
/**
|
|
@@ -104,9 +103,12 @@ function isKeptList(value) {
|
|
|
104
103
|
* write out a good copy because an old one is unreadable would be refusing the very thing it is
|
|
105
104
|
* for.
|
|
106
105
|
*/
|
|
107
|
-
export function readKeptList(accountId) {
|
|
106
|
+
export async function readKeptList(accountId) {
|
|
108
107
|
try {
|
|
109
|
-
const
|
|
108
|
+
const held = await host().state.read(keptListKey(accountId));
|
|
109
|
+
if (held === undefined)
|
|
110
|
+
return null;
|
|
111
|
+
const parsed = JSON.parse(fromUtf8(held));
|
|
110
112
|
return isKeptList(parsed) ? parsed : null;
|
|
111
113
|
}
|
|
112
114
|
catch {
|
|
@@ -121,17 +123,19 @@ export function readKeptList(accountId) {
|
|
|
121
123
|
* a run that then read an older list from the server must not overwrite the newest copy with
|
|
122
124
|
* it. The record refuses a lower version while it exists; this is what holds when it does not.
|
|
123
125
|
*/
|
|
124
|
-
function writeKept(accountId, seq, ct) {
|
|
125
|
-
const held = readKeptList(accountId);
|
|
126
|
+
async function writeKept(accountId, seq, ct) {
|
|
127
|
+
const held = await readKeptList(accountId);
|
|
126
128
|
if (held !== null && held.seq > seq)
|
|
127
129
|
return;
|
|
128
130
|
const copy = { seq, savedAt: new Date().toISOString(), ct };
|
|
129
|
-
|
|
130
|
-
writeFileSync(keptListPath(accountId), `${JSON.stringify(copy, null, 2)}\n`, { mode: 0o600 });
|
|
131
|
+
await host().state.write(keptListKey(accountId), utf8(`${JSON.stringify(copy, null, 2)}\n`));
|
|
131
132
|
}
|
|
132
|
-
function readSeen() {
|
|
133
|
+
async function readSeen() {
|
|
133
134
|
try {
|
|
134
|
-
const
|
|
135
|
+
const held = await host().state.read(STATE_KEY);
|
|
136
|
+
if (held === undefined)
|
|
137
|
+
return {};
|
|
138
|
+
const parsed = JSON.parse(fromUtf8(held));
|
|
135
139
|
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
136
140
|
}
|
|
137
141
|
catch {
|
|
@@ -147,14 +151,13 @@ function readSeen() {
|
|
|
147
151
|
* would be two chances for a caller to update one and not the other — after which the machine
|
|
148
152
|
* would hold a copy of one version while claiming to have seen another.
|
|
149
153
|
*/
|
|
150
|
-
function writeSeen(accountId, seq, fp, ct) {
|
|
151
|
-
const all = readSeen();
|
|
154
|
+
async function writeSeen(accountId, seq, fp, ct) {
|
|
155
|
+
const all = await readSeen();
|
|
152
156
|
all[accountId] = { seq, fingerprint: fp };
|
|
153
|
-
|
|
154
|
-
writeFileSync(statePath(), `${JSON.stringify(all, null, 2)}\n`, { mode: 0o600 });
|
|
157
|
+
await host().state.write(STATE_KEY, utf8(`${JSON.stringify(all, null, 2)}\n`));
|
|
155
158
|
// The record goes first: it is the safety device, and a machine that failed to keep a copy must
|
|
156
159
|
// still refuse an older list afterwards.
|
|
157
|
-
writeKept(accountId, seq, ct);
|
|
160
|
+
await writeKept(accountId, seq, ct);
|
|
158
161
|
}
|
|
159
162
|
/**
|
|
160
163
|
* Record a version this machine WROTE, so the server cannot serve an older one back afterwards.
|
|
@@ -164,11 +167,11 @@ function writeSeen(accountId, seq, fp, ct) {
|
|
|
164
167
|
* a rollback.
|
|
165
168
|
*/
|
|
166
169
|
export async function recordWrittenList(accountId, seq, ct) {
|
|
167
|
-
writeSeen(accountId, seq, await fingerprint(ct), ct);
|
|
170
|
+
await writeSeen(accountId, seq, await fingerprint(ct), ct);
|
|
168
171
|
}
|
|
169
172
|
/** True when this machine has a record for the account — i.e. a rollback would be visible. */
|
|
170
|
-
export function hasSeenBefore(accountId) {
|
|
171
|
-
return
|
|
173
|
+
export async function hasSeenBefore(accountId) {
|
|
174
|
+
return accountId in (await readSeen());
|
|
172
175
|
}
|
|
173
176
|
/**
|
|
174
177
|
* Fetch and open the account's file list.
|
|
@@ -178,10 +181,10 @@ export function hasSeenBefore(accountId) {
|
|
|
178
181
|
*/
|
|
179
182
|
export async function readFileList(base, apiKey, accountCode, accountId) {
|
|
180
183
|
const answer = asResponse(await request(base, "/v1/manifest", { token: apiKey }));
|
|
181
|
-
const first = !hasSeenBefore(accountId);
|
|
184
|
+
const first = !(await hasSeenBefore(accountId));
|
|
182
185
|
if (answer.state === "absent")
|
|
183
186
|
return { manifest: null, firstTimeOnThisMachine: first };
|
|
184
|
-
const seen = readSeen()[accountId];
|
|
187
|
+
const seen = (await readSeen())[accountId];
|
|
185
188
|
if (seen !== undefined && answer.seq < seen.seq) {
|
|
186
189
|
throw new NmtsError(`The server offered file-list version ${answer.seq}; this machine already saw ${seen.seq}.`, {
|
|
187
190
|
nextStep: "Nothing was changed. A list that goes backwards means an older copy is being served, " +
|
|
@@ -214,7 +217,7 @@ export async function readFileList(base, apiKey, accountCode, accountId) {
|
|
|
214
217
|
try {
|
|
215
218
|
let body;
|
|
216
219
|
try {
|
|
217
|
-
body = crypt.envelope_open(key,
|
|
220
|
+
body = crypt.envelope_open(key, utf8(AAD.fileList), fromBase64Url(answer.ct));
|
|
218
221
|
}
|
|
219
222
|
catch {
|
|
220
223
|
throw new NmtsError("The file list did not open with this account's key.", {
|
|
@@ -252,7 +255,7 @@ export async function readFileList(base, apiKey, accountCode, accountId) {
|
|
|
252
255
|
// ⛔ THE VERSION THAT IS KEPT IS THE SEALED ONE, and so are the bytes it came out of. Believing
|
|
253
256
|
// the server's column here would let it decide which copy this machine keeps. The bytes are
|
|
254
257
|
// the INDEX at version 2; its chunks are kept beside it, by name, in the chunk store.
|
|
255
|
-
writeSeen(accountId, manifest.seq, fp, answer.ct);
|
|
258
|
+
await writeSeen(accountId, manifest.seq, fp, answer.ct);
|
|
256
259
|
return out;
|
|
257
260
|
}
|
|
258
261
|
finally {
|
package/dist/mcp-tools/tiers.js
CHANGED
|
@@ -141,7 +141,7 @@ export function withTiers(tools, asker) {
|
|
|
141
141
|
...tool,
|
|
142
142
|
description: `${tierLine(tier)}${tool.description}`,
|
|
143
143
|
annotations: annotationsOf(tool.name),
|
|
144
|
-
run: async (args) => (await passTool(tool.name, args, currentMode(), asker())) ?? (await tool.run(args)),
|
|
144
|
+
run: async (args) => (await passTool(tool.name, args, await currentMode(), asker())) ?? (await tool.run(args)),
|
|
145
145
|
};
|
|
146
146
|
});
|
|
147
147
|
}
|
package/dist/network.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const NETWORKS: readonly ["mainnet", "testnet"];
|
|
2
2
|
export type Network = (typeof NETWORKS)[number];
|
|
3
|
-
export
|
|
3
|
+
export { NETWORK_ENV_VAR } from "./env-vars.ts";
|
|
4
4
|
/**
|
|
5
5
|
* Decide the network for this run.
|
|
6
6
|
*
|
package/dist/network.js
CHANGED
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
// ⛔ SO THE RULE HERE IS: the live server implies mainnet, and ANY other server must say which
|
|
10
10
|
// network it is. There is no fallback. A tool that has to guess where somebody's files are
|
|
11
11
|
// should stop instead.
|
|
12
|
+
import { NETWORK_ENV_VAR } from "./env-vars.js";
|
|
12
13
|
import { NmtsError } from "./errors.js";
|
|
14
|
+
import { host } from "./host.js";
|
|
13
15
|
import { DEFAULT_SERVER } from "./server.js";
|
|
14
16
|
export const NETWORKS = ["mainnet", "testnet"];
|
|
15
|
-
|
|
17
|
+
// Re-exported: the name itself lives in a module with no imports, because `nmts --help`
|
|
18
|
+
// prints it (`env-vars.ts`).
|
|
19
|
+
export { NETWORK_ENV_VAR } from "./env-vars.js";
|
|
16
20
|
function isNetwork(value) {
|
|
17
21
|
return NETWORKS.includes(value);
|
|
18
22
|
}
|
|
@@ -23,7 +27,7 @@ function isNetwork(value) {
|
|
|
23
27
|
* make: the live server is mainnet. Anything else and it refuses.
|
|
24
28
|
*/
|
|
25
29
|
export function resolveNetwork(server, explicit) {
|
|
26
|
-
const stated = explicit ??
|
|
30
|
+
const stated = explicit ?? host().env(NETWORK_ENV_VAR);
|
|
27
31
|
if (stated !== undefined && stated.length > 0) {
|
|
28
32
|
if (!isNetwork(stated)) {
|
|
29
33
|
throw new NmtsError(`Not a network: ${stated}`, {
|