@beechcms/api 0.6.6 → 0.7.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 (37) hide show
  1. package/assets/dashboard/assets/{chart-DA5IFPWq.js → chart-B1qG9c0-.js} +1 -1
  2. package/assets/dashboard/assets/{index-CM0tZF7n.js → index-0fKOe8Wq.js} +35 -35
  3. package/assets/dashboard/assets/index-Bj2upDZk.css +1 -0
  4. package/assets/dashboard/assets/{pie-chart-recharts-BiqT5XVc.js → pie-chart-recharts-DSf6kc6u.js} +1 -1
  5. package/assets/dashboard/assets/{timeseries-chart-recharts-B6lrNF3r.js → timeseries-chart-recharts-CqM5c6MV.js} +1 -1
  6. package/assets/dashboard/index.html +2 -2
  7. package/dist/auth/constants.d.ts +10 -0
  8. package/dist/auth/utils/refresh-token.d.ts +1 -0
  9. package/dist/features/automations/api/automations.schema.d.ts +16 -16
  10. package/dist/features/automations/engine/cron-runner.utils.d.ts +1 -0
  11. package/dist/features/content/constants.d.ts +12 -0
  12. package/dist/features/rotate-field/rotate-field.schema.d.ts +11 -0
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +150 -125
  15. package/dist/public/access-policy.d.ts +1 -1
  16. package/dist/public/api-key-middleware.d.ts +6 -0
  17. package/dist/public/cache-utils.d.ts +10 -0
  18. package/dist/public/problem-details.d.ts +32 -0
  19. package/dist/public/public-errors.d.ts +13 -0
  20. package/dist/public/response-builder.d.ts +22 -0
  21. package/dist/shared/db/fixtures/demo-data.fixtures.d.ts +12 -0
  22. package/dist/shared/db/repositories/content.repository.d1.d.ts +4 -3
  23. package/dist/shared/db/repositories/d1-setup-checklist.repository.d.ts +12 -0
  24. package/dist/shared/db/repositories/demo-data.repository.d1.d.ts +1 -1
  25. package/dist/shared/demo-data-sql.d.ts +1 -0
  26. package/dist/shared/email/email.types.d.ts +79 -0
  27. package/dist/shared/email/templates/automation-mail.d.ts +7 -0
  28. package/dist/shared/email/templates/password-changed.d.ts +15 -0
  29. package/dist/shared/email/templates/password-reset.d.ts +18 -0
  30. package/dist/shared/email/templates/shell.d.ts +58 -0
  31. package/dist/shared/jobs/fts-sync.d.ts +1 -0
  32. package/dist/shared/storage/upload.d.ts +7 -0
  33. package/dist/shared/utils/request-utils.d.ts +13 -0
  34. package/dist/shared/utils/storage-utils.d.ts +7 -0
  35. package/migrations/0000_v040_base.sql +14 -1
  36. package/package.json +2 -2
  37. package/assets/dashboard/assets/index-B58kBLrh.css +0 -1
package/dist/index.js CHANGED
@@ -2249,25 +2249,25 @@ setupApp.post("/auth/setup", async (context) => {
2249
2249
  id: "sec-0",
2250
2250
  hideLabel: true,
2251
2251
  columns: [
2252
- { id: "c0", widgets: [{ id: "w0", type: "core/text", config: { content: "# Acme SaaS Analytics\nBenvenuto nella dashboard di produzione. Qui puoi monitorare le metriche in tempo reale." } }] }
2252
+ { id: "c0", widgets: [{ id: "w0", type: "core/text", config: { content: "# Acme SaaS Analytics\nWelcome to your production dashboard. Monitor real-time metrics and KPIs here." } }] }
2253
2253
  ]
2254
2254
  },
2255
2255
  {
2256
2256
  id: "sec-1",
2257
2257
  hideLabel: true,
2258
2258
  columns: [
2259
- { id: "c1", widgets: [{ id: "w1", type: "core/stat", config: { seedSlug: "clienti", formula: { op: "sum", column: "mrr" }, window: "all" }, title: "Totale MRR" }] },
2260
- { id: "c2", widgets: [{ id: "w2", type: "core/stat", config: { seedSlug: "clienti", formula: { op: "count" }, window: "all" }, title: "Clienti Attivi" }] },
2261
- { id: "c3", widgets: [{ id: "w3", type: "core/stat", config: { seedSlug: "abbonamenti", formula: { op: "count" }, window: "all" }, title: "Abbonamenti" }] },
2262
- { id: "c4", widgets: [{ id: "w4", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "ticket_status", value: "open" }, window: "all" }, title: "Ticket Aperti" }] }
2259
+ { id: "c1", widgets: [{ id: "w1", type: "core/stat", config: { seedSlug: "clienti", formula: { op: "sum", column: "mrr" }, window: "all" }, title: "Total MRR" }] },
2260
+ { id: "c2", widgets: [{ id: "w2", type: "core/stat", config: { seedSlug: "clienti", formula: { op: "count" }, window: "all" }, title: "Active Customers" }] },
2261
+ { id: "c3", widgets: [{ id: "w3", type: "core/stat", config: { seedSlug: "abbonamenti", formula: { op: "count" }, window: "all" }, title: "Subscriptions" }] },
2262
+ { id: "c4", widgets: [{ id: "w4", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "ticket_status", value: "open" }, window: "all" }, title: "Open Tickets" }] }
2263
2263
  ]
2264
2264
  },
2265
2265
  {
2266
2266
  id: "sec-2",
2267
2267
  hideLabel: true,
2268
2268
  columns: [
2269
- { id: "c5", widgets: [{ id: "w5", type: "core/area-chart", config: { seedSlug: "clienti", groupColumn: "created_at", window: "all" }, title: "Andamento Iscrizioni" }] },
2270
- { id: "c6", widgets: [{ id: "w6", type: "core/pie-chart", config: { seedSlug: "clienti", column: "tier", window: "all" }, title: "Distribuzione Piani" }] }
2269
+ { id: "c5", widgets: [{ id: "w5", type: "core/area-chart", config: { seedSlug: "clienti", groupColumn: "created_at", window: "all" }, title: "Signups Trend" }] },
2270
+ { id: "c6", widgets: [{ id: "w6", type: "core/pie-chart", config: { seedSlug: "clienti", column: "tier", window: "all" }, title: "Plan Distribution" }] }
2271
2271
  ],
2272
2272
  columnSpans: [8, 4]
2273
2273
  },
