selfstore 1.0.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/dist/advanced.d.ts +90 -0
  4. package/dist/advanced.js +41 -0
  5. package/dist/advanced.js.map +1 -0
  6. package/dist/backups.d.ts +182 -0
  7. package/dist/backups.js +360 -0
  8. package/dist/backups.js.map +1 -0
  9. package/dist/cache-De5_sALA.d.ts +129 -0
  10. package/dist/cache-lock-LZILZNLP.js +23 -0
  11. package/dist/cache-lock-LZILZNLP.js.map +1 -0
  12. package/dist/chunk-ATUPHVLG.js +441 -0
  13. package/dist/chunk-ATUPHVLG.js.map +1 -0
  14. package/dist/chunk-BJ3RUMGI.js +45 -0
  15. package/dist/chunk-BJ3RUMGI.js.map +1 -0
  16. package/dist/chunk-CUWB5EWF.js +732 -0
  17. package/dist/chunk-CUWB5EWF.js.map +1 -0
  18. package/dist/chunk-FDPAKHQS.js +230 -0
  19. package/dist/chunk-FDPAKHQS.js.map +1 -0
  20. package/dist/chunk-IBIEKK36.js +331 -0
  21. package/dist/chunk-IBIEKK36.js.map +1 -0
  22. package/dist/chunk-JB4JHM7V.js +474 -0
  23. package/dist/chunk-JB4JHM7V.js.map +1 -0
  24. package/dist/chunk-N4RPMX3Y.js +1908 -0
  25. package/dist/chunk-N4RPMX3Y.js.map +1 -0
  26. package/dist/chunk-OWLX6PNT.js +231 -0
  27. package/dist/chunk-OWLX6PNT.js.map +1 -0
  28. package/dist/chunk-QBCU7OOP.js +20 -0
  29. package/dist/chunk-QBCU7OOP.js.map +1 -0
  30. package/dist/chunk-SGISYMT5.js +46 -0
  31. package/dist/chunk-SGISYMT5.js.map +1 -0
  32. package/dist/chunk-VBFMMR7Q.js +29 -0
  33. package/dist/chunk-VBFMMR7Q.js.map +1 -0
  34. package/dist/drive-Bv94UM8b.d.ts +222 -0
  35. package/dist/flows.d.ts +306 -0
  36. package/dist/flows.js +153 -0
  37. package/dist/flows.js.map +1 -0
  38. package/dist/groups.d.ts +39 -0
  39. package/dist/groups.js +103 -0
  40. package/dist/groups.js.map +1 -0
  41. package/dist/households.d.ts +214 -0
  42. package/dist/households.js +410 -0
  43. package/dist/households.js.map +1 -0
  44. package/dist/index.d.ts +205 -0
  45. package/dist/index.js +249 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/kdf-worker.js +2395 -0
  48. package/dist/kdf-worker.js.map +1 -0
  49. package/dist/local-2py_vGGy.d.ts +47 -0
  50. package/dist/local-AM22RWRR.js +7 -0
  51. package/dist/local-AM22RWRR.js.map +1 -0
  52. package/dist/machine-ALMkT3mk.d.ts +17 -0
  53. package/dist/store-ChIGEOph.d.ts +577 -0
  54. package/dist/sync.d.ts +107 -0
  55. package/dist/sync.js +3 -0
  56. package/dist/sync.js.map +1 -0
  57. package/dist/widgets.d.ts +426 -0
  58. package/dist/widgets.js +2579 -0
  59. package/dist/widgets.js.map +1 -0
  60. package/llms.txt +405 -0
  61. package/package.json +99 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/households/codec.ts","../src/households/group.ts"],"names":[],"mappings":"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","file":"households.js","sourcesContent":["// Household share transport (capability-link model): the plain shapes the\n// bulletin and the copy-announce carry, plus a dependency-free base64url\n// codec. The link key K is the shared group secret - possession is\n// membership, no per-member identity, no signed manifest. Payloads carry\n// only file ids of link-readable files whose content is encrypted under K,\n// so an id alone reveals nothing. Pure by contract: no secret held, no I/O,\n// no framework - shapes, validation and transport strings only.\n\n// --- Errors -----------------------------------------------------------------\n\n/** Why a code was refused. `malformed` covers bad transport, wrong structure or\n * a code decoded as the wrong kind of payload; `unsupported-version` is a\n * numeric `v` this build does not understand. */\nexport type HouseholdCodeErrorCode = 'malformed' | 'unsupported-version';\n\n/** The single typed error this module throws, so callers branch on `code`\n * instead of parsing messages. A decoder that throws never returns a partially\n * parsed object - it fails whole. */\nexport class HouseholdCodeError extends Error {\n\treadonly code: HouseholdCodeErrorCode;\n\tconstructor(code: HouseholdCodeErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'HouseholdCodeError';\n\t\tthis.code = code;\n\t\tObject.setPrototypeOf(this, HouseholdCodeError.prototype);\n\t}\n}\n\nconst malformed = (message: string): HouseholdCodeError => new HouseholdCodeError('malformed', message);\n\n// --- Types ------------------------------------------------------------------\n\n/** Where a member published their encrypted copy. A discriminated union on\n * `provider` so a future transport (WebDAV, ...) slots in beside Drive without\n * changing the payload shapes. Today: Drive only. */\nexport type CopyLink = DriveCopyLink;\n\n/** A copy hosted on the owner's Google Drive. `fileId` is the Drive file id (a\n * public-link file, ciphertext under K); the owner fields are a cosmetic \"on\n * X's Drive\" hint, both optional. */\nexport interface DriveCopyLink {\n\tprovider: 'drive';\n\tfileId: string;\n\townerEmail?: string;\n\townerName?: string;\n}\n\n/** The bulletin's control record (rides one reserved collection inside the\n * K-encrypted bulletin): where members announce their copy (`mailboxId`, a\n * secret only K-holders can read) and the roster of every known copy (the\n * admin's own first). */\nexport interface SharePayload {\n\tv: 1;\n\tmailboxId: string;\n\troster: CopyLink[];\n}\n\n/** Member -> admin, through the announce mailbox: \"here is my copy, fold it\".\n * No identity, no signature: knowing the mailboxId (which lives inside the\n * K-encrypted bulletin) already proves the sender holds K. */\nexport interface AnnouncePayload {\n\tv: 1;\n\tcopy: CopyLink;\n}\n\n// --- base64url (isomorphic, dependency-free) --------------------------------\n\nconst utf8Encoder = new TextEncoder();\n// `fatal` makes a truncated or corrupted byte run throw instead of silently\n// yielding U+FFFD, so a mangled code fails fast in the decoder.\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: true });\n\nfunction bytesToBinary(bytes: Uint8Array): string {\n\tlet out = '';\n\tfor (let i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]);\n\treturn out;\n}\n\nfunction binaryToBytes(binary: string): Uint8Array {\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n\treturn bytes;\n}\n\n/** base64url without padding of raw bytes. btoa + TextEncoder work in the\n * browser and in Node 20+, so the codec stays dependency-free and isomorphic. */\nfunction bytesToBase64url(bytes: Uint8Array): string {\n\treturn btoa(bytesToBinary(bytes)).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction base64urlEncode(text: string): string {\n\treturn bytesToBase64url(utf8Encoder.encode(text));\n}\n\n/** Inverse of base64urlEncode. Throws on any character outside the alphabet or\n * on an invalid UTF-8 byte run; decodeJson turns that into a 'malformed'. */\nfunction base64urlDecode(code: string): string {\n\tconst b64 = code.replace(/-/g, '+').replace(/_/g, '/');\n\tconst padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n\treturn utf8Decoder.decode(binaryToBytes(atob(padded)));\n}\n\n/** A short, url-safe, random id (9 bytes -> 12 base64url chars) from WebCrypto -\n * used for the announce mailbox id. This repo bans Math.random; getRandomValues\n * is the sanctioned entropy source and exists in every browser and Node 20+. */\nexport function randomId(): string {\n\tconst bytes = new Uint8Array(9);\n\tcrypto.getRandomValues(bytes);\n\treturn bytesToBase64url(bytes);\n}\n\n// --- Structural guards ------------------------------------------------------\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n\treturn typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction isString(v: unknown): v is string {\n\treturn typeof v === 'string';\n}\n\n/** Enforce the format version. A numeric `v` other than 1 is a code from a\n * future/older build (unsupported-version); anything else is garbage. */\nfunction requireV1(v: unknown): void {\n\tif (v === 1) return;\n\tif (typeof v === 'number') {\n\t\tthrow new HouseholdCodeError('unsupported-version', `payload version ${v} is not supported (expected 1)`);\n\t}\n\tthrow malformed('missing or non-numeric version field');\n}\n\n/** Decode base64url -> JSON, mapping any failure to a single 'malformed'. */\nfunction decodeJson(code: string): unknown {\n\tif (typeof code !== 'string' || code.length === 0) throw malformed('code must be a non-empty string');\n\tlet json: string;\n\ttry {\n\t\tjson = base64urlDecode(code);\n\t} catch {\n\t\tthrow malformed('code is not valid base64url');\n\t}\n\ttry {\n\t\treturn JSON.parse(json) as unknown;\n\t} catch {\n\t\tthrow malformed('code payload is not valid JSON');\n\t}\n}\n\n/** Rebuild a CopyLink field by field, so a decoded link carries exactly the\n * known keys (no smuggled extras) and every present field has the right type. */\nexport function toCopyLink(v: unknown): CopyLink {\n\tif (!isRecord(v)) throw malformed('copy link is not an object');\n\tif (v.provider === 'drive') {\n\t\tif (!isString(v.fileId)) throw malformed('drive copy link needs a string fileId');\n\t\tif (v.ownerEmail !== undefined && !isString(v.ownerEmail)) throw malformed('copy link ownerEmail must be a string');\n\t\tif (v.ownerName !== undefined && !isString(v.ownerName)) throw malformed('copy link ownerName must be a string');\n\t\tconst link: DriveCopyLink = { provider: 'drive', fileId: v.fileId };\n\t\tif (isString(v.ownerEmail)) link.ownerEmail = v.ownerEmail;\n\t\tif (isString(v.ownerName)) link.ownerName = v.ownerName;\n\t\treturn link;\n\t}\n\tthrow malformed(`unknown copy-link provider ${JSON.stringify(v.provider)}`);\n}\n\n/** Every entry of a roster array validated as a CopyLink, de-duplicated by\n * fileId (first occurrence wins). An empty roster is structurally valid.\n * Exported for the backend, which revalidates the roster it re-publishes. */\nexport function toRoster(v: unknown): CopyLink[] {\n\tif (!Array.isArray(v)) throw malformed('roster is not an array');\n\tconst seen = new Set<string>();\n\tconst out: CopyLink[] = [];\n\tfor (const entry of v) {\n\t\tconst link = toCopyLink(entry);\n\t\tif (seen.has(link.fileId)) continue;\n\t\tseen.add(link.fileId);\n\t\tout.push(link);\n\t}\n\treturn out;\n}\n\n// --- Codec ------------------------------------------------------------------\n// Transport = base64url(no padding) of compact JSON. Each encoder is a pure,\n// deterministic function of its input; each decoder validates the version and\n// the full structure and throws HouseholdCodeError on anything it cannot trust.\n\nexport function encodeShare(payload: SharePayload): string {\n\treturn base64urlEncode(JSON.stringify(payload));\n}\n\nexport function decodeShare(code: string): SharePayload {\n\tconst raw = decodeJson(code);\n\tif (!isRecord(raw)) throw malformed('share payload is not an object');\n\trequireV1(raw.v);\n\tif (!isString(raw.mailboxId)) throw malformed('share payload needs a string mailboxId');\n\treturn { v: 1, mailboxId: raw.mailboxId, roster: toRoster(raw.roster) };\n}\n\nexport function encodeAnnounce(payload: AnnouncePayload): string {\n\treturn base64urlEncode(JSON.stringify(payload));\n}\n\nexport function decodeAnnounce(code: string): AnnouncePayload {\n\tconst raw = decodeJson(code);\n\tif (!isRecord(raw)) throw malformed('announce is not an object');\n\trequireV1(raw.v);\n\treturn { v: 1, copy: toCopyLink(raw.copy) };\n}\n","// Household sharing, mirror model: read-write while every member's own file\n// stays exactly what its owner chose. Each member publishes a dedicated copy\n// of their file sealed under the link key K (a selfstore mirror, republished\n// after every save that moved data), and attaches the others' copies as\n// read-only peers keyed by K - reading the other's copy is what\n// resynchronizes. A password-protected store refuses to share, honestly.\n//\n// The invitation link carries K in its fragment; possession of K is\n// membership, no per-member identity. Revoking means ending the share (the\n// one real revocation, see leave()); everyone keeps what already converged\n// into their own file - the local-first reality.\n//\n// A device may hold SEVERAL memberships, one per share, each bound to its own\n// wallet file (the `wallet` getter): at most the in-scope one is armed, the\n// others lie dormant. Without the getter (single-wallet hosts) the engine\n// keeps the historical one-membership rule.\n//\n// Injectable by contract (store / kv / backend), framework-free,\n// network-free: every durable side effect goes through store or backend.\n\nimport { randomId, type CopyLink, type SharePayload } from './codec';\nimport type { BackupTarget, PeerSource } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { LocalStore } from '../persistence/store';\n\n// --- Injected abstractions --------------------------------------------------\n\n/** What the bulletin holds once opened: the read-only projection (scrubbed\n * collections, empty when the share is revoked) and the control record. */\nexport interface IncomingShare {\n\tprojection: Record<string, unknown[]>;\n\tshare: SharePayload;\n}\n\n/**\n * The sharing transport, the sole Drive/relay/mailbox-specific seam. A test\n * provides an in-memory fake; the host app provides a real one (e.g. Drive).\n * Everything here carries only public data - file ids and the roster.\n */\nexport interface ShareBackend {\n\t/** Create this member's dedicated copy file (link-readable, empty until the\n\t * mirror publishes into it) and return the link others read it through.\n\t * `existingFileId` reuses a known copy on restore instead of minting a\n\t * second file. */\n\tcreateCopy(existingFileId?: string, shareLabel?: string): Promise<CopyLink>;\n\t/** A write-capable target over a copy file, for the store MIRROR. Never\n\t * adopted as the store's own target - the store's own connection stays what\n\t * it is. */\n\tcopyTarget(fileId: string): BackupTarget;\n\t/** Teardown: make a copy file private and delete it (best-effort). Only\n\t * ever called on MY own copy - the others' files are theirs. */\n\tdropCopy(fileId: string): Promise<void>;\n\t/** Admin: write (or rewrite) the bulletin - the encrypted file the invitation\n\t * link points at, holding the read-only projection plus the control record.\n\t * Idempotent per group; returns where it lives (the link's fileId + key). */\n\tpublishBulletin(key: string, payload: SharePayload): Promise<{ fileId: string; key: string }>;\n\t/** Admin: stop the invitation - the bulletin drops its projection and roster,\n\t * so the link no longer previews or admits. */\n\trevokeBulletin(): Promise<void>;\n\t/** Open an invitation link's bulletin (through the relay) and STASH it for the\n\t * join, returning its content for the read-only preview. Null when the link\n\t * is dead or the key wrong. */\n\topenIncoming(fileId: string, key: string): Promise<IncomingShare | null>;\n\t/** The invitation opened by openIncoming and stashed for the join, or null. */\n\ttakeStashedIncoming(): Promise<{ key: string; fileId: string; content: IncomingShare } | null>;\n\t/** Re-read a joined share's bulletin (member): the freshest roster. The\n\t * engine passes the invitation capability it joined through, so every\n\t * membership rereads ITS bulletin. Null when unreachable or revoked. */\n\trereadJoined(fileId: string, key: string): Promise<SharePayload | null>;\n\t/** Member: announce my copy to the group's mailbox so the admin folds it. */\n\tannounce(mailboxId: string, copy: CopyLink): Promise<void>;\n\t/** Admin: drain the announce mailbox - the copies members published. */\n\ttakeAnnounces(mailboxId: string): Promise<CopyLink[]>;\n\t/** A read-only PeerSource over another member's published copy. */\n\tpeerSource(link: CopyLink): PeerSource;\n}\n\n// --- Persisted group ----------------------------------------------------------\n\n/** Everything needed to re-arm ONE share on reboot. A device may hold several\n * (one per joined silo); they persist together under the single KV key. */\ninterface StoredGroup {\n\trole: 'admin' | 'member';\n\t/** The wallet FILE this share is bound to (captured at start/join when the\n\t * host injects `wallet`). The mirror and peers arm only while it is the\n\t * attached one: another silo's data must never publish into the copy, and\n\t * the shared data must never fold into another silo. Absent = unbound\n\t * (single-wallet hosts), the share follows the store wherever it points. */\n\twalletFileId?: string;\n\t/** The shared group key K (the invitation link's fragment key). */\n\tkey: string;\n\t/** Where members announce their copy (a secret only K-holders read). */\n\tmailboxId: string;\n\t/** Known copies (the admin's own first). */\n\troster: CopyLink[];\n\t/** My own published copy (never the member's own file). */\n\tmyCopy: CopyLink;\n\t/** Admin: where the bulletin lives - what the link carries. Member: the\n\t * invitation opened to join - the roster refresh rereads it. (Absent on\n\t * pre-multi member records: their refresh stays dormant until a re-join.) */\n\tbulletin?: { fileId: string; key: string };\n\t/** Admin only: false once revoked (the bulletin stops previewing/admitting). */\n\tlive?: boolean;\n\t/** Admin only: a roster change could not be rebroadcast (offline mid-fold);\n\t * the next converge republishes even though nothing new was folded. */\n\tneedsRebroadcast?: boolean;\n}\n\n/** The persisted shape: every membership on this device. A lone pre-multi\n * record lifts into a one-entry list on first load. */\ninterface StoredGroups {\n\tv: 2;\n\tgroups: StoredGroup[];\n}\n\n/** The default KV key the group lives under. Exported so a caller can wipe\n * it. Hosts with a pre-existing record under another key pass `storageKey`. */\nexport const HOUSEHOLD_GROUP_KEY = 'selfstore:households:group:v1';\n\n/** Every membership publishes through its own mirror: the id is scoped by the\n * copy file (unique per share) so several never collide on one store. */\nconst mirrorId = (group: StoredGroup): string => `household-copy:${group.myCopy.fileId}`;\n\n// --- Public surface -----------------------------------------------------------\n\n/** One membership, as the UI lists them: which silo carries it and whose\n * share it is. */\nexport interface MembershipInfo {\n\t/** The wallet file this membership is bound to (null = unbound legacy). */\n\twalletFileId: string | null;\n\tisAdmin: boolean;\n\t/** My published copy's file id for this share. */\n\tselfFileId: string;\n\t/** Who shares it with me (the admin's roster label); null for my own share. */\n\tsharedBy: string | null;\n\tmemberCount: number;\n}\n\n/** The reactive-friendly snapshot the UI reads (hosts wrap it in their own\n * reactivity - a Svelte rune, a React store, anything). The singular fields\n * describe the PRIMARY membership (the admin one when it exists, else the\n * first); `memberships` lists them all. */\nexport interface HouseholdGroupState {\n\t/** A share exists on this device (admin or member). */\n\tactive: boolean;\n\tisAdmin: boolean;\n\t/** How many copies are in the roster (this device included). */\n\tmemberCount: number;\n\t/** The roster's cosmetic owner labels, for a \"who has access\" list. */\n\tmembers: { fileId: string; label: string }[];\n\t/** My own copy's file id, so the UI can list everyone ELSE. */\n\tselfFileId: string | null;\n\t/** The wallet file the share is bound to, or null when unbound. The host\n\t * compares it to its own attached-file id to render \"this silo carries the\n\t * share\" and to gate its own publications the same way the engine does. */\n\twalletFileId: string | null;\n\t/** Admin: the live invitation's capability, so the panel can re-show the\n\t * link without republishing anything. */\n\tinviteCapability: { fileId: string; key: string } | null;\n\t/** Every membership on this device, admin and joined alike. */\n\tmemberships: MembershipInfo[];\n}\n\nexport interface HouseholdGroup {\n\treadonly state: HouseholdGroupState;\n\t/** Admin: start sharing. The member's own file is untouched - a dedicated\n\t * copy is created and mirrored under a fresh K, and the invitation\n\t * bulletin is published. Refused on a password-protected store: remove\n\t * the password to share, or keep it and do not. */\n\tstartShare(): Promise<{ fileId: string; key: string }>;\n\t/** Admin: re-show (and refresh) the standing invitation link. Idempotent. */\n\tinvite(): Promise<{ fileId: string; key: string }>;\n\t/** /share: open an invitation link's bulletin and remember it for the join. */\n\topenIncoming(fileId: string, key: string): Promise<IncomingShare | null>;\n\t/** Member: after openIncoming + own store connected, join read-write now.\n\t * Your own file stays yours: a dedicated copy is created and mirrored\n\t * under K, the others' copies attach as keyed peers (the next converge\n\t * folds the shared data into your own file), and my copy is announced.\n\t * Same honest rule as startShare: a password-protected store refuses.\n\t * With a `wallet` getter, one membership per share: joining another share\n\t * ADDS a membership (bound to the attached silo), and a re-join over the\n\t * same silo replaces its stale one. Without the getter, the historical\n\t * single-membership rule holds ('mismatch' on a different share). */\n\tjoin(): Promise<'joined' | 'no-invite' | 'mismatch' | 'error'>;\n\t/** Both: one convergence round (admin folds new announces + rebroadcasts;\n\t * everyone re-arms mirror/peers). Called on every converge. */\n\tsyncGroup(): Promise<void>;\n\t/** End ONE share on this device - and, for the admin, this is the real\n\t * revocation (bearer link: the lock changes for everyone). The bulletin\n\t * is revoked, my copy file is dropped, mirror and peers detach. The\n\t * member's own file is untouched: everyone keeps what already converged into\n\t * their own file - the local-first reality. Re-sharing mints a fresh K\n\t * and a fresh link for whoever should stay. `walletFileId` names WHICH\n\t * membership; omitted, the primary one (admin first) goes. */\n\tleave(walletFileId?: string | null): Promise<void>;\n\t/** On boot: re-arm the mirror and peers of the current share. */\n\trestore(): Promise<void>;\n}\n\n// --- Factory --------------------------------------------------------------------\n\nexport function createHouseholdGroup(deps: {\n\tstore: LocalStore;\n\tkv: KV;\n\tbackend: ShareBackend;\n\t/** KV key the group record lives under. Default HOUSEHOLD_GROUP_KEY. */\n\tstorageKey?: string;\n\t/** The currently attached wallet file id, for multi-backup hosts. When\n\t * given, start/join bind the share to that file and the engine goes\n\t * dormant whenever another one is attached. Omit on single-wallet hosts. */\n\twallet?: () => Promise<string | null>;\n}): HouseholdGroup {\n\tconst { store, kv, backend, wallet } = deps;\n\tconst storageKey = deps.storageKey ?? HOUSEHOLD_GROUP_KEY;\n\n\tlet groups: StoredGroup[] | null = null;\n\tlet loaded = false;\n\t// Member roster mirror: how many consecutive fresh reads a known peer has\n\t// been MISSING from the bulletin. Removals apply only at 2 - a single stale\n\t// relay read must never cut a still-valid member's data off. Keys are\n\t// scoped per share (`mailboxId:fileId`).\n\tconst rosterMisses = new Map<string, number>();\n\n\tasync function load(): Promise<StoredGroup[]> {\n\t\tif (!loaded) {\n\t\t\tconst raw = await kv.get<StoredGroups | StoredGroup>(storageKey);\n\t\t\tif (!raw) groups = [];\n\t\t\telse if ((raw as StoredGroups).v === 2) groups = (raw as StoredGroups).groups;\n\t\t\t// A pre-multi single record lifts into a one-entry list, verbatim.\n\t\t\telse groups = [raw as StoredGroup];\n\t\t\tloaded = true;\n\t\t}\n\t\treturn groups as StoredGroup[];\n\t}\n\n\t/** Write the current list back (the callers mutate `groups` in place). An\n\t * empty list clears the key: no membership, no record. */\n\tasync function persist(): Promise<void> {\n\t\tloaded = true;\n\t\tif (!groups || groups.length === 0) {\n\t\t\tgroups = groups ?? [];\n\t\t\tawait kv.del(storageKey);\n\t\t\treturn;\n\t\t}\n\t\tawait kv.set(storageKey, { v: 2, groups } satisfies StoredGroups);\n\t}\n\n\tconst adminGroup = (list: StoredGroup[]): StoredGroup | null =>\n\t\tlist.find((g) => g.role === 'admin') ?? null;\n\t/** The membership the singular state fields describe: admin first. */\n\tconst primaryGroup = (list: StoredGroup[]): StoredGroup | null =>\n\t\tadminGroup(list) ?? list[0] ?? null;\n\t/** Who shares a joined group with me: the admin's copy leads the roster. */\n\tconst sharedByOf = (g: StoredGroup): string | null =>\n\t\tg.role === 'member' ? (g.roster[0]?.ownerEmail ?? g.roster[0]?.ownerName ?? null) : null;\n\n\t/** A fresh 32-byte key, base64url, for a new share (the link fragment). */\n\tfunction randomKey(): string {\n\t\tconst bytes = crypto.getRandomValues(new Uint8Array(32));\n\t\tlet s = '';\n\t\tfor (const b of bytes) s += String.fromCharCode(b);\n\t\treturn btoa(s).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n\t}\n\n\t/** The honest rule, enforced at both entrances: sharing never re-keys the\n\t * own file, so a password-protected store cannot publish a readable copy\n\t * without lowering the bar its owner chose. Refuse plainly. */\n\tfunction refuseProtected(): void {\n\t\tif (store.state.encrypted) {\n\t\t\tthrow new Error(\n\t\t\t\t'household-group: a password-protected store cannot be shared - remove the password first'\n\t\t\t);\n\t\t}\n\t}\n\n\t/** Whether the share's bound wallet is the attached one. Unbound (or no\n\t * getter) is always in scope - the single-wallet behavior. */\n\tasync function inScope(group: StoredGroup): Promise<boolean> {\n\t\tif (!group.walletFileId || !wallet) return true;\n\t\treturn (await wallet()) === group.walletFileId;\n\t}\n\n\t/** Out-of-scope dormancy: tear down BOTH directions. The mirror would leak\n\t * the attached silo's data into the copy; the peers would fold the shared\n\t * data into that silo. Nothing else moves until the bound wallet is back. */\n\tfunction disarm(group: StoredGroup): void {\n\t\tstore.detachMirror(mirrorId(group));\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId !== group.myCopy.fileId) store.detachPeer(link.fileId);\n\t\t}\n\t}\n\n\t/** Re-arm my copy's mirror (idempotent - safe on every converge/boot). */\n\tfunction ensureMirror(group: StoredGroup): void {\n\t\tconst id = mirrorId(group);\n\t\tif (store.state.mirrors.some((m) => m.id === id)) return;\n\t\tstore.attachMirror(backend.copyTarget(group.myCopy.fileId), {\n\t\t\tpassword: group.key,\n\t\t\tid\n\t\t});\n\t}\n\n\t/** Attach every roster copy that is not mine and not already attached as a\n\t * read-only peer under K (its own key - the member's file has none). Idempotent. */\n\tfunction attachPeers(group: StoredGroup): void {\n\t\tconst already = new Set(store.state.peers.map((p) => p.id));\n\t\tfor (const link of group.roster) {\n\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\tif (already.has(link.fileId)) continue;\n\t\t\tstore.attachPeer(backend.peerSource(link), { id: link.fileId, password: group.key });\n\t\t}\n\t}\n\n\t/** Merge new copies into a roster, de-duplicated by fileId, dropping mine. */\n\tfunction mergeRoster(roster: CopyLink[], incoming: CopyLink[], selfFileId: string): CopyLink[] {\n\t\tconst byId = new Map(roster.map((l) => [l.fileId, l]));\n\t\tfor (const link of incoming) {\n\t\t\tif (link.fileId === selfFileId) continue;\n\t\t\tbyId.set(link.fileId, link);\n\t\t}\n\t\treturn [...byId.values()];\n\t}\n\n\treturn {\n\t\tget state(): HouseholdGroupState {\n\t\t\tconst list = groups ?? [];\n\t\t\tconst memberships: MembershipInfo[] = list.map((g) => ({\n\t\t\t\twalletFileId: g.walletFileId ?? null,\n\t\t\t\tisAdmin: g.role === 'admin',\n\t\t\t\tselfFileId: g.myCopy.fileId,\n\t\t\t\tsharedBy: sharedByOf(g),\n\t\t\t\tmemberCount: g.roster.length\n\t\t\t}));\n\t\t\tconst group = primaryGroup(list);\n\t\t\tif (!group) {\n\t\t\t\treturn {\n\t\t\t\t\tactive: false,\n\t\t\t\t\tisAdmin: false,\n\t\t\t\t\tmemberCount: 0,\n\t\t\t\t\tmembers: [],\n\t\t\t\t\tselfFileId: null,\n\t\t\t\t\twalletFileId: null,\n\t\t\t\t\tinviteCapability: null,\n\t\t\t\t\tmemberships\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tactive: true,\n\t\t\t\tisAdmin: group.role === 'admin',\n\t\t\t\tmemberCount: group.roster.length,\n\t\t\t\tmembers: group.roster.map((l) => ({\n\t\t\t\t\tfileId: l.fileId,\n\t\t\t\t\tlabel: l.ownerEmail ?? l.ownerName ?? l.fileId.slice(0, 8)\n\t\t\t\t})),\n\t\t\t\tselfFileId: group.myCopy.fileId,\n\t\t\t\twalletFileId: group.walletFileId ?? null,\n\t\t\t\tinviteCapability:\n\t\t\t\t\tgroup.role === 'admin' && group.live === true && group.bulletin ? group.bulletin : null,\n\t\t\t\tmemberships\n\t\t\t};\n\t\t},\n\n\t\tasync startShare(): Promise<{ fileId: string; key: string }> {\n\t\t\tconst list = await load();\n\t\t\t// One ADMIN share per device; joined memberships do not block it.\n\t\t\tif (adminGroup(list)) {\n\t\t\t\tthrow new Error('household-group: a share already exists on this device');\n\t\t\t}\n\t\t\trefuseProtected();\n\t\t\tconst key = randomKey();\n\t\t\tconst mailboxId = randomId();\n\t\t\tconst walletFileId = (await wallet?.()) ?? undefined;\n\t\t\t// The copy is a dedicated file - the member's own file is never in the roster.\n\t\t\tconst link = await backend.createCopy();\n\t\t\tconst roster = [link];\n\t\t\t// Persist before the publish: a failure at any later step leaves a\n\t\t\t// recoverable half-share (restore re-arms the mirror, invite() finishes\n\t\t\t// the publication). Nothing here is irreversible for the member's own file.\n\t\t\tconst group: StoredGroup = {\n\t\t\t\trole: 'admin',\n\t\t\t\twalletFileId,\n\t\t\t\tkey,\n\t\t\t\tmailboxId,\n\t\t\t\troster,\n\t\t\t\tmyCopy: link,\n\t\t\t\tlive: false\n\t\t\t};\n\t\t\tlist.push(group);\n\t\t\tawait persist();\n\t\t\t// The mirror fills the copy right away and keeps it fresh after every\n\t\t\t// save/converge that moves data. The member's own file is not touched.\n\t\t\tensureMirror(group);\n\t\t\tconst where = await backend.publishBulletin(key, { v: 1, mailboxId, roster });\n\t\t\tgroup.bulletin = where;\n\t\t\tgroup.live = true;\n\t\t\tawait persist();\n\t\t\treturn where;\n\t\t},\n\n\t\tasync invite(): Promise<{ fileId: string; key: string }> {\n\t\t\tconst group = adminGroup(await load());\n\t\t\tif (!group) {\n\t\t\t\tthrow new Error('household-group: invite() requires an admin share');\n\t\t\t}\n\t\t\t// Refresh the bulletin (fresh projection + current roster) and mark live.\n\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\tv: 1,\n\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\troster: group.roster\n\t\t\t});\n\t\t\tgroup.bulletin = where;\n\t\t\tgroup.live = true;\n\t\t\tawait persist();\n\t\t\treturn where;\n\t\t},\n\n\t\tasync openIncoming(fileId: string, key: string): Promise<IncomingShare | null> {\n\t\t\treturn backend.openIncoming(fileId, key);\n\t\t},\n\n\t\tasync join(): Promise<'joined' | 'no-invite' | 'mismatch' | 'error'> {\n\t\t\tconst list = await load();\n\t\t\tconst incoming = await backend.takeStashedIncoming();\n\t\t\t// No invitation opened: a device already holding a membership answers\n\t\t\t// the historical idempotent 'joined'; a bare one names the dead end.\n\t\t\tif (!incoming) return list.length > 0 ? 'joined' : 'no-invite';\n\t\t\tconst { key, fileId, content } = incoming;\n\t\t\tif (list.some((g) => g.mailboxId === content.share.mailboxId)) return 'joined';\n\t\t\t// Single-wallet hosts (no getter) keep the one-membership rule: an\n\t\t\t// invitation to ANOTHER share is a dead end, not a silent second join.\n\t\t\tif (!wallet && list.length > 0) return 'mismatch';\n\t\t\trefuseProtected();\n\t\t\tconst walletFileId = (await wallet?.()) ?? undefined;\n\t\t\t// A re-join over the same silo replaces its stale membership (a rotated\n\t\t\t// link): two shares must never publish the same wallet.\n\t\t\tif (walletFileId) {\n\t\t\t\tconst stale = list.find((g) => g.walletFileId === walletFileId);\n\t\t\t\tif (stale) {\n\t\t\t\t\tdisarm(stale);\n\t\t\t\t\tawait backend.dropCopy(stale.myCopy.fileId).catch(() => undefined);\n\t\t\t\t\tgroups = list.filter((g) => g !== stale);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// My copy is a dedicated file; my own store stays mine. The shared\n\t\t\t// data arrives by folding the others' copies into it on the next\n\t\t\t// converge - one store per person, converging.\n\t\t\tconst link = await backend.createCopy(undefined, content.share.roster[0]?.ownerEmail);\n\t\t\tconst roster = mergeRoster(content.share.roster, [link], '');\n\t\t\tconst group: StoredGroup = {\n\t\t\t\trole: 'member',\n\t\t\t\twalletFileId,\n\t\t\t\tkey,\n\t\t\t\tmailboxId: content.share.mailboxId,\n\t\t\t\troster,\n\t\t\t\tmyCopy: link,\n\t\t\t\t// The invitation capability, kept so THIS membership rereads ITS\n\t\t\t\t// bulletin on converge (roster removals, lost-announce self-heal).\n\t\t\t\tbulletin: { fileId, key }\n\t\t\t};\n\t\t\t(groups as StoredGroup[]).push(group);\n\t\t\tawait persist();\n\t\t\tensureMirror(group);\n\t\t\tattachPeers(group);\n\t\t\t// Announce my copy so the admin folds it into everyone's roster.\n\t\t\tawait backend.announce(content.share.mailboxId, link).catch(() => undefined);\n\t\t\treturn 'joined';\n\t\t},\n\n\t\tasync syncGroup(): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\tfor (const group of list) {\n\t\t\t\t// Defense in depth for the honest rule: a store that became\n\t\t\t\t// password-protected (or joined from a device whose store turned out\n\t\t\t\t// protected) must never publish a link-readable copy. Tear the mirror\n\t\t\t\t// down if one was armed; the share lies dormant on this device until\n\t\t\t\t// the password is removed. (Folding INBOUND peers would be harmless,\n\t\t\t\t// but a dormant share should read as dormant - arm nothing.)\n\t\t\t\tif (store.state.encrypted) {\n\t\t\t\t\tstore.detachMirror(mirrorId(group));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Another silo is attached: full dormancy, both directions (see disarm).\n\t\t\t\tif (!(await inScope(group))) {\n\t\t\t\t\tdisarm(group);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tensureMirror(group);\n\t\t\t\tif (group.role === 'admin') {\n\t\t\t\t\t// Fold any newly-announced member copies into the roster and rebroadcast.\n\t\t\t\t\tconst announced = await backend.takeAnnounces(group.mailboxId);\n\t\t\t\t\tconst roster = mergeRoster(group.roster, announced, group.myCopy.fileId);\n\t\t\t\t\tconst grew = roster.length !== group.roster.length;\n\t\t\t\t\tconst needed = group.needsRebroadcast === true;\n\t\t\t\t\tgroup.roster = roster;\n\t\t\t\t\tawait persist();\n\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t// Rebroadcast on growth or when a previous rebroadcast failed: keyed\n\t\t\t\t\t// on `grew` alone, an offline publish left the bulletin stale forever\n\t\t\t\t\t// (the announce was already consumed, so no later round re-grew).\n\t\t\t\t\tif ((grew || needed) && group.live === true) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst where = await backend.publishBulletin(group.key, {\n\t\t\t\t\t\t\t\tv: 1,\n\t\t\t\t\t\t\t\tmailboxId: group.mailboxId,\n\t\t\t\t\t\t\t\troster\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tgroup.bulletin = where;\n\t\t\t\t\t\t\tgroup.needsRebroadcast = undefined;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tgroup.needsRebroadcast = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait persist();\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// The admin's bulletin roster is AUTHORITATIVE for members: mirror it\n\t\t\t\t\t// (replace, not merge) so removals propagate, not only newcomers.\n\t\t\t\t\t// Reread through the invitation THIS membership joined on (pre-multi\n\t\t\t\t\t// records carry none: their refresh stays dormant, peers still fold).\n\t\t\t\t\tconst share = group.bulletin\n\t\t\t\t\t\t? await backend.rereadJoined(group.bulletin.fileId, group.bulletin.key)\n\t\t\t\t\t\t: null;\n\t\t\t\t\tif (share) {\n\t\t\t\t\t\tconst byId = new Map(share.roster.map((l) => [l.fileId, l]));\n\t\t\t\t\t\t// SELF-HEAL: my own copy missing from a fresh roster means my\n\t\t\t\t\t\t// announce was lost. Announce again; the admin's fold de-dups.\n\t\t\t\t\t\tif (!byId.has(group.myCopy.fileId)) {\n\t\t\t\t\t\t\tbyId.set(group.myCopy.fileId, group.myCopy);\n\t\t\t\t\t\t\tawait backend.announce(group.mailboxId, group.myCopy).catch(() => undefined);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Removals apply CONSERVATIVELY: a peer drops only after two\n\t\t\t\t\t\t// consecutive fresh reads without it (stale relay tolerance).\n\t\t\t\t\t\tfor (const link of group.roster) {\n\t\t\t\t\t\t\tif (link.fileId === group.myCopy.fileId) continue;\n\t\t\t\t\t\t\tconst missKey = `${group.mailboxId}:${link.fileId}`;\n\t\t\t\t\t\t\tif (byId.has(link.fileId)) {\n\t\t\t\t\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst misses = (rosterMisses.get(missKey) ?? 0) + 1;\n\t\t\t\t\t\t\tif (misses >= 2) {\n\t\t\t\t\t\t\t\trosterMisses.delete(missKey);\n\t\t\t\t\t\t\t\tstore.detachPeer(link.fileId);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\trosterMisses.set(missKey, misses);\n\t\t\t\t\t\t\t\tbyId.set(link.fileId, link);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgroup.roster = [...byId.values()];\n\t\t\t\t\t\tawait persist();\n\t\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tattachPeers(group);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tasync leave(walletFileId?: string | null): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\tconst group =\n\t\t\t\twalletFileId != null\n\t\t\t\t\t? (list.find((g) => g.walletFileId === walletFileId) ?? null)\n\t\t\t\t\t: primaryGroup(list);\n\t\t\tif (!group) return;\n\t\t\t// Detach the plumbing first: nothing publishes or folds anymore.\n\t\t\tdisarm(group);\n\t\t\tif (group.role === 'admin') {\n\t\t\t\ttry {\n\t\t\t\t\tawait backend.revokeBulletin();\n\t\t\t\t} catch {\n\t\t\t\t\t/* tolerable: the bulletin only ever holds K-encrypted data */\n\t\t\t\t}\n\t\t\t}\n\t\t\t// My copy file dies with the membership (private first, then gone).\n\t\t\t// The member's own file is untouched: it never carried the share key, and the\n\t\t\t// data already converged into it stays - local-first honesty.\n\t\t\tawait backend.dropCopy(group.myCopy.fileId).catch(() => undefined);\n\t\t\tgroups = list.filter((g) => g !== group);\n\t\t\tfor (const k of [...rosterMisses.keys()]) {\n\t\t\t\tif (k.startsWith(`${group.mailboxId}:`)) rosterMisses.delete(k);\n\t\t\t}\n\t\t\tawait persist();\n\t\t},\n\n\t\tasync restore(): Promise<void> {\n\t\t\tconst list = await load();\n\t\t\t// Same dormancy rules as syncGroup: a protected store never mirrors,\n\t\t\t// and a share bound to another silo arms nothing while it is away.\n\t\t\tif (store.state.encrypted) return;\n\t\t\tfor (const group of list) {\n\t\t\t\tif (!(await inScope(group))) continue;\n\t\t\t\tensureMirror(group);\n\t\t\t\tattachPeers(group);\n\t\t\t}\n\t\t}\n\t};\n}\n"]}
@@ -0,0 +1,205 @@
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-ChIGEOph.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-ChIGEOph.js';
3
+ import { L as LocalCache, K as KV } from './cache-De5_sALA.js';
4
+ import { SyncConfig } from './sync.js';
5
+ import { D as DriveAuth, W as WebdavConfig, S as S3Config } from './drive-Bv94UM8b.js';
6
+ export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-2py_vGGy.js';
7
+
8
+ declare const BACKUP_EXTENSION = ".zip";
9
+ declare const BACKUP_MIME = "application/zip";
10
+
11
+ /** Start writing a backup. Naming the app via .as() is the required next step. */
12
+ declare function backup(snapshot: Snapshot): BackupDraft;
13
+ /** Start reading a backup (a picked File, a fetched Blob, or raw bytes). */
14
+ declare function restore(input: Blob | Uint8Array): RestoreBuilder;
15
+ declare class BackupDraft {
16
+ private readonly snapshot;
17
+ constructor(snapshot: Snapshot);
18
+ /** Name the writing app (stored cleartext in the backup's metadata). */
19
+ as(app: string, appVersion?: string): BackupBuilder;
20
+ }
21
+ declare class BackupBuilder {
22
+ protected readonly snapshot: Snapshot;
23
+ protected readonly opts: EncodeOptions;
24
+ constructor(snapshot: Snapshot, opts: EncodeOptions);
25
+ /** Encrypt the backup (AES-256-GCM over an Argon2id-derived key). */
26
+ encryptedWith(password: string): EncryptedBackupBuilder;
27
+ /** The backup as raw bytes. */
28
+ toBytes(): Promise<Uint8Array>;
29
+ /** The backup as a Blob, ready to upload or hand to a save dialog. */
30
+ toBlob(): Promise<Blob>;
31
+ /** Save the backup to disk (File System Access API, else a download).
32
+ * Browser-only terminal; defaults to `<app>-<date>.zip`. */
33
+ toDisk(filename?: string): Promise<void>;
34
+ }
35
+ declare class EncryptedBackupBuilder extends BackupBuilder {
36
+ constructor(snapshot: Snapshot, opts: EncodeOptions);
37
+ /** Brand the README shipped inside the encrypted ZIP. */
38
+ withReadme(text: string): EncryptedBackupBuilder;
39
+ }
40
+ declare class RestoreBuilder {
41
+ private readonly input;
42
+ private password?;
43
+ constructor(input: Blob | Uint8Array);
44
+ /** Accepts undefined so callers can pass straight through from an optional field. */
45
+ withPassword(password?: string): this;
46
+ /** The cleartext metadata (app, date, encryption), without decrypting. */
47
+ meta(): Promise<Header>;
48
+ /** True if reading will require a password. */
49
+ isEncrypted(): Promise<boolean>;
50
+ /** Read the backup into a snapshot. Reserved `__*` collections (library
51
+ * bookkeeping) are stripped. PASSWORD_REQUIRED when encrypted and no
52
+ * password given, DECRYPT_FAILED on a wrong one. */
53
+ read(): Promise<Snapshot>;
54
+ }
55
+
56
+ /** A record as the simple store sees it: plain JSON with a string id (the id
57
+ * field name can be remapped per collection via `sync.ids`). */
58
+ type SimpleRecord = Record<string, unknown>;
59
+ /** Unlock callback for the ultra-sensitive cacheLock mode: return the session
60
+ * secret - a password (Argon2id-derived) or an app-provided key (e.g. a passkey
61
+ * PRF result). Called at boot; called again with { failed: true } after a wrong
62
+ * secret so the app can re-prompt. Throw to abort the boot (user cancelled). */
63
+ type CacheUnlock = (attempt: {
64
+ failed: boolean;
65
+ }) => Promise<string | CryptoKey>;
66
+ /** Options - every one of them optional, the defaults carry a real app. */
67
+ interface SimpleOptions {
68
+ /** Your DATA schema version (default 1). Bump it together with `migrate`. */
69
+ schema?: number;
70
+ /** Upgrade a snapshot written by an older schema version. */
71
+ migrate?: (fromVersion: number, snap: Snapshot) => Snapshot;
72
+ /** Per-collection merge tuning (id field mapping, strategies). The default -
73
+ * records keyed by `id`, later edit wins, deletes propagate - fits most apps. */
74
+ sync?: SyncConfig;
75
+ /** Google Drive auth, when this app uses Drive: providing it here lets a
76
+ * connected Drive backup restore itself on the next start. */
77
+ drive?: DriveAuth;
78
+ /** Where the working copy lives (default: IndexedDB named after the app;
79
+ * in-memory when IndexedDB does not exist, e.g. tests or SSR). */
80
+ cache?: LocalCache;
81
+ /** Auto-save debounce in ms. */
82
+ debounceMs?: number;
83
+ /** Cross-tab coordination (default on in browsers). */
84
+ multiTab?: boolean;
85
+ /** Refuse to ever write or export a plaintext backup: connecting a
86
+ * destination then demands a password (or a group). Off by default. */
87
+ requireEncryption?: boolean;
88
+ /** Reject a backup password weaker than this policy (length, character
89
+ * classes) at protect()/addKey time. Preview the rules with
90
+ * checkPasswordPolicy for a live UI hint. Omit for no policy. */
91
+ passwordPolicy?: PasswordPolicy;
92
+ /** Ultra-sensitive mode: seal the local cache under a key derived from a
93
+ * secret held in memory (this callback), never on disk - so a copy of the
94
+ * browser profile cannot read it. One unlock per session; branch it on the
95
+ * app's existing login to leave the UX unchanged. Browser (IndexedDB) only. */
96
+ cacheLock?: CacheUnlock;
97
+ /** Wire tab focus / network return / interval / tab hide automatically
98
+ * (default true in browsers). Pass false to drive syncing yourself. */
99
+ autoSync?: boolean;
100
+ }
101
+ /** What connecting a destination did. 'merged': it already held a backup and
102
+ * both sides were folded together. 'started': it was empty, this device's data
103
+ * is now its content. 'manual': non-Chromium file fallback - offer
104
+ * downloadBackup() instead. 'cancelled': the user closed the picker/consent. */
105
+ type ConnectOutcome = 'merged' | 'started' | 'manual' | 'cancelled';
106
+ interface SimpleStore<S extends Record<string, SimpleRecord> = Record<string, SimpleRecord>> {
107
+ /** Every record of a collection (treat as read-only; write via put/remove). */
108
+ all<K extends keyof S & string>(collection: K): readonly S[K][];
109
+ /** One record by id, or undefined. */
110
+ get<K extends keyof S & string>(collection: K, id: string): S[K] | undefined;
111
+ /** Insert or replace one record (auto-saves, debounced). Throws a TypeError
112
+ * when the record has no non-empty string id. */
113
+ put<K extends keyof S & string>(collection: K, record: S[K]): Promise<void>;
114
+ /** Insert or replace many records in one save. */
115
+ putAll<K extends keyof S & string>(collection: K, records: S[K][]): Promise<void>;
116
+ /** Delete one record by id (propagates to other devices). Unknown id: no-op. */
117
+ remove<K extends keyof S & string>(collection: K, id: string): Promise<void>;
118
+ /** Empty a collection (every removal propagates). */
119
+ clear<K extends keyof S & string>(collection: K): Promise<void>;
120
+ /** Called after any data change: your writes, another tab, another device,
121
+ * a restore. Returns an unsubscribe. */
122
+ onChange(fn: () => void): () => void;
123
+ /** Connect Google Drive as the durable home. An existing backup there is
124
+ * MERGED with this device (multi-device); an encrypted one needs its
125
+ * password up front (PASSWORD_REQUIRED is thrown before anything changes). */
126
+ connectDrive(auth: DriveAuth, opts?: {
127
+ password?: string;
128
+ }): Promise<ConnectOutcome>;
129
+ /** Connect a disk file (File System Access; falls back to manual download). */
130
+ connectFile(opts?: {
131
+ password?: string;
132
+ }): Promise<ConnectOutcome>;
133
+ /** Connect a WebDAV server (Nextcloud, ownCloud, your own). */
134
+ connectWebdav(config: WebdavConfig, opts?: {
135
+ password?: string;
136
+ }): Promise<ConnectOutcome>;
137
+ /** Connect an S3-compatible bucket you control (Amazon S3, R2, B2, MinIO). */
138
+ connectS3(config: S3Config, opts?: {
139
+ password?: string;
140
+ }): Promise<ConnectOutcome>;
141
+ /** Connect any custom BackupTarget with the same merge semantics. */
142
+ connectTarget(target: BackupTarget, opts?: {
143
+ password?: string;
144
+ }): Promise<ConnectOutcome>;
145
+ /** Back to device-only (the destination keeps its last backup). */
146
+ disconnect(): Promise<void>;
147
+ /** Also write the same encrypted backup to this second destination on
148
+ * every save. A broken copy never gates the store (state.replicas). */
149
+ addReplica(target: BackupTarget, opts?: {
150
+ id?: string;
151
+ }): string;
152
+ /** Stop writing the copy (the destination keeps its last backup). */
153
+ removeReplica(id: string): void;
154
+ /** Encrypt the durable backup end to end with this password (reversible). */
155
+ protect(password: string): Promise<void>;
156
+ /** Remove the backup password. */
157
+ unprotect(): Promise<void>;
158
+ /** status.action === 'unlock': supply the password to resume. */
159
+ unlock(password: string): Promise<boolean>;
160
+ /** status.action === 'reconnect': re-run the destination's auth gesture. */
161
+ reconnect(): Promise<boolean>;
162
+ /** The portable backup file (a real ZIP; encrypted when protect() is on). */
163
+ exportBackup(): Promise<Blob>;
164
+ /** Download the backup (also resolves the 'manual' file mode's pending flag). */
165
+ downloadBackup(filename?: string): Promise<void>;
166
+ /** Load a backup file into this store, replacing the local data (removals
167
+ * propagate like edits). Throws PASSWORD_REQUIRED / DECRYPT_FAILED. */
168
+ importBackup(file: Blob | Uint8Array, opts?: {
169
+ password?: string;
170
+ }): Promise<void>;
171
+ /** Headless status: { state, severity, action, labelKey } - map labelKey to
172
+ * your own copy. */
173
+ readonly status: StatusDescriptor;
174
+ /** The last problem: { code, labelKey, message } - show labelKey, log message. */
175
+ readonly error: StoreError | null;
176
+ /** The full underlying state (advanced reads: journal, peers, mode...). */
177
+ readonly state: LocalStoreState;
178
+ /** Fires on any state change (status flips included) - for framework bindings. */
179
+ subscribe(fn: () => void): () => void;
180
+ /** Save now (called for you on tab hide when autoSync is on). */
181
+ flush(): Promise<void>;
182
+ /** Converge with the destination now (a user gesture like pull-to-refresh). */
183
+ sync(): Promise<void>;
184
+ /** Drop timers and listeners (tests, SPA teardown). */
185
+ dispose(): void;
186
+ /** The escape hatch: the full advanced store this simple one is built on -
187
+ * peers, groups, custom strategies, everything (see selfstore/advanced). */
188
+ readonly advanced: LocalStore;
189
+ /** The attachment point for 'selfstore/flows': the engine plus the cache KV
190
+ * and backup file name a flow needs to build destination targets. Apps on
191
+ * the advanced store hand a flow the same three by themselves. */
192
+ readonly flowHost: {
193
+ engine: LocalStore;
194
+ kv: KV;
195
+ backupName: string;
196
+ };
197
+ }
198
+ /**
199
+ * Open (or create) the app's local store. Awaits the initial load, so the
200
+ * returned store is ready: data readable, destination restored, first converge
201
+ * done. See SimpleOptions for the defaults.
202
+ */
203
+ declare function selfstore<S extends Record<string, SimpleRecord> = Record<string, SimpleRecord>>(app: string, options?: SimpleOptions): Promise<SimpleStore<S>>;
204
+
205
+ export { BACKUP_EXTENSION, BACKUP_MIME, BackupBuilder, BackupDraft, type CacheUnlock, type ConnectOutcome, DriveAuth, EncryptedBackupBuilder, Header, LocalStore, LocalStoreState, PasswordPolicy, RestoreBuilder, S3Config, type SimpleOptions, type SimpleRecord, type SimpleStore, Snapshot, StatusDescriptor, StoreError, SyncConfig, WebdavConfig, backup, restore, selfstore };
package/dist/index.js ADDED
@@ -0,0 +1,249 @@
1
+ import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-N4RPMX3Y.js';
2
+ export { gisDriveAuth } from './chunk-N4RPMX3Y.js';
3
+ import { saveToDisk } from './chunk-BJ3RUMGI.js';
4
+ export { pickFromDisk, saveToDisk } from './chunk-BJ3RUMGI.js';
5
+ import './chunk-IBIEKK36.js';
6
+ import { fromSession, fromSession2, fromSession3, connect4, connect3, isSupported, connect2, connect } from './chunk-CUWB5EWF.js';
7
+ import { restore } from './chunk-JB4JHM7V.js';
8
+ export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-JB4JHM7V.js';
9
+ import './chunk-OWLX6PNT.js';
10
+ import './chunk-QBCU7OOP.js';
11
+ import './chunk-FDPAKHQS.js';
12
+ import { SelfstoreError } from './chunk-VBFMMR7Q.js';
13
+ export { SelfstoreError, errorLabelKey, isSelfstoreError } from './chunk-VBFMMR7Q.js';
14
+
15
+ // src/simple/simple.ts
16
+ function idFieldOf(sync, collection) {
17
+ const mapped = sync?.ids?.[collection];
18
+ if (mapped == null) return "id";
19
+ return mapped.includes(".") ? null : mapped;
20
+ }
21
+ function requireStringId(record, field, collection) {
22
+ if (field === null) return "";
23
+ const id = record[field];
24
+ if (typeof id !== "string" || id.length === 0) {
25
+ throw new TypeError(
26
+ `selfstore: a record in "${collection}" needs a non-empty STRING "${field}" (got ${JSON.stringify(id)}). String ids are what the multi-device merge keys on; map another field with { sync: { ids: { ${collection}: 'yourField' } } }.`
27
+ );
28
+ }
29
+ return id;
30
+ }
31
+ function hasIndexedDb() {
32
+ return typeof indexedDB !== "undefined";
33
+ }
34
+ async function selfstore(app, options = {}) {
35
+ if (typeof app !== "string" || app.length === 0) {
36
+ throw new TypeError('selfstore: pass your app name, e.g. selfstore("my-app").');
37
+ }
38
+ let collections = {};
39
+ let files = [];
40
+ const dataSubs = /* @__PURE__ */ new Set();
41
+ const emitData = () => dataSubs.forEach((fn) => fn());
42
+ const cache = options.cache ?? (hasIndexedDb() ? indexedDbCache(app, options.cacheLock ? { lock: true } : void 0) : memoryCache());
43
+ const backupName = `${app}.zip`;
44
+ let driveAuth = options.drive ?? null;
45
+ if (options.cacheLock && isLockableCache(cache)) {
46
+ let failed = false;
47
+ while (!await cache.unlock(await options.cacheLock({ failed }))) failed = true;
48
+ }
49
+ const store = createLocalStore({
50
+ app,
51
+ schemaVersion: options.schema ?? 1,
52
+ gather: () => structuredClone({ collections, files }),
53
+ apply: (snap) => {
54
+ collections = snap.collections ?? {};
55
+ files = snap.files ?? [];
56
+ emitData();
57
+ },
58
+ migrate: options.migrate,
59
+ sync: options.sync,
60
+ cache,
61
+ debounceMs: options.debounceMs,
62
+ multiTab: options.multiTab,
63
+ requireEncryption: options.requireEncryption,
64
+ passwordPolicy: options.passwordPolicy,
65
+ restoreTarget: async (kind) => {
66
+ if (kind === "drive" && driveAuth) {
67
+ return fromSession({ auth: driveAuth, kv: cache.kv, fileName: backupName });
68
+ }
69
+ if (kind === "file") return fromSession2({ kv: cache.kv });
70
+ if (kind === "webdav") return fromSession3({ kv: cache.kv });
71
+ return null;
72
+ }
73
+ });
74
+ await store.init();
75
+ const teardown = [];
76
+ if (options.autoSync !== false && typeof document !== "undefined" && typeof window !== "undefined") {
77
+ const onVisibility = () => {
78
+ if (document.visibilityState === "hidden") void store.flush();
79
+ else void store.syncIfStale("focus");
80
+ };
81
+ const onOnline = () => void store.syncIfStale("online");
82
+ const onPageHide = () => void store.flush();
83
+ const interval = setInterval(() => {
84
+ if (document.visibilityState === "visible") void store.syncIfStale("interval");
85
+ }, 5 * 6e4);
86
+ document.addEventListener("visibilitychange", onVisibility);
87
+ window.addEventListener("online", onOnline);
88
+ window.addEventListener("pagehide", onPageHide);
89
+ teardown.push(() => {
90
+ clearInterval(interval);
91
+ document.removeEventListener("visibilitychange", onVisibility);
92
+ window.removeEventListener("online", onOnline);
93
+ window.removeEventListener("pagehide", onPageHide);
94
+ });
95
+ }
96
+ function mutate(collection, next) {
97
+ collections = { ...collections, [collection]: next };
98
+ emitData();
99
+ store.schedule();
100
+ return Promise.resolve();
101
+ }
102
+ async function connectTarget(target, opts = {}) {
103
+ const info = await store.inspectTarget(target);
104
+ if (info.hasBackup) {
105
+ if (info.encrypted && !opts.password) {
106
+ throw new SelfstoreError(
107
+ "PASSWORD_REQUIRED",
108
+ "This destination holds an encrypted backup: pass its password to connect."
109
+ );
110
+ }
111
+ await store.attachTarget(target, { password: opts.password ?? null, strategy: "merge" });
112
+ return "merged";
113
+ }
114
+ await store.attachTarget(target, { password: opts.password ?? null, strategy: "replace-remote" });
115
+ return "started";
116
+ }
117
+ const simple = {
118
+ all(collection) {
119
+ return collections[collection] ?? [];
120
+ },
121
+ get(collection, id) {
122
+ const field = idFieldOf(options.sync, collection) ?? "id";
123
+ return (collections[collection] ?? []).find((r) => r[field] === id);
124
+ },
125
+ put(collection, record) {
126
+ const field = idFieldOf(options.sync, collection);
127
+ const id = requireStringId(record, field, collection);
128
+ const rows = collections[collection] ?? [];
129
+ const at = field === null ? -1 : rows.findIndex((r) => r[field] === id);
130
+ return mutate(
131
+ collection,
132
+ at === -1 ? [...rows, record] : rows.map((r, i) => i === at ? record : r)
133
+ );
134
+ },
135
+ putAll(collection, records) {
136
+ const field = idFieldOf(options.sync, collection);
137
+ const byId = /* @__PURE__ */ new Map();
138
+ for (const r of records) requireStringId(r, field, collection);
139
+ const rows = [...collections[collection] ?? []];
140
+ if (field !== null) {
141
+ rows.forEach((r, i) => byId.set(r[field], i));
142
+ for (const r of records) {
143
+ const at = byId.get(r[field]);
144
+ if (typeof at === "number") rows[at] = r;
145
+ else rows.push(r);
146
+ }
147
+ } else {
148
+ rows.push(...records);
149
+ }
150
+ return mutate(collection, rows);
151
+ },
152
+ remove(collection, id) {
153
+ const field = idFieldOf(options.sync, collection) ?? "id";
154
+ const rows = collections[collection] ?? [];
155
+ const next = rows.filter((r) => r[field] !== id);
156
+ return next.length === rows.length ? Promise.resolve() : mutate(collection, next);
157
+ },
158
+ clear(collection) {
159
+ if ((collections[collection] ?? []).length === 0) return Promise.resolve();
160
+ return mutate(collection, []);
161
+ },
162
+ onChange(fn) {
163
+ dataSubs.add(fn);
164
+ return () => dataSubs.delete(fn);
165
+ },
166
+ async connectDrive(auth, opts) {
167
+ driveAuth = auth;
168
+ const target = await connect({ auth, kv: cache.kv, fileName: backupName });
169
+ if (!target) return "cancelled";
170
+ return connectTarget(target, opts);
171
+ },
172
+ async connectFile(opts) {
173
+ if (!isSupported()) {
174
+ await store.setManualFile();
175
+ return "manual";
176
+ }
177
+ const target = await connect2({ kv: cache.kv, fileName: backupName });
178
+ if (!target) return "cancelled";
179
+ return connectTarget(target, opts);
180
+ },
181
+ async connectWebdav(config, opts) {
182
+ const target = await connect3({ kv: cache.kv, config });
183
+ if (!target) {
184
+ throw new SelfstoreError(
185
+ "TARGET_UNAVAILABLE",
186
+ "The WebDAV server did not answer (URL, credentials or CORS)."
187
+ );
188
+ }
189
+ return connectTarget(target, opts);
190
+ },
191
+ async connectS3(config, opts) {
192
+ const target = await connect4({ kv: cache.kv, config });
193
+ if (!target) {
194
+ throw new SelfstoreError(
195
+ "TARGET_UNAVAILABLE",
196
+ "The S3 endpoint did not answer (endpoint, credentials or CORS)."
197
+ );
198
+ }
199
+ return connectTarget(target, opts);
200
+ },
201
+ connectTarget,
202
+ disconnect: () => store.detachTarget(),
203
+ addReplica: (target, opts) => store.attachReplica(target, opts),
204
+ removeReplica: (id) => store.detachReplica(id),
205
+ protect: (password) => store.setEncryption(password),
206
+ unprotect: () => store.setEncryption(null),
207
+ unlock: (password) => store.unlock(password),
208
+ reconnect: () => store.reconnect(),
209
+ exportBackup: () => store.exportBlob(),
210
+ async downloadBackup(filename) {
211
+ await saveToDisk(await store.exportBlob(), filename ?? backupName);
212
+ store.markDownloaded();
213
+ },
214
+ async importBackup(file, opts) {
215
+ const reader = restore(file);
216
+ const snap = opts?.password ? await reader.withPassword(opts.password).read() : await reader.read();
217
+ collections = snap.collections ?? {};
218
+ files = snap.files ?? [];
219
+ emitData();
220
+ store.schedule();
221
+ },
222
+ get status() {
223
+ return store.state.status;
224
+ },
225
+ get error() {
226
+ return store.state.lastError;
227
+ },
228
+ get state() {
229
+ return store.state;
230
+ },
231
+ subscribe: (fn) => store.subscribe(fn),
232
+ flush: () => store.flush(),
233
+ async sync() {
234
+ await store.syncNow();
235
+ },
236
+ dispose() {
237
+ teardown.forEach((fn) => fn());
238
+ teardown.length = 0;
239
+ store.dispose();
240
+ },
241
+ advanced: store,
242
+ flowHost: { engine: store, kv: cache.kv, backupName }
243
+ };
244
+ return simple;
245
+ }
246
+
247
+ export { selfstore };
248
+ //# sourceMappingURL=index.js.map
249
+ //# sourceMappingURL=index.js.map