selfstore 1.4.0 → 1.6.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/dist/flows.d.ts CHANGED
@@ -1,326 +1,6 @@
1
- import { t as StoreError, B as BackupTarget, L as LocalStore, k as ReplicaState } from './store-C20_6JBo.js';
2
- import { K as KV } from './cache-ilsK3Tki.js';
3
- import { W as WebdavConfig, S as S3Config, D as DriveAuth } from './drive-BhiTdk-Q.js';
4
- import { F as FlowStore } from './machine-ALMkT3mk.js';
5
- export { w as withDeadline } from './machine-ALMkT3mk.js';
1
+ export { c as ConnectFlow, a as ConnectFlowOptions, h as ConnectFlowOutcome, b as ConnectKind, i as ConnectResolution, j as ConnectSnapshot, k as ConnectStep, C as ConnectTargets, l as Connector, F as FlowHost, J as JoinEngine, g as JoinFlow, m as JoinOutcome, n as JoinPreview, o as JoinSnapshot, p as JoinStep, q as REPLICA_ID, R as ReplicaFlow, r as ReplicaFlowOptions, s as ReplicaSnapshot, t as ReplicaStep, u as ShareBusy, d as ShareEngine, f as ShareFlow, e as ShareLevel, v as ShareLinkInfo, w as ShareMemberInfo, x as ShareSnapshot, y as connectFlow, z as joinFlow, A as replicaFlow, B as shareFlow } from './flows-BBMVdP1S.js';
2
+ export { F as FlowStore, w as withDeadline } from './machine-ALMkT3mk.js';
3
+ import './store-C20_6JBo.js';
4
+ import './cache-ilsK3Tki.js';
6
5
  import './sync.js';
