@noy-db/browser 0.5.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/index.cjs +427 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +402 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
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,50 @@
|
|
|
1
|
+
# @noy-db/browser
|
|
2
|
+
|
|
3
|
+
> Browser storage adapter for [noy-db](https://github.com/vLannaAi/noy-db) — localStorage and IndexedDB with optional key obfuscation.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@noy-db/browser)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @noy-db/core @noy-db/browser
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createNoydb } from '@noy-db/core'
|
|
17
|
+
import { browser } from '@noy-db/browser'
|
|
18
|
+
|
|
19
|
+
const db = await createNoydb({
|
|
20
|
+
adapter: browser({
|
|
21
|
+
backend: 'localStorage', // or 'indexedDB' or 'auto'
|
|
22
|
+
prefix: 'my-app',
|
|
23
|
+
obfuscate: true, // hash keys + XOR-encode metadata
|
|
24
|
+
}),
|
|
25
|
+
userId: 'alice',
|
|
26
|
+
passphrase: await promptUser(),
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Key obfuscation
|
|
31
|
+
|
|
32
|
+
With `obfuscate: true`, storage keys look like:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
my-app:d2e076ae:f4494ed9:7f2f8a9c → { _iv: "…", _data: "…" }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
No collection names, no record IDs, no compartment names visible in DevTools. Combined with AES-256-GCM ciphertext in `_data`, this gives full metadata privacy on the client.
|
|
39
|
+
|
|
40
|
+
## Backends
|
|
41
|
+
|
|
42
|
+
| Backend | Use when |
|
|
43
|
+
|---------|----------|
|
|
44
|
+
| `localStorage` | Small datasets (<5MB), synchronous API, simpler DevTools inspection |
|
|
45
|
+
| `indexedDB` | Larger datasets, better performance, binary-friendly |
|
|
46
|
+
| `auto` | Prefers IndexedDB, falls back to localStorage |
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT © vLannaAi — see the [noy-db repo](https://github.com/vLannaAi/noy-db) for full documentation.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
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
|
+
browser: () => browser
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var import_core = require("@noy-db/core");
|
|
27
|
+
function browser(options = {}) {
|
|
28
|
+
const prefix = options.prefix ?? "noydb";
|
|
29
|
+
const obfuscate = options.obfuscate ?? false;
|
|
30
|
+
const obfKey = obfuscate ? makeObfKey(prefix) : "";
|
|
31
|
+
const useIndexedDB = options.backend === "indexedDB" || options.backend !== "localStorage" && typeof indexedDB !== "undefined";
|
|
32
|
+
if (useIndexedDB && typeof indexedDB !== "undefined") {
|
|
33
|
+
return createIndexedDBAdapter(prefix, obfuscate, obfKey);
|
|
34
|
+
}
|
|
35
|
+
return createLocalStorageAdapter(prefix, obfuscate, obfKey);
|
|
36
|
+
}
|
|
37
|
+
function fnv1a(str) {
|
|
38
|
+
let hash = 2166136261;
|
|
39
|
+
for (let i = 0; i < str.length; i++) {
|
|
40
|
+
hash ^= str.charCodeAt(i);
|
|
41
|
+
hash = Math.imul(hash, 16777619);
|
|
42
|
+
}
|
|
43
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
44
|
+
}
|
|
45
|
+
function hashComponent(value, obfuscate) {
|
|
46
|
+
return obfuscate ? fnv1a(value) : value;
|
|
47
|
+
}
|
|
48
|
+
function xorEncode(plaintext, key) {
|
|
49
|
+
const bytes = new TextEncoder().encode(plaintext);
|
|
50
|
+
const keyBytes = new TextEncoder().encode(key);
|
|
51
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
52
|
+
bytes[i] = bytes[i] ^ keyBytes[i % keyBytes.length];
|
|
53
|
+
}
|
|
54
|
+
let binary = "";
|
|
55
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
56
|
+
binary += String.fromCharCode(bytes[i]);
|
|
57
|
+
}
|
|
58
|
+
return btoa(binary);
|
|
59
|
+
}
|
|
60
|
+
function xorDecode(encoded, key) {
|
|
61
|
+
const binary = atob(encoded);
|
|
62
|
+
const bytes = new Uint8Array(binary.length);
|
|
63
|
+
const keyBytes = new TextEncoder().encode(key);
|
|
64
|
+
for (let i = 0; i < binary.length; i++) {
|
|
65
|
+
bytes[i] = binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length];
|
|
66
|
+
}
|
|
67
|
+
return new TextDecoder().decode(bytes);
|
|
68
|
+
}
|
|
69
|
+
function makeObfKey(prefix) {
|
|
70
|
+
return prefix + ":noydb-obf-key";
|
|
71
|
+
}
|
|
72
|
+
function wrapValue(envelope, collection, id, obfuscate, obfKey) {
|
|
73
|
+
if (!obfuscate) return JSON.stringify(envelope);
|
|
74
|
+
let safeEnvelope = envelope;
|
|
75
|
+
if (!envelope._iv && envelope._data) {
|
|
76
|
+
safeEnvelope = { ...safeEnvelope, _data: xorEncode(envelope._data, obfKey) };
|
|
77
|
+
}
|
|
78
|
+
if (envelope._by) {
|
|
79
|
+
safeEnvelope = { ...safeEnvelope, _by: xorEncode(envelope._by, obfKey) };
|
|
80
|
+
}
|
|
81
|
+
const stored = {
|
|
82
|
+
_oi: xorEncode(id, obfKey),
|
|
83
|
+
_oc: xorEncode(collection, obfKey),
|
|
84
|
+
_e: safeEnvelope
|
|
85
|
+
};
|
|
86
|
+
return JSON.stringify(stored);
|
|
87
|
+
}
|
|
88
|
+
function unwrapValue(raw, obfuscate, obfKey) {
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
if (!obfuscate || !("_e" in parsed)) {
|
|
91
|
+
const env = parsed;
|
|
92
|
+
return { envelope: env, origId: "", origCol: "" };
|
|
93
|
+
}
|
|
94
|
+
let envelope = parsed._e;
|
|
95
|
+
if (!envelope._iv && envelope._data) {
|
|
96
|
+
envelope = { ...envelope, _data: xorDecode(envelope._data, obfKey) };
|
|
97
|
+
}
|
|
98
|
+
if (envelope._by) {
|
|
99
|
+
envelope = { ...envelope, _by: xorDecode(envelope._by, obfKey) };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
envelope,
|
|
103
|
+
origId: xorDecode(parsed._oi, obfKey),
|
|
104
|
+
origCol: xorDecode(parsed._oc, obfKey)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function createLocalStorageAdapter(prefix, obfuscate, obfKey) {
|
|
108
|
+
function key(compartment, collection, id) {
|
|
109
|
+
return `${prefix}:${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`;
|
|
110
|
+
}
|
|
111
|
+
function collectionPrefix(compartment, collection) {
|
|
112
|
+
return `${prefix}:${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
|
|
113
|
+
}
|
|
114
|
+
function compartmentPrefix(compartment) {
|
|
115
|
+
return `${prefix}:${hashComponent(compartment, obfuscate)}:`;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
name: "browser:localStorage",
|
|
119
|
+
async get(compartment, collection, id) {
|
|
120
|
+
const data = localStorage.getItem(key(compartment, collection, id));
|
|
121
|
+
if (!data) return null;
|
|
122
|
+
return unwrapValue(data, obfuscate, obfKey).envelope;
|
|
123
|
+
},
|
|
124
|
+
async put(compartment, collection, id, envelope, expectedVersion) {
|
|
125
|
+
const k = key(compartment, collection, id);
|
|
126
|
+
if (expectedVersion !== void 0) {
|
|
127
|
+
const existing = localStorage.getItem(k);
|
|
128
|
+
if (existing) {
|
|
129
|
+
const current = unwrapValue(existing, obfuscate, obfKey).envelope;
|
|
130
|
+
if (current._v !== expectedVersion) {
|
|
131
|
+
throw new import_core.ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
localStorage.setItem(k, wrapValue(envelope, collection, id, obfuscate, obfKey));
|
|
136
|
+
},
|
|
137
|
+
async delete(compartment, collection, id) {
|
|
138
|
+
localStorage.removeItem(key(compartment, collection, id));
|
|
139
|
+
},
|
|
140
|
+
async list(compartment, collection) {
|
|
141
|
+
const pfx = collectionPrefix(compartment, collection);
|
|
142
|
+
const ids = [];
|
|
143
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
144
|
+
const k = localStorage.key(i);
|
|
145
|
+
if (!k?.startsWith(pfx)) continue;
|
|
146
|
+
if (obfuscate) {
|
|
147
|
+
const raw = localStorage.getItem(k);
|
|
148
|
+
if (raw) {
|
|
149
|
+
const { origId } = unwrapValue(raw, true, obfKey);
|
|
150
|
+
ids.push(origId);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
ids.push(k.slice(pfx.length));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return ids;
|
|
157
|
+
},
|
|
158
|
+
async loadAll(compartment) {
|
|
159
|
+
const pfx = compartmentPrefix(compartment);
|
|
160
|
+
const snapshot = {};
|
|
161
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
162
|
+
const k = localStorage.key(i);
|
|
163
|
+
if (!k?.startsWith(pfx)) continue;
|
|
164
|
+
const raw = localStorage.getItem(k);
|
|
165
|
+
if (!raw) continue;
|
|
166
|
+
let collection;
|
|
167
|
+
let id;
|
|
168
|
+
if (obfuscate) {
|
|
169
|
+
const { envelope, origId, origCol } = unwrapValue(raw, true, obfKey);
|
|
170
|
+
if (origCol.startsWith("_")) continue;
|
|
171
|
+
collection = origCol;
|
|
172
|
+
id = origId;
|
|
173
|
+
if (!snapshot[collection]) snapshot[collection] = {};
|
|
174
|
+
snapshot[collection][id] = envelope;
|
|
175
|
+
} else {
|
|
176
|
+
const rest = k.slice(pfx.length);
|
|
177
|
+
const colonIdx = rest.indexOf(":");
|
|
178
|
+
if (colonIdx < 0) continue;
|
|
179
|
+
collection = rest.slice(0, colonIdx);
|
|
180
|
+
id = rest.slice(colonIdx + 1);
|
|
181
|
+
if (collection.startsWith("_")) continue;
|
|
182
|
+
if (!snapshot[collection]) snapshot[collection] = {};
|
|
183
|
+
snapshot[collection][id] = JSON.parse(raw);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return snapshot;
|
|
187
|
+
},
|
|
188
|
+
async saveAll(compartment, data) {
|
|
189
|
+
for (const [collection, records] of Object.entries(data)) {
|
|
190
|
+
for (const [id, envelope] of Object.entries(records)) {
|
|
191
|
+
localStorage.setItem(
|
|
192
|
+
key(compartment, collection, id),
|
|
193
|
+
wrapValue(envelope, collection, id, obfuscate, obfKey)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
async ping() {
|
|
199
|
+
try {
|
|
200
|
+
const testKey = `${prefix}:__ping__`;
|
|
201
|
+
localStorage.setItem(testKey, "1");
|
|
202
|
+
localStorage.removeItem(testKey);
|
|
203
|
+
return true;
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
/**
|
|
209
|
+
* Paginate over a collection. Cursor is a numeric offset (as a string)
|
|
210
|
+
* into the sorted localStorage key list. Sorting by key gives stable
|
|
211
|
+
* ordering across page fetches even when other code is mutating
|
|
212
|
+
* unrelated keys in the same prefix.
|
|
213
|
+
*
|
|
214
|
+
* Note: localStorage's `length` and `key(i)` are O(N) per call in some
|
|
215
|
+
* browsers, so listing the matching keys upfront is faster than
|
|
216
|
+
* iterating in slices.
|
|
217
|
+
*/
|
|
218
|
+
async listPage(compartment, collection, cursor, limit = 100) {
|
|
219
|
+
const pfx = collectionPrefix(compartment, collection);
|
|
220
|
+
const matchedKeys = [];
|
|
221
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
222
|
+
const k = localStorage.key(i);
|
|
223
|
+
if (k?.startsWith(pfx)) matchedKeys.push(k);
|
|
224
|
+
}
|
|
225
|
+
matchedKeys.sort();
|
|
226
|
+
const start = cursor ? parseInt(cursor, 10) : 0;
|
|
227
|
+
const end = Math.min(start + limit, matchedKeys.length);
|
|
228
|
+
const items = [];
|
|
229
|
+
for (let i = start; i < end; i++) {
|
|
230
|
+
const k = matchedKeys[i];
|
|
231
|
+
const raw = localStorage.getItem(k);
|
|
232
|
+
if (!raw) continue;
|
|
233
|
+
const { envelope, origId } = unwrapValue(raw, obfuscate, obfKey);
|
|
234
|
+
const id = obfuscate ? origId : k.slice(pfx.length);
|
|
235
|
+
items.push({ id, envelope });
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
items,
|
|
239
|
+
nextCursor: end < matchedKeys.length ? String(end) : null
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function createIndexedDBAdapter(prefix, obfuscate, obfKey) {
|
|
245
|
+
const DB_NAME = `${prefix}_noydb`;
|
|
246
|
+
const STORE_NAME = "records";
|
|
247
|
+
let dbPromise = null;
|
|
248
|
+
function openDB() {
|
|
249
|
+
if (!dbPromise) {
|
|
250
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
251
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
252
|
+
request.onupgradeneeded = () => {
|
|
253
|
+
const db = request.result;
|
|
254
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
255
|
+
db.createObjectStore(STORE_NAME);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
request.onsuccess = () => resolve(request.result);
|
|
259
|
+
request.onerror = () => reject(request.error);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return dbPromise;
|
|
263
|
+
}
|
|
264
|
+
function key(compartment, collection, id) {
|
|
265
|
+
return `${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`;
|
|
266
|
+
}
|
|
267
|
+
function tx(mode) {
|
|
268
|
+
return openDB().then((db) => {
|
|
269
|
+
const transaction = db.transaction(STORE_NAME, mode);
|
|
270
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
271
|
+
const complete = new Promise((resolve, reject) => {
|
|
272
|
+
transaction.oncomplete = () => resolve();
|
|
273
|
+
transaction.onerror = () => reject(transaction.error);
|
|
274
|
+
});
|
|
275
|
+
return { store, complete };
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function idbRequest(request) {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
request.onsuccess = () => resolve(request.result);
|
|
281
|
+
request.onerror = () => reject(request.error);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
name: "browser:indexedDB",
|
|
286
|
+
async get(compartment, collection, id) {
|
|
287
|
+
const { store } = await tx("readonly");
|
|
288
|
+
const raw = await idbRequest(store.get(key(compartment, collection, id)));
|
|
289
|
+
if (!raw) return null;
|
|
290
|
+
if (obfuscate && typeof raw === "object" && "_e" in raw) {
|
|
291
|
+
return raw._e;
|
|
292
|
+
}
|
|
293
|
+
return raw;
|
|
294
|
+
},
|
|
295
|
+
async put(compartment, collection, id, envelope, expectedVersion) {
|
|
296
|
+
const k = key(compartment, collection, id);
|
|
297
|
+
if (expectedVersion !== void 0) {
|
|
298
|
+
const { store: readStore } = await tx("readonly");
|
|
299
|
+
const existing = await idbRequest(readStore.get(k));
|
|
300
|
+
if (existing) {
|
|
301
|
+
const env = obfuscate && "_e" in existing ? existing._e : existing;
|
|
302
|
+
if (env._v !== expectedVersion) {
|
|
303
|
+
throw new import_core.ConflictError(env._v, `Version conflict: expected ${expectedVersion}, found ${env._v}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope;
|
|
308
|
+
const { store, complete } = await tx("readwrite");
|
|
309
|
+
store.put(value, k);
|
|
310
|
+
await complete;
|
|
311
|
+
},
|
|
312
|
+
async delete(compartment, collection, id) {
|
|
313
|
+
const { store, complete } = await tx("readwrite");
|
|
314
|
+
store.delete(key(compartment, collection, id));
|
|
315
|
+
await complete;
|
|
316
|
+
},
|
|
317
|
+
async list(compartment, collection) {
|
|
318
|
+
const pfx = `${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
|
|
319
|
+
const { store } = await tx("readonly");
|
|
320
|
+
const allKeys = await idbRequest(store.getAllKeys());
|
|
321
|
+
if (!obfuscate) {
|
|
322
|
+
return allKeys.filter((k) => typeof k === "string" && k.startsWith(pfx)).map((k) => k.slice(pfx.length));
|
|
323
|
+
}
|
|
324
|
+
const ids = [];
|
|
325
|
+
for (const k of allKeys) {
|
|
326
|
+
if (typeof k !== "string" || !k.startsWith(pfx)) continue;
|
|
327
|
+
const raw = await idbRequest(store.get(k));
|
|
328
|
+
if (raw && typeof raw === "object" && "_oi" in raw) {
|
|
329
|
+
ids.push(xorDecode(raw._oi, obfKey));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return ids;
|
|
333
|
+
},
|
|
334
|
+
async loadAll(compartment) {
|
|
335
|
+
const pfx = `${hashComponent(compartment, obfuscate)}:`;
|
|
336
|
+
const { store } = await tx("readonly");
|
|
337
|
+
const allKeys = await idbRequest(store.getAllKeys());
|
|
338
|
+
const snapshot = {};
|
|
339
|
+
for (const k of allKeys) {
|
|
340
|
+
if (typeof k !== "string" || !k.startsWith(pfx)) continue;
|
|
341
|
+
const raw = await idbRequest(store.get(k));
|
|
342
|
+
if (!raw) continue;
|
|
343
|
+
let collection;
|
|
344
|
+
let id;
|
|
345
|
+
let envelope;
|
|
346
|
+
if (obfuscate && typeof raw === "object" && "_e" in raw) {
|
|
347
|
+
const stored = raw;
|
|
348
|
+
collection = xorDecode(stored._oc, obfKey);
|
|
349
|
+
id = xorDecode(stored._oi, obfKey);
|
|
350
|
+
envelope = stored._e;
|
|
351
|
+
} else {
|
|
352
|
+
const rest = k.slice(pfx.length);
|
|
353
|
+
const colonIdx = rest.indexOf(":");
|
|
354
|
+
if (colonIdx < 0) continue;
|
|
355
|
+
collection = rest.slice(0, colonIdx);
|
|
356
|
+
id = rest.slice(colonIdx + 1);
|
|
357
|
+
envelope = raw;
|
|
358
|
+
}
|
|
359
|
+
if (collection.startsWith("_")) continue;
|
|
360
|
+
if (!snapshot[collection]) snapshot[collection] = {};
|
|
361
|
+
snapshot[collection][id] = envelope;
|
|
362
|
+
}
|
|
363
|
+
return snapshot;
|
|
364
|
+
},
|
|
365
|
+
async saveAll(compartment, data) {
|
|
366
|
+
const { store, complete } = await tx("readwrite");
|
|
367
|
+
for (const [collection, records] of Object.entries(data)) {
|
|
368
|
+
for (const [id, envelope] of Object.entries(records)) {
|
|
369
|
+
const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope;
|
|
370
|
+
store.put(value, key(compartment, collection, id));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
await complete;
|
|
374
|
+
},
|
|
375
|
+
async ping() {
|
|
376
|
+
try {
|
|
377
|
+
await openDB();
|
|
378
|
+
return true;
|
|
379
|
+
} catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
/**
|
|
384
|
+
* Paginate over a collection backed by IndexedDB.
|
|
385
|
+
*
|
|
386
|
+
* Strategy: read every key in the prefix once (sorted), then slice
|
|
387
|
+
* by cursor offset. IndexedDB's `getAllKeys()` returns sorted keys
|
|
388
|
+
* efficiently for the modern browsers we target (Chrome 87+,
|
|
389
|
+
* Firefox 78+, Safari 14+, Edge 88+ — same baseline as the rest of
|
|
390
|
+
* the v0.3 build target).
|
|
391
|
+
*/
|
|
392
|
+
async listPage(compartment, collection, cursor, limit = 100) {
|
|
393
|
+
const pfx = `${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
|
|
394
|
+
const { store } = await tx("readonly");
|
|
395
|
+
const allKeys = await idbRequest(store.getAllKeys());
|
|
396
|
+
const matchedKeys = allKeys.filter((k) => typeof k === "string" && k.startsWith(pfx)).sort();
|
|
397
|
+
const start = cursor ? parseInt(cursor, 10) : 0;
|
|
398
|
+
const end = Math.min(start + limit, matchedKeys.length);
|
|
399
|
+
const items = [];
|
|
400
|
+
for (let i = start; i < end; i++) {
|
|
401
|
+
const k = matchedKeys[i];
|
|
402
|
+
const raw = await idbRequest(store.get(k));
|
|
403
|
+
if (!raw) continue;
|
|
404
|
+
let envelope;
|
|
405
|
+
let id;
|
|
406
|
+
if (obfuscate && typeof raw === "object" && "_e" in raw) {
|
|
407
|
+
const stored = raw;
|
|
408
|
+
envelope = stored._e;
|
|
409
|
+
id = xorDecode(stored._oi, obfKey);
|
|
410
|
+
} else {
|
|
411
|
+
envelope = raw;
|
|
412
|
+
id = k.slice(pfx.length);
|
|
413
|
+
}
|
|
414
|
+
items.push({ id, envelope });
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
items,
|
|
418
|
+
nextCursor: end < matchedKeys.length ? String(end) : null
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
424
|
+
0 && (module.exports = {
|
|
425
|
+
browser
|
|
426
|
+
});
|
|
427
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { NoydbAdapter, EncryptedEnvelope, CompartmentSnapshot } from '@noy-db/core'\nimport { ConflictError } from '@noy-db/core'\n\nexport interface BrowserOptions {\n /** Storage key prefix. Default: 'noydb'. */\n prefix?: string\n /** Force a specific storage backend. Default: auto-detect. */\n backend?: 'localStorage' | 'indexedDB'\n /** Obfuscate storage keys so collection/record names are not readable. Default: false. */\n obfuscate?: boolean\n}\n\n/**\n * Create a browser storage adapter.\n * Uses localStorage for small datasets (<5MB) or IndexedDB for larger ones.\n *\n * Key scheme (normal): `{prefix}:{compartment}:{collection}:{id}`\n * Key scheme (obfuscated): `{prefix}:{hash}:{hash}:{hash}`\n */\nexport function browser(options: BrowserOptions = {}): NoydbAdapter {\n const prefix = options.prefix ?? 'noydb'\n const obfuscate = options.obfuscate ?? false\n\n const obfKey = obfuscate ? makeObfKey(prefix) : ''\n\n const useIndexedDB = options.backend === 'indexedDB' ||\n (options.backend !== 'localStorage' && typeof indexedDB !== 'undefined')\n\n if (useIndexedDB && typeof indexedDB !== 'undefined') {\n return createIndexedDBAdapter(prefix, obfuscate, obfKey)\n }\n\n return createLocalStorageAdapter(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\nfunction wrapValue(envelope: EncryptedEnvelope, collection: string, id: string, obfuscate: boolean, obfKey: string): string {\n if (!obfuscate) return JSON.stringify(envelope)\n\n // Encode plaintext metadata that could leak information\n let safeEnvelope = envelope\n\n // If _data is plaintext (e.g. keyring: _iv is empty), XOR-encode it\n if (!envelope._iv && envelope._data) {\n safeEnvelope = { ...safeEnvelope, _data: xorEncode(envelope._data, obfKey) }\n }\n\n // XOR-encode _by (user attribution) if present\n if (envelope._by) {\n safeEnvelope = { ...safeEnvelope, _by: xorEncode(envelope._by, obfKey) }\n }\n\n const stored: StoredValue = {\n _oi: xorEncode(id, obfKey),\n _oc: xorEncode(collection, obfKey),\n _e: safeEnvelope,\n }\n return JSON.stringify(stored)\n}\n\nfunction unwrapValue(raw: string, obfuscate: boolean, obfKey: string): { envelope: EncryptedEnvelope; origId: string; origCol: string } {\n const parsed = JSON.parse(raw) as StoredValue | EncryptedEnvelope\n if (!obfuscate || !('_e' in parsed)) {\n const env = parsed as EncryptedEnvelope\n return { envelope: env, origId: '', origCol: '' }\n }\n\n let envelope = parsed._e\n // Decode _data if it was XOR-encoded (keyring entries with empty _iv)\n if (!envelope._iv && envelope._data) {\n envelope = { ...envelope, _data: xorDecode(envelope._data, obfKey) }\n }\n // Decode _by if it was XOR-encoded\n if (envelope._by) {\n envelope = { ...envelope, _by: xorDecode(envelope._by, obfKey) }\n }\n\n return {\n envelope,\n origId: xorDecode(parsed._oi, obfKey),\n origCol: xorDecode(parsed._oc, obfKey),\n }\n}\n\n// ─── localStorage Backend ──────────────────────────────────────────────\n\nfunction createLocalStorageAdapter(prefix: string, obfuscate: boolean, obfKey: string): NoydbAdapter {\n function key(compartment: string, collection: string, id: string): string {\n return `${prefix}:${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`\n }\n\n function collectionPrefix(compartment: string, collection: string): string {\n return `${prefix}:${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:`\n }\n\n function compartmentPrefix(compartment: string): string {\n return `${prefix}:${hashComponent(compartment, obfuscate)}:`\n }\n\n return {\n name: 'browser:localStorage',\n\n async get(compartment, collection, id) {\n const data = localStorage.getItem(key(compartment, collection, id))\n if (!data) return null\n return unwrapValue(data, obfuscate, obfKey).envelope\n },\n\n async put(compartment, collection, id, envelope, expectedVersion) {\n const k = key(compartment, collection, id)\n\n if (expectedVersion !== undefined) {\n const existing = localStorage.getItem(k)\n if (existing) {\n const current = unwrapValue(existing, obfuscate, obfKey).envelope\n if (current._v !== expectedVersion) {\n throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`)\n }\n }\n }\n\n localStorage.setItem(k, wrapValue(envelope, collection, id, obfuscate, obfKey))\n },\n\n async delete(compartment, collection, id) {\n localStorage.removeItem(key(compartment, collection, id))\n },\n\n async list(compartment, collection) {\n const pfx = collectionPrefix(compartment, collection)\n const ids: string[] = []\n for (let i = 0; i < localStorage.length; i++) {\n const k = localStorage.key(i)\n if (!k?.startsWith(pfx)) continue\n\n if (obfuscate) {\n // Read stored value to get original ID\n const raw = localStorage.getItem(k)\n if (raw) {\n const { origId } = unwrapValue(raw, true, obfKey)\n ids.push(origId)\n }\n } else {\n ids.push(k.slice(pfx.length))\n }\n }\n return ids\n },\n\n async loadAll(compartment) {\n const pfx = compartmentPrefix(compartment)\n const snapshot: CompartmentSnapshot = {}\n\n for (let i = 0; i < localStorage.length; i++) {\n const k = localStorage.key(i)\n if (!k?.startsWith(pfx)) continue\n\n const raw = localStorage.getItem(k)\n if (!raw) continue\n\n let collection: string\n let id: string\n\n if (obfuscate) {\n const { envelope, origId, origCol } = unwrapValue(raw, true, obfKey)\n if (origCol.startsWith('_')) continue\n collection = origCol\n id = origId\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection]![id] = envelope\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 if (collection.startsWith('_')) continue\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection]![id] = JSON.parse(raw) as EncryptedEnvelope\n }\n }\n\n return snapshot\n },\n\n async saveAll(compartment, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n localStorage.setItem(\n key(compartment, collection, id),\n wrapValue(envelope, collection, id, obfuscate, obfKey),\n )\n }\n }\n },\n\n async ping() {\n try {\n const testKey = `${prefix}:__ping__`\n localStorage.setItem(testKey, '1')\n localStorage.removeItem(testKey)\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection. Cursor is a numeric offset (as a string)\n * into the sorted localStorage key list. Sorting by key gives stable\n * ordering across page fetches even when other code is mutating\n * unrelated keys in the same prefix.\n *\n * Note: localStorage's `length` and `key(i)` are O(N) per call in some\n * browsers, so listing the matching keys upfront is faster than\n * iterating in slices.\n */\n async listPage(compartment, collection, cursor, limit = 100) {\n const pfx = collectionPrefix(compartment, collection)\n const matchedKeys: string[] = []\n for (let i = 0; i < localStorage.length; i++) {\n const k = localStorage.key(i)\n if (k?.startsWith(pfx)) matchedKeys.push(k)\n }\n matchedKeys.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 = localStorage.getItem(k)\n if (!raw) continue\n const { envelope, origId } = unwrapValue(raw, obfuscate, obfKey)\n const id = obfuscate ? origId : k.slice(pfx.length)\n items.push({ id, envelope })\n }\n\n return {\n items,\n nextCursor: end < matchedKeys.length ? String(end) : null,\n }\n },\n }\n}\n\n// ─── IndexedDB Backend ─────────────────────────────────────────────────\n\nfunction createIndexedDBAdapter(prefix: string, obfuscate: boolean, obfKey: string): NoydbAdapter {\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(compartment: string, collection: string, id: string): string {\n return `${hashComponent(compartment, 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(compartment, collection, id) {\n const { store } = await tx('readonly')\n const raw = await idbRequest(store.get(key(compartment, 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(compartment, collection, id, envelope, expectedVersion) {\n const k = key(compartment, collection, id)\n\n if (expectedVersion !== undefined) {\n const { store: readStore } = await tx('readonly')\n const existing = await idbRequest(readStore.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 const { store, complete } = await tx('readwrite')\n store.put(value, k)\n await complete\n },\n\n async delete(compartment, collection, id) {\n const { store, complete } = await tx('readwrite')\n store.delete(key(compartment, collection, id))\n await complete\n },\n\n async list(compartment, collection) {\n const pfx = `${hashComponent(compartment, 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(compartment) {\n const pfx = `${hashComponent(compartment, obfuscate)}:`\n const { store } = await tx('readonly')\n const allKeys = await idbRequest(store.getAllKeys()) as string[]\n const snapshot: CompartmentSnapshot = {}\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(compartment, 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(compartment, 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 v0.3 build target).\n */\n async listPage(compartment, collection, cursor, limit = 100) {\n const pfx = `${hashComponent(compartment, 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/dist/index.d.cts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NoydbAdapter } from '@noy-db/core';
|
|
2
|
+
|
|
3
|
+
interface BrowserOptions {
|
|
4
|
+
/** Storage key prefix. Default: 'noydb'. */
|
|
5
|
+
prefix?: string;
|
|
6
|
+
/** Force a specific storage backend. Default: auto-detect. */
|
|
7
|
+
backend?: 'localStorage' | 'indexedDB';
|
|
8
|
+
/** Obfuscate storage keys so collection/record names are not readable. Default: false. */
|
|
9
|
+
obfuscate?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a browser storage adapter.
|
|
13
|
+
* Uses localStorage for small datasets (<5MB) or IndexedDB for larger ones.
|
|
14
|
+
*
|
|
15
|
+
* Key scheme (normal): `{prefix}:{compartment}:{collection}:{id}`
|
|
16
|
+
* Key scheme (obfuscated): `{prefix}:{hash}:{hash}:{hash}`
|
|
17
|
+
*/
|
|
18
|
+
declare function browser(options?: BrowserOptions): NoydbAdapter;
|
|
19
|
+
|
|
20
|
+
export { type BrowserOptions, browser };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NoydbAdapter } from '@noy-db/core';
|
|
2
|
+
|
|
3
|
+
interface BrowserOptions {
|
|
4
|
+
/** Storage key prefix. Default: 'noydb'. */
|
|
5
|
+
prefix?: string;
|
|
6
|
+
/** Force a specific storage backend. Default: auto-detect. */
|
|
7
|
+
backend?: 'localStorage' | 'indexedDB';
|
|
8
|
+
/** Obfuscate storage keys so collection/record names are not readable. Default: false. */
|
|
9
|
+
obfuscate?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a browser storage adapter.
|
|
13
|
+
* Uses localStorage for small datasets (<5MB) or IndexedDB for larger ones.
|
|
14
|
+
*
|
|
15
|
+
* Key scheme (normal): `{prefix}:{compartment}:{collection}:{id}`
|
|
16
|
+
* Key scheme (obfuscated): `{prefix}:{hash}:{hash}:{hash}`
|
|
17
|
+
*/
|
|
18
|
+
declare function browser(options?: BrowserOptions): NoydbAdapter;
|
|
19
|
+
|
|
20
|
+
export { type BrowserOptions, browser };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { ConflictError } from "@noy-db/core";
|
|
3
|
+
function browser(options = {}) {
|
|
4
|
+
const prefix = options.prefix ?? "noydb";
|
|
5
|
+
const obfuscate = options.obfuscate ?? false;
|
|
6
|
+
const obfKey = obfuscate ? makeObfKey(prefix) : "";
|
|
7
|
+
const useIndexedDB = options.backend === "indexedDB" || options.backend !== "localStorage" && typeof indexedDB !== "undefined";
|
|
8
|
+
if (useIndexedDB && typeof indexedDB !== "undefined") {
|
|
9
|
+
return createIndexedDBAdapter(prefix, obfuscate, obfKey);
|
|
10
|
+
}
|
|
11
|
+
return createLocalStorageAdapter(prefix, obfuscate, obfKey);
|
|
12
|
+
}
|
|
13
|
+
function fnv1a(str) {
|
|
14
|
+
let hash = 2166136261;
|
|
15
|
+
for (let i = 0; i < str.length; i++) {
|
|
16
|
+
hash ^= str.charCodeAt(i);
|
|
17
|
+
hash = Math.imul(hash, 16777619);
|
|
18
|
+
}
|
|
19
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
20
|
+
}
|
|
21
|
+
function hashComponent(value, obfuscate) {
|
|
22
|
+
return obfuscate ? fnv1a(value) : value;
|
|
23
|
+
}
|
|
24
|
+
function xorEncode(plaintext, key) {
|
|
25
|
+
const bytes = new TextEncoder().encode(plaintext);
|
|
26
|
+
const keyBytes = new TextEncoder().encode(key);
|
|
27
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
28
|
+
bytes[i] = bytes[i] ^ keyBytes[i % keyBytes.length];
|
|
29
|
+
}
|
|
30
|
+
let binary = "";
|
|
31
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
32
|
+
binary += String.fromCharCode(bytes[i]);
|
|
33
|
+
}
|
|
34
|
+
return btoa(binary);
|
|
35
|
+
}
|
|
36
|
+
function xorDecode(encoded, key) {
|
|
37
|
+
const binary = atob(encoded);
|
|
38
|
+
const bytes = new Uint8Array(binary.length);
|
|
39
|
+
const keyBytes = new TextEncoder().encode(key);
|
|
40
|
+
for (let i = 0; i < binary.length; i++) {
|
|
41
|
+
bytes[i] = binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length];
|
|
42
|
+
}
|
|
43
|
+
return new TextDecoder().decode(bytes);
|
|
44
|
+
}
|
|
45
|
+
function makeObfKey(prefix) {
|
|
46
|
+
return prefix + ":noydb-obf-key";
|
|
47
|
+
}
|
|
48
|
+
function wrapValue(envelope, collection, id, obfuscate, obfKey) {
|
|
49
|
+
if (!obfuscate) return JSON.stringify(envelope);
|
|
50
|
+
let safeEnvelope = envelope;
|
|
51
|
+
if (!envelope._iv && envelope._data) {
|
|
52
|
+
safeEnvelope = { ...safeEnvelope, _data: xorEncode(envelope._data, obfKey) };
|
|
53
|
+
}
|
|
54
|
+
if (envelope._by) {
|
|
55
|
+
safeEnvelope = { ...safeEnvelope, _by: xorEncode(envelope._by, obfKey) };
|
|
56
|
+
}
|
|
57
|
+
const stored = {
|
|
58
|
+
_oi: xorEncode(id, obfKey),
|
|
59
|
+
_oc: xorEncode(collection, obfKey),
|
|
60
|
+
_e: safeEnvelope
|
|
61
|
+
};
|
|
62
|
+
return JSON.stringify(stored);
|
|
63
|
+
}
|
|
64
|
+
function unwrapValue(raw, obfuscate, obfKey) {
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
if (!obfuscate || !("_e" in parsed)) {
|
|
67
|
+
const env = parsed;
|
|
68
|
+
return { envelope: env, origId: "", origCol: "" };
|
|
69
|
+
}
|
|
70
|
+
let envelope = parsed._e;
|
|
71
|
+
if (!envelope._iv && envelope._data) {
|
|
72
|
+
envelope = { ...envelope, _data: xorDecode(envelope._data, obfKey) };
|
|
73
|
+
}
|
|
74
|
+
if (envelope._by) {
|
|
75
|
+
envelope = { ...envelope, _by: xorDecode(envelope._by, obfKey) };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
envelope,
|
|
79
|
+
origId: xorDecode(parsed._oi, obfKey),
|
|
80
|
+
origCol: xorDecode(parsed._oc, obfKey)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function createLocalStorageAdapter(prefix, obfuscate, obfKey) {
|
|
84
|
+
function key(compartment, collection, id) {
|
|
85
|
+
return `${prefix}:${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`;
|
|
86
|
+
}
|
|
87
|
+
function collectionPrefix(compartment, collection) {
|
|
88
|
+
return `${prefix}:${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
|
|
89
|
+
}
|
|
90
|
+
function compartmentPrefix(compartment) {
|
|
91
|
+
return `${prefix}:${hashComponent(compartment, obfuscate)}:`;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
name: "browser:localStorage",
|
|
95
|
+
async get(compartment, collection, id) {
|
|
96
|
+
const data = localStorage.getItem(key(compartment, collection, id));
|
|
97
|
+
if (!data) return null;
|
|
98
|
+
return unwrapValue(data, obfuscate, obfKey).envelope;
|
|
99
|
+
},
|
|
100
|
+
async put(compartment, collection, id, envelope, expectedVersion) {
|
|
101
|
+
const k = key(compartment, collection, id);
|
|
102
|
+
if (expectedVersion !== void 0) {
|
|
103
|
+
const existing = localStorage.getItem(k);
|
|
104
|
+
if (existing) {
|
|
105
|
+
const current = unwrapValue(existing, obfuscate, obfKey).envelope;
|
|
106
|
+
if (current._v !== expectedVersion) {
|
|
107
|
+
throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
localStorage.setItem(k, wrapValue(envelope, collection, id, obfuscate, obfKey));
|
|
112
|
+
},
|
|
113
|
+
async delete(compartment, collection, id) {
|
|
114
|
+
localStorage.removeItem(key(compartment, collection, id));
|
|
115
|
+
},
|
|
116
|
+
async list(compartment, collection) {
|
|
117
|
+
const pfx = collectionPrefix(compartment, collection);
|
|
118
|
+
const ids = [];
|
|
119
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
120
|
+
const k = localStorage.key(i);
|
|
121
|
+
if (!k?.startsWith(pfx)) continue;
|
|
122
|
+
if (obfuscate) {
|
|
123
|
+
const raw = localStorage.getItem(k);
|
|
124
|
+
if (raw) {
|
|
125
|
+
const { origId } = unwrapValue(raw, true, obfKey);
|
|
126
|
+
ids.push(origId);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
ids.push(k.slice(pfx.length));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return ids;
|
|
133
|
+
},
|
|
134
|
+
async loadAll(compartment) {
|
|
135
|
+
const pfx = compartmentPrefix(compartment);
|
|
136
|
+
const snapshot = {};
|
|
137
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
138
|
+
const k = localStorage.key(i);
|
|
139
|
+
if (!k?.startsWith(pfx)) continue;
|
|
140
|
+
const raw = localStorage.getItem(k);
|
|
141
|
+
if (!raw) continue;
|
|
142
|
+
let collection;
|
|
143
|
+
let id;
|
|
144
|
+
if (obfuscate) {
|
|
145
|
+
const { envelope, origId, origCol } = unwrapValue(raw, true, obfKey);
|
|
146
|
+
if (origCol.startsWith("_")) continue;
|
|
147
|
+
collection = origCol;
|
|
148
|
+
id = origId;
|
|
149
|
+
if (!snapshot[collection]) snapshot[collection] = {};
|
|
150
|
+
snapshot[collection][id] = envelope;
|
|
151
|
+
} else {
|
|
152
|
+
const rest = k.slice(pfx.length);
|
|
153
|
+
const colonIdx = rest.indexOf(":");
|
|
154
|
+
if (colonIdx < 0) continue;
|
|
155
|
+
collection = rest.slice(0, colonIdx);
|
|
156
|
+
id = rest.slice(colonIdx + 1);
|
|
157
|
+
if (collection.startsWith("_")) continue;
|
|
158
|
+
if (!snapshot[collection]) snapshot[collection] = {};
|
|
159
|
+
snapshot[collection][id] = JSON.parse(raw);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return snapshot;
|
|
163
|
+
},
|
|
164
|
+
async saveAll(compartment, data) {
|
|
165
|
+
for (const [collection, records] of Object.entries(data)) {
|
|
166
|
+
for (const [id, envelope] of Object.entries(records)) {
|
|
167
|
+
localStorage.setItem(
|
|
168
|
+
key(compartment, collection, id),
|
|
169
|
+
wrapValue(envelope, collection, id, obfuscate, obfKey)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
async ping() {
|
|
175
|
+
try {
|
|
176
|
+
const testKey = `${prefix}:__ping__`;
|
|
177
|
+
localStorage.setItem(testKey, "1");
|
|
178
|
+
localStorage.removeItem(testKey);
|
|
179
|
+
return true;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
/**
|
|
185
|
+
* Paginate over a collection. Cursor is a numeric offset (as a string)
|
|
186
|
+
* into the sorted localStorage key list. Sorting by key gives stable
|
|
187
|
+
* ordering across page fetches even when other code is mutating
|
|
188
|
+
* unrelated keys in the same prefix.
|
|
189
|
+
*
|
|
190
|
+
* Note: localStorage's `length` and `key(i)` are O(N) per call in some
|
|
191
|
+
* browsers, so listing the matching keys upfront is faster than
|
|
192
|
+
* iterating in slices.
|
|
193
|
+
*/
|
|
194
|
+
async listPage(compartment, collection, cursor, limit = 100) {
|
|
195
|
+
const pfx = collectionPrefix(compartment, collection);
|
|
196
|
+
const matchedKeys = [];
|
|
197
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
198
|
+
const k = localStorage.key(i);
|
|
199
|
+
if (k?.startsWith(pfx)) matchedKeys.push(k);
|
|
200
|
+
}
|
|
201
|
+
matchedKeys.sort();
|
|
202
|
+
const start = cursor ? parseInt(cursor, 10) : 0;
|
|
203
|
+
const end = Math.min(start + limit, matchedKeys.length);
|
|
204
|
+
const items = [];
|
|
205
|
+
for (let i = start; i < end; i++) {
|
|
206
|
+
const k = matchedKeys[i];
|
|
207
|
+
const raw = localStorage.getItem(k);
|
|
208
|
+
if (!raw) continue;
|
|
209
|
+
const { envelope, origId } = unwrapValue(raw, obfuscate, obfKey);
|
|
210
|
+
const id = obfuscate ? origId : k.slice(pfx.length);
|
|
211
|
+
items.push({ id, envelope });
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
items,
|
|
215
|
+
nextCursor: end < matchedKeys.length ? String(end) : null
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function createIndexedDBAdapter(prefix, obfuscate, obfKey) {
|
|
221
|
+
const DB_NAME = `${prefix}_noydb`;
|
|
222
|
+
const STORE_NAME = "records";
|
|
223
|
+
let dbPromise = null;
|
|
224
|
+
function openDB() {
|
|
225
|
+
if (!dbPromise) {
|
|
226
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
227
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
228
|
+
request.onupgradeneeded = () => {
|
|
229
|
+
const db = request.result;
|
|
230
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
231
|
+
db.createObjectStore(STORE_NAME);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
request.onsuccess = () => resolve(request.result);
|
|
235
|
+
request.onerror = () => reject(request.error);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return dbPromise;
|
|
239
|
+
}
|
|
240
|
+
function key(compartment, collection, id) {
|
|
241
|
+
return `${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`;
|
|
242
|
+
}
|
|
243
|
+
function tx(mode) {
|
|
244
|
+
return openDB().then((db) => {
|
|
245
|
+
const transaction = db.transaction(STORE_NAME, mode);
|
|
246
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
247
|
+
const complete = new Promise((resolve, reject) => {
|
|
248
|
+
transaction.oncomplete = () => resolve();
|
|
249
|
+
transaction.onerror = () => reject(transaction.error);
|
|
250
|
+
});
|
|
251
|
+
return { store, complete };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function idbRequest(request) {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
request.onsuccess = () => resolve(request.result);
|
|
257
|
+
request.onerror = () => reject(request.error);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
name: "browser:indexedDB",
|
|
262
|
+
async get(compartment, collection, id) {
|
|
263
|
+
const { store } = await tx("readonly");
|
|
264
|
+
const raw = await idbRequest(store.get(key(compartment, collection, id)));
|
|
265
|
+
if (!raw) return null;
|
|
266
|
+
if (obfuscate && typeof raw === "object" && "_e" in raw) {
|
|
267
|
+
return raw._e;
|
|
268
|
+
}
|
|
269
|
+
return raw;
|
|
270
|
+
},
|
|
271
|
+
async put(compartment, collection, id, envelope, expectedVersion) {
|
|
272
|
+
const k = key(compartment, collection, id);
|
|
273
|
+
if (expectedVersion !== void 0) {
|
|
274
|
+
const { store: readStore } = await tx("readonly");
|
|
275
|
+
const existing = await idbRequest(readStore.get(k));
|
|
276
|
+
if (existing) {
|
|
277
|
+
const env = obfuscate && "_e" in existing ? existing._e : existing;
|
|
278
|
+
if (env._v !== expectedVersion) {
|
|
279
|
+
throw new ConflictError(env._v, `Version conflict: expected ${expectedVersion}, found ${env._v}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope;
|
|
284
|
+
const { store, complete } = await tx("readwrite");
|
|
285
|
+
store.put(value, k);
|
|
286
|
+
await complete;
|
|
287
|
+
},
|
|
288
|
+
async delete(compartment, collection, id) {
|
|
289
|
+
const { store, complete } = await tx("readwrite");
|
|
290
|
+
store.delete(key(compartment, collection, id));
|
|
291
|
+
await complete;
|
|
292
|
+
},
|
|
293
|
+
async list(compartment, collection) {
|
|
294
|
+
const pfx = `${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
|
|
295
|
+
const { store } = await tx("readonly");
|
|
296
|
+
const allKeys = await idbRequest(store.getAllKeys());
|
|
297
|
+
if (!obfuscate) {
|
|
298
|
+
return allKeys.filter((k) => typeof k === "string" && k.startsWith(pfx)).map((k) => k.slice(pfx.length));
|
|
299
|
+
}
|
|
300
|
+
const ids = [];
|
|
301
|
+
for (const k of allKeys) {
|
|
302
|
+
if (typeof k !== "string" || !k.startsWith(pfx)) continue;
|
|
303
|
+
const raw = await idbRequest(store.get(k));
|
|
304
|
+
if (raw && typeof raw === "object" && "_oi" in raw) {
|
|
305
|
+
ids.push(xorDecode(raw._oi, obfKey));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return ids;
|
|
309
|
+
},
|
|
310
|
+
async loadAll(compartment) {
|
|
311
|
+
const pfx = `${hashComponent(compartment, obfuscate)}:`;
|
|
312
|
+
const { store } = await tx("readonly");
|
|
313
|
+
const allKeys = await idbRequest(store.getAllKeys());
|
|
314
|
+
const snapshot = {};
|
|
315
|
+
for (const k of allKeys) {
|
|
316
|
+
if (typeof k !== "string" || !k.startsWith(pfx)) continue;
|
|
317
|
+
const raw = await idbRequest(store.get(k));
|
|
318
|
+
if (!raw) continue;
|
|
319
|
+
let collection;
|
|
320
|
+
let id;
|
|
321
|
+
let envelope;
|
|
322
|
+
if (obfuscate && typeof raw === "object" && "_e" in raw) {
|
|
323
|
+
const stored = raw;
|
|
324
|
+
collection = xorDecode(stored._oc, obfKey);
|
|
325
|
+
id = xorDecode(stored._oi, obfKey);
|
|
326
|
+
envelope = stored._e;
|
|
327
|
+
} else {
|
|
328
|
+
const rest = k.slice(pfx.length);
|
|
329
|
+
const colonIdx = rest.indexOf(":");
|
|
330
|
+
if (colonIdx < 0) continue;
|
|
331
|
+
collection = rest.slice(0, colonIdx);
|
|
332
|
+
id = rest.slice(colonIdx + 1);
|
|
333
|
+
envelope = raw;
|
|
334
|
+
}
|
|
335
|
+
if (collection.startsWith("_")) continue;
|
|
336
|
+
if (!snapshot[collection]) snapshot[collection] = {};
|
|
337
|
+
snapshot[collection][id] = envelope;
|
|
338
|
+
}
|
|
339
|
+
return snapshot;
|
|
340
|
+
},
|
|
341
|
+
async saveAll(compartment, data) {
|
|
342
|
+
const { store, complete } = await tx("readwrite");
|
|
343
|
+
for (const [collection, records] of Object.entries(data)) {
|
|
344
|
+
for (const [id, envelope] of Object.entries(records)) {
|
|
345
|
+
const value = obfuscate ? { _oi: xorEncode(id, obfKey), _oc: xorEncode(collection, obfKey), _e: envelope } : envelope;
|
|
346
|
+
store.put(value, key(compartment, collection, id));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
await complete;
|
|
350
|
+
},
|
|
351
|
+
async ping() {
|
|
352
|
+
try {
|
|
353
|
+
await openDB();
|
|
354
|
+
return true;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
/**
|
|
360
|
+
* Paginate over a collection backed by IndexedDB.
|
|
361
|
+
*
|
|
362
|
+
* Strategy: read every key in the prefix once (sorted), then slice
|
|
363
|
+
* by cursor offset. IndexedDB's `getAllKeys()` returns sorted keys
|
|
364
|
+
* efficiently for the modern browsers we target (Chrome 87+,
|
|
365
|
+
* Firefox 78+, Safari 14+, Edge 88+ — same baseline as the rest of
|
|
366
|
+
* the v0.3 build target).
|
|
367
|
+
*/
|
|
368
|
+
async listPage(compartment, collection, cursor, limit = 100) {
|
|
369
|
+
const pfx = `${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:`;
|
|
370
|
+
const { store } = await tx("readonly");
|
|
371
|
+
const allKeys = await idbRequest(store.getAllKeys());
|
|
372
|
+
const matchedKeys = allKeys.filter((k) => typeof k === "string" && k.startsWith(pfx)).sort();
|
|
373
|
+
const start = cursor ? parseInt(cursor, 10) : 0;
|
|
374
|
+
const end = Math.min(start + limit, matchedKeys.length);
|
|
375
|
+
const items = [];
|
|
376
|
+
for (let i = start; i < end; i++) {
|
|
377
|
+
const k = matchedKeys[i];
|
|
378
|
+
const raw = await idbRequest(store.get(k));
|
|
379
|
+
if (!raw) continue;
|
|
380
|
+
let envelope;
|
|
381
|
+
let id;
|
|
382
|
+
if (obfuscate && typeof raw === "object" && "_e" in raw) {
|
|
383
|
+
const stored = raw;
|
|
384
|
+
envelope = stored._e;
|
|
385
|
+
id = xorDecode(stored._oi, obfKey);
|
|
386
|
+
} else {
|
|
387
|
+
envelope = raw;
|
|
388
|
+
id = k.slice(pfx.length);
|
|
389
|
+
}
|
|
390
|
+
items.push({ id, envelope });
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
items,
|
|
394
|
+
nextCursor: end < matchedKeys.length ? String(end) : null
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
export {
|
|
400
|
+
browser
|
|
401
|
+
};
|
|
402
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { NoydbAdapter, EncryptedEnvelope, CompartmentSnapshot } from '@noy-db/core'\nimport { ConflictError } from '@noy-db/core'\n\nexport interface BrowserOptions {\n /** Storage key prefix. Default: 'noydb'. */\n prefix?: string\n /** Force a specific storage backend. Default: auto-detect. */\n backend?: 'localStorage' | 'indexedDB'\n /** Obfuscate storage keys so collection/record names are not readable. Default: false. */\n obfuscate?: boolean\n}\n\n/**\n * Create a browser storage adapter.\n * Uses localStorage for small datasets (<5MB) or IndexedDB for larger ones.\n *\n * Key scheme (normal): `{prefix}:{compartment}:{collection}:{id}`\n * Key scheme (obfuscated): `{prefix}:{hash}:{hash}:{hash}`\n */\nexport function browser(options: BrowserOptions = {}): NoydbAdapter {\n const prefix = options.prefix ?? 'noydb'\n const obfuscate = options.obfuscate ?? false\n\n const obfKey = obfuscate ? makeObfKey(prefix) : ''\n\n const useIndexedDB = options.backend === 'indexedDB' ||\n (options.backend !== 'localStorage' && typeof indexedDB !== 'undefined')\n\n if (useIndexedDB && typeof indexedDB !== 'undefined') {\n return createIndexedDBAdapter(prefix, obfuscate, obfKey)\n }\n\n return createLocalStorageAdapter(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\nfunction wrapValue(envelope: EncryptedEnvelope, collection: string, id: string, obfuscate: boolean, obfKey: string): string {\n if (!obfuscate) return JSON.stringify(envelope)\n\n // Encode plaintext metadata that could leak information\n let safeEnvelope = envelope\n\n // If _data is plaintext (e.g. keyring: _iv is empty), XOR-encode it\n if (!envelope._iv && envelope._data) {\n safeEnvelope = { ...safeEnvelope, _data: xorEncode(envelope._data, obfKey) }\n }\n\n // XOR-encode _by (user attribution) if present\n if (envelope._by) {\n safeEnvelope = { ...safeEnvelope, _by: xorEncode(envelope._by, obfKey) }\n }\n\n const stored: StoredValue = {\n _oi: xorEncode(id, obfKey),\n _oc: xorEncode(collection, obfKey),\n _e: safeEnvelope,\n }\n return JSON.stringify(stored)\n}\n\nfunction unwrapValue(raw: string, obfuscate: boolean, obfKey: string): { envelope: EncryptedEnvelope; origId: string; origCol: string } {\n const parsed = JSON.parse(raw) as StoredValue | EncryptedEnvelope\n if (!obfuscate || !('_e' in parsed)) {\n const env = parsed as EncryptedEnvelope\n return { envelope: env, origId: '', origCol: '' }\n }\n\n let envelope = parsed._e\n // Decode _data if it was XOR-encoded (keyring entries with empty _iv)\n if (!envelope._iv && envelope._data) {\n envelope = { ...envelope, _data: xorDecode(envelope._data, obfKey) }\n }\n // Decode _by if it was XOR-encoded\n if (envelope._by) {\n envelope = { ...envelope, _by: xorDecode(envelope._by, obfKey) }\n }\n\n return {\n envelope,\n origId: xorDecode(parsed._oi, obfKey),\n origCol: xorDecode(parsed._oc, obfKey),\n }\n}\n\n// ─── localStorage Backend ──────────────────────────────────────────────\n\nfunction createLocalStorageAdapter(prefix: string, obfuscate: boolean, obfKey: string): NoydbAdapter {\n function key(compartment: string, collection: string, id: string): string {\n return `${prefix}:${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:${hashComponent(id, obfuscate)}`\n }\n\n function collectionPrefix(compartment: string, collection: string): string {\n return `${prefix}:${hashComponent(compartment, obfuscate)}:${hashComponent(collection, obfuscate)}:`\n }\n\n function compartmentPrefix(compartment: string): string {\n return `${prefix}:${hashComponent(compartment, obfuscate)}:`\n }\n\n return {\n name: 'browser:localStorage',\n\n async get(compartment, collection, id) {\n const data = localStorage.getItem(key(compartment, collection, id))\n if (!data) return null\n return unwrapValue(data, obfuscate, obfKey).envelope\n },\n\n async put(compartment, collection, id, envelope, expectedVersion) {\n const k = key(compartment, collection, id)\n\n if (expectedVersion !== undefined) {\n const existing = localStorage.getItem(k)\n if (existing) {\n const current = unwrapValue(existing, obfuscate, obfKey).envelope\n if (current._v !== expectedVersion) {\n throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`)\n }\n }\n }\n\n localStorage.setItem(k, wrapValue(envelope, collection, id, obfuscate, obfKey))\n },\n\n async delete(compartment, collection, id) {\n localStorage.removeItem(key(compartment, collection, id))\n },\n\n async list(compartment, collection) {\n const pfx = collectionPrefix(compartment, collection)\n const ids: string[] = []\n for (let i = 0; i < localStorage.length; i++) {\n const k = localStorage.key(i)\n if (!k?.startsWith(pfx)) continue\n\n if (obfuscate) {\n // Read stored value to get original ID\n const raw = localStorage.getItem(k)\n if (raw) {\n const { origId } = unwrapValue(raw, true, obfKey)\n ids.push(origId)\n }\n } else {\n ids.push(k.slice(pfx.length))\n }\n }\n return ids\n },\n\n async loadAll(compartment) {\n const pfx = compartmentPrefix(compartment)\n const snapshot: CompartmentSnapshot = {}\n\n for (let i = 0; i < localStorage.length; i++) {\n const k = localStorage.key(i)\n if (!k?.startsWith(pfx)) continue\n\n const raw = localStorage.getItem(k)\n if (!raw) continue\n\n let collection: string\n let id: string\n\n if (obfuscate) {\n const { envelope, origId, origCol } = unwrapValue(raw, true, obfKey)\n if (origCol.startsWith('_')) continue\n collection = origCol\n id = origId\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection]![id] = envelope\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 if (collection.startsWith('_')) continue\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection]![id] = JSON.parse(raw) as EncryptedEnvelope\n }\n }\n\n return snapshot\n },\n\n async saveAll(compartment, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n localStorage.setItem(\n key(compartment, collection, id),\n wrapValue(envelope, collection, id, obfuscate, obfKey),\n )\n }\n }\n },\n\n async ping() {\n try {\n const testKey = `${prefix}:__ping__`\n localStorage.setItem(testKey, '1')\n localStorage.removeItem(testKey)\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection. Cursor is a numeric offset (as a string)\n * into the sorted localStorage key list. Sorting by key gives stable\n * ordering across page fetches even when other code is mutating\n * unrelated keys in the same prefix.\n *\n * Note: localStorage's `length` and `key(i)` are O(N) per call in some\n * browsers, so listing the matching keys upfront is faster than\n * iterating in slices.\n */\n async listPage(compartment, collection, cursor, limit = 100) {\n const pfx = collectionPrefix(compartment, collection)\n const matchedKeys: string[] = []\n for (let i = 0; i < localStorage.length; i++) {\n const k = localStorage.key(i)\n if (k?.startsWith(pfx)) matchedKeys.push(k)\n }\n matchedKeys.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 = localStorage.getItem(k)\n if (!raw) continue\n const { envelope, origId } = unwrapValue(raw, obfuscate, obfKey)\n const id = obfuscate ? origId : k.slice(pfx.length)\n items.push({ id, envelope })\n }\n\n return {\n items,\n nextCursor: end < matchedKeys.length ? String(end) : null,\n }\n },\n }\n}\n\n// ─── IndexedDB Backend ─────────────────────────────────────────────────\n\nfunction createIndexedDBAdapter(prefix: string, obfuscate: boolean, obfKey: string): NoydbAdapter {\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(compartment: string, collection: string, id: string): string {\n return `${hashComponent(compartment, 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(compartment, collection, id) {\n const { store } = await tx('readonly')\n const raw = await idbRequest(store.get(key(compartment, 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(compartment, collection, id, envelope, expectedVersion) {\n const k = key(compartment, collection, id)\n\n if (expectedVersion !== undefined) {\n const { store: readStore } = await tx('readonly')\n const existing = await idbRequest(readStore.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 const { store, complete } = await tx('readwrite')\n store.put(value, k)\n await complete\n },\n\n async delete(compartment, collection, id) {\n const { store, complete } = await tx('readwrite')\n store.delete(key(compartment, collection, id))\n await complete\n },\n\n async list(compartment, collection) {\n const pfx = `${hashComponent(compartment, 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(compartment) {\n const pfx = `${hashComponent(compartment, obfuscate)}:`\n const { store } = await tx('readonly')\n const allKeys = await idbRequest(store.getAllKeys()) as string[]\n const snapshot: CompartmentSnapshot = {}\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(compartment, 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(compartment, 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 v0.3 build target).\n */\n async listPage(compartment, collection, cursor, limit = 100) {\n const pfx = `${hashComponent(compartment, 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,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noy-db/browser",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Browser storage adapter for noy-db — localStorage and IndexedDB with optional key obfuscation",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
|
+
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/browser#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/vLannaAi/noy-db.git",
|
|
11
|
+
"directory": "packages/browser"
|
|
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/core": "^0.5.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"happy-dom": "^18.0.0",
|
|
46
|
+
"@noy-db/core": "0.5.0",
|
|
47
|
+
"@noy-db/test-adapter-conformance": "0.0.0"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"noy-db",
|
|
51
|
+
"adapter",
|
|
52
|
+
"browser",
|
|
53
|
+
"localstorage",
|
|
54
|
+
"indexeddb",
|
|
55
|
+
"web",
|
|
56
|
+
"offline-first",
|
|
57
|
+
"encryption",
|
|
58
|
+
"zero-knowledge",
|
|
59
|
+
"obfuscation"
|
|
60
|
+
],
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsup",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"lint": "eslint src/",
|
|
65
|
+
"typecheck": "tsc --noEmit"
|
|
66
|
+
}
|
|
67
|
+
}
|