wative-core 2.3.9 → 2.4.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +127 -91
  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-D_557LVN.d.ts → container-provider--npoeR2N.d.ts} +1 -1
  12. package/dist/{container-provider-CkKGyHis.d.cts → container-provider-Ca_5z1uQ.d.cts} +1 -1
  13. package/dist/{evm-Xw4zBLUH.d.ts → evm-EHpaW_sK.d.ts} +1 -1
  14. package/dist/{evm-gsdoVj16.d.cts → evm-hSJJOVG3.d.cts} +1 -1
  15. package/dist/{file-sink-C_SlT9IC.d.cts → file-sink-7ZAutH45.d.cts} +2 -2
  16. package/dist/{file-sink-D33jvque.d.ts → file-sink-Bk5zYHw-.d.ts} +2 -2
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.cts +12 -8
  19. package/dist/index.d.ts +12 -8
  20. package/dist/index.js +1 -1
  21. package/dist/index.node.cjs +1 -1
  22. package/dist/index.node.d.cts +6 -6
  23. package/dist/index.node.d.ts +6 -6
  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-CUuqpl1h.d.ts → svm-B5q8YaV9.d.ts} +1 -1
  30. package/dist/{svm-CTCJsYri.d.cts → svm-CXFwmeMo.d.cts} +1 -1
  31. package/dist/{workspace-Du_jy51u.d.cts → workspace-kq3QyAgm.d.cts} +51 -10
  32. package/dist/{workspace-Du_jy51u.d.ts → workspace-kq3QyAgm.d.ts} +51 -10
  33. package/examples/15-package-resolution.test.cjs +4 -4
  34. package/examples/19-simple-provider.test.cjs +134 -0
  35. package/examples/README.md +10 -11
  36. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { c as Address, G as SvmTxBuildParams, F as SvmTransaction } from './workspace-Du_jy51u.js';
1
+ import { c as Address, G as SvmTxBuildParams, F as SvmTransaction } from './workspace-kq3QyAgm.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-Du_jy51u.cjs';
1
+ import { c as Address, G as SvmTxBuildParams, F as SvmTransaction } from './workspace-kq3QyAgm.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,20 +170,45 @@ 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
  }
171
208
 
172
- type WativeErrorCode = "WORKSPACE_LOCKED" | "ACCOUNT_LOCKED" | "RECORD_LOCKED" | "BAD_PASSWORD" | "WEAK_PASSWORD" | "RECORD_NOT_FOUND" | "PROVIDER_IO" | "PERMISSION_DENIED" | "DISK_FULL" | "PARAMETER_ERROR" | "DECRYPT_FAILED" | "ENCRYPT_FAILED" | "ALGORITHM_IRREVERSIBLE" | "INVALID_MNEMONIC" | "INVALID_PRIVATE_KEY" | "UNSUPPORTED_NETWORK" | "TX_BUILD_FAILED" | "TX_SIGN_FAILED" | "TX_SUBMIT_FAILED" | "TX_TIMEOUT" | "TX_DROPPED" | "TX_ABORTED" | "RPC_UNREACHABLE" | "STORAGE_NOT_DURABLE" | "UNSUPPORTED_OP";
209
+ type WativeErrorCode = "WORKSPACE_LOCKED" | "ACCOUNT_LOCKED" | "RECORD_LOCKED" | "BAD_PASSWORD" | "WEAK_PASSWORD" | "RECORD_NOT_FOUND" | "PROVIDER_IO" | "PERMISSION_DENIED" | "DISK_FULL" | "PARAMETER_ERROR" | "DECRYPT_FAILED" | "ENCRYPT_FAILED" | "ALGORITHM_IRREVERSIBLE" | "INVALID_MNEMONIC" | "INVALID_PRIVATE_KEY" | "UNSUPPORTED_NETWORK" | "TX_BUILD_FAILED" | "TX_SIGN_FAILED" | "TX_SUBMIT_FAILED" | "TX_TIMEOUT" | "TX_DROPPED" | "TX_ABORTED" | "RPC_UNREACHABLE"
210
+
211
+ | "RPC_REJECTED" | "STORAGE_NOT_DURABLE" | "UNSUPPORTED_OP";
173
212
  interface WativeErrorOpts {
174
213
  readonly cause?: unknown;
175
214
  readonly details?: Record<string, unknown>;
@@ -312,8 +351,8 @@ declare class EvmTransaction implements Transaction {
312
351
  }>;
313
352
  address?: Address;
314
353
  });
