@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.
Files changed (156) hide show
  1. package/AGENTS.md +11 -12
  2. package/CHANGELOG.md +43 -0
  3. package/README.ko.md +33 -35
  4. package/README.md +21 -22
  5. package/dist/account.js +2 -1
  6. package/dist/arg-options.d.ts +169 -0
  7. package/dist/arg-options.js +10 -0
  8. package/dist/args.d.ts +2 -157
  9. package/dist/args.js +2 -0
  10. package/dist/artifact-about.d.ts +1 -1
  11. package/dist/autonomy.d.ts +3 -10
  12. package/dist/autonomy.js +15 -17
  13. package/dist/bytes.d.ts +15 -0
  14. package/dist/bytes.js +63 -0
  15. package/dist/collision.d.ts +5 -5
  16. package/dist/collision.js +19 -22
  17. package/dist/commands/delete-account.js +1 -1
  18. package/dist/commands/erase.js +1 -1
  19. package/dist/commands/extend.d.ts +2 -0
  20. package/dist/commands/extend.js +8 -1
  21. package/dist/commands/get.js +1 -1
  22. package/dist/commands/listfile.js +1 -1
  23. package/dist/commands/mcp.js +5 -1
  24. package/dist/commands/mode.js +2 -2
  25. package/dist/commands/on-collision.d.ts +1 -1
  26. package/dist/commands/on-collision.js +3 -3
  27. package/dist/commands/platform.d.ts +9 -0
  28. package/dist/commands/platform.js +124 -0
  29. package/dist/commands/pull.js +1 -1
  30. package/dist/commands/push-wallet.js +8 -1
  31. package/dist/commands/push.d.ts +2 -0
  32. package/dist/commands/push.js +6 -4
  33. package/dist/commands/put-payer.d.ts +8 -0
  34. package/dist/commands/put-payer.js +41 -0
  35. package/dist/commands/put-wallet.d.ts +9 -3
  36. package/dist/commands/put-wallet.js +108 -186
  37. package/dist/commands/put.d.ts +5 -7
  38. package/dist/commands/put.js +11 -31
  39. package/dist/commands/receive.js +1 -1
  40. package/dist/commands/settings.js +1 -1
  41. package/dist/commands/support.js +3 -3
  42. package/dist/commands/unlock.js +2 -2
  43. package/dist/commands/wallet-donate.d.ts +3 -0
  44. package/dist/commands/wallet-donate.js +6 -2
  45. package/dist/commands/wallet-hall.d.ts +4 -1
  46. package/dist/commands/wallet-hall.js +16 -8
  47. package/dist/commands/wallet-list.d.ts +18 -0
  48. package/dist/commands/wallet-list.js +116 -0
  49. package/dist/commands/wallet-send.d.ts +4 -0
  50. package/dist/commands/wallet-send.js +6 -2
  51. package/dist/commands/wallet-storage-ops.d.ts +4 -1
  52. package/dist/commands/wallet-storage-ops.js +7 -2
  53. package/dist/commands/wallet-swap.d.ts +3 -0
  54. package/dist/commands/wallet-swap.js +8 -4
  55. package/dist/commands/wallet-use.d.ts +7 -0
  56. package/dist/commands/wallet-use.js +55 -0
  57. package/dist/commands/wallet.d.ts +4 -0
  58. package/dist/commands/wallet.js +20 -8
  59. package/dist/crypto.d.ts +8 -5
  60. package/dist/crypto.js +15 -61
  61. package/dist/download-sink-node.d.ts +49 -0
  62. package/dist/download-sink-node.js +204 -0
  63. package/dist/download-sink.d.ts +0 -48
  64. package/dist/download-sink.js +6 -199
  65. package/dist/download.js +5 -4
  66. package/dist/engine-node.d.ts +19 -0
  67. package/dist/engine-node.js +77 -0
  68. package/dist/env-vars.d.ts +20 -0
  69. package/dist/env-vars.js +27 -0
  70. package/dist/extend-plan.d.ts +2 -0
  71. package/dist/gate.js +1 -1
  72. package/dist/help.js +12 -5
  73. package/dist/host-contract.d.ts +7 -0
  74. package/dist/host-contract.js +95 -0
  75. package/dist/host-node.d.ts +27 -0
  76. package/dist/host-node.js +192 -0
  77. package/dist/host.d.ts +99 -0
  78. package/dist/host.js +59 -0
  79. package/dist/index.d.ts +6 -40
  80. package/dist/index.js +20 -32
  81. package/dist/main.js +11 -10
  82. package/dist/manifest-chunk-cache.d.ts +3 -3
  83. package/dist/manifest-chunk-cache.js +39 -44
  84. package/dist/manifest-chunk-flow.js +15 -14
  85. package/dist/manifest-write.js +1 -1
  86. package/dist/manifest.d.ts +2 -2
  87. package/dist/manifest.js +37 -34
  88. package/dist/mcp-tools/tiers.js +1 -1
  89. package/dist/network.d.ts +1 -1
  90. package/dist/network.js +6 -2
  91. package/dist/platform-sign.d.ts +143 -0
  92. package/dist/platform-sign.js +220 -0
  93. package/dist/portable.d.ts +59 -0
  94. package/dist/portable.js +82 -0
  95. package/dist/product.d.ts +1 -1
  96. package/dist/product.js +1 -1
  97. package/dist/progress-node.d.ts +2 -0
  98. package/dist/progress-node.js +14 -0
  99. package/dist/progress.d.ts +0 -1
  100. package/dist/progress.js +4 -8
  101. package/dist/redact.js +3 -2
  102. package/dist/registration.js +5 -2
  103. package/dist/risk.d.ts +11 -0
  104. package/dist/risk.js +16 -0
  105. package/dist/run-log.d.ts +4 -4
  106. package/dist/run-log.js +29 -36
  107. package/dist/seal.js +5 -4
  108. package/dist/server.d.ts +1 -1
  109. package/dist/server.js +6 -2
  110. package/dist/setup-questions.js +2 -2
  111. package/dist/shared/lib/drive/manifest-ops.d.ts +8 -30
  112. package/dist/shared/lib/drive/manifest-ops.js +7 -39
  113. package/dist/shared/lib/drive/manifest-settings-patch.d.ts +52 -0
  114. package/dist/shared/lib/drive/manifest-settings-patch.js +110 -0
  115. package/dist/shared/lib/drive/manifest-settings.d.ts +55 -4
  116. package/dist/shared/lib/drive/manifest-settings.js +66 -28
  117. package/dist/shared/lib/wallet/discover.d.ts +54 -0
  118. package/dist/shared/lib/wallet/discover.js +66 -0
  119. package/dist/standing-tip.d.ts +3 -0
  120. package/dist/standing-tip.js +1 -0
  121. package/dist/upload-file-node.d.ts +3 -0
  122. package/dist/upload-file-node.js +38 -0
  123. package/dist/upload-file.d.ts +2 -2
  124. package/dist/upload-file.js +15 -45
  125. package/dist/upload-price-node.d.ts +8 -0
  126. package/dist/upload-price-node.js +37 -0
  127. package/dist/upload-price.d.ts +0 -8
  128. package/dist/upload-price.js +4 -33
  129. package/dist/upload-steps.js +3 -3
  130. package/dist/upload-store.d.ts +14 -14
  131. package/dist/upload-store.js +68 -75
  132. package/dist/upload-wallet-chain.js +3 -2
  133. package/dist/upload-wallet-put.d.ts +118 -0
  134. package/dist/upload-wallet-put.js +221 -0
  135. package/dist/upload-wallet.d.ts +3 -0
  136. package/dist/upload-wallet.js +18 -15
  137. package/dist/upload.js +16 -15
  138. package/dist/wallet-list-chain.d.ts +4 -0
  139. package/dist/wallet-list-chain.js +21 -0
  140. package/dist/wallet-pay-index.d.ts +21 -0
  141. package/dist/wallet-pay-index.js +68 -0
  142. package/dist/wallet-sign-seams.d.ts +69 -0
  143. package/dist/wallet-sign-seams.js +16 -0
  144. package/dist/wallet-sign.d.ts +3 -53
  145. package/dist/wallet-sign.js +22 -18
  146. package/dist/wallet.d.ts +13 -13
  147. package/dist/wallet.js +15 -15
  148. package/dist/walrus-write.js +3 -2
  149. package/dist/walrus.d.ts +1 -16
  150. package/dist/walrus.js +11 -22
  151. package/docs/commands/credits.md +1 -1
  152. package/docs/commands/platform.md +14 -0
  153. package/docs/commands/support.md +1 -1
  154. package/docs/commands/trash.md +1 -1
  155. package/docs/commands/wallet.md +26 -8
  156. package/package.json +13 -2