7
-
8
- type ConnectKind = 'drive' | 'file' | 'webdav' | 's3';
9
- /** Produce a connected BackupTarget, or null when the user cancelled the
10
- * gesture (consent refused, picker closed). Custom connectors slot custom
11
- * targets - or test fakes - into the same journey. */
12
- type Connector = () => Promise<BackupTarget | null>;
13
- /** Which destinations the flow offers, and how to open each one. The offer
14
- * renders in this OBJECT's insertion order: the host decides which
15
- * destination leads by how it writes the object, not by a ranking here. */
16
- interface ConnectTargets {
17
- /** Google Drive: the auth strategy (e.g. gisDriveAuth), or a custom connector. */
18
- drive?: DriveAuth | Connector;
19
- /** Local file via the browser's picker (true), or a custom connector. On
20
- * browsers without the File System Access API the flow degrades to the
21
- * manual download mode by itself. The object form offers TWO gestures on
22
- * the same destination: `create` (save picker: start or overwrite a backup
23
- * file) and `open` (open picker: adopt an EXISTING backup file - it then
24
- * goes through the same inspect/password/resolution journey). */
25
- file?: true | Connector | {
26
- create?: true | Connector;
27
- open?: true | Connector;
28
- };
29
- /** WebDAV: true renders a 'form' step and expects submitWebdav(config);
30
- * a fixed WebdavConfig skips the form; or a custom connector. */
31
- webdav?: true | WebdavConfig | Connector;
32
- /** S3-compatible bucket (Amazon S3, R2, B2, MinIO): true renders a 'form'
33
- * step and expects submitS3(config); a fixed S3Config skips the form; or a
34
- * custom connector. Offer it alongside `webdav` and the widget groups both
35
- * under one "your server" entry with a WebDAV/S3 toggle. */
36
- s3?: true | S3Config | Connector;
37
- }
38
- /** What the flows need from the store. The simple store exposes this as
39
- * `store.flowHost`; an advanced-store app builds it by hand. */
40
- interface FlowHost {
41
- engine: LocalStore;
42
- kv: KV;
43
- /** Backup file name on the destination (the simple store uses `app.zip`). */
44
- backupName: string;
45
- }
46
- interface ConnectFlowOptions {
47
- /** Declare when this device holds data worth a question: only then does an
48
- * existing backup raise the 'conflict' step with the destructive choices.
49
- * Default: never - the flow applies defaultResolution. */
50
- hasLocalData?: () => boolean;
51
- /** How to reconcile with a destination that already holds a backup when the
52
- * conflict step is not raised. Default 'merge' - the only choice that
53
- * loses nothing on either side. 'resume' adopts the backup (the
54
- * destination wins; this device's working copy is replaced), 'replace'
55
- * overwrites the backup with this device. An encrypted backup still asks
56
- * for its password first, whatever the resolution; an empty destination
57
- * is not concerned (this device's data simply becomes its content). */
58
- defaultResolution?: ConnectResolution;
59
- /** Skip the password step on an encrypted backup: adopt it AS is (resume)
60
- * and complete with the store locked - nothing readable, nothing
61
- * clobbered - for the host to unlock on its own surface (engine.unlock)
62
- * after the connect. Adoption is forced whatever defaultResolution says:
63
- * without the proven key a merge is impossible and a replace would
64
- * destroy a backup no one proved they own. Hosts that let a device
65
- * holding real local data connect should keep the password-first
66
- * journey - adoption replaces the working copy. */
67
- deferUnlock?: boolean;
68
- /**
69
- * A secret the host already holds (an app passphrase, a derived key), used
70
- * to encrypt the backup when this journey creates or adopts one that is NOT
71
- * already encrypted - an empty destination, or a plaintext file.
72
- *
73
- * Two things need it. A store built with `requireEncryption` refuses a
74
- * passwordless attach outright (ENCRYPTION_REQUIRED), so without this the
75
- * connect journey cannot complete at all. And even without that setting, a
76
- * fresh backup is better born encrypted than written in clear and protected
77
- * on a second write, which would leave cleartext on the destination in
78
- * between.
79
- *
80
- * Never used against a destination whose backup is already encrypted: that
81
- * one is proven through the password step (or adopted locked under
82
- * `deferUnlock`), so a host secret that happens to differ can neither fail
83
- * the attach nor overwrite what it cannot read. Resolved lazily, so the
84
- * secret is only read when a connect actually needs it. Must satisfy the
85
- * store's password policy.
86
- */
87
- password?: string | (() => string | Promise<string>);
88
- /** Deadline for each network leg (inspect, trial-read, attach), ms. */
89
- deadlineMs?: number;
90
- }
91
- type ConnectStep = 'choose' | 'form' | 'authorizing' | 'password' | 'conflict' | 'connected' | 'error';
92
- /** How to reconcile with a destination that already holds a backup. */
93
- type ConnectResolution = 'merge' | 'resume' | 'replace';
94
- /** What connecting did. 'merged': both sides folded. 'started': empty
95
- * destination now holds this device's data. 'resumed': the backup won,
96
- * this device adopted it. 'replaced': this device won, the backup was
97
- * overwritten. 'manual': degraded download-on-demand file mode. */
98
- type ConnectFlowOutcome = 'merged' | 'started' | 'resumed' | 'replaced' | 'manual';
99
- interface ConnectSnapshot {
100
- step: ConnectStep;
101
- /** The destinations to offer, in the order given. */
102
- kinds: readonly ConnectKind[];
103
- /** The destination being connected (null while choosing). */
104
- kind: ConnectKind | null;
105
- /** Set when step === 'connected'. */
106
- outcome: ConnectFlowOutcome | null;
107
- /** Known after inspection: the destination already holds a backup. */
108
- hasBackup: boolean;
109
- /** ...and that backup is encrypted. */
110
- encrypted: boolean;
111
- /** The last submitPassword() was wrong (stay on 'password', say so). */
112
- passwordError: boolean;
113
- /** An async leg is in flight (render a spinner, disable the actions). */
114
- busy: boolean;
115
- /** Set when step === 'error': show `labelKey`, log `message`. */
116
- error: StoreError | null;
117
- }
118
- interface ConnectFlow extends FlowStore<ConnectSnapshot> {
119
- /** Pick a destination. Call it inside the user's click: the consent popup /
120
- * file picker opens synchronously from the gesture. Ignored while a leg is
121
- * already in flight (one popup per gesture). For a `file` target declared
122
- * with the object form, `variant` picks the gesture: 'create' (default,
123
- * save picker) or 'open' (adopt an existing backup file). */
124
- choose(kind: ConnectKind, variant?: 'create' | 'open'): void;
125
- /** 'form' step (webdav: true): submit the server config. */
126
- submitWebdav(config: WebdavConfig): void;
127
- /** 'form' step (s3: true): submit the bucket config. */
128
- submitS3(config: S3Config): void;
129
- /** 'password' step: prove the backup's password. Wrong password sets
130
- * passwordError and stays here; nothing is attached either way. */
131
- submitPassword(password: string): void;
132
- /** 'password' step escape hatch: give up on a forgotten password and start a
133
- * fresh backup over the protected one. The encrypted backup is overwritten
134
- * by this device's data (possibly empty) and lost for good - the only way
135
- * out when the password cannot be recovered. */
136
- overwrite(): void;
137
- /** 'conflict' step: pick the reconciliation. */
138
- resolveConflict(how: ConnectResolution): void;
139
- /** Abandon the journey in progress: back to 'choose'. A late resolution of
140
- * the abandoned leg is discarded. */
141
- cancel(): void;
142
- /** 'error' step: back to 'choose' to try again. */
143
- retry(): void;
144
- }
145
- declare function connectFlow(store: FlowHost | {
146
- flowHost: FlowHost;
147
- }, targets: ConnectTargets, options?: ConnectFlowOptions): ConnectFlow;
148
-
149
- /** The one replica this flow manages, as its id on the engine. */
150
- declare const REPLICA_ID = "replica";
151
- type ReplicaStep = 'idle' | 'choose' | 'form-webdav' | 'form-s3';
152
- interface ReplicaSnapshot {
153
- step: ReplicaStep;
154
- /** The kinds the host offers, in ConnectTargets order. */
155
- kinds: ConnectKind[];
156
- busy: boolean;
157
- error: StoreError | null;
158
- /** The live engine entry (label, lastPublishAt, lastError), null when none. */
159
- replica: ReplicaState | null;
160
- }
161
- interface ReplicaFlowOptions {
162
- /** File name of the copy on its destination. Default: the primary backup
163
- * name with `.replica` before the extension ("App.zip" -> "App.replica.zip"),
164
- * which a same-account Drive never confuses with the primary file and the
165
- * backups panel never lists as a backup of its own. */
166
- fileName?: string;
167
- /** Rebuild the target of a custom Connector kind on restore() (built-in
168
- * kinds rebuild by themselves). Without it, custom kinds are not restored. */
169
- restoreTarget?: (kind: ConnectKind) => Promise<BackupTarget | null>;
170
- }
171
- interface ReplicaFlow extends FlowStore<ReplicaSnapshot> {
172
- /** Show the destination choice (from the user's gesture). */
173
- open(): void;
174
- /** Back to idle; never an error. */
175
- cancel(): void;
176
- /** Run a destination (inside the user's click - pickers and consent popups
177
- * open from it). `webdav: true` / `s3: true` route to their form step. */
178
- pick(kind: ConnectKind): void;
179
- /** 'form-webdav' step: submit the server config. */
180
- submitWebdav(config: WebdavConfig): void;
181
- /** 'form-s3' step: submit the bucket config. */
182
- submitS3(config: S3Config): void;
183
- /** Detach the copy and forget its session. Never deletes remote data. */
184
- remove(): Promise<void>;
185
- /** Silent reboot: re-attach the copy recorded by a past session, if any.
186
- * Never throws - an unreachable destination retries at the next boot. */
187
- restore(): Promise<void>;
188
- /** Detach from the engine (the replica itself stays attached). */
189
- dispose(): void;
190
- }
191
- declare function replicaFlow(store: FlowHost | {
192
- flowHost: FlowHost;
193
- }, targets: ConnectTargets, options?: ReplicaFlowOptions): ReplicaFlow;
194
-
195
- type ShareLevel = 'read' | 'write';
196
- interface ShareLinkInfo {
197
- /** Engine-scoped stable id (revoke keys on it). */
198
- id: string;
199
- /** The invitation URL to hand out. */
200
- url: string;
201
- level: ShareLevel;
202
- }
203
- interface ShareMemberInfo {
204
- /** Engine-scoped stable id (removeMember keys on it). */
205
- id: string;
206
- label?: string;
207
- /** This row is the local user. */
208
- self?: boolean;
209
- /** This row owns the share (the app may render a badge, the flow does not care). */
210
- owner?: boolean;
211
- }
212
- /** The app-side port the flow drives. Implementations wrap whatever transport
213
- * the app shares through; every method may throw (SelfstoreError preferred,
214
- * anything is normalized). */
215
- interface ShareEngine {
216
- /** The current truth: active links and members. */
217
- list(): Promise<{
218
- links: ShareLinkInfo[];
219
- members: ShareMemberInfo[];
220
- }>;
221
- createLink(opts: {
222
- level: ShareLevel;
223
- }): Promise<ShareLinkInfo>;
224
- revokeLink(id: string): Promise<void>;
225
- /** Optional: engines without member management simply omit it. */
226
- removeMember?(id: string): Promise<void>;
227
- /** Optional: END the whole share in one gesture - every link dead, every
228
- * member out. Engines whose transport has such a move (an invitation to
229
- * kill, a file to re-privatize) expose it; the widget renders the action
230
- * only then. */
231
- revokeAll?(): Promise<void>;
232
- }
233
- type ShareBusy = 'refresh' | 'create' | 'revoke' | 'remove' | 'revoke-all' | null;
234
- interface ShareSnapshot {
235
- links: readonly ShareLinkInfo[];
236
- members: readonly ShareMemberInfo[];
237
- /** The operation in flight, or null. */
238
- busy: ShareBusy;
239
- /** The last refresh failed: the lists shown are the last-known good ones. */
240
- stale: boolean;
241
- /** The last operation's problem (the lists are never cleared by an error). */
242
- error: StoreError | null;
243
- /** True once the engine offers removeMember (render the row action at all). */
244
- canRemoveMembers: boolean;
245
- /** True once the engine offers revokeAll (render the end-the-share action). */
246
- canRevokeAll: boolean;
247
- }
248
- interface ShareFlow extends FlowStore<ShareSnapshot> {
249
- /** Re-list from the engine. Failure keeps the last-known lists (stale). */
250
- refresh(): Promise<void>;
251
- /** Create an invitation link. Resolves to it, or null when refused (an
252
- * operation was already in flight). */
253
- createLink(opts: {
254
- level: ShareLevel;
255
- }): Promise<ShareLinkInfo | null>;
256
- /** Revoke a link. The link leaves the snapshot only once the engine
257
- * confirmed. Resolves to whether it did. */
258
- revokeLink(id: string): Promise<boolean>;
259
- /** Remove a member (never yourself - refused up front). Resolves to whether
260
- * the engine confirmed. */
261
- removeMember(id: string): Promise<boolean>;
262
- /** End the whole share: every link and member leaves the snapshot only
263
- * once the engine confirmed. Resolves to whether it did. */
264
- revokeAll(): Promise<boolean>;
265
- }
266
- declare function shareFlow(engine: ShareEngine, options?: {
267
- deadlineMs?: number;
268
- }): ShareFlow;
269
-
270
- /** What the user is about to join - enough to render an honest question. */
271
- interface JoinPreview {
272
- /** The share's display name, when the link carries one. */
273
- label?: string;
274
- /** Who shared it (a name, an email - whatever the engine knows). */
275
- from?: string;
276
- level?: ShareLevel;
277
- }
278
- /** How joining ended, other than plain failure:
279
- * 'joined' - this device now follows the share.
280
- * 'mismatch' - this device already follows a DIFFERENT share: joining a
281
- * second one is refused (leave the first, or use another
282
- * device/profile).
283
- * 'no-invite' - the invitation is spent, expired, or aimed at another
284
- * account (switching accounts may be the fix). */
285
- type JoinOutcome = 'joined' | 'mismatch' | 'no-invite';
286
- /** The app-side port: how a link is read and honoured on this transport. */
287
- interface JoinEngine {
288
- /** Read-only look at the invitation. Must change NOTHING. */
289
- preview(link: string): Promise<JoinPreview>;
290
- /** Consume the invitation and wire the share. Resolve null when the user
291
- * backed out of a popup the engine had to open (an account chooser, a
292
- * consent screen): the flow returns to 'ready' silently - backing out of
293
- * a human gesture is not an error, here as everywhere in these flows. */
294
- join(link: string): Promise<JoinOutcome | null>;
295
- /** Optional: re-run the account chooser (the invite may belong to another
296
- * account). The flow re-previews after it. */
297
- switchAccount?(): Promise<void>;
298
- }
299
- type JoinStep = 'previewing' | 'ready' | 'joining' | 'joined' | 'mismatch' | 'no-invite' | 'error';
300
- interface JoinSnapshot {
301
- /** The invitation this flow was opened with - held here so the UI can
302
- * always find it again. */
303
- link: string;
304
- step: JoinStep;
305
- /** Set once previewed (kept through errors, so retry re-renders context). */
306
- preview: JoinPreview | null;
307
- /** The engine offers an account switch. */
308
- canSwitchAccount: boolean;
309
- busy: boolean;
310
- /** Set when step === 'error': show `labelKey`, log `message`. */
311
- error: StoreError | null;
312
- }
313
- interface JoinFlow extends FlowStore<JoinSnapshot> {
314
- /** Say yes: consume the invitation. Only from 'ready'. */
315
- accept(): void;
316
- /** Re-run the account chooser, then preview again under the new account.
317
- * Available from 'ready', 'no-invite' and 'error' when the engine offers it. */
318
- switchAccount(): void;
319
- /** From 'error': preview again and resume the journey. */
320
- retry(): void;
321
- }
322
- declare function joinFlow(link: string, engine: JoinEngine, options?: {
323
- deadlineMs?: number;
324
- }): JoinFlow;
325
-
326
- export { type ConnectFlow, type ConnectFlowOptions, type ConnectFlowOutcome, type ConnectKind, type ConnectResolution, type ConnectSnapshot, type ConnectStep, type ConnectTargets, type Connector, type FlowHost, FlowStore, type JoinEngine, type JoinFlow, type JoinOutcome, type JoinPreview, type JoinSnapshot, type JoinStep, REPLICA_ID, type ReplicaFlow, type ReplicaFlowOptions, type ReplicaSnapshot, type ReplicaStep, type ShareBusy, type ShareEngine, type ShareFlow, type ShareLevel, type ShareLinkInfo, type ShareMemberInfo, type ShareSnapshot, connectFlow, joinFlow, replicaFlow, shareFlow };
6
+ import './drive-BhiTdk-Q.js';
package/dist/flows.js CHANGED
@@ -1,6 +1,6 @@
1
- export { connectFlow, joinFlow, shareFlow } from './chunk-3XR3GLR2.js';
1
+ export { connectFlow, joinFlow, shareFlow } from './chunk-QVXZLZ6R.js';
2
2
  import { connect, connect2, connect4, connect3, fromSession, fromSession2, fromSession3, fromSession4 } from './chunk-TEIAFKFS.js';