@@ -2275,8 +2275,8 @@ setupApp.post("/auth/setup", async (context) => {
2275
2275
  id: "sec-3",
2276
2276
  hideLabel: true,
2277
2277
  columns: [
2278
- { id: "c7", widgets: [{ id: "w7", type: "core/data-table", config: { seedSlug: "clienti", pageSize: 5 }, title: "Ultimi Iscritti" }] },
2279
- { id: "c8", widgets: [{ id: "w8", type: "core/data-table", config: { seedSlug: "changelog", pageSize: 5 }, title: "Ultime Release" }] }
2278
+ { id: "c7", widgets: [{ id: "w7", type: "core/data-table", config: { seedSlug: "clienti", pageSize: 5 }, title: "Recent Signups" }] },
2279
+ { id: "c8", widgets: [{ id: "w8", type: "core/data-table", config: { seedSlug: "changelog", pageSize: 5 }, title: "Latest Releases" }] }
2280
2280
  ],
2281
2281
  columnSpans: [6, 6]
2282
2282
  }
@@ -2292,17 +2292,17 @@ setupApp.post("/auth/setup", async (context) => {
2292
2292
  id: "sec-4",
2293
2293
  hideLabel: true,
2294
2294
  columns: [
2295
- { id: "c9", widgets: [{ id: "w9", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "priority", value: "high" }, window: "all" }, title: "Ticket Alta Priorit\xE0" }] },
2296
- { id: "c10", widgets: [{ id: "w10", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "ticket_status", value: "closed" }, window: "all" }, title: "Ticket Chiusi" }] },
2297
- { id: "c11", widgets: [{ id: "w11", type: "core/stat", config: { seedSlug: "changelog", formula: { op: "count" }, window: "all" }, title: "Update Rilasciati" }] }
2295
+ { id: "c9", widgets: [{ id: "w9", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "priority", value: "high" }, window: "all" }, title: "High Priority Tickets" }] },
2296
+ { id: "c10", widgets: [{ id: "w10", type: "core/stat", config: { seedSlug: "ticket", formula: { op: "countWhere", column: "ticket_status", value: "closed" }, window: "all" }, title: "Closed Tickets" }] },
2297
+ { id: "c11", widgets: [{ id: "w11", type: "core/stat", config: { seedSlug: "changelog", formula: { op: "count" }, window: "all" }, title: "Released Updates" }] }
2298
2298
  ]
2299
2299
  },
2300
2300
  {
2301
2301
  id: "sec-5",
2302
2302
  hideLabel: true,
2303
2303
  columns: [
2304
- { id: "c12", widgets: [{ id: "w12", type: "core/bar-chart", config: { seedSlug: "ticket", groupColumn: "created_at", window: "all" }, title: "Ticket nel tempo" }] },
2305
- { id: "c13", widgets: [{ id: "w13", type: "core/line-chart", config: { seedSlug: "abbonamenti", groupColumn: "created_at", window: "all" }, title: "Nuovi Abbonamenti" }] }
2304
+ { id: "c12", widgets: [{ id: "w12", type: "core/bar-chart", config: { seedSlug: "ticket", groupColumn: "created_at", window: "all" }, title: "Tickets Over Time" }] },
2305
+ { id: "c13", widgets: [{ id: "w13", type: "core/line-chart", config: { seedSlug: "abbonamenti", groupColumn: "created_at", window: "all" }, title: "New Subscriptions" }] }
2306
2306
  ],
2307
2307
  columnSpans: [6, 6]
2308
2308
  },
@@ -2310,7 +2310,7 @@ setupApp.post("/auth/setup", async (context) => {
2310
2310
  id: "sec-6",
2311
2311
  hideLabel: true,
2312
2312
  columns: [
2313
- { id: "c14", widgets: [{ id: "w14", type: "core/data-table", config: { seedSlug: "ticket", pageSize: 10 }, title: "Ticket in lavorazione" }] }
2313
+ { id: "c14", widgets: [{ id: "w14", type: "core/data-table", config: { seedSlug: "ticket", pageSize: 10 }, title: "Active Tickets" }] }
2314
2314
  ]
2315
2315
  }
2316
2316
  ]
@@ -2342,6 +2342,7 @@ import { Hono as Hono6 } from "hono";
2342
2342
  import {
2343
2343
  validateAndSanitizeSeedPayload as validateAndSanitizeSeedPayload5,
2344
2344
  resolvePolicies as resolvePolicies5,
2345
+ EntryNotFoundError as EntryNotFoundError6,
2345
2346
  RelationTargetNotFoundError
2346
2347
  } from "@beechcms/core";
2347
2348
 
