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.
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/dist/advanced.d.ts +90 -0
- package/dist/advanced.js +41 -0
- package/dist/advanced.js.map +1 -0
- package/dist/backups.d.ts +182 -0
- package/dist/backups.js +360 -0
- package/dist/backups.js.map +1 -0
- package/dist/cache-De5_sALA.d.ts +129 -0
- package/dist/cache-lock-LZILZNLP.js +23 -0
- package/dist/cache-lock-LZILZNLP.js.map +1 -0
- package/dist/chunk-ATUPHVLG.js +441 -0
- package/dist/chunk-ATUPHVLG.js.map +1 -0
- package/dist/chunk-BJ3RUMGI.js +45 -0
- package/dist/chunk-BJ3RUMGI.js.map +1 -0
- package/dist/chunk-CUWB5EWF.js +732 -0
- package/dist/chunk-CUWB5EWF.js.map +1 -0
- package/dist/chunk-FDPAKHQS.js +230 -0
- package/dist/chunk-FDPAKHQS.js.map +1 -0
- package/dist/chunk-IBIEKK36.js +331 -0
- package/dist/chunk-IBIEKK36.js.map +1 -0
- package/dist/chunk-JB4JHM7V.js +474 -0
- package/dist/chunk-JB4JHM7V.js.map +1 -0
- package/dist/chunk-N4RPMX3Y.js +1908 -0
- package/dist/chunk-N4RPMX3Y.js.map +1 -0
- package/dist/chunk-OWLX6PNT.js +231 -0
- package/dist/chunk-OWLX6PNT.js.map +1 -0
- package/dist/chunk-QBCU7OOP.js +20 -0
- package/dist/chunk-QBCU7OOP.js.map +1 -0
- package/dist/chunk-SGISYMT5.js +46 -0
- package/dist/chunk-SGISYMT5.js.map +1 -0
- package/dist/chunk-VBFMMR7Q.js +29 -0
- package/dist/chunk-VBFMMR7Q.js.map +1 -0
- package/dist/drive-Bv94UM8b.d.ts +222 -0
- package/dist/flows.d.ts +306 -0
- package/dist/flows.js +153 -0
- package/dist/flows.js.map +1 -0
- package/dist/groups.d.ts +39 -0
- package/dist/groups.js +103 -0
- package/dist/groups.js.map +1 -0
- package/dist/households.d.ts +214 -0
- package/dist/households.js +410 -0
- package/dist/households.js.map +1 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +249 -0
- package/dist/index.js.map +1 -0
- package/dist/kdf-worker.js +2395 -0
- package/dist/kdf-worker.js.map +1 -0
- package/dist/local-2py_vGGy.d.ts +47 -0
- package/dist/local-AM22RWRR.js +7 -0
- package/dist/local-AM22RWRR.js.map +1 -0
- package/dist/machine-ALMkT3mk.d.ts +17 -0
- package/dist/store-ChIGEOph.d.ts +577 -0
- package/dist/sync.d.ts +107 -0
- package/dist/sync.js +3 -0
- package/dist/sync.js.map +1 -0
- package/dist/widgets.d.ts +426 -0
- package/dist/widgets.js +2579 -0
- package/dist/widgets.js.map +1 -0
- package/llms.txt +405 -0
- 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 };
|