@@ -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>;
@@ -9,25 +9,21 @@
9
9
  //
10
10
  // So they are written down BEFORE the money moves. That ordering is the whole design.
11
11
  //
12
- // ⛔ WHAT IS ON DISK IS ALREADY PUBLIC. The `.bin` is the sealed NCF-3 stream — the exact bytes
13
- // about to be handed to a public storage network. It is still written 0600, because "already
14
- // public" is about the CONTENT and the file's presence would otherwise say which files this
15
- // account uploaded and when.
12
+ // ⛔ WHAT IS KEPT IS ALREADY PUBLIC. The `.bin` half is the sealed NCF-3 stream — the exact bytes
13
+ // about to be handed to a public storage network. The host still keeps it as privately as it
14
+ // can (0600 on this machine), because "already public" is about the CONTENT and the record's
15
+ // presence would otherwise say which files this account uploaded and when.
16
16
  //
17
- // ⛔ THE FILE NAME IS NOT A CONTENT FINGERPRINT. Keying by SHA-256 of the plaintext would leave a
18
- // directory of hashes matchable against published hash sets — the very thing sealing the
19
- // content hash avoids. The key mixes the account's data key in, so it identifies the file only
20
- // to somebody who already holds the account.
21
- import { createHash } from "node:crypto";
22
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
23
- import { join } from "node:path";
24
- import { configDir, modesAreEnforced } from "./credentials.js";
25
- import { chmodSync } from "node:fs";
17
+ // ⛔ THE KEY IS NOT A CONTENT FINGERPRINT. Keying by SHA-256 of the plaintext would leave a store
18
+ // of hashes matchable against published hash sets — the very thing sealing the content hash
19
+ // avoids. The key mixes the account's data key in, so it identifies the file only to somebody
20
+ // who already holds the account.
21
+ import { sha256 } from "@noble/hashes/sha2.js";
22
+ import { fromUtf8, toBase64Url, utf8 } from "./bytes.js";
26
23
  import { NmtsError } from "./errors.js";
