@cancia/astro 0.10.0 → 0.12.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 (44) hide show
  1. package/dist/chunk-4PYVIIOO.js +25 -0
  2. package/dist/{chunk-DIV2FFYX.js → chunk-5R3I5RLT.js} +15 -8
  3. package/dist/chunk-6GUOGDWO.js +172 -0
  4. package/dist/chunk-DEI3FBNY.js +65 -0
  5. package/dist/{chunk-ONKCUTPU.js → chunk-DOFLZ7KG.js} +6 -7
  6. package/dist/{chunk-GJIX7MWC.js → chunk-ECPYHIAW.js} +9 -1
  7. package/dist/{chunk-YGJ3JHXF.js → chunk-QQEUCOXQ.js} +11 -0
  8. package/dist/{chunk-NVXNJ4YC.js → chunk-R7VA3Z5N.js} +53 -11
  9. package/dist/{chunk-5IPHDIC6.js → chunk-RHFTQW4W.js} +10 -1
  10. package/dist/{chunk-2NVZWZPE.js → chunk-S5YLB6P3.js} +310 -7
  11. package/dist/{chunk-LJSSPOSW.js → chunk-VF3EXXEM.js} +139 -16
  12. package/dist/chunk-XINNLVZP.js +183 -0
  13. package/dist/chunk-XZCXWILA.js +145 -0
  14. package/dist/content.js +3 -3
  15. package/dist/{upload-DwCGjXbz.d.ts → email-BRK4WAO2.d.ts} +9 -1
  16. package/dist/endpoints/auth-callback.d.ts +1 -0
  17. package/dist/endpoints/auth-callback.js +9 -0
  18. package/dist/endpoints/auth-logout.d.ts +1 -0
  19. package/dist/endpoints/auth-logout.js +9 -0
  20. package/dist/endpoints/auth-request.d.ts +18 -0
  21. package/dist/endpoints/auth-request.js +9 -0
  22. package/dist/endpoints/auth-session.d.ts +1 -0
  23. package/dist/endpoints/auth-session.js +9 -0
  24. package/dist/endpoints/auth.js +10 -2
  25. package/dist/endpoints/content.js +12 -12
  26. package/dist/endpoints/lists.js +4 -1
  27. package/dist/endpoints/publish.js +8 -6
  28. package/dist/endpoints/schemas.js +6 -3
  29. package/dist/endpoints/upload.js +11 -10
  30. package/dist/{git-backed-DtiH52EI.d.ts → git-backed-CwTFfUS4.d.ts} +2 -2
  31. package/dist/{github-client-Db0pIrqE.d.ts → github-client-Db5EPcSL.d.ts} +1 -1
  32. package/dist/index.d.ts +32 -6
  33. package/dist/index.js +125 -35
  34. package/dist/loader/index.d.ts +34 -4
  35. package/dist/loader/index.js +3 -3
  36. package/dist/runtime.d.ts +69 -3
  37. package/dist/runtime.js +6 -4
  38. package/dist/schema/index.d.ts +14 -1
  39. package/dist/schema/index.js +1 -1
  40. package/dist/storage/index.d.ts +4 -4
  41. package/dist/storage/index.js +5 -5
  42. package/dist/{types-BMlLS-OS.d.ts → types-CKzfz5iu.d.ts} +1 -1
  43. package/package.json +1 -1
  44. package/dist/chunk-MXVOJRAM.js +0 -504
@@ -339,15 +339,318 @@ function createJsonFileAdapterV2(opts = {}) {
339
339
  };
340
340
  }
341
341
 
