@needmoretruth/nmts-cli 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/AGENTS.md +591 -0
  2. package/LICENSE +202 -0
  3. package/LICENSING.md +49 -0
  4. package/README.ko.md +641 -0
  5. package/README.md +657 -0
  6. package/dist/account-proof.js +78 -0
  7. package/dist/account.js +75 -0
  8. package/dist/api-key.js +310 -0
  9. package/dist/api.js +271 -0
  10. package/dist/args.js +137 -0
  11. package/dist/artifact-about.js +70 -0
  12. package/dist/autonomy.js +98 -0
  13. package/dist/code-access.js +76 -0
  14. package/dist/code-vault.js +233 -0
  15. package/dist/collision.js +133 -0
  16. package/dist/commands/balance.js +93 -0
  17. package/dist/commands/consent.js +77 -0
  18. package/dist/commands/create.js +356 -0
  19. package/dist/commands/env.js +136 -0
  20. package/dist/commands/expiring.js +167 -0
  21. package/dist/commands/extend.js +236 -0
  22. package/dist/commands/get.js +130 -0
  23. package/dist/commands/kit.js +136 -0
  24. package/dist/commands/listfile.js +105 -0
  25. package/dist/commands/login.js +335 -0
  26. package/dist/commands/logout.js +33 -0
  27. package/dist/commands/ls.js +202 -0
  28. package/dist/commands/marks.js +176 -0
  29. package/dist/commands/mcp.js +139 -0
  30. package/dist/commands/mode.js +54 -0
  31. package/dist/commands/on-collision.js +45 -0
  32. package/dist/commands/organise.js +296 -0
  33. package/dist/commands/public-code.js +89 -0
  34. package/dist/commands/pull.js +185 -0
  35. package/dist/commands/push.js +278 -0
  36. package/dist/commands/put.js +282 -0
  37. package/dist/commands/rebuild.js +160 -0
  38. package/dist/commands/receive.js +125 -0
  39. package/dist/commands/recovery-list.js +104 -0
  40. package/dist/commands/recovery.js +291 -0
  41. package/dist/commands/s3.js +241 -0
  42. package/dist/commands/settings.js +28 -0
  43. package/dist/commands/share.js +276 -0
  44. package/dist/commands/sweep.js +240 -0
  45. package/dist/commands/trash.js +288 -0
  46. package/dist/commands/trial.js +264 -0
  47. package/dist/commands/update.js +169 -0
  48. package/dist/commands/usage.js +84 -0
  49. package/dist/commands/verify.js +331 -0
  50. package/dist/commands/wallet.js +122 -0
  51. package/dist/commands/whoami.js +33 -0
  52. package/dist/consent.js +200 -0
  53. package/dist/credentials.js +311 -0
  54. package/dist/crypto-surface.js +54 -0
  55. package/dist/crypto.js +137 -0
  56. package/dist/download-part.js +144 -0
  57. package/dist/download-sink.js +213 -0
  58. package/dist/download.js +163 -0
  59. package/dist/drive-paths.js +170 -0
  60. package/dist/environment.js +190 -0
  61. package/dist/errors.js +71 -0
  62. package/dist/exit.js +107 -0
  63. package/dist/expiry.js +115 -0
  64. package/dist/extend-chain.js +137 -0
  65. package/dist/extend-plan.js +149 -0
  66. package/dist/extend-sign.js +130 -0
  67. package/dist/guards.js +15 -0
  68. package/dist/help.js +192 -0
  69. package/dist/human-check.js +54 -0
  70. package/dist/item-trash.js +25 -0
  71. package/dist/kit-file.js +133 -0
  72. package/dist/list-file.js +71 -0
  73. package/dist/list-view-find.js +71 -0
  74. package/dist/list-view-order.js +96 -0
  75. package/dist/main.js +394 -0
  76. package/dist/manifest-create.js +81 -0
  77. package/dist/manifest-write.js +244 -0
  78. package/dist/manifest.js +213 -0
  79. package/dist/mark-render.js +45 -0
  80. package/dist/mcp-args.js +154 -0
  81. package/dist/mcp-tools/context.js +50 -0
  82. package/dist/mcp-tools/files.js +133 -0
  83. package/dist/mcp-tools/organise.js +128 -0
  84. package/dist/mcp-tools/reads.js +89 -0
  85. package/dist/mcp-tools/share.js +61 -0
  86. package/dist/mcp.js +142 -0
  87. package/dist/net-retry.js +84 -0
  88. package/dist/network.js +43 -0
  89. package/dist/notice.js +45 -0
  90. package/dist/product.js +38 -0
  91. package/dist/progress.js +110 -0
  92. package/dist/prompt.js +145 -0
  93. package/dist/rebuild.js +255 -0
  94. package/dist/recovery-assemble.js +148 -0
  95. package/dist/recovery-build.js +260 -0
  96. package/dist/recovery-map-file.js +114 -0
  97. package/dist/recovery-map.js +191 -0
  98. package/dist/recovery-release.js +229 -0
  99. package/dist/recovery-seq.js +63 -0
  100. package/dist/recovery-source.js +143 -0
  101. package/dist/registration.js +83 -0
  102. package/dist/s3/listing.js +134 -0
  103. package/dist/s3/multipart.js +104 -0
  104. package/dist/s3/response-sink.js +41 -0
  105. package/dist/s3/same-file.js +117 -0
  106. package/dist/s3/server.js +252 -0
  107. package/dist/s3/sigv4.js +168 -0
  108. package/dist/s3/staging.js +87 -0
  109. package/dist/s3/xml.js +80 -0
  110. package/dist/safe-path.js +89 -0
  111. package/dist/seal.js +206 -0
  112. package/dist/secret-reader.js +112 -0
  113. package/dist/server.js +33 -0
  114. package/dist/session.js +49 -0
  115. package/dist/setup-questions.js +31 -0
  116. package/dist/share.js +195 -0
  117. package/dist/shared/lib/crypto/size-padding.js +126 -0
  118. package/dist/shared/lib/drive/manifest-codec.js +287 -0
  119. package/dist/shared/lib/drive/manifest-index.js +235 -0
  120. package/dist/shared/lib/drive/manifest-ops.js +267 -0
  121. package/dist/shared/lib/drive/manifest-settings.js +65 -0
  122. package/dist/shared/lib/drive/name-conflict.js +121 -0
  123. package/dist/shared/lib/drive/unique-name.js +48 -0
  124. package/dist/shared/lib/extend/epochs.js +256 -0
  125. package/dist/shared/lib/net/retry-budget.js +95 -0
  126. package/dist/shared/lib/share/shared-file-info.js +65 -0
  127. package/dist/shared/lib/storage-network.js +64 -0
  128. package/dist/shared/lib/upload/part-plan.js +28 -0
  129. package/dist/stdout.js +109 -0
  130. package/dist/trash-sweep.js +123 -0
  131. package/dist/units.js +19 -0
  132. package/dist/update-check.js +164 -0
  133. package/dist/update-source.js +143 -0
  134. package/dist/upload-api.js +98 -0
  135. package/dist/upload-file.js +242 -0
  136. package/dist/upload-price.js +117 -0
  137. package/dist/upload-steps.js +111 -0
  138. package/dist/upload-store.js +252 -0
  139. package/dist/upload-wire.js +25 -0
  140. package/dist/upload.js +294 -0
  141. package/dist/usage-report.js +54 -0
  142. package/dist/wallet-chain.js +76 -0
  143. package/dist/wallet.js +209 -0
  144. package/dist/walrus-write.js +188 -0
  145. package/dist/walrus.js +156 -0
  146. package/package.json +56 -0
  147. package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +469 -0
  148. package/vendor/nmts-crypto/nmts_crypto_wasm.js +1300 -0
  149. package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
  150. package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +55 -0
  151. package/vendor/nmts-crypto/package.json +17 -0
