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.
- package/LICENSE +201 -0
- package/README.md +14 -2
- package/dist/device-db-D3SBnJdR.d.cts +326 -0
- package/dist/device-db-D3SBnJdR.d.ts +326 -0
- package/dist/idb.cjs +181 -0
- package/dist/idb.cjs.map +1 -0
- package/dist/idb.d.cts +72 -0
- package/dist/idb.d.ts +72 -0
- package/dist/idb.js +178 -0
- package/dist/idb.js.map +1 -0
- package/dist/index.cjs +541 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +237 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.js +520 -0
- package/dist/index.js.map +1 -0
- package/dist/vault-db-BJazN5Ht.js +500 -0
- package/dist/vault-db-BJazN5Ht.js.map +1 -0
- package/dist/vault-db-C19pMom1.cjs +697 -0
- package/dist/vault-db-C19pMom1.cjs.map +1 -0
- package/package.json +95 -3
|
@@ -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
|
package/dist/idb.cjs.map
ADDED
|
@@ -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
|