@@ -2503,18 +2504,17 @@ draftApp.post("/:slug/:id/draft/publish", draftGuard, async (context) => {
2503
2504
  const id = context.req.param("id");
2504
2505
  const seed = context.get("getSeed")(slug);
2505
2506
  const repository = context.get("repository");
2506
- const draft = await repository.getDraft(seed, id);
2507
- if (!draft) {
2508
- return publicProblem(context, {
2509
- type: "draft-not-found",
2510
- title: "Not Found",
2511
- status: 404,
2512
- detail: "No pending draft to publish"
2513
- });
2514
- }
2515
2507
  try {
2516
2508
  await repository.publishDraft(seed, id);
2517
2509
  } catch (err) {
2510
+ if (err instanceof EntryNotFoundError6) {
2511
+ return publicProblem(context, {
2512
+ type: "draft-not-found",
2513
+ title: "Not Found",
2514
+ status: 404,
2515
+ detail: "No pending draft to publish"
2516
+ });
2517
+ }
2518
2518
  if (err instanceof RelationTargetNotFoundError) {
2519
2519
  return publicProblem(context, {
2520
2520
  type: "relation-target-not-found",
@@ -2525,9 +2525,7 @@ draftApp.post("/:slug/:id/draft/publish", draftGuard, async (context) => {
2525
2525
  }
2526
2526
  throw err;
2527
2527
  }
2528
- const displayValue = draft[seed.displayNameAlias];
2529
- const displayStr = typeof displayValue === "string" ? displayValue : id;
2530
- logDraftActivity(context, id, slug, displayStr, "draft published");
2528
+ logDraftActivity(context, id, slug, id, "draft published");
2531
2529
  return context.json({ success: true });
2532
2530
  });
2533
2531
  draftApp.delete("/:slug/:id/draft", draftGuard, async (context) => {
@@ -4736,7 +4734,7 @@ function withCachedResponse(edgeCache, cacheKey, response) {
4736
4734
  }
4737
4735
 
4738
4736
  // src/public/read-single.ts
4739
- import { EntryNotFoundError as EntryNotFoundError6 } from "@beechcms/core";
4737
+ import { EntryNotFoundError as EntryNotFoundError7 } from "@beechcms/core";
4740
4738
 
4741
4739
  // src/public/entry-projection.ts
4742
4740
  import { filterEntryForActor as filterEntryForActor2 } from "@beechcms/core";
@@ -4787,7 +4785,7 @@ async function readSingleEntry(input) {
4787
4785
  meta: buildPublicSingleMeta(seedSlug)
4788
4786
  };
4789
4787
  } catch (error) {
4790
- if (error instanceof EntryNotFoundError6) {
4788
+ if (error instanceof EntryNotFoundError7) {
4791
4789
  return { ok: false, detail: `Entry '${label}' not found for content type '${seedSlug}'.` };
4792
4790
  }
4793
4791
  throw error;
@@ -5171,7 +5169,7 @@ async function publicAddHandler(context) {
5171
5169
  }
5172
5170
 
5173
5171
  // src/public/public-edit.ts
5174
- import { isValidContentStatus as isValidContentStatus4, resolvePolicies as resolvePolicies9, EntryNotFoundError as EntryNotFoundError7 } from "@beechcms/core";
5172
+ import { isValidContentStatus as isValidContentStatus4, resolvePolicies as resolvePolicies9, EntryNotFoundError as EntryNotFoundError8 } from "@beechcms/core";
5175
5173
  var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5176
5174
  function errorMessage(context, error) {
5177
5175
  if (context.env.ENV !== "production" && error instanceof Error) return error.message;
@@ -5298,7 +5296,7 @@ async function publicEditHandler(context) {
5298
5296
  );
5299
5297
  return context.json({ success: true, id, slug: slugResult.value.nextSlug }, 200);
5300
5298
  } catch (error) {
5301
- if (error instanceof EntryNotFoundError7) {
5299
+ if (error instanceof EntryNotFoundError8) {
5302
5300
  return publicProblem(context, { type: "entry-not-found", title: "Not Found", status: 404, detail: `Entry '${id}' not found for content type '${seedSlug}'.` });
5303
5301
  }
5304
5302
  console.error("Public edit error:", error);
@@ -5604,7 +5602,7 @@ import { createMiddleware as createMiddleware2 } from "hono/factory";
5604
5602
 
5605
5603
  // src/shared/db/repositories/content.repository.d1.ts
5606
5604
  import {
5607
- EntryNotFoundError as EntryNotFoundError8,
5605
+ EntryNotFoundError as EntryNotFoundError9,
5608
5606
  RepositoryError as RepositoryError2,
5609
5607
  SlugConflictError as SlugConflictError4,
5610
5608
  RelationTargetNotFoundError as RelationTargetNotFoundError2,
@@ -5862,13 +5860,13 @@ var D1ContentRepository = class extends BaseD1Repository {
5862
5860
  const tableName = this.getTableName(seed.slug);
5863
5861
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE id = ? LIMIT 1`).bind(id).first();
5864
5862
  if (!entryRow) {
5865
- throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
5863
+ throw new EntryNotFoundError9(`Entry ${id} not found in ${seed.slug}`);
5866
5864
  }
5867
5865
  const data = await this.rowToData(seed, entryRow);
5868
5866
  await this.attachMultiRelations(seed, id, data);
5869
5867
  return data;
5870
5868
  } catch (error) {
5871
- if (error instanceof EntryNotFoundError8) throw error;
5869
+ if (error instanceof EntryNotFoundError9) throw error;
5872
5870
  throw this.mapError(error, `findById(${seed.slug}, ${id})`);
5873
5871
  }
5874
5872
  }
@@ -5882,13 +5880,13 @@ var D1ContentRepository = class extends BaseD1Repository {
5882
5880
  const tableName = this.getTableName(seed.slug);
5883
5881
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE slug = ? LIMIT 1`).bind(slug).first();
5884
5882
  if (!entryRow) {
5885
- throw new EntryNotFoundError8(`Entry with slug "${slug}" not found in ${seed.slug}`);
5883
+ throw new EntryNotFoundError9(`Entry with slug "${slug}" not found in ${seed.slug}`);
5886
5884
  }
5887
5885
  const data = await this.rowToData(seed, entryRow);
5888
5886
  await this.attachMultiRelations(seed, entryRow.id, data);
5889
5887
  return data;
5890
5888
  } catch (error) {
5891
- if (error instanceof EntryNotFoundError8) throw error;
5889
+ if (error instanceof EntryNotFoundError9) throw error;
5892
5890
  throw this.mapError(error, `findBySlug(${seed.slug}, ${slug})`);
5893
5891
  }
5894
5892
  }
@@ -6265,16 +6263,16 @@ var D1ContentRepository = class extends BaseD1Repository {
6265
6263
  if (batchStmts.length === 1) {
6266
6264
  const updateResult = await batchStmts[0].run();
6267
6265
  if (updateResult.meta.changes === 0) {
6268
- throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6266
+ throw new EntryNotFoundError9(`Entry ${id} not found in ${seed.slug}`);
6269
6267
  }
6270
6268
  } else if (batchStmts.length > 1) {
6271
6269
  const results = await this.database.batch(batchStmts);
6272
6270
  if (stmt && (results[0].meta?.changes ?? 0) === 0) {
6273
- throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6271
+ throw new EntryNotFoundError9(`Entry ${id} not found in ${seed.slug}`);
6274
6272
  }
6275
6273
  }
6276
6274
  } catch (error) {
6277
- if (error instanceof EntryNotFoundError8) throw error;
6275
+ if (error instanceof EntryNotFoundError9) throw error;
6278
6276
  throw this.mapError(error, `update(${seed.slug}, ${id})`);
6279
6277
  }
6280
6278
  if (this.hooks?.afterUpdate) {
@@ -6299,12 +6297,12 @@ var D1ContentRepository = class extends BaseD1Repository {
6299
6297
  const tableName = this.getTableName(seed.slug);
6300
6298
  const entryRow = await this.database.prepare(`SELECT * FROM ${tableName} WHERE id = ?`).bind(id).first();
6301
6299
  if (!entryRow) {
6302
- throw new EntryNotFoundError8(`Entry ${id} not found in ${seed.slug}`);
6300
+ throw new EntryNotFoundError9(`Entry ${id} not found in ${seed.slug}`);
6303
6301
  }
6304
6302
  await this.database.prepare(`DELETE FROM ${tableName} WHERE id = ?`).bind(id).run();
6305
6303
  row = await this.rowToData(seed, entryRow);
6306
6304
  } catch (error) {
6307
- if (error instanceof EntryNotFoundError8) throw error;
6305
+ if (error instanceof EntryNotFoundError9) throw error;
6308
6306
  throw this.mapError(error, `delete(${seed.slug}, ${id})`);
6309
6307
  }
6310
6308
  if (this.hooks?.afterDelete) {
@@ -6560,7 +6558,8 @@ var D1ContentRepository = class extends BaseD1Repository {
6560
6558
  try {
6561
6559
  if (!seed.allowDrafts) return false;
6562
6560
  const draftTableName = this.getTableName(seed.slug, true);
6563
- const draftExistsResult = await this.database.prepare(`SELECT 1 FROM ${draftTableName} WHERE entry_id = ? LIMIT 1`).bind(entryId).first();
6561
+ const liveTableName = this.getTableName(seed.slug);
6562
+ const draftExistsResult = await this.database.prepare(`SELECT 1 FROM ${draftTableName} WHERE entry_id = ? UNION SELECT 1 FROM ${liveTableName} WHERE id = ? AND status = 'draft' LIMIT 1`).bind(entryId, entryId).first();
6564
6563
  return draftExistsResult !== null;
6565
6564
  } catch (error) {
6566
6565
  throw this.mapError(error, `hasDraft(${seed.slug}, ${entryId})`);
@@ -6569,7 +6568,8 @@ var D1ContentRepository = class extends BaseD1Repository {
6569
6568
  /**
6570
6569
  * Promotes a draft to the live entry: validates all relation targets referenced by the
6571
6570
  * draft still exist, copies the draft's scalar fields into the live row, replaces the
6572
- * live junction rows with the draft's, and deletes the draft — all as one batch.
6571
+ * live junction rows with the draft's, sets status to 'published', and deletes the draft — all as one batch.
6572
+ * If no mirror draft row exists but the live entry has status = 'draft', updates status to 'published'.
6573
6573
  * A no-op if `seed.allowDrafts` is false.
6574
6574
  *
6575
6575
  * @throws EntryNotFoundError if no draft row exists for `entryId`.
@@ -6582,7 +6582,12 @@ var D1ContentRepository = class extends BaseD1Repository {
6582
6582
  const liveTableName = this.getTableName(seed.slug);
6583
6583
  const draftRow = await this.database.prepare(`SELECT * FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId).first();
6584
6584
  if (!draftRow) {
6585
- throw new EntryNotFoundError8(`No draft found for ${entryId} in ${seed.slug}`);
6585
+ const liveRow = await this.database.prepare(`SELECT 1 FROM ${liveTableName} WHERE id = ? AND status = 'draft'`).bind(entryId).first();
6586
+ if (!liveRow) {
6587
+ throw new EntryNotFoundError9(`No draft found for ${entryId} in ${seed.slug}`);
6588
+ }
6589
+ await this.database.prepare(`UPDATE ${liveTableName} SET status = 'published', updated_at = (unixepoch()) WHERE id = ?`).bind(entryId).run();
6590
+ return;
6586
6591
  }
6587
6592
  let touchedFields = [];
6588
6593
  if (draftRow && typeof draftRow["_touched_fields"] === "string") {
@@ -6606,15 +6611,9 @@ var D1ContentRepository = class extends BaseD1Repository {
6606
6611
  queryBindings.push(draftRow[branch.alias]);
6607
6612
  }
6608
6613
  }
6609
- let updateSql = "";
6610
- if (updateClauses.length > 0) {
6611
- updateClauses.push("updated_at = (unixepoch())");
6612
- updateSql = `UPDATE ${liveTableName} SET ${updateClauses.join(", ")} WHERE id = ?`;
6613
- queryBindings.push(entryId);
6614
- } else {
6615
- updateSql = `UPDATE ${liveTableName} SET updated_at = (unixepoch()) WHERE id = ?`;
6616
- queryBindings.push(entryId);
6617
- }
6614
+ updateClauses.push("status = 'published'", "updated_at = (unixepoch())");
6615
+ const updateSql = `UPDATE ${liveTableName} SET ${updateClauses.join(", ")} WHERE id = ?`;
6616
+ queryBindings.push(entryId);
6618
6617
  const batchStmts = [
6619
6618
  this.database.prepare(updateSql).bind(...queryBindings),
6620
6619
  this.database.prepare(`DELETE FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId)
@@ -6637,7 +6636,7 @@ var D1ContentRepository = class extends BaseD1Repository {
6637
6636
  }
6638
6637
  await this.database.batch(batchStmts);
6639
6638
  } catch (error) {
6640
- if (error instanceof EntryNotFoundError8) throw error;
6639
+ if (error instanceof EntryNotFoundError9) throw error;
6641
6640
  if (error instanceof RelationTargetNotFoundError2) throw error;
6642
6641
  throw this.mapError(error, `publishDraft(${seed.slug}, ${entryId})`);
6643
6642
  }
@@ -6665,15 +6664,28 @@ var D1ContentRepository = class extends BaseD1Repository {
6665
6664
  try {
6666
6665
  if (!seed.allowDrafts) return;
6667
6666
  const draftTableName = this.getTableName(seed.slug, true);
6668
- await this.database.prepare(`DELETE FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId).run();
6667
+ const liveTableName = this.getTableName(seed.slug);
6668
+ const hasMirror = await this.database.prepare(`SELECT 1 FROM ${draftTableName} WHERE entry_id = ? LIMIT 1`).bind(entryId).first();
6669
+ if (hasMirror) {
6670
+ const batchStmts = [
6671
+ this.database.prepare(`DELETE FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId)
6672
+ ];
6673
+ for (const branch of multiRelBranches(seed)) {
6674
+ const jdt = jDraftTable(seed.slug, branch.alias);
6675
+ batchStmts.push(this.database.prepare(`DELETE FROM ${jdt} WHERE entry_id = ?`).bind(entryId));
6676
+ }
6677
+ await this.database.batch(batchStmts);
6678
+ } else {
6679
+ await this.database.prepare(`DELETE FROM ${liveTableName} WHERE id = ? AND status = 'draft'`).bind(entryId).run();
6680
+ }
6669
6681
  } catch (error) {
6670
6682
  throw this.mapError(error, `deleteDraft(${seed.slug}, ${entryId})`);
6671
6683
  }
6672
6684
  }
6673
6685
  /**
6674
6686
  * Finds every pending draft across the given seeds, `UNION ALL`-ing one query per seed
6675
- * so results span heterogeneous `content_{slug}_drafts` tables, then left-joins each
6676
- * against `activity_logs` to attribute the most recent "draft saved" action.
6687
+ * so results span heterogeneous `content_{slug}_drafts` tables and live draft entries,
6688
+ * then left-joins each against `activity_logs` to attribute the most recent action.
6677
6689
  *
6678
6690
  * @param seeds Seeds to search; drafts are looked up per-seed since draft tables aren't unified.
6679
6691
  * @returns Draft summaries ordered by most-recently-updated first. `title` falls back to the
@@ -6689,17 +6701,18 @@ var D1ContentRepository = class extends BaseD1Repository {
6689
6701
  const liveTable = `content_${seed.slug}`;
6690
6702
  const titleCol = seed.displayNameAlias;
6691
6703
  const isScalar = seed.branches.some((b) => b.alias === titleCol && !(b.type === "relation" && b.multiple === true));
6692
- const titleExpr = isScalar ? `COALESCE(d.${titleCol}, l.${titleCol})` : `COALESCE(l.slug, d.entry_id)`;
6704
+ const titleExpr = isScalar ? `COALESCE(d.${titleCol}, l.${titleCol}, l.slug, d.entry_id, l.id)` : `COALESCE(l.slug, d.entry_id, l.id)`;
6693
6705
  const seedLabel = seed.labelPlural ?? seed.label;
6694
6706
  unionSelects.push(`
6695
6707
  SELECT
6696
6708
  ? AS seed_slug,
6697
6709
  ? AS seed_label,
6698
- d.entry_id AS id,
6699
- d.updated_at AS updated_at,
6710
+ COALESCE(d.entry_id, l.id) AS id,
6711
+ COALESCE(d.updated_at, l.updated_at) AS updated_at,
6700
6712
  ${titleExpr} AS title
6701
- FROM ${draftTable} d
6702
- LEFT JOIN ${liveTable} l ON l.id = d.entry_id
6713
+ FROM ${liveTable} l
6714
+ LEFT JOIN ${draftTable} d ON d.entry_id = l.id
6715
+ WHERE l.status = 'draft' OR d.entry_id IS NOT NULL
6703
6716
  `);
6704
6717
  bindings.push(seed.slug, seedLabel);
6705
6718
  }
@@ -6721,8 +6734,7 @@ var D1ContentRepository = class extends BaseD1Repository {
6721
6734
  SELECT id FROM activity_logs
6722
6735
  WHERE entity_id = ad.id
6723
6736
  AND entity_slug = ad.seed_slug
6724
- AND action = 'update'
6725
- AND json_extract(details, '$.note') = 'draft saved'
6737
+ AND action IN ('create', 'update')
6726
6738
  ORDER BY created_at DESC
6727
6739
  LIMIT 1
6728
6740
  )
@@ -7196,10 +7208,13 @@ var D1NotificationRepository = class {
7196
7208
  }
7197
7209
  async create(record) {
7198
7210
  const generatedId = this.idGenerator.uuid();
7211
+ const safeType = Object.hasOwn(record, "type") && record.type ? record.type : "info";
7212
+ const safeTitle = Object.hasOwn(record, "title") && typeof record.title === "string" ? record.title : "";
7213
+ const safeMessage = Object.hasOwn(record, "message") && typeof record.message === "string" ? record.message : "";
7199
7214
  await this.database.prepare(
7200
7215
  `INSERT INTO notifications (id, title, message, type)
7201
7216
  VALUES (?, ?, ?, ?)`
7202
- ).bind(generatedId, record.title, record.message, record.type).run();
7217
+ ).bind(generatedId, safeTitle, safeMessage, safeType).run();
7203
7218
  return generatedId;
7204
7219
  }
7205
7220
  async markRead(notificationId) {
@@ -7216,13 +7231,14 @@ var D1NotificationRepository = class {
7216
7231
  }
7217
7232
  };
7218
7233
  function mapRowToRecord2(row) {
7234
+ const safeType = Object.hasOwn(row, "type") && row.type ? row.type : "info";
7219
7235
  return {
7220
- id: row.id,
7221
- title: row.title,
7222
- message: row.message,
7223
- type: row.type,
7224
- isRead: row.is_read === 1,
7225
- createdAt: row.created_at
7236
+ id: Object.hasOwn(row, "id") ? row.id : "",
7237
+ title: Object.hasOwn(row, "title") ? row.title : "",
7238
+ message: Object.hasOwn(row, "message") ? row.message : "",
7239
+ type: safeType,
7240
+ isRead: Object.hasOwn(row, "is_read") ? row.is_read === 1 : false,
7241
+ createdAt: Object.hasOwn(row, "created_at") ? row.created_at : 0
7226
7242
  };
7227
7243
  }
7228
7244
 
@@ -7256,12 +7272,12 @@ var D1WidgetRepository = class {
7256
7272
  database;
7257
7273
  async aggregate(seed, formula, window) {
7258
7274
  const aggregateExpression = this.buildAggregateExpression(seed, formula);
7259
- const { sql: timeWindowFilter, bindings } = this.buildTimeWindowFilter(window);
7275
+ const { sql: timeWindowFilter, bindings: timeBindings } = this.buildTimeWindowFilter(window);
7260
7276
  const tableName = `content_${seed.slug}`;
7261
- const sql = `SELECT ${aggregateExpression} as computed_value
7277
+ const sql = `SELECT ${aggregateExpression.sql} as computed_value
7262
7278
  FROM ${tableName}
7263
7279
  WHERE ${timeWindowFilter}`;
7264
- const row = await this.database.prepare(sql).bind(...bindings).first();
7280
+ const row = await this.database.prepare(sql).bind(...aggregateExpression.bindings, ...timeBindings).first();
7265
7281
  return row?.computed_value ?? 0;
7266
7282
  }
7267
7283
  async growth(seed, formula, window) {
@@ -7269,8 +7285,8 @@ var D1WidgetRepository = class {
7269
7285
  const { currentFilter, previousFilter } = this.buildPreviousWindowFilter(window);
7270
7286
  const tableName = `content_${seed.slug}`;
7271
7287
  const [currentRow, previousRow] = await Promise.all([
7272
- this.database.prepare(`SELECT ${aggregateExpression} as computed_value FROM ${tableName} WHERE ${currentFilter.sql}`).bind(...currentFilter.bindings).first(),
7273
- this.database.prepare(`SELECT ${aggregateExpression} as computed_value FROM ${tableName} WHERE ${previousFilter.sql}`).bind(...previousFilter.bindings).first()
7288
+ this.database.prepare(`SELECT ${aggregateExpression.sql} as computed_value FROM ${tableName} WHERE ${currentFilter.sql}`).bind(...aggregateExpression.bindings, ...currentFilter.bindings).first(),
7289
+ this.database.prepare(`SELECT ${aggregateExpression.sql} as computed_value FROM ${tableName} WHERE ${previousFilter.sql}`).bind(...aggregateExpression.bindings, ...previousFilter.bindings).first()
7274
7290
  ]);
7275
7291
  return {
7276
7292
  currentValue: currentRow?.computed_value ?? 0,
@@ -7330,16 +7346,16 @@ var D1WidgetRepository = class {
7330
7346
  async timeseries(seed, formula, window, groupColumn) {
7331
7347
  const groupColumnExpression = this.resolveColumnExpression(seed, groupColumn);
7332
7348
  const aggregateExpression = this.buildAggregateExpression(seed, formula);
7333
- const { sql: timeWindowFilter, bindings } = this.buildTimeWindowFilter(window);
7349
+ const { sql: timeWindowFilter, bindings: timeBindings } = this.buildTimeWindowFilter(window);
7334
7350
  const tableName = `content_${seed.slug}`;
7335
7351
  const dateBucketExpression = groupColumnExpression === "created_at" ? `strftime('%Y-%m-%d', created_at, 'unixepoch')` : `strftime('%Y-%m-%d', CAST(${groupColumnExpression} AS INTEGER), 'unixepoch')`;
7336
7352
  const sql = `SELECT ${dateBucketExpression} as bucket_label,
7337
- ${aggregateExpression} as bucket_value
7353
+ ${aggregateExpression.sql} as bucket_value
7338
7354
  FROM ${tableName}
7339
7355
  WHERE ${timeWindowFilter}
7340
7356
  GROUP BY bucket_label
7341
7357
  ORDER BY bucket_label ASC`;
7342
- const rows = await this.database.prepare(sql).bind(...bindings).all();
7358
+ const rows = await this.database.prepare(sql).bind(...aggregateExpression.bindings, ...timeBindings).all();
7343
7359
  return (rows.results ?? []).map((row) => ({
7344
7360
  label: row.bucket_label ?? "",
7345
7361
  value: row.bucket_value ?? 0
@@ -7374,44 +7390,46 @@ var D1WidgetRepository = class {
7374
7390
  buildAggregateExpression(seed, formula) {
7375
7391
  switch (formula.op) {
7376
7392
  case "count":
7377
- return "COUNT(*)";
7393
+ return { sql: "COUNT(*)", bindings: [] };
7378
7394
  case "sum": {
7379
7395
  const column = this.resolveColumnExpression(seed, formula.column);
7380
- return `SUM(CAST(${column} AS REAL))`;
7396
+ return { sql: `SUM(CAST(${column} AS REAL))`, bindings: [] };
7381
7397
  }
7382
7398
  case "avg": {
7383
7399
  const column = this.resolveColumnExpression(seed, formula.column);
7384
- return `AVG(CAST(${column} AS REAL))`;
7400
+ return { sql: `AVG(CAST(${column} AS REAL))`, bindings: [] };
7385
7401
  }
7386
7402
  case "min": {
7387
7403
  const column = this.resolveColumnExpression(seed, formula.column);
7388
- return `MIN(CAST(${column} AS REAL))`;
7404
+ return { sql: `MIN(CAST(${column} AS REAL))`, bindings: [] };
7389
7405
  }
7390
7406
  case "max": {
7391
7407
  const column = this.resolveColumnExpression(seed, formula.column);
7392
- return `MAX(CAST(${column} AS REAL))`;
7408
+ return { sql: `MAX(CAST(${column} AS REAL))`, bindings: [] };
7393
7409
  }
7394
7410
  case "countWhere":
7395
7411
  return this.buildCountWhereExpression(seed, formula.column, formula.value);
7396
7412
  case "percentageOf": {
7397
7413
  const numerator = this.resolveColumnExpression(seed, formula.numeratorColumn);
7398
7414
  const denominator = this.resolveColumnExpression(seed, formula.denominatorColumn);
7399
- return `CASE WHEN SUM(CAST(${denominator} AS REAL)) = 0 THEN 0 ELSE (SUM(CAST(${numerator} AS REAL)) * 100.0 / SUM(CAST(${denominator} AS REAL))) END`;
7415
+ return {
7416
+ sql: `CASE WHEN SUM(CAST(${denominator} AS REAL)) = 0 THEN 0 ELSE (SUM(CAST(${numerator} AS REAL)) * 100.0 / SUM(CAST(${denominator} AS REAL))) END`,
7417
+ bindings: []
7418
+ };
7400
7419
  }
7401
7420
  }
7402
7421
  }
7403
7422
  buildCountWhereExpression(seed, alias, value) {
7404
7423
  const column = this.resolveColumnExpression(seed, alias);
7405
- if (value === null) return `COUNT(CASE WHEN ${column} IS NULL THEN 1 END)`;
7424
+ if (value === null) return { sql: `COUNT(CASE WHEN ${column} IS NULL THEN 1 END)`, bindings: [] };
7406
7425
  if (typeof value === "boolean") {
7407
7426
  const numericValue = value ? 1 : 0;
7408
- return `COUNT(CASE WHEN ${column} = ${numericValue} THEN 1 END)`;
7427
+ return { sql: `COUNT(CASE WHEN ${column} = ? THEN 1 END)`, bindings: [numericValue] };
7409
7428
  }
7410
7429
  if (typeof value === "number" && Number.isFinite(value)) {
7411
- return `COUNT(CASE WHEN CAST(${column} AS REAL) = ${value} THEN 1 END)`;
7430
+ return { sql: `COUNT(CASE WHEN CAST(${column} AS REAL) = ? THEN 1 END)`, bindings: [value] };
7412
7431
  }
7413
- const escaped = String(value).replace(/'/g, "''");
7414
- return `COUNT(CASE WHEN ${column} = '${escaped}' THEN 1 END)`;
7432
+ return { sql: `COUNT(CASE WHEN ${column} = ? THEN 1 END)`, bindings: [value] };
7415
7433
  }
7416
7434
  buildTimeWindowFilter(window) {
7417
7435
  if (isDateRange(window)) {
@@ -7689,10 +7707,10 @@ var D1SiteSettingsRepository = class {
7689
7707
  var nowSec = Math.floor(Date.now() / 1e3);
7690
7708
  var DAY = 86400;
7691
7709
  var t_c01 = nowSec - 25 * DAY;
7692
- var t_c02 = nowSec - 20 * DAY;
7693
- var t_c03 = nowSec - 15 * DAY;
7694
- var t_c04 = nowSec - 10 * DAY;
7695
- var t_c05 = nowSec - 3 * DAY;
7710
+ var t_c02 = nowSec - 18 * DAY;
7711
+ var t_c03 = nowSec - 12 * DAY;
7712
+ var t_c04 = nowSec - 6 * DAY;
7713
+ var t_c05 = nowSec - 1 * DAY;
7696
7714
  function toIsoDate(sec) {
7697
7715
  return new Date(sec * 1e3).toISOString().split("T")[0];
7698
7716
  }
@@ -7723,11 +7741,11 @@ var DEMO_CLIENTI_FIXTURES = [
7723
7741
  },
7724
7742
  {
7725
7743
  id: "c0200000-0000-4000-8000-000000000002",
7726
- slug: "davide-ferrari",
7744
+ slug: "david-ferrari",
7727
7745
  status: "published",
7728
7746
  data: {
7729
- name: "Davide Ferrari",
7730
- email: "davide.ferrari@innovasolutions.it",
7747
+ name: "David Ferrari",
7748
+ email: "david.ferrari@innovasolutions.io",
7731
7749
  company: "Innova Solutions",
7732
7750
  tier: "pro",
7733
7751
  account_status: "active",
@@ -7736,11 +7754,11 @@ var DEMO_CLIENTI_FIXTURES = [
7736
7754
  },
7737
7755
  {
7738
7756
  id: "c0300000-0000-4000-8000-000000000003",
7739
- slug: "filippo-lombardi",
7757
+ slug: "philip-lombardi",
7740
7758
  status: "published",
7741
7759
  data: {
7742
- name: "Filippo Lombardi",
7743
- email: "filippo.lombardi@novalabs.dev",
7760
+ name: "Philip Lombardi",
7761
+ email: "philip.lombardi@novalabs.dev",
7744
7762
  company: "Nova Labs",
7745
7763
  tier: "free",
7746
7764
  account_status: "churned",
@@ -7749,11 +7767,11 @@ var DEMO_CLIENTI_FIXTURES = [
7749
7767
  },
7750
7768
  {
7751
7769
  id: "c0400000-0000-4000-8000-000000000004",
7752
- slug: "elisa-gallo",
7770
+ slug: "sarah-gallo",
7753
7771
  status: "published",
7754
7772
  data: {
7755
- name: "Elisa Gallo",
7756
- email: "elisa.gallo@alphadynamics.io",
7773
+ name: "Sarah Gallo",
7774
+ email: "sarah.gallo@alphadynamics.io",
7757
7775
  company: "Alpha Dynamics",
7758
7776
  tier: "enterprise",
7759
7777
  account_status: "active",
@@ -7762,11 +7780,11 @@ var DEMO_CLIENTI_FIXTURES = [
7762
7780
  },
7763
7781
  {
7764
7782
  id: "c0500000-0000-4000-8000-000000000005",
7765
- slug: "matteo-conti",
7783
+ slug: "matthew-conti",
7766
7784
  status: "published",
7767
7785
  data: {
7768
- name: "Matteo Conti",
7769
- email: "matteo.conti@cloudbase.app",
7786
+ name: "Matthew Conti",
7787
+ email: "matthew.conti@cloudbase.app",
7770
7788
  company: "CloudBase",
7771
7789
  tier: "free",
7772
7790
  account_status: "active",
@@ -7777,7 +7795,7 @@ var DEMO_CLIENTI_FIXTURES = [
7777
7795
  var DEMO_ABBONAMENTI_FIXTURES = [
7778
7796
  {
7779
7797
  id: "a0100000-0000-4000-8000-000000000001",
7780
- slug: "abbonamento-vertex-digital",
7798
+ slug: "subscription-vertex-digital",
7781
7799
  status: "published",
7782
7800
  data: {
7783
7801
  customer_id: "c0100000-0000-4000-8000-000000000001",
@@ -7788,7 +7806,7 @@ var DEMO_ABBONAMENTI_FIXTURES = [
7788
7806
  },
7789
7807
  {
7790
7808
  id: "a0200000-0000-4000-8000-000000000002",
7791
- slug: "abbonamento-innova-solutions",
7809
+ slug: "subscription-innova-solutions",
7792
7810
  status: "published",
7793
7811
  data: {
7794
7812
  customer_id: "c0200000-0000-4000-8000-000000000002",
@@ -7799,7 +7817,7 @@ var DEMO_ABBONAMENTI_FIXTURES = [
7799
7817
  },
7800
7818
  {
7801
7819
  id: "a0300000-0000-4000-8000-000000000003",
7802
- slug: "abbonamento-alpha-dynamics",
7820
+ slug: "subscription-alpha-dynamics",
7803
7821
  status: "published",
7804
7822
  data: {
7805
7823
  customer_id: "c0400000-0000-4000-8000-000000000004",
@@ -7812,10 +7830,10 @@ var DEMO_ABBONAMENTI_FIXTURES = [
7812
7830
  var DEMO_TICKET_FIXTURES = [
7813
7831
  {
7814
7832
  id: "t0100000-0000-4000-8000-000000000001",
7815
- slug: "integrazione-webhook-fallita",
7833
+ slug: "webhook-integration-failure",
7816
7834
  status: "published",
7817
7835
  data: {
7818
- title: "Integrazione webhook fallita su eventi invoice.paid",
7836
+ title: "Webhook integration failure on invoice.paid event",
7819
7837
  customer_id: "c0100000-0000-4000-8000-000000000001",
7820
7838
  priority: "high",
7821
7839
  category: "technical",
@@ -7824,10 +7842,10 @@ var DEMO_TICKET_FIXTURES = [
7824
7842
  },
7825
7843
  {
7826
7844
  id: "t0200000-0000-4000-8000-000000000002",
7827
- slug: "richiesta-fattura-elettronica",
7845
+ slug: "annual-billing-vat-invoicing",
7828
7846
  status: "published",
7829
7847
  data: {
7830
- title: "Richiesta variazione codice SDI per fatturazione annuale",
7848
+ title: "Annual billing VAT invoice code update request",
7831
7849
  customer_id: "c0400000-0000-4000-8000-000000000004",
7832
7850
  priority: "medium",
7833
7851
  category: "billing",
@@ -7843,7 +7861,7 @@ var DEMO_CHANGELOG_FIXTURES = [
7843
7861
  data: {
7844
7862
  version: "v2.4.0",
7845
7863
  release_date: toIsoDate(nowSec - 22 * DAY),
7846
- features: toRichtextDoc("Introdotto il supporto per la privacy nativa a 4 livelli, cifratura AES-256-GCM e Blind Indexing per la ricerca sicura.")
7864
+ features: toRichtextDoc("Introduced 4-tier native privacy support, AES-256-GCM encryption, and Blind Indexing for secure search.")
7847
7865
  }
7848
7866
  },
7849
7867
  {
@@ -7853,31 +7871,31 @@ var DEMO_CHANGELOG_FIXTURES = [
7853
7871
  data: {
7854
7872
  version: "v2.3.0",
7855
7873
  release_date: toIsoDate(nowSec - 8 * DAY),
7856
- features: toRichtextDoc("Aggiunto l engine di automazioni avanzate e il posizionamento dinamico delle schede Kanban.")
7874
+ features: toRichtextDoc("Added advanced automation engine and dynamic Kanban card positioning.")
7857
7875
  }
7858
7876
  }
7859
7877
  ];
7860
7878
  var DEMO_ARTICOLI_FIXTURES = [
7861
7879
  {
7862
7880
  id: "ar100000-0000-4000-8000-000000000001",
7863
- slug: "guida-alla-privacy-application-level-encryption",
7881
+ slug: "guide-to-application-level-encryption",
7864
7882
  status: "published",
7865
7883
  data: {
7866
- title: "Guida all Application-Level Encryption in BeechCMS",
7884
+ title: "Guide to Application-Level Encryption in BeechCMS",
7867
7885
  author: "Flavio De Musso",
7868
7886
  cover_image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71.jpg?w=800&q=80",
7869
- body: toRichtextDoc("Scopri come proteggere i dati riservati dei tuoi clienti direttamente nell engine senza sacrificare le performance edge di Cloudflare Workers.")
7887
+ body: toRichtextDoc("Learn how to protect confidential customer data directly inside the engine without sacrificing Cloudflare Workers edge performance.")
7870
7888
  }
7871
7889
  },
7872
7890
  {
7873
7891
  id: "ar200000-0000-4000-8000-000000000002",
7874
- slug: "architettura-botanical-engine-e-d1",
7892
+ slug: "botanical-engine-and-cloudflare-d1",
7875
7893
  status: "published",
7876
7894
  data: {
7877
- title: "Architettura Botanical Engine & Cloudflare D1",
7895
+ title: "Botanical Engine Architecture & Cloudflare D1",
7878
7896
  author: "Beech Core Team",
7879
7897
  cover_image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f.jpg?w=800&q=80",
7880
- body: toRichtextDoc("Una panoramica approfondita sulla separazione tra i contratti di dominio di @beechcms/core e la persistenza SQLite edge.")
7898
+ body: toRichtextDoc("An in-depth overview of the separation between @beechcms/core domain contracts and edge SQLite persistence.")
7881
7899
  }
7882
7900
  }
7883
7901
  ];
@@ -7978,9 +7996,13 @@ var D1SeedLayoutRepository = class {
7978
7996
  await this.db.prepare("DELETE FROM seed_layouts WHERE slug = ?").bind(slug).run();
7979
7997
  }
7980
7998
  async getViewConfig(slug) {
7981
- const row = await this.db.prepare("SELECT view_config FROM seed_layouts WHERE slug = ?").bind(slug).first();
7982
- if (!row?.view_config) return null;
7983
- return seedViewConfigSchema2.parse(JSON.parse(row.view_config));
7999
+ try {
8000
+ const row = await this.db.prepare("SELECT view_config FROM seed_layouts WHERE slug = ?").bind(slug).first();
8001
+ if (!row?.view_config) return null;
8002
+ return seedViewConfigSchema2.parse(JSON.parse(row.view_config));
8003
+ } catch {
8004
+ return null;
8005
+ }
7984
8006
  }
7985
8007
  async setViewConfig(slug, config, updatedBy) {
7986
8008
  const json = JSON.stringify(config);
@@ -9939,6 +9961,9 @@ function createBeechApp(config) {
9939
9961
  apiPublic.use("*", publicRateLimitMiddleware());
9940
9962
  apiPublic.use("*", apiKeyMiddleware());
9941
9963
  apiPublic.route("/", publicRoutes);
9964
+ apiPublic.all("*", (c) => {
9965
+ return c.json({ error: "Not Found", message: `Public endpoint ${c.req.method} ${c.req.path} does not exist.` }, 404);
9966
+ });
9942
9967
  app.route("/api/v1/public", apiPublic);
9943
9968
  app.route("/api/webhooks", webhooksApp);
9944
9969
  app.get("/api/media/:key{.+}", (context) => serveMediaHandler(context));
@@ -9967,7 +9992,7 @@ function createBeechApp(config) {
9967
9992
  const headers = new Headers(assetResponse.headers);
9968
9993
  headers.set(
9969
9994
  "Content-Security-Policy",
9970
- "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self'; frame-ancestors 'none'"
9995
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; connect-src 'self' http: https:; frame-ancestors 'none'"
9971
9996
  );
9972
9997
  headers.set("X-Frame-Options", "DENY");
9973
9998
  headers.set("X-Content-Type-Options", "nosniff");