selfstore 1.3.0 → 1.5.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/README.md +7 -1
- package/dist/advanced.d.ts +7 -7
- package/dist/advanced.js +1 -1
- package/dist/backups.d.ts +2 -2
- package/dist/{cache-De5_sALA.d.ts → cache-ilsK3Tki.d.ts} +11 -0
- package/dist/{chunk-XI3MGK4M.js → chunk-FL66ACWR.js} +36 -2
- package/dist/chunk-FL66ACWR.js.map +1 -0
- package/dist/{chunk-3XR3GLR2.js → chunk-I243KZMZ.js} +2 -2
- package/dist/chunk-I243KZMZ.js.map +1 -0
- package/dist/{drive-DNiDXBDe.d.ts → drive-BhiTdk-Q.d.ts} +2 -2
- package/dist/flows-BBMVdP1S.d.ts +325 -0
- package/dist/flows.d.ts +5 -325
- package/dist/flows.js +1 -1
- package/dist/flows.js.map +1 -1
- package/dist/groups.d.ts +2 -2
- package/dist/households.d.ts +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{local-DsXG7QW7.d.ts → local-6OALceUy.d.ts} +2 -2
- package/dist/{store-2RnED1Vq.d.ts → store-C20_6JBo.d.ts} +1 -1
- package/dist/widgets.d.ts +106 -20
- package/dist/widgets.js +299 -2
- package/dist/widgets.js.map +1 -1
- package/llms.txt +8 -3
- package/package.json +2 -1
- package/dist/chunk-3XR3GLR2.js.map +0 -1
- package/dist/chunk-XI3MGK4M.js.map +0 -1
package/dist/flows.d.ts
CHANGED
|
@@ -1,326 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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,4 +1,4 @@
|
|
|
1
|
-
export { connectFlow, joinFlow, shareFlow } from './chunk-
|
|
1
|
+
export { connectFlow, joinFlow, shareFlow } from './chunk-I243KZMZ.js';
|
|
2
2
|
import { connect, connect2, connect4, connect3, fromSession, fromSession2, fromSession3, fromSession4 } from './chunk-TEIAFKFS.js';
|
|
3
3
|
import './chunk-SRKUHFRG.js';
|
|
4
4
|
import { makeMachine, toStoreError } from './chunk-43BJDRPT.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/groups.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { G as GroupIdentity, K as KV } from './cache-
|
|
2
|
-
export { a as GROUP_KEYING, b as GroupManifest, c as GroupMember, S as SignedManifest, g as generateIdentity, d as groupCryptoAvailable, k as keyId, n as newGroupId, o as openManifest, p as publicIdentity, s as signManifest } from './cache-
|
|
1
|
+
import { G as GroupIdentity, K as KV } from './cache-ilsK3Tki.js';
|
|
2
|
+
export { a as GROUP_KEYING, b as GroupManifest, c as GroupMember, S as SignedManifest, g as generateIdentity, d as groupCryptoAvailable, k as keyId, n as newGroupId, o as openManifest, p as publicIdentity, s as signManifest } from './cache-ilsK3Tki.js';
|
|
3
3
|
|
|
4
4
|
interface IdentityVault {
|
|
5
5
|
/** The stored identity, or null when none exists yet. On a protected
|
package/dist/households.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as BackupTarget, g as PeerSource, L as LocalStore } from './store-
|
|
2
|
-
import { K as KV } from './cache-
|
|
1
|
+
import { B as BackupTarget, g as PeerSource, L as LocalStore } from './store-C20_6JBo.js';
|
|
2
|
+
import { K as KV } from './cache-ilsK3Tki.js';
|
|
3
3
|
import './sync.js';
|
|
4
4
|
|
|
5
5
|
/** Why a code was refused. `malformed` covers bad transport, wrong structure or
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { n as Snapshot, E as EncodeOptions, H as Header, e as PasswordPolicy, B as BackupTarget, q as StatusDescriptor, t as StoreError, c as LocalStoreState, L as LocalStore } from './store-
|
|
2
|
-
export { d as Mode, P as PasswordCheck, f as PasswordRequirement, S as SelfstoreError, l as SelfstoreErrorCode, m as Severity, o as SnapshotFile, p as StatusAction, s as StorageState, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-
|
|
3
|
-
import { L as LocalCache, K as KV } from './cache-
|
|
1
|
+
import { n as Snapshot, E as EncodeOptions, H as Header, e as PasswordPolicy, B as BackupTarget, q as StatusDescriptor, t as StoreError, c as LocalStoreState, L as LocalStore } from './store-C20_6JBo.js';
|
|
2
|
+
export { d as Mode, P as PasswordCheck, f as PasswordRequirement, S as SelfstoreError, l as SelfstoreErrorCode, m as Severity, o as SnapshotFile, p as StatusAction, s as StorageState, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-C20_6JBo.js';
|
|
3
|
+
import { L as LocalCache, K as KV } from './cache-ilsK3Tki.js';
|
|
4
4
|
import { SyncConfig } from './sync.js';
|
|
5
|
-
import { D as DriveAuth, W as WebdavConfig, S as S3Config } from './drive-
|
|
6
|
-
export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-
|
|
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';
|
|
7
7
|
|
|
8
8
|
declare const BACKUP_EXTENSION = ".zip";
|
|
9
9
|
declare const BACKUP_MIME = "application/zip";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-
|
|
2
|
-
export { gisDriveAuth } from './chunk-
|
|
1
|
+
import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-FL66ACWR.js';
|
|
2
|
+
export { gisDriveAuth } from './chunk-FL66ACWR.js';
|
|
3
3
|
import { saveToDisk } from './chunk-UWVAKBU4.js';
|
|
4
4
|
export { pickFromDisk, saveToDisk } from './chunk-UWVAKBU4.js';
|
|
5
5
|
import './chunk-SKWHKM25.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Snapshot, E as EncodeOptions, H as Header } from './store-
|
|
2
|
-
import { D as DriveAuth } from './drive-
|
|
1
|
+
import { n as Snapshot, E as EncodeOptions, H as Header } from './store-C20_6JBo.js';
|
|
2
|
+
import { D as DriveAuth } from './drive-BhiTdk-Q.js';
|
|
3
3
|
|
|
4
4
|
/** Serialize a snapshot to a portable backup blob (compressed; encrypted if a password is given). */
|
|
5
5
|
declare function exportSnapshot(snapshot: Snapshot, opts: EncodeOptions): Promise<Blob>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as GroupIdentity, S as SignedManifest, L as LocalCache } from './cache-
|
|
1
|
+
import { G as GroupIdentity, S as SignedManifest, L as LocalCache } from './cache-ilsK3Tki.js';
|
|
2
2
|
import { CollectionChanges, Conflict, SyncConfig } from './sync.js';
|
|
3
3
|
|
|
4
4
|
/** A binary attachment (image, PDF, any bytes). */
|
package/dist/widgets.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as StoreLike, C as ConnectTargets, a as ConnectFlowOptions, b as ConnectKind, c as ConnectFlow, d as ShareEngine, e as ShareLevel, f as ShareFlow, J as JoinEngine, g as JoinFlow, R as ReplicaFlow } from './flows-BBMVdP1S.js';
|
|
2
2
|
import { BackupsManager } from './backups.js';
|
|
3
|
-
import './store-
|
|
4
|
-
import './cache-
|
|
3
|
+
import './store-C20_6JBo.js';
|
|
4
|
+
import './cache-ilsK3Tki.js';
|
|
5
5
|
import './sync.js';
|
|
6
|
-
import './drive-
|
|
6
|
+
import './drive-BhiTdk-Q.js';
|
|
7
7
|
import './machine-ALMkT3mk.js';
|
|
8
8
|
|
|
9
9
|
/** Flat key -> string map; widgets merge a partial override over EN defaults. */
|
|
@@ -84,12 +84,8 @@ declare class SelfstoreConnectElement extends FlowWidget {
|
|
|
84
84
|
static get observedAttributes(): string[];
|
|
85
85
|
protected defaults(): WidgetLabels;
|
|
86
86
|
/** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
|
|
87
|
-
get store():
|
|
88
|
-
|
|
89
|
-
} | null;
|
|
90
|
-
set store(v: FlowHost | {
|
|
91
|
-
flowHost: FlowHost;
|
|
92
|
-
} | null);
|
|
87
|
+
get store(): StoreLike | null;
|
|
88
|
+
set store(v: StoreLike | null);
|
|
93
89
|
get targets(): ConnectTargets | null;
|
|
94
90
|
set targets(v: ConnectTargets | null);
|
|
95
91
|
get options(): ConnectFlowOptions;
|
|
@@ -264,12 +260,8 @@ declare class SelfstoreStatusElement extends FlowWidget {
|
|
|
264
260
|
static get observedAttributes(): string[];
|
|
265
261
|
protected defaults(): WidgetLabels;
|
|
266
262
|
/** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
|
|
267
|
-
get store():
|
|
268
|
-
|
|
269
|
-
} | null;
|
|
270
|
-
set store(v: FlowHost | {
|
|
271
|
-
flowHost: FlowHost;
|
|
272
|
-
} | null);
|
|
263
|
+
get store(): StoreLike | null;
|
|
264
|
+
set store(v: StoreLike | null);
|
|
273
265
|
/** 'row' (dot + text + action) or 'dot' (the dot alone). Also the attribute. */
|
|
274
266
|
get variant(): 'row' | 'dot';
|
|
275
267
|
set variant(v: 'row' | 'dot' | null);
|
|
@@ -417,10 +409,104 @@ declare class SelfstoreBackupsElement extends FlowWidget {
|
|
|
417
409
|
private closePw;
|
|
418
410
|
}
|
|
419
411
|
|
|
412
|
+
/**
|
|
413
|
+
* <selfstore-gate>: the first-run screen that asks where the data should live,
|
|
414
|
+
* before the app has a durable home. Data that lives only in a browser profile
|
|
415
|
+
* dies with it, so the question is worth a screen of its own rather than a line
|
|
416
|
+
* buried in a settings pane.
|
|
417
|
+
*
|
|
418
|
+
* It is a frame around <selfstore-connect>, and it decides ONE thing the plain
|
|
419
|
+
* connect widget cannot: whether to be on screen at all. That condition is the
|
|
420
|
+
* engine's own `status.action === 'choose-destination'`, which covers both an
|
|
421
|
+
* ephemeral store and one still on the device-only cache, and which ranks a
|
|
422
|
+
* destination needing attention above a missing one - so the gate never demands
|
|
423
|
+
* a choice when the real problem is a broken connection.
|
|
424
|
+
*
|
|
425
|
+
* import { defineSelfstoreWidgets } from 'selfstore/widgets';
|
|
426
|
+
* defineSelfstoreWidgets();
|
|
427
|
+
*
|
|
428
|
+
* const el = document.querySelector('selfstore-gate');
|
|
429
|
+
* el.store = store;
|
|
430
|
+
* el.targets = { file: true, webdav: true };
|
|
431
|
+
*
|
|
432
|
+
* The host's own chrome goes in the light DOM through three slots: `brand`
|
|
433
|
+
* (above the title), `extra` (under the destinations, e.g. a link to a demo)
|
|
434
|
+
* and `footer` (the app's usual foot). Slotted nodes are never rebuilt.
|
|
435
|
+
*/
|
|
436
|
+
|
|
437
|
+
declare class SelfstoreGateElement extends FlowWidget {
|
|
438
|
+
#private;
|
|
439
|
+
static get observedAttributes(): string[];
|
|
440
|
+
protected defaults(): WidgetLabels;
|
|
441
|
+
/** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
|
|
442
|
+
get store(): StoreLike | null;
|
|
443
|
+
set store(v: StoreLike | null);
|
|
444
|
+
/** The destinations to offer, exactly as <selfstore-connect> takes them.
|
|
445
|
+
* Read when the gate opens; a change while it is open waits for the next
|
|
446
|
+
* opening rather than rebuilding a journey under the user. */
|
|
447
|
+
get targets(): ConnectTargets | null;
|
|
448
|
+
set targets(v: ConnectTargets | null);
|
|
449
|
+
/** Options for the connect flow (`defaultResolution`, `password`, ...).
|
|
450
|
+
* Same timing as `targets`. */
|
|
451
|
+
get options(): ConnectFlowOptions;
|
|
452
|
+
set options(v: ConnectFlowOptions);
|
|
453
|
+
get icons(): Partial<Record<ConnectKind, string>>;
|
|
454
|
+
set icons(v: Partial<Record<ConnectKind, string>> | null);
|
|
455
|
+
get recommended(): ConnectKind | null;
|
|
456
|
+
set recommended(v: ConnectKind | null);
|
|
457
|
+
get advanced(): ConnectKind[];
|
|
458
|
+
set advanced(v: ConnectKind[] | null);
|
|
459
|
+
get webdavPresets(): WebdavPreset[];
|
|
460
|
+
set webdavPresets(v: WebdavPreset[] | null);
|
|
461
|
+
/** Whether the app has finished booting. Leave it true for a plain drop-in;
|
|
462
|
+
* an app that restores its destination asynchronously sets it false until
|
|
463
|
+
* that settles, so a connected user never glimpses the gate on the way in.
|
|
464
|
+
* Also the `armed` attribute (`armed="false"` to hold it shut). */
|
|
465
|
+
get armed(): boolean;
|
|
466
|
+
set armed(v: boolean);
|
|
467
|
+
/** Whether to offer the way out (device-only is a working mode, just a
|
|
468
|
+
* fragile one). Also the `deferrable` attribute. Part of the frame, so it
|
|
469
|
+
* follows the same timing as `targets`: read when the gate opens. */
|
|
470
|
+
get deferrable(): boolean;
|
|
471
|
+
set deferrable(v: boolean);
|
|
472
|
+
/** Set once the user chose to stay device-only; keeps the gate shut for the
|
|
473
|
+
* rest of the session. Assign false to bring it back (a "choose a
|
|
474
|
+
* destination" entry somewhere else in the app). */
|
|
475
|
+
get deferred(): boolean;
|
|
476
|
+
set deferred(v: boolean);
|
|
477
|
+
/** Whether the gate is currently on screen. */
|
|
478
|
+
get open(): boolean;
|
|
479
|
+
/** The connect element the gate built, for programmatic control (its own
|
|
480
|
+
* `flow` included). Null while the gate is shut. */
|
|
481
|
+
get connect(): SelfstoreConnectElement | null;
|
|
482
|
+
constructor();
|
|
483
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
484
|
+
connectedCallback(): void;
|
|
485
|
+
private host;
|
|
486
|
+
private wire;
|
|
487
|
+
/** The library already derives "no durable home yet" - asking the target
|
|
488
|
+
* kind by hand would miss the ranking and nag over a broken connection. */
|
|
489
|
+
private shouldOpen;
|
|
490
|
+
private sync;
|
|
491
|
+
/** Rebuilding the frame would re-append the child connect element, and a
|
|
492
|
+
* custom element that leaves the DOM and comes back rebuilds its flow: a
|
|
493
|
+
* journey in progress would be dropped on the floor. So while the gate is
|
|
494
|
+
* open the frame stands and late cosmetics are pushed into the child.
|
|
495
|
+
* Set `labels` before `store` for them to reach the frame's own copy. */
|
|
496
|
+
protected rerender(): void;
|
|
497
|
+
/** The gate's title already asks the question; the child's heading would
|
|
498
|
+
* repeat it. An empty label removes a heading, and a host that wants it
|
|
499
|
+
* back just says so in its own map. */
|
|
500
|
+
private childLabels;
|
|
501
|
+
private buildConnect;
|
|
502
|
+
private deferButton;
|
|
503
|
+
protected view(into: HTMLElement): void;
|
|
504
|
+
}
|
|
505
|
+
|
|
420
506
|
/** Register the elements as <PREFIX-connect>, <PREFIX-share>, <PREFIX-join>,
|
|
421
|
-
* <PREFIX-status> and <PREFIX-
|
|
422
|
-
* call twice; throws in environments without custom
|
|
423
|
-
* only). */
|
|
507
|
+
* <PREFIX-status>, <PREFIX-backups> and <PREFIX-gate> (default prefix
|
|
508
|
+
* 'selfstore'). Safe to call twice; throws in environments without custom
|
|
509
|
+
* elements (browser code only). */
|
|
424
510
|
declare function defineSelfstoreWidgets(prefix?: string): void;
|
|
425
511
|
|
|
426
|
-
export { type BackupsAction, FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, type ShareAction, type WebdavPreset, type WidgetLabels, defineSelfstoreWidgets };
|
|
512
|
+
export { type BackupsAction, FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, type ShareAction, type WebdavPreset, type WidgetLabels, defineSelfstoreWidgets };
|