selfstore 1.8.9 → 1.8.11

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.
@@ -0,0 +1,394 @@
1
+ import { makeMachine } from './chunk-W22V5CRM.js';
2
+ import { FILE_ID_KEY, findOrCreateOwnFile, renameBackup, deleteBackup, createBackup, preview, listBackups } from './chunk-J5MFXJU6.js';
3
+
4
+ // src/backups/manager.ts
5
+ var DEFAULT_KEYS = {
6
+ registry: "selfstore:backups:registry:v1",
7
+ encrypted: "selfstore:backups:encrypted:v1",
8
+ shared: "selfstore:backups:shared:v1",
9
+ joined: "selfstore:backups:joined:v1",
10
+ owner: "selfstore:backups:owner:v1"
11
+ };
12
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ function defaultNaming(naming) {
14
+ const dot = naming.canonicalName.lastIndexOf(".");
15
+ const stem = dot > 0 ? naming.canonicalName.slice(0, dot) : naming.canonicalName;
16
+ const ext = dot > 0 ? naming.canonicalName.slice(dot) : "";
17
+ const named = new RegExp(`^${escapeRe(stem)} \\((.+)\\)${escapeRe(ext)}$`);
18
+ return {
19
+ namedFileFor: (label) => `${stem} (${label.trim()})${ext}`,
20
+ parseLabel: (name) => {
21
+ if (name === naming.canonicalName) return null;
22
+ const m = named.exec(name);
23
+ return m ? m[1] : void 0;
24
+ }
25
+ };
26
+ }
27
+ function createBackupsManager(deps) {
28
+ const { store, kv, host } = deps;
29
+ const KEYS = { ...DEFAULT_KEYS, ...deps.keys };
30
+ const namingDefaults = defaultNaming(deps.naming);
31
+ const naming = {
32
+ namedFileFor: deps.naming.namedFileFor ?? namingDefaults.namedFileFor,
33
+ parseLabel: deps.naming.parseLabel ?? namingDefaults.parseLabel
34
+ };
35
+ const m = makeMachine({
36
+ registry: { personalFileId: null, shared: [] },
37
+ activeFileId: null,
38
+ joined: false,
39
+ owner: null,
40
+ lastError: null
41
+ });
42
+ const fail = (code) => m.set({ lastError: code });
43
+ let ownIds = null;
44
+ let ownCanonicalId = null;
45
+ function learnOwn(files) {
46
+ const ids = /* @__PURE__ */ new Set();
47
+ let canonical = null;
48
+ for (const f of files) {
49
+ const label = naming.parseLabel(f.name);
50
+ if (label === void 0) continue;
51
+ ids.add(f.id);
52
+ if (label === null) canonical = f.id;
53
+ }
54
+ ownIds = ids;
55
+ ownCanonicalId = canonical;
56
+ }
57
+ async function ensureOwnKnown() {
58
+ if (ownIds !== null) return;
59
+ try {
60
+ learnOwn(await host.list());
61
+ } catch {
62
+ }
63
+ }
64
+ async function loadRegistry() {
65
+ const reg = await kv.get(KEYS.registry) ?? { personalFileId: null, shared: [] };
66
+ const excluded = await host.excludedFileIds?.() ?? [];
67
+ const bad = new Set(excluded.filter((x) => typeof x === "string"));
68
+ if (bad.size === 0) return reg;
69
+ const healed = {
70
+ personalFileId: reg.personalFileId && bad.has(reg.personalFileId) ? null : reg.personalFileId,
71
+ shared: reg.shared.filter((s) => !bad.has(s.fileId))
72
+ };
73
+ if (healed.personalFileId !== reg.personalFileId || healed.shared.length !== reg.shared.length) {
74
+ await kv.set(KEYS.registry, healed);
75
+ }
76
+ return healed;
77
+ }
78
+ async function saveRegistry(next) {
79
+ m.set({ registry: next });
80
+ await kv.set(KEYS.registry, next);
81
+ }
82
+ async function noteEncrypted(fileId, encrypted) {
83
+ const map = await kv.get(KEYS.encrypted) ?? {};
84
+ if (map[fileId] === encrypted) return;
85
+ map[fileId] = encrypted;
86
+ await kv.set(KEYS.encrypted, map);
87
+ }
88
+ async function registerActive() {
89
+ if (store.state.targetKind !== host.kind) {
90
+ m.set({ activeFileId: null });
91
+ return;
92
+ }
93
+ const fileId = await kv.get(host.activeIdKey) ?? null;
94
+ m.set({ activeFileId: fileId });
95
+ if (!fileId) return;
96
+ const excluded = await host.excludedFileIds?.() ?? [];
97
+ if (excluded.includes(fileId)) return;
98
+ const reg = await loadRegistry();
99
+ if (m.snapshot.joined) {
100
+ const owner = m.snapshot.owner;
101
+ const entry = { fileId, ownerEmail: owner?.email ?? null, ownerName: owner?.name ?? null };
102
+ await saveRegistry({
103
+ personalFileId: reg.personalFileId === fileId ? null : reg.personalFileId,
104
+ shared: [entry, ...reg.shared.filter((s) => s.fileId !== fileId)]
105
+ });
106
+ return;
107
+ }
108
+ const canonical = fileId === ownCanonicalId || reg.personalFileId === fileId || ownIds === null && reg.personalFileId === null;
109
+ if (canonical) {
110
+ await saveRegistry({
111
+ personalFileId: fileId,
112
+ shared: reg.shared.filter((s) => s.fileId !== fileId)
113
+ });
114
+ } else if (reg.shared.some((s) => s.fileId === fileId)) {
115
+ await saveRegistry({
116
+ personalFileId: reg.personalFileId,
117
+ shared: reg.shared.filter((s) => s.fileId !== fileId)
118
+ });
119
+ }
120
+ }
121
+ async function ensureSession() {
122
+ if (!host.ensureSession) return true;
123
+ return host.ensureSession();
124
+ }
125
+ async function markActive(fileId) {
126
+ const reg = await loadRegistry();
127
+ const knownShared = reg.shared.find((s) => s.fileId === fileId);
128
+ let mine = reg.personalFileId === fileId;
129
+ if (!mine && !knownShared) {
130
+ await ensureOwnKnown();
131
+ mine = ownIds === null ? reg.personalFileId === null : ownIds.has(fileId);
132
+ }
133
+ if (mine || !knownShared && ownIds?.has(fileId)) {
134
+ await kv.del(KEYS.joined);
135
+ await kv.del(KEYS.owner);
136
+ m.set({ joined: false, owner: null });
137
+ } else {
138
+ const owner = knownShared ? { email: knownShared.ownerEmail, name: knownShared.ownerName } : await host.fileOwner?.(fileId).catch(() => null) ?? null;
139
+ m.set({ joined: true, owner });
140
+ await kv.set(KEYS.joined, true);
141
+ if (owner) await kv.set(KEYS.owner, owner);
142
+ else await kv.del(KEYS.owner);
143
+ }
144
+ await registerActive();
145
+ }
146
+ async function attachFixed(fileId, passphrase, strategy) {
147
+ const target = host.open(fileId);
148
+ await store.attachTarget(target, {
149
+ password: passphrase,
150
+ strategy,
151
+ keepSession: store.state.targetKind === host.kind,
152
+ wipe: true
153
+ });
154
+ await kv.set(host.activeIdKey, fileId);
155
+ }
156
+ return {
157
+ get snapshot() {
158
+ return m.snapshot;
159
+ },
160
+ subscribe: m.subscribe,
161
+ fileNameFor(label) {
162
+ return naming.namedFileFor(label.trim());
163
+ },
164
+ async hydrate() {
165
+ const joined = await kv.get(KEYS.joined) === true;
166
+ const owner = await kv.get(KEYS.owner) ?? null;
167
+ m.set({ joined, owner, registry: await loadRegistry() });
168
+ await registerActive();
169
+ },
170
+ async refresh() {
171
+ m.set({ registry: await loadRegistry() });
172
+ await registerActive();
173
+ },
174
+ markActive,
175
+ async list() {
176
+ const files = await host.list();
177
+ learnOwn(files);
178
+ const enc = await kv.get(KEYS.encrypted) ?? {};
179
+ const shr = await kv.get(KEYS.shared) ?? {};
180
+ const rows = [];
181
+ for (const f of files) {
182
+ const label = naming.parseLabel(f.name);
183
+ if (label === void 0) continue;
184
+ rows.push({
185
+ fileId: f.id,
186
+ name: f.name,
187
+ label,
188
+ modifiedAt: f.modifiedTime ? Date.parse(f.modifiedTime) || null : null,
189
+ encrypted: enc[f.id] ?? null,
190
+ shared: shr[f.id] ?? null
191
+ });
192
+ }
193
+ return rows;
194
+ },
195
+ async probeEncryption(fileId) {
196
+ try {
197
+ const info = await store.inspectTarget(host.open(fileId));
198
+ if (!info.hasBackup) return null;
199
+ await noteEncrypted(fileId, info.encrypted);
200
+ return info.encrypted;
201
+ } catch {
202
+ return null;
203
+ }
204
+ },
205
+ async noteShared(fileId, shared) {
206
+ const map = await kv.get(KEYS.shared) ?? {};
207
+ if (map[fileId] === shared) return;
208
+ map[fileId] = shared;
209
+ await kv.set(KEYS.shared, map);
210
+ },
211
+ async openBackup(fileId, passphrase = null) {
212
+ m.set({ lastError: null });
213
+ try {
214
+ if (!await ensureSession()) {
215
+ fail("cancelled");
216
+ return "failed";
217
+ }
218
+ const info = await store.inspectTarget(host.open(fileId));
219
+ if (!info.hasBackup) {
220
+ const reg = await loadRegistry();
221
+ await saveRegistry({
222
+ personalFileId: reg.personalFileId === fileId ? null : reg.personalFileId,
223
+ shared: reg.shared.filter((s) => s.fileId !== fileId)
224
+ });
225
+ fail("gone");
226
+ return "failed";
227
+ }
228
+ await noteEncrypted(fileId, info.encrypted);
229
+ if (info.encrypted && !passphrase) return "encrypted";
230
+ await attachFixed(fileId, passphrase, "replace-local");
231
+ await markActive(fileId);
232
+ return store.state.locked ? "encrypted" : "ok";
233
+ } catch {
234
+ fail("failed");
235
+ return "failed";
236
+ }
237
+ },
238
+ async openPersonal(passphrase = null) {
239
+ m.set({ lastError: null });
240
+ try {
241
+ const own = await host.findOrCreatePersonal();
242
+ if (!own) {
243
+ fail("cancelled");
244
+ return "failed";
245
+ }
246
+ ownCanonicalId = own.fileId;
247
+ ownIds?.add(own.fileId);
248
+ const reg = await loadRegistry();
249
+ await saveRegistry({
250
+ personalFileId: own.fileId,
251
+ shared: reg.shared.filter((s) => s.fileId !== own.fileId)
252
+ });
253
+ if (!own.created) return await this.openBackup(own.fileId, passphrase);
254
+ await attachFixed(own.fileId, null, "replace-remote");
255
+ await markActive(own.fileId);
256
+ return "ok";
257
+ } catch {
258
+ fail("failed");
259
+ return "failed";
260
+ }
261
+ },
262
+ async createNamed(label) {
263
+ m.set({ lastError: null });
264
+ const clean = label.trim();
265
+ if (!clean) {
266
+ fail("failed");
267
+ return "failed";
268
+ }
269
+ try {
270
+ if (!await ensureSession()) {
271
+ fail("cancelled");
272
+ return "failed";
273
+ }
274
+ const { fileId } = await host.create(naming.namedFileFor(clean));
275
+ ownIds?.add(fileId);
276
+ await attachFixed(fileId, null, "replace-remote");
277
+ await noteEncrypted(fileId, false);
278
+ await markActive(fileId);
279
+ return "ok";
280
+ } catch {
281
+ fail("failed");
282
+ return "failed";
283
+ }
284
+ },
285
+ async registerShared(fileId, owner) {
286
+ const reg = await loadRegistry();
287
+ await saveRegistry({
288
+ personalFileId: reg.personalFileId === fileId ? null : reg.personalFileId,
289
+ shared: [
290
+ { fileId, ownerEmail: owner.email, ownerName: owner.name },
291
+ ...reg.shared.filter((s) => s.fileId !== fileId)
292
+ ]
293
+ });
294
+ },
295
+ async createShared(fileName, owner) {
296
+ m.set({ lastError: null });
297
+ try {
298
+ if (!await ensureSession()) {
299
+ fail("cancelled");
300
+ return "failed";
301
+ }
302
+ const { fileId } = await host.create(fileName);
303
+ await this.registerShared(fileId, owner);
304
+ await attachFixed(fileId, null, "replace-remote");
305
+ await noteEncrypted(fileId, false);
306
+ await markActive(fileId);
307
+ return "ok";
308
+ } catch {
309
+ fail("failed");
310
+ return "failed";
311
+ }
312
+ },
313
+ async renameBackup(fileId, label) {
314
+ m.set({ lastError: null });
315
+ const clean = label.trim();
316
+ if (!clean || !host.rename) {
317
+ fail("failed");
318
+ return "failed";
319
+ }
320
+ try {
321
+ await host.rename(fileId, naming.namedFileFor(clean));
322
+ return "ok";
323
+ } catch {
324
+ fail("failed");
325
+ return "failed";
326
+ }
327
+ },
328
+ async deleteBackup(fileId) {
329
+ m.set({ lastError: null });
330
+ try {
331
+ if (fileId === m.snapshot.activeFileId) {
332
+ m.set({ activeFileId: null });
333
+ await store.detachTarget({ keepSession: true });
334
+ await kv.del(host.activeIdKey);
335
+ }
336
+ await host.remove(fileId);
337
+ const reg = await loadRegistry();
338
+ await saveRegistry({
339
+ personalFileId: reg.personalFileId === fileId ? null : reg.personalFileId,
340
+ shared: reg.shared.filter((s) => s.fileId !== fileId)
341
+ });
342
+ return true;
343
+ } catch {
344
+ fail("failed");
345
+ return false;
346
+ }
347
+ },
348
+ async forgetShared(fileId) {
349
+ const reg = await loadRegistry();
350
+ await saveRegistry({
351
+ personalFileId: reg.personalFileId,
352
+ shared: reg.shared.filter((s) => s.fileId !== fileId)
353
+ });
354
+ }
355
+ };
356
+ }
357
+
358
+ // src/backups/drive-host.ts
359
+ function driveBackupsHost(opts) {
360
+ return {
361
+ kind: "drive",
362
+ activeIdKey: FILE_ID_KEY,
363
+ async list() {
364
+ return listBackups({ auth: opts.auth, nameContains: opts.nameContains });
365
+ },
366
+ open(fileId) {
367
+ return preview(opts, fileId);
368
+ },
369
+ async create(fileName) {
370
+ return createBackup({ auth: opts.auth, fileName });
371
+ },
372
+ async remove(fileId) {
373
+ return deleteBackup({ auth: opts.auth, fileId });
374
+ },
375
+ async rename(fileId, fileName) {
376
+ return renameBackup({ auth: opts.auth, fileId, fileName });
377
+ },
378
+ async findOrCreatePersonal() {
379
+ return findOrCreateOwnFile(opts);
380
+ },
381
+ async ensureSession() {
382
+ try {
383
+ await opts.auth.token();
384
+ return true;
385
+ } catch {
386
+ return opts.auth.reconnect();
387
+ }
388
+ }
389
+ };
390
+ }
391
+
392
+ export { createBackupsManager, driveBackupsHost };
393
+ //# sourceMappingURL=chunk-HOG2IKEP.js.map
394
+ //# sourceMappingURL=chunk-HOG2IKEP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/backups/manager.ts","../src/backups/drive-host.ts"],"names":[],"mappings":"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","file":"chunk-HOG2IKEP.js","sourcesContent":["// Multi-backup manager: several backup files on one destination account, one\n// attached at a time, each an isolated snapshot - opening one replaces the\n// local data, never merges, never touches the other files.\n//\n// It owns the bookkeeping a backup panel needs: the self-healing registry of\n// files this device has connected to; the per-file memories a listing cannot\n// provide (encrypted? shared? - those bits live inside the files, learned as\n// they are opened or probed and remembered in the kv); and the open / create\n// / rename / delete gestures with their isolation invariants (fixed-id\n// targets, active-id committed only after a successful attach, wipe on every\n// switch so no sync-meta crosses files).\n//\n// Injectable and headless: a LocalStore + a kv + a BackupsHost port for the\n// destination I/O. Errors surface as stable codes, never prose; hosts wrap\n// subscribe in their own reactivity.\n\nimport type { KV } from '../persistence/cache';\nimport type { LocalStore } from '../persistence/store';\nimport type { BackupTarget } from '../persistence/target';\nimport { makeMachine, type FlowStore } from '../flows/machine';\n\n// --- Ports --------------------------------------------------------------------\n\n/** What the destination reports about a listed file (one cheap metadata call). */\nexport interface BackupFileInfo {\n\tid: string;\n\tname: string;\n\t/** RFC 3339 last-write timestamp, when the destination reports one. */\n\tmodifiedTime: string | null;\n}\n\n/** The destination I/O the manager drives - the host wires its own adapter\n * (e.g. over the Drive target ops). Every method may throw; the manager turns\n * failures into stable error codes. */\nexport interface BackupsHost {\n\t/** The target kind the managed files live on (e.g. 'drive'): the manager\n\t * only stamps an active file while the store is attached to this kind. */\n\tkind: string;\n\t/** The kv key the attached file id is persisted under (the same one the\n\t * target's connect path reads first). */\n\tactiveIdKey: string;\n\t/** List candidate backup files on the account. The manager applies the\n\t * naming rules on what comes back; internal files simply do not match. */\n\tlist(): Promise<BackupFileInfo[]>;\n\t/** A target over a FIXED file id - never re-resolved by name. The fixed id\n\t * is what keeps two backups from silently merging mid-switch. */\n\topen(fileId: string): BackupTarget;\n\t/** Create an empty file under this name and return its id. Refuse a name\n\t * the account already carries (a duplicate poisons find-by-name). */\n\tcreate(fileName: string): Promise<{ fileId: string }>;\n\t/** Delete a file for good. */\n\tremove(fileId: string): Promise<void>;\n\t/** Rename a file in place (metadata only: the id must not change). */\n\trename?(fileId: string, fileName: string): Promise<void>;\n\t/** Find (or create) the PERSONAL file by its canonical name, ignoring any\n\t * currently adopted id. Null when the user backs out of the session. */\n\tfindOrCreatePersonal(): Promise<{ fileId: string; created: boolean } | null>;\n\t/** Make sure a usable session exists before a remote call (re-consent when\n\t * it lapsed). False = the user refused; the gesture aborts as 'cancelled'.\n\t * Absent = sessions are the target's own business. */\n\tensureSession?(): Promise<boolean>;\n\t/** Resolve the owner label of a file this account does not own, for the\n\t * shared registry (\"shared by X\"). Best-effort. */\n\tfileOwner?(fileId: string): Promise<{ email: string | null; name: string | null } | null>;\n\t/** File ids that must never enter the registry even though the store may\n\t * attach them (e.g. a sharing engine's published copy file). */\n\texcludedFileIds?(): Promise<(string | null | undefined)[]>;\n}\n\n/** How backup files are named on the destination. */\nexport interface BackupsNaming {\n\t/** The personal, unnamed file (\"App.zip\"). Its label reads as null. */\n\tcanonicalName: string;\n\t/** label -> file name. Default: \"Stem (label).ext\" derived from\n\t * canonicalName (\"App.zip\" + \"family\" -> \"App (family).zip\"). */\n\tnamedFileFor?(label: string): string;\n\t/** file name -> parsed label; null = the personal file; undefined = not one\n\t * of ours (dropped from listings). Default matches the default namedFileFor. */\n\tparseLabel?(name: string): string | null | undefined;\n}\n\n/** The kv keys the manager persists under - override to keep a pre-existing\n * install's records. */\nexport interface BackupsKeys {\n\t/** The known-backups registry. */\n\tregistry?: string;\n\t/** fileId -> encrypted? map. */\n\tencrypted?: string;\n\t/** fileId -> shared? map. */\n\tshared?: string;\n\t/** Set while the ACTIVE file is not the personal one (another account's). */\n\tjoined?: string;\n\t/** Cached owner label of that file. */\n\towner?: string;\n}\n\n// --- Shapes -------------------------------------------------------------------\n\n/** Every backup this device has connected to: its own personal file and each\n * OTHER account's file it was let into. Isolated snapshots, all of them. */\nexport interface KnownBackups {\n\tpersonalFileId: string | null;\n\tshared: { fileId: string; ownerEmail: string | null; ownerName: string | null }[];\n}\n\n/** One row of the \"my backups\" list: a file this account owns. */\nexport interface BackupRow {\n\tfileId: string;\n\tname: string;\n\t/** Parsed display label; null for the personal, unnamed file. */\n\tlabel: string | null;\n\t/** Last-write time, epoch ms (null when the destination did not say). */\n\tmodifiedAt: number | null;\n\t/** true/false once known (opened, created or probed); null until then. */\n\tencrypted: boolean | null;\n\t/** true/false once learned (while this file was the active one); null\n\t * until then. A share link existing is the shared state. */\n\tshared: boolean | null;\n}\n\n/** The owner label of a joined (another account's) file. */\nexport interface BackupOwner {\n\temail: string | null;\n\tname: string | null;\n}\n\n/** Stable failure codes; the host maps them to its own copy. */\nexport type BackupsErrorCode = 'cancelled' | 'gone' | 'failed';\n\nexport interface BackupsSnapshot {\n\tregistry: KnownBackups;\n\t/** The file currently attached, when the store is on the managed kind. */\n\tactiveFileId: string | null;\n\t/** The active file is not the personal one (someone else's). */\n\tjoined: boolean;\n\t/** Who owns the joined file, when known. */\n\towner: BackupOwner | null;\n\t/** The last gesture's failure, or null. Reset at each gesture start. */\n\tlastError: BackupsErrorCode | null;\n}\n\nexport interface BackupsManager extends FlowStore<BackupsSnapshot> {\n\t/** Boot: load registry + joined/owner from kv and stamp the active file.\n\t * Kv-only, no destination call. */\n\thydrate(): Promise<void>;\n\t/** Re-read the registry and restamp the active file. Kv-only, cheap. */\n\trefresh(): Promise<void>;\n\t/** After an attach the manager did not make (a connect widget, a join):\n\t * record the now-active file in the registry and restamp joined/owner. */\n\tmarkActive(fileId: string): Promise<void>;\n\t/** Live list of this account's own files, per the naming rules, decorated\n\t * with the learned memories. One destination listing call. */\n\tlist(): Promise<BackupRow[]>;\n\t/** Learn a listed file's encrypted bit without opening it for real: one\n\t * download + header read, remembered for good. Null when unreachable. */\n\tprobeEncryption(fileId: string): Promise<boolean | null>;\n\t/** Record the ACTIVE file's live share state, keyed by file id, so its row\n\t * stays truthful after a switch away. */\n\tnoteShared(fileId: string, shared: boolean): Promise<void>;\n\t/** Open a known backup: an isolated snapshot replaces the local data.\n\t * 'encrypted' = collect its passphrase and call again with it. A file the\n\t * destination reports GONE is pruned from the registry (lastError 'gone'). */\n\topenBackup(fileId: string, passphrase?: string | null): Promise<'ok' | 'encrypted' | 'failed'>;\n\t/** Open (or create blank) the personal file. */\n\topenPersonal(passphrase?: string | null): Promise<'ok' | 'encrypted' | 'failed'>;\n\t/** Create a brand-new empty named backup and open it (starts blank - never\n\t * seeded from the currently loaded file). */\n\tcreateNamed(label: string): Promise<'ok' | 'failed'>;\n\t/** Record a file in the SHARED registry under the sharer's label, without\n\t * opening it. For a joined portfolio's dedicated wallet: the file may live\n\t * on this very account, yet it presents as \"shared by X\", never as mine. */\n\tregisterShared(fileId: string, owner: BackupOwner): Promise<void>;\n\t/** Create a joined portfolio's dedicated wallet: a file under the given\n\t * name (outside the own-files naming on purpose), attached BLANK - never\n\t * seeded from the previously loaded silo - and recorded in the shared\n\t * registry under the sharer's label. The host stages the shared content\n\t * afterwards (restore the projection, then save). */\n\tcreateShared(fileName: string, owner: BackupOwner): Promise<'ok' | 'failed'>;\n\t/** Rename a file in place (id unchanged, so renaming the ACTIVE file never\n\t * detaches it). The host target refuses a name already carried. */\n\trenameBackup(fileId: string, label: string): Promise<'ok' | 'failed'>;\n\t/** Delete a file for good. Deleting the ACTIVE one detaches first (the app\n\t * goes device-only, local data stays), so nothing writes mid-delete. */\n\tdeleteBackup(fileId: string): Promise<boolean>;\n\t/** Drop a KNOWN shared (another account's) file from this device's\n\t * registry. Forgetting only - the owner's file is never touched. */\n\tforgetShared(fileId: string): Promise<void>;\n\t/** The file name a label produces (the create form's preview line). */\n\tfileNameFor(label: string): string;\n}\n\n// --- Defaults -----------------------------------------------------------------\n\nconst DEFAULT_KEYS: Required<BackupsKeys> = {\n\tregistry: 'selfstore:backups:registry:v1',\n\tencrypted: 'selfstore:backups:encrypted:v1',\n\tshared: 'selfstore:backups:shared:v1',\n\tjoined: 'selfstore:backups:joined:v1',\n\towner: 'selfstore:backups:owner:v1'\n};\n\nconst escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/** The default naming scheme: \"App.zip\" -> personal; \"App (label).zip\" ->\n * named. Derived from the canonical name's stem and extension. */\nfunction defaultNaming(\n\tnaming: BackupsNaming\n): Required<Pick<BackupsNaming, 'namedFileFor' | 'parseLabel'>> {\n\tconst dot = naming.canonicalName.lastIndexOf('.');\n\tconst stem = dot > 0 ? naming.canonicalName.slice(0, dot) : naming.canonicalName;\n\tconst ext = dot > 0 ? naming.canonicalName.slice(dot) : '';\n\tconst named = new RegExp(`^${escapeRe(stem)} \\\\((.+)\\\\)${escapeRe(ext)}$`);\n\treturn {\n\t\tnamedFileFor: (label: string) => `${stem} (${label.trim()})${ext}`,\n\t\tparseLabel: (name: string) => {\n\t\t\tif (name === naming.canonicalName) return null;\n\t\t\tconst m = named.exec(name);\n\t\t\treturn m ? m[1] : undefined;\n\t\t}\n\t};\n}\n\n// --- Factory ------------------------------------------------------------------\n\nexport function createBackupsManager(deps: {\n\tstore: LocalStore;\n\tkv: KV;\n\thost: BackupsHost;\n\tnaming: BackupsNaming;\n\tkeys?: BackupsKeys;\n}): BackupsManager {\n\tconst { store, kv, host } = deps;\n\tconst KEYS = { ...DEFAULT_KEYS, ...deps.keys };\n\tconst namingDefaults = defaultNaming(deps.naming);\n\tconst naming = {\n\t\tnamedFileFor: deps.naming.namedFileFor ?? namingDefaults.namedFileFor,\n\t\tparseLabel: deps.naming.parseLabel ?? namingDefaults.parseLabel\n\t};\n\n\tconst m = makeMachine<BackupsSnapshot>({\n\t\tregistry: { personalFileId: null, shared: [] },\n\t\tactiveFileId: null,\n\t\tjoined: false,\n\t\towner: null,\n\t\tlastError: null\n\t});\n\n\tconst fail = (code: BackupsErrorCode): void => m.set({ lastError: code });\n\n\t// Which file ids the destination lists as OURS, and which one carries the\n\t// canonical (personal) name. Filled by list(), refreshed lazily where a\n\t// truthful own/foreign call is needed. Null until first learned.\n\tlet ownIds: Set<string> | null = null;\n\tlet ownCanonicalId: string | null = null;\n\n\tfunction learnOwn(files: BackupFileInfo[]): void {\n\t\tconst ids = new Set<string>();\n\t\tlet canonical: string | null = null;\n\t\tfor (const f of files) {\n\t\t\tconst label = naming.parseLabel(f.name);\n\t\t\tif (label === undefined) continue;\n\t\t\tids.add(f.id);\n\t\t\tif (label === null) canonical = f.id;\n\t\t}\n\t\townIds = ids;\n\t\townCanonicalId = canonical;\n\t}\n\n\tasync function ensureOwnKnown(): Promise<void> {\n\t\tif (ownIds !== null) return;\n\t\ttry {\n\t\t\tlearnOwn(await host.list());\n\t\t} catch {\n\t\t\t// Offline: leave unknown - markActive falls back on the registry alone.\n\t\t}\n\t}\n\n\t/** Load the registry, self-healing: excluded ids (a sharing engine's copy\n\t * file) must never sit in it - recorded as \"personal\" they would mislabel\n\t * the chooser and let a later open treat a shared file as one's own. */\n\tasync function loadRegistry(): Promise<KnownBackups> {\n\t\tconst reg = (await kv.get<KnownBackups>(KEYS.registry)) ?? { personalFileId: null, shared: [] };\n\t\tconst excluded = (await host.excludedFileIds?.()) ?? [];\n\t\tconst bad = new Set(excluded.filter((x): x is string => typeof x === 'string'));\n\t\tif (bad.size === 0) return reg;\n\t\tconst healed: KnownBackups = {\n\t\t\tpersonalFileId: reg.personalFileId && bad.has(reg.personalFileId) ? null : reg.personalFileId,\n\t\t\tshared: reg.shared.filter((s) => !bad.has(s.fileId))\n\t\t};\n\t\tif (\n\t\t\thealed.personalFileId !== reg.personalFileId ||\n\t\t\thealed.shared.length !== reg.shared.length\n\t\t) {\n\t\t\tawait kv.set(KEYS.registry, healed);\n\t\t}\n\t\treturn healed;\n\t}\n\n\tasync function saveRegistry(next: KnownBackups): Promise<void> {\n\t\tm.set({ registry: next });\n\t\tawait kv.set(KEYS.registry, next);\n\t}\n\n\tasync function noteEncrypted(fileId: string, encrypted: boolean): Promise<void> {\n\t\tconst map = (await kv.get<Record<string, boolean>>(KEYS.encrypted)) ?? {};\n\t\tif (map[fileId] === encrypted) return;\n\t\tmap[fileId] = encrypted;\n\t\tawait kv.set(KEYS.encrypted, map);\n\t}\n\n\t/** Stamp the currently attached file (from the target's own persisted id)\n\t * into the snapshot and the registry. Kv-only. */\n\tasync function registerActive(): Promise<void> {\n\t\tif (store.state.targetKind !== host.kind) {\n\t\t\tm.set({ activeFileId: null });\n\t\t\treturn;\n\t\t}\n\t\tconst fileId = (await kv.get<string>(host.activeIdKey)) ?? null;\n\t\tm.set({ activeFileId: fileId });\n\t\tif (!fileId) return;\n\t\tconst excluded = (await host.excludedFileIds?.()) ?? [];\n\t\tif (excluded.includes(fileId)) return;\n\t\tconst reg = await loadRegistry();\n\t\tif (m.snapshot.joined) {\n\t\t\tconst owner = m.snapshot.owner;\n\t\t\tconst entry = { fileId, ownerEmail: owner?.email ?? null, ownerName: owner?.name ?? null };\n\t\t\tawait saveRegistry({\n\t\t\t\tpersonalFileId: reg.personalFileId === fileId ? null : reg.personalFileId,\n\t\t\t\tshared: [entry, ...reg.shared.filter((s) => s.fileId !== fileId)]\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\t// Not joined. The personal slot belongs to the CANONICAL file alone: an\n\t\t// own NAMED backup lists live and must not clobber it (the slot is the\n\t\t// escape back to the personal file).\n\t\tconst canonical =\n\t\t\tfileId === ownCanonicalId ||\n\t\t\treg.personalFileId === fileId ||\n\t\t\t(ownIds === null && reg.personalFileId === null);\n\t\tif (canonical) {\n\t\t\tawait saveRegistry({\n\t\t\t\tpersonalFileId: fileId,\n\t\t\t\tshared: reg.shared.filter((s) => s.fileId !== fileId)\n\t\t\t});\n\t\t} else if (reg.shared.some((s) => s.fileId === fileId)) {\n\t\t\tawait saveRegistry({\n\t\t\t\tpersonalFileId: reg.personalFileId,\n\t\t\t\tshared: reg.shared.filter((s) => s.fileId !== fileId)\n\t\t\t});\n\t\t}\n\t}\n\n\t/** Session guard shared by the remote gestures: absent hook = no guard. */\n\tasync function ensureSession(): Promise<boolean> {\n\t\tif (!host.ensureSession) return true;\n\t\treturn host.ensureSession();\n\t}\n\n\tasync function markActive(fileId: string): Promise<void> {\n\t\tconst reg = await loadRegistry();\n\t\tconst knownShared = reg.shared.find((s) => s.fileId === fileId);\n\t\tlet mine = reg.personalFileId === fileId;\n\t\tif (!mine && !knownShared) {\n\t\t\t// Unknown file: ask the destination whose it is (one listing call,\n\t\t\t// cached). An own named backup must never read as joined - labeled\n\t\t\t// \"shared by me\", it would sit in the wrong section for good.\n\t\t\tawait ensureOwnKnown();\n\t\t\tmine = ownIds === null ? reg.personalFileId === null : ownIds.has(fileId);\n\t\t}\n\t\tif (mine || (!knownShared && ownIds?.has(fileId))) {\n\t\t\tawait kv.del(KEYS.joined);\n\t\t\tawait kv.del(KEYS.owner);\n\t\t\tm.set({ joined: false, owner: null });\n\t\t} else {\n\t\t\tconst owner: BackupOwner | null = knownShared\n\t\t\t\t? { email: knownShared.ownerEmail, name: knownShared.ownerName }\n\t\t\t\t: ((await host.fileOwner?.(fileId).catch(() => null)) ?? null);\n\t\t\tm.set({ joined: true, owner });\n\t\t\tawait kv.set(KEYS.joined, true);\n\t\t\tif (owner) await kv.set(KEYS.owner, owner);\n\t\t\telse await kv.del(KEYS.owner);\n\t\t}\n\t\tawait registerActive();\n\t}\n\n\t/** The one attach path every open shares: FIXED-id target, and the\n\t * remembered id committed only after the attach succeeds - committed up\n\t * front, the OUTGOING target (which resolves its file lazily from that\n\t * same key) would flush the old file's data into the one being opened:\n\t * two \"isolated\" backups silently merged. `wipe` keeps sync-meta from\n\t * crossing files (cross-file tombstones erase the other side). */\n\tasync function attachFixed(\n\t\tfileId: string,\n\t\tpassphrase: string | null,\n\t\tstrategy: 'replace-local' | 'replace-remote'\n\t): Promise<void> {\n\t\tconst target = host.open(fileId);\n\t\tawait store.attachTarget(target, {\n\t\t\tpassword: passphrase,\n\t\t\tstrategy,\n\t\t\tkeepSession: store.state.targetKind === host.kind,\n\t\t\twipe: true\n\t\t});\n\t\tawait kv.set(host.activeIdKey, fileId);\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\tfileNameFor(label: string): string {\n\t\t\treturn naming.namedFileFor(label.trim());\n\t\t},\n\n\t\tasync hydrate(): Promise<void> {\n\t\t\tconst joined = (await kv.get(KEYS.joined)) === true;\n\t\t\tconst owner = (await kv.get<BackupOwner>(KEYS.owner)) ?? null;\n\t\t\tm.set({ joined, owner, registry: await loadRegistry() });\n\t\t\tawait registerActive();\n\t\t},\n\n\t\tasync refresh(): Promise<void> {\n\t\t\tm.set({ registry: await loadRegistry() });\n\t\t\tawait registerActive();\n\t\t},\n\n\t\tmarkActive,\n\n\t\tasync list(): Promise<BackupRow[]> {\n\t\t\tconst files = await host.list();\n\t\t\tlearnOwn(files);\n\t\t\tconst enc = (await kv.get<Record<string, boolean>>(KEYS.encrypted)) ?? {};\n\t\t\tconst shr = (await kv.get<Record<string, boolean>>(KEYS.shared)) ?? {};\n\t\t\tconst rows: BackupRow[] = [];\n\t\t\tfor (const f of files) {\n\t\t\t\tconst label = naming.parseLabel(f.name);\n\t\t\t\tif (label === undefined) continue; // not one of ours (a copy, a bulletin, ...)\n\t\t\t\trows.push({\n\t\t\t\t\tfileId: f.id,\n\t\t\t\t\tname: f.name,\n\t\t\t\t\tlabel,\n\t\t\t\t\tmodifiedAt: f.modifiedTime ? Date.parse(f.modifiedTime) || null : null,\n\t\t\t\t\tencrypted: enc[f.id] ?? null,\n\t\t\t\t\tshared: shr[f.id] ?? null\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn rows;\n\t\t},\n\n\t\tasync probeEncryption(fileId: string): Promise<boolean | null> {\n\t\t\ttry {\n\t\t\t\tconst info = await store.inspectTarget(host.open(fileId));\n\t\t\t\tif (!info.hasBackup) return null;\n\t\t\t\tawait noteEncrypted(fileId, info.encrypted);\n\t\t\t\treturn info.encrypted;\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\n\t\tasync noteShared(fileId: string, shared: boolean): Promise<void> {\n\t\t\tconst map = (await kv.get<Record<string, boolean>>(KEYS.shared)) ?? {};\n\t\t\tif (map[fileId] === shared) return;\n\t\t\tmap[fileId] = shared;\n\t\t\tawait kv.set(KEYS.shared, map);\n\t\t},\n\n\t\tasync openBackup(fileId, passphrase = null): Promise<'ok' | 'encrypted' | 'failed'> {\n\t\t\tm.set({ lastError: null });\n\t\t\ttry {\n\t\t\t\tif (!(await ensureSession())) {\n\t\t\t\t\tfail('cancelled');\n\t\t\t\t\treturn 'failed';\n\t\t\t\t}\n\t\t\t\tconst info = await store.inspectTarget(host.open(fileId));\n\t\t\t\tif (!info.hasBackup) {\n\t\t\t\t\t// Dead entry (deleted, or unshared from this account): prune it so\n\t\t\t\t\t// its row stops re-failing forever. A transient failure throws\n\t\t\t\t\t// instead and never prunes - dropping a good record on a blip\n\t\t\t\t\t// would be far worse than one more failed tap.\n\t\t\t\t\tconst reg = await loadRegistry();\n\t\t\t\t\tawait saveRegistry({\n\t\t\t\t\t\tpersonalFileId: reg.personalFileId === fileId ? null : reg.personalFileId,\n\t\t\t\t\t\tshared: reg.shared.filter((s) => s.fileId !== fileId)\n\t\t\t\t\t});\n\t\t\t\t\tfail('gone');\n\t\t\t\t\treturn 'failed';\n\t\t\t\t}\n\t\t\t\tawait noteEncrypted(fileId, info.encrypted); // the listing cannot tell\n\t\t\t\tif (info.encrypted && !passphrase) return 'encrypted';\n\t\t\t\tawait attachFixed(fileId, passphrase, 'replace-local');\n\t\t\t\tawait markActive(fileId);\n\t\t\t\treturn store.state.locked ? 'encrypted' : 'ok';\n\t\t\t} catch {\n\t\t\t\tfail('failed');\n\t\t\t\treturn 'failed';\n\t\t\t}\n\t\t},\n\n\t\tasync openPersonal(passphrase = null): Promise<'ok' | 'encrypted' | 'failed'> {\n\t\t\tm.set({ lastError: null });\n\t\t\ttry {\n\t\t\t\tconst own = await host.findOrCreatePersonal();\n\t\t\t\tif (!own) {\n\t\t\t\t\tfail('cancelled');\n\t\t\t\t\treturn 'failed';\n\t\t\t\t}\n\t\t\t\townCanonicalId = own.fileId;\n\t\t\t\townIds?.add(own.fileId);\n\t\t\t\tconst reg = await loadRegistry();\n\t\t\t\tawait saveRegistry({\n\t\t\t\t\tpersonalFileId: own.fileId,\n\t\t\t\t\tshared: reg.shared.filter((s) => s.fileId !== own.fileId)\n\t\t\t\t});\n\t\t\t\tif (!own.created) return await this.openBackup(own.fileId, passphrase);\n\t\t\t\tawait attachFixed(own.fileId, null, 'replace-remote');\n\t\t\t\tawait markActive(own.fileId);\n\t\t\t\treturn 'ok';\n\t\t\t} catch {\n\t\t\t\tfail('failed');\n\t\t\t\treturn 'failed';\n\t\t\t}\n\t\t},\n\n\t\tasync createNamed(label): Promise<'ok' | 'failed'> {\n\t\t\tm.set({ lastError: null });\n\t\t\tconst clean = label.trim();\n\t\t\tif (!clean) {\n\t\t\t\tfail('failed');\n\t\t\t\treturn 'failed';\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tif (!(await ensureSession())) {\n\t\t\t\t\tfail('cancelled');\n\t\t\t\t\treturn 'failed';\n\t\t\t\t}\n\t\t\t\tconst { fileId } = await host.create(naming.namedFileFor(clean));\n\t\t\t\townIds?.add(fileId);\n\t\t\t\tawait attachFixed(fileId, null, 'replace-remote');\n\t\t\t\tawait noteEncrypted(fileId, false); // a fresh backup starts unprotected\n\t\t\t\tawait markActive(fileId);\n\t\t\t\treturn 'ok';\n\t\t\t} catch {\n\t\t\t\tfail('failed');\n\t\t\t\treturn 'failed';\n\t\t\t}\n\t\t},\n\n\t\tasync registerShared(fileId, owner): Promise<void> {\n\t\t\tconst reg = await loadRegistry();\n\t\t\tawait saveRegistry({\n\t\t\t\tpersonalFileId: reg.personalFileId === fileId ? null : reg.personalFileId,\n\t\t\t\tshared: [\n\t\t\t\t\t{ fileId, ownerEmail: owner.email, ownerName: owner.name },\n\t\t\t\t\t...reg.shared.filter((s) => s.fileId !== fileId)\n\t\t\t\t]\n\t\t\t});\n\t\t},\n\n\t\tasync createShared(fileName, owner): Promise<'ok' | 'failed'> {\n\t\t\tm.set({ lastError: null });\n\t\t\ttry {\n\t\t\t\tif (!(await ensureSession())) {\n\t\t\t\t\tfail('cancelled');\n\t\t\t\t\treturn 'failed';\n\t\t\t\t}\n\t\t\t\tconst { fileId } = await host.create(fileName);\n\t\t\t\t// Registered as shared BEFORE the attach: markActive must find it in\n\t\t\t\t// the shared registry, or it would resolve this account as the owner\n\t\t\t\t// and file the joined wallet under \"mine\".\n\t\t\t\tawait this.registerShared(fileId, owner);\n\t\t\t\tawait attachFixed(fileId, null, 'replace-remote');\n\t\t\t\tawait noteEncrypted(fileId, false); // a member wallet is never protected\n\t\t\t\tawait markActive(fileId);\n\t\t\t\treturn 'ok';\n\t\t\t} catch {\n\t\t\t\tfail('failed');\n\t\t\t\treturn 'failed';\n\t\t\t}\n\t\t},\n\n\t\tasync renameBackup(fileId, label): Promise<'ok' | 'failed'> {\n\t\t\tm.set({ lastError: null });\n\t\t\tconst clean = label.trim();\n\t\t\tif (!clean || !host.rename) {\n\t\t\t\tfail('failed');\n\t\t\t\treturn 'failed';\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait host.rename(fileId, naming.namedFileFor(clean));\n\t\t\t\treturn 'ok';\n\t\t\t} catch {\n\t\t\t\tfail('failed');\n\t\t\t\treturn 'failed';\n\t\t\t}\n\t\t},\n\n\t\tasync deleteBackup(fileId): Promise<boolean> {\n\t\t\tm.set({ lastError: null });\n\t\t\ttry {\n\t\t\t\tif (fileId === m.snapshot.activeFileId) {\n\t\t\t\t\t// Detach first: the app goes device-only (local data stays) so\n\t\t\t\t\t// nothing writes to the file as it is removed. keepSession: a\n\t\t\t\t\t// default detach runs the target's disconnect, which forgets the\n\t\t\t\t\t// destination credentials - the remove below still needs them,\n\t\t\t\t\t// and the user deleted a FILE, not their account connection.\n\t\t\t\t\tm.set({ activeFileId: null });\n\t\t\t\t\tawait store.detachTarget({ keepSession: true });\n\t\t\t\t\t// The remembered file id would normally be cleared by that\n\t\t\t\t\t// disconnect; do it here so nothing dangles on a deleted file.\n\t\t\t\t\tawait kv.del(host.activeIdKey);\n\t\t\t\t}\n\t\t\t\tawait host.remove(fileId);\n\t\t\t\tconst reg = await loadRegistry();\n\t\t\t\tawait saveRegistry({\n\t\t\t\t\tpersonalFileId: reg.personalFileId === fileId ? null : reg.personalFileId,\n\t\t\t\t\tshared: reg.shared.filter((s) => s.fileId !== fileId)\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\tfail('failed');\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\n\t\tasync forgetShared(fileId): Promise<void> {\n\t\t\tconst reg = await loadRegistry();\n\t\t\tawait saveRegistry({\n\t\t\t\tpersonalFileId: reg.personalFileId,\n\t\t\t\tshared: reg.shared.filter((s) => s.fileId !== fileId)\n\t\t\t});\n\t\t}\n\t};\n}\n","// The BackupsHost port, wired over the Drive target's own operations.\n//\n// The manager is deliberately injectable: it drives a destination through the\n// port and knows nothing about Drive. But the library already ships every\n// operation that port asks for - list, open-by-id, create, rename, delete,\n// find-or-create the personal file - so every app wiring them together again\n// was writing the same adapter, and getting the same three details wrong:\n//\n// - `open` must be a target over a FIXED id (`preview`), not a connect: a\n// connect re-resolves by name and would silently merge two backups.\n// - the active id lives under the SAME kv key the connect path reads, or a\n// reload adopts a different file than the one on screen.\n// - a listing must not raise the reconnect gate: `ensureSession` re-consents\n// first, so a lapsed session is a question, not an error.\n//\n// This is that adapter, once.\n\nimport type { BackupFileInfo, BackupsHost } from './manager';\nimport type { BackupTarget } from '../persistence/target';\nimport type { DriveOptions } from '../persistence/targets/drive';\nimport {\n\tFILE_ID_KEY,\n\tcreateBackup,\n\tdeleteBackup,\n\tfindOrCreateOwnFile,\n\tlistBackups,\n\tpreview,\n\trenameBackup\n} from '../persistence/targets/drive';\n\nexport interface DriveBackupsHostOptions extends DriveOptions {\n\t/** Narrow the listing server-side to names containing this fragment (the\n\t * app's own stem, typically). The manager still applies its naming rules on\n\t * what comes back, so this is a bandwidth hint and never a filter to rely\n\t * on for correctness. Default: the whole visible listing. */\n\tnameContains?: string;\n}\n\n/**\n * The Drive destination as a `BackupsHost`, ready for `createBackupsManager`\n * (and therefore for `<selfstore-backups>`).\n *\n * ```ts\n * const manager = createBackupsManager({\n * store: store.engine,\n * kv: store.kv,\n * host: driveBackupsHost({ auth, kv: store.kv, fileName: 'app.zip' }),\n * naming: { canonicalName: 'app.zip' }\n * });\n * ```\n *\n * The `drive.file` scope already narrows what a listing can see to the files\n * this app created or was handed, so nothing here can enumerate a user's Drive.\n */\nexport function driveBackupsHost(opts: DriveBackupsHostOptions): BackupsHost {\n\treturn {\n\t\tkind: 'drive',\n\t\tactiveIdKey: FILE_ID_KEY,\n\n\t\tasync list(): Promise<BackupFileInfo[]> {\n\t\t\treturn listBackups({ auth: opts.auth, nameContains: opts.nameContains });\n\t\t},\n\n\t\topen(fileId: string): BackupTarget {\n\t\t\t// `preview`, not `connect`: a target bound to this exact id, and it\n\t\t\t// writes nothing to the kv - the manager decides what becomes active.\n\t\t\treturn preview(opts, fileId);\n\t\t},\n\n\t\tasync create(fileName: string): Promise<{ fileId: string }> {\n\t\t\treturn createBackup({ auth: opts.auth, fileName });\n\t\t},\n\n\t\tasync remove(fileId: string): Promise<void> {\n\t\t\treturn deleteBackup({ auth: opts.auth, fileId });\n\t\t},\n\n\t\tasync rename(fileId: string, fileName: string): Promise<void> {\n\t\t\treturn renameBackup({ auth: opts.auth, fileId, fileName });\n\t\t},\n\n\t\tasync findOrCreatePersonal(): Promise<{ fileId: string; created: boolean } | null> {\n\t\t\treturn findOrCreateOwnFile(opts);\n\t\t},\n\n\t\tasync ensureSession(): Promise<boolean> {\n\t\t\t// A live token is the cheap answer. Only when there is none does the\n\t\t\t// user get a consent popup - and refusing it is an answer, not a\n\t\t\t// failure: the caller aborts the gesture as cancelled.\n\t\t\ttry {\n\t\t\t\tawait opts.auth.token();\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn opts.auth.reconnect();\n\t\t\t}\n\t\t}\n\t};\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { d as Mode, e as PasswordCheck, g as PasswordRequirement, l 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 { R as ResumeOffer, C as ConnectTargets } from './connect-DaZFPJdt.js';
6
+ import { f as BackupsManager } from './manager-BqzTVaep.js';
6
7
  import { a as DriveAuth } from './drive-B2fm71qD.js';
7
8
  import { W as WebdavConfig, S as S3Config } from './s3-DgyTwMRc.js';
8
9
  export { f as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-CldSa9EY.js';
@@ -265,6 +266,17 @@ interface SimpleStore<S extends Record<string, SimpleRecord> = Record<string, Si
265
266
  * ahead of the plain destinations, instead of re-deriving a decision the
266
267
  * library already has on file. Null when there is nothing to resume. */
267
268
  resumeOffer(): ResumeOffer | null;
269
+ /**
270
+ * The several-backups manager for this store's destination, or null when
271
+ * there is nothing to manage several of (no Drive configured).
272
+ *
273
+ * The port under it - list, open by id, create, rename, delete - was the last
274
+ * thing an app still assembled from the session the store had built, and the
275
+ * one place it could pick the wrong file id. Built once and remembered; the
276
+ * backups module is imported on demand, so an app that never asks does not
277
+ * carry it.
278
+ */
279
+ backups(): Promise<BackupsManager | null>;
268
280
  /**
269
281
  * The destinations this store can offer, ready for a connect flow or widget:
270
282
  * Drive first when the app configured it, a file always.
package/dist/index.js CHANGED
@@ -64,6 +64,7 @@ async function selfstore(app, options = {}) {
64
64
  const emitData = () => dataSubs.forEach((fn) => fn());
65
65
  const cache = options.cache ?? (hasIndexedDb() ? indexedDbCache(app, options.cacheLock ? { lock: true } : void 0) : memoryCache());
66
66
  const backupName = `${app}.zip`;
67
+ let backupsManager = null;
67
68
  let driveAuth = isDriveAuth(options.drive) ? options.drive : options.drive ? gisDriveAuth({
68
69
  clientId: options.drive.clientId,
69
70
  scope: options.drive.scope,
@@ -286,6 +287,19 @@ async function selfstore(app, options = {}) {
286
287
  get account() {
287
288
  return rememberedDriveAccount(app) ?? null;
288
289
  },
290
+ async backups() {
291
+ if (!driveAuth) return null;
292
+ backupsManager ??= await (async () => {
293
+ const { createBackupsManager, driveBackupsHost } = await import('./manager-drive-4WLKOOXO.js');
294
+ return createBackupsManager({
295
+ store,
296
+ kv: cache.kv,
297
+ host: driveBackupsHost({ auth: driveAuth, kv: cache.kv, fileName: backupName }),
298
+ naming: { canonicalName: backupName }
299
+ });
300
+ })();
301
+ return backupsManager;
302
+ },
289
303
  destinations() {
290
304
  return driveAuth ? { drive: driveAuth, file: true } : { file: true };
291
305
  },