selfstore 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/dist/advanced.d.ts +90 -0
  4. package/dist/advanced.js +41 -0
  5. package/dist/advanced.js.map +1 -0
  6. package/dist/backups.d.ts +182 -0
  7. package/dist/backups.js +360 -0
  8. package/dist/backups.js.map +1 -0
  9. package/dist/cache-De5_sALA.d.ts +129 -0
  10. package/dist/cache-lock-LZILZNLP.js +23 -0
  11. package/dist/cache-lock-LZILZNLP.js.map +1 -0
  12. package/dist/chunk-ATUPHVLG.js +441 -0
  13. package/dist/chunk-ATUPHVLG.js.map +1 -0
  14. package/dist/chunk-BJ3RUMGI.js +45 -0
  15. package/dist/chunk-BJ3RUMGI.js.map +1 -0
  16. package/dist/chunk-CUWB5EWF.js +732 -0
  17. package/dist/chunk-CUWB5EWF.js.map +1 -0
  18. package/dist/chunk-FDPAKHQS.js +230 -0
  19. package/dist/chunk-FDPAKHQS.js.map +1 -0
  20. package/dist/chunk-IBIEKK36.js +331 -0
  21. package/dist/chunk-IBIEKK36.js.map +1 -0
  22. package/dist/chunk-JB4JHM7V.js +474 -0
  23. package/dist/chunk-JB4JHM7V.js.map +1 -0
  24. package/dist/chunk-N4RPMX3Y.js +1908 -0
  25. package/dist/chunk-N4RPMX3Y.js.map +1 -0
  26. package/dist/chunk-OWLX6PNT.js +231 -0
  27. package/dist/chunk-OWLX6PNT.js.map +1 -0
  28. package/dist/chunk-QBCU7OOP.js +20 -0
  29. package/dist/chunk-QBCU7OOP.js.map +1 -0
  30. package/dist/chunk-SGISYMT5.js +46 -0
  31. package/dist/chunk-SGISYMT5.js.map +1 -0
  32. package/dist/chunk-VBFMMR7Q.js +29 -0
  33. package/dist/chunk-VBFMMR7Q.js.map +1 -0
  34. package/dist/drive-Bv94UM8b.d.ts +222 -0
  35. package/dist/flows.d.ts +306 -0
  36. package/dist/flows.js +153 -0
  37. package/dist/flows.js.map +1 -0
  38. package/dist/groups.d.ts +39 -0
  39. package/dist/groups.js +103 -0
  40. package/dist/groups.js.map +1 -0
  41. package/dist/households.d.ts +214 -0
  42. package/dist/households.js +410 -0
  43. package/dist/households.js.map +1 -0
  44. package/dist/index.d.ts +205 -0
  45. package/dist/index.js +249 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/kdf-worker.js +2395 -0
  48. package/dist/kdf-worker.js.map +1 -0
  49. package/dist/local-2py_vGGy.d.ts +47 -0
  50. package/dist/local-AM22RWRR.js +7 -0
  51. package/dist/local-AM22RWRR.js.map +1 -0
  52. package/dist/machine-ALMkT3mk.d.ts +17 -0
  53. package/dist/store-ChIGEOph.d.ts +577 -0
  54. package/dist/sync.d.ts +107 -0
  55. package/dist/sync.js +3 -0
  56. package/dist/sync.js.map +1 -0
  57. package/dist/widgets.d.ts +426 -0
  58. package/dist/widgets.js +2579 -0
  59. package/dist/widgets.js.map +1 -0
  60. package/llms.txt +405 -0
  61. package/package.json +99 -0
