@noy-db/to-browser-idb 0.1.0-pre.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vLannaAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @noy-db/to-browser-idb
2
+
3
+ [![npm](https://img.shields.io/npm/v/%40noy-db/to-browser-idb.svg)](https://www.npmjs.com/package/@noy-db/to-browser-idb)
4
+
5
+ > IndexedDB adapter for noy-db with optional key obfuscation
6
+
7
+ Part of [**`@noy-db/hub`**](https://www.npmjs.com/package/@noy-db/hub) — the zero-knowledge, offline-first, encrypted document store.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @noy-db/hub @noy-db/to-browser-idb
13
+ ```
14
+
15
+ ## What it is
16
+
17
+ IndexedDB adapter for noy-db with optional key obfuscation
18
+
19
+ ## Status
20
+
21
+ **Pre-release** (`0.1.0-pre.1`). API may change before `1.0`.
22
+
23
+ ## Documentation
24
+
25
+ See the [main repository](https://github.com/vLannaAi/noy-db#readme) for setup, examples, and the full subsystem catalog.
26
+
27
+ - Source — [`packages/to-browser-idb`](https://github.com/vLannaAi/noy-db/tree/main/packages/to-browser-idb)
28
+ - Issues — [github.com/vLannaAi/noy-db/issues](https://github.com/vLannaAi/noy-db/issues)
29
+ - Spec — [`SPEC.md`](https://github.com/vLannaAi/noy-db/blob/main/SPEC.md)
30
+
31
+ ## License
32
+
33
+ [MIT](./LICENSE) © vLannaAi
package/dist/index.cjs ADDED
@@ -0,0 +1,250 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ browserIdbStore: () => browserIdbStore
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_hub = require("@noy-db/hub");
27
+ function browserIdbStore(options = {}) {
28
+ const prefix = options.prefix ?? "noydb";
29
+ const obfuscate = options.obfuscate ?? false;
30
+ const obfKey = obfuscate ? makeObfKey(prefix) : "";
31
+ return createIndexedDBAdapter(prefix, obfuscate, obfKey);
32
+ }
33
+ function fnv1a(str) {
34
+ let hash = 2166136261;
35
+ for (let i = 0; i < str.length; i++) {
36
+ hash ^= str.charCodeAt(i);
37
+ hash = Math.imul(hash, 16777619);
38
+ }
39
+ return (hash >>> 0).toString(16).padStart(8, "0");
40
+ }
41
+ function hashComponent(value, obfuscate) {
42
+ return obfuscate ? fnv1a(value) : value;
43
+ }
44
+ function xorEncode(plaintext, key) {
45
+ const bytes = new TextEncoder().encode(plaintext);
46
+ const keyBytes = new TextEncoder().encode(key);
47
+ for (let i = 0; i < bytes.length; i++) {
48
+ bytes[i] = bytes[i] ^ keyBytes[i % keyBytes.length];
49
+ }
50
+ let binary = "";
51
+ for (let i = 0; i < bytes.length; i++) {
52
+ binary += String.fromCharCode(bytes[i]);
53
+ }
54
+ return btoa(binary);
55
+ }
56
+ function xorDecode(encoded, key) {
57
+ const binary = atob(encoded);
58
+ const bytes = new Uint8Array(binary.length);
59
+ const keyBytes = new TextEncoder().encode(key);
60
+ for (let i = 0; i < binary.length; i++) {
61
+ bytes[i] = binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length];
62
+ }
63
+ return new TextDecoder().decode(bytes);
64
+ }
65
+ function makeObfKey(prefix) {
66
+ return prefix + ":noydb-obf-key";
67
+ }
68
+ function createIndexedDBAdapter(prefix, obfuscate, obfKey) {
69
+ const DB_NAME = `${prefix}_noydb`;
70
+ const STORE_NAME = "records";
71
+ let dbPromise = null;
72
+ function openDB() {
73
+ if (!dbPromise) {
74
+ dbPromise = new Promise((resolve, reject) => {
75
+ const request = indexedDB.open(DB_NAME, 1);
76
+ request.onupgradeneeded = () => {
77
+ const db = request.result;
78
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
79
+ db.createObjectStore(STORE_NAME);
80
+ }
81
+ };
82
+ request.onsuccess = () => resolve(request.result);
83
+ request.onerror = () => reject(request.error);
84
+ });
85
+ }
86
+ return dbPromise;
87
+ }
88
+ function key(vault, collection, id) {
89
+ return `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`;
90
+ }
91
+ function tx(mode) {
92
+ return openDB().then((db) => {
93
+ const transaction = db.transaction(STORE_NAME, mode);
94
+ const store = transaction.objectStore(STORE_NAME);
95
+ const complete = new Promise((resolve, reject) => {
96
+ transaction.oncomplete = () => resolve();
97
+ transaction.onerror = () => reject(transaction.error);
98
+ });
99
+ return { store, complete };
100
+ });
101
+ }
102
+ function idbRequest(request) {
103
+ return new Promise((resolve, reject) => {
104
+ request.onsuccess = () => resolve(request.result);
105
+ request.onerror = () => reject(request.error);
106
+ });
107
+ }
108
+ return {
109
+ name: "browser:indexedDB",
110
+ async get(vault, collection, id) {
111
+ const { store } = await tx("readonly");
112
+ const raw = await idbRequest(store.get(key(vault, collection, id)));
113
+ if (!raw) return null;
114
+ if (obfuscate && typeof raw === "object" && "_e" in raw) {
115
+ return raw._e;
116
+ }
117
+ return raw;
118
+ },
119
+ async put(vault, collection, id, envelope, expectedVersion) {
120
+ const k = key(vault, collection, id);
121
+ const { store, complete } = await tx("readwrite");
122
+ if (expectedVersion !== void 0) {
123
+ const existing = await idbRequest(store.get(k));
124
+ if (existing) {
125
+ const env = obfuscate && "_e" in existing ? existing._e : existing;
126
+ if (env._v !== expectedVersion) {
127
+ throw new import_hub.ConflictError(env._v, `Version conflict: expected ${expectedVersion}, found ${env._v}`);
128
+ }
129
+ }
130
+ }
131
+ const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope;
132
+ store.put(value, k);
133
+ await complete;
134
+ },
135
+ async delete(vault, collection, id) {
136
+ const { store, complete } = await tx("readwrite");
137
+ store.delete(key(vault, collection, id));
138
+ await complete;
139
+ },
140
+ async list(vault, collection) {
141
+ const pfx = `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
142
+ const { store } = await tx("readonly");
143
+ const allKeys = await idbRequest(store.getAllKeys());
144
+ if (!obfuscate) {
145
+ return allKeys.filter((k) => typeof k === "string" && k.startsWith(pfx)).map((k) => k.slice(pfx.length));
146
+ }
147
+ const ids = [];
148
+ for (const k of allKeys) {
149
+ if (typeof k !== "string" || !k.startsWith(pfx)) continue;
150
+ const raw = await idbRequest(store.get(k));
151
+ if (raw && typeof raw === "object" && "_oi" in raw) {
152
+ ids.push(xorDecode(raw._oi, obfKey));
153
+ }
154
+ }
155
+ return ids;
156
+ },
157
+ async loadAll(vault) {
158
+ const pfx = `${hashComponent(vault, obfuscate)}:`;
159
+ const { store } = await tx("readonly");
160
+ const allKeys = await idbRequest(store.getAllKeys());
161
+ const snapshot = {};
162
+ for (const k of allKeys) {
163
+ if (typeof k !== "string" || !k.startsWith(pfx)) continue;
164
+ const raw = await idbRequest(store.get(k));
165
+ if (!raw) continue;
166
+ let collection;
167
+ let id;
168
+ let envelope;
169
+ if (obfuscate && typeof raw === "object" && "_e" in raw) {
170
+ const stored = raw;
171
+ collection = xorDecode(stored._oc, obfKey);
172
+ id = xorDecode(stored._oi, obfKey);
173
+ envelope = stored._e;
174
+ } else {
175
+ const rest = k.slice(pfx.length);
176
+ const colonIdx = rest.indexOf(":");
177
+ if (colonIdx < 0) continue;
178
+ collection = rest.slice(0, colonIdx);
179
+ id = rest.slice(colonIdx + 1);
180
+ envelope = raw;
181
+ }
182
+ if (collection.startsWith("_")) continue;
183
+ if (!snapshot[collection]) snapshot[collection] = {};
184
+ snapshot[collection][id] = envelope;
185
+ }
186
+ return snapshot;
187
+ },
188
+ async saveAll(vault, data) {
189
+ const { store, complete } = await tx("readwrite");
190
+ for (const [collection, records] of Object.entries(data)) {
191
+ for (const [id, envelope] of Object.entries(records)) {
192
+ const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope;
193
+ store.put(value, key(vault, collection, id));
194
+ }
195
+ }
196
+ await complete;
197
+ },
198
+ async ping() {
199
+ try {
200
+ await openDB();
201
+ return true;
202
+ } catch {
203
+ return false;
204
+ }
205
+ },
206
+ /**
207
+ * Paginate over a collection backed by IndexedDB.
208
+ *
209
+ * Strategy: read every key in the prefix once (sorted), then slice
210
+ * by cursor offset. IndexedDB's `getAllKeys()` returns sorted keys
211
+ * efficiently for the modern browsers we target (Chrome 87+,
212
+ * Firefox 78+, Safari 14+, Edge 88+ — same baseline as the rest of
213
+ * the build target).
214
+ */
215
+ async listPage(vault, collection, cursor, limit = 100) {
216
+ const pfx = `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
217
+ const { store } = await tx("readonly");
218
+ const allKeys = await idbRequest(store.getAllKeys());
219
+ const matchedKeys = allKeys.filter((k) => typeof k === "string" && k.startsWith(pfx)).sort();
220
+ const start = cursor ? parseInt(cursor, 10) : 0;
221
+ const end = Math.min(start + limit, matchedKeys.length);
222
+ const items = [];
223
+ for (let i = start; i < end; i++) {
224
+ const k = matchedKeys[i];
225
+ const raw = await idbRequest(store.get(k));
226
+ if (!raw) continue;
227
+ let envelope;
228
+ let id;
229
+ if (obfuscate && typeof raw === "object" && "_e" in raw) {
230
+ const stored = raw;
231
+ envelope = stored._e;
232
+ id = xorDecode(stored._oi, obfKey);
233
+ } else {
234
+ envelope = raw;
235
+ id = k.slice(pfx.length);
236
+ }
237
+ items.push({ id, envelope });
238
+ }
239
+ return {
240
+ items,
241
+ nextCursor: end < matchedKeys.length ? String(end) : null
242
+ };
243
+ }
244
+ };
245
+ }
246
+ // Annotate the CommonJS export names for ESM import in node:
247
+ 0 && (module.exports = {
248
+ browserIdbStore
249
+ });
250
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-browser-idb** — IndexedDB-backed NOYDB store.\n *\n * The recommended browser store. Uses a single IndexedDB database\n * (`{prefix}_noydb`) with one object store (`records`). Composite string\n * keys (`{vault}:{collection}:{id}`) avoid the need for complex indexes.\n *\n * ## Why IndexedDB over localStorage\n *\n * - **Larger quota** — typically 50–80% of available disk vs. localStorage's\n * 5 MB hard cap.\n * - **True atomic CAS** — the compare-and-swap check and write happen inside\n * a single `readwrite` IDB transaction, so concurrent writes from two tabs\n * cannot interleave. (`casAtomic: true`.)\n * - **Async API** — doesn't block the main thread during large reads.\n *\n * ## Obfuscation mode\n *\n * Identical to `@noy-db/to-browser-local` — FNV-1a hashed keys + XOR-encoded\n * metadata. Envelope `_data` is always AES-GCM ciphertext regardless of this\n * setting.\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `true` — single readwrite IDB transaction per put |\n * | `listPage` | ✓ — cursor-based pagination via `getAllKeys()` |\n * | `ping` | ✓ — `openDB()` round-trip |\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\n\n/**\n * Options for `browserIdbStore()`.\n *\n * All NOYDB data is stored in a single IndexedDB database named\n * `{prefix}_noydb`, using composite string keys of the form\n * `{vault}:{collection}:{id}`. CAS checks and writes happen inside a\n * single `readwrite` IDB transaction, giving true atomic compare-and-swap\n * (`casAtomic: true`). Enable `obfuscate` to hash key components and\n * XOR-encode metadata — same obfuscation model as `browserLocalStore`.\n */\nexport interface BrowserIdbOptions {\n /** Storage key prefix. Default: 'noydb'. */\n prefix?: string\n /** Obfuscate storage keys so collection/record names are not readable. Default: false. */\n obfuscate?: boolean\n}\n\n/**\n * Create an IndexedDB-backed store adapter.\n *\n * Key scheme (normal): `{vault}:{collection}:{id}`\n * Key scheme (obfuscated): `{hash}:{hash}:{hash}`\n *\n * StoreCapabilities:\n * casAtomic: true — CAS check + write happen inside a single readwrite IDB transaction\n * auth: { kind: 'browser-origin', flow: 'implicit', required: false }\n */\nexport function browserIdbStore(options: BrowserIdbOptions = {}): NoydbStore {\n const prefix = options.prefix ?? 'noydb'\n const obfuscate = options.obfuscate ?? false\n const obfKey = obfuscate ? makeObfKey(prefix) : ''\n\n return createIndexedDBAdapter(prefix, obfuscate, obfKey)\n}\n\n// ─── Key Obfuscation ───────────────────────────────────────────────────\n\n/**\n * FNV-1a 32-bit hash → 8-char hex string.\n * Not cryptographic — just makes keys opaque to casual inspection.\n */\nfunction fnv1a(str: string): string {\n let hash = 0x811c9dc5\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n\nfunction hashComponent(value: string, obfuscate: boolean): string {\n return obfuscate ? fnv1a(value) : value\n}\n\n// ─── XOR Encode/Decode (makes metadata unreadable in storage) ──────────\n\n/** XOR-encode a string with a repeating key, return base64. */\nfunction xorEncode(plaintext: string, key: string): string {\n const bytes = new TextEncoder().encode(plaintext)\n const keyBytes = new TextEncoder().encode(key)\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = bytes[i]! ^ keyBytes[i % keyBytes.length]!\n }\n let binary = ''\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!)\n }\n return btoa(binary)\n}\n\n/** Decode a base64 XOR-encoded string. */\nfunction xorDecode(encoded: string, key: string): string {\n const binary = atob(encoded)\n const bytes = new Uint8Array(binary.length)\n const keyBytes = new TextEncoder().encode(key)\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length]!\n }\n return new TextDecoder().decode(bytes)\n}\n\n/** Stored value wraps envelope + encoded original key parts. */\ninterface StoredValue {\n /** Encoded original record ID. */\n _oi: string\n /** Encoded original collection name. */\n _oc: string\n /** The encrypted envelope. */\n _e: EncryptedEnvelope\n}\n\nfunction makeObfKey(prefix: string): string {\n return prefix + ':noydb-obf-key'\n}\n\n// ─── IndexedDB Backend ─────────────────────────────────────────────────\n\nfunction createIndexedDBAdapter(prefix: string, obfuscate: boolean, obfKey: string): NoydbStore {\n const DB_NAME = `${prefix}_noydb`\n const STORE_NAME = 'records'\n let dbPromise: Promise<IDBDatabase> | null = null\n\n function openDB(): Promise<IDBDatabase> {\n if (!dbPromise) {\n dbPromise = new Promise((resolve, reject) => {\n const request = indexedDB.open(DB_NAME, 1)\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME)\n }\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n }\n return dbPromise\n }\n\n function key(vault: string, collection: string, id: string): string {\n return `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`\n }\n\n function tx(mode: IDBTransactionMode): Promise<{ store: IDBObjectStore; complete: Promise<void> }> {\n return openDB().then(db => {\n const transaction = db.transaction(STORE_NAME, mode)\n const store = transaction.objectStore(STORE_NAME)\n const complete = new Promise<void>((resolve, reject) => {\n transaction.oncomplete = () => resolve()\n transaction.onerror = () => reject(transaction.error)\n })\n return { store, complete }\n })\n }\n\n function idbRequest<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n }\n\n return {\n name: 'browser:indexedDB',\n\n async get(vault, collection, id) {\n const { store } = await tx('readonly')\n const raw = await idbRequest(store.get(key(vault, collection, id)))\n if (!raw) return null\n if (obfuscate && typeof raw === 'object' && '_e' in (raw as StoredValue)) {\n return (raw as StoredValue)._e\n }\n return raw as EncryptedEnvelope\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const k = key(vault, collection, id)\n\n const { store, complete } = await tx('readwrite')\n if (expectedVersion !== undefined) {\n const existing = await idbRequest(store.get(k))\n if (existing) {\n const env = obfuscate && '_e' in (existing as StoredValue) ? (existing as StoredValue)._e : existing as EncryptedEnvelope\n if (env._v !== expectedVersion) {\n throw new ConflictError(env._v, `Version conflict: expected ${expectedVersion}, found ${env._v}`)\n }\n }\n }\n\n const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope\n store.put(value, k)\n await complete\n },\n\n async delete(vault, collection, id) {\n const { store, complete } = await tx('readwrite')\n store.delete(key(vault, collection, id))\n await complete\n },\n\n async list(vault, collection) {\n const pfx = `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:`\n const { store } = await tx('readonly')\n const allKeys = await idbRequest(store.getAllKeys()) as string[]\n\n if (!obfuscate) {\n return allKeys\n .filter(k => typeof k === 'string' && k.startsWith(pfx))\n .map(k => k.slice(pfx.length))\n }\n\n // Obfuscated: need to read values for original IDs\n const ids: string[] = []\n for (const k of allKeys) {\n if (typeof k !== 'string' || !k.startsWith(pfx)) continue\n const raw = await idbRequest(store.get(k))\n if (raw && typeof raw === 'object' && '_oi' in (raw as StoredValue)) {\n ids.push(xorDecode((raw as StoredValue)._oi, obfKey))\n }\n }\n return ids\n },\n\n async loadAll(vault) {\n const pfx = `${hashComponent(vault, obfuscate)}:`\n const { store } = await tx('readonly')\n const allKeys = await idbRequest(store.getAllKeys()) as string[]\n const snapshot: VaultSnapshot = {}\n\n for (const k of allKeys) {\n if (typeof k !== 'string' || !k.startsWith(pfx)) continue\n\n const raw = await idbRequest(store.get(k))\n if (!raw) continue\n\n let collection: string\n let id: string\n let envelope: EncryptedEnvelope\n\n if (obfuscate && typeof raw === 'object' && '_e' in (raw as StoredValue)) {\n const stored = raw as StoredValue\n collection = xorDecode(stored._oc, obfKey)\n id = xorDecode(stored._oi, obfKey)\n envelope = stored._e\n } else {\n const rest = k.slice(pfx.length)\n const colonIdx = rest.indexOf(':')\n if (colonIdx < 0) continue\n collection = rest.slice(0, colonIdx)\n id = rest.slice(colonIdx + 1)\n envelope = raw as EncryptedEnvelope\n }\n\n if (collection.startsWith('_')) continue\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection]![id] = envelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n const { store, complete } = await tx('readwrite')\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope\n store.put(value, key(vault, collection, id))\n }\n }\n await complete\n },\n\n async ping() {\n try {\n await openDB()\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection backed by IndexedDB.\n *\n * Strategy: read every key in the prefix once (sorted), then slice\n * by cursor offset. IndexedDB's `getAllKeys()` returns sorted keys\n * efficiently for the modern browsers we target (Chrome 87+,\n * Firefox 78+, Safari 14+, Edge 88+ — same baseline as the rest of\n * the build target).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:`\n const { store } = await tx('readonly')\n const allKeys = await idbRequest(store.getAllKeys()) as string[]\n const matchedKeys = allKeys\n .filter(k => typeof k === 'string' && k.startsWith(pfx))\n .sort()\n\n const start = cursor ? parseInt(cursor, 10) : 0\n const end = Math.min(start + limit, matchedKeys.length)\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (let i = start; i < end; i++) {\n const k = matchedKeys[i]!\n const raw = await idbRequest(store.get(k))\n if (!raw) continue\n\n let envelope: EncryptedEnvelope\n let id: string\n if (obfuscate && typeof raw === 'object' && '_e' in (raw as StoredValue)) {\n const stored = raw as StoredValue\n envelope = stored._e\n id = xorDecode(stored._oi, obfKey)\n } else {\n envelope = raw as EncryptedEnvelope\n id = k.slice(pfx.length)\n }\n items.push({ id, envelope })\n }\n\n return {\n items,\n nextCursor: end < matchedKeys.length ? String(end) : null,\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,64 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-browser-idb** — IndexedDB-backed NOYDB store.
5
+ *
6
+ * The recommended browser store. Uses a single IndexedDB database
7
+ * (`{prefix}_noydb`) with one object store (`records`). Composite string
8
+ * keys (`{vault}:{collection}:{id}`) avoid the need for complex indexes.
9
+ *
10
+ * ## Why IndexedDB over localStorage
11
+ *
12
+ * - **Larger quota** — typically 50–80% of available disk vs. localStorage's
13
+ * 5 MB hard cap.
14
+ * - **True atomic CAS** — the compare-and-swap check and write happen inside
15
+ * a single `readwrite` IDB transaction, so concurrent writes from two tabs
16
+ * cannot interleave. (`casAtomic: true`.)
17
+ * - **Async API** — doesn't block the main thread during large reads.
18
+ *
19
+ * ## Obfuscation mode
20
+ *
21
+ * Identical to `@noy-db/to-browser-local` — FNV-1a hashed keys + XOR-encoded
22
+ * metadata. Envelope `_data` is always AES-GCM ciphertext regardless of this
23
+ * setting.
24
+ *
25
+ * ## Capabilities
26
+ *
27
+ * | Capability | Value |
28
+ * |---|---|
29
+ * | `casAtomic` | `true` — single readwrite IDB transaction per put |
30
+ * | `listPage` | ✓ — cursor-based pagination via `getAllKeys()` |
31
+ * | `ping` | ✓ — `openDB()` round-trip |
32
+ *
33
+ * @packageDocumentation
34
+ */
35
+
36
+ /**
37
+ * Options for `browserIdbStore()`.
38
+ *
39
+ * All NOYDB data is stored in a single IndexedDB database named
40
+ * `{prefix}_noydb`, using composite string keys of the form
41
+ * `{vault}:{collection}:{id}`. CAS checks and writes happen inside a
42
+ * single `readwrite` IDB transaction, giving true atomic compare-and-swap
43
+ * (`casAtomic: true`). Enable `obfuscate` to hash key components and
44
+ * XOR-encode metadata — same obfuscation model as `browserLocalStore`.
45
+ */
46
+ interface BrowserIdbOptions {
47
+ /** Storage key prefix. Default: 'noydb'. */
48
+ prefix?: string;
49
+ /** Obfuscate storage keys so collection/record names are not readable. Default: false. */
50
+ obfuscate?: boolean;
51
+ }
52
+ /**
53
+ * Create an IndexedDB-backed store adapter.
54
+ *
55
+ * Key scheme (normal): `{vault}:{collection}:{id}`
56
+ * Key scheme (obfuscated): `{hash}:{hash}:{hash}`
57
+ *
58
+ * StoreCapabilities:
59
+ * casAtomic: true — CAS check + write happen inside a single readwrite IDB transaction
60
+ * auth: { kind: 'browser-origin', flow: 'implicit', required: false }
61
+ */
62
+ declare function browserIdbStore(options?: BrowserIdbOptions): NoydbStore;
63
+
64
+ export { type BrowserIdbOptions, browserIdbStore };
@@ -0,0 +1,64 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-browser-idb** — IndexedDB-backed NOYDB store.
5
+ *
6
+ * The recommended browser store. Uses a single IndexedDB database
7
+ * (`{prefix}_noydb`) with one object store (`records`). Composite string
8
+ * keys (`{vault}:{collection}:{id}`) avoid the need for complex indexes.
9
+ *
10
+ * ## Why IndexedDB over localStorage
11
+ *
12
+ * - **Larger quota** — typically 50–80% of available disk vs. localStorage's
13
+ * 5 MB hard cap.
14
+ * - **True atomic CAS** — the compare-and-swap check and write happen inside
15
+ * a single `readwrite` IDB transaction, so concurrent writes from two tabs
16
+ * cannot interleave. (`casAtomic: true`.)
17
+ * - **Async API** — doesn't block the main thread during large reads.
18
+ *
19
+ * ## Obfuscation mode
20
+ *
21
+ * Identical to `@noy-db/to-browser-local` — FNV-1a hashed keys + XOR-encoded
22
+ * metadata. Envelope `_data` is always AES-GCM ciphertext regardless of this
23
+ * setting.
24
+ *
25
+ * ## Capabilities
26
+ *
27
+ * | Capability | Value |
28
+ * |---|---|
29
+ * | `casAtomic` | `true` — single readwrite IDB transaction per put |
30
+ * | `listPage` | ✓ — cursor-based pagination via `getAllKeys()` |
31
+ * | `ping` | ✓ — `openDB()` round-trip |
32
+ *
33
+ * @packageDocumentation
34
+ */
35
+
36
+ /**
37
+ * Options for `browserIdbStore()`.
38
+ *
39
+ * All NOYDB data is stored in a single IndexedDB database named
40
+ * `{prefix}_noydb`, using composite string keys of the form
41
+ * `{vault}:{collection}:{id}`. CAS checks and writes happen inside a
42
+ * single `readwrite` IDB transaction, giving true atomic compare-and-swap
43
+ * (`casAtomic: true`). Enable `obfuscate` to hash key components and
44
+ * XOR-encode metadata — same obfuscation model as `browserLocalStore`.
45
+ */
46
+ interface BrowserIdbOptions {
47
+ /** Storage key prefix. Default: 'noydb'. */
48
+ prefix?: string;
49
+ /** Obfuscate storage keys so collection/record names are not readable. Default: false. */
50
+ obfuscate?: boolean;
51
+ }
52
+ /**
53
+ * Create an IndexedDB-backed store adapter.
54
+ *
55
+ * Key scheme (normal): `{vault}:{collection}:{id}`
56
+ * Key scheme (obfuscated): `{hash}:{hash}:{hash}`
57
+ *
58
+ * StoreCapabilities:
59
+ * casAtomic: true — CAS check + write happen inside a single readwrite IDB transaction
60
+ * auth: { kind: 'browser-origin', flow: 'implicit', required: false }
61
+ */
62
+ declare function browserIdbStore(options?: BrowserIdbOptions): NoydbStore;
63
+
64
+ export { type BrowserIdbOptions, browserIdbStore };
package/dist/index.js ADDED
@@ -0,0 +1,225 @@
1
+ // src/index.ts
2
+ import { ConflictError } from "@noy-db/hub";
3
+ function browserIdbStore(options = {}) {
4
+ const prefix = options.prefix ?? "noydb";
5
+ const obfuscate = options.obfuscate ?? false;
6
+ const obfKey = obfuscate ? makeObfKey(prefix) : "";
7
+ return createIndexedDBAdapter(prefix, obfuscate, obfKey);
8
+ }
9
+ function fnv1a(str) {
10
+ let hash = 2166136261;
11
+ for (let i = 0; i < str.length; i++) {
12
+ hash ^= str.charCodeAt(i);
13
+ hash = Math.imul(hash, 16777619);
14
+ }
15
+ return (hash >>> 0).toString(16).padStart(8, "0");
16
+ }
17
+ function hashComponent(value, obfuscate) {
18
+ return obfuscate ? fnv1a(value) : value;
19
+ }
20
+ function xorEncode(plaintext, key) {
21
+ const bytes = new TextEncoder().encode(plaintext);
22
+ const keyBytes = new TextEncoder().encode(key);
23
+ for (let i = 0; i < bytes.length; i++) {
24
+ bytes[i] = bytes[i] ^ keyBytes[i % keyBytes.length];
25
+ }
26
+ let binary = "";
27
+ for (let i = 0; i < bytes.length; i++) {
28
+ binary += String.fromCharCode(bytes[i]);
29
+ }
30
+ return btoa(binary);
31
+ }
32
+ function xorDecode(encoded, key) {
33
+ const binary = atob(encoded);
34
+ const bytes = new Uint8Array(binary.length);
35
+ const keyBytes = new TextEncoder().encode(key);
36
+ for (let i = 0; i < binary.length; i++) {
37
+ bytes[i] = binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length];
38
+ }
39
+ return new TextDecoder().decode(bytes);
40
+ }
41
+ function makeObfKey(prefix) {
42
+ return prefix + ":noydb-obf-key";
43
+ }
44
+ function createIndexedDBAdapter(prefix, obfuscate, obfKey) {
45
+ const DB_NAME = `${prefix}_noydb`;
46
+ const STORE_NAME = "records";
47
+ let dbPromise = null;
48
+ function openDB() {
49
+ if (!dbPromise) {
50
+ dbPromise = new Promise((resolve, reject) => {
51
+ const request = indexedDB.open(DB_NAME, 1);
52
+ request.onupgradeneeded = () => {
53
+ const db = request.result;
54
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
55
+ db.createObjectStore(STORE_NAME);
56
+ }
57
+ };
58
+ request.onsuccess = () => resolve(request.result);
59
+ request.onerror = () => reject(request.error);
60
+ });
61
+ }
62
+ return dbPromise;
63
+ }
64
+ function key(vault, collection, id) {
65
+ return `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`;
66
+ }
67
+ function tx(mode) {
68
+ return openDB().then((db) => {
69
+ const transaction = db.transaction(STORE_NAME, mode);
70
+ const store = transaction.objectStore(STORE_NAME);
71
+ const complete = new Promise((resolve, reject) => {
72
+ transaction.oncomplete = () => resolve();
73
+ transaction.onerror = () => reject(transaction.error);
74
+ });
75
+ return { store, complete };
76
+ });
77
+ }
78
+ function idbRequest(request) {
79
+ return new Promise((resolve, reject) => {
80
+ request.onsuccess = () => resolve(request.result);
81
+ request.onerror = () => reject(request.error);
82
+ });
83
+ }
84
+ return {
85
+ name: "browser:indexedDB",
86
+ async get(vault, collection, id) {
87
+ const { store } = await tx("readonly");
88
+ const raw = await idbRequest(store.get(key(vault, collection, id)));
89
+ if (!raw) return null;
90
+ if (obfuscate && typeof raw === "object" && "_e" in raw) {
91
+ return raw._e;
92
+ }
93
+ return raw;
94
+ },
95
+ async put(vault, collection, id, envelope, expectedVersion) {
96
+ const k = key(vault, collection, id);
97
+ const { store, complete } = await tx("readwrite");
98
+ if (expectedVersion !== void 0) {
99
+ const existing = await idbRequest(store.get(k));
100
+ if (existing) {
101
+ const env = obfuscate && "_e" in existing ? existing._e : existing;
102
+ if (env._v !== expectedVersion) {
103
+ throw new ConflictError(env._v, `Version conflict: expected ${expectedVersion}, found ${env._v}`);
104
+ }
105
+ }
106
+ }
107
+ const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope;
108
+ store.put(value, k);
109
+ await complete;
110
+ },
111
+ async delete(vault, collection, id) {
112
+ const { store, complete } = await tx("readwrite");
113
+ store.delete(key(vault, collection, id));
114
+ await complete;
115
+ },
116
+ async list(vault, collection) {
117
+ const pfx = `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
118
+ const { store } = await tx("readonly");
119
+ const allKeys = await idbRequest(store.getAllKeys());
120
+ if (!obfuscate) {
121
+ return allKeys.filter((k) => typeof k === "string" && k.startsWith(pfx)).map((k) => k.slice(pfx.length));
122
+ }
123
+ const ids = [];
124
+ for (const k of allKeys) {
125
+ if (typeof k !== "string" || !k.startsWith(pfx)) continue;
126
+ const raw = await idbRequest(store.get(k));
127
+ if (raw && typeof raw === "object" && "_oi" in raw) {
128
+ ids.push(xorDecode(raw._oi, obfKey));
129
+ }
130
+ }
131
+ return ids;
132
+ },
133
+ async loadAll(vault) {
134
+ const pfx = `${hashComponent(vault, obfuscate)}:`;
135
+ const { store } = await tx("readonly");
136
+ const allKeys = await idbRequest(store.getAllKeys());
137
+ const snapshot = {};
138
+ for (const k of allKeys) {
139
+ if (typeof k !== "string" || !k.startsWith(pfx)) continue;
140
+ const raw = await idbRequest(store.get(k));
141
+ if (!raw) continue;
142
+ let collection;
143
+ let id;
144
+ let envelope;
145
+ if (obfuscate && typeof raw === "object" && "_e" in raw) {
146
+ const stored = raw;
147
+ collection = xorDecode(stored._oc, obfKey);
148
+ id = xorDecode(stored._oi, obfKey);
149
+ envelope = stored._e;
150
+ } else {
151
+ const rest = k.slice(pfx.length);
152
+ const colonIdx = rest.indexOf(":");
153
+ if (colonIdx < 0) continue;
154
+ collection = rest.slice(0, colonIdx);
155
+ id = rest.slice(colonIdx + 1);
156
+ envelope = raw;
157
+ }
158
+ if (collection.startsWith("_")) continue;
159
+ if (!snapshot[collection]) snapshot[collection] = {};
160
+ snapshot[collection][id] = envelope;
161
+ }
162
+ return snapshot;
163
+ },
164
+ async saveAll(vault, data) {
165
+ const { store, complete } = await tx("readwrite");
166
+ for (const [collection, records] of Object.entries(data)) {
167
+ for (const [id, envelope] of Object.entries(records)) {
168
+ const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope;
169
+ store.put(value, key(vault, collection, id));
170
+ }
171
+ }
172
+ await complete;
173
+ },
174
+ async ping() {
175
+ try {
176
+ await openDB();
177
+ return true;
178
+ } catch {
179
+ return false;
180
+ }
181
+ },
182
+ /**
183
+ * Paginate over a collection backed by IndexedDB.
184
+ *
185
+ * Strategy: read every key in the prefix once (sorted), then slice
186
+ * by cursor offset. IndexedDB's `getAllKeys()` returns sorted keys
187
+ * efficiently for the modern browsers we target (Chrome 87+,
188
+ * Firefox 78+, Safari 14+, Edge 88+ — same baseline as the rest of
189
+ * the build target).
190
+ */
191
+ async listPage(vault, collection, cursor, limit = 100) {
192
+ const pfx = `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
193
+ const { store } = await tx("readonly");
194
+ const allKeys = await idbRequest(store.getAllKeys());
195
+ const matchedKeys = allKeys.filter((k) => typeof k === "string" && k.startsWith(pfx)).sort();
196
+ const start = cursor ? parseInt(cursor, 10) : 0;
197
+ const end = Math.min(start + limit, matchedKeys.length);
198
+ const items = [];
199
+ for (let i = start; i < end; i++) {
200
+ const k = matchedKeys[i];
201
+ const raw = await idbRequest(store.get(k));
202
+ if (!raw) continue;
203
+ let envelope;
204
+ let id;
205
+ if (obfuscate && typeof raw === "object" && "_e" in raw) {
206
+ const stored = raw;
207
+ envelope = stored._e;
208
+ id = xorDecode(stored._oi, obfKey);
209
+ } else {
210
+ envelope = raw;
211
+ id = k.slice(pfx.length);
212
+ }
213
+ items.push({ id, envelope });
214
+ }
215
+ return {
216
+ items,
217
+ nextCursor: end < matchedKeys.length ? String(end) : null
218
+ };
219
+ }
220
+ };
221
+ }
222
+ export {
223
+ browserIdbStore
224
+ };
225
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-browser-idb** — IndexedDB-backed NOYDB store.\n *\n * The recommended browser store. Uses a single IndexedDB database\n * (`{prefix}_noydb`) with one object store (`records`). Composite string\n * keys (`{vault}:{collection}:{id}`) avoid the need for complex indexes.\n *\n * ## Why IndexedDB over localStorage\n *\n * - **Larger quota** — typically 50–80% of available disk vs. localStorage's\n * 5 MB hard cap.\n * - **True atomic CAS** — the compare-and-swap check and write happen inside\n * a single `readwrite` IDB transaction, so concurrent writes from two tabs\n * cannot interleave. (`casAtomic: true`.)\n * - **Async API** — doesn't block the main thread during large reads.\n *\n * ## Obfuscation mode\n *\n * Identical to `@noy-db/to-browser-local` — FNV-1a hashed keys + XOR-encoded\n * metadata. Envelope `_data` is always AES-GCM ciphertext regardless of this\n * setting.\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `true` — single readwrite IDB transaction per put |\n * | `listPage` | ✓ — cursor-based pagination via `getAllKeys()` |\n * | `ping` | ✓ — `openDB()` round-trip |\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\n\n/**\n * Options for `browserIdbStore()`.\n *\n * All NOYDB data is stored in a single IndexedDB database named\n * `{prefix}_noydb`, using composite string keys of the form\n * `{vault}:{collection}:{id}`. CAS checks and writes happen inside a\n * single `readwrite` IDB transaction, giving true atomic compare-and-swap\n * (`casAtomic: true`). Enable `obfuscate` to hash key components and\n * XOR-encode metadata — same obfuscation model as `browserLocalStore`.\n */\nexport interface BrowserIdbOptions {\n /** Storage key prefix. Default: 'noydb'. */\n prefix?: string\n /** Obfuscate storage keys so collection/record names are not readable. Default: false. */\n obfuscate?: boolean\n}\n\n/**\n * Create an IndexedDB-backed store adapter.\n *\n * Key scheme (normal): `{vault}:{collection}:{id}`\n * Key scheme (obfuscated): `{hash}:{hash}:{hash}`\n *\n * StoreCapabilities:\n * casAtomic: true — CAS check + write happen inside a single readwrite IDB transaction\n * auth: { kind: 'browser-origin', flow: 'implicit', required: false }\n */\nexport function browserIdbStore(options: BrowserIdbOptions = {}): NoydbStore {\n const prefix = options.prefix ?? 'noydb'\n const obfuscate = options.obfuscate ?? false\n const obfKey = obfuscate ? makeObfKey(prefix) : ''\n\n return createIndexedDBAdapter(prefix, obfuscate, obfKey)\n}\n\n// ─── Key Obfuscation ───────────────────────────────────────────────────\n\n/**\n * FNV-1a 32-bit hash → 8-char hex string.\n * Not cryptographic — just makes keys opaque to casual inspection.\n */\nfunction fnv1a(str: string): string {\n let hash = 0x811c9dc5\n for (let i = 0; i < str.length; i++) {\n hash ^= str.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n\nfunction hashComponent(value: string, obfuscate: boolean): string {\n return obfuscate ? fnv1a(value) : value\n}\n\n// ─── XOR Encode/Decode (makes metadata unreadable in storage) ──────────\n\n/** XOR-encode a string with a repeating key, return base64. */\nfunction xorEncode(plaintext: string, key: string): string {\n const bytes = new TextEncoder().encode(plaintext)\n const keyBytes = new TextEncoder().encode(key)\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = bytes[i]! ^ keyBytes[i % keyBytes.length]!\n }\n let binary = ''\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!)\n }\n return btoa(binary)\n}\n\n/** Decode a base64 XOR-encoded string. */\nfunction xorDecode(encoded: string, key: string): string {\n const binary = atob(encoded)\n const bytes = new Uint8Array(binary.length)\n const keyBytes = new TextEncoder().encode(key)\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length]!\n }\n return new TextDecoder().decode(bytes)\n}\n\n/** Stored value wraps envelope + encoded original key parts. */\ninterface StoredValue {\n /** Encoded original record ID. */\n _oi: string\n /** Encoded original collection name. */\n _oc: string\n /** The encrypted envelope. */\n _e: EncryptedEnvelope\n}\n\nfunction makeObfKey(prefix: string): string {\n return prefix + ':noydb-obf-key'\n}\n\n// ─── IndexedDB Backend ─────────────────────────────────────────────────\n\nfunction createIndexedDBAdapter(prefix: string, obfuscate: boolean, obfKey: string): NoydbStore {\n const DB_NAME = `${prefix}_noydb`\n const STORE_NAME = 'records'\n let dbPromise: Promise<IDBDatabase> | null = null\n\n function openDB(): Promise<IDBDatabase> {\n if (!dbPromise) {\n dbPromise = new Promise((resolve, reject) => {\n const request = indexedDB.open(DB_NAME, 1)\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(STORE_NAME)) {\n db.createObjectStore(STORE_NAME)\n }\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n }\n return dbPromise\n }\n\n function key(vault: string, collection: string, id: string): string {\n return `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`\n }\n\n function tx(mode: IDBTransactionMode): Promise<{ store: IDBObjectStore; complete: Promise<void> }> {\n return openDB().then(db => {\n const transaction = db.transaction(STORE_NAME, mode)\n const store = transaction.objectStore(STORE_NAME)\n const complete = new Promise<void>((resolve, reject) => {\n transaction.oncomplete = () => resolve()\n transaction.onerror = () => reject(transaction.error)\n })\n return { store, complete }\n })\n }\n\n function idbRequest<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n }\n\n return {\n name: 'browser:indexedDB',\n\n async get(vault, collection, id) {\n const { store } = await tx('readonly')\n const raw = await idbRequest(store.get(key(vault, collection, id)))\n if (!raw) return null\n if (obfuscate && typeof raw === 'object' && '_e' in (raw as StoredValue)) {\n return (raw as StoredValue)._e\n }\n return raw as EncryptedEnvelope\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const k = key(vault, collection, id)\n\n const { store, complete } = await tx('readwrite')\n if (expectedVersion !== undefined) {\n const existing = await idbRequest(store.get(k))\n if (existing) {\n const env = obfuscate && '_e' in (existing as StoredValue) ? (existing as StoredValue)._e : existing as EncryptedEnvelope\n if (env._v !== expectedVersion) {\n throw new ConflictError(env._v, `Version conflict: expected ${expectedVersion}, found ${env._v}`)\n }\n }\n }\n\n const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope\n store.put(value, k)\n await complete\n },\n\n async delete(vault, collection, id) {\n const { store, complete } = await tx('readwrite')\n store.delete(key(vault, collection, id))\n await complete\n },\n\n async list(vault, collection) {\n const pfx = `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:`\n const { store } = await tx('readonly')\n const allKeys = await idbRequest(store.getAllKeys()) as string[]\n\n if (!obfuscate) {\n return allKeys\n .filter(k => typeof k === 'string' && k.startsWith(pfx))\n .map(k => k.slice(pfx.length))\n }\n\n // Obfuscated: need to read values for original IDs\n const ids: string[] = []\n for (const k of allKeys) {\n if (typeof k !== 'string' || !k.startsWith(pfx)) continue\n const raw = await idbRequest(store.get(k))\n if (raw && typeof raw === 'object' && '_oi' in (raw as StoredValue)) {\n ids.push(xorDecode((raw as StoredValue)._oi, obfKey))\n }\n }\n return ids\n },\n\n async loadAll(vault) {\n const pfx = `${hashComponent(vault, obfuscate)}:`\n const { store } = await tx('readonly')\n const allKeys = await idbRequest(store.getAllKeys()) as string[]\n const snapshot: VaultSnapshot = {}\n\n for (const k of allKeys) {\n if (typeof k !== 'string' || !k.startsWith(pfx)) continue\n\n const raw = await idbRequest(store.get(k))\n if (!raw) continue\n\n let collection: string\n let id: string\n let envelope: EncryptedEnvelope\n\n if (obfuscate && typeof raw === 'object' && '_e' in (raw as StoredValue)) {\n const stored = raw as StoredValue\n collection = xorDecode(stored._oc, obfKey)\n id = xorDecode(stored._oi, obfKey)\n envelope = stored._e\n } else {\n const rest = k.slice(pfx.length)\n const colonIdx = rest.indexOf(':')\n if (colonIdx < 0) continue\n collection = rest.slice(0, colonIdx)\n id = rest.slice(colonIdx + 1)\n envelope = raw as EncryptedEnvelope\n }\n\n if (collection.startsWith('_')) continue\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection]![id] = envelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n const { store, complete } = await tx('readwrite')\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope\n store.put(value, key(vault, collection, id))\n }\n }\n await complete\n },\n\n async ping() {\n try {\n await openDB()\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection backed by IndexedDB.\n *\n * Strategy: read every key in the prefix once (sorted), then slice\n * by cursor offset. IndexedDB's `getAllKeys()` returns sorted keys\n * efficiently for the modern browsers we target (Chrome 87+,\n * Firefox 78+, Safari 14+, Edge 88+ — same baseline as the rest of\n * the build target).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = `${hashComponent(vault, obfuscate)}:${hashComponent(collection, obfuscate)}:`\n const { store } = await tx('readonly')\n const allKeys = await idbRequest(store.getAllKeys()) as string[]\n const matchedKeys = allKeys\n .filter(k => typeof k === 'string' && k.startsWith(pfx))\n .sort()\n\n const start = cursor ? parseInt(cursor, 10) : 0\n const end = Math.min(start + limit, matchedKeys.length)\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (let i = start; i < end; i++) {\n const k = matchedKeys[i]!\n const raw = await idbRequest(store.get(k))\n if (!raw) continue\n\n let envelope: EncryptedEnvelope\n let id: string\n if (obfuscate && typeof raw === 'object' && '_e' in (raw as StoredValue)) {\n const stored = raw as StoredValue\n envelope = stored._e\n id = xorDecode(stored._oi, obfKey)\n } else {\n envelope = raw as EncryptedEnvelope\n id = k.slice(pfx.length)\n }\n items.push({ id, envelope })\n }\n\n return {\n items,\n nextCursor: end < matchedKeys.length ? String(end) : null,\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@noy-db/to-browser-idb",
3
+ "version": "0.1.0-pre.3",
4
+ "description": "IndexedDB adapter for noy-db with optional key obfuscation",
5
+ "license": "MIT",
6
+ "author": "vLannaAi <vicio@lanna.ai>",
7
+ "homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/to-browser-idb#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vLannaAi/noy-db.git",
11
+ "directory": "packages/to-browser-idb"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vLannaAi/noy-db/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@noy-db/hub": "0.1.0-pre.3"
43
+ },
44
+ "devDependencies": {
45
+ "fake-indexeddb": "^6.2.5",
46
+ "happy-dom": "^18.0.0",
47
+ "@noy-db/hub": "0.1.0-pre.3",
48
+ "@noy-db/test-adapter-conformance": "0.0.0"
49
+ },
50
+ "keywords": [
51
+ "noy-db",
52
+ "adapter",
53
+ "browser",
54
+ "indexeddb",
55
+ "web",
56
+ "offline-first",
57
+ "encryption",
58
+ "zero-knowledge",
59
+ "obfuscation"
60
+ ],
61
+ "publishConfig": {
62
+ "access": "public",
63
+ "tag": "latest"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup",
67
+ "test": "vitest run",
68
+ "lint": "eslint src/",
69
+ "typecheck": "tsc --noEmit"
70
+ }
71
+ }