342
+ // src/storage/sqlite-v2.ts
343
+ import { createRequire } from "module";
344
+ import { randomUUID as randomUUID2 } from "crypto";
345
+ import { mkdirSync as mkdirSync3 } from "fs";
346
+ import { dirname as dirname3 } from "path";
347
+ var require2 = createRequire(import.meta.url);
348
+ function openDB(dbPath) {
349
+ const Database = require2("better-sqlite3");
350
+ if (dbPath !== ":memory:") mkdirSync3(dirname3(dbPath), { recursive: true });
351
+ const db = new Database(dbPath);
352
+ db.pragma("journal_mode = WAL");
353
+ db.pragma("foreign_keys = ON");
354
+ db.exec(`
355
+ CREATE TABLE IF NOT EXISTS kv (
356
+ site TEXT NOT NULL, key TEXT NOT NULL, lang TEXT NOT NULL, value TEXT NOT NULL,
357
+ PRIMARY KEY (site, key, lang)
358
+ );
359
+ CREATE TABLE IF NOT EXISTS pages (
360
+ site TEXT NOT NULL, route TEXT NOT NULL, meta TEXT NOT NULL,
361
+ PRIMARY KEY (site, route)
362
+ );
363
+ CREATE TABLE IF NOT EXISTS list_entries (
364
+ site TEXT NOT NULL, list TEXT NOT NULL, id TEXT NOT NULL, locale TEXT NOT NULL,
365
+ data TEXT NOT NULL,
366
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
367
+ PRIMARY KEY (site, list, id, locale)
368
+ );
369
+ CREATE TABLE IF NOT EXISTS list_order (
370
+ site TEXT NOT NULL, list TEXT NOT NULL, ids TEXT NOT NULL,
371
+ PRIMARY KEY (site, list)
372
+ );
373
+ `);
374
+ return db;
375
+ }
376
+ function makeKVStore(db) {
377
+ const getStmt = db.prepare("SELECT value FROM kv WHERE site=? AND key=? AND lang=?");
378
+ const setStmt = db.prepare(
379
+ `INSERT INTO kv (site, key, lang, value) VALUES (?, ?, ?, ?)
380
+ ON CONFLICT(site, key, lang) DO UPDATE SET value=excluded.value`
381
+ );
382
+ const getAllStmt = db.prepare("SELECT key, lang, value FROM kv WHERE site=?");
383
+ const delStmt = db.prepare("DELETE FROM kv WHERE site=? AND key=? AND lang=?");
384
+ return {
385
+ async get(site, key, lang) {
386
+ const row = getStmt.get(site, key, lang);
387
+ return row?.value ?? null;
388
+ },
389
+ async set(site, key, lang, value) {
390
+ setStmt.run(site, key, lang, value);
391
+ },
392
+ async getAll(site) {
393
+ const rows = getAllStmt.all(site);
394
+ const out = {};
395
+ for (const r of rows) out[`${r.key}.${r.lang}`] = r.value;
396
+ return out;
397
+ },
398
+ async delete(site, key, lang) {
399
+ delStmt.run(site, key, lang);
400
+ }
401
+ };
402
+ }
403
+ function makePageStore2(db) {
404
+ const getStmt = db.prepare("SELECT meta FROM pages WHERE site=? AND route=?");
405
+ const listStmt = db.prepare("SELECT route, meta FROM pages WHERE site=?");
406
+ const upsertStmt = db.prepare(
407
+ `INSERT INTO pages (site, route, meta) VALUES (?, ?, ?)
408
+ ON CONFLICT(site, route) DO UPDATE SET meta=excluded.meta`
409
+ );
410
+ const delStmt = db.prepare("DELETE FROM pages WHERE site=? AND route=?");
411
+ function parseMeta(raw) {
412
+ return JSON.parse(raw);
413
+ }
414
+ return {
415
+ async get(site, route) {
416
+ const row = getStmt.get(site, route);
417
+ if (!row) return null;
418
+ const meta = parseMeta(row.meta);
419
+ return { route, meta, _rev: hashRev(meta) };
420
+ },
421
+ async list(site) {
422
+ const rows = listStmt.all(site);
423
+ return rows.map((r) => {
424
+ const meta = parseMeta(r.meta);
425
+ return { route: r.route, meta, _rev: hashRev(meta) };
426
+ });
427
+ },
428
+ async set(site, route, meta, rev) {
429
+ const existing = getStmt.get(site, route);
430
+ if (existing) {
431
+ const currentRev = hashRev(parseMeta(existing.meta));
432
+ if (rev !== currentRev) throw new RevConflictError();
433
+ }
434
+ upsertStmt.run(site, route, JSON.stringify(meta));
435
+ return { route, meta, _rev: hashRev(meta) };
436
+ },
437
+ async delete(site, route) {
438
+ delStmt.run(site, route);
439
+ }
440
+ };
441
+ }
442
+ function rowToEntry(row) {
443
+ const data = JSON.parse(row.data);
444
+ return {
445
+ id: row.id,
446
+ locale: row.locale,
447
+ data,
448
+ createdAt: row.created_at,
449
+ updatedAt: row.updated_at,
450
+ _rev: hashRev(data)
451
+ };
452
+ }
453
+ function makeListStore2(db) {
454
+ const getOrderStmt = db.prepare("SELECT ids FROM list_order WHERE site=? AND list=?");
455
+ const upsertOrderStmt = db.prepare(
456
+ `INSERT INTO list_order (site, list, ids) VALUES (?, ?, ?)
457
+ ON CONFLICT(site, list) DO UPDATE SET ids=excluded.ids`
458
+ );
459
+ const getEntryStmt = db.prepare(
460
+ "SELECT id, locale, data, created_at, updated_at FROM list_entries WHERE site=? AND list=? AND id=? AND locale=?"
461
+ );
462
+ const allEntriesStmt = db.prepare(
463
+ "SELECT id, locale, data, created_at, updated_at FROM list_entries WHERE site=? AND list=? ORDER BY locale"
464
+ );
465
+ const localeEntriesStmt = db.prepare(
466
+ "SELECT id, locale, data, created_at, updated_at FROM list_entries WHERE site=? AND list=? AND locale=?"
467
+ );
468
+ const insertEntryStmt = db.prepare(
469
+ `INSERT INTO list_entries (site, list, id, locale, data, created_at, updated_at)
470
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
471
+ );
472
+ const updateEntryStmt = db.prepare(
473
+ "UPDATE list_entries SET data=?, updated_at=? WHERE site=? AND list=? AND id=? AND locale=?"
474
+ );
475
+ const deleteEntryStmt = db.prepare(
476
+ "DELETE FROM list_entries WHERE site=? AND list=? AND id=? AND locale=?"
477
+ );
478
+ const deleteEntryAllLocalesStmt = db.prepare(
479
+ "DELETE FROM list_entries WHERE site=? AND list=? AND id=?"
480
+ );
481
+ const distinctIdsStmt = db.prepare(
482
+ "SELECT DISTINCT id FROM list_entries WHERE site=? AND list=?"
483
+ );
484
+ function readOrder2(site, listName) {
485
+ const row = getOrderStmt.get(site, listName);
486
+ if (!row) return [];
487
+ return JSON.parse(row.ids);
488
+ }
489
+ function writeOrder2(site, listName, ids) {
490
+ upsertOrderStmt.run(site, listName, JSON.stringify(ids));
491
+ }
492
+ function distinctIds(site, listName) {
493
+ const rows = distinctIdsStmt.all(site, listName);
494
+ return new Set(rows.map((r) => r.id));
495
+ }
496
+ const createTx = db.transaction(
497
+ (site, listName, id, locale, dataJson, now) => {
498
+ insertEntryStmt.run(site, listName, id, locale, dataJson, now, now);
499
+ const order = readOrder2(site, listName);
500
+ const next = orderAfterCreate(order, id);
501
+ if (next !== order) writeOrder2(site, listName, next);
502
+ }
503
+ );
504
+ const deleteTx = db.transaction((site, listName, id, locale) => {
505
+ deleteEntryStmt.run(site, listName, id, locale);
506
+ const stillExists = distinctIds(site, listName).has(id);
507
+ const order = readOrder2(site, listName);
508
+ const next = orderAfterDelete(order, id, stillExists);
509
+ if (next !== order) writeOrder2(site, listName, next);
510
+ });
511
+ const reorderTx = db.transaction(
512
+ (site, listName, order, dropped) => {
513
+ for (const id of dropped) {
514
+ deleteEntryAllLocalesStmt.run(site, listName, id);
515
+ }
516
+ writeOrder2(site, listName, order);
517
+ }
518
+ );
519
+ return {
520
+ async list(site, listName, locale) {
521
+ const order = readOrder2(site, listName);
522
+ if (locale !== void 0) {
523
+ const rows2 = localeEntriesStmt.all(site, listName, locale);
524
+ const byId2 = /* @__PURE__ */ new Map();
525
+ for (const r of rows2) byId2.set(r.id, r);
526
+ const sorted = applyOrder(order, new Set(byId2.keys()));
527
+ const entries2 = [];
528
+ for (const id of sorted) {
529
+ const r = byId2.get(id);
530
+ if (r) entries2.push(rowToEntry(r));
531
+ }
532
+ return entries2;
533
+ }
534
+ const rows = allEntriesStmt.all(site, listName);
535
+ const byId = /* @__PURE__ */ new Map();
536
+ const allIds = /* @__PURE__ */ new Set();
537
+ for (const r of rows) {
538
+ allIds.add(r.id);
539
+ const list = byId.get(r.id) ?? [];
540
+ list.push(r);
541
+ byId.set(r.id, list);
542
+ }
543
+ const sortedIds = applyOrder(order, allIds);
544
+ const entries = [];
545
+ for (const id of sortedIds) {
546
+ const group = byId.get(id) ?? [];
547
+ for (const r of group) entries.push(rowToEntry(r));
548
+ }
549
+ return entries;
550
+ },
551
+ async get(site, listName, id, locale) {
552
+ const row = getEntryStmt.get(site, listName, id, locale);
553
+ return row ? rowToEntry(row) : null;
554
+ },
555
+ async create(site, listName, data, locale, id) {
556
+ const finalId = id ?? randomUUID2();
557
+ const existing = getEntryStmt.get(site, listName, finalId, locale);
558
+ if (existing) {
559
+ throw new Error(entryExistsMessage(finalId, listName, locale));
560
+ }
561
+ const now = (/* @__PURE__ */ new Date()).toISOString();
562
+ createTx(site, listName, finalId, locale, JSON.stringify(data), now);
563
+ return {
564
+ id: finalId,
565
+ locale,
566
+ data,
567
+ createdAt: now,
568
+ updatedAt: now,
569
+ _rev: hashRev(data)
570
+ };
571
+ },
572
+ async update(site, listName, id, locale, data, rev) {
573
+ const existing = getEntryStmt.get(site, listName, id, locale);
574
+ if (!existing) {
575
+ throw new Error(entryNotFoundMessage(id, listName, locale));
576
+ }
577
+ const existingData = JSON.parse(existing.data);
578
+ if (hashRev(existingData) !== rev) throw new RevConflictError();
579
+ const now = (/* @__PURE__ */ new Date()).toISOString();
580
+ updateEntryStmt.run(JSON.stringify(data), now, site, listName, id, locale);
581
+ return {
582
+ id,
583
+ locale,
584
+ data,
585
+ createdAt: existing.created_at,
586
+ updatedAt: now,
587
+ _rev: hashRev(data)
588
+ };
589
+ },
590
+ async delete(site, listName, id, locale) {
591
+ const existing = getEntryStmt.get(site, listName, id, locale);
592
+ if (!existing) return;
593
+ deleteTx(site, listName, id, locale);
594
+ },
595
+ async reorder(site, listName, ids) {
596
+ const all = distinctIds(site, listName);
597
+ const { order, dropped } = planReorder(ids, all, listName);
598
+ reorderTx(site, listName, order, dropped);
599
+ },
600
+ async translations(site, listName) {
601
+ const rows = allEntriesStmt.all(site, listName);
602
+ const idToLocales = /* @__PURE__ */ new Map();
603
+ for (const r of rows) {
604
+ const list = idToLocales.get(r.id) ?? [];
605
+ list.push(r.locale);
606
+ idToLocales.set(r.id, list);
607
+ }
608
+ const order = readOrder2(site, listName);
609
+ const orderedIds = applyOrder(order, new Set(idToLocales.keys()));
610
+ return orderedIds.map((id) => ({
611
+ id,
612
+ locales: idToLocales.get(id) ?? []
613
+ }));
614
+ }
615
+ };
616
+ }
617
+ var _connections = /* @__PURE__ */ new Map();
618
+ function getConnection(dbPath) {
619
+ let db = _connections.get(dbPath);
620
+ if (!db) {
621
+ db = openDB(dbPath);
622
+ _connections.set(dbPath, db);
623
+ }
624
+ return db;
625
+ }
626
+ function createSqliteAdapterV2(opts = {}) {
627
+ const root = opts.projectRoot ?? process.cwd();
628
+ const dbPath = opts.dbPath ?? `${root}/cancia.db`;
629
+ const db = getConnection(dbPath);
630
+ return {
631
+ kv: makeKVStore(db),
632
+ pages: makePageStore2(db),
633
+ lists: makeListStore2(db)
634
+ };
635
+ }
636
+ function closeSqliteAdapterV2(dbPath) {
637
+ if (dbPath === void 0) {
638
+ for (const db2 of _connections.values()) db2.close();
639
+ _connections.clear();
640
+ return;
641
+ }
642
+ const db = _connections.get(dbPath);
643
+ if (db) {
644
+ db.close();
645
+ _connections.delete(dbPath);
646
+ }
647
+ }
648
+
342
649
  export {
343
650
  createJsonFileAdapter,
344
651
  canonicalize,
345
652
  hashRev,
346
- applyOrder,
347
- orderAfterCreate,
348
- orderAfterDelete,
349
- planReorder,
350
- entryExistsMessage,
351
- entryNotFoundMessage,
352
- createJsonFileAdapterV2
653
+ createJsonFileAdapterV2,
654
+ createSqliteAdapterV2,
655
+ closeSqliteAdapterV2
353
656
  };
@@ -1,15 +1,127 @@
1
1
  import {
2
- detectImageType,
2
+ detectUploadType,
3
3
  isValidSite
4
- } from "./chunk-5IPHDIC6.js";
4
+ } from "./chunk-RHFTQW4W.js";
5
5
  import {
6
- createGitBackedAdapter,
7
- createSqliteAdapterV2
8
- } from "./chunk-MXVOJRAM.js";
6
+ guardRequest
7
+ } from "./chunk-4PYVIIOO.js";
8
+ import {
9
+ createGitBackedAdapter
10
+ } from "./chunk-XINNLVZP.js";
9
11
  import {
10
12
  createJsonFileAdapter,
11
- createJsonFileAdapterV2
12
- } from "./chunk-2NVZWZPE.js";
13
+ createJsonFileAdapterV2,
14
+ createSqliteAdapterV2
15
+ } from "./chunk-S5YLB6P3.js";
16
+
17
+ // src/auth/sqlite-store.ts
18
+ import { createRequire } from "module";
19
+ import { mkdirSync } from "fs";
20
+ import { dirname } from "path";
21
+ var require2 = createRequire(import.meta.url);
22
+ var connections = /* @__PURE__ */ new Map();
23
+ function openAuthDB(dbPath) {
24
+ const cached = connections.get(dbPath);
25
+ if (cached) return cached;
26
+ const Database = require2("better-sqlite3");
27
+ if (dbPath !== ":memory:") mkdirSync(dirname(dbPath), { recursive: true });
28
+ const db = new Database(dbPath);
29
+ db.pragma("journal_mode = WAL");
30
+ db.exec(`
31
+ CREATE TABLE IF NOT EXISTS editors (
32
+ site TEXT NOT NULL, email TEXT NOT NULL, last_seen_at TEXT,
33
+ PRIMARY KEY (site, email)
34
+ );
35
+ CREATE TABLE IF NOT EXISTS sessions (
36
+ site TEXT NOT NULL, id TEXT NOT NULL, email TEXT NOT NULL,
37
+ created_at TEXT NOT NULL, expires_at TEXT NOT NULL,
38
+ PRIMARY KEY (site, id)
39
+ );
40
+ CREATE TABLE IF NOT EXISTS login_tokens (
41
+ site TEXT NOT NULL, hash TEXT NOT NULL, email TEXT NOT NULL,
42
+ expires_at TEXT NOT NULL,
43
+ PRIMARY KEY (site, hash)
44
+ );
45
+ `);
46
+ connections.set(dbPath, db);
47
+ return db;
48
+ }
49
+ function createSqliteAuthStore(opts) {
50
+ const db = openAuthDB(opts.dbPath);
51
+ const insertEditor = db.prepare(
52
+ `INSERT INTO editors (site, email, last_seen_at) VALUES (?, ?, NULL)
53
+ ON CONFLICT(site, email) DO NOTHING`
54
+ );
55
+ const deleteOtherEditors = db.prepare("DELETE FROM editors WHERE site=?");
56
+ const selectEditor = db.prepare("SELECT email FROM editors WHERE site=? AND email=?");
57
+ const insertToken = db.prepare(
58
+ `INSERT INTO login_tokens (site, hash, email, expires_at) VALUES (?, ?, ?, ?)
59
+ ON CONFLICT(site, hash) DO NOTHING`
60
+ );
61
+ const selectToken = db.prepare(
62
+ "SELECT hash, email, expires_at FROM login_tokens WHERE site=? AND hash=?"
63
+ );
64
+ const deleteToken = db.prepare("DELETE FROM login_tokens WHERE site=? AND hash=?");
65
+ const insertSession = db.prepare(
66
+ "INSERT INTO sessions (site, id, email, created_at, expires_at) VALUES (?, ?, ?, ?, ?)"
67
+ );
68
+ const selectSession = db.prepare(
69
+ "SELECT id, email, created_at, expires_at FROM sessions WHERE site=? AND id=?"
70
+ );
71
+ const touchStmt = db.prepare("UPDATE sessions SET expires_at=? WHERE site=? AND id=?");
72
+ const deleteSessionStmt = db.prepare("DELETE FROM sessions WHERE site=? AND id=?");
73
+ const purgeTokens = db.prepare("DELETE FROM login_tokens WHERE site=? AND expires_at<=?");
74
+ const purgeSessions = db.prepare("DELETE FROM sessions WHERE site=? AND expires_at<=?");
75
+ const seedTx = db.transaction((site, emails) => {
76
+ deleteOtherEditors.run(site);
77
+ for (const email of emails) insertEditor.run(site, email);
78
+ });
79
+ const consumeTx = db.transaction((site, hash) => {
80
+ const row = selectToken.get(site, hash);
81
+ if (!row) return void 0;
82
+ deleteToken.run(site, hash);
83
+ return row;
84
+ });
85
+ return {
86
+ async seedEditors(site, emails) {
87
+ seedTx(site, emails);
88
+ },
89
+ async isEditor(site, email) {
90
+ return selectEditor.get(site, email) !== void 0;
91
+ },
92
+ async createLoginToken(site, rec) {
93
+ insertToken.run(site, rec.hash, rec.email, rec.expiresAt);
94
+ },
95
+ async consumeLoginToken(site, hash) {
96
+ const row = consumeTx(site, hash);
97
+ if (!row) return null;
98
+ return { hash: row.hash, email: row.email, expiresAt: row.expires_at };
99
+ },
100
+ async createSession(site, rec) {
101
+ insertSession.run(site, rec.id, rec.email, rec.createdAt, rec.expiresAt);
102
+ },
103
+ async getSession(site, id) {
104
+ const row = selectSession.get(site, id);
105
+ if (!row) return null;
106
+ return {
107
+ id: row.id,
108
+ email: row.email,
109
+ createdAt: row.created_at,
110
+ expiresAt: row.expires_at
111
+ };
112
+ },
113
+ async touchSession(site, id, expiresAt) {
114
+ touchStmt.run(expiresAt, site, id);
115
+ },
116
+ async deleteSession(site, id) {
117
+ deleteSessionStmt.run(site, id);
118
+ },
119
+ async purgeExpired(site, now) {
120
+ purgeTokens.run(site, now);
121
+ purgeSessions.run(site, now);
122
+ }
123
+ };
124
+ }
13
125
 
14
126
  // src/routes/upload.ts
15
127
  import { writeFile, mkdir } from "fs/promises";
@@ -27,14 +139,10 @@ function makeLocalUploadHandler(opts) {
27
139
  return `${publicUrlBase}/${site}/${name}`;
28
140
  };
29
141
  }
30
- function makeUploadRoute(uploadHandler, secret, maxMB = 10) {
142
+ function makeUploadRoute(uploadHandler, secret, maxMB = 10, auth) {
31
143
  return async ({ request }) => {
32
- if (secret) {
33
- const token = request.headers.get("Authorization")?.replace("Bearer ", "").trim();
34
- if (token !== secret) {
35
- return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
36
- }
37
- }
144
+ const deny = (await guardRequest(request, { secret, auth })).deny;
145
+ if (deny) return deny;
38
146
  const form = await request.formData().catch(() => null);
39
147
  const file = form?.get("file");
40
148
  const site = form?.get("site");
@@ -51,9 +159,9 @@ function makeUploadRoute(uploadHandler, secret, maxMB = 10) {
51
159
  return new Response(JSON.stringify({ error: `File too large (max ${maxMB}MB)` }), { status: 413 });
52
160
  }
53
161
  const buf = new Uint8Array(await file.arrayBuffer());
54
- const detected = detectImageType(buf);
162
+ const detected = detectUploadType(buf);
55
163
  if (!detected) {
56
- return new Response(JSON.stringify({ error: "File type not allowed" }), { status: 415 });
164
+ return new Response(JSON.stringify({ error: "That file type is not supported. Use a JPEG, PNG, GIF, WebP, AVIF or PDF." }), { status: 415 });
57
165
  }
58
166
  const url = await uploadHandler(file, site, detected);
59
167
  return new Response(JSON.stringify({ url }), {
@@ -188,6 +296,18 @@ function buildRuntimeFromBaked(baked) {
188
296
  const storageV2 = buildStorageV2(baked, projectRoot);
189
297
  const storage = storageV2 ? storageV2.kv : lazyJsonFileAdapter(projectRoot);
190
298
  const uploadHandler = buildUploadHandler(baked, projectRoot);
299
+ const site = baked.site ?? "default";
300
+ const auth = baked.editors?.length ? {
301
+ store: createSqliteAuthStore({
302
+ dbPath: baked.authDbPath ? resolveDbPath(baked.authDbPath, projectRoot) : `${projectRoot}/cancia.db`
303
+ }),
304
+ site
305
+ } : void 0;
306
+ if (auth) {
307
+ auth.store.seedEditors(site, baked.editors.map((e) => e.trim().toLowerCase())).catch((err) => {
308
+ console.error(`[cancia] could not seed editors: ${err?.message ?? err}`);
309
+ });
310
+ }
191
311
  return {
192
312
  storage,
193
313
  storageV2,
@@ -196,6 +316,8 @@ function buildRuntimeFromBaked(baked) {
196
316
  defaultLocale: baked.defaultLocale,
197
317
  locales: baked.locales,
198
318
  secret,
319
+ site,
320
+ auth,
199
321
  uploadHandler,
200
322
  deployHook,
201
323
  deployHookToken,
@@ -269,6 +391,7 @@ export {
269
391
  makeLocalUploadHandler,
270
392
  makeUploadRoute,
271
393
  makeR2UploadHandler,
394
+ createSqliteAuthStore,
272
395
  setBakedConfig,
273
396
  setCanciaRuntime,
274
397
  __resetRuntimeForTests,
@@ -0,0 +1,183 @@
1
+ import {
2
+ createGitHubClient
3
+ } from "./chunk-U7V53JX7.js";
4
+
5
+ // src/storage/git-backed.ts
6
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
7
+ import { join, relative } from "path";
8
+ function createGitBackedAdapter(opts) {
9
+ const projectRoot = opts.projectRoot ?? process.cwd();
10
+ const branch = opts.branch ?? "main";
11
+ const debounceMs = opts.debounceMs ?? 3e3;
12
+ const commitMessage = opts.commitMessage ?? "Cancia: content update";
13
+ const warn = opts.warn ?? ((m) => console.warn(m));
14
+ const onError = opts.onError ?? ((m, e) => console.error(m, e));
15
+ const kvPath = opts.contentPaths?.kvPath ?? join(projectRoot, "cancia-content.json");
16
+ const pagesPath = opts.contentPaths?.pagesPath ?? join(projectRoot, ".cancia", "pages.json");
17
+ const listsDir = opts.contentPaths?.listsDir ?? join(projectRoot, ".cancia", "lists");
18
+ const token = opts.token ?? process.env.CANCIA_GITHUB_TOKEN ?? "";
19
+ let client = null;
20
+ if (opts.client) {
21
+ client = opts.client;
22
+ } else if (token) {
23
+ client = createGitHubClient({
24
+ repo: opts.repo,
25
+ branch,
26
+ token,
27
+ committer: opts.committer,
28
+ fetch: opts.fetch,
29
+ apiBase: opts.apiBase
30
+ });
31
+ }
32
+ const gitEnabled = client !== null;
33
+ if (!gitEnabled) {
34
+ warn(
35
+ "[cancia] Git-backed storage: no GitHub token (CANCIA_GITHUB_TOKEN) \u2014 running local-only. Edits save to disk but are NOT committed/pushed."
36
+ );
37
+ }
38
+ const dirty = /* @__PURE__ */ new Set();
39
+ let timer = null;
40
+ let flushing = null;
41
+ let rerunRequested = false;
42
+ function toRepoPath(absPath) {
43
+ return relative(projectRoot, absPath).split("\\").join("/");
44
+ }
45
+ function markDirty(absPath) {
46
+ dirty.add(absPath);
47
+ }
48
+ function markListDirty(listName, site) {
49
+ const siteDir = join(listsDir, listName, site);
50
+ if (!existsSync(siteDir)) return;
51
+ const walk = (dir) => {
52
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
53
+ const full = join(dir, entry.name);
54
+ if (entry.isDirectory()) walk(full);
55
+ else if (entry.isFile()) markDirty(full);
56
+ }
57
+ };
58
+ walk(siteDir);
59
+ }
60
+ function scheduleFlush() {
61
+ if (!gitEnabled) return;
62
+ if (timer) clearTimeout(timer);
63
+ timer = setTimeout(() => {
64
+ timer = null;
65
+ void runFlush();
66
+ }, debounceMs);
67
+ }
68
+ async function runFlush() {
69
+ if (flushing) {
70
+ rerunRequested = true;
71
+ return flushing;
72
+ }
73
+ flushing = doFlush().finally(() => {
74
+ flushing = null;
75
+ if (rerunRequested) {
76
+ rerunRequested = false;
77
+ void runFlush();
78
+ }
79
+ });
80
+ return flushing;
81
+ }
82
+ async function doFlush() {
83
+ if (!client || dirty.size === 0) return;
84
+ const batch = [...dirty];
85
+ const files = [];
86
+ for (const abs of batch) {
87
+ if (!existsSync(abs) || !statSync(abs).isFile()) continue;
88
+ files.push({ path: toRepoPath(abs), content: readFileSync(abs, "utf-8") });
89
+ }
90
+ if (files.length === 0) {
91
+ for (const abs of batch) dirty.delete(abs);
92
+ return;
93
+ }
94
+ try {
95
+ await client.commitFiles(files, commitMessage);
96
+ for (const abs of batch) dirty.delete(abs);
97
+ } catch (err) {
98
+ onError(
99
+ "[cancia] Git-backed storage: commit failed \u2014 data saved locally, will retry on next flush.",
100
+ err
101
+ );
102
+ throw err;
103
+ }
104
+ }
105
+ async function flush() {
106
+ if (!gitEnabled) return;
107
+ if (timer) {
108
+ clearTimeout(timer);
109
+ timer = null;
110
+ }
111
+ await runFlush();
112
+ }
113
+ const kv = {
114
+ get: (site, key, lang) => opts.local.kv.get(site, key, lang),
115
+ getAll: (site) => opts.local.kv.getAll(site),
116
+ async set(site, key, lang, value) {
117
+ await opts.local.kv.set(site, key, lang, value);
118
+ markDirty(kvPath);
119
+ scheduleFlush();
120
+ },
121
+ async delete(site, key, lang) {
122
+ await opts.local.kv.delete(site, key, lang);
123
+ markDirty(kvPath);
124
+ scheduleFlush();
125
+ }
126
+ };
127
+ const pages = {
128
+ get: (site, route) => opts.local.pages.get(site, route),
129
+ list: (site) => opts.local.pages.list(site),
130
+ async set(site, route, meta, rev) {
131
+ const result = await opts.local.pages.set(site, route, meta, rev);
132
+ markDirty(pagesPath);
133
+ scheduleFlush();
134
+ return result;
135
+ },
136
+ async delete(site, route) {
137
+ await opts.local.pages.delete(site, route);
138
+ markDirty(pagesPath);
139
+ scheduleFlush();
140
+ }
141
+ };
142
+ const lists = {
143
+ list: (site, listName, locale) => opts.local.lists.list(site, listName, locale),
144
+ get: (site, listName, id, locale) => opts.local.lists.get(site, listName, id, locale),
145
+ translations: (site, listName) => opts.local.lists.translations(site, listName),
146
+ async create(site, listName, data, locale, id) {
147
+ const entry = await opts.local.lists.create(site, listName, data, locale, id);
148
+ markListDirty(listName, site);
149
+ scheduleFlush();
150
+ return entry;
151
+ },
152
+ async update(site, listName, id, locale, data, rev) {
153
+ const entry = await opts.local.lists.update(site, listName, id, locale, data, rev);
154
+ markListDirty(listName, site);
155
+ scheduleFlush();
156
+ return entry;
157
+ },
158
+ async delete(site, listName, id, locale) {
159
+ await opts.local.lists.delete(site, listName, id, locale);
160
+ markListDirty(listName, site);
161
+ scheduleFlush();
162
+ },
163
+ async reorder(site, listName, ids) {
164
+ await opts.local.lists.reorder(site, listName, ids);
165
+ markListDirty(listName, site);
166
+ scheduleFlush();
167
+ }
168
+ };
169
+ const git = {
170
+ flush,
171
+ get gitEnabled() {
172
+ return gitEnabled;
173
+ },
174
+ pendingPaths() {
175
+ return [...dirty].map(toRepoPath);
176
+ }
177
+ };
178
+ return { kv, pages, lists, git };
179
+ }
180
+
181
+ export {
182
+ createGitBackedAdapter
183
+ };