3
- import './chunk-SRKUHFRG.js';
3
+ import './chunk-GJRHMI2O.js';
4
4
  import { makeMachine, toStoreError } from './chunk-43BJDRPT.js';
5
5
  export { withDeadline } from './chunk-43BJDRPT.js';
6
6
  import './chunk-KHER6MWR.js';
package/dist/flows.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/flows/replica.ts"],"names":["connect","config","fromSession"],"mappings":"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","file":"flows.js","sourcesContent":["// The backup-copy journey: attach ONE replica (a second destination receiving\n// the same encrypted backup as the primary home) behind a small state machine\n// the widgets can render. Reuses the connect flow's target specs, but there is\n// no password step, no conflict step and no deadline: a replica carries the\n// primary's bytes as-is, and every connector may contain a human gesture.\n\nimport type { BackupTarget } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { ReplicaState, StoreError } from '../persistence/store';\nimport { makeMachine, toStoreError, type FlowStore } from './machine';\nimport type { ConnectKind, ConnectTargets, Connector, FlowHost } from './connect';\nimport {\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\ttype DriveAuth\n} from '../persistence/targets/drive';\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\ttype WebdavConfig\n} from '../persistence/targets/webdav';\nimport {\n\tconnect as s3Connect,\n\tfromSession as s3FromSession,\n\ttype S3Config\n} from '../persistence/targets/s3';\n\n/** The one replica this flow manages, as its id on the engine. */\nexport const REPLICA_ID = 'replica';\n\n// The replica's target session lives in the SAME kv as the primary's, so every\n// key is prefixed: a second driveTarget (say) must never read or clobber the\n// primary connection's session keys.\nconst SCOPE = 'replica:';\nconst RECORD_KEY = 'record';\n\n/** What survives a reboot: which provider the copy lives on. The target's own\n * session (file id, handle, server config) is persisted by the target module\n * itself, under the scoped kv. */\ninterface ReplicaRecord {\n\tkind: ConnectKind;\n}\n\nexport type ReplicaStep = 'idle' | 'choose' | 'form-webdav' | 'form-s3';\n\nexport interface ReplicaSnapshot {\n\tstep: ReplicaStep;\n\t/** The kinds the host offers, in ConnectTargets order. */\n\tkinds: ConnectKind[];\n\tbusy: boolean;\n\terror: StoreError | null;\n\t/** The live engine entry (label, lastPublishAt, lastError), null when none. */\n\treplica: ReplicaState | null;\n}\n\nexport interface ReplicaFlowOptions {\n\t/** File name of the copy on its destination. Default: the primary backup\n\t * name with `.replica` before the extension (\"App.zip\" -> \"App.replica.zip\"),\n\t * which a same-account Drive never confuses with the primary file and the\n\t * backups panel never lists as a backup of its own. */\n\tfileName?: string;\n\t/** Rebuild the target of a custom Connector kind on restore() (built-in\n\t * kinds rebuild by themselves). Without it, custom kinds are not restored. */\n\trestoreTarget?: (kind: ConnectKind) => Promise<BackupTarget | null>;\n}\n\nexport interface ReplicaFlow extends FlowStore<ReplicaSnapshot> {\n\t/** Show the destination choice (from the user's gesture). */\n\topen(): void;\n\t/** Back to idle; never an error. */\n\tcancel(): void;\n\t/** Run a destination (inside the user's click - pickers and consent popups\n\t * open from it). `webdav: true` / `s3: true` route to their form step. */\n\tpick(kind: ConnectKind): void;\n\t/** 'form-webdav' step: submit the server config. */\n\tsubmitWebdav(config: WebdavConfig): void;\n\t/** 'form-s3' step: submit the bucket config. */\n\tsubmitS3(config: S3Config): void;\n\t/** Detach the copy and forget its session. Never deletes remote data. */\n\tremove(): Promise<void>;\n\t/** Silent reboot: re-attach the copy recorded by a past session, if any.\n\t * Never throws - an unreachable destination retries at the next boot. */\n\trestore(): Promise<void>;\n\t/** Detach from the engine (the replica itself stays attached). */\n\tdispose(): void;\n}\n\nfunction scopedKv(kv: KV, prefix: string): KV {\n\treturn {\n\t\tget: <T = unknown>(key: string) => kv.get<T>(prefix + key),\n\t\tset: (key: string, value: unknown) => kv.set(prefix + key, value),\n\t\tdel: (key: string) => kv.del(prefix + key)\n\t};\n}\n\nfunction replicaFileName(backupName: string): string {\n\tconst dot = backupName.lastIndexOf('.');\n\treturn dot > 0\n\t\t? `${backupName.slice(0, dot)}.replica${backupName.slice(dot)}`\n\t\t: `${backupName}.replica`;\n}\n\nexport function replicaFlow(\n\tstore: FlowHost | { flowHost: FlowHost },\n\ttargets: ConnectTargets,\n\toptions: ReplicaFlowOptions = {}\n): ReplicaFlow {\n\tconst host = 'flowHost' in store ? store.flowHost : store;\n\tconst engine = host.engine;\n\tconst skv = scopedKv(host.kv, SCOPE);\n\tconst fileName = options.fileName ?? replicaFileName(host.backupName);\n\tconst kinds = (Object.keys(targets) as ConnectKind[]).filter(\n\t\t(k) => (k === 'drive' || k === 'file' || k === 'webdav' || k === 's3') && targets[k] != null\n\t);\n\n\tconst current = (): ReplicaState | null =>\n\t\tengine.state.replicas.find((r) => r.id === REPLICA_ID) ?? null;\n\n\tconst m = makeMachine<ReplicaSnapshot>({\n\t\tstep: 'idle',\n\t\tkinds,\n\t\tbusy: false,\n\t\terror: null,\n\t\treplica: current()\n\t});\n\n\t// Follow the engine: every publish (or publish failure) refreshes the entry.\n\tconst unsub = engine.subscribe(() => m.set({ replica: current() }));\n\n\t// Held for remove(): disconnect() lets the target forget its own session.\n\tlet attached: BackupTarget | null = null;\n\n\tfunction connectorFor(kind: ConnectKind): Connector | null {\n\t\tconst spec = targets[kind];\n\t\tif (spec == null) return null;\n\t\tif (typeof spec === 'function') return spec;\n\t\tif (kind === 'drive') {\n\t\t\tconst auth = spec as DriveAuth;\n\t\t\treturn () => driveConnect({ auth, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') {\n\t\t\treturn () => fileConnect({ kv: skv, fileName });\n\t\t}\n\t\tif (kind === 's3') {\n\t\t\tconst config = spec as S3Config; // 's3: true' goes through the form step\n\t\t\treturn () => s3Connect({ kv: skv, config });\n\t\t}\n\t\tconst config = spec as WebdavConfig; // 'webdav: true' goes through the form step\n\t\treturn () => webdavConnect({ kv: skv, config });\n\t}\n\n\tasync function run(kind: ConnectKind, connector: Connector): Promise<void> {\n\t\tm.set({ busy: true, error: null });\n\t\ttry {\n\t\t\tconst target = await connector();\n\t\t\tif (!target) {\n\t\t\t\t// Cancelled picker/consent: stay where the user was, no error.\n\t\t\t\tm.set({ busy: false });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID); // swap, never a dup-id throw\n\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\tattached = target;\n\t\t\tawait skv.set(RECORD_KEY, { kind } satisfies ReplicaRecord);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t} catch (e) {\n\t\t\tm.set({ busy: false, error: toStoreError(e) });\n\t\t}\n\t}\n\n\tasync function builtinFromSession(kind: ConnectKind): Promise<BackupTarget | null> {\n\t\tif (kind === 'drive') {\n\t\t\tconst spec = targets.drive;\n\t\t\tif (spec == null || typeof spec === 'function') return null;\n\t\t\treturn driveFromSession({ auth: spec, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') return fileFromSession({ kv: skv });\n\t\tif (kind === 'webdav') return webdavFromSession({ kv: skv });\n\t\treturn s3FromSession({ kv: skv });\n\t}\n\n\treturn {\n\t\tget snapshot() {\n\t\t\treturn m.snapshot;\n\t\t},\n\t\tsubscribe: m.subscribe,\n\n\t\topen() {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tm.set({ step: 'choose', error: null });\n\t\t},\n\n\t\tcancel() {\n\t\t\tm.set({ step: 'idle', busy: false, error: null });\n\t\t},\n\n\t\tpick(kind) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tconst spec = targets[kind];\n\t\t\tif (spec == null) return;\n\t\t\tif (kind === 'webdav' && spec === true) {\n\t\t\t\tm.set({ step: 'form-webdav', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kind === 's3' && spec === true) {\n\t\t\t\tm.set({ step: 'form-s3', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst connector = connectorFor(kind);\n\t\t\tif (connector) void run(kind, connector);\n\t\t},\n\n\t\tsubmitWebdav(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('webdav', () => webdavConnect({ kv: skv, config }));\n\t\t},\n\n\t\tsubmitS3(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('s3', () => s3Connect({ kv: skv, config }));\n\t\t},\n\n\t\tasync remove() {\n\t\t\tconst target = attached;\n\t\t\tattached = null;\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID);\n\t\t\tawait skv.del(RECORD_KEY);\n\t\t\t// Forget locally only - a BackupTarget's disconnect never deletes\n\t\t\t// remote data, so the copy stays where it is for a manual recovery.\n\t\t\tawait target?.disconnect().catch(() => undefined);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t},\n\n\t\tasync restore() {\n\t\t\ttry {\n\t\t\t\tif (current()) return;\n\t\t\t\tconst record = await skv.get<ReplicaRecord>(RECORD_KEY);\n\t\t\t\tif (!record) return;\n\t\t\t\tconst target =\n\t\t\t\t\t(await options.restoreTarget?.(record.kind)) ?? (await builtinFromSession(record.kind));\n\t\t\t\tif (!target) return; // unreachable or custom kind: retry next boot\n\t\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\t\tattached = target;\n\t\t\t\tm.set({ replica: current() });\n\t\t\t} catch {\n\t\t\t\t// Silent reboot by contract: the record stays, the next boot retries.\n\t\t\t}\n\t\t},\n\n\t\tdispose() {\n\t\t\tunsub();\n\t\t}\n\t};\n}\n"]}
1
+ {"version":3,"sources":["../src/flows/replica.ts"],"names":["connect","config","fromSession"],"mappings":"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","file":"flows.js","sourcesContent":["// The backup-copy journey: attach ONE replica (a second destination receiving\n// the same encrypted backup as the primary home) behind a small state machine\n// the widgets can render. Reuses the connect flow's target specs, but there is\n// no password step, no conflict step and no deadline: a replica carries the\n// primary's bytes as-is, and every connector may contain a human gesture.\n\nimport type { BackupTarget } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { ReplicaState, StoreError } from '../persistence/store';\nimport { makeMachine, toStoreError, type FlowStore } from './machine';\nimport type { ConnectKind, ConnectTargets, Connector, StoreLike } from './connect';\nimport {\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\ttype DriveAuth\n} from '../persistence/targets/drive';\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\ttype WebdavConfig\n} from '../persistence/targets/webdav';\nimport {\n\tconnect as s3Connect,\n\tfromSession as s3FromSession,\n\ttype S3Config\n} from '../persistence/targets/s3';\n\n/** The one replica this flow manages, as its id on the engine. */\nexport const REPLICA_ID = 'replica';\n\n// The replica's target session lives in the SAME kv as the primary's, so every\n// key is prefixed: a second driveTarget (say) must never read or clobber the\n// primary connection's session keys.\nconst SCOPE = 'replica:';\nconst RECORD_KEY = 'record';\n\n/** What survives a reboot: which provider the copy lives on. The target's own\n * session (file id, handle, server config) is persisted by the target module\n * itself, under the scoped kv. */\ninterface ReplicaRecord {\n\tkind: ConnectKind;\n}\n\nexport type ReplicaStep = 'idle' | 'choose' | 'form-webdav' | 'form-s3';\n\nexport interface ReplicaSnapshot {\n\tstep: ReplicaStep;\n\t/** The kinds the host offers, in ConnectTargets order. */\n\tkinds: ConnectKind[];\n\tbusy: boolean;\n\terror: StoreError | null;\n\t/** The live engine entry (label, lastPublishAt, lastError), null when none. */\n\treplica: ReplicaState | null;\n}\n\nexport interface ReplicaFlowOptions {\n\t/** File name of the copy on its destination. Default: the primary backup\n\t * name with `.replica` before the extension (\"App.zip\" -> \"App.replica.zip\"),\n\t * which a same-account Drive never confuses with the primary file and the\n\t * backups panel never lists as a backup of its own. */\n\tfileName?: string;\n\t/** Rebuild the target of a custom Connector kind on restore() (built-in\n\t * kinds rebuild by themselves). Without it, custom kinds are not restored. */\n\trestoreTarget?: (kind: ConnectKind) => Promise<BackupTarget | null>;\n}\n\nexport interface ReplicaFlow extends FlowStore<ReplicaSnapshot> {\n\t/** Show the destination choice (from the user's gesture). */\n\topen(): void;\n\t/** Back to idle; never an error. */\n\tcancel(): void;\n\t/** Run a destination (inside the user's click - pickers and consent popups\n\t * open from it). `webdav: true` / `s3: true` route to their form step. */\n\tpick(kind: ConnectKind): void;\n\t/** 'form-webdav' step: submit the server config. */\n\tsubmitWebdav(config: WebdavConfig): void;\n\t/** 'form-s3' step: submit the bucket config. */\n\tsubmitS3(config: S3Config): void;\n\t/** Detach the copy and forget its session. Never deletes remote data. */\n\tremove(): Promise<void>;\n\t/** Silent reboot: re-attach the copy recorded by a past session, if any.\n\t * Never throws - an unreachable destination retries at the next boot. */\n\trestore(): Promise<void>;\n\t/** Detach from the engine (the replica itself stays attached). */\n\tdispose(): void;\n}\n\nfunction scopedKv(kv: KV, prefix: string): KV {\n\treturn {\n\t\tget: <T = unknown>(key: string) => kv.get<T>(prefix + key),\n\t\tset: (key: string, value: unknown) => kv.set(prefix + key, value),\n\t\tdel: (key: string) => kv.del(prefix + key)\n\t};\n}\n\nfunction replicaFileName(backupName: string): string {\n\tconst dot = backupName.lastIndexOf('.');\n\treturn dot > 0\n\t\t? `${backupName.slice(0, dot)}.replica${backupName.slice(dot)}`\n\t\t: `${backupName}.replica`;\n}\n\nexport function replicaFlow(\n\tstore: StoreLike,\n\ttargets: ConnectTargets,\n\toptions: ReplicaFlowOptions = {}\n): ReplicaFlow {\n\tconst host = 'flowHost' in store ? store.flowHost : store;\n\tconst engine = host.engine;\n\tconst skv = scopedKv(host.kv, SCOPE);\n\tconst fileName = options.fileName ?? replicaFileName(host.backupName);\n\tconst kinds = (Object.keys(targets) as ConnectKind[]).filter(\n\t\t(k) => (k === 'drive' || k === 'file' || k === 'webdav' || k === 's3') && targets[k] != null\n\t);\n\n\tconst current = (): ReplicaState | null =>\n\t\tengine.state.replicas.find((r) => r.id === REPLICA_ID) ?? null;\n\n\tconst m = makeMachine<ReplicaSnapshot>({\n\t\tstep: 'idle',\n\t\tkinds,\n\t\tbusy: false,\n\t\terror: null,\n\t\treplica: current()\n\t});\n\n\t// Follow the engine: every publish (or publish failure) refreshes the entry.\n\tconst unsub = engine.subscribe(() => m.set({ replica: current() }));\n\n\t// Held for remove(): disconnect() lets the target forget its own session.\n\tlet attached: BackupTarget | null = null;\n\n\tfunction connectorFor(kind: ConnectKind): Connector | null {\n\t\tconst spec = targets[kind];\n\t\tif (spec == null) return null;\n\t\tif (typeof spec === 'function') return spec;\n\t\tif (kind === 'drive') {\n\t\t\tconst auth = spec as DriveAuth;\n\t\t\treturn () => driveConnect({ auth, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') {\n\t\t\treturn () => fileConnect({ kv: skv, fileName });\n\t\t}\n\t\tif (kind === 's3') {\n\t\t\tconst config = spec as S3Config; // 's3: true' goes through the form step\n\t\t\treturn () => s3Connect({ kv: skv, config });\n\t\t}\n\t\tconst config = spec as WebdavConfig; // 'webdav: true' goes through the form step\n\t\treturn () => webdavConnect({ kv: skv, config });\n\t}\n\n\tasync function run(kind: ConnectKind, connector: Connector): Promise<void> {\n\t\tm.set({ busy: true, error: null });\n\t\ttry {\n\t\t\tconst target = await connector();\n\t\t\tif (!target) {\n\t\t\t\t// Cancelled picker/consent: stay where the user was, no error.\n\t\t\t\tm.set({ busy: false });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID); // swap, never a dup-id throw\n\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\tattached = target;\n\t\t\tawait skv.set(RECORD_KEY, { kind } satisfies ReplicaRecord);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t} catch (e) {\n\t\t\tm.set({ busy: false, error: toStoreError(e) });\n\t\t}\n\t}\n\n\tasync function builtinFromSession(kind: ConnectKind): Promise<BackupTarget | null> {\n\t\tif (kind === 'drive') {\n\t\t\tconst spec = targets.drive;\n\t\t\tif (spec == null || typeof spec === 'function') return null;\n\t\t\treturn driveFromSession({ auth: spec, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') return fileFromSession({ kv: skv });\n\t\tif (kind === 'webdav') return webdavFromSession({ kv: skv });\n\t\treturn s3FromSession({ kv: skv });\n\t}\n\n\treturn {\n\t\tget snapshot() {\n\t\t\treturn m.snapshot;\n\t\t},\n\t\tsubscribe: m.subscribe,\n\n\t\topen() {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tm.set({ step: 'choose', error: null });\n\t\t},\n\n\t\tcancel() {\n\t\t\tm.set({ step: 'idle', busy: false, error: null });\n\t\t},\n\n\t\tpick(kind) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tconst spec = targets[kind];\n\t\t\tif (spec == null) return;\n\t\t\tif (kind === 'webdav' && spec === true) {\n\t\t\t\tm.set({ step: 'form-webdav', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kind === 's3' && spec === true) {\n\t\t\t\tm.set({ step: 'form-s3', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst connector = connectorFor(kind);\n\t\t\tif (connector) void run(kind, connector);\n\t\t},\n\n\t\tsubmitWebdav(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('webdav', () => webdavConnect({ kv: skv, config }));\n\t\t},\n\n\t\tsubmitS3(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('s3', () => s3Connect({ kv: skv, config }));\n\t\t},\n\n\t\tasync remove() {\n\t\t\tconst target = attached;\n\t\t\tattached = null;\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID);\n\t\t\tawait skv.del(RECORD_KEY);\n\t\t\t// Forget locally only - a BackupTarget's disconnect never deletes\n\t\t\t// remote data, so the copy stays where it is for a manual recovery.\n\t\t\tawait target?.disconnect().catch(() => undefined);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t},\n\n\t\tasync restore() {\n\t\t\ttry {\n\t\t\t\tif (current()) return;\n\t\t\t\tconst record = await skv.get<ReplicaRecord>(RECORD_KEY);\n\t\t\t\tif (!record) return;\n\t\t\t\tconst target =\n\t\t\t\t\t(await options.restoreTarget?.(record.kind)) ?? (await builtinFromSession(record.kind));\n\t\t\t\tif (!target) return; // unreachable or custom kind: retry next boot\n\t\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\t\tattached = target;\n\t\t\t\tm.set({ replica: current() });\n\t\t\t} catch {\n\t\t\t\t// Silent reboot by contract: the record stays, the next boot retries.\n\t\t\t}\n\t\t},\n\n\t\tdispose() {\n\t\t\tunsub();\n\t\t}\n\t};\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { d as Mode, P as PasswordCheck, f as PasswordRequirement, S as Selfstore
3
3
  import { L as LocalCache, K as KV } from './cache-ilsK3Tki.js';
4
4
  import { SyncConfig } from './sync.js';
5
5
  import { D as DriveAuth, W as WebdavConfig, S as S3Config } from './drive-BhiTdk-Q.js';
6
- export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-6OALceUy.js';
6
+ export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-C9mTIwSf.js';
7
7
 
8
8
  declare const BACKUP_EXTENSION = ".zip";
9
9
  declare const BACKUP_MIME = "application/zip";
@@ -29,8 +29,10 @@ declare class BackupBuilder {
29
29
  /** The backup as a Blob, ready to upload or hand to a save dialog. */
30
30
  toBlob(): Promise<Blob>;
31
31
  /** Save the backup to disk (File System Access API, else a download).
32
- * Browser-only terminal; defaults to `<app>-<date>.zip`. */
33
- toDisk(filename?: string): Promise<void>;
32
+ * Browser-only terminal; defaults to `<app>-<date>.zip`. False when the
33
+ * user closed the save dialog: nothing was written, so do not tell them
34
+ * they have a backup. */
35
+ toDisk(filename?: string): Promise<boolean>;
34
36
  }
35
37
  declare class EncryptedBackupBuilder extends BackupBuilder {
36
38
  constructor(snapshot: Snapshot, opts: EncodeOptions);
@@ -161,8 +163,10 @@ interface SimpleStore<S extends Record<string, SimpleRecord> = Record<string, Si
161
163
  reconnect(): Promise<boolean>;
162
164
  /** The portable backup file (a real ZIP; encrypted when protect() is on). */
163
165
  exportBackup(): Promise<Blob>;
164
- /** Download the backup (also resolves the 'manual' file mode's pending flag). */
165
- downloadBackup(filename?: string): Promise<void>;
166
+ /** Download the backup (also resolves the 'manual' file mode's pending flag).
167
+ * False when the user closed the save dialog: nothing was written, the
168
+ * pending flag stands, and the app must not record a backup. */
169
+ downloadBackup(filename?: string): Promise<boolean>;
166
170
  /** Load a backup file into this store, replacing the local data (removals
167
171
  * propagate like edits). Throws PASSWORD_REQUIRED / DECRYPT_FAILED. */
168
172
  importBackup(file: Blob | Uint8Array, opts?: {
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-FL66ACWR.js';
2
- export { gisDriveAuth } from './chunk-FL66ACWR.js';
3
- import { saveToDisk } from './chunk-UWVAKBU4.js';
4
- export { pickFromDisk, saveToDisk } from './chunk-UWVAKBU4.js';
1
+ import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-KSV2F3ZN.js';
2
+ export { gisDriveAuth } from './chunk-KSV2F3ZN.js';
3
+ import { saveToDisk } from './chunk-RI4C6PRJ.js';
4
+ export { pickFromDisk, saveToDisk } from './chunk-RI4C6PRJ.js';
5
5
  import './chunk-SKWHKM25.js';
6
6
  import { fromSession, fromSession2, fromSession3, connect4, connect3, isSupported, connect2, connect } from './chunk-TEIAFKFS.js';
7
- import { restore } from './chunk-SRKUHFRG.js';
8
- export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-SRKUHFRG.js';
7
+ import { restore } from './chunk-GJRHMI2O.js';
8
+ export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-GJRHMI2O.js';
9
9
  import './chunk-KHER6MWR.js';
10
10
  import './chunk-QBCU7OOP.js';
11
11
  import './chunk-6746AL4I.js';
@@ -211,8 +211,9 @@ async function selfstore(app, options = {}) {
211
211
  reconnect: () => store.reconnect(),
212
212
  exportBackup: () => store.exportBlob(),
213
213
  async downloadBackup(filename) {
214
- await saveToDisk(await store.exportBlob(), filename ?? backupName);
215
- store.markDownloaded();
214
+ const written = await saveToDisk(await store.exportBlob(), filename ?? backupName);
215
+ if (written) store.markDownloaded();
216
+ return written;
216
217
  },
217
218
  async importBackup(file, opts) {
218
219
  const reader = restore(file);