27
- /** Where unfinished uploads live. */
28
- export function uploadsDir() {
29
- return join(configDir(), "uploads");
30
- }
24
+ import { host } from "./host.js";
25
+ /** The area of the host's state these records live in. One key per file below it. */
26
+ const AREA = "uploads";
31
27
  /**
32
28
  * A stable, account-scoped name for one file's upload attempt.
33
29
  *
@@ -71,12 +67,12 @@ export function reservationKeyStreamed(dataKey, plaintextChunks, name, destinati
71
67
  * what lets both come out of a single pass instead of two reads of a very large file.
72
68
  */
73
69
  export function startReservationKey(dataKey) {
74
- return createHash("sha256").update(dataKey);
70
+ return sha256.create().update(dataKey);
75
71
  }
76
72
  /** Finish it. The name and the destination go in last, exactly as the one-shot form does. */
77
73
  export function finishReservationKey(hash, name, destination) {
78
- const digest = hash.update(new TextEncoder().encode(`\u0000${name}\u0000${destination}`)).digest();
79
- return Buffer.from(digest).toString("base64url").slice(0, 32);
74
+ const digest = hash.update(utf8(`\u0000${name}\u0000${destination}`)).digest();
75
+ return toBase64Url(digest).slice(0, 32);
80
76
  }
81
77
  /**
82
78
  * The record name for ONE part of a file.
@@ -89,9 +85,9 @@ export function finishReservationKey(hash, name, destination) {
89
85
  export function partKey(fileKey, partIndex) {
90
86
  return `${fileKey}~p${partIndex}`;
91
87
  }
92
- function paths(key) {
93
- const dir = uploadsDir();
94
- return { json: join(dir, `${key}.json`), bin: join(dir, `${key}.bin`) };
88
+ /** The two keys one part's reservation occupies: its record, and the sealed bytes it bought. */
89
+ function keysFor(key) {
90
+ return { json: `${AREA}/${key}.json`, bin: `${AREA}/${key}.bin` };
95
91
  }