315
- constructor(from: EvmAddress, to: string, value: bigint, data?: string, opts?: {
316
- chainId?: ChainId;
354
+ constructor(from: EvmAddress, to: string, value: bigint, data: string | undefined, opts: {
355
+ chainId: ChainId;
317
356
  type?: EvmTxType;
318
357
  nonce?: number;
319
358
  gasPrice?: bigint;
@@ -714,6 +753,8 @@ declare class Workspace {
714
753
 
715
754
  _registerLiveAccount(account: Account): void;
716
755
 
756
+ _releaseAccount(account: Account): void;
757
+
717
758
  _enqueueNetworkMutation<T>(fn: () => Promise<T>): Promise<T>;
718
759
  private constructor();
719
760
  get locked(): boolean;
@@ -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,20 +170,45 @@ 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
  }
171
208
 
172
- type WativeErrorCode = "WORKSPACE_LOCKED" | "ACCOUNT_LOCKED" | "RECORD_LOCKED" | "BAD_PASSWORD" | "WEAK_PASSWORD" | "RECORD_NOT_FOUND" | "PROVIDER_IO" | "PERMISSION_DENIED" | "DISK_FULL" | "PARAMETER_ERROR" | "DECRYPT_FAILED" | "ENCRYPT_FAILED" | "ALGORITHM_IRREVERSIBLE" | "INVALID_MNEMONIC" | "INVALID_PRIVATE_KEY" | "UNSUPPORTED_NETWORK" | "TX_BUILD_FAILED" | "TX_SIGN_FAILED" | "TX_SUBMIT_FAILED" | "TX_TIMEOUT" | "TX_DROPPED" | "TX_ABORTED" | "RPC_UNREACHABLE" | "STORAGE_NOT_DURABLE" | "UNSUPPORTED_OP";
209
+ type WativeErrorCode = "WORKSPACE_LOCKED" | "ACCOUNT_LOCKED" | "RECORD_LOCKED" | "BAD_PASSWORD" | "WEAK_PASSWORD" | "RECORD_NOT_FOUND" | "PROVIDER_IO" | "PERMISSION_DENIED" | "DISK_FULL" | "PARAMETER_ERROR" | "DECRYPT_FAILED" | "ENCRYPT_FAILED" | "ALGORITHM_IRREVERSIBLE" | "INVALID_MNEMONIC" | "INVALID_PRIVATE_KEY" | "UNSUPPORTED_NETWORK" | "TX_BUILD_FAILED" | "TX_SIGN_FAILED" | "TX_SUBMIT_FAILED" | "TX_TIMEOUT" | "TX_DROPPED" | "TX_ABORTED" | "RPC_UNREACHABLE"
210
+
211
+ | "RPC_REJECTED" | "STORAGE_NOT_DURABLE" | "UNSUPPORTED_OP";
173
212
  interface WativeErrorOpts {
174
213
  readonly cause?: unknown;
175
214
  readonly details?: Record<string, unknown>;
@@ -312,8 +351,8 @@ declare class EvmTransaction implements Transaction {
312
351
  }>;
313
352
  address?: Address;
314
353
  });
315
- constructor(from: EvmAddress, to: string, value: bigint, data?: string, opts?: {
316
- chainId?: ChainId;
354
+ constructor(from: EvmAddress, to: string, value: bigint, data: string | undefined, opts: {
355
+ chainId: ChainId;
317
356
  type?: EvmTxType;
318
357
  nonce?: number;
319
358
  gasPrice?: bigint;
@@ -714,6 +753,8 @@ declare class Workspace {
714
753
 
715
754
  _registerLiveAccount(account: Account): void;
716
755
 
756
+ _releaseAccount(account: Account): void;
757
+
717
758
  _enqueueNetworkMutation<T>(fn: () => Promise<T>): Promise<T>;
718
759
  private constructor();
719
760
  get locked(): boolean;
@@ -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>`.
@@ -37,7 +37,7 @@ All tests work fully offline — no RPC calls, no real networks. Each test creat
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.3.9",
3
+ "version": "2.4.1",
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",