@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.
- package/AGENTS.md +591 -0
- package/LICENSE +202 -0
- package/LICENSING.md +49 -0
- package/README.ko.md +641 -0
- package/README.md +657 -0
- package/dist/account-proof.js +78 -0
- package/dist/account.js +75 -0
- package/dist/api-key.js +310 -0
- package/dist/api.js +271 -0
- package/dist/args.js +137 -0
- package/dist/artifact-about.js +70 -0
- package/dist/autonomy.js +98 -0
- package/dist/code-access.js +76 -0
- package/dist/code-vault.js +233 -0
- package/dist/collision.js +133 -0
- package/dist/commands/balance.js +93 -0
- package/dist/commands/consent.js +77 -0
- package/dist/commands/create.js +356 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/expiring.js +167 -0
- package/dist/commands/extend.js +236 -0
- package/dist/commands/get.js +130 -0
- package/dist/commands/kit.js +136 -0
- package/dist/commands/listfile.js +105 -0
- package/dist/commands/login.js +335 -0
- package/dist/commands/logout.js +33 -0
- package/dist/commands/ls.js +202 -0
- package/dist/commands/marks.js +176 -0
- package/dist/commands/mcp.js +139 -0
- package/dist/commands/mode.js +54 -0
- package/dist/commands/on-collision.js +45 -0
- package/dist/commands/organise.js +296 -0
- package/dist/commands/public-code.js +89 -0
- package/dist/commands/pull.js +185 -0
- package/dist/commands/push.js +278 -0
- package/dist/commands/put.js +282 -0
- package/dist/commands/rebuild.js +160 -0
- package/dist/commands/receive.js +125 -0
- package/dist/commands/recovery-list.js +104 -0
- package/dist/commands/recovery.js +291 -0
- package/dist/commands/s3.js +241 -0
- package/dist/commands/settings.js +28 -0
- package/dist/commands/share.js +276 -0
- package/dist/commands/sweep.js +240 -0
- package/dist/commands/trash.js +288 -0
- package/dist/commands/trial.js +264 -0
- package/dist/commands/update.js +169 -0
- package/dist/commands/usage.js +84 -0
- package/dist/commands/verify.js +331 -0
- package/dist/commands/wallet.js +122 -0
- package/dist/commands/whoami.js +33 -0
- package/dist/consent.js +200 -0
- package/dist/credentials.js +311 -0
- package/dist/crypto-surface.js +54 -0
- package/dist/crypto.js +137 -0
- package/dist/download-part.js +144 -0
- package/dist/download-sink.js +213 -0
- package/dist/download.js +163 -0
- package/dist/drive-paths.js +170 -0
- package/dist/environment.js +190 -0
- package/dist/errors.js +71 -0
- package/dist/exit.js +107 -0
- package/dist/expiry.js +115 -0
- package/dist/extend-chain.js +137 -0
- package/dist/extend-plan.js +149 -0
- package/dist/extend-sign.js +130 -0
- package/dist/guards.js +15 -0
- package/dist/help.js +192 -0
- package/dist/human-check.js +54 -0
- package/dist/item-trash.js +25 -0
- package/dist/kit-file.js +133 -0
- package/dist/list-file.js +71 -0
- package/dist/list-view-find.js +71 -0
- package/dist/list-view-order.js +96 -0
- package/dist/main.js +394 -0
- package/dist/manifest-create.js +81 -0
- package/dist/manifest-write.js +244 -0
- package/dist/manifest.js +213 -0
- package/dist/mark-render.js +45 -0
- package/dist/mcp-args.js +154 -0
- package/dist/mcp-tools/context.js +50 -0
- package/dist/mcp-tools/files.js +133 -0
- package/dist/mcp-tools/organise.js +128 -0
- package/dist/mcp-tools/reads.js +89 -0
- package/dist/mcp-tools/share.js +61 -0
- package/dist/mcp.js +142 -0
- package/dist/net-retry.js +84 -0
- package/dist/network.js +43 -0
- package/dist/notice.js +45 -0
- package/dist/product.js +38 -0
- package/dist/progress.js +110 -0
- package/dist/prompt.js +145 -0
- package/dist/rebuild.js +255 -0
- package/dist/recovery-assemble.js +148 -0
- package/dist/recovery-build.js +260 -0
- package/dist/recovery-map-file.js +114 -0
- package/dist/recovery-map.js +191 -0
- package/dist/recovery-release.js +229 -0
- package/dist/recovery-seq.js +63 -0
- package/dist/recovery-source.js +143 -0
- package/dist/registration.js +83 -0
- package/dist/s3/listing.js +134 -0
- package/dist/s3/multipart.js +104 -0
- package/dist/s3/response-sink.js +41 -0
- package/dist/s3/same-file.js +117 -0
- package/dist/s3/server.js +252 -0
- package/dist/s3/sigv4.js +168 -0
- package/dist/s3/staging.js +87 -0
- package/dist/s3/xml.js +80 -0
- package/dist/safe-path.js +89 -0
- package/dist/seal.js +206 -0
- package/dist/secret-reader.js +112 -0
- package/dist/server.js +33 -0
- package/dist/session.js +49 -0
- package/dist/setup-questions.js +31 -0
- package/dist/share.js +195 -0
- package/dist/shared/lib/crypto/size-padding.js +126 -0
- package/dist/shared/lib/drive/manifest-codec.js +287 -0
- package/dist/shared/lib/drive/manifest-index.js +235 -0
- package/dist/shared/lib/drive/manifest-ops.js +267 -0
- package/dist/shared/lib/drive/manifest-settings.js +65 -0
- package/dist/shared/lib/drive/name-conflict.js +121 -0
- package/dist/shared/lib/drive/unique-name.js +48 -0
- package/dist/shared/lib/extend/epochs.js +256 -0
- package/dist/shared/lib/net/retry-budget.js +95 -0
- package/dist/shared/lib/share/shared-file-info.js +65 -0
- package/dist/shared/lib/storage-network.js +64 -0
- package/dist/shared/lib/upload/part-plan.js +28 -0
- package/dist/stdout.js +109 -0
- package/dist/trash-sweep.js +123 -0
- package/dist/units.js +19 -0
- package/dist/update-check.js +164 -0
- package/dist/update-source.js +143 -0
- package/dist/upload-api.js +98 -0
- package/dist/upload-file.js +242 -0
- package/dist/upload-price.js +117 -0
- package/dist/upload-steps.js +111 -0
- package/dist/upload-store.js +252 -0
- package/dist/upload-wire.js +25 -0
- package/dist/upload.js +294 -0
- package/dist/usage-report.js +54 -0
- package/dist/wallet-chain.js +76 -0
- package/dist/wallet.js +209 -0
- package/dist/walrus-write.js +188 -0
- package/dist/walrus.js +156 -0
- package/package.json +56 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +469 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm.js +1300 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +55 -0
- package/vendor/nmts-crypto/package.json +17 -0
package/dist/help.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// What `nmts` prints when asked what it can do.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ COMMANDS THAT ARE NOT BUILT ARE MARKED, NOT HIDDEN. An agent that discovers a command by
|
|
4
|
+
// running it and getting "unknown command" learns the wrong thing — it will try synonyms. Being
|
|
5
|
+
// told the command exists and is unfinished is a fact it can act on: stop, and do not retry.
|
|
6
|
+
import { AGENTS_DOC, BINARY_NAME, HOME_URL, PRODUCT_NAME, SOURCE_URL } from "./product.js";
|
|
7
|
+
import { API_KEY_ENV_VAR, API_KEY_FILE_ENV_VAR, CODE_ENV_VAR, CODE_FILE_ENV_VAR, PASSPHRASE_ENV_VAR, } from "./credentials.js";
|
|
8
|
+
import { NETWORK_ENV_VAR } from "./network.js";
|
|
9
|
+
import { LATEST_RELEASE_URL, NO_CHECK_ENV_VAR } from "./update-source.js";
|
|
10
|
+
import { SERVER_ENV_VAR } from "./server.js";
|
|
11
|
+
import { AGGREGATOR_ENV_VAR } from "./walrus.js";
|
|
12
|
+
import { CHECK_DOES_NOT_PROVE, CHECK_PROVES, RECOVERY_TOOL_URL, wrapText, } from "./recovery-release.js";
|
|
13
|
+
export function helpText(version) {
|
|
14
|
+
return [
|
|
15
|
+
`${PRODUCT_NAME} ${version} — command-line access to end-to-end encrypted NMTS storage.`,
|
|
16
|
+
``,
|
|
17
|
+
`USAGE`,
|
|
18
|
+
` ${BINARY_NAME} <command> [options]`,
|
|
19
|
+
``,
|
|
20
|
+
`COMMANDS`,
|
|
21
|
+
` login Keep an account code on this machine, sealed — and take an API key`,
|
|
22
|
+
` logout Remove the stored account code`,
|
|
23
|
+
` whoami Show which account the stored code belongs to (offline)`,
|
|
24
|
+
` create Make a NEW account. Prints its code once — nothing can print it again`,
|
|
25
|
+
` ls List files in the account`,
|
|
26
|
+
` usage What the account holds: counts, bytes, largest files, trash`,
|
|
27
|
+
` balance Credits left, what they buy, and the ceilings on spending`,
|
|
28
|
+
` trial What is left of this week's free credits. \`trial apply\` asks for some`,
|
|
29
|
+
` expiring List files whose bought storage runs out soon`,
|
|
30
|
+
` extend <path> Buy more storage time for one file — **spends WAL from the wallet**`,
|
|
31
|
+
` public-code The code other accounts send files to. \`--publish\` makes it reachable`,
|
|
32
|
+
` wallet Show the account's wallet address, and its SUI and WAL balances`,
|
|
33
|
+
` wallet address Just the address, derived on this machine — no network call`,
|
|
34
|
+
` put <file> Encrypt one file and upload it, paid with credits`,
|
|
35
|
+
` get <path> Download one file and decrypt it`,
|
|
36
|
+
` pull [folder] Download a whole folder, or the whole account, keeping its shape`,
|
|
37
|
+
` push <directory> Upload a whole directory, keeping its shape — **spends credits**`,
|
|
38
|
+
` rm <paths> Move things to the trash — restorable for 30 days`,
|
|
39
|
+
` restore <paths> Bring things back out of the trash`,
|
|
40
|
+
` sweep Drop trash entries whose 30 days have run out. Asks first`,
|
|
41
|
+
` rebuild Build a file list from the server's rows, for an account with none`,
|
|
42
|
+
` listfile Write this machine's copy of the sealed file list out as a file`,
|
|
43
|
+
` mkdir <path> Make a folder, and any folder above it that is missing`,
|
|
44
|
+
` mv <paths> <folder> Move things into a folder. \`/\` is the top of the drive`,
|
|
45
|
+
` rename <path> <name> Give one thing a new name`,
|
|
46
|
+
` star <files> Star files — they are gathered in favourites as well`,
|
|
47
|
+
` unstar <files> Take the star off`,
|
|
48
|
+
` pin <files> Hold files at the top of the folder they are in`,
|
|
49
|
+
` unpin <files> Let them fall back into the ordinary order`,
|
|
50
|
+
` label <name> <files> Put one label on files. A label exists while a file wears it`,
|
|
51
|
+
` unlabel <name> <files>`,
|
|
52
|
+
` Take one label off files`,
|
|
53
|
+
` share <path> <addr> Give one file to another account. Withdrawing does not recall it`,
|
|
54
|
+
` shares What was shared with this account`,
|
|
55
|
+
` receive <id> Download one file somebody shared with this account`,
|
|
56
|
+
` unshare <id> Withdraw a share you sent, or remove one you were sent`,
|
|
57
|
+
` env What this machine is, and what it means for the account code`,
|
|
58
|
+
` consent See what this machine has agreed to, and change it`,
|
|
59
|
+
` mode How much an agent driving this tool may decide without asking`,
|
|
60
|
+
` on-collision What to do when an upload's name is already in use`,
|
|
61
|
+
` update Install the newest published release of this tool`,
|
|
62
|
+
` recovery Download the standalone program that reads files back without NMTS`,
|
|
63
|
+
` recovery-list Write the file that finds your bytes without NMTS. Holds no code`,
|
|
64
|
+
` kit Recovery kit: that list AND your account code, together in one file`,
|
|
65
|
+
` verify Ask a person to pass the check that opens this account's limits`,
|
|
66
|
+
` mcp Serve this account's commands as tools, for an agent that speaks MCP`,
|
|
67
|
+
` s3 Serve the drive to any S3 program, on this machine only`,
|
|
68
|
+
``,
|
|
69
|
+
`OPTIONS`,
|
|
70
|
+
` --server <url> NMTS server (default ${SERVER_ENV_VAR} or the live one)`,
|
|
71
|
+
` --network <name> mainnet or testnet. Required for any server but the live one`,
|
|
72
|
+
` --json Machine-readable output (ls, usage, get, put, env, verify,`,
|
|
73
|
+
` expiring, sweep, rebuild, pull, wallet, shares, share, receive,`,
|
|
74
|
+
` recovery, balance, trial, extend, recovery-list, kit, update,`,
|
|
75
|
+
` create — create needs --out as well, so the code never enters`,
|
|
76
|
+
` the output)`,
|
|
77
|
+
` --all Include what is in the trash (ls)`,
|
|
78
|
+
` --find <text> List only the files whose name contains this text (ls). Folders`,
|
|
79
|
+
` appear only where they hold a match`,
|
|
80
|
+
` --sort <key> name, size or date. Default: whole paths, ascending (ls)`,
|
|
81
|
+
` --desc Reverse whichever order is in effect (ls)`,
|
|
82
|
+
` --hidden Include entries whose name begins with a dot (push)`,
|
|
83
|
+
` --out <path> Where to write files (get, pull, mcp, listfile, recovery,`,
|
|
84
|
+
` recovery-list, kit, create). Default: here`,
|
|
85
|
+
` --out - Send a fetched file to stdout instead of writing it (get, listfile)`,
|
|
86
|
+
` --force Replace a file that is already there (get, pull, listfile,`,
|
|
87
|
+
` recovery, recovery-list, kit), or rebuild an account this machine`,
|
|
88
|
+
` has seen a list for (rebuild). \`create\` never replaces a file`,
|
|
89
|
+
` --name <name> The name the uploaded file gets in the drive (put)`,
|
|
90
|
+
` --to <folder> Destination folder, as \`${BINARY_NAME} ls\` prints it (put, push)`,
|
|
91
|
+
` --dry-run Say what it would cost, and stop (put, push, extend), or what it`,
|
|
92
|
+
` would install, and stop (update). With \`extend\` nothing is`,
|
|
93
|
+
` signed and the wallet key is never touched`,
|
|
94
|
+
` --part-size <n> Bytes per part for a large upload (put, push). Default 64MiB.`,
|
|
95
|
+
` --on-collision <what> rename | overwrite, for THIS run only (put, push). Without it the`,
|
|
96
|
+
` machine's own setting decides -- see \`${BINARY_NAME} on-collision\`.`,
|
|
97
|
+
` a plain number, or one with KiB, MiB or GiB. Bigger parts mean`,
|
|
98
|
+
` fewer purchases; smaller parts use less memory`,
|
|
99
|
+
` --yes Go ahead with something that cannot be taken back (sweep, rebuild),`,
|
|
100
|
+
` or extend a file that is nowhere near its deadline (extend)`,
|
|
101
|
+
` --epochs <n> How many of the storage network's epochs to add (extend)`,
|
|
102
|
+
` --port <n> Which loopback port to listen on (s3). Default 9000`,
|
|
103
|
+
` --accept-terms <v> The version of the Terms of Service a PERSON read and accepts for`,
|
|
104
|
+
` the new account (create). A version, never "the current one"`,
|
|
105
|
+
` --accept-privacy <v> The version of the Privacy Policy accepted in the same act (create)`,
|
|
106
|
+
` --publish Publish this account's public code (public-code). Permanent —`,
|
|
107
|
+
` it cannot be withdrawn or changed afterwards`,
|
|
108
|
+
` --status Say whether the check is live, and stop (verify)`,
|
|
109
|
+
` --plain Store the code unsealed instead (login). Asks for an agreement`,
|
|
110
|
+
` --env Store nothing; print the variable to set (login). Same agreement`,
|
|
111
|
+
` --version Print the version and exit`,
|
|
112
|
+
` --help Print this and exit`,
|
|
113
|
+
``,
|
|
114
|
+
`ENVIRONMENT`,
|
|
115
|
+
` ${CODE_FILE_ENV_VAR.padEnd(24)}Names a FILE holding the account code. The recommended way, and`,
|
|
116
|
+
` the only one that asks nothing: the file is read, never copied.`,
|
|
117
|
+
` ${CODE_ENV_VAR.padEnd(24)}The account code itself. Takes precedence over anything stored,`,
|
|
118
|
+
` and asks once — an environment variable is readable through`,
|
|
119
|
+
` \`docker inspect\`, /proc/<pid>/environ and most CI logs.`,
|
|
120
|
+
` ${PASSPHRASE_ENV_VAR.padEnd(24)}Opens a sealed stored code without a terminal.`,
|
|
121
|
+
` ${API_KEY_FILE_ENV_VAR.padEnd(24)}Names a FILE holding the API key. \`${BINARY_NAME} login\` reads this`,
|
|
122
|
+
` first, and a container cannot leak it the way it leaks a value.`,
|
|
123
|
+
` ${API_KEY_ENV_VAR.padEnd(24)}Key made on the account screen. It waives the human check a`,
|
|
124
|
+
` browser sign-in does, and nothing else — it opens no file.`,
|
|
125
|
+
` \`${BINARY_NAME} login\` checks one it finds here with the server and`,
|
|
126
|
+
` then stores it, so it outlives the terminal it was set in.`,
|
|
127
|
+
` ${SERVER_ENV_VAR.padEnd(24)}Server to talk to. For development stacks.`,
|
|
128
|
+
` ${AGGREGATOR_ENV_VAR.padEnd(24)}Storage-network read hosts, comma-separated. Replaces`,
|
|
129
|
+
` the built-in list rather than adding to it.`,
|
|
130
|
+
` ${NETWORK_ENV_VAR.padEnd(24)}mainnet or testnet. Never guessed: a wrong one looks in a place`,
|
|
131
|
+
` your files were never stored.`,
|
|
132
|
+
` ${NO_CHECK_ENV_VAR.padEnd(24)}Set to anything to stop the version check described below.`,
|
|
133
|
+
``,
|
|
134
|
+
// ⛔ THE TWO SENTENCES ABOUT THE CHECK ARE IMPORTED, NOT RETYPED. They are also printed by
|
|
135
|
+
// the command itself, and a second copy is how one of them starts promising more than the
|
|
136
|
+
// other. What is added here is the other half of the same honesty: where the file goes.
|
|
137
|
+
`THE RECOVERY PROGRAM`,
|
|
138
|
+
` \`${BINARY_NAME} recovery\` downloads one executable for this machine from the releases at`,
|
|
139
|
+
``,
|
|
140
|
+
` ${RECOVERY_TOOL_URL}`,
|
|
141
|
+
``,
|
|
142
|
+
...wrapText(CHECK_PROVES).map((line) => ` ${line}`),
|
|
143
|
+
...wrapText(CHECK_DOES_NOT_PROVE).map((line) => ` ${line}`),
|
|
144
|
+
``,
|
|
145
|
+
` It writes one file where you point it, refuses a name that is already taken unless you`,
|
|
146
|
+
` pass --force, and installs nothing: no copy goes onto your PATH.`,
|
|
147
|
+
``,
|
|
148
|
+
// ⛔ IT IS WRITTEN DOWN BECAUSE IT IS THE ONLY REQUEST THIS TOOL MAKES THAT NOBODY ASKED FOR.
|
|
149
|
+
// Everything else it sends goes to the NMTS server or the storage network because a command
|
|
150
|
+
// needed it. This one goes to a third host, on its own, and a tool that does that without
|
|
151
|
+
// saying so is one whose network behaviour has to be found out with a packet capture.
|
|
152
|
+
`THE VERSION CHECK`,
|
|
153
|
+
` Once a day, after a command has finished, this asks which release of this tool is newest:`,
|
|
154
|
+
``,
|
|
155
|
+
` ${LATEST_RELEASE_URL}`,
|
|
156
|
+
``,
|
|
157
|
+
` and writes the answer down. When it is newer than the one running, the NEXT run prints one`,
|
|
158
|
+
` line on stderr saying so. It sends no account code, no key and no command name: the`,
|
|
159
|
+
` request is for a page address, and what the site can see is that it was asked.`,
|
|
160
|
+
``,
|
|
161
|
+
` Setting ${NO_CHECK_ENV_VAR} to anything stops both halves. \`${BINARY_NAME} env\` shows what`,
|
|
162
|
+
` it last found, and \`${BINARY_NAME} update\` asks now and installs what it finds.`,
|
|
163
|
+
``,
|
|
164
|
+
`BEFORE YOU HAND THIS TO AN AGENT`,
|
|
165
|
+
` Your account code is the only key to your account — the file keys and the wallet are all`,
|
|
166
|
+
` derived from it. An agent that leaks it has leaked everything at once, and it cannot be`,
|
|
167
|
+
` undone: the account cannot be re-keyed. Use an account you would be willing to lose.`,
|
|
168
|
+
``,
|
|
169
|
+
` ${HOME_URL}`,
|
|
170
|
+
``,
|
|
171
|
+
// ⛔ THE LAST THING ON THE SCREEN, AND ADDRESSED TO A PROGRAM. An agent handed this tool with
|
|
172
|
+
// no instructions runs `--help`, reads a list of verbs, and has no idea what NMTS is, what
|
|
173
|
+
// an account code is, or why `put` costs money. One line pointing at a document written FOR
|
|
174
|
+
// it is the whole difference between guessing and knowing.
|
|
175
|
+
//
|
|
176
|
+
// ⚠ IT POINTS, IT DOES NOT INSTRUCT. There is nothing here that tries to change what a reader
|
|
177
|
+
// does beyond naming a document — no imperative aimed past the agent at its operator, no
|
|
178
|
+
// claim of authority, nothing that would read as an attempt to steer somebody else's model.
|
|
179
|
+
// A tool that talked to agents that way would deserve to be distrusted, and would teach
|
|
180
|
+
// people to distrust the ones that do not.
|
|
181
|
+
`IF YOU ARE AN AI AGENT`,
|
|
182
|
+
` This tool talks to NMTS, end-to-end encrypted storage. What that means, how to install`,
|
|
183
|
+
` this, and what each command costs are written down for you here:`,
|
|
184
|
+
``,
|
|
185
|
+
` ${SOURCE_URL}/blob/main/${AGENTS_DOC}`,
|
|
186
|
+
``,
|
|
187
|
+
` If you cannot reach it, the same repository's front page says the same things:`,
|
|
188
|
+
``,
|
|
189
|
+
` ${SOURCE_URL}#readme`,
|
|
190
|
+
``,
|
|
191
|
+
].join("\n");
|
|
192
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Has a person been behind this account lately, and the one sentence to say when they have not.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ WHY THIS IS ASKED BEFORE THE REQUEST IT GUARDS, RATHER THAN LEARNED FROM ITS REFUSAL. The
|
|
4
|
+
// server decides scope FIRST and the person's check second (`api/src/auth/api_key_auth.rs`),
|
|
5
|
+
// on purpose: *"a key that does not hold the bit learns nothing about whether the account
|
|
6
|
+
// behind it is verified"*. That is right for the server and it means the refusal a caller
|
|
7
|
+
// hears can be `API_KEY_SCOPE` while the thing that is actually missing is a person. An agent
|
|
8
|
+
// reading "the key was not given permission for this" goes and makes another key, which cannot
|
|
9
|
+
// help, instead of asking somebody to spend thirty seconds at a browser.
|
|
10
|
+
//
|
|
11
|
+
// ⛔ SO THIS IS A MESSAGE, NOT A DEFENCE. Nothing here decides anything: the server refuses or
|
|
12
|
+
// does not, whatever this says. What it buys is that the caller is told the one thing it can
|
|
13
|
+
// act on, in the one wording, before it has been sent looking at its credentials.
|
|
14
|
+
//
|
|
15
|
+
// ⚠ AND IT COSTS ONE READ. `GET /v1/agent/verify` is reachable by ANY key, holds no scope
|
|
16
|
+
// requirement, and answers a single boolean — it is the cheapest question in this API and the
|
|
17
|
+
// only one that can be asked before knowing whether the key is allowed to do anything at all.
|
|
18
|
+
import { request } from "./api.js";
|
|
19
|
+
import { NmtsError } from "./errors.js";
|
|
20
|
+
import { isRecord } from "./guards.js";
|
|
21
|
+
import { BINARY_NAME } from "./product.js";
|
|
22
|
+
/**
|
|
23
|
+
* Ask the server whether a person's check is live for the account this key belongs to.
|
|
24
|
+
*
|
|
25
|
+
* ⛔ THE ADDRESS IS WRITTEN OUT AT THE CALL rather than held in a constant, for the reason
|
|
26
|
+
* `commands/verify.ts` gives: the gate that checks this tool's addresses against the server's
|
|
27
|
+
* own routes reads the literal in the call, and a constant would make it stop looking here.
|
|
28
|
+
*/
|
|
29
|
+
export async function humanCheck(server, apiKey) {
|
|
30
|
+
const answer = await request(server, "/v1/agent/verify", { token: apiKey });
|
|
31
|
+
if (!isRecord(answer) || typeof answer["verified"] !== "boolean") {
|
|
32
|
+
throw new NmtsError("The server's answer did not say whether this account is verified.", {
|
|
33
|
+
exitCode: 1,
|
|
34
|
+
nextStep: `This version of \`${BINARY_NAME}\` and that server do not agree about this. Update the tool, or check --server.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const until = answer["verified_until"];
|
|
38
|
+
return { live: answer["verified"], until: typeof until === "string" ? until : null };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The one refusal for "a person has to do something first".
|
|
42
|
+
*
|
|
43
|
+
* ⛔ IT NAMES THE COMMAND AND SAYS WHO HAS TO RUN IT. `verify` prints a short code and waits; the
|
|
44
|
+
* typing is a person's, at a browser, and no amount of retrying here replaces it. Saying only
|
|
45
|
+
* "refused" would send an agent round the loop of credentials it already has.
|
|
46
|
+
*/
|
|
47
|
+
export function askAPersonToVerify(whatIsRefused) {
|
|
48
|
+
return new NmtsError(`${whatIsRefused} until somebody passes this account's human check.`, {
|
|
49
|
+
exitCode: 4,
|
|
50
|
+
nextStep: `Run \`${BINARY_NAME} verify\`. It prints a short code and an address; a person opens that ` +
|
|
51
|
+
`address, types the code, and this account's limits are lifted for four of the server's ` +
|
|
52
|
+
`weeks. Nothing on this machine can pass that check — being unable to is what it measures.`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Move one item into the trash on the server, or bring it back.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ 404 IS "ALREADY IN THE STATE YOU ASKED FOR", which is what a half-finished run leaves behind.
|
|
4
|
+
// Treating it as a failure would make the retry of an interrupted command impossible -- the one
|
|
5
|
+
// moment the retry is needed.
|
|
6
|
+
//
|
|
7
|
+
// ⛔ AND THIS IS AS FAR AS AN API KEY REACHES. `POST /v1/items/erase` -- the one that destroys a
|
|
8
|
+
// row for good -- is closed to keys and stays closed (`api` domain/agent_routes.rs, Reach::Never:
|
|
9
|
+
// "PERMANENT destruction. The soft delete is reachable because it is recoverable; this is the
|
|
10
|
+
// line where that stops."). Anything in this tool that says a file is gone forever is wrong.
|
|
11
|
+
import { request, ServerError } from "./api.js";
|
|
12
|
+
export async function setTrashed(base, apiKey, id, trashed) {
|
|
13
|
+
const held = encodeURIComponent(id);
|
|
14
|
+
try {
|
|
15
|
+
if (trashed)
|
|
16
|
+
await request(base, `/v1/items/${held}`, { method: "DELETE", token: apiKey });
|
|
17
|
+
else
|
|
18
|
+
await request(base, `/v1/items/${held}/restore`, { method: "POST", token: apiKey, body: {} });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof ServerError && error.status === 404)
|
|
22
|
+
return;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/kit-file.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The RECOVERY KIT — the one file that carries everything, account code included.
|
|
2
|
+
//
|
|
3
|
+
// ⛔⛔ IT HOLDS THE ACCOUNT CODE IN THE CLEAR, AND THAT IS THE FORMAT. It was decided deliberately
|
|
4
|
+
// and against the earlier rule, by the owner, with the cost stated: whoever holds this file
|
|
5
|
+
// holds the account AND the wallet, because one code derives both. The two artefacts therefore
|
|
6
|
+
// mean two different things and every screen and every command must say which is which:
|
|
7
|
+
// · recovery LIST — sealed. Worthless to a thief. The account code opens it.
|
|
8
|
+
// · recovery KIT — everything. One stolen file is a total loss.
|
|
9
|
+
// The warning is not a formality; it is what makes the choice an honest one to offer.
|
|
10
|
+
//
|
|
11
|
+
// ⛔ THERE IS NO SPECIFICATION FOR THIS FORMAT. Its only authorities are the two programs that
|
|
12
|
+
// already write and read it: `web/src/lib/auth/recovery-kit.ts` (writer) and
|
|
13
|
+
// `recovery/src/kitfile.rs` (reader). The shape below was read off BOTH.
|
|
14
|
+
// ⚠ WHERE THEY DIFFER: the reader requires only `format`, `version` and `account_id`, treats
|
|
15
|
+
// `account_code` and `recovery_list` as optional, and reads nothing else — no
|
|
16
|
+
// `generated_at`, no `account_fingerprint`, no `recovery_manifest_blob`, no `about`. The
|
|
17
|
+
// writer always emits all of them. That is not a contradiction, it is the reader being
|
|
18
|
+
// deliberately lenient (it sets no `deny_unknown_fields`), and it is why adding a field never
|
|
19
|
+
// moved the kit version. This writer emits the writer's full set.
|
|
20
|
+
//
|
|
21
|
+
// ⛔ THE MACHINE BLOCK IS DELIMITED BY FIXED ASCII, NEVER BY THE HEADING ABOVE IT. The headings are
|
|
22
|
+
// written in whatever language the person was using; a parser that looked for one would work in
|
|
23
|
+
// one language and fail in another — on the day somebody is recovering, which is the only day
|
|
24
|
+
// it matters.
|
|
25
|
+
//
|
|
26
|
+
// ⚠ THE HUMAN HALF IS ENGLISH ONLY, and the browser's is bilingual. The English sentences below
|
|
27
|
+
// are the site's own, word for word (`RecoveryKit.*` in `web/src/messages/en.json`), so the two
|
|
28
|
+
// programs do not describe one artefact differently; the Korean half lives in message bundles
|
|
29
|
+
// this package does not import, and inventing a translation here would be worse than the gap.
|
|
30
|
+
// `cli/src/list-file.ts` set that precedent for the sibling artefact.
|
|
31
|
+
import { createHash } from "node:crypto";
|
|
32
|
+
import { artifactAbout } from "./artifact-about.js";
|
|
33
|
+
import { RECOVERY_TOOL_URL } from "./recovery-release.js";
|
|
34
|
+
/** Start of the machine block. ⛔ Fixed bytes, never translated. */
|
|
35
|
+
export const KIT_DATA_BEGIN = "--- BEGIN NMTS RECOVERY KIT DATA ---";
|
|
36
|
+
/** End of the machine block. */
|
|
37
|
+
export const KIT_DATA_END = "--- END NMTS RECOVERY KIT DATA ---";
|
|
38
|
+
/** The marker a reader matches on before anything else is attempted. */
|
|
39
|
+
export const KIT_FORMAT = "nmts-recovery-kit";
|
|
40
|
+
/**
|
|
41
|
+
* Kit version. 2 is the one that carries the recovery list; v1 carried only the code.
|
|
42
|
+
*
|
|
43
|
+
* ⛔ NOT RAISED FOR ANYTHING THIS TOOL ADDS. `MAX_KIT_VERSION` in the standalone program refuses a
|
|
44
|
+
* higher number outright, so a bump breaks every published build in exchange for fields those
|
|
45
|
+
* builds are happy to ignore.
|
|
46
|
+
*/
|
|
47
|
+
export const KIT_VERSION = 2;
|
|
48
|
+
/**
|
|
49
|
+
* A short, human-checkable fingerprint of the PUBLIC account id.
|
|
50
|
+
*
|
|
51
|
+
* ⚠ RESTATED FROM `web/src/lib/auth/account-code.ts::accountIdFingerprint`, which this package
|
|
52
|
+
* cannot import. It has to produce the same string: a person comparing a kit written here with
|
|
53
|
+
* one written in a browser is checking two spellings of the same account.
|
|
54
|
+
*
|
|
55
|
+
* ⛔ IT FINGERPRINTS THE ACCOUNT ID, WHICH IS PUBLIC — never the account code and never a key. It
|
|
56
|
+
* lets somebody confirm two files refer to one account without either of them exposing a secret.
|
|
57
|
+
*/
|
|
58
|
+
export function accountIdFingerprint(accountId) {
|
|
59
|
+
const hex = createHash("sha256")
|
|
60
|
+
.update(accountId, "utf8")
|
|
61
|
+
.digest("hex")
|
|
62
|
+
.slice(0, 16)
|
|
63
|
+
.toUpperCase();
|
|
64
|
+
// Grouped 4 by 4 for reading aloud: "AB12-CD34-EF56-7890".
|
|
65
|
+
return hex.replace(/(.{4})(?=.)/g, "$1-");
|
|
66
|
+
}
|
|
67
|
+
/** Build the kit's text: a part for a person, then a part for a program. */
|
|
68
|
+
export function buildRecoveryKit(input) {
|
|
69
|
+
const data = {
|
|
70
|
+
format: KIT_FORMAT,
|
|
71
|
+
version: KIT_VERSION,
|
|
72
|
+
generated_at: input.generatedAt,
|
|
73
|
+
account_id: input.accountId,
|
|
74
|
+
account_fingerprint: accountIdFingerprint(input.accountId),
|
|
75
|
+
account_code: input.code,
|
|
76
|
+
// ⛔ ALWAYS NULL HERE. That field names a copy of the list on the storage network, and this
|
|
77
|
+
// tool writes no such copy — a value would be an address nothing was ever written to.
|
|
78
|
+
recovery_manifest_blob: null,
|
|
79
|
+
recovery_list: input.recoveryList,
|
|
80
|
+
about: artifactAbout("recovery-kit"),
|
|
81
|
+
};
|
|
82
|
+
const lines = [
|
|
83
|
+
`# NMTS Recovery Kit`,
|
|
84
|
+
`Created: ${input.generatedAt}`,
|
|
85
|
+
``,
|
|
86
|
+
// ⛔ THE THEFT WARNING COMES FIRST, ABOVE THE CODE IT IS ABOUT. A caution printed underneath
|
|
87
|
+
// the thing it cautions about has already been disregarded by the time it is read.
|
|
88
|
+
`⛔ Anyone who holds this file holds this account: every file in it, and the wallet that pays ` +
|
|
89
|
+
`for storage. One account code opens both.`,
|
|
90
|
+
`Do not keep it in a folder that syncs or backs up on its own, and do not send it to anyone. ` +
|
|
91
|
+
`A drawer is often safer than a cloud folder.`,
|
|
92
|
+
``,
|
|
93
|
+
`This file is the only way to recover your NMTS files. Keep it private.`,
|
|
94
|
+
`This file carries your account code in the clear, your account identifier and fingerprint, ` +
|
|
95
|
+
`and the whole recovery list.`,
|
|
96
|
+
`Anyone with this code can open your files. If you lose it, no one — including NMTS — can ` +
|
|
97
|
+
`recover them.`,
|
|
98
|
+
``,
|
|
99
|
+
`Account code:`,
|
|
100
|
+
` ${input.code}`,
|
|
101
|
+
``,
|
|
102
|
+
`Account identifier: ${data.account_id}`,
|
|
103
|
+
`Fingerprint: ${data.account_fingerprint}`,
|
|
104
|
+
`Recovery manifest blob: not yet created`,
|
|
105
|
+
`Recovery list: ${input.recoveryList === null
|
|
106
|
+
? `not included — this account had no files when the kit was made`
|
|
107
|
+
: `included in this file (${input.listFileCount ?? 0} files)`}`,
|
|
108
|
+
``,
|
|
109
|
+
`--- HOW TO GET YOUR FILES BACK (English) ---`,
|
|
110
|
+
`1. Get the recovery program from ${RECOVERY_TOOL_URL} — it runs on Linux, macOS and ` +
|
|
111
|
+
`Windows, and its full source is there.`,
|
|
112
|
+
`2. Open a terminal where you saved the program, and run:`,
|
|
113
|
+
` nmts-recovery --map <this file> --out <a folder to write into>`,
|
|
114
|
+
` It takes the account code and the file list out of this file, fetches your files from ` +
|
|
115
|
+
`public Walrus storage, checks every piece, and writes them out.`,
|
|
116
|
+
`3. To click instead of typing, run: nmts-recovery --gui`,
|
|
117
|
+
` That opens a page only this machine can reach. The program still does the work; the page ` +
|
|
118
|
+
`only shows the list and sends back what you ticked.`,
|
|
119
|
+
`4. To see what your account code derives — your public code and your wallet addresses — ` +
|
|
120
|
+
`run: nmts-recovery --derive`,
|
|
121
|
+
`If you would rather it opened no network connections, run it with --print-fetch-plan: it ` +
|
|
122
|
+
`prints the exact addresses to fetch by hand, then restores from the folder you filled.`,
|
|
123
|
+
``,
|
|
124
|
+
`--- FOR A PROGRAM ---`,
|
|
125
|
+
KIT_DATA_BEGIN,
|
|
126
|
+
JSON.stringify(data, null, 2),
|
|
127
|
+
KIT_DATA_END,
|
|
128
|
+
``,
|
|
129
|
+
];
|
|
130
|
+
// A short id slug keeps filenames distinct without printing the whole account id.
|
|
131
|
+
const slug = input.accountId.replace(/[^A-Za-z0-9]/g, "").slice(0, 8) || "account";
|
|
132
|
+
return { filename: `nmts-recovery-kit-${slug}.txt`, content: lines.join("\n") };
|
|
133
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The sealed file list as a FILE somebody keeps — the same wrapper the browser hands out.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ ONE FORMAT, NOT TWO. `format`, `version` and the `.nmtslist` extension are what a reader
|
|
4
|
+
// matches on, and a person recovering an account may hold copies written by a browser and by
|
|
5
|
+
// this command months apart. A second spelling of the same artefact would mean whoever helps
|
|
6
|
+
// them has two formats to work out instead of one, at the worst possible moment. Everything in
|
|
7
|
+
// the document below is therefore the browser's, byte for byte, with the two exceptions named
|
|
8
|
+
// at the bottom of this comment.
|
|
9
|
+
//
|
|
10
|
+
// ⛔ NOTHING SECRET IN THE HEADER, AND THAT INCLUDES SIZES. The plaintext part of this file says
|
|
11
|
+
// what the file IS — never what is inside it, not a file name, not a count, not a total. The
|
|
12
|
+
// contents are one sealed envelope that opens with the account code and nothing else, so a
|
|
13
|
+
// stranger who finds this learns which account it belongs to and no more.
|
|
14
|
+
//
|
|
15
|
+
// ⛔ THE ACCOUNT CODE IS NOT IN IT, and that is not an oversight to fix later. This file plus the
|
|
16
|
+
// code is the account; keeping both in one place would make one theft into a total loss.
|
|
17
|
+
//
|
|
18
|
+
// ⚠ WHAT DIFFERS FROM THE BROWSER'S COPY, and neither is a format difference:
|
|
19
|
+
// · The finder's note is English only. The browser ships it in English and Korean because it
|
|
20
|
+
// cannot know which language the person who finds the file years later reads; this package
|
|
21
|
+
// ships English strings only, and inventing a translation here would be worse than the gap.
|
|
22
|
+
// · `about.app_version` names THIS PROGRAM and its version rather than the site release. It is
|
|
23
|
+
// the field's own contract — a claim the writer makes about itself — and a person holding two
|
|
24
|
+
// copies of one account's list can then tell which program wrote each.
|
|
25
|
+
import { artifactAbout, WRITTEN_BY } from "./artifact-about.js";
|
|
26
|
+
/** Wrapper format identifier, distinct from the recovery list's. */
|
|
27
|
+
export const LIST_FILE_FORMAT = "nmts-file-list";
|
|
28
|
+
/** The shell's version. Not the file list's own version, which is `seq`. */
|
|
29
|
+
export const LIST_FILE_VERSION = 1;
|
|
30
|
+
/** Filename extension. Deliberately not the recovery list's: the two must not be confusable. */
|
|
31
|
+
export const LIST_FILE_EXTENSION = "nmtslist";
|
|
32
|
+
// ⚠ THE HEADER BLOCK IS NOT SPELLED HERE. Three artefacts carry the same one — this file, the
|
|
33
|
+
// recovery list and the recovery kit — and it is built in `artifact-about.ts` so that a renamed
|
|
34
|
+
// program or a moved document reaches all three. `WRITTEN_BY` is re-exported because it is the
|
|
35
|
+
// value this wrapper puts in `app_version`, and a test reads it from here.
|
|
36
|
+
export { WRITTEN_BY };
|
|
37
|
+
/** Plain-language lines for whoever finds this file with no idea what it is. */
|
|
38
|
+
const NOTE = [
|
|
39
|
+
`This file is an encrypted copy of an NMTS (nmts.me) file list — the names, folders and file ` +
|
|
40
|
+
`keys of one account, locked with its account code.`,
|
|
41
|
+
`It is written by the \`nmts\` command from the copy that machine keeps. A higher number in the ` +
|
|
42
|
+
`filename is a newer copy.`,
|
|
43
|
+
`It does not replace the recovery list: storage-network addresses live only in the recovery ` +
|
|
44
|
+
`list. Keep both, somewhere other than the account code.`,
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Build the document and the name to offer it under.
|
|
48
|
+
*
|
|
49
|
+
* Pure on purpose: it touches no disk and no clock, so what it produces can be compared against
|
|
50
|
+
* the format itself rather than against whatever the machine running it happened to be doing.
|
|
51
|
+
*
|
|
52
|
+
* ⚠ THE VERSION IS IN THE FILENAME, ZERO-PADDED, so a folder holding several copies sorts into
|
|
53
|
+
* the order they were written and the newest is the last one.
|
|
54
|
+
*/
|
|
55
|
+
export function buildFileListFile(input) {
|
|
56
|
+
const doc = {
|
|
57
|
+
format: LIST_FILE_FORMAT,
|
|
58
|
+
version: LIST_FILE_VERSION,
|
|
59
|
+
seq: input.seq,
|
|
60
|
+
...(input.savedAt === undefined ? {} : { saved_at: input.savedAt }),
|
|
61
|
+
account_id: input.accountId,
|
|
62
|
+
sealed: input.sealed,
|
|
63
|
+
note: [...NOTE],
|
|
64
|
+
about: artifactAbout("file-list"),
|
|
65
|
+
};
|
|
66
|
+
const slug = input.accountId.replace(/[^A-Za-z0-9]/g, "").slice(0, 8) || "account";
|
|
67
|
+
return {
|
|
68
|
+
filename: `nmts-file-list-${slug}-${String(input.seq).padStart(4, "0")}.${LIST_FILE_EXTENSION}`,
|
|
69
|
+
content: `${JSON.stringify(doc, null, 2)}\n`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// `--find` — which entries a name query leaves in the listing.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ THE QUERY NAMES FILES. A folder is never matched by it, and a folder whose own name contains
|
|
4
|
+
// the text is not a result. Somebody searching for `report` wants the files called report, and
|
|
5
|
+
// a listing that also printed an empty folder called `reports` would be answering a question
|
|
6
|
+
// nobody asked — worse, it would print a row with nothing under it and no way to tell whether
|
|
7
|
+
// the folder is empty or whether its contents were filtered away.
|
|
8
|
+
//
|
|
9
|
+
// ⛔ BUT A MATCH KEEPS ITS FOLDERS. `ls` prints whole paths, and the folders on the way to a match
|
|
10
|
+
// are part of what that path means; dropping them would leave the listing describing a drive
|
|
11
|
+
// whose shape is not this account's. So the entries that survive are: every file that matches,
|
|
12
|
+
// plus every folder between one of those files and the top of the drive.
|
|
13
|
+
//
|
|
14
|
+
// ⛔ WHICH MEANS: UNDER A QUERY, A FOLDER HOLDING NO MATCH IS NOT PRINTED — an empty folder never
|
|
15
|
+
// is, and neither is a full one whose files all fail the query. That omission is deliberate and
|
|
16
|
+
// it is SAID OUT LOUD by the listing that made it, because a filtered listing that looks like a
|
|
17
|
+
// full one is exactly how somebody concludes a folder was lost.
|
|
18
|
+
//
|
|
19
|
+
// ⚠ FOLDING, NOT BYTES. Both sides are lowercased through the locale before comparing, which is
|
|
20
|
+
// what makes the search case-insensitive in the alphabets that have cases and harmless in the
|
|
21
|
+
// ones that do not. The stored name is never touched: it belongs to whoever wrote it.
|
|
22
|
+
import { KIND_FOLDER } from "./drive-paths.js";
|
|
23
|
+
/** The text to look for, folded. Empty means the option carried nothing to search on. */
|
|
24
|
+
export function needleOf(query) {
|
|
25
|
+
return query.trim().toLocaleLowerCase();
|
|
26
|
+
}
|
|
27
|
+
/** Does this name contain the (already folded) needle? */
|
|
28
|
+
export function nameContains(name, needle) {
|
|
29
|
+
return name.toLocaleLowerCase().includes(needle);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Walk from an entry to the top of the drive, marking every folder on the way as one to keep.
|
|
33
|
+
*
|
|
34
|
+
* ⚠ The `seen` set is not defensive tidiness. A list where two folders are each other's parent
|
|
35
|
+
* would loop here forever, and a list is a document that can be rebuilt by any device and any
|
|
36
|
+
* older build of any of them.
|
|
37
|
+
*/
|
|
38
|
+
function keepAncestors(index, entry, keep) {
|
|
39
|
+
const seen = new Set([entry.id]);
|
|
40
|
+
let parentId = entry.parentId;
|
|
41
|
+
while (parentId !== null && !seen.has(parentId)) {
|
|
42
|
+
seen.add(parentId);
|
|
43
|
+
const parent = index.byId.get(parentId);
|
|
44
|
+
// A broken chain simply stops: the file is printed with the detached mark its path already
|
|
45
|
+
// carries, and nothing above the break is invented.
|
|
46
|
+
if (parent === undefined)
|
|
47
|
+
return;
|
|
48
|
+
keep.add(parent.id);
|
|
49
|
+
parentId = parent.parentId;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The ids to print for one query: the matching files, and the folders that lead to them.
|
|
54
|
+
*
|
|
55
|
+
* `entries` is what the listing was going to show anyway — live only, or everything under `--all`
|
|
56
|
+
* — and the ancestors added here are inside that same set by construction. Being in the trash is
|
|
57
|
+
* INHERITED, so a live file cannot sit under a trashed folder, and with `--all` there is nothing
|
|
58
|
+
* left to be outside the set.
|
|
59
|
+
*/
|
|
60
|
+
export function idsForQuery(index, entries, needle) {
|
|
61
|
+
const keep = new Set();
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (entry.kind === KIND_FOLDER)
|
|
64
|
+
continue;
|
|
65
|
+
if (!nameContains(entry.name, needle))
|
|
66
|
+
continue;
|
|
67
|
+
keep.add(entry.id);
|
|
68
|
+
keepAncestors(index, entry, keep);
|
|
69
|
+
}
|
|
70
|
+
return keep;
|
|
71
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The order a listing comes out in — the browser drive's rule, written out again here.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ IT IS A TRANSCRIPTION, AND THAT IS THE WHOLE POINT. One account is read in a browser and from
|
|
4
|
+
// this tool, often minutes apart. If `--sort size` here and the size order there disagreed
|
|
5
|
+
// about two files of equal size, or about where folders sit, somebody comparing the two screens
|
|
6
|
+
// has no way to tell which of them is lying about their own drive — both are complete, exact
|
|
7
|
+
// listings of the same sealed list. So the rules below are the ones the NMTS web drive sorts
|
|
8
|
+
// by, transcribed in full.
|
|
9
|
+
//
|
|
10
|
+
// ⛔ TRANSCRIBED RATHER THAN IMPORTED. Nothing in this package reads the browser tree: that tree
|
|
11
|
+
// is React-bound and built by a different toolchain, and this tool has to keep running when it
|
|
12
|
+
// is packaged on its own. What holds the two together is that the rule is small enough to write
|
|
13
|
+
// down completely — which is what the block below does.
|
|
14
|
+
//
|
|
15
|
+
// WHAT THE RULE IS, IN FULL:
|
|
16
|
+
// 1. Names compare with the machine's own locale and NUMERIC collation, so `photo 2` comes
|
|
17
|
+
// before `photo 10`. Plain text order puts 10 first, which reads as a defect to anybody who
|
|
18
|
+
// has ever numbered files.
|
|
19
|
+
// 2. A tie on size or date falls back to the name, so two runs over one unchanged list print the
|
|
20
|
+
// same order rather than wobbling.
|
|
21
|
+
// 3. Descending is the EXACT REVERSE of ascending — the tie-break included, and not the
|
|
22
|
+
// comparator with its sign flipped. Flipping the sign would leave the name tie-break running
|
|
23
|
+
// upwards inside a downwards list, so reversing the sort would leave the tied rows sitting in
|
|
24
|
+
// the order they were already in.
|
|
25
|
+
// 4. Folders are their own group, above the files, whichever way the sort runs. The browser
|
|
26
|
+
// draws them as a separate section above the file list; a flat listing has no sections, so
|
|
27
|
+
// the group order is what carries that promise across.
|
|
28
|
+
// 5. Rows carrying the pinned mark are lifted to the top of their group afterwards, keeping the
|
|
29
|
+
// order the sort gave them. That is the only thing pinning is for, and it has to hold under
|
|
30
|
+
// every key and both directions or it means nothing.
|
|
31
|
+
//
|
|
32
|
+
// ⚠ A FOLDER HAS NO SIZE OF ITS OWN — the sealed list stores 0 — so sorting folders by size sorts
|
|
33
|
+
// them by name. That is the honest outcome rather than a gap: a folder's weight is known only by
|
|
34
|
+
// walking it, and inventing one to sort by would be a fabricated number in a column of measured
|
|
35
|
+
// ones.
|
|
36
|
+
//
|
|
37
|
+
// ⚠ ONE DIFFERENCE THAT IS NOT A DIFFERENCE. The browser compares an RFC3339 string; the sealed
|
|
38
|
+
// list holds milliseconds and this compares those. They order the same instants — the string
|
|
39
|
+
// form is zero-padded and in UTC, so text order is chronological order — and they tie on the
|
|
40
|
+
// same rows, because equal milliseconds render as one equal string.
|
|
41
|
+
import { KIND_FOLDER } from "./drive-paths.js";
|
|
42
|
+
import { NmtsError } from "./errors.js";
|
|
43
|
+
/** What `--sort` accepts, in the order the help text names them. */
|
|
44
|
+
export const SORT_KEYS = ["name", "size", "date"];
|
|
45
|
+
/**
|
|
46
|
+
* One of the three keys, or the refusal that names all three.
|
|
47
|
+
*
|
|
48
|
+
* ⛔ IT IS CHECKED BEFORE ANYTHING IS FETCHED. A misspelled key is a wrong command line, not a
|
|
49
|
+
* failing account, and an agent that read "could not list your files" after typing `--sort
|
|
50
|
+
* largest` would go looking at the account. Exit 2 says the same thing every other bad option
|
|
51
|
+
* in this tool says.
|
|
52
|
+
*/
|
|
53
|
+
export function parseSortKey(input) {
|
|
54
|
+
// Compared rather than asserted: the compiler checks that every key in the list is a SortKey,
|
|
55
|
+
// which a cast from `string` would not.
|
|
56
|
+
for (const key of SORT_KEYS)
|
|
57
|
+
if (key === input)
|
|
58
|
+
return key;
|
|
59
|
+
throw new NmtsError(`"${input}" is not something this can sort by.`, {
|
|
60
|
+
exitCode: 2,
|
|
61
|
+
nextStep: `Nothing was listed. Sort by one of: ${SORT_KEYS.join(", ")}.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function byName(a, b) {
|
|
65
|
+
return a.name.localeCompare(b.name, undefined, { numeric: true });
|
|
66
|
+
}
|
|
67
|
+
function comparatorFor(sort) {
|
|
68
|
+
if (sort === "size")
|
|
69
|
+
return (a, b) => a.size - b.size || byName(a, b);
|
|
70
|
+
if (sort === "date")
|
|
71
|
+
return (a, b) => a.createdAt - b.createdAt || byName(a, b);
|
|
72
|
+
return byName;
|
|
73
|
+
}
|
|
74
|
+
/** One group — folders, or files — ordered, reversed if asked, then the pinned rows lifted. */
|
|
75
|
+
function orderGroup(rows, sort, dir) {
|
|
76
|
+
const sorted = [...rows].sort(comparatorFor(sort));
|
|
77
|
+
const ordered = dir === "desc" ? sorted.reverse() : sorted;
|
|
78
|
+
if (!ordered.some((row) => row.pinned === true))
|
|
79
|
+
return ordered;
|
|
80
|
+
return [
|
|
81
|
+
...ordered.filter((row) => row.pinned === true),
|
|
82
|
+
...ordered.filter((row) => row.pinned !== true),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Rows in the browser's order: folders first, then files, each group by the chosen key.
|
|
87
|
+
*
|
|
88
|
+
* The input array is left alone — a caller printing one order and counting over another would
|
|
89
|
+
* otherwise depend on which of the two ran first.
|
|
90
|
+
*/
|
|
91
|
+
export function orderRows(rows, sort, dir) {
|
|
92
|
+
return [
|
|
93
|
+
...orderGroup(rows.filter((row) => row.kind === KIND_FOLDER), sort, dir),
|
|
94
|
+
...orderGroup(rows.filter((row) => row.kind !== KIND_FOLDER), sort, dir),
|
|
95
|
+
];
|
|
96
|
+
}
|