@needmoretruth/nmts-cli 0.38.1 → 0.39.0
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 +14 -35
- package/CHANGELOG.md +9 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/dist/artifact-about.d.ts +1 -1
- package/dist/commands/erase.js +12 -107
- package/dist/drive-erase.d.ts +83 -0
- package/dist/drive-erase.js +161 -0
- package/dist/platform-sign.d.ts +9 -1
- package/dist/platform-sign.js +9 -1
- package/dist/product.d.ts +1 -1
- package/dist/product.js +1 -1
- package/package.json +5 -1
package/AGENTS.md
CHANGED
|
@@ -42,25 +42,12 @@ once, for an unlock (`nmts help login`).
|
|
|
42
42
|
|
|
43
43
|
## What is built on it today
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- **
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
NMTS key is held by the person (the product cannot read the files) or by the product
|
|
52
|
-
(`Nmts.managed`), and every SDK method works the same either way. The
|
|
53
|
-
[terms](https://nmts.me/terms) provide for it in 3.7, for commercial products too.
|
|
54
|
-
- **An S3 endpoint.** `@needmoretruth/nmts-sdk/gateway` speaks S3 with a bucket and a key pair per
|
|
55
|
-
customer, so django-storages, Rails Active Storage, rclone and backup tools store into NMTS
|
|
56
|
-
unchanged.
|
|
57
|
-
- **Files that come back without NMTS.** [nmts-recovery](https://github.com/needmoretruth/nmts-recovery)
|
|
58
|
-
rebuilds an account's files from Walrus with the NMTS key and the recovery list the person saved.
|
|
59
|
-
No NMTS server takes part.
|
|
60
|
-
|
|
61
|
-
An account pays with credits or straight from its own wallet. From the wallet, WAL goes to Walrus
|
|
62
|
-
and SUI to the chain, and NMTS takes no fee. NMTS does not inspect or certify products built with the SDK: who holds the
|
|
63
|
-
keys in such a product is that product's own statement.
|
|
45
|
+
- **Storage for an agent**, outliving a session or a machine: this command, or [its MCP server](#if-your-client-speaks-mcp).
|
|
46
|
+
- **Encrypted storage inside a product.** The [SDK](https://github.com/needmoretruth/nmts-sdk) (`npm install @needmoretruth/nmts-sdk`; it has an `AGENTS.md` of its own) opens an account per user. The user holds the NMTS key, or the product does (`Nmts.managed`); every method works either way, and [terms](https://nmts.me/terms) 3.7 provides for it, commercially too.
|
|
47
|
+
- **An S3 endpoint**, `@needmoretruth/nmts-sdk/gateway`: a bucket and a key pair per customer, so django-storages, Rails Active Storage, rclone and backup tools store into NMTS unchanged.
|
|
48
|
+
- **Files that come back without NMTS.** [nmts-recovery](https://github.com/needmoretruth/nmts-recovery) rebuilds them from Walrus with the NMTS key and the recovery list the person saved. No NMTS server takes part.
|
|
49
|
+
|
|
50
|
+
All of it works with what is published now. An account pays with credits or from its own wallet — WAL to Walrus, SUI to the chain, no fee to NMTS. NMTS does not inspect or certify products built with the SDK: who holds the keys in one is that product's own statement.
|
|
64
51
|
|
|
65
52
|
## What only a person can do, once
|
|
66
53
|
|
|
@@ -189,12 +176,10 @@ work around it. Setup and the sealed-code rule: `nmts help mcp`.
|
|
|
189
176
|
|
|
190
177
|
## Remembering this across sessions
|
|
191
178
|
|
|
192
|
-
You will lose this context. The person has to put it somewhere your next session reads: the file
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
you have not used*; or, for an MCP client, the server entry. The smallest thing that works is one
|
|
197
|
-
line pointing at this file.
|
|
179
|
+
You will lose this context. The person has to put it somewhere your next session reads: the file your client
|
|
180
|
+
loads from a repository root, holding the first section above and the two environment variable names; a saved
|
|
181
|
+
instruction saying *the `nmts` command manages files in an NMTS account; run `nmts env` first, then `nmts --help`,
|
|
182
|
+
and `nmts help <command>` before a command you have not used*; or, for an MCP client, the server entry. One line pointing at this file is the smallest thing that works.
|
|
198
183
|
|
|
199
184
|
## Reporting a problem or an idea
|
|
200
185
|
|
|
@@ -204,17 +189,11 @@ run at all, write to `nmts@nmts.me` with what you ran and what it said.
|
|
|
204
189
|
|
|
205
190
|
## Exit codes
|
|
206
191
|
|
|
207
|
-
`0` done · `1` something went wrong · `2` the command line was wrong · `3` not signed in ·
|
|
208
|
-
|
|
209
|
-
`130` cancelled.
|
|
192
|
+
`0` done · `1` something went wrong · `2` the command line was wrong · `3` not signed in · `4` the command exists
|
|
193
|
+
but could not do it · `5` waiting on the person's agreement · `130` cancelled.
|
|
210
194
|
|
|
211
|
-
## Licence
|
|
195
|
+
## Licence and source
|
|
212
196
|
|
|
213
197
|
Apache-2.0 (it was AGPL-3.0-only until 2026-08-30). **Calling this program from your own code puts
|
|
214
198
|
no obligation on your code at all.** See [LICENSING.md](https://github.com/needmoretruth/nmts-cli/blob/main/LICENSING.md) if you are asked.
|
|
215
|
-
|
|
216
|
-
## Source
|
|
217
|
-
|
|
218
|
-
<https://github.com/needmoretruth/nmts-cli> · The same operations as a library, for a program that calls
|
|
219
|
-
NMTS from its own code: `npm install @needmoretruth/nmts-sdk` · <https://github.com/needmoretruth/nmts-sdk>
|
|
220
|
-
(its own document for agents is `AGENTS.md` there).
|
|
199
|
+
Source: <https://github.com/needmoretruth/nmts-cli>.
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
Each version's entry is what changed for the person or the program using `nmts`. The product's own
|
|
4
4
|
update history, which covers the site and the server too, is at https://nmts.me/updates.
|
|
5
5
|
|
|
6
|
+
## 0.39.0 — 2026-09-20
|
|
7
|
+
|
|
8
|
+
- The library surface has a new entry, `@needmoretruth/nmts-cli/drive-erase`: what `nmts erase` does,
|
|
9
|
+
without the prompt or the printing, so a program can erase files for good with the same order of
|
|
10
|
+
operations. `nmts erase` is unchanged: the same sentence to type, the same output, the same exit
|
|
11
|
+
codes. `nmts-sdk` 0.5.0 is built on it.
|
|
12
|
+
- A delegation token can carry a fifth scope, `files_erase`. The server still asks for the NMTS
|
|
13
|
+
key's own proof on the two erase requests.
|
|
14
|
+
|
|
6
15
|
## 0.38.1 — 2026-09-20
|
|
7
16
|
|
|
8
17
|
- The folder and trash functions behind `@needmoretruth/nmts-cli/drive-edit` no longer import
|
package/README.ko.md
CHANGED
|
@@ -50,7 +50,7 @@ nmts --help
|
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
52
|
npm install -g github:needmoretruth/nmts-cli # 기본 브랜치
|
|
53
|
-
npm install -g github:needmoretruth/nmts-cli#v0.
|
|
53
|
+
npm install -g github:needmoretruth/nmts-cli#v0.39.0 # 버전을 고정할 때
|
|
54
54
|
npm install -g https://github.com/needmoretruth/nmts-cli/releases/latest/download/nmts.tgz
|
|
55
55
|
```
|
|
56
56
|
|
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ default branch, from a pinned version, or from the tarball attached to the
|
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
53
|
npm install -g github:needmoretruth/nmts-cli # the default branch
|
|
54
|
-
npm install -g github:needmoretruth/nmts-cli#v0.
|
|
54
|
+
npm install -g github:needmoretruth/nmts-cli#v0.39.0 # a pinned version
|
|
55
55
|
npm install -g https://github.com/needmoretruth/nmts-cli/releases/latest/download/nmts.tgz
|
|
56
56
|
```
|
|
57
57
|
|
package/dist/artifact-about.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare const CRYPTO_SPEC_URL = "https://github.com/needmoretruth/nmts-re
|
|
|
11
11
|
* is the field's own contract — what the writer says about itself — and a person holding two
|
|
12
12
|
* copies of one account's artefacts can then tell which program made each.
|
|
13
13
|
*/
|
|
14
|
-
export declare const WRITTEN_BY = "nmts-cli 0.
|
|
14
|
+
export declare const WRITTEN_BY = "nmts-cli 0.39.0";
|
|
15
15
|
/** Which of the three artefacts a wrapper is. A reader holding several can sort them. */
|
|
16
16
|
export type ArtifactKind = "recovery-list" | "file-list" | "recovery-kit";
|
|
17
17
|
/**
|
package/dist/commands/erase.js
CHANGED
|
@@ -9,28 +9,21 @@
|
|
|
9
9
|
// with credits, `--release-storage` asks the server to destroy the treasury's storage under
|
|
10
10
|
// each file first, which it does on the chain one blob at a time and reports per file.
|
|
11
11
|
//
|
|
12
|
-
// ⛔ THE
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// ⛔ THE ORDER, AND WHAT IT PROTECTS, ARE IN `drive-erase.ts` — the server first, the list last, a
|
|
13
|
+
// failed release erasing nothing behind it. This file is the terminal over that: the sentence
|
|
14
|
+
// that has to be typed, the lines a person reads, and the exit code. Nothing about what is
|
|
15
|
+
// destroyed is decided twice.
|
|
15
16
|
//
|
|
16
17
|
// ⛔ THE SENTENCE IS TYPED IN EVERY MODE BUT SKIP-PERMISSIONS, where the tier gate's `--reason`
|
|
17
18
|
// and `--yes` stand for it — the same rule as `delete-account`, because it is the same tier.
|
|
18
19
|
import { accountProofFor } from "../account-proof.js";
|
|
19
|
-
import { request, ServerError } from "../api.js";
|
|
20
20
|
import { currentMode } from "../autonomy.js";
|
|
21
21
|
import { CONFIRM_SENTENCE } from "./delete-account.js";
|
|
22
|
-
import {
|
|
22
|
+
import { eraseFiles, planErase } from "../drive-erase.js";
|
|
23
23
|
import { NmtsError } from "../errors.js";
|
|
24
|
-
import { readFileList } from "../manifest.js";
|
|
25
|
-
import { applyToList, batchTargets } from "../manifest-write.js";
|
|
26
24
|
import { BINARY_NAME } from "../product.js";
|
|
27
25
|
import { promptLine, stdinIsATerminal } from "../prompt.js";
|
|
28
26
|
import { openSession } from "../session.js";
|
|
29
|
-
import { filesUnder } from "../drive-edit.js";
|
|
30
|
-
/** The server takes at most this many ids in one erase (`ERASE_BATCH_MAX`). */
|
|
31
|
-
const BATCH = 200;
|
|
32
|
-
/** The refusal a no-deposit release gets when the balance cannot cover the doubled fee. */
|
|
33
|
-
const FEE_INSUFFICIENT = "DEPOSIT_FEE_INSUFFICIENT";
|
|
34
27
|
export async function erase(paths, options = {}) {
|
|
35
28
|
const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
|
|
36
29
|
if (paths.length === 0) {
|
|
@@ -48,20 +41,11 @@ export async function erase(paths, options = {}) {
|
|
|
48
41
|
});
|
|
49
42
|
}
|
|
50
43
|
const session = await openSession(options);
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const index = buildIndex(entries);
|
|
54
|
-
const targets = batchTargets(entries, paths, { includeTrashed: true, nothingHappened: "Nothing was erased." });
|
|
55
|
-
const files = uniqueById(targets.flatMap((t) => (t.kind === KIND_FILE ? [t] : filesUnder(entries, t.id))));
|
|
56
|
-
const going = uniqueById([...targets, ...files]);
|
|
57
|
-
if (files.length === 0) {
|
|
58
|
-
throw new NmtsError(`Nothing named holds a file; empty folders are removed with \`${BINARY_NAME} rm\`.`, {
|
|
59
|
-
exitCode: 4,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
44
|
+
const plan = await planErase(session, paths);
|
|
45
|
+
const files = plan.files;
|
|
62
46
|
say(`This erases ${files.length} file${files.length === 1 ? "" : "s"} for good. It cannot be undone, and not by the trash.`);
|
|
63
47
|
for (const f of files)
|
|
64
|
-
say(` ${
|
|
48
|
+
say(` ${f.path}`);
|
|
65
49
|
say(``);
|
|
66
50
|
say(` Erased: the server's record of each file and this account's key to it, and its shares.`);
|
|
67
51
|
if (options.releaseStorage === true) {
|
|
@@ -81,65 +65,12 @@ export async function erase(paths, options = {}) {
|
|
|
81
65
|
}
|
|
82
66
|
// ⛔ THE PROOF IS BUILT FOR THIS ONE RUN AND NOTHING KEEPS IT.
|
|
83
67
|
const accountProof = await accountProofFor({ code: session.code, source: session.source });
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
for (const f of files) {
|
|
87
|
-
const path = fullPathOf(index, f);
|
|
88
|
-
try {
|
|
89
|
-
const reply = await request(session.server, `/v1/items/${encodeURIComponent(f.id)}/release-storage`, {
|
|
90
|
-
method: "POST",
|
|
91
|
-
body: {},
|
|
92
|
-
token: session.apiKey,
|
|
93
|
-
accountProof,
|
|
94
|
-
});
|
|
95
|
-
releases.push({ path, refused: null, ...counts(reply), ...fee(reply) });
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
// ⛔ THE ONE REFUSAL THAT STOPS THE RUN. A file with no deposit pays twice the chain fee
|
|
99
|
-
// out of the balance, and a balance that cannot cover it means the release did not
|
|
100
|
-
// happen — erasing behind it would destroy the account's key to bytes that are still
|
|
101
|
-
// being served and still being paid for. Nothing is erased, and the two numbers say
|
|
102
|
-
// exactly how far short the balance is.
|
|
103
|
-
if (error instanceof ServerError && error.code === FEE_INSUFFICIENT) {
|
|
104
|
-
throw new NmtsError(`${path}: releasing its storage costs ${amount(error, "needed_credits")} credits and this ` +
|
|
105
|
-
`account has ${amount(error, "balance_credits")}. It has no deposit, so the fee comes out of the balance.`, {
|
|
106
|
-
exitCode: 4,
|
|
107
|
-
nextStep: `Nothing was erased. Buy credits and run this again, or leave --release-storage off ` +
|
|
108
|
-
`to erase the file and let its storage run out on its own.`,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
// ⚠ "Not ours to destroy" is an answer, not a failure: the storage was bought by the
|
|
112
|
-
// wallet, and the erase goes on. A refusal of the KEY or the proof, and anything the
|
|
113
|
-
// server could not do, stops the run before a row is touched.
|
|
114
|
-
if (error instanceof ServerError && error.status !== 401 && error.status !== 403 && error.status < 500) {
|
|
115
|
-
releases.push({ path, released: 0, alreadyReleased: 0, failed: 0, feeCredits: 0, fromDeposit: false, refused: error.message });
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
let erased = 0;
|
|
123
|
-
for (let i = 0; i < files.length; i += BATCH) {
|
|
124
|
-
const reply = await request(session.server, "/v1/items/erase", {
|
|
125
|
-
method: "POST",
|
|
126
|
-
body: { item_ids: files.slice(i, i + BATCH).map((f) => f.id) },
|
|
127
|
-
token: session.apiKey,
|
|
128
|
-
accountProof,
|
|
129
|
-
});
|
|
130
|
-
erased += typeof reply === "object" && reply !== null && typeof Reflect.get(reply, "erased") === "number"
|
|
131
|
-
? Number(Reflect.get(reply, "erased"))
|
|
132
|
-
: 0;
|
|
133
|
-
}
|
|
134
|
-
// ⛔ THE LIST GOES LAST, and re-decided against the list as it is on this attempt: only the
|
|
135
|
-
// ids this run erased leave it, whatever another device wrote in between.
|
|
136
|
-
const ids = new Set(going.map((e) => e.id));
|
|
137
|
-
const result = await applyToList(session, (current) => {
|
|
138
|
-
const still = current.filter((e) => ids.has(e.id)).map((e) => e.id);
|
|
139
|
-
return still.length === 0 ? null : { op: "purge", ids: still };
|
|
68
|
+
const outcome = await eraseFiles({ ...session, accountProof }, plan, {
|
|
69
|
+
releaseStorage: options.releaseStorage === true,
|
|
140
70
|
});
|
|
71
|
+
const { erased, releases } = outcome;
|
|
141
72
|
if (options.json) {
|
|
142
|
-
say(JSON.stringify({ erased, files: files
|
|
73
|
+
say(JSON.stringify({ erased, files: outcome.files, releases, seq: outcome.seq }));
|
|
143
74
|
return 0;
|
|
144
75
|
}
|
|
145
76
|
say(`Erased ${erased} file${erased === 1 ? "" : "s"}. Their entries are out of the file list.`);
|
|
@@ -165,29 +96,3 @@ function feeLine(r) {
|
|
|
165
96
|
? `${credits} taken from that file's deposit — nothing came out of the balance`
|
|
166
97
|
: `${credits} taken from the balance — that file had no deposit, so the fee is doubled`;
|
|
167
98
|
}
|
|
168
|
-
/** The three counts a release answers with, read defensively. */
|
|
169
|
-
function counts(reply) {
|
|
170
|
-
const n = (name) => {
|
|
171
|
-
const v = typeof reply === "object" && reply !== null ? Reflect.get(reply, name) : undefined;
|
|
172
|
-
return typeof v === "number" ? v : 0;
|
|
173
|
-
};
|
|
174
|
-
return { released: n("released"), alreadyReleased: n("already_released"), failed: n("failed") };
|
|
175
|
-
}
|
|
176
|
-
/** What it cost, read the same defensive way. An older server says neither, which reads as 0. */
|
|
177
|
-
function fee(reply) {
|
|
178
|
-
const at = (name) => typeof reply === "object" && reply !== null ? Reflect.get(reply, name) : undefined;
|
|
179
|
-
const charged = at("fee_credits");
|
|
180
|
-
return {
|
|
181
|
-
feeCredits: typeof charged === "number" ? charged : 0,
|
|
182
|
-
fromDeposit: at("from_deposit") === true,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
/** One credit amount out of a refusal's details, or `?` when the server did not name it. */
|
|
186
|
-
function amount(error, field) {
|
|
187
|
-
const value = error.details[field];
|
|
188
|
-
return typeof value === "number" ? String(value) : "?";
|
|
189
|
-
}
|
|
190
|
-
function uniqueById(list) {
|
|
191
|
-
const seen = new Set();
|
|
192
|
-
return list.filter((e) => (seen.has(e.id) ? false : (seen.add(e.id), true)));
|
|
193
|
-
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ListEditInput } from "./manifest-write.ts";
|
|
2
|
+
/** What the erasing underneath takes: where to talk, what opens the list, and whose list it is. */
|
|
3
|
+
export interface EraseInput extends ListEditInput {
|
|
4
|
+
/**
|
|
5
|
+
* The account code's own proof for this one run, base64url — what the two permanent doors ask
|
|
6
|
+
* for beside the credential (`x-nmts-account-proof`).
|
|
7
|
+
*
|
|
8
|
+
* ⛔ BUILT BY THE CALLER AND KEPT BY NOBODY. It is not the NMTS key and opens no file; what it
|
|
9
|
+
* proves is possession of the code, which is exactly the question these two doors ask.
|
|
10
|
+
*/
|
|
11
|
+
accountProof: string;
|
|
12
|
+
}
|
|
13
|
+
export interface EraseOptions {
|
|
14
|
+
/** Also destroy the treasury's storage under credit-paid files, before erasing them. */
|
|
15
|
+
releaseStorage?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** One thing this run acts on: its id, and where it sits in the list as it was read. */
|
|
18
|
+
export interface ErasePath {
|
|
19
|
+
id: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
/** What one run will destroy, worked out before anything is sent. */
|
|
23
|
+
export interface ErasePlan {
|
|
24
|
+
/** Every FILE going: the ones named, and every file under a folder that was named. */
|
|
25
|
+
readonly files: readonly ErasePath[];
|
|
26
|
+
/**
|
|
27
|
+
* The ids leaving the sealed list — the files above and the folders that were named.
|
|
28
|
+
*
|
|
29
|
+
* ⚠ WIDER THAN `files` ON PURPOSE. A named folder has no server row of its own, so nothing is
|
|
30
|
+
* erased for it; its entry still has to go, or the list keeps a folder whose contents are gone.
|
|
31
|
+
*/
|
|
32
|
+
readonly going: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/** What one file's storage release came back with. */
|
|
35
|
+
export interface StorageRelease {
|
|
36
|
+
path: string;
|
|
37
|
+
released: number;
|
|
38
|
+
alreadyReleased: number;
|
|
39
|
+
failed: number;
|
|
40
|
+
/**
|
|
41
|
+
* Credits the release actually cost, and where they came from.
|
|
42
|
+
*
|
|
43
|
+
* ⛔ TWO FIELDS, NOT ONE, because "it cost nothing" and "it cost nothing OUT OF THE BALANCE" are
|
|
44
|
+
* different answers and only one of them is true. A release paid out of the file's own
|
|
45
|
+
* deposit charges the balance nothing; a file with no deposit pays twice the fee from the
|
|
46
|
+
* balance, and somebody watching their credits needs to be able to tell which happened.
|
|
47
|
+
*/
|
|
48
|
+
feeCredits: number;
|
|
49
|
+
fromDeposit: boolean;
|
|
50
|
+
/** The server's typed refusal when the storage was not the treasury's to destroy. */
|
|
51
|
+
refused: string | null;
|
|
52
|
+
}
|
|
53
|
+
/** What one run did. */
|
|
54
|
+
export interface EraseOutcome {
|
|
55
|
+
/** How many server rows went. Lower than what was asked for when one was already gone. */
|
|
56
|
+
erased: number;
|
|
57
|
+
/** The files it acted on, with the paths they had. */
|
|
58
|
+
files: ErasePath[];
|
|
59
|
+
/** One per file whose storage was asked about — empty unless `releaseStorage` was asked for. */
|
|
60
|
+
releases: StorageRelease[];
|
|
61
|
+
/** The version of the sealed list after the entries left it. */
|
|
62
|
+
seq: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Work out what erasing these paths would destroy, without destroying anything.
|
|
66
|
+
*
|
|
67
|
+
* ⛔ ITS OWN STEP BECAUSE ONE CALLER HAS TO SHOW THE LIST BEFORE IT ASKS. `nmts erase` prints every
|
|
68
|
+
* file it is about to destroy and then waits for a typed sentence; folding this into the act
|
|
69
|
+
* would leave the terminal with nothing to print, and reading the list twice would leave the
|
|
70
|
+
* two reads free to disagree about what is in it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function planErase(input: ListEditInput, paths: readonly string[]): Promise<ErasePlan>;
|
|
73
|
+
/**
|
|
74
|
+
* Destroy what the plan names. ⛔ Irreversible, and nothing below asks whether the caller meant it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function eraseFiles(input: EraseInput, plan: ErasePlan, options?: EraseOptions): Promise<EraseOutcome>;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve these paths and destroy what they name, in one call.
|
|
79
|
+
*
|
|
80
|
+
* ⚠ FOR A CALLER THAT HAS ALREADY DECIDED. Anything that shows a person what is about to go should
|
|
81
|
+
* use `planErase` first, so what it shows is what it then destroys.
|
|
82
|
+
*/
|
|
83
|
+
export declare function erasePaths(input: EraseInput, paths: readonly string[], options?: EraseOptions): Promise<EraseOutcome>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Erasing files for good, without a terminal: the targets resolved, the treasury's storage let go,
|
|
2
|
+
// the server's records destroyed and the entries taken out of the sealed list — decided, done and
|
|
3
|
+
// handed back rather than printed.
|
|
4
|
+
//
|
|
5
|
+
// ⛔ ONE IMPLEMENTATION, TWO CALLERS, WHICH IS THE WHOLE REASON THIS FILE EXISTS — the argument
|
|
6
|
+
// `drive-edit.ts` makes one verb family over. `commands/erase.ts` is a terminal's shape: it
|
|
7
|
+
// prints the list, asks for the typed sentence and answers an exit code. The SDK is somebody
|
|
8
|
+
// else's program and needs the same act with none of those. A second implementation of "what
|
|
9
|
+
// order do these three destructions happen in" would be a second place for the rules below to
|
|
10
|
+
// be got right, and the copy nobody re-reads is the one that quietly disagrees — about the one
|
|
11
|
+
// verb in this package that nothing can undo.
|
|
12
|
+
//
|
|
13
|
+
// ⛔ NOTHING HERE WRITES TO A STREAM, PICKS AN EXIT CODE OR ASKS ANYBODY ANYTHING. The confirmation
|
|
14
|
+
// is the caller's: a terminal types a sentence, a program passes one. This file is reached only
|
|
15
|
+
// after somebody decided, and it destroys what it is given.
|
|
16
|
+
//
|
|
17
|
+
// ⛔ THE SERVER GOES FIRST AND THE LIST LAST, which is the order the trash keeps and for the same
|
|
18
|
+
// reason: a row erased before its list entry leaves a file the person can see and never open.
|
|
19
|
+
// A release that FAILS stops the run before a single row is touched — nothing is erased behind
|
|
20
|
+
// one. A release the server REFUSES ("that storage was bought by the wallet, not by credits")
|
|
21
|
+
// is an answer rather than a failure: it is reported on that file and the erase goes on.
|
|
22
|
+
//
|
|
23
|
+
// ⛔ THE PROOF IS THE CALLER'S TO BUILD AND IS NOT DERIVED HERE. Both doors ask for the account
|
|
24
|
+
// code's own proof, and WHETHER this run may make one is a policy question with two different
|
|
25
|
+
// answers — the command line asks for an agreement first (`account-proof.ts`), a library caller
|
|
26
|
+
// has already made that decision by calling. So it arrives as a value. Nothing here reaches for
|
|
27
|
+
// `node:`: the SDK's browser entry bundles what this exports.
|
|
28
|
+
//
|
|
29
|
+
// ⚠ THE PIECES IT BORROWS COME FROM `drive-edit/` DIRECTLY, the way that folder's own files borrow
|
|
30
|
+
// from each other. This is a sibling of that family rather than an outside caller.
|
|
31
|
+
import { request, ServerError } from "./api.js";
|
|
32
|
+
import { DriveEditError, resolving } from "./drive-edit/errors.js";
|
|
33
|
+
import { filesUnder, uniqueById } from "./drive-edit/tree.js";
|
|
34
|
+
import { buildIndex, fullPathOf, KIND_FILE } from "./drive-paths.js";
|
|
35
|
+
import { NmtsError } from "./errors.js";
|
|
36
|
+
import { readFileList } from "./manifest.js";
|
|
37
|
+
import { applyToList, batchTargets } from "./manifest-write.js";
|
|
38
|
+
import { BINARY_NAME } from "./product.js";
|
|
39
|
+
/** The server takes at most this many ids in one erase (`ERASE_BATCH_MAX`). */
|
|
40
|
+
const BATCH = 200;
|
|
41
|
+
/** The refusal a no-deposit release gets when the balance cannot cover the doubled fee. */
|
|
42
|
+
const FEE_INSUFFICIENT = "DEPOSIT_FEE_INSUFFICIENT";
|
|
43
|
+
/**
|
|
44
|
+
* Work out what erasing these paths would destroy, without destroying anything.
|
|
45
|
+
*
|
|
46
|
+
* ⛔ ITS OWN STEP BECAUSE ONE CALLER HAS TO SHOW THE LIST BEFORE IT ASKS. `nmts erase` prints every
|
|
47
|
+
* file it is about to destroy and then waits for a typed sentence; folding this into the act
|
|
48
|
+
* would leave the terminal with nothing to print, and reading the list twice would leave the
|
|
49
|
+
* two reads free to disagree about what is in it.
|
|
50
|
+
*/
|
|
51
|
+
export async function planErase(input, paths) {
|
|
52
|
+
const list = await readFileList(input.server, input.apiKey, input.code, input.accountId);
|
|
53
|
+
const entries = list.manifest?.entries ?? [];
|
|
54
|
+
const index = buildIndex(entries);
|
|
55
|
+
const targets = resolving(() => batchTargets(entries, paths, { includeTrashed: true, nothingHappened: "Nothing was erased." }));
|
|
56
|
+
const files = uniqueById(targets.flatMap((t) => (t.kind === KIND_FILE ? [t] : filesUnder(entries, t.id))));
|
|
57
|
+
const going = uniqueById([...targets, ...files]);
|
|
58
|
+
if (files.length === 0) {
|
|
59
|
+
throw new DriveEditError("NOT_FOUND", `Nothing named holds a file; empty folders are removed with \`${BINARY_NAME} rm\`.`, {
|
|
60
|
+
exitCode: 4,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
files: files.map((f) => ({ id: f.id, path: fullPathOf(index, f) })),
|
|
65
|
+
going: going.map((e) => e.id),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Destroy what the plan names. ⛔ Irreversible, and nothing below asks whether the caller meant it.
|
|
70
|
+
*/
|
|
71
|
+
export async function eraseFiles(input, plan, options = {}) {
|
|
72
|
+
const releases = [];
|
|
73
|
+
if (options.releaseStorage === true) {
|
|
74
|
+
for (const f of plan.files) {
|
|
75
|
+
const path = f.path;
|
|
76
|
+
try {
|
|
77
|
+
const reply = await request(input.server, `/v1/items/${encodeURIComponent(f.id)}/release-storage`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: {},
|
|
80
|
+
token: input.apiKey,
|
|
81
|
+
accountProof: input.accountProof,
|
|
82
|
+
});
|
|
83
|
+
releases.push({ path, refused: null, ...counts(reply), ...fee(reply) });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// ⛔ THE ONE REFUSAL THAT STOPS THE RUN. A file with no deposit pays twice the chain fee
|
|
87
|
+
// out of the balance, and a balance that cannot cover it means the release did not
|
|
88
|
+
// happen — erasing behind it would destroy the account's key to bytes that are still
|
|
89
|
+
// being served and still being paid for. Nothing is erased, and the two numbers say
|
|
90
|
+
// exactly how far short the balance is.
|
|
91
|
+
if (error instanceof ServerError && error.code === FEE_INSUFFICIENT) {
|
|
92
|
+
throw new NmtsError(`${path}: releasing its storage costs ${amount(error, "needed_credits")} credits and this ` +
|
|
93
|
+
`account has ${amount(error, "balance_credits")}. It has no deposit, so the fee comes out of the balance.`, {
|
|
94
|
+
exitCode: 4,
|
|
95
|
+
nextStep: `Nothing was erased. Buy credits and run this again, or leave --release-storage off ` +
|
|
96
|
+
`to erase the file and let its storage run out on its own.`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// ⚠ "Not ours to destroy" is an answer, not a failure: the storage was bought by the
|
|
100
|
+
// wallet, and the erase goes on. A refusal of the KEY or the proof, and anything the
|
|
101
|
+
// server could not do, stops the run before a row is touched.
|
|
102
|
+
if (error instanceof ServerError && error.status !== 401 && error.status !== 403 && error.status < 500) {
|
|
103
|
+
releases.push({ path, released: 0, alreadyReleased: 0, failed: 0, feeCredits: 0, fromDeposit: false, refused: error.message });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let erased = 0;
|
|
111
|
+
for (let i = 0; i < plan.files.length; i += BATCH) {
|
|
112
|
+
const reply = await request(input.server, "/v1/items/erase", {
|
|
113
|
+
method: "POST",
|
|
114
|
+
body: { item_ids: plan.files.slice(i, i + BATCH).map((f) => f.id) },
|
|
115
|
+
token: input.apiKey,
|
|
116
|
+
accountProof: input.accountProof,
|
|
117
|
+
});
|
|
118
|
+
erased += typeof reply === "object" && reply !== null && typeof Reflect.get(reply, "erased") === "number"
|
|
119
|
+
? Number(Reflect.get(reply, "erased"))
|
|
120
|
+
: 0;
|
|
121
|
+
}
|
|
122
|
+
// ⛔ THE LIST GOES LAST, and re-decided against the list as it is on this attempt: only the
|
|
123
|
+
// ids this run erased leave it, whatever another device wrote in between.
|
|
124
|
+
const ids = new Set(plan.going);
|
|
125
|
+
const result = await applyToList(input, (current) => {
|
|
126
|
+
const still = current.filter((e) => ids.has(e.id)).map((e) => e.id);
|
|
127
|
+
return still.length === 0 ? null : { op: "purge", ids: still };
|
|
128
|
+
});
|
|
129
|
+
return { erased, files: [...plan.files], releases, seq: result.seq };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve these paths and destroy what they name, in one call.
|
|
133
|
+
*
|
|
134
|
+
* ⚠ FOR A CALLER THAT HAS ALREADY DECIDED. Anything that shows a person what is about to go should
|
|
135
|
+
* use `planErase` first, so what it shows is what it then destroys.
|
|
136
|
+
*/
|
|
137
|
+
export async function erasePaths(input, paths, options = {}) {
|
|
138
|
+
return eraseFiles(input, await planErase(input, paths), options);
|
|
139
|
+
}
|
|
140
|
+
/** The three counts a release answers with, read defensively. */
|
|
141
|
+
function counts(reply) {
|
|
142
|
+
const n = (name) => {
|
|
143
|
+
const v = typeof reply === "object" && reply !== null ? Reflect.get(reply, name) : undefined;
|
|
144
|
+
return typeof v === "number" ? v : 0;
|
|
145
|
+
};
|
|
146
|
+
return { released: n("released"), alreadyReleased: n("already_released"), failed: n("failed") };
|
|
147
|
+
}
|
|
148
|
+
/** What it cost, read the same defensive way. An older server says neither, which reads as 0. */
|
|
149
|
+
function fee(reply) {
|
|
150
|
+
const at = (name) => typeof reply === "object" && reply !== null ? Reflect.get(reply, name) : undefined;
|
|
151
|
+
const charged = at("fee_credits");
|
|
152
|
+
return {
|
|
153
|
+
feeCredits: typeof charged === "number" ? charged : 0,
|
|
154
|
+
fromDeposit: at("from_deposit") === true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/** One credit amount out of a refusal's details, or `?` when the server did not name it. */
|
|
158
|
+
function amount(error, field) {
|
|
159
|
+
const value = error.details[field];
|
|
160
|
+
return typeof value === "number" ? String(value) : "?";
|
|
161
|
+
}
|
package/dist/platform-sign.d.ts
CHANGED
|
@@ -25,14 +25,22 @@ export declare const DELEGATION_MAX_TTL_SECS = 2592000;
|
|
|
25
25
|
* ⛔ THE FIRST THREE ARE AN API KEY'S OWN SCOPES, value for value. One vocabulary, so that a
|
|
26
26
|
* business asking for "read and write" asks for the number a person's key already means by it.
|
|
27
27
|
* `register` is the fourth and opens exactly one door: making the account the token names.
|
|
28
|
+
*
|
|
29
|
+
* ⛔ `files_erase` IS THE FIFTH, AND AN API KEY HAS NO BIT OF THAT NAME. It opens the two acts
|
|
30
|
+
* nothing undoes: erasing a file's record and this account's key to it, and destroying the
|
|
31
|
+
* treasury's storage under a file. Its own name rather than `files_write`, so a token minted so
|
|
32
|
+
* an app can upload cannot destroy — and it does not stand alone: both doors also ask for the
|
|
33
|
+
* account code's own proof on the same request, which is an act only whoever holds the code can
|
|
34
|
+
* perform. The token is the business's permission; the proof is the key holder's.
|
|
28
35
|
*/
|
|
29
36
|
export declare const SCOPE_BITS: {
|
|
30
37
|
readonly files_read: 1;
|
|
31
38
|
readonly files_write: 2;
|
|
32
39
|
readonly storage_spend: 4;
|
|
33
40
|
readonly register: 8;
|
|
41
|
+
readonly files_erase: 16;
|
|
34
42
|
};
|
|
35
|
-
/** One of the
|
|
43
|
+
/** One of the five names above. */
|
|
36
44
|
export type ScopeName = keyof typeof SCOPE_BITS;
|
|
37
45
|
/** Every bit a token may carry. */
|
|
38
46
|
export declare const SCOPE_ALL: number;
|
package/dist/platform-sign.js
CHANGED
|
@@ -53,15 +53,23 @@ export const DELEGATION_MAX_TTL_SECS = 2_592_000;
|
|
|
53
53
|
* ⛔ THE FIRST THREE ARE AN API KEY'S OWN SCOPES, value for value. One vocabulary, so that a
|
|
54
54
|
* business asking for "read and write" asks for the number a person's key already means by it.
|
|
55
55
|
* `register` is the fourth and opens exactly one door: making the account the token names.
|
|
56
|
+
*
|
|
57
|
+
* ⛔ `files_erase` IS THE FIFTH, AND AN API KEY HAS NO BIT OF THAT NAME. It opens the two acts
|
|
58
|
+
* nothing undoes: erasing a file's record and this account's key to it, and destroying the
|
|
59
|
+
* treasury's storage under a file. Its own name rather than `files_write`, so a token minted so
|
|
60
|
+
* an app can upload cannot destroy — and it does not stand alone: both doors also ask for the
|
|
61
|
+
* account code's own proof on the same request, which is an act only whoever holds the code can
|
|
62
|
+
* perform. The token is the business's permission; the proof is the key holder's.
|
|
56
63
|
*/
|
|
57
64
|
export const SCOPE_BITS = {
|
|
58
65
|
files_read: 1,
|
|
59
66
|
files_write: 2,
|
|
60
67
|
storage_spend: 4,
|
|
61
68
|
register: 8,
|
|
69
|
+
files_erase: 16,
|
|
62
70
|
};
|
|
63
71
|
/** Every bit a token may carry. */
|
|
64
|
-
export const SCOPE_ALL = SCOPE_BITS.files_read | SCOPE_BITS.files_write | SCOPE_BITS.storage_spend | SCOPE_BITS.register;
|
|
72
|
+
export const SCOPE_ALL = SCOPE_BITS.files_read | SCOPE_BITS.files_write | SCOPE_BITS.storage_spend | SCOPE_BITS.register | SCOPE_BITS.files_erase;
|
|
65
73
|
/**
|
|
66
74
|
* A new key pair, from the runtime's own random source.
|
|
67
75
|
*
|
package/dist/product.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare const BINARY_NAME = "nmts";
|
|
|
9
9
|
* beside it. Here rather than in `main.ts` because the MCP server has to say it too, and a
|
|
10
10
|
* command importing the entry point is a cycle waiting to bite.
|
|
11
11
|
*/
|
|
12
|
-
export declare const VERSION = "0.
|
|
12
|
+
export declare const VERSION = "0.39.0";
|
|
13
13
|
/** Where the product lives, for messages that need to send somebody somewhere real. */
|
|
14
14
|
export declare const HOME_URL = "https://nmts.me";
|
|
15
15
|
/** The source, so a person holding only the built program can find what it was built from. */
|
package/dist/product.js
CHANGED
|
@@ -18,7 +18,7 @@ export const BINARY_NAME = "nmts";
|
|
|
18
18
|
* beside it. Here rather than in `main.ts` because the MCP server has to say it too, and a
|
|
19
19
|
* command importing the entry point is a cycle waiting to bite.
|
|
20
20
|
*/
|
|
21
|
-
export const VERSION = "0.
|
|
21
|
+
export const VERSION = "0.39.0";
|
|
22
22
|
/** Where the product lives, for messages that need to send somebody somewhere real. */
|
|
23
23
|
export const HOME_URL = "https://nmts.me";
|
|
24
24
|
/** The source, so a person holding only the built program can find what it was built from. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needmoretruth/nmts-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Command-line and MCP access to NMTS (NeedMoreTruthStorage, https://nmts.me): open-source, end-to-end encrypted storage on the Walrus network. No fee to NMTS; you pay the network from your own wallet. For people and for their agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|
|
@@ -81,6 +81,10 @@
|
|
|
81
81
|
"types": "./dist/drive-edit.d.ts",
|
|
82
82
|
"default": "./dist/drive-edit.js"
|
|
83
83
|
},
|
|
84
|
+
"./drive-erase": {
|
|
85
|
+
"types": "./dist/drive-erase.d.ts",
|
|
86
|
+
"default": "./dist/drive-erase.js"
|
|
87
|
+
},
|
|
84
88
|
"./s3-gateway": {
|
|
85
89
|
"types": "./dist/s3-gateway.d.ts",
|
|
86
90
|
"default": "./dist/s3-gateway.js"
|