wative-core 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +163 -112
  3. package/dist/artifacts/evm/index.cjs +1 -1
  4. package/dist/artifacts/evm/index.d.cts +2 -2
  5. package/dist/artifacts/evm/index.d.ts +2 -2
  6. package/dist/artifacts/evm/index.js +1 -1
  7. package/dist/artifacts/svm/index.cjs +1 -1
  8. package/dist/artifacts/svm/index.d.cts +3 -3
  9. package/dist/artifacts/svm/index.d.ts +3 -3
  10. package/dist/artifacts/svm/index.js +1 -1
  11. package/dist/{container-provider-D2QluIe_.d.ts → container-provider-B8M0ylrK.d.ts} +1 -1
  12. package/dist/{container-provider-DAUXclw_.d.cts → container-provider-YxyMpkhG.d.cts} +1 -1
  13. package/dist/{evm-GxBuNjq7.d.ts → evm-Bo3Dy492.d.ts} +1 -1
  14. package/dist/{evm-qXdZWugm.d.cts → evm-DA6Sh6jv.d.cts} +1 -1
  15. package/dist/{file-sink-B0EjCBvG.d.ts → file-sink-Be5iLbuT.d.ts} +2 -2
  16. package/dist/{file-sink-DSOZbsJM.d.cts → file-sink-KCCpKHRj.d.cts} +2 -2
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.cts +6 -6
  19. package/dist/index.d.ts +6 -6
  20. package/dist/index.js +1 -1
  21. package/dist/index.node.cjs +1 -1
  22. package/dist/index.node.d.cts +5 -5
  23. package/dist/index.node.d.ts +5 -5
  24. package/dist/index.node.js +1 -1
  25. package/dist/node/index.cjs +1 -1
  26. package/dist/node/index.d.cts +3 -3
  27. package/dist/node/index.d.ts +3 -3
  28. package/dist/node/index.js +1 -1
  29. package/dist/{svm-Ca4Xyw0Y.d.ts → svm-B51NevS8.d.ts} +1 -1
  30. package/dist/{svm-B_c0L-rJ.d.cts → svm-DswX6XRv.d.cts} +1 -1
  31. package/dist/{workspace-DdaY7sWa.d.cts → workspace-CBxKR80D.d.cts} +48 -10
  32. package/dist/{workspace-DdaY7sWa.d.ts → workspace-CBxKR80D.d.ts} +48 -10
  33. package/examples/03-pk-account.test.cjs +65 -0
  34. package/examples/15-package-resolution.test.cjs +4 -4
  35. package/examples/19-simple-provider.test.cjs +134 -0
  36. package/examples/README.md +11 -12
  37. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { c as Address, G as SvmTxBuildParams, F as SvmTransaction } from './workspace-DdaY7sWa.js';
1
+ import { c as Address, G as SvmTxBuildParams, F as SvmTransaction } from './workspace-CBxKR80D.js';
2
2
 
