@needmoretruth/nmts-cli 0.35.0 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/AGENTS.md +11 -12
  2. package/CHANGELOG.md +26 -0
  3. package/README.ko.md +19 -21
  4. package/README.md +21 -22
  5. package/dist/account.js +2 -1
  6. package/dist/arg-options.d.ts +3 -1
  7. package/dist/artifact-about.d.ts +1 -1
  8. package/dist/autonomy.d.ts +3 -10
  9. package/dist/autonomy.js +15 -17
  10. package/dist/bytes.d.ts +15 -0
  11. package/dist/bytes.js +63 -0
  12. package/dist/collision.d.ts +5 -5
  13. package/dist/collision.js +19 -22
  14. package/dist/commands/delete-account.js +1 -1
  15. package/dist/commands/erase.js +1 -1
  16. package/dist/commands/get.js +1 -1
  17. package/dist/commands/listfile.js +1 -1
  18. package/dist/commands/mcp.js +5 -1
  19. package/dist/commands/mode.js +2 -2
  20. package/dist/commands/on-collision.d.ts +1 -1
  21. package/dist/commands/on-collision.js +3 -3
  22. package/dist/commands/platform.d.ts +9 -0
  23. package/dist/commands/platform.js +124 -0
  24. package/dist/commands/pull.js +1 -1
  25. package/dist/commands/push-wallet.js +2 -1
  26. package/dist/commands/push.js +6 -4
  27. package/dist/commands/put-wallet.js +5 -3
  28. package/dist/commands/put.js +8 -5
  29. package/dist/commands/receive.js +1 -1
  30. package/dist/commands/settings.js +1 -1
  31. package/dist/commands/support.js +3 -3
  32. package/dist/commands/unlock.js +2 -2
  33. package/dist/commands/wallet-hall.d.ts +4 -1
  34. package/dist/commands/wallet-hall.js +16 -8
  35. package/dist/commands/wallet-storage-ops.d.ts +4 -1
  36. package/dist/commands/wallet-storage-ops.js +7 -2
  37. package/dist/commands/wallet-swap.d.ts +3 -0
  38. package/dist/commands/wallet-swap.js +8 -4
  39. package/dist/commands/wallet.d.ts +1 -1
  40. package/dist/crypto.d.ts +8 -5
  41. package/dist/crypto.js +15 -61
  42. package/dist/download-sink-node.d.ts +49 -0
  43. package/dist/download-sink-node.js +204 -0
  44. package/dist/download-sink.d.ts +0 -48
  45. package/dist/download-sink.js +6 -199
  46. package/dist/download.js +5 -4
  47. package/dist/engine-node.d.ts +19 -0
  48. package/dist/engine-node.js +77 -0
  49. package/dist/env-vars.d.ts +20 -0
  50. package/dist/env-vars.js +27 -0
  51. package/dist/gate.js +1 -1
  52. package/dist/help.js +9 -4
  53. package/dist/host-contract.d.ts +7 -0
  54. package/dist/host-contract.js +95 -0
  55. package/dist/host-node.d.ts +27 -0
  56. package/dist/host-node.js +192 -0
  57. package/dist/host.d.ts +99 -0
  58. package/dist/host.js +59 -0
  59. package/dist/index.d.ts +6 -49
  60. package/dist/index.js +20 -45
  61. package/dist/main.js +7 -6
  62. package/dist/manifest-chunk-cache.d.ts +3 -3
  63. package/dist/manifest-chunk-cache.js +39 -44
  64. package/dist/manifest-chunk-flow.js +15 -14
  65. package/dist/manifest-write.js +1 -1
  66. package/dist/manifest.d.ts +2 -2
  67. package/dist/manifest.js +37 -34
  68. package/dist/mcp-tools/tiers.js +1 -1
  69. package/dist/network.d.ts +1 -1
  70. package/dist/network.js +6 -2
  71. package/dist/platform-sign.d.ts +143 -0
  72. package/dist/platform-sign.js +220 -0
  73. package/dist/portable.d.ts +59 -0
  74. package/dist/portable.js +82 -0
  75. package/dist/product.d.ts +1 -1
  76. package/dist/product.js +1 -1
  77. package/dist/progress-node.d.ts +2 -0
  78. package/dist/progress-node.js +14 -0
  79. package/dist/progress.d.ts +0 -1
  80. package/dist/progress.js +4 -8
  81. package/dist/redact.js +3 -2
  82. package/dist/registration.js +5 -2
  83. package/dist/risk.d.ts +7 -0
  84. package/dist/risk.js +10 -0
  85. package/dist/run-log.d.ts +4 -4
  86. package/dist/run-log.js +29 -36
  87. package/dist/seal.js +5 -4
  88. package/dist/server.d.ts +1 -1
  89. package/dist/server.js +6 -2
  90. package/dist/setup-questions.js +2 -2
  91. package/dist/upload-file-node.d.ts +3 -0
  92. package/dist/upload-file-node.js +38 -0
  93. package/dist/upload-file.d.ts +2 -2
  94. package/dist/upload-file.js +15 -45
  95. package/dist/upload-price-node.d.ts +8 -0
  96. package/dist/upload-price-node.js +37 -0
  97. package/dist/upload-price.d.ts +0 -8
  98. package/dist/upload-price.js +4 -33
  99. package/dist/upload-steps.js +3 -3
  100. package/dist/upload-store.d.ts +14 -14
  101. package/dist/upload-store.js +68 -75
  102. package/dist/upload-wallet-chain.js +3 -2
  103. package/dist/upload-wallet-put.js +2 -2
  104. package/dist/upload-wallet.js +16 -15
  105. package/dist/upload.js +16 -15
  106. package/dist/wallet-sign-seams.d.ts +12 -0
  107. package/dist/wallet-sign.js +10 -13
  108. package/dist/walrus-write.js +3 -2
  109. package/dist/walrus.d.ts +1 -16
  110. package/dist/walrus.js +11 -22
  111. package/docs/commands/credits.md +1 -1
  112. package/docs/commands/platform.md +14 -0
  113. package/docs/commands/support.md +1 -1
  114. package/docs/commands/trash.md +1 -1
  115. package/docs/commands/wallet.md +11 -7
  116. package/package.json +13 -2