@@ -0,0 +1,160 @@
1
+ // `nmts rebuild` — building a file list for an account that has none.
2
+ //
3
+ // ⛔ IT IS ITS OWN COMMAND, AND IT ASKS. Rebuilding WRITES: it seals a new file list and puts it on
4
+ // the server, and from then on that account's names are placeholders on every device. Doing
5
+ // that as a side effect of `ls` would mean somebody who pointed this tool at the wrong account
6
+ // — a mistyped variable, a second code on the same machine — would come back to an account that
7
+ // had been rebuilt without anybody deciding to. So it is a verb somebody types, and a run
8
+ // without `--yes` reports exactly what would happen and changes nothing.
9
+ //
10
+ // ⛔ AND IT IS NOT A ONCE-PER-MACHINE AGREEMENT. Those fit a capability — this machine may spend,
11
+ // this machine may hold the code unsealed. What is being decided here is not "may this tool
12
+ // ever rebuild" but "is THIS account, today, the one whose list is missing", and an answer
13
+ // given once for every future account on this machine is not an answer to that question.
14
+ //
15
+ // ⛔ TWO THINGS STOP IT FROM WRITING OVER A LIST, and they fail differently on purpose:
16
+ // · it reads the list first, and an account that has one is refused before the account is
17
+ // listed at all;
18
+ // · the write itself declares `base_seq: null`, which the server accepts only while no list
19
+ // exists — so a list that appears while this runs comes back as a refusal rather than as a
20
+ // replacement.
21
+ //
22
+ // ⚠ A THIRD CASE IS NOT A MISSING LIST AT ALL: this machine has a record of a list for this
23
+ // account and the server now says there is none. That is a list that WENT missing — the shape a
24
+ // server would take to make a device throw away its real names — so it stops, and `--force` is
25
+ // how somebody who knows their list was genuinely lost goes ahead anyway.
26
+ import { NmtsError } from "../errors.js";
27
+ import { createFirstList } from "../manifest-create.js";
28
+ import { readFileList } from "../manifest.js";
29
+ import { BINARY_NAME } from "../product.js";
30
+ import { rebuildFromServer } from "../rebuild.js";
31
+ import { openSession } from "../session.js";
32
+ import { TRASH_RETENTION_DAYS } from "../trash-sweep.js";
33
+ /** The machine-readable answer, printed once on whichever path the run takes. */
34
+ function summary(built, wrote, extra = {}) {
35
+ return JSON.stringify({
36
+ entries: built.entries.length,
37
+ live: built.live,
38
+ trashed: built.trashed,
39
+ keyless: built.keyless,
40
+ unaccounted: built.unaccounted,
41
+ namesRecovered: false,
42
+ foldersRecovered: false,
43
+ wrote,
44
+ ...extra,
45
+ });
46
+ }
47
+ export async function rebuild(options = {}) {
48
+ const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
49
+ const session = await openSession(options);
50
+ const current = await readFileList(session.server, session.apiKey, session.code, session.accountId);
51
+ if (current.manifest !== null) {
52
+ throw new NmtsError(`This account already has a file list (version ${current.seq ?? 0}).`, {
53
+ exitCode: 4,
54
+ nextStep: `Nothing was changed. Rebuilding is for an account whose list is missing; over an existing ` +
55
+ `one it would replace real names with placeholders. Run \`${BINARY_NAME} ls\` to see what ` +
56
+ `the list holds.`,
57
+ });
58
+ }
59
+ if (!current.firstTimeOnThisMachine && options.force !== true) {
60
+ throw new NmtsError(`This machine has seen a file list for this account, and the server now says there is none.`, {
61
+ exitCode: 4,
62
+ nextStep: `Nothing was changed. A list that was there and is not is different from one that never ` +
63
+ `existed: it is also what being shown an emptied account would look like. Open the account ` +
64
+ `in a browser and check before rebuilding. If the list really was lost, ` +
65
+ `\`${BINARY_NAME} rebuild --yes --force\` goes ahead.`,
66
+ });
67
+ }
68
+ // ⚠ A LARGE ACCOUNT IS TWO WHOLE LISTINGS, a hundred rows at a time, and a person watching a
69
+ // command that says nothing for a minute concludes it has hung. Every thousandth row is often
70
+ // enough to show movement and rare enough not to bury what comes after it. The machine-readable
71
+ // run stays silent: its output is one object, and a progress line on that stream would break it.
72
+ let announced = 0;
73
+ const built = await rebuildFromServer({
74
+ server: session.server,
75
+ apiKey: session.apiKey,
76
+ onProgress: (read) => {
77
+ if (options.json === true || read < announced + 1000)
78
+ return;
79
+ announced = read;
80
+ say(` read ${read} stored files so far...`);
81
+ },
82
+ });
83
+ if (built.entries.length === 0) {
84
+ if (options.json) {
85
+ say(summary(built, false));
86
+ return 0;
87
+ }
88
+ say(`This account has no file list, and the server holds no stored files for it either.`);
89
+ say(`There is nothing to rebuild from. Nothing was changed.`);
90
+ return 0;
91
+ }
92
+ if (options.yes !== true) {
93
+ if (options.json) {
94
+ say(summary(built, false));
95
+ return 5;
96
+ }
97
+ describe(say, built);
98
+ say(``);
99
+ say(`Nothing was changed. To go ahead: ${BINARY_NAME} rebuild --yes`);
100
+ say(`⛔ If a program is reading this on somebody's behalf: show it to them and let them decide.`);
101
+ return 5;
102
+ }
103
+ const written = await createFirstList(session, built.entries);
104
+ if (options.json) {
105
+ say(summary(built, true, { seq: written.seq }));
106
+ return 0;
107
+ }
108
+ say(`Wrote file list version ${written.seq}: ${built.entries.length} ` +
109
+ `entr${built.entries.length === 1 ? "y" : "ies"} (${built.live} live, ${built.trashed} in the trash).`);
110
+ say(``);
111
+ say(` Every file is at the top of the drive under a placeholder name — the server had no name,`);
112
+ say(` no folder and no placement to give back.`);
113
+ say(` \`${BINARY_NAME} ls\` shows them; \`${BINARY_NAME} rename\` and \`${BINARY_NAME} mv\` put them back.`);
114
+ reportGaps(say, built);
115
+ return 0;
116
+ }
117
+ /** What a rebuild would do, in the words somebody is about to decide on. */
118
+ function describe(say, built) {
119
+ say(`This account has no file list, and the server still holds ${built.entries.length} ` +
120
+ `stored file${built.entries.length === 1 ? "" : "s"} for it. The files are there; what is ` +
121
+ `missing is the list that names them.`);
122
+ say(``);
123
+ say(`A rebuild would recover:`);
124
+ say(` · ${built.live} live and ${built.trashed} in the trash — the trash keeps its remaining days`);
125
+ say(` · every file's key, without which its bytes could never be opened again`);
126
+ say(` · the sealed content hash, so a later download still checks itself`);
127
+ say(` · each file's real dates and its size`);
128
+ say(``);
129
+ say(`It cannot recover:`);
130
+ say(` · names — each file gets a placeholder such as \`recovered-a1b2c3d4\`, for you to type over`);
131
+ say(` · folders and where each file sat — the rebuilt drive is flat`);
132
+ say(` · stars, labels, share receipts and account settings`);
133
+ say(``);
134
+ say(` None of that is a fault: the server was built not to know it. It keeps a row per stored`);
135
+ say(` file and the key that opens it, and nothing about what the file is called.`);
136
+ reportGaps(say, built);
137
+ }
138
+ /** The parts of the account this rebuild could not account for. Printed on both paths. */
139
+ function reportGaps(say, built) {
140
+ if (built.keyless > 0) {
141
+ say(``);
142
+ say(` ⚠ ${built.keyless} of them carr${built.keyless === 1 ? "ies" : "y"} no key on the server.` +
143
+ ` ${built.keyless === 1 ? "It appears" : "They appear"} in the list and`);
144
+ say(` nothing can open ${built.keyless === 1 ? "it" : "them"} — the entry is still what says the file was there.`);
145
+ }
146
+ if (built.unaccounted === null) {
147
+ say(``);
148
+ say(` ⚠ The check that compares this against everything the server holds could not be`);
149
+ say(` finished, so this cannot say whether any row was left out.`);
150
+ return;
151
+ }
152
+ if (built.unaccounted > 0) {
153
+ say(``);
154
+ say(` ⚠ ${built.unaccounted} row${built.unaccounted === 1 ? "" : "s"} the server holds ` +
155
+ `${built.unaccounted === 1 ? "is" : "are"} not in this rebuild.`);
156
+ say(` The trash listing ends at ${TRASH_RETENTION_DAYS} days, so anything thrown away longer ago than that`);
157
+ say(` is in no listing this can read, and its key is not recoverable here. The bytes stay`);
158
+ say(` on the storage network until the term already paid for runs out.`);
159
+ }
160
+ }
@@ -0,0 +1,125 @@
1
+ // `nmts receive <id>` — download a file somebody shared with this account.
2
+ //
3
+ // ⛔ THE LENGTH COMES FROM THE SENDER, NOT FROM THE SERVER. The number the server holds is
4
+ // bytes-on-the-network: larger than the file by a fixed amount per stored piece, and larger
5
+ // again when the sender rounded the stored size up to hide the true one. Only the sender knows
6
+ // the real length, and they sealed it beside the name. Taking the server's number instead
7
+ // produces a file with padding written into the end of it, and the hash check then refuses a
8
+ // download that was otherwise perfect.
9
+ //
10
+ // ⛔ AND THE HASH IS THE SENDER'S TOO, sealed under the file's own key. The recipient holds that
11
+ // key and nothing else of the sender's, which is exactly why the check works at all: a storage
12
+ // network that returned different bytes cannot produce a matching hash without the key.
13
+ import { resolve } from "node:path";
14
+ import { request } from "../api.js";
15
+ import { loadCrypto } from "../crypto.js";
16
+ import { fetchWithKey } from "../download.js";
17
+ import { fileSink, stdoutSink } from "../download-sink.js";
18
+ import { NmtsError } from "../errors.js";
19
+ import { destinationFor } from "../safe-path.js";
20
+ import { resolveNetwork } from "../network.js";
21
+ import { BINARY_NAME } from "../product.js";
22
+ import { openSession } from "../session.js";
23
+ import { openReceived, openSharedDigest, shareKeysOf } from "../share.js";
24
+ import { processStdout, STDOUT_TARGET } from "../stdout.js";
25
+ export async function receive(id, options = {}) {
26
+ const toStdout = options.out === "-";
27
+ const say = options.write ?? ((line) => (toStdout ? process.stderr : process.stdout).write(`${line}\n`));
28
+ if (id === undefined || id === "") {
29
+ throw new NmtsError("Say which share to receive.", {
30
+ exitCode: 2,
31
+ nextStep: `\`${BINARY_NAME} receive <id>\` — the id \`${BINARY_NAME} shares\` prints.`,
32
+ });
33
+ }
34
+ const session = await openSession({ server: options.server, network: options.network });
35
+ const chain = resolveNetwork(session.server, session.network);
36
+ const crypt = await loadCrypto();
37
+ const keys = shareKeysOf(crypt, session.code);
38
+ try {
39
+ // ⛔ THE ROW IS FOUND IN THE LISTING, not asked for by id. The listing is what carries the
40
+ // sender's published identity, and without that identity the envelope cannot be
41
+ // authenticated — an unauthenticated open is not one worth doing.
42
+ const answer = await request(session.server, "/v1/shares/received", {
43
+ token: session.apiKey,
44
+ });
45
+ const rows = typeof answer === "object" && answer !== null ? Reflect.get(answer, "shares") : null;
46
+ if (!Array.isArray(rows))
47
+ throw new NmtsError("The server did not list any shares.");
48
+ const row = rows.find((candidate) => typeof candidate === "object" && candidate !== null && Reflect.get(candidate, "id") === id);
49
+ if (row === undefined) {
50
+ throw new NmtsError(`Nothing shared with this account has the id "${id}".`, {
51
+ exitCode: 4,
52
+ nextStep: `Nothing was written. \`${BINARY_NAME} shares\` lists what is there.`,
53
+ });
54
+ }
55
+ const opened = openReceived(crypt, keys, row);
56
+ if (opened.dek === null || opened.name === null) {
57
+ throw new NmtsError(`That share ${opened.problem ?? "did not open"}.`, {
58
+ exitCode: 1,
59
+ nextStep: "Nothing was written. The sender can withdraw it and share the file again.",
60
+ });
61
+ }
62
+ if (opened.size === null) {
63
+ // ⛔ NOT A GUESS. Without the sender's number there is nothing to trim the stored bytes back
64
+ // to, and taking the stored length would write padding into the end of the file.
65
+ opened.dek.fill(0);
66
+ throw new NmtsError("The sender did not record this file's real length.", {
67
+ exitCode: 1,
68
+ nextStep: "Nothing was written. It was shared by an older client; ask them to share it again from " +
69
+ "a current one, which seals the length beside the name.",
70
+ });
71
+ }
72
+ const expected = openSharedDigest(crypt, opened.dek, opened.digestCt);
73
+ if (expected === null) {
74
+ opened.dek.fill(0);
75
+ throw new NmtsError("This share's content hash did not open with the file's own key.", {
76
+ exitCode: 1,
77
+ nextStep: "Nothing was written. There would be nothing to check the bytes against.",
78
+ });
79
+ }
80
+ const destination = toStdout
81
+ ? null
82
+ : options.out !== undefined
83
+ ? resolve(options.out)
84
+ : options.intoDir !== undefined
85
+ ? destinationFor(options.intoDir, opened.name)
86
+ : resolve(opened.name);
87
+ // ⛔ `--force` has nothing to overwrite on the stdout branch and is ignored there on purpose.
88
+ // On the disk branch the file streams into a temporary name beside the destination and is
89
+ // renamed into place only once the sender's digest matches, so a share that turns out to be
90
+ // wrong — or a transfer that stops half way — leaves nothing under the name it was given.
91
+ const sink = destination === null
92
+ ? stdoutSink(options.stdout ?? processStdout())
93
+ : fileSink(destination, { force: options.force === true });
94
+ const fetched = await fetchWithKey({
95
+ base: session.server,
96
+ apiKey: session.apiKey,
97
+ descriptorPath: `/v1/shares/${encodeURIComponent(id)}/parts?for=download`,
98
+ size: opened.size,
99
+ dek: opened.dek,
100
+ expected,
101
+ chain,
102
+ sink,
103
+ });
104
+ if (!fetched.delivered)
105
+ return 0;
106
+ if (options.json) {
107
+ say(JSON.stringify({
108
+ id,
109
+ name: opened.name,
110
+ bytes: fetched.byteCount,
111
+ from: opened.sender,
112
+ parts: fetched.partCount,
113
+ out: destination ?? STDOUT_TARGET,
114
+ }));
115
+ return 0;
116
+ }
117
+ say(`${opened.name} ${fetched.byteCount} bytes`);
118
+ say(` from ${opened.sender ?? ""}`);
119
+ say(` checked against the hash the sender sealed with it`);
120
+ return 0;
121
+ }
122
+ finally {
123
+ keys.wipe();
124
+ }
125
+ }
@@ -0,0 +1,104 @@
1
+ // `nmts recovery-list` — writing the account's recovery list out as a file.
2
+ //
3
+ // ⛔ WHY IT EXISTS. The recovery list is the answer to "NMTS is gone and I still have my account
4
+ // code": it holds, encrypted, where every file's bytes are on the public storage network, and
5
+ // the key that opens each of them. The account screen has always been able to write one. An
6
+ // account used only from a terminal could not, so the artefact that exists for the day this
7
+ // service is not there did not exist for those accounts at all.
8
+ //
9
+ // ⛔ IT IS NOT THE FILE-LIST COPY, AND NEITHER REPLACES THE OTHER. `nmts listfile` writes the names
10
+ // and keys this machine has seen; this one writes the storage addresses, which that file has
11
+ // none of. Keep both, and keep both somewhere other than the account code.
12
+ //
13
+ // ⛔ THE ACCOUNT CODE IS NOT IN THE FILE. This file plus the code is the account, so keeping them
14
+ // together turns one theft into a total loss. The artefact that deliberately carries both is
15
+ // `nmts kit`, and it says so about itself.
16
+ //
17
+ // ⛔ NO PARTIAL LIST, EVER. If any page, name or key does not reconcile, nothing is written and the
18
+ // reason is printed. A list quietly missing files tells somebody they are covered when they are
19
+ // not, and they find out on the one day it cannot be repaired.
20
+ import { existsSync, statSync, writeFileSync } from "node:fs";
21
+ import { isAbsolute, join, resolve } from "node:path";
22
+ import { NmtsError } from "../errors.js";
23
+ import { assembleRecoveryList, recordRecoveryList } from "../recovery-assemble.js";
24
+ export async function recoveryList(options = {}) {
25
+ const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
26
+ const assembled = await assembleRecoveryList({
27
+ server: options.server,
28
+ network: options.network,
29
+ });
30
+ const { built, file, seq } = assembled;
31
+ // ⛔ ASKED BEFORE THE ATTEMPT AND ENFORCED BY THE WRITE ITSELF. The `wx` flag below is what
32
+ // actually reserves the name; this only turns the common case into a sentence rather than a
33
+ // system error.
34
+ const destination = destinationFor(options.out, file.filename);
35
+ if (options.force !== true && existsSync(destination))
36
+ throw alreadyThere(destination);
37
+ try {
38
+ // ⛔ 0600. These bytes are one account's whole storage index, sealed — and a mode is cheaper to
39
+ // get right than an explanation of why it did not matter.
40
+ writeFileSync(destination, file.content, {
41
+ flag: options.force === true ? "w" : "wx",
42
+ mode: 0o600,
43
+ });
44
+ }
45
+ catch (error) {
46
+ if (error instanceof Error && Reflect.get(error, "code") === "EEXIST") {
47
+ throw alreadyThere(destination);
48
+ }
49
+ throw error;
50
+ }
51
+ await recordRecoveryList(assembled, destination);
52
+ if (options.json === true) {
53
+ say(JSON.stringify({
54
+ writtenTo: destination,
55
+ seq,
56
+ nrm: built.doc.v,
57
+ files: built.fileCount,
58
+ bytes: built.totalBytes,
59
+ capturedAt: assembled.capturedAt,
60
+ // ⛔ REACHES A READER THAT ONLY PARSES JSON. An agent has to be able to say the account is
61
+ // not fully covered without a person reading the lines below.
62
+ missingFromSource: built.missingFromSource,
63
+ }));
64
+ return 0;
65
+ }
66
+ say(`Wrote ${destination}`);
67
+ say(``);
68
+ say(` Recovery list ${seq} — ${built.fileCount} files, ${built.totalBytes} bytes described.`);
69
+ say(` It holds where each file's bytes are on the storage network, and the key that opens`);
70
+ say(` each one, sealed with the account code.`);
71
+ say(``);
72
+ say(` Recorded with the server: version ${seq}, kept on this machine (no storage-network`);
73
+ say(` copy), read from the account at ${assembled.capturedAt}.`);
74
+ if (built.missingFromSource.length > 0) {
75
+ say(``);
76
+ say(` ⚠ ${built.missingFromSource.length} file(s) in your file list are not in the account's`);
77
+ say(` stored files, so they are not in this list. An upload that never finished looks like`);
78
+ say(` this. Nothing here is covered for them.`);
79
+ }
80
+ say(``);
81
+ say(` ⛔ It does not contain the account code. Keep it somewhere other than the code: together`);
82
+ say(` they are the whole account.`);
83
+ say(` ⚠ A list goes stale. Run this again after uploading: of two files, the one whose filename`);
84
+ say(` carries the higher number supersedes the other.`);
85
+ return 0;
86
+ }
87
+ /** Where the file goes: the given file name, inside the given directory, or this directory. */
88
+ function destinationFor(out, filename) {
89
+ if (out === undefined)
90
+ return join(process.cwd(), filename);
91
+ const target = isAbsolute(out) ? out : resolve(process.cwd(), out);
92
+ // A directory that exists means "put it in here under its own name"; anything else is the name
93
+ // to write. Guessing the other way round would rename the artefact whose filename is how a
94
+ // person tells two copies apart.
95
+ if (existsSync(target) && statSync(target).isDirectory())
96
+ return join(target, filename);
97
+ return target;
98
+ }
99
+ function alreadyThere(destination) {
100
+ return new NmtsError(`${destination} is already there.`, {
101
+ exitCode: 4,
102
+ nextStep: `Nothing was written. Pass --out to choose another name, or --force to replace it.`,
103
+ });
104
+ }