@orbinum/sdk 0.25.1 → 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.
- package/README.md +355 -23
- package/dist/adapters/indexeddb/index.d.mts +152 -0
- package/dist/adapters/indexeddb/index.d.ts +152 -0
- package/dist/adapters/indexeddb/index.js +381 -0
- package/dist/adapters/indexeddb/index.mjs +326 -0
- package/dist/chunk-JMYU5QAK.mjs +40 -0
- package/dist/chunk-Y6LNYJAJ.mjs +1242 -0
- package/dist/index-JYVjYJtf.d.mts +353 -0
- package/dist/index-JYVjYJtf.d.ts +353 -0
- package/dist/index.d.mts +4772 -3018
- package/dist/index.d.ts +4772 -3018
- package/dist/index.js +6194 -3317
- package/dist/index.mjs +5562 -3947
- package/dist/secretStore-CF6Nse__.d.mts +292 -0
- package/dist/secretStore-CF6Nse__.d.ts +292 -0
- package/dist/wallet/worker/index.d.mts +1 -0
- package/dist/wallet/worker/index.d.ts +1 -0
- package/dist/wallet/worker/index.js +859 -0
- package/dist/wallet/worker/index.mjs +28 -0
- package/package.json +23 -6
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDeviceKeyProvider
|
|
3
|
+
} from "../../chunk-JMYU5QAK.mjs";
|
|
4
|
+
|
|
5
|
+
// src/adapters/indexeddb/idb.ts
|
|
6
|
+
function idbRequest(req) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
req.onsuccess = () => resolve(req.result);
|
|
9
|
+
req.onerror = () => reject(req.error);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/adapters/indexeddb/VaultStorage.ts
|
|
14
|
+
var DB_VERSION = 1;
|
|
15
|
+
var STORE_CONFIG = "vault_config";
|
|
16
|
+
var STORE_NOTES = "vault_notes";
|
|
17
|
+
var STORE_TX_HISTORY = "vault_tx_history";
|
|
18
|
+
var STORE_NULLIFIERS = "nullifier_set";
|
|
19
|
+
var STORE_NULLIFIER_SYNC = "nullifier_sync";
|
|
20
|
+
var IndexedDbVaultStorage = class {
|
|
21
|
+
name;
|
|
22
|
+
idb;
|
|
23
|
+
db = null;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.name = options.name;
|
|
26
|
+
const factory = options.indexedDB ?? globalThis.indexedDB;
|
|
27
|
+
if (!factory) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"IndexedDB is unavailable. Pass `indexedDB`, or use a different VaultStorage."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
this.idb = factory;
|
|
33
|
+
}
|
|
34
|
+
openDB() {
|
|
35
|
+
if (this.db) return Promise.resolve(this.db);
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const req = this.idb.open(this.name, DB_VERSION);
|
|
38
|
+
req.onupgradeneeded = (e) => {
|
|
39
|
+
const db = e.target.result;
|
|
40
|
+
for (const [store, keyPath] of [
|
|
41
|
+
[STORE_CONFIG, "id"],
|
|
42
|
+
[STORE_NOTES, "commitmentTag"],
|
|
43
|
+
[STORE_TX_HISTORY, "id"],
|
|
44
|
+
[STORE_NULLIFIERS, "h"],
|
|
45
|
+
[STORE_NULLIFIER_SYNC, "id"]
|
|
46
|
+
]) {
|
|
47
|
+
if (!db.objectStoreNames.contains(store)) {
|
|
48
|
+
db.createObjectStore(store, { keyPath });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
req.onsuccess = (e) => {
|
|
53
|
+
const db = e.target.result;
|
|
54
|
+
db.onclose = () => {
|
|
55
|
+
if (this.db === db) this.db = null;
|
|
56
|
+
};
|
|
57
|
+
db.onversionchange = () => {
|
|
58
|
+
db.close();
|
|
59
|
+
if (this.db === db) this.db = null;
|
|
60
|
+
};
|
|
61
|
+
this.db = db;
|
|
62
|
+
resolve(db);
|
|
63
|
+
};
|
|
64
|
+
req.onerror = () => reject(new Error(`Failed to open IndexedDB: ${String(req.error?.message)}`));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Runs one transaction, reopening once if the cached connection was already
|
|
69
|
+
* dead.
|
|
70
|
+
*
|
|
71
|
+
* `onclose` does not fire in every closing path — notably a connection
|
|
72
|
+
* killed between `openDB()` and the transaction call — so this retry is what
|
|
73
|
+
* actually makes the adapter self-healing. `InvalidStateError` means "this
|
|
74
|
+
* handle is finished", and a fresh one is the only fix. Once is enough: a
|
|
75
|
+
* second failure is a real problem, not a stale handle.
|
|
76
|
+
*
|
|
77
|
+
* `run` must build its requests synchronously (no await before the last
|
|
78
|
+
* one), or the transaction auto-commits underneath it.
|
|
79
|
+
*/
|
|
80
|
+
async withDB(run) {
|
|
81
|
+
try {
|
|
82
|
+
return await run(await this.openDB());
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err?.name !== "InvalidStateError") throw err;
|
|
85
|
+
this.db = null;
|
|
86
|
+
return run(await this.openDB());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ── Config ───────────────────────────────────────────────────────────────
|
|
90
|
+
async getConfig() {
|
|
91
|
+
return this.withDB(async (db) => {
|
|
92
|
+
const tx = db.transaction(STORE_CONFIG, "readonly");
|
|
93
|
+
const result = await idbRequest(
|
|
94
|
+
tx.objectStore(STORE_CONFIG).get("main")
|
|
95
|
+
);
|
|
96
|
+
return result ?? null;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async putConfig(config) {
|
|
100
|
+
await this.withDB(async (db) => {
|
|
101
|
+
const tx = db.transaction(STORE_CONFIG, "readwrite");
|
|
102
|
+
await idbRequest(tx.objectStore(STORE_CONFIG).put(config));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Read-modify-write in ONE transaction.
|
|
107
|
+
*
|
|
108
|
+
* Doing this as getConfig() then putConfig() spans two transactions, so two
|
|
109
|
+
* concurrent callers both read the old record and the second write wins. For
|
|
110
|
+
* `selfEphCounter` that lost increment means two notes derive the SAME
|
|
111
|
+
* ephemeral index and publish one ephPk twice, linking them as
|
|
112
|
+
* same-creator — a privacy leak, not a lost UI update. A single readwrite
|
|
113
|
+
* transaction serialises them, since IndexedDB scopes those per store.
|
|
114
|
+
*/
|
|
115
|
+
async updateConfig(mutate) {
|
|
116
|
+
return this.withDB(async (db) => {
|
|
117
|
+
const tx = db.transaction(STORE_CONFIG, "readwrite");
|
|
118
|
+
const store = tx.objectStore(STORE_CONFIG);
|
|
119
|
+
const current = await idbRequest(store.get("main"));
|
|
120
|
+
if (!current) return null;
|
|
121
|
+
const updated = mutate(current);
|
|
122
|
+
await idbRequest(store.put(updated));
|
|
123
|
+
return updated;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async hasVault() {
|
|
127
|
+
return await this.getConfig() !== null;
|
|
128
|
+
}
|
|
129
|
+
// ── Notes ────────────────────────────────────────────────────────────────
|
|
130
|
+
async getAllNoteRecords() {
|
|
131
|
+
return this.withDB((db) => {
|
|
132
|
+
const tx = db.transaction(STORE_NOTES, "readonly");
|
|
133
|
+
return idbRequest(tx.objectStore(STORE_NOTES).getAll());
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async putNote(record) {
|
|
137
|
+
await this.putNotes([record]);
|
|
138
|
+
}
|
|
139
|
+
async putNotes(records) {
|
|
140
|
+
if (records.length === 0) return;
|
|
141
|
+
await this.withDB(async (db) => {
|
|
142
|
+
const tx = db.transaction(STORE_NOTES, "readwrite");
|
|
143
|
+
const store = tx.objectStore(STORE_NOTES);
|
|
144
|
+
await Promise.all(records.map((r) => idbRequest(store.put(r))));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async deleteNote(commitmentTag) {
|
|
148
|
+
await this.deleteNotes([commitmentTag]);
|
|
149
|
+
}
|
|
150
|
+
async deleteNotes(commitmentTags) {
|
|
151
|
+
if (commitmentTags.length === 0) return;
|
|
152
|
+
await this.withDB(async (db) => {
|
|
153
|
+
const tx = db.transaction(STORE_NOTES, "readwrite");
|
|
154
|
+
const store = tx.objectStore(STORE_NOTES);
|
|
155
|
+
await Promise.all(commitmentTags.map((tag) => idbRequest(store.delete(tag))));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async clearNotes() {
|
|
159
|
+
await this.withDB(async (db) => {
|
|
160
|
+
const tx = db.transaction(STORE_NOTES, "readwrite");
|
|
161
|
+
await idbRequest(tx.objectStore(STORE_NOTES).clear());
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// ── Transaction history ──────────────────────────────────────────────────
|
|
165
|
+
// Storage-dumb: rows are written and read as-is. Encryption lives in
|
|
166
|
+
// VaultStore, which is what a caller should use.
|
|
167
|
+
async addTxRecord(record) {
|
|
168
|
+
await this.withDB(async (db) => {
|
|
169
|
+
const tx = db.transaction(STORE_TX_HISTORY, "readwrite");
|
|
170
|
+
await idbRequest(tx.objectStore(STORE_TX_HISTORY).put(record));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async getAllTxRecords() {
|
|
174
|
+
return this.withDB((db) => {
|
|
175
|
+
const tx = db.transaction(STORE_TX_HISTORY, "readonly");
|
|
176
|
+
return idbRequest(tx.objectStore(STORE_TX_HISTORY).getAll());
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// ── Nullifier cache ──────────────────────────────────────────────────────
|
|
180
|
+
/**
|
|
181
|
+
* Persists one sealed chunk AND the sync progress it produced in a single
|
|
182
|
+
* transaction. Both must land together: progress ahead of the data would
|
|
183
|
+
* make the next sync resume past chunks that were never stored, leaving
|
|
184
|
+
* spent notes looking unspent.
|
|
185
|
+
*/
|
|
186
|
+
async putNullifierChunk(entries, meta) {
|
|
187
|
+
await this.withDB(async (db) => {
|
|
188
|
+
const tx = db.transaction([STORE_NULLIFIERS, STORE_NULLIFIER_SYNC], "readwrite");
|
|
189
|
+
const store = tx.objectStore(STORE_NULLIFIERS);
|
|
190
|
+
await Promise.all([
|
|
191
|
+
...entries.map((e) => idbRequest(store.put(e))),
|
|
192
|
+
idbRequest(tx.objectStore(STORE_NULLIFIER_SYNC).put(meta))
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async getNullifierSyncMeta() {
|
|
197
|
+
return this.withDB(async (db) => {
|
|
198
|
+
const tx = db.transaction(STORE_NULLIFIER_SYNC, "readonly");
|
|
199
|
+
const result = await idbRequest(
|
|
200
|
+
tx.objectStore(STORE_NULLIFIER_SYNC).get("main")
|
|
201
|
+
);
|
|
202
|
+
return result ?? null;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Which of `hexes` the cache holds, as batch point-gets.
|
|
207
|
+
*
|
|
208
|
+
* Local lookups are the whole point: asking a server whether one specific
|
|
209
|
+
* nullifier is spent would tell it which notes this wallet owns.
|
|
210
|
+
*/
|
|
211
|
+
async getSpentNullifiers(hexes) {
|
|
212
|
+
if (hexes.length === 0) return /* @__PURE__ */ new Map();
|
|
213
|
+
return this.withDB(async (db) => {
|
|
214
|
+
const tx = db.transaction(STORE_NULLIFIERS, "readonly");
|
|
215
|
+
const store = tx.objectStore(STORE_NULLIFIERS);
|
|
216
|
+
const results = await Promise.all(
|
|
217
|
+
hexes.map((h) => idbRequest(store.get(h)))
|
|
218
|
+
);
|
|
219
|
+
return new Map(
|
|
220
|
+
results.filter((r) => r !== void 0).map((r) => [r.h, { spentAt: r.ts ?? null, txHash: r.tx ?? null }])
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async countNullifiers() {
|
|
225
|
+
return this.withDB((db) => {
|
|
226
|
+
const tx = db.transaction(STORE_NULLIFIERS, "readonly");
|
|
227
|
+
return idbRequest(tx.objectStore(STORE_NULLIFIERS).count());
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async clearNullifierCache() {
|
|
231
|
+
await this.withDB(async (db) => {
|
|
232
|
+
const tx = db.transaction([STORE_NULLIFIERS, STORE_NULLIFIER_SYNC], "readwrite");
|
|
233
|
+
await Promise.all([
|
|
234
|
+
idbRequest(tx.objectStore(STORE_NULLIFIERS).clear()),
|
|
235
|
+
idbRequest(tx.objectStore(STORE_NULLIFIER_SYNC).clear())
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/** Closes the cached connection. The next call reopens it. */
|
|
240
|
+
close() {
|
|
241
|
+
this.db?.close();
|
|
242
|
+
this.db = null;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/adapters/indexeddb/deviceKeyStore.ts
|
|
247
|
+
var KEYSTORE_DB = "orbinum-keystore";
|
|
248
|
+
var KEYSTORE_STORE = "keys";
|
|
249
|
+
var DEVICE_KEY_ID = "device";
|
|
250
|
+
function createIndexedDbDeviceKeyStore(indexedDBFactory) {
|
|
251
|
+
const idb = indexedDBFactory ?? globalThis.indexedDB;
|
|
252
|
+
if (!idb) throw new Error("IndexedDB is unavailable; supply a different DeviceKeyStore.");
|
|
253
|
+
const open = () => new Promise((resolve, reject) => {
|
|
254
|
+
const req = idb.open(KEYSTORE_DB, 1);
|
|
255
|
+
req.onupgradeneeded = () => {
|
|
256
|
+
const db = req.result;
|
|
257
|
+
if (!db.objectStoreNames.contains(KEYSTORE_STORE)) {
|
|
258
|
+
db.createObjectStore(KEYSTORE_STORE, { keyPath: "id" });
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
req.onsuccess = () => resolve(req.result);
|
|
262
|
+
req.onerror = () => reject(new Error(`Failed to open keystore: ${String(req.error?.message)}`));
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
async load() {
|
|
266
|
+
const db = await open();
|
|
267
|
+
try {
|
|
268
|
+
const row = await idbRequest(
|
|
269
|
+
db.transaction(KEYSTORE_STORE, "readonly").objectStore(KEYSTORE_STORE).get(DEVICE_KEY_ID)
|
|
270
|
+
);
|
|
271
|
+
return row?.key ?? null;
|
|
272
|
+
} finally {
|
|
273
|
+
db.close();
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
async save(key) {
|
|
277
|
+
const db = await open();
|
|
278
|
+
try {
|
|
279
|
+
await idbRequest(
|
|
280
|
+
db.transaction(KEYSTORE_STORE, "readwrite").objectStore(KEYSTORE_STORE).put({ id: DEVICE_KEY_ID, key })
|
|
281
|
+
);
|
|
282
|
+
} finally {
|
|
283
|
+
db.close();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
var getOrCreateIndexedDbDeviceKey = createDeviceKeyProvider({
|
|
289
|
+
load: () => createIndexedDbDeviceKeyStore().load(),
|
|
290
|
+
save: (key) => createIndexedDbDeviceKeyStore().save(key)
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// src/adapters/indexeddb/secretStore.ts
|
|
294
|
+
function createWebStorageSecretStore(storage, sessionStorageArea) {
|
|
295
|
+
const durable = () => {
|
|
296
|
+
const area = storage ?? globalThis.localStorage;
|
|
297
|
+
if (!area) throw new Error("Web Storage is unavailable; supply a different SecretStore.");
|
|
298
|
+
return area;
|
|
299
|
+
};
|
|
300
|
+
const session = () => sessionStorageArea === void 0 ? globalThis.sessionStorage ?? null : sessionStorageArea;
|
|
301
|
+
return {
|
|
302
|
+
async get(key) {
|
|
303
|
+
return durable().getItem(key) ?? session()?.getItem(key) ?? null;
|
|
304
|
+
},
|
|
305
|
+
async set(key, value) {
|
|
306
|
+
durable().setItem(key, value);
|
|
307
|
+
session()?.removeItem(key);
|
|
308
|
+
},
|
|
309
|
+
async remove(key) {
|
|
310
|
+
durable().removeItem(key);
|
|
311
|
+
session()?.removeItem(key);
|
|
312
|
+
},
|
|
313
|
+
async keys() {
|
|
314
|
+
const all = new Set(Object.keys(durable()));
|
|
315
|
+
const area = session();
|
|
316
|
+
if (area) for (const k of Object.keys(area)) all.add(k);
|
|
317
|
+
return [...all];
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export {
|
|
322
|
+
IndexedDbVaultStorage,
|
|
323
|
+
createIndexedDbDeviceKeyStore,
|
|
324
|
+
createWebStorageSecretStore,
|
|
325
|
+
getOrCreateIndexedDbDeviceKey
|
|
326
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/wallet/identity/deviceKey.ts
|
|
2
|
+
async function generateDeviceKey(extractable = false) {
|
|
3
|
+
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, extractable, [
|
|
4
|
+
"encrypt",
|
|
5
|
+
"decrypt"
|
|
6
|
+
]);
|
|
7
|
+
}
|
|
8
|
+
async function importDeviceKey(raw) {
|
|
9
|
+
if (raw.length !== 32) {
|
|
10
|
+
throw new Error(`Device key must be 32 bytes, got ${raw.length}.`);
|
|
11
|
+
}
|
|
12
|
+
return crypto.subtle.importKey("raw", raw.slice(0), "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
13
|
+
}
|
|
14
|
+
function createDeviceKeyProvider(store) {
|
|
15
|
+
let cached = null;
|
|
16
|
+
let inFlight = null;
|
|
17
|
+
return async function getOrCreateDeviceKey() {
|
|
18
|
+
if (cached) return cached;
|
|
19
|
+
if (inFlight) return inFlight;
|
|
20
|
+
inFlight = (async () => {
|
|
21
|
+
const existing = await store.load();
|
|
22
|
+
if (existing) return existing;
|
|
23
|
+
const key = await generateDeviceKey();
|
|
24
|
+
await store.save(key);
|
|
25
|
+
return key;
|
|
26
|
+
})();
|
|
27
|
+
try {
|
|
28
|
+
cached = await inFlight;
|
|
29
|
+
return cached;
|
|
30
|
+
} finally {
|
|
31
|
+
inFlight = null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
generateDeviceKey,
|
|
38
|
+
importDeviceKey,
|
|
39
|
+
createDeviceKeyProvider
|
|
40
|
+
};
|