selfstore 1.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/dist/advanced.d.ts +90 -0
  4. package/dist/advanced.js +41 -0
  5. package/dist/advanced.js.map +1 -0
  6. package/dist/backups.d.ts +182 -0
  7. package/dist/backups.js +360 -0
  8. package/dist/backups.js.map +1 -0
  9. package/dist/cache-De5_sALA.d.ts +129 -0
  10. package/dist/cache-lock-LZILZNLP.js +23 -0
  11. package/dist/cache-lock-LZILZNLP.js.map +1 -0
  12. package/dist/chunk-ATUPHVLG.js +441 -0
  13. package/dist/chunk-ATUPHVLG.js.map +1 -0
  14. package/dist/chunk-BJ3RUMGI.js +45 -0
  15. package/dist/chunk-BJ3RUMGI.js.map +1 -0
  16. package/dist/chunk-CUWB5EWF.js +732 -0
  17. package/dist/chunk-CUWB5EWF.js.map +1 -0
  18. package/dist/chunk-FDPAKHQS.js +230 -0
  19. package/dist/chunk-FDPAKHQS.js.map +1 -0
  20. package/dist/chunk-IBIEKK36.js +331 -0
  21. package/dist/chunk-IBIEKK36.js.map +1 -0
  22. package/dist/chunk-JB4JHM7V.js +474 -0
  23. package/dist/chunk-JB4JHM7V.js.map +1 -0
  24. package/dist/chunk-N4RPMX3Y.js +1908 -0
  25. package/dist/chunk-N4RPMX3Y.js.map +1 -0
  26. package/dist/chunk-OWLX6PNT.js +231 -0
  27. package/dist/chunk-OWLX6PNT.js.map +1 -0
  28. package/dist/chunk-QBCU7OOP.js +20 -0
  29. package/dist/chunk-QBCU7OOP.js.map +1 -0
  30. package/dist/chunk-SGISYMT5.js +46 -0
  31. package/dist/chunk-SGISYMT5.js.map +1 -0
  32. package/dist/chunk-VBFMMR7Q.js +29 -0
  33. package/dist/chunk-VBFMMR7Q.js.map +1 -0
  34. package/dist/drive-Bv94UM8b.d.ts +222 -0
  35. package/dist/flows.d.ts +306 -0
  36. package/dist/flows.js +153 -0
  37. package/dist/flows.js.map +1 -0
  38. package/dist/groups.d.ts +39 -0
  39. package/dist/groups.js +103 -0
  40. package/dist/groups.js.map +1 -0
  41. package/dist/households.d.ts +214 -0
  42. package/dist/households.js +410 -0
  43. package/dist/households.js.map +1 -0
  44. package/dist/index.d.ts +205 -0
  45. package/dist/index.js +249 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/kdf-worker.js +2395 -0
  48. package/dist/kdf-worker.js.map +1 -0
  49. package/dist/local-2py_vGGy.d.ts +47 -0
  50. package/dist/local-AM22RWRR.js +7 -0
  51. package/dist/local-AM22RWRR.js.map +1 -0
  52. package/dist/machine-ALMkT3mk.d.ts +17 -0
  53. package/dist/store-ChIGEOph.d.ts +577 -0
  54. package/dist/sync.d.ts +107 -0
  55. package/dist/sync.js +3 -0
  56. package/dist/sync.js.map +1 -0
  57. package/dist/widgets.d.ts +426 -0
  58. package/dist/widgets.js +2579 -0
  59. package/dist/widgets.js.map +1 -0
  60. package/llms.txt +405 -0
  61. package/package.json +99 -0
