selfstore 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/advanced.d.ts +5 -5
- package/dist/advanced.js +7 -7
- package/dist/backups.d.ts +1 -1
- package/dist/backups.js +1 -1
- package/dist/backups.js.map +1 -1
- package/dist/{cache-lock-LZILZNLP.js → cache-lock-NXIR6JRB.js} +3 -3
- package/dist/{cache-lock-LZILZNLP.js.map → cache-lock-NXIR6JRB.js.map} +1 -1
- package/dist/{chunk-SGISYMT5.js → chunk-43BJDRPT.js} +2 -2
- package/dist/{chunk-SGISYMT5.js.map → chunk-43BJDRPT.js.map} +1 -1
- package/dist/{chunk-FDPAKHQS.js → chunk-6746AL4I.js} +9 -4
- package/dist/chunk-6746AL4I.js.map +1 -0
- package/dist/{chunk-ATUPHVLG.js → chunk-CUFKME4S.js} +5 -5
- package/dist/chunk-CUFKME4S.js.map +1 -0
- package/dist/{chunk-OWLX6PNT.js → chunk-KHER6MWR.js} +26 -8
- package/dist/chunk-KHER6MWR.js.map +1 -0
- package/dist/{chunk-IBIEKK36.js → chunk-SKWHKM25.js} +24 -5
- package/dist/chunk-SKWHKM25.js.map +1 -0
- package/dist/{chunk-JB4JHM7V.js → chunk-SRKUHFRG.js} +17 -8
- package/dist/chunk-SRKUHFRG.js.map +1 -0
- package/dist/{chunk-CUWB5EWF.js → chunk-TEIAFKFS.js} +63 -22
- package/dist/chunk-TEIAFKFS.js.map +1 -0
- package/dist/{chunk-BJ3RUMGI.js → chunk-UWVAKBU4.js} +3 -3
- package/dist/{chunk-BJ3RUMGI.js.map → chunk-UWVAKBU4.js.map} +1 -1
- package/dist/{chunk-N4RPMX3Y.js → chunk-XI3MGK4M.js} +120 -30
- package/dist/chunk-XI3MGK4M.js.map +1 -0
- package/dist/{drive-Bv94UM8b.d.ts → drive-DNiDXBDe.d.ts} +7 -2
- package/dist/flows.d.ts +2 -2
- package/dist/flows.js +7 -7
- package/dist/flows.js.map +1 -1
- package/dist/groups.js +3 -3
- package/dist/groups.js.map +1 -1
- package/dist/households.d.ts +1 -1
- package/dist/households.js +10 -4
- package/dist/households.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -11
- package/dist/index.js.map +1 -1
- package/dist/{local-2py_vGGy.d.ts → local-DsXG7QW7.d.ts} +2 -2
- package/dist/local-KIP25T7I.js +7 -0
- package/dist/{local-AM22RWRR.js.map → local-KIP25T7I.js.map} +1 -1
- package/dist/{store-ChIGEOph.d.ts → store-2RnED1Vq.d.ts} +5 -0
- package/dist/sync.js +1 -1
- package/dist/widgets.d.ts +2 -2
- package/dist/widgets.js +445 -86
- package/dist/widgets.js.map +1 -1
- package/package.json +129 -97
- package/dist/chunk-ATUPHVLG.js.map +0 -1
- package/dist/chunk-CUWB5EWF.js.map +0 -1
- package/dist/chunk-FDPAKHQS.js.map +0 -1
- package/dist/chunk-IBIEKK36.js.map +0 -1
- package/dist/chunk-JB4JHM7V.js.map +0 -1
- package/dist/chunk-N4RPMX3Y.js.map +0 -1
- package/dist/chunk-OWLX6PNT.js.map +0 -1
- package/dist/local-AM22RWRR.js +0 -7
package/dist/flows.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/flows/replica.ts"],"names":["connect","config","fromSession"],"mappings":"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","file":"flows.js","sourcesContent":["// The backup-copy journey: attach ONE replica (a second destination receiving\n// the same encrypted backup as the primary home) behind a small state machine\n// the widgets can render. Reuses the connect flow's target specs, but there is\n// no password step, no conflict step and no deadline: a replica carries the\n// primary's bytes as-is, and every connector may contain a human gesture.\n\nimport type { BackupTarget } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { ReplicaState, StoreError } from '../persistence/store';\nimport { makeMachine, toStoreError, type FlowStore } from './machine';\nimport type { ConnectKind, ConnectTargets, Connector, FlowHost } from './connect';\nimport {\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\ttype DriveAuth\n} from '../persistence/targets/drive';\nimport { connect as fileConnect, fromSession as fileFromSession } from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\ttype WebdavConfig\n} from '../persistence/targets/webdav';\nimport { connect as s3Connect, fromSession as s3FromSession, type S3Config } from '../persistence/targets/s3';\n\n/** The one replica this flow manages, as its id on the engine. */\nexport const REPLICA_ID = 'replica';\n\n// The replica's target session lives in the SAME kv as the primary's, so every\n// key is prefixed: a second driveTarget (say) must never read or clobber the\n// primary connection's session keys.\nconst SCOPE = 'replica:';\nconst RECORD_KEY = 'record';\n\n/** What survives a reboot: which provider the copy lives on. The target's own\n * session (file id, handle, server config) is persisted by the target module\n * itself, under the scoped kv. */\ninterface ReplicaRecord {\n\tkind: ConnectKind;\n}\n\nexport type ReplicaStep = 'idle' | 'choose' | 'form-webdav' | 'form-s3';\n\nexport interface ReplicaSnapshot {\n\tstep: ReplicaStep;\n\t/** The kinds the host offers, in ConnectTargets order. */\n\tkinds: ConnectKind[];\n\tbusy: boolean;\n\terror: StoreError | null;\n\t/** The live engine entry (label, lastPublishAt, lastError), null when none. */\n\treplica: ReplicaState | null;\n}\n\nexport interface ReplicaFlowOptions {\n\t/** File name of the copy on its destination. Default: the primary backup\n\t * name with `.replica` before the extension (\"App.zip\" -> \"App.replica.zip\"),\n\t * which a same-account Drive never confuses with the primary file and the\n\t * backups panel never lists as a backup of its own. */\n\tfileName?: string;\n\t/** Rebuild the target of a custom Connector kind on restore() (built-in\n\t * kinds rebuild by themselves). Without it, custom kinds are not restored. */\n\trestoreTarget?: (kind: ConnectKind) => Promise<BackupTarget | null>;\n}\n\nexport interface ReplicaFlow extends FlowStore<ReplicaSnapshot> {\n\t/** Show the destination choice (from the user's gesture). */\n\topen(): void;\n\t/** Back to idle; never an error. */\n\tcancel(): void;\n\t/** Run a destination (inside the user's click - pickers and consent popups\n\t * open from it). `webdav: true` / `s3: true` route to their form step. */\n\tpick(kind: ConnectKind): void;\n\t/** 'form-webdav' step: submit the server config. */\n\tsubmitWebdav(config: WebdavConfig): void;\n\t/** 'form-s3' step: submit the bucket config. */\n\tsubmitS3(config: S3Config): void;\n\t/** Detach the copy and forget its session. Never deletes remote data. */\n\tremove(): Promise<void>;\n\t/** Silent reboot: re-attach the copy recorded by a past session, if any.\n\t * Never throws - an unreachable destination retries at the next boot. */\n\trestore(): Promise<void>;\n\t/** Detach from the engine (the replica itself stays attached). */\n\tdispose(): void;\n}\n\nfunction scopedKv(kv: KV, prefix: string): KV {\n\treturn {\n\t\tget: <T = unknown>(key: string) => kv.get<T>(prefix + key),\n\t\tset: (key: string, value: unknown) => kv.set(prefix + key, value),\n\t\tdel: (key: string) => kv.del(prefix + key)\n\t};\n}\n\nfunction replicaFileName(backupName: string): string {\n\tconst dot = backupName.lastIndexOf('.');\n\treturn dot > 0\n\t\t? `${backupName.slice(0, dot)}.replica${backupName.slice(dot)}`\n\t\t: `${backupName}.replica`;\n}\n\nexport function replicaFlow(\n\tstore: FlowHost | { flowHost: FlowHost },\n\ttargets: ConnectTargets,\n\toptions: ReplicaFlowOptions = {}\n): ReplicaFlow {\n\tconst host = 'flowHost' in store ? store.flowHost : store;\n\tconst engine = host.engine;\n\tconst skv = scopedKv(host.kv, SCOPE);\n\tconst fileName = options.fileName ?? replicaFileName(host.backupName);\n\tconst kinds = (Object.keys(targets) as ConnectKind[]).filter(\n\t\t(k) => (k === 'drive' || k === 'file' || k === 'webdav' || k === 's3') && targets[k] != null\n\t);\n\n\tconst current = (): ReplicaState | null =>\n\t\tengine.state.replicas.find((r) => r.id === REPLICA_ID) ?? null;\n\n\tconst m = makeMachine<ReplicaSnapshot>({\n\t\tstep: 'idle',\n\t\tkinds,\n\t\tbusy: false,\n\t\terror: null,\n\t\treplica: current()\n\t});\n\n\t// Follow the engine: every publish (or publish failure) refreshes the entry.\n\tconst unsub = engine.subscribe(() => m.set({ replica: current() }));\n\n\t// Held for remove(): disconnect() lets the target forget its own session.\n\tlet attached: BackupTarget | null = null;\n\n\tfunction connectorFor(kind: ConnectKind): Connector | null {\n\t\tconst spec = targets[kind];\n\t\tif (spec == null) return null;\n\t\tif (typeof spec === 'function') return spec;\n\t\tif (kind === 'drive') {\n\t\t\tconst auth = spec as DriveAuth;\n\t\t\treturn () => driveConnect({ auth, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') {\n\t\t\treturn () => fileConnect({ kv: skv, fileName });\n\t\t}\n\t\tif (kind === 's3') {\n\t\t\tconst config = spec as S3Config; // 's3: true' goes through the form step\n\t\t\treturn () => s3Connect({ kv: skv, config });\n\t\t}\n\t\tconst config = spec as WebdavConfig; // 'webdav: true' goes through the form step\n\t\treturn () => webdavConnect({ kv: skv, config });\n\t}\n\n\tasync function run(kind: ConnectKind, connector: Connector): Promise<void> {\n\t\tm.set({ busy: true, error: null });\n\t\ttry {\n\t\t\tconst target = await connector();\n\t\t\tif (!target) {\n\t\t\t\t// Cancelled picker/consent: stay where the user was, no error.\n\t\t\t\tm.set({ busy: false });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID); // swap, never a dup-id throw\n\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\tattached = target;\n\t\t\tawait skv.set(RECORD_KEY, { kind } satisfies ReplicaRecord);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t} catch (e) {\n\t\t\tm.set({ busy: false, error: toStoreError(e) });\n\t\t}\n\t}\n\n\tasync function builtinFromSession(kind: ConnectKind): Promise<BackupTarget | null> {\n\t\tif (kind === 'drive') {\n\t\t\tconst spec = targets.drive;\n\t\t\tif (spec == null || typeof spec === 'function') return null;\n\t\t\treturn driveFromSession({ auth: spec, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') return fileFromSession({ kv: skv });\n\t\tif (kind === 'webdav') return webdavFromSession({ kv: skv });\n\t\treturn s3FromSession({ kv: skv });\n\t}\n\n\treturn {\n\t\tget snapshot() {\n\t\t\treturn m.snapshot;\n\t\t},\n\t\tsubscribe: m.subscribe,\n\n\t\topen() {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tm.set({ step: 'choose', error: null });\n\t\t},\n\n\t\tcancel() {\n\t\t\tm.set({ step: 'idle', busy: false, error: null });\n\t\t},\n\n\t\tpick(kind) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tconst spec = targets[kind];\n\t\t\tif (spec == null) return;\n\t\t\tif (kind === 'webdav' && spec === true) {\n\t\t\t\tm.set({ step: 'form-webdav', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kind === 's3' && spec === true) {\n\t\t\t\tm.set({ step: 'form-s3', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst connector = connectorFor(kind);\n\t\t\tif (connector) void run(kind, connector);\n\t\t},\n\n\t\tsubmitWebdav(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('webdav', () => webdavConnect({ kv: skv, config }));\n\t\t},\n\n\t\tsubmitS3(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('s3', () => s3Connect({ kv: skv, config }));\n\t\t},\n\n\t\tasync remove() {\n\t\t\tconst target = attached;\n\t\t\tattached = null;\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID);\n\t\t\tawait skv.del(RECORD_KEY);\n\t\t\t// Forget locally only - a BackupTarget's disconnect never deletes\n\t\t\t// remote data, so the copy stays where it is for a manual recovery.\n\t\t\tawait target?.disconnect().catch(() => undefined);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t},\n\n\t\tasync restore() {\n\t\t\ttry {\n\t\t\t\tif (current()) return;\n\t\t\t\tconst record = await skv.get<ReplicaRecord>(RECORD_KEY);\n\t\t\t\tif (!record) return;\n\t\t\t\tconst target =\n\t\t\t\t\t(await options.restoreTarget?.(record.kind)) ?? (await builtinFromSession(record.kind));\n\t\t\t\tif (!target) return; // unreachable or custom kind: retry next boot\n\t\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\t\tattached = target;\n\t\t\t\tm.set({ replica: current() });\n\t\t\t} catch {\n\t\t\t\t// Silent reboot by contract: the record stays, the next boot retries.\n\t\t\t}\n\t\t},\n\n\t\tdispose() {\n\t\t\tunsub();\n\t\t}\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/flows/replica.ts"],"names":["connect","config","fromSession"],"mappings":"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","file":"flows.js","sourcesContent":["// The backup-copy journey: attach ONE replica (a second destination receiving\n// the same encrypted backup as the primary home) behind a small state machine\n// the widgets can render. Reuses the connect flow's target specs, but there is\n// no password step, no conflict step and no deadline: a replica carries the\n// primary's bytes as-is, and every connector may contain a human gesture.\n\nimport type { BackupTarget } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { ReplicaState, StoreError } from '../persistence/store';\nimport { makeMachine, toStoreError, type FlowStore } from './machine';\nimport type { ConnectKind, ConnectTargets, Connector, FlowHost } from './connect';\nimport {\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\ttype DriveAuth\n} from '../persistence/targets/drive';\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\ttype WebdavConfig\n} from '../persistence/targets/webdav';\nimport {\n\tconnect as s3Connect,\n\tfromSession as s3FromSession,\n\ttype S3Config\n} from '../persistence/targets/s3';\n\n/** The one replica this flow manages, as its id on the engine. */\nexport const REPLICA_ID = 'replica';\n\n// The replica's target session lives in the SAME kv as the primary's, so every\n// key is prefixed: a second driveTarget (say) must never read or clobber the\n// primary connection's session keys.\nconst SCOPE = 'replica:';\nconst RECORD_KEY = 'record';\n\n/** What survives a reboot: which provider the copy lives on. The target's own\n * session (file id, handle, server config) is persisted by the target module\n * itself, under the scoped kv. */\ninterface ReplicaRecord {\n\tkind: ConnectKind;\n}\n\nexport type ReplicaStep = 'idle' | 'choose' | 'form-webdav' | 'form-s3';\n\nexport interface ReplicaSnapshot {\n\tstep: ReplicaStep;\n\t/** The kinds the host offers, in ConnectTargets order. */\n\tkinds: ConnectKind[];\n\tbusy: boolean;\n\terror: StoreError | null;\n\t/** The live engine entry (label, lastPublishAt, lastError), null when none. */\n\treplica: ReplicaState | null;\n}\n\nexport interface ReplicaFlowOptions {\n\t/** File name of the copy on its destination. Default: the primary backup\n\t * name with `.replica` before the extension (\"App.zip\" -> \"App.replica.zip\"),\n\t * which a same-account Drive never confuses with the primary file and the\n\t * backups panel never lists as a backup of its own. */\n\tfileName?: string;\n\t/** Rebuild the target of a custom Connector kind on restore() (built-in\n\t * kinds rebuild by themselves). Without it, custom kinds are not restored. */\n\trestoreTarget?: (kind: ConnectKind) => Promise<BackupTarget | null>;\n}\n\nexport interface ReplicaFlow extends FlowStore<ReplicaSnapshot> {\n\t/** Show the destination choice (from the user's gesture). */\n\topen(): void;\n\t/** Back to idle; never an error. */\n\tcancel(): void;\n\t/** Run a destination (inside the user's click - pickers and consent popups\n\t * open from it). `webdav: true` / `s3: true` route to their form step. */\n\tpick(kind: ConnectKind): void;\n\t/** 'form-webdav' step: submit the server config. */\n\tsubmitWebdav(config: WebdavConfig): void;\n\t/** 'form-s3' step: submit the bucket config. */\n\tsubmitS3(config: S3Config): void;\n\t/** Detach the copy and forget its session. Never deletes remote data. */\n\tremove(): Promise<void>;\n\t/** Silent reboot: re-attach the copy recorded by a past session, if any.\n\t * Never throws - an unreachable destination retries at the next boot. */\n\trestore(): Promise<void>;\n\t/** Detach from the engine (the replica itself stays attached). */\n\tdispose(): void;\n}\n\nfunction scopedKv(kv: KV, prefix: string): KV {\n\treturn {\n\t\tget: <T = unknown>(key: string) => kv.get<T>(prefix + key),\n\t\tset: (key: string, value: unknown) => kv.set(prefix + key, value),\n\t\tdel: (key: string) => kv.del(prefix + key)\n\t};\n}\n\nfunction replicaFileName(backupName: string): string {\n\tconst dot = backupName.lastIndexOf('.');\n\treturn dot > 0\n\t\t? `${backupName.slice(0, dot)}.replica${backupName.slice(dot)}`\n\t\t: `${backupName}.replica`;\n}\n\nexport function replicaFlow(\n\tstore: FlowHost | { flowHost: FlowHost },\n\ttargets: ConnectTargets,\n\toptions: ReplicaFlowOptions = {}\n): ReplicaFlow {\n\tconst host = 'flowHost' in store ? store.flowHost : store;\n\tconst engine = host.engine;\n\tconst skv = scopedKv(host.kv, SCOPE);\n\tconst fileName = options.fileName ?? replicaFileName(host.backupName);\n\tconst kinds = (Object.keys(targets) as ConnectKind[]).filter(\n\t\t(k) => (k === 'drive' || k === 'file' || k === 'webdav' || k === 's3') && targets[k] != null\n\t);\n\n\tconst current = (): ReplicaState | null =>\n\t\tengine.state.replicas.find((r) => r.id === REPLICA_ID) ?? null;\n\n\tconst m = makeMachine<ReplicaSnapshot>({\n\t\tstep: 'idle',\n\t\tkinds,\n\t\tbusy: false,\n\t\terror: null,\n\t\treplica: current()\n\t});\n\n\t// Follow the engine: every publish (or publish failure) refreshes the entry.\n\tconst unsub = engine.subscribe(() => m.set({ replica: current() }));\n\n\t// Held for remove(): disconnect() lets the target forget its own session.\n\tlet attached: BackupTarget | null = null;\n\n\tfunction connectorFor(kind: ConnectKind): Connector | null {\n\t\tconst spec = targets[kind];\n\t\tif (spec == null) return null;\n\t\tif (typeof spec === 'function') return spec;\n\t\tif (kind === 'drive') {\n\t\t\tconst auth = spec as DriveAuth;\n\t\t\treturn () => driveConnect({ auth, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') {\n\t\t\treturn () => fileConnect({ kv: skv, fileName });\n\t\t}\n\t\tif (kind === 's3') {\n\t\t\tconst config = spec as S3Config; // 's3: true' goes through the form step\n\t\t\treturn () => s3Connect({ kv: skv, config });\n\t\t}\n\t\tconst config = spec as WebdavConfig; // 'webdav: true' goes through the form step\n\t\treturn () => webdavConnect({ kv: skv, config });\n\t}\n\n\tasync function run(kind: ConnectKind, connector: Connector): Promise<void> {\n\t\tm.set({ busy: true, error: null });\n\t\ttry {\n\t\t\tconst target = await connector();\n\t\t\tif (!target) {\n\t\t\t\t// Cancelled picker/consent: stay where the user was, no error.\n\t\t\t\tm.set({ busy: false });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID); // swap, never a dup-id throw\n\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\tattached = target;\n\t\t\tawait skv.set(RECORD_KEY, { kind } satisfies ReplicaRecord);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t} catch (e) {\n\t\t\tm.set({ busy: false, error: toStoreError(e) });\n\t\t}\n\t}\n\n\tasync function builtinFromSession(kind: ConnectKind): Promise<BackupTarget | null> {\n\t\tif (kind === 'drive') {\n\t\t\tconst spec = targets.drive;\n\t\t\tif (spec == null || typeof spec === 'function') return null;\n\t\t\treturn driveFromSession({ auth: spec, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') return fileFromSession({ kv: skv });\n\t\tif (kind === 'webdav') return webdavFromSession({ kv: skv });\n\t\treturn s3FromSession({ kv: skv });\n\t}\n\n\treturn {\n\t\tget snapshot() {\n\t\t\treturn m.snapshot;\n\t\t},\n\t\tsubscribe: m.subscribe,\n\n\t\topen() {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tm.set({ step: 'choose', error: null });\n\t\t},\n\n\t\tcancel() {\n\t\t\tm.set({ step: 'idle', busy: false, error: null });\n\t\t},\n\n\t\tpick(kind) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tconst spec = targets[kind];\n\t\t\tif (spec == null) return;\n\t\t\tif (kind === 'webdav' && spec === true) {\n\t\t\t\tm.set({ step: 'form-webdav', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kind === 's3' && spec === true) {\n\t\t\t\tm.set({ step: 'form-s3', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst connector = connectorFor(kind);\n\t\t\tif (connector) void run(kind, connector);\n\t\t},\n\n\t\tsubmitWebdav(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('webdav', () => webdavConnect({ kv: skv, config }));\n\t\t},\n\n\t\tsubmitS3(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('s3', () => s3Connect({ kv: skv, config }));\n\t\t},\n\n\t\tasync remove() {\n\t\t\tconst target = attached;\n\t\t\tattached = null;\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID);\n\t\t\tawait skv.del(RECORD_KEY);\n\t\t\t// Forget locally only - a BackupTarget's disconnect never deletes\n\t\t\t// remote data, so the copy stays where it is for a manual recovery.\n\t\t\tawait target?.disconnect().catch(() => undefined);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t},\n\n\t\tasync restore() {\n\t\t\ttry {\n\t\t\t\tif (current()) return;\n\t\t\t\tconst record = await skv.get<ReplicaRecord>(RECORD_KEY);\n\t\t\t\tif (!record) return;\n\t\t\t\tconst target =\n\t\t\t\t\t(await options.restoreTarget?.(record.kind)) ?? (await builtinFromSession(record.kind));\n\t\t\t\tif (!target) return; // unreachable or custom kind: retry next boot\n\t\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\t\tattached = target;\n\t\t\t\tm.set({ replica: current() });\n\t\t\t} catch {\n\t\t\t\t// Silent reboot by contract: the record stays, the next boot retries.\n\t\t\t}\n\t\t},\n\n\t\tdispose() {\n\t\t\tunsub();\n\t\t}\n\t};\n}\n"]}
|
package/dist/groups.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { generateIdentity } from './chunk-
|
|
2
|
-
export { GROUP_KEYING, generateIdentity, groupCryptoAvailable, keyId, newGroupId, openManifest, publicIdentity, signManifest } from './chunk-
|
|
1
|
+
import { generateIdentity } from './chunk-KHER6MWR.js';
|
|
2
|
+
export { GROUP_KEYING, generateIdentity, groupCryptoAvailable, keyId, newGroupId, openManifest, publicIdentity, signManifest } from './chunk-KHER6MWR.js';
|
|
3
3
|
import { seal, isEnvelope, unseal, newDeviceKey } from './chunk-QBCU7OOP.js';
|
|
4
|
-
import { assertKdfBounds, deriveKey, freshKdfParams } from './chunk-
|
|
4
|
+
import { assertKdfBounds, deriveKey, freshKdfParams } from './chunk-6746AL4I.js';
|
|
5
5
|
import { SelfstoreError } from './chunk-VBFMMR7Q.js';
|
|
6
6
|
|
|
7
7
|
// src/persistence/identity.ts
|
package/dist/groups.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/persistence/identity.ts"],"names":[],"mappings":"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","file":"groups.js","sourcesContent":["// At-rest storage for a member identity (passwordless groups). Private keys\n// are sealed under a non-extractable device key in the same kv - defeats disk\n// forensics and partial exfiltration, not code running in the origin or a\n// copy of the whole profile (THREAT-MODEL.md).\n//\n// The opt-in passphrase lock reseals the identity under Argon2id(passphrase)\n// instead, closing the profile-copy gap at the cost of an unlock prompt. The\n// vault keeps no unlocked session state: unlock() returns the identity, the\n// caller holds it in memory, and at rest it stays sealed. A bare envelope\n// keeps meaning device mode, so pre-lock vaults load unchanged.\n//\n// The identity is one person's; move it between their devices over a trusted\n// channel (QR shown locally, encrypted note) - never through the group's\n// shared storage.\n\nimport { seal, unseal, isEnvelope, newDeviceKey, type EncEnvelope } from './cache-crypto';\nimport type { KV } from './cache';\nimport { generateIdentity, type GroupIdentity } from '../selfstore/group';\nimport { deriveKey, freshKdfParams, assertKdfBounds } from '../selfstore/crypto';\nimport type { KdfParams } from '../selfstore/types';\nimport { SelfstoreError } from '../selfstore/errors';\n\nconst VAULT_KEY = 'groupVaultKey';\nconst IDENTITY_KEY = 'groupIdentity';\n\nconst utf8 = { enc: new TextEncoder(), dec: new TextDecoder() };\n\n// Local kv bookkeeping only - not part of the interchange format (SPEC.md).\ninterface VaultLock {\n\tselfstoreVaultLock: 1;\n\tkdf: KdfParams;\n\tenvelope: EncEnvelope;\n}\n\nfunction isVaultLock(v: unknown): v is VaultLock {\n\treturn (\n\t\ttypeof v === 'object' && v !== null && (v as { selfstoreVaultLock?: unknown }).selfstoreVaultLock === 1\n\t);\n}\n\nconst encode = (identity: GroupIdentity): Uint8Array => utf8.enc.encode(JSON.stringify(identity));\nconst decode = (bytes: Uint8Array): GroupIdentity =>\n\tJSON.parse(utf8.dec.decode(bytes)) as GroupIdentity;\n\nexport interface IdentityVault {\n\t/** The stored identity, or null when none exists yet. On a protected\n\t * vault this throws PASSWORD_REQUIRED rather than returning null - a\n\t * locked identity exists, and null would invite loadOrCreate to mint a\n\t * second one over it. */\n\tload(): Promise<GroupIdentity | null>;\n\t/** Seal and store an identity, overwriting any previous one. Throws\n\t * PASSWORD_REQUIRED on a protected vault: a locked identity is never\n\t * silently replaced (unprotect() or clear() first). */\n\tsave(identity: GroupIdentity): Promise<void>;\n\t/** load(), creating and storing a fresh identity when absent. Same\n\t * PASSWORD_REQUIRED rule as load(). */\n\tloadOrCreate(): Promise<GroupIdentity>;\n\t/** Forget the identity on this device. Allowed even when protected -\n\t * forgetting leaks nothing, and it is the honest lost-passphrase escape\n\t * hatch (re-import from another device, or get re-invited). */\n\tclear(): Promise<void>;\n\tisProtected(): Promise<boolean>;\n\t/** Decrypt a protected vault and return the identity. Stateless: the\n\t * caller holds it in memory, the stored value stays sealed. Wrong\n\t * passphrase and tampered blob are one DECRYPT_FAILED; hostile KDF\n\t * params are refused before any derivation. */\n\tunlock(passphrase: string): Promise<GroupIdentity>;\n\t/** Reseal the identity under Argon2id(passphrase); an empty vault gets a\n\t * fresh identity, created-and-protected in one step. Changing the\n\t * passphrase is unprotect(old) then protect(new) - an already-protected\n\t * vault throws PASSWORD_REQUIRED, never a silent rekey. */\n\tprotect(passphrase: string): Promise<void>;\n\t/** Unlock with `passphrase`, then reseal under the device key. A wrong\n\t * passphrase throws DECRYPT_FAILED and leaves the vault untouched. */\n\tunprotect(passphrase: string): Promise<void>;\n}\n\n/** An identity vault over the app's kv (e.g. `indexedDbCache(app).kv`). */\nexport function identityVault(kv: KV): IdentityVault {\n\tasync function vaultKey(): Promise<CryptoKey> {\n\t\tconst existing = await kv.get<CryptoKey>(VAULT_KEY);\n\t\tif (existing) return existing;\n\t\tconst fresh = await newDeviceKey();\n\t\tawait kv.set(VAULT_KEY, fresh);\n\t\treturn fresh;\n\t}\n\n\t/** Device mode's write path: a bare envelope under the device key. */\n\tasync function sealDevice(identity: GroupIdentity): Promise<void> {\n\t\tawait kv.set(IDENTITY_KEY, await seal(await vaultKey(), encode(identity)));\n\t}\n\n\tasync function load(): Promise<GroupIdentity | null> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected. Call unlock(passphrase) - ' +\n\t\t\t\t\t'load()/loadOrCreate() never open or replace a locked identity.'\n\t\t\t);\n\t\t}\n\t\tif (!isEnvelope(stored)) return null;\n\t\treturn decode(await unseal(await vaultKey(), stored));\n\t}\n\n\tasync function save(identity: GroupIdentity): Promise<void> {\n\t\tif (isVaultLock(await kv.get(IDENTITY_KEY))) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected; save() never overwrites a locked ' +\n\t\t\t\t\t'identity. unprotect(passphrase) first, or clear() to forget it.'\n\t\t\t);\n\t\t}\n\t\tawait sealDevice(identity);\n\t}\n\n\tasync function unlock(passphrase: string): Promise<GroupIdentity> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (!isVaultLock(stored)) {\n\t\t\tthrow new TypeError('unlock(): the vault is not passphrase-protected (check isProtected()).');\n\t\t}\n\t\tif (!stored.kdf || !isEnvelope(stored.envelope)) {\n\t\t\tthrow new SelfstoreError('BAD_FORMAT', 'Missing or malformed vault lock parameters.');\n\t\t}\n\t\t// Same read-side ceiling as backup decrypts: a tampered kv must not make\n\t\t// unlock allocate gigabytes (memory-hard bomb) before any passphrase check.\n\t\tassertKdfBounds(stored.kdf);\n\t\ttry {\n\t\t\treturn decode(await unseal(await deriveKey(passphrase, stored.kdf), stored.envelope));\n\t\t} catch (e) {\n\t\t\tif (e instanceof SelfstoreError) throw e;\n\t\t\tthrow new SelfstoreError('DECRYPT_FAILED', 'Wrong passphrase or tampered identity vault.');\n\t\t}\n\t}\n\n\tasync function protect(passphrase: string): Promise<void> {\n\t\tif (!passphrase) throw new TypeError('protect(): a non-empty passphrase is required.');\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is already passphrase-protected. To change the passphrase: ' +\n\t\t\t\t\t'unprotect(oldPassphrase), then protect(newPassphrase).'\n\t\t\t);\n\t\t}\n\t\tconst identity = isEnvelope(stored)\n\t\t\t? decode(await unseal(await vaultKey(), stored)) // reseal the existing identity\n\t\t\t: await generateIdentity(); // empty vault: create-and-protect in one step\n\t\tconst kdf = freshKdfParams();\n\t\tconst envelope = await seal(await deriveKey(passphrase, kdf), encode(identity));\n\t\tawait kv.set(IDENTITY_KEY, { selfstoreVaultLock: 1, kdf, envelope } satisfies VaultLock);\n\t}\n\n\treturn {\n\t\tload,\n\t\tsave,\n\t\tunlock,\n\t\tprotect,\n\t\tasync loadOrCreate(): Promise<GroupIdentity> {\n\t\t\tconst existing = await load(); // PASSWORD_REQUIRED when protected: never mint over a lock\n\t\t\tif (existing) return existing;\n\t\t\tconst fresh = await generateIdentity();\n\t\t\tawait save(fresh);\n\t\t\treturn fresh;\n\t\t},\n\t\tasync clear(): Promise<void> {\n\t\t\tawait kv.del(IDENTITY_KEY);\n\t\t},\n\t\tasync isProtected(): Promise<boolean> {\n\t\t\treturn isVaultLock(await kv.get(IDENTITY_KEY));\n\t\t},\n\t\tasync unprotect(passphrase: string): Promise<void> {\n\t\t\t// unlock() enforces \"protected\" and the passphrase; only then rewrite.\n\t\t\tawait sealDevice(await unlock(passphrase));\n\t\t}\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/persistence/identity.ts"],"names":[],"mappings":"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","file":"groups.js","sourcesContent":["// At-rest storage for a member identity (passwordless groups). Private keys\n// are sealed under a non-extractable device key in the same kv - defeats disk\n// forensics and partial exfiltration, not code running in the origin or a\n// copy of the whole profile (THREAT-MODEL.md).\n//\n// The opt-in passphrase lock reseals the identity under Argon2id(passphrase)\n// instead, closing the profile-copy gap at the cost of an unlock prompt. The\n// vault keeps no unlocked session state: unlock() returns the identity, the\n// caller holds it in memory, and at rest it stays sealed. A bare envelope\n// keeps meaning device mode, so pre-lock vaults load unchanged.\n//\n// The identity is one person's; move it between their devices over a trusted\n// channel (QR shown locally, encrypted note) - never through the group's\n// shared storage.\n\nimport { seal, unseal, isEnvelope, newDeviceKey, type EncEnvelope } from './cache-crypto';\nimport type { KV } from './cache';\nimport { generateIdentity, type GroupIdentity } from '../selfstore/group';\nimport { deriveKey, freshKdfParams, assertKdfBounds } from '../selfstore/crypto';\nimport type { KdfParams } from '../selfstore/types';\nimport { SelfstoreError } from '../selfstore/errors';\n\nconst VAULT_KEY = 'groupVaultKey';\nconst IDENTITY_KEY = 'groupIdentity';\n\nconst utf8 = { enc: new TextEncoder(), dec: new TextDecoder() };\n\n// Local kv bookkeeping only - not part of the interchange format (SPEC.md).\ninterface VaultLock {\n\tselfstoreVaultLock: 1;\n\tkdf: KdfParams;\n\tenvelope: EncEnvelope;\n}\n\nfunction isVaultLock(v: unknown): v is VaultLock {\n\treturn (\n\t\ttypeof v === 'object' &&\n\t\tv !== null &&\n\t\t(v as { selfstoreVaultLock?: unknown }).selfstoreVaultLock === 1\n\t);\n}\n\nconst encode = (identity: GroupIdentity): Uint8Array => utf8.enc.encode(JSON.stringify(identity));\nconst decode = (bytes: Uint8Array): GroupIdentity =>\n\tJSON.parse(utf8.dec.decode(bytes)) as GroupIdentity;\n\nexport interface IdentityVault {\n\t/** The stored identity, or null when none exists yet. On a protected\n\t * vault this throws PASSWORD_REQUIRED rather than returning null - a\n\t * locked identity exists, and null would invite loadOrCreate to mint a\n\t * second one over it. */\n\tload(): Promise<GroupIdentity | null>;\n\t/** Seal and store an identity, overwriting any previous one. Throws\n\t * PASSWORD_REQUIRED on a protected vault: a locked identity is never\n\t * silently replaced (unprotect() or clear() first). */\n\tsave(identity: GroupIdentity): Promise<void>;\n\t/** load(), creating and storing a fresh identity when absent. Same\n\t * PASSWORD_REQUIRED rule as load(). */\n\tloadOrCreate(): Promise<GroupIdentity>;\n\t/** Forget the identity on this device. Allowed even when protected -\n\t * forgetting leaks nothing, and it is the honest lost-passphrase escape\n\t * hatch (re-import from another device, or get re-invited). */\n\tclear(): Promise<void>;\n\tisProtected(): Promise<boolean>;\n\t/** Decrypt a protected vault and return the identity. Stateless: the\n\t * caller holds it in memory, the stored value stays sealed. Wrong\n\t * passphrase and tampered blob are one DECRYPT_FAILED; hostile KDF\n\t * params are refused before any derivation. */\n\tunlock(passphrase: string): Promise<GroupIdentity>;\n\t/** Reseal the identity under Argon2id(passphrase); an empty vault gets a\n\t * fresh identity, created-and-protected in one step. Changing the\n\t * passphrase is unprotect(old) then protect(new) - an already-protected\n\t * vault throws PASSWORD_REQUIRED, never a silent rekey. */\n\tprotect(passphrase: string): Promise<void>;\n\t/** Unlock with `passphrase`, then reseal under the device key. A wrong\n\t * passphrase throws DECRYPT_FAILED and leaves the vault untouched. */\n\tunprotect(passphrase: string): Promise<void>;\n}\n\n/** An identity vault over the app's kv (e.g. `indexedDbCache(app).kv`). */\nexport function identityVault(kv: KV): IdentityVault {\n\tasync function vaultKey(): Promise<CryptoKey> {\n\t\tconst existing = await kv.get<CryptoKey>(VAULT_KEY);\n\t\tif (existing) return existing;\n\t\tconst fresh = await newDeviceKey();\n\t\tawait kv.set(VAULT_KEY, fresh);\n\t\treturn fresh;\n\t}\n\n\t/** Device mode's write path: a bare envelope under the device key. */\n\tasync function sealDevice(identity: GroupIdentity): Promise<void> {\n\t\tawait kv.set(IDENTITY_KEY, await seal(await vaultKey(), encode(identity)));\n\t}\n\n\tasync function load(): Promise<GroupIdentity | null> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected. Call unlock(passphrase) - ' +\n\t\t\t\t\t'load()/loadOrCreate() never open or replace a locked identity.'\n\t\t\t);\n\t\t}\n\t\tif (!isEnvelope(stored)) return null;\n\t\treturn decode(await unseal(await vaultKey(), stored));\n\t}\n\n\tasync function save(identity: GroupIdentity): Promise<void> {\n\t\tif (isVaultLock(await kv.get(IDENTITY_KEY))) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected; save() never overwrites a locked ' +\n\t\t\t\t\t'identity. unprotect(passphrase) first, or clear() to forget it.'\n\t\t\t);\n\t\t}\n\t\tawait sealDevice(identity);\n\t}\n\n\tasync function unlock(passphrase: string): Promise<GroupIdentity> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (!isVaultLock(stored)) {\n\t\t\tthrow new TypeError('unlock(): the vault is not passphrase-protected (check isProtected()).');\n\t\t}\n\t\tif (!stored.kdf || !isEnvelope(stored.envelope)) {\n\t\t\tthrow new SelfstoreError('BAD_FORMAT', 'Missing or malformed vault lock parameters.');\n\t\t}\n\t\t// Same read-side ceiling as backup decrypts: a tampered kv must not make\n\t\t// unlock allocate gigabytes (memory-hard bomb) before any passphrase check.\n\t\tassertKdfBounds(stored.kdf);\n\t\ttry {\n\t\t\treturn decode(await unseal(await deriveKey(passphrase, stored.kdf), stored.envelope));\n\t\t} catch (e) {\n\t\t\tif (e instanceof SelfstoreError) throw e;\n\t\t\tthrow new SelfstoreError('DECRYPT_FAILED', 'Wrong passphrase or tampered identity vault.');\n\t\t}\n\t}\n\n\tasync function protect(passphrase: string): Promise<void> {\n\t\tif (!passphrase) throw new TypeError('protect(): a non-empty passphrase is required.');\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is already passphrase-protected. To change the passphrase: ' +\n\t\t\t\t\t'unprotect(oldPassphrase), then protect(newPassphrase).'\n\t\t\t);\n\t\t}\n\t\tconst identity = isEnvelope(stored)\n\t\t\t? decode(await unseal(await vaultKey(), stored)) // reseal the existing identity\n\t\t\t: await generateIdentity(); // empty vault: create-and-protect in one step\n\t\tconst kdf = freshKdfParams();\n\t\tconst envelope = await seal(await deriveKey(passphrase, kdf), encode(identity));\n\t\tawait kv.set(IDENTITY_KEY, { selfstoreVaultLock: 1, kdf, envelope } satisfies VaultLock);\n\t}\n\n\treturn {\n\t\tload,\n\t\tsave,\n\t\tunlock,\n\t\tprotect,\n\t\tasync loadOrCreate(): Promise<GroupIdentity> {\n\t\t\tconst existing = await load(); // PASSWORD_REQUIRED when protected: never mint over a lock\n\t\t\tif (existing) return existing;\n\t\t\tconst fresh = await generateIdentity();\n\t\t\tawait save(fresh);\n\t\t\treturn fresh;\n\t\t},\n\t\tasync clear(): Promise<void> {\n\t\t\tawait kv.del(IDENTITY_KEY);\n\t\t},\n\t\tasync isProtected(): Promise<boolean> {\n\t\t\treturn isVaultLock(await kv.get(IDENTITY_KEY));\n\t\t},\n\t\tasync unprotect(passphrase: string): Promise<void> {\n\t\t\t// unlock() enforces \"protected\" and the passphrase; only then rewrite.\n\t\t\tawait sealDevice(await unlock(passphrase));\n\t\t}\n\t};\n}\n"]}
|
package/dist/households.d.ts
CHANGED
package/dist/households.js
CHANGED
|
@@ -46,12 +46,16 @@ function isString(v) {
|
|
|
46
46
|
function requireV1(v) {
|
|
47
47
|
if (v === 1) return;
|
|
48
48
|
if (typeof v === "number") {
|
|
49
|
-
throw new HouseholdCodeError(
|
|
49
|
+
throw new HouseholdCodeError(
|
|
50
|
+
"unsupported-version",
|
|
51
|
+
`payload version ${v} is not supported (expected 1)`
|
|
52
|
+
);
|
|
50
53
|
}
|
|
51
54
|
throw malformed("missing or non-numeric version field");
|
|
52
55
|
}
|
|
53
56
|
function decodeJson(code) {
|
|
54
|
-
if (typeof code !== "string" || code.length === 0)
|
|
57
|
+
if (typeof code !== "string" || code.length === 0)
|
|
58
|
+
throw malformed("code must be a non-empty string");
|
|
55
59
|
let json;
|
|
56
60
|
try {
|
|
57
61
|
json = base64urlDecode(code);
|
|
@@ -68,8 +72,10 @@ function toCopyLink(v) {
|
|
|
68
72
|
if (!isRecord(v)) throw malformed("copy link is not an object");
|
|
69
73
|
if (v.provider === "drive") {
|
|
70
74
|
if (!isString(v.fileId)) throw malformed("drive copy link needs a string fileId");
|
|
71
|
-
if (v.ownerEmail !== void 0 && !isString(v.ownerEmail))
|
|
72
|
-
|
|
75
|
+
if (v.ownerEmail !== void 0 && !isString(v.ownerEmail))
|
|
76
|
+
throw malformed("copy link ownerEmail must be a string");
|
|
77
|
+
if (v.ownerName !== void 0 && !isString(v.ownerName))
|
|
78
|
+
throw malformed("copy link ownerName must be a string");
|
|
73
79
|
const link = { provider: "drive", fileId: v.fileId };
|
|
74
80
|
if (isString(v.ownerEmail)) link.ownerEmail = v.ownerEmail;
|
|
75
81
|
if (isString(v.ownerName)) link.ownerName = v.ownerName;
|
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 => new 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('unsupported-version', `payload version ${v} is not supported (expected 1)`);\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) throw 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)) throw malformed('copy link ownerEmail must be a string');\n\t\tif (v.ownerName !== undefined && !isString(v.ownerName)) throw 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\trereadJoined(fileId: string, key: string): Promise<SharePayload | null>;\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}\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}\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\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}));\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.\n\t\t\tawait backend.announce(content.share.mailboxId, link).catch(() => undefined);\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') {\n\t\t\t\t\t// Fold any newly-announced member copies into the roster and rebroadcast.\n\t\t\t\t\tconst announced = await backend.takeAnnounces(group.mailboxId);\n\t\t\t\t\tconst roster = mergeRoster(group.roster, announced, group.myCopy.fileId);\n\t\t\t\t\tconst grew = roster.length !== group.roster.length;\n\t\t\t\t\tconst needed = group.needsRebroadcast === true;\n\t\t\t\t\tgroup.roster = roster;\n\t\t\t\t\tawait persist();\n\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t// Rebroadcast on growth or when a previous rebroadcast failed: keyed\n\t\t\t\t\t// on `grew` alone, an offline publish left the bulletin stale forever\n\t\t\t\t\t// (the announce was already consumed, so no later round re-grew).\n\t\t\t\t\tif ((grew || needed) && group.live === true) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\t\t\t\t\tv: 1,\n\t\t\t\t\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\t\t\t\t\troster\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tgroup.bulletin = where;\n\t\t\t\t\t\t\tgroup.needsRebroadcast = undefined;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tgroup.needsRebroadcast = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait persist();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// The admin's bulletin roster is AUTHORITATIVE for members: mirror it\n\t\t\t\t\t// (replace, not merge) so removals propagate, not only newcomers.\n\t\t\t\t\t// Reread through the invitation THIS membership joined on (pre-multi\n\t\t\t\t\t// records carry none: their refresh stays dormant, peers still fold).\n\t\t\t\t\tconst share = group.bulletin\n\t\t\t\t\t\t? await backend.rereadJoined(group.bulletin.fileId, group.bulletin.key)\n\t\t\t\t\t\t: null;\n\t\t\t\t\tif (share) {\n\t\t\t\t\t\tconst byId = new Map(share.roster.map((l) => [l.fileId, l]));\n\t\t\t\t\t\t// SELF-HEAL: my own copy missing from a fresh roster means my\n\t\t\t\t\t\t// announce was lost. Announce again; the admin's fold de-dups.\n\t\t\t\t\t\tif (!byId.has(group.myCopy.fileId)) {\n\t\t\t\t\t\t\tbyId.set(group.myCopy.fileId, group.myCopy);\n\t\t\t\t\t\t\tawait backend.announce(group.mailboxId, group.myCopy).catch(() => undefined);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Removals apply CONSERVATIVELY: a peer drops only after two\n\t\t\t\t\t\t// consecutive fresh reads without it (stale relay tolerance).\n\t\t\t\t\t\tfor (const link of group.roster) {\n\t\t\t\t\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\t\t\t\t\tconst missKey = `${group.mailboxId}:${link.fileId}`;\n\t\t\t\t\t\t\tif (byId.has(link.fileId)) {\n\t\t\t\t\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst misses = (rosterMisses.get(missKey) ?? 0) + 1;\n\t\t\t\t\t\t\tif (misses >= 2) {\n\t\t\t\t\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\t\t\t\t\tstore.detachPeer(link.fileId);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\trosterMisses.set(missKey, misses);\n\t\t\t\t\t\t\t\tbyId.set(link.fileId, link);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgroup.roster = [...byId.values()];\n\t\t\t\t\t\tawait persist();\n\t\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t}\n\t\t\t\t}\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\trereadJoined(fileId: string, key: string): Promise<SharePayload | null>;\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}\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}\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\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}));\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.\n\t\t\tawait backend.announce(content.share.mailboxId, link).catch(() => undefined);\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') {\n\t\t\t\t\t// Fold any newly-announced member copies into the roster and rebroadcast.\n\t\t\t\t\tconst announced = await backend.takeAnnounces(group.mailboxId);\n\t\t\t\t\tconst roster = mergeRoster(group.roster, announced, group.myCopy.fileId);\n\t\t\t\t\tconst grew = roster.length !== group.roster.length;\n\t\t\t\t\tconst needed = group.needsRebroadcast === true;\n\t\t\t\t\tgroup.roster = roster;\n\t\t\t\t\tawait persist();\n\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t// Rebroadcast on growth or when a previous rebroadcast failed: keyed\n\t\t\t\t\t// on `grew` alone, an offline publish left the bulletin stale forever\n\t\t\t\t\t// (the announce was already consumed, so no later round re-grew).\n\t\t\t\t\tif ((grew || needed) && group.live === true) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\t\t\t\t\tv: 1,\n\t\t\t\t\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\t\t\t\t\troster\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tgroup.bulletin = where;\n\t\t\t\t\t\t\tgroup.needsRebroadcast = undefined;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tgroup.needsRebroadcast = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait persist();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// The admin's bulletin roster is AUTHORITATIVE for members: mirror it\n\t\t\t\t\t// (replace, not merge) so removals propagate, not only newcomers.\n\t\t\t\t\t// Reread through the invitation THIS membership joined on (pre-multi\n\t\t\t\t\t// records carry none: their refresh stays dormant, peers still fold).\n\t\t\t\t\tconst share = group.bulletin\n\t\t\t\t\t\t? await backend.rereadJoined(group.bulletin.fileId, group.bulletin.key)\n\t\t\t\t\t\t: null;\n\t\t\t\t\tif (share) {\n\t\t\t\t\t\tconst byId = new Map(share.roster.map((l) => [l.fileId, l]));\n\t\t\t\t\t\t// SELF-HEAL: my own copy missing from a fresh roster means my\n\t\t\t\t\t\t// announce was lost. Announce again; the admin's fold de-dups.\n\t\t\t\t\t\tif (!byId.has(group.myCopy.fileId)) {\n\t\t\t\t\t\t\tbyId.set(group.myCopy.fileId, group.myCopy);\n\t\t\t\t\t\t\tawait backend.announce(group.mailboxId, group.myCopy).catch(() => undefined);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Removals apply CONSERVATIVELY: a peer drops only after two\n\t\t\t\t\t\t// consecutive fresh reads without it (stale relay tolerance).\n\t\t\t\t\t\tfor (const link of group.roster) {\n\t\t\t\t\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\t\t\t\t\tconst missKey = `${group.mailboxId}:${link.fileId}`;\n\t\t\t\t\t\t\tif (byId.has(link.fileId)) {\n\t\t\t\t\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst misses = (rosterMisses.get(missKey) ?? 0) + 1;\n\t\t\t\t\t\t\tif (misses >= 2) {\n\t\t\t\t\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\t\t\t\t\tstore.detachPeer(link.fileId);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\trosterMisses.set(missKey, misses);\n\t\t\t\t\t\t\t\tbyId.set(link.fileId, link);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgroup.roster = [...byId.values()];\n\t\t\t\t\t\tawait persist();\n\t\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t}\n\t\t\t\t}\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"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { n as Snapshot, E as EncodeOptions, H as Header, e as PasswordPolicy, B as BackupTarget, q as StatusDescriptor, t as StoreError, c as LocalStoreState, L as LocalStore } from './store-
|
|
2
|
-
export { d as Mode, P as PasswordCheck, f as PasswordRequirement, S as SelfstoreError, l as SelfstoreErrorCode, m as Severity, o as SnapshotFile, p as StatusAction, s as StorageState, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-
|
|
1
|
+
import { n as Snapshot, E as EncodeOptions, H as Header, e as PasswordPolicy, B as BackupTarget, q as StatusDescriptor, t as StoreError, c as LocalStoreState, L as LocalStore } from './store-2RnED1Vq.js';
|
|
2
|
+
export { d as Mode, P as PasswordCheck, f as PasswordRequirement, S as SelfstoreError, l as SelfstoreErrorCode, m as Severity, o as SnapshotFile, p as StatusAction, s as StorageState, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-2RnED1Vq.js';
|
|
3
3
|
import { L as LocalCache, K as KV } from './cache-De5_sALA.js';
|
|
4
4
|
import { SyncConfig } from './sync.js';
|
|
5
|
-
import { D as DriveAuth, W as WebdavConfig, S as S3Config } from './drive-
|
|
6
|
-
export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-
|
|
5
|
+
import { D as DriveAuth, W as WebdavConfig, S as S3Config } from './drive-DNiDXBDe.js';
|
|
6
|
+
export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-DsXG7QW7.js';
|
|
7
7
|
|
|
8
8
|
declare const BACKUP_EXTENSION = ".zip";
|
|
9
9
|
declare const BACKUP_MIME = "application/zip";
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-
|
|
2
|
-
export { gisDriveAuth } from './chunk-
|
|
3
|
-
import { saveToDisk } from './chunk-
|
|
4
|
-
export { pickFromDisk, saveToDisk } from './chunk-
|
|
5
|
-
import './chunk-
|
|
6
|
-
import { fromSession, fromSession2, fromSession3, connect4, connect3, isSupported, connect2, connect } from './chunk-
|
|
7
|
-
import { restore } from './chunk-
|
|
8
|
-
export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-
|
|
9
|
-
import './chunk-
|
|
1
|
+
import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-XI3MGK4M.js';
|
|
2
|
+
export { gisDriveAuth } from './chunk-XI3MGK4M.js';
|
|
3
|
+
import { saveToDisk } from './chunk-UWVAKBU4.js';
|
|
4
|
+
export { pickFromDisk, saveToDisk } from './chunk-UWVAKBU4.js';
|
|
5
|
+
import './chunk-SKWHKM25.js';
|
|
6
|
+
import { fromSession, fromSession2, fromSession3, connect4, connect3, isSupported, connect2, connect } from './chunk-TEIAFKFS.js';
|
|
7
|
+
import { restore } from './chunk-SRKUHFRG.js';
|
|
8
|
+
export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-SRKUHFRG.js';
|
|
9
|
+
import './chunk-KHER6MWR.js';
|
|
10
10
|
import './chunk-QBCU7OOP.js';
|
|
11
|
-
import './chunk-
|
|
11
|
+
import './chunk-6746AL4I.js';
|
|
12
12
|
import { SelfstoreError } from './chunk-VBFMMR7Q.js';
|
|
13
13
|
export { SelfstoreError, errorLabelKey, isSelfstoreError } from './chunk-VBFMMR7Q.js';
|
|
14
14
|
|
|
@@ -111,7 +111,10 @@ async function selfstore(app, options = {}) {
|
|
|
111
111
|
await store.attachTarget(target, { password: opts.password ?? null, strategy: "merge" });
|
|
112
112
|
return "merged";
|
|
113
113
|
}
|
|
114
|
-
await store.attachTarget(target, {
|
|
114
|
+
await store.attachTarget(target, {
|
|
115
|
+
password: opts.password ?? null,
|
|
116
|
+
strategy: "replace-remote"
|
|
117
|
+
});
|
|
115
118
|
return "started";
|
|
116
119
|
}
|
|
117
120
|
const simple = {
|