selfstore 1.8.2 → 1.8.4

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 (57) hide show
  1. package/README.md +1 -1
  2. package/dist/advanced.d.ts +8 -4
  3. package/dist/advanced.js +14 -13
  4. package/dist/advanced.js.map +1 -1
  5. package/dist/backups.d.ts +31 -179
  6. package/dist/backups.js +40 -3
  7. package/dist/backups.js.map +1 -1
  8. package/dist/{cache-lock-RCN3JJ4Q.js → cache-lock-KWJUORTS.js} +3 -4
  9. package/dist/cache-lock-KWJUORTS.js.map +1 -0
  10. package/dist/{chunk-HK4VPO65.js → chunk-3OBJOMQ2.js} +28 -4
  11. package/dist/chunk-3OBJOMQ2.js.map +1 -0
  12. package/dist/{chunk-HDR75TQ7.js → chunk-DCEXWSM6.js} +3 -3
  13. package/dist/{chunk-HDR75TQ7.js.map → chunk-DCEXWSM6.js.map} +1 -1
  14. package/dist/{chunk-7N2YR3QW.js → chunk-ELDSPUTS.js} +7 -8
  15. package/dist/chunk-ELDSPUTS.js.map +1 -0
  16. package/dist/chunk-J5MFXJU6.js +310 -0
  17. package/dist/chunk-J5MFXJU6.js.map +1 -0
  18. package/dist/{chunk-BNDFMU3J.js → chunk-MGDXR3RB.js} +5 -6
  19. package/dist/chunk-MGDXR3RB.js.map +1 -0
  20. package/dist/{chunk-453KRCMM.js → chunk-O3Y6FORN.js} +3 -4
  21. package/dist/chunk-O3Y6FORN.js.map +1 -0
  22. package/dist/{chunk-JMJZIZKY.js → chunk-PNX7AJYO.js} +13 -12
  23. package/dist/chunk-PNX7AJYO.js.map +1 -0
  24. package/dist/{chunk-5NS7YQNP.js → chunk-TKWU52LK.js} +26 -312
  25. package/dist/chunk-TKWU52LK.js.map +1 -0
  26. package/dist/{chunk-U5F4GI7C.js → chunk-W22V5CRM.js} +3 -3
  27. package/dist/{chunk-U5F4GI7C.js.map → chunk-W22V5CRM.js.map} +1 -1
  28. package/dist/{drive-l_4-m1eT.d.ts → drive-CTYvGg_7.d.ts} +23 -110
  29. package/dist/{flows-C_uookek.d.ts → flows-1EntUZ91.d.ts} +2 -1
  30. package/dist/flows.d.ts +3 -2
  31. package/dist/flows.js +16 -16
  32. package/dist/flows.js.map +1 -1
  33. package/dist/groups.js +3 -4
  34. package/dist/groups.js.map +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.js +18 -18
  37. package/dist/index.js.map +1 -1
  38. package/dist/{local-bfv4Ld7s.d.ts → local-C52j3DeH.d.ts} +1 -1
  39. package/dist/local-DPS35F5C.js +6 -0
  40. package/dist/{local-2YREU4Q2.js.map → local-DPS35F5C.js.map} +1 -1
  41. package/dist/manager-DZ1c_aSS.d.ts +181 -0
  42. package/dist/s3-DvsnA-FK.d.ts +112 -0
  43. package/dist/widgets.d.ts +90 -7
  44. package/dist/widgets.js +281 -18
  45. package/dist/widgets.js.map +1 -1
  46. package/llms.txt +13 -2
  47. package/package.json +1 -1
  48. package/dist/cache-lock-RCN3JJ4Q.js.map +0 -1
  49. package/dist/chunk-453KRCMM.js.map +0 -1
  50. package/dist/chunk-5NS7YQNP.js.map +0 -1
  51. package/dist/chunk-7N2YR3QW.js.map +0 -1
  52. package/dist/chunk-BNDFMU3J.js.map +0 -1
  53. package/dist/chunk-CPN2KUD5.js +0 -29
  54. package/dist/chunk-CPN2KUD5.js.map +0 -1
  55. package/dist/chunk-HK4VPO65.js.map +0 -1
  56. package/dist/chunk-JMJZIZKY.js.map +0 -1
  57. package/dist/local-2YREU4Q2.js +0 -7
@@ -1,114 +1,6 @@
1
- import { B as BackupTarget, P as PeerSource } from './store-BiQ-BuVl.js';
1
+ import { B as BackupTarget } from './store-BiQ-BuVl.js';
2
2
  import { K as KV } from './cache-ilsK3Tki.js';
3
3
 
4
- /**
5
- * WebDAV backup target: the store re-writes one .selfstore file on a WebDAV
6
- * server the USER controls (Nextcloud, ownCloud, a plain WebDAV mount...). No
7
- * Google, no vendor broker - the browser talks straight to the server the user
8
- * owns. The bytes are the same end-to-end-encrypted .selfstore, so the server
9
- * only ever holds ciphertext.
10
- *
11
- * Two things the environment must allow, both documented for the user:
12
- * - the server must send CORS for the app origin (methods PUT/GET/HEAD and the
13
- * Authorization header), since browsers block cross-origin WebDAV otherwise;
14
- * - the app's CSP connect-src must permit the server host.
15
- *
16
- * Credentials are stored ENCRYPTED at rest under a per-device non-extractable
17
- * key, so a set-and-forget connection never leaves a cleartext password in
18
- * IndexedDB. The version marker prefers ETag, falling back to Last-Modified
19
- * (the latter is CORS-safelisted, so it is readable without extra server config).
20
- */
21
-
22
- interface WebdavConfig {
23
- /** Full URL of the backup file, e.g. https://cloud.me/remote.php/dav/files/me/backup.zip */
24
- url: string;
25
- username: string;
26
- password: string;
27
- }
28
- interface WebdavConnectOptions {
29
- /** Where the (encrypted) server config is persisted across sessions. */
30
- kv: KV;
31
- config: WebdavConfig;
32
- }
33
- interface WebdavPeerOptions {
34
- /** URL of ANOTHER member's published copy, read-only: a Nextcloud/ownCloud
35
- * read-only share link, or a file URL they granted you read access to. */
36
- url: string;
37
- /** Optional Basic-auth credentials, when the read-only share needs them.
38
- * Omit for a public/tokenised share link (a Nextcloud `/s/<token>/download`
39
- * URL) that needs none. Held in memory only - a peer is re-attached each
40
- * session (see attachPeer), so nothing is persisted here. */
41
- username?: string;
42
- password?: string;
43
- /** Display label (defaults to the URL host). */
44
- label?: string;
45
- }
46
- /** A read-only WebDAV peer: another member's published copy, fetched over a
47
- * shared URL, for `store.attachPeer`. It is the read half of a WebDAV target
48
- * (GET + HEAD only, never a write), so a group can share read-write without
49
- * Google Drive - each member publishes to their own WebDAV and shares a
50
- * read-only link. Failures follow the peer protocol: a 401/403 is a genuine
51
- * loss of read access (AuthExpiredError), 404 means "not published yet"
52
- * (null), anything else is transient. */
53
- declare function peer(opts: WebdavPeerOptions): PeerSource;
54
- /** Validate + persist the config, returning the target, or null when the server
55
- * is unreachable, rejects the credentials, or blocks the cross-origin request. */
56
- declare function connect$2(opts: WebdavConnectOptions): Promise<BackupTarget | null>;
57
- /** Rebuild the target from the encrypted config persisted last session, or null. */
58
- declare function fromSession$2(opts: {
59
- kv: KV;
60
- }): Promise<BackupTarget | null>;
61
-
62
- /**
63
- * S3 backup target: the store re-writes one object on an S3-compatible bucket
64
- * the USER controls - Amazon S3, Cloudflare R2, Backblaze B2, MinIO, any server
65
- * that speaks the S3 REST API and SigV4. No vendor broker: the browser signs
66
- * each request itself and talks straight to the endpoint. The bytes are the
67
- * same end-to-end-encrypted .selfstore, so the bucket only ever holds
68
- * ciphertext.
69
- *
70
- * Two things the environment must allow, both documented for the user:
71
- * - the bucket must send CORS for the app origin: allowed methods GET/PUT/HEAD,
72
- * allowed headers authorization/x-amz-*, and ExposeHeaders ETag (so the
73
- * version marker is readable), or browsers block the cross-origin request;
74
- * - the app's CSP connect-src must permit the endpoint host.
75
- *
76
- * Credentials (the secret access key) are stored ENCRYPTED at rest under a
77
- * per-device non-extractable key, exactly like the WebDAV target, so a
78
- * set-and-forget connection never leaves a cleartext secret in IndexedDB.
79
- *
80
- * Path style is the default: the object lives at endpoint/bucket/key, which
81
- * every S3-compatible server accepts and needs no per-bucket DNS. Set
82
- * forcePathStyle:false for AWS virtual-hosted style (bucket as a host label).
83
- */
84
-
85
- interface S3Config {
86
- /** Endpoint origin, e.g. https://s3.eu-west-3.amazonaws.com,
87
- * https://<account>.r2.cloudflarestorage.com, https://minio.example.com. */
88
- endpoint: string;
89
- /** Signing region ("eu-west-3"; R2 uses "auto"; MinIO any configured value). */
90
- region: string;
91
- bucket: string;
92
- /** Object key (the backup file), e.g. backups/app.selfstore. */
93
- key: string;
94
- accessKeyId: string;
95
- secretAccessKey: string;
96
- /** endpoint/bucket/key (default) vs bucket.endpoint/key. Default true. */
97
- forcePathStyle?: boolean;
98
- }
99
- interface S3ConnectOptions {
100
- /** Where the (encrypted) config is persisted across sessions. */
101
- kv: KV;
102
- config: S3Config;
103
- }
104
- /** Validate + persist the config, returning the target, or null when the bucket
105
- * is unreachable, rejects the credentials, or blocks the cross-origin request. */
106
- declare function connect$1(opts: S3ConnectOptions): Promise<BackupTarget | null>;
107
- /** Rebuild the target from the encrypted config persisted last session, or null. */
108
- declare function fromSession$1(opts: {
109
- kv: KV;
110
- }): Promise<BackupTarget | null>;
111
-
112
4
  /**
113
5
  * Google Drive backup target. Does the Drive REST file operations (find-or-create,
114
6
  * upload, download) for a single backup file. It only ships bytes; the store
@@ -174,6 +66,27 @@ declare function findOrCreateOwnFile(opts: DriveOptions): Promise<{
174
66
  fileId: string;
175
67
  created: boolean;
176
68
  } | null>;
69
+ /** Who a destination is held by, as far as it will say. */
70
+ interface DriveAccount {
71
+ /** The account's address, when Drive reports one. */
72
+ email: string | null;
73
+ /** Its display name, when Drive reports one. */
74
+ name: string | null;
75
+ }
76
+ /** Which Google account the current session belongs to.
77
+ *
78
+ * A destination named "Google Drive" is not an address: several accounts look
79
+ * alike, and a user who connected the wrong one has no way to tell from a
80
+ * brand name alone. This is the one call that lets a surface say WHICH.
81
+ *
82
+ * Cheap (one metadata poke, allowed by the `drive.file` scope) but not free:
83
+ * the answer only changes when the session does, so a caller should ask on
84
+ * connect and on reconnect - not on every render. A destination that will not
85
+ * say answers nulls rather than throwing; a genuinely lost session throws
86
+ * AuthExpiredError like every other call here. */
87
+ declare function account(opts: {
88
+ auth: DriveAuth;
89
+ }): Promise<DriveAccount>;
177
90
  /** One row of `listBackups()`: what Drive reports about a file the app can
178
91
  * see. Whether the backup is encrypted is not here on purpose - that lives
179
92
  * inside the file (inspect it, or remember it app-side); a listing must stay
@@ -224,4 +137,4 @@ declare function renameBackup(opts: {
224
137
  fileName: string;
225
138
  }): Promise<void>;
226
139
 
227
- export { type DriveAuth as D, type S3Config as S, type WebdavConfig as W, adopt as a, findOrCreateOwnFile as b, connect as c, createBackup as d, deleteBackup as e, fromSession as f, connect$1 as g, fromSession$1 as h, connect$2 as i, fromSession$2 as j, peer as k, listBackups as l, type DriveBackupInfo as m, type DriveOptions as n, type S3ConnectOptions as o, preview as p, type WebdavConnectOptions as q, renameBackup as r, type WebdavPeerOptions as s };
140
+ export { type DriveOptions as D, adopt as a, findOrCreateOwnFile as b, connect as c, createBackup as d, deleteBackup as e, fromSession as f, account as g, type DriveAccount as h, type DriveAuth as i, type DriveBackupInfo as j, listBackups as l, preview as p, renameBackup as r };
@@ -1,6 +1,7 @@
1
1
  import { t as StoreError, B as BackupTarget, L as LocalStore, k as ReplicaState } from './store-BiQ-BuVl.js';
2
2
  import { K as KV } from './cache-ilsK3Tki.js';
3
- import { W as WebdavConfig, S as S3Config, D as DriveAuth } from './drive-l_4-m1eT.js';
3
+ import { i as DriveAuth } from './drive-CTYvGg_7.js';
4
+ import { W as WebdavConfig, S as S3Config } from './s3-DvsnA-FK.js';
4
5
  import { F as FlowStore } from './machine-ALMkT3mk.js';
5
6
 
6
7
  type ConnectKind = 'drive' | 'file' | 'webdav' | 's3';
package/dist/flows.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export { c as ConnectFlow, a as ConnectFlowOptions, h as ConnectFlowOutcome, b as ConnectKind, i as ConnectResolution, j as ConnectSnapshot, k as ConnectStep, C as ConnectTargets, l as Connector, F as FlowHost, J as JoinEngine, g as JoinFlow, m as JoinOutcome, n as JoinPreview, o as JoinSnapshot, p as JoinStep, q as REPLICA_ID, R as ReplicaFlow, r as ReplicaFlowOptions, s as ReplicaSnapshot, t as ReplicaStep, u as ShareBusy, d as ShareEngine, f as ShareFlow, e as ShareLevel, v as ShareLinkInfo, w as ShareMemberInfo, x as ShareSnapshot, y as connectFlow, z as joinFlow, A as replicaFlow, B as shareFlow } from './flows-C_uookek.js';
1
+ export { c as ConnectFlow, a as ConnectFlowOptions, h as ConnectFlowOutcome, b as ConnectKind, i as ConnectResolution, j as ConnectSnapshot, k as ConnectStep, C as ConnectTargets, l as Connector, F as FlowHost, J as JoinEngine, g as JoinFlow, m as JoinOutcome, n as JoinPreview, o as JoinSnapshot, p as JoinStep, q as REPLICA_ID, R as ReplicaFlow, r as ReplicaFlowOptions, s as ReplicaSnapshot, t as ReplicaStep, u as ShareBusy, d as ShareEngine, f as ShareFlow, e as ShareLevel, v as ShareLinkInfo, w as ShareMemberInfo, x as ShareSnapshot, y as connectFlow, z as joinFlow, A as replicaFlow, B as shareFlow } from './flows-1EntUZ91.js';
2
2
  export { F as FlowStore, w as withDeadline } from './machine-ALMkT3mk.js';
3
3
  import './store-BiQ-BuVl.js';
4
4
  import './cache-ilsK3Tki.js';
5
5
  import './sync.js';
6
- import './drive-l_4-m1eT.js';
6
+ import './drive-CTYvGg_7.js';
7
+ import './s3-DvsnA-FK.js';
package/dist/flows.js CHANGED
@@ -1,12 +1,12 @@
1
- export { connectFlow, joinFlow, shareFlow } from './chunk-JMJZIZKY.js';
2
- import { connect, connect2, connect4, connect3, fromSession, fromSession2, fromSession3, fromSession4 } from './chunk-5NS7YQNP.js';
3
- import './chunk-BNDFMU3J.js';
4
- import { makeMachine, toStoreError } from './chunk-U5F4GI7C.js';
5
- export { withDeadline } from './chunk-U5F4GI7C.js';
6
- import './chunk-453KRCMM.js';
1
+ export { connectFlow, joinFlow, shareFlow } from './chunk-PNX7AJYO.js';
2
+ import { connect as connect$1, connect3, connect2, fromSession as fromSession$1, fromSession2, fromSession3 } from './chunk-TKWU52LK.js';
3
+ import { makeMachine, toStoreError } from './chunk-W22V5CRM.js';
4
+ export { withDeadline } from './chunk-W22V5CRM.js';
5
+ import { connect, fromSession } from './chunk-J5MFXJU6.js';
6
+ import './chunk-MGDXR3RB.js';
7
7
  import './chunk-QBCU7OOP.js';
8
- import './chunk-HK4VPO65.js';
9
- import './chunk-CPN2KUD5.js';
8
+ import './chunk-O3Y6FORN.js';
9
+ import './chunk-3OBJOMQ2.js';
10
10
 
11
11
  // src/flows/replica.ts
12
12
  var REPLICA_ID = "replica";
@@ -50,14 +50,14 @@ function replicaFlow(store, targets, options = {}) {
50
50
  return () => connect({ auth, kv: skv, fileName });
51
51
  }
52
52
  if (kind === "file") {
53
- return () => connect2({ kv: skv, fileName });
53
+ return () => connect$1({ kv: skv, fileName });
54
54
  }
55
55
  if (kind === "s3") {
56
56
  const config2 = spec;
57
- return () => connect4({ kv: skv, config: config2 });
57
+ return () => connect3({ kv: skv, config: config2 });
58
58
  }
59
59
  const config = spec;
60
- return () => connect3({ kv: skv, config });
60
+ return () => connect2({ kv: skv, config });
61
61
  }
62
62
  async function run(kind, connector) {
63
63
  m.set({ busy: true, error: null });
@@ -82,9 +82,9 @@ function replicaFlow(store, targets, options = {}) {
82
82
  if (spec == null || typeof spec === "function") return null;
83
83
  return fromSession({ auth: spec, kv: skv, fileName });
84
84
  }
85
- if (kind === "file") return fromSession2({ kv: skv });
86
- if (kind === "webdav") return fromSession3({ kv: skv });
87
- return fromSession4({ kv: skv });
85
+ if (kind === "file") return fromSession$1({ kv: skv });
86
+ if (kind === "webdav") return fromSession2({ kv: skv });
87
+ return fromSession3({ kv: skv });
88
88
  }
89
89
  return {
90
90
  get snapshot() {
@@ -115,11 +115,11 @@ function replicaFlow(store, targets, options = {}) {
115
115
  },
116
116
  submitWebdav(config) {
117
117
  if (m.snapshot.busy) return;
118
- void run("webdav", () => connect3({ kv: skv, config }));
118
+ void run("webdav", () => connect2({ kv: skv, config }));
119
119
  },
120
120
  submitS3(config) {
121
121
  if (m.snapshot.busy) return;
122
- void run("s3", () => connect4({ kv: skv, config }));
122
+ void run("s3", () => connect3({ kv: skv, config }));
123
123
  },
124
124
  async remove() {
125
125
  const target = attached;
package/dist/flows.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/flows/replica.ts"],"names":["connect","config","fromSession"],"mappings":"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","file":"flows.js","sourcesContent":["// The backup-copy journey: attach ONE replica (a second destination receiving\n// the same encrypted backup as the primary home) behind a small state machine\n// the widgets can render. Reuses the connect flow's target specs, but there is\n// no password step, no conflict step and no deadline: a replica carries the\n// primary's bytes as-is, and every connector may contain a human gesture.\n\nimport type { BackupTarget } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { ReplicaState, StoreError } from '../persistence/store';\nimport { makeMachine, toStoreError, type FlowStore } from './machine';\nimport type { ConnectKind, ConnectTargets, Connector, StoreLike } from './connect';\nimport {\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\ttype DriveAuth\n} from '../persistence/targets/drive';\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\ttype WebdavConfig\n} from '../persistence/targets/webdav';\nimport {\n\tconnect as s3Connect,\n\tfromSession as s3FromSession,\n\ttype S3Config\n} from '../persistence/targets/s3';\n\n/** The one replica this flow manages, as its id on the engine. */\nexport const REPLICA_ID = 'replica';\n\n// The replica's target session lives in the SAME kv as the primary's, so every\n// key is prefixed: a second driveTarget (say) must never read or clobber the\n// primary connection's session keys.\nconst SCOPE = 'replica:';\nconst RECORD_KEY = 'record';\n\n/** What survives a reboot: which provider the copy lives on. The target's own\n * session (file id, handle, server config) is persisted by the target module\n * itself, under the scoped kv. */\ninterface ReplicaRecord {\n\tkind: ConnectKind;\n}\n\nexport type ReplicaStep = 'idle' | 'choose' | 'form-webdav' | 'form-s3';\n\nexport interface ReplicaSnapshot {\n\tstep: ReplicaStep;\n\t/** The kinds the host offers, in ConnectTargets order. */\n\tkinds: ConnectKind[];\n\tbusy: boolean;\n\terror: StoreError | null;\n\t/** The live engine entry (label, lastPublishAt, lastError), null when none. */\n\treplica: ReplicaState | null;\n}\n\nexport interface ReplicaFlowOptions {\n\t/** File name of the copy on its destination. Default: the primary backup\n\t * name with `.replica` before the extension (\"App.zip\" -> \"App.replica.zip\"),\n\t * which a same-account Drive never confuses with the primary file and the\n\t * backups panel never lists as a backup of its own. */\n\tfileName?: string;\n\t/** Rebuild the target of a custom Connector kind on restore() (built-in\n\t * kinds rebuild by themselves). Without it, custom kinds are not restored. */\n\trestoreTarget?: (kind: ConnectKind) => Promise<BackupTarget | null>;\n}\n\nexport interface ReplicaFlow extends FlowStore<ReplicaSnapshot> {\n\t/** Show the destination choice (from the user's gesture). */\n\topen(): void;\n\t/** Back to idle; never an error. */\n\tcancel(): void;\n\t/** Run a destination (inside the user's click - pickers and consent popups\n\t * open from it). `webdav: true` / `s3: true` route to their form step. */\n\tpick(kind: ConnectKind): void;\n\t/** 'form-webdav' step: submit the server config. */\n\tsubmitWebdav(config: WebdavConfig): void;\n\t/** 'form-s3' step: submit the bucket config. */\n\tsubmitS3(config: S3Config): void;\n\t/** Detach the copy and forget its session. Never deletes remote data. */\n\tremove(): Promise<void>;\n\t/** Silent reboot: re-attach the copy recorded by a past session, if any.\n\t * Never throws - an unreachable destination retries at the next boot. */\n\trestore(): Promise<void>;\n\t/** Detach from the engine (the replica itself stays attached). */\n\tdispose(): void;\n}\n\nfunction scopedKv(kv: KV, prefix: string): KV {\n\treturn {\n\t\tget: <T = unknown>(key: string) => kv.get<T>(prefix + key),\n\t\tset: (key: string, value: unknown) => kv.set(prefix + key, value),\n\t\tdel: (key: string) => kv.del(prefix + key)\n\t};\n}\n\nfunction replicaFileName(backupName: string): string {\n\tconst dot = backupName.lastIndexOf('.');\n\treturn dot > 0\n\t\t? `${backupName.slice(0, dot)}.replica${backupName.slice(dot)}`\n\t\t: `${backupName}.replica`;\n}\n\nexport function replicaFlow(\n\tstore: StoreLike,\n\ttargets: ConnectTargets,\n\toptions: ReplicaFlowOptions = {}\n): ReplicaFlow {\n\tconst host = 'flowHost' in store ? store.flowHost : store;\n\tconst engine = host.engine;\n\tconst skv = scopedKv(host.kv, SCOPE);\n\tconst fileName = options.fileName ?? replicaFileName(host.backupName);\n\tconst kinds = (Object.keys(targets) as ConnectKind[]).filter(\n\t\t(k) => (k === 'drive' || k === 'file' || k === 'webdav' || k === 's3') && targets[k] != null\n\t);\n\n\tconst current = (): ReplicaState | null =>\n\t\tengine.state.replicas.find((r) => r.id === REPLICA_ID) ?? null;\n\n\tconst m = makeMachine<ReplicaSnapshot>({\n\t\tstep: 'idle',\n\t\tkinds,\n\t\tbusy: false,\n\t\terror: null,\n\t\treplica: current()\n\t});\n\n\t// Follow the engine: every publish (or publish failure) refreshes the entry.\n\tconst unsub = engine.subscribe(() => m.set({ replica: current() }));\n\n\t// Held for remove(): disconnect() lets the target forget its own session.\n\tlet attached: BackupTarget | null = null;\n\n\tfunction connectorFor(kind: ConnectKind): Connector | null {\n\t\tconst spec = targets[kind];\n\t\tif (spec == null) return null;\n\t\tif (typeof spec === 'function') return spec;\n\t\tif (kind === 'drive') {\n\t\t\tconst auth = spec as DriveAuth;\n\t\t\treturn () => driveConnect({ auth, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') {\n\t\t\treturn () => fileConnect({ kv: skv, fileName });\n\t\t}\n\t\tif (kind === 's3') {\n\t\t\tconst config = spec as S3Config; // 's3: true' goes through the form step\n\t\t\treturn () => s3Connect({ kv: skv, config });\n\t\t}\n\t\tconst config = spec as WebdavConfig; // 'webdav: true' goes through the form step\n\t\treturn () => webdavConnect({ kv: skv, config });\n\t}\n\n\tasync function run(kind: ConnectKind, connector: Connector): Promise<void> {\n\t\tm.set({ busy: true, error: null });\n\t\ttry {\n\t\t\tconst target = await connector();\n\t\t\tif (!target) {\n\t\t\t\t// Cancelled picker/consent: stay where the user was, no error.\n\t\t\t\tm.set({ busy: false });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID); // swap, never a dup-id throw\n\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\tattached = target;\n\t\t\tawait skv.set(RECORD_KEY, { kind } satisfies ReplicaRecord);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t} catch (e) {\n\t\t\tm.set({ busy: false, error: toStoreError(e) });\n\t\t}\n\t}\n\n\tasync function builtinFromSession(kind: ConnectKind): Promise<BackupTarget | null> {\n\t\tif (kind === 'drive') {\n\t\t\tconst spec = targets.drive;\n\t\t\tif (spec == null || typeof spec === 'function') return null;\n\t\t\treturn driveFromSession({ auth: spec, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') return fileFromSession({ kv: skv });\n\t\tif (kind === 'webdav') return webdavFromSession({ kv: skv });\n\t\treturn s3FromSession({ kv: skv });\n\t}\n\n\treturn {\n\t\tget snapshot() {\n\t\t\treturn m.snapshot;\n\t\t},\n\t\tsubscribe: m.subscribe,\n\n\t\topen() {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tm.set({ step: 'choose', error: null });\n\t\t},\n\n\t\tcancel() {\n\t\t\tm.set({ step: 'idle', busy: false, error: null });\n\t\t},\n\n\t\tpick(kind) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tconst spec = targets[kind];\n\t\t\tif (spec == null) return;\n\t\t\tif (kind === 'webdav' && spec === true) {\n\t\t\t\tm.set({ step: 'form-webdav', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kind === 's3' && spec === true) {\n\t\t\t\tm.set({ step: 'form-s3', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst connector = connectorFor(kind);\n\t\t\tif (connector) void run(kind, connector);\n\t\t},\n\n\t\tsubmitWebdav(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('webdav', () => webdavConnect({ kv: skv, config }));\n\t\t},\n\n\t\tsubmitS3(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('s3', () => s3Connect({ kv: skv, config }));\n\t\t},\n\n\t\tasync remove() {\n\t\t\tconst target = attached;\n\t\t\tattached = null;\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID);\n\t\t\tawait skv.del(RECORD_KEY);\n\t\t\t// Forget locally only - a BackupTarget's disconnect never deletes\n\t\t\t// remote data, so the copy stays where it is for a manual recovery.\n\t\t\tawait target?.disconnect().catch(() => undefined);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t},\n\n\t\tasync restore() {\n\t\t\ttry {\n\t\t\t\tif (current()) return;\n\t\t\t\tconst record = await skv.get<ReplicaRecord>(RECORD_KEY);\n\t\t\t\tif (!record) return;\n\t\t\t\tconst target =\n\t\t\t\t\t(await options.restoreTarget?.(record.kind)) ?? (await builtinFromSession(record.kind));\n\t\t\t\tif (!target) return; // unreachable or custom kind: retry next boot\n\t\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\t\tattached = target;\n\t\t\t\tm.set({ replica: current() });\n\t\t\t} catch {\n\t\t\t\t// Silent reboot by contract: the record stays, the next boot retries.\n\t\t\t}\n\t\t},\n\n\t\tdispose() {\n\t\t\tunsub();\n\t\t}\n\t};\n}\n"]}
1
+ {"version":3,"sources":["../src/flows/replica.ts"],"names":["connect","config","fromSession"],"mappings":"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","file":"flows.js","sourcesContent":["// The backup-copy journey: attach ONE replica (a second destination receiving\n// the same encrypted backup as the primary home) behind a small state machine\n// the widgets can render. Reuses the connect flow's target specs, but there is\n// no password step, no conflict step and no deadline: a replica carries the\n// primary's bytes as-is, and every connector may contain a human gesture.\n\nimport type { BackupTarget } from '../persistence/target';\nimport type { KV } from '../persistence/cache';\nimport type { ReplicaState, StoreError } from '../persistence/store';\nimport { makeMachine, toStoreError, type FlowStore } from './machine';\nimport type { ConnectKind, ConnectTargets, Connector, StoreLike } from './connect';\nimport {\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\ttype DriveAuth\n} from '../persistence/targets/drive';\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\ttype WebdavConfig\n} from '../persistence/targets/webdav';\nimport {\n\tconnect as s3Connect,\n\tfromSession as s3FromSession,\n\ttype S3Config\n} from '../persistence/targets/s3';\n\n/** The one replica this flow manages, as its id on the engine. */\nexport const REPLICA_ID = 'replica';\n\n// The replica's target session lives in the SAME kv as the primary's, so every\n// key is prefixed: a second driveTarget (say) must never read or clobber the\n// primary connection's session keys.\nconst SCOPE = 'replica:';\nconst RECORD_KEY = 'record';\n\n/** What survives a reboot: which provider the copy lives on. The target's own\n * session (file id, handle, server config) is persisted by the target module\n * itself, under the scoped kv. */\ninterface ReplicaRecord {\n\tkind: ConnectKind;\n}\n\nexport type ReplicaStep = 'idle' | 'choose' | 'form-webdav' | 'form-s3';\n\nexport interface ReplicaSnapshot {\n\tstep: ReplicaStep;\n\t/** The kinds the host offers, in ConnectTargets order. */\n\tkinds: ConnectKind[];\n\tbusy: boolean;\n\terror: StoreError | null;\n\t/** The live engine entry (label, lastPublishAt, lastError), null when none. */\n\treplica: ReplicaState | null;\n}\n\nexport interface ReplicaFlowOptions {\n\t/** File name of the copy on its destination. Default: the primary backup\n\t * name with `.replica` before the extension (\"App.zip\" -> \"App.replica.zip\"),\n\t * which a same-account Drive never confuses with the primary file and the\n\t * backups panel never lists as a backup of its own. */\n\tfileName?: string;\n\t/** Rebuild the target of a custom Connector kind on restore() (built-in\n\t * kinds rebuild by themselves). Without it, custom kinds are not restored. */\n\trestoreTarget?: (kind: ConnectKind) => Promise<BackupTarget | null>;\n}\n\nexport interface ReplicaFlow extends FlowStore<ReplicaSnapshot> {\n\t/** Show the destination choice (from the user's gesture). */\n\topen(): void;\n\t/** Back to idle; never an error. */\n\tcancel(): void;\n\t/** Run a destination (inside the user's click - pickers and consent popups\n\t * open from it). `webdav: true` / `s3: true` route to their form step. */\n\tpick(kind: ConnectKind): void;\n\t/** 'form-webdav' step: submit the server config. */\n\tsubmitWebdav(config: WebdavConfig): void;\n\t/** 'form-s3' step: submit the bucket config. */\n\tsubmitS3(config: S3Config): void;\n\t/** Detach the copy and forget its session. Never deletes remote data. */\n\tremove(): Promise<void>;\n\t/** Silent reboot: re-attach the copy recorded by a past session, if any.\n\t * Never throws - an unreachable destination retries at the next boot. */\n\trestore(): Promise<void>;\n\t/** Detach from the engine (the replica itself stays attached). */\n\tdispose(): void;\n}\n\nfunction scopedKv(kv: KV, prefix: string): KV {\n\treturn {\n\t\tget: <T = unknown>(key: string) => kv.get<T>(prefix + key),\n\t\tset: (key: string, value: unknown) => kv.set(prefix + key, value),\n\t\tdel: (key: string) => kv.del(prefix + key)\n\t};\n}\n\nfunction replicaFileName(backupName: string): string {\n\tconst dot = backupName.lastIndexOf('.');\n\treturn dot > 0\n\t\t? `${backupName.slice(0, dot)}.replica${backupName.slice(dot)}`\n\t\t: `${backupName}.replica`;\n}\n\nexport function replicaFlow(\n\tstore: StoreLike,\n\ttargets: ConnectTargets,\n\toptions: ReplicaFlowOptions = {}\n): ReplicaFlow {\n\tconst host = 'flowHost' in store ? store.flowHost : store;\n\tconst engine = host.engine;\n\tconst skv = scopedKv(host.kv, SCOPE);\n\tconst fileName = options.fileName ?? replicaFileName(host.backupName);\n\tconst kinds = (Object.keys(targets) as ConnectKind[]).filter(\n\t\t(k) => (k === 'drive' || k === 'file' || k === 'webdav' || k === 's3') && targets[k] != null\n\t);\n\n\tconst current = (): ReplicaState | null =>\n\t\tengine.state.replicas.find((r) => r.id === REPLICA_ID) ?? null;\n\n\tconst m = makeMachine<ReplicaSnapshot>({\n\t\tstep: 'idle',\n\t\tkinds,\n\t\tbusy: false,\n\t\terror: null,\n\t\treplica: current()\n\t});\n\n\t// Follow the engine: every publish (or publish failure) refreshes the entry.\n\tconst unsub = engine.subscribe(() => m.set({ replica: current() }));\n\n\t// Held for remove(): disconnect() lets the target forget its own session.\n\tlet attached: BackupTarget | null = null;\n\n\tfunction connectorFor(kind: ConnectKind): Connector | null {\n\t\tconst spec = targets[kind];\n\t\tif (spec == null) return null;\n\t\tif (typeof spec === 'function') return spec;\n\t\tif (kind === 'drive') {\n\t\t\tconst auth = spec as DriveAuth;\n\t\t\treturn () => driveConnect({ auth, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') {\n\t\t\treturn () => fileConnect({ kv: skv, fileName });\n\t\t}\n\t\tif (kind === 's3') {\n\t\t\tconst config = spec as S3Config; // 's3: true' goes through the form step\n\t\t\treturn () => s3Connect({ kv: skv, config });\n\t\t}\n\t\tconst config = spec as WebdavConfig; // 'webdav: true' goes through the form step\n\t\treturn () => webdavConnect({ kv: skv, config });\n\t}\n\n\tasync function run(kind: ConnectKind, connector: Connector): Promise<void> {\n\t\tm.set({ busy: true, error: null });\n\t\ttry {\n\t\t\tconst target = await connector();\n\t\t\tif (!target) {\n\t\t\t\t// Cancelled picker/consent: stay where the user was, no error.\n\t\t\t\tm.set({ busy: false });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID); // swap, never a dup-id throw\n\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\tattached = target;\n\t\t\tawait skv.set(RECORD_KEY, { kind } satisfies ReplicaRecord);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t} catch (e) {\n\t\t\tm.set({ busy: false, error: toStoreError(e) });\n\t\t}\n\t}\n\n\tasync function builtinFromSession(kind: ConnectKind): Promise<BackupTarget | null> {\n\t\tif (kind === 'drive') {\n\t\t\tconst spec = targets.drive;\n\t\t\tif (spec == null || typeof spec === 'function') return null;\n\t\t\treturn driveFromSession({ auth: spec, kv: skv, fileName });\n\t\t}\n\t\tif (kind === 'file') return fileFromSession({ kv: skv });\n\t\tif (kind === 'webdav') return webdavFromSession({ kv: skv });\n\t\treturn s3FromSession({ kv: skv });\n\t}\n\n\treturn {\n\t\tget snapshot() {\n\t\t\treturn m.snapshot;\n\t\t},\n\t\tsubscribe: m.subscribe,\n\n\t\topen() {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tm.set({ step: 'choose', error: null });\n\t\t},\n\n\t\tcancel() {\n\t\t\tm.set({ step: 'idle', busy: false, error: null });\n\t\t},\n\n\t\tpick(kind) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tconst spec = targets[kind];\n\t\t\tif (spec == null) return;\n\t\t\tif (kind === 'webdav' && spec === true) {\n\t\t\t\tm.set({ step: 'form-webdav', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kind === 's3' && spec === true) {\n\t\t\t\tm.set({ step: 'form-s3', error: null });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst connector = connectorFor(kind);\n\t\t\tif (connector) void run(kind, connector);\n\t\t},\n\n\t\tsubmitWebdav(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('webdav', () => webdavConnect({ kv: skv, config }));\n\t\t},\n\n\t\tsubmitS3(config) {\n\t\t\tif (m.snapshot.busy) return;\n\t\t\tvoid run('s3', () => s3Connect({ kv: skv, config }));\n\t\t},\n\n\t\tasync remove() {\n\t\t\tconst target = attached;\n\t\t\tattached = null;\n\t\t\tif (current()) engine.detachReplica(REPLICA_ID);\n\t\t\tawait skv.del(RECORD_KEY);\n\t\t\t// Forget locally only - a BackupTarget's disconnect never deletes\n\t\t\t// remote data, so the copy stays where it is for a manual recovery.\n\t\t\tawait target?.disconnect().catch(() => undefined);\n\t\t\tm.set({ step: 'idle', busy: false, error: null, replica: current() });\n\t\t},\n\n\t\tasync restore() {\n\t\t\ttry {\n\t\t\t\tif (current()) return;\n\t\t\t\tconst record = await skv.get<ReplicaRecord>(RECORD_KEY);\n\t\t\t\tif (!record) return;\n\t\t\t\tconst target =\n\t\t\t\t\t(await options.restoreTarget?.(record.kind)) ?? (await builtinFromSession(record.kind));\n\t\t\t\tif (!target) return; // unreachable or custom kind: retry next boot\n\t\t\t\tengine.attachReplica(target, { id: REPLICA_ID });\n\t\t\t\tattached = target;\n\t\t\t\tm.set({ replica: current() });\n\t\t\t} catch {\n\t\t\t\t// Silent reboot by contract: the record stays, the next boot retries.\n\t\t\t}\n\t\t},\n\n\t\tdispose() {\n\t\t\tunsub();\n\t\t}\n\t};\n}\n"]}
package/dist/groups.js CHANGED
@@ -1,8 +1,7 @@
1
- import { generateIdentity } from './chunk-453KRCMM.js';
2
- export { GROUP_KEYING, generateIdentity, groupCryptoAvailable, keyId, newGroupId, openManifest, publicIdentity, signManifest } from './chunk-453KRCMM.js';
3
1
  import { seal, isEnvelope, unseal, newDeviceKey } from './chunk-QBCU7OOP.js';
4
- import { assertKdfBounds, deriveKey, freshKdfParams } from './chunk-HK4VPO65.js';
5
- import { SelfstoreError } from './chunk-CPN2KUD5.js';
2
+ import { generateIdentity } from './chunk-O3Y6FORN.js';
3
+ export { GROUP_KEYING, generateIdentity, groupCryptoAvailable, keyId, newGroupId, openManifest, publicIdentity, signManifest } from './chunk-O3Y6FORN.js';
4
+ import { SelfstoreError, assertKdfBounds, deriveKey, freshKdfParams } from './chunk-3OBJOMQ2.js';
6
5
 
7
6
  // src/persistence/identity.ts
8
7
  var VAULT_KEY = "groupVaultKey";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/persistence/identity.ts"],"names":[],"mappings":"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","file":"groups.js","sourcesContent":["// At-rest storage for a member identity (passwordless groups). Private keys\n// are sealed under a non-extractable device key in the same kv - defeats disk\n// forensics and partial exfiltration, not code running in the origin or a\n// copy of the whole profile (THREAT-MODEL.md).\n//\n// The opt-in passphrase lock reseals the identity under Argon2id(passphrase)\n// instead, closing the profile-copy gap at the cost of an unlock prompt. The\n// vault keeps no unlocked session state: unlock() returns the identity, the\n// caller holds it in memory, and at rest it stays sealed. A bare envelope\n// keeps meaning device mode, so pre-lock vaults load unchanged.\n//\n// The identity is one person's; move it between their devices over a trusted\n// channel (QR shown locally, encrypted note) - never through the group's\n// shared storage.\n\nimport { seal, unseal, isEnvelope, newDeviceKey, type EncEnvelope } from './cache-crypto';\nimport type { KV } from './cache';\nimport { generateIdentity, type GroupIdentity } from '../selfstore/group';\nimport { deriveKey, freshKdfParams, assertKdfBounds } from '../selfstore/crypto';\nimport type { KdfParams } from '../selfstore/types';\nimport { SelfstoreError } from '../selfstore/errors';\n\nconst VAULT_KEY = 'groupVaultKey';\nconst IDENTITY_KEY = 'groupIdentity';\n\nconst utf8 = { enc: new TextEncoder(), dec: new TextDecoder() };\n\n// Local kv bookkeeping only - not part of the interchange format (SPEC.md).\ninterface VaultLock {\n\tselfstoreVaultLock: 1;\n\tkdf: KdfParams;\n\tenvelope: EncEnvelope;\n}\n\nfunction isVaultLock(v: unknown): v is VaultLock {\n\treturn (\n\t\ttypeof v === 'object' &&\n\t\tv !== null &&\n\t\t(v as { selfstoreVaultLock?: unknown }).selfstoreVaultLock === 1\n\t);\n}\n\nconst encode = (identity: GroupIdentity): Uint8Array => utf8.enc.encode(JSON.stringify(identity));\nconst decode = (bytes: Uint8Array): GroupIdentity =>\n\tJSON.parse(utf8.dec.decode(bytes)) as GroupIdentity;\n\nexport interface IdentityVault {\n\t/** The stored identity, or null when none exists yet. On a protected\n\t * vault this throws PASSWORD_REQUIRED rather than returning null - a\n\t * locked identity exists, and null would invite loadOrCreate to mint a\n\t * second one over it. */\n\tload(): Promise<GroupIdentity | null>;\n\t/** Seal and store an identity, overwriting any previous one. Throws\n\t * PASSWORD_REQUIRED on a protected vault: a locked identity is never\n\t * silently replaced (unprotect() or clear() first). */\n\tsave(identity: GroupIdentity): Promise<void>;\n\t/** load(), creating and storing a fresh identity when absent. Same\n\t * PASSWORD_REQUIRED rule as load(). */\n\tloadOrCreate(): Promise<GroupIdentity>;\n\t/** Forget the identity on this device. Allowed even when protected -\n\t * forgetting leaks nothing, and it is the honest lost-passphrase escape\n\t * hatch (re-import from another device, or get re-invited). */\n\tclear(): Promise<void>;\n\tisProtected(): Promise<boolean>;\n\t/** Decrypt a protected vault and return the identity. Stateless: the\n\t * caller holds it in memory, the stored value stays sealed. Wrong\n\t * passphrase and tampered blob are one DECRYPT_FAILED; hostile KDF\n\t * params are refused before any derivation. */\n\tunlock(passphrase: string): Promise<GroupIdentity>;\n\t/** Reseal the identity under Argon2id(passphrase); an empty vault gets a\n\t * fresh identity, created-and-protected in one step. Changing the\n\t * passphrase is unprotect(old) then protect(new) - an already-protected\n\t * vault throws PASSWORD_REQUIRED, never a silent rekey. */\n\tprotect(passphrase: string): Promise<void>;\n\t/** Unlock with `passphrase`, then reseal under the device key. A wrong\n\t * passphrase throws DECRYPT_FAILED and leaves the vault untouched. */\n\tunprotect(passphrase: string): Promise<void>;\n}\n\n/** An identity vault over the app's kv (e.g. `indexedDbCache(app).kv`). */\nexport function identityVault(kv: KV): IdentityVault {\n\tasync function vaultKey(): Promise<CryptoKey> {\n\t\tconst existing = await kv.get<CryptoKey>(VAULT_KEY);\n\t\tif (existing) return existing;\n\t\tconst fresh = await newDeviceKey();\n\t\tawait kv.set(VAULT_KEY, fresh);\n\t\treturn fresh;\n\t}\n\n\t/** Device mode's write path: a bare envelope under the device key. */\n\tasync function sealDevice(identity: GroupIdentity): Promise<void> {\n\t\tawait kv.set(IDENTITY_KEY, await seal(await vaultKey(), encode(identity)));\n\t}\n\n\tasync function load(): Promise<GroupIdentity | null> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected. Call unlock(passphrase) - ' +\n\t\t\t\t\t'load()/loadOrCreate() never open or replace a locked identity.'\n\t\t\t);\n\t\t}\n\t\tif (!isEnvelope(stored)) return null;\n\t\treturn decode(await unseal(await vaultKey(), stored));\n\t}\n\n\tasync function save(identity: GroupIdentity): Promise<void> {\n\t\tif (isVaultLock(await kv.get(IDENTITY_KEY))) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected; save() never overwrites a locked ' +\n\t\t\t\t\t'identity. unprotect(passphrase) first, or clear() to forget it.'\n\t\t\t);\n\t\t}\n\t\tawait sealDevice(identity);\n\t}\n\n\tasync function unlock(passphrase: string): Promise<GroupIdentity> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (!isVaultLock(stored)) {\n\t\t\tthrow new TypeError('unlock(): the vault is not passphrase-protected (check isProtected()).');\n\t\t}\n\t\tif (!stored.kdf || !isEnvelope(stored.envelope)) {\n\t\t\tthrow new SelfstoreError('BAD_FORMAT', 'Missing or malformed vault lock parameters.');\n\t\t}\n\t\t// Same read-side ceiling as backup decrypts: a tampered kv must not make\n\t\t// unlock allocate gigabytes (memory-hard bomb) before any passphrase check.\n\t\tassertKdfBounds(stored.kdf);\n\t\ttry {\n\t\t\treturn decode(await unseal(await deriveKey(passphrase, stored.kdf), stored.envelope));\n\t\t} catch (e) {\n\t\t\tif (e instanceof SelfstoreError) throw e;\n\t\t\tthrow new SelfstoreError('DECRYPT_FAILED', 'Wrong passphrase or tampered identity vault.');\n\t\t}\n\t}\n\n\tasync function protect(passphrase: string): Promise<void> {\n\t\tif (!passphrase) throw new TypeError('protect(): a non-empty passphrase is required.');\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is already passphrase-protected. To change the passphrase: ' +\n\t\t\t\t\t'unprotect(oldPassphrase), then protect(newPassphrase).'\n\t\t\t);\n\t\t}\n\t\tconst identity = isEnvelope(stored)\n\t\t\t? decode(await unseal(await vaultKey(), stored)) // reseal the existing identity\n\t\t\t: await generateIdentity(); // empty vault: create-and-protect in one step\n\t\tconst kdf = freshKdfParams();\n\t\tconst envelope = await seal(await deriveKey(passphrase, kdf), encode(identity));\n\t\tawait kv.set(IDENTITY_KEY, { selfstoreVaultLock: 1, kdf, envelope } satisfies VaultLock);\n\t}\n\n\treturn {\n\t\tload,\n\t\tsave,\n\t\tunlock,\n\t\tprotect,\n\t\tasync loadOrCreate(): Promise<GroupIdentity> {\n\t\t\tconst existing = await load(); // PASSWORD_REQUIRED when protected: never mint over a lock\n\t\t\tif (existing) return existing;\n\t\t\tconst fresh = await generateIdentity();\n\t\t\tawait save(fresh);\n\t\t\treturn fresh;\n\t\t},\n\t\tasync clear(): Promise<void> {\n\t\t\tawait kv.del(IDENTITY_KEY);\n\t\t},\n\t\tasync isProtected(): Promise<boolean> {\n\t\t\treturn isVaultLock(await kv.get(IDENTITY_KEY));\n\t\t},\n\t\tasync unprotect(passphrase: string): Promise<void> {\n\t\t\t// unlock() enforces \"protected\" and the passphrase; only then rewrite.\n\t\t\tawait sealDevice(await unlock(passphrase));\n\t\t}\n\t};\n}\n"]}
1
+ {"version":3,"sources":["../src/persistence/identity.ts"],"names":[],"mappings":"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","file":"groups.js","sourcesContent":["// At-rest storage for a member identity (passwordless groups). Private keys\n// are sealed under a non-extractable device key in the same kv - defeats disk\n// forensics and partial exfiltration, not code running in the origin or a\n// copy of the whole profile (THREAT-MODEL.md).\n//\n// The opt-in passphrase lock reseals the identity under Argon2id(passphrase)\n// instead, closing the profile-copy gap at the cost of an unlock prompt. The\n// vault keeps no unlocked session state: unlock() returns the identity, the\n// caller holds it in memory, and at rest it stays sealed. A bare envelope\n// keeps meaning device mode, so pre-lock vaults load unchanged.\n//\n// The identity is one person's; move it between their devices over a trusted\n// channel (QR shown locally, encrypted note) - never through the group's\n// shared storage.\n\nimport { seal, unseal, isEnvelope, newDeviceKey, type EncEnvelope } from './cache-crypto';\nimport type { KV } from './cache';\nimport { generateIdentity, type GroupIdentity } from '../selfstore/group';\nimport { deriveKey, freshKdfParams, assertKdfBounds } from '../selfstore/crypto';\nimport type { KdfParams } from '../selfstore/types';\nimport { SelfstoreError } from '../selfstore/errors';\n\nconst VAULT_KEY = 'groupVaultKey';\nconst IDENTITY_KEY = 'groupIdentity';\n\nconst utf8 = { enc: new TextEncoder(), dec: new TextDecoder() };\n\n// Local kv bookkeeping only - not part of the interchange format (SPEC.md).\ninterface VaultLock {\n\tselfstoreVaultLock: 1;\n\tkdf: KdfParams;\n\tenvelope: EncEnvelope;\n}\n\nfunction isVaultLock(v: unknown): v is VaultLock {\n\treturn (\n\t\ttypeof v === 'object' &&\n\t\tv !== null &&\n\t\t(v as { selfstoreVaultLock?: unknown }).selfstoreVaultLock === 1\n\t);\n}\n\nconst encode = (identity: GroupIdentity): Uint8Array => utf8.enc.encode(JSON.stringify(identity));\nconst decode = (bytes: Uint8Array): GroupIdentity =>\n\tJSON.parse(utf8.dec.decode(bytes)) as GroupIdentity;\n\nexport interface IdentityVault {\n\t/** The stored identity, or null when none exists yet. On a protected\n\t * vault this throws PASSWORD_REQUIRED rather than returning null - a\n\t * locked identity exists, and null would invite loadOrCreate to mint a\n\t * second one over it. */\n\tload(): Promise<GroupIdentity | null>;\n\t/** Seal and store an identity, overwriting any previous one. Throws\n\t * PASSWORD_REQUIRED on a protected vault: a locked identity is never\n\t * silently replaced (unprotect() or clear() first). */\n\tsave(identity: GroupIdentity): Promise<void>;\n\t/** load(), creating and storing a fresh identity when absent. Same\n\t * PASSWORD_REQUIRED rule as load(). */\n\tloadOrCreate(): Promise<GroupIdentity>;\n\t/** Forget the identity on this device. Allowed even when protected -\n\t * forgetting leaks nothing, and it is the honest lost-passphrase escape\n\t * hatch (re-import from another device, or get re-invited). */\n\tclear(): Promise<void>;\n\tisProtected(): Promise<boolean>;\n\t/** Decrypt a protected vault and return the identity. Stateless: the\n\t * caller holds it in memory, the stored value stays sealed. Wrong\n\t * passphrase and tampered blob are one DECRYPT_FAILED; hostile KDF\n\t * params are refused before any derivation. */\n\tunlock(passphrase: string): Promise<GroupIdentity>;\n\t/** Reseal the identity under Argon2id(passphrase); an empty vault gets a\n\t * fresh identity, created-and-protected in one step. Changing the\n\t * passphrase is unprotect(old) then protect(new) - an already-protected\n\t * vault throws PASSWORD_REQUIRED, never a silent rekey. */\n\tprotect(passphrase: string): Promise<void>;\n\t/** Unlock with `passphrase`, then reseal under the device key. A wrong\n\t * passphrase throws DECRYPT_FAILED and leaves the vault untouched. */\n\tunprotect(passphrase: string): Promise<void>;\n}\n\n/** An identity vault over the app's kv (e.g. `indexedDbCache(app).kv`). */\nexport function identityVault(kv: KV): IdentityVault {\n\tasync function vaultKey(): Promise<CryptoKey> {\n\t\tconst existing = await kv.get<CryptoKey>(VAULT_KEY);\n\t\tif (existing) return existing;\n\t\tconst fresh = await newDeviceKey();\n\t\tawait kv.set(VAULT_KEY, fresh);\n\t\treturn fresh;\n\t}\n\n\t/** Device mode's write path: a bare envelope under the device key. */\n\tasync function sealDevice(identity: GroupIdentity): Promise<void> {\n\t\tawait kv.set(IDENTITY_KEY, await seal(await vaultKey(), encode(identity)));\n\t}\n\n\tasync function load(): Promise<GroupIdentity | null> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected. Call unlock(passphrase) - ' +\n\t\t\t\t\t'load()/loadOrCreate() never open or replace a locked identity.'\n\t\t\t);\n\t\t}\n\t\tif (!isEnvelope(stored)) return null;\n\t\treturn decode(await unseal(await vaultKey(), stored));\n\t}\n\n\tasync function save(identity: GroupIdentity): Promise<void> {\n\t\tif (isVaultLock(await kv.get(IDENTITY_KEY))) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected; save() never overwrites a locked ' +\n\t\t\t\t\t'identity. unprotect(passphrase) first, or clear() to forget it.'\n\t\t\t);\n\t\t}\n\t\tawait sealDevice(identity);\n\t}\n\n\tasync function unlock(passphrase: string): Promise<GroupIdentity> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (!isVaultLock(stored)) {\n\t\t\tthrow new TypeError('unlock(): the vault is not passphrase-protected (check isProtected()).');\n\t\t}\n\t\tif (!stored.kdf || !isEnvelope(stored.envelope)) {\n\t\t\tthrow new SelfstoreError('BAD_FORMAT', 'Missing or malformed vault lock parameters.');\n\t\t}\n\t\t// Same read-side ceiling as backup decrypts: a tampered kv must not make\n\t\t// unlock allocate gigabytes (memory-hard bomb) before any passphrase check.\n\t\tassertKdfBounds(stored.kdf);\n\t\ttry {\n\t\t\treturn decode(await unseal(await deriveKey(passphrase, stored.kdf), stored.envelope));\n\t\t} catch (e) {\n\t\t\tif (e instanceof SelfstoreError) throw e;\n\t\t\tthrow new SelfstoreError('DECRYPT_FAILED', 'Wrong passphrase or tampered identity vault.');\n\t\t}\n\t}\n\n\tasync function protect(passphrase: string): Promise<void> {\n\t\tif (!passphrase) throw new TypeError('protect(): a non-empty passphrase is required.');\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is already passphrase-protected. To change the passphrase: ' +\n\t\t\t\t\t'unprotect(oldPassphrase), then protect(newPassphrase).'\n\t\t\t);\n\t\t}\n\t\tconst identity = isEnvelope(stored)\n\t\t\t? decode(await unseal(await vaultKey(), stored)) // reseal the existing identity\n\t\t\t: await generateIdentity(); // empty vault: create-and-protect in one step\n\t\tconst kdf = freshKdfParams();\n\t\tconst envelope = await seal(await deriveKey(passphrase, kdf), encode(identity));\n\t\tawait kv.set(IDENTITY_KEY, { selfstoreVaultLock: 1, kdf, envelope } satisfies VaultLock);\n\t}\n\n\treturn {\n\t\tload,\n\t\tsave,\n\t\tunlock,\n\t\tprotect,\n\t\tasync loadOrCreate(): Promise<GroupIdentity> {\n\t\t\tconst existing = await load(); // PASSWORD_REQUIRED when protected: never mint over a lock\n\t\t\tif (existing) return existing;\n\t\t\tconst fresh = await generateIdentity();\n\t\t\tawait save(fresh);\n\t\t\treturn fresh;\n\t\t},\n\t\tasync clear(): Promise<void> {\n\t\t\tawait kv.del(IDENTITY_KEY);\n\t\t},\n\t\tasync isProtected(): Promise<boolean> {\n\t\t\treturn isVaultLock(await kv.get(IDENTITY_KEY));\n\t\t},\n\t\tasync unprotect(passphrase: string): Promise<void> {\n\t\t\t// unlock() enforces \"protected\" and the passphrase; only then rewrite.\n\t\t\tawait sealDevice(await unlock(passphrase));\n\t\t}\n\t};\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -2,8 +2,9 @@ import { n as Snapshot, E as EncodeOptions, H as Header, f as PasswordPolicy, B
2
2
  export { d as Mode, e as PasswordCheck, g 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-BiQ-BuVl.js';
3
3
  import { L as LocalCache, K as KV } from './cache-ilsK3Tki.js';
4
4
  import { SyncConfig } from './sync.js';
5
- import { D as DriveAuth, W as WebdavConfig, S as S3Config } from './drive-l_4-m1eT.js';
6
- export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-bfv4Ld7s.js';
5
+ import { i as DriveAuth } from './drive-CTYvGg_7.js';
6
+ import { W as WebdavConfig, S as S3Config } from './s3-DvsnA-FK.js';
7
+ export { c as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-C52j3DeH.js';
7
8
 
8
9
  declare const BACKUP_EXTENSION = ".zip";
9
10
  declare const BACKUP_MIME = "application/zip";
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
- import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-7N2YR3QW.js';
2
- export { gisDriveAuth } from './chunk-7N2YR3QW.js';
3
- import { saveToDisk } from './chunk-HDR75TQ7.js';
4
- export { pickFromDisk, saveToDisk } from './chunk-HDR75TQ7.js';
1
+ import { indexedDbCache, memoryCache, isLockableCache, createLocalStore } from './chunk-ELDSPUTS.js';
2
+ export { gisDriveAuth } from './chunk-ELDSPUTS.js';
5
3
  import './chunk-SKWHKM25.js';
6
- import { fromSession, fromSession2, fromSession3, isSupported, connect4, connect3, connect2, connect } from './chunk-5NS7YQNP.js';
7
- export { hasDesktopFiles, useDesktopFiles } from './chunk-5NS7YQNP.js';
8
- import { restore } from './chunk-BNDFMU3J.js';
9
- export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-BNDFMU3J.js';
10
- import './chunk-453KRCMM.js';
4
+ import { saveToDisk } from './chunk-DCEXWSM6.js';
5
+ export { pickFromDisk, saveToDisk } from './chunk-DCEXWSM6.js';
6
+ import { fromSession as fromSession$1, fromSession2, isSupported, connect3, connect2, connect } from './chunk-TKWU52LK.js';
7
+ export { hasDesktopFiles, useDesktopFiles } from './chunk-TKWU52LK.js';
8
+ import { fromSession, connect as connect$1 } from './chunk-J5MFXJU6.js';
9
+ import { restore } from './chunk-MGDXR3RB.js';
10
+ export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-MGDXR3RB.js';
11
11
  import './chunk-QBCU7OOP.js';
12
- import './chunk-HK4VPO65.js';
13
- import { SelfstoreError } from './chunk-CPN2KUD5.js';
14
- export { SelfstoreError, errorLabelKey, isSelfstoreError } from './chunk-CPN2KUD5.js';
12
+ import './chunk-O3Y6FORN.js';
13
+ import { SelfstoreError } from './chunk-3OBJOMQ2.js';
14
+ export { SelfstoreError, errorLabelKey, isSelfstoreError } from './chunk-3OBJOMQ2.js';
15
15
 
16
16
  // src/simple/simple.ts
17
17
  function idFieldOf(sync, collection) {
@@ -67,8 +67,8 @@ async function selfstore(app, options = {}) {
67
67
  if (kind === "drive" && driveAuth) {
68
68
  return fromSession({ auth: driveAuth, kv: cache.kv, fileName: backupName });
69
69
  }
70
- if (kind === "file") return fromSession2({ kv: cache.kv });
71
- if (kind === "webdav") return fromSession3({ kv: cache.kv });
70
+ if (kind === "file") return fromSession$1({ kv: cache.kv });
71
+ if (kind === "webdav") return fromSession2({ kv: cache.kv });
72
72
  return null;
73
73
  }
74
74
  });
@@ -178,13 +178,13 @@ async function selfstore(app, options = {}) {
178
178
  },
179
179
  async connectDrive(auth, opts) {
180
180
  driveAuth = auth;
181
- const target = await connect({ auth, kv: cache.kv, fileName: backupName });
181
+ const target = await connect$1({ auth, kv: cache.kv, fileName: backupName });
182
182
  if (!target) return "cancelled";
183
183
  return connectTarget(target, opts);
184
184
  },
185
185
  async connectFile(opts) {
186
186
  if (isSupported()) {
187
- const target = await connect2({ kv: cache.kv, fileName: backupName });
187
+ const target = await connect({ kv: cache.kv, fileName: backupName });
188
188
  if (target) return connectTarget(target, opts);
189
189
  if (isSupported()) return "cancelled";
190
190
  }
@@ -192,7 +192,7 @@ async function selfstore(app, options = {}) {
192
192
  return "manual";
193
193
  },
194
194
  async connectWebdav(config, opts) {
195
- const target = await connect3({ kv: cache.kv, config });
195
+ const target = await connect2({ kv: cache.kv, config });
196
196
  if (!target) {
197
197
  throw new SelfstoreError(
198
198
  "TARGET_UNAVAILABLE",
@@ -202,7 +202,7 @@ async function selfstore(app, options = {}) {
202
202
  return connectTarget(target, opts);
203
203
  },
204
204
  async connectS3(config, opts) {
205
- const target = await connect4({ kv: cache.kv, config });
205
+ const target = await connect3({ kv: cache.kv, config });
206
206
  if (!target) {
207
207
  throw new SelfstoreError(
208
208
  "TARGET_UNAVAILABLE",