@needmoretruth/nmts-sdk 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ // Turning a root and the two address options into an opened account: who it is, which server,
2
+ // which network.
3
+ //
4
+ // ⛔ NOTHING HERE READS THE ENVIRONMENT OR ASKS A PERSON. The command-line tool's session module
5
+ // does both, because a terminal is where a person is; a library runs inside somebody else's
6
+ // server, where a prompt hangs and an environment read is a decision the caller did not make.
7
+ // `env.ts` is the one place this package reads the environment, and only when asked to.
8
+ //
9
+ // ⛔ THE CODE IS BORROWED, NOT HELD. `Opened` carries the ROOT, not the key, so nothing below this
10
+ // line changes when the key moves from a person's device to a business's sealed store.
11
+ // `withAccount` is the one door through which a verb borrows it, which is why "one call, one
12
+ // opening" is a number a test can read off a managed root — and why nothing this package keeps
13
+ // between calls can hold a code.
14
+ //
15
+ // ⛔ THE ACCOUNT CODE IS HELD, NOT COPIED, for the length of the work. It is the only key to the
16
+ // account. Every derivation below takes it, uses the bytes it needs, and wipes the rest before
17
+ // returning — the same discipline the command-line tool keeps, for the same reason: a live copy
18
+ // of the derived buffer is a live copy of the account.
19
+ import { DERIVED, identityOf, loadCrypto, resolveNetwork, resolveServer, } from "@needmoretruth/nmts-cli";
20
+ import { requireText } from "./root.js";
21
+ /**
22
+ * Check what can be checked without the code, and work out where this account talks.
23
+ *
24
+ * The key is only checked for being present; the server is what judges a key, on the first request.
25
+ * Nothing here borrows the code, so a caller who got the key wrong is refused without a business's
26
+ * store having been opened for nothing.
27
+ */
28
+ export function openAccount(root, options = {}) {
29
+ const apiKey = requireText(root.identity.apiKey, "API key", "Make one on the account screen at nmts.me and pass it as `apiKey`, or set NMTS_API_KEY_FILE.");
30
+ const server = resolveServer(options.server);
31
+ return { root, apiKey, server, network: resolveNetwork(server, options.network) };
32
+ }
33
+ /**
34
+ * Borrow the account's code for the length of `body`, and work out the account it names.
35
+ *
36
+ * ⛔ ONE CALL, ONE BORROW. Every verb in this package wraps the whole of itself in this, so a
37
+ * managed root is asked to open its sealed code exactly once however many requests the verb makes.
38
+ *
39
+ * ⛔ THE ACCOUNT ID IS DERIVED HERE RATHER THAN KEPT, because deriving it is the only check there
40
+ * is that a code is one: nothing about a string says which account it opens until the engine has
41
+ * parsed it. It is a pure function of the code, and the code is only here.
42
+ */
43
+ export async function withAccount(opened, body) {
44
+ return opened.root.withCode(async (code) => {
45
+ const identity = await identityOf(code);
46
+ return body({
47
+ code,
48
+ apiKey: opened.apiKey,
49
+ server: opened.server,
50
+ network: opened.network,
51
+ accountId: identity.accountId,
52
+ });
53
+ });
54
+ }
55
+ /**
56
+ * Run `body` with the account's data key, then wipe it.
57
+ *
58
+ * The data key wraps every file key in the account. It exists for exactly as long as the work
59
+ * that needs it, and the derivation output it came out of — which holds every other key too — is
60
+ * zeroed before `body` even starts.
61
+ */
62
+ export async function withDataKey(code, body) {
63
+ const crypt = await loadCrypto();
64
+ const [from, to] = DERIVED.dataKey;
65
+ const derived = crypt.kdf_derive(crypt.account_code_parse(code));
66
+ const dataKey = derived.slice(from, to);
67
+ derived.fill(0);
68
+ try {
69
+ return await body(crypt, dataKey);
70
+ }
71
+ finally {
72
+ dataKey.fill(0);
73
+ }
74
+ }
@@ -0,0 +1,63 @@
1
+ import { type ChainReader, type Network } from "@needmoretruth/nmts-cli";
2
+ import { type Held, type Opened } from "./session.ts";
3
+ /** One of the wallets this account's NMTS key opens. */
4
+ export interface WalletInfo {
5
+ /** The index the key derives it at. 0 is the wallet every account starts with. */
6
+ index: number;
7
+ /** Its Sui address — the same address on every network. */
8
+ address: string;
9
+ /** Is this the wallet storage is paid from? Exactly one wallet of an account is. */
10
+ active: boolean;
11
+ }
12
+ /** What `setActiveWallet()` answers: the wallet that pays now, and whether this call moved it. */
13
+ export interface ActiveWallet {
14
+ index: number;
15
+ address: string;
16
+ /** False when the account already paid from this wallet, so nothing was written. */
17
+ changed: boolean;
18
+ }
19
+ /**
20
+ * What the walk asks a chain. ⚠ Seams, not options: no caller of `wallets()` reaches them, and a
21
+ * test hands in a chain that answers from a table exactly as the command-line tool's own do.
22
+ */
23
+ export interface WalletReads {
24
+ openChain?: ((network: Network, address: string) => ChainReader | Promise<ChainReader>) | undefined;
25
+ hasHistory?: ((network: Network, address: string) => Promise<boolean>) | undefined;
26
+ }
27
+ /**
28
+ * A wallet number as a caller wrote it.
29
+ *
30
+ * ⛔ REFUSED, NEVER ROUNDED. `1.5` and `-1` are calls to correct; taking either of them to a
31
+ * neighbouring wallet would pay from an address nobody named. The write path below clamps, which
32
+ * is right for a slider and wrong for an argument, so the refusal happens before it.
33
+ */
34
+ export declare function requireWalletIndex(value: number): number;
35
+ /**
36
+ * Which wallet this account pays from, out of its sealed list.
37
+ *
38
+ * An account that has never written the setting pays from the first wallet — that is a READ
39
+ * answer and not a fallback: the list opened, and it said nothing about a wallet.
40
+ */
41
+ export declare function payingWallet(held: Held): Promise<number>;
42
+ /**
43
+ * Every wallet this account has, in index order: the ones it has made, and any further out that a
44
+ * chain says have been used.
45
+ *
46
+ * Nothing is spent and nothing is written. The walk costs one balance read per number it asks
47
+ * about, plus a transaction question for a wallet that holds neither coin — a wallet somebody
48
+ * emptied is in use too, and a walk that asked about balances alone would report it as one that
49
+ * does not exist.
50
+ *
51
+ * ⚠ A WALLET FUNDED FURTHER OUT THAN THE GAP IS NOT FOUND HERE. It is not lost: numbers come from
52
+ * the key, so `walletAddress({ index })` and `setActiveWallet(index)` still reach it.
53
+ */
54
+ export declare function wallets(opened: Opened, reads?: WalletReads): Promise<WalletInfo[]>;
55
+ /**
56
+ * Say which of this key's wallets pays for storage from now on, on this account rather than on
57
+ * this machine.
58
+ *
59
+ * Nothing is deleted and nothing is created: every number a key can derive already exists, and
60
+ * this says which of them the next payment comes out of. The account's count of wallets comes up
61
+ * with it, never down, so every screen lists the wallet it just named.
62
+ */
63
+ export declare function setActiveWallet(opened: Opened, index: number): Promise<ActiveWallet>;
@@ -0,0 +1,129 @@
1
+ // Every wallet one NMTS key opens, and which of them pays for storage.
2
+ //
3
+ // ⛔ THE NUMBERS COME FROM THE KEY, SO THERE IS NOTHING TO CREATE AND NOTHING TO DELETE. One NMTS
4
+ // key derives a wallet at every index (NCF-3 §1.3); "which of them exist" is not a list anybody
5
+ // keeps, so it is answered by WALKING — ask the chain about a number, then the next, and stop
6
+ // after twenty unused ones in a row. The walk itself is the command-line package's own
7
+ // (`discoverWallets`), so this library and `nmts wallet list` show one key the same wallets.
8
+ //
9
+ // ⛔ WHICH ONE PAYS IS THE ACCOUNT'S, NOT THIS PROCESS'S. It rides inside the sealed file list,
10
+ // where no server can read it, so a phone, a browser and a program built on this package all
11
+ // spend from the same address. Money leaving from one address on one device and another address
12
+ // on another is how a balance goes missing without anything failing.
13
+ //
14
+ // ⛔ AND IT IS REFUSED RATHER THAN GUESSED. When the list cannot be read this does not fall back to
15
+ // wallet 0: that would name one address in a price and sign with another, on an account whose
16
+ // owner may never have funded the first. "I do not know which wallet should pay" is an answer;
17
+ // wallet 0 is not. It is the command-line tool's rule (`wallet-pay-index.ts`), for money that
18
+ // does not come back either way.
19
+ import { activeWalletOf, applyManyToList, chainReader, discoverWallets, hasHistory, NmtsError, readBalances, readFileList, walCoinType, walletAddress, walletCountOf, WALLET_INDEX_LIMIT, } from "@needmoretruth/nmts-cli";
20
+ import { withAccount } from "./session.js";
21
+ /**
22
+ * A wallet number as a caller wrote it.
23
+ *
24
+ * ⛔ REFUSED, NEVER ROUNDED. `1.5` and `-1` are calls to correct; taking either of them to a
25
+ * neighbouring wallet would pay from an address nobody named. The write path below clamps, which
26
+ * is right for a slider and wrong for an argument, so the refusal happens before it.
27
+ */
28
+ export function requireWalletIndex(value) {
29
+ if (!Number.isSafeInteger(value) || value < 0 || value >= WALLET_INDEX_LIMIT) {
30
+ throw new NmtsError(`A wallet number is a whole number from 0 to ${WALLET_INDEX_LIMIT - 1}.`, {
31
+ exitCode: 2,
32
+ nextStep: "`wallets()` lists this key's wallets and their numbers.",
33
+ });
34
+ }
35
+ return value;
36
+ }
37
+ /**
38
+ * Which wallet this account pays from, out of its sealed list.
39
+ *
40
+ * An account that has never written the setting pays from the first wallet — that is a READ
41
+ * answer and not a fallback: the list opened, and it said nothing about a wallet.
42
+ */
43
+ export async function payingWallet(held) {
44
+ try {
45
+ const list = await readFileList(held.server, held.apiKey, held.code, held.accountId);
46
+ return activeWalletOf(list.manifest?.settings);
47
+ }
48
+ catch (error) {
49
+ // A refusal this package already worded — no API key, a server that said no — is passed on as
50
+ // it stands; it names the thing to fix, and a second sentence over it would bury that.
51
+ if (error instanceof NmtsError)
52
+ throw error;
53
+ throw new NmtsError(`Which wallet this account pays from is written in its file list, and it could not be read: ` +
54
+ `${error instanceof Error ? error.message : String(error)}`, {
55
+ exitCode: 1,
56
+ nextStep: "Nothing was signed. Try again when the list can be read, or name the wallet for this one " +
57
+ "call — `walletAddress({ index })` and `put(…, { wallet })` each take a number.",
58
+ });
59
+ }
60
+ }
61
+ /**
62
+ * Every wallet this account has, in index order: the ones it has made, and any further out that a
63
+ * chain says have been used.
64
+ *
65
+ * Nothing is spent and nothing is written. The walk costs one balance read per number it asks
66
+ * about, plus a transaction question for a wallet that holds neither coin — a wallet somebody
67
+ * emptied is in use too, and a walk that asked about balances alone would report it as one that
68
+ * does not exist.
69
+ *
70
+ * ⚠ A WALLET FUNDED FURTHER OUT THAN THE GAP IS NOT FOUND HERE. It is not lost: numbers come from
71
+ * the key, so `walletAddress({ index })` and `setActiveWallet(index)` still reach it.
72
+ */
73
+ export async function wallets(opened, reads = {}) {
74
+ const open = reads.openChain ?? chainReader;
75
+ const history = reads.hasHistory ?? hasHistory;
76
+ return withAccount(opened, async (held) => {
77
+ const list = await readFileList(held.server, held.apiKey, held.code, held.accountId);
78
+ const settings = list.manifest?.settings;
79
+ const active = activeWalletOf(settings);
80
+ const count = walletCountOf(settings);
81
+ const walType = walCoinType(held.network);
82
+ // Every number the walk asked about, so no address is derived or read twice.
83
+ const seen = new Map();
84
+ const scan = await discoverWallets(async (index) => {
85
+ const address = await walletAddress(held.code, index);
86
+ seen.set(index, address);
87
+ const balances = await readBalances(await open(held.network, address), walType);
88
+ // ⛔ A BALANCE THAT COULD NOT BE READ IS NOT A ZERO, so it is not a wallet this walk gets to
89
+ // call unused: the transaction question below still has its say, and a wallet that has
90
+ // one is reported whatever the node did with the balance.
91
+ const holds = (coin) => coin.read && coin.baseUnits > 0n;
92
+ if (holds(balances.sui) || holds(balances.wal))
93
+ return true;
94
+ return history(held.network, address);
95
+ }, { count });
96
+ // ⛔ THE COUNT IS READ AND NOT RAISED HERE. "Making a wallet" is a person's act on the wallets
97
+ // screen, and a library that wrote the account's list while answering a question about it
98
+ // would be spending a file-list version on a read its caller did not ask to change anything
99
+ // with. What the walk found is reported; what the account holds stays what it held.
100
+ return [...seen]
101
+ .filter(([index]) => index < count || scan.used.includes(index))
102
+ .map(([index, address]) => ({ index, address, active: index === active }))
103
+ .sort((a, b) => a.index - b.index);
104
+ });
105
+ }
106
+ /**
107
+ * Say which of this key's wallets pays for storage from now on, on this account rather than on
108
+ * this machine.
109
+ *
110
+ * Nothing is deleted and nothing is created: every number a key can derive already exists, and
111
+ * this says which of them the next payment comes out of. The account's count of wallets comes up
112
+ * with it, never down, so every screen lists the wallet it just named.
113
+ */
114
+ export async function setActiveWallet(opened, index) {
115
+ // ⛔ BEFORE THE NETWORK. A number that is not one is a call to fix, and a run that opened the
116
+ // account first — a business's sealed store, for a managed root — would refuse for the wrong
117
+ // reason after having opened it for nothing.
118
+ const wanted = requireWalletIndex(index);
119
+ return withAccount(opened, async (held) => {
120
+ const before = await readFileList(held.server, held.apiKey, held.code, held.accountId);
121
+ // The setting lives IN the list, so an account with no list has nowhere to put it — the same
122
+ // refusal `nmts wallet use` gives, with the same way out.
123
+ if (before.manifest === null) {
124
+ throw new NmtsError("This account has no file list yet; which wallet pays lives in the list, and there is nothing to write it into.", { exitCode: 4, nextStep: "Upload once with `put()` and set the wallet after." });
125
+ }
126
+ const result = await applyManyToList(held, () => [], { activeWallet: wanted });
127
+ return { index: wanted, address: await walletAddress(held.code, wanted), changed: result.changed };
128
+ });
129
+ }
@@ -0,0 +1,16 @@
1
+ // The README's quickstart, runnable: `node examples/quickstart.mjs ./some-file`.
2
+ //
3
+ // Needs NMTS_ACCOUNT_CODE_FILE (or NMTS_ACCOUNT_CODE) and NMTS_API_KEY_FILE (or NMTS_API_KEY) in
4
+ // the environment. The upload spends credits: one per started MiB of sealed bytes.
5
+ import { Nmts } from "@needmoretruth/nmts-sdk";
6
+
7
+ const file = process.argv[2] ?? "./README.md";
8
+ const nmts = Nmts.fromEnv();
9
+
10
+ const put = await nmts.put(file);
11
+ console.log(`stored ${put.path} — ${put.bytes} bytes, ${put.credits} credit(s)`);
12
+
13
+ const bytes = await nmts.get(put.path);
14
+ console.log(`fetched ${bytes.length} bytes back`);
15
+
16
+ for (const entry of await nmts.list()) console.log(`${entry.kind === "folder" ? "d" : "f"} ${entry.size}\t${entry.path}`);
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@needmoretruth/nmts-sdk",
3
+ "version": "0.1.1",
4
+ "description": "Put, get and list files in an NMTS account from your own program: open-source, end-to-end encrypted storage on the Walrus network, with the keys on your machine. One constructor, three methods, typed.",
5
+ "license": "Apache-2.0",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "homepage": "https://github.com/needmoretruth/nmts-sdk",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/needmoretruth/nmts-sdk.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/needmoretruth/nmts-sdk/issues"
16
+ },
17
+ "keywords": [
18
+ "nmts",
19
+ "walrus",
20
+ "sui",
21
+ "encryption",
22
+ "e2ee",
23
+ "storage",
24
+ "sdk",
25
+ "backend",
26
+ "agent"
27
+ ],
28
+ "type": "module",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "examples",
43
+ "README.md",
44
+ "AGENTS.md",
45
+ "LICENSE",
46
+ "LICENSING.md"
47
+ ],
48
+ "scripts": {
49
+ "compile": "tsc -p tsconfig.json",
50
+ "typecheck": "tsc -p tsconfig.json --noEmit",
51
+ "prepack": "npm run compile",
52
+ "pretest": "node scripts/ensure-cli.mjs",
53
+ "test": "node --test \"test/**/*.test.ts\""
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^24.3.0",
57
+ "typescript": "^5.9.3"
58
+ },
59
+ "dependencies": {
60
+ "@needmoretruth/nmts-cli": "^0.35.0"
61
+ }
62
+ }