@@ -0,0 +1,732 @@
1
+ import { seal, isEnvelope, unseal, newDeviceKey } from './chunk-QBCU7OOP.js';
2
+ import { AuthExpiredError, SelfstoreError, isAuthExpired } from './chunk-VBFMMR7Q.js';
3
+
4
+ // src/persistence/targets/drive.ts
5
+ var FILE_ID_KEY = "driveFileId";
6
+ var DRIVE_UPLOAD = "https://www.googleapis.com/upload/drive/v3/files";
7
+ var DRIVE_FILES = "https://www.googleapis.com/drive/v3/files";
8
+ var DATA_DEADLINE_MS = 3e4;
9
+ var META_DEADLINE_MS = 15e3;
10
+ async function driveFetch(auth, run) {
11
+ const r = await run(await auth.token());
12
+ if (r.status !== 401) return r;
13
+ return run(await auth.token({ forceRefresh: true }));
14
+ }
15
+ async function findExistingFile(token, fileNames) {
16
+ for (const name of fileNames) {
17
+ const safe = name.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
18
+ const q = encodeURIComponent(`name='${safe}' and trashed=false and 'me' in owners`);
19
+ const r = await fetch(`${DRIVE_FILES}?q=${q}&fields=files(id)&pageSize=1`, {
20
+ headers: { Authorization: `Bearer ${token}` },
21
+ signal: AbortSignal.timeout(META_DEADLINE_MS)
22
+ });
23
+ if (!r.ok) continue;
24
+ const data = await r.json();
25
+ const id = data.files?.[0]?.id;
26
+ if (id) return id;
27
+ }
28
+ return null;
29
+ }
30
+ function isPermanentDriveWrite(status, body) {
31
+ if (status === 404) return true;
32
+ if (status === 403) return !/ratelimit|dailylimit/i.test(body);
33
+ return false;
34
+ }
35
+ async function createFile(token, fileName) {
36
+ const boundary = "SLFS_BOUNDARY";
37
+ const meta = JSON.stringify({ name: fileName, mimeType: "application/octet-stream" });
38
+ const body = [
39
+ `--${boundary}\r
40
+ Content-Type: application/json; charset=UTF-8\r
41
+ \r
42
+ `,
43
+ meta,
44
+ `\r
45
+ --${boundary}\r
46
+ Content-Type: application/octet-stream\r
47
+ \r
48
+ \r
49
+ --${boundary}--`
50
+ ].join("");
51
+ const r = await fetch(`${DRIVE_UPLOAD}?uploadType=multipart`, {
52
+ method: "POST",
53
+ headers: {
54
+ Authorization: `Bearer ${token}`,
55
+ "Content-Type": `multipart/related; boundary=${boundary}`
56
+ },
57
+ body,
58
+ signal: AbortSignal.timeout(META_DEADLINE_MS)
59
+ });
60
+ if (!r.ok) throw new SelfstoreError("TARGET_WRITE_FAILED", `Drive create failed: ${r.status}`);
61
+ const data = await r.json();
62
+ return data.id;
63
+ }
64
+ function makeTarget(opts, fixedId) {
65
+ const { auth, kv } = opts;
66
+ let captured = fixedId;
67
+ const fileIdOf = async () => captured ??= await kv.get(FILE_ID_KEY);
68
+ return {
69
+ kind: "drive",
70
+ label: "Google Drive",
71
+ async save(blob) {
72
+ const fileId = await fileIdOf();
73
+ if (!fileId) throw new SelfstoreError("NOT_CONNECTED", "Drive not connected.");
74
+ const r = await driveFetch(
75
+ auth,
76
+ (token) => fetch(`${DRIVE_UPLOAD}/${fileId}?uploadType=media&fields=version`, {
77
+ method: "PATCH",
78
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/octet-stream" },
79
+ body: blob,
80
+ signal: AbortSignal.timeout(DATA_DEADLINE_MS)
81
+ })
82
+ );
83
+ if (!r.ok) {
84
+ if (r.status === 401) throw new AuthExpiredError("Drive rejected the token (401).");
85
+ const body = await r.text().catch(() => "");
86
+ if (isPermanentDriveWrite(r.status, body)) {
87
+ throw new SelfstoreError(
88
+ "TARGET_GONE",
89
+ `Drive backup unwritable (${r.status}): the file is gone or access was withdrawn.`
90
+ );
91
+ }
92
+ throw new SelfstoreError("TARGET_WRITE_FAILED", `Drive upload failed: ${r.status}`);
93
+ }
94
+ try {
95
+ const data = await r.json();
96
+ return data.version != null ? String(data.version) : null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ },
101
+ async load() {
102
+ const fileId = await fileIdOf();
103
+ if (!fileId) return null;
104
+ const r = await driveFetch(
105
+ auth,
106
+ (token) => fetch(`${DRIVE_FILES}/${fileId}?alt=media`, {
107
+ headers: { Authorization: `Bearer ${token}` },
108
+ signal: AbortSignal.timeout(DATA_DEADLINE_MS)
109
+ })
110
+ );
111
+ if (r.status === 404) return null;
112
+ if (r.status === 401) throw new AuthExpiredError("Drive rejected the token (401).");
113
+ if (!r.ok) {
114
+ throw new SelfstoreError("TARGET_UNAVAILABLE", `Drive download failed: ${r.status}`);
115
+ }
116
+ return r.blob();
117
+ },
118
+ async stat() {
119
+ try {
120
+ const fileId = await fileIdOf();
121
+ if (!fileId) return null;
122
+ const r = await driveFetch(
123
+ auth,
124
+ (token) => fetch(`${DRIVE_FILES}/${fileId}?fields=version`, {
125
+ headers: { Authorization: `Bearer ${token}` },
126
+ signal: AbortSignal.timeout(META_DEADLINE_MS)
127
+ })
128
+ );
129
+ if (!r.ok) return null;
130
+ const data = await r.json();
131
+ return data.version != null ? String(data.version) : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ },
136
+ async isReady() {
137
+ try {
138
+ await auth.token();
139
+ return true;
140
+ } catch (e) {
141
+ if (isAuthExpired(e)) throw e;
142
+ return false;
143
+ }
144
+ },
145
+ async reconnect() {
146
+ return auth.reconnect();
147
+ },
148
+ async disconnect() {
149
+ await auth.forget();
150
+ const current = await kv.get(FILE_ID_KEY);
151
+ const own = await fileIdOf();
152
+ if (!current || !own || current === own) await kv.del(FILE_ID_KEY);
153
+ }
154
+ };
155
+ }
156
+ async function connect(opts) {
157
+ if (!await opts.auth.reconnect()) return null;
158
+ const token = await opts.auth.token();
159
+ let fileId = await opts.kv.get(FILE_ID_KEY);
160
+ if (!fileId) {
161
+ fileId = await findExistingFile(token, [opts.fileName]) ?? await createFile(token, opts.fileName);
162
+ await opts.kv.set(FILE_ID_KEY, fileId);
163
+ }
164
+ return makeTarget(opts);
165
+ }
166
+ async function fromSession(opts) {
167
+ return await opts.kv.get(FILE_ID_KEY) ? makeTarget(opts) : null;
168
+ }
169
+ function preview(opts, fileId) {
170
+ return makeTarget(opts, fileId);
171
+ }
172
+ async function adopt(opts, fileId) {
173
+ await opts.kv.set(FILE_ID_KEY, fileId);
174
+ return makeTarget(opts);
175
+ }
176
+ async function findOrCreateOwnFile(opts) {
177
+ let token;
178
+ try {
179
+ token = await opts.auth.token();
180
+ } catch {
181
+ if (!await opts.auth.reconnect()) return null;
182
+ token = await opts.auth.token();
183
+ }
184
+ const existing = await findExistingFile(token, [opts.fileName]);
185
+ if (existing) return { fileId: existing, created: false };
186
+ return { fileId: await createFile(token, opts.fileName), created: true };
187
+ }
188
+ async function listBackups(opts) {
189
+ const clauses = ["trashed=false", "'me' in owners"];
190
+ if (opts.nameContains) {
191
+ const safe = opts.nameContains.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
192
+ clauses.push(`name contains '${safe}'`);
193
+ }
194
+ const q = encodeURIComponent(clauses.join(" and "));
195
+ const fields = encodeURIComponent("files(id,name,modifiedTime,size)");
196
+ const r = await driveFetch(
197
+ opts.auth,
198
+ (token) => fetch(`${DRIVE_FILES}?q=${q}&fields=${fields}&pageSize=100&orderBy=modifiedTime%20desc`, {
199
+ headers: { Authorization: `Bearer ${token}` },
200
+ signal: AbortSignal.timeout(META_DEADLINE_MS)
201
+ })
202
+ );
203
+ if (r.status === 401) throw new AuthExpiredError("Drive rejected the token (401).");
204
+ if (!r.ok) throw new SelfstoreError("TARGET_UNAVAILABLE", `Drive list failed: ${r.status}`);
205
+ const data = await r.json();
206
+ return (data.files ?? []).map((f) => ({
207
+ id: f.id,
208
+ name: f.name,
209
+ modifiedTime: f.modifiedTime ?? null,
210
+ size: f.size != null ? Number(f.size) : null
211
+ }));
212
+ }
213
+ async function createBackup(opts) {
214
+ const token = await opts.auth.token();
215
+ const existing = await findExistingFile(token, [opts.fileName]);
216
+ if (existing) {
217
+ throw new SelfstoreError(
218
+ "TARGET_WRITE_FAILED",
219
+ `A file named "${opts.fileName}" already exists on Drive.`
220
+ );
221
+ }
222
+ return { fileId: await createFile(token, opts.fileName) };
223
+ }
224
+ async function deleteBackup(opts) {
225
+ const r = await driveFetch(
226
+ opts.auth,
227
+ (token) => fetch(`${DRIVE_FILES}/${opts.fileId}`, {
228
+ method: "DELETE",
229
+ headers: { Authorization: `Bearer ${token}` },
230
+ signal: AbortSignal.timeout(META_DEADLINE_MS)
231
+ })
232
+ );
233
+ if (r.status === 401) throw new AuthExpiredError("Drive rejected the token (401).");
234
+ if (r.status === 404) return;
235
+ if (!r.ok) throw new SelfstoreError("TARGET_WRITE_FAILED", `Drive delete failed: ${r.status}`);
236
+ }
237
+ async function renameBackup(opts) {
238
+ const token = await opts.auth.token();
239
+ const existing = await findExistingFile(token, [opts.fileName]);
240
+ if (existing && existing !== opts.fileId) {
241
+ throw new SelfstoreError(
242
+ "TARGET_WRITE_FAILED",
243
+ `A file named "${opts.fileName}" already exists on Drive.`
244
+ );
245
+ }
246
+ if (existing === opts.fileId) return;
247
+ const r = await driveFetch(
248
+ opts.auth,
249
+ (token2) => fetch(`${DRIVE_FILES}/${opts.fileId}?fields=id`, {
250
+ method: "PATCH",
251
+ headers: { Authorization: `Bearer ${token2}`, "Content-Type": "application/json" },
252
+ body: JSON.stringify({ name: opts.fileName }),
253
+ signal: AbortSignal.timeout(META_DEADLINE_MS)
254
+ })
255
+ );
256
+ if (r.status === 401) throw new AuthExpiredError("Drive rejected the token (401).");
257
+ if (!r.ok) throw new SelfstoreError("TARGET_WRITE_FAILED", `Drive rename failed: ${r.status}`);
258
+ }
259
+
260
+ // src/persistence/targets/file.ts
261
+ var HANDLE_KEY = "fileHandle";
262
+ function isSupported() {
263
+ return typeof window !== "undefined" && "showSaveFilePicker" in window;
264
+ }
265
+ function isPermissionLoss(e) {
266
+ return e instanceof DOMException && (e.name === "NotAllowedError" || e.name === "SecurityError");
267
+ }
268
+ function fromHandle(handle, kv) {
269
+ return {
270
+ kind: "file",
271
+ label: handle.name,
272
+ async save(blob) {
273
+ try {
274
+ const writable = await handle.createWritable();
275
+ await writable.write(blob);
276
+ await writable.close();
277
+ } catch (e) {
278
+ if (isPermissionLoss(e)) {
279
+ throw new AuthExpiredError("File permission was revoked.");
280
+ }
281
+ throw new SelfstoreError("TARGET_WRITE_FAILED", `File write failed: ${String(e)}`);
282
+ }
283
+ try {
284
+ return String((await handle.getFile()).lastModified);
285
+ } catch {
286
+ return null;
287
+ }
288
+ },
289
+ async load() {
290
+ return handle.getFile();
291
+ },
292
+ async stat() {
293
+ try {
294
+ return String((await handle.getFile()).lastModified);
295
+ } catch {
296
+ return null;
297
+ }
298
+ },
299
+ async isReady() {
300
+ return await handle.queryPermission({ mode: "readwrite" }) === "granted";
301
+ },
302
+ async reconnect() {
303
+ return await handle.requestPermission({ mode: "readwrite" }) === "granted";
304
+ },
305
+ async disconnect() {
306
+ await kv.del(HANDLE_KEY);
307
+ }
308
+ };
309
+ }
310
+ async function connect2(opts) {
311
+ const picker = window.showSaveFilePicker;
312
+ if (!picker) return null;
313
+ try {
314
+ const handle = await picker({
315
+ suggestedName: opts.fileName,
316
+ types: [{ description: "Backup", accept: { "application/zip": [".zip"] } }]
317
+ });
318
+ await opts.kv.set(HANDLE_KEY, handle);
319
+ return fromHandle(handle, opts.kv);
320
+ } catch (e) {
321
+ if (e instanceof DOMException && e.name === "AbortError") return null;
322
+ throw e;
323
+ }
324
+ }
325
+ async function openExisting(opts) {
326
+ const picker = window.showOpenFilePicker;
327
+ if (!picker) return null;
328
+ try {
329
+ const [handle] = await picker({
330
+ multiple: false,
331
+ types: [{ description: "Backup", accept: { "application/zip": [".zip"] } }]
332
+ });
333
+ if (!handle) return null;
334
+ if (await handle.requestPermission({ mode: "readwrite" }) !== "granted") return null;
335
+ await opts.kv.set(HANDLE_KEY, handle);
336
+ return fromHandle(handle, opts.kv);
337
+ } catch (e) {
338
+ if (e instanceof DOMException && e.name === "AbortError") return null;
339
+ throw e;
340
+ }
341
+ }
342
+ async function fromSession2(opts) {
343
+ const handle = await opts.kv.get(HANDLE_KEY);
344
+ return handle ? fromHandle(handle, opts.kv) : null;
345
+ }
346
+
347
+ // src/persistence/targets/webdav.ts
348
+ var CONFIG_KEY = "webdavConfig";
349
+ var KEY_KEY = "webdavKey";
350
+ var DATA_DEADLINE_MS2 = 3e4;
351
+ var META_DEADLINE_MS2 = 15e3;
352
+ async function configKey(kv) {
353
+ const existing = await kv.get(KEY_KEY);
354
+ if (existing) return existing;
355
+ const fresh = await newDeviceKey();
356
+ await kv.set(KEY_KEY, fresh);
357
+ return fresh;
358
+ }
359
+ async function saveConfig(kv, c) {
360
+ const bytes = new TextEncoder().encode(JSON.stringify(c));
361
+ await kv.set(CONFIG_KEY, await seal(await configKey(kv), bytes));
362
+ }
363
+ async function loadConfig(kv) {
364
+ const stored = await kv.get(CONFIG_KEY);
365
+ if (!isEnvelope(stored)) return null;
366
+ const bytes = await unseal(await configKey(kv), stored);
367
+ return JSON.parse(new TextDecoder().decode(bytes));
368
+ }
369
+ function labelOf(url) {
370
+ try {
371
+ return new URL(url).host;
372
+ } catch {
373
+ return "WebDAV";
374
+ }
375
+ }
376
+ function assertSecureUrl(url) {
377
+ let u;
378
+ try {
379
+ u = new URL(url);
380
+ } catch {
381
+ throw new TypeError(`webdavTarget: invalid URL "${url}".`);
382
+ }
383
+ const loopback = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
384
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && loopback)) {
385
+ throw new TypeError(
386
+ `webdavTarget: refusing "${u.protocol}" - Basic-auth credentials need https (http is allowed only for localhost).`
387
+ );
388
+ }
389
+ }
390
+ function versionOf(res) {
391
+ return res.headers.get("ETag") ?? res.headers.get("Last-Modified") ?? null;
392
+ }
393
+ function fromConfig(c, kv) {
394
+ const headers = { Authorization: "Basic " + btoa(`${c.username}:${c.password}`) };
395
+ async function probe() {
396
+ try {
397
+ return await fetch(c.url, {
398
+ method: "HEAD",
399
+ headers,
400
+ signal: AbortSignal.timeout(META_DEADLINE_MS2)
401
+ });
402
+ } catch {
403
+ return null;
404
+ }
405
+ }
406
+ async function stat() {
407
+ const res = await probe();
408
+ return res && res.ok ? versionOf(res) : null;
409
+ }
410
+ async function ready() {
411
+ const res = await probe();
412
+ if (!res) return false;
413
+ if (res.status === 401 || res.status === 403) {
414
+ throw new AuthExpiredError(`WebDAV rejected the credentials (${res.status}).`);
415
+ }
416
+ return res.ok || res.status === 404;
417
+ }
418
+ async function reconnectProbe() {
419
+ const res = await probe();
420
+ return !!res && (res.ok || res.status === 404);
421
+ }
422
+ return {
423
+ kind: "webdav",
424
+ label: labelOf(c.url),
425
+ async save(blob) {
426
+ const res = await fetch(c.url, {
427
+ method: "PUT",
428
+ headers,
429
+ body: blob,
430
+ signal: AbortSignal.timeout(DATA_DEADLINE_MS2)
431
+ });
432
+ if (res.status === 401 || res.status === 403) {
433
+ throw new AuthExpiredError(`WebDAV rejected the credentials (${res.status}).`);
434
+ }
435
+ if (!res.ok) throw new SelfstoreError("TARGET_WRITE_FAILED", `WebDAV PUT failed: ${res.status}`);
436
+ return versionOf(res) ?? await stat();
437
+ },
438
+ async load() {
439
+ const res = await fetch(c.url, {
440
+ method: "GET",
441
+ headers,
442
+ signal: AbortSignal.timeout(DATA_DEADLINE_MS2)
443
+ });
444
+ if (res.status === 404) return null;
445
+ if (res.status === 401 || res.status === 403) {
446
+ throw new AuthExpiredError(`WebDAV rejected the credentials (${res.status}).`);
447
+ }
448
+ if (!res.ok) throw new SelfstoreError("TARGET_UNAVAILABLE", `WebDAV GET failed: ${res.status}`);
449
+ return res.blob();
450
+ },
451
+ stat,
452
+ isReady: ready,
453
+ reconnect: reconnectProbe,
454
+ // stored credentials: re-check reachability, no gesture
455
+ async disconnect() {
456
+ await kv.del(CONFIG_KEY);
457
+ await kv.del(KEY_KEY);
458
+ }
459
+ };
460
+ }
461
+ function peer(opts) {
462
+ if (opts.username !== void 0) assertSecureUrl(opts.url);
463
+ const headers = opts.username !== void 0 ? { Authorization: "Basic " + btoa(`${opts.username}:${opts.password ?? ""}`) } : void 0;
464
+ return {
465
+ label: opts.label ?? labelOf(opts.url),
466
+ async load() {
467
+ const res = await fetch(opts.url, {
468
+ method: "GET",
469
+ headers,
470
+ signal: AbortSignal.timeout(DATA_DEADLINE_MS2)
471
+ });
472
+ if (res.status === 404) return null;
473
+ if (res.status === 401 || res.status === 403) {
474
+ throw new AuthExpiredError(`WebDAV peer rejected the credentials (${res.status}).`);
475
+ }
476
+ if (!res.ok) {
477
+ throw new SelfstoreError("TARGET_UNAVAILABLE", `WebDAV peer GET failed: ${res.status}`);
478
+ }
479
+ return res.blob();
480
+ },
481
+ async stat() {
482
+ try {
483
+ const res = await fetch(opts.url, {
484
+ method: "HEAD",
485
+ headers,
486
+ signal: AbortSignal.timeout(META_DEADLINE_MS2)
487
+ });
488
+ return res.ok ? versionOf(res) : null;
489
+ } catch {
490
+ return null;
491
+ }
492
+ }
493
+ };
494
+ }
495
+ async function connect3(opts) {
496
+ assertSecureUrl(opts.config.url);
497
+ const target = fromConfig(opts.config, opts.kv);
498
+ let ok = false;
499
+ try {
500
+ ok = await target.isReady();
501
+ } catch {
502
+ ok = false;
503
+ }
504
+ if (!ok) return null;
505
+ await saveConfig(opts.kv, opts.config);
506
+ return target;
507
+ }
508
+ async function fromSession3(opts) {
509
+ const config = await loadConfig(opts.kv);
510
+ return config ? fromConfig(config, opts.kv) : null;
511
+ }
512
+
513
+ // src/persistence/targets/sigv4.ts
514
+ var encoder = new TextEncoder();
515
+ function hex(bytes) {
516
+ let out = "";
517
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
518
+ return out;
519
+ }
520
+ async function sha256Hex(bytes) {
521
+ return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
522
+ }
523
+ var EMPTY_PAYLOAD_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
524
+ async function hmac(key, data) {
525
+ const k = await crypto.subtle.importKey(
526
+ "raw",
527
+ key,
528
+ { name: "HMAC", hash: "SHA-256" },
529
+ false,
530
+ ["sign"]
531
+ );
532
+ return new Uint8Array(await crypto.subtle.sign("HMAC", k, data));
533
+ }
534
+ function encodeSegment(s) {
535
+ return encodeURIComponent(s).replace(
536
+ /[!*'()]/g,
537
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
538
+ );
539
+ }
540
+ function encodeS3Path(path) {
541
+ return path.split("/").map((seg) => encodeSegment(seg)).join("/");
542
+ }
543
+ function basicAmzDate() {
544
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
545
+ }
546
+ async function signS3(input) {
547
+ const service = input.service ?? "s3";
548
+ const amzDate = input.amzDate ?? basicAmzDate();
549
+ const dateStamp = amzDate.slice(0, 8);
550
+ const { host } = new URL(input.origin);
551
+ const canonicalUri = encodeS3Path(input.path);
552
+ const headers = {
553
+ host,
554
+ "x-amz-content-sha256": input.payloadHashHex,
555
+ "x-amz-date": amzDate
556
+ };
557
+ for (const [name, value] of Object.entries({})) {
558
+ headers[name.toLowerCase()] = value;
559
+ }
560
+ const signedNames = Object.keys(headers).sort();
561
+ const canonicalHeaders = signedNames.map((n) => `${n}:${headers[n].trim()}
562
+ `).join("");
563
+ const signedHeaders = signedNames.join(";");
564
+ const canonicalRequest = [
565
+ input.method,
566
+ canonicalUri,
567
+ "",
568
+ // no query string on any of our single-object requests
569
+ canonicalHeaders,
570
+ signedHeaders,
571
+ input.payloadHashHex
572
+ ].join("\n");
573
+ const scope = `${dateStamp}/${input.region}/${service}/aws4_request`;
574
+ const stringToSign = [
575
+ "AWS4-HMAC-SHA256",
576
+ amzDate,
577
+ scope,
578
+ await sha256Hex(encoder.encode(canonicalRequest))
579
+ ].join("\n");
580
+ const kDate = await hmac(encoder.encode("AWS4" + input.secretAccessKey), encoder.encode(dateStamp));
581
+ const kRegion = await hmac(kDate, encoder.encode(input.region));
582
+ const kService = await hmac(kRegion, encoder.encode(service));
583
+ const kSigning = await hmac(kService, encoder.encode("aws4_request"));
584
+ const signature = hex(await hmac(kSigning, encoder.encode(stringToSign)));
585
+ const authorization = `AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
586
+ const sendHeaders = { authorization };
587
+ for (const n of signedNames) if (n !== "host") sendHeaders[n] = headers[n];
588
+ return { url: input.origin.replace(/\/$/, "") + canonicalUri, headers: sendHeaders };
589
+ }
590
+
591
+ // src/persistence/targets/s3.ts
592
+ var CONFIG_KEY2 = "s3Config";
593
+ var KEY_KEY2 = "s3Key";
594
+ var DATA_DEADLINE_MS3 = 3e4;
595
+ var META_DEADLINE_MS3 = 15e3;
596
+ async function configKey2(kv) {
597
+ const existing = await kv.get(KEY_KEY2);
598
+ if (existing) return existing;
599
+ const fresh = await newDeviceKey();
600
+ await kv.set(KEY_KEY2, fresh);
601
+ return fresh;
602
+ }
603
+ async function saveConfig2(kv, c) {
604
+ const bytes = new TextEncoder().encode(JSON.stringify(c));
605
+ await kv.set(CONFIG_KEY2, await seal(await configKey2(kv), bytes));
606
+ }
607
+ async function loadConfig2(kv) {
608
+ const stored = await kv.get(CONFIG_KEY2);
609
+ if (!isEnvelope(stored)) return null;
610
+ const bytes = await unseal(await configKey2(kv), stored);
611
+ return JSON.parse(new TextDecoder().decode(bytes));
612
+ }
613
+ function assertSecureEndpoint(endpoint) {
614
+ let u;
615
+ try {
616
+ u = new URL(endpoint);
617
+ } catch {
618
+ throw new TypeError(`s3Target: invalid endpoint "${endpoint}".`);
619
+ }
620
+ const loopback = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
621
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && loopback)) {
622
+ throw new TypeError(
623
+ `s3Target: refusing "${u.protocol}" - S3 requests need https (http is allowed only for localhost).`
624
+ );
625
+ }
626
+ return u;
627
+ }
628
+ function labelOf2(c) {
629
+ try {
630
+ return `${c.bucket} (${new URL(c.endpoint).host})`;
631
+ } catch {
632
+ return c.bucket || "S3";
633
+ }
634
+ }
635
+ function versionOf2(res) {
636
+ return res.headers.get("ETag") ?? res.headers.get("Last-Modified") ?? null;
637
+ }
638
+ function fromConfig2(c, kv) {
639
+ const endpointUrl = assertSecureEndpoint(c.endpoint);
640
+ const pathStyle = c.forcePathStyle ?? true;
641
+ const origin = pathStyle ? `${endpointUrl.protocol}//${endpointUrl.host}` : `${endpointUrl.protocol}//${c.bucket}.${endpointUrl.host}`;
642
+ const objectPath = pathStyle ? `/${c.bucket}/${c.key}` : `/${c.key}`;
643
+ async function request(method, payloadHashHex, body, extraHeaders, deadlineMs = DATA_DEADLINE_MS3) {
644
+ const signed = await signS3({
645
+ method,
646
+ origin,
647
+ path: objectPath,
648
+ region: c.region,
649
+ accessKeyId: c.accessKeyId,
650
+ secretAccessKey: c.secretAccessKey,
651
+ payloadHashHex});
652
+ return fetch(signed.url, {
653
+ method,
654
+ headers: signed.headers,
655
+ body,
656
+ signal: AbortSignal.timeout(deadlineMs)
657
+ });
658
+ }
659
+ async function probe() {
660
+ try {
661
+ return await request("HEAD", EMPTY_PAYLOAD_SHA256, void 0, void 0, META_DEADLINE_MS3);
662
+ } catch {
663
+ return null;
664
+ }
665
+ }
666
+ async function stat() {
667
+ const res = await probe();
668
+ return res && res.ok ? versionOf2(res) : null;
669
+ }
670
+ async function ready() {
671
+ const res = await probe();
672
+ if (!res) return false;
673
+ if (res.status === 403) {
674
+ throw new AuthExpiredError("S3 rejected the credentials (403).");
675
+ }
676
+ return res.ok || res.status === 404;
677
+ }
678
+ async function reconnectProbe() {
679
+ const res = await probe();
680
+ return !!res && (res.ok || res.status === 404);
681
+ }
682
+ return {
683
+ kind: "s3",
684
+ label: labelOf2(c),
685
+ async save(blob) {
686
+ const bytes = new Uint8Array(await blob.arrayBuffer());
687
+ const res = await request("PUT", await sha256Hex(bytes), bytes);
688
+ if (res.status === 403) {
689
+ throw new AuthExpiredError("S3 rejected the credentials (403).");
690
+ }
691
+ if (!res.ok) throw new SelfstoreError("TARGET_WRITE_FAILED", `S3 PUT failed: ${res.status}`);
692
+ return versionOf2(res) ?? await stat();
693
+ },
694
+ async load() {
695
+ const res = await request("GET", EMPTY_PAYLOAD_SHA256);
696
+ if (res.status === 404) return null;
697
+ if (res.status === 403) {
698
+ throw new AuthExpiredError("S3 rejected the credentials (403).");
699
+ }
700
+ if (!res.ok) throw new SelfstoreError("TARGET_UNAVAILABLE", `S3 GET failed: ${res.status}`);
701
+ return res.blob();
702
+ },
703
+ stat,
704
+ isReady: ready,
705
+ reconnect: reconnectProbe,
706
+ async disconnect() {
707
+ await kv.del(CONFIG_KEY2);
708
+ await kv.del(KEY_KEY2);
709
+ }
710
+ };
711
+ }
712
+ async function connect4(opts) {
713
+ assertSecureEndpoint(opts.config.endpoint);
714
+ const target = fromConfig2(opts.config, opts.kv);
715
+ let ok = false;
716
+ try {
717
+ ok = await target.isReady();
718
+ } catch {
719
+ ok = false;
720
+ }
721
+ if (!ok) return null;
722
+ await saveConfig2(opts.kv, opts.config);
723
+ return target;
724
+ }
725
+ async function fromSession4(opts) {
726
+ const config = await loadConfig2(opts.kv);
727
+ return config ? fromConfig2(config, opts.kv) : null;
728
+ }
729
+
730
+ export { FILE_ID_KEY, adopt, connect, connect2, connect3, connect4, createBackup, deleteBackup, findOrCreateOwnFile, fromSession, fromSession2, fromSession3, fromSession4, isSupported, listBackups, openExisting, peer, preview, renameBackup };
731
+ //# sourceMappingURL=chunk-CUWB5EWF.js.map
732
+ //# sourceMappingURL=chunk-CUWB5EWF.js.map