3
3
  interface SvmInstruction {
4
4
  readonly programId: string;
@@ -1,4 +1,4 @@
1
- import { c as Address, G as SvmTxBuildParams, F as SvmTransaction } from './workspace-DdaY7sWa.cjs';
1
+ import { c as Address, G as SvmTxBuildParams, F as SvmTransaction } from './workspace-CBxKR80D.cjs';
2
2
 
3
3
  interface SvmInstruction {
4
4
  readonly programId: string;
@@ -118,7 +118,7 @@ interface Settling<T> {
118
118
  }
119
119
 
120
120
  type RecordType = "CONFIG" | "ACCOUNTS" | "NETWORK" | "ASSET" | "LOG";
121
-
121
+ /** Pre-unlock structural state of a provider's container — see `Provider.inspectContainer`. */
122
122
  type ContainerState = "empty" | "workspace" | "foreign";
123
123
  interface RecordInit<T> {
124
124
  readonly provider: Provider;
@@ -128,7 +128,11 @@ interface RecordInit<T> {
128
128
  readonly password?: string;
129
129
  readonly decrypt?: (ciphertext: Uint8Array, password: string) => Uint8Array;
130
130
  readonly persist?: (value: T, password: string) => Promise<void>;
131
-
131
+ /**
132
+ * Throws if the session that issued this record has ended. Supplied by
133
+ * providers that HAVE a session; a provider without one omits it and the
134
+ * record behaves as before.
135
+ */
132
136
  readonly requireLiveSession?: () => void;
133
137
  }
134
138
  declare class Record$1<T> {
@@ -138,9 +142,19 @@ declare class Record$1<T> {
138
142
  constructor(slug: Slug, path: string, init?: RecordInit<T>);
139
143
  get locked(): boolean;
140
144
  get value(): T;
141
-
145
+ /**
146
+ * Pure CPU — decrypts the in-memory blob. Sync.
147
+ *
148
+ * BAD_PASSWORD means the password is wrong: an AES-GCM auth-tag mismatch on
149
+ * the locked path, or a mismatch against the password that opened the record
150
+ * on the unlocked one. DECRYPT_FAILED means the record cannot answer —
151
+ * structural damage or corruption on the locked path, and on the unlocked
152
+ * path a record carrying neither a password nor ciphertext, which has nothing
153
+ * to verify against. Callers can still read the two apart as "wrong password"
154
+ * versus "cannot be checked".
155
+ */
142
156
  unlock(password: string): this;
143
-
157
+ /** Persists changes via the owning Provider. */
144
158
  save(): Promise<void>;
145
159
  }
146
160
  declare abstract class Provider {
@@ -156,15 +170,38 @@ declare abstract class Provider {
156
170
  abstract unlockContainer(password: string): Promise<boolean>;
157
171
  abstract lockContainer(): Promise<void>;
158
172
  abstract isContainerUnlocked(): boolean;
159
-
173
+ /**
174
+ * Structural inspection of the container BEFORE unlock. `Workspace.open()`
175
+ * uses it to auto-select create-vs-open without a `create` flag:
176
+ * - `"empty"` — no container yet; safe to initialize a new workspace.
177
+ * - `"workspace"` — an existing wative workspace; open it.
178
+ * - `"foreign"` — non-empty storage that is NOT a wative workspace; `open()`
179
+ * refuses rather than scaffolding a workspace into it.
180
+ * The base default returns `"workspace"` (proceed as open-or-create, no guard),
181
+ * so existing custom providers are unaffected. Providers that can cheaply
182
+ * inspect their storage — e.g. `HybridProvider` on a filesystem — override this
183
+ * to detect `"foreign"` and stop a mistyped path from creating a workspace
184
+ * inside unrelated data.
185
+ */
160
186
  inspectContainer(): Promise<ContainerState>;
161
187
  abstract loadRecord<T>(type: RecordType, slug?: Slug): Promise<Record$1<T>>;
162
188
  abstract loadRecords<T>(type: RecordType): Promise<ReadonlyArray<Record$1<T>>>;
163
189
  abstract writeRecord<T>(type: RecordType, slug: Slug, value: T): Promise<void>;
164
190
  abstract dropRecord(type: RecordType, slug: Slug): Promise<void>;
165
-
191
+ /**
192
+ * Initialize an empty container by writing the four root records
193
+ * (CONFIG, NETWORK, ASSET, LOG) from the bundled template files at
194
+ * `src/workspace_template/`. Idempotent: existing records are left
195
+ * untouched. The ACCOUNTS namespace is left empty — per-account records
196
+ * are created lazily by `accounts.create(...)`.
197
+ *
198
+ * Subclasses inherit this default; override only if a backend needs
199
+ * non-record initialization (e.g. allocating a database schema).
200
+ */
166
201
  initialize(): Promise<void>;
167
-
202
+ /** Any storage at all under the accounts area, however named. Defaults to
203
+ * "nothing", so a provider that does not implement it can only fail to add a
204
+ * refusal reason — never cause one. */
168
205
  protected _hasRawAccountData(): Promise<boolean>;
169
206
  abstract close(): Promise<void>;
170
207
  }
@@ -314,8 +351,8 @@ declare class EvmTransaction implements Transaction {
314
351
  }>;
315
352
  address?: Address;
316
353
  });
317
- constructor(from: EvmAddress, to: string, value: bigint, data?: string, opts?: {
318
- chainId?: ChainId;
354
+ constructor(from: EvmAddress, to: string, value: bigint, data: string | undefined, opts: {
355
+ chainId: ChainId;
319
356
  type?: EvmTxType;
320
357
  nonce?: number;
321
358
  gasPrice?: bigint;
@@ -585,7 +622,8 @@ declare class Account {
585
622
  after: number;
586
623
  dropped: number;
587
624
  }>;
588
- importPrivateKey(pk: string, vm: ChainVM): Promise<Wallet>;
625
+
626
+ importPrivateKey(pk: string, vm?: ChainVM): Promise<Wallet>;
589
627
  drop(): Promise<void>;
590
628
 
591
629
  filterAddress(network: NetworkLike, addr?: string): Address | null;
@@ -118,7 +118,7 @@ interface Settling<T> {
118
118
  }
119
119
 
120
120
  type RecordType = "CONFIG" | "ACCOUNTS" | "NETWORK" | "ASSET" | "LOG";
121
-
121
+ /** Pre-unlock structural state of a provider's container — see `Provider.inspectContainer`. */
122
122
  type ContainerState = "empty" | "workspace" | "foreign";
123
123
  interface RecordInit<T> {
124
124
  readonly provider: Provider;
@@ -128,7 +128,11 @@ interface RecordInit<T> {
128
128
  readonly password?: string;
129
129
  readonly decrypt?: (ciphertext: Uint8Array, password: string) => Uint8Array;
130
130
  readonly persist?: (value: T, password: string) => Promise<void>;
131
-
131
+ /**
132
+ * Throws if the session that issued this record has ended. Supplied by
133
+ * providers that HAVE a session; a provider without one omits it and the
134
+ * record behaves as before.
135
+ */
132
136
  readonly requireLiveSession?: () => void;
133
137
  }
134
138
  declare class Record$1<T> {
@@ -138,9 +142,19 @@ declare class Record$1<T> {
138
142
  constructor(slug: Slug, path: string, init?: RecordInit<T>);
139
143
  get locked(): boolean;
140
144
  get value(): T;
141
-
145
+ /**
146
+ * Pure CPU — decrypts the in-memory blob. Sync.
147
+ *
148
+ * BAD_PASSWORD means the password is wrong: an AES-GCM auth-tag mismatch on
149
+ * the locked path, or a mismatch against the password that opened the record
150
+ * on the unlocked one. DECRYPT_FAILED means the record cannot answer —
151
+ * structural damage or corruption on the locked path, and on the unlocked
152
+ * path a record carrying neither a password nor ciphertext, which has nothing
153
+ * to verify against. Callers can still read the two apart as "wrong password"
154
+ * versus "cannot be checked".
155
+ */
142
156
  unlock(password: string): this;
143
-
157
+ /** Persists changes via the owning Provider. */
144
158
  save(): Promise<void>;
145
159
  }
146
160
  declare abstract class Provider {
@@ -156,15 +170,38 @@ declare abstract class Provider {
156
170
  abstract unlockContainer(password: string): Promise<boolean>;
157
171
  abstract lockContainer(): Promise<void>;
158
172
  abstract isContainerUnlocked(): boolean;
159
-
173
+ /**
174
+ * Structural inspection of the container BEFORE unlock. `Workspace.open()`
175
+ * uses it to auto-select create-vs-open without a `create` flag:
176
+ * - `"empty"` — no container yet; safe to initialize a new workspace.
177
+ * - `"workspace"` — an existing wative workspace; open it.
178
+ * - `"foreign"` — non-empty storage that is NOT a wative workspace; `open()`
179
+ * refuses rather than scaffolding a workspace into it.
180
+ * The base default returns `"workspace"` (proceed as open-or-create, no guard),
181
+ * so existing custom providers are unaffected. Providers that can cheaply
182
+ * inspect their storage — e.g. `HybridProvider` on a filesystem — override this
183
+ * to detect `"foreign"` and stop a mistyped path from creating a workspace
184
+ * inside unrelated data.
185
+ */
160
186
  inspectContainer(): Promise<ContainerState>;
161
187
  abstract loadRecord<T>(type: RecordType, slug?: Slug): Promise<Record$1<T>>;
162
188
  abstract loadRecords<T>(type: RecordType): Promise<ReadonlyArray<Record$1<T>>>;
163
189
  abstract writeRecord<T>(type: RecordType, slug: Slug, value: T): Promise<void>;
164
190
  abstract dropRecord(type: RecordType, slug: Slug): Promise<void>;
165
-
191
+ /**
192
+ * Initialize an empty container by writing the four root records
193
+ * (CONFIG, NETWORK, ASSET, LOG) from the bundled template files at
194
+ * `src/workspace_template/`. Idempotent: existing records are left
195
+ * untouched. The ACCOUNTS namespace is left empty — per-account records
196
+ * are created lazily by `accounts.create(...)`.
197
+ *
198
+ * Subclasses inherit this default; override only if a backend needs
199
+ * non-record initialization (e.g. allocating a database schema).
200
+ */
166
201
  initialize(): Promise<void>;
167
-
202
+ /** Any storage at all under the accounts area, however named. Defaults to
203
+ * "nothing", so a provider that does not implement it can only fail to add a
204
+ * refusal reason — never cause one. */
168
205
  protected _hasRawAccountData(): Promise<boolean>;
169
206
  abstract close(): Promise<void>;
170
207
  }
@@ -314,8 +351,8 @@ declare class EvmTransaction implements Transaction {
314
351
  }>;
315
352
  address?: Address;
316
353
  });
317
- constructor(from: EvmAddress, to: string, value: bigint, data?: string, opts?: {
318
- chainId?: ChainId;
354
+ constructor(from: EvmAddress, to: string, value: bigint, data: string | undefined, opts: {
355
+ chainId: ChainId;
319
356
  type?: EvmTxType;
320
357
  nonce?: number;
321
358
  gasPrice?: bigint;
@@ -585,7 +622,8 @@ declare class Account {
585
622
  after: number;
586
623
  dropped: number;
587
624
  }>;
588
- importPrivateKey(pk: string, vm: ChainVM): Promise<Wallet>;
625
+
626
+ importPrivateKey(pk: string, vm?: ChainVM): Promise<Wallet>;
589
627
  drop(): Promise<void>;
590
628
 
591
629
  filterAddress(network: NetworkLike, addr?: string): Address | null;
@@ -1,4 +1,18 @@
1
1
  // 03 — PK account flows. Heterogeneous private keys, one wallet per import.
2
+ //
3
+ // Two things worth noticing as you read:
4
+ //
5
+ // * `vm` is optional. The key formats do not overlap — 64 hex characters for
6
+ // EVM, 80–96 base58 (or a solana-keygen JSON array) for Solana — so the
7
+ // chain is read off the key. Pass it anyway when you want the mismatch to
8
+ // be an error rather than an inference.
9
+ //
10
+ // * One import gives you TWO addresses. The key signs on its own chain, and
11
+ // the same 32 secret bytes seed a keypair on the other one, so the wallet
12
+ // has an address on both. The chain you imported for stays first.
13
+ // ⚠️ One secret controls both addresses. That is different from an HD
14
+ // account, where a slot's two addresses come from two derivation paths and
15
+ // are independent keys.
2
16
 
3
17
  const test = require("node:test");
4
18
  const assert = require("node:assert");
@@ -10,6 +24,16 @@ const { Workspace } = require("wative-core");
10
24
 
11
25
  const EVM_PK = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d";
12
26
  const SVM_PK = "xtGWcHvQsr5ue8zG2F5fm31bW8vyn597Y92gUWaZZg3S1Z6FeJMATL8KU3xJMGbLfALnokcct9y5wYtCRrwNXZW";
27
+ // A second Solana key, base58 of its 64-byte secret key.
28
+ const SVM_PK_2 = "gaiC7Rnf9J6tV3SGwJyzvMDdz9RMmreSWZDyDK682MUKZHaWdZbbqgf35qD7FSwXka2Rf2o4MuhbWQTg82W9mbV";
29
+ // A third, in the shape `solana-keygen` writes: the 64 secret-key bytes as
30
+ // JSON. This is literally the contents of ~/.config/solana/id.json.
31
+ const SVM_KEYGEN_BYTES = [
32
+ 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51,
33
+ 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51,
34
+ 23, 203, 121, 251, 43, 65, 32, 242, 177, 236, 101, 228, 25, 141, 110, 8,
35
+ 178, 142, 129, 63, 235, 1, 228, 164, 0, 131, 155, 133, 225, 128, 128, 206,
36
+ ];
13
37
 
14
38
  test("PK account — create from EVM key + import a Solana key", async () => {
15
39
  const root = mkdtempSync(join(tmpdir(), "wative-pk-"));
@@ -24,11 +48,52 @@ test("PK account — create from EVM key + import a Solana key", async () => {
24
48
  );
25
49
  assert.strictEqual(acc.organizationType, "PK");
26
50
  assert.strictEqual(acc.wallets.length, 1);
51
+ // The chain the key is for comes first; its sibling follows.
27
52
  assert.strictEqual(acc.wallets[0].addresses[0].vm, "evm");
53
+ assert.strictEqual(acc.wallets[0].addresses[1].vm, "svm");
28
54
 
29
55
  await acc.importPrivateKey(SVM_PK, "svm");
30
56
  assert.strictEqual(acc.wallets.length, 2);
31
57
  assert.strictEqual(acc.wallets[1].addresses[0].vm, "svm");
58
+ assert.strictEqual(acc.wallets[1].addresses[1].vm, "evm");
59
+
60
+ await ws.lock();
61
+ });
62
+
63
+ // The three ways you are likely to be holding a private key, and what each
64
+ // one does without being told which chain it belongs to.
65
+ test("PK account — the key formats importPrivateKey accepts", async () => {
66
+ const root = mkdtempSync(join(tmpdir(), "wative-pk-fmt-"));
67
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
68
+ const acc = await ws.accounts.create("Formats", "wsp-pwd", EVM_PK, undefined, { kind: "PK" });
69
+
70
+ // 1. EVM: 32 bytes of hex, 0x-prefix optional. No vm argument needed.
71
+ const evm = await acc.importPrivateKey(
72
+ "0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1",
73
+ );
74
+ assert.strictEqual(evm.addresses[0].vm, "evm");
75
+
76
+ // 2. Solana: base58 of the 64-byte secret key.
77
+ const svm = await acc.importPrivateKey(SVM_PK_2);
78
+ assert.strictEqual(svm.addresses[0].vm, "svm");
79
+
80
+ // 3. Solana: the JSON byte array `solana-keygen` writes to id.json. This is
81
+ // what you get from `cat ~/.config/solana/id.json`, usable as-is.
82
+ const fromKeygen = await acc.importPrivateKey(JSON.stringify(SVM_KEYGEN_BYTES));
83
+ assert.strictEqual(fromKeygen.addresses[0].vm, "svm");
84
+
85
+ // A value that is neither is refused rather than guessed at.
86
+ await assert.rejects(
87
+ acc.importPrivateKey("definitely-not-a-key"),
88
+ (err) => err.code === "INVALID_PRIVATE_KEY",
89
+ );
90
+
91
+ // So is 32 bytes of hex that is not a valid secp256k1 scalar — being the
92
+ // right length is not the same as being a key.
93
+ await assert.rejects(
94
+ acc.importPrivateKey("0x" + "0".repeat(64), "evm"),
95
+ (err) => err.code === "INVALID_PRIVATE_KEY",
96
+ );
32
97
 
33
98
  await ws.lock();
34
99
  });
@@ -1,7 +1,7 @@
1
- // 15 — Package resolution. Every other example imports `../dist/index.cjs`
2
- // directly, which bypasses the `exports` map in package.json entirely. This
3
- // file is the only one that resolves the package the way a real consumer does:
4
- // by its bare name. It therefore covers the export conditions (`import` vs
1
+ // 15 — Package resolution. Every example now resolves the package by its bare
2
+ // name, so they all exercise the `exports` map; this file is the one that tests
3
+ // the map ITSELF rather than using it incidentally — every declared subpath,
4
+ // under both conditions. It therefore covers the export conditions (`import` vs
5
5
  // `require`), the declared subpaths, and the parity between the two builds.
6
6
  //
7
7
  // It works without installing anything because Node resolves a package's own
@@ -0,0 +1,134 @@
1
+ // 19 — The SHORT way to write a storage backend.
2
+ //
3
+ // Example 07 extends `Provider` and implements the record layer by hand, which
4
+ // is what you want when the storage system already has its own encryption or
5
+ // its own idea of a record. Most of the time you want this instead: extend
6
+ // `ContainerProvider` and implement six methods that move bytes around. It
7
+ // supplies the container session, the record API and all the encryption, so a
8
+ // working backend is about twenty lines.
9
+ //
10
+ // The store here is a plain Map with no persistence and no cleverness — the
11
+ // point is how little you have to write, not the store itself.
12
+
13
+ const test = require("node:test");
14
+ const assert = require("node:assert");
15
+
16
+ const { Workspace, ContainerProvider, WativeError } = require("wative-core");
17
+
18
+ /** A whole storage backend. Keys are strings, values are bytes. */
19
+ class MapProvider extends ContainerProvider {
20
+ #store = new Map();
21
+ #dirs = new Set([""]);
22
+
23
+ _exist(path) {
24
+ // A directory "exists" if anything sits under it — the library asks about
25
+ // both files and directories through this one method.
26
+ if (this.#store.has(path) || this.#dirs.has(path)) return true;
27
+ const prefix = path.endsWith("/") ? path : path + "/";
28
+ for (const k of this.#store.keys()) if (k.startsWith(prefix)) return true;
29
+ return false;
30
+ }
31
+
32
+ _read(path) {
33
+ const v = this.#store.get(path);
34
+ if (!v) throw new WativeError("RECORD_NOT_FOUND", `nothing at ${path}`);
35
+ return v;
36
+ }
37
+
38
+ _write(path, data) {
39
+ this.#store.set(path, data);
40
+ }
41
+
42
+ _remove(path) {
43
+ this.#store.delete(path);
44
+ }
45
+
46
+ _ensureDir(path) {
47
+ this.#dirs.add(path);
48
+ }
49
+
50
+ _listItems(path) {
51
+ const prefix = path.endsWith("/") ? path : path + "/";
52
+ const names = new Set();
53
+ for (const k of this.#store.keys()) {
54
+ if (!k.startsWith(prefix)) continue;
55
+ const head = k.slice(prefix.length).split("/")[0];
56
+ if (head) names.add(head);
57
+ }
58
+ return [...names];
59
+ }
60
+
61
+ /** Not part of the contract — this example looks at the raw bytes below. */
62
+ dumpForTest() {
63
+ return this.#store;
64
+ }
65
+ }
66
+
67
+ test("a twenty-line backend runs a whole workspace", async () => {
68
+ const provider = new MapProvider("map://desk");
69
+
70
+ let ws = await Workspace.open({ provider, password: "wsp-pwd" });
71
+ assert.strictEqual(ws.locked, false);
72
+
73
+ // The built-in networks are there, exactly as with any other backend.
74
+ assert.ok(ws.networks.map((n) => n.slug).includes("ethereum"));
75
+
76
+ const acc = await ws.accounts.create(
77
+ "Desk One",
78
+ "wsp-pwd",
79
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
80
+ );
81
+ await acc.deriveWallets(2);
82
+ assert.strictEqual(acc.wallets.length, 3);
83
+
84
+ // An address, and a signature from it — the full stack, on a Map.
85
+ const evm = acc.wallets[0].evm;
86
+ assert.match(evm.publicKey, /^0x[0-9a-fA-F]{40}$/);
87
+ const sig = await evm.signMessage("hello from a Map");
88
+ assert.match(sig, /^0x[0-9a-fA-F]{130}$/);
89
+
90
+ await ws.lock();
91
+
92
+ // Reopen from the same store: everything came back.
93
+ ws = await Workspace.open({ provider, password: "wsp-pwd" });
94
+ const again = ws.accounts.bySlug(acc.slug);
95
+ assert.ok(again);
96
+ assert.strictEqual(again.wallets.length, 3);
97
+ await again.tryUnlock("wsp-pwd");
98
+ assert.strictEqual(again.wallets[0].evm.publicKey, evm.publicKey);
99
+
100
+ await ws.lock();
101
+ });
102
+
103
+ test("the encryption comes with the base class, not with your code", async () => {
104
+ // Nothing above encrypts anything, yet nothing readable reaches the store.
105
+ // That is the reason to extend `ContainerProvider`: a backend cannot weaken
106
+ // the encryption because a backend never performs it.
107
+ const provider = new MapProvider("map://opaque");
108
+ const ws = await Workspace.open({ provider, password: "wsp-pwd" });
109
+ await ws.accounts.create(
110
+ "Secret Holder",
111
+ "wsp-pwd",
112
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
113
+ );
114
+ await ws.lock();
115
+
116
+ const everything = Buffer.concat(
117
+ [...provider.dumpForTest().values()].map((b) => Buffer.from(b)),
118
+ ).toString("latin1");
119
+
120
+ assert.ok(everything.length > 0, "the store should not be empty");
121
+ assert.ok(!everything.includes("abandon"), "the mnemonic must not appear in stored bytes");
122
+ assert.ok(!everything.includes("Secret Holder"), "the account name must not appear either");
123
+ });
124
+
125
+ test("a wrong password cannot open the container", async () => {
126
+ const provider = new MapProvider("map://guarded");
127
+ const ws = await Workspace.open({ provider, password: "right-pwd" });
128
+ await ws.lock();
129
+
130
+ await assert.rejects(
131
+ Workspace.open({ provider, password: "wrong-pwd" }),
132
+ (err) => err.code === "BAD_PASSWORD",
133
+ );
134
+ });
@@ -14,13 +14,13 @@ Most files are written as CommonJS (`.test.cjs`) because the CJS build is the mo
14
14
  From the repo root:
15
15
 
16
16
  ```bash
17
- node --test tests/
17
+ node --test examples/
18
18
  ```
19
19
 
20
20
  Or run a single file:
21
21
 
22
22
  ```bash
23
- node --test tests/01-quick-start.test.cjs
23
+ node --test examples/01-quick-start.test.cjs
24
24
  ```
25
25
 
26
26
  If you are reading this inside `node_modules` after installing the package, copy the folder out before running `--test` — Node's test runner skips anything under `node_modules`. A single file can still be run in place with plain `node <file>`.
@@ -33,11 +33,11 @@ All tests work fully offline — no RPC calls, no real networks. Each test creat
33
33
  |---|---|
34
34
  | [01-quick-start.test.cjs](01-quick-start.test.cjs) | Open a workspace, create an HD account, derive 5 wallets, inspect EVM + Solana addresses, lock. |
35
35
  | [02-hd-account.test.cjs](02-hd-account.test.cjs) | HD-specific flows — deriving / slicing wallets, dumping the mnemonic, account-level passwords with `resetPassword`, round-trip across lock + reopen. |
36
- | [03-pk-account.test.cjs](03-pk-account.test.cjs) | PK accounts — creating from a private key, importing more keys (mix EVM + Solana under one account), duplicate-import rejection, dropping wallets. |
36
+ | [03-pk-account.test.cjs](03-pk-account.test.cjs) | PK accounts — creating from a private key, importing more keys, and the three key formats accepted (EVM hex, Solana base58, `solana-keygen` JSON) with the chain inferred from the key. Also shows each import producing an address on both chains, duplicate-import rejection, and dropping wallets. |
37
37
  | [04-network-management.test.cjs](04-network-management.test.cjs) | The 10 pre-loaded networks, adding a brand-new network, overriding a pre-loaded RPC URL via `update()`, dropping user networks, the built-in protection guard. |
38
38
  | [05-asset-management.test.cjs](05-asset-management.test.cjs) | The 25 pre-loaded tokens, adding a custom user token, id collision rejection, contract-address collision rejection, dropping user tokens, `workspace.filter(q, "Asset")` search. |
39
39
  | [06-address-signing.test.cjs](06-address-signing.test.cjs) | Personal-message signing on EVM and Solana, building a transaction fully offline (no RPC). |
40
- | [07-custom-provider.test.cjs](07-custom-provider.test.cjs) | Plugging your own storage backend by extending the `Provider` base class. The example uses an in-memory key-value store but the same shape works for databases, cloud storage, and browser storage like IndexedDB. |
40
+ | [07-custom-provider.test.cjs](07-custom-provider.test.cjs) | Taking over the record layer as well, by extending `Provider` rather than `ContainerProvider` — for storage that has its own encryption or its own idea of a record. Longer than [19](19-simple-provider.test.cjs); start there unless you need this. |
41
41
  | [08-persistence.test.cjs](08-persistence.test.cjs) | Round-trip across `lock()` + reopen — multiple accounts, user networks, custom assets, and logger configuration. |
42
42
  | [09-workspace-filter.test.cjs](09-workspace-filter.test.cjs) | `workspace.filter(query, objective)` for all four objectives — Account (by name or slug), Wallet (by id or tag), Address (by public key), Asset (by symbol or contract address). |
43
43
  | [10-default-network.test.cjs](10-default-network.test.cjs) | `account.setDefaultNetwork(value)` accepts a slug, a chain id, a hex chain id, or a `Network` instance. The choice persists across reopen. Unknown networks are rejected. |
@@ -48,22 +48,21 @@ All tests work fully offline — no RPC calls, no real networks. Each test creat
48
48
  | [15-package-resolution.test.cjs](15-package-resolution.test.cjs) | Resolution by bare package name — every declared subpath under both `require()` and `import()`, identical export surfaces from the two builds, and the declared Node version. |
49
49
  | [16-known-answer-vectors.test.cjs](16-known-answer-vectors.test.cjs) | Exact expected values — published BIP-44 addresses for the canonical mnemonic, a known address from an imported private key, and a fixed EIP-191 signature. Catches derivation from the wrong path or signing with the wrong key. |
50
50
  | [17-locked-state.test.cjs](17-locked-state.test.cjs) | What locking enforces — which operations refuse and with which error code, that a locked account holds no mnemonic in the clear, and that unlocking restores identical behaviour. |
51
+ | [18-envelope-v3.test.cjs](18-envelope-v3.test.cjs) | `HybridProviderV3` — writing a workspace that derives one key for the whole workspace instead of one per stored secret, reopening it, and confirming a wrong password is still refused. |
52
+ | [19-simple-provider.test.cjs](19-simple-provider.test.cjs) | The short way to write a storage backend — extend `ContainerProvider` and implement six methods that move bytes. Runs a whole workspace on a `Map`, signs from a derived address, reopens it, and checks that nothing readable reaches the store. |
51
53
 
52
54
  ## Adapting for your project
53
55
 
54
- In each file, replace:
55
-
56
- ```js
57
- const { Workspace } = require("../dist/index.cjs");
58
- ```
59
-
60
- with:
56
+ Nothing to change. Every file already imports the package by name:
61
57
 
62
58
  ```js
63
59
  const { Workspace } = require("wative-core");
64
60
  ```
65
61
 
66
- once you've done `pnpm add wative-core` (or `npm i wative-core`) in your own project. Or, if your project is ESM-flagged, use `import` instead — both syntax styles resolve to the right entry via the package's `exports` map.
62
+ so once you have run `pnpm add wative-core` (or `npm i wative-core`), copying a
63
+ file into your own project and running it works as-is. If your project is
64
+ ESM-flagged, use `import` instead — both resolve through the package's `exports`
65
+ map.
67
66
 
68
67
  ## Notes
69
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wative-core",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Hyperscale crypto wallet management library — multi-workspace, multi-chain, originally designed for on-chain market makers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.node.cjs",