@orbinum/sdk 0.25.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ import { V as VaultStorage, a as VaultConfigRecord, E as EncryptedNoteRecord, b as EncryptedTxRecord, C as CachedNullifier, N as NullifierSyncMeta, S as SpendDetails, D as DeviceKeyStore, c as SecretStore } from '../../secretStore-CF6Nse__.js';
2
+
3
+ /**
4
+ * `VaultStorage` over IndexedDB — the browser's copy of a wallet's notes.
5
+ *
6
+ * The DOM lib reference above is scoped to this file: the package's tsconfig
7
+ * ships `lib: ["esnext"]`, so nothing outside this entry point can reach for a
8
+ * browser API by accident.
9
+ *
10
+ * Ships in its own subpath so a consumer on Node, React Native or a worker
11
+ * implements their own backend and never loads this module — the interface it
12
+ * satisfies lives in the root entry.
13
+ *
14
+ * ## Object stores
15
+ *
16
+ * vault_config one record, id "main": schema version, scan cursor, the
17
+ * ephemeral-index counters
18
+ * vault_notes one record per note, keyed by its BLINDED commitment tag.
19
+ * The note itself is encrypted and its identifiers are
20
+ * blinded, so a database dump reveals nothing linkable to
21
+ * chain activity while equality lookups still work.
22
+ * vault_tx_history encrypted outgoing-transfer records
23
+ * nullifier_set the spent-nullifier mirror. Public chain data, stored in
24
+ * the clear on purpose: it is the same set every wallet
25
+ * downloads, so encrypting it would protect nothing and
26
+ * make membership checks cost a decrypt each.
27
+ * nullifier_sync sync progress for the above
28
+ *
29
+ * The database NAME is supplied by the caller rather than fixed here. One vault
30
+ * per (chain, account) is what keeps a wallet from reading notes that belong to
31
+ * a different chain or a different key, and only the host knows those.
32
+ */
33
+
34
+ interface IndexedDbVaultStorageOptions {
35
+ /**
36
+ * Database name. Use one per (chain, account): a vault opened against the
37
+ * wrong chain holds notes whose commitments no longer exist, and one opened
38
+ * under another account cannot decrypt anything it finds.
39
+ */
40
+ name: string;
41
+ /**
42
+ * IndexedDB factory. Defaults to the global one; pass a fake to test without
43
+ * a browser.
44
+ */
45
+ indexedDB?: IDBFactory | undefined;
46
+ }
47
+ /** Browser-backed `VaultStorage`. One instance per database. */
48
+ declare class IndexedDbVaultStorage implements VaultStorage {
49
+ private readonly name;
50
+ private readonly idb;
51
+ private db;
52
+ constructor(options: IndexedDbVaultStorageOptions);
53
+ private openDB;
54
+ /**
55
+ * Runs one transaction, reopening once if the cached connection was already
56
+ * dead.
57
+ *
58
+ * `onclose` does not fire in every closing path — notably a connection
59
+ * killed between `openDB()` and the transaction call — so this retry is what
60
+ * actually makes the adapter self-healing. `InvalidStateError` means "this
61
+ * handle is finished", and a fresh one is the only fix. Once is enough: a
62
+ * second failure is a real problem, not a stale handle.
63
+ *
64
+ * `run` must build its requests synchronously (no await before the last
65
+ * one), or the transaction auto-commits underneath it.
66
+ */
67
+ private withDB;
68
+ getConfig(): Promise<VaultConfigRecord | null>;
69
+ putConfig(config: VaultConfigRecord): Promise<void>;
70
+ /**
71
+ * Read-modify-write in ONE transaction.
72
+ *
73
+ * Doing this as getConfig() then putConfig() spans two transactions, so two
74
+ * concurrent callers both read the old record and the second write wins. For
75
+ * `selfEphCounter` that lost increment means two notes derive the SAME
76
+ * ephemeral index and publish one ephPk twice, linking them as
77
+ * same-creator — a privacy leak, not a lost UI update. A single readwrite
78
+ * transaction serialises them, since IndexedDB scopes those per store.
79
+ */
80
+ updateConfig(mutate: (config: VaultConfigRecord) => VaultConfigRecord): Promise<VaultConfigRecord | null>;
81
+ hasVault(): Promise<boolean>;
82
+ getAllNoteRecords(): Promise<EncryptedNoteRecord[]>;
83
+ putNote(record: EncryptedNoteRecord): Promise<void>;
84
+ putNotes(records: EncryptedNoteRecord[]): Promise<void>;
85
+ deleteNote(commitmentTag: string): Promise<void>;
86
+ deleteNotes(commitmentTags: string[]): Promise<void>;
87
+ clearNotes(): Promise<void>;
88
+ addTxRecord(record: EncryptedTxRecord): Promise<void>;
89
+ getAllTxRecords(): Promise<EncryptedTxRecord[]>;
90
+ /**
91
+ * Persists one sealed chunk AND the sync progress it produced in a single
92
+ * transaction. Both must land together: progress ahead of the data would
93
+ * make the next sync resume past chunks that were never stored, leaving
94
+ * spent notes looking unspent.
95
+ */
96
+ putNullifierChunk(entries: CachedNullifier[], meta: NullifierSyncMeta): Promise<void>;
97
+ getNullifierSyncMeta(): Promise<NullifierSyncMeta | null>;
98
+ /**
99
+ * Which of `hexes` the cache holds, as batch point-gets.
100
+ *
101
+ * Local lookups are the whole point: asking a server whether one specific
102
+ * nullifier is spent would tell it which notes this wallet owns.
103
+ */
104
+ getSpentNullifiers(hexes: string[]): Promise<Map<string, SpendDetails>>;
105
+ countNullifiers(): Promise<number>;
106
+ clearNullifierCache(): Promise<void>;
107
+ /** Closes the cached connection. The next call reopens it. */
108
+ close(): void;
109
+ }
110
+
111
+ /**
112
+ * A `DeviceKeyStore` backed by a tiny dedicated IndexedDB.
113
+ *
114
+ * IndexedDB rather than localStorage because it stores a `CryptoKey` HANDLE via
115
+ * structured clone. The key is generated non-extractable, so its material never
116
+ * becomes visible to JavaScript — a storage dump yields an opaque handle, not
117
+ * bytes. localStorage can only hold strings, which would mean exporting the key.
118
+ *
119
+ * Its own database, separate from the vault: the device key outlives any single
120
+ * vault and must survive one being dropped.
121
+ */
122
+ declare function createIndexedDbDeviceKeyStore(indexedDBFactory?: IDBFactory): DeviceKeyStore;
123
+ /**
124
+ * The browser device key, generated and persisted on first use.
125
+ *
126
+ * The store is built on the FIRST CALL, not at import time: an extension's
127
+ * service worker and a test environment can both import this module before
128
+ * IndexedDB is reachable, and failing there would take down everything that
129
+ * merely imports the entry point.
130
+ */
131
+ declare const getOrCreateIndexedDbDeviceKey: () => Promise<CryptoKey>;
132
+
133
+ /**
134
+ * A `SecretStore` over Web Storage.
135
+ *
136
+ * No IndexedDB involved — it ships from this entry point because a consumer
137
+ * reaching for browser persistence wants both adapters together, and splitting
138
+ * them across two subpaths would buy nothing.
139
+ */
140
+
141
+ /**
142
+ * A `SecretStore` over Web Storage.
143
+ *
144
+ * Reads fall back to `sessionStorage` so a value written by an older build, or
145
+ * by a deliberately session-scoped flow, is still found. Writes always go to the
146
+ * durable store and clear the session copy, so one key never lives in both.
147
+ *
148
+ * Values are encrypted before they arrive here — see `sessionCache`.
149
+ */
150
+ declare function createWebStorageSecretStore(storage?: Storage, sessionStorageArea?: Storage | null): SecretStore;
151
+
152
+ export { IndexedDbVaultStorage, type IndexedDbVaultStorageOptions, createIndexedDbDeviceKeyStore, createWebStorageSecretStore, getOrCreateIndexedDbDeviceKey };
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters/indexeddb/index.ts
21
+ var indexeddb_exports = {};
22
+ __export(indexeddb_exports, {
23
+ IndexedDbVaultStorage: () => IndexedDbVaultStorage,
24
+ createIndexedDbDeviceKeyStore: () => createIndexedDbDeviceKeyStore,
25
+ createWebStorageSecretStore: () => createWebStorageSecretStore,
26
+ getOrCreateIndexedDbDeviceKey: () => getOrCreateIndexedDbDeviceKey
27
+ });
28
+ module.exports = __toCommonJS(indexeddb_exports);
29
+
30
+ // src/adapters/indexeddb/idb.ts
31
+ function idbRequest(req) {
32
+ return new Promise((resolve, reject) => {
33
+ req.onsuccess = () => resolve(req.result);
34
+ req.onerror = () => reject(req.error);
35
+ });
36
+ }
37
+
38
+ // src/adapters/indexeddb/VaultStorage.ts
39
+ var DB_VERSION = 1;
40
+ var STORE_CONFIG = "vault_config";
41
+ var STORE_NOTES = "vault_notes";
42
+ var STORE_TX_HISTORY = "vault_tx_history";
43
+ var STORE_NULLIFIERS = "nullifier_set";
44
+ var STORE_NULLIFIER_SYNC = "nullifier_sync";
45
+ var IndexedDbVaultStorage = class {
46
+ name;
47
+ idb;
48
+ db = null;
49
+ constructor(options) {
50
+ this.name = options.name;
51
+ const factory = options.indexedDB ?? globalThis.indexedDB;
52
+ if (!factory) {
53
+ throw new Error(
54
+ "IndexedDB is unavailable. Pass `indexedDB`, or use a different VaultStorage."
55
+ );
56
+ }
57
+ this.idb = factory;
58
+ }
59
+ openDB() {
60
+ if (this.db) return Promise.resolve(this.db);
61
+ return new Promise((resolve, reject) => {
62
+ const req = this.idb.open(this.name, DB_VERSION);
63
+ req.onupgradeneeded = (e) => {
64
+ const db = e.target.result;
65
+ for (const [store, keyPath] of [
66
+ [STORE_CONFIG, "id"],
67
+ [STORE_NOTES, "commitmentTag"],
68
+ [STORE_TX_HISTORY, "id"],
69
+ [STORE_NULLIFIERS, "h"],
70
+ [STORE_NULLIFIER_SYNC, "id"]
71
+ ]) {
72
+ if (!db.objectStoreNames.contains(store)) {
73
+ db.createObjectStore(store, { keyPath });
74
+ }
75
+ }
76
+ };
77
+ req.onsuccess = (e) => {
78
+ const db = e.target.result;
79
+ db.onclose = () => {
80
+ if (this.db === db) this.db = null;
81
+ };
82
+ db.onversionchange = () => {
83
+ db.close();
84
+ if (this.db === db) this.db = null;
85
+ };
86
+ this.db = db;
87
+ resolve(db);
88
+ };
89
+ req.onerror = () => reject(new Error(`Failed to open IndexedDB: ${String(req.error?.message)}`));
90
+ });
91
+ }
92
+ /**
93
+ * Runs one transaction, reopening once if the cached connection was already
94
+ * dead.
95
+ *
96
+ * `onclose` does not fire in every closing path — notably a connection
97
+ * killed between `openDB()` and the transaction call — so this retry is what
98
+ * actually makes the adapter self-healing. `InvalidStateError` means "this
99
+ * handle is finished", and a fresh one is the only fix. Once is enough: a
100
+ * second failure is a real problem, not a stale handle.
101
+ *
102
+ * `run` must build its requests synchronously (no await before the last
103
+ * one), or the transaction auto-commits underneath it.
104
+ */
105
+ async withDB(run) {
106
+ try {
107
+ return await run(await this.openDB());
108
+ } catch (err) {
109
+ if (err?.name !== "InvalidStateError") throw err;
110
+ this.db = null;
111
+ return run(await this.openDB());
112
+ }
113
+ }
114
+ // ── Config ───────────────────────────────────────────────────────────────
115
+ async getConfig() {
116
+ return this.withDB(async (db) => {
117
+ const tx = db.transaction(STORE_CONFIG, "readonly");
118
+ const result = await idbRequest(
119
+ tx.objectStore(STORE_CONFIG).get("main")
120
+ );
121
+ return result ?? null;
122
+ });
123
+ }
124
+ async putConfig(config) {
125
+ await this.withDB(async (db) => {
126
+ const tx = db.transaction(STORE_CONFIG, "readwrite");
127
+ await idbRequest(tx.objectStore(STORE_CONFIG).put(config));
128
+ });
129
+ }
130
+ /**
131
+ * Read-modify-write in ONE transaction.
132
+ *
133
+ * Doing this as getConfig() then putConfig() spans two transactions, so two
134
+ * concurrent callers both read the old record and the second write wins. For
135
+ * `selfEphCounter` that lost increment means two notes derive the SAME
136
+ * ephemeral index and publish one ephPk twice, linking them as
137
+ * same-creator — a privacy leak, not a lost UI update. A single readwrite
138
+ * transaction serialises them, since IndexedDB scopes those per store.
139
+ */
140
+ async updateConfig(mutate) {
141
+ return this.withDB(async (db) => {
142
+ const tx = db.transaction(STORE_CONFIG, "readwrite");
143
+ const store = tx.objectStore(STORE_CONFIG);
144
+ const current = await idbRequest(store.get("main"));
145
+ if (!current) return null;
146
+ const updated = mutate(current);
147
+ await idbRequest(store.put(updated));
148
+ return updated;
149
+ });
150
+ }
151
+ async hasVault() {
152
+ return await this.getConfig() !== null;
153
+ }
154
+ // ── Notes ────────────────────────────────────────────────────────────────
155
+ async getAllNoteRecords() {
156
+ return this.withDB((db) => {
157
+ const tx = db.transaction(STORE_NOTES, "readonly");
158
+ return idbRequest(tx.objectStore(STORE_NOTES).getAll());
159
+ });
160
+ }
161
+ async putNote(record) {
162
+ await this.putNotes([record]);
163
+ }
164
+ async putNotes(records) {
165
+ if (records.length === 0) return;
166
+ await this.withDB(async (db) => {
167
+ const tx = db.transaction(STORE_NOTES, "readwrite");
168
+ const store = tx.objectStore(STORE_NOTES);
169
+ await Promise.all(records.map((r) => idbRequest(store.put(r))));
170
+ });
171
+ }
172
+ async deleteNote(commitmentTag) {
173
+ await this.deleteNotes([commitmentTag]);
174
+ }
175
+ async deleteNotes(commitmentTags) {
176
+ if (commitmentTags.length === 0) return;
177
+ await this.withDB(async (db) => {
178
+ const tx = db.transaction(STORE_NOTES, "readwrite");
179
+ const store = tx.objectStore(STORE_NOTES);
180
+ await Promise.all(commitmentTags.map((tag) => idbRequest(store.delete(tag))));
181
+ });
182
+ }
183
+ async clearNotes() {
184
+ await this.withDB(async (db) => {
185
+ const tx = db.transaction(STORE_NOTES, "readwrite");
186
+ await idbRequest(tx.objectStore(STORE_NOTES).clear());
187
+ });
188
+ }
189
+ // ── Transaction history ──────────────────────────────────────────────────
190
+ // Storage-dumb: rows are written and read as-is. Encryption lives in
191
+ // VaultStore, which is what a caller should use.
192
+ async addTxRecord(record) {
193
+ await this.withDB(async (db) => {
194
+ const tx = db.transaction(STORE_TX_HISTORY, "readwrite");
195
+ await idbRequest(tx.objectStore(STORE_TX_HISTORY).put(record));
196
+ });
197
+ }
198
+ async getAllTxRecords() {
199
+ return this.withDB((db) => {
200
+ const tx = db.transaction(STORE_TX_HISTORY, "readonly");
201
+ return idbRequest(tx.objectStore(STORE_TX_HISTORY).getAll());
202
+ });
203
+ }
204
+ // ── Nullifier cache ──────────────────────────────────────────────────────
205
+ /**
206
+ * Persists one sealed chunk AND the sync progress it produced in a single
207
+ * transaction. Both must land together: progress ahead of the data would
208
+ * make the next sync resume past chunks that were never stored, leaving
209
+ * spent notes looking unspent.
210
+ */
211
+ async putNullifierChunk(entries, meta) {
212
+ await this.withDB(async (db) => {
213
+ const tx = db.transaction([STORE_NULLIFIERS, STORE_NULLIFIER_SYNC], "readwrite");
214
+ const store = tx.objectStore(STORE_NULLIFIERS);
215
+ await Promise.all([
216
+ ...entries.map((e) => idbRequest(store.put(e))),
217
+ idbRequest(tx.objectStore(STORE_NULLIFIER_SYNC).put(meta))
218
+ ]);
219
+ });
220
+ }
221
+ async getNullifierSyncMeta() {
222
+ return this.withDB(async (db) => {
223
+ const tx = db.transaction(STORE_NULLIFIER_SYNC, "readonly");
224
+ const result = await idbRequest(
225
+ tx.objectStore(STORE_NULLIFIER_SYNC).get("main")
226
+ );
227
+ return result ?? null;
228
+ });
229
+ }
230
+ /**
231
+ * Which of `hexes` the cache holds, as batch point-gets.
232
+ *
233
+ * Local lookups are the whole point: asking a server whether one specific
234
+ * nullifier is spent would tell it which notes this wallet owns.
235
+ */
236
+ async getSpentNullifiers(hexes) {
237
+ if (hexes.length === 0) return /* @__PURE__ */ new Map();
238
+ return this.withDB(async (db) => {
239
+ const tx = db.transaction(STORE_NULLIFIERS, "readonly");
240
+ const store = tx.objectStore(STORE_NULLIFIERS);
241
+ const results = await Promise.all(
242
+ hexes.map((h) => idbRequest(store.get(h)))
243
+ );
244
+ return new Map(
245
+ results.filter((r) => r !== void 0).map((r) => [r.h, { spentAt: r.ts ?? null, txHash: r.tx ?? null }])
246
+ );
247
+ });
248
+ }
249
+ async countNullifiers() {
250
+ return this.withDB((db) => {
251
+ const tx = db.transaction(STORE_NULLIFIERS, "readonly");
252
+ return idbRequest(tx.objectStore(STORE_NULLIFIERS).count());
253
+ });
254
+ }
255
+ async clearNullifierCache() {
256
+ await this.withDB(async (db) => {
257
+ const tx = db.transaction([STORE_NULLIFIERS, STORE_NULLIFIER_SYNC], "readwrite");
258
+ await Promise.all([
259
+ idbRequest(tx.objectStore(STORE_NULLIFIERS).clear()),
260
+ idbRequest(tx.objectStore(STORE_NULLIFIER_SYNC).clear())
261
+ ]);
262
+ });
263
+ }
264
+ /** Closes the cached connection. The next call reopens it. */
265
+ close() {
266
+ this.db?.close();
267
+ this.db = null;
268
+ }
269
+ };
270
+
271
+ // src/wallet/identity/deviceKey.ts
272
+ async function generateDeviceKey(extractable = false) {
273
+ return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, extractable, [
274
+ "encrypt",
275
+ "decrypt"
276
+ ]);
277
+ }
278
+ function createDeviceKeyProvider(store) {
279
+ let cached = null;
280
+ let inFlight = null;
281
+ return async function getOrCreateDeviceKey() {
282
+ if (cached) return cached;
283
+ if (inFlight) return inFlight;
284
+ inFlight = (async () => {
285
+ const existing = await store.load();
286
+ if (existing) return existing;
287
+ const key = await generateDeviceKey();
288
+ await store.save(key);
289
+ return key;
290
+ })();
291
+ try {
292
+ cached = await inFlight;
293
+ return cached;
294
+ } finally {
295
+ inFlight = null;
296
+ }
297
+ };
298
+ }
299
+
300
+ // src/adapters/indexeddb/deviceKeyStore.ts
301
+ var KEYSTORE_DB = "orbinum-keystore";
302
+ var KEYSTORE_STORE = "keys";
303
+ var DEVICE_KEY_ID = "device";
304
+ function createIndexedDbDeviceKeyStore(indexedDBFactory) {
305
+ const idb = indexedDBFactory ?? globalThis.indexedDB;
306
+ if (!idb) throw new Error("IndexedDB is unavailable; supply a different DeviceKeyStore.");
307
+ const open = () => new Promise((resolve, reject) => {
308
+ const req = idb.open(KEYSTORE_DB, 1);
309
+ req.onupgradeneeded = () => {
310
+ const db = req.result;
311
+ if (!db.objectStoreNames.contains(KEYSTORE_STORE)) {
312
+ db.createObjectStore(KEYSTORE_STORE, { keyPath: "id" });
313
+ }
314
+ };
315
+ req.onsuccess = () => resolve(req.result);
316
+ req.onerror = () => reject(new Error(`Failed to open keystore: ${String(req.error?.message)}`));
317
+ });
318
+ return {
319
+ async load() {
320
+ const db = await open();
321
+ try {
322
+ const row = await idbRequest(
323
+ db.transaction(KEYSTORE_STORE, "readonly").objectStore(KEYSTORE_STORE).get(DEVICE_KEY_ID)
324
+ );
325
+ return row?.key ?? null;
326
+ } finally {
327
+ db.close();
328
+ }
329
+ },
330
+ async save(key) {
331
+ const db = await open();
332
+ try {
333
+ await idbRequest(
334
+ db.transaction(KEYSTORE_STORE, "readwrite").objectStore(KEYSTORE_STORE).put({ id: DEVICE_KEY_ID, key })
335
+ );
336
+ } finally {
337
+ db.close();
338
+ }
339
+ }
340
+ };
341
+ }
342
+ var getOrCreateIndexedDbDeviceKey = createDeviceKeyProvider({
343
+ load: () => createIndexedDbDeviceKeyStore().load(),
344
+ save: (key) => createIndexedDbDeviceKeyStore().save(key)
345
+ });
346
+
347
+ // src/adapters/indexeddb/secretStore.ts
348
+ function createWebStorageSecretStore(storage, sessionStorageArea) {
349
+ const durable = () => {
350
+ const area = storage ?? globalThis.localStorage;
351
+ if (!area) throw new Error("Web Storage is unavailable; supply a different SecretStore.");
352
+ return area;
353
+ };
354
+ const session = () => sessionStorageArea === void 0 ? globalThis.sessionStorage ?? null : sessionStorageArea;
355
+ return {
356
+ async get(key) {
357
+ return durable().getItem(key) ?? session()?.getItem(key) ?? null;
358
+ },
359
+ async set(key, value) {
360
+ durable().setItem(key, value);
361
+ session()?.removeItem(key);
362
+ },
363
+ async remove(key) {
364
+ durable().removeItem(key);
365
+ session()?.removeItem(key);
366
+ },
367
+ async keys() {
368
+ const all = new Set(Object.keys(durable()));
369
+ const area = session();
370
+ if (area) for (const k of Object.keys(area)) all.add(k);
371
+ return [...all];
372
+ }
373
+ };
374
+ }
375
+ // Annotate the CommonJS export names for ESM import in node:
376
+ 0 && (module.exports = {
377
+ IndexedDbVaultStorage,
378
+ createIndexedDbDeviceKeyStore,
379
+ createWebStorageSecretStore,
380
+ getOrCreateIndexedDbDeviceKey
381
+ });