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.
- package/README.md +1 -1
- package/dist/advanced.d.ts +8 -4
- package/dist/advanced.js +14 -13
- package/dist/advanced.js.map +1 -1
- package/dist/backups.d.ts +31 -179
- package/dist/backups.js +40 -3
- package/dist/backups.js.map +1 -1
- package/dist/{cache-lock-RCN3JJ4Q.js → cache-lock-KWJUORTS.js} +3 -4
- package/dist/cache-lock-KWJUORTS.js.map +1 -0
- package/dist/{chunk-HK4VPO65.js → chunk-3OBJOMQ2.js} +28 -4
- package/dist/chunk-3OBJOMQ2.js.map +1 -0
- package/dist/{chunk-HDR75TQ7.js → chunk-DCEXWSM6.js} +3 -3
- package/dist/{chunk-HDR75TQ7.js.map → chunk-DCEXWSM6.js.map} +1 -1
- package/dist/{chunk-7N2YR3QW.js → chunk-ELDSPUTS.js} +7 -8
- package/dist/chunk-ELDSPUTS.js.map +1 -0
- package/dist/chunk-J5MFXJU6.js +310 -0
- package/dist/chunk-J5MFXJU6.js.map +1 -0
- package/dist/{chunk-BNDFMU3J.js → chunk-MGDXR3RB.js} +5 -6
- package/dist/chunk-MGDXR3RB.js.map +1 -0
- package/dist/{chunk-453KRCMM.js → chunk-O3Y6FORN.js} +3 -4
- package/dist/chunk-O3Y6FORN.js.map +1 -0
- package/dist/{chunk-JMJZIZKY.js → chunk-PNX7AJYO.js} +13 -12
- package/dist/chunk-PNX7AJYO.js.map +1 -0
- package/dist/{chunk-5NS7YQNP.js → chunk-TKWU52LK.js} +26 -312
- package/dist/chunk-TKWU52LK.js.map +1 -0
- package/dist/{chunk-U5F4GI7C.js → chunk-W22V5CRM.js} +3 -3
- package/dist/{chunk-U5F4GI7C.js.map → chunk-W22V5CRM.js.map} +1 -1
- package/dist/{drive-l_4-m1eT.d.ts → drive-CTYvGg_7.d.ts} +23 -110
- package/dist/{flows-C_uookek.d.ts → flows-1EntUZ91.d.ts} +2 -1
- package/dist/flows.d.ts +3 -2
- package/dist/flows.js +16 -16
- package/dist/flows.js.map +1 -1
- package/dist/groups.js +3 -4
- package/dist/groups.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +18 -18
- package/dist/index.js.map +1 -1
- package/dist/{local-bfv4Ld7s.d.ts → local-C52j3DeH.d.ts} +1 -1
- package/dist/local-DPS35F5C.js +6 -0
- package/dist/{local-2YREU4Q2.js.map → local-DPS35F5C.js.map} +1 -1
- package/dist/manager-DZ1c_aSS.d.ts +181 -0
- package/dist/s3-DvsnA-FK.d.ts +112 -0
- package/dist/widgets.d.ts +90 -7
- package/dist/widgets.js +281 -18
- package/dist/widgets.js.map +1 -1
- package/llms.txt +13 -2
- package/package.json +1 -1
- package/dist/cache-lock-RCN3JJ4Q.js.map +0 -1
- package/dist/chunk-453KRCMM.js.map +0 -1
- package/dist/chunk-5NS7YQNP.js.map +0 -1
- package/dist/chunk-7N2YR3QW.js.map +0 -1
- package/dist/chunk-BNDFMU3J.js.map +0 -1
- package/dist/chunk-CPN2KUD5.js +0 -29
- package/dist/chunk-CPN2KUD5.js.map +0 -1
- package/dist/chunk-HK4VPO65.js.map +0 -1
- package/dist/chunk-JMJZIZKY.js.map +0 -1
- package/dist/local-2YREU4Q2.js +0 -7
|
@@ -1,114 +1,6 @@
|
|
|
1
|
-
import { B as BackupTarget
|
|
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
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
2
|
-
import { connect
|
|
3
|
-
import './chunk-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import './chunk-
|
|
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-
|
|
9
|
-
import './chunk-
|
|
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 () =>
|
|
53
|
+
return () => connect$1({ kv: skv, fileName });
|
|
54
54
|
}
|
|
55
55
|
if (kind === "s3") {
|
|
56
56
|
const config2 = spec;
|
|
57
|
-
return () =>
|
|
57
|
+
return () => connect3({ kv: skv, config: config2 });
|
|
58
58
|
}
|
|
59
59
|
const config = spec;
|
|
60
|
-
return () =>
|
|
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
|
|
86
|
-
if (kind === "webdav") return
|
|
87
|
-
return
|
|
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", () =>
|
|
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", () =>
|
|
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 {
|
|
5
|
-
|
|
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";
|
package/dist/groups.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/persistence/identity.ts"],"names":[],"mappings":";;;;;;;AAsBA,IAAM,SAAA,GAAY,eAAA;AAClB,IAAM,YAAA,GAAe,eAAA;AAErB,IAAM,IAAA,GAAO,EAAE,GAAA,EAAK,IAAI,aAAY,EAAG,GAAA,EAAK,IAAI,WAAA,EAAY,EAAE;AAS9D,SAAS,YAAY,CAAA,EAA4B;AAChD,EAAA,OACC,OAAO,CAAA,KAAM,QAAA,IACb,CAAA,KAAM,IAAA,IACL,EAAuC,kBAAA,KAAuB,CAAA;AAEjE;AAEA,IAAM,MAAA,GAAS,CAAC,QAAA,KAAwC,IAAA,CAAK,IAAI,MAAA,CAAO,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAC,CAAA;AAChG,IAAM,MAAA,GAAS,CAAC,KAAA,KACf,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,KAAK,CAAC,CAAA;AAoC3B,SAAS,cAAc,EAAA,EAAuB;AACpD,EAAA,eAAe,QAAA,GAA+B;AAC7C,IAAA,MAAM,QAAA,GAAW,MAAM,EAAA,CAAG,GAAA,CAAe,SAAS,CAAA;AAClD,IAAA,IAAI,UAAU,OAAO,QAAA;AACrB,IAAA,MAAM,KAAA,GAAQ,MAAM,YAAA,EAAa;AACjC,IAAA,MAAM,EAAA,CAAG,GAAA,CAAI,SAAA,EAAW,KAAK,CAAA;AAC7B,IAAA,OAAO,KAAA;AAAA,EACR;AAGA,EAAA,eAAe,WAAW,QAAA,EAAwC;AACjE,IAAA,MAAM,EAAA,CAAG,GAAA,CAAI,YAAA,EAAc,MAAM,IAAA,CAAK,MAAM,QAAA,EAAS,EAAG,MAAA,CAAO,QAAQ,CAAC,CAAC,CAAA;AAAA,EAC1E;AAEA,EAAA,eAAe,IAAA,GAAsC;AACpD,IAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,GAAA,CAAI,YAAY,CAAA;AACxC,IAAA,IAAI,WAAA,CAAY,MAAM,CAAA,EAAG;AACxB,MAAA,MAAM,IAAI,cAAA;AAAA,QACT,mBAAA;AAAA,QACA;AAAA,OAED;AAAA,IACD;AACA,IAAA,IAAI,CAAC,UAAA,CAAW,MAAM,CAAA,EAAG,OAAO,IAAA;AAChC,IAAA,OAAO,OAAO,MAAM,MAAA,CAAO,MAAM,QAAA,EAAS,EAAG,MAAM,CAAC,CAAA;AAAA,EACrD;AAEA,EAAA,eAAe,KAAK,QAAA,EAAwC;AAC3D,IAAA,IAAI,YAAY,MAAM,EAAA,CAAG,GAAA,CAAI,YAAY,CAAC,CAAA,EAAG;AAC5C,MAAA,MAAM,IAAI,cAAA;AAAA,QACT,mBAAA;AAAA,QACA;AAAA,OAED;AAAA,IACD;AACA,IAAA,MAAM,WAAW,QAAQ,CAAA;AAAA,EAC1B;AAEA,EAAA,eAAe,OAAO,UAAA,EAA4C;AACjE,IAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,GAAA,CAAI,YAAY,CAAA;AACxC,IAAA,IAAI,CAAC,WAAA,CAAY,MAAM,CAAA,EAAG;AACzB,MAAA,MAAM,IAAI,UAAU,wEAAwE,CAAA;AAAA,IAC7F;AACA,IAAA,IAAI,CAAC,MAAA,CAAO,GAAA,IAAO,CAAC,UAAA,CAAW,MAAA,CAAO,QAAQ,CAAA,EAAG;AAChD,MAAA,MAAM,IAAI,cAAA,CAAe,YAAA,EAAc,6CAA6C,CAAA;AAAA,IACrF;AAGA,IAAA,eAAA,CAAgB,OAAO,GAAG,CAAA;AAC1B,IAAA,IAAI;AACH,MAAA,OAAO,MAAA,CAAO,MAAM,MAAA,CAAO,MAAM,SAAA,CAAU,UAAA,EAAY,MAAA,CAAO,GAAG,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAC,CAAA;AAAA,IACrF,SAAS,CAAA,EAAG;AACX,MAAA,IAAI,CAAA,YAAa,gBAAgB,MAAM,CAAA;AACvC,MAAA,MAAM,IAAI,cAAA,CAAe,gBAAA,EAAkB,8CAA8C,CAAA;AAAA,IAC1F;AAAA,EACD;AAEA,EAAA,eAAe,QAAQ,UAAA,EAAmC;AACzD,IAAA,IAAI,CAAC,UAAA,EAAY,MAAM,IAAI,UAAU,gDAAgD,CAAA;AACrF,IAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,GAAA,CAAI,YAAY,CAAA;AACxC,IAAA,IAAI,WAAA,CAAY,MAAM,CAAA,EAAG;AACxB,MAAA,MAAM,IAAI,cAAA;AAAA,QACT,mBAAA;AAAA,QACA;AAAA,OAED;AAAA,IACD;AACA,IAAA,MAAM,QAAA,GAAW,UAAA,CAAW,MAAM,CAAA,GAC/B,OAAO,MAAM,MAAA,CAAO,MAAM,QAAA,EAAS,EAAG,MAAM,CAAC,CAAA,GAC7C,MAAM,gBAAA,EAAiB;AAC1B,IAAA,MAAM,MAAM,cAAA,EAAe;AAC3B,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,MAAM,SAAA,CAAU,YAAY,GAAG,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAC,CAAA;AAC9E,IAAA,MAAM,EAAA,CAAG,IAAI,YAAA,EAAc,EAAE,oBAAoB,CAAA,EAAG,GAAA,EAAK,UAA8B,CAAA;AAAA,EACxF;AAEA,EAAA,OAAO;AAAA,IACN,IAAA;AAAA,IACA,IAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAM,YAAA,GAAuC;AAC5C,MAAA,MAAM,QAAA,GAAW,MAAM,IAAA,EAAK;AAC5B,MAAA,IAAI,UAAU,OAAO,QAAA;AACrB,MAAA,MAAM,KAAA,GAAQ,MAAM,gBAAA,EAAiB;AACrC,MAAA,MAAM,KAAK,KAAK,CAAA;AAChB,MAAA,OAAO,KAAA;AAAA,IACR,CAAA;AAAA,IACA,MAAM,KAAA,GAAuB;AAC5B,MAAA,MAAM,EAAA,CAAG,IAAI,YAAY,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,MAAM,WAAA,GAAgC;AACrC,MAAA,OAAO,WAAA,CAAY,MAAM,EAAA,CAAG,GAAA,CAAI,YAAY,CAAC,CAAA;AAAA,IAC9C,CAAA;AAAA,IACA,MAAM,UAAU,UAAA,EAAmC;AAElD,MAAA,MAAM,UAAA,CAAW,MAAM,MAAA,CAAO,UAAU,CAAC,CAAA;AAAA,IAC1C;AAAA,GACD;AACD","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 {
|
|
6
|
-
|
|
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-
|
|
2
|
-
export { gisDriveAuth } from './chunk-
|
|
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 {
|
|
7
|
-
export {
|
|
8
|
-
import {
|
|
9
|
-
export {
|
|
10
|
-
import './chunk-
|
|
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-
|
|
13
|
-
import { SelfstoreError } from './chunk-
|
|
14
|
-
export { SelfstoreError, errorLabelKey, isSelfstoreError } from './chunk-
|
|
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
|
|
71
|
-
if (kind === "webdav") return
|
|
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
|
|
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
|
|
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
|
|
205
|
+
const target = await connect3({ kv: cache.kv, config });
|
|
206
206
|
if (!target) {
|
|
207
207
|
throw new SelfstoreError(
|
|
208
208
|
"TARGET_UNAVAILABLE",
|