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,231 @@
|
|
|
1
|
+
import { toBase64, fromBase64 } from './chunk-FDPAKHQS.js';
|
|
2
|
+
import { SelfstoreError } from './chunk-VBFMMR7Q.js';
|
|
3
|
+
|
|
4
|
+
// src/selfstore/group.ts
|
|
5
|
+
var GROUP_KEYING = "x25519-hkdf-sha256";
|
|
6
|
+
var DATA_KEY_BYTES = 32;
|
|
7
|
+
var CURVE_PUB_BYTES = 32;
|
|
8
|
+
var MAX_RECIPIENTS = 256;
|
|
9
|
+
var DOMAIN_BOX = "selfstore-group-sig-v1";
|
|
10
|
+
var DOMAIN_MANIFEST = "selfstore-group-manifest-v1";
|
|
11
|
+
var HKDF_INFO = "selfstore-group-wrap-v1";
|
|
12
|
+
var utf8 = new TextEncoder();
|
|
13
|
+
function src(u8) {
|
|
14
|
+
return u8;
|
|
15
|
+
}
|
|
16
|
+
function concat(...parts) {
|
|
17
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
18
|
+
let o = 0;
|
|
19
|
+
for (const p of parts) {
|
|
20
|
+
out.set(p, o);
|
|
21
|
+
o += p.length;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
async function sha256(bytes) {
|
|
26
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", src(bytes)));
|
|
27
|
+
}
|
|
28
|
+
var curvesOk = null;
|
|
29
|
+
function groupCryptoAvailable() {
|
|
30
|
+
curvesOk ??= (async () => {
|
|
31
|
+
try {
|
|
32
|
+
await crypto.subtle.generateKey("Ed25519", false, ["sign", "verify"]);
|
|
33
|
+
await crypto.subtle.generateKey("X25519", false, ["deriveBits"]);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
return curvesOk;
|
|
40
|
+
}
|
|
41
|
+
async function requireCurves() {
|
|
42
|
+
if (!await groupCryptoAvailable()) {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
"Group mode needs WebCrypto Ed25519 and X25519 (evergreen browsers, Node 20+); this platform does not provide them. Use password mode instead, or upgrade the runtime."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var importSigPriv = (b64) => crypto.subtle.importKey("pkcs8", src(fromBase64(b64)), "Ed25519", false, ["sign"]);
|
|
49
|
+
var importSigPub = (b64) => crypto.subtle.importKey("raw", src(fromBase64(b64)), "Ed25519", false, ["verify"]);
|
|
50
|
+
var importEncPriv = (b64) => crypto.subtle.importKey("pkcs8", src(fromBase64(b64)), "X25519", false, ["deriveBits"]);
|
|
51
|
+
var importEncPub = (b64) => crypto.subtle.importKey("raw", src(fromBase64(b64)), "X25519", false, []);
|
|
52
|
+
async function generateIdentity() {
|
|
53
|
+
await requireCurves();
|
|
54
|
+
const sig = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
|
|
55
|
+
const enc = await crypto.subtle.generateKey("X25519", true, ["deriveBits"]);
|
|
56
|
+
const raw = async (k) => toBase64(new Uint8Array(await crypto.subtle.exportKey("raw", k)));
|
|
57
|
+
const pkcs8 = async (k) => toBase64(new Uint8Array(await crypto.subtle.exportKey("pkcs8", k)));
|
|
58
|
+
return {
|
|
59
|
+
sigPub: await raw(sig.publicKey),
|
|
60
|
+
sigPriv: await pkcs8(sig.privateKey),
|
|
61
|
+
encPub: await raw(enc.publicKey),
|
|
62
|
+
encPriv: await pkcs8(enc.privateKey)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function publicIdentity(id) {
|
|
66
|
+
return { sig: id.sigPub, enc: id.encPub };
|
|
67
|
+
}
|
|
68
|
+
function newGroupId() {
|
|
69
|
+
return toBase64(crypto.getRandomValues(new Uint8Array(16)));
|
|
70
|
+
}
|
|
71
|
+
async function keyId(encPubB64) {
|
|
72
|
+
return toBase64((await sha256(fromBase64(encPubB64))).subarray(0, 8));
|
|
73
|
+
}
|
|
74
|
+
async function stanzaWrapKey(priv, otherPub, ephPubRaw, recipientPubRaw, usage) {
|
|
75
|
+
const shared = new Uint8Array(
|
|
76
|
+
await crypto.subtle.deriveBits({ name: "X25519", public: otherPub }, priv, 256)
|
|
77
|
+
);
|
|
78
|
+
const ikm = await crypto.subtle.importKey("raw", src(shared), "HKDF", false, ["deriveBits"]);
|
|
79
|
+
const bits = new Uint8Array(
|
|
80
|
+
await crypto.subtle.deriveBits(
|
|
81
|
+
{
|
|
82
|
+
name: "HKDF",
|
|
83
|
+
hash: "SHA-256",
|
|
84
|
+
salt: src(concat(ephPubRaw, recipientPubRaw)),
|
|
85
|
+
info: src(utf8.encode(HKDF_INFO))
|
|
86
|
+
},
|
|
87
|
+
ikm,
|
|
88
|
+
256
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
return crypto.subtle.importKey("raw", src(bits), "AES-GCM", false, [usage]);
|
|
92
|
+
}
|
|
93
|
+
async function sealDataKey(dataKey, recipientEncPubsB64) {
|
|
94
|
+
await requireCurves();
|
|
95
|
+
if (dataKey.length !== DATA_KEY_BYTES) {
|
|
96
|
+
throw new TypeError(`sealDataKey: data key must be ${DATA_KEY_BYTES} bytes (AES-256).`);
|
|
97
|
+
}
|
|
98
|
+
if (recipientEncPubsB64.length > MAX_RECIPIENTS) {
|
|
99
|
+
throw new TypeError(`sealDataKey: too many recipients (max ${MAX_RECIPIENTS}).`);
|
|
100
|
+
}
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const r of recipientEncPubsB64) {
|
|
103
|
+
const eph = await crypto.subtle.generateKey("X25519", false, ["deriveBits"]);
|
|
104
|
+
const ephPubRaw = new Uint8Array(await crypto.subtle.exportKey("raw", eph.publicKey));
|
|
105
|
+
const key = await stanzaWrapKey(
|
|
106
|
+
eph.privateKey,
|
|
107
|
+
await importEncPub(r),
|
|
108
|
+
ephPubRaw,
|
|
109
|
+
fromBase64(r),
|
|
110
|
+
"encrypt"
|
|
111
|
+
);
|
|
112
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
113
|
+
const wrap = new Uint8Array(
|
|
114
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, src(dataKey))
|
|
115
|
+
);
|
|
116
|
+
out.push({ kid: await keyId(r), epk: toBase64(ephPubRaw), iv: toBase64(iv), wrap: toBase64(wrap) });
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
async function openDataKey(stanzas, identity) {
|
|
121
|
+
await requireCurves();
|
|
122
|
+
if (stanzas.length > MAX_RECIPIENTS) {
|
|
123
|
+
throw new SelfstoreError("BAD_FORMAT", `Too many recipient stanzas (max ${MAX_RECIPIENTS}).`);
|
|
124
|
+
}
|
|
125
|
+
const myKid = await keyId(identity.encPub);
|
|
126
|
+
const mine = stanzas.filter((s) => s.kid === myKid);
|
|
127
|
+
const candidates = mine.length > 0 ? mine : stanzas;
|
|
128
|
+
const priv = await importEncPriv(identity.encPriv);
|
|
129
|
+
const myPubRaw = fromBase64(identity.encPub);
|
|
130
|
+
for (const s of candidates) {
|
|
131
|
+
try {
|
|
132
|
+
const key = await stanzaWrapKey(priv, await importEncPub(s.epk), fromBase64(s.epk), myPubRaw, "decrypt");
|
|
133
|
+
const pt = new Uint8Array(
|
|
134
|
+
await crypto.subtle.decrypt({ name: "AES-GCM", iv: src(fromBase64(s.iv)) }, key, src(fromBase64(s.wrap)))
|
|
135
|
+
);
|
|
136
|
+
if (pt.length === DATA_KEY_BYTES) return pt;
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new SelfstoreError(
|
|
141
|
+
"NOT_A_RECIPIENT",
|
|
142
|
+
"No envelope opens for this identity: it is not (or no longer) a recipient of this copy."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
async function gcmSealRaw(key32, plaintext) {
|
|
146
|
+
const key = await crypto.subtle.importKey("raw", src(key32), "AES-GCM", false, ["encrypt"]);
|
|
147
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
148
|
+
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, src(plaintext));
|
|
149
|
+
return { iv: toBase64(iv), ciphertext: new Uint8Array(ct) };
|
|
150
|
+
}
|
|
151
|
+
async function gcmOpenRaw(key32, ivB64, ciphertext) {
|
|
152
|
+
try {
|
|
153
|
+
const key = await crypto.subtle.importKey("raw", src(key32), "AES-GCM", false, ["decrypt"]);
|
|
154
|
+
const pt = await crypto.subtle.decrypt(
|
|
155
|
+
{ name: "AES-GCM", iv: src(fromBase64(ivB64)) },
|
|
156
|
+
key,
|
|
157
|
+
src(ciphertext)
|
|
158
|
+
);
|
|
159
|
+
return new Uint8Array(pt);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new SelfstoreError("DECRYPT_FAILED", "Corrupted or mismatched group ciphertext.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function signBox(metaBytes, dataEnc, sigPrivB64) {
|
|
165
|
+
await requireCurves();
|
|
166
|
+
const msg = concat(utf8.encode(DOMAIN_BOX), await sha256(metaBytes), await sha256(dataEnc));
|
|
167
|
+
const key = await importSigPriv(sigPrivB64);
|
|
168
|
+
return new Uint8Array(await crypto.subtle.sign("Ed25519", key, src(msg)));
|
|
169
|
+
}
|
|
170
|
+
async function verifyBox(metaBytes, dataEnc, sig, sigPubB64) {
|
|
171
|
+
try {
|
|
172
|
+
await requireCurves();
|
|
173
|
+
const msg = concat(utf8.encode(DOMAIN_BOX), await sha256(metaBytes), await sha256(dataEnc));
|
|
174
|
+
return await crypto.subtle.verify("Ed25519", await importSigPub(sigPubB64), src(sig), src(msg));
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function signManifest(manifest, adminSigPrivB64) {
|
|
180
|
+
await requireCurves();
|
|
181
|
+
const payload = utf8.encode(JSON.stringify(manifest));
|
|
182
|
+
const key = await importSigPriv(adminSigPrivB64);
|
|
183
|
+
const msg = concat(utf8.encode(DOMAIN_MANIFEST), payload);
|
|
184
|
+
const sig = new Uint8Array(await crypto.subtle.sign("Ed25519", key, src(msg)));
|
|
185
|
+
return { selfstoreManifest: 1, payload: toBase64(payload), sig: toBase64(sig) };
|
|
186
|
+
}
|
|
187
|
+
async function openManifest(signed, adminSigPubB64, expectedGroupB64) {
|
|
188
|
+
await requireCurves();
|
|
189
|
+
const bad = (why) => new SelfstoreError("SIGNATURE_INVALID", why);
|
|
190
|
+
if (!signed || signed.selfstoreManifest !== 1 || typeof signed.payload !== "string") {
|
|
191
|
+
throw bad("Not a signed selfstore manifest.");
|
|
192
|
+
}
|
|
193
|
+
let payload;
|
|
194
|
+
let ok = false;
|
|
195
|
+
try {
|
|
196
|
+
payload = fromBase64(signed.payload);
|
|
197
|
+
const msg = concat(utf8.encode(DOMAIN_MANIFEST), payload);
|
|
198
|
+
ok = await crypto.subtle.verify(
|
|
199
|
+
"Ed25519",
|
|
200
|
+
await importSigPub(adminSigPubB64),
|
|
201
|
+
src(fromBase64(signed.sig ?? "")),
|
|
202
|
+
src(msg)
|
|
203
|
+
);
|
|
204
|
+
} catch {
|
|
205
|
+
throw bad("Malformed manifest signature or keys.");
|
|
206
|
+
}
|
|
207
|
+
if (!ok) throw bad("Manifest signature invalid for the pinned admin key.");
|
|
208
|
+
const m = JSON.parse(new TextDecoder().decode(payload));
|
|
209
|
+
const is32ByteKey = (b64) => {
|
|
210
|
+
try {
|
|
211
|
+
return fromBase64(b64).length === CURVE_PUB_BYTES;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const shapeOk = m && m.v === 1 && typeof m.group === "string" && Number.isInteger(m.seq) && m.seq >= 0 && m.admin === adminSigPubB64 && Array.isArray(m.members) && m.members.length <= MAX_RECIPIENTS && m.members.every(
|
|
217
|
+
(mem) => mem && typeof mem.id === "string" && mem.id.length > 0 && is32ByteKey(mem.sig) && is32ByteKey(mem.enc)
|
|
218
|
+
);
|
|
219
|
+
if (!shapeOk) throw bad("Manifest shape invalid or admin key mismatch.");
|
|
220
|
+
if (new Set(m.members.map((mem) => mem.id)).size !== m.members.length) {
|
|
221
|
+
throw bad("Manifest has duplicate member ids.");
|
|
222
|
+
}
|
|
223
|
+
if (expectedGroupB64 !== void 0 && m.group !== expectedGroupB64) {
|
|
224
|
+
throw bad("Manifest is for a different group than expected.");
|
|
225
|
+
}
|
|
226
|
+
return m;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { GROUP_KEYING, gcmOpenRaw, gcmSealRaw, generateIdentity, groupCryptoAvailable, keyId, newGroupId, openDataKey, openManifest, publicIdentity, sealDataKey, signBox, signManifest, verifyBox };
|
|
230
|
+
//# sourceMappingURL=chunk-OWLX6PNT.js.map
|
|
231
|
+
//# sourceMappingURL=chunk-OWLX6PNT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/selfstore/group.ts"],"names":[],"mappings":"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","file":"chunk-OWLX6PNT.js","sourcesContent":["// Passwordless groups: the public-key layer under peers (see PEERS.md).\n//\n// Each member holds an Ed25519 keypair to sign the copies they publish and an\n// X25519 keypair to receive data keys. A copy is encrypted once under a fresh\n// AES-256-GCM data key, enveloped per member (ephemeral ECDH + HKDF + GCM\n// wrap - the age recipient model). Membership is a manifest signed by a\n// single admin key; a copy is trusted only when its signature verifies and\n// its author is in the manifest.\n//\n// WebCrypto only, no polyfill: the supply-chain surface stays flat, and\n// evergreen browsers plus Node 20+ have everything needed.\n\nimport type { RecipientStanza } from './types';\nimport { toBase64, fromBase64 } from './base64';\nimport { SelfstoreError } from './errors';\n\n/** The `keying` header value for group-mode boxes (see SPEC.md section 12). */\nexport const GROUP_KEYING = 'x25519-hkdf-sha256';\n\n// Enforced on both seal and open, so a member cannot publish a copy under a\n// narrower key (16 bytes -> AES-128) and have readers accept it.\nconst DATA_KEY_BYTES = 32;\nconst CURVE_PUB_BYTES = 32;\n// Group mode targets a handful of members; the cap keeps a crafted copy from\n// demanding one asymmetric operation per stanza without bound.\nexport const MAX_RECIPIENTS = 256;\n\n// Domain separation: every signature and derivation binds its purpose, so\n// bytes signed in one role can never be replayed in another.\nconst DOMAIN_BOX = 'selfstore-group-sig-v1';\nconst DOMAIN_MANIFEST = 'selfstore-group-manifest-v1';\nconst HKDF_INFO = 'selfstore-group-wrap-v1';\n\nconst utf8 = new TextEncoder();\n\n/**\n * A member's full identity: private halves PKCS#8 DER, public halves raw 32\n * bytes, all base64. Treat the *Priv fields like a password - seal them at\n * rest (identityVault) and move them only over a trusted channel.\n */\nexport interface GroupIdentity {\n\tsigPub: string;\n\tsigPriv: string;\n\tencPub: string;\n\tencPriv: string;\n}\n\n/** One member as the manifest lists them: public keys only. */\nexport interface GroupMember {\n\tid: string;\n\tlabel?: string;\n\t/** Ed25519 public key (base64 raw): verifies the copies this member signs. */\n\tsig: string;\n\t/** X25519 public key (base64 raw): receives the data-key envelopes. */\n\tenc: string;\n}\n\n/**\n * The membership document, single-writer (the admin), carried as a\n * SignedManifest. `seq` grows on every change; consumers refuse a lower seq\n * than one already applied (the store persists the high-water mark).\n */\nexport interface GroupManifest {\n\tv: 1;\n\t/** Random group id (base64), so seq high-water marks never collide. */\n\tgroup: string;\n\tseq: number;\n\t/** The admin's Ed25519 public key. Members pin it at join time (TOFU). */\n\tadmin: string;\n\tmembers: GroupMember[];\n}\n\n/**\n * A manifest as it travels: the exact signed payload bytes plus the admin's\n * signature. Signing bytes rather than re-serialized JSON sidesteps\n * canonicalization entirely.\n */\nexport interface SignedManifest {\n\tselfstoreManifest: 1;\n\tpayload: string;\n\tsig: string;\n}\n\n/** Coerce to the ArrayBuffer-backed view WebCrypto's types want (safe here). */\nfunction src(u8: Uint8Array): Uint8Array<ArrayBuffer> {\n\treturn u8 as Uint8Array<ArrayBuffer>;\n}\n\nfunction concat(...parts: Uint8Array[]): Uint8Array {\n\tconst out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));\n\tlet o = 0;\n\tfor (const p of parts) {\n\t\tout.set(p, o);\n\t\to += p.length;\n\t}\n\treturn out;\n}\n\nasync function sha256(bytes: Uint8Array): Promise<Uint8Array> {\n\treturn new Uint8Array(await crypto.subtle.digest('SHA-256', src(bytes)));\n}\n\nlet curvesOk: Promise<boolean> | null = null;\n\n/** Whether WebCrypto implements Ed25519 + X25519 here. Password mode never needs this. */\nexport function groupCryptoAvailable(): Promise<boolean> {\n\tcurvesOk ??= (async () => {\n\t\ttry {\n\t\t\tawait crypto.subtle.generateKey('Ed25519', false, ['sign', 'verify']);\n\t\t\tawait crypto.subtle.generateKey('X25519', false, ['deriveBits']);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t})();\n\treturn curvesOk;\n}\n\nasync function requireCurves(): Promise<void> {\n\tif (!(await groupCryptoAvailable())) {\n\t\tthrow new TypeError(\n\t\t\t'Group mode needs WebCrypto Ed25519 and X25519 (evergreen browsers, Node 20+); ' +\n\t\t\t\t'this platform does not provide them. Use password mode instead, or upgrade the runtime.'\n\t\t);\n\t}\n}\n\nconst importSigPriv = (b64: string): Promise<CryptoKey> =>\n\tcrypto.subtle.importKey('pkcs8', src(fromBase64(b64)), 'Ed25519', false, ['sign']);\nconst importSigPub = (b64: string): Promise<CryptoKey> =>\n\tcrypto.subtle.importKey('raw', src(fromBase64(b64)), 'Ed25519', false, ['verify']);\nconst importEncPriv = (b64: string): Promise<CryptoKey> =>\n\tcrypto.subtle.importKey('pkcs8', src(fromBase64(b64)), 'X25519', false, ['deriveBits']);\nconst importEncPub = (b64: string): Promise<CryptoKey> =>\n\tcrypto.subtle.importKey('raw', src(fromBase64(b64)), 'X25519', false, []);\n\n/** Generate a fresh member identity (two keypairs). */\nexport async function generateIdentity(): Promise<GroupIdentity> {\n\tawait requireCurves();\n\tconst sig = (await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify'])) as CryptoKeyPair;\n\tconst enc = (await crypto.subtle.generateKey('X25519', true, ['deriveBits'])) as CryptoKeyPair;\n\tconst raw = async (k: CryptoKey): Promise<string> =>\n\t\ttoBase64(new Uint8Array(await crypto.subtle.exportKey('raw', k)));\n\tconst pkcs8 = async (k: CryptoKey): Promise<string> =>\n\t\ttoBase64(new Uint8Array(await crypto.subtle.exportKey('pkcs8', k)));\n\treturn {\n\t\tsigPub: await raw(sig.publicKey),\n\t\tsigPriv: await pkcs8(sig.privateKey),\n\t\tencPub: await raw(enc.publicKey),\n\t\tencPriv: await pkcs8(enc.privateKey)\n\t};\n}\n\n/** The shareable half of an identity: what the admin puts in the manifest. */\nexport function publicIdentity(id: GroupIdentity): { sig: string; enc: string } {\n\treturn { sig: id.sigPub, enc: id.encPub };\n}\n\n/** A fresh random group id for a new manifest. */\nexport function newGroupId(): string {\n\treturn toBase64(crypto.getRandomValues(new Uint8Array(16)));\n}\n\n/** First 8 bytes of SHA-256(raw X25519 public), base64: finds a reader's stanza while naming nobody. */\nexport async function keyId(encPubB64: string): Promise<string> {\n\treturn toBase64((await sha256(fromBase64(encPubB64))).subarray(0, 8));\n}\n\n// Wrap key for one stanza: HKDF-SHA256 over the X25519 shared secret, salted\n// with ephemeralPub || recipientPub so the key binds to this exact pair.\nasync function stanzaWrapKey(\n\tpriv: CryptoKey,\n\totherPub: CryptoKey,\n\tephPubRaw: Uint8Array,\n\trecipientPubRaw: Uint8Array,\n\tusage: 'encrypt' | 'decrypt'\n): Promise<CryptoKey> {\n\tconst shared = new Uint8Array(\n\t\tawait crypto.subtle.deriveBits({ name: 'X25519', public: otherPub }, priv, 256)\n\t);\n\tconst ikm = await crypto.subtle.importKey('raw', src(shared), 'HKDF', false, ['deriveBits']);\n\tconst bits = new Uint8Array(\n\t\tawait crypto.subtle.deriveBits(\n\t\t\t{\n\t\t\t\tname: 'HKDF',\n\t\t\t\thash: 'SHA-256',\n\t\t\t\tsalt: src(concat(ephPubRaw, recipientPubRaw)),\n\t\t\t\tinfo: src(utf8.encode(HKDF_INFO))\n\t\t\t},\n\t\t\tikm,\n\t\t\t256\n\t\t)\n\t);\n\treturn crypto.subtle.importKey('raw', src(bits), 'AES-GCM', false, [usage]);\n}\n\n/** Envelope a data key: one ephemeral keypair and one stanza per recipient. */\nexport async function sealDataKey(\n\tdataKey: Uint8Array,\n\trecipientEncPubsB64: string[]\n): Promise<RecipientStanza[]> {\n\tawait requireCurves();\n\tif (dataKey.length !== DATA_KEY_BYTES) {\n\t\tthrow new TypeError(`sealDataKey: data key must be ${DATA_KEY_BYTES} bytes (AES-256).`);\n\t}\n\tif (recipientEncPubsB64.length > MAX_RECIPIENTS) {\n\t\tthrow new TypeError(`sealDataKey: too many recipients (max ${MAX_RECIPIENTS}).`);\n\t}\n\tconst out: RecipientStanza[] = [];\n\tfor (const r of recipientEncPubsB64) {\n\t\tconst eph = (await crypto.subtle.generateKey('X25519', false, ['deriveBits'])) as CryptoKeyPair;\n\t\tconst ephPubRaw = new Uint8Array(await crypto.subtle.exportKey('raw', eph.publicKey));\n\t\tconst key = await stanzaWrapKey(\n\t\t\teph.privateKey,\n\t\t\tawait importEncPub(r),\n\t\t\tephPubRaw,\n\t\t\tfromBase64(r),\n\t\t\t'encrypt'\n\t\t);\n\t\tconst iv = crypto.getRandomValues(new Uint8Array(12));\n\t\tconst wrap = new Uint8Array(\n\t\t\tawait crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, src(dataKey))\n\t\t);\n\t\tout.push({ kid: await keyId(r), epk: toBase64(ephPubRaw), iv: toBase64(iv), wrap: toBase64(wrap) });\n\t}\n\treturn out;\n}\n\n/**\n * Open the stanza addressed to this identity and return the data key.\n * NOT_A_RECIPIENT when none opens (identity removed, or the copy predates\n * its joining).\n */\nexport async function openDataKey(\n\tstanzas: RecipientStanza[],\n\tidentity: Pick<GroupIdentity, 'encPub' | 'encPriv'>\n): Promise<Uint8Array> {\n\tawait requireCurves();\n\tif (stanzas.length > MAX_RECIPIENTS) {\n\t\tthrow new SelfstoreError('BAD_FORMAT', `Too many recipient stanzas (max ${MAX_RECIPIENTS}).`);\n\t}\n\tconst myKid = await keyId(identity.encPub);\n\tconst mine = stanzas.filter((s) => s.kid === myKid);\n\t// On a kid miss try them all; MAX_RECIPIENTS bounds that work.\n\tconst candidates = mine.length > 0 ? mine : stanzas;\n\tconst priv = await importEncPriv(identity.encPriv);\n\tconst myPubRaw = fromBase64(identity.encPub);\n\tfor (const s of candidates) {\n\t\ttry {\n\t\t\tconst key = await stanzaWrapKey(priv, await importEncPub(s.epk), fromBase64(s.epk), myPubRaw, 'decrypt');\n\t\t\tconst pt = new Uint8Array(\n\t\t\t\tawait crypto.subtle.decrypt({ name: 'AES-GCM', iv: src(fromBase64(s.iv)) }, key, src(fromBase64(s.wrap)))\n\t\t\t);\n\t\t\t// Any other length is a key-downgrade attempt, not a valid envelope.\n\t\t\tif (pt.length === DATA_KEY_BYTES) return pt;\n\t\t} catch {\n\t\t\t/* not our stanza (or tampered): try the next candidate */\n\t\t}\n\t}\n\tthrow new SelfstoreError(\n\t\t'NOT_A_RECIPIENT',\n\t\t'No envelope opens for this identity: it is not (or no longer) a recipient of this copy.'\n\t);\n}\n\n/** AES-256-GCM under a raw 32-byte key (the group data key). */\nexport async function gcmSealRaw(\n\tkey32: Uint8Array,\n\tplaintext: Uint8Array\n): Promise<{ iv: string; ciphertext: Uint8Array }> {\n\tconst key = await crypto.subtle.importKey('raw', src(key32), 'AES-GCM', false, ['encrypt']);\n\tconst iv = crypto.getRandomValues(new Uint8Array(12));\n\tconst ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, src(plaintext));\n\treturn { iv: toBase64(iv), ciphertext: new Uint8Array(ct) };\n}\n\n/** Decrypt AES-256-GCM under a raw key; a tag failure is DECRYPT_FAILED. */\nexport async function gcmOpenRaw(\n\tkey32: Uint8Array,\n\tivB64: string,\n\tciphertext: Uint8Array\n): Promise<Uint8Array> {\n\ttry {\n\t\tconst key = await crypto.subtle.importKey('raw', src(key32), 'AES-GCM', false, ['decrypt']);\n\t\tconst pt = await crypto.subtle.decrypt(\n\t\t\t{ name: 'AES-GCM', iv: src(fromBase64(ivB64)) },\n\t\t\tkey,\n\t\t\tsrc(ciphertext)\n\t\t);\n\t\treturn new Uint8Array(pt);\n\t} catch {\n\t\tthrow new SelfstoreError('DECRYPT_FAILED', 'Corrupted or mismatched group ciphertext.');\n\t}\n}\n\n/**\n * Ed25519 over domain || SHA-256(meta.json) || SHA-256(data.enc): the two\n * digests cover everything a reader trusts (recipients, author claim, iv,\n * ciphertext). Returns the raw 64-byte sig.bin entry.\n */\nexport async function signBox(\n\tmetaBytes: Uint8Array,\n\tdataEnc: Uint8Array,\n\tsigPrivB64: string\n): Promise<Uint8Array> {\n\tawait requireCurves();\n\tconst msg = concat(utf8.encode(DOMAIN_BOX), await sha256(metaBytes), await sha256(dataEnc));\n\tconst key = await importSigPriv(sigPrivB64);\n\treturn new Uint8Array(await crypto.subtle.sign('Ed25519', key, src(msg)));\n}\n\n/** False on any failure (bad key, bad bytes); the caller maps it to SIGNATURE_INVALID. */\nexport async function verifyBox(\n\tmetaBytes: Uint8Array,\n\tdataEnc: Uint8Array,\n\tsig: Uint8Array,\n\tsigPubB64: string\n): Promise<boolean> {\n\ttry {\n\t\tawait requireCurves();\n\t\tconst msg = concat(utf8.encode(DOMAIN_BOX), await sha256(metaBytes), await sha256(dataEnc));\n\t\treturn await crypto.subtle.verify('Ed25519', await importSigPub(sigPubB64), src(sig), src(msg));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Sign a manifest with the admin key, over the exact payload bytes. */\nexport async function signManifest(\n\tmanifest: GroupManifest,\n\tadminSigPrivB64: string\n): Promise<SignedManifest> {\n\tawait requireCurves();\n\tconst payload = utf8.encode(JSON.stringify(manifest));\n\tconst key = await importSigPriv(adminSigPrivB64);\n\tconst msg = concat(utf8.encode(DOMAIN_MANIFEST), payload);\n\tconst sig = new Uint8Array(await crypto.subtle.sign('Ed25519', key, src(msg)));\n\treturn { selfstoreManifest: 1, payload: toBase64(payload), sig: toBase64(sig) };\n}\n\n/**\n * Verify and parse a signed manifest against the pinned admin key (TOFU from\n * the invite). Rollback (seq) is enforced by the store, which persists the\n * high-water mark.\n */\nexport async function openManifest(\n\tsigned: SignedManifest,\n\tadminSigPubB64: string,\n\texpectedGroupB64?: string\n): Promise<GroupManifest> {\n\tawait requireCurves();\n\tconst bad = (why: string): SelfstoreError => new SelfstoreError('SIGNATURE_INVALID', why);\n\tif (!signed || signed.selfstoreManifest !== 1 || typeof signed.payload !== 'string') {\n\t\tthrow bad('Not a signed selfstore manifest.');\n\t}\n\tlet payload: Uint8Array;\n\tlet ok = false;\n\ttry {\n\t\tpayload = fromBase64(signed.payload);\n\t\tconst msg = concat(utf8.encode(DOMAIN_MANIFEST), payload);\n\t\tok = await crypto.subtle.verify(\n\t\t\t'Ed25519',\n\t\t\tawait importSigPub(adminSigPubB64),\n\t\t\tsrc(fromBase64(signed.sig ?? '')),\n\t\t\tsrc(msg)\n\t\t);\n\t} catch {\n\t\tthrow bad('Malformed manifest signature or keys.');\n\t}\n\tif (!ok) throw bad('Manifest signature invalid for the pinned admin key.');\n\tconst m = JSON.parse(new TextDecoder().decode(payload)) as GroupManifest;\n\t// Reject malformed member keys here, not later at import time.\n\tconst is32ByteKey = (b64: string): boolean => {\n\t\ttry {\n\t\t\treturn fromBase64(b64).length === CURVE_PUB_BYTES;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t};\n\tconst shapeOk =\n\t\tm &&\n\t\tm.v === 1 &&\n\t\ttypeof m.group === 'string' &&\n\t\tNumber.isInteger(m.seq) &&\n\t\tm.seq >= 0 &&\n\t\tm.admin === adminSigPubB64 &&\n\t\tArray.isArray(m.members) &&\n\t\tm.members.length <= MAX_RECIPIENTS &&\n\t\tm.members.every(\n\t\t\t(mem) =>\n\t\t\t\tmem &&\n\t\t\t\ttypeof mem.id === 'string' &&\n\t\t\t\tmem.id.length > 0 &&\n\t\t\t\tis32ByteKey(mem.sig) &&\n\t\t\t\tis32ByteKey(mem.enc)\n\t\t);\n\tif (!shapeOk) throw bad('Manifest shape invalid or admin key mismatch.');\n\t// A member id resolves an author to a person; duplicates make that ambiguous.\n\tif (new Set(m.members.map((mem) => mem.id)).size !== m.members.length) {\n\t\tthrow bad('Manifest has duplicate member ids.');\n\t}\n\t// An admin who reuses one key across groups must not get a manifest for\n\t// group Y accepted where X is meant.\n\tif (expectedGroupB64 !== undefined && m.group !== expectedGroupB64) {\n\t\tthrow bad('Manifest is for a different group than expected.');\n\t}\n\treturn m;\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/persistence/cache-crypto.ts
|
|
2
|
+
function isEnvelope(v) {
|
|
3
|
+
return typeof v === "object" && v !== null && v.__enc === 1;
|
|
4
|
+
}
|
|
5
|
+
function newDeviceKey() {
|
|
6
|
+
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
7
|
+
}
|
|
8
|
+
async function seal(key, plain) {
|
|
9
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
10
|
+
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plain);
|
|
11
|
+
return { __enc: 1, iv: iv.buffer, ct };
|
|
12
|
+
}
|
|
13
|
+
async function unseal(key, env) {
|
|
14
|
+
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: env.iv }, key, env.ct);
|
|
15
|
+
return new Uint8Array(plain);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { isEnvelope, newDeviceKey, seal, unseal };
|
|
19
|
+
//# sourceMappingURL=chunk-QBCU7OOP.js.map
|
|
20
|
+
//# sourceMappingURL=chunk-QBCU7OOP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/persistence/cache-crypto.ts"],"names":[],"mappings":";AAcO,SAAS,WAAW,CAAA,EAA8B;AACxD,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,IAAA,IAAS,EAA0B,KAAA,KAAU,CAAA;AACpF;AAGO,SAAS,YAAA,GAAmC;AAClD,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,WAAA,CAAY,EAAE,IAAA,EAAM,SAAA,EAAW,MAAA,EAAQ,GAAA,EAAI,EAAG,KAAA,EAAO,CAAC,SAAA,EAAW,SAAS,CAAC,CAAA;AACjG;AAGA,eAAsB,IAAA,CAAK,KAAgB,KAAA,EAAyC;AACnF,EAAA,MAAM,KAAK,MAAA,CAAO,eAAA,CAAgB,IAAI,UAAA,CAAW,EAAE,CAAC,CAAA;AACpD,EAAA,MAAM,EAAA,GAAK,MAAM,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,EAAE,IAAA,EAAM,SAAA,EAAW,EAAA,EAAG,EAAG,GAAA,EAAK,KAAqB,CAAA;AAC1F,EAAA,OAAO,EAAE,KAAA,EAAO,CAAA,EAAG,EAAA,EAAI,EAAA,CAAG,QAAQ,EAAA,EAAG;AACtC;AAKA,eAAsB,MAAA,CAAO,KAAgB,GAAA,EAAuC;AACnF,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,CAAO,MAAA,CAAO,QAAQ,EAAE,IAAA,EAAM,SAAA,EAAW,EAAA,EAAI,GAAA,CAAI,EAAA,EAAG,EAAG,GAAA,EAAK,IAAI,EAAE,CAAA;AACtF,EAAA,OAAO,IAAI,WAAW,KAAK,CAAA;AAC5B","file":"chunk-QBCU7OOP.js","sourcesContent":["// At-rest encryption for the local cache: AES-256-GCM, fresh IV per record,\n// under a non-extractable device key kept next to the data. Defeats casual\n// inspection, partial exfiltration, and disk forensics of the store; does not\n// stop code running in the origin or a copy of the whole browser profile\n// (the key travels with it). No IndexedDB in here - unit-tests on WebCrypto.\n\n/** An encrypted record: the GCM IV plus the ciphertext, both raw bytes. */\nexport interface EncEnvelope {\n\t__enc: 1;\n\tiv: ArrayBuffer;\n\tct: ArrayBuffer;\n}\n\n/** Whether a stored value is one of our encrypted envelopes. */\nexport function isEnvelope(v: unknown): v is EncEnvelope {\n\treturn typeof v === 'object' && v !== null && (v as { __enc?: unknown }).__enc === 1;\n}\n\n/** Fresh non-extractable AES-256-GCM device key - its bytes can never be read back out. */\nexport function newDeviceKey(): Promise<CryptoKey> {\n\treturn crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);\n}\n\n/** Encrypt bytes into an envelope with a fresh 96-bit IV. */\nexport async function seal(key: CryptoKey, plain: Uint8Array): Promise<EncEnvelope> {\n\tconst iv = crypto.getRandomValues(new Uint8Array(12));\n\tconst ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plain as BufferSource);\n\treturn { __enc: 1, iv: iv.buffer, ct };\n}\n\n/** Decrypt an envelope. Throws on wrong key or tampered bytes - let it\n * propagate, a decrypt failure is never \"no data\" (that would risk\n * overwriting recoverable ciphertext). */\nexport async function unseal(key: CryptoKey, env: EncEnvelope): Promise<Uint8Array> {\n\tconst plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: env.iv }, key, env.ct);\n\treturn new Uint8Array(plain);\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SelfstoreError, isSelfstoreError, errorLabelKey } from './chunk-VBFMMR7Q.js';
|
|
2
|
+
|
|
3
|
+
// src/flows/machine.ts
|
|
4
|
+
function makeMachine(initial) {
|
|
5
|
+
let snapshot = initial;
|
|
6
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
return {
|
|
8
|
+
get snapshot() {
|
|
9
|
+
return snapshot;
|
|
10
|
+
},
|
|
11
|
+
set(patch) {
|
|
12
|
+
snapshot = { ...snapshot, ...patch };
|
|
13
|
+
listeners.forEach((fn) => fn(snapshot));
|
|
14
|
+
},
|
|
15
|
+
subscribe(listener) {
|
|
16
|
+
listeners.add(listener);
|
|
17
|
+
listener(snapshot);
|
|
18
|
+
return () => listeners.delete(listener);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function withDeadline(work, ms, what) {
|
|
23
|
+
let timer;
|
|
24
|
+
const gate = new Promise((_resolve, reject) => {
|
|
25
|
+
timer = setTimeout(
|
|
26
|
+
() => reject(new SelfstoreError("TARGET_UNAVAILABLE", `${what} did not answer within ${ms}ms.`)),
|
|
27
|
+
ms
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
return await Promise.race([work, gate]);
|
|
32
|
+
} finally {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function toStoreError(e) {
|
|
37
|
+
if (isSelfstoreError(e)) {
|
|
38
|
+
return { code: e.code, labelKey: errorLabelKey(e.code), message: e.message };
|
|
39
|
+
}
|
|
40
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
41
|
+
return { code: "TARGET_UNAVAILABLE", labelKey: errorLabelKey("TARGET_UNAVAILABLE"), message };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { makeMachine, toStoreError, withDeadline };
|
|
45
|
+
//# sourceMappingURL=chunk-SGISYMT5.js.map
|
|
46
|
+
//# sourceMappingURL=chunk-SGISYMT5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/flows/machine.ts"],"names":[],"mappings":";;;AAuBO,SAAS,YAA8B,OAAA,EAAwB;AACrE,EAAA,IAAI,QAAA,GAAW,OAAA;AACf,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,OAAO;AAAA,IACN,IAAI,QAAA,GAAc;AACjB,MAAA,OAAO,QAAA;AAAA,IACR,CAAA;AAAA,IACA,IAAI,KAAA,EAAyB;AAC5B,MAAA,QAAA,GAAW,EAAE,GAAG,QAAA,EAAU,GAAG,KAAA,EAAM;AACnC,MAAA,SAAA,CAAU,OAAA,CAAQ,CAAC,EAAA,KAAO,EAAA,CAAG,QAAQ,CAAC,CAAA;AAAA,IACvC,CAAA;AAAA,IACA,UAAU,QAAA,EAAsC;AAC/C,MAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,MAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,MAAA,OAAO,MAAM,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,IACvC;AAAA,GACD;AACD;AAOA,eAAsB,YAAA,CAAgB,IAAA,EAAkB,EAAA,EAAY,IAAA,EAA0B;AAC7F,EAAA,IAAI,KAAA;AACJ,EAAA,MAAM,IAAA,GAAO,IAAI,OAAA,CAAe,CAAC,UAAU,MAAA,KAAW;AACrD,IAAA,KAAA,GAAQ,UAAA;AAAA,MACP,MAAM,MAAA,CAAO,IAAI,cAAA,CAAe,oBAAA,EAAsB,GAAG,IAAI,CAAA,uBAAA,EAA0B,EAAE,CAAA,GAAA,CAAK,CAAC,CAAA;AAAA,MAC/F;AAAA,KACD;AAAA,EACD,CAAC,CAAA;AACD,EAAA,IAAI;AACH,IAAA,OAAO,MAAM,OAAA,CAAQ,IAAA,CAAK,CAAC,IAAA,EAAM,IAAI,CAAC,CAAA;AAAA,EACvC,CAAA,SAAE;AACD,IAAA,YAAA,CAAa,KAAK,CAAA;AAAA,EACnB;AACD;AAMO,SAAS,aAAa,CAAA,EAAwB;AACpD,EAAA,IAAI,gBAAA,CAAiB,CAAC,CAAA,EAAG;AACxB,IAAA,OAAO,EAAE,IAAA,EAAM,CAAA,CAAE,IAAA,EAAM,QAAA,EAAU,aAAA,CAAc,CAAA,CAAE,IAAI,CAAA,EAAG,OAAA,EAAS,CAAA,CAAE,OAAA,EAAQ;AAAA,EAC5E;AACA,EAAA,MAAM,UAAU,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,OAAO,CAAC,CAAA;AACzD,EAAA,OAAO,EAAE,IAAA,EAAM,oBAAA,EAAsB,UAAU,aAAA,CAAc,oBAAoB,GAAG,OAAA,EAAQ;AAC7F","file":"chunk-SGISYMT5.js","sourcesContent":["// Shared plumbing for the flows: a tiny observable snapshot (fits the\n// Svelte store contract and React's useSyncExternalStore), a deadline\n// wrapper for network legs, and the normalizer that turns anything thrown\n// into the stable { code, labelKey, message } shape.\n\nimport type { StoreError } from '../persistence/store';\nimport { SelfstoreError, errorLabelKey, isSelfstoreError } from '../selfstore/errors';\n\n/** What every flow exposes: the current snapshot plus a subscription.\n * `subscribe` calls the listener IMMEDIATELY with the current snapshot and on\n * every change after - that is the Svelte store contract, so a flow works as\n * `$flow` in Svelte; React reads `useSyncExternalStore(flow.subscribe, () =>\n * flow.snapshot)`; everyone else just re-renders in the callback. */\nexport interface FlowStore<T> {\n\treadonly snapshot: T;\n\tsubscribe(listener: (snapshot: T) => void): () => void;\n}\n\nexport interface Machine<T> extends FlowStore<T> {\n\t/** Merge a partial into the snapshot and notify. */\n\tset(patch: Partial<T>): void;\n}\n\nexport function makeMachine<T extends object>(initial: T): Machine<T> {\n\tlet snapshot = initial;\n\tconst listeners = new Set<(s: T) => void>();\n\treturn {\n\t\tget snapshot(): T {\n\t\t\treturn snapshot;\n\t\t},\n\t\tset(patch: Partial<T>): void {\n\t\t\tsnapshot = { ...snapshot, ...patch };\n\t\t\tlisteners.forEach((fn) => fn(snapshot));\n\t\t},\n\t\tsubscribe(listener: (s: T) => void): () => void {\n\t\t\tlisteners.add(listener);\n\t\t\tlistener(snapshot);\n\t\t\treturn () => listeners.delete(listener);\n\t\t}\n\t};\n}\n\n/** Bound a network leg so a stuck destination surfaces as a retryable error\n * instead of a flow frozen on a spinner. The rejection is TARGET_UNAVAILABLE -\n * transient by contract - because \"did not answer in time\" says nothing about\n * access being lost. Never wrap a human step (a consent popup, a picker):\n * people are allowed to be slower than a deadline. */\nexport async function withDeadline<T>(work: Promise<T>, ms: number, what: string): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst gate = new Promise<never>((_resolve, reject) => {\n\t\ttimer = setTimeout(\n\t\t\t() => reject(new SelfstoreError('TARGET_UNAVAILABLE', `${what} did not answer within ${ms}ms.`)),\n\t\t\tms\n\t\t);\n\t});\n\ttry {\n\t\treturn await Promise.race([work, gate]);\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\n/** Normalize anything thrown into the stable StoreError shape the whole\n * library reports with: show `labelKey`, log `message`. Unknown errors map to\n * TARGET_UNAVAILABLE (transient, retryable) - the flow never invents new\n * codes for the app to branch on. */\nexport function toStoreError(e: unknown): StoreError {\n\tif (isSelfstoreError(e)) {\n\t\treturn { code: e.code, labelKey: errorLabelKey(e.code), message: e.message };\n\t}\n\tconst message = e instanceof Error ? e.message : String(e);\n\treturn { code: 'TARGET_UNAVAILABLE', labelKey: errorLabelKey('TARGET_UNAVAILABLE'), message };\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/selfstore/errors.ts
|
|
2
|
+
function errorLabelKey(code) {
|
|
3
|
+
return "error." + code.toLowerCase().replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
|
|
4
|
+
}
|
|
5
|
+
var SelfstoreError = class extends Error {
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "SelfstoreError";
|
|
10
|
+
}
|
|
11
|
+
code;
|
|
12
|
+
};
|
|
13
|
+
var AuthExpiredError = class extends SelfstoreError {
|
|
14
|
+
constructor(message = "Access to the destination expired or was revoked.") {
|
|
15
|
+
super("AUTH_EXPIRED", message);
|
|
16
|
+
this.name = "AuthExpiredError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function isSelfstoreError(e) {
|
|
20
|
+
return e instanceof Error && typeof e.code === "string" && (e.name === "SelfstoreError" || e.name === "AuthExpiredError");
|
|
21
|
+
}
|
|
22
|
+
function isAuthExpired(e) {
|
|
23
|
+
if (typeof e !== "object" || e === null) return false;
|
|
24
|
+
return e.code === "AUTH_EXPIRED";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { AuthExpiredError, SelfstoreError, errorLabelKey, isAuthExpired, isSelfstoreError };
|
|
28
|
+
//# sourceMappingURL=chunk-VBFMMR7Q.js.map
|
|
29
|
+
//# sourceMappingURL=chunk-VBFMMR7Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/selfstore/errors.ts"],"names":[],"mappings":";AAiCO,SAAS,cAAc,IAAA,EAAkC;AAC/D,EAAA,OAAO,QAAA,GAAW,IAAA,CAAK,WAAA,EAAY,CAAE,OAAA,CAAQ,WAAA,EAAa,CAAC,EAAA,EAAI,CAAA,KAAc,CAAA,CAAE,WAAA,EAAa,CAAA;AAC7F;AAEO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA,EACzC,WAAA,CACU,MACT,OAAA,EACC;AACD,IAAA,KAAA,CAAM,OAAO,CAAA;AAHJ,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAIT,IAAA,IAAA,CAAK,IAAA,GAAO,gBAAA;AAAA,EACb;AAAA,EALU,IAAA;AAMX;AASO,IAAM,gBAAA,GAAN,cAA+B,cAAA,CAAe;AAAA,EACpD,WAAA,CAAY,UAAU,mDAAA,EAAqD;AAC1E,IAAA,KAAA,CAAM,gBAAgB,OAAO,CAAA;AAC7B,IAAA,IAAA,CAAK,IAAA,GAAO,kBAAA;AAAA,EACb;AACD;AAMO,SAAS,iBAAiB,CAAA,EAAiC;AACjE,EAAA,OACC,CAAA,YAAa,KAAA,IACb,OAAQ,CAAA,CAAyB,IAAA,KAAS,aACzC,CAAA,CAAE,IAAA,KAAS,gBAAA,IAAoB,CAAA,CAAE,IAAA,KAAS,kBAAA,CAAA;AAE7C;AAGO,SAAS,cAAc,CAAA,EAAqB;AAClD,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,MAAM,OAAO,KAAA;AAChD,EAAA,OAAQ,EAAyB,IAAA,KAAS,cAAA;AAC3C","file":"chunk-VBFMMR7Q.js","sourcesContent":["// One stable error-code enum for the whole library, format layer and\n// persistence layer alike. Codes may be added in a minor release (keep a\n// default branch in exhaustive switches); existing codes never change meaning.\n\nexport type SelfstoreErrorCode =\n\t// backup format / crypto\n\t| 'BAD_FORMAT' // not a backup file / corrupt framing\n\t| 'UNSUPPORTED_VERSION' // file (or cipher/KDF) from a newer format version\n\t| 'PASSWORD_REQUIRED' // encrypted backup opened without a password, or vault used before unlock()\n\t| 'DECRYPT_FAILED' // wrong password, or tampered/corrupt ciphertext\n\t| 'TOO_LARGE' // archive entry exceeds the size guard (zip-bomb defence)\n\t// durable targets / store lifecycle\n\t| 'AUTH_EXPIRED' // access genuinely lost: only a user gesture (reconnect) fixes it\n\t| 'TARGET_UNAVAILABLE' // transient: offline, cold start, 5xx - retry later\n\t| 'TARGET_WRITE_FAILED' // destination refused or failed the write (non-auth), retryable\n\t| 'TARGET_GONE' // destination permanently unwritable (file deleted, permission withdrawn, storage full); retrying cannot fix it\n\t| 'NOT_CONNECTED' // no connected destination (no file id, no config)\n\t| 'WEAK_PASSWORD' // password fails the store's configured passwordPolicy\n\t| 'ENCRYPTION_REQUIRED' // store set requireEncryption: a plaintext attach, backup or export is refused\n\t| 'UNEXPECTEDLY_UNENCRYPTED' // downgrade guard: expected encrypted, found plaintext\n\t| 'SCHEMA_TOO_NEW' // data written by a newer app schema: update the app to sync again\n\t// passwordless groups (see PEERS.md)\n\t| 'IDENTITY_REQUIRED' // group-encrypted copy opened without a member identity\n\t| 'SIGNATURE_INVALID' // copy/manifest signature failed, or author not a trusted member\n\t| 'NOT_A_RECIPIENT' // valid group copy, but no envelope for this identity\n\t| 'MANIFEST_ROLLBACK'; // membership manifest older than one already applied\n\n/**\n * i18n key for an error code: lower-camel-cased under `error.`, e.g.\n * 'error.authExpired'. The library ships the key, the app owns the copy;\n * consumers never have to show the raw developer message. Deterministic, so\n * codes added later get a key for free - keep a fallback for unknown ones.\n */\nexport function errorLabelKey(code: SelfstoreErrorCode): string {\n\treturn 'error.' + code.toLowerCase().replace(/_([a-z])/g, (_m, c: string) => c.toUpperCase());\n}\n\nexport class SelfstoreError extends Error {\n\tconstructor(\n\t\treadonly code: SelfstoreErrorCode,\n\t\tmessage: string\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'SelfstoreError';\n\t}\n}\n\n/**\n * Genuine loss of access to a destination (session gone, token revoked):\n * only a user gesture fixes it. Anything else a target throws is treated as\n * transient and retried, so a network hiccup never raises the blocking\n * reconnect gate over a still-valid connection. Custom targets and DriveAuth\n * implementations throw this to signal the genuine case.\n */\nexport class AuthExpiredError extends SelfstoreError {\n\tconstructor(message = 'Access to the destination expired or was revoked.') {\n\t\tsuper('AUTH_EXPIRED', message);\n\t\tthis.name = 'AuthExpiredError';\n\t}\n}\n\n/**\n * Shape check rather than instanceof, so it survives two bundled copies of\n * the library. The name check keeps foreign coded errors (ENOENT...) out.\n */\nexport function isSelfstoreError(e: unknown): e is SelfstoreError {\n\treturn (\n\t\te instanceof Error &&\n\t\ttypeof (e as { code?: unknown }).code === 'string' &&\n\t\t(e.name === 'SelfstoreError' || e.name === 'AuthExpiredError')\n\t);\n}\n\n/** True on genuine access loss, false on transient blips. */\nexport function isAuthExpired(e: unknown): boolean {\n\tif (typeof e !== 'object' || e === null) return false;\n\treturn (e as { code?: unknown }).code === 'AUTH_EXPIRED';\n}\n"]}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { B as BackupTarget, g as PeerSource } from './store-ChIGEOph.js';
|
|
2
|
+
import { K as KV } from './cache-De5_sALA.js';
|
|
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
|
+
/**
|
|
113
|
+
* Google Drive backup target. Does the Drive REST file operations (find-or-create,
|
|
114
|
+
* upload, download) for a single backup file. It only ships bytes; the store
|
|
115
|
+
* decides whether they are encrypted. How an access token is obtained (a browser
|
|
116
|
+
* OAuth flow, a backend broker, ...) is injected via DriveAuth, so this target
|
|
117
|
+
* stays free of any specific auth strategy or backend.
|
|
118
|
+
*
|
|
119
|
+
* Beyond the standard connect/fromSession lifecycle, this module exposes
|
|
120
|
+
* shared-file primitives (preview, adopt, findOrCreateOwnFile) for apps whose
|
|
121
|
+
* users point several devices - or several people - at one Drive-hosted backup
|
|
122
|
+
* file: preview it read-only, then adopt it as this device's backup.
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/** How the target gets access. Injected, so the auth strategy lives in the app.
|
|
126
|
+
* Error protocol: a genuine loss of access (the session is gone, re-consent
|
|
127
|
+
* needed) must throw AuthExpiredError - or any error with code 'AUTH_EXPIRED' -
|
|
128
|
+
* while a transient offline/cold-start failure throws a plain error, so the
|
|
129
|
+
* store retries it instead of raising the reconnect gate. */
|
|
130
|
+
interface DriveAuth {
|
|
131
|
+
/** A fresh access token, or throw if access must be re-established. Pass
|
|
132
|
+
* `{ forceRefresh: true }` to bypass any cached token and mint a new one:
|
|
133
|
+
* the target calls this after a 401 to tell a merely STALE token (a refresh
|
|
134
|
+
* fixes it, no gate) from a genuinely lost session (the refresh 401s too).
|
|
135
|
+
* An implementation that cannot force a refresh may ignore the flag; the
|
|
136
|
+
* target then retries with the same token, no worse than before. */
|
|
137
|
+
token(opts?: {
|
|
138
|
+
forceRefresh?: boolean;
|
|
139
|
+
}): Promise<string>;
|
|
140
|
+
/** Re-establish access with a user gesture; resolves to whether it worked. */
|
|
141
|
+
reconnect(): Promise<boolean>;
|
|
142
|
+
/** Forget the connection, locally and server-side. */
|
|
143
|
+
forget(): Promise<void>;
|
|
144
|
+
}
|
|
145
|
+
interface DriveOptions {
|
|
146
|
+
auth: DriveAuth;
|
|
147
|
+
kv: KV;
|
|
148
|
+
/** Name of the backup file created in the user's Drive. */
|
|
149
|
+
fileName: string;
|
|
150
|
+
}
|
|
151
|
+
/** First-time connect: authorize (a user gesture), then find-or-create the file.
|
|
152
|
+
* Returns the target, or null if the user cancelled the consent. */
|
|
153
|
+
declare function connect(opts: DriveOptions): Promise<BackupTarget | null>;
|
|
154
|
+
/** Rebuild the target from a past session's saved connection (no UI), or null. */
|
|
155
|
+
declare function fromSession(opts: DriveOptions): Promise<BackupTarget | null>;
|
|
156
|
+
/** A read-only-intent preview of one specific (typically shared) file, before
|
|
157
|
+
* adopting it. Writes NOTHING to the KV: until the user confirms, the device's
|
|
158
|
+
* own backup wiring must stay untouched. */
|
|
159
|
+
declare function preview(opts: DriveOptions, fileId: string): BackupTarget;
|
|
160
|
+
/** Adopt a specific (typically shared) file as this device's backup. Persists
|
|
161
|
+
* the file id, so next sessions restore it like any Drive backup. */
|
|
162
|
+
declare function adopt(opts: DriveOptions, fileId: string): Promise<BackupTarget>;
|
|
163
|
+
/** Find (or create) the user's own backup file, ignoring the currently adopted
|
|
164
|
+
* id (which may point at someone else's shared file). Reuses the live session
|
|
165
|
+
* when possible and only falls back to the consent popup. Adopts NOTHING: the
|
|
166
|
+
* caller decides whether to load the file or start it blank. Null when the
|
|
167
|
+
* consent is refused. */
|
|
168
|
+
declare function findOrCreateOwnFile(opts: DriveOptions): Promise<{
|
|
169
|
+
fileId: string;
|
|
170
|
+
created: boolean;
|
|
171
|
+
} | null>;
|
|
172
|
+
/** One row of `listBackups()`: what Drive reports about a file the app can
|
|
173
|
+
* see. Whether the backup is encrypted is not here on purpose - that lives
|
|
174
|
+
* inside the file (inspect it, or remember it app-side); a listing must stay
|
|
175
|
+
* one cheap metadata call, never N downloads. */
|
|
176
|
+
interface DriveBackupInfo {
|
|
177
|
+
id: string;
|
|
178
|
+
name: string;
|
|
179
|
+
/** RFC 3339 timestamp of the last write, as Drive reports it. */
|
|
180
|
+
modifiedTime: string | null;
|
|
181
|
+
/** Size in bytes, when Drive reports one. */
|
|
182
|
+
size: number | null;
|
|
183
|
+
}
|
|
184
|
+
/** List the files this app can see on the user's own Drive (the drive.file
|
|
185
|
+
* scope already narrows visibility to files the app created or was handed).
|
|
186
|
+
* `nameContains` narrows server-side; selfstore ships no naming convention -
|
|
187
|
+
* the app applies its own exact rules on what comes back (its backups, its
|
|
188
|
+
* internal files). Newest first. */
|
|
189
|
+
declare function listBackups(opts: {
|
|
190
|
+
auth: DriveAuth;
|
|
191
|
+
nameContains?: string;
|
|
192
|
+
}): Promise<DriveBackupInfo[]>;
|
|
193
|
+
/** Create a brand-new empty backup file with this exact name and answer its
|
|
194
|
+
* id. Refuses when a non-trashed owned file already carries the name: Drive
|
|
195
|
+
* happily stores duplicate names, and a duplicate would poison every
|
|
196
|
+
* find-by-name reconnect that follows. Adopts nothing - the caller decides
|
|
197
|
+
* to attach it (typically with `wipe: true` so it starts blank). */
|
|
198
|
+
declare function createBackup(opts: {
|
|
199
|
+
auth: DriveAuth;
|
|
200
|
+
fileName: string;
|
|
201
|
+
}): Promise<{
|
|
202
|
+
fileId: string;
|
|
203
|
+
}>;
|
|
204
|
+
/** Delete a backup file from Drive, for good (not the trash: the file's data
|
|
205
|
+
* must actually be gone, not lingering). A 404 is success - deleting what is
|
|
206
|
+
* already gone is not an error. The caller owns the guard against deleting
|
|
207
|
+
* the currently attached file. */
|
|
208
|
+
declare function deleteBackup(opts: {
|
|
209
|
+
auth: DriveAuth;
|
|
210
|
+
fileId: string;
|
|
211
|
+
}): Promise<void>;
|
|
212
|
+
/** Rename a backup file (metadata only - the id, the content and every open
|
|
213
|
+
* target bound to it are untouched). Refuses when another owned file already
|
|
214
|
+
* carries the new name, for the same reason createBackup does: a duplicate
|
|
215
|
+
* poisons find-by-name. A 404 means the file is gone, not a success here. */
|
|
216
|
+
declare function renameBackup(opts: {
|
|
217
|
+
auth: DriveAuth;
|
|
218
|
+
fileId: string;
|
|
219
|
+
fileName: string;
|
|
220
|
+
}): Promise<void>;
|
|
221
|
+
|
|
222
|
+
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 };
|