safebyok 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,326 @@
1
+ import * as Z from "zod";
2
+ //#region src/types.d.ts
3
+ /**
4
+ * The JSON a whisper is made of. `null` is legal here -- inside an array or object -- because
5
+ * real JSON payloads carry it; see {@link Whisper} for why a whisper can't be null as a whole.
6
+ */
7
+ type WhisperJson = string | number | boolean | null | WhisperJson[] | {
8
+ [ckey: string]: WhisperJson;
9
+ };
10
+ /**
11
+ * The secret kept in a lockbox: a JSON value -- a string, number, boolean, array or plain object.
12
+ * Two shapes are refused rather than silently mangled:
13
+ * * `undefined`, anywhere in the value: JSON cannot carry it.
14
+ * * a bare `null` as the whole whisper: nobody's secret is "nothing here", and emptying a
15
+ * lockbox is `erase`'s job. (`null` inside a structure is fine: {@link WhisperJson}.)
16
+ *
17
+ * @example 'sk-ant-api03-...'
18
+ * @example { apiKey: 'sk-ant-...', orgId: null }
19
+ */
20
+ type Whisper = Exclude<WhisperJson, null>;
21
+ /**
22
+ * Validator for a {@link Whisper}: accepts exactly what JSON can carry, refuses what JSON would
23
+ * mangle (undefined, functions, class instances such as Date or Map), and refuses a bare null.
24
+ *
25
+ * @example Whisper.parse('sk-ant-...') // a string passes through
26
+ * @example Whisper.safeParse(new Date()).success // false: JSON would flatten it to {}
27
+ * @example Whisper.safeParse(undefined).success // false: undefined cannot be stored
28
+ * @example Whisper.safeParse(null).success // false: emptying a lockbox is erase's job
29
+ * @example Whisper.safeParse({ orgId: null }).success // true: null inside a structure is JSON
30
+ */
31
+ declare const Whisper: Z.ZodType<Whisper>;
32
+ /**
33
+ * A freely chosen identifier (STYLE.md tag glossary): lowercase words of letters and digits
34
+ * joined by single underscores, starting with a letter, two to forty characters. Shared by every
35
+ * label in the package: `applabel`, `vaultlabel`, `boxlabel`, `userlabel`.
36
+ *
37
+ * @example Label.parse('anthropic_api_key')
38
+ * @example Label.safeParse('ANTHROPIC_API_KEY').success // false: lowercase only
39
+ * @example Label.safeParse('_nope').success // false
40
+ */
41
+ declare const Label: Z.ZodString;
42
+ /**
43
+ * What names one vault: the host application's label, the authenticated user, and the vault's own
44
+ * label. Two users may each have a vault labelled `main` without collision; so may two apps
45
+ * sharing a backend.
46
+ */
47
+ declare const VaultScope: Z.ZodObject<{
48
+ applabel: Z.ZodString;
49
+ user_id: Z.ZodString;
50
+ vaultlabel: Z.ZodString;
51
+ }, Z.z.core.$strip>;
52
+ type VaultScope = Z.infer<typeof VaultScope>;
53
+ /**
54
+ * One lockbox as the storage layer holds it: the whisper ciphered under its own lockboxKey, that
55
+ * lockboxKey wrapped under the vaultKey, and the public shell -- boxlabel, note, timestamps --
56
+ * a locked vault may show. Nothing in this record opens without the vaultKey.
57
+ *
58
+ * `revision_id` is minted fresh on every write and bound into both encryptions, so ciphertext
59
+ * moved between revisions (or boxes, or vaults) refuses to open.
60
+ *
61
+ * Stored fields are underbar_case: this record is written to the storage layer as-is, and it is
62
+ * plain JSON -- any backend that can hold text can hold it.
63
+ */
64
+ declare const Lockbox: Z.ZodObject<{
65
+ format: Z.ZodLiteral<"safebyok-lockbox-v1">;
66
+ boxlabel: Z.ZodString;
67
+ revision_id: Z.ZodString;
68
+ lockbox_key_wrapped: Z.ZodObject<{
69
+ cipher_iv_b64: Z.ZodString;
70
+ ciphertext_b64: Z.ZodString;
71
+ }, Z.z.core.$strip>;
72
+ whisper_ciphered: Z.ZodObject<{
73
+ cipher_iv_b64: Z.ZodString;
74
+ ciphertext_b64: Z.ZodString;
75
+ }, Z.z.core.$strip>;
76
+ note: Z.ZodOptional<Z.ZodString>;
77
+ created_at: Z.ZodString;
78
+ updated_at: Z.ZodString;
79
+ }, Z.z.core.$strip>;
80
+ type Lockbox = Z.infer<typeof Lockbox>;
81
+ /**
82
+ * The public face of a lockbox: everything but its two encrypted fields. This is what `list()`
83
+ * returns and what a locked vault may show.
84
+ */
85
+ declare const LockboxSummary: Z.ZodObject<{
86
+ boxlabel: Z.ZodString;
87
+ revision_id: Z.ZodString;
88
+ note: Z.ZodOptional<Z.ZodString>;
89
+ created_at: Z.ZodString;
90
+ updated_at: Z.ZodString;
91
+ }, Z.z.core.$strip>;
92
+ type LockboxSummary = Z.infer<typeof LockboxSummary>;
93
+ /**
94
+ * The per-vault record the storage layer holds so any device can unlock the vault: the
95
+ * passphraseSalt, the keyDeriverLabel saying how the passphraseKey is made, and the vaultKey
96
+ * wrapped under that passphraseKey. All of it is safe in the open; none of it opens without
97
+ * the passphrase.
98
+ *
99
+ * `passphrase_epoch` goes up by one on every passphrase change, so other devices and remembered
100
+ * unlocks know to ask again.
101
+ */
102
+ declare const VaultBundle: Z.ZodObject<{
103
+ format: Z.ZodLiteral<"safebyok-vault-v1">;
104
+ applabel: Z.ZodString;
105
+ user_id: Z.ZodString;
106
+ vaultlabel: Z.ZodString;
107
+ vault_id: Z.ZodString;
108
+ passphrase_epoch: Z.ZodNumber;
109
+ key_deriver_label: Z.ZodString;
110
+ passphrase_salt_b64: Z.ZodString;
111
+ vault_key_passphrase_wrapped: Z.ZodObject<{
112
+ cipher_iv_b64: Z.ZodString;
113
+ ciphertext_b64: Z.ZodString;
114
+ }, Z.z.core.$strip>;
115
+ created_at: Z.ZodString;
116
+ updated_at: Z.ZodString;
117
+ }, Z.z.core.$strip>;
118
+ type VaultBundle = Z.infer<typeof VaultBundle>;
119
+ /**
120
+ * The device-local record of a remembered unlock: this browser's localUnlockKey (a
121
+ * non-extractable handle -- it cannot leave the device even through the code that made it) and
122
+ * the vaultKey wrapped under it. Held only by structured-clone storage (IndexedDB) or memory;
123
+ * it has no JSON form, and nothing in it ever goes to the backend.
124
+ */
125
+ declare const DeviceUnlock: Z.ZodObject<{
126
+ format: Z.ZodLiteral<"safebyok-unlock-v1">;
127
+ applabel: Z.ZodString;
128
+ user_id: Z.ZodString;
129
+ vaultlabel: Z.ZodString;
130
+ vault_id: Z.ZodString;
131
+ passphrase_epoch: Z.ZodNumber;
132
+ local_unlock_key: Z.ZodCustom<CryptoKey, CryptoKey>;
133
+ vault_key_local_wrapped: Z.ZodObject<{
134
+ cipher_iv_b64: Z.ZodString;
135
+ ciphertext_b64: Z.ZodString;
136
+ }, Z.z.core.$strip>;
137
+ expires_at: Z.ZodNumber;
138
+ }, Z.z.core.$strip>;
139
+ type DeviceUnlock = Z.infer<typeof DeviceUnlock>;
140
+ /** How long an entered passphrase is remembered on the device, unless the caller says otherwise. */
141
+ declare const SevenDaysMs: number;
142
+ /**
143
+ * The largest whisper a vault stores unless the caller says otherwise, in bytes of its JSON:
144
+ * several times the beefiest credential (an RSA-8192 private key as PEM is about 6.4 KB), and
145
+ * well inside what every storage backend can hold. A larger whisper is usually a sign that the
146
+ * wrong value is being stored.
147
+ */
148
+ declare const MaxWhisperBytes = 40444;
149
+ /** Injected time source, so expiry is testable; defaults to the wall clock. */
150
+ type Clock = {
151
+ now(): number;
152
+ };
153
+ /** The wall clock: the default `Clock`. */
154
+ declare const wallClock: Clock;
155
+ //#endregion
156
+ //#region src/string-store.d.ts
157
+ /**
158
+ * The smallest storage the stub adapters need: strings by string ckey, with change notification
159
+ * so the vault and the demo's inspector can watch it. The browser demo backs one with
160
+ * localStorage; the tests back one with a Map.
161
+ */
162
+ type StringStore = {
163
+ /** The stored text under `ckey`, or null when there is none. */
164
+ read(ckey: string): string | null;
165
+ write(ckey: string, text: string): void;
166
+ erase(ckey: string): void;
167
+ /** Every ckey currently stored, in no particular order. */
168
+ ckeys(): string[];
169
+ /** Calls `listener` after any write or erase; returns the unsubscribe. */
170
+ subscribe(listener: () => void): () => void;
171
+ };
172
+ /**
173
+ * In-memory StringStore over a Map: what the tests use, and a fallback anywhere localStorage
174
+ * is missing.
175
+ *
176
+ * @example
177
+ * const store = memoryStore()
178
+ * store.write('greeting', 'hi'); store.read('greeting') // 'hi'
179
+ */
180
+ declare const memoryStore: () => StringStore;
181
+ /**
182
+ * StringStore over a DOM Storage object (localStorage), confined to one prefix: each store sees
183
+ * only its own entries, so the demo can keep a pretend-remote database and a device cache in the
184
+ * same localStorage while showing them as the distinct places they stand in for.
185
+ *
186
+ * Change notification covers this tab's own writes (DOM `storage` events only fire in *other*
187
+ * tabs, which the demo does not chase).
188
+ *
189
+ * @param webStorage - The Storage to write through, usually `window.localStorage`.
190
+ * @param prefix - Namespace for this store's entries, e.g. 'safebyok-demo:db:'.
191
+ *
192
+ * @example
193
+ * const dbStore = domStore(localStorage, 'safebyok-demo:db:')
194
+ * dbStore.write('vault:acme:u1:main', text) // lands at 'safebyok-demo:db:vault:acme:u1:main'
195
+ */
196
+ declare const domStore: (webStorage: Storage, prefix: string) => StringStore;
197
+ //#endregion
198
+ //#region src/vault-db.d.ts
199
+ /**
200
+ * Remote storage for vault records, as the host application plugs it in: extend this class and
201
+ * implement the seven `fetch`/`save`/`trash` primitives against your backend. That is the whole
202
+ * job -- you are a shelf, not a safe:
203
+ *
204
+ * * **Store what you are handed, return what you stored.** Every record is plain JSON (binary
205
+ * fields are base64url text), so any backend that can hold JSON can hold a vault, as-is.
206
+ * * **Never look inside.** Every secret field arrives encrypted; nothing you hold can open a
207
+ * vault, and nothing safebyok does ever needs you to understand a record.
208
+ * * **Enforce who may reach which rows.** Your backend should let a signed-in user reach only
209
+ * their own records: safebyok's encryption protects the contents, your authorization decides who
210
+ * may fetch or overwrite them at all.
211
+ * * **Don't repair.** Return a record exactly as stored, or null when there is none. safebyok
212
+ * validates everything it reads back; a damaged record is safebyok's to refuse, never yours to fix
213
+ * or quietly drop.
214
+ *
215
+ * The verbs the vault actually calls -- `readBundle`, `listBoxes`, `eraseVault` and the rest --
216
+ * are built for you on top of your primitives, validation included. Each compound verb documents
217
+ * the default composition; a backend that can do better (a transactional delete, a projected
218
+ * list, a push subscription) overrides that one method and leaves the rest.
219
+ */
220
+ declare abstract class VaultDB {
221
+ /**
222
+ * The most this backend holds in one record, in bytes: a plain fact about the backend, or
223
+ * `Infinity` when it sets no per-record limit. A vault refuses to open with a `maxWhisperBytes`
224
+ * over half of it, leaving room for encoding and the record's other fields.
225
+ */
226
+ abstract readonly maxRecordBytes: number;
227
+ /** The stored bundle record for `scope`, exactly as saved, or null when there is none. */
228
+ protected abstract fetchBundle(scope: VaultScope): Promise<unknown>;
229
+ /** Store `bundle`, replacing any bundle already under its scope. */
230
+ protected abstract saveBundle(bundle: VaultBundle): Promise<void>;
231
+ /** Remove the bundle record for `scope`; succeed quietly when there is none. */
232
+ protected abstract trashBundle(scope: VaultScope): Promise<void>;
233
+ /** The stored lockbox record under `boxlabel`, exactly as saved, or null when there is none. */
234
+ protected abstract fetchBox(scope: VaultScope, boxlabel: string): Promise<unknown>;
235
+ /** Store `lockbox`, replacing any lockbox already under its boxlabel. */
236
+ protected abstract saveBox(scope: VaultScope, lockbox: Lockbox): Promise<void>;
237
+ /** Remove the lockbox record under `boxlabel`; succeed quietly when there is none. */
238
+ protected abstract trashBox(scope: VaultScope, boxlabel: string): Promise<void>;
239
+ /** Every stored lockbox record in `scope`, in any order. */
240
+ protected abstract fetchBoxes(scope: VaultScope): Promise<unknown[]>;
241
+ /** The vault's bundle, validated, or null when the vault has never been created. */
242
+ readBundle(scope: VaultScope): Promise<VaultBundle | null>;
243
+ /** Store `bundle`, replacing whatever bundle its scope held. */
244
+ writeBundle(bundle: VaultBundle): Promise<void>;
245
+ /**
246
+ * Store `bundle` only if its scope holds none, and return whichever bundle ends up there: the
247
+ * given one, or the one another device got in first with.
248
+ *
249
+ * Default composition: a fetch, then a save -- two devices creating at the same instant can
250
+ * still race between those steps. A backend with an atomic create should override this with
251
+ * one, and resolve the race for real.
252
+ */
253
+ createBundle(bundle: VaultBundle): Promise<VaultBundle>;
254
+ /** One lockbox record, validated, or null when there is none under that label. */
255
+ readBox(scope: VaultScope, boxlabel: string): Promise<Lockbox | null>;
256
+ /** Store `lockbox`, replacing whatever its boxlabel held. */
257
+ writeBox(scope: VaultScope, lockbox: Lockbox): Promise<void>;
258
+ /** Remove the lockbox under `boxlabel`. */
259
+ eraseBox(scope: VaultScope, boxlabel: string): Promise<void>;
260
+ /**
261
+ * Public faces of every lockbox in the vault -- ciphered fields stripped, sorted by boxlabel --
262
+ * valid while locked.
263
+ *
264
+ * Default composition: fetch every full record and strip it here. A backend that can project
265
+ * away the ciphered fields server-side should override this and skip hauling them.
266
+ */
267
+ listBoxes(scope: VaultScope): Promise<LockboxSummary[]>;
268
+ /**
269
+ * Remove the bundle and every lockbox: the backend's part of deleting a vault.
270
+ *
271
+ * Default composition: trash each lockbox, then the bundle -- an interruption can strand some
272
+ * rows, though never usable ones. A backend with a transaction or batch delete should override
273
+ * this with one.
274
+ */
275
+ eraseVault(scope: VaultScope): Promise<void>;
276
+ /**
277
+ * Calls `onChange` after any write touching `scope`, from this device or another; returns the
278
+ * unsubscribe. The default never calls: safebyok refreshes after its own writes regardless, so all
279
+ * a silent backend loses is news of *other* devices' writes. A backend with live queries or
280
+ * push (Convex, a websocket) should override this and ring the bell.
281
+ */
282
+ subscribe(_scope: VaultScope, _onChange: () => void): () => void;
283
+ }
284
+ /**
285
+ * VaultDB over a {@link StringStore}: rows as JSON text under scope-prefixed ckeys. The demo
286
+ * backs one with localStorage to stand in for a real backend; tests back one with a Map. Every
287
+ * secret field in what it stores is ciphertext, so the only thing pretend about it is reach --
288
+ * rows stay on this device instead of syncing through a shared database.
289
+ *
290
+ * @example
291
+ * const vaultDB = localVaultDB(memoryStore())
292
+ * await vaultDB.readBundle({ applabel: 'acme', user_id: 'u1', vaultlabel: 'main' }) // null
293
+ */
294
+ declare const localVaultDB: (store: StringStore) => VaultDB;
295
+ //#endregion
296
+ //#region src/device-db.d.ts
297
+ /**
298
+ * Device-local storage for remembered unlocks: what lets "enter your passphrase once, keep it for
299
+ * seven days" survive a page reload. A {@link DeviceUnlock} record holds this browser's
300
+ * localUnlockKey -- a non-extractable CryptoKey handle, which has no JSON form -- so an adapter
301
+ * must keep records by structured clone (IndexedDB: see `safebyok/idb`) or in memory; nothing here
302
+ * ever goes to the backend.
303
+ *
304
+ * An adapter may answer a damaged or unreadable record with null rather than an error: losing a
305
+ * remembered unlock only means asking for the passphrase again, and that is the safe failure.
306
+ */
307
+ interface DeviceDB {
308
+ /** The remembered unlock for one vault, or null when there is none. Expiry is the vault's call. */
309
+ readUnlock(scope: VaultScope): Promise<DeviceUnlock | null>;
310
+ writeUnlock(unlock: DeviceUnlock): Promise<void>;
311
+ eraseUnlock(scope: VaultScope): Promise<void>;
312
+ /** Remove every remembered unlock on this device: the device's part of a full reset. */
313
+ eraseAll(): Promise<void>;
314
+ }
315
+ /**
316
+ * DeviceDB over a Map: what the tests use, and the fallback anywhere without IndexedDB. Remembered
317
+ * unlocks last exactly as long as the page -- which is to say, it remembers nothing, safely.
318
+ *
319
+ * @example
320
+ * const deviceDB = memoryDeviceDB()
321
+ * await deviceDB.readUnlock({ applabel: 'acme', user_id: 'u1', vaultlabel: 'main' }) // null
322
+ */
323
+ declare const memoryDeviceDB: () => DeviceDB;
324
+ //#endregion
325
+ export { Whisper as _, StringStore as a, Clock as c, Lockbox as d, LockboxSummary as f, VaultScope as g, VaultBundle as h, localVaultDB as i, DeviceUnlock as l, SevenDaysMs as m, memoryDeviceDB as n, domStore as o, MaxWhisperBytes as p, VaultDB as r, memoryStore as s, DeviceDB as t, Label as u, WhisperJson as v, wallClock as y };
326
+ //# sourceMappingURL=device-db-D3SBnJdR.d.ts.map
package/dist/idb.cjs ADDED
@@ -0,0 +1,181 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_vault_db = require("./vault-db-C19pMom1.cjs");
3
+ let idb_keyval = require("idb-keyval");
4
+ //#region src/idb.ts
5
+ const shelfStores = /* @__PURE__ */ new Map();
6
+ const shelfStore = (dbName, shelf) => {
7
+ const ckey = `${dbName}:${shelf}`;
8
+ const kept = shelfStores.get(ckey);
9
+ if (kept !== void 0) return kept;
10
+ const fresh = (0, idb_keyval.createStore)(`${dbName}:${shelf}`, "records");
11
+ shelfStores.set(ckey, fresh);
12
+ return fresh;
13
+ };
14
+ const bells = /* @__PURE__ */ new Map();
15
+ const channels = /* @__PURE__ */ new Map();
16
+ const bellCkey = (dbName, shelf) => `safebyok-idb:${dbName}:${shelf}`;
17
+ const ringBell = (dbName, shelf) => {
18
+ const ckey = bellCkey(dbName, shelf);
19
+ bells.get(ckey)?.forEach((listener) => {
20
+ listener();
21
+ });
22
+ channels.get(ckey)?.postMessage("changed");
23
+ };
24
+ const onBell = (dbName, shelf, listener) => {
25
+ const ckey = bellCkey(dbName, shelf);
26
+ let ringers = bells.get(ckey);
27
+ if (ringers === void 0) {
28
+ ringers = /* @__PURE__ */ new Set();
29
+ bells.set(ckey, ringers);
30
+ if (typeof BroadcastChannel !== "undefined") {
31
+ const channel = new BroadcastChannel(ckey);
32
+ channel.addEventListener("message", () => {
33
+ bells.get(ckey)?.forEach((ring) => {
34
+ ring();
35
+ });
36
+ });
37
+ channels.set(ckey, channel);
38
+ }
39
+ }
40
+ ringers.add(listener);
41
+ return () => {
42
+ ringers.delete(listener);
43
+ };
44
+ };
45
+ const scopePath = (scope) => `${scope.applabel}:${scope.user_id}:${scope.vaultlabel}`;
46
+ const bundleCkey = (scope) => `vault:${scopePath(scope)}`;
47
+ const boxCkey = (scope, boxlabel) => `box:${scopePath(scope)}:${boxlabel}`;
48
+ const boxPrefix = (scope) => `box:${scopePath(scope)}:`;
49
+ const unlockCkey = (scope) => `unlock:${scopePath(scope)}`;
50
+ /**
51
+ * VaultDB over this browser's IndexedDB: the local-only tier. Nothing syncs -- this "backend"
52
+ * never leaves the device -- but every secret field is ciphertext all the same, and the stored
53
+ * rows are exactly what a real backend would hold.
54
+ *
55
+ * @example
56
+ * const vaultDB = idbVaultDB({ dbName: 'my_app' })
57
+ * const vault = await openVault({ applabel: 'my_app', userId, vaultlabel: 'main', vaultDB, deviceDB: idbDeviceDB({ dbName: 'my_app' }) })
58
+ */
59
+ const idbVaultDB = (opts = {}) => new IdbVaultDB(opts.dbName ?? "safebyok");
60
+ var IdbVaultDB = class extends require_vault_db.VaultDB {
61
+ dbName;
62
+ maxRecordBytes = Infinity;
63
+ constructor(dbName) {
64
+ super();
65
+ this.dbName = dbName;
66
+ }
67
+ shelf() {
68
+ return shelfStore(this.dbName, "vault_db");
69
+ }
70
+ ring() {
71
+ ringBell(this.dbName, "vault_db");
72
+ }
73
+ async fetchBundle(scope) {
74
+ return await (0, idb_keyval.get)(bundleCkey(scope), this.shelf()) ?? null;
75
+ }
76
+ async saveBundle(bundle) {
77
+ await (0, idb_keyval.set)(bundleCkey(bundle), bundle, this.shelf());
78
+ this.ring();
79
+ }
80
+ async trashBundle(scope) {
81
+ await (0, idb_keyval.del)(bundleCkey(scope), this.shelf());
82
+ this.ring();
83
+ }
84
+ async fetchBox(scope, boxlabel) {
85
+ return await (0, idb_keyval.get)(boxCkey(scope, boxlabel), this.shelf()) ?? null;
86
+ }
87
+ async saveBox(scope, lockbox) {
88
+ await (0, idb_keyval.set)(boxCkey(scope, lockbox.boxlabel), lockbox, this.shelf());
89
+ this.ring();
90
+ }
91
+ async trashBox(scope, boxlabel) {
92
+ await (0, idb_keyval.del)(boxCkey(scope, boxlabel), this.shelf());
93
+ this.ring();
94
+ }
95
+ async fetchBoxes(scope) {
96
+ return (await (0, idb_keyval.entries)(this.shelf())).filter(([ckey]) => typeof ckey === "string" && ckey.startsWith(boxPrefix(scope))).map(([, row]) => row);
97
+ }
98
+ subscribe(_scope, onChange) {
99
+ return onBell(this.dbName, "vault_db", onChange);
100
+ }
101
+ };
102
+ /**
103
+ * DeviceDB over this browser's IndexedDB: the real device storage. IndexedDB keeps the
104
+ * DeviceUnlock record by structured clone, which is the only way a browser persists the
105
+ * non-extractable localUnlockKey -- as a handle, never as bytes.
106
+ *
107
+ * A record that will not read back (damaged, or written by another version) reads as "no
108
+ * remembered unlock" and is dropped: losing the convenience is the safe failure.
109
+ *
110
+ * @example
111
+ * const deviceDB = idbDeviceDB()
112
+ * await deviceDB.readUnlock({ applabel: 'acme', user_id: 'u1', vaultlabel: 'main' }) // null
113
+ */
114
+ const idbDeviceDB = (opts = {}) => {
115
+ const dbName = opts.dbName ?? "safebyok";
116
+ const shelf = () => shelfStore(dbName, "device_db");
117
+ const ring = () => {
118
+ ringBell(dbName, "device_db");
119
+ };
120
+ return {
121
+ readUnlock: async (scope) => {
122
+ const raw = await (0, idb_keyval.get)(unlockCkey(scope), shelf());
123
+ if (raw === void 0 || raw === null) return null;
124
+ const checked = require_vault_db.DeviceUnlock.safeParse(raw);
125
+ if (!checked.success) {
126
+ await (0, idb_keyval.del)(unlockCkey(scope), shelf());
127
+ ring();
128
+ return null;
129
+ }
130
+ return checked.data;
131
+ },
132
+ writeUnlock: async (unlock) => {
133
+ await (0, idb_keyval.set)(unlockCkey(unlock), unlock, shelf());
134
+ ring();
135
+ },
136
+ eraseUnlock: async (scope) => {
137
+ await (0, idb_keyval.del)(unlockCkey(scope), shelf());
138
+ ring();
139
+ },
140
+ eraseAll: async () => {
141
+ const ckeys = await (0, idb_keyval.keys)(shelf());
142
+ for (const ckey of ckeys) if (typeof ckey === "string" && ckey.startsWith("unlock:")) await (0, idb_keyval.del)(ckey, shelf());
143
+ ring();
144
+ }
145
+ };
146
+ };
147
+ /**
148
+ * A window onto one safebyok shelf in IndexedDB, for inspectors and debug panels: the raw rows, a
149
+ * change bell, and a wipe. This is how the demo shows "what the backend sees" without asking the
150
+ * vault's opinion -- what it reports is what is actually stored.
151
+ *
152
+ * @example
153
+ * const inspector = inspectIdb({ shelf: 'vault_db' })
154
+ * const rows = await inspector.rows()
155
+ * const stop = inspector.subscribe(() => { void inspector.rows().then(render) })
156
+ */
157
+ const inspectIdb = (opts) => {
158
+ const dbName = opts.dbName ?? "safebyok";
159
+ return {
160
+ /** Every stored row on the shelf, sorted by ckey. */
161
+ rows: async () => {
162
+ return (await (0, idb_keyval.entries)(shelfStore(dbName, opts.shelf))).filter((pair) => typeof pair[0] === "string").map(([ckey, row]) => ({
163
+ ckey,
164
+ row
165
+ })).toSorted((one, two) => one.ckey.localeCompare(two.ckey));
166
+ },
167
+ /** Calls `listener` after any write to the shelf, this tab or another; returns the unsubscribe. */
168
+ subscribe: (listener) => onBell(dbName, opts.shelf, listener),
169
+ /** Remove every row on the shelf. */
170
+ wipe: async () => {
171
+ await (0, idb_keyval.clear)(shelfStore(dbName, opts.shelf));
172
+ ringBell(dbName, opts.shelf);
173
+ }
174
+ };
175
+ };
176
+ //#endregion
177
+ exports.idbDeviceDB = idbDeviceDB;
178
+ exports.idbVaultDB = idbVaultDB;
179
+ exports.inspectIdb = inspectIdb;
180
+
181
+ //# sourceMappingURL=idb.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb.cjs","names":["createStore","VaultDB","get","set","del","entries","DeviceUnlock","keys","clear"],"sources":["../src/idb.ts"],"sourcesContent":["import { createStore, del, entries, get, keys, set, clear, type UseStore } from 'idb-keyval'\nimport { DeviceUnlock, Lockbox, VaultBundle, type VaultScope } from './types'\nimport { VaultDB } from './vault-db'\nimport type { DeviceDB } from './device-db'\n\n/**\n * The browser adapters, over IndexedDB (through `idb-keyval`): a vault that lives entirely on\n * this device. `idbDeviceDB` is the real thing -- device storage is exactly what IndexedDB is,\n * and the only browser storage that can hold a non-extractable localUnlockKey. `idbVaultDB` is\n * the no-backend tier: everything a synced backend would hold, kept on this device instead, so a\n * purely client-side app still gets its whispers encrypted at rest.\n *\n * Browser storage is best-effort: the browser may evict it under pressure, and \"clear site data\"\n * removes it. An app leaning on `idbVaultDB` should call `navigator.storage.persist()` and say\n * plainly that the vault lives only in this browser.\n *\n * This entry point touches IndexedDB only when an adapter is used, so importing it is safe\n * anywhere; the adapters themselves need a browser (or anything else with `indexedDB`).\n */\n\nexport type IdbOpts = {\n /** The IndexedDB database to keep records in. Default 'safebyok'. */\n dbName?: string\n}\n\n/** Which of the two safebyok shelves an IndexedDB database holds records on. */\nexport type IdbShelf = 'vault_db' | 'device_db'\n\n// One UseStore per (dbName, shelf), shared by adapters and inspectors alike: idb-keyval opens\n// the underlying database lazily, so making these is free until something reads or writes.\nconst shelfStores = new Map<string, UseStore>()\nconst shelfStore = (dbName: string, shelf: IdbShelf): UseStore => {\n const ckey = `${dbName}:${shelf}`\n const kept = shelfStores.get(ckey)\n if (kept !== undefined) { return kept }\n const fresh = createStore(`${dbName}:${shelf}`, 'records')\n shelfStores.set(ckey, fresh)\n return fresh\n}\n\n// Change bells, one per (dbName, shelf): local listeners hear this tab's writes directly, and a\n// BroadcastChannel carries them to other tabs (the channel never echoes to its own sender).\nconst bells = new Map<string, Set<() => void>>()\nconst channels = new Map<string, BroadcastChannel>()\nconst bellCkey = (dbName: string, shelf: IdbShelf) => `safebyok-idb:${dbName}:${shelf}`\n\nconst ringBell = (dbName: string, shelf: IdbShelf) => {\n const ckey = bellCkey(dbName, shelf)\n bells.get(ckey)?.forEach((listener) => { listener() })\n channels.get(ckey)?.postMessage('changed')\n}\n\nconst onBell = (dbName: string, shelf: IdbShelf, listener: () => void): (() => void) => {\n const ckey = bellCkey(dbName, shelf)\n let ringers = bells.get(ckey)\n if (ringers === undefined) {\n ringers = new Set()\n bells.set(ckey, ringers)\n if (typeof BroadcastChannel !== 'undefined') {\n const channel = new BroadcastChannel(ckey)\n channel.addEventListener('message', () => { bells.get(ckey)?.forEach((ring) => { ring() }) })\n channels.set(ckey, channel)\n }\n }\n ringers.add(listener)\n return () => { ringers.delete(listener) }\n}\n\nconst scopePath = (scope: VaultScope) => `${scope.applabel}:${scope.user_id}:${scope.vaultlabel}`\nconst bundleCkey = (scope: VaultScope) => `vault:${scopePath(scope)}`\nconst boxCkey = (scope: VaultScope, boxlabel: string) => `box:${scopePath(scope)}:${boxlabel}`\nconst boxPrefix = (scope: VaultScope) => `box:${scopePath(scope)}:`\nconst unlockCkey = (scope: VaultScope) => `unlock:${scopePath(scope)}`\n\n/**\n * VaultDB over this browser's IndexedDB: the local-only tier. Nothing syncs -- this \"backend\"\n * never leaves the device -- but every secret field is ciphertext all the same, and the stored\n * rows are exactly what a real backend would hold.\n *\n * @example\n * const vaultDB = idbVaultDB({ dbName: 'my_app' })\n * const vault = await openVault({ applabel: 'my_app', userId, vaultlabel: 'main', vaultDB, deviceDB: idbDeviceDB({ dbName: 'my_app' }) })\n */\nexport const idbVaultDB = (opts: IdbOpts = {}): VaultDB => new IdbVaultDB(opts.dbName ?? 'safebyok')\n\nclass IdbVaultDB extends VaultDB {\n // IndexedDB sets no per-record limit: the origin's storage quota bounds the whole database.\n readonly maxRecordBytes = Infinity\n\n constructor(private readonly dbName: string) { super() }\n private shelf() { return shelfStore(this.dbName, 'vault_db') }\n private ring() { ringBell(this.dbName, 'vault_db') }\n\n protected async fetchBundle(scope: VaultScope): Promise<unknown> {\n return (await get(bundleCkey(scope), this.shelf())) ?? null\n }\n\n protected async saveBundle(bundle: VaultBundle): Promise<void> {\n await set(bundleCkey(bundle), bundle, this.shelf())\n this.ring()\n }\n\n protected async trashBundle(scope: VaultScope): Promise<void> {\n await del(bundleCkey(scope), this.shelf())\n this.ring()\n }\n\n protected async fetchBox(scope: VaultScope, boxlabel: string): Promise<unknown> {\n return (await get(boxCkey(scope, boxlabel), this.shelf())) ?? null\n }\n\n protected async saveBox(scope: VaultScope, lockbox: Lockbox): Promise<void> {\n await set(boxCkey(scope, lockbox.boxlabel), lockbox, this.shelf())\n this.ring()\n }\n\n protected async trashBox(scope: VaultScope, boxlabel: string): Promise<void> {\n await del(boxCkey(scope, boxlabel), this.shelf())\n this.ring()\n }\n\n protected async fetchBoxes(scope: VaultScope): Promise<unknown[]> {\n const rows = await entries(this.shelf())\n return rows\n .filter(([ckey]) => typeof ckey === 'string' && ckey.startsWith(boxPrefix(scope)))\n .map(([, row]) => row as unknown)\n }\n\n // Rings on every write to the shelf, not only ones in scope: a spurious refresh is harmless,\n // and cross-tab news arrives over the BroadcastChannel for free.\n override subscribe(_scope: VaultScope, onChange: () => void): () => void {\n return onBell(this.dbName, 'vault_db', onChange)\n }\n}\n\n/**\n * DeviceDB over this browser's IndexedDB: the real device storage. IndexedDB keeps the\n * DeviceUnlock record by structured clone, which is the only way a browser persists the\n * non-extractable localUnlockKey -- as a handle, never as bytes.\n *\n * A record that will not read back (damaged, or written by another version) reads as \"no\n * remembered unlock\" and is dropped: losing the convenience is the safe failure.\n *\n * @example\n * const deviceDB = idbDeviceDB()\n * await deviceDB.readUnlock({ applabel: 'acme', user_id: 'u1', vaultlabel: 'main' }) // null\n */\nexport const idbDeviceDB = (opts: IdbOpts = {}): DeviceDB => {\n const dbName = opts.dbName ?? 'safebyok'\n const shelf = () => shelfStore(dbName, 'device_db')\n const ring = () => { ringBell(dbName, 'device_db') }\n\n return {\n readUnlock: async (scope) => {\n const raw: unknown = await get(unlockCkey(scope), shelf())\n if (raw === undefined || raw === null) { return null }\n const checked = DeviceUnlock.safeParse(raw)\n if (! checked.success) {\n await del(unlockCkey(scope), shelf())\n ring()\n return null\n }\n return checked.data\n },\n\n writeUnlock: async (unlock) => {\n await set(unlockCkey(unlock), unlock, shelf())\n ring()\n },\n\n eraseUnlock: async (scope) => {\n await del(unlockCkey(scope), shelf())\n ring()\n },\n\n eraseAll: async () => {\n const ckeys = await keys(shelf())\n for (const ckey of ckeys) {\n if (typeof ckey === 'string' && ckey.startsWith('unlock:')) { await del(ckey, shelf()) }\n }\n ring()\n },\n }\n}\n\n/** One stored row, as {@link inspectIdb} reports it: its ckey and the raw stored value. */\nexport type IdbRow = { ckey: string, row: unknown }\n\n/**\n * A window onto one safebyok shelf in IndexedDB, for inspectors and debug panels: the raw rows, a\n * change bell, and a wipe. This is how the demo shows \"what the backend sees\" without asking the\n * vault's opinion -- what it reports is what is actually stored.\n *\n * @example\n * const inspector = inspectIdb({ shelf: 'vault_db' })\n * const rows = await inspector.rows()\n * const stop = inspector.subscribe(() => { void inspector.rows().then(render) })\n */\nexport const inspectIdb = (opts: IdbOpts & { shelf: IdbShelf }) => {\n const dbName = opts.dbName ?? 'safebyok'\n return {\n /** Every stored row on the shelf, sorted by ckey. */\n rows: async (): Promise<IdbRow[]> => {\n const stored = await entries(shelfStore(dbName, opts.shelf))\n return stored\n .filter((pair): pair is [string, unknown] => typeof pair[0] === 'string')\n .map(([ckey, row]) => ({ ckey, row }))\n .toSorted((one, two) => one.ckey.localeCompare(two.ckey))\n },\n /** Calls `listener` after any write to the shelf, this tab or another; returns the unsubscribe. */\n subscribe: (listener: () => void): (() => void) => onBell(dbName, opts.shelf, listener),\n /** Remove every row on the shelf. */\n wipe: async (): Promise<void> => {\n await clear(shelfStore(dbName, opts.shelf))\n ringBell(dbName, opts.shelf)\n },\n }\n}\n"],"mappings":"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"}
package/dist/idb.d.cts ADDED
@@ -0,0 +1,72 @@
1
+ import { r as VaultDB, t as DeviceDB } from "./device-db-D3SBnJdR.cjs";
2
+ //#region src/idb.d.ts
3
+ /**
4
+ * The browser adapters, over IndexedDB (through `idb-keyval`): a vault that lives entirely on
5
+ * this device. `idbDeviceDB` is the real thing -- device storage is exactly what IndexedDB is,
6
+ * and the only browser storage that can hold a non-extractable localUnlockKey. `idbVaultDB` is
7
+ * the no-backend tier: everything a synced backend would hold, kept on this device instead, so a
8
+ * purely client-side app still gets its whispers encrypted at rest.
9
+ *
10
+ * Browser storage is best-effort: the browser may evict it under pressure, and "clear site data"
11
+ * removes it. An app leaning on `idbVaultDB` should call `navigator.storage.persist()` and say
12
+ * plainly that the vault lives only in this browser.
13
+ *
14
+ * This entry point touches IndexedDB only when an adapter is used, so importing it is safe
15
+ * anywhere; the adapters themselves need a browser (or anything else with `indexedDB`).
16
+ */
17
+ export type IdbOpts = {
18
+ /** The IndexedDB database to keep records in. Default 'safebyok'. */
19
+ dbName?: string;
20
+ };
21
+ /** Which of the two safebyok shelves an IndexedDB database holds records on. */
22
+ export type IdbShelf = 'vault_db' | 'device_db';
23
+ /**
24
+ * VaultDB over this browser's IndexedDB: the local-only tier. Nothing syncs -- this "backend"
25
+ * never leaves the device -- but every secret field is ciphertext all the same, and the stored
26
+ * rows are exactly what a real backend would hold.
27
+ *
28
+ * @example
29
+ * const vaultDB = idbVaultDB({ dbName: 'my_app' })
30
+ * const vault = await openVault({ applabel: 'my_app', userId, vaultlabel: 'main', vaultDB, deviceDB: idbDeviceDB({ dbName: 'my_app' }) })
31
+ */
32
+ export declare const idbVaultDB: (opts?: IdbOpts) => VaultDB;
33
+ /**
34
+ * DeviceDB over this browser's IndexedDB: the real device storage. IndexedDB keeps the
35
+ * DeviceUnlock record by structured clone, which is the only way a browser persists the
36
+ * non-extractable localUnlockKey -- as a handle, never as bytes.
37
+ *
38
+ * A record that will not read back (damaged, or written by another version) reads as "no
39
+ * remembered unlock" and is dropped: losing the convenience is the safe failure.
40
+ *
41
+ * @example
42
+ * const deviceDB = idbDeviceDB()
43
+ * await deviceDB.readUnlock({ applabel: 'acme', user_id: 'u1', vaultlabel: 'main' }) // null
44
+ */
45
+ export declare const idbDeviceDB: (opts?: IdbOpts) => DeviceDB;
46
+ /** One stored row, as {@link inspectIdb} reports it: its ckey and the raw stored value. */
47
+ export type IdbRow = {
48
+ ckey: string;
49
+ row: unknown;
50
+ };
51
+ /**
52
+ * A window onto one safebyok shelf in IndexedDB, for inspectors and debug panels: the raw rows, a
53
+ * change bell, and a wipe. This is how the demo shows "what the backend sees" without asking the
54
+ * vault's opinion -- what it reports is what is actually stored.
55
+ *
56
+ * @example
57
+ * const inspector = inspectIdb({ shelf: 'vault_db' })
58
+ * const rows = await inspector.rows()
59
+ * const stop = inspector.subscribe(() => { void inspector.rows().then(render) })
60
+ */
61
+ export declare const inspectIdb: (opts: IdbOpts & {
62
+ shelf: IdbShelf;
63
+ }) => {
64
+ /** Every stored row on the shelf, sorted by ckey. */
65
+ rows: () => Promise<IdbRow[]>;
66
+ /** Calls `listener` after any write to the shelf, this tab or another; returns the unsubscribe. */
67
+ subscribe: (listener: () => void) => (() => void);
68
+ /** Remove every row on the shelf. */
69
+ wipe: () => Promise<void>;
70
+ };
71
+ //#endregion
72
+ //# sourceMappingURL=idb.d.cts.map