96
92
  function isReservation(value) {
97
93
  if (typeof value !== "object" || value === null)
@@ -114,23 +110,25 @@ function isReservation(value) {
114
110
  * resume that only needs to commit still reads every byte of a very large upload off the disk.
115
111
  * They are fetched separately, by the one step that actually pushes them.
116
112
  */
117
- export function readReservationRecord(key) {
118
- const { json, bin } = paths(key);
119
- if (!existsSync(json) || !existsSync(bin))
113
+ export async function readReservationRecord(key) {
114
+ const { json, bin } = keysFor(key);
115
+ const state = host().state;
116
+ const held = await state.read(json);
117
+ if (held === undefined || (await state.read(bin)) === undefined)
120
118
  return null;
121
119
  let parsed;
122
120
  try {
123
- parsed = JSON.parse(readFileSync(json, "utf8"));
121
+ parsed = JSON.parse(fromUtf8(held));
124
122
  }
125
123
  catch {
126
124
  // ⛔ Unreadable is not the same as absent, and treating it as absent would buy storage twice.
127
- throw new NmtsError(`An unfinished upload record at ${json} could not be read.`, {
125
+ throw new NmtsError(`The unfinished upload record ${json} could not be read.`, {
128
126
  nextStep: "It names storage this account may already have paid for. Move it aside rather than " +
129
127
  "deleting it if the upload matters, then try again.",
130
128
  });
131
129
  }
132
130
  if (!isReservation(parsed)) {
133
- throw new NmtsError(`The unfinished upload record at ${json} is not in a shape this version knows.`, {
131
+ throw new NmtsError(`The unfinished upload record ${json} is not in a shape this version knows.`, {
134
132
  nextStep: "Move it aside and try again. Nothing was sent.",
135
133
  });
136
134
  }
@@ -143,38 +141,36 @@ export function readReservationRecord(key) {
143
141
  * different blob from the one the treasury registered — the relay refuses them, forever, and
144
142
  * the credits are gone.
145
143
  */
146
- export function readReservationBytes(key) {
147
- const { bin } = paths(key);
148
- return new Uint8Array(readFileSync(bin));
144
+ export async function readReservationBytes(key) {
145
+ const { bin } = keysFor(key);
146
+ const held = await host().state.read(bin);
147
+ if (held === undefined) {
148
+ throw new NmtsError(`The sealed bytes of ${bin} are gone.`, {
149
+ nextStep: "Nothing was sent. The storage they bought cannot be filled by re-sealing — run the " +
150
+ "upload again to buy storage for a fresh sealing of the same file.",
151
+ });
152
+ }
153
+ return held;
149
154
  }
150
155
  /** The record and its bytes together, for the callers that need both. */
151
- export function readReservation(key) {
152
- const record = readReservationRecord(key);
156
+ export async function readReservation(key) {
157
+ const record = await readReservationRecord(key);
153
158
  if (record === null)
154
159
  return null;
155
- return { record, sealed: readReservationBytes(key) };
160
+ return { record, sealed: await readReservationBytes(key) };
156
161
  }
157
162
  /** Write the record and its sealed bytes. Called BEFORE the reserve, and again after it answers. */
158
- export function writeReservation(key, record, sealed) {
159
- const dir = uploadsDir();
160
- mkdirSync(dir, { recursive: true, mode: 0o700 });
161
- if (modesAreEnforced())
162
- chmodSync(dir, 0o700);
163
- const { json, bin } = paths(key);
164
- // WRITTEN ASIDE AND RENAMED OVER, never truncated in place. A rename within one directory is
165
- // atomic, so a reader sees the old record or the new one and never a half-written one. The
166
- // write that matters is the LAST one — the one that adds the item id — because a truncated
167
- // file there is the only local pointer to a file that is already paid for and committed, and
168
- // losing it makes that file invisible.
169
- atomically(bin, sealed);
170
- atomically(json, Buffer.from(`${JSON.stringify(record, null, 2)}\n`, "utf8"));
171
- }
172
- function atomically(target, bytes) {
173
- const scratch = `${target}.${process.pid}.tmp`;
174
- writeFileSync(scratch, bytes, { mode: 0o600 });
175
- if (modesAreEnforced())
176
- chmodSync(scratch, 0o600);
177
- renameSync(scratch, target);
163
+ export async function writeReservation(key, record, sealed) {
164
+ const { json, bin } = keysFor(key);
165
+ // THE BYTES GO DOWN FIRST, AND THE RECORD IS WHAT MAKES THE PAIR COUNT. A reader treats a
166
+ // record without its bytes as no reservation at all, so a run that stopped between these two
167
+ // writes leaves nothing that can be resumed — rather than a record pointing at bytes that are
168
+ // not there. Each write on its own is all-or-nothing; that is the host's promise, and the
169
+ // write that matters most is the LAST one, because a half-written record is the only local
170
+ // pointer to a file that is already paid for.
171
+ const state = host().state;
172
+ await state.write(bin, sealed);
173
+ await state.write(json, utf8(`${JSON.stringify(record, null, 2)}\n`));
178
174
  }
179
175
  /**
180
176
  * Forget a reservation.
@@ -183,11 +179,11 @@ function atomically(target, bytes) {
183
179
  * upload already succeeded" was not true of every caller. What IS true of all of them is that
184
180
  * nothing further depends on the record, which is why it never throws.
185
181
  */
186
- export function clearReservation(key) {
187
- const { json, bin } = paths(key);
188
- for (const path of [json, bin]) {
182
+ export async function clearReservation(key) {
183
+ const { json, bin } = keysFor(key);
184
+ for (const name of [json, bin]) {
189
185
  try {
190
- rmSync(path, { force: true });
186
+ await host().state.remove(name);
191
187
  }
192
188
  catch {
193
189
  // ⚠ A record that cannot be removed is left where it is. That is not free -- a record
@@ -197,8 +193,8 @@ export function clearReservation(key) {
197
193
  }
198
194
  }
199
195
  }
200
- function itemPath(fileKey) {
201
- return join(uploadsDir(), `${fileKey}.item.json`);
196
+ function itemKey(fileKey) {
197
+ return `${AREA}/${fileKey}.item.json`;
202
198
  }
203
199
  function isItemRecord(value) {
204
200
  if (typeof value !== "object" || value === null)
@@ -209,41 +205,38 @@ function isItemRecord(value) {
209
205
  return id === undefined || typeof id === "string";
210
206
  }
211
207
  /** What is known about this file's commit, or `null` when it has not been attempted. */
212
- export function readItemRecord(fileKey) {
213
- const path = itemPath(fileKey);
214
- if (!existsSync(path))
208
+ export async function readItemRecord(fileKey) {
209
+ const path = itemKey(fileKey);
210
+ const held = await host().state.read(path);
211
+ if (held === undefined)
215
212
  return null;
216
213
  let parsed;
217
214
  try {
218
- parsed = JSON.parse(readFileSync(path, "utf8"));
215
+ parsed = JSON.parse(fromUtf8(held));
219
216
  }
220
217
  catch {
221
218
  // ⛔ Same reasoning as an unreadable reservation: unreadable is not absent. This file is the
222
219
  // only local pointer to storage that is already bought and possibly already committed.
223
- throw new NmtsError(`An unfinished upload record at ${path} could not be read.`, {
220
+ throw new NmtsError(`The unfinished upload record ${path} could not be read.`, {
224
221
  nextStep: "It names a file this account may already have paid for. Move it aside rather than " +
225
222
  "deleting it if the upload matters, then try again.",
226
223
  });
227
224
  }
228
225
  if (!isItemRecord(parsed)) {
229
- throw new NmtsError(`The unfinished upload record at ${path} is not in a shape this version knows.`, {
226
+ throw new NmtsError(`The unfinished upload record ${path} is not in a shape this version knows.`, {
230
227
  nextStep: "Move it aside and try again. Nothing was sent.",
231
228
  });
232
229
  }
233
230
  return parsed;
234
231
  }
235
232
  /** Write the file-level record. Called before the commit, and again once it has an id. */
236
- export function writeItemRecord(fileKey, record) {
237
- const dir = uploadsDir();
238
- mkdirSync(dir, { recursive: true, mode: 0o700 });
239
- if (modesAreEnforced())
240
- chmodSync(dir, 0o700);
241
- atomically(itemPath(fileKey), Buffer.from(`${JSON.stringify(record, null, 2)}\n`, "utf8"));
233
+ export async function writeItemRecord(fileKey, record) {
234
+ await host().state.write(itemKey(fileKey), utf8(`${JSON.stringify(record, null, 2)}\n`));
242
235
  }
243
236
  /** Forget the file-level record. Never throws, for the same reason `clearReservation` does not. */
244
- export function clearItemRecord(fileKey) {
237
+ export async function clearItemRecord(fileKey) {
245
238
  try {
246
- rmSync(itemPath(fileKey), { force: true });
239
+ await host().state.remove(itemKey(fileKey));
247
240
  }
248
241
  catch {
249
242
  // ⚠ Left where it is. A stale one carrying an item id would be resumed rather than
@@ -21,6 +21,7 @@ import { bcs } from "@mysten/sui/bcs";
21
21
  import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
22
22
  import { Transaction } from "@mysten/sui/transactions";
23
23
  import { blobIdFromInt, blobIdToInt, MAINNET_WALRUS_PACKAGE_CONFIG, TESTNET_WALRUS_PACKAGE_CONFIG, walrus, } from "@mysten/walrus";
24
+ import { fromBase64Url } from "./bytes.js";
24
25
  import { NmtsError } from "./errors.js";
25
26
  import { extendReads, netGasFee, readBlobLease } from "./extend-chain.js";
26
27
  import { readOwnedStorage, readStorageType } from "./shared/lib/storage-control/chain.js";
@@ -104,8 +105,8 @@ export function certifyTransaction(client, input) {
104
105
  deletable: true,
105
106
  certificate: {
106
107
  signers: input.certificate.signers,
107
- serializedMessage: new Uint8Array(Buffer.from(input.certificate.serialized_message_b64, "base64url")),
108
- signature: new Uint8Array(Buffer.from(input.certificate.signature_b64, "base64url")),
108
+ serializedMessage: fromBase64Url(input.certificate.serialized_message_b64),
109
+ signature: fromBase64Url(input.certificate.signature_b64),
109
110
  },
110
111
  });
111
112
  }
@@ -0,0 +1,118 @@
1
+ import type { OnCollision } from "./collision.ts";
2
+ import { type CryptoGlue } from "./crypto.ts";
3
+ import type { Network } from "./network.ts";
4
+ import type { PaddingRule } from "./shared/lib/crypto/size-padding.ts";
5
+ import { type FileUploadStep, type PlaintextSource } from "./upload-file.ts";
6
+ import { type StorageChoice, type UploadBudget, type WalletUploadReads } from "./upload-wallet-plan.ts";
7
+ import type { BlobProtocol, UploadApi } from "./upload-wire.ts";
8
+ import type { Spend } from "./wallet-grant.ts";
9
+ import type { SignBlobCertify, SignBlobRegister } from "./wallet-sign.ts";
10
+ /** The account this upload belongs to, and how it seals. */
11
+ export interface WalletPutContext {
12
+ /** ⛔ The NMTS key. It seals the file AND derives the wallet that pays. It is never written down. */
13
+ code: string;
14
+ apiKey: string;
15
+ server: string;
16
+ network: Network;
17
+ accountId: string;
18
+ crypt: CryptoGlue;
19
+ /** How much of the file goes into one part. */
20
+ partSize: number;
21
+ /** The rounding rule from the account's sealed list — it changes the stored size, so the price. */
22
+ rule: PaddingRule;
23
+ /** What to do about a name already in use. Absent = the machine's setting, as `addEntry` reads it. */
24
+ onCollision?: OnCollision | undefined;
25
+ /**
26
+ * Which of this key's wallets pays, by index (0 = the first one).
27
+ *
28
+ * ⛔ THE CALLER RESOLVES IT, BEFORE ANYTHING IS PRICED. It is the account's own number, kept in
29
+ * the sealed file list (`wallet-pay-index.ts`), and a library that guessed at it here would
30
+ * price one address and sign with another.
31
+ */
32
+ wallet: number;
33
+ }
34
+ /** One file and where it goes — already resolved, because a library resolves nothing by asking. */
35
+ export interface WalletPutFile {
36
+ source: PlaintextSource;
37
+ name: string;
38
+ parentId: string | null;
39
+ /** The destination AS TYPED — part of the reservation key, so both runs of a resume agree. */
40
+ destination: string;
41
+ }
42
+ /** The chain, the signatures, the wire and the four things a caller may be told. */
43
+ export interface WalletPutSeams {
44
+ /** How many of the storage network's epochs to buy. Default `DEFAULT_UPLOAD_EPOCHS`. */
45
+ epochs?: string | number | undefined;
46
+ /** `fit`, `whole`, or a held resource's object id. Absent = buy new storage. */
47
+ storage?: string | undefined;
48
+ /** Stop at the review. Nothing is sealed, signed or sent, and no signer is even loaded. */
49
+ dryRun?: boolean | undefined;
50
+ /** ⚠ A SEAM, NOT AN OPTION — what the chain is asked. */
51
+ readChain?: ((network: Network, relayUrl: string) => WalletUploadReads | Promise<WalletUploadReads>) | undefined;
52
+ /** ⛔ SEPARATE FROM THE READS so a caller can prove the review stops before this. */
53
+ sign?: {
54
+ register: SignBlobRegister;
55
+ certify: SignBlobCertify;
56
+ } | undefined;
57
+ /** ⚠ SEAMS, NOT OPTIONS — the storage-network protocol and the server calls. */
58
+ protocol?: ((network: Network, bodyBytes: number, onSent: (sent: number, total: number) => void) => BlobProtocol & {
59
+ relayUrl: string;
60
+ }) | undefined;
61
+ api?: UploadApi | undefined;
62
+ /** Told as sealed bytes leave for the relay. */
63
+ onProgress?: ((sent: number, total: number) => void) | undefined;
64
+ /** Told about each step as it starts. */
65
+ onStep?: ((step: FileUploadStep) => void) | undefined;
66
+ /** Told the numbers the moment they are known — before the dry run returns and before a refusal. */
67
+ onReview?: ((review: WalletPutReview) => void) | undefined;
68
+ /** ⛔ THE GATE. Throwing here stops the upload with nothing signed. See the module header. */
69
+ agree?: ((review: WalletPutReview) => void) | undefined;
70
+ /** Told what left the wallet, after each signature. */
71
+ onSpend?: ((spend: Spend) => void) | undefined;
72
+ }
73
+ /** What this upload would buy, in the order somebody deciding needs it. Base units throughout. */
74
+ export interface WalletPutReview {
75
+ name: string;
76
+ /** Plaintext bytes. */
77
+ bytes: number;
78
+ /** Bytes the storage network holds, padding and sealing included. */
79
+ sealedBytes: number;
80
+ parts: number;
81
+ epochs: number;
82
+ /** The term in days, read from the network's own clock. */
83
+ days: string;
84
+ /** The epoch the storage runs to. */
85
+ endEpoch: number;
86
+ /** The relay's tip over every part, in MIST. Paid inside the register signatures. */
87
+ tipMist: bigint;
88
+ /** Where the storage comes from. */
89
+ storage: StorageChoice;
90
+ /** Free storage resources the wallet holds when none was asked for; null when unreadable. */
91
+ heldResources: number | null;
92
+ /** The wallet against the price: what is needed, what is held, and the shortfall if there is one. */
93
+ budget: UploadBudget;
94
+ }
95
+ /** A dry run's answer, or a finished upload's. The review is in both, so both report one price. */
96
+ export type WalletPutOutcome = {
97
+ kind: "review";
98
+ review: WalletPutReview;
99
+ } | {
100
+ kind: "uploaded";
101
+ review: WalletPutReview;
102
+ itemId: string;
103
+ /** The name it got — numbered if the one asked for was taken. */
104
+ savedAs: string;
105
+ /** The id of the file this one displaced into the trash, when the machine overwrites. */
106
+ replaced: string | null;
107
+ /** The file-list version this write produced. */
108
+ fileListVersion: number;
109
+ /** True when every part was already signed for by an earlier run: this call spent nothing. */
110
+ resumed: boolean;
111
+ };
112
+ /**
113
+ * Price, agree, sign, upload and record ONE file, paid from the wallet the NMTS key derives.
114
+ *
115
+ * ⛔ THE SIGNING MODULE IS LOADED ONLY AFTER `agree` HAS RETURNED. A dry run and a refusal never
116
+ * bring the code that can spend into memory.
117
+ */
118
+ export declare function walletPut(ctx: WalletPutContext, file: WalletPutFile, seams?: WalletPutSeams): Promise<WalletPutOutcome>;