package/dist/run-log.js CHANGED
@@ -21,12 +21,13 @@
21
21
  // `nmts get` to report a failure it did not have. Every write is inside a `try` that swallows.
22
22
  // ⚠ There is no debug switch in this tool to say so through, so it is silent — which is why
23
23
  // this paragraph is here rather than a line of output.
24
- import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
25
- import { join } from "node:path";
26
24
  import { OPTIONS_TAKING_A_VALUE } from "./args.js";
27
- import { configDir, modesAreEnforced } from "./credentials.js";
25
+ import { fromUtf8, utf8 } from "./bytes.js";
26
+ import { host } from "./host.js";
28
27
  import { redact } from "./redact.js";
29
28
  import { VERSION } from "./product.js";
29
+ /** The one key the record lives under. On this machine that is `runs.jsonl` in the config directory. */
30
+ export const RUN_LOG_KEY = "runlog";
30
31
  /** Set this to `1` and nothing is written. */
31
32
  export const NO_RUN_LOG_ENV_VAR = "NMTS_NO_RUN_LOG";
32
33
  /**
@@ -69,13 +70,9 @@ const HIDDEN_VALUES = {
69
70
  * common and should not be given one just for this.
70
71
  */
71
72
  let collected = [];
72
- /** Where the log lives. Inside the tool's own directory, which is already 0700. */
73
- export function runLogPath() {
74
- return join(configDir(), "runs.jsonl");
75
- }
76
73
  /** Has the person turned it off? */
77
74
  export function runLogIsOff() {
78
- return process.env[NO_RUN_LOG_ENV_VAR] === "1";
75
+ return host().env(NO_RUN_LOG_ENV_VAR) === "1";
79
76
  }
80
77
  function remember(event) {
81
78
  if (runLogIsOff())
@@ -158,41 +155,34 @@ export function safeArgs(argv) {
158
155
  }
159
156
  return out;
160
157
  }
161
- /** Write one line, creating the file 0600 if it is not there yet. */
162
- function append(line) {
163
- const dir = configDir();
164
- mkdirSync(dir, { recursive: true, mode: 0o700 });
165
- const path = runLogPath();
166
- if (!existsSync(path)) {
167
- writeFileSync(path, "", { mode: 0o600 });
168
- if (modesAreEnforced())
169
- chmodSync(path, 0o600);
170
- }
171
- appendFileSync(path, line);
172
- if (statSync(path).size > MAX_LOG_BYTES)
173
- trim(path);
174
- }
175
158
  /**
176
- * Drop the oldest whole lines until what is left fits.
159
+ * Add one line, trimming the oldest away when the record has grown past its cap.
160
+ *
161
+ * ⛔ WHOLE LINES. Cutting at a byte offset leaves a half-written JSON object at the top, and a
162
+ * reader that meets one has to decide whether the record is corrupt or merely trimmed.
177
163
  *
178
- * WHOLE LINES. Cutting the file at a byte offset leaves a half-written JSON object at the top,
179
- * and a reader that meets one has to decide whether the file is corrupt or merely trimmed.
164
+ * READ, ADD, WRITE, rather than an append. A host's state is bytes under a key the only shape
165
+ * a browser's store and a file both have and the record is a quarter of a megabyte at most,
166
+ * which is what makes rewriting it affordable at the end of a run.
180
167
  */
181
- function trim(path) {
182
- const lines = readFileSync(path, "utf8").split("\n");
168
+ async function append(line) {
169
+ const state = host().state;
170
+ const held = await state.read(RUN_LOG_KEY);
171
+ const lines = (held === undefined ? "" : fromUtf8(held)).split("\n");
172
+ lines.push(line.replace(/\n$/, ""));
183
173
  const kept = [];
184
174
  let bytes = 0;
185
175
  for (let i = lines.length - 1; i >= 0; i -= 1) {
186
- const line = lines[i];
187
- if (line === undefined || line === "")
176
+ const one = lines[i];
177
+ if (one === undefined || one === "")
188
178
  continue;
189
- const size = Buffer.byteLength(line, "utf8") + 1;
179
+ const size = utf8(one).length + 1;
190
180
  if (bytes + size > MAX_LOG_BYTES)
191
181
  break;
192
- kept.unshift(line);
182
+ kept.unshift(one);
193
183
  bytes += size;
194
184
  }
195
- writeFileSync(path, kept.length === 0 ? "" : `${kept.join("\n")}\n`, { mode: 0o600 });
185
+ await state.write(RUN_LOG_KEY, utf8(kept.length === 0 ? "" : `${kept.join("\n")}\n`));
196
186
  }
197
187
  /**
198
188
  * Write this run down, and forget what was collected.
@@ -200,7 +190,7 @@ function trim(path) {
200
190
  * Returns nothing and throws nothing: see the header. A run that could not be written is a run
201
191
  * that is missing from a report, which is a smaller problem than a command that failed for it.
202
192
  */
203
- export function recordRun(argv, exit, ms, now = new Date()) {
193
+ export async function recordRun(argv, exit, ms, now = new Date()) {
204
194
  if (runLogIsOff()) {
205
195
  forgetRun();
206
196
  return;
@@ -220,7 +210,7 @@ export function recordRun(argv, exit, ms, now = new Date()) {
220
210
  };
221
211
  forgetRun();
222
212
  try {
223
- append(`${JSON.stringify(record)}\n`);
213
+ await append(`${JSON.stringify(record)}\n`);
224
214
  }
225
215
  catch {
226
216
  // Swallowed on purpose. See the header: the log is a convenience and the command is the work.
@@ -274,10 +264,13 @@ function asRecord(value) {
274
264
  };
275
265
  }
276
266
  /** The newest `count` runs, oldest first. An unreadable or missing file is no runs. */
277
- export function readRuns(count) {
267
+ export async function readRuns(count) {
278
268
  let text;
279
269
  try {
280
- text = readFileSync(runLogPath(), "utf8");
270
+ const held = await host().state.read(RUN_LOG_KEY);
271
+ if (held === undefined)
272
+ return [];
273
+ text = fromUtf8(held);
281
274
  }
282
275
  catch {
283
276
  return [];
package/dist/seal.js CHANGED
@@ -8,7 +8,8 @@
8
8
  // ⛔ THE PLAINTEXT NEVER LEAVES THIS PROCESS. What goes out is the NCF-3 stream; what the server
9
9
  // is told is its LENGTH. The name, the folder and the real size are written into the account's
10
10
  // sealed file list, which the server cannot open.
11
- import { createHash } from "node:crypto";
11
+ import { sha256 } from "@noble/hashes/sha2.js";
12
+ import { toBase64Url } from "./bytes.js";
12
13
  import { AAD } from "./crypto.js";
13
14
  import { NmtsError } from "./errors.js";
14
15
  import { chunkCount, sealedLenFor as sealedLength, } from "./shared/lib/crypto/size-padding.js";
@@ -108,8 +109,8 @@ export function fileSecrets(crypt, dataKey, contentDigest) {
108
109
  const contentHashCt = crypt.envelope_seal(dataKey, encoder.encode(AAD.contentHash), contentDigest);
109
110
  return {
110
111
  dek,
111
- dekWrapped: Buffer.from(dekWrapped).toString("base64url"),
112
- contentHashCt: Buffer.from(contentHashCt).toString("base64url"),
112
+ dekWrapped: toBase64Url(dekWrapped),
113
+ contentHashCt: toBase64Url(contentHashCt),
113
114
  };
114
115
  }
115
116
  /**
@@ -180,7 +181,7 @@ export async function sealFile(crypt, dataKey, plaintext) {
180
181
  nextStep: "The storage network has nothing to store and would refuse the reservation.",
181
182
  });
182
183
  }
183
- const digest = new Uint8Array(createHash("sha256").update(plaintext).digest());
184
+ const digest = sha256(plaintext);
184
185
  const secrets = fileSecrets(crypt, dataKey, digest);
185
186
  digest.fill(0);
186
187
  try {
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const DEFAULT_SERVER = "https://nmts.me";
2
- export declare const SERVER_ENV_VAR = "NMTS_SERVER";
2
+ export { SERVER_ENV_VAR } from "./env-vars.ts";
3
3
  /**
4
4
  * Resolve the server for this run: an explicit argument, then the environment, then the default.
5
5
  *
package/dist/server.js CHANGED
@@ -4,9 +4,13 @@
4
4
  // development against a local stack, not as a knob to tune. It is read from an argument or the
5
5
  // environment rather than stored, so a credentials file cannot silently point a later run
6
6
  // somewhere else than the run that wrote it.
7
+ import { SERVER_ENV_VAR } from "./env-vars.js";
7
8
  import { NmtsError } from "./errors.js";
9
+ import { host } from "./host.js";
8
10
  export const DEFAULT_SERVER = "https://nmts.me";
9
- export const SERVER_ENV_VAR = "NMTS_SERVER";
11
+ // Re-exported so every caller still finds it here; the name itself lives in a module with no
12
+ // imports, because `nmts --help` prints it (`env-vars.ts`).
13
+ export { SERVER_ENV_VAR } from "./env-vars.js";
10
14
  /**
11
15
  * Resolve the server for this run: an explicit argument, then the environment, then the default.
12
16
  *
@@ -14,7 +18,7 @@ export const SERVER_ENV_VAR = "NMTS_SERVER";
14
18
  * typo that lands on another scheme must stop here rather than somewhere further in.
15
19
  */
16
20
  export function resolveServer(explicit) {
17
- const raw = explicit ?? process.env[SERVER_ENV_VAR] ?? DEFAULT_SERVER;
21
+ const raw = explicit ?? host().env(SERVER_ENV_VAR) ?? DEFAULT_SERVER;
18
22
  let url;
19
23
  try {
20
24
  url = new URL(raw);
@@ -19,13 +19,13 @@ import { promptLine, stdinIsATerminal } from "./prompt.js";
19
19
  * never sees a line.
20
20
  */
21
21
  export async function askAboutCollisions(say) {
22
- if (hasChosen() || !stdinIsATerminal())
22
+ if ((await hasChosen()) || !stdinIsATerminal())
23
23
  return;
24
24
  say(``);
25
25
  say(`When a file with that name is already in the drive:`);
26
26
  say(` ${ANSWER_NUMBER.rename} ${COLLISION_MEANS.rename}`);
27
27
  say(` ${ANSWER_NUMBER.overwrite} ${COLLISION_MEANS.overwrite}`);
28
28
  const choice = readAnswer(await promptLine(`[${ANSWER_NUMBER.rename}] `));
29
- setChoice(choice, VERSION, new Date());
29
+ await setChoice(choice, VERSION, new Date());
30
30
  say(`${choice} — ${COLLISION_MEANS[choice]} Change it: ${BINARY_NAME} on-collision <rename|overwrite>`);
31
31
  }
@@ -0,0 +1,3 @@
1
+ import { type PlaintextSource } from "./upload-file.ts";
2
+ /** Read a file off the disk, a chunk at a time. */
3
+ export declare function fileSource(path: string, size: number): PlaintextSource;
@@ -0,0 +1,38 @@
1
+ // A file on THIS MACHINE, read a chunk at a time, as a plaintext source.
2
+ //
3
+ // ⛔ THE SEAM IS `upload-file.ts`'S AND THIS IS THE ONE IMPLEMENTATION THAT NEEDS A DISK. A caller
4
+ // in a browser hands in a `Blob` instead (the SDK's `blobSource`), and everything downstream —
5
+ // the part plan, the sealing, the reservation records — is the same code for both.
6
+ import { open } from "node:fs/promises";
7
+ import { NmtsError } from "./errors.js";
8
+ import { READ_CHUNK_BYTES } from "./upload-file.js";
9
+ /** Read a file off the disk, a chunk at a time. */
10
+ export function fileSource(path, size) {
11
+ return {
12
+ size,
13
+ async *read(offset, length) {
14
+ const handle = await open(path, "r");
15
+ try {
16
+ const buffer = new Uint8Array(Math.min(READ_CHUNK_BYTES, length));
17
+ let at = 0;
18
+ while (at < length) {
19
+ const want = Math.min(buffer.length, length - at);
20
+ const { bytesRead } = await handle.read(buffer, 0, want, offset + at);
21
+ if (bytesRead === 0) {
22
+ // ⛔ SHORT IS NOT DONE. The plan was made from the size this file had when it was
23
+ // measured; a read that ends early means it shrank underneath us, and sealing what
24
+ // arrived would declare a length the bytes do not match.
25
+ throw new NmtsError(`${path} ended after ${at} of ${length} bytes.`, {
26
+ nextStep: "Nothing was sent. The file changed while it was being read.",
27
+ });
28
+ }
29
+ at += bytesRead;
30
+ yield new Uint8Array(buffer.subarray(0, bytesRead));
31
+ }
32
+ }
33
+ finally {
34
+ await handle.close();
35
+ }
36
+ },
37
+ };
38
+ }
@@ -2,6 +2,8 @@ import { type CryptoGlue } from "./crypto.ts";
2
2
  import { type PaddingRule } from "./shared/lib/crypto/size-padding.ts";
3
3
  import { entryOf } from "./upload.ts";
4
4
  import type { BlobProtocol, PaidPart, UploadApi, UploadInput, UploadResult, UploadStep } from "./upload-wire.ts";
5
+ /** How much plaintext is handed to the engine at a time. Matches the format's own chunk size. */
6
+ export declare const READ_CHUNK_BYTES: number;
5
7
  /**
6
8
  * Where the plaintext comes from.
7
9
  *
@@ -15,8 +17,6 @@ export interface PlaintextSource {
15
17
  /** Read `[offset, offset + length)`, in pieces small enough to hold. */
16
18
  read(offset: number, length: number): AsyncIterable<Uint8Array>;
17
19
  }
18
- /** Read a file off the disk, a chunk at a time. */
19
- export declare function fileSource(path: string, size: number): PlaintextSource;
20
20
  export interface FileUploadInput {
21
21
  api: UploadApi;
22
22
  protocol: BlobProtocol;
@@ -15,8 +15,8 @@
15
15
  // the reservation key, and the SHA-256 the account will check the contents against — because
16
16
  // both are needed BEFORE the first part is sealed. The second pass is the sealing itself. The
17
17
  // alternative is holding the file, which is the thing this module exists to avoid.
18
- import { createHash } from "node:crypto";
19
- import { open } from "node:fs/promises";
18
+ import { sha256 } from "@noble/hashes/sha2.js";
19
+ import { fromBase64Url } from "./bytes.js";
20
20
  import { AAD } from "./crypto.js";
21
21
  import { NmtsError } from "./errors.js";
22
22
  import { fileSecrets, sealPart } from "./seal.js";
@@ -27,37 +27,7 @@ import { buyAndPushPart, entryOf } from "./upload.js";
27
27
  import { commitItem } from "./upload-steps.js";
28
28
  import { finishReservationKey, partKey, readItemRecord, readReservationBytes, readReservationRecord, startReservationKey, } from "./upload-store.js";
29
29
  /** How much plaintext is handed to the engine at a time. Matches the format's own chunk size. */
30
- const READ_CHUNK_BYTES = 4 * 2 ** 20;
31
- /** Read a file off the disk, a chunk at a time. */
32
- export function fileSource(path, size) {
33
- return {
34
- size,
35
- async *read(offset, length) {
36
- const handle = await open(path, "r");
37
- try {
38
- const buffer = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, length));
39
- let at = 0;
40
- while (at < length) {
41
- const want = Math.min(buffer.length, length - at);
42
- const { bytesRead } = await handle.read(buffer, 0, want, offset + at);
43
- if (bytesRead === 0) {
44
- // ⛔ SHORT IS NOT DONE. The plan was made from the size this file had when it was
45
- // measured; a read that ends early means it shrank underneath us, and sealing what
46
- // arrived would declare a length the bytes do not match.
47
- throw new NmtsError(`${path} ended after ${at} of ${length} bytes.`, {
48
- nextStep: "Nothing was sent. The file changed while it was being read.",
49
- });
50
- }
51
- at += bytesRead;
52
- yield new Uint8Array(buffer.subarray(0, bytesRead));
53
- }
54
- }
55
- finally {
56
- await handle.close();
57
- }
58
- },
59
- };
60
- }
30
+ export const READ_CHUNK_BYTES = 4 * 2 ** 20;
61
31
  /**
62
32
  * Upload one file and return what the caller must write into the account's file list.
63
33
  *
@@ -83,15 +53,15 @@ export async function uploadFile(input) {
83
53
  // `POST /v1/items` exists and is paid for; all that can still be missing is the account's own
84
54
  // list. Asking the server about every part again would be a round trip per part to learn
85
55
  // something the record already says.
86
- const committed = readItemRecord(fileKey);
56
+ const committed = await readItemRecord(fileKey);
87
57
  if (committed?.itemId !== undefined) {
88
- const entry = recordedEntry(fileKey, plan.length, source.size);
58
+ const entry = await recordedEntry(fileKey, plan.length, source.size);
89
59
  if (entry !== null) {
90
60
  return { itemId: committed.itemId, resumed: true, ledgerIds: [], fileKey, parts: plan.length, entry };
91
61
  }
92
62
  }
93
63
  // ── the file's secrets: from the record if one exists, otherwise made now ──
94
- const secrets = openSecrets(input, fileKey, plan.length, contentDigest);
64
+ const secrets = await openSecrets(input, fileKey, plan.length, contentDigest);
95
65
  try {
96
66
  const entry = {
97
67
  name: input.name,
@@ -103,7 +73,7 @@ export async function uploadFile(input) {
103
73
  const paid = [];
104
74
  for (const range of plan) {
105
75
  const key = partKey(fileKey, range.partIndex);
106
- const stored = readReservationRecord(key);
76
+ const stored = await readReservationRecord(key);
107
77
  // ⛔ A PART THAT IS WRITTEN DOWN IS NEVER SEALED AGAIN. Its bytes are a particular sealing
108
78
  // the treasury may already have paid to register; a fresh one is a different blob.
109
79
  // ⛔ THE LAST PART ONLY. Every reader recovers the parts' real lengths from the file's size
@@ -119,7 +89,7 @@ export async function uploadFile(input) {
119
89
  : range.length;
120
90
  const sealed = stored === null
121
91
  ? await sealPartOf(input, secrets.dek, range, plan.length, sealFrom)
122
- : readReservationBytes(key);
92
+ : await readReservationBytes(key);
123
93
  paid.push(await (input.buy ?? buyAndPushPart)({
124
94
  api: input.api,
125
95
  protocol: input.protocol,
@@ -162,12 +132,12 @@ export function partKeysOf(fileKey, parts) {
162
132
  }
163
133
  /** Read the whole file once, feeding the key hash and returning the content digest. */
164
134
  async function hashWhole(source, keyHash) {
165
- const content = createHash("sha256");
135
+ const content = sha256.create();
166
136
  for await (const chunk of source.read(0, source.size)) {
167
137
  keyHash.update(chunk);
168
138
  content.update(chunk);
169
139
  }
170
- return new Uint8Array(content.digest());
140
+ return content.digest();
171
141
  }
172
142
  /**
173
143
  * The file's key and sealed content hash — from a written-down part if there is one.
@@ -176,12 +146,12 @@ async function hashWhole(source, keyHash) {
176
146
  * the bytes already on the network cannot be re-sealed. Unwrapping the recorded one is what
177
147
  * makes the parts still to come belong to the same file.
178
148
  */
179
- function openSecrets(input, fileKey, parts, contentDigest) {
149
+ async function openSecrets(input, fileKey, parts, contentDigest) {
180
150
  for (const key of partKeysOf(fileKey, parts)) {
181
- const record = readReservationRecord(key);
151
+ const record = await readReservationRecord(key);
182
152
  if (record === null)
183
153
  continue;
184
- const wrapped = new Uint8Array(Buffer.from(record.dekWrapped, "base64url"));
154
+ const wrapped = fromBase64Url(record.dekWrapped);
185
155
  const dek = input.crypt.envelope_open(input.dataKey, new TextEncoder().encode(AAD.dekWrap), wrapped);
186
156
  return { dek, dekWrapped: record.dekWrapped, contentHashCt: record.contentHashCt };
187
157
  }
@@ -226,9 +196,9 @@ export { entryOf };
226
196
  * sealed with. A run that wrote its own freshly generated one into the list would produce a
227
197
  * file that is paid for, present, correctly named and impossible to open.
228
198
  */
229
- function recordedEntry(fileKey, parts, size) {
199
+ async function recordedEntry(fileKey, parts, size) {
230
200
  for (const key of partKeysOf(fileKey, parts)) {
231
- const record = readReservationRecord(key);
201
+ const record = await readReservationRecord(key);
232
202
  if (record === null)
233
203
  continue;
234
204
  return {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * How big the local file is, with the two refusals that are worth their own words.
3
+ *
4
+ * ⛔ IT IS NOT READ HERE. A file large enough to need several parts is a file too large to hold,
5
+ * and the size is all that is needed to plan the upload and quote its price. The bytes are read
6
+ * later, a slice at a time, by the part that is being sealed.
7
+ */
8
+ export declare function measureLocal(path: string): number;
@@ -0,0 +1,37 @@
1
+ // How big a file on THIS MACHINE is — the one thing the upload price needs that a browser has no
2
+ // equivalent of.
3
+ //
4
+ // ⛔ THE ARITHMETIC IS NOT HERE. `upload-price.ts` turns a size into parts and a price and runs
5
+ // anywhere; this is the one call that asks a disk, and it is separate so that the price is
6
+ // reachable from a page where bytes arrive as a `Blob` and their length is already known.
7
+ import { statSync } from "node:fs";
8
+ import { NmtsError } from "./errors.js";
9
+ /**
10
+ * How big the local file is, with the two refusals that are worth their own words.
11
+ *
12
+ * ⛔ IT IS NOT READ HERE. A file large enough to need several parts is a file too large to hold,
13
+ * and the size is all that is needed to plan the upload and quote its price. The bytes are read
14
+ * later, a slice at a time, by the part that is being sealed.
15
+ */
16
+ export function measureLocal(path) {
17
+ let stat;
18
+ try {
19
+ stat = statSync(path);
20
+ }
21
+ catch {
22
+ throw new NmtsError(`There is no file at ${path}.`, { exitCode: 4 });
23
+ }
24
+ if (stat.isDirectory()) {
25
+ throw new NmtsError(`${path} is a folder.`, {
26
+ exitCode: 4,
27
+ nextStep: "This version uploads one file at a time.",
28
+ });
29
+ }
30
+ if (stat.size === 0) {
31
+ throw new NmtsError(`${path} is empty.`, {
32
+ exitCode: 4,
33
+ nextStep: "The storage network has nothing to store and would refuse the reservation.",
34
+ });
35
+ }
36
+ return stat.size;
37
+ }
@@ -18,14 +18,6 @@ export declare const CREDIT_BYTES: number;
18
18
  * shows afterwards. The server is still the authority; this is a quote, not a promise.
19
19
  */
20
20
  export declare function creditsFor(sealedBytes: number, epochs?: number): number;
21
- /**
22
- * How big the local file is, with the two refusals that are worth their own words.
23
- *
24
- * ⛔ IT IS NOT READ HERE. A file large enough to need several parts is a file too large to hold,
25
- * and the size is all that is needed to plan the upload and quote its price. The bytes are read
26
- * later, a slice at a time, by the part that is being sealed.
27
- */
28
- export declare function measureLocal(path: string): number;
29
21
  /**
30
22
  * How much of the file goes into one part.
31
23
  *
@@ -1,10 +1,10 @@
1
1
  // What an upload will cost, and how the file is cut up to pay for it.
2
2
  //
3
3
  // ⛔ SPLIT OUT OF `put.ts` SO THAT FILE STAYS READABLE IN ONE SITTING. Everything here is
4
- // arithmetic over numbers — no network, no crypto, no disk beyond one `stat` — which is what
5
- // lets `--dry-run` answer without reading a very large file, and what lets a test drive the
6
- // price without an account.
7
- import { statSync } from "node:fs";
4
+ // arithmetic over numbers — no network, no crypto and no disk at all — which is what lets
5
+ // `--dry-run` answer without reading a very large file, and what lets a test drive the price
6
+ // without an account. Measuring a file on this machine is Node's, and is in
7
+ // `upload-price-node.ts`.
8
8
  import { NmtsError } from "./errors.js";
9
9
  import { DEFAULT_PART_BYTES, NCF3_SHAPE, sealedLenFor } from "./seal.js";
10
10
  import { paddedPlaintextLen } from "./shared/lib/crypto/size-padding.js";
@@ -30,35 +30,6 @@ export function creditsFor(sealedBytes, epochs = UPLOAD_EPOCHS) {
30
30
  const mib = Math.ceil(sealedBytes / CREDIT_BYTES);
31
31
  return Math.ceil((mib * epochs) / UPLOAD_EPOCHS);
32
32
  }
33
- /**
34
- * How big the local file is, with the two refusals that are worth their own words.
35
- *
36
- * ⛔ IT IS NOT READ HERE. A file large enough to need several parts is a file too large to hold,
37
- * and the size is all that is needed to plan the upload and quote its price. The bytes are read
38
- * later, a slice at a time, by the part that is being sealed.
39
- */
40
- export function measureLocal(path) {
41
- let stat;
42
- try {
43
- stat = statSync(path);
44
- }
45
- catch {
46
- throw new NmtsError(`There is no file at ${path}.`, { exitCode: 4 });
47
- }
48
- if (stat.isDirectory()) {
49
- throw new NmtsError(`${path} is a folder.`, {
50
- exitCode: 4,
51
- nextStep: "This version uploads one file at a time.",
52
- });
53
- }
54
- if (stat.size === 0) {
55
- throw new NmtsError(`${path} is empty.`, {
56
- exitCode: 4,
57
- nextStep: "The storage network has nothing to store and would refuse the reservation.",
58
- });
59
- }
60
- return stat.size;
61
- }
62
33
  /**
63
34
  * How much of the file goes into one part.
64
35
  *
@@ -70,14 +70,14 @@ export async function commitItem(input, fileKey, parts) {
70
70
  // Advisory only — the chain is the authority on a blob's life. 0 when this machine could not
71
71
  // read the epoch clock: a number we do not have is not a number to invent.
72
72
  const expiryEpoch = input.currentEpoch === null ? 0 : input.currentEpoch + input.epochs;
73
- const previous = readItemRecord(fileKey);
73
+ const previous = await readItemRecord(fileKey);
74
74
  // ⛔ ALREADY COMMITTED IS NOT COMMITTED AGAIN. The record outlives the commit precisely so a run
75
75
  // that died before writing the file list does not make a second file out of storage that is
76
76
  // already named.
77
77
  if (previous?.itemId !== undefined)
78
78
  return previous.itemId;
79
79
  const attempt = previous?.attempt ?? 0;
80
- writeItemRecord(fileKey, { attempt });
80
+ await writeItemRecord(fileKey, { attempt });
81
81
  let view;
82
82
  try {
83
83
  view = await input.api.createItem({
@@ -100,7 +100,7 @@ export async function commitItem(input, fileKey, parts) {
100
100
  // ⛔ Written down before returning: from here on the file EXISTS and is paid for, and the only
101
101
  // thing still missing is the account's own list. Losing the record now would make it
102
102
  // unreachable.
103
- writeItemRecord(fileKey, { attempt, itemId: view.id });
103
+ await writeItemRecord(fileKey, { attempt, itemId: view.id });
104
104
  return view.id;
105
105
  }
106
106
  /**
@@ -1,6 +1,6 @@
1
- import { type Hash } from "node:crypto";
2
- /** Where unfinished uploads live. */
3
- export declare function uploadsDir(): string;
1
+ import { sha256 } from "@noble/hashes/sha2.js";
2
+ /** The running hash a reservation key is built in. */
3
+ export type ReservationHash = ReturnType<typeof sha256.create>;
4
4
  /**
5
5
  * A stable, account-scoped name for one file's upload attempt.
6
6
  *
@@ -36,9 +36,9 @@ export declare function reservationKeyStreamed(dataKey: Uint8Array, plaintextChu
36
36
  * the SHA-256 the account checks its contents against; handing the caller the running hash is
37
37
  * what lets both come out of a single pass instead of two reads of a very large file.
38
38
  */
39
- export declare function startReservationKey(dataKey: Uint8Array): Hash;
39
+ export declare function startReservationKey(dataKey: Uint8Array): ReservationHash;
40
40
  /** Finish it. The name and the destination go in last, exactly as the one-shot form does. */
41
- export declare function finishReservationKey(hash: Hash, name: string, destination: string): string;
41
+ export declare function finishReservationKey(hash: ReservationHash, name: string, destination: string): string;
42
42
  /**
43
43
  * The record name for ONE part of a file.
44
44
  *
@@ -131,7 +131,7 @@ export interface Reservation {
131
131
  * resume that only needs to commit still reads every byte of a very large upload off the disk.
132
132
  * They are fetched separately, by the one step that actually pushes them.
133
133
  */
134
- export declare function readReservationRecord(key: string): Reservation | null;
134
+ export declare function readReservationRecord(key: string): Promise<Reservation | null>;
135
135
  /**
136
136
  * The sealed bytes a reservation bought.
137
137
  *
@@ -139,14 +139,14 @@ export declare function readReservationRecord(key: string): Reservation | null;
139
139
  * different blob from the one the treasury registered — the relay refuses them, forever, and
140
140
  * the credits are gone.
141
141
  */
142
- export declare function readReservationBytes(key: string): Uint8Array;
142
+ export declare function readReservationBytes(key: string): Promise<Uint8Array>;
143
143
  /** The record and its bytes together, for the callers that need both. */
144
- export declare function readReservation(key: string): {
144
+ export declare function readReservation(key: string): Promise<{
145
145
  record: Reservation;
146
146
  sealed: Uint8Array;
147
- } | null;
147
+ } | null>;
148
148
  /** Write the record and its sealed bytes. Called BEFORE the reserve, and again after it answers. */
149
- export declare function writeReservation(key: string, record: Reservation, sealed: Uint8Array): void;
149
+ export declare function writeReservation(key: string, record: Reservation, sealed: Uint8Array): Promise<void>;
150
150
  /**
151
151
  * Forget a reservation.
152
152
  *
@@ -154,7 +154,7 @@ export declare function writeReservation(key: string, record: Reservation, seale
154
154
  * upload already succeeded" was not true of every caller. What IS true of all of them is that
155
155
  * nothing further depends on the record, which is why it never throws.
156
156
  */
157
- export declare function clearReservation(key: string): void;
157
+ export declare function clearReservation(key: string): Promise<void>;
158
158
  /**
159
159
  * The FILE-level half of an unfinished upload: what happened after every part was paid for.
160
160
  *
@@ -175,8 +175,8 @@ export interface ItemRecord {
175
175
  attempt: number;
176
176
  }
177
177
  /** What is known about this file's commit, or `null` when it has not been attempted. */
178
- export declare function readItemRecord(fileKey: string): ItemRecord | null;
178
+ export declare function readItemRecord(fileKey: string): Promise<ItemRecord | null>;
179
179
  /** Write the file-level record. Called before the commit, and again once it has an id. */
180
- export declare function writeItemRecord(fileKey: string, record: ItemRecord): void;
180
+ export declare function writeItemRecord(fileKey: string, record: ItemRecord): Promise<void>;
181
181
  /** Forget the file-level record. Never throws, for the same reason `clearReservation` does not. */
182
- export declare function clearItemRecord(fileKey: string): void;
182
+ export declare function clearItemRecord(fileKey: string): Promise<void>;