@qntx/wallet-engine 0.0.0-stage → 0.4.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/dist/idb.d.mts ADDED
@@ -0,0 +1,28 @@
1
+ //#region src/store/port.d.ts
2
+ /**
3
+ * Engine storage port: named collections of byte values. `collection` is synchronous and cheap —
4
+ * the same collection object backs every call, so guards (closed) live on the store.
5
+ */
6
+ type EngineCollection = {
7
+ readonly get: (key: string) => Promise<Uint8Array | undefined>;
8
+ readonly put: (key: string, bytes: Uint8Array) => Promise<void>;
9
+ readonly delete: (key: string) => Promise<void>;
10
+ /** Every key in the collection, in an unspecified order. */
11
+ readonly list: () => Promise<ReadonlyArray<string>>;
12
+ };
13
+ type EngineStore = {
14
+ readonly collection: (name: string) => EngineCollection;
15
+ readonly close: () => Promise<void>;
16
+ };
17
+ //#endregion
18
+ //#region src/idb/indexeddb.d.ts
19
+ export type Random = (length: number) => Uint8Array;
20
+ export type IndexedDbEngineStoreOptions = {
21
+ readonly name: string;
22
+ readonly dataKey: Uint8Array;
23
+ readonly random: Random;
24
+ };
25
+ export declare function openIndexedDbEngineStore(options: IndexedDbEngineStoreOptions): Promise<EngineStore>;
26
+ //#endregion
27
+ export {};
28
+ //# sourceMappingURL=idb.d.mts.map
package/dist/idb.mjs ADDED
@@ -0,0 +1,134 @@
1
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
2
+ //#region src/errors.ts
3
+ /** Typed engine errors: store codes are `closed` and `integrity` (decrypt or bind failure). */
4
+ var EngineStoreError = class extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.name = "EngineStoreError";
9
+ this.code = code;
10
+ }
11
+ };
12
+ //#endregion
13
+ //#region src/store/guard.ts
14
+ /** NUL is the composite-key/AAD separator — a name or key containing it collides. */
15
+ function checkName(kind, value) {
16
+ if (value.includes("\0")) throw new EngineStoreError("invalid-input", `${kind} must not contain NUL`);
17
+ }
18
+ //#endregion
19
+ //#region src/idb/indexeddb.ts
20
+ /**
21
+ * The Web EngineStore: one IndexedDB database, one `records` object store, values encrypted per
22
+ * record with XChaCha20-Poly1305. Key = the injected 32-byte data key; nonce = 24 random bytes
23
+ * stored as nonce‖ciphertext; AAD = UTF-8 `<collection>\0<key>` — moving a record to another key or
24
+ * collection fails the MAC, same as a tampered payload. `close()` drops the connection, wipes the
25
+ * module-held key copy and latches every call to `closed`.
26
+ */
27
+ const STORE = "records";
28
+ const id = (collection, recordKey) => `${collection}\u0000${recordKey}`;
29
+ const aad = (collection, recordKey) => new TextEncoder().encode(`${collection}\u0000${recordKey}`);
30
+ async function request(req) {
31
+ return new Promise((resolve, reject) => {
32
+ req.addEventListener("success", () => {
33
+ resolve(req.result);
34
+ });
35
+ req.addEventListener("error", () => {
36
+ reject(new Error("indexeddb request failed", { cause: req.error }));
37
+ });
38
+ });
39
+ }
40
+ async function done(tx) {
41
+ return new Promise((resolve, reject) => {
42
+ tx.addEventListener("complete", () => {
43
+ resolve();
44
+ });
45
+ tx.addEventListener("error", () => {
46
+ reject(new Error("indexeddb transaction failed", { cause: tx.error }));
47
+ });
48
+ tx.addEventListener("abort", () => {
49
+ reject(new Error("indexeddb transaction aborted", { cause: tx.error }));
50
+ });
51
+ });
52
+ }
53
+ async function openIndexedDbEngineStore(options) {
54
+ const key = new Uint8Array(options.dataKey);
55
+ return new Promise((resolve, reject) => {
56
+ const open = indexedDB.open(options.name, 1);
57
+ open.addEventListener("upgradeneeded", () => {
58
+ open.result.createObjectStore(STORE);
59
+ });
60
+ open.addEventListener("success", () => {
61
+ resolve(makeStore(open.result, key, options.random));
62
+ });
63
+ open.addEventListener("error", () => {
64
+ key.fill(0);
65
+ reject(new Error("indexeddb open failed", { cause: open.error }));
66
+ });
67
+ });
68
+ }
69
+ function makeStore(db, key, random) {
70
+ let closed = false;
71
+ const guard = () => {
72
+ if (closed) throw new EngineStoreError("closed", "engine store is closed");
73
+ return db;
74
+ };
75
+ const seal = (collection, recordKey, bytes) => {
76
+ const nonce = random(24);
77
+ const ciphertext = xchacha20poly1305(key, nonce, aad(collection, recordKey)).encrypt(bytes);
78
+ const record = new Uint8Array(24 + ciphertext.length);
79
+ record.set(nonce, 0);
80
+ record.set(ciphertext, 24);
81
+ return record;
82
+ };
83
+ const open = (collection, recordKey, record) => {
84
+ if (record.length < 24) throw new EngineStoreError("integrity", "engine record too short");
85
+ try {
86
+ return xchacha20poly1305(key, record.slice(0, 24), aad(collection, recordKey)).decrypt(record.slice(24));
87
+ } catch {
88
+ throw new EngineStoreError("integrity", "engine record failed integrity");
89
+ }
90
+ };
91
+ const make = (name) => ({
92
+ get: async (recordKey) => {
93
+ const database = guard();
94
+ checkName("key", recordKey);
95
+ const record = await request(database.transaction(STORE, "readonly").objectStore(STORE).get(id(name, recordKey)));
96
+ return record === void 0 ? void 0 : open(name, recordKey, record);
97
+ },
98
+ put: async (recordKey, bytes) => {
99
+ const database = guard();
100
+ checkName("key", recordKey);
101
+ const tx = database.transaction(STORE, "readwrite");
102
+ tx.objectStore(STORE).put(seal(name, recordKey, bytes), id(name, recordKey));
103
+ await done(tx);
104
+ },
105
+ delete: async (recordKey) => {
106
+ const database = guard();
107
+ checkName("key", recordKey);
108
+ const tx = database.transaction(STORE, "readwrite");
109
+ tx.objectStore(STORE).delete(id(name, recordKey));
110
+ await done(tx);
111
+ },
112
+ list: async () => {
113
+ const keys = await request(guard().transaction(STORE, "readonly").objectStore(STORE).getAllKeys());
114
+ const prefix = `${name}\u0000`;
115
+ return keys.filter((k) => typeof k === "string" && k.startsWith(prefix)).map((k) => k.slice(prefix.length));
116
+ }
117
+ });
118
+ return {
119
+ collection: (name) => {
120
+ checkName("collection", name);
121
+ return make(name);
122
+ },
123
+ close: async () => {
124
+ closed = true;
125
+ db.close();
126
+ key.fill(0);
127
+ await Promise.resolve();
128
+ }
129
+ };
130
+ }
131
+ //#endregion
132
+ export { openIndexedDbEngineStore };
133
+
134
+ //# sourceMappingURL=idb.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb.mjs","names":[],"sources":["../src/errors.ts","../src/store/guard.ts","../src/idb/indexeddb.ts"],"sourcesContent":["export type EngineErrorCode = \"closed\" | \"integrity\" | \"invalid-input\";\n\n/** Typed engine errors: store codes are `closed` and `integrity` (decrypt or bind failure). */\nexport class EngineStoreError extends Error {\n readonly code: EngineErrorCode;\n\n constructor(code: EngineErrorCode, message: string) {\n super(message);\n this.name = \"EngineStoreError\";\n this.code = code;\n }\n}\n","import { EngineStoreError } from \"../errors.ts\";\n\n/** NUL is the composite-key/AAD separator — a name or key containing it collides. */\nexport function checkName(kind: \"collection\" | \"key\", value: string): void {\n if (value.includes(\"\\u0000\")) {\n throw new EngineStoreError(\"invalid-input\", `${kind} must not contain NUL`);\n }\n}\n","/**\n * The Web EngineStore: one IndexedDB database, one `records` object store, values encrypted per\n * record with XChaCha20-Poly1305. Key = the injected 32-byte data key; nonce = 24 random bytes\n * stored as nonce‖ciphertext; AAD = UTF-8 `<collection>\\0<key>` — moving a record to another key or\n * collection fails the MAC, same as a tampered payload. `close()` drops the connection, wipes the\n * module-held key copy and latches every call to `closed`.\n */\nimport { xchacha20poly1305 } from \"@noble/ciphers/chacha.js\";\n\nimport { EngineStoreError } from \"../errors.ts\";\nimport { checkName } from \"../store/guard.ts\";\nimport type { EngineCollection, EngineStore } from \"../store/port.ts\";\n\nexport type Random = (length: number) => Uint8Array;\n\nexport type IndexedDbEngineStoreOptions = {\n readonly name: string;\n readonly dataKey: Uint8Array;\n readonly random: Random;\n};\n\nconst STORE = \"records\";\n\nconst id = (collection: string, recordKey: string): string => `${collection}\\u0000${recordKey}`;\nconst aad = (collection: string, recordKey: string): Uint8Array =>\n new TextEncoder().encode(`${collection}\\u0000${recordKey}`);\n\nasync function request<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n req.addEventListener(\"success\", () => {\n resolve(req.result);\n });\n req.addEventListener(\"error\", () => {\n reject(new Error(\"indexeddb request failed\", { cause: req.error }));\n });\n });\n}\n\nasync function done(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.addEventListener(\"complete\", () => {\n resolve();\n });\n tx.addEventListener(\"error\", () => {\n reject(new Error(\"indexeddb transaction failed\", { cause: tx.error }));\n });\n tx.addEventListener(\"abort\", () => {\n reject(new Error(\"indexeddb transaction aborted\", { cause: tx.error }));\n });\n });\n}\n\nexport async function openIndexedDbEngineStore(\n options: IndexedDbEngineStoreOptions,\n): Promise<EngineStore> {\n const key = new Uint8Array(options.dataKey);\n return new Promise<EngineStore>((resolve, reject) => {\n const open = indexedDB.open(options.name, 1);\n open.addEventListener(\"upgradeneeded\", () => {\n open.result.createObjectStore(STORE);\n });\n open.addEventListener(\"success\", () => {\n resolve(makeStore(open.result, key, options.random));\n });\n open.addEventListener(\"error\", () => {\n key.fill(0);\n reject(new Error(\"indexeddb open failed\", { cause: open.error }));\n });\n });\n}\n\nfunction makeStore(db: IDBDatabase, key: Uint8Array, random: Random): EngineStore {\n let closed = false;\n\n const guard = (): IDBDatabase => {\n if (closed) {\n throw new EngineStoreError(\"closed\", \"engine store is closed\");\n }\n return db;\n };\n\n const seal = (collection: string, recordKey: string, bytes: Uint8Array): Uint8Array => {\n const nonce = random(24);\n const ciphertext = xchacha20poly1305(key, nonce, aad(collection, recordKey)).encrypt(bytes);\n const record = new Uint8Array(24 + ciphertext.length);\n record.set(nonce, 0);\n record.set(ciphertext, 24);\n return record;\n };\n\n const open = (collection: string, recordKey: string, record: Uint8Array): Uint8Array => {\n if (record.length < 24) {\n throw new EngineStoreError(\"integrity\", \"engine record too short\");\n }\n try {\n return xchacha20poly1305(key, record.slice(0, 24), aad(collection, recordKey)).decrypt(\n record.slice(24),\n );\n } catch {\n throw new EngineStoreError(\"integrity\", \"engine record failed integrity\");\n }\n };\n\n const make = (name: string): EngineCollection => ({\n get: async (recordKey) => {\n const database = guard();\n checkName(\"key\", recordKey);\n const tx = database.transaction(STORE, \"readonly\");\n const raw = tx.objectStore(STORE).get(id(name, recordKey));\n // oxlint-disable-next-line typescript/no-unsafe-argument -- IDB get() is any-typed by spec\n const record = await request<Uint8Array | undefined>(raw);\n return record === undefined ? undefined : open(name, recordKey, record);\n },\n put: async (recordKey, bytes) => {\n const database = guard();\n checkName(\"key\", recordKey);\n const tx = database.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).put(seal(name, recordKey, bytes), id(name, recordKey));\n await done(tx);\n },\n delete: async (recordKey) => {\n const database = guard();\n checkName(\"key\", recordKey);\n const tx = database.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete(id(name, recordKey));\n await done(tx);\n },\n list: async () => {\n const database = guard();\n const tx = database.transaction(STORE, \"readonly\");\n const all = tx.objectStore(STORE).getAllKeys();\n const keys = await request<IDBValidKey[]>(all);\n const prefix = `${name}\\u0000`;\n return keys\n .filter((k): k is string => typeof k === \"string\" && k.startsWith(prefix))\n .map((k) => k.slice(prefix.length));\n },\n });\n\n return {\n collection: (name) => {\n checkName(\"collection\", name);\n return make(name);\n },\n close: async () => {\n closed = true;\n db.close();\n key.fill(0);\n await Promise.resolve();\n },\n };\n}\n"],"mappings":"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"}