selfstore 1.8.34 → 1.8.36
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 +6 -2
- package/dist/advanced.d.ts +9 -9
- package/dist/advanced.js +2 -2
- package/dist/backups.d.ts +4 -4
- package/dist/{cache-D8Z4u5Jz.d.ts → cache-DBIn_Uo8.d.ts} +9 -1
- package/dist/{chunk-A2RRJLIF.js → chunk-3P2W3XQD.js} +36 -23
- package/dist/chunk-3P2W3XQD.js.map +1 -0
- package/dist/{chunk-HQSK7HK3.js → chunk-JE3BLKXO.js} +86 -32
- package/dist/chunk-JE3BLKXO.js.map +1 -0
- package/dist/{chunk-VRWAV3GI.js → chunk-UU4PS3UH.js} +19 -9
- package/dist/chunk-UU4PS3UH.js.map +1 -0
- package/dist/{connect-B9f58dFZ.d.ts → connect-CdlhiTBy.d.ts} +4 -4
- package/dist/{drive-D5YLXmp_.d.ts → drive-BCRil91Q.d.ts} +2 -2
- package/dist/flows.d.ts +4 -4
- package/dist/flows.js +1 -1
- package/dist/groups.d.ts +2 -2
- package/dist/households.d.ts +7 -0
- package/dist/households.js +13 -3
- package/dist/households.js.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +3 -3
- package/dist/{local-CmS0xFkK.d.ts → local-CpoBeJWk.d.ts} +2 -2
- package/dist/{manager-DCV_7B6M.d.ts → manager-0puQEpZc.d.ts} +2 -2
- package/dist/passkey.d.ts +6 -4
- package/dist/passkey.js +5 -4
- package/dist/passkey.js.map +1 -1
- package/dist/proof.d.ts +6 -6
- package/dist/{s3-mHLrEywJ.d.ts → s3-kikEHI-g.d.ts} +2 -2
- package/dist/{simple-Bnh2j78j.d.ts → simple-BmUQpsBM.d.ts} +6 -6
- package/dist/{store-DSvZwwr1.d.ts → store-bp1VRW7R.d.ts} +1 -1
- package/dist/sync.js +1 -1
- package/dist/widgets.d.ts +5 -5
- package/dist/widgets.js +1 -1
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/dist/chunk-A2RRJLIF.js.map +0 -1
- package/dist/chunk-HQSK7HK3.js.map +0 -1
- package/dist/chunk-VRWAV3GI.js.map +0 -1
|
@@ -338,13 +338,23 @@ function shareFlow(engine, options = {}) {
|
|
|
338
338
|
m.set({ busy: op });
|
|
339
339
|
return true;
|
|
340
340
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
341
|
+
let listing = null;
|
|
342
|
+
function doRefresh() {
|
|
343
|
+
listing ??= (async () => {
|
|
344
|
+
try {
|
|
345
|
+
const { links, members } = await withDeadline(
|
|
346
|
+
engine.list(),
|
|
347
|
+
deadlineMs,
|
|
348
|
+
"The share listing"
|
|
349
|
+
);
|
|
350
|
+
m.set({ links, members, busy: null, stale: false, error: null });
|
|
351
|
+
} catch (e) {
|
|
352
|
+
m.set({ busy: null, stale: true, error: toStoreError(e) });
|
|
353
|
+
} finally {
|
|
354
|
+
listing = null;
|
|
355
|
+
}
|
|
356
|
+
})();
|
|
357
|
+
return listing;
|
|
348
358
|
}
|
|
349
359
|
const flow = {
|
|
350
360
|
get snapshot() {
|
|
@@ -493,5 +503,5 @@ function joinFlow(link, engine, options = {}) {
|
|
|
493
503
|
}
|
|
494
504
|
|
|
495
505
|
export { connectFlow, joinFlow, shareFlow };
|
|
496
|
-
//# sourceMappingURL=chunk-
|
|
497
|
-
//# sourceMappingURL=chunk-
|
|
506
|
+
//# sourceMappingURL=chunk-UU4PS3UH.js.map
|
|
507
|
+
//# sourceMappingURL=chunk-UU4PS3UH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/flows/connect.ts","../src/flows/share.ts","../src/flows/join.ts"],"names":["connect","config","DEFAULT_DEADLINE_MS"],"mappings":"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","file":"chunk-UU4PS3UH.js","sourcesContent":["// connectFlow: the \"where does my data live\" journey as a headless state\n// machine. It owns ordering and failure rules that took a series of\n// production hotfixes to get right, each covered by a test:\n//\n// choose -> (form) -> authorizing -> (password) -> (conflict) -> connected\n// \\-> error\n//\n// One popup per gesture (re-entry ignored while a leg is in flight), and a\n// cancelled popup is not an error - back to 'choose', silently. An encrypted\n// backup's password is proven by a trial read while the store is untouched,\n// so a wrong password can never half-connect (deferUnlock opts out: adopt\n// locked, unlock where the host chooses). overwrite() is the forgotten-\n// password escape: local wiped first, tiny blank write. Merge is the default\n// reconciliation; the destructive choices hide behind 'conflict', which only\n// appears when the app declared local data worth asking about. Network legs\n// run under a deadline; human steps never do. A cancelled journey stays\n// dead - late resolutions are discarded (generation counter).\n\nimport type { LocalStore, StoreError } from '../persistence/store';\nimport type { BackupTarget } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport { restore } from '../selfstore/fluent';\nimport { SelfstoreError, isSelfstoreError } from '../selfstore/errors';\nimport { connect as driveConnect, type DriveAuth } from '../persistence/targets/drive';\nimport {\n\tconnect as fileConnect,\n\tisSupported as fileIsSupported,\n\topenExisting as fileOpenExisting\n} from '../persistence/targets/file';\n// WebDAV and S3 arrive on the gesture that uses them - see targets/servers.ts.\nimport { webdavConnect, s3Connect } from '../persistence/targets/servers';\nimport type { WebdavConfig } from '../persistence/targets/webdav';\nimport type { S3Config } from '../persistence/targets/s3';\nimport { makeMachine, toStoreError, withDeadline, type FlowStore } from './machine';\n\nexport type ConnectKind = 'drive' | 'file' | 'webdav' | 's3';\n\n/** Produce a connected BackupTarget, or null when the user cancelled the\n * gesture (consent refused, picker closed). Custom connectors slot custom\n * targets - or test fakes - into the same journey. */\nexport type Connector = () => Promise<BackupTarget | null>;\n\n/** Which destinations the flow offers, and how to open each one. The offer\n * renders in this OBJECT's insertion order: the host decides which\n * destination leads by how it writes the object, not by a ranking here. */\nexport interface ConnectTargets {\n\t/** Google Drive: the auth strategy (e.g. gisDriveAuth), or a custom connector. */\n\tdrive?: DriveAuth | Connector;\n\t/** Local file via the browser's picker (true), or a custom connector. On\n\t * browsers without the File System Access API the flow degrades to the\n\t * manual download mode by itself. The object form offers TWO gestures on\n\t * the same destination: `create` (save picker: start or overwrite a backup\n\t * file) and `open` (open picker: adopt an EXISTING backup file - it then\n\t * goes through the same inspect/password/resolution journey). */\n\tfile?: true | Connector | { create?: true | Connector; open?: true | Connector };\n\t/** WebDAV: true renders a 'form' step and expects submitWebdav(config);\n\t * a fixed WebdavConfig skips the form; or a custom connector. */\n\twebdav?: true | WebdavConfig | Connector;\n\t/** S3-compatible bucket (Amazon S3, R2, B2, MinIO): true renders a 'form'\n\t * step and expects submitS3(config); a fixed S3Config skips the form; or a\n\t * custom connector. Offer it alongside `webdav` and the widget groups both\n\t * under one \"your server\" entry with a WebDAV/S3 toggle. */\n\ts3?: true | S3Config | Connector;\n}\n\n/** What the flows need from the store. The simple store exposes this as\n * `store.flowHost`; an advanced-store app builds it by hand. */\nexport interface FlowHost {\n\tengine: LocalStore;\n\tkv: KV;\n\t/** Backup file name on the destination (the simple store uses `app.zip`). */\n\tbackupName: string;\n}\n\n/** What every flow and widget accepts as \"the store\": the simple facade, which\n * carries its ports on `flowHost`, or a hand-built host. */\nexport type StoreLike = FlowHost | { flowHost: FlowHost };\n\n/**\n * A destination this device has already used, offered as one gesture back to\n * it rather than as the question it answered the first time.\n *\n * The screen that asks \"where should we save your data?\" is right exactly\n * once. Every time after that - a device whose destination was detached, a\n * browser whose storage was cleared - the app usually still knows the answer,\n * and asking again makes the returning user re-derive a decision they already\n * made. This offer carries that memory: it goes first, above a separator, and\n * says WHICH backup it will reopen.\n *\n * The host supplies the connector, because only the host knows how to reach\n * the remembered destination (adopting a known file id, not searching by\n * name). The flow supplies what follows: no conflict step, and no chooser.\n */\nexport interface ResumeOffer {\n\t/** Which destination it is. Picks the icon and the wording, nothing else. */\n\tkind: ConnectKind;\n\t/**\n\t * What exactly is being reopened - the account, the file name. Rendered as\n\t * the card's second line: \"resume my backup\" only answers the user's\n\t * question when it can name the backup.\n\t */\n\tdetail?: string;\n\t/** Reopen it. Same contract as any connector: null when the user cancels. */\n\tconnect: Connector;\n}\n\nexport interface ConnectFlowOptions {\n\t/**\n\t * The destination this device already used, offered ahead of the others.\n\t * Absent (the default) means every visit is a first visit.\n\t *\n\t * Taking this offer never raises the conflict step, whatever\n\t * `hasLocalData` says: reopening one's own backup is not a merge question,\n\t * it IS the answer to it. The question still stands for a destination the\n\t * user newly points at.\n\t */\n\tresume?: ResumeOffer;\n\t/** Declare when this device holds data worth a question: only then does an\n\t * existing backup raise the 'conflict' step with the destructive choices.\n\t * Default: never - the flow applies defaultResolution. */\n\thasLocalData?: () => boolean;\n\t/** How to reconcile with a destination that already holds a backup when the\n\t * conflict step is not raised. Default 'merge' - the only choice that\n\t * loses nothing on either side. 'resume' adopts the backup (the\n\t * destination wins; this device's working copy is replaced), 'replace'\n\t * overwrites the backup with this device. An encrypted backup still asks\n\t * for its password first, whatever the resolution; an empty destination\n\t * is not concerned (this device's data simply becomes its content). */\n\tdefaultResolution?: ConnectResolution;\n\t/** Skip the password step on an encrypted backup: adopt it AS is (resume)\n\t * and complete with the store locked - nothing readable, nothing\n\t * clobbered - for the host to unlock on its own surface (engine.unlock)\n\t * after the connect. Adoption is forced whatever defaultResolution says:\n\t * without the proven key a merge is impossible and a replace would\n\t * destroy a backup no one proved they own. Hosts that let a device\n\t * holding real local data connect should keep the password-first\n\t * journey - adoption replaces the working copy. */\n\tdeferUnlock?: boolean;\n\t/**\n\t * A secret the host already holds (an app passphrase, a derived key), used\n\t * to encrypt the backup when this journey creates or adopts one that is NOT\n\t * already encrypted - an empty destination, or a plaintext file.\n\t *\n\t * Two things need it. A store built with `requireEncryption` refuses a\n\t * passwordless attach outright (ENCRYPTION_REQUIRED), so without this the\n\t * connect journey cannot complete at all. And even without that setting, a\n\t * fresh backup is better born encrypted than written in clear and protected\n\t * on a second write, which would leave cleartext on the destination in\n\t * between.\n\t *\n\t * Never used against a destination whose backup is already encrypted: that\n\t * one is proven through the password step (or adopted locked under\n\t * `deferUnlock`), so a host secret that happens to differ can neither fail\n\t * the attach nor overwrite what it cannot read. Resolved lazily, so the\n\t * secret is only read when a connect actually needs it. Must satisfy the\n\t * store's password policy.\n\t */\n\tpassword?: string | (() => string | Promise<string>);\n\t/** Deadline for each network leg (inspect, trial-read, attach), ms. */\n\tdeadlineMs?: number;\n}\n\nexport type ConnectStep =\n\t'choose' | 'form' | 'authorizing' | 'password' | 'conflict' | 'connected' | 'error';\n\n/** How to reconcile with a destination that already holds a backup. */\nexport type ConnectResolution = 'merge' | 'resume' | 'replace';\n\n/** What connecting did. 'merged': both sides folded. 'started': empty\n * destination now holds this device's data. 'resumed': the backup won,\n * this device adopted it. 'replaced': this device won, the backup was\n * overwritten. 'manual': degraded download-on-demand file mode. */\nexport type ConnectFlowOutcome = 'merged' | 'started' | 'resumed' | 'replaced' | 'manual';\n\nexport interface ConnectSnapshot {\n\tstep: ConnectStep;\n\t/** The destinations to offer, in the order given. */\n\tkinds: readonly ConnectKind[];\n\t/** The destination being connected (null while choosing). */\n\tkind: ConnectKind | null;\n\t/** Set when step === 'connected'. */\n\toutcome: ConnectFlowOutcome | null;\n\t/** Known after inspection: the destination already holds a backup. */\n\thasBackup: boolean;\n\t/** ...and that backup is encrypted. */\n\tencrypted: boolean;\n\t/** The last submitPassword() was wrong (stay on 'password', say so). */\n\tpasswordError: boolean;\n\t/** An async leg is in flight (render a spinner, disable the actions). */\n\tbusy: boolean;\n\t/** Set when step === 'error': show `labelKey`, log `message`. */\n\terror: StoreError | null;\n}\n\nexport interface ConnectFlow extends FlowStore<ConnectSnapshot> {\n\t/** Pick a destination. Call it inside the user's click: the consent popup /\n\t * file picker opens synchronously from the gesture. Ignored while a leg is\n\t * already in flight (one popup per gesture). For a `file` target declared\n\t * with the object form, `variant` picks the gesture: 'create' (default,\n\t * save picker) or 'open' (adopt an existing backup file). */\n\tchoose(kind: ConnectKind, variant?: 'create' | 'open'): void;\n\t/** Reopen the remembered destination (`options.resume`). Same gesture rule\n\t * as choose - call it inside the click. No-op when nothing is remembered.\n\t * Never raises the conflict step: this backup is already this device's. */\n\tresume(): void;\n\t/** 'form' step (webdav: true): submit the server config. */\n\tsubmitWebdav(config: WebdavConfig): void;\n\t/** 'form' step (s3: true): submit the bucket config. */\n\tsubmitS3(config: S3Config): void;\n\t/** 'password' step: prove the backup's password. Wrong password sets\n\t * passwordError and stays here; nothing is attached either way. */\n\tsubmitPassword(password: string): void;\n\t/** 'password' step escape hatch: give up on a forgotten password and start a\n\t * fresh backup over the protected one. The encrypted backup is overwritten\n\t * by this device's data (possibly empty) and lost for good - the only way\n\t * out when the password cannot be recovered. */\n\toverwrite(): void;\n\t/** 'conflict' step: pick the reconciliation. */\n\tresolveConflict(how: ConnectResolution): void;\n\t/** Abandon the journey in progress: back to 'choose'. A late resolution of\n\t * the abandoned leg is discarded. */\n\tcancel(): void;\n\t/** 'error' step: back to 'choose' to try again. */\n\tretry(): void;\n}\n\nconst DEFAULT_DEADLINE_MS = 30_000;\n\n/** A resolution's attach plan: which strategy runs, which outcome it names. */\nfunction planFor(how: ConnectResolution): {\n\tstrategy: 'merge' | 'replace-local' | 'replace-remote';\n\toutcome: ConnectFlowOutcome;\n} {\n\tif (how === 'resume') return { strategy: 'replace-local', outcome: 'resumed' };\n\tif (how === 'replace') return { strategy: 'replace-remote', outcome: 'replaced' };\n\treturn { strategy: 'merge', outcome: 'merged' };\n}\n\nexport function connectFlow(\n\tstore: StoreLike,\n\ttargets: ConnectTargets,\n\toptions: ConnectFlowOptions = {}\n): ConnectFlow {\n\tconst host = 'flowHost' in store ? store.flowHost : store;\n\tconst deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS;\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 m = makeMachine<ConnectSnapshot>({\n\t\tstep: 'choose',\n\t\tkinds,\n\t\tkind: null,\n\t\toutcome: null,\n\t\thasBackup: false,\n\t\tencrypted: false,\n\t\tpasswordError: false,\n\t\tbusy: false,\n\t\terror: null\n\t});\n\n\t// The journey's working set. `generation` invalidates in-flight legs on\n\t// cancel(): whoever resolves late compares its captured generation and\n\t// drops its result instead of resurrecting an abandoned journey.\n\tlet generation = 0;\n\tlet target: BackupTarget | null = null;\n\tlet password: string | null = null;\n\t// Set at inspection: the destination already holds an ENCRYPTED backup, so\n\t// its key is the one that matters and the host secret must stay out of it.\n\tlet remoteEncrypted = false;\n\t// This journey took the resume offer rather than picking a destination.\n\t// Reset with every other piece of journey state, so a cancelled resume\n\t// followed by an ordinary choice is an ordinary choice.\n\tlet resuming = false;\n\n\tconst backToChoose = (): void => {\n\t\tgeneration++;\n\t\ttarget = null;\n\t\tpassword = null;\n\t\tremoteEncrypted = false;\n\t\tresuming = false;\n\t\tm.set({\n\t\t\tstep: 'choose',\n\t\t\tkind: null,\n\t\t\toutcome: null,\n\t\t\thasBackup: false,\n\t\t\tencrypted: false,\n\t\t\tpasswordError: false,\n\t\t\tbusy: false,\n\t\t\terror: null\n\t\t});\n\t};\n\n\tconst fail = (e: unknown): void => {\n\t\tpassword = null;\n\t\tm.set({ step: 'error', busy: false, error: toStoreError(e) });\n\t};\n\n\t/** The host's own secret, resolved on demand (see options.password). */\n\tconst hostSecret = async (): Promise<string | null> => {\n\t\tconst supplied = options.password;\n\t\tif (supplied === undefined) return null;\n\t\treturn typeof supplied === 'function' ? await supplied() : supplied;\n\t};\n\n\tfunction connectorFor(\n\t\tkind: ConnectKind,\n\t\tvariant: 'create' | 'open' = 'create'\n\t): 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: host.kv, fileName: host.backupName });\n\t\t}\n\t\tif (kind === 'file') {\n\t\t\t// Object form: each gesture has its own spec ('open' has no built-in\n\t\t\t// meaning for other kinds, so it resolves here only).\n\t\t\tif (typeof spec === 'object' && !('url' in spec)) {\n\t\t\t\tconst sub = (spec as { create?: true | Connector; open?: true | Connector })[variant];\n\t\t\t\tif (sub == null) return null;\n\t\t\t\tif (typeof sub === 'function') return sub;\n\t\t\t\treturn variant === 'open'\n\t\t\t\t\t? () => fileOpenExisting({ kv: host.kv })\n\t\t\t\t\t: () => fileConnect({ kv: host.kv, fileName: host.backupName });\n\t\t\t}\n\t\t\treturn () => fileConnect({ kv: host.kv, fileName: host.backupName });\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: host.kv, config });\n\t\t}\n\t\tconst config = spec as WebdavConfig; // 'webdav: true' goes through the form step\n\t\treturn () => webdavConnect({ kv: host.kv, config });\n\t}\n\n\t/** No picker on this browser: download-on-demand instead of a dead button. */\n\tfunction degradeToManual(kind: ConnectKind): void {\n\t\tconst gen = ++generation;\n\t\tm.set({ step: 'authorizing', kind, busy: true, error: null });\n\t\tvoid host.engine\n\t\t\t.setManualFile()\n\t\t\t.then(() => {\n\t\t\t\tif (gen !== generation) return;\n\t\t\t\tm.set({ step: 'connected', outcome: 'manual', busy: false });\n\t\t\t})\n\t\t\t.catch((e) => {\n\t\t\t\tif (gen !== generation) return;\n\t\t\t\tfail(e);\n\t\t\t});\n\t}\n\n\t/** After the target is in hand: inspect, then route to password / conflict /\n\t * attach. Everything here is network - deadline-bounded, generation-checked. */\n\tasync function inspectAndRoute(gen: number, t: BackupTarget): Promise<void> {\n\t\tconst info = await withDeadline(host.engine.inspectTarget(t), deadlineMs, 'The destination');\n\t\tif (gen !== generation) return;\n\t\ttarget = t;\n\t\tremoteEncrypted = info.encrypted;\n\t\tm.set({ hasBackup: info.hasBackup, encrypted: info.encrypted });\n\t\tif (!info.hasBackup) {\n\t\t\t// Empty destination: this device's data becomes its content. Nothing\n\t\t\t// to ask, nothing to lose.\n\t\t\tawait attach(gen, 'replace-remote', 'started');\n\t\t\treturn;\n\t\t}\n\t\tif (info.app !== null && info.app !== host.engine.app) {\n\t\t\t// Another app's backup: say so NOW, before the password step. Asking\n\t\t\t// first taught the user their password \"does not work\" on a file that\n\t\t\t// was never this app's to open - and deferUnlock went further and\n\t\t\t// silently adopted the foreign file as this app's home. The header\n\t\t\t// names its app in cleartext precisely so a wrong pick can be named\n\t\t\t// for what it is; a header that does not say accuses no one.\n\t\t\tm.set({\n\t\t\t\tstep: 'error',\n\t\t\t\tbusy: false,\n\t\t\t\terror: toStoreError(\n\t\t\t\t\tnew SelfstoreError(\n\t\t\t\t\t\t'FOREIGN_BACKUP',\n\t\t\t\t\t\t`This backup was written by \"${info.app}\", not \"${host.engine.app}\". Open it in its own application.`\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (info.encrypted) {\n\t\t\tif (options.deferUnlock) {\n\t\t\t\t// The host owns the unlock surface: adopt the backup as it stands.\n\t\t\t\t// A null password on an encrypted file lands the store locked -\n\t\t\t\t// nothing readable, nothing clobbered - until engine.unlock()\n\t\t\t\t// proves the password wherever the host asks for it. The conflict\n\t\t\t\t// step is moot here: without the key, merge cannot read the remote\n\t\t\t\t// and replace would destroy a backup no one proved they own.\n\t\t\t\tawait attach(gen, 'replace-local', 'resumed');\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Ask before touching anything. The password gates every resolution,\n\t\t\t// including 'replace': overwriting an encrypted backup demands proof\n\t\t\t// of ownership, or a typo away from someone's data.\n\t\t\tm.set({ step: 'password', busy: false, passwordError: false });\n\t\t\treturn;\n\t\t}\n\t\trouteAfterUnlock(gen);\n\t}\n\n\t/** Password proven (or none needed): either ask the conflict question, or\n\t * apply the default resolution - 'merge' unless the host chose otherwise,\n\t * the only default that loses nothing on either side. */\n\tfunction routeAfterUnlock(gen: number): void {\n\t\tif (gen !== generation) return;\n\t\t// `resuming` survives the password detour on purpose: the journey is one\n\t\t// generation, and the question \"is this the backup I already had\" does\n\t\t// not change while its password is being typed. Reopening one's own\n\t\t// backup is not a merge question, so the conflict step never rises on\n\t\t// that path - only on a destination newly pointed at.\n\t\tif (!resuming && options.hasLocalData?.() === true) {\n\t\t\tm.set({ step: 'conflict', busy: false });\n\t\t\treturn;\n\t\t}\n\t\tconst plan = planFor(resuming ? 'resume' : (options.defaultResolution ?? 'merge'));\n\t\tvoid attach(gen, plan.strategy, plan.outcome);\n\t}\n\n\tasync function attach(\n\t\tgen: number,\n\t\tstrategy: 'merge' | 'replace-local' | 'replace-remote',\n\t\toutcome: ConnectFlowOutcome,\n\t\twipe = false\n\t): Promise<void> {\n\t\tif (gen !== generation || !target) return;\n\t\tm.set({ busy: true });\n\t\ttry {\n\t\t\t// A backup that is already encrypted carries its own key: it was proven\n\t\t\t// at the password step, or deliberately left locked. Everywhere else -\n\t\t\t// an empty destination, a plaintext one - the host secret (when it\n\t\t\t// offers one) makes the backup encrypted from its very first write.\n\t\t\tconst secret = password ?? (remoteEncrypted ? null : await hostSecret());\n\t\t\tif (gen !== generation) return;\n\t\t\tawait withDeadline(\n\t\t\t\thost.engine.attachTarget(target, { password: secret, strategy, wipe }),\n\t\t\t\tdeadlineMs,\n\t\t\t\t'The destination'\n\t\t\t);\n\t\t\tif (gen !== generation) return;\n\t\t\tpassword = null;\n\t\t\tm.set({ step: 'connected', outcome, busy: false, error: null });\n\t\t} catch (e) {\n\t\t\tif (gen !== generation) return;\n\t\t\tfail(e);\n\t\t}\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\tresume(): void {\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || (step !== 'choose' && step !== 'error')) return; // one popup per gesture\n\t\t\tconst offer = options.resume;\n\t\t\tif (!offer) return;\n\t\t\tresuming = true;\n\t\t\tconst gen = ++generation;\n\t\t\tm.set({ step: 'authorizing', kind: offer.kind, busy: true, error: null });\n\t\t\t// No degraded mode and no form here: the host handed us a connector\n\t\t\t// that knows exactly which backup to reopen. A null answer is the\n\t\t\t// user closing the consent popup - back to the choice, no error.\n\t\t\tvoid offer\n\t\t\t\t.connect()\n\t\t\t\t.then(async (t) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tif (!t) {\n\t\t\t\t\t\tbackToChoose();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait inspectAndRoute(gen, t);\n\t\t\t\t})\n\t\t\t\t.catch((e) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tfail(e);\n\t\t\t\t});\n\t\t},\n\n\t\tchoose(kind: ConnectKind, variant: 'create' | 'open' = 'create'): void {\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || (step !== 'choose' && step !== 'error')) return; // one popup per gesture\n\t\t\tresuming = false;\n\t\t\tconst connector = connectorFor(kind, variant);\n\t\t\tif (!connector) return;\n\t\t\tif (\n\t\t\t\t(kind === 'webdav' && targets.webdav === true) ||\n\t\t\t\t(kind === 's3' && targets.s3 === true)\n\t\t\t) {\n\t\t\t\tm.set({ step: 'form', kind, error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Degraded manual mode only makes sense for CREATING a backup through\n\t\t\t// the built-in save picker: adopting an existing file needs the real\n\t\t\t// picker, and a custom connector owns its own fallback.\n\t\t\tconst builtinCreate =\n\t\t\t\ttargets.file === true ||\n\t\t\t\t(typeof targets.file === 'object' &&\n\t\t\t\t\t(targets.file as { create?: unknown }).create !== undefined &&\n\t\t\t\t\ttypeof (targets.file as { create?: unknown }).create !== 'function');\n\t\t\tconst canDegrade = kind === 'file' && variant === 'create' && builtinCreate;\n\t\t\tif (canDegrade && !fileIsSupported()) {\n\t\t\t\tdegradeToManual(kind);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst gen = ++generation;\n\t\t\tm.set({ step: 'authorizing', kind, busy: true, error: null });\n\t\t\t// The connector runs the human gesture (popup, picker): no deadline on\n\t\t\t// people. The network legs after it are bounded in inspectAndRoute.\n\t\t\tvoid connector()\n\t\t\t\t.then(async (t) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tif (!t) {\n\t\t\t\t\t\t// The picker was there, and refused: this browser has no file\n\t\t\t\t\t\t// mode, we just could not know before asking. Same landing as\n\t\t\t\t\t\t// a browser without the API - the offer must not send the\n\t\t\t\t\t\t// practitioner back to a button that will refuse again.\n\t\t\t\t\t\tif (canDegrade && !fileIsSupported()) {\n\t\t\t\t\t\t\tdegradeToManual(kind);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Drive/file/custom answer null for a cancelled popup or picker:\n\t\t\t\t\t\t// not an error, no message. A FIXED webdav/s3 config answering\n\t\t\t\t\t\t// null means the server did not: that one is a retryable error.\n\t\t\t\t\t\tif (kind === 'webdav' || kind === 's3') {\n\t\t\t\t\t\t\tfail(\n\t\t\t\t\t\t\t\tnew SelfstoreError(\n\t\t\t\t\t\t\t\t\t'TARGET_UNAVAILABLE',\n\t\t\t\t\t\t\t\t\t'The server did not answer (address, credentials or CORS).'\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbackToChoose();\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait inspectAndRoute(gen, t);\n\t\t\t\t})\n\t\t\t\t.catch((e) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tfail(e);\n\t\t\t\t});\n\t\t},\n\n\t\tsubmitWebdav(config: WebdavConfig): void {\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || step !== 'form') return;\n\t\t\tconst gen = ++generation;\n\t\t\tm.set({ step: 'authorizing', busy: true });\n\t\t\tvoid withDeadline(webdavConnect({ kv: host.kv, config }), deadlineMs, 'The WebDAV server')\n\t\t\t\t.then(async (t) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tif (!t) {\n\t\t\t\t\t\t// webdavConnect answers null when the server does not: with an\n\t\t\t\t\t\t// explicit config this is a wrong-address error, not a cancel.\n\t\t\t\t\t\tfail(\n\t\t\t\t\t\t\tnew SelfstoreError(\n\t\t\t\t\t\t\t\t'TARGET_UNAVAILABLE',\n\t\t\t\t\t\t\t\t'The WebDAV server did not answer (URL, credentials or CORS).'\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait inspectAndRoute(gen, t);\n\t\t\t\t})\n\t\t\t\t.catch((e) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tfail(e);\n\t\t\t\t});\n\t\t},\n\n\t\tsubmitS3(config: S3Config): void {\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || step !== 'form') return;\n\t\t\tconst gen = ++generation;\n\t\t\tm.set({ step: 'authorizing', busy: true });\n\t\t\tvoid withDeadline(s3Connect({ kv: host.kv, config }), deadlineMs, 'The S3 endpoint')\n\t\t\t\t.then(async (t) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tif (!t) {\n\t\t\t\t\t\t// s3Connect answers null when the endpoint does not: with an\n\t\t\t\t\t\t// explicit config this is a wrong-config error, not a cancel.\n\t\t\t\t\t\tfail(\n\t\t\t\t\t\t\tnew SelfstoreError(\n\t\t\t\t\t\t\t\t'TARGET_UNAVAILABLE',\n\t\t\t\t\t\t\t\t'The S3 endpoint did not answer (endpoint, credentials or CORS).'\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait inspectAndRoute(gen, t);\n\t\t\t\t})\n\t\t\t\t.catch((e) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tfail(e);\n\t\t\t\t});\n\t\t},\n\n\t\tsubmitPassword(pw: string): void {\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || step !== 'password' || !target) return;\n\t\t\tconst gen = generation;\n\t\t\tm.set({ busy: true, passwordError: false });\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// PROVE the password against the real backup while the store is\n\t\t\t\t\t// still untouched: download and trial-read. A wrong password is a\n\t\t\t\t\t// retype, never a locked store or a gate.\n\t\t\t\t\tconst blob = await withDeadline(target!.load(), deadlineMs, 'The destination');\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tif (!blob) {\n\t\t\t\t\t\tthrow new SelfstoreError('TARGET_UNAVAILABLE', 'The backup could not be downloaded.');\n\t\t\t\t\t}\n\t\t\t\t\tawait restore(blob).withPassword(pw).read();\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tpassword = pw;\n\t\t\t\t\tm.set({ busy: false });\n\t\t\t\t\trouteAfterUnlock(gen);\n\t\t\t\t} catch (e) {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tif (isSelfstoreError(e) && e.code === 'DECRYPT_FAILED') {\n\t\t\t\t\t\tm.set({ busy: false, passwordError: true }); // stay, say so, retype\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tfail(e);\n\t\t\t\t}\n\t\t\t})();\n\t\t},\n\n\t\toverwrite(): void {\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || step !== 'password' || !target) return;\n\t\t\t// The password is forgotten: stop proving it and START fresh over the\n\t\t\t// protected backup. wipe:true empties local first (fresh sync meta), so\n\t\t\t// this writes a blank new backup - never this device's existing data -\n\t\t\t// and the owner is never stuck behind a lost secret. A blank write is\n\t\t\t// also tiny, so it cannot time out the way pushing a large local store\n\t\t\t// would.\n\t\t\tpassword = null;\n\t\t\tvoid attach(generation, 'replace-remote', 'replaced', true);\n\t\t},\n\n\t\tresolveConflict(how: ConnectResolution): void {\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || step !== 'conflict') return;\n\t\t\tconst plan = planFor(how);\n\t\t\tvoid attach(generation, plan.strategy, plan.outcome);\n\t\t},\n\n\t\tcancel(): void {\n\t\t\tif (m.snapshot.step === 'connected') return; // done is done\n\t\t\tbackToChoose();\n\t\t},\n\n\t\tretry(): void {\n\t\t\tif (m.snapshot.step !== 'error') return;\n\t\t\tbackToChoose();\n\t\t}\n\t};\n}\n","// shareFlow: the \"who can see this\" panel as a headless machine over an\n// injected ShareEngine - how links travel (Drive public link, relay,\n// mailbox) is the app's business. The flow owns what every such panel gets\n// wrong first: a transient listing failure never blanks it (last-known\n// links stay, flagged stale - a member who saw an empty panel once will\n// revoke something in panic); one operation at a time, so a double-click\n// cannot create two links; removals leave the snapshot only after the\n// engine confirmed them, never optimistically; removing yourself is refused\n// up front (leaving is its own journey, not a row action). Every leg runs\n// under a deadline and reports the stable { code, labelKey, message } shape.\n\nimport type { StoreError } from '../persistence/store';\nimport { makeMachine, toStoreError, withDeadline, type FlowStore } from './machine';\n\nexport type ShareLevel = 'read' | 'write';\n\nexport interface ShareLinkInfo {\n\t/** Engine-scoped stable id (revoke keys on it). */\n\tid: string;\n\t/** The invitation URL to hand out. */\n\turl: string;\n\tlevel: ShareLevel;\n}\n\nexport interface ShareMemberInfo {\n\t/** Engine-scoped stable id (removeMember keys on it). */\n\tid: string;\n\tlabel?: string;\n\t/** This row is the local user. */\n\tself?: boolean;\n\t/** This row owns the share (the app may render a badge, the flow does not care). */\n\towner?: boolean;\n}\n\n/** The app-side port the flow drives. Implementations wrap whatever transport\n * the app shares through; every method may throw (SelfstoreError preferred,\n * anything is normalized). */\nexport interface ShareEngine {\n\t/** The current truth: active links and members. */\n\tlist(): Promise<{ links: ShareLinkInfo[]; members: ShareMemberInfo[] }>;\n\tcreateLink(opts: { level: ShareLevel }): Promise<ShareLinkInfo>;\n\trevokeLink(id: string): Promise<void>;\n\t/** Optional: engines without member management simply omit it. */\n\tremoveMember?(id: string): Promise<void>;\n\t/** Optional: END the whole share in one gesture - every link dead, every\n\t * member out. Engines whose transport has such a move (an invitation to\n\t * kill, a file to re-privatize) expose it; the widget renders the action\n\t * only then. */\n\trevokeAll?(): Promise<void>;\n}\n\nexport type ShareBusy = 'refresh' | 'create' | 'revoke' | 'remove' | 'revoke-all' | null;\n\nexport interface ShareSnapshot {\n\tlinks: readonly ShareLinkInfo[];\n\tmembers: readonly ShareMemberInfo[];\n\t/** The operation in flight, or null. */\n\tbusy: ShareBusy;\n\t/** The last refresh failed: the lists shown are the last-known good ones. */\n\tstale: boolean;\n\t/** The last operation's problem (the lists are never cleared by an error). */\n\terror: StoreError | null;\n\t/** True once the engine offers removeMember (render the row action at all). */\n\tcanRemoveMembers: boolean;\n\t/** True once the engine offers revokeAll (render the end-the-share action). */\n\tcanRevokeAll: boolean;\n}\n\nexport interface ShareFlow extends FlowStore<ShareSnapshot> {\n\t/** Re-list from the engine. Failure keeps the last-known lists (stale). */\n\trefresh(): Promise<void>;\n\t/** Create an invitation link. Resolves to it, or null when refused (an\n\t * operation was already in flight). */\n\tcreateLink(opts: { level: ShareLevel }): Promise<ShareLinkInfo | null>;\n\t/** Revoke a link. The link leaves the snapshot only once the engine\n\t * confirmed. Resolves to whether it did. */\n\trevokeLink(id: string): Promise<boolean>;\n\t/** Remove a member (never yourself - refused up front). Resolves to whether\n\t * the engine confirmed. */\n\tremoveMember(id: string): Promise<boolean>;\n\t/** End the whole share: every link and member leaves the snapshot only\n\t * once the engine confirmed. Resolves to whether it did. */\n\trevokeAll(): Promise<boolean>;\n}\n\nconst DEFAULT_DEADLINE_MS = 30_000;\n\nexport function shareFlow(engine: ShareEngine, options: { deadlineMs?: number } = {}): ShareFlow {\n\tconst deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS;\n\n\tconst m = makeMachine<ShareSnapshot>({\n\t\tlinks: [],\n\t\tmembers: [],\n\t\tbusy: 'refresh',\n\t\tstale: false,\n\t\terror: null,\n\t\tcanRemoveMembers: typeof engine.removeMember === 'function',\n\t\tcanRevokeAll: typeof engine.revokeAll === 'function'\n\t});\n\n\t/** Serialize the operations: one at a time, the others answer \"refused\". */\n\tfunction begin(op: Exclude<ShareBusy, null>): boolean {\n\t\tif (m.snapshot.busy !== null && !(op === 'refresh' && m.snapshot.busy === 'refresh')) {\n\t\t\treturn false;\n\t\t}\n\t\tm.set({ busy: op });\n\t\treturn true;\n\t}\n\n\t/** The listing in flight, shared by every refresh asked meanwhile. Two\n\t * listings at once let the first clear `busy` while the second ran, so a\n\t * revoke slipped in between and the second's older answer brought the\n\t * revoked link back. */\n\tlet listing: Promise<void> | null = null;\n\n\tfunction doRefresh(): Promise<void> {\n\t\tlisting ??= (async () => {\n\t\t\ttry {\n\t\t\t\tconst { links, members } = await withDeadline(\n\t\t\t\t\tengine.list(),\n\t\t\t\t\tdeadlineMs,\n\t\t\t\t\t'The share listing'\n\t\t\t\t);\n\t\t\t\tm.set({ links, members, busy: null, stale: false, error: null });\n\t\t\t} catch (e) {\n\t\t\t\t// Keep the last-known lists: a hiccup must never paint the share as\n\t\t\t\t// gone. Just say the view may be behind.\n\t\t\t\tm.set({ busy: null, stale: true, error: toStoreError(e) });\n\t\t\t} finally {\n\t\t\t\tlisting = null;\n\t\t\t}\n\t\t})();\n\t\treturn listing;\n\t}\n\n\tconst flow: ShareFlow = {\n\t\tget snapshot() {\n\t\t\treturn m.snapshot;\n\t\t},\n\t\tsubscribe: m.subscribe,\n\n\t\tasync refresh(): Promise<void> {\n\t\t\tif (m.snapshot.busy !== null && m.snapshot.busy !== 'refresh') return;\n\t\t\tm.set({ busy: 'refresh' });\n\t\t\tawait doRefresh();\n\t\t},\n\n\t\tasync createLink(opts): Promise<ShareLinkInfo | null> {\n\t\t\tif (!begin('create')) return null;\n\t\t\ttry {\n\t\t\t\tconst link = await withDeadline(engine.createLink(opts), deadlineMs, 'Creating the link');\n\t\t\t\tm.set({ links: [...m.snapshot.links, link], busy: null, error: null });\n\t\t\t\treturn link;\n\t\t\t} catch (e) {\n\t\t\t\tm.set({ busy: null, error: toStoreError(e) });\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\n\t\tasync revokeLink(id: string): Promise<boolean> {\n\t\t\tif (!begin('revoke')) return false;\n\t\t\ttry {\n\t\t\t\tawait withDeadline(engine.revokeLink(id), deadlineMs, 'Revoking the link');\n\t\t\t\t// Confirmed: NOW it leaves the snapshot.\n\t\t\t\tm.set({ links: m.snapshot.links.filter((l) => l.id !== id), busy: null, error: null });\n\t\t\t\treturn true;\n\t\t\t} catch (e) {\n\t\t\t\tm.set({ busy: null, error: toStoreError(e) });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\n\t\tasync removeMember(id: string): Promise<boolean> {\n\t\t\tconst remove = engine.removeMember?.bind(engine);\n\t\t\tif (!remove) {\n\t\t\t\tthrow new TypeError('shareFlow: this engine does not manage members (no removeMember).');\n\t\t\t}\n\t\t\tconst row = m.snapshot.members.find((mb) => mb.id === id);\n\t\t\tif (row?.self) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t'shareFlow: refusing to remove yourself - leaving a share is its own journey, not a row action.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!begin('remove')) return false;\n\t\t\ttry {\n\t\t\t\tawait withDeadline(remove(id), deadlineMs, 'Removing the member');\n\t\t\t\tm.set({\n\t\t\t\t\tmembers: m.snapshot.members.filter((mb) => mb.id !== id),\n\t\t\t\t\tbusy: null,\n\t\t\t\t\terror: null\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t} catch (e) {\n\t\t\t\tm.set({ busy: null, error: toStoreError(e) });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\n\t\tasync revokeAll(): Promise<boolean> {\n\t\t\tconst revoke = engine.revokeAll?.bind(engine);\n\t\t\tif (!revoke) {\n\t\t\t\tthrow new TypeError('shareFlow: this engine does not end the share (no revokeAll).');\n\t\t\t}\n\t\t\tif (!begin('revoke-all')) return false;\n\t\t\ttry {\n\t\t\t\tawait withDeadline(revoke(), deadlineMs, 'Ending the share');\n\t\t\t\t// Confirmed: the share is over, every link and member with it.\n\t\t\t\tm.set({ links: [], members: [], busy: null, error: null });\n\t\t\t\treturn true;\n\t\t\t} catch (e) {\n\t\t\t\tm.set({ busy: null, error: toStoreError(e) });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t};\n\n\t// The panel opens on the truth: list right away (failures land as stale).\n\tvoid doRefresh();\n\n\treturn flow;\n}\n","// joinFlow: opening an invitation link, as a headless machine over an\n// injected JoinEngine. Preview first, join on an explicit yes. The flow\n// holds the link itself, so a re-render, a login roundtrip or an account\n// switch cannot lose the invitation out from under the user. Joining is\n// single-flight. Mismatch (\"this device already follows another share\") and\n// no-invite (\"spent, or not for this account\") are outcomes with their own\n// steps, not generic errors - each has a different way out. Switching\n// accounts re-previews under the new account. A cancelled or superseded leg\n// cannot resurrect the flow, and every network leg runs under a deadline.\n\nimport type { StoreError } from '../persistence/store';\nimport { makeMachine, toStoreError, withDeadline, type FlowStore } from './machine';\nimport type { ShareLevel } from './share';\n\n/** What the user is about to join - enough to render an honest question. */\nexport interface JoinPreview {\n\t/** The share's display name, when the link carries one. */\n\tlabel?: string;\n\t/** Who shared it (a name, an email - whatever the engine knows). */\n\tfrom?: string;\n\tlevel?: ShareLevel;\n}\n\n/** How joining ended, other than plain failure:\n * 'joined' - this device now follows the share.\n * 'mismatch' - this device already follows a DIFFERENT share: joining a\n * second one is refused (leave the first, or use another\n * device/profile).\n * 'no-invite' - the invitation is spent, expired, or aimed at another\n * account (switching accounts may be the fix). */\nexport type JoinOutcome = 'joined' | 'mismatch' | 'no-invite';\n\n/** The app-side port: how a link is read and honoured on this transport. */\nexport interface JoinEngine {\n\t/** Read-only look at the invitation. Must change NOTHING. */\n\tpreview(link: string): Promise<JoinPreview>;\n\t/** Consume the invitation and wire the share. Resolve null when the user\n\t * backed out of a popup the engine had to open (an account chooser, a\n\t * consent screen): the flow returns to 'ready' silently - backing out of\n\t * a human gesture is not an error, here as everywhere in these flows. */\n\tjoin(link: string): Promise<JoinOutcome | null>;\n\t/** Optional: re-run the account chooser (the invite may belong to another\n\t * account). The flow re-previews after it. */\n\tswitchAccount?(): Promise<void>;\n}\n\nexport type JoinStep =\n\t'previewing' | 'ready' | 'joining' | 'joined' | 'mismatch' | 'no-invite' | 'error';\n\nexport interface JoinSnapshot {\n\t/** The invitation this flow was opened with - held here so the UI can\n\t * always find it again. */\n\tlink: string;\n\tstep: JoinStep;\n\t/** Set once previewed (kept through errors, so retry re-renders context). */\n\tpreview: JoinPreview | null;\n\t/** The engine offers an account switch. */\n\tcanSwitchAccount: boolean;\n\tbusy: boolean;\n\t/** Set when step === 'error': show `labelKey`, log `message`. */\n\terror: StoreError | null;\n}\n\nexport interface JoinFlow extends FlowStore<JoinSnapshot> {\n\t/** Say yes: consume the invitation. Only from 'ready'. */\n\taccept(): void;\n\t/** Re-run the account chooser, then preview again under the new account.\n\t * Available from 'ready', 'no-invite' and 'error' when the engine offers it. */\n\tswitchAccount(): void;\n\t/** From 'error': preview again and resume the journey. */\n\tretry(): void;\n}\n\nconst DEFAULT_DEADLINE_MS = 30_000;\n\nexport function joinFlow(\n\tlink: string,\n\tengine: JoinEngine,\n\toptions: { deadlineMs?: number } = {}\n): JoinFlow {\n\tconst deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS;\n\n\tconst m = makeMachine<JoinSnapshot>({\n\t\tlink,\n\t\tstep: 'previewing',\n\t\tpreview: null,\n\t\tcanSwitchAccount: typeof engine.switchAccount === 'function',\n\t\tbusy: true,\n\t\terror: null\n\t});\n\n\tlet generation = 0;\n\n\tfunction preview(): void {\n\t\tconst gen = ++generation;\n\t\tm.set({ step: 'previewing', busy: true, error: null });\n\t\tvoid withDeadline(engine.preview(link), deadlineMs, 'The invitation')\n\t\t\t.then((p) => {\n\t\t\t\tif (gen !== generation) return;\n\t\t\t\tm.set({ step: 'ready', preview: p, busy: false });\n\t\t\t})\n\t\t\t.catch((e) => {\n\t\t\t\tif (gen !== generation) return;\n\t\t\t\tm.set({ step: 'error', busy: false, error: toStoreError(e) });\n\t\t\t});\n\t}\n\n\tpreview();\n\n\treturn {\n\t\tget snapshot() {\n\t\t\treturn m.snapshot;\n\t\t},\n\t\tsubscribe: m.subscribe,\n\n\t\taccept(): void {\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || step !== 'ready') return; // single-flight: one join per yes\n\t\t\tconst gen = ++generation;\n\t\t\tm.set({ step: 'joining', busy: true, error: null });\n\t\t\tvoid withDeadline(engine.join(link), deadlineMs, 'Joining')\n\t\t\t\t.then((outcome) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tif (outcome === null) {\n\t\t\t\t\t\t// The user backed out of the engine's popup: the question is\n\t\t\t\t\t\t// still live, the preview still good - just ask again.\n\t\t\t\t\t\tm.set({ step: 'ready', busy: false });\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tm.set({ step: outcome, busy: false });\n\t\t\t\t})\n\t\t\t\t.catch((e) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tm.set({ step: 'error', busy: false, error: toStoreError(e) });\n\t\t\t\t});\n\t\t},\n\n\t\tswitchAccount(): void {\n\t\t\tconst switcher = engine.switchAccount?.bind(engine);\n\t\t\tif (!switcher) return;\n\t\t\tconst { step, busy } = m.snapshot;\n\t\t\tif (busy || (step !== 'ready' && step !== 'no-invite' && step !== 'error')) return;\n\t\t\tconst gen = ++generation;\n\t\t\tm.set({ busy: true, error: null });\n\t\t\tvoid switcher()\n\t\t\t\t.then(() => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tpreview(); // the other account may hold the right invite: look again\n\t\t\t\t})\n\t\t\t\t.catch((e) => {\n\t\t\t\t\tif (gen !== generation) return;\n\t\t\t\t\tm.set({ step: 'error', busy: false, error: toStoreError(e) });\n\t\t\t\t});\n\t\t},\n\n\t\tretry(): void {\n\t\t\tif (m.snapshot.step !== 'error') return;\n\t\t\tpreview();\n\t\t}\n\t};\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as StoreError, B as BackupTarget, L as LocalStore } from './store-
|
|
2
|
-
import { K as KV } from './cache-
|
|
3
|
-
import { a as DriveAuth } from './drive-
|
|
4
|
-
import { W as WebdavConfig, S as S3Config } from './s3-
|
|
1
|
+
import { S as StoreError, B as BackupTarget, L as LocalStore } from './store-bp1VRW7R.js';
|
|
2
|
+
import { K as KV } from './cache-DBIn_Uo8.js';
|
|
3
|
+
import { a as DriveAuth } from './drive-BCRil91Q.js';
|
|
4
|
+
import { W as WebdavConfig, S as S3Config } from './s3-kikEHI-g.js';
|
|
5
5
|
import { F as FlowStore } from './machine-Dhbx3Eut.js';
|
|
6
6
|
|
|
7
7
|
type ConnectKind = 'drive' | 'file' | 'webdav' | 's3';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as BackupTarget } from './store-
|
|
2
|
-
import { K as KV } from './cache-
|
|
1
|
+
import { B as BackupTarget } from './store-bp1VRW7R.js';
|
|
2
|
+
import { K as KV } from './cache-DBIn_Uo8.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Google Drive backup target. Does the Drive REST file operations (find-or-create,
|
package/dist/flows.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { b as ConnectKind, S as StoreLike, C as ConnectTargets } from './connect-
|
|
2
|
-
export { c as ConnectFlow, a as ConnectFlowOptions, d as ConnectFlowOutcome, e as ConnectResolution, f as ConnectSnapshot, g as ConnectStep, h as Connector, F as FlowHost, R as ResumeOffer, i as connectFlow } from './connect-
|
|
1
|
+
import { b as ConnectKind, S as StoreLike, C as ConnectTargets } from './connect-CdlhiTBy.js';
|
|
2
|
+
export { c as ConnectFlow, a as ConnectFlowOptions, d as ConnectFlowOutcome, e as ConnectResolution, f as ConnectSnapshot, g as ConnectStep, h as Connector, F as FlowHost, R as ResumeOffer, i as connectFlow } from './connect-CdlhiTBy.js';
|
|
3
3
|
import { StoreError, ReplicaState, BackupTarget } from './advanced.js';
|
|
4
4
|
import { F as FlowStore } from './machine-Dhbx3Eut.js';
|
|
5
5
|
export { w as withDeadline } from './machine-Dhbx3Eut.js';
|
|
6
6
|
import { WebdavConfig, S3Config } from './advanced.js';
|
|
7
|
-
import './cache-
|
|
8
|
-
import './drive-
|
|
7
|
+
import './cache-DBIn_Uo8.js';
|
|
8
|
+
import './drive-BCRil91Q.js';
|
|
9
9
|
import './sync.js';
|
|
10
10
|
|
|
11
11
|
/** The one replica this flow manages, as its id on the engine. */
|
package/dist/flows.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { connectFlow, joinFlow, shareFlow } from './chunk-
|
|
1
|
+
export { connectFlow, joinFlow, shareFlow } from './chunk-UU4PS3UH.js';
|
|
2
2
|
import { s3Connect, webdavConnect, webdavFromSession, s3FromSession } from './chunk-TNYL6FNW.js';
|
|
3
3
|
import { connect as connect$1, fromSession as fromSession$1 } from './chunk-HVHQLPP6.js';
|
|
4
4
|
import { makeMachine, toStoreError } from './chunk-2BNTCXQN.js';
|
package/dist/groups.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { G as GroupIdentity } from './cache-
|
|
1
|
+
import { G as GroupIdentity } from './cache-DBIn_Uo8.js';
|
|
2
2
|
import { KV } from './advanced.js';
|
|
3
|
-
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-
|
|
3
|
+
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-DBIn_Uo8.js';
|
|
4
4
|
|
|
5
5
|
interface IdentityVault {
|
|
6
6
|
/** The stored identity, or null when none exists yet. On a protected
|
package/dist/households.d.ts
CHANGED
|
@@ -215,6 +215,13 @@ declare function createHouseholdGroup(deps: {
|
|
|
215
215
|
store: LocalStore;
|
|
216
216
|
kv: KV;
|
|
217
217
|
backend: ShareBackend;
|
|
218
|
+
/** The cache's sealed space (`cache.sealedKv`). Given, the membership
|
|
219
|
+
* record - K, the mailbox id, the invitation capability: the secrets that
|
|
220
|
+
* ARE the membership - lives there, ciphertext at rest and, on a locked
|
|
221
|
+
* cache, unreadable until the session unlocks. A record a previous version
|
|
222
|
+
* wrote in the clear `kv` moves over on first load. Omitted, the record
|
|
223
|
+
* stays in `kv` as before. */
|
|
224
|
+
sealedKv?: KV;
|
|
218
225
|
/** KV key the group record lives under. Default HOUSEHOLD_GROUP_KEY. */
|
|
219
226
|
storageKey?: string;
|
|
220
227
|
/** The currently attached wallet file id, for multi-backup hosts. When
|
package/dist/households.js
CHANGED
|
@@ -121,12 +121,22 @@ var mirrorId = (group) => `household-copy:${group.myCopy.fileId}`;
|
|
|
121
121
|
function createHouseholdGroup(deps) {
|
|
122
122
|
const { store, kv, backend, wallet } = deps;
|
|
123
123
|
const storageKey = deps.storageKey ?? HOUSEHOLD_GROUP_KEY;
|
|
124
|
+
const recordKv = deps.sealedKv ?? kv;
|
|
124
125
|
let groups = null;
|
|
125
126
|
let loaded = false;
|
|
126
127
|
const rosterMisses = /* @__PURE__ */ new Map();
|
|
128
|
+
async function readRecord() {
|
|
129
|
+
const sealed = await recordKv.get(storageKey);
|
|
130
|
+
if (sealed !== void 0 || recordKv === kv) return sealed;
|
|
131
|
+
const clear = await kv.get(storageKey);
|
|
132
|
+
if (clear === void 0) return void 0;
|
|
133
|
+
await recordKv.set(storageKey, clear);
|
|
134
|
+
await kv.del(storageKey);
|
|
135
|
+
return clear;
|
|
136
|
+
}
|
|
127
137
|
async function load() {
|
|
128
138
|
if (!loaded) {
|
|
129
|
-
const raw = await
|
|
139
|
+
const raw = await readRecord();
|
|
130
140
|
if (!raw) groups = [];
|
|
131
141
|
else if (raw.v === 2) groups = raw.groups;
|
|
132
142
|
else groups = [raw];
|
|
@@ -138,10 +148,10 @@ function createHouseholdGroup(deps) {
|
|
|
138
148
|
loaded = true;
|
|
139
149
|
if (!groups || groups.length === 0) {
|
|
140
150
|
groups = groups ?? [];
|
|
141
|
-
await
|
|
151
|
+
await recordKv.del(storageKey);
|
|
142
152
|
return;
|
|
143
153
|
}
|
|
144
|
-
await
|
|
154
|
+
await recordKv.set(storageKey, { v: 2, groups });
|
|
145
155
|
}
|
|
146
156
|
const adminGroup = (list) => list.find((g) => g.role === "admin") ?? null;
|
|
147
157
|
const primaryGroup = (list) => adminGroup(list) ?? list[0] ?? null;
|
package/dist/households.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/households/codec.ts","../src/households/group.ts"],"names":[],"mappings":"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","file":"households.js","sourcesContent":["// Household share transport (capability-link model): the plain shapes the\n// bulletin and the copy-announce carry, plus a dependency-free base64url\n// codec. The link key K is the shared group secret - possession is\n// membership, no per-member identity, no signed manifest. Payloads carry\n// only file ids of link-readable files whose content is encrypted under K,\n// so an id alone reveals nothing. Pure by contract: no secret held, no I/O,\n// no framework - shapes, validation and transport strings only.\n\n// --- Errors -----------------------------------------------------------------\n\n/** Why a code was refused. `malformed` covers bad transport, wrong structure or\n * a code decoded as the wrong kind of payload; `unsupported-version` is a\n * numeric `v` this build does not understand. */\nexport type HouseholdCodeErrorCode = 'malformed' | 'unsupported-version';\n\n/** The single typed error this module throws, so callers branch on `code`\n * instead of parsing messages. A decoder that throws never returns a partially\n * parsed object - it fails whole. */\nexport class HouseholdCodeError extends Error {\n\treadonly code: HouseholdCodeErrorCode;\n\tconstructor(code: HouseholdCodeErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'HouseholdCodeError';\n\t\tthis.code = code;\n\t\tObject.setPrototypeOf(this, HouseholdCodeError.prototype);\n\t}\n}\n\nconst malformed = (message: string): HouseholdCodeError =>\n\tnew HouseholdCodeError('malformed', message);\n\n// --- Types ------------------------------------------------------------------\n\n/** Where a member published their encrypted copy. A discriminated union on\n * `provider` so a future transport (WebDAV, ...) slots in beside Drive without\n * changing the payload shapes. Today: Drive only. */\nexport type CopyLink = DriveCopyLink;\n\n/** A copy hosted on the owner's Google Drive. `fileId` is the Drive file id (a\n * public-link file, ciphertext under K); the owner fields are a cosmetic \"on\n * X's Drive\" hint, both optional. */\nexport interface DriveCopyLink {\n\tprovider: 'drive';\n\tfileId: string;\n\townerEmail?: string;\n\townerName?: string;\n}\n\n/** The bulletin's control record (rides one reserved collection inside the\n * K-encrypted bulletin): where members announce their copy (`mailboxId`, a\n * secret only K-holders can read) and the roster of every known copy (the\n * admin's own first). */\nexport interface SharePayload {\n\tv: 1;\n\tmailboxId: string;\n\troster: CopyLink[];\n}\n\n/** Member -> admin, through the announce mailbox: \"here is my copy, fold it\".\n * No identity, no signature: knowing the mailboxId (which lives inside the\n * K-encrypted bulletin) already proves the sender holds K. */\nexport interface AnnouncePayload {\n\tv: 1;\n\tcopy: CopyLink;\n}\n\n// --- base64url (isomorphic, dependency-free) --------------------------------\n\nconst utf8Encoder = new TextEncoder();\n// `fatal` makes a truncated or corrupted byte run throw instead of silently\n// yielding U+FFFD, so a mangled code fails fast in the decoder.\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: true });\n\nfunction bytesToBinary(bytes: Uint8Array): string {\n\tlet out = '';\n\tfor (let i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]);\n\treturn out;\n}\n\nfunction binaryToBytes(binary: string): Uint8Array {\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n\treturn bytes;\n}\n\n/** base64url without padding of raw bytes. btoa + TextEncoder work in the\n * browser and in Node 20+, so the codec stays dependency-free and isomorphic. */\nfunction bytesToBase64url(bytes: Uint8Array): string {\n\treturn btoa(bytesToBinary(bytes)).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction base64urlEncode(text: string): string {\n\treturn bytesToBase64url(utf8Encoder.encode(text));\n}\n\n/** Inverse of base64urlEncode. Throws on any character outside the alphabet or\n * on an invalid UTF-8 byte run; decodeJson turns that into a 'malformed'. */\nfunction base64urlDecode(code: string): string {\n\tconst b64 = code.replace(/-/g, '+').replace(/_/g, '/');\n\tconst padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n\treturn utf8Decoder.decode(binaryToBytes(atob(padded)));\n}\n\n/** A short, url-safe, random id (9 bytes -> 12 base64url chars) from WebCrypto -\n * used for the announce mailbox id. This repo bans Math.random; getRandomValues\n * is the sanctioned entropy source and exists in every browser and Node 20+. */\nexport function randomId(): string {\n\tconst bytes = new Uint8Array(9);\n\tcrypto.getRandomValues(bytes);\n\treturn bytesToBase64url(bytes);\n}\n\n// --- Structural guards ------------------------------------------------------\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n\treturn typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction isString(v: unknown): v is string {\n\treturn typeof v === 'string';\n}\n\n/** Enforce the format version. A numeric `v` other than 1 is a code from a\n * future/older build (unsupported-version); anything else is garbage. */\nfunction requireV1(v: unknown): void {\n\tif (v === 1) return;\n\tif (typeof v === 'number') {\n\t\tthrow new HouseholdCodeError(\n\t\t\t'unsupported-version',\n\t\t\t`payload version ${v} is not supported (expected 1)`\n\t\t);\n\t}\n\tthrow malformed('missing or non-numeric version field');\n}\n\n/** Decode base64url -> JSON, mapping any failure to a single 'malformed'. */\nfunction decodeJson(code: string): unknown {\n\tif (typeof code !== 'string' || code.length === 0)\n\t\tthrow malformed('code must be a non-empty string');\n\tlet json: string;\n\ttry {\n\t\tjson = base64urlDecode(code);\n\t} catch {\n\t\tthrow malformed('code is not valid base64url');\n\t}\n\ttry {\n\t\treturn JSON.parse(json) as unknown;\n\t} catch {\n\t\tthrow malformed('code payload is not valid JSON');\n\t}\n}\n\n/** Rebuild a CopyLink field by field, so a decoded link carries exactly the\n * known keys (no smuggled extras) and every present field has the right type. */\nexport function toCopyLink(v: unknown): CopyLink {\n\tif (!isRecord(v)) throw malformed('copy link is not an object');\n\tif (v.provider === 'drive') {\n\t\tif (!isString(v.fileId)) throw malformed('drive copy link needs a string fileId');\n\t\tif (v.ownerEmail !== undefined && !isString(v.ownerEmail))\n\t\t\tthrow malformed('copy link ownerEmail must be a string');\n\t\tif (v.ownerName !== undefined && !isString(v.ownerName))\n\t\t\tthrow malformed('copy link ownerName must be a string');\n\t\tconst link: DriveCopyLink = { provider: 'drive', fileId: v.fileId };\n\t\tif (isString(v.ownerEmail)) link.ownerEmail = v.ownerEmail;\n\t\tif (isString(v.ownerName)) link.ownerName = v.ownerName;\n\t\treturn link;\n\t}\n\tthrow malformed(`unknown copy-link provider ${JSON.stringify(v.provider)}`);\n}\n\n/** Every entry of a roster array validated as a CopyLink, de-duplicated by\n * fileId (first occurrence wins). An empty roster is structurally valid.\n * Exported for the backend, which revalidates the roster it re-publishes. */\nexport function toRoster(v: unknown): CopyLink[] {\n\tif (!Array.isArray(v)) throw malformed('roster is not an array');\n\tconst seen = new Set<string>();\n\tconst out: CopyLink[] = [];\n\tfor (const entry of v) {\n\t\tconst link = toCopyLink(entry);\n\t\tif (seen.has(link.fileId)) continue;\n\t\tseen.add(link.fileId);\n\t\tout.push(link);\n\t}\n\treturn out;\n}\n\n// --- Codec ------------------------------------------------------------------\n// Transport = base64url(no padding) of compact JSON. Each encoder is a pure,\n// deterministic function of its input; each decoder validates the version and\n// the full structure and throws HouseholdCodeError on anything it cannot trust.\n\nexport function encodeShare(payload: SharePayload): string {\n\treturn base64urlEncode(JSON.stringify(payload));\n}\n\nexport function decodeShare(code: string): SharePayload {\n\tconst raw = decodeJson(code);\n\tif (!isRecord(raw)) throw malformed('share payload is not an object');\n\trequireV1(raw.v);\n\tif (!isString(raw.mailboxId)) throw malformed('share payload needs a string mailboxId');\n\treturn { v: 1, mailboxId: raw.mailboxId, roster: toRoster(raw.roster) };\n}\n\nexport function encodeAnnounce(payload: AnnouncePayload): string {\n\treturn base64urlEncode(JSON.stringify(payload));\n}\n\nexport function decodeAnnounce(code: string): AnnouncePayload {\n\tconst raw = decodeJson(code);\n\tif (!isRecord(raw)) throw malformed('announce is not an object');\n\trequireV1(raw.v);\n\treturn { v: 1, copy: toCopyLink(raw.copy) };\n}\n","// Household sharing, mirror model: read-write while every member's own file\n// stays exactly what its owner chose. Each member publishes a dedicated copy\n// of their file sealed under the link key K (a selfstore mirror, republished\n// after every save that moved data), and attaches the others' copies as\n// read-only peers keyed by K - reading the other's copy is what\n// resynchronizes. A password-protected store refuses to share, honestly.\n//\n// The invitation link carries K in its fragment; possession of K is\n// membership, no per-member identity. Revoking means ending the share (the\n// one real revocation, see leave()); everyone keeps what already converged\n// into their own file - the local-first reality.\n//\n// A device may hold SEVERAL memberships, one per share, each bound to its own\n// wallet file (the `wallet` getter): at most the in-scope one is armed, the\n// others lie dormant. Without the getter (single-wallet hosts) the engine\n// keeps the historical one-membership rule.\n//\n// Injectable by contract (store / kv / backend), framework-free,\n// network-free: every durable side effect goes through store or backend.\n\nimport { randomId, type CopyLink, type SharePayload } from './codec';\nimport type { BackupTarget, PeerSource } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { LocalStore } from '../persistence/store';\n\n// --- Injected abstractions --------------------------------------------------\n\n/** What the bulletin holds once opened: the read-only projection (scrubbed\n * collections, empty when the share is revoked) and the control record. */\nexport interface IncomingShare {\n\tprojection: Record<string, unknown[]>;\n\tshare: SharePayload;\n}\n\n/**\n * The sharing transport, the sole Drive/relay/mailbox-specific seam. A test\n * provides an in-memory fake; the host app provides a real one (e.g. Drive).\n * Everything here carries only public data - file ids and the roster.\n */\nexport interface ShareBackend {\n\t/** Create this member's dedicated copy file (link-readable, empty until the\n\t * mirror publishes into it) and return the link others read it through.\n\t * `existingFileId` reuses a known copy on restore instead of minting a\n\t * second file. */\n\tcreateCopy(existingFileId?: string, shareLabel?: string): Promise<CopyLink>;\n\t/** A write-capable target over a copy file, for the store MIRROR. Never\n\t * adopted as the store's own target - the store's own connection stays what\n\t * it is. */\n\tcopyTarget(fileId: string): BackupTarget;\n\t/** Teardown: make a copy file private and delete it (best-effort). Only\n\t * ever called on MY own copy - the others' files are theirs. */\n\tdropCopy(fileId: string): Promise<void>;\n\t/** Admin: write (or rewrite) the bulletin - the encrypted file the invitation\n\t * link points at, holding the read-only projection plus the control record.\n\t * Idempotent per group; returns where it lives (the link's fileId + key). */\n\tpublishBulletin(key: string, payload: SharePayload): Promise<{ fileId: string; key: string }>;\n\t/** Admin: stop the invitation - the bulletin drops its projection and roster,\n\t * so the link no longer previews or admits. */\n\trevokeBulletin(): Promise<void>;\n\t/** Open an invitation link's bulletin (through the relay) and STASH it for the\n\t * join, returning its content for the read-only preview. Null when the link\n\t * is dead or the key wrong. */\n\topenIncoming(fileId: string, key: string): Promise<IncomingShare | null>;\n\t/** The invitation opened by openIncoming and stashed for the join, or null. */\n\ttakeStashedIncoming(): Promise<{ key: string; fileId: string; content: IncomingShare } | null>;\n\t/** Re-read a joined share's bulletin (member): the freshest roster. The\n\t * engine passes the invitation capability it joined through, so every\n\t * membership rereads ITS bulletin. Null when unreachable or revoked -\n\t * nothing is concluded. The literal 'unreadable' when the bulletin WAS\n\t * fetched but cannot be opened (the key no longer decrypts it - the admin\n\t * rotated the link): the engine counts consecutive ones toward the stale\n\t * verdict. A backend that cannot tell may keep returning null; the stale\n\t * detection then simply never fires. */\n\trereadJoined(fileId: string, key: string): Promise<SharePayload | null | 'unreadable'>;\n\t/** Member: announce my copy to the group's mailbox so the admin folds it. */\n\tannounce(mailboxId: string, copy: CopyLink): Promise<void>;\n\t/** Admin: drain the announce mailbox - the copies members published. */\n\ttakeAnnounces(mailboxId: string): Promise<CopyLink[]>;\n\t/** A read-only PeerSource over another member's published copy. */\n\tpeerSource(link: CopyLink): PeerSource;\n}\n\n// --- Persisted group ----------------------------------------------------------\n\n/** Everything needed to re-arm ONE share on reboot. A device may hold several\n * (one per joined silo); they persist together under the single KV key. */\ninterface StoredGroup {\n\trole: 'admin' | 'member';\n\t/** The wallet FILE this share is bound to (captured at start/join when the\n\t * host injects `wallet`). The mirror and peers arm only while it is the\n\t * attached one: another silo's data must never publish into the copy, and\n\t * the shared data must never fold into another silo. Absent = unbound\n\t * (single-wallet hosts), the share follows the store wherever it points. */\n\twalletFileId?: string;\n\t/** The shared group key K (the invitation link's fragment key). */\n\tkey: string;\n\t/** Where members announce their copy (a secret only K-holders read). */\n\tmailboxId: string;\n\t/** Known copies (the admin's own first). */\n\troster: CopyLink[];\n\t/** My own published copy (never the member's own file). */\n\tmyCopy: CopyLink;\n\t/** Admin: where the bulletin lives - what the link carries. Member: the\n\t * invitation opened to join - the roster refresh rereads it. (Absent on\n\t * pre-multi member records: their refresh stays dormant until a re-join.) */\n\tbulletin?: { fileId: string; key: string };\n\t/** Admin only: false once revoked (the bulletin stops previewing/admitting). */\n\tlive?: boolean;\n\t/** Admin only: a roster change could not be rebroadcast (offline mid-fold);\n\t * the next converge republishes even though nothing new was folded. */\n\tneedsRebroadcast?: boolean;\n\t/** Member only: my copy has not been SEEN in a fresh roster yet - the join\n\t * announce (or a later one) may have been lost. While set, every converge\n\t * re-announces, readable bulletin or not, so a joiner whose first announce\n\t * died on a network blip still becomes visible without re-opening the\n\t * invitation. Cleared the moment a fresh roster carries my copy. */\n\tannouncePending?: boolean;\n\t/** Member only: consecutive rereads whose bulletin was FETCHED but no longer\n\t * decrypts under my key. Two in a row set `stale` (one bad read must not\n\t * condemn a share over a hiccup); any readable read resets both. */\n\tunreadableReads?: number;\n\t/** Member only: the admin rotated the link (see unreadableReads) - my edits\n\t * publish into a copy nobody folds anymore. The host should surface \"this\n\t * share was renewed, ask for a fresh invitation\" instead of letting the\n\t * member edit into the void. */\n\tstale?: boolean;\n}\n\n/** The persisted shape: every membership on this device. A lone pre-multi\n * record lifts into a one-entry list on first load. */\ninterface StoredGroups {\n\tv: 2;\n\tgroups: StoredGroup[];\n}\n\n/** The default KV key the group lives under. Exported so a caller can wipe\n * it. Hosts with a pre-existing record under another key pass `storageKey`. */\nexport const HOUSEHOLD_GROUP_KEY = 'selfstore:households:group:v1';\n\n/** Every membership publishes through its own mirror: the id is scoped by the\n * copy file (unique per share) so several never collide on one store. */\nconst mirrorId = (group: StoredGroup): string => `household-copy:${group.myCopy.fileId}`;\n\n// --- Public surface -----------------------------------------------------------\n\n/** One membership, as the UI lists them: which silo carries it and whose\n * share it is. */\nexport interface MembershipInfo {\n\t/** The wallet file this membership is bound to (null = unbound legacy). */\n\twalletFileId: string | null;\n\tisAdmin: boolean;\n\t/** My published copy's file id for this share. */\n\tselfFileId: string;\n\t/** Who shares it with me (the admin's roster label); null for my own share. */\n\tsharedBy: string | null;\n\tmemberCount: number;\n\t/** Member: my copy is not in a fresh roster yet (the announce may have been\n\t * lost; the engine keeps re-announcing). The host can show \"waiting for the\n\t * admin to see you\" instead of a false everything-is-fine. */\n\tannouncePending: boolean;\n\t/** Member: the admin rotated the link - edits publish into the void until a\n\t * fresh invitation is joined. The host must surface it. */\n\tstale: boolean;\n}\n\n/** The reactive-friendly snapshot the UI reads (hosts wrap it in their own\n * reactivity - a Svelte rune, a React store, anything). The singular fields\n * describe the PRIMARY membership (the admin one when it exists, else the\n * first); `memberships` lists them all. */\nexport interface HouseholdGroupState {\n\t/** A share exists on this device (admin or member). */\n\tactive: boolean;\n\tisAdmin: boolean;\n\t/** How many copies are in the roster (this device included). */\n\tmemberCount: number;\n\t/** The roster's cosmetic owner labels, for a \"who has access\" list. */\n\tmembers: { fileId: string; label: string }[];\n\t/** My own copy's file id, so the UI can list everyone ELSE. */\n\tselfFileId: string | null;\n\t/** The wallet file the share is bound to, or null when unbound. The host\n\t * compares it to its own attached-file id to render \"this silo carries the\n\t * share\" and to gate its own publications the same way the engine does. */\n\twalletFileId: string | null;\n\t/** Admin: the live invitation's capability, so the panel can re-show the\n\t * link without republishing anything. */\n\tinviteCapability: { fileId: string; key: string } | null;\n\t/** Every membership on this device, admin and joined alike. */\n\tmemberships: MembershipInfo[];\n}\n\nexport interface HouseholdGroup {\n\treadonly state: HouseholdGroupState;\n\t/** Admin: start sharing. The member's own file is untouched - a dedicated\n\t * copy is created and mirrored under a fresh K, and the invitation\n\t * bulletin is published. Refused on a password-protected store: remove\n\t * the password to share, or keep it and do not. */\n\tstartShare(): Promise<{ fileId: string; key: string }>;\n\t/** Admin: re-show (and refresh) the standing invitation link. Idempotent. */\n\tinvite(): Promise<{ fileId: string; key: string }>;\n\t/** /share: open an invitation link's bulletin and remember it for the join. */\n\topenIncoming(fileId: string, key: string): Promise<IncomingShare | null>;\n\t/** Member: after openIncoming + own store connected, join read-write now.\n\t * Your own file stays yours: a dedicated copy is created and mirrored\n\t * under K, the others' copies attach as keyed peers (the next converge\n\t * folds the shared data into your own file), and my copy is announced.\n\t * Same honest rule as startShare: a password-protected store refuses.\n\t * With a `wallet` getter, one membership per share: joining another share\n\t * ADDS a membership (bound to the attached silo), and a re-join over the\n\t * same silo replaces its stale one. Without the getter, the historical\n\t * single-membership rule holds ('mismatch' on a different share). */\n\tjoin(): Promise<'joined' | 'no-invite' | 'mismatch' | 'error'>;\n\t/** Both: one convergence round (admin folds new announces + rebroadcasts;\n\t * everyone re-arms mirror/peers). Called on every converge. */\n\tsyncGroup(): Promise<void>;\n\t/** End ONE share on this device - and, for the admin, this is the real\n\t * revocation (bearer link: the lock changes for everyone). The bulletin\n\t * is revoked, my copy file is dropped, mirror and peers detach. The\n\t * member's own file is untouched: everyone keeps what already converged into\n\t * their own file - the local-first reality. Re-sharing mints a fresh K\n\t * and a fresh link for whoever should stay. `walletFileId` names WHICH\n\t * membership; omitted, the primary one (admin first) goes. */\n\tleave(walletFileId?: string | null): Promise<void>;\n\t/** On boot: re-arm the mirror and peers of the current share. */\n\trestore(): Promise<void>;\n}\n\n// --- Factory --------------------------------------------------------------------\n\nexport function createHouseholdGroup(deps: {\n\tstore: LocalStore;\n\tkv: KV;\n\tbackend: ShareBackend;\n\t/** KV key the group record lives under. Default HOUSEHOLD_GROUP_KEY. */\n\tstorageKey?: string;\n\t/** The currently attached wallet file id, for multi-backup hosts. When\n\t * given, start/join bind the share to that file and the engine goes\n\t * dormant whenever another one is attached. Omit on single-wallet hosts. */\n\twallet?: () => Promise<string | null>;\n}): HouseholdGroup {\n\tconst { store, kv, backend, wallet } = deps;\n\tconst storageKey = deps.storageKey ?? HOUSEHOLD_GROUP_KEY;\n\n\tlet groups: StoredGroup[] | null = null;\n\tlet loaded = false;\n\t// Member roster mirror: how many consecutive fresh reads a known peer has\n\t// been MISSING from the bulletin. Removals apply only at 2 - a single stale\n\t// relay read must never cut a still-valid member's data off. Keys are\n\t// scoped per share (`mailboxId:fileId`).\n\tconst rosterMisses = new Map<string, number>();\n\n\tasync function load(): Promise<StoredGroup[]> {\n\t\tif (!loaded) {\n\t\t\tconst raw = await kv.get<StoredGroups | StoredGroup>(storageKey);\n\t\t\tif (!raw) groups = [];\n\t\t\telse if ((raw as StoredGroups).v === 2) groups = (raw as StoredGroups).groups;\n\t\t\t// A pre-multi single record lifts into a one-entry list, verbatim.\n\t\t\telse groups = [raw as StoredGroup];\n\t\t\tloaded = true;\n\t\t}\n\t\treturn groups as StoredGroup[];\n\t}\n\n\t/** Write the current list back (the callers mutate `groups` in place). An\n\t * empty list clears the key: no membership, no record. */\n\tasync function persist(): Promise<void> {\n\t\tloaded = true;\n\t\tif (!groups || groups.length === 0) {\n\t\t\tgroups = groups ?? [];\n\t\t\tawait kv.del(storageKey);\n\t\t\treturn;\n\t\t}\n\t\tawait kv.set(storageKey, { v: 2, groups } satisfies StoredGroups);\n\t}\n\n\tconst adminGroup = (list: StoredGroup[]): StoredGroup | null =>\n\t\tlist.find((g) => g.role === 'admin') ?? null;\n\t/** The membership the singular state fields describe: admin first. */\n\tconst primaryGroup = (list: StoredGroup[]): StoredGroup | null =>\n\t\tadminGroup(list) ?? list[0] ?? null;\n\t/** Who shares a joined group with me: the admin's copy leads the roster. */\n\tconst sharedByOf = (g: StoredGroup): string | null =>\n\t\tg.role === 'member' ? (g.roster[0]?.ownerEmail ?? g.roster[0]?.ownerName ?? null) : null;\n\n\t/** A fresh 32-byte key, base64url, for a new share (the link fragment). */\n\tfunction randomKey(): string {\n\t\tconst bytes = crypto.getRandomValues(new Uint8Array(32));\n\t\tlet s = '';\n\t\tfor (const b of bytes) s += String.fromCharCode(b);\n\t\treturn btoa(s).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n\t}\n\n\t/** The honest rule, enforced at both entrances: sharing never re-keys the\n\t * own file, so a password-protected store cannot publish a readable copy\n\t * without lowering the bar its owner chose. Refuse plainly. */\n\tfunction refuseProtected(): void {\n\t\tif (store.state.encrypted) {\n\t\t\tthrow new Error(\n\t\t\t\t'household-group: a password-protected store cannot be shared - remove the password first'\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Whether the share's bound wallet is the attached one. Unbound (or no\n\t * getter) is always in scope - the single-wallet behavior. */\n\tasync function inScope(group: StoredGroup): Promise<boolean> {\n\t\tif (!group.walletFileId || !wallet) return true;\n\t\treturn (await wallet()) === group.walletFileId;\n\t}\n\n\t/** Out-of-scope dormancy: tear down BOTH directions. The mirror would leak\n\t * the attached silo's data into the copy; the peers would fold the shared\n\t * data into that silo. Nothing else moves until the bound wallet is back. */\n\tfunction disarm(group: StoredGroup): void {\n\t\tstore.detachMirror(mirrorId(group));\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId !== group.myCopy.fileId) store.detachPeer(link.fileId);\n\t\t}\n\t}\n\n\t/** Re-arm my copy's mirror (idempotent - safe on every converge/boot). */\n\tfunction ensureMirror(group: StoredGroup): void {\n\t\tconst id = mirrorId(group);\n\t\tif (store.state.mirrors.some((m) => m.id === id)) return;\n\t\tstore.attachMirror(backend.copyTarget(group.myCopy.fileId), {\n\t\t\tpassword: group.key,\n\t\t\tid\n\t\t});\n\t}\n\n\t/** Attach every roster copy that is not mine and not already attached as a\n\t * read-only peer under K (its own key - the member's file has none). Idempotent. */\n\tfunction attachPeers(group: StoredGroup): void {\n\t\tconst already = new Set(store.state.peers.map((p) => p.id));\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\tif (already.has(link.fileId)) continue;\n\t\t\tstore.attachPeer(backend.peerSource(link), { id: link.fileId, password: group.key });\n\t\t}\n\t}\n\n\t/** Merge new copies into a roster, de-duplicated by fileId, dropping mine. */\n\tfunction mergeRoster(roster: CopyLink[], incoming: CopyLink[], selfFileId: string): CopyLink[] {\n\t\tconst byId = new Map(roster.map((l) => [l.fileId, l]));\n\t\tfor (const link of incoming) {\n\t\t\tif (link.fileId === selfFileId) continue;\n\t\t\tbyId.set(link.fileId, link);\n\t\t}\n\t\treturn [...byId.values()];\n\t}\n\n\t/** Post an announce with small bounded retries; true when one landed. A lost\n\t * announce strands the joiner - visible to nobody, editing into the void -\n\t * so it is worth a few immediate retries before falling back to the\n\t * persistent announcePending re-announce loop. */\n\tasync function announceWithRetry(mailboxId: string, copy: CopyLink): Promise<boolean> {\n\t\tfor (let attempt = 0; attempt < 3; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait backend.announce(mailboxId, copy);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\tif (attempt < 2) await new Promise((r) => setTimeout(r, 800 * (attempt + 1)));\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** One admin converge: fold newly-announced member copies, rebroadcast. */\n\tasync function adminRound(group: StoredGroup): Promise<void> {\n\t\tconst announced = await backend.takeAnnounces(group.mailboxId);\n\t\tconst roster = mergeRoster(group.roster, announced, group.myCopy.fileId);\n\t\tconst grew = roster.length !== group.roster.length;\n\t\tconst needed = group.needsRebroadcast === true;\n\t\tgroup.roster = roster;\n\t\tawait persist();\n\t\tattachPeers(group);\n\t\t// Rebroadcast on growth or when a previous rebroadcast failed: keyed\n\t\t// on `grew` alone, an offline publish left the bulletin stale forever\n\t\t// (the announce was already consumed, so no later round re-grew).\n\t\tif ((grew || needed) && group.live === true) {\n\t\t\ttry {\n\t\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\t\troster\n\t\t\t\t});\n\t\t\t\tgroup.bulletin = where;\n\t\t\t\tgroup.needsRebroadcast = undefined;\n\t\t\t} catch {\n\t\t\t\tgroup.needsRebroadcast = true;\n\t\t\t}\n\t\t\tawait persist();\n\t\t}\n\t}\n\n\t/** Mirror the authoritative roster onto the local one. Removals apply\n\t * CONSERVATIVELY: a peer drops only after two consecutive fresh reads\n\t * without it (stale relay tolerance). */\n\tfunction applyRosterRemovals(group: StoredGroup, byId: Map<string, CopyLink>): void {\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\tconst missKey = `${group.mailboxId}:${link.fileId}`;\n\t\t\tif (byId.has(link.fileId)) {\n\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst misses = (rosterMisses.get(missKey) ?? 0) + 1;\n\t\t\tif (misses >= 2) {\n\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\tstore.detachPeer(link.fileId);\n\t\t\t} else {\n\t\t\t\trosterMisses.set(missKey, misses);\n\t\t\t\tbyId.set(link.fileId, link);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** One member converge: reread the bulletin (the admin's roster is\n\t * AUTHORITATIVE - replace, not merge, so removals propagate), self-heal a\n\t * lost announce, track the stale verdict. Pre-multi records carry no\n\t * bulletin: their refresh stays dormant, peers still fold. */\n\tasync function memberRound(group: StoredGroup): Promise<void> {\n\t\tconst share = group.bulletin\n\t\t\t? await backend.rereadJoined(group.bulletin.fileId, group.bulletin.key)\n\t\t\t: null;\n\t\tif (share === 'unreadable') {\n\t\t\t// Fetched but no longer decrypts: the admin rotated the link. Two\n\t\t\t// consecutive verdicts set the stale flag the host must surface -\n\t\t\t// from here my edits publish into a copy nobody folds. Peers keep\n\t\t\t// folding (the member keeps what already converged).\n\t\t\tconst n = (group.unreadableReads ?? 0) + 1;\n\t\t\tgroup.unreadableReads = n;\n\t\t\tif (n >= 2) group.stale = true;\n\t\t\tawait persist();\n\t\t\tattachPeers(group);\n\t\t\treturn;\n\t\t}\n\t\tif (!share) {\n\t\t\t// Unreachable bulletin: conclude nothing - but a pending announce\n\t\t\t// still retries (the mailbox may answer even when the bulletin\n\t\t\t// relay does not), so a stranded joiner heals as soon as ANY\n\t\t\t// network path opens, not only after a full bulletin read.\n\t\t\tif (group.announcePending) {\n\t\t\t\tawait backend.announce(group.mailboxId, group.myCopy).catch(() => undefined);\n\t\t\t}\n\t\t\tattachPeers(group);\n\t\t\treturn;\n\t\t}\n\t\t// A readable bulletin is proof the share lives: clear staleness.\n\t\tgroup.unreadableReads = undefined;\n\t\tgroup.stale = undefined;\n\t\tconst byId = new Map(share.roster.map((l) => [l.fileId, l]));\n\t\t// SELF-HEAL: my own copy missing from a fresh roster means my\n\t\t// announce was lost. Announce again (with retries); the admin's\n\t\t// fold de-dups. announcePending tracks the truth either way, so\n\t\t// the host can say \"waiting to be seen\" instead of false calm.\n\t\tif (!byId.has(group.myCopy.fileId)) {\n\t\t\tbyId.set(group.myCopy.fileId, group.myCopy);\n\t\t\tgroup.announcePending = true;\n\t\t\tawait announceWithRetry(group.mailboxId, group.myCopy);\n\t\t} else {\n\t\t\tgroup.announcePending = undefined;\n\t\t}\n\t\tapplyRosterRemovals(group, byId);\n\t\tgroup.roster = [...byId.values()];\n\t\tawait persist();\n\t\tattachPeers(group);\n\t}\n\n\treturn {\n\t\tget state(): HouseholdGroupState {\n\t\t\tconst list = groups ?? [];\n\t\t\tconst memberships: MembershipInfo[] = list.map((g) => ({\n\t\t\t\twalletFileId: g.walletFileId ?? null,\n\t\t\t\tisAdmin: g.role === 'admin',\n\t\t\t\tselfFileId: g.myCopy.fileId,\n\t\t\t\tsharedBy: sharedByOf(g),\n\t\t\t\tmemberCount: g.roster.length,\n\t\t\t\tannouncePending: g.announcePending === true,\n\t\t\t\tstale: g.stale === true\n\t\t\t}));\n\t\t\tconst group = primaryGroup(list);\n\t\t\tif (!group) {\n\t\t\t\treturn {\n\t\t\t\t\tactive: false,\n\t\t\t\t\tisAdmin: false,\n\t\t\t\t\tmemberCount: 0,\n\t\t\t\t\tmembers: [],\n\t\t\t\t\tselfFileId: null,\n\t\t\t\t\twalletFileId: null,\n\t\t\t\t\tinviteCapability: null,\n\t\t\t\t\tmemberships\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tactive: true,\n\t\t\t\tisAdmin: group.role === 'admin',\n\t\t\t\tmemberCount: group.roster.length,\n\t\t\t\tmembers: group.roster.map((l) => ({\n\t\t\t\t\tfileId: l.fileId,\n\t\t\t\t\tlabel: l.ownerEmail ?? l.ownerName ?? l.fileId.slice(0, 8)\n\t\t\t\t})),\n\t\t\t\tselfFileId: group.myCopy.fileId,\n\t\t\t\twalletFileId: group.walletFileId ?? null,\n\t\t\t\tinviteCapability:\n\t\t\t\t\tgroup.role === 'admin' && group.live === true && group.bulletin ? group.bulletin : null,\n\t\t\t\tmemberships\n\t\t\t};\n\t\t},\n\n\t\tasync startShare(): Promise<{ fileId: string; key: string }> {\n\t\t\tconst list = await load();\n\t\t\t// One ADMIN share per device; joined memberships do not block it.\n\t\t\tif (adminGroup(list)) {\n\t\t\t\tthrow new Error('household-group: a share already exists on this device');\n\t\t\t}\n\t\t\trefuseProtected();\n\t\t\tconst key = randomKey();\n\t\t\tconst mailboxId = randomId();\n\t\t\tconst walletFileId = (await wallet?.()) ?? undefined;\n\t\t\t// The copy is a dedicated file - the member's own file is never in the roster.\n\t\t\tconst link = await backend.createCopy();\n\t\t\tconst roster = [link];\n\t\t\t// Persist before the publish: a failure at any later step leaves a\n\t\t\t// recoverable half-share (restore re-arms the mirror, invite() finishes\n\t\t\t// the publication). Nothing here is irreversible for the member's own file.\n\t\t\tconst group: StoredGroup = {\n\t\t\t\trole: 'admin',\n\t\t\t\twalletFileId,\n\t\t\t\tkey,\n\t\t\t\tmailboxId,\n\t\t\t\troster,\n\t\t\t\tmyCopy: link,\n\t\t\t\tlive: false\n\t\t\t};\n\t\t\tlist.push(group);\n\t\t\tawait persist();\n\t\t\t// The mirror fills the copy right away and keeps it fresh after every\n\t\t\t// save/converge that moves data. The member's own file is not touched.\n\t\t\tensureMirror(group);\n\t\t\tconst where = await backend.publishBulletin(key, { v: 1, mailboxId, roster });\n\t\t\tgroup.bulletin = where;\n\t\t\tgroup.live = true;\n\t\t\tawait persist();\n\t\t\treturn where;\n\t\t},\n\n\t\tasync invite(): Promise<{ fileId: string; key: string }> {\n\t\t\tconst group = adminGroup(await load());\n\t\t\tif (!group) {\n\t\t\t\tthrow new Error('household-group: invite() requires an admin share');\n\t\t\t}\n\t\t\t// Refresh the bulletin (fresh projection + current roster) and mark live.\n\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\tv: 1,\n\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\troster: group.roster\n\t\t\t});\n\t\t\tgroup.bulletin = where;\n\t\t\tgroup.live = true;\n\t\t\tawait persist();\n\t\t\treturn where;\n\t\t},\n\n\t\tasync openIncoming(fileId: string, key: string): Promise<IncomingShare | null> {\n\t\t\treturn backend.openIncoming(fileId, key);\n\t\t},\n\n\t\tasync join(): Promise<'joined' | 'no-invite' | 'mismatch' | 'error'> {\n\t\t\tconst list = await load();\n\t\t\tconst incoming = await backend.takeStashedIncoming();\n\t\t\t// No invitation opened: a device already holding a membership answers\n\t\t\t// the historical idempotent 'joined'; a bare one names the dead end.\n\t\t\tif (!incoming) return list.length > 0 ? 'joined' : 'no-invite';\n\t\t\tconst { key, fileId, content } = incoming;\n\t\t\tif (list.some((g) => g.mailboxId === content.share.mailboxId)) return 'joined';\n\t\t\t// Single-wallet hosts (no getter) keep the one-membership rule: an\n\t\t\t// invitation to ANOTHER share is a dead end, not a silent second join.\n\t\t\tif (!wallet && list.length > 0) return 'mismatch';\n\t\t\trefuseProtected();\n\t\t\tconst walletFileId = (await wallet?.()) ?? undefined;\n\t\t\t// A re-join over the same silo replaces its stale membership (a rotated\n\t\t\t// link): two shares must never publish the same wallet.\n\t\t\tif (walletFileId) {\n\t\t\t\tconst stale = list.find((g) => g.walletFileId === walletFileId);\n\t\t\t\tif (stale) {\n\t\t\t\t\tdisarm(stale);\n\t\t\t\t\tawait backend.dropCopy(stale.myCopy.fileId).catch(() => undefined);\n\t\t\t\t\tgroups = list.filter((g) => g !== stale);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// My copy is a dedicated file; my own store stays mine. The shared\n\t\t\t// data arrives by folding the others' copies into it on the next\n\t\t\t// converge - one store per person, converging.\n\t\t\tconst link = await backend.createCopy(undefined, content.share.roster[0]?.ownerEmail);\n\t\t\tconst roster = mergeRoster(content.share.roster, [link], '');\n\t\t\tconst group: StoredGroup = {\n\t\t\t\trole: 'member',\n\t\t\t\twalletFileId,\n\t\t\t\tkey,\n\t\t\t\tmailboxId: content.share.mailboxId,\n\t\t\t\troster,\n\t\t\t\tmyCopy: link,\n\t\t\t\t// The invitation capability, kept so THIS membership rereads ITS\n\t\t\t\t// bulletin on converge (roster removals, lost-announce self-heal).\n\t\t\t\tbulletin: { fileId, key }\n\t\t\t};\n\t\t\t(groups as StoredGroup[]).push(group);\n\t\t\tawait persist();\n\t\t\tensureMirror(group);\n\t\t\tattachPeers(group);\n\t\t\t// Announce my copy so the admin folds it into everyone's roster. NOT\n\t\t\t// best-effort-and-forget: a swallowed failure here once stranded a\n\t\t\t// joiner for good - member on their side, visible to nobody, editing\n\t\t\t// into the void. Retry now; still failing, persist announcePending so\n\t\t\t// every converge keeps announcing until a fresh roster carries me.\n\t\t\tif (!(await announceWithRetry(content.share.mailboxId, link))) {\n\t\t\t\tgroup.announcePending = true;\n\t\t\t\tawait persist();\n\t\t\t}\n\t\t\treturn 'joined';\n\t\t},\n\n\t\tasync syncGroup(): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\tfor (const group of list) {\n\t\t\t\t// Defense in depth for the honest rule: a store that became\n\t\t\t\t// password-protected (or joined from a device whose store turned out\n\t\t\t\t// protected) must never publish a link-readable copy. Tear the mirror\n\t\t\t\t// down if one was armed; the share lies dormant on this device until\n\t\t\t\t// the password is removed. (Folding INBOUND peers would be harmless,\n\t\t\t\t// but a dormant share should read as dormant - arm nothing.)\n\t\t\t\tif (store.state.encrypted) {\n\t\t\t\t\tstore.detachMirror(mirrorId(group));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Another silo is attached: full dormancy, both directions (see disarm).\n\t\t\t\tif (!(await inScope(group))) {\n\t\t\t\t\tdisarm(group);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tensureMirror(group);\n\t\t\t\tif (group.role === 'admin') await adminRound(group);\n\t\t\t\telse await memberRound(group);\n\t\t\t}\n\t\t},\n\n\t\tasync leave(walletFileId?: string | null): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\tconst group =\n\t\t\t\twalletFileId != null\n\t\t\t\t\t? (list.find((g) => g.walletFileId === walletFileId) ?? null)\n\t\t\t\t\t: primaryGroup(list);\n\t\t\tif (!group) return;\n\t\t\t// Detach the plumbing first: nothing publishes or folds anymore.\n\t\t\tdisarm(group);\n\t\t\tif (group.role === 'admin') {\n\t\t\t\ttry {\n\t\t\t\t\tawait backend.revokeBulletin();\n\t\t\t\t} catch {\n\t\t\t\t\t/* tolerable: the bulletin only ever holds K-encrypted data */\n\t\t\t\t}\n\t\t\t}\n\t\t\t// My copy file dies with the membership (private first, then gone).\n\t\t\t// The member's own file is untouched: it never carried the share key, and the\n\t\t\t// data already converged into it stays - local-first honesty.\n\t\t\tawait backend.dropCopy(group.myCopy.fileId).catch(() => undefined);\n\t\t\tgroups = list.filter((g) => g !== group);\n\t\t\tfor (const k of [...rosterMisses.keys()]) {\n\t\t\t\tif (k.startsWith(`${group.mailboxId}:`)) rosterMisses.delete(k);\n\t\t\t}\n\t\t\tawait persist();\n\t\t},\n\n\t\tasync restore(): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\t// Same dormancy rules as syncGroup: a protected store never mirrors,\n\t\t\t// and a share bound to another silo arms nothing while it is away.\n\t\t\tif (store.state.encrypted) return;\n\t\t\tfor (const group of list) {\n\t\t\t\tif (!(await inScope(group))) continue;\n\t\t\t\tensureMirror(group);\n\t\t\t\tattachPeers(group);\n\t\t\t}\n\t\t}\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/households/codec.ts","../src/households/group.ts"],"names":[],"mappings":"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","file":"households.js","sourcesContent":["// Household share transport (capability-link model): the plain shapes the\n// bulletin and the copy-announce carry, plus a dependency-free base64url\n// codec. The link key K is the shared group secret - possession is\n// membership, no per-member identity, no signed manifest. Payloads carry\n// only file ids of link-readable files whose content is encrypted under K,\n// so an id alone reveals nothing. Pure by contract: no secret held, no I/O,\n// no framework - shapes, validation and transport strings only.\n\n// --- Errors -----------------------------------------------------------------\n\n/** Why a code was refused. `malformed` covers bad transport, wrong structure or\n * a code decoded as the wrong kind of payload; `unsupported-version` is a\n * numeric `v` this build does not understand. */\nexport type HouseholdCodeErrorCode = 'malformed' | 'unsupported-version';\n\n/** The single typed error this module throws, so callers branch on `code`\n * instead of parsing messages. A decoder that throws never returns a partially\n * parsed object - it fails whole. */\nexport class HouseholdCodeError extends Error {\n\treadonly code: HouseholdCodeErrorCode;\n\tconstructor(code: HouseholdCodeErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'HouseholdCodeError';\n\t\tthis.code = code;\n\t\tObject.setPrototypeOf(this, HouseholdCodeError.prototype);\n\t}\n}\n\nconst malformed = (message: string): HouseholdCodeError =>\n\tnew HouseholdCodeError('malformed', message);\n\n// --- Types ------------------------------------------------------------------\n\n/** Where a member published their encrypted copy. A discriminated union on\n * `provider` so a future transport (WebDAV, ...) slots in beside Drive without\n * changing the payload shapes. Today: Drive only. */\nexport type CopyLink = DriveCopyLink;\n\n/** A copy hosted on the owner's Google Drive. `fileId` is the Drive file id (a\n * public-link file, ciphertext under K); the owner fields are a cosmetic \"on\n * X's Drive\" hint, both optional. */\nexport interface DriveCopyLink {\n\tprovider: 'drive';\n\tfileId: string;\n\townerEmail?: string;\n\townerName?: string;\n}\n\n/** The bulletin's control record (rides one reserved collection inside the\n * K-encrypted bulletin): where members announce their copy (`mailboxId`, a\n * secret only K-holders can read) and the roster of every known copy (the\n * admin's own first). */\nexport interface SharePayload {\n\tv: 1;\n\tmailboxId: string;\n\troster: CopyLink[];\n}\n\n/** Member -> admin, through the announce mailbox: \"here is my copy, fold it\".\n * No identity, no signature: knowing the mailboxId (which lives inside the\n * K-encrypted bulletin) already proves the sender holds K. */\nexport interface AnnouncePayload {\n\tv: 1;\n\tcopy: CopyLink;\n}\n\n// --- base64url (isomorphic, dependency-free) --------------------------------\n\nconst utf8Encoder = new TextEncoder();\n// `fatal` makes a truncated or corrupted byte run throw instead of silently\n// yielding U+FFFD, so a mangled code fails fast in the decoder.\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: true });\n\nfunction bytesToBinary(bytes: Uint8Array): string {\n\tlet out = '';\n\tfor (let i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]);\n\treturn out;\n}\n\nfunction binaryToBytes(binary: string): Uint8Array {\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n\treturn bytes;\n}\n\n/** base64url without padding of raw bytes. btoa + TextEncoder work in the\n * browser and in Node 20+, so the codec stays dependency-free and isomorphic. */\nfunction bytesToBase64url(bytes: Uint8Array): string {\n\treturn btoa(bytesToBinary(bytes)).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction base64urlEncode(text: string): string {\n\treturn bytesToBase64url(utf8Encoder.encode(text));\n}\n\n/** Inverse of base64urlEncode. Throws on any character outside the alphabet or\n * on an invalid UTF-8 byte run; decodeJson turns that into a 'malformed'. */\nfunction base64urlDecode(code: string): string {\n\tconst b64 = code.replace(/-/g, '+').replace(/_/g, '/');\n\tconst padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n\treturn utf8Decoder.decode(binaryToBytes(atob(padded)));\n}\n\n/** A short, url-safe, random id (9 bytes -> 12 base64url chars) from WebCrypto -\n * used for the announce mailbox id. This repo bans Math.random; getRandomValues\n * is the sanctioned entropy source and exists in every browser and Node 20+. */\nexport function randomId(): string {\n\tconst bytes = new Uint8Array(9);\n\tcrypto.getRandomValues(bytes);\n\treturn bytesToBase64url(bytes);\n}\n\n// --- Structural guards ------------------------------------------------------\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n\treturn typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction isString(v: unknown): v is string {\n\treturn typeof v === 'string';\n}\n\n/** Enforce the format version. A numeric `v` other than 1 is a code from a\n * future/older build (unsupported-version); anything else is garbage. */\nfunction requireV1(v: unknown): void {\n\tif (v === 1) return;\n\tif (typeof v === 'number') {\n\t\tthrow new HouseholdCodeError(\n\t\t\t'unsupported-version',\n\t\t\t`payload version ${v} is not supported (expected 1)`\n\t\t);\n\t}\n\tthrow malformed('missing or non-numeric version field');\n}\n\n/** Decode base64url -> JSON, mapping any failure to a single 'malformed'. */\nfunction decodeJson(code: string): unknown {\n\tif (typeof code !== 'string' || code.length === 0)\n\t\tthrow malformed('code must be a non-empty string');\n\tlet json: string;\n\ttry {\n\t\tjson = base64urlDecode(code);\n\t} catch {\n\t\tthrow malformed('code is not valid base64url');\n\t}\n\ttry {\n\t\treturn JSON.parse(json) as unknown;\n\t} catch {\n\t\tthrow malformed('code payload is not valid JSON');\n\t}\n}\n\n/** Rebuild a CopyLink field by field, so a decoded link carries exactly the\n * known keys (no smuggled extras) and every present field has the right type. */\nexport function toCopyLink(v: unknown): CopyLink {\n\tif (!isRecord(v)) throw malformed('copy link is not an object');\n\tif (v.provider === 'drive') {\n\t\tif (!isString(v.fileId)) throw malformed('drive copy link needs a string fileId');\n\t\tif (v.ownerEmail !== undefined && !isString(v.ownerEmail))\n\t\t\tthrow malformed('copy link ownerEmail must be a string');\n\t\tif (v.ownerName !== undefined && !isString(v.ownerName))\n\t\t\tthrow malformed('copy link ownerName must be a string');\n\t\tconst link: DriveCopyLink = { provider: 'drive', fileId: v.fileId };\n\t\tif (isString(v.ownerEmail)) link.ownerEmail = v.ownerEmail;\n\t\tif (isString(v.ownerName)) link.ownerName = v.ownerName;\n\t\treturn link;\n\t}\n\tthrow malformed(`unknown copy-link provider ${JSON.stringify(v.provider)}`);\n}\n\n/** Every entry of a roster array validated as a CopyLink, de-duplicated by\n * fileId (first occurrence wins). An empty roster is structurally valid.\n * Exported for the backend, which revalidates the roster it re-publishes. */\nexport function toRoster(v: unknown): CopyLink[] {\n\tif (!Array.isArray(v)) throw malformed('roster is not an array');\n\tconst seen = new Set<string>();\n\tconst out: CopyLink[] = [];\n\tfor (const entry of v) {\n\t\tconst link = toCopyLink(entry);\n\t\tif (seen.has(link.fileId)) continue;\n\t\tseen.add(link.fileId);\n\t\tout.push(link);\n\t}\n\treturn out;\n}\n\n// --- Codec ------------------------------------------------------------------\n// Transport = base64url(no padding) of compact JSON. Each encoder is a pure,\n// deterministic function of its input; each decoder validates the version and\n// the full structure and throws HouseholdCodeError on anything it cannot trust.\n\nexport function encodeShare(payload: SharePayload): string {\n\treturn base64urlEncode(JSON.stringify(payload));\n}\n\nexport function decodeShare(code: string): SharePayload {\n\tconst raw = decodeJson(code);\n\tif (!isRecord(raw)) throw malformed('share payload is not an object');\n\trequireV1(raw.v);\n\tif (!isString(raw.mailboxId)) throw malformed('share payload needs a string mailboxId');\n\treturn { v: 1, mailboxId: raw.mailboxId, roster: toRoster(raw.roster) };\n}\n\nexport function encodeAnnounce(payload: AnnouncePayload): string {\n\treturn base64urlEncode(JSON.stringify(payload));\n}\n\nexport function decodeAnnounce(code: string): AnnouncePayload {\n\tconst raw = decodeJson(code);\n\tif (!isRecord(raw)) throw malformed('announce is not an object');\n\trequireV1(raw.v);\n\treturn { v: 1, copy: toCopyLink(raw.copy) };\n}\n","// Household sharing, mirror model: read-write while every member's own file\n// stays exactly what its owner chose. Each member publishes a dedicated copy\n// of their file sealed under the link key K (a selfstore mirror, republished\n// after every save that moved data), and attaches the others' copies as\n// read-only peers keyed by K - reading the other's copy is what\n// resynchronizes. A password-protected store refuses to share, honestly.\n//\n// The invitation link carries K in its fragment; possession of K is\n// membership, no per-member identity. Revoking means ending the share (the\n// one real revocation, see leave()); everyone keeps what already converged\n// into their own file - the local-first reality.\n//\n// A device may hold SEVERAL memberships, one per share, each bound to its own\n// wallet file (the `wallet` getter): at most the in-scope one is armed, the\n// others lie dormant. Without the getter (single-wallet hosts) the engine\n// keeps the historical one-membership rule.\n//\n// Injectable by contract (store / kv / backend), framework-free,\n// network-free: every durable side effect goes through store or backend.\n\nimport { randomId, type CopyLink, type SharePayload } from './codec';\nimport type { BackupTarget, PeerSource } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { LocalStore } from '../persistence/store';\n\n// --- Injected abstractions --------------------------------------------------\n\n/** What the bulletin holds once opened: the read-only projection (scrubbed\n * collections, empty when the share is revoked) and the control record. */\nexport interface IncomingShare {\n\tprojection: Record<string, unknown[]>;\n\tshare: SharePayload;\n}\n\n/**\n * The sharing transport, the sole Drive/relay/mailbox-specific seam. A test\n * provides an in-memory fake; the host app provides a real one (e.g. Drive).\n * Everything here carries only public data - file ids and the roster.\n */\nexport interface ShareBackend {\n\t/** Create this member's dedicated copy file (link-readable, empty until the\n\t * mirror publishes into it) and return the link others read it through.\n\t * `existingFileId` reuses a known copy on restore instead of minting a\n\t * second file. */\n\tcreateCopy(existingFileId?: string, shareLabel?: string): Promise<CopyLink>;\n\t/** A write-capable target over a copy file, for the store MIRROR. Never\n\t * adopted as the store's own target - the store's own connection stays what\n\t * it is. */\n\tcopyTarget(fileId: string): BackupTarget;\n\t/** Teardown: make a copy file private and delete it (best-effort). Only\n\t * ever called on MY own copy - the others' files are theirs. */\n\tdropCopy(fileId: string): Promise<void>;\n\t/** Admin: write (or rewrite) the bulletin - the encrypted file the invitation\n\t * link points at, holding the read-only projection plus the control record.\n\t * Idempotent per group; returns where it lives (the link's fileId + key). */\n\tpublishBulletin(key: string, payload: SharePayload): Promise<{ fileId: string; key: string }>;\n\t/** Admin: stop the invitation - the bulletin drops its projection and roster,\n\t * so the link no longer previews or admits. */\n\trevokeBulletin(): Promise<void>;\n\t/** Open an invitation link's bulletin (through the relay) and STASH it for the\n\t * join, returning its content for the read-only preview. Null when the link\n\t * is dead or the key wrong. */\n\topenIncoming(fileId: string, key: string): Promise<IncomingShare | null>;\n\t/** The invitation opened by openIncoming and stashed for the join, or null. */\n\ttakeStashedIncoming(): Promise<{ key: string; fileId: string; content: IncomingShare } | null>;\n\t/** Re-read a joined share's bulletin (member): the freshest roster. The\n\t * engine passes the invitation capability it joined through, so every\n\t * membership rereads ITS bulletin. Null when unreachable or revoked -\n\t * nothing is concluded. The literal 'unreadable' when the bulletin WAS\n\t * fetched but cannot be opened (the key no longer decrypts it - the admin\n\t * rotated the link): the engine counts consecutive ones toward the stale\n\t * verdict. A backend that cannot tell may keep returning null; the stale\n\t * detection then simply never fires. */\n\trereadJoined(fileId: string, key: string): Promise<SharePayload | null | 'unreadable'>;\n\t/** Member: announce my copy to the group's mailbox so the admin folds it. */\n\tannounce(mailboxId: string, copy: CopyLink): Promise<void>;\n\t/** Admin: drain the announce mailbox - the copies members published. */\n\ttakeAnnounces(mailboxId: string): Promise<CopyLink[]>;\n\t/** A read-only PeerSource over another member's published copy. */\n\tpeerSource(link: CopyLink): PeerSource;\n}\n\n// --- Persisted group ----------------------------------------------------------\n\n/** Everything needed to re-arm ONE share on reboot. A device may hold several\n * (one per joined silo); they persist together under the single KV key. */\ninterface StoredGroup {\n\trole: 'admin' | 'member';\n\t/** The wallet FILE this share is bound to (captured at start/join when the\n\t * host injects `wallet`). The mirror and peers arm only while it is the\n\t * attached one: another silo's data must never publish into the copy, and\n\t * the shared data must never fold into another silo. Absent = unbound\n\t * (single-wallet hosts), the share follows the store wherever it points. */\n\twalletFileId?: string;\n\t/** The shared group key K (the invitation link's fragment key). */\n\tkey: string;\n\t/** Where members announce their copy (a secret only K-holders read). */\n\tmailboxId: string;\n\t/** Known copies (the admin's own first). */\n\troster: CopyLink[];\n\t/** My own published copy (never the member's own file). */\n\tmyCopy: CopyLink;\n\t/** Admin: where the bulletin lives - what the link carries. Member: the\n\t * invitation opened to join - the roster refresh rereads it. (Absent on\n\t * pre-multi member records: their refresh stays dormant until a re-join.) */\n\tbulletin?: { fileId: string; key: string };\n\t/** Admin only: false once revoked (the bulletin stops previewing/admitting). */\n\tlive?: boolean;\n\t/** Admin only: a roster change could not be rebroadcast (offline mid-fold);\n\t * the next converge republishes even though nothing new was folded. */\n\tneedsRebroadcast?: boolean;\n\t/** Member only: my copy has not been SEEN in a fresh roster yet - the join\n\t * announce (or a later one) may have been lost. While set, every converge\n\t * re-announces, readable bulletin or not, so a joiner whose first announce\n\t * died on a network blip still becomes visible without re-opening the\n\t * invitation. Cleared the moment a fresh roster carries my copy. */\n\tannouncePending?: boolean;\n\t/** Member only: consecutive rereads whose bulletin was FETCHED but no longer\n\t * decrypts under my key. Two in a row set `stale` (one bad read must not\n\t * condemn a share over a hiccup); any readable read resets both. */\n\tunreadableReads?: number;\n\t/** Member only: the admin rotated the link (see unreadableReads) - my edits\n\t * publish into a copy nobody folds anymore. The host should surface \"this\n\t * share was renewed, ask for a fresh invitation\" instead of letting the\n\t * member edit into the void. */\n\tstale?: boolean;\n}\n\n/** The persisted shape: every membership on this device. A lone pre-multi\n * record lifts into a one-entry list on first load. */\ninterface StoredGroups {\n\tv: 2;\n\tgroups: StoredGroup[];\n}\n\n/** The default KV key the group lives under. Exported so a caller can wipe\n * it. Hosts with a pre-existing record under another key pass `storageKey`. */\nexport const HOUSEHOLD_GROUP_KEY = 'selfstore:households:group:v1';\n\n/** Every membership publishes through its own mirror: the id is scoped by the\n * copy file (unique per share) so several never collide on one store. */\nconst mirrorId = (group: StoredGroup): string => `household-copy:${group.myCopy.fileId}`;\n\n// --- Public surface -----------------------------------------------------------\n\n/** One membership, as the UI lists them: which silo carries it and whose\n * share it is. */\nexport interface MembershipInfo {\n\t/** The wallet file this membership is bound to (null = unbound legacy). */\n\twalletFileId: string | null;\n\tisAdmin: boolean;\n\t/** My published copy's file id for this share. */\n\tselfFileId: string;\n\t/** Who shares it with me (the admin's roster label); null for my own share. */\n\tsharedBy: string | null;\n\tmemberCount: number;\n\t/** Member: my copy is not in a fresh roster yet (the announce may have been\n\t * lost; the engine keeps re-announcing). The host can show \"waiting for the\n\t * admin to see you\" instead of a false everything-is-fine. */\n\tannouncePending: boolean;\n\t/** Member: the admin rotated the link - edits publish into the void until a\n\t * fresh invitation is joined. The host must surface it. */\n\tstale: boolean;\n}\n\n/** The reactive-friendly snapshot the UI reads (hosts wrap it in their own\n * reactivity - a Svelte rune, a React store, anything). The singular fields\n * describe the PRIMARY membership (the admin one when it exists, else the\n * first); `memberships` lists them all. */\nexport interface HouseholdGroupState {\n\t/** A share exists on this device (admin or member). */\n\tactive: boolean;\n\tisAdmin: boolean;\n\t/** How many copies are in the roster (this device included). */\n\tmemberCount: number;\n\t/** The roster's cosmetic owner labels, for a \"who has access\" list. */\n\tmembers: { fileId: string; label: string }[];\n\t/** My own copy's file id, so the UI can list everyone ELSE. */\n\tselfFileId: string | null;\n\t/** The wallet file the share is bound to, or null when unbound. The host\n\t * compares it to its own attached-file id to render \"this silo carries the\n\t * share\" and to gate its own publications the same way the engine does. */\n\twalletFileId: string | null;\n\t/** Admin: the live invitation's capability, so the panel can re-show the\n\t * link without republishing anything. */\n\tinviteCapability: { fileId: string; key: string } | null;\n\t/** Every membership on this device, admin and joined alike. */\n\tmemberships: MembershipInfo[];\n}\n\nexport interface HouseholdGroup {\n\treadonly state: HouseholdGroupState;\n\t/** Admin: start sharing. The member's own file is untouched - a dedicated\n\t * copy is created and mirrored under a fresh K, and the invitation\n\t * bulletin is published. Refused on a password-protected store: remove\n\t * the password to share, or keep it and do not. */\n\tstartShare(): Promise<{ fileId: string; key: string }>;\n\t/** Admin: re-show (and refresh) the standing invitation link. Idempotent. */\n\tinvite(): Promise<{ fileId: string; key: string }>;\n\t/** /share: open an invitation link's bulletin and remember it for the join. */\n\topenIncoming(fileId: string, key: string): Promise<IncomingShare | null>;\n\t/** Member: after openIncoming + own store connected, join read-write now.\n\t * Your own file stays yours: a dedicated copy is created and mirrored\n\t * under K, the others' copies attach as keyed peers (the next converge\n\t * folds the shared data into your own file), and my copy is announced.\n\t * Same honest rule as startShare: a password-protected store refuses.\n\t * With a `wallet` getter, one membership per share: joining another share\n\t * ADDS a membership (bound to the attached silo), and a re-join over the\n\t * same silo replaces its stale one. Without the getter, the historical\n\t * single-membership rule holds ('mismatch' on a different share). */\n\tjoin(): Promise<'joined' | 'no-invite' | 'mismatch' | 'error'>;\n\t/** Both: one convergence round (admin folds new announces + rebroadcasts;\n\t * everyone re-arms mirror/peers). Called on every converge. */\n\tsyncGroup(): Promise<void>;\n\t/** End ONE share on this device - and, for the admin, this is the real\n\t * revocation (bearer link: the lock changes for everyone). The bulletin\n\t * is revoked, my copy file is dropped, mirror and peers detach. The\n\t * member's own file is untouched: everyone keeps what already converged into\n\t * their own file - the local-first reality. Re-sharing mints a fresh K\n\t * and a fresh link for whoever should stay. `walletFileId` names WHICH\n\t * membership; omitted, the primary one (admin first) goes. */\n\tleave(walletFileId?: string | null): Promise<void>;\n\t/** On boot: re-arm the mirror and peers of the current share. */\n\trestore(): Promise<void>;\n}\n\n// --- Factory --------------------------------------------------------------------\n\nexport function createHouseholdGroup(deps: {\n\tstore: LocalStore;\n\tkv: KV;\n\tbackend: ShareBackend;\n\t/** The cache's sealed space (`cache.sealedKv`). Given, the membership\n\t * record - K, the mailbox id, the invitation capability: the secrets that\n\t * ARE the membership - lives there, ciphertext at rest and, on a locked\n\t * cache, unreadable until the session unlocks. A record a previous version\n\t * wrote in the clear `kv` moves over on first load. Omitted, the record\n\t * stays in `kv` as before. */\n\tsealedKv?: KV;\n\t/** KV key the group record lives under. Default HOUSEHOLD_GROUP_KEY. */\n\tstorageKey?: string;\n\t/** The currently attached wallet file id, for multi-backup hosts. When\n\t * given, start/join bind the share to that file and the engine goes\n\t * dormant whenever another one is attached. Omit on single-wallet hosts. */\n\twallet?: () => Promise<string | null>;\n}): HouseholdGroup {\n\tconst { store, kv, backend, wallet } = deps;\n\tconst storageKey = deps.storageKey ?? HOUSEHOLD_GROUP_KEY;\n\t// Where the record lives: the sealed space when the host hands one over.\n\tconst recordKv = deps.sealedKv ?? kv;\n\n\tlet groups: StoredGroup[] | null = null;\n\tlet loaded = false;\n\t// Member roster mirror: how many consecutive fresh reads a known peer has\n\t// been MISSING from the bulletin. Removals apply only at 2 - a single stale\n\t// relay read must never cut a still-valid member's data off. Keys are\n\t// scoped per share (`mailboxId:fileId`).\n\tconst rosterMisses = new Map<string, number>();\n\n\t/** The stored record. A record an earlier version left in the clear kv\n\t * moves into the sealed space the first time it is read, and the clear\n\t * copy goes: K must not stay readable next to its ciphertext. */\n\tasync function readRecord(): Promise<StoredGroups | StoredGroup | undefined> {\n\t\tconst sealed = await recordKv.get<StoredGroups | StoredGroup>(storageKey);\n\t\tif (sealed !== undefined || recordKv === kv) return sealed;\n\t\tconst clear = await kv.get<StoredGroups | StoredGroup>(storageKey);\n\t\tif (clear === undefined) return undefined;\n\t\tawait recordKv.set(storageKey, clear);\n\t\tawait kv.del(storageKey);\n\t\treturn clear;\n\t}\n\n\tasync function load(): Promise<StoredGroup[]> {\n\t\tif (!loaded) {\n\t\t\tconst raw = await readRecord();\n\t\t\tif (!raw) groups = [];\n\t\t\telse if ((raw as StoredGroups).v === 2) groups = (raw as StoredGroups).groups;\n\t\t\t// A pre-multi single record lifts into a one-entry list, verbatim.\n\t\t\telse groups = [raw as StoredGroup];\n\t\t\tloaded = true;\n\t\t}\n\t\treturn groups as StoredGroup[];\n\t}\n\n\t/** Write the current list back (the callers mutate `groups` in place). An\n\t * empty list clears the key: no membership, no record. */\n\tasync function persist(): Promise<void> {\n\t\tloaded = true;\n\t\tif (!groups || groups.length === 0) {\n\t\t\tgroups = groups ?? [];\n\t\t\tawait recordKv.del(storageKey);\n\t\t\treturn;\n\t\t}\n\t\tawait recordKv.set(storageKey, { v: 2, groups } satisfies StoredGroups);\n\t}\n\n\tconst adminGroup = (list: StoredGroup[]): StoredGroup | null =>\n\t\tlist.find((g) => g.role === 'admin') ?? null;\n\t/** The membership the singular state fields describe: admin first. */\n\tconst primaryGroup = (list: StoredGroup[]): StoredGroup | null =>\n\t\tadminGroup(list) ?? list[0] ?? null;\n\t/** Who shares a joined group with me: the admin's copy leads the roster. */\n\tconst sharedByOf = (g: StoredGroup): string | null =>\n\t\tg.role === 'member' ? (g.roster[0]?.ownerEmail ?? g.roster[0]?.ownerName ?? null) : null;\n\n\t/** A fresh 32-byte key, base64url, for a new share (the link fragment). */\n\tfunction randomKey(): string {\n\t\tconst bytes = crypto.getRandomValues(new Uint8Array(32));\n\t\tlet s = '';\n\t\tfor (const b of bytes) s += String.fromCharCode(b);\n\t\treturn btoa(s).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n\t}\n\n\t/** The honest rule, enforced at both entrances: sharing never re-keys the\n\t * own file, so a password-protected store cannot publish a readable copy\n\t * without lowering the bar its owner chose. Refuse plainly. */\n\tfunction refuseProtected(): void {\n\t\tif (store.state.encrypted) {\n\t\t\tthrow new Error(\n\t\t\t\t'household-group: a password-protected store cannot be shared - remove the password first'\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Whether the share's bound wallet is the attached one. Unbound (or no\n\t * getter) is always in scope - the single-wallet behavior. */\n\tasync function inScope(group: StoredGroup): Promise<boolean> {\n\t\tif (!group.walletFileId || !wallet) return true;\n\t\treturn (await wallet()) === group.walletFileId;\n\t}\n\n\t/** Out-of-scope dormancy: tear down BOTH directions. The mirror would leak\n\t * the attached silo's data into the copy; the peers would fold the shared\n\t * data into that silo. Nothing else moves until the bound wallet is back. */\n\tfunction disarm(group: StoredGroup): void {\n\t\tstore.detachMirror(mirrorId(group));\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId !== group.myCopy.fileId) store.detachPeer(link.fileId);\n\t\t}\n\t}\n\n\t/** Re-arm my copy's mirror (idempotent - safe on every converge/boot). */\n\tfunction ensureMirror(group: StoredGroup): void {\n\t\tconst id = mirrorId(group);\n\t\tif (store.state.mirrors.some((m) => m.id === id)) return;\n\t\tstore.attachMirror(backend.copyTarget(group.myCopy.fileId), {\n\t\t\tpassword: group.key,\n\t\t\tid\n\t\t});\n\t}\n\n\t/** Attach every roster copy that is not mine and not already attached as a\n\t * read-only peer under K (its own key - the member's file has none). Idempotent. */\n\tfunction attachPeers(group: StoredGroup): void {\n\t\tconst already = new Set(store.state.peers.map((p) => p.id));\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\tif (already.has(link.fileId)) continue;\n\t\t\tstore.attachPeer(backend.peerSource(link), { id: link.fileId, password: group.key });\n\t\t}\n\t}\n\n\t/** Merge new copies into a roster, de-duplicated by fileId, dropping mine. */\n\tfunction mergeRoster(roster: CopyLink[], incoming: CopyLink[], selfFileId: string): CopyLink[] {\n\t\tconst byId = new Map(roster.map((l) => [l.fileId, l]));\n\t\tfor (const link of incoming) {\n\t\t\tif (link.fileId === selfFileId) continue;\n\t\t\tbyId.set(link.fileId, link);\n\t\t}\n\t\treturn [...byId.values()];\n\t}\n\n\t/** Post an announce with small bounded retries; true when one landed. A lost\n\t * announce strands the joiner - visible to nobody, editing into the void -\n\t * so it is worth a few immediate retries before falling back to the\n\t * persistent announcePending re-announce loop. */\n\tasync function announceWithRetry(mailboxId: string, copy: CopyLink): Promise<boolean> {\n\t\tfor (let attempt = 0; attempt < 3; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait backend.announce(mailboxId, copy);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\tif (attempt < 2) await new Promise((r) => setTimeout(r, 800 * (attempt + 1)));\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** One admin converge: fold newly-announced member copies, rebroadcast. */\n\tasync function adminRound(group: StoredGroup): Promise<void> {\n\t\tconst announced = await backend.takeAnnounces(group.mailboxId);\n\t\tconst roster = mergeRoster(group.roster, announced, group.myCopy.fileId);\n\t\tconst grew = roster.length !== group.roster.length;\n\t\tconst needed = group.needsRebroadcast === true;\n\t\tgroup.roster = roster;\n\t\tawait persist();\n\t\tattachPeers(group);\n\t\t// Rebroadcast on growth or when a previous rebroadcast failed: keyed\n\t\t// on `grew` alone, an offline publish left the bulletin stale forever\n\t\t// (the announce was already consumed, so no later round re-grew).\n\t\tif ((grew || needed) && group.live === true) {\n\t\t\ttry {\n\t\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\t\troster\n\t\t\t\t});\n\t\t\t\tgroup.bulletin = where;\n\t\t\t\tgroup.needsRebroadcast = undefined;\n\t\t\t} catch {\n\t\t\t\tgroup.needsRebroadcast = true;\n\t\t\t}\n\t\t\tawait persist();\n\t\t}\n\t}\n\n\t/** Mirror the authoritative roster onto the local one. Removals apply\n\t * CONSERVATIVELY: a peer drops only after two consecutive fresh reads\n\t * without it (stale relay tolerance). */\n\tfunction applyRosterRemovals(group: StoredGroup, byId: Map<string, CopyLink>): void {\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\tconst missKey = `${group.mailboxId}:${link.fileId}`;\n\t\t\tif (byId.has(link.fileId)) {\n\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst misses = (rosterMisses.get(missKey) ?? 0) + 1;\n\t\t\tif (misses >= 2) {\n\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\tstore.detachPeer(link.fileId);\n\t\t\t} else {\n\t\t\t\trosterMisses.set(missKey, misses);\n\t\t\t\tbyId.set(link.fileId, link);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** One member converge: reread the bulletin (the admin's roster is\n\t * AUTHORITATIVE - replace, not merge, so removals propagate), self-heal a\n\t * lost announce, track the stale verdict. Pre-multi records carry no\n\t * bulletin: their refresh stays dormant, peers still fold. */\n\tasync function memberRound(group: StoredGroup): Promise<void> {\n\t\tconst share = group.bulletin\n\t\t\t? await backend.rereadJoined(group.bulletin.fileId, group.bulletin.key)\n\t\t\t: null;\n\t\tif (share === 'unreadable') {\n\t\t\t// Fetched but no longer decrypts: the admin rotated the link. Two\n\t\t\t// consecutive verdicts set the stale flag the host must surface -\n\t\t\t// from here my edits publish into a copy nobody folds. Peers keep\n\t\t\t// folding (the member keeps what already converged).\n\t\t\tconst n = (group.unreadableReads ?? 0) + 1;\n\t\t\tgroup.unreadableReads = n;\n\t\t\tif (n >= 2) group.stale = true;\n\t\t\tawait persist();\n\t\t\tattachPeers(group);\n\t\t\treturn;\n\t\t}\n\t\tif (!share) {\n\t\t\t// Unreachable bulletin: conclude nothing - but a pending announce\n\t\t\t// still retries (the mailbox may answer even when the bulletin\n\t\t\t// relay does not), so a stranded joiner heals as soon as ANY\n\t\t\t// network path opens, not only after a full bulletin read.\n\t\t\tif (group.announcePending) {\n\t\t\t\tawait backend.announce(group.mailboxId, group.myCopy).catch(() => undefined);\n\t\t\t}\n\t\t\tattachPeers(group);\n\t\t\treturn;\n\t\t}\n\t\t// A readable bulletin is proof the share lives: clear staleness.\n\t\tgroup.unreadableReads = undefined;\n\t\tgroup.stale = undefined;\n\t\tconst byId = new Map(share.roster.map((l) => [l.fileId, l]));\n\t\t// SELF-HEAL: my own copy missing from a fresh roster means my\n\t\t// announce was lost. Announce again (with retries); the admin's\n\t\t// fold de-dups. announcePending tracks the truth either way, so\n\t\t// the host can say \"waiting to be seen\" instead of false calm.\n\t\tif (!byId.has(group.myCopy.fileId)) {\n\t\t\tbyId.set(group.myCopy.fileId, group.myCopy);\n\t\t\tgroup.announcePending = true;\n\t\t\tawait announceWithRetry(group.mailboxId, group.myCopy);\n\t\t} else {\n\t\t\tgroup.announcePending = undefined;\n\t\t}\n\t\tapplyRosterRemovals(group, byId);\n\t\tgroup.roster = [...byId.values()];\n\t\tawait persist();\n\t\tattachPeers(group);\n\t}\n\n\treturn {\n\t\tget state(): HouseholdGroupState {\n\t\t\tconst list = groups ?? [];\n\t\t\tconst memberships: MembershipInfo[] = list.map((g) => ({\n\t\t\t\twalletFileId: g.walletFileId ?? null,\n\t\t\t\tisAdmin: g.role === 'admin',\n\t\t\t\tselfFileId: g.myCopy.fileId,\n\t\t\t\tsharedBy: sharedByOf(g),\n\t\t\t\tmemberCount: g.roster.length,\n\t\t\t\tannouncePending: g.announcePending === true,\n\t\t\t\tstale: g.stale === true\n\t\t\t}));\n\t\t\tconst group = primaryGroup(list);\n\t\t\tif (!group) {\n\t\t\t\treturn {\n\t\t\t\t\tactive: false,\n\t\t\t\t\tisAdmin: false,\n\t\t\t\t\tmemberCount: 0,\n\t\t\t\t\tmembers: [],\n\t\t\t\t\tselfFileId: null,\n\t\t\t\t\twalletFileId: null,\n\t\t\t\t\tinviteCapability: null,\n\t\t\t\t\tmemberships\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tactive: true,\n\t\t\t\tisAdmin: group.role === 'admin',\n\t\t\t\tmemberCount: group.roster.length,\n\t\t\t\tmembers: group.roster.map((l) => ({\n\t\t\t\t\tfileId: l.fileId,\n\t\t\t\t\tlabel: l.ownerEmail ?? l.ownerName ?? l.fileId.slice(0, 8)\n\t\t\t\t})),\n\t\t\t\tselfFileId: group.myCopy.fileId,\n\t\t\t\twalletFileId: group.walletFileId ?? null,\n\t\t\t\tinviteCapability:\n\t\t\t\t\tgroup.role === 'admin' && group.live === true && group.bulletin ? group.bulletin : null,\n\t\t\t\tmemberships\n\t\t\t};\n\t\t},\n\n\t\tasync startShare(): Promise<{ fileId: string; key: string }> {\n\t\t\tconst list = await load();\n\t\t\t// One ADMIN share per device; joined memberships do not block it.\n\t\t\tif (adminGroup(list)) {\n\t\t\t\tthrow new Error('household-group: a share already exists on this device');\n\t\t\t}\n\t\t\trefuseProtected();\n\t\t\tconst key = randomKey();\n\t\t\tconst mailboxId = randomId();\n\t\t\tconst walletFileId = (await wallet?.()) ?? undefined;\n\t\t\t// The copy is a dedicated file - the member's own file is never in the roster.\n\t\t\tconst link = await backend.createCopy();\n\t\t\tconst roster = [link];\n\t\t\t// Persist before the publish: a failure at any later step leaves a\n\t\t\t// recoverable half-share (restore re-arms the mirror, invite() finishes\n\t\t\t// the publication). Nothing here is irreversible for the member's own file.\n\t\t\tconst group: StoredGroup = {\n\t\t\t\trole: 'admin',\n\t\t\t\twalletFileId,\n\t\t\t\tkey,\n\t\t\t\tmailboxId,\n\t\t\t\troster,\n\t\t\t\tmyCopy: link,\n\t\t\t\tlive: false\n\t\t\t};\n\t\t\tlist.push(group);\n\t\t\tawait persist();\n\t\t\t// The mirror fills the copy right away and keeps it fresh after every\n\t\t\t// save/converge that moves data. The member's own file is not touched.\n\t\t\tensureMirror(group);\n\t\t\tconst where = await backend.publishBulletin(key, { v: 1, mailboxId, roster });\n\t\t\tgroup.bulletin = where;\n\t\t\tgroup.live = true;\n\t\t\tawait persist();\n\t\t\treturn where;\n\t\t},\n\n\t\tasync invite(): Promise<{ fileId: string; key: string }> {\n\t\t\tconst group = adminGroup(await load());\n\t\t\tif (!group) {\n\t\t\t\tthrow new Error('household-group: invite() requires an admin share');\n\t\t\t}\n\t\t\t// Refresh the bulletin (fresh projection + current roster) and mark live.\n\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\tv: 1,\n\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\troster: group.roster\n\t\t\t});\n\t\t\tgroup.bulletin = where;\n\t\t\tgroup.live = true;\n\t\t\tawait persist();\n\t\t\treturn where;\n\t\t},\n\n\t\tasync openIncoming(fileId: string, key: string): Promise<IncomingShare | null> {\n\t\t\treturn backend.openIncoming(fileId, key);\n\t\t},\n\n\t\tasync join(): Promise<'joined' | 'no-invite' | 'mismatch' | 'error'> {\n\t\t\tconst list = await load();\n\t\t\tconst incoming = await backend.takeStashedIncoming();\n\t\t\t// No invitation opened: a device already holding a membership answers\n\t\t\t// the historical idempotent 'joined'; a bare one names the dead end.\n\t\t\tif (!incoming) return list.length > 0 ? 'joined' : 'no-invite';\n\t\t\tconst { key, fileId, content } = incoming;\n\t\t\tif (list.some((g) => g.mailboxId === content.share.mailboxId)) return 'joined';\n\t\t\t// Single-wallet hosts (no getter) keep the one-membership rule: an\n\t\t\t// invitation to ANOTHER share is a dead end, not a silent second join.\n\t\t\tif (!wallet && list.length > 0) return 'mismatch';\n\t\t\trefuseProtected();\n\t\t\tconst walletFileId = (await wallet?.()) ?? undefined;\n\t\t\t// A re-join over the same silo replaces its stale membership (a rotated\n\t\t\t// link): two shares must never publish the same wallet.\n\t\t\tif (walletFileId) {\n\t\t\t\tconst stale = list.find((g) => g.walletFileId === walletFileId);\n\t\t\t\tif (stale) {\n\t\t\t\t\tdisarm(stale);\n\t\t\t\t\tawait backend.dropCopy(stale.myCopy.fileId).catch(() => undefined);\n\t\t\t\t\tgroups = list.filter((g) => g !== stale);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// My copy is a dedicated file; my own store stays mine. The shared\n\t\t\t// data arrives by folding the others' copies into it on the next\n\t\t\t// converge - one store per person, converging.\n\t\t\tconst link = await backend.createCopy(undefined, content.share.roster[0]?.ownerEmail);\n\t\t\tconst roster = mergeRoster(content.share.roster, [link], '');\n\t\t\tconst group: StoredGroup = {\n\t\t\t\trole: 'member',\n\t\t\t\twalletFileId,\n\t\t\t\tkey,\n\t\t\t\tmailboxId: content.share.mailboxId,\n\t\t\t\troster,\n\t\t\t\tmyCopy: link,\n\t\t\t\t// The invitation capability, kept so THIS membership rereads ITS\n\t\t\t\t// bulletin on converge (roster removals, lost-announce self-heal).\n\t\t\t\tbulletin: { fileId, key }\n\t\t\t};\n\t\t\t(groups as StoredGroup[]).push(group);\n\t\t\tawait persist();\n\t\t\tensureMirror(group);\n\t\t\tattachPeers(group);\n\t\t\t// Announce my copy so the admin folds it into everyone's roster. NOT\n\t\t\t// best-effort-and-forget: a swallowed failure here once stranded a\n\t\t\t// joiner for good - member on their side, visible to nobody, editing\n\t\t\t// into the void. Retry now; still failing, persist announcePending so\n\t\t\t// every converge keeps announcing until a fresh roster carries me.\n\t\t\tif (!(await announceWithRetry(content.share.mailboxId, link))) {\n\t\t\t\tgroup.announcePending = true;\n\t\t\t\tawait persist();\n\t\t\t}\n\t\t\treturn 'joined';\n\t\t},\n\n\t\tasync syncGroup(): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\tfor (const group of list) {\n\t\t\t\t// Defense in depth for the honest rule: a store that became\n\t\t\t\t// password-protected (or joined from a device whose store turned out\n\t\t\t\t// protected) must never publish a link-readable copy. Tear the mirror\n\t\t\t\t// down if one was armed; the share lies dormant on this device until\n\t\t\t\t// the password is removed. (Folding INBOUND peers would be harmless,\n\t\t\t\t// but a dormant share should read as dormant - arm nothing.)\n\t\t\t\tif (store.state.encrypted) {\n\t\t\t\t\tstore.detachMirror(mirrorId(group));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Another silo is attached: full dormancy, both directions (see disarm).\n\t\t\t\tif (!(await inScope(group))) {\n\t\t\t\t\tdisarm(group);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tensureMirror(group);\n\t\t\t\tif (group.role === 'admin') await adminRound(group);\n\t\t\t\telse await memberRound(group);\n\t\t\t}\n\t\t},\n\n\t\tasync leave(walletFileId?: string | null): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\tconst group =\n\t\t\t\twalletFileId != null\n\t\t\t\t\t? (list.find((g) => g.walletFileId === walletFileId) ?? null)\n\t\t\t\t\t: primaryGroup(list);\n\t\t\tif (!group) return;\n\t\t\t// Detach the plumbing first: nothing publishes or folds anymore.\n\t\t\tdisarm(group);\n\t\t\tif (group.role === 'admin') {\n\t\t\t\ttry {\n\t\t\t\t\tawait backend.revokeBulletin();\n\t\t\t\t} catch {\n\t\t\t\t\t/* tolerable: the bulletin only ever holds K-encrypted data */\n\t\t\t\t}\n\t\t\t}\n\t\t\t// My copy file dies with the membership (private first, then gone).\n\t\t\t// The member's own file is untouched: it never carried the share key, and the\n\t\t\t// data already converged into it stays - local-first honesty.\n\t\t\tawait backend.dropCopy(group.myCopy.fileId).catch(() => undefined);\n\t\t\tgroups = list.filter((g) => g !== group);\n\t\t\tfor (const k of [...rosterMisses.keys()]) {\n\t\t\t\tif (k.startsWith(`${group.mailboxId}:`)) rosterMisses.delete(k);\n\t\t\t}\n\t\t\tawait persist();\n\t\t},\n\n\t\tasync restore(): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\t// Same dormancy rules as syncGroup: a protected store never mirrors,\n\t\t\t// and a share bound to another silo arms nothing while it is away.\n\t\t\tif (store.state.encrypted) return;\n\t\t\tfor (const group of list) {\n\t\t\t\tif (!(await inScope(group))) continue;\n\t\t\t\tensureMirror(group);\n\t\t\t\tattachPeers(group);\n\t\t\t}\n\t\t}\n\t};\n}\n"]}
|