selfstore 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/dist/advanced.d.ts +90 -0
- package/dist/advanced.js +41 -0
- package/dist/advanced.js.map +1 -0
- package/dist/backups.d.ts +182 -0
- package/dist/backups.js +360 -0
- package/dist/backups.js.map +1 -0
- package/dist/cache-De5_sALA.d.ts +129 -0
- package/dist/cache-lock-LZILZNLP.js +23 -0
- package/dist/cache-lock-LZILZNLP.js.map +1 -0
- package/dist/chunk-ATUPHVLG.js +441 -0
- package/dist/chunk-ATUPHVLG.js.map +1 -0
- package/dist/chunk-BJ3RUMGI.js +45 -0
- package/dist/chunk-BJ3RUMGI.js.map +1 -0
- package/dist/chunk-CUWB5EWF.js +732 -0
- package/dist/chunk-CUWB5EWF.js.map +1 -0
- package/dist/chunk-FDPAKHQS.js +230 -0
- package/dist/chunk-FDPAKHQS.js.map +1 -0
- package/dist/chunk-IBIEKK36.js +331 -0
- package/dist/chunk-IBIEKK36.js.map +1 -0
- package/dist/chunk-JB4JHM7V.js +474 -0
- package/dist/chunk-JB4JHM7V.js.map +1 -0
- package/dist/chunk-N4RPMX3Y.js +1908 -0
- package/dist/chunk-N4RPMX3Y.js.map +1 -0
- package/dist/chunk-OWLX6PNT.js +231 -0
- package/dist/chunk-OWLX6PNT.js.map +1 -0
- package/dist/chunk-QBCU7OOP.js +20 -0
- package/dist/chunk-QBCU7OOP.js.map +1 -0
- package/dist/chunk-SGISYMT5.js +46 -0
- package/dist/chunk-SGISYMT5.js.map +1 -0
- package/dist/chunk-VBFMMR7Q.js +29 -0
- package/dist/chunk-VBFMMR7Q.js.map +1 -0
- package/dist/drive-Bv94UM8b.d.ts +222 -0
- package/dist/flows.d.ts +306 -0
- package/dist/flows.js +153 -0
- package/dist/flows.js.map +1 -0
- package/dist/groups.d.ts +39 -0
- package/dist/groups.js +103 -0
- package/dist/groups.js.map +1 -0
- package/dist/households.d.ts +214 -0
- package/dist/households.js +410 -0
- package/dist/households.js.map +1 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +249 -0
- package/dist/index.js.map +1 -0
- package/dist/kdf-worker.js +2395 -0
- package/dist/kdf-worker.js.map +1 -0
- package/dist/local-2py_vGGy.d.ts +47 -0
- package/dist/local-AM22RWRR.js +7 -0
- package/dist/local-AM22RWRR.js.map +1 -0
- package/dist/machine-ALMkT3mk.d.ts +17 -0
- package/dist/store-ChIGEOph.d.ts +577 -0
- package/dist/sync.d.ts +107 -0
- package/dist/sync.js +3 -0
- package/dist/sync.js.map +1 -0
- package/dist/widgets.d.ts +426 -0
- package/dist/widgets.js +2579 -0
- package/dist/widgets.js.map +1 -0
- package/llms.txt +405 -0
- package/package.json +99 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** The `keying` header value for group-mode boxes (see SPEC.md section 12). */
|
|
2
|
+
declare const GROUP_KEYING = "x25519-hkdf-sha256";
|
|
3
|
+
/**
|
|
4
|
+
* A member's full identity: private halves PKCS#8 DER, public halves raw 32
|
|
5
|
+
* bytes, all base64. Treat the *Priv fields like a password - seal them at
|
|
6
|
+
* rest (identityVault) and move them only over a trusted channel.
|
|
7
|
+
*/
|
|
8
|
+
interface GroupIdentity {
|
|
9
|
+
sigPub: string;
|
|
10
|
+
sigPriv: string;
|
|
11
|
+
encPub: string;
|
|
12
|
+
encPriv: string;
|
|
13
|
+
}
|
|
14
|
+
/** One member as the manifest lists them: public keys only. */
|
|
15
|
+
interface GroupMember {
|
|
16
|
+
id: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Ed25519 public key (base64 raw): verifies the copies this member signs. */
|
|
19
|
+
sig: string;
|
|
20
|
+
/** X25519 public key (base64 raw): receives the data-key envelopes. */
|
|
21
|
+
enc: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The membership document, single-writer (the admin), carried as a
|
|
25
|
+
* SignedManifest. `seq` grows on every change; consumers refuse a lower seq
|
|
26
|
+
* than one already applied (the store persists the high-water mark).
|
|
27
|
+
*/
|
|
28
|
+
interface GroupManifest {
|
|
29
|
+
v: 1;
|
|
30
|
+
/** Random group id (base64), so seq high-water marks never collide. */
|
|
31
|
+
group: string;
|
|
32
|
+
seq: number;
|
|
33
|
+
/** The admin's Ed25519 public key. Members pin it at join time (TOFU). */
|
|
34
|
+
admin: string;
|
|
35
|
+
members: GroupMember[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A manifest as it travels: the exact signed payload bytes plus the admin's
|
|
39
|
+
* signature. Signing bytes rather than re-serialized JSON sidesteps
|
|
40
|
+
* canonicalization entirely.
|
|
41
|
+
*/
|
|
42
|
+
interface SignedManifest {
|
|
43
|
+
selfstoreManifest: 1;
|
|
44
|
+
payload: string;
|
|
45
|
+
sig: string;
|
|
46
|
+
}
|
|
47
|
+
/** Whether WebCrypto implements Ed25519 + X25519 here. Password mode never needs this. */
|
|
48
|
+
declare function groupCryptoAvailable(): Promise<boolean>;
|
|
49
|
+
/** Generate a fresh member identity (two keypairs). */
|
|
50
|
+
declare function generateIdentity(): Promise<GroupIdentity>;
|
|
51
|
+
/** The shareable half of an identity: what the admin puts in the manifest. */
|
|
52
|
+
declare function publicIdentity(id: GroupIdentity): {
|
|
53
|
+
sig: string;
|
|
54
|
+
enc: string;
|
|
55
|
+
};
|
|
56
|
+
/** A fresh random group id for a new manifest. */
|
|
57
|
+
declare function newGroupId(): string;
|
|
58
|
+
/** First 8 bytes of SHA-256(raw X25519 public), base64: finds a reader's stanza while naming nobody. */
|
|
59
|
+
declare function keyId(encPubB64: string): Promise<string>;
|
|
60
|
+
/** Sign a manifest with the admin key, over the exact payload bytes. */
|
|
61
|
+
declare function signManifest(manifest: GroupManifest, adminSigPrivB64: string): Promise<SignedManifest>;
|
|
62
|
+
/**
|
|
63
|
+
* Verify and parse a signed manifest against the pinned admin key (TOFU from
|
|
64
|
+
* the invite). Rollback (seq) is enforced by the store, which persists the
|
|
65
|
+
* high-water mark.
|
|
66
|
+
*/
|
|
67
|
+
declare function openManifest(signed: SignedManifest, adminSigPubB64: string, expectedGroupB64?: string): Promise<GroupManifest>;
|
|
68
|
+
|
|
69
|
+
/** A binary attachment kept as a real Blob. */
|
|
70
|
+
interface CachedFile {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
mime: string;
|
|
74
|
+
blob: Blob;
|
|
75
|
+
}
|
|
76
|
+
/** A tiny async key/value space for the store's bookkeeping. */
|
|
77
|
+
interface KV {
|
|
78
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
79
|
+
set(key: string, value: unknown): Promise<void>;
|
|
80
|
+
del(key: string): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
interface LocalCache {
|
|
83
|
+
readonly kv: KV;
|
|
84
|
+
/** Load the persisted collections + files, or null if nothing was ever saved. */
|
|
85
|
+
load(): Promise<{
|
|
86
|
+
collections: Record<string, unknown[]>;
|
|
87
|
+
files: CachedFile[];
|
|
88
|
+
} | null>;
|
|
89
|
+
/** Overwrite the collections snapshot (small; written on every change). */
|
|
90
|
+
saveCollections(collections: Record<string, unknown[]>): Promise<void>;
|
|
91
|
+
/** Reconcile the file store: add new blobs, drop removed ones (immutable by id). */
|
|
92
|
+
saveFiles(files: CachedFile[]): Promise<void>;
|
|
93
|
+
/** Wipe every locally-stored byte (the "forget me on this device" action). */
|
|
94
|
+
clear(): Promise<void>;
|
|
95
|
+
/** Ask the platform to make storage persistent (not evicted). Best-effort;
|
|
96
|
+
* optional because only browser-backed caches have the concept. */
|
|
97
|
+
requestPersistent?(): Promise<boolean>;
|
|
98
|
+
}
|
|
99
|
+
/** A cache whose payload seal key is not on disk but supplied per session from
|
|
100
|
+
* a secret held in memory (a password, or an app-provided key such as a passkey
|
|
101
|
+
* PRF). Until unlock() succeeds each session, load()/save*() reject with
|
|
102
|
+
* PASSWORD_REQUIRED. Defeats a full-profile copy, which a device key cannot -
|
|
103
|
+
* see indexedDbCache(name, { lock: true }). */
|
|
104
|
+
interface LockableCache extends LocalCache {
|
|
105
|
+
/** Supply the session secret. A string is Argon2id-derived (the salt is
|
|
106
|
+
* persisted in the clear); a CryptoKey is used as-is. Returns false when it
|
|
107
|
+
* does not open existing data - the cache stays locked. The first secret on
|
|
108
|
+
* an empty cache sets the lock. */
|
|
109
|
+
unlock(secret: string | CryptoKey): Promise<boolean>;
|
|
110
|
+
/** Drop the in-memory key: reads and writes lock until unlock() is called. */
|
|
111
|
+
lockNow(): void;
|
|
112
|
+
/** Whether no seal key is currently held. */
|
|
113
|
+
readonly locked: boolean;
|
|
114
|
+
}
|
|
115
|
+
/** Narrow a cache to its lockable shape (created with { lock: true }). */
|
|
116
|
+
declare function isLockableCache(cache: LocalCache): cache is LockableCache;
|
|
117
|
+
/** In-memory LocalCache for tests and SSR paths where IndexedDB does not
|
|
118
|
+
* exist. Not encrypted - nothing is at rest. */
|
|
119
|
+
declare function memoryCache(): LocalCache;
|
|
120
|
+
/** The default browser cache. `name` scopes the database to one app, so two
|
|
121
|
+
* apps on the same origin never collide. */
|
|
122
|
+
declare function indexedDbCache(name: string, opts: {
|
|
123
|
+
lock: true;
|
|
124
|
+
}): LockableCache;
|
|
125
|
+
declare function indexedDbCache(name: string, opts?: {
|
|
126
|
+
lock?: boolean;
|
|
127
|
+
}): LocalCache;
|
|
128
|
+
|
|
129
|
+
export { type CachedFile as C, type GroupIdentity as G, type KV as K, type LocalCache as L, type SignedManifest as S, GROUP_KEYING as a, type GroupManifest as b, type GroupMember as c, groupCryptoAvailable as d, type LockableCache as e, isLockableCache as f, generateIdentity as g, indexedDbCache as i, keyId as k, memoryCache as m, newGroupId as n, openManifest as o, publicIdentity as p, signManifest as s };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { unseal } from './chunk-QBCU7OOP.js';
|
|
2
|
+
import { freshKdfParams, deriveKey } from './chunk-FDPAKHQS.js';
|
|
3
|
+
import './chunk-VBFMMR7Q.js';
|
|
4
|
+
|
|
5
|
+
// src/persistence/cache-lock.ts
|
|
6
|
+
function freshCacheKdf() {
|
|
7
|
+
return freshKdfParams();
|
|
8
|
+
}
|
|
9
|
+
function keyFromPassword(password, kdf) {
|
|
10
|
+
return deriveKey(password, kdf);
|
|
11
|
+
}
|
|
12
|
+
async function opens(key, probe) {
|
|
13
|
+
try {
|
|
14
|
+
await unseal(key, probe);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { freshCacheKdf, keyFromPassword, opens };
|
|
22
|
+
//# sourceMappingURL=cache-lock-LZILZNLP.js.map
|
|
23
|
+
//# sourceMappingURL=cache-lock-LZILZNLP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/persistence/cache-lock.ts"],"names":[],"mappings":";;;;;AAeO,SAAS,aAAA,GAA0B;AACzC,EAAA,OAAO,cAAA,EAAe;AACvB;AAGO,SAAS,eAAA,CAAgB,UAAkB,GAAA,EAAmC;AACpF,EAAA,OAAO,SAAA,CAAU,UAAU,GAAG,CAAA;AAC/B;AAIA,eAAsB,KAAA,CAAM,KAAgB,KAAA,EAAsC;AACjF,EAAA,IAAI;AACH,IAAA,MAAM,MAAA,CAAO,KAAK,KAAK,CAAA;AACvB,IAAA,OAAO,IAAA;AAAA,EACR,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,KAAA;AAAA,EACR;AACD","file":"cache-lock-LZILZNLP.js","sourcesContent":["// Seal-key material for a lock-mode cache. The key is derived from a password\n// (the backup's Argon2id KDF) or supplied by the app (e.g. a passkey PRF key),\n// and is held in memory only - never written next to the data. That is what a\n// device-key seal (cache-crypto) cannot give: a copy of the whole browser\n// profile carries the device key, but not a secret kept in RAM. Only the KDF\n// salt is persisted, and a salt is not secret.\n\nimport { unseal, type EncEnvelope } from './cache-crypto';\nimport { deriveKey, freshKdfParams } from '../selfstore/crypto';\nimport type { KdfParams } from '../selfstore/types';\n\n/** Argon2id parameters for a password-locked cache; persisted in the clear. */\nexport type CacheKdf = KdfParams;\n\n/** Fresh Argon2id parameters (random salt, current default cost). */\nexport function freshCacheKdf(): CacheKdf {\n\treturn freshKdfParams();\n}\n\n/** Derive the AES-GCM seal key from a password and its stored parameters. */\nexport function keyFromPassword(password: string, kdf: CacheKdf): Promise<CryptoKey> {\n\treturn deriveKey(password, kdf);\n}\n\n/** True if `key` decrypts `probe`. A wrong key fails AES-GCM authentication;\n * that is a wrong-secret answer, never an error to surface. */\nexport async function opens(key: CryptoKey, probe: EncEnvelope): Promise<boolean> {\n\ttry {\n\t\tawait unseal(key, probe);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { connect4, connect3, isSupported, connect, openExisting, connect2 } from './chunk-CUWB5EWF.js';
|
|
2
|
+
import { restore } from './chunk-JB4JHM7V.js';
|
|
3
|
+
import { makeMachine, withDeadline, toStoreError } from './chunk-SGISYMT5.js';
|
|
4
|
+
import { SelfstoreError, isSelfstoreError } from './chunk-VBFMMR7Q.js';
|
|
5
|
+
|
|
6
|
+
// src/flows/connect.ts
|
|
7
|
+
var DEFAULT_DEADLINE_MS = 3e4;
|
|
8
|
+
function planFor(how) {
|
|
9
|
+
if (how === "resume") return { strategy: "replace-local", outcome: "resumed" };
|
|
10
|
+
if (how === "replace") return { strategy: "replace-remote", outcome: "replaced" };
|
|
11
|
+
return { strategy: "merge", outcome: "merged" };
|
|
12
|
+
}
|
|
13
|
+
function connectFlow(store, targets, options = {}) {
|
|
14
|
+
const host = "flowHost" in store ? store.flowHost : store;
|
|
15
|
+
const deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS;
|
|
16
|
+
const kinds = Object.keys(targets).filter(
|
|
17
|
+
(k) => (k === "drive" || k === "file" || k === "webdav" || k === "s3") && targets[k] != null
|
|
18
|
+
);
|
|
19
|
+
const m = makeMachine({
|
|
20
|
+
step: "choose",
|
|
21
|
+
kinds,
|
|
22
|
+
kind: null,
|
|
23
|
+
outcome: null,
|
|
24
|
+
hasBackup: false,
|
|
25
|
+
encrypted: false,
|
|
26
|
+
passwordError: false,
|
|
27
|
+
busy: false,
|
|
28
|
+
error: null
|
|
29
|
+
});
|
|
30
|
+
let generation = 0;
|
|
31
|
+
let target = null;
|
|
32
|
+
let password = null;
|
|
33
|
+
const backToChoose = () => {
|
|
34
|
+
generation++;
|
|
35
|
+
target = null;
|
|
36
|
+
password = null;
|
|
37
|
+
m.set({
|
|
38
|
+
step: "choose",
|
|
39
|
+
kind: null,
|
|
40
|
+
outcome: null,
|
|
41
|
+
hasBackup: false,
|
|
42
|
+
encrypted: false,
|
|
43
|
+
passwordError: false,
|
|
44
|
+
busy: false,
|
|
45
|
+
error: null
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const fail = (e) => {
|
|
49
|
+
password = null;
|
|
50
|
+
m.set({ step: "error", busy: false, error: toStoreError(e) });
|
|
51
|
+
};
|
|
52
|
+
function connectorFor(kind, variant = "create") {
|
|
53
|
+
const spec = targets[kind];
|
|
54
|
+
if (spec == null) return null;
|
|
55
|
+
if (typeof spec === "function") return spec;
|
|
56
|
+
if (kind === "drive") {
|
|
57
|
+
const auth = spec;
|
|
58
|
+
return () => connect({ auth, kv: host.kv, fileName: host.backupName });
|
|
59
|
+
}
|
|
60
|
+
if (kind === "file") {
|
|
61
|
+
if (typeof spec === "object" && !("url" in spec)) {
|
|
62
|
+
const sub = spec[variant];
|
|
63
|
+
if (sub == null) return null;
|
|
64
|
+
if (typeof sub === "function") return sub;
|
|
65
|
+
return variant === "open" ? () => openExisting({ kv: host.kv }) : () => connect2({ kv: host.kv, fileName: host.backupName });
|
|
66
|
+
}
|
|
67
|
+
return () => connect2({ kv: host.kv, fileName: host.backupName });
|
|
68
|
+
}
|
|
69
|
+
if (kind === "s3") {
|
|
70
|
+
const config2 = spec;
|
|
71
|
+
return () => connect4({ kv: host.kv, config: config2 });
|
|
72
|
+
}
|
|
73
|
+
const config = spec;
|
|
74
|
+
return () => connect3({ kv: host.kv, config });
|
|
75
|
+
}
|
|
76
|
+
async function inspectAndRoute(gen, t) {
|
|
77
|
+
const info = await withDeadline(host.engine.inspectTarget(t), deadlineMs, "The destination");
|
|
78
|
+
if (gen !== generation) return;
|
|
79
|
+
target = t;
|
|
80
|
+
m.set({ hasBackup: info.hasBackup, encrypted: info.encrypted });
|
|
81
|
+
if (!info.hasBackup) {
|
|
82
|
+
await attach(gen, "replace-remote", "started");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (info.encrypted) {
|
|
86
|
+
if (options.deferUnlock) {
|
|
87
|
+
await attach(gen, "replace-local", "resumed");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
m.set({ step: "password", busy: false, passwordError: false });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
routeAfterUnlock(gen);
|
|
94
|
+
}
|
|
95
|
+
function routeAfterUnlock(gen) {
|
|
96
|
+
if (gen !== generation) return;
|
|
97
|
+
if (options.hasLocalData?.() === true) {
|
|
98
|
+
m.set({ step: "conflict", busy: false });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const plan = planFor(options.defaultResolution ?? "merge");
|
|
102
|
+
void attach(gen, plan.strategy, plan.outcome);
|
|
103
|
+
}
|
|
104
|
+
async function attach(gen, strategy, outcome, wipe = false) {
|
|
105
|
+
if (gen !== generation || !target) return;
|
|
106
|
+
m.set({ busy: true });
|
|
107
|
+
try {
|
|
108
|
+
await withDeadline(
|
|
109
|
+
host.engine.attachTarget(target, { password, strategy, wipe }),
|
|
110
|
+
deadlineMs,
|
|
111
|
+
"The destination"
|
|
112
|
+
);
|
|
113
|
+
if (gen !== generation) return;
|
|
114
|
+
password = null;
|
|
115
|
+
m.set({ step: "connected", outcome, busy: false, error: null });
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (gen !== generation) return;
|
|
118
|
+
fail(e);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
get snapshot() {
|
|
123
|
+
return m.snapshot;
|
|
124
|
+
},
|
|
125
|
+
subscribe: m.subscribe,
|
|
126
|
+
choose(kind, variant = "create") {
|
|
127
|
+
const { step, busy } = m.snapshot;
|
|
128
|
+
if (busy || step !== "choose" && step !== "error") return;
|
|
129
|
+
const connector = connectorFor(kind, variant);
|
|
130
|
+
if (!connector) return;
|
|
131
|
+
if (kind === "webdav" && targets.webdav === true || kind === "s3" && targets.s3 === true) {
|
|
132
|
+
m.set({ step: "form", kind, error: null });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const builtinCreate = targets.file === true || typeof targets.file === "object" && targets.file.create !== void 0 && typeof targets.file.create !== "function";
|
|
136
|
+
if (kind === "file" && variant === "create" && builtinCreate && !isSupported()) {
|
|
137
|
+
const gen2 = ++generation;
|
|
138
|
+
m.set({ step: "authorizing", kind, busy: true, error: null });
|
|
139
|
+
void host.engine.setManualFile().then(() => {
|
|
140
|
+
if (gen2 !== generation) return;
|
|
141
|
+
m.set({ step: "connected", outcome: "manual", busy: false });
|
|
142
|
+
}).catch((e) => {
|
|
143
|
+
if (gen2 !== generation) return;
|
|
144
|
+
fail(e);
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const gen = ++generation;
|
|
149
|
+
m.set({ step: "authorizing", kind, busy: true, error: null });
|
|
150
|
+
void connector().then(async (t) => {
|
|
151
|
+
if (gen !== generation) return;
|
|
152
|
+
if (!t) {
|
|
153
|
+
if (kind === "webdav" || kind === "s3") {
|
|
154
|
+
fail(
|
|
155
|
+
new SelfstoreError(
|
|
156
|
+
"TARGET_UNAVAILABLE",
|
|
157
|
+
"The server did not answer (address, credentials or CORS)."
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
backToChoose();
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
await inspectAndRoute(gen, t);
|
|
166
|
+
}).catch((e) => {
|
|
167
|
+
if (gen !== generation) return;
|
|
168
|
+
fail(e);
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
submitWebdav(config) {
|
|
172
|
+
const { step, busy } = m.snapshot;
|
|
173
|
+
if (busy || step !== "form") return;
|
|
174
|
+
const gen = ++generation;
|
|
175
|
+
m.set({ step: "authorizing", busy: true });
|
|
176
|
+
void withDeadline(connect3({ kv: host.kv, config }), deadlineMs, "The WebDAV server").then(async (t) => {
|
|
177
|
+
if (gen !== generation) return;
|
|
178
|
+
if (!t) {
|
|
179
|
+
fail(
|
|
180
|
+
new SelfstoreError(
|
|
181
|
+
"TARGET_UNAVAILABLE",
|
|
182
|
+
"The WebDAV server did not answer (URL, credentials or CORS)."
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
await inspectAndRoute(gen, t);
|
|
188
|
+
}).catch((e) => {
|
|
189
|
+
if (gen !== generation) return;
|
|
190
|
+
fail(e);
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
submitS3(config) {
|
|
194
|
+
const { step, busy } = m.snapshot;
|
|
195
|
+
if (busy || step !== "form") return;
|
|
196
|
+
const gen = ++generation;
|
|
197
|
+
m.set({ step: "authorizing", busy: true });
|
|
198
|
+
void withDeadline(connect4({ kv: host.kv, config }), deadlineMs, "The S3 endpoint").then(async (t) => {
|
|
199
|
+
if (gen !== generation) return;
|
|
200
|
+
if (!t) {
|
|
201
|
+
fail(
|
|
202
|
+
new SelfstoreError(
|
|
203
|
+
"TARGET_UNAVAILABLE",
|
|
204
|
+
"The S3 endpoint did not answer (endpoint, credentials or CORS)."
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await inspectAndRoute(gen, t);
|
|
210
|
+
}).catch((e) => {
|
|
211
|
+
if (gen !== generation) return;
|
|
212
|
+
fail(e);
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
submitPassword(pw) {
|
|
216
|
+
const { step, busy } = m.snapshot;
|
|
217
|
+
if (busy || step !== "password" || !target) return;
|
|
218
|
+
const gen = generation;
|
|
219
|
+
m.set({ busy: true, passwordError: false });
|
|
220
|
+
void (async () => {
|
|
221
|
+
try {
|
|
222
|
+
const blob = await withDeadline(target.load(), deadlineMs, "The destination");
|
|
223
|
+
if (gen !== generation) return;
|
|
224
|
+
if (!blob) {
|
|
225
|
+
throw new SelfstoreError("TARGET_UNAVAILABLE", "The backup could not be downloaded.");
|
|
226
|
+
}
|
|
227
|
+
await restore(blob).withPassword(pw).read();
|
|
228
|
+
if (gen !== generation) return;
|
|
229
|
+
password = pw;
|
|
230
|
+
m.set({ busy: false });
|
|
231
|
+
routeAfterUnlock(gen);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
if (gen !== generation) return;
|
|
234
|
+
if (isSelfstoreError(e) && e.code === "DECRYPT_FAILED") {
|
|
235
|
+
m.set({ busy: false, passwordError: true });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
fail(e);
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
},
|
|
242
|
+
overwrite() {
|
|
243
|
+
const { step, busy } = m.snapshot;
|
|
244
|
+
if (busy || step !== "password" || !target) return;
|
|
245
|
+
password = null;
|
|
246
|
+
void attach(generation, "replace-remote", "replaced", true);
|
|
247
|
+
},
|
|
248
|
+
resolveConflict(how) {
|
|
249
|
+
const { step, busy } = m.snapshot;
|
|
250
|
+
if (busy || step !== "conflict") return;
|
|
251
|
+
const plan = planFor(how);
|
|
252
|
+
void attach(generation, plan.strategy, plan.outcome);
|
|
253
|
+
},
|
|
254
|
+
cancel() {
|
|
255
|
+
if (m.snapshot.step === "connected") return;
|
|
256
|
+
backToChoose();
|
|
257
|
+
},
|
|
258
|
+
retry() {
|
|
259
|
+
if (m.snapshot.step !== "error") return;
|
|
260
|
+
backToChoose();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/flows/share.ts
|
|
266
|
+
var DEFAULT_DEADLINE_MS2 = 3e4;
|
|
267
|
+
function shareFlow(engine, options = {}) {
|
|
268
|
+
const deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS2;
|
|
269
|
+
const m = makeMachine({
|
|
270
|
+
links: [],
|
|
271
|
+
members: [],
|
|
272
|
+
busy: "refresh",
|
|
273
|
+
stale: false,
|
|
274
|
+
error: null,
|
|
275
|
+
canRemoveMembers: typeof engine.removeMember === "function",
|
|
276
|
+
canRevokeAll: typeof engine.revokeAll === "function"
|
|
277
|
+
});
|
|
278
|
+
function begin(op) {
|
|
279
|
+
if (m.snapshot.busy !== null && !(op === "refresh" && m.snapshot.busy === "refresh")) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
m.set({ busy: op });
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
async function doRefresh() {
|
|
286
|
+
try {
|
|
287
|
+
const { links, members } = await withDeadline(engine.list(), deadlineMs, "The share listing");
|
|
288
|
+
m.set({ links, members, busy: null, stale: false, error: null });
|
|
289
|
+
} catch (e) {
|
|
290
|
+
m.set({ busy: null, stale: true, error: toStoreError(e) });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const flow = {
|
|
294
|
+
get snapshot() {
|
|
295
|
+
return m.snapshot;
|
|
296
|
+
},
|
|
297
|
+
subscribe: m.subscribe,
|
|
298
|
+
async refresh() {
|
|
299
|
+
if (m.snapshot.busy !== null && m.snapshot.busy !== "refresh") return;
|
|
300
|
+
m.set({ busy: "refresh" });
|
|
301
|
+
await doRefresh();
|
|
302
|
+
},
|
|
303
|
+
async createLink(opts) {
|
|
304
|
+
if (!begin("create")) return null;
|
|
305
|
+
try {
|
|
306
|
+
const link = await withDeadline(engine.createLink(opts), deadlineMs, "Creating the link");
|
|
307
|
+
m.set({ links: [...m.snapshot.links, link], busy: null, error: null });
|
|
308
|
+
return link;
|
|
309
|
+
} catch (e) {
|
|
310
|
+
m.set({ busy: null, error: toStoreError(e) });
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
async revokeLink(id) {
|
|
315
|
+
if (!begin("revoke")) return false;
|
|
316
|
+
try {
|
|
317
|
+
await withDeadline(engine.revokeLink(id), deadlineMs, "Revoking the link");
|
|
318
|
+
m.set({ links: m.snapshot.links.filter((l) => l.id !== id), busy: null, error: null });
|
|
319
|
+
return true;
|
|
320
|
+
} catch (e) {
|
|
321
|
+
m.set({ busy: null, error: toStoreError(e) });
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
async removeMember(id) {
|
|
326
|
+
const remove = engine.removeMember?.bind(engine);
|
|
327
|
+
if (!remove) {
|
|
328
|
+
throw new TypeError("shareFlow: this engine does not manage members (no removeMember).");
|
|
329
|
+
}
|
|
330
|
+
const row = m.snapshot.members.find((mb) => mb.id === id);
|
|
331
|
+
if (row?.self) {
|
|
332
|
+
throw new TypeError(
|
|
333
|
+
"shareFlow: refusing to remove yourself - leaving a share is its own journey, not a row action."
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (!begin("remove")) return false;
|
|
337
|
+
try {
|
|
338
|
+
await withDeadline(remove(id), deadlineMs, "Removing the member");
|
|
339
|
+
m.set({
|
|
340
|
+
members: m.snapshot.members.filter((mb) => mb.id !== id),
|
|
341
|
+
busy: null,
|
|
342
|
+
error: null
|
|
343
|
+
});
|
|
344
|
+
return true;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
m.set({ busy: null, error: toStoreError(e) });
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
async revokeAll() {
|
|
351
|
+
const revoke = engine.revokeAll?.bind(engine);
|
|
352
|
+
if (!revoke) {
|
|
353
|
+
throw new TypeError("shareFlow: this engine does not end the share (no revokeAll).");
|
|
354
|
+
}
|
|
355
|
+
if (!begin("revoke-all")) return false;
|
|
356
|
+
try {
|
|
357
|
+
await withDeadline(revoke(), deadlineMs, "Ending the share");
|
|
358
|
+
m.set({ links: [], members: [], busy: null, error: null });
|
|
359
|
+
return true;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
m.set({ busy: null, error: toStoreError(e) });
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
void doRefresh();
|
|
367
|
+
return flow;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/flows/join.ts
|
|
371
|
+
var DEFAULT_DEADLINE_MS3 = 3e4;
|
|
372
|
+
function joinFlow(link, engine, options = {}) {
|
|
373
|
+
const deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS3;
|
|
374
|
+
const m = makeMachine({
|
|
375
|
+
link,
|
|
376
|
+
step: "previewing",
|
|
377
|
+
preview: null,
|
|
378
|
+
canSwitchAccount: typeof engine.switchAccount === "function",
|
|
379
|
+
busy: true,
|
|
380
|
+
error: null
|
|
381
|
+
});
|
|
382
|
+
let generation = 0;
|
|
383
|
+
function preview() {
|
|
384
|
+
const gen = ++generation;
|
|
385
|
+
m.set({ step: "previewing", busy: true, error: null });
|
|
386
|
+
void withDeadline(engine.preview(link), deadlineMs, "The invitation").then((p) => {
|
|
387
|
+
if (gen !== generation) return;
|
|
388
|
+
m.set({ step: "ready", preview: p, busy: false });
|
|
389
|
+
}).catch((e) => {
|
|
390
|
+
if (gen !== generation) return;
|
|
391
|
+
m.set({ step: "error", busy: false, error: toStoreError(e) });
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
preview();
|
|
395
|
+
return {
|
|
396
|
+
get snapshot() {
|
|
397
|
+
return m.snapshot;
|
|
398
|
+
},
|
|
399
|
+
subscribe: m.subscribe,
|
|
400
|
+
accept() {
|
|
401
|
+
const { step, busy } = m.snapshot;
|
|
402
|
+
if (busy || step !== "ready") return;
|
|
403
|
+
const gen = ++generation;
|
|
404
|
+
m.set({ step: "joining", busy: true, error: null });
|
|
405
|
+
void withDeadline(engine.join(link), deadlineMs, "Joining").then((outcome) => {
|
|
406
|
+
if (gen !== generation) return;
|
|
407
|
+
if (outcome === null) {
|
|
408
|
+
m.set({ step: "ready", busy: false });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
m.set({ step: outcome, busy: false });
|
|
412
|
+
}).catch((e) => {
|
|
413
|
+
if (gen !== generation) return;
|
|
414
|
+
m.set({ step: "error", busy: false, error: toStoreError(e) });
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
switchAccount() {
|
|
418
|
+
const switcher = engine.switchAccount?.bind(engine);
|
|
419
|
+
if (!switcher) return;
|
|
420
|
+
const { step, busy } = m.snapshot;
|
|
421
|
+
if (busy || step !== "ready" && step !== "no-invite" && step !== "error") return;
|
|
422
|
+
const gen = ++generation;
|
|
423
|
+
m.set({ busy: true, error: null });
|
|
424
|
+
void switcher().then(() => {
|
|
425
|
+
if (gen !== generation) return;
|
|
426
|
+
preview();
|
|
427
|
+
}).catch((e) => {
|
|
428
|
+
if (gen !== generation) return;
|
|
429
|
+
m.set({ step: "error", busy: false, error: toStoreError(e) });
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
retry() {
|
|
433
|
+
if (m.snapshot.step !== "error") return;
|
|
434
|
+
preview();
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export { connectFlow, joinFlow, shareFlow };
|
|
440
|
+
//# sourceMappingURL=chunk-ATUPHVLG.js.map
|
|
441
|
+
//# sourceMappingURL=chunk-ATUPHVLG.js.map
|