selfstore 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.d.ts +5 -5
- package/dist/advanced.js +2 -2
- package/dist/backups.d.ts +1 -1
- package/dist/{chunk-Z2MRZSEP.js → chunk-CUFKME4S.js} +3 -3
- package/dist/{chunk-Z2MRZSEP.js.map → chunk-CUFKME4S.js.map} +1 -1
- package/dist/{chunk-P34WACTD.js → chunk-TEIAFKFS.js} +53 -17
- package/dist/chunk-TEIAFKFS.js.map +1 -0
- package/dist/{chunk-5AAWNPCP.js → chunk-XI3MGK4M.js} +3 -2
- 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 +2 -2
- package/dist/households.d.ts +15 -3
- package/dist/households.js +93 -55
- package/dist/households.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/{local-2py_vGGy.d.ts → local-DsXG7QW7.d.ts} +2 -2
- package/dist/{store-ChIGEOph.d.ts → store-2RnED1Vq.d.ts} +5 -0
- package/dist/widgets.d.ts +2 -2
- package/dist/widgets.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-5AAWNPCP.js.map +0 -1
- package/dist/chunk-P34WACTD.js.map +0 -1
package/dist/households.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as BackupTarget, g as PeerSource, L as LocalStore } from './store-
|
|
1
|
+
import { B as BackupTarget, g as PeerSource, L as LocalStore } from './store-2RnED1Vq.js';
|
|
2
2
|
import { K as KV } from './cache-De5_sALA.js';
|
|
3
3
|
import './sync.js';
|
|
4
4
|
|
|
@@ -104,8 +104,13 @@ interface ShareBackend {
|
|
|
104
104
|
} | null>;
|
|
105
105
|
/** Re-read a joined share's bulletin (member): the freshest roster. The
|
|
106
106
|
* engine passes the invitation capability it joined through, so every
|
|
107
|
-
* membership rereads ITS bulletin. Null when unreachable or revoked
|
|
108
|
-
|
|
107
|
+
* membership rereads ITS bulletin. Null when unreachable or revoked -
|
|
108
|
+
* nothing is concluded. The literal 'unreadable' when the bulletin WAS
|
|
109
|
+
* fetched but cannot be opened (the key no longer decrypts it - the admin
|
|
110
|
+
* rotated the link): the engine counts consecutive ones toward the stale
|
|
111
|
+
* verdict. A backend that cannot tell may keep returning null; the stale
|
|
112
|
+
* detection then simply never fires. */
|
|
113
|
+
rereadJoined(fileId: string, key: string): Promise<SharePayload | null | 'unreadable'>;
|
|
109
114
|
/** Member: announce my copy to the group's mailbox so the admin folds it. */
|
|
110
115
|
announce(mailboxId: string, copy: CopyLink): Promise<void>;
|
|
111
116
|
/** Admin: drain the announce mailbox - the copies members published. */
|
|
@@ -127,6 +132,13 @@ interface MembershipInfo {
|
|
|
127
132
|
/** Who shares it with me (the admin's roster label); null for my own share. */
|
|
128
133
|
sharedBy: string | null;
|
|
129
134
|
memberCount: number;
|
|
135
|
+
/** Member: my copy is not in a fresh roster yet (the announce may have been
|
|
136
|
+
* lost; the engine keeps re-announcing). The host can show "waiting for the
|
|
137
|
+
* admin to see you" instead of a false everything-is-fine. */
|
|
138
|
+
announcePending: boolean;
|
|
139
|
+
/** Member: the admin rotated the link - edits publish into the void until a
|
|
140
|
+
* fresh invitation is joined. The host must surface it. */
|
|
141
|
+
stale: boolean;
|
|
130
142
|
}
|
|
131
143
|
/** The reactive-friendly snapshot the UI reads (hosts wrap it in their own
|
|
132
144
|
* reactivity - a Svelte rune, a React store, anything). The singular fields
|
package/dist/households.js
CHANGED
|
@@ -193,6 +193,90 @@ function createHouseholdGroup(deps) {
|
|
|
193
193
|
}
|
|
194
194
|
return [...byId.values()];
|
|
195
195
|
}
|
|
196
|
+
async function announceWithRetry(mailboxId, copy) {
|
|
197
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
198
|
+
try {
|
|
199
|
+
await backend.announce(mailboxId, copy);
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
if (attempt < 2) await new Promise((r) => setTimeout(r, 800 * (attempt + 1)));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
async function adminRound(group) {
|
|
208
|
+
const announced = await backend.takeAnnounces(group.mailboxId);
|
|
209
|
+
const roster = mergeRoster(group.roster, announced, group.myCopy.fileId);
|
|
210
|
+
const grew = roster.length !== group.roster.length;
|
|
211
|
+
const needed = group.needsRebroadcast === true;
|
|
212
|
+
group.roster = roster;
|
|
213
|
+
await persist();
|
|
214
|
+
attachPeers(group);
|
|
215
|
+
if ((grew || needed) && group.live === true) {
|
|
216
|
+
try {
|
|
217
|
+
const where = await backend.publishBulletin(group.key, {
|
|
218
|
+
v: 1,
|
|
219
|
+
mailboxId: group.mailboxId,
|
|
220
|
+
roster
|
|
221
|
+
});
|
|
222
|
+
group.bulletin = where;
|
|
223
|
+
group.needsRebroadcast = void 0;
|
|
224
|
+
} catch {
|
|
225
|
+
group.needsRebroadcast = true;
|
|
226
|
+
}
|
|
227
|
+
await persist();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function applyRosterRemovals(group, byId) {
|
|
231
|
+
for (const link of group.roster) {
|
|
232
|
+
if (link.fileId === group.myCopy.fileId) continue;
|
|
233
|
+
const missKey = `${group.mailboxId}:${link.fileId}`;
|
|
234
|
+
if (byId.has(link.fileId)) {
|
|
235
|
+
rosterMisses.delete(missKey);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const misses = (rosterMisses.get(missKey) ?? 0) + 1;
|
|
239
|
+
if (misses >= 2) {
|
|
240
|
+
rosterMisses.delete(missKey);
|
|
241
|
+
store.detachPeer(link.fileId);
|
|
242
|
+
} else {
|
|
243
|
+
rosterMisses.set(missKey, misses);
|
|
244
|
+
byId.set(link.fileId, link);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function memberRound(group) {
|
|
249
|
+
const share = group.bulletin ? await backend.rereadJoined(group.bulletin.fileId, group.bulletin.key) : null;
|
|
250
|
+
if (share === "unreadable") {
|
|
251
|
+
const n = (group.unreadableReads ?? 0) + 1;
|
|
252
|
+
group.unreadableReads = n;
|
|
253
|
+
if (n >= 2) group.stale = true;
|
|
254
|
+
await persist();
|
|
255
|
+
attachPeers(group);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!share) {
|
|
259
|
+
if (group.announcePending) {
|
|
260
|
+
await backend.announce(group.mailboxId, group.myCopy).catch(() => void 0);
|
|
261
|
+
}
|
|
262
|
+
attachPeers(group);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
group.unreadableReads = void 0;
|
|
266
|
+
group.stale = void 0;
|
|
267
|
+
const byId = new Map(share.roster.map((l) => [l.fileId, l]));
|
|
268
|
+
if (!byId.has(group.myCopy.fileId)) {
|
|
269
|
+
byId.set(group.myCopy.fileId, group.myCopy);
|
|
270
|
+
group.announcePending = true;
|
|
271
|
+
await announceWithRetry(group.mailboxId, group.myCopy);
|
|
272
|
+
} else {
|
|
273
|
+
group.announcePending = void 0;
|
|
274
|
+
}
|
|
275
|
+
applyRosterRemovals(group, byId);
|
|
276
|
+
group.roster = [...byId.values()];
|
|
277
|
+
await persist();
|
|
278
|
+
attachPeers(group);
|
|
279
|
+
}
|
|
196
280
|
return {
|
|
197
281
|
get state() {
|
|
198
282
|
const list = groups ?? [];
|
|
@@ -201,7 +285,9 @@ function createHouseholdGroup(deps) {
|
|
|
201
285
|
isAdmin: g.role === "admin",
|
|
202
286
|
selfFileId: g.myCopy.fileId,
|
|
203
287
|
sharedBy: sharedByOf(g),
|
|
204
|
-
memberCount: g.roster.length
|
|
288
|
+
memberCount: g.roster.length,
|
|
289
|
+
announcePending: g.announcePending === true,
|
|
290
|
+
stale: g.stale === true
|
|
205
291
|
}));
|
|
206
292
|
const group = primaryGroup(list);
|
|
207
293
|
if (!group) {
|
|
@@ -311,7 +397,10 @@ function createHouseholdGroup(deps) {
|
|
|
311
397
|
await persist();
|
|
312
398
|
ensureMirror(group);
|
|
313
399
|
attachPeers(group);
|
|
314
|
-
await
|
|
400
|
+
if (!await announceWithRetry(content.share.mailboxId, link)) {
|
|
401
|
+
group.announcePending = true;
|
|
402
|
+
await persist();
|
|
403
|
+
}
|
|
315
404
|
return "joined";
|
|
316
405
|
},
|
|
317
406
|
async syncGroup() {
|
|
@@ -326,59 +415,8 @@ function createHouseholdGroup(deps) {
|
|
|
326
415
|
continue;
|
|
327
416
|
}
|
|
328
417
|
ensureMirror(group);
|
|
329
|
-
if (group.role === "admin")
|
|
330
|
-
|
|
331
|
-
const roster = mergeRoster(group.roster, announced, group.myCopy.fileId);
|
|
332
|
-
const grew = roster.length !== group.roster.length;
|
|
333
|
-
const needed = group.needsRebroadcast === true;
|
|
334
|
-
group.roster = roster;
|
|
335
|
-
await persist();
|
|
336
|
-
attachPeers(group);
|
|
337
|
-
if ((grew || needed) && group.live === true) {
|
|
338
|
-
try {
|
|
339
|
-
const where = await backend.publishBulletin(group.key, {
|
|
340
|
-
v: 1,
|
|
341
|
-
mailboxId: group.mailboxId,
|
|
342
|
-
roster
|
|
343
|
-
});
|
|
344
|
-
group.bulletin = where;
|
|
345
|
-
group.needsRebroadcast = void 0;
|
|
346
|
-
} catch {
|
|
347
|
-
group.needsRebroadcast = true;
|
|
348
|
-
}
|
|
349
|
-
await persist();
|
|
350
|
-
}
|
|
351
|
-
} else {
|
|
352
|
-
const share = group.bulletin ? await backend.rereadJoined(group.bulletin.fileId, group.bulletin.key) : null;
|
|
353
|
-
if (share) {
|
|
354
|
-
const byId = new Map(share.roster.map((l) => [l.fileId, l]));
|
|
355
|
-
if (!byId.has(group.myCopy.fileId)) {
|
|
356
|
-
byId.set(group.myCopy.fileId, group.myCopy);
|
|
357
|
-
await backend.announce(group.mailboxId, group.myCopy).catch(() => void 0);
|
|
358
|
-
}
|
|
359
|
-
for (const link of group.roster) {
|
|
360
|
-
if (link.fileId === group.myCopy.fileId) continue;
|
|
361
|
-
const missKey = `${group.mailboxId}:${link.fileId}`;
|
|
362
|
-
if (byId.has(link.fileId)) {
|
|
363
|
-
rosterMisses.delete(missKey);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
const misses = (rosterMisses.get(missKey) ?? 0) + 1;
|
|
367
|
-
if (misses >= 2) {
|
|
368
|
-
rosterMisses.delete(missKey);
|
|
369
|
-
store.detachPeer(link.fileId);
|
|
370
|
-
} else {
|
|
371
|
-
rosterMisses.set(missKey, misses);
|
|
372
|
-
byId.set(link.fileId, link);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
group.roster = [...byId.values()];
|
|
376
|
-
await persist();
|
|
377
|
-
attachPeers(group);
|
|
378
|
-
} else {
|
|
379
|
-
attachPeers(group);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
418
|
+
if (group.role === "admin") await adminRound(group);
|
|
419
|
+
else await memberRound(group);
|
|
382
420
|
}
|
|
383
421
|
},
|
|
384
422
|
async leave(walletFileId) {
|
package/dist/households.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/households/codec.ts","../src/households/group.ts"],"names":[],"mappings":"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","file":"households.js","sourcesContent":["// Household share transport (capability-link model): the plain shapes the\n// bulletin and the copy-announce carry, plus a dependency-free base64url\n// codec. The link key K is the shared group secret - possession is\n// membership, no per-member identity, no signed manifest. Payloads carry\n// only file ids of link-readable files whose content is encrypted under K,\n// so an id alone reveals nothing. Pure by contract: no secret held, no I/O,\n// no framework - shapes, validation and transport strings only.\n\n// --- Errors -----------------------------------------------------------------\n\n/** Why a code was refused. `malformed` covers bad transport, wrong structure or\n * a code decoded as the wrong kind of payload; `unsupported-version` is a\n * numeric `v` this build does not understand. */\nexport type HouseholdCodeErrorCode = 'malformed' | 'unsupported-version';\n\n/** The single typed error this module throws, so callers branch on `code`\n * instead of parsing messages. A decoder that throws never returns a partially\n * parsed object - it fails whole. */\nexport class HouseholdCodeError extends Error {\n\treadonly code: HouseholdCodeErrorCode;\n\tconstructor(code: HouseholdCodeErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'HouseholdCodeError';\n\t\tthis.code = code;\n\t\tObject.setPrototypeOf(this, HouseholdCodeError.prototype);\n\t}\n}\n\nconst malformed = (message: string): HouseholdCodeError =>\n\tnew HouseholdCodeError('malformed', message);\n\n// --- Types ------------------------------------------------------------------\n\n/** Where a member published their encrypted copy. A discriminated union on\n * `provider` so a future transport (WebDAV, ...) slots in beside Drive without\n * changing the payload shapes. Today: Drive only. */\nexport type CopyLink = DriveCopyLink;\n\n/** A copy hosted on the owner's Google Drive. `fileId` is the Drive file id (a\n * public-link file, ciphertext under K); the owner fields are a cosmetic \"on\n * X's Drive\" hint, both optional. */\nexport interface DriveCopyLink {\n\tprovider: 'drive';\n\tfileId: string;\n\townerEmail?: string;\n\townerName?: string;\n}\n\n/** The bulletin's control record (rides one reserved collection inside the\n * K-encrypted bulletin): where members announce their copy (`mailboxId`, a\n * secret only K-holders can read) and the roster of every known copy (the\n * admin's own first). */\nexport interface SharePayload {\n\tv: 1;\n\tmailboxId: string;\n\troster: CopyLink[];\n}\n\n/** Member -> admin, through the announce mailbox: \"here is my copy, fold it\".\n * No identity, no signature: knowing the mailboxId (which lives inside the\n * K-encrypted bulletin) already proves the sender holds K. */\nexport interface AnnouncePayload {\n\tv: 1;\n\tcopy: CopyLink;\n}\n\n// --- base64url (isomorphic, dependency-free) --------------------------------\n\nconst utf8Encoder = new TextEncoder();\n// `fatal` makes a truncated or corrupted byte run throw instead of silently\n// yielding U+FFFD, so a mangled code fails fast in the decoder.\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: true });\n\nfunction bytesToBinary(bytes: Uint8Array): string {\n\tlet out = '';\n\tfor (let i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]);\n\treturn out;\n}\n\nfunction binaryToBytes(binary: string): Uint8Array {\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n\treturn bytes;\n}\n\n/** base64url without padding of raw bytes. btoa + TextEncoder work in the\n * browser and in Node 20+, so the codec stays dependency-free and isomorphic. */\nfunction bytesToBase64url(bytes: Uint8Array): string {\n\treturn btoa(bytesToBinary(bytes)).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction base64urlEncode(text: string): string {\n\treturn bytesToBase64url(utf8Encoder.encode(text));\n}\n\n/** Inverse of base64urlEncode. Throws on any character outside the alphabet or\n * on an invalid UTF-8 byte run; decodeJson turns that into a 'malformed'. */\nfunction base64urlDecode(code: string): string {\n\tconst b64 = code.replace(/-/g, '+').replace(/_/g, '/');\n\tconst padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n\treturn utf8Decoder.decode(binaryToBytes(atob(padded)));\n}\n\n/** A short, url-safe, random id (9 bytes -> 12 base64url chars) from WebCrypto -\n * used for the announce mailbox id. This repo bans Math.random; getRandomValues\n * is the sanctioned entropy source and exists in every browser and Node 20+. */\nexport function randomId(): string {\n\tconst bytes = new Uint8Array(9);\n\tcrypto.getRandomValues(bytes);\n\treturn bytesToBase64url(bytes);\n}\n\n// --- Structural guards ------------------------------------------------------\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n\treturn typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction isString(v: unknown): v is string {\n\treturn typeof v === 'string';\n}\n\n/** Enforce the format version. A numeric `v` other than 1 is a code from a\n * future/older build (unsupported-version); anything else is garbage. */\nfunction requireV1(v: unknown): void {\n\tif (v === 1) return;\n\tif (typeof v === 'number') {\n\t\tthrow new HouseholdCodeError(\n\t\t\t'unsupported-version',\n\t\t\t`payload version ${v} is not supported (expected 1)`\n\t\t);\n\t}\n\tthrow malformed('missing or non-numeric version field');\n}\n\n/** Decode base64url -> JSON, mapping any failure to a single 'malformed'. */\nfunction decodeJson(code: string): unknown {\n\tif (typeof code !== 'string' || code.length === 0)\n\t\tthrow malformed('code must be a non-empty string');\n\tlet json: string;\n\ttry {\n\t\tjson = base64urlDecode(code);\n\t} catch {\n\t\tthrow malformed('code is not valid base64url');\n\t}\n\ttry {\n\t\treturn JSON.parse(json) as unknown;\n\t} catch {\n\t\tthrow malformed('code payload is not valid JSON');\n\t}\n}\n\n/** Rebuild a CopyLink field by field, so a decoded link carries exactly the\n * known keys (no smuggled extras) and every present field has the right type. */\nexport function toCopyLink(v: unknown): CopyLink {\n\tif (!isRecord(v)) throw malformed('copy link is not an object');\n\tif (v.provider === 'drive') {\n\t\tif (!isString(v.fileId)) throw malformed('drive copy link needs a string fileId');\n\t\tif (v.ownerEmail !== undefined && !isString(v.ownerEmail))\n\t\t\tthrow malformed('copy link ownerEmail must be a string');\n\t\tif (v.ownerName !== undefined && !isString(v.ownerName))\n\t\t\tthrow malformed('copy link ownerName must be a string');\n\t\tconst link: DriveCopyLink = { provider: 'drive', fileId: v.fileId };\n\t\tif (isString(v.ownerEmail)) link.ownerEmail = v.ownerEmail;\n\t\tif (isString(v.ownerName)) link.ownerName = v.ownerName;\n\t\treturn link;\n\t}\n\tthrow malformed(`unknown copy-link provider ${JSON.stringify(v.provider)}`);\n}\n\n/** Every entry of a roster array validated as a CopyLink, de-duplicated by\n * fileId (first occurrence wins). An empty roster is structurally valid.\n * Exported for the backend, which revalidates the roster it re-publishes. */\nexport function toRoster(v: unknown): CopyLink[] {\n\tif (!Array.isArray(v)) throw malformed('roster is not an array');\n\tconst seen = new Set<string>();\n\tconst out: CopyLink[] = [];\n\tfor (const entry of v) {\n\t\tconst link = toCopyLink(entry);\n\t\tif (seen.has(link.fileId)) continue;\n\t\tseen.add(link.fileId);\n\t\tout.push(link);\n\t}\n\treturn out;\n}\n\n// --- Codec ------------------------------------------------------------------\n// Transport = base64url(no padding) of compact JSON. Each encoder is a pure,\n// deterministic function of its input; each decoder validates the version and\n// the full structure and throws HouseholdCodeError on anything it cannot trust.\n\nexport function encodeShare(payload: SharePayload): string {\n\treturn base64urlEncode(JSON.stringify(payload));\n}\n\nexport function decodeShare(code: string): SharePayload {\n\tconst raw = decodeJson(code);\n\tif (!isRecord(raw)) throw malformed('share payload is not an object');\n\trequireV1(raw.v);\n\tif (!isString(raw.mailboxId)) throw malformed('share payload needs a string mailboxId');\n\treturn { v: 1, mailboxId: raw.mailboxId, roster: toRoster(raw.roster) };\n}\n\nexport function encodeAnnounce(payload: AnnouncePayload): string {\n\treturn base64urlEncode(JSON.stringify(payload));\n}\n\nexport function decodeAnnounce(code: string): AnnouncePayload {\n\tconst raw = decodeJson(code);\n\tif (!isRecord(raw)) throw malformed('announce is not an object');\n\trequireV1(raw.v);\n\treturn { v: 1, copy: toCopyLink(raw.copy) };\n}\n","// Household sharing, mirror model: read-write while every member's own file\n// stays exactly what its owner chose. Each member publishes a dedicated copy\n// of their file sealed under the link key K (a selfstore mirror, republished\n// after every save that moved data), and attaches the others' copies as\n// read-only peers keyed by K - reading the other's copy is what\n// resynchronizes. A password-protected store refuses to share, honestly.\n//\n// The invitation link carries K in its fragment; possession of K is\n// membership, no per-member identity. Revoking means ending the share (the\n// one real revocation, see leave()); everyone keeps what already converged\n// into their own file - the local-first reality.\n//\n// A device may hold SEVERAL memberships, one per share, each bound to its own\n// wallet file (the `wallet` getter): at most the in-scope one is armed, the\n// others lie dormant. Without the getter (single-wallet hosts) the engine\n// keeps the historical one-membership rule.\n//\n// Injectable by contract (store / kv / backend), framework-free,\n// network-free: every durable side effect goes through store or backend.\n\nimport { randomId, type CopyLink, type SharePayload } from './codec';\nimport type { BackupTarget, PeerSource } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { LocalStore } from '../persistence/store';\n\n// --- Injected abstractions --------------------------------------------------\n\n/** What the bulletin holds once opened: the read-only projection (scrubbed\n * collections, empty when the share is revoked) and the control record. */\nexport interface IncomingShare {\n\tprojection: Record<string, unknown[]>;\n\tshare: SharePayload;\n}\n\n/**\n * The sharing transport, the sole Drive/relay/mailbox-specific seam. A test\n * provides an in-memory fake; the host app provides a real one (e.g. Drive).\n * Everything here carries only public data - file ids and the roster.\n */\nexport interface ShareBackend {\n\t/** Create this member's dedicated copy file (link-readable, empty until the\n\t * mirror publishes into it) and return the link others read it through.\n\t * `existingFileId` reuses a known copy on restore instead of minting a\n\t * second file. */\n\tcreateCopy(existingFileId?: string, shareLabel?: string): Promise<CopyLink>;\n\t/** A write-capable target over a copy file, for the store MIRROR. Never\n\t * adopted as the store's own target - the store's own connection stays what\n\t * it is. */\n\tcopyTarget(fileId: string): BackupTarget;\n\t/** Teardown: make a copy file private and delete it (best-effort). Only\n\t * ever called on MY own copy - the others' files are theirs. */\n\tdropCopy(fileId: string): Promise<void>;\n\t/** Admin: write (or rewrite) the bulletin - the encrypted file the invitation\n\t * link points at, holding the read-only projection plus the control record.\n\t * Idempotent per group; returns where it lives (the link's fileId + key). */\n\tpublishBulletin(key: string, payload: SharePayload): Promise<{ fileId: string; key: string }>;\n\t/** Admin: stop the invitation - the bulletin drops its projection and roster,\n\t * so the link no longer previews or admits. */\n\trevokeBulletin(): Promise<void>;\n\t/** Open an invitation link's bulletin (through the relay) and STASH it for the\n\t * join, returning its content for the read-only preview. Null when the link\n\t * is dead or the key wrong. */\n\topenIncoming(fileId: string, key: string): Promise<IncomingShare | null>;\n\t/** The invitation opened by openIncoming and stashed for the join, or null. */\n\ttakeStashedIncoming(): Promise<{ key: string; fileId: string; content: IncomingShare } | null>;\n\t/** Re-read a joined share's bulletin (member): the freshest roster. The\n\t * engine passes the invitation capability it joined through, so every\n\t * membership rereads ITS bulletin. Null when unreachable or revoked. */\n\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\t * nothing is concluded. The literal 'unreadable' when the bulletin WAS\n\t * fetched but cannot be opened (the key no longer decrypts it - the admin\n\t * rotated the link): the engine counts consecutive ones toward the stale\n\t * verdict. A backend that cannot tell may keep returning null; the stale\n\t * detection then simply never fires. */\n\trereadJoined(fileId: string, key: string): Promise<SharePayload | null | 'unreadable'>;\n\t/** Member: announce my copy to the group's mailbox so the admin folds it. */\n\tannounce(mailboxId: string, copy: CopyLink): Promise<void>;\n\t/** Admin: drain the announce mailbox - the copies members published. */\n\ttakeAnnounces(mailboxId: string): Promise<CopyLink[]>;\n\t/** A read-only PeerSource over another member's published copy. */\n\tpeerSource(link: CopyLink): PeerSource;\n}\n\n// --- Persisted group ----------------------------------------------------------\n\n/** Everything needed to re-arm ONE share on reboot. A device may hold several\n * (one per joined silo); they persist together under the single KV key. */\ninterface StoredGroup {\n\trole: 'admin' | 'member';\n\t/** The wallet FILE this share is bound to (captured at start/join when the\n\t * host injects `wallet`). The mirror and peers arm only while it is the\n\t * attached one: another silo's data must never publish into the copy, and\n\t * the shared data must never fold into another silo. Absent = unbound\n\t * (single-wallet hosts), the share follows the store wherever it points. */\n\twalletFileId?: string;\n\t/** The shared group key K (the invitation link's fragment key). */\n\tkey: string;\n\t/** Where members announce their copy (a secret only K-holders read). */\n\tmailboxId: string;\n\t/** Known copies (the admin's own first). */\n\troster: CopyLink[];\n\t/** My own published copy (never the member's own file). */\n\tmyCopy: CopyLink;\n\t/** Admin: where the bulletin lives - what the link carries. Member: the\n\t * invitation opened to join - the roster refresh rereads it. (Absent on\n\t * pre-multi member records: their refresh stays dormant until a re-join.) */\n\tbulletin?: { fileId: string; key: string };\n\t/** Admin only: false once revoked (the bulletin stops previewing/admitting). */\n\tlive?: boolean;\n\t/** Admin only: a roster change could not be rebroadcast (offline mid-fold);\n\t * the next converge republishes even though nothing new was folded. */\n\tneedsRebroadcast?: boolean;\n\t/** Member only: my copy has not been SEEN in a fresh roster yet - the join\n\t * announce (or a later one) may have been lost. While set, every converge\n\t * re-announces, readable bulletin or not, so a joiner whose first announce\n\t * died on a network blip still becomes visible without re-opening the\n\t * invitation. Cleared the moment a fresh roster carries my copy. */\n\tannouncePending?: boolean;\n\t/** Member only: consecutive rereads whose bulletin was FETCHED but no longer\n\t * decrypts under my key. Two in a row set `stale` (one bad read must not\n\t * condemn a share over a hiccup); any readable read resets both. */\n\tunreadableReads?: number;\n\t/** Member only: the admin rotated the link (see unreadableReads) - my edits\n\t * publish into a copy nobody folds anymore. The host should surface \"this\n\t * share was renewed, ask for a fresh invitation\" instead of letting the\n\t * member edit into the void. */\n\tstale?: boolean;\n}\n\n/** The persisted shape: every membership on this device. A lone pre-multi\n * record lifts into a one-entry list on first load. */\ninterface StoredGroups {\n\tv: 2;\n\tgroups: StoredGroup[];\n}\n\n/** The default KV key the group lives under. Exported so a caller can wipe\n * it. Hosts with a pre-existing record under another key pass `storageKey`. */\nexport const HOUSEHOLD_GROUP_KEY = 'selfstore:households:group:v1';\n\n/** Every membership publishes through its own mirror: the id is scoped by the\n * copy file (unique per share) so several never collide on one store. */\nconst mirrorId = (group: StoredGroup): string => `household-copy:${group.myCopy.fileId}`;\n\n// --- Public surface -----------------------------------------------------------\n\n/** One membership, as the UI lists them: which silo carries it and whose\n * share it is. */\nexport interface MembershipInfo {\n\t/** The wallet file this membership is bound to (null = unbound legacy). */\n\twalletFileId: string | null;\n\tisAdmin: boolean;\n\t/** My published copy's file id for this share. */\n\tselfFileId: string;\n\t/** Who shares it with me (the admin's roster label); null for my own share. */\n\tsharedBy: string | null;\n\tmemberCount: number;\n\t/** Member: my copy is not in a fresh roster yet (the announce may have been\n\t * lost; the engine keeps re-announcing). The host can show \"waiting for the\n\t * admin to see you\" instead of a false everything-is-fine. */\n\tannouncePending: boolean;\n\t/** Member: the admin rotated the link - edits publish into the void until a\n\t * fresh invitation is joined. The host must surface it. */\n\tstale: boolean;\n}\n\n/** The reactive-friendly snapshot the UI reads (hosts wrap it in their own\n * reactivity - a Svelte rune, a React store, anything). The singular fields\n * describe the PRIMARY membership (the admin one when it exists, else the\n * first); `memberships` lists them all. */\nexport interface HouseholdGroupState {\n\t/** A share exists on this device (admin or member). */\n\tactive: boolean;\n\tisAdmin: boolean;\n\t/** How many copies are in the roster (this device included). */\n\tmemberCount: number;\n\t/** The roster's cosmetic owner labels, for a \"who has access\" list. */\n\tmembers: { fileId: string; label: string }[];\n\t/** My own copy's file id, so the UI can list everyone ELSE. */\n\tselfFileId: string | null;\n\t/** The wallet file the share is bound to, or null when unbound. The host\n\t * compares it to its own attached-file id to render \"this silo carries the\n\t * share\" and to gate its own publications the same way the engine does. */\n\twalletFileId: string | null;\n\t/** Admin: the live invitation's capability, so the panel can re-show the\n\t * link without republishing anything. */\n\tinviteCapability: { fileId: string; key: string } | null;\n\t/** Every membership on this device, admin and joined alike. */\n\tmemberships: MembershipInfo[];\n}\n\nexport interface HouseholdGroup {\n\treadonly state: HouseholdGroupState;\n\t/** Admin: start sharing. The member's own file is untouched - a dedicated\n\t * copy is created and mirrored under a fresh K, and the invitation\n\t * bulletin is published. Refused on a password-protected store: remove\n\t * the password to share, or keep it and do not. */\n\tstartShare(): Promise<{ fileId: string; key: string }>;\n\t/** Admin: re-show (and refresh) the standing invitation link. Idempotent. */\n\tinvite(): Promise<{ fileId: string; key: string }>;\n\t/** /share: open an invitation link's bulletin and remember it for the join. */\n\topenIncoming(fileId: string, key: string): Promise<IncomingShare | null>;\n\t/** Member: after openIncoming + own store connected, join read-write now.\n\t * Your own file stays yours: a dedicated copy is created and mirrored\n\t * under K, the others' copies attach as keyed peers (the next converge\n\t * folds the shared data into your own file), and my copy is announced.\n\t * Same honest rule as startShare: a password-protected store refuses.\n\t * With a `wallet` getter, one membership per share: joining another share\n\t * ADDS a membership (bound to the attached silo), and a re-join over the\n\t * same silo replaces its stale one. Without the getter, the historical\n\t * single-membership rule holds ('mismatch' on a different share). */\n\tjoin(): Promise<'joined' | 'no-invite' | 'mismatch' | 'error'>;\n\t/** Both: one convergence round (admin folds new announces + rebroadcasts;\n\t * everyone re-arms mirror/peers). Called on every converge. */\n\tsyncGroup(): Promise<void>;\n\t/** End ONE share on this device - and, for the admin, this is the real\n\t * revocation (bearer link: the lock changes for everyone). The bulletin\n\t * is revoked, my copy file is dropped, mirror and peers detach. The\n\t * member's own file is untouched: everyone keeps what already converged into\n\t * their own file - the local-first reality. Re-sharing mints a fresh K\n\t * and a fresh link for whoever should stay. `walletFileId` names WHICH\n\t * membership; omitted, the primary one (admin first) goes. */\n\tleave(walletFileId?: string | null): Promise<void>;\n\t/** On boot: re-arm the mirror and peers of the current share. */\n\trestore(): Promise<void>;\n}\n\n// --- Factory --------------------------------------------------------------------\n\nexport function createHouseholdGroup(deps: {\n\tstore: LocalStore;\n\tkv: KV;\n\tbackend: ShareBackend;\n\t/** KV key the group record lives under. Default HOUSEHOLD_GROUP_KEY. */\n\tstorageKey?: string;\n\t/** The currently attached wallet file id, for multi-backup hosts. When\n\t * given, start/join bind the share to that file and the engine goes\n\t * dormant whenever another one is attached. Omit on single-wallet hosts. */\n\twallet?: () => Promise<string | null>;\n}): HouseholdGroup {\n\tconst { store, kv, backend, wallet } = deps;\n\tconst storageKey = deps.storageKey ?? HOUSEHOLD_GROUP_KEY;\n\n\tlet groups: StoredGroup[] | null = null;\n\tlet loaded = false;\n\t// Member roster mirror: how many consecutive fresh reads a known peer has\n\t// been MISSING from the bulletin. Removals apply only at 2 - a single stale\n\t// relay read must never cut a still-valid member's data off. Keys are\n\t// scoped per share (`mailboxId:fileId`).\n\tconst rosterMisses = new Map<string, number>();\n\n\tasync function load(): Promise<StoredGroup[]> {\n\t\tif (!loaded) {\n\t\t\tconst raw = await kv.get<StoredGroups | StoredGroup>(storageKey);\n\t\t\tif (!raw) groups = [];\n\t\t\telse if ((raw as StoredGroups).v === 2) groups = (raw as StoredGroups).groups;\n\t\t\t// A pre-multi single record lifts into a one-entry list, verbatim.\n\t\t\telse groups = [raw as StoredGroup];\n\t\t\tloaded = true;\n\t\t}\n\t\treturn groups as StoredGroup[];\n\t}\n\n\t/** Write the current list back (the callers mutate `groups` in place). An\n\t * empty list clears the key: no membership, no record. */\n\tasync function persist(): Promise<void> {\n\t\tloaded = true;\n\t\tif (!groups || groups.length === 0) {\n\t\t\tgroups = groups ?? [];\n\t\t\tawait kv.del(storageKey);\n\t\t\treturn;\n\t\t}\n\t\tawait kv.set(storageKey, { v: 2, groups } satisfies StoredGroups);\n\t}\n\n\tconst adminGroup = (list: StoredGroup[]): StoredGroup | null =>\n\t\tlist.find((g) => g.role === 'admin') ?? null;\n\t/** The membership the singular state fields describe: admin first. */\n\tconst primaryGroup = (list: StoredGroup[]): StoredGroup | null =>\n\t\tadminGroup(list) ?? list[0] ?? null;\n\t/** Who shares a joined group with me: the admin's copy leads the roster. */\n\tconst sharedByOf = (g: StoredGroup): string | null =>\n\t\tg.role === 'member' ? (g.roster[0]?.ownerEmail ?? g.roster[0]?.ownerName ?? null) : null;\n\n\t/** A fresh 32-byte key, base64url, for a new share (the link fragment). */\n\tfunction randomKey(): string {\n\t\tconst bytes = crypto.getRandomValues(new Uint8Array(32));\n\t\tlet s = '';\n\t\tfor (const b of bytes) s += String.fromCharCode(b);\n\t\treturn btoa(s).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n\t}\n\n\t/** The honest rule, enforced at both entrances: sharing never re-keys the\n\t * own file, so a password-protected store cannot publish a readable copy\n\t * without lowering the bar its owner chose. Refuse plainly. */\n\tfunction refuseProtected(): void {\n\t\tif (store.state.encrypted) {\n\t\t\tthrow new Error(\n\t\t\t\t'household-group: a password-protected store cannot be shared - remove the password first'\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Whether the share's bound wallet is the attached one. Unbound (or no\n\t * getter) is always in scope - the single-wallet behavior. */\n\tasync function inScope(group: StoredGroup): Promise<boolean> {\n\t\tif (!group.walletFileId || !wallet) return true;\n\t\treturn (await wallet()) === group.walletFileId;\n\t}\n\n\t/** Out-of-scope dormancy: tear down BOTH directions. The mirror would leak\n\t * the attached silo's data into the copy; the peers would fold the shared\n\t * data into that silo. Nothing else moves until the bound wallet is back. */\n\tfunction disarm(group: StoredGroup): void {\n\t\tstore.detachMirror(mirrorId(group));\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId !== group.myCopy.fileId) store.detachPeer(link.fileId);\n\t\t}\n\t}\n\n\t/** Re-arm my copy's mirror (idempotent - safe on every converge/boot). */\n\tfunction ensureMirror(group: StoredGroup): void {\n\t\tconst id = mirrorId(group);\n\t\tif (store.state.mirrors.some((m) => m.id === id)) return;\n\t\tstore.attachMirror(backend.copyTarget(group.myCopy.fileId), {\n\t\t\tpassword: group.key,\n\t\t\tid\n\t\t});\n\t}\n\n\t/** Attach every roster copy that is not mine and not already attached as a\n\t * read-only peer under K (its own key - the member's file has none). Idempotent. */\n\tfunction attachPeers(group: StoredGroup): void {\n\t\tconst already = new Set(store.state.peers.map((p) => p.id));\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\tif (already.has(link.fileId)) continue;\n\t\t\tstore.attachPeer(backend.peerSource(link), { id: link.fileId, password: group.key });\n\t\t}\n\t}\n\n\t/** Merge new copies into a roster, de-duplicated by fileId, dropping mine. */\n\tfunction mergeRoster(roster: CopyLink[], incoming: CopyLink[], selfFileId: string): CopyLink[] {\n\t\tconst byId = new Map(roster.map((l) => [l.fileId, l]));\n\t\tfor (const link of incoming) {\n\t\t\tif (link.fileId === selfFileId) continue;\n\t\t\tbyId.set(link.fileId, link);\n\t\t}\n\t\treturn [...byId.values()];\n\t}\n\n\t/** Post an announce with small bounded retries; true when one landed. A lost\n\t * announce strands the joiner - visible to nobody, editing into the void -\n\t * so it is worth a few immediate retries before falling back to the\n\t * persistent announcePending re-announce loop. */\n\tasync function announceWithRetry(mailboxId: string, copy: CopyLink): Promise<boolean> {\n\t\tfor (let attempt = 0; attempt < 3; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait backend.announce(mailboxId, copy);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\tif (attempt < 2) await new Promise((r) => setTimeout(r, 800 * (attempt + 1)));\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** One admin converge: fold newly-announced member copies, rebroadcast. */\n\tasync function adminRound(group: StoredGroup): Promise<void> {\n\t\tconst announced = await backend.takeAnnounces(group.mailboxId);\n\t\tconst roster = mergeRoster(group.roster, announced, group.myCopy.fileId);\n\t\tconst grew = roster.length !== group.roster.length;\n\t\tconst needed = group.needsRebroadcast === true;\n\t\tgroup.roster = roster;\n\t\tawait persist();\n\t\tattachPeers(group);\n\t\t// Rebroadcast on growth or when a previous rebroadcast failed: keyed\n\t\t// on `grew` alone, an offline publish left the bulletin stale forever\n\t\t// (the announce was already consumed, so no later round re-grew).\n\t\tif ((grew || needed) && group.live === true) {\n\t\t\ttry {\n\t\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\t\tv: 1,\n\t\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\t\troster\n\t\t\t\t});\n\t\t\t\tgroup.bulletin = where;\n\t\t\t\tgroup.needsRebroadcast = undefined;\n\t\t\t} catch {\n\t\t\t\tgroup.needsRebroadcast = true;\n\t\t\t}\n\t\t\tawait persist();\n\t\t}\n\t}\n\n\t/** Mirror the authoritative roster onto the local one. Removals apply\n\t * CONSERVATIVELY: a peer drops only after two consecutive fresh reads\n\t * without it (stale relay tolerance). */\n\tfunction applyRosterRemovals(group: StoredGroup, byId: Map<string, CopyLink>): void {\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\tconst missKey = `${group.mailboxId}:${link.fileId}`;\n\t\t\tif (byId.has(link.fileId)) {\n\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst misses = (rosterMisses.get(missKey) ?? 0) + 1;\n\t\t\tif (misses >= 2) {\n\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\tstore.detachPeer(link.fileId);\n\t\t\t} else {\n\t\t\t\trosterMisses.set(missKey, misses);\n\t\t\t\tbyId.set(link.fileId, link);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** One member converge: reread the bulletin (the admin's roster is\n\t * AUTHORITATIVE - replace, not merge, so removals propagate), self-heal a\n\t * lost announce, track the stale verdict. Pre-multi records carry no\n\t * bulletin: their refresh stays dormant, peers still fold. */\n\tasync function memberRound(group: StoredGroup): Promise<void> {\n\t\tconst share = group.bulletin\n\t\t\t? await backend.rereadJoined(group.bulletin.fileId, group.bulletin.key)\n\t\t\t: null;\n\t\tif (share === 'unreadable') {\n\t\t\t// Fetched but no longer decrypts: the admin rotated the link. Two\n\t\t\t// consecutive verdicts set the stale flag the host must surface -\n\t\t\t// from here my edits publish into a copy nobody folds. Peers keep\n\t\t\t// folding (the member keeps what already converged).\n\t\t\tconst n = (group.unreadableReads ?? 0) + 1;\n\t\t\tgroup.unreadableReads = n;\n\t\t\tif (n >= 2) group.stale = true;\n\t\t\tawait persist();\n\t\t\tattachPeers(group);\n\t\t\treturn;\n\t\t}\n\t\tif (!share) {\n\t\t\t// Unreachable bulletin: conclude nothing - but a pending announce\n\t\t\t// still retries (the mailbox may answer even when the bulletin\n\t\t\t// relay does not), so a stranded joiner heals as soon as ANY\n\t\t\t// network path opens, not only after a full bulletin read.\n\t\t\tif (group.announcePending) {\n\t\t\t\tawait backend.announce(group.mailboxId, group.myCopy).catch(() => undefined);\n\t\t\t}\n\t\t\tattachPeers(group);\n\t\t\treturn;\n\t\t}\n\t\t// A readable bulletin is proof the share lives: clear staleness.\n\t\tgroup.unreadableReads = undefined;\n\t\tgroup.stale = undefined;\n\t\tconst byId = new Map(share.roster.map((l) => [l.fileId, l]));\n\t\t// SELF-HEAL: my own copy missing from a fresh roster means my\n\t\t// announce was lost. Announce again (with retries); the admin's\n\t\t// fold de-dups. announcePending tracks the truth either way, so\n\t\t// the host can say \"waiting to be seen\" instead of false calm.\n\t\tif (!byId.has(group.myCopy.fileId)) {\n\t\t\tbyId.set(group.myCopy.fileId, group.myCopy);\n\t\t\tgroup.announcePending = true;\n\t\t\tawait announceWithRetry(group.mailboxId, group.myCopy);\n\t\t} else {\n\t\t\tgroup.announcePending = undefined;\n\t\t}\n\t\tapplyRosterRemovals(group, byId);\n\t\tgroup.roster = [...byId.values()];\n\t\tawait persist();\n\t\tattachPeers(group);\n\t}\n\n\treturn {\n\t\tget state(): HouseholdGroupState {\n\t\t\tconst list = groups ?? [];\n\t\t\tconst memberships: MembershipInfo[] = list.map((g) => ({\n\t\t\t\twalletFileId: g.walletFileId ?? null,\n\t\t\t\tisAdmin: g.role === 'admin',\n\t\t\t\tselfFileId: g.myCopy.fileId,\n\t\t\t\tsharedBy: sharedByOf(g),\n\t\t\t\tmemberCount: g.roster.length,\n\t\t\t\tannouncePending: g.announcePending === true,\n\t\t\t\tstale: g.stale === true\n\t\t\t}));\n\t\t\tconst group = primaryGroup(list);\n\t\t\tif (!group) {\n\t\t\t\treturn {\n\t\t\t\t\tactive: false,\n\t\t\t\t\tisAdmin: false,\n\t\t\t\t\tmemberCount: 0,\n\t\t\t\t\tmembers: [],\n\t\t\t\t\tselfFileId: null,\n\t\t\t\t\twalletFileId: null,\n\t\t\t\t\tinviteCapability: null,\n\t\t\t\t\tmemberships\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tactive: true,\n\t\t\t\tisAdmin: group.role === 'admin',\n\t\t\t\tmemberCount: group.roster.length,\n\t\t\t\tmembers: group.roster.map((l) => ({\n\t\t\t\t\tfileId: l.fileId,\n\t\t\t\t\tlabel: l.ownerEmail ?? l.ownerName ?? l.fileId.slice(0, 8)\n\t\t\t\t})),\n\t\t\t\tselfFileId: group.myCopy.fileId,\n\t\t\t\twalletFileId: group.walletFileId ?? null,\n\t\t\t\tinviteCapability:\n\t\t\t\t\tgroup.role === 'admin' && group.live === true && group.bulletin ? group.bulletin : null,\n\t\t\t\tmemberships\n\t\t\t};\n\t\t},\n\n\t\tasync startShare(): Promise<{ fileId: string; key: string }> {\n\t\t\tconst list = await load();\n\t\t\t// One ADMIN share per device; joined memberships do not block it.\n\t\t\tif (adminGroup(list)) {\n\t\t\t\tthrow new Error('household-group: a share already exists on this device');\n\t\t\t}\n\t\t\trefuseProtected();\n\t\t\tconst key = randomKey();\n\t\t\tconst mailboxId = randomId();\n\t\t\tconst walletFileId = (await wallet?.()) ?? undefined;\n\t\t\t// The copy is a dedicated file - the member's own file is never in the roster.\n\t\t\tconst link = await backend.createCopy();\n\t\t\tconst roster = [link];\n\t\t\t// Persist before the publish: a failure at any later step leaves a\n\t\t\t// recoverable half-share (restore re-arms the mirror, invite() finishes\n\t\t\t// the publication). Nothing here is irreversible for the member's own file.\n\t\t\tconst group: StoredGroup = {\n\t\t\t\trole: 'admin',\n\t\t\t\twalletFileId,\n\t\t\t\tkey,\n\t\t\t\tmailboxId,\n\t\t\t\troster,\n\t\t\t\tmyCopy: link,\n\t\t\t\tlive: false\n\t\t\t};\n\t\t\tlist.push(group);\n\t\t\tawait persist();\n\t\t\t// The mirror fills the copy right away and keeps it fresh after every\n\t\t\t// save/converge that moves data. The member's own file is not touched.\n\t\t\tensureMirror(group);\n\t\t\tconst where = await backend.publishBulletin(key, { v: 1, mailboxId, roster });\n\t\t\tgroup.bulletin = where;\n\t\t\tgroup.live = true;\n\t\t\tawait persist();\n\t\t\treturn where;\n\t\t},\n\n\t\tasync invite(): Promise<{ fileId: string; key: string }> {\n\t\t\tconst group = adminGroup(await load());\n\t\t\tif (!group) {\n\t\t\t\tthrow new Error('household-group: invite() requires an admin share');\n\t\t\t}\n\t\t\t// Refresh the bulletin (fresh projection + current roster) and mark live.\n\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\tv: 1,\n\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\troster: group.roster\n\t\t\t});\n\t\t\tgroup.bulletin = where;\n\t\t\tgroup.live = true;\n\t\t\tawait persist();\n\t\t\treturn where;\n\t\t},\n\n\t\tasync openIncoming(fileId: string, key: string): Promise<IncomingShare | null> {\n\t\t\treturn backend.openIncoming(fileId, key);\n\t\t},\n\n\t\tasync join(): Promise<'joined' | 'no-invite' | 'mismatch' | 'error'> {\n\t\t\tconst list = await load();\n\t\t\tconst incoming = await backend.takeStashedIncoming();\n\t\t\t// No invitation opened: a device already holding a membership answers\n\t\t\t// the historical idempotent 'joined'; a bare one names the dead end.\n\t\t\tif (!incoming) return list.length > 0 ? 'joined' : 'no-invite';\n\t\t\tconst { key, fileId, content } = incoming;\n\t\t\tif (list.some((g) => g.mailboxId === content.share.mailboxId)) return 'joined';\n\t\t\t// Single-wallet hosts (no getter) keep the one-membership rule: an\n\t\t\t// invitation to ANOTHER share is a dead end, not a silent second join.\n\t\t\tif (!wallet && list.length > 0) return 'mismatch';\n\t\t\trefuseProtected();\n\t\t\tconst walletFileId = (await wallet?.()) ?? undefined;\n\t\t\t// A re-join over the same silo replaces its stale membership (a rotated\n\t\t\t// link): two shares must never publish the same wallet.\n\t\t\tif (walletFileId) {\n\t\t\t\tconst stale = list.find((g) => g.walletFileId === walletFileId);\n\t\t\t\tif (stale) {\n\t\t\t\t\tdisarm(stale);\n\t\t\t\t\tawait backend.dropCopy(stale.myCopy.fileId).catch(() => undefined);\n\t\t\t\t\tgroups = list.filter((g) => g !== stale);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// My copy is a dedicated file; my own store stays mine. The shared\n\t\t\t// data arrives by folding the others' copies into it on the next\n\t\t\t// converge - one store per person, converging.\n\t\t\tconst link = await backend.createCopy(undefined, content.share.roster[0]?.ownerEmail);\n\t\t\tconst roster = mergeRoster(content.share.roster, [link], '');\n\t\t\tconst group: StoredGroup = {\n\t\t\t\trole: 'member',\n\t\t\t\twalletFileId,\n\t\t\t\tkey,\n\t\t\t\tmailboxId: content.share.mailboxId,\n\t\t\t\troster,\n\t\t\t\tmyCopy: link,\n\t\t\t\t// The invitation capability, kept so THIS membership rereads ITS\n\t\t\t\t// bulletin on converge (roster removals, lost-announce self-heal).\n\t\t\t\tbulletin: { fileId, key }\n\t\t\t};\n\t\t\t(groups as StoredGroup[]).push(group);\n\t\t\tawait persist();\n\t\t\tensureMirror(group);\n\t\t\tattachPeers(group);\n\t\t\t// Announce my copy so the admin folds it into everyone's roster. NOT\n\t\t\t// best-effort-and-forget: a swallowed failure here once stranded a\n\t\t\t// joiner for good - member on their side, visible to nobody, editing\n\t\t\t// into the void. Retry now; still failing, persist announcePending so\n\t\t\t// every converge keeps announcing until a fresh roster carries me.\n\t\t\tif (!(await announceWithRetry(content.share.mailboxId, link))) {\n\t\t\t\tgroup.announcePending = true;\n\t\t\t\tawait persist();\n\t\t\t}\n\t\t\treturn 'joined';\n\t\t},\n\n\t\tasync syncGroup(): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\tfor (const group of list) {\n\t\t\t\t// Defense in depth for the honest rule: a store that became\n\t\t\t\t// password-protected (or joined from a device whose store turned out\n\t\t\t\t// protected) must never publish a link-readable copy. Tear the mirror\n\t\t\t\t// down if one was armed; the share lies dormant on this device until\n\t\t\t\t// the password is removed. (Folding INBOUND peers would be harmless,\n\t\t\t\t// but a dormant share should read as dormant - arm nothing.)\n\t\t\t\tif (store.state.encrypted) {\n\t\t\t\t\tstore.detachMirror(mirrorId(group));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Another silo is attached: full dormancy, both directions (see disarm).\n\t\t\t\tif (!(await inScope(group))) {\n\t\t\t\t\tdisarm(group);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tensureMirror(group);\n\t\t\t\tif (group.role === 'admin') await adminRound(group);\n\t\t\t\telse await memberRound(group);\n\t\t\t}\n\t\t},\n\n\t\tasync leave(walletFileId?: string | null): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\tconst group =\n\t\t\t\twalletFileId != null\n\t\t\t\t\t? (list.find((g) => g.walletFileId === walletFileId) ?? null)\n\t\t\t\t\t: primaryGroup(list);\n\t\t\tif (!group) return;\n\t\t\t// Detach the plumbing first: nothing publishes or folds anymore.\n\t\t\tdisarm(group);\n\t\t\tif (group.role === 'admin') {\n\t\t\t\ttry {\n\t\t\t\t\tawait backend.revokeBulletin();\n\t\t\t\t} catch {\n\t\t\t\t\t/* tolerable: the bulletin only ever holds K-encrypted data */\n\t\t\t\t}\n\t\t\t}\n\t\t\t// My copy file dies with the membership (private first, then gone).\n\t\t\t// The member's own file is untouched: it never carried the share key, and the\n\t\t\t// data already converged into it stays - local-first honesty.\n\t\t\tawait backend.dropCopy(group.myCopy.fileId).catch(() => undefined);\n\t\t\tgroups = list.filter((g) => g !== group);\n\t\t\tfor (const k of [...rosterMisses.keys()]) {\n\t\t\t\tif (k.startsWith(`${group.mailboxId}:`)) rosterMisses.delete(k);\n\t\t\t}\n\t\t\tawait persist();\n\t\t},\n\n\t\tasync restore(): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\t// Same dormancy rules as syncGroup: a protected store never mirrors,\n\t\t\t// and a share bound to another silo arms nothing while it is away.\n\t\t\tif (store.state.encrypted) return;\n\t\t\tfor (const group of list) {\n\t\t\t\tif (!(await inScope(group))) continue;\n\t\t\t\tensureMirror(group);\n\t\t\t\tattachPeers(group);\n\t\t\t}\n\t\t}\n\t};\n}\n"]}
|
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,9 +1,9 @@
|
|
|
1
|
-
import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-
|
|
2
|
-
export { gisDriveAuth } from './chunk-
|
|
1
|
+
import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-XI3MGK4M.js';
|
|
2
|
+
export { gisDriveAuth } from './chunk-XI3MGK4M.js';
|
|
3
3
|
import { saveToDisk } from './chunk-UWVAKBU4.js';
|
|
4
4
|
export { pickFromDisk, saveToDisk } from './chunk-UWVAKBU4.js';
|
|
5
5
|
import './chunk-SKWHKM25.js';
|
|
6
|
-
import { fromSession, fromSession2, fromSession3, connect4, connect3, isSupported, connect2, connect } from './chunk-
|
|
6
|
+
import { fromSession, fromSession2, fromSession3, connect4, connect3, isSupported, connect2, connect } from './chunk-TEIAFKFS.js';
|
|
7
7
|
import { restore } from './chunk-SRKUHFRG.js';
|
|
8
8
|
export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-SRKUHFRG.js';
|
|
9
9
|
import './chunk-KHER6MWR.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Snapshot, E as EncodeOptions, H as Header } from './store-
|
|
2
|
-
import { D as DriveAuth } from './drive-
|
|
1
|
+
import { n as Snapshot, E as EncodeOptions, H as Header } from './store-2RnED1Vq.js';
|
|
2
|
+
import { D as DriveAuth } from './drive-DNiDXBDe.js';
|
|
3
3
|
|
|
4
4
|
/** Serialize a snapshot to a portable backup blob (compressed; encrypted if a password is given). */
|
|
5
5
|
declare function exportSnapshot(snapshot: Snapshot, opts: EncodeOptions): Promise<Blob>;
|
|
@@ -268,6 +268,11 @@ interface BackupTarget {
|
|
|
268
268
|
reconnect(): Promise<boolean>;
|
|
269
269
|
/** Forget the target locally. Never deletes the remote data. */
|
|
270
270
|
disconnect(): Promise<void>;
|
|
271
|
+
/** Cut any in-flight network request NOW (best-effort). The store calls it
|
|
272
|
+
* when the user detaches the target, so a suspended request cannot make
|
|
273
|
+
* the detach wait behind its network deadline. Optional: without it,
|
|
274
|
+
* in-flight calls simply run to their own deadlines first. */
|
|
275
|
+
abortInFlight?(): void;
|
|
271
276
|
}
|
|
272
277
|
|
|
273
278
|
type Mode = 'persistent' | 'ephemeral';
|
package/dist/widgets.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { FlowHost, ConnectTargets, ConnectFlowOptions, ConnectKind, ConnectFlow, ShareEngine, ShareLevel, ShareFlow, JoinEngine, JoinFlow, ReplicaFlow } from './flows.js';
|
|
2
2
|
import { BackupsManager } from './backups.js';
|
|
3
|
-
import './store-
|
|
3
|
+
import './store-2RnED1Vq.js';
|
|
4
4
|
import './cache-De5_sALA.js';
|
|
5
5
|
import './sync.js';
|
|
6
|
-
import './drive-
|
|
6
|
+
import './drive-DNiDXBDe.js';
|
|
7
7
|
import './machine-ALMkT3mk.js';
|
|
8
8
|
|
|
9
9
|
/** Flat key -> string map; widgets merge a partial override over EN defaults. */
|
package/dist/widgets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { connectFlow, shareFlow, joinFlow } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
import { connectFlow, shareFlow, joinFlow } from './chunk-CUFKME4S.js';
|
|
2
|
+
import './chunk-TEIAFKFS.js';
|
|
3
3
|
import './chunk-SRKUHFRG.js';
|
|
4
4
|
import './chunk-43BJDRPT.js';
|
|
5
5
|
import './chunk-KHER6MWR.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "selfstore",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Local-first storage for browser apps: an IndexedDB working copy, portable encrypted ZIP backups (AES-256-GCM over Argon2id), durable homes (disk, Google Drive, WebDAV) and a serverless multi-device merge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|