@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.
- package/AGENTS.md +176 -0
- package/LICENSE +202 -0
- package/LICENSING.md +45 -0
- package/README.md +241 -0
- package/dist/env.d.ts +15 -0
- package/dist/env.js +42 -0
- package/dist/get.d.ts +35 -0
- package/dist/get.js +116 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +13 -0
- package/dist/list.d.ts +39 -0
- package/dist/list.js +35 -0
- package/dist/nmts.d.ts +103 -0
- package/dist/nmts.js +157 -0
- package/dist/product.d.ts +11 -0
- package/dist/product.js +12 -0
- package/dist/put-wallet.d.ts +18 -0
- package/dist/put-wallet.js +116 -0
- package/dist/put.d.ts +151 -0
- package/dist/put.js +168 -0
- package/dist/root.d.ts +67 -0
- package/dist/root.js +76 -0
- package/dist/session.d.ts +54 -0
- package/dist/session.js +74 -0
- package/dist/wallets.d.ts +63 -0
- package/dist/wallets.js +129 -0
- package/examples/quickstart.mjs +16 -0
- package/package.json +62 -0
package/dist/session.js
ADDED
|
@@ -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>;
|
package/dist/wallets.js
ADDED
|
@@ -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
|
+
}
|