@@ -0,0 +1,47 @@
1
+ import { n as Snapshot, E as EncodeOptions, H as Header } from './store-ChIGEOph.js';
2
+ import { D as DriveAuth } from './drive-Bv94UM8b.js';
3
+
4
+ /** Serialize a snapshot to a portable backup blob (compressed; encrypted if a password is given). */
5
+ declare function exportSnapshot(snapshot: Snapshot, opts: EncodeOptions): Promise<Blob>;
6
+ /** Read a backup blob (or raw bytes) back into a snapshot. */
7
+ declare function importSnapshot(input: Blob | Uint8Array, opts?: {
8
+ password?: string;
9
+ }): Promise<Snapshot>;
10
+ /** Peek at a backup's cleartext metadata (app, date, whether it's encrypted) without decrypting. */
11
+ declare function inspect(input: Blob | Uint8Array): Promise<Header>;
12
+ /** True if the backup needs a password to import. */
13
+ declare function isEncrypted(input: Blob | Uint8Array): Promise<boolean>;
14
+ /** Re-encrypt under a new password. `from` opens the file, `to` protects the
15
+ * result; omit `to` for an unencrypted backup. */
16
+ declare function changePassword(input: Blob | Uint8Array, opts: {
17
+ from?: string;
18
+ to?: string;
19
+ readme?: string;
20
+ }): Promise<Blob>;
21
+
22
+ /**
23
+ * A client-only DriveAuth: the Google Identity Services token flow, no backend.
24
+ * The access token is cached in sessionStorage so a reload reuses it, and a
25
+ * silent re-acquisition is attempted when it lapses; if that fails (the Google
26
+ * session expired, or third-party cookies are blocked) the Drive target reports
27
+ * needs-attention and the user reconnects in one click. For a connection that
28
+ * never re-prompts, back the Drive target with a server that holds a refresh
29
+ * token instead (a broker implementing the same DriveAuth interface).
30
+ */
31
+
32
+ /** Build a client-only DriveAuth for the given OAuth client id (no backend). */
33
+ declare function gisDriveAuth(opts: {
34
+ clientId: string;
35
+ scope?: string;
36
+ }): DriveAuth;
37
+
38
+ /**
39
+ * Local-disk helpers: the DOM edge of selfstore, kept out of the core so the
40
+ * core stays environment-agnostic and testable.
41
+ */
42
+ /** Save a backup blob to disk via the File System Access API, else a download. */
43
+ declare function saveToDisk(blob: Blob, filename: string): Promise<void>;
44
+ /** Prompt the user to pick a backup file from disk. Resolves null if cancelled. */
45
+ declare function pickFromDisk(): Promise<File | null>;
46
+
47
+ export { inspect as a, isEncrypted as b, changePassword as c, exportSnapshot as e, gisDriveAuth as g, importSnapshot as i, pickFromDisk as p, saveToDisk as s };
@@ -0,0 +1,7 @@
1
+ export { pickFromDisk, saveToDisk } from './chunk-BJ3RUMGI.js';
2
+ import './chunk-JB4JHM7V.js';
3
+ import './chunk-OWLX6PNT.js';
4
+ import './chunk-FDPAKHQS.js';
5
+ import './chunk-VBFMMR7Q.js';
6
+ //# sourceMappingURL=local-AM22RWRR.js.map
7
+ //# sourceMappingURL=local-AM22RWRR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"local-AM22RWRR.js"}
@@ -0,0 +1,17 @@
1
+ /** What every flow exposes: the current snapshot plus a subscription.
2
+ * `subscribe` calls the listener IMMEDIATELY with the current snapshot and on
3
+ * every change after - that is the Svelte store contract, so a flow works as
4
+ * `$flow` in Svelte; React reads `useSyncExternalStore(flow.subscribe, () =>
5
+ * flow.snapshot)`; everyone else just re-renders in the callback. */
6
+ interface FlowStore<T> {
7
+ readonly snapshot: T;
8
+ subscribe(listener: (snapshot: T) => void): () => void;
9
+ }
10
+ /** Bound a network leg so a stuck destination surfaces as a retryable error
11
+ * instead of a flow frozen on a spinner. The rejection is TARGET_UNAVAILABLE -
12
+ * transient by contract - because "did not answer in time" says nothing about
13
+ * access being lost. Never wrap a human step (a consent popup, a picker):
14
+ * people are allowed to be slower than a deadline. */
15
+ declare function withDeadline<T>(work: Promise<T>, ms: number, what: string): Promise<T>;
16
+
17
+ export { type FlowStore as F, withDeadline as w };
@@ -0,0 +1,577 @@
1
+ import { G as GroupIdentity, S as SignedManifest, L as LocalCache } from './cache-De5_sALA.js';
2
+ import { CollectionChanges, Conflict, SyncConfig } from './sync.js';
3
+
4
+ /** A binary attachment (image, PDF, any bytes). */
5
+ interface SnapshotFile {
6
+ id: string;
7
+ name: string;
8
+ mime: string;
9
+ bytes: Uint8Array;
10
+ }
11
+ /**
12
+ * Collection names starting with `__` are reserved for library bookkeeping
13
+ * (createLocalStore keeps its sync metadata under `__store`). The fluent
14
+ * `backup()` rejects them and `read()` strips them.
15
+ */
16
+ declare const RESERVED_COLLECTION_PREFIX = "__";
17
+ /** Everything a backup carries. Collection names must not start with `__`. */
18
+ interface Snapshot {
19
+ collections: Record<string, unknown[]>;
20
+ files: SnapshotFile[];
21
+ }
22
+ /** Key-derivation parameters. Stored cleartext so older files still decode. */
23
+ interface KdfParams {
24
+ algo: 'argon2id';
25
+ salt: string;
26
+ m: number;
27
+ t: number;
28
+ p: number;
29
+ }
30
+ /**
31
+ * One wrap of the random data key under one password; any slot's password
32
+ * opens the backup. Adding or removing a slot leaves the data key alone.
33
+ * Changing a password rotates the data key (setEncryption) - that is the
34
+ * operation that actually revokes access.
35
+ */
36
+ interface PasswordSlot {
37
+ /** Random id so an app can list or drop a slot without knowing its
38
+ * password. Cleartext, not authenticated. */
39
+ id: string;
40
+ /** Absent on password slots, so files written before external keys
41
+ * existed still parse. */
42
+ kind?: 'password';
43
+ kdf: KdfParams;
44
+ /** 96-bit IV of the wrap, base64. */
45
+ iv: string;
46
+ /** AES-256-GCM(KEK, dataKey) with tag appended: 48 bytes, base64. */
47
+ wrapped: string;
48
+ }
49
+ /**
50
+ * Data key wrapped under a caller-supplied 32-byte secret (passkey PRF,
51
+ * hardware token). The library never performs the WebAuthn or hardware
52
+ * exchange; the app derives the secret and hands it in. KEK is
53
+ * HKDF-SHA256 of the secret - no Argon2, the input is already
54
+ * high-entropy. Shares the `keys[]` table with password slots, so a
55
+ * backup can hold e.g. a passkey plus a recovery password.
56
+ */
57
+ interface ExternalSlot {
58
+ id: string;
59
+ kind: 'external';
60
+ /** App-owned locator for re-deriving the secret (say, a WebAuthn
61
+ * credential id). Cleartext, never a security input. */
62
+ keyRef: string;
63
+ /** 96-bit IV of the wrap, base64. */
64
+ iv: string;
65
+ /** AES-256-GCM(KEK, dataKey) with tag appended: 48 bytes, base64. */
66
+ wrapped: string;
67
+ }
68
+ type KeySlot = PasswordSlot | ExternalSlot;
69
+ /**
70
+ * Group mode (format 2): the per-publication AES key wrapped for one
71
+ * recipient. `kid` = first 8 bytes of SHA-256(recipient X25519 public),
72
+ * base64 - a locator, not a name. `epk` is the ephemeral X25519 public;
73
+ * `wrap` is AES-256-GCM(dataKey) under HKDF of the ECDH shared secret.
74
+ */
75
+ interface RecipientStanza {
76
+ kid: string;
77
+ epk: string;
78
+ iv: string;
79
+ wrap: string;
80
+ }
81
+ /**
82
+ * Cleartext metadata: the `meta.json` entry of the ZIP.
83
+ *
84
+ * app/appVersion/createdAt are cosmetic and unauthenticated - never base a
85
+ * security decision on them. In password-envelope mode the wrapped data key
86
+ * authenticates the payload; in group mode the author's Ed25519 signature
87
+ * (sig.bin) covers the exact meta.json and data.enc bytes.
88
+ */
89
+ interface Header {
90
+ /** Format generation: 2 = plain ZIP, 3 = group, 4 = password envelope.
91
+ * Absent means 2. */
92
+ format?: number;
93
+ app: string;
94
+ /** Writing app's release version, informational. */
95
+ appVersion?: string;
96
+ /** Writing app's data schema version (createLocalStore `version`).
97
+ * Distinct from appVersion: this one drives migration. */
98
+ schemaVersion?: number;
99
+ createdAt: string;
100
+ encryption: 'none' | 'aes-256-gcm';
101
+ /** 96-bit IV of data.enc, base64. Present when encrypted. */
102
+ iv?: string;
103
+ /** Group mode: data-key envelope scheme ('x25519-hkdf-sha256'). */
104
+ keying?: string;
105
+ /** Group mode: author's Ed25519 public key. Trust only after verifying
106
+ * sig.bin and membership in the group manifest. */
107
+ author?: string;
108
+ /** Group mode: one data-key envelope per recipient. */
109
+ recipients?: RecipientStanza[];
110
+ /** Password envelope only: the data key wrapped once per password.
111
+ * Present iff format === 4. */
112
+ keys?: KeySlot[];
113
+ }
114
+ /** Options for writing a backup. */
115
+ interface EncodeOptions {
116
+ app: string;
117
+ appVersion?: string;
118
+ schemaVersion?: number;
119
+ /** Omit or leave empty for an unencrypted backup (a plain, readable ZIP). */
120
+ password?: string;
121
+ /** Group mode: encrypt to these recipients (raw X25519 publics, base64)
122
+ * under a fresh data key and sign with the author's Ed25519 key.
123
+ * Mutually exclusive with `password`. */
124
+ group?: {
125
+ recipients: string[];
126
+ sign: {
127
+ pub: string;
128
+ priv: string;
129
+ };
130
+ };
131
+ /** README text shipped inside an encrypted backup, for whoever opens the
132
+ * ZIP by hand. Defaults to a neutral note; apps should brand it. */
133
+ readme?: string;
134
+ /** Rewrite path: reuse this exact envelope (held data key + slot table
135
+ * from the last read) so a writer that knows only one password keeps
136
+ * every other slot. Mutually exclusive with `password` and `group`. */
137
+ envelope?: {
138
+ dataKey: Uint8Array;
139
+ slots: KeySlot[];
140
+ };
141
+ }
142
+
143
+ type SelfstoreErrorCode = 'BAD_FORMAT' | 'UNSUPPORTED_VERSION' | 'PASSWORD_REQUIRED' | 'DECRYPT_FAILED' | 'TOO_LARGE' | 'AUTH_EXPIRED' | 'TARGET_UNAVAILABLE' | 'TARGET_WRITE_FAILED' | 'TARGET_GONE' | 'NOT_CONNECTED' | 'WEAK_PASSWORD' | 'ENCRYPTION_REQUIRED' | 'UNEXPECTEDLY_UNENCRYPTED' | 'SCHEMA_TOO_NEW' | 'IDENTITY_REQUIRED' | 'SIGNATURE_INVALID' | 'NOT_A_RECIPIENT' | 'MANIFEST_ROLLBACK';
144
+ /**
145
+ * i18n key for an error code: lower-camel-cased under `error.`, e.g.
146
+ * 'error.authExpired'. The library ships the key, the app owns the copy;
147
+ * consumers never have to show the raw developer message. Deterministic, so
148
+ * codes added later get a key for free - keep a fallback for unknown ones.
149
+ */
150
+ declare function errorLabelKey(code: SelfstoreErrorCode): string;
151
+ declare class SelfstoreError extends Error {
152
+ readonly code: SelfstoreErrorCode;
153
+ constructor(code: SelfstoreErrorCode, message: string);
154
+ }
155
+ /**
156
+ * Genuine loss of access to a destination (session gone, token revoked):
157
+ * only a user gesture fixes it. Anything else a target throws is treated as
158
+ * transient and retried, so a network hiccup never raises the blocking
159
+ * reconnect gate over a still-valid connection. Custom targets and DriveAuth
160
+ * implementations throw this to signal the genuine case.
161
+ */
162
+ declare class AuthExpiredError extends SelfstoreError {
163
+ constructor(message?: string);
164
+ }
165
+ /**
166
+ * Shape check rather than instanceof, so it survives two bundled copies of
167
+ * the library. The name check keeps foreign coded errors (ENOENT...) out.
168
+ */
169
+ declare function isSelfstoreError(e: unknown): e is SelfstoreError;
170
+ /** True on genuine access loss, false on transient blips. */
171
+ declare function isAuthExpired(e: unknown): boolean;
172
+
173
+ interface PasswordPolicy {
174
+ /** Minimum length in code points (so an emoji counts as one). Default 0. */
175
+ minLength?: number;
176
+ /** Require at least one lowercase letter (any script with case). */
177
+ requireLowercase?: boolean;
178
+ /** Require at least one uppercase letter (any script with case). */
179
+ requireUppercase?: boolean;
180
+ /** Require at least one digit. */
181
+ requireDigit?: boolean;
182
+ /** Require at least one symbol: anything that is not a letter or a number. */
183
+ requireSymbol?: boolean;
184
+ }
185
+ /** A stable code for each unmet requirement, so the app maps it to its own copy
186
+ * (like the error codes). `minLength` covers the length rule. */
187
+ type PasswordRequirement = 'minLength' | 'lowercase' | 'uppercase' | 'digit' | 'symbol';
188
+ interface PasswordCheck {
189
+ ok: boolean;
190
+ /** The requirements the password fails, in a stable order. Empty when ok. */
191
+ unmet: PasswordRequirement[];
192
+ }
193
+ /** Measure a password against a policy. Pure and synchronous: the same call
194
+ * drives a live strength hint in the UI and the store's enforcement. An empty
195
+ * or absent policy passes everything. */
196
+ declare function checkPasswordPolicy(password: string, policy: PasswordPolicy): PasswordCheck;
197
+
198
+ type Severity = 'ok' | 'info' | 'warn' | 'danger';
199
+ type StorageState = 'ephemeral' | 'cache-only' | 'saving' | 'saved' | 'pending-download' | 'needs-attention';
200
+ /** A suggested action the UI can wire to a button. */
201
+ type StatusAction = 'choose-destination' | 'download' | 'reconnect' | 'unlock';
202
+ /** The headless descriptor the UI renders. */
203
+ interface StatusDescriptor {
204
+ state: StorageState;
205
+ severity: Severity;
206
+ actionable: boolean;
207
+ action?: StatusAction;
208
+ /** Stable i18n key; the app owns the actual copy. */
209
+ labelKey: string;
210
+ }
211
+ interface StatusInput {
212
+ /** False means ephemeral (the local cache is off). */
213
+ persistent: boolean;
214
+ /** Any BackupTarget kind, or a reserved store mode ('device', 'file-manual'). */
215
+ targetKind: string;
216
+ saving: boolean;
217
+ /** The durable home needs a user gesture (token expired, permission lapsed, locked). */
218
+ needsAttention: boolean;
219
+ /** The attention is a lock (password not in memory): the gesture is unlock, not reconnect. */
220
+ locked: boolean;
221
+ /** Degraded file mode has changes awaiting a manual download. */
222
+ pendingDownload: boolean;
223
+ }
224
+ /** Map the store's raw flags to a single, ranked status; the most important wins. */
225
+ declare function deriveStatus(input: StatusInput): StatusDescriptor;
226
+
227
+ type BuiltinTargetKind = 'file' | 'drive' | 'webdav';
228
+ /**
229
+ * The two kinds the store keeps for itself: 'device' (cache-only) and
230
+ * 'file-manual' (degraded download-on-demand). attachTarget refuses a target
231
+ * claiming either - they name the absence of a target.
232
+ */
233
+ declare const RESERVED_STORE_MODES: readonly ["device", "file-manual"];
234
+ /** Single source of truth for "is this kind a store mode, not a durable target". */
235
+ declare function isReservedStoreMode(kind: string): boolean;
236
+ /**
237
+ * The read-only subset of BackupTarget a peer must provide. Any BackupTarget
238
+ * satisfies it structurally; its save() is simply never called. Peer failures
239
+ * follow the same protocol but are recorded per peer and never gate the store.
240
+ */
241
+ interface PeerSource {
242
+ /** The peer's published copy, or null when none exists yet. */
243
+ load(): Promise<Blob | null>;
244
+ /** Cheap remote change marker, so unchanged copies can be skipped. Absent means "cannot tell". */
245
+ stat?(): Promise<string | null>;
246
+ /** Display name (defaults to the peer id). */
247
+ readonly label?: string;
248
+ }
249
+ interface BackupTarget {
250
+ /** Stable discriminator, persisted and handed back to restoreTarget(kind)
251
+ * next session. Custom targets pick any string outside the built-ins and
252
+ * RESERVED_STORE_MODES. */
253
+ readonly kind: string;
254
+ /** Human label for the connected destination (file name, "Google Drive"). */
255
+ readonly label: string;
256
+ /** Push the backup blob. May resolve to the remote's new version marker
257
+ * when the write reports it atomically (else the store calls stat()). */
258
+ save(blob: Blob): Promise<string | null>;
259
+ /** Latest backup blob, or null if none exists / unavailable. */
260
+ load(): Promise<Blob | null>;
261
+ /** Cheap remote change marker (Drive version, file mtime): lets the store
262
+ * spot another replica's writes without downloading. */
263
+ stat?(): Promise<string | null>;
264
+ /** Writable right now, without a user gesture? False for a transient blip
265
+ * (retried later); AuthExpiredError for a genuine loss of access. */
266
+ isReady(): Promise<boolean>;
267
+ /** Re-acquire access (needs a user gesture); resolves to whether it worked. */
268
+ reconnect(): Promise<boolean>;
269
+ /** Forget the target locally. Never deletes the remote data. */
270
+ disconnect(): Promise<void>;
271
+ }
272
+
273
+ type Mode = 'persistent' | 'ephemeral';
274
+ /** Any BackupTarget kind, or a reserved store mode ('device', 'file-manual').
275
+ * Deliberately open: a custom target's kind flows through here. */
276
+ type TargetKind = 'device' | 'file-manual' | (string & {});
277
+ /** What triggered a converge with the remote (for the sync journal). */
278
+ type SyncSource = 'boot' | 'focus' | 'online' | 'interval' | 'manual' | 'push' | 'connect';
279
+ /** One journal line: a converge that actually changed local data. */
280
+ interface SyncJournalEntry {
281
+ at: number;
282
+ source: SyncSource;
283
+ changes: Record<string, CollectionChanges>;
284
+ /** Concurrent same-record edits, auto-resolved latest-wins. Both values are
285
+ * carried so the app can show or restore the overwritten one. */
286
+ conflicts?: Conflict[];
287
+ }
288
+ /** The last problem the store hit, as a stable code the app maps to its own wording. */
289
+ interface StoreError {
290
+ code: SelfstoreErrorCode;
291
+ /** Stable i18n key for `code` (errorLabelKey). Show this, mapped to your copy. */
292
+ labelKey: string;
293
+ /** Developer detail for logs - English, may carry an HTTP status. Never display it. */
294
+ message: string;
295
+ }
296
+ /**
297
+ * Group mode: this member's identity, the admin key pinned at join time
298
+ * (TOFU, from the invite), and the manifest as it travels. The store verifies
299
+ * the manifest itself - signature, member-key shapes, group binding - so
300
+ * group security never depends on the app remembering to call openManifest.
301
+ */
302
+ interface StoreGroupConfig {
303
+ identity: GroupIdentity;
304
+ /** The admin's Ed25519 public key (base64 raw), pinned from the invite. */
305
+ admin: string;
306
+ /** The admin-signed manifest exactly as distributed (signManifest output). */
307
+ manifest: SignedManifest;
308
+ }
309
+ /** One attached peer (another member's read-only copy - see attachPeer). */
310
+ interface PeerState {
311
+ id: string;
312
+ label: string;
313
+ /** Last successful fold-in (ms epoch), or null. */
314
+ lastSyncAt: number | null;
315
+ /** Group mode: the manifest member whose signature the last folded copy carried. */
316
+ author: string | null;
317
+ /** Per-peer on purpose: a broken peer never gates the store itself. */
318
+ lastError: StoreError | null;
319
+ }
320
+ /** One write-only published copy of this store (see attachMirror). */
321
+ interface MirrorState {
322
+ id: string;
323
+ label: string;
324
+ /** Last successful publish (ms epoch), or null. */
325
+ lastPublishAt: number | null;
326
+ /** Per-mirror on purpose: a broken mirror never gates the store itself. */
327
+ lastError: StoreError | null;
328
+ }
329
+ /** One secondary synced copy of this store (see attachReplica). */
330
+ interface ReplicaState {
331
+ id: string;
332
+ label: string;
333
+ /** Last successful publish (ms epoch), or null. */
334
+ lastPublishAt: number | null;
335
+ /** Per-replica on purpose: a broken replica never gates the store itself. */
336
+ lastError: StoreError | null;
337
+ }
338
+ /** The read-only snapshot of the store the UI renders. */
339
+ interface LocalStoreState {
340
+ ready: boolean;
341
+ mode: Mode;
342
+ targetKind: TargetKind;
343
+ label: string | null;
344
+ encrypted: boolean;
345
+ locked: boolean;
346
+ saving: boolean;
347
+ lastSavedAt: number | null;
348
+ status: StatusDescriptor;
349
+ /** The last durable-target problem (upload/refresh), or null. */
350
+ lastError: StoreError | null;
351
+ /** Recent converges that changed local data, newest first (device-local). */
352
+ journal: SyncJournalEntry[];
353
+ /** The last live converge that changed data (drives the "synced" toast). */
354
+ lastSync: SyncJournalEntry | null;
355
+ /** Attached peers, in attach order. */
356
+ peers: PeerState[];
357
+ /** Attached mirrors, in attach order. */
358
+ mirrors: MirrorState[];
359
+ /** Attached replicas (secondary synced copies), in attach order. */
360
+ replicas: ReplicaState[];
361
+ }
362
+ interface LocalStoreOptions {
363
+ /** App id; names the backup and scopes the default cache database. */
364
+ app: string;
365
+ /** Data schema version; bump when gather's shape changes. Stamped into every
366
+ * backup, drives migrate() and the SCHEMA_TOO_NEW gate. */
367
+ schemaVersion: number;
368
+ /** Release version ("1.4.2"), stamped informationally into the header. */
369
+ appVersion?: string;
370
+ /** Branded README shipped inside encrypted backups. */
371
+ readme?: string;
372
+ /** Refuse to ever write or export a plaintext backup. attachTarget without a
373
+ * password or group is rejected, setEncryption(null) is rejected, and
374
+ * exportBlob refuses a cleartext copy - the backup that leaves the device is
375
+ * always ciphertext. Scope is deliberately the travelling copy: the
376
+ * on-device working cache stays governed by the browser profile, as ever.
377
+ * Off by default. */
378
+ requireEncryption?: boolean;
379
+ /** Reject a backup password that fails this strength policy (length and
380
+ * character classes): setEncryption, addEncryptionKey and a password given
381
+ * to attachTarget throw WEAK_PASSWORD before touching anything. The app can
382
+ * preview the same rules with checkPasswordPolicy so its UI blocks a weak
383
+ * password before it is ever submitted. Omit for no policy. */
384
+ passwordPolicy?: PasswordPolicy;
385
+ /** Gather the app's live state into a portable snapshot. */
386
+ gather: () => Promise<Snapshot> | Snapshot;
387
+ /** Apply a snapshot into the app (replaces current state). */
388
+ apply: (snap: Snapshot) => void;
389
+ /** Upgrade a snapshot written by an older version to the current shape. */
390
+ migrate?: (fromVersion: number, snap: Snapshot) => Snapshot;
391
+ /** Per-collection merge strategies. Omit for everything lww-set. */
392
+ sync?: SyncConfig;
393
+ /** Derived collections whose merges are routine noise (recomputed data, not
394
+ * user edits): converges touching only these skip the sync journal. */
395
+ journalSilent?: string[];
396
+ /** Prune delete-tombstones older than this many ms on each save. Safe only
397
+ * when it comfortably exceeds the longest a device stays offline: a device
398
+ * that never saw a delete resurrects the record once the tombstone is gone
399
+ * everywhere. Omit to keep every tombstone; weeks is a reasonable value. */
400
+ tombstoneHorizonMs?: number;
401
+ /** Coordinate several tabs of the same app (default: on in browser
402
+ * windows). Every save first folds what other tabs persisted, writes
403
+ * serialize through a cross-tab Web Lock where available, and a
404
+ * BroadcastChannel refreshes the other tabs after each write. Data and
405
+ * bookkeeping only: a target's connection stays per tab (restoreTarget at
406
+ * boot), and mode/target switches reach other tabs on reload. */
407
+ multiTab?: boolean;
408
+ /** The working store under the app. */
409
+ cache: LocalCache;
410
+ /** Rebuild a durable target connected in a past session. Receives the kind
411
+ * persisted at attach time - built-in or custom. */
412
+ restoreTarget?: (kind: string) => Promise<BackupTarget | null>;
413
+ /** Debounce window for auto-save (ms). Default 800. */
414
+ debounceMs?: number;
415
+ /** Diagnostics sink, developer-facing (defaults to the console). */
416
+ logger?: {
417
+ warn(...args: unknown[]): void;
418
+ error(...args: unknown[]): void;
419
+ };
420
+ }
421
+ interface LocalStore {
422
+ readonly state: LocalStoreState;
423
+ subscribe(fn: () => void): () => void;
424
+ /** Hydrate from the cache, restore a saved durable target, converge with it. */
425
+ init(): Promise<void>;
426
+ /** Schedule a debounced save (call after each mutation). */
427
+ schedule(): void;
428
+ /** Save now, cancelling the debounce (use on tab hide). */
429
+ flush(): Promise<void>;
430
+ /** Converge with the remote only if it changed since the last sync (a cheap
431
+ * stat, throttled). Call on tab focus, network return and a slow interval. */
432
+ syncIfStale(source: SyncSource): Promise<void>;
433
+ /** Converge with the remote now (user gesture). Resolves to what changed
434
+ * locally, or null when already up to date. */
435
+ syncNow(): Promise<SyncJournalEntry | null>;
436
+ /** Inspect a target for an existing backup, without changing any state. */
437
+ inspectTarget(target: BackupTarget): Promise<{
438
+ hasBackup: boolean;
439
+ date: string | null;
440
+ encrypted: boolean;
441
+ }>;
442
+ /** Attach a durable home. `strategy` reconciles with what is already on the
443
+ * target: 'merge' (default, multi-device), 'replace-local' (load the
444
+ * target into the app) or 'replace-remote' (overwrite the target).
445
+ * `keepSession` leaves the departing target's credentials untouched (a
446
+ * backup switch behind one account). `wipe` starts the new home blank -
447
+ * empty data, fresh sync meta, never seeded from the departing one. */
448
+ attachTarget(target: BackupTarget, opts?: {
449
+ password?: string | null;
450
+ /** Group mode: publish signed copies enveloped for every manifest
451
+ * member, trust only member-signed copies back. The manifest is
452
+ * verified here, against the pinned admin key, before anything
453
+ * about the store changes. Mutually exclusive with `password`.
454
+ * Not persisted: re-attach with the group config at boot. */
455
+ group?: StoreGroupConfig;
456
+ strategy?: 'merge' | 'replace-local' | 'replace-remote';
457
+ keepSession?: boolean;
458
+ wipe?: boolean;
459
+ }): Promise<void>;
460
+ /** Apply a newer membership manifest. Verified against the admin key and
461
+ * group id pinned at attach (a swapped admin or foreign group never
462
+ * lands), with seq monotonicity across sessions (MANIFEST_ROLLBACK on an
463
+ * older one). The next publish re-envelopes for the new member list -
464
+ * which is how removal takes effect: a removed member stops being a
465
+ * recipient of anything published from now on. */
466
+ setGroup(manifest: SignedManifest): Promise<void>;
467
+ /** Attach another member's published copy as a read-only peer. Every
468
+ * converge pulls each peer's copy and merges it as one more replica, then
469
+ * publishes the merged state to this store's own target - crossed
470
+ * read-only links are how read-write sharing emerges. A peer copy opens
471
+ * with its own `password` when given, else the session password. Peer
472
+ * problems land on `state.peers` and never gate the store. Not persisted:
473
+ * re-attach at boot. Resolves to the peer id (pass `id` to pin it). */
474
+ attachPeer(source: PeerSource, opts?: {
475
+ id?: string;
476
+ password?: string;
477
+ }): string;
478
+ /** Detach a peer locally (never touches the member's copy). Unknown id: no-op. */
479
+ detachPeer(id: string): void;
480
+ /** Publish a write-only mirror: after every save or converge that moved
481
+ * data, the current state is re-encoded under the mirror's own password
482
+ * and saved to the mirror target. The store's own file is never re-keyed -
483
+ * that is the point: your file stays yours, the copy others read travels
484
+ * under the share key. The envelope is minted once (no KDF per publish);
485
+ * mirror problems land on `state.mirrors` and never gate the store. Not
486
+ * persisted: re-attach at boot. Resolves to the mirror id. */
487
+ attachMirror(target: BackupTarget, opts: {
488
+ password: string;
489
+ id?: string;
490
+ }): string;
491
+ /** Detach a mirror locally (the published copy stays where it is). */
492
+ detachMirror(id: string): void;
493
+ /** Attach a secondary synced copy: after every save or converge that moved
494
+ * data, the CURRENT backup - same bytes, same key as the primary home - is
495
+ * also written to this target. A resilience mirror of your own file (Drive
496
+ * plus an S3 bucket, say), not a share: any device can later attach it as
497
+ * its primary with the same password. Publishing reuses the primary's
498
+ * envelope, so it costs no extra KDF; replica problems land on
499
+ * `state.replicas` and never gate the store. Not persisted: re-attach at
500
+ * boot. Resolves to the replica id (pass `id` to pin it). */
501
+ attachReplica(target: BackupTarget, opts?: {
502
+ id?: string;
503
+ }): string;
504
+ /** Detach a replica locally (the copy already written stays where it is). */
505
+ detachReplica(id: string): void;
506
+ /** Switch to degraded download-on-demand mode (non-Chromium file). */
507
+ setManualFile(): Promise<void>;
508
+ /** Unlock an encrypted target: the password is held in memory only, then a
509
+ * converge runs. False on a wrong password (the gate re-raises) or when
510
+ * the target is not locked (use `reconnect`). */
511
+ unlock(password: string): Promise<boolean>;
512
+ /** Re-acquire access after a genuine loss: run the target's reconnect
513
+ * gesture, then converge. False when locked (use `unlock`) or absent. */
514
+ reconnect(): Promise<boolean>;
515
+ /** Re-lock now: drop the in-memory password (encrypted target). */
516
+ lock(): void;
517
+ /** Detach the durable home (does not delete it); back to cache-only.
518
+ * `keepSession` skips the target's disconnect so its credentials survive -
519
+ * for a caller that still needs the destination right after (deleting the
520
+ * detached file, attaching another one on the same account). */
521
+ detachTarget(opts?: {
522
+ keepSession?: boolean;
523
+ }): Promise<void>;
524
+ /** Build the portable .selfstore blob (for a manual download or dated copy). */
525
+ exportBlob(): Promise<Blob>;
526
+ /** Mark the file-manual pending download as resolved. */
527
+ markDownloaded(): void;
528
+ /** Stop persisting (shared computer). */
529
+ setEphemeral(): void;
530
+ /** Resume persisting to the cache. */
531
+ leaveEphemeral(): Promise<void>;
532
+ /** Wipe all app data. With a destination connected, keep the connection and
533
+ * empty its backup too; with none, forget locally and return to the
534
+ * cache-only default. The UI empties immediately, no reload. */
535
+ forget(): Promise<void>;
536
+ /** Add, change or remove the backup password - reversible. A non-empty
537
+ * string (re)encrypts; null decrypts. Needs a connected, unlocked target;
538
+ * rewrites cache and remote. Setting a password rotates the data key
539
+ * (fresh key, single slot), so every previously authorized password
540
+ * stops opening the file. */
541
+ setEncryption(password: string | null): Promise<void>;
542
+ /** Add another password that opens this backup: the data key gains one
543
+ * slot, nothing is re-encrypted, existing passwords keep working - how
544
+ * sharing adds its key without clobbering the owner's. Verified against
545
+ * the built bytes before the write lands. Resolves to the slot id. */
546
+ addEncryptionKey(password: string, id?: string): Promise<string>;
547
+ /** Remove one password slot by id. Refuses the last slot (that is
548
+ * setEncryption(null)) and refuses to lock the current session out.
549
+ * Bearer reality: whoever held that password may hold old bytes - only a
550
+ * rotation makes new writes unreadable to them. */
551
+ removeEncryptionKey(id: string): Promise<void>;
552
+ /** Encrypt under an external key (passkey PRF, hardware token) instead of
553
+ * a typed password: fresh data key, one external slot. `keyRef` is stored
554
+ * verbatim so the app can re-derive the secret. Rotates like
555
+ * setEncryption; the app owns the WebAuthn exchange and hands in the
556
+ * 32-byte secret. */
557
+ setExternalEncryption(secret: Uint8Array, keyRef: string): Promise<void>;
558
+ /** Add an external-key slot to an already-encrypted backup - a passkey
559
+ * alongside a recovery password, or the reverse. Needs an unlocked
560
+ * session; verified against the built bytes before the write lands. */
561
+ addExternalKey(secret: Uint8Array, keyRef: string, id?: string): Promise<string>;
562
+ /** unlock(), but trying the external slots with `secret`. */
563
+ unlockWithExternal(secret: Uint8Array): Promise<boolean>;
564
+ /** The current slot table (id + kind, so a passkey slot can be labelled).
565
+ * Empty when unknown: plaintext, locked, or not read yet this session. */
566
+ listEncryptionKeys(): {
567
+ id: string;
568
+ kind: 'password' | 'external';
569
+ }[];
570
+ /** Cancel timers, close the cross-tab channel, drop subscribers - nothing
571
+ * fires into a dead app after unmount. Persisted data is untouched; the
572
+ * store is not usable afterwards. */
573
+ dispose(): void;
574
+ }
575
+ declare function createLocalStore(opts: LocalStoreOptions): LocalStore;
576
+
577
+ export { AuthExpiredError as A, type BackupTarget as B, errorLabelKey as C, isAuthExpired as D, type EncodeOptions as E, isReservedStoreMode as F, isSelfstoreError as G, type Header as H, type KdfParams as K, type LocalStore as L, type MirrorState as M, type PasswordCheck as P, RESERVED_COLLECTION_PREFIX as R, SelfstoreError as S, type TargetKind as T, type BuiltinTargetKind as a, type LocalStoreOptions as b, type LocalStoreState as c, type Mode as d, type PasswordPolicy as e, type PasswordRequirement as f, type PeerSource as g, type PeerState as h, RESERVED_STORE_MODES as i, type RecipientStanza as j, type ReplicaState as k, type SelfstoreErrorCode as l, type Severity as m, type Snapshot as n, type SnapshotFile as o, type StatusAction as p, type StatusDescriptor as q, type StatusInput as r, type StorageState as s, type StoreError as t, type StoreGroupConfig as u, type SyncJournalEntry as v, type SyncSource as w, checkPasswordPolicy as x, createLocalStore as y, deriveStatus as z };