dineway 0.1.3

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 (96) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +89 -0
  3. package/dist/adapters-BlzWJG82.d.mts +106 -0
  4. package/dist/apply-CAPvMfoU.mjs +1339 -0
  5. package/dist/astro/index.d.mts +50 -0
  6. package/dist/astro/index.mjs +1326 -0
  7. package/dist/astro/middleware/auth.d.mts +30 -0
  8. package/dist/astro/middleware/auth.mjs +708 -0
  9. package/dist/astro/middleware/redirect.d.mts +21 -0
  10. package/dist/astro/middleware/redirect.mjs +62 -0
  11. package/dist/astro/middleware/request-context.d.mts +17 -0
  12. package/dist/astro/middleware/request-context.mjs +1371 -0
  13. package/dist/astro/middleware/setup.d.mts +19 -0
  14. package/dist/astro/middleware/setup.mjs +46 -0
  15. package/dist/astro/middleware.d.mts +12 -0
  16. package/dist/astro/middleware.mjs +1716 -0
  17. package/dist/astro/types.d.mts +269 -0
  18. package/dist/astro/types.mjs +1 -0
  19. package/dist/base64-F8-DUraK.mjs +58 -0
  20. package/dist/byline-DeWCMU_i.mjs +234 -0
  21. package/dist/bylines-DyqBV9EQ.mjs +137 -0
  22. package/dist/chunk-ClPoSABd.mjs +21 -0
  23. package/dist/cli/index.d.mts +1 -0
  24. package/dist/cli/index.mjs +3987 -0
  25. package/dist/client/external-auth-headers.d.mts +38 -0
  26. package/dist/client/external-auth-headers.mjs +101 -0
  27. package/dist/client/index.d.mts +397 -0
  28. package/dist/client/index.mjs +345 -0
  29. package/dist/config-Cq8H0SfX.mjs +46 -0
  30. package/dist/connection-C9pxzuag.mjs +52 -0
  31. package/dist/content-zSgdNmnt.mjs +836 -0
  32. package/dist/db/index.d.mts +4 -0
  33. package/dist/db/index.mjs +62 -0
  34. package/dist/db/libsql.d.mts +10 -0
  35. package/dist/db/libsql.mjs +21 -0
  36. package/dist/db/postgres.d.mts +10 -0
  37. package/dist/db/postgres.mjs +29 -0
  38. package/dist/db/sqlite.d.mts +10 -0
  39. package/dist/db/sqlite.mjs +15 -0
  40. package/dist/default-WYlzADZL.mjs +80 -0
  41. package/dist/dialect-helpers-B9uSp2GJ.mjs +89 -0
  42. package/dist/error-DrxtnGPg.mjs +26 -0
  43. package/dist/index-C-jx21qs.d.mts +4771 -0
  44. package/dist/index.d.mts +16 -0
  45. package/dist/index.mjs +30 -0
  46. package/dist/load-C6FCD1FU.mjs +27 -0
  47. package/dist/loader-qKmo0wAY.mjs +446 -0
  48. package/dist/manifest-schema-CTSEyIJ3.mjs +186 -0
  49. package/dist/media/index.d.mts +25 -0
  50. package/dist/media/index.mjs +54 -0
  51. package/dist/media/local-runtime.d.mts +38 -0
  52. package/dist/media/local-runtime.mjs +132 -0
  53. package/dist/media-DMTr80Gv.mjs +199 -0
  54. package/dist/mode-BlyYtIFO.mjs +22 -0
  55. package/dist/page/index.d.mts +148 -0
  56. package/dist/page/index.mjs +419 -0
  57. package/dist/placeholder-B3knXwNc.mjs +267 -0
  58. package/dist/placeholder-bOx1xCTY.d.mts +283 -0
  59. package/dist/plugin-utils.d.mts +57 -0
  60. package/dist/plugin-utils.mjs +77 -0
  61. package/dist/plugins/adapt-sandbox-entry.d.mts +21 -0
  62. package/dist/plugins/adapt-sandbox-entry.mjs +112 -0
  63. package/dist/query-BiaPl_g2.mjs +459 -0
  64. package/dist/redirect-JPqLAbxa.mjs +328 -0
  65. package/dist/registry-DSd1GWB8.mjs +851 -0
  66. package/dist/request-context.d.mts +49 -0
  67. package/dist/request-context.mjs +42 -0
  68. package/dist/runner-B5l1JfOj.d.mts +26 -0
  69. package/dist/runner-BGUGywgG.mjs +1529 -0
  70. package/dist/runtime.d.mts +25 -0
  71. package/dist/runtime.mjs +41 -0
  72. package/dist/search-BNruJHDL.mjs +11054 -0
  73. package/dist/seed/index.d.mts +3 -0
  74. package/dist/seed/index.mjs +15 -0
  75. package/dist/seo/index.d.mts +69 -0
  76. package/dist/seo/index.mjs +69 -0
  77. package/dist/storage/local.d.mts +38 -0
  78. package/dist/storage/local.mjs +165 -0
  79. package/dist/storage/s3.d.mts +31 -0
  80. package/dist/storage/s3.mjs +174 -0
  81. package/dist/tokens-4vgYuXsZ.mjs +170 -0
  82. package/dist/transport-C5FYnid7.mjs +417 -0
  83. package/dist/transport-gIL-e43D.d.mts +41 -0
  84. package/dist/types-BawVha09.mjs +30 -0
  85. package/dist/types-BgQeVaPj.d.mts +192 -0
  86. package/dist/types-CLLdsG3g.d.mts +103 -0
  87. package/dist/types-D38djUXv.d.mts +1196 -0
  88. package/dist/types-DShnjzb6.mjs +15 -0
  89. package/dist/types-DkvMXalq.d.mts +425 -0
  90. package/dist/types-DuNbGKjF.mjs +74 -0
  91. package/dist/types-ju-_ORz7.d.mts +182 -0
  92. package/dist/validate-CXnRKfJK.mjs +327 -0
  93. package/dist/validate-CqRJb_xU.mjs +96 -0
  94. package/dist/validate-DVKJJ-M_.d.mts +377 -0
  95. package/locals.d.ts +47 -0
  96. package/package.json +313 -0
@@ -0,0 +1,1716 @@
1
+ import "../connection-C9pxzuag.mjs";
2
+ import { a as isSqlite } from "../dialect-helpers-B9uSp2GJ.mjs";
3
+ import { r as runMigrations } from "../runner-BGUGywgG.mjs";
4
+ import { $ as CronExecutor, $t as handleContentSchedule, At as handleMediaGet, Bt as handleContentCountScheduled, Ct as createPluginBundleStore, Et as PluginStateRepository, Ft as handleRevisionRestore, Gt as handleContentDuplicate, Ht as handleContentCreate, J as devConsoleEmailDeliver, Jt as handleContentList, K as PluginRouteRegistry, Kt as handleContentGet, Mt as handleMediaUpdate, Nt as handleRevisionGet, Ot as handleMediaCreate, Pt as handleRevisionList, Q as resolveExclusiveHooks, Qt as handleContentRestore, Rt as hashString, Ut as handleContentDelete, Vt as handleContentCountTrashed, Wt as handleContentDiscardDraft, Xt as handleContentPermanentDelete, Y as EmailPipeline, Yt as handleContentListTrashed, Z as createHookPipeline, Zt as handleContentPublish, en as handleContentTranslations, et as extractRequestMeta, in as validateRev, jt as handleMediaList, kt as handleMediaDelete, nn as handleContentUnschedule, nt as definePlugin, q as DEV_CONSOLE_EMAIL_PLUGIN_ID, qt as handleContentGetIncludingTrashed, rn as handleContentUpdate, tn as handleContentUnpublish, tt as sanitizeHeadersForSandbox, zt as handleContentCompare } from "../search-BNruJHDL.mjs";
5
+ import { r as RevisionRepository } from "../content-zSgdNmnt.mjs";
6
+ import "../base64-F8-DUraK.mjs";
7
+ import "../types-BawVha09.mjs";
8
+ import { t as MediaRepository } from "../media-DMTr80Gv.mjs";
9
+ import { f as OptionsRepository } from "../apply-CAPvMfoU.mjs";
10
+ import { i as FTSManager, n as SchemaRegistry } from "../registry-DSd1GWB8.mjs";
11
+ import "../redirect-JPqLAbxa.mjs";
12
+ import "../byline-DeWCMU_i.mjs";
13
+ import { n as normalizeMediaValue } from "../placeholder-B3knXwNc.mjs";
14
+ import { i as setI18nConfig } from "../config-Cq8H0SfX.mjs";
15
+ import { getRequestContext, runWithContext } from "../request-context.mjs";
16
+ import { n as getDb } from "../loader-qKmo0wAY.mjs";
17
+ import { r as normalizeManifestRoute } from "../manifest-schema-CTSEyIJ3.mjs";
18
+ import "../query-BiaPl_g2.mjs";
19
+ import "../tokens-4vgYuXsZ.mjs";
20
+ import "../bylines-DyqBV9EQ.mjs";
21
+ import "../load-C6FCD1FU.mjs";
22
+ import "../index.mjs";
23
+ import { t as getAuthMode } from "../mode-BlyYtIFO.mjs";
24
+ import { Kysely, sql } from "kysely";
25
+ import { defineMiddleware } from "astro:middleware";
26
+ import virtualConfig from "virtual:dineway/config";
27
+ import { createDialect } from "virtual:dineway/dialect";
28
+ import { mediaProviders } from "virtual:dineway/media-providers";
29
+ import { plugins } from "virtual:dineway/plugins";
30
+ import { createSandboxRunner, sandboxEnabled } from "virtual:dineway/sandbox-runner";
31
+ import { sandboxedPlugins } from "virtual:dineway/sandboxed-plugins";
32
+ import { createStorage } from "virtual:dineway/storage";
33
+ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
34
+
35
+ //#region src/auth/challenge-store.ts
36
+ /**
37
+ * Clean up expired challenges.
38
+ * Should be called periodically (e.g., on startup, or via cron).
39
+ */
40
+ async function cleanupExpiredChallenges(db) {
41
+ const now = (/* @__PURE__ */ new Date()).toISOString();
42
+ const result = await db.deleteFrom("auth_challenges").where("expires_at", "<", now).executeTakeFirst();
43
+ return Number(result.numDeletedRows ?? 0);
44
+ }
45
+
46
+ //#endregion
47
+ //#region src/cleanup.ts
48
+ /**
49
+ * System cleanup
50
+ *
51
+ * Runs periodic maintenance tasks that prevent unbounded accumulation of
52
+ * expired or stale data. Called from cron scheduler ticks and (for latency-
53
+ * sensitive subsystems) inline during relevant requests.
54
+ *
55
+ * Each subsystem cleanup is independent and non-fatal -- if one fails, the
56
+ * rest still run. Failures are logged but never surface to callers.
57
+ */
58
+ /** Max revisions to keep per entry during periodic pruning */
59
+ const REVISION_KEEP_COUNT = 50;
60
+ /** Only prune entries that exceed this threshold */
61
+ const REVISION_PRUNE_THRESHOLD = REVISION_KEEP_COUNT;
62
+ /**
63
+ * Run all system cleanup tasks.
64
+ *
65
+ * Safe to call frequently -- each task is a single DELETE with a WHERE clause,
66
+ * so repeated calls with nothing to clean are cheap (no-op queries).
67
+ *
68
+ * @param db - The database instance
69
+ * @param storage - Optional storage backend for deleting orphaned files.
70
+ * When omitted, pending upload DB rows are still deleted but the
71
+ * corresponding files in object storage are not removed.
72
+ */
73
+ async function runSystemCleanup(db, storage) {
74
+ const result = {
75
+ challenges: -1,
76
+ expiredTokens: -1,
77
+ pendingUploads: -1,
78
+ pendingUploadFiles: -1,
79
+ revisionsPruned: -1
80
+ };
81
+ try {
82
+ result.challenges = await cleanupExpiredChallenges(db);
83
+ } catch (error) {
84
+ console.error("[cleanup] Failed to clean expired challenges:", error);
85
+ }
86
+ try {
87
+ await createKyselyAdapter(db).deleteExpiredTokens();
88
+ result.expiredTokens = 0;
89
+ } catch (error) {
90
+ console.error("[cleanup] Failed to clean expired tokens:", error);
91
+ }
92
+ try {
93
+ const orphanedKeys = await new MediaRepository(db).cleanupPendingUploads();
94
+ result.pendingUploads = orphanedKeys.length;
95
+ if (storage && orphanedKeys.length > 0) {
96
+ let filesDeleted = 0;
97
+ for (const key of orphanedKeys) try {
98
+ await storage.delete(key);
99
+ filesDeleted++;
100
+ } catch (error) {
101
+ console.error(`[cleanup] Failed to delete storage file ${key}:`, error);
102
+ }
103
+ result.pendingUploadFiles = filesDeleted;
104
+ } else result.pendingUploadFiles = 0;
105
+ } catch (error) {
106
+ console.error("[cleanup] Failed to clean pending uploads:", error);
107
+ }
108
+ try {
109
+ result.revisionsPruned = await pruneExcessiveRevisions(db);
110
+ } catch (error) {
111
+ console.error("[cleanup] Failed to prune revisions:", error);
112
+ }
113
+ return result;
114
+ }
115
+ /**
116
+ * Find entries with more than REVISION_PRUNE_THRESHOLD revisions and prune
117
+ * them down to REVISION_KEEP_COUNT.
118
+ */
119
+ async function pruneExcessiveRevisions(db) {
120
+ const entries = await sql`
121
+ SELECT collection, entry_id, COUNT(*) as cnt
122
+ FROM revisions
123
+ GROUP BY collection, entry_id
124
+ HAVING cnt > ${REVISION_PRUNE_THRESHOLD}
125
+ `.execute(db);
126
+ if (entries.rows.length === 0) return 0;
127
+ const revisionRepo = new RevisionRepository(db);
128
+ let totalPruned = 0;
129
+ for (const row of entries.rows) try {
130
+ const pruned = await revisionRepo.pruneOldRevisions(row.collection, row.entry_id, REVISION_KEEP_COUNT);
131
+ totalPruned += pruned;
132
+ } catch (error) {
133
+ console.error(`[cleanup] Failed to prune revisions for ${row.collection}/${row.entry_id}:`, error);
134
+ }
135
+ return totalPruned;
136
+ }
137
+
138
+ //#endregion
139
+ //#region src/comments/moderator.ts
140
+ /** Plugin ID for the built-in default comment moderator */
141
+ const DEFAULT_COMMENT_MODERATOR_PLUGIN_ID = "dineway-default-comment-moderator";
142
+ /**
143
+ * The comment:moderate handler for the built-in default moderator.
144
+ */
145
+ async function defaultCommentModerate(event, _ctx) {
146
+ const { comment, collectionSettings, priorApprovedCount } = event;
147
+ if (collectionSettings.commentsAutoApproveUsers && comment.authorUserId) return {
148
+ status: "approved",
149
+ reason: "Authenticated CMS user"
150
+ };
151
+ if (collectionSettings.commentsModeration === "none") return {
152
+ status: "approved",
153
+ reason: "Moderation disabled"
154
+ };
155
+ if (collectionSettings.commentsModeration === "first_time" && priorApprovedCount > 0) return {
156
+ status: "approved",
157
+ reason: "Returning commenter"
158
+ };
159
+ return {
160
+ status: "pending",
161
+ reason: "Held for review"
162
+ };
163
+ }
164
+
165
+ //#endregion
166
+ //#region src/plugins/sandbox/runtime-options.ts
167
+ function createSandboxRunnerRuntimeOptions(input) {
168
+ return {
169
+ db: input.db,
170
+ mediaStorage: input.storage ?? void 0,
171
+ cronReschedule: input.cronReschedule,
172
+ siteInfo: input.siteInfo ? {
173
+ name: input.siteInfo.siteName ?? "",
174
+ url: input.siteInfo.siteUrl ?? "",
175
+ locale: input.siteInfo.locale ?? "en"
176
+ } : void 0
177
+ };
178
+ }
179
+
180
+ //#endregion
181
+ //#region src/plugins/scheduler/node.ts
182
+ /** Minimum polling interval (ms) — prevents tight loops if next_run_at is in the past */
183
+ const MIN_INTERVAL_MS = 1e3;
184
+ /** Maximum polling interval (ms) — wake up periodically to check for stale locks */
185
+ const MAX_INTERVAL_MS = 300 * 1e3;
186
+ var NodeCronScheduler = class {
187
+ timer = null;
188
+ running = false;
189
+ systemCleanup = null;
190
+ constructor(executor) {
191
+ this.executor = executor;
192
+ }
193
+ setSystemCleanup(fn) {
194
+ this.systemCleanup = fn;
195
+ }
196
+ start() {
197
+ this.running = true;
198
+ this.arm();
199
+ }
200
+ stop() {
201
+ this.running = false;
202
+ if (this.timer) {
203
+ clearTimeout(this.timer);
204
+ this.timer = null;
205
+ }
206
+ }
207
+ reschedule() {
208
+ if (!this.running) return;
209
+ if (this.timer) {
210
+ clearTimeout(this.timer);
211
+ this.timer = null;
212
+ }
213
+ this.arm();
214
+ }
215
+ arm() {
216
+ if (!this.running) return;
217
+ this.executor.getNextDueTime().then((nextDue) => {
218
+ if (!this.running) return void 0;
219
+ let delayMs;
220
+ if (nextDue) {
221
+ const dueAt = new Date(nextDue).getTime();
222
+ delayMs = Math.max(dueAt - Date.now(), MIN_INTERVAL_MS);
223
+ delayMs = Math.min(delayMs, MAX_INTERVAL_MS);
224
+ } else delayMs = MAX_INTERVAL_MS;
225
+ this.timer = setTimeout(() => {
226
+ if (!this.running) return;
227
+ this.executeTick();
228
+ }, delayMs);
229
+ if (this.timer && typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
230
+ }).catch((error) => {
231
+ console.error("[cron:node] Failed to get next due time:", error);
232
+ if (this.running) {
233
+ this.timer = setTimeout(() => this.arm(), MAX_INTERVAL_MS);
234
+ if (this.timer && typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
235
+ }
236
+ });
237
+ }
238
+ executeTick() {
239
+ if (!this.running) return;
240
+ const tasks = [this.executor.tick(), this.executor.recoverStaleLocks()];
241
+ if (this.systemCleanup) tasks.push(this.systemCleanup());
242
+ Promise.allSettled(tasks).then((results) => {
243
+ for (const r of results) if (r.status === "rejected") console.error("[cron:node] Tick task failed:", r.reason);
244
+ }).finally(() => {
245
+ if (this.running) this.arm();
246
+ });
247
+ }
248
+ };
249
+
250
+ //#endregion
251
+ //#region src/plugins/scheduler/piggyback.ts
252
+ /** Minimum interval between tick attempts (ms) */
253
+ const DEBOUNCE_MS = 60 * 1e3;
254
+ var PiggybackScheduler = class {
255
+ lastTickAt = 0;
256
+ running = false;
257
+ systemCleanup = null;
258
+ constructor(executor) {
259
+ this.executor = executor;
260
+ }
261
+ setSystemCleanup(fn) {
262
+ this.systemCleanup = fn;
263
+ }
264
+ start() {
265
+ this.running = true;
266
+ }
267
+ stop() {
268
+ this.running = false;
269
+ }
270
+ /**
271
+ * No-op for piggyback — tick happens on next request.
272
+ */
273
+ reschedule() {}
274
+ /**
275
+ * Call this from middleware on each request.
276
+ * Debounced: only actually ticks if enough time has passed.
277
+ */
278
+ onRequest() {
279
+ if (!this.running) return;
280
+ const now = Date.now();
281
+ if (now - this.lastTickAt < DEBOUNCE_MS) return;
282
+ this.lastTickAt = now;
283
+ const tasks = [this.executor.tick(), this.executor.recoverStaleLocks()];
284
+ if (this.systemCleanup) tasks.push(this.systemCleanup());
285
+ Promise.allSettled(tasks).then((results) => {
286
+ for (const r of results) if (r.status === "rejected") console.error("[cron:piggyback] Tick task failed:", r.reason);
287
+ });
288
+ }
289
+ };
290
+
291
+ //#endregion
292
+ //#region src/plugins/scheduler/strategy.ts
293
+ function hasUnref(handle) {
294
+ return typeof handle === "object" && handle !== null && "unref" in handle && typeof handle.unref === "function";
295
+ }
296
+ function shouldUsePiggybackScheduler(createTimer = () => globalThis.setTimeout(() => {}, 0), cancelTimer = (handle) => globalThis.clearTimeout(handle)) {
297
+ const handle = createTimer();
298
+ cancelTimer(handle);
299
+ return !hasUnref(handle);
300
+ }
301
+
302
+ //#endregion
303
+ //#region src/dineway-runtime.ts
304
+ const LEADING_SLASH_PATTERN = /^\//;
305
+ const VALID_METADATA_KINDS = new Set([
306
+ "meta",
307
+ "property",
308
+ "link",
309
+ "jsonld"
310
+ ]);
311
+ /** Security-critical allowlist for link rel values from sandboxed plugins */
312
+ const VALID_LINK_REL = new Set([
313
+ "canonical",
314
+ "alternate",
315
+ "author",
316
+ "license",
317
+ "site.standard.document"
318
+ ]);
319
+ /**
320
+ * Runtime validation for sandboxed plugin metadata contributions.
321
+ * Sandboxed plugins return `unknown` across the RPC boundary — we must
322
+ * verify the shape before passing to the metadata collector.
323
+ */
324
+ function isValidMetadataContribution(c) {
325
+ if (!c || typeof c !== "object" || !("kind" in c)) return false;
326
+ const obj = c;
327
+ if (typeof obj.kind !== "string" || !VALID_METADATA_KINDS.has(obj.kind)) return false;
328
+ switch (obj.kind) {
329
+ case "meta": return typeof obj.name === "string" && typeof obj.content === "string";
330
+ case "property": return typeof obj.property === "string" && typeof obj.content === "string";
331
+ case "link": return typeof obj.href === "string" && typeof obj.rel === "string" && VALID_LINK_REL.has(obj.rel);
332
+ case "jsonld": return obj.graph != null && typeof obj.graph === "object";
333
+ default: return false;
334
+ }
335
+ }
336
+ function isKyselyDatabase(value) {
337
+ return value instanceof Kysely;
338
+ }
339
+ function getRequestContextDatabase() {
340
+ const db = getRequestContext()?.db;
341
+ return isKyselyDatabase(db) ? db : null;
342
+ }
343
+ function parseStringArray(value) {
344
+ if (!value) return [];
345
+ try {
346
+ const parsed = JSON.parse(value);
347
+ return Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string") ? parsed.toSorted() : [];
348
+ } catch {
349
+ return [];
350
+ }
351
+ }
352
+ /**
353
+ * Map schema field types to editor field kinds
354
+ */
355
+ const FIELD_TYPE_TO_KIND = {
356
+ string: "string",
357
+ slug: "string",
358
+ text: "richText",
359
+ number: "number",
360
+ integer: "number",
361
+ boolean: "boolean",
362
+ datetime: "datetime",
363
+ select: "select",
364
+ multiSelect: "multiSelect",
365
+ portableText: "portableText",
366
+ image: "image",
367
+ file: "file",
368
+ reference: "reference",
369
+ json: "json",
370
+ repeater: "repeater"
371
+ };
372
+ /**
373
+ * Convert a ContentItem to Record<string, unknown> for hook consumption.
374
+ * Hooks receive the full item as a flat record.
375
+ */
376
+ function contentItemToRecord(item) {
377
+ return { ...item };
378
+ }
379
+ const dbCache = /* @__PURE__ */ new Map();
380
+ let dbInitPromise = null;
381
+ const storageCache = /* @__PURE__ */ new Map();
382
+ const sandboxedPluginCache = /* @__PURE__ */ new Map();
383
+ const marketplacePluginKeys = /* @__PURE__ */ new Set();
384
+ /** Manifest metadata for marketplace plugins: pluginId -> manifest admin config */
385
+ const marketplaceManifestCache = /* @__PURE__ */ new Map();
386
+ /** Route metadata for sandboxed plugins: pluginId -> routeName -> RouteMeta */
387
+ const sandboxedRouteMetaCache = /* @__PURE__ */ new Map();
388
+ let sandboxRunner = null;
389
+ /**
390
+ * DinewayRuntime - singleton per worker
391
+ */
392
+ var DinewayRuntime = class DinewayRuntime {
393
+ /**
394
+ * The singleton database instance (worker-lifetime cached).
395
+ * Use the `db` getter instead — it checks the request context first
396
+ * for per-request overrides from preview/playground sidecars and other
397
+ * request-scoped database flows.
398
+ */
399
+ _db;
400
+ storage;
401
+ configuredPlugins;
402
+ sandboxedPlugins;
403
+ sandboxedPluginEntries;
404
+ schemaRegistry;
405
+ _hooks;
406
+ config;
407
+ mediaProviders;
408
+ mediaProviderEntries;
409
+ cronExecutor;
410
+ email;
411
+ cronScheduler;
412
+ enabledPlugins;
413
+ pluginStates;
414
+ /** Current hook pipeline. Use the `hooks` getter for external access. */
415
+ get hooks() {
416
+ return this._hooks;
417
+ }
418
+ /** All plugins eligible for the hook pipeline (includes built-in plugins).
419
+ * Stored so we can rebuild the pipeline when plugins are enabled/disabled. */
420
+ allPipelinePlugins;
421
+ /** Factory options for the hook pipeline context factory */
422
+ pipelineFactoryOptions;
423
+ /** Dependencies needed for exclusive hook resolution */
424
+ runtimeDeps;
425
+ /** Mutable ref for the cron invokeCronHook closure to read the current pipeline */
426
+ pipelineRef;
427
+ /**
428
+ * Get the database instance for the current request.
429
+ *
430
+ * Checks the ALS-based request context first — middleware sets a
431
+ * per-request Kysely instance there for preview/playground session
432
+ * databases and other request-scoped overrides. Falls back to the
433
+ * singleton instance.
434
+ */
435
+ get db() {
436
+ const requestDb = getRequestContextDatabase();
437
+ if (requestDb) return requestDb;
438
+ return this._db;
439
+ }
440
+ constructor(db, storage, configuredPlugins, sandboxedPlugins, sandboxedPluginEntries, hooks, enabledPlugins, pluginStates, config, mediaProviders, mediaProviderEntries, cronExecutor, cronScheduler, emailPipeline, allPipelinePlugins, pipelineFactoryOptions, runtimeDeps, pipelineRef) {
441
+ this._db = db;
442
+ this.storage = storage;
443
+ this.configuredPlugins = configuredPlugins;
444
+ this.sandboxedPlugins = sandboxedPlugins;
445
+ this.sandboxedPluginEntries = sandboxedPluginEntries;
446
+ this.schemaRegistry = new SchemaRegistry(db);
447
+ this._hooks = hooks;
448
+ this.enabledPlugins = enabledPlugins;
449
+ this.pluginStates = pluginStates;
450
+ this.config = config;
451
+ this.mediaProviders = mediaProviders;
452
+ this.mediaProviderEntries = mediaProviderEntries;
453
+ this.cronExecutor = cronExecutor;
454
+ this.cronScheduler = cronScheduler;
455
+ this.email = emailPipeline;
456
+ this.allPipelinePlugins = allPipelinePlugins;
457
+ this.pipelineFactoryOptions = pipelineFactoryOptions;
458
+ this.runtimeDeps = runtimeDeps;
459
+ this.pipelineRef = pipelineRef;
460
+ }
461
+ /**
462
+ * Get the sandbox runner instance (for marketplace install/update)
463
+ */
464
+ getSandboxRunner() {
465
+ return sandboxRunner;
466
+ }
467
+ /**
468
+ * Tick the cron system from request context (piggyback mode).
469
+ * Call this from middleware on each request to ensure cron tasks
470
+ * execute even when no dedicated scheduler is available.
471
+ */
472
+ tickCron() {
473
+ if (this.cronScheduler instanceof PiggybackScheduler) this.cronScheduler.onRequest();
474
+ }
475
+ /**
476
+ * Stop the cron scheduler gracefully.
477
+ * Call during worker shutdown or hot-reload.
478
+ */
479
+ async stopCron() {
480
+ if (this.cronScheduler) await this.cronScheduler.stop();
481
+ }
482
+ /**
483
+ * Update in-memory plugin status and rebuild the hook pipeline.
484
+ *
485
+ * Rebuilding the pipeline ensures disabled plugins' hooks stop firing
486
+ * and re-enabled plugins' hooks start firing again without a restart.
487
+ * Exclusive hook selections are re-resolved after each rebuild.
488
+ */
489
+ async setPluginStatus(pluginId, status) {
490
+ this.pluginStates.set(pluginId, status);
491
+ if (status === "active") {
492
+ this.enabledPlugins.add(pluginId);
493
+ await this.rebuildHookPipeline();
494
+ await this._hooks.runPluginActivate(pluginId);
495
+ } else {
496
+ await this._hooks.runPluginDeactivate(pluginId);
497
+ this.enabledPlugins.delete(pluginId);
498
+ await this.rebuildHookPipeline();
499
+ }
500
+ }
501
+ /**
502
+ * Rebuild the hook pipeline from the current set of enabled plugins.
503
+ *
504
+ * Filters `allPipelinePlugins` to only those in `enabledPlugins`,
505
+ * creates a fresh HookPipeline, re-resolves exclusive hook selections,
506
+ * and re-wires the context factory so existing references (cron
507
+ * callbacks, email pipeline) use the new pipeline.
508
+ */
509
+ async rebuildHookPipeline() {
510
+ const newPipeline = createHookPipeline(this.allPipelinePlugins.filter((p) => this.enabledPlugins.has(p.id)), this.pipelineFactoryOptions);
511
+ await DinewayRuntime.resolveExclusiveHooks(newPipeline, this.db, this.runtimeDeps);
512
+ if (this.email) newPipeline.setContextFactory({
513
+ db: this.db,
514
+ emailPipeline: this.email
515
+ });
516
+ if (this.cronScheduler) {
517
+ const scheduler = this.cronScheduler;
518
+ newPipeline.setContextFactory({ cronReschedule: () => scheduler.reschedule() });
519
+ }
520
+ if (this.email) this.email.setPipeline(newPipeline);
521
+ this.pipelineRef.current = newPipeline;
522
+ this._hooks = newPipeline;
523
+ }
524
+ /**
525
+ * Synchronize marketplace plugin runtime state with DB + storage.
526
+ *
527
+ * Ensures install/update/uninstall changes take effect immediately in the
528
+ * current worker: loads newly active plugins and removes uninstalled ones.
529
+ */
530
+ async syncMarketplacePlugins() {
531
+ if (!this.config.marketplace || !this.storage) return;
532
+ if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
533
+ try {
534
+ const marketplaceStates = await new PluginStateRepository(this.db).getMarketplacePlugins();
535
+ const bundleStore = createPluginBundleStore(this.storage);
536
+ const desired = /* @__PURE__ */ new Map();
537
+ for (const state of marketplaceStates) {
538
+ this.pluginStates.set(state.pluginId, state.status);
539
+ if (state.status === "active") this.enabledPlugins.add(state.pluginId);
540
+ else this.enabledPlugins.delete(state.pluginId);
541
+ if (state.status !== "active") continue;
542
+ desired.set(state.pluginId, state.marketplaceVersion ?? state.version);
543
+ }
544
+ const keysToRemove = [];
545
+ for (const key of marketplacePluginKeys) {
546
+ const [pluginId] = key.split(":");
547
+ if (!pluginId) continue;
548
+ const desiredVersion = desired.get(pluginId);
549
+ if (desiredVersion && key === `${pluginId}:${desiredVersion}`) continue;
550
+ keysToRemove.push(key);
551
+ }
552
+ for (const key of keysToRemove) {
553
+ const [pluginId] = key.split(":");
554
+ if (!pluginId) continue;
555
+ if (!desired.get(pluginId)) {
556
+ this.pluginStates.delete(pluginId);
557
+ this.enabledPlugins.delete(pluginId);
558
+ }
559
+ const existing = sandboxedPluginCache.get(key);
560
+ if (existing) try {
561
+ await existing.terminate();
562
+ } catch (error) {
563
+ console.warn(`Dineway: Failed to terminate sandboxed plugin ${key}:`, error);
564
+ }
565
+ sandboxedPluginCache.delete(key);
566
+ this.sandboxedPlugins.delete(key);
567
+ marketplacePluginKeys.delete(key);
568
+ if (pluginId) {
569
+ sandboxedRouteMetaCache.delete(pluginId);
570
+ marketplaceManifestCache.delete(pluginId);
571
+ }
572
+ }
573
+ for (const [pluginId, version] of desired) {
574
+ const key = `${pluginId}:${version}`;
575
+ if (sandboxedPluginCache.has(key)) {
576
+ marketplacePluginKeys.add(key);
577
+ continue;
578
+ }
579
+ const bundle = await bundleStore.read(pluginId, version);
580
+ if (!bundle) {
581
+ console.warn(`Dineway: Marketplace plugin ${pluginId}@${version} not found in bundle storage`);
582
+ continue;
583
+ }
584
+ const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
585
+ sandboxedPluginCache.set(key, loaded);
586
+ this.sandboxedPlugins.set(key, loaded);
587
+ marketplacePluginKeys.add(key);
588
+ marketplaceManifestCache.set(pluginId, {
589
+ id: bundle.manifest.id,
590
+ version: bundle.manifest.version,
591
+ admin: bundle.manifest.admin
592
+ });
593
+ if (bundle.manifest.routes.length > 0) {
594
+ const routeMetaMap = /* @__PURE__ */ new Map();
595
+ for (const entry of bundle.manifest.routes) {
596
+ const normalized = normalizeManifestRoute(entry);
597
+ routeMetaMap.set(normalized.name, { public: normalized.public === true });
598
+ }
599
+ sandboxedRouteMetaCache.set(pluginId, routeMetaMap);
600
+ } else sandboxedRouteMetaCache.delete(pluginId);
601
+ }
602
+ } catch (error) {
603
+ console.error("Dineway: Failed to sync marketplace plugins:", error);
604
+ }
605
+ }
606
+ /**
607
+ * Create and initialize the runtime
608
+ */
609
+ static async create(deps) {
610
+ const db = await DinewayRuntime.getDatabase(deps);
611
+ if (isSqlite(db)) try {
612
+ const repaired = await new FTSManager(db).verifyAndRepairAll();
613
+ if (repaired > 0) console.log(`Repaired ${repaired} corrupted FTS index(es) at startup`);
614
+ } catch {}
615
+ const storage = DinewayRuntime.getStorage(deps);
616
+ let pluginStates = /* @__PURE__ */ new Map();
617
+ try {
618
+ const states = await db.selectFrom("_plugin_state").select(["plugin_id", "status"]).execute();
619
+ pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
620
+ } catch {}
621
+ const enabledPlugins = /* @__PURE__ */ new Set();
622
+ for (const plugin of deps.plugins) {
623
+ const status = pluginStates.get(plugin.id);
624
+ if (status === void 0 || status === "active") enabledPlugins.add(plugin.id);
625
+ }
626
+ let siteInfo;
627
+ try {
628
+ const optionsRepo = new OptionsRepository(db);
629
+ const siteName = await optionsRepo.get("dineway:site_title");
630
+ const siteUrl = await optionsRepo.get("dineway:site_url");
631
+ const locale = await optionsRepo.get("dineway:locale");
632
+ siteInfo = {
633
+ siteName: siteName ?? void 0,
634
+ siteUrl: siteUrl ?? void 0,
635
+ locale: locale ?? void 0
636
+ };
637
+ } catch {}
638
+ const allPipelinePlugins = [...deps.plugins];
639
+ if (import.meta.env.DEV) try {
640
+ const devConsolePlugin = definePlugin({
641
+ id: DEV_CONSOLE_EMAIL_PLUGIN_ID,
642
+ version: "0.0.0",
643
+ capabilities: ["email:provide"],
644
+ hooks: { "email:deliver": {
645
+ exclusive: true,
646
+ handler: devConsoleEmailDeliver
647
+ } }
648
+ });
649
+ allPipelinePlugins.push(devConsolePlugin);
650
+ enabledPlugins.add(devConsolePlugin.id);
651
+ } catch (error) {
652
+ console.warn("[email] Failed to register dev console email provider:", error);
653
+ }
654
+ try {
655
+ const defaultModeratorPlugin = definePlugin({
656
+ id: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
657
+ version: "0.0.0",
658
+ capabilities: ["read:users"],
659
+ hooks: { "comment:moderate": {
660
+ exclusive: true,
661
+ handler: defaultCommentModerate
662
+ } }
663
+ });
664
+ allPipelinePlugins.push(defaultModeratorPlugin);
665
+ enabledPlugins.add(defaultModeratorPlugin.id);
666
+ } catch (error) {
667
+ console.warn("[comments] Failed to register default moderator:", error);
668
+ }
669
+ const enabledPluginList = allPipelinePlugins.filter((p) => enabledPlugins.has(p.id));
670
+ const pipelineFactoryOptions = {
671
+ db,
672
+ storage: storage ?? void 0,
673
+ siteInfo
674
+ };
675
+ const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
676
+ let cronScheduler = null;
677
+ const sandboxRunnerOptions = createSandboxRunnerRuntimeOptions({
678
+ db,
679
+ storage,
680
+ siteInfo,
681
+ cronReschedule: () => cronScheduler?.reschedule()
682
+ });
683
+ const sandboxedPlugins = await DinewayRuntime.loadSandboxedPlugins(deps, sandboxRunnerOptions);
684
+ if (deps.config.marketplace && storage) await DinewayRuntime.loadMarketplacePlugins(db, storage, deps, sandboxedPlugins, sandboxRunnerOptions);
685
+ const mediaProviders = /* @__PURE__ */ new Map();
686
+ const mediaProviderEntries = deps.mediaProviderEntries ?? [];
687
+ const providerContext = {
688
+ db,
689
+ storage
690
+ };
691
+ for (const entry of mediaProviderEntries) try {
692
+ const provider = entry.createProvider(providerContext);
693
+ mediaProviders.set(entry.id, provider);
694
+ } catch (error) {
695
+ console.warn(`Failed to initialize media provider "${entry.id}":`, error);
696
+ }
697
+ await DinewayRuntime.resolveExclusiveHooks(pipeline, db, deps);
698
+ const emailPipeline = new EmailPipeline(pipeline);
699
+ if (sandboxRunner) sandboxRunner.setEmailSend((message, pluginId) => emailPipeline.send(message, pluginId));
700
+ const pipelineRef = { current: pipeline };
701
+ const invokeCronHook = async (pluginId, event) => {
702
+ const result = await pipelineRef.current.invokeCronHook(pluginId, event);
703
+ if (!result.success && result.error) throw result.error;
704
+ };
705
+ pipeline.setContextFactory({
706
+ db,
707
+ emailPipeline
708
+ });
709
+ let cronExecutor = null;
710
+ try {
711
+ cronExecutor = new CronExecutor(db, invokeCronHook);
712
+ const recovered = await cronExecutor.recoverStaleLocks();
713
+ if (recovered > 0) console.log(`[cron] Recovered ${recovered} stale task lock(s)`);
714
+ if (shouldUsePiggybackScheduler()) cronScheduler = new PiggybackScheduler(cronExecutor);
715
+ else cronScheduler = new NodeCronScheduler(cronExecutor);
716
+ cronScheduler.setSystemCleanup(async () => {
717
+ try {
718
+ await runSystemCleanup(db, storage ?? void 0);
719
+ } catch (error) {
720
+ console.error("[cleanup] System cleanup failed:", error);
721
+ }
722
+ });
723
+ pipeline.setContextFactory({ cronReschedule: () => cronScheduler?.reschedule() });
724
+ await cronScheduler.start();
725
+ } catch (error) {
726
+ console.warn("[cron] Failed to initialize cron system:", error);
727
+ }
728
+ return new DinewayRuntime(db, storage, deps.plugins, sandboxedPlugins, deps.sandboxedPluginEntries, pipeline, enabledPlugins, pluginStates, deps.config, mediaProviders, mediaProviderEntries, cronExecutor, cronScheduler, emailPipeline, allPipelinePlugins, pipelineFactoryOptions, deps, pipelineRef);
729
+ }
730
+ /**
731
+ * Get a media provider by ID
732
+ */
733
+ getMediaProvider(providerId) {
734
+ return this.mediaProviders.get(providerId);
735
+ }
736
+ /**
737
+ * Get all media provider entries (for admin UI)
738
+ */
739
+ getMediaProviderList() {
740
+ return this.mediaProviderEntries.map((e) => ({
741
+ id: e.id,
742
+ name: e.name,
743
+ icon: e.icon,
744
+ capabilities: e.capabilities
745
+ }));
746
+ }
747
+ /**
748
+ * Get or create database instance
749
+ */
750
+ static async getDatabase(deps) {
751
+ const requestDb = getRequestContextDatabase();
752
+ if (requestDb) return requestDb;
753
+ const dbConfig = deps.config.database;
754
+ if (!dbConfig) try {
755
+ return await getDb();
756
+ } catch {
757
+ throw new Error("Dineway database not configured. Either configure database in astro.config.mjs or use dinewayLoader in live.config.ts");
758
+ }
759
+ const cacheKey = dbConfig.entrypoint;
760
+ const cached = dbCache.get(cacheKey);
761
+ if (cached) return cached;
762
+ if (dbInitPromise) return dbInitPromise;
763
+ dbInitPromise = (async () => {
764
+ const db = new Kysely({ dialect: deps.createDialect(dbConfig.config) });
765
+ await runMigrations(db);
766
+ try {
767
+ const [collectionCount, setupOption] = await Promise.all([db.selectFrom("_dineway_collections").select((eb) => eb.fn.countAll().as("count")).executeTakeFirstOrThrow(), db.selectFrom("options").select("value").where("name", "=", "dineway:setup_complete").executeTakeFirst()]);
768
+ const setupDone = (() => {
769
+ try {
770
+ return setupOption && JSON.parse(setupOption.value) === true;
771
+ } catch {
772
+ return false;
773
+ }
774
+ })();
775
+ if (collectionCount.count === 0 && !setupDone) {
776
+ const { applySeed } = await import("../apply-CAPvMfoU.mjs").then((n) => n.n);
777
+ const { loadSeed } = await import("../load-C6FCD1FU.mjs").then((n) => n.r);
778
+ const { validateSeed } = await import("../validate-CXnRKfJK.mjs").then((n) => n.n);
779
+ const seed = await loadSeed();
780
+ if (validateSeed(seed).valid) {
781
+ await applySeed(db, seed, { onConflict: "skip" });
782
+ console.log("Auto-seeded default collections");
783
+ }
784
+ }
785
+ } catch {}
786
+ dbCache.set(cacheKey, db);
787
+ return db;
788
+ })();
789
+ try {
790
+ return await dbInitPromise;
791
+ } finally {
792
+ dbInitPromise = null;
793
+ }
794
+ }
795
+ /**
796
+ * Get or create storage instance
797
+ */
798
+ static getStorage(deps) {
799
+ const storageConfig = deps.config.storage;
800
+ if (!storageConfig || !deps.createStorage) return null;
801
+ const cacheKey = storageConfig.entrypoint;
802
+ const cached = storageCache.get(cacheKey);
803
+ if (cached) return cached;
804
+ const storage = deps.createStorage(storageConfig.config);
805
+ storageCache.set(cacheKey, storage);
806
+ return storage;
807
+ }
808
+ /**
809
+ * Load sandboxed plugins using SandboxRunner
810
+ */
811
+ static async loadSandboxedPlugins(deps, runnerOptions) {
812
+ if (sandboxedPluginCache.size > 0) return sandboxedPluginCache;
813
+ if (!deps.sandboxEnabled || deps.sandboxedPluginEntries.length === 0) return sandboxedPluginCache;
814
+ if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(runnerOptions);
815
+ if (!sandboxRunner) return sandboxedPluginCache;
816
+ if (!sandboxRunner.isAvailable()) {
817
+ console.debug("Dineway: Sandbox runner not available (missing bindings), skipping sandbox");
818
+ return sandboxedPluginCache;
819
+ }
820
+ for (const entry of deps.sandboxedPluginEntries) {
821
+ const pluginKey = `${entry.id}:${entry.version}`;
822
+ if (sandboxedPluginCache.has(pluginKey)) continue;
823
+ try {
824
+ const manifest = {
825
+ id: entry.id,
826
+ version: entry.version,
827
+ capabilities: entry.capabilities ?? [],
828
+ allowedHosts: entry.allowedHosts ?? [],
829
+ storage: entry.storage ?? {},
830
+ hooks: [],
831
+ routes: [],
832
+ admin: {}
833
+ };
834
+ const plugin = await sandboxRunner.load(manifest, entry.code);
835
+ sandboxedPluginCache.set(pluginKey, plugin);
836
+ console.log(`Dineway: Loaded sandboxed plugin ${pluginKey} with capabilities: [${manifest.capabilities.join(", ")}]`);
837
+ } catch (error) {
838
+ console.error(`Dineway: Failed to load sandboxed plugin ${entry.id}:`, error);
839
+ }
840
+ }
841
+ return sandboxedPluginCache;
842
+ }
843
+ /**
844
+ * Cold-start: load marketplace-installed plugins from site-local bundle storage
845
+ *
846
+ * Queries _plugin_state for source='marketplace' rows, fetches each bundle
847
+ * from persisted storage, and loads via SandboxRunner.
848
+ */
849
+ static async loadMarketplacePlugins(db, storage, deps, cache, runnerOptions) {
850
+ if (!sandboxRunner && deps.createSandboxRunner) sandboxRunner = deps.createSandboxRunner(runnerOptions);
851
+ if (!sandboxRunner || !sandboxRunner.isAvailable()) return;
852
+ try {
853
+ const marketplacePlugins = await new PluginStateRepository(db).getMarketplacePlugins();
854
+ const bundleStore = createPluginBundleStore(storage);
855
+ for (const plugin of marketplacePlugins) {
856
+ if (plugin.status !== "active") continue;
857
+ const version = plugin.marketplaceVersion ?? plugin.version;
858
+ const pluginKey = `${plugin.pluginId}:${version}`;
859
+ if (cache.has(pluginKey)) continue;
860
+ try {
861
+ const bundle = await bundleStore.read(plugin.pluginId, version);
862
+ if (!bundle) {
863
+ console.warn(`Dineway: Marketplace plugin ${plugin.pluginId}@${version} not found in bundle storage`);
864
+ continue;
865
+ }
866
+ const loaded = await sandboxRunner.load(bundle.manifest, bundle.backendCode);
867
+ cache.set(pluginKey, loaded);
868
+ marketplacePluginKeys.add(pluginKey);
869
+ marketplaceManifestCache.set(plugin.pluginId, {
870
+ id: bundle.manifest.id,
871
+ version: bundle.manifest.version,
872
+ admin: bundle.manifest.admin
873
+ });
874
+ if (bundle.manifest.routes.length > 0) {
875
+ const routeMeta = /* @__PURE__ */ new Map();
876
+ for (const entry of bundle.manifest.routes) {
877
+ const normalized = normalizeManifestRoute(entry);
878
+ routeMeta.set(normalized.name, { public: normalized.public === true });
879
+ }
880
+ sandboxedRouteMetaCache.set(plugin.pluginId, routeMeta);
881
+ }
882
+ console.log(`Dineway: Loaded marketplace plugin ${pluginKey} with capabilities: [${bundle.manifest.capabilities.join(", ")}]`);
883
+ } catch (error) {
884
+ console.error(`Dineway: Failed to load marketplace plugin ${plugin.pluginId}:`, error);
885
+ }
886
+ }
887
+ } catch {}
888
+ }
889
+ /**
890
+ * Resolve exclusive hook selections on startup.
891
+ *
892
+ * Delegates to the shared resolveExclusiveHooks() in hooks.ts.
893
+ * The runtime version considers all pipeline providers as "active" since
894
+ * the pipeline was already built from only active/enabled plugins.
895
+ */
896
+ static async resolveExclusiveHooks(pipeline, db, deps) {
897
+ if (pipeline.getRegisteredExclusiveHooks().length === 0) return;
898
+ let optionsRepo;
899
+ try {
900
+ optionsRepo = new OptionsRepository(db);
901
+ } catch {
902
+ return;
903
+ }
904
+ const preferredHints = /* @__PURE__ */ new Map();
905
+ for (const entry of deps.sandboxedPluginEntries) if (entry.preferred && entry.preferred.length > 0) preferredHints.set(entry.id, entry.preferred);
906
+ await resolveExclusiveHooks({
907
+ pipeline,
908
+ isActive: () => true,
909
+ getOption: (key) => optionsRepo.get(key),
910
+ setOption: (key, value) => optionsRepo.set(key, value),
911
+ deleteOption: async (key) => {
912
+ await optionsRepo.delete(key);
913
+ },
914
+ preferredHints
915
+ });
916
+ }
917
+ /**
918
+ * Build the manifest (rebuilt on each request for freshness)
919
+ */
920
+ async getManifest() {
921
+ const manifestCollections = {};
922
+ try {
923
+ const registry = new SchemaRegistry(this.db);
924
+ const dbCollections = await registry.listCollections();
925
+ for (const collection of dbCollections) {
926
+ const collectionWithFields = await registry.getCollectionWithFields(collection.slug);
927
+ const fields = {};
928
+ if (collectionWithFields?.fields) for (const field of collectionWithFields.fields) {
929
+ const entry = {
930
+ kind: FIELD_TYPE_TO_KIND[field.type] ?? "string",
931
+ label: field.label,
932
+ required: field.required
933
+ };
934
+ if (field.widget) entry.widget = field.widget;
935
+ if (field.options) entry.options = field.options;
936
+ if (field.validation?.options) entry.options = field.validation.options.map((v) => ({
937
+ value: v,
938
+ label: v.charAt(0).toUpperCase() + v.slice(1)
939
+ }));
940
+ if (field.type === "repeater" && field.validation) entry.validation = field.validation;
941
+ fields[field.slug] = entry;
942
+ }
943
+ manifestCollections[collection.slug] = {
944
+ label: collection.label,
945
+ labelSingular: collection.labelSingular || collection.label,
946
+ supports: collection.supports || [],
947
+ hasSeo: collection.hasSeo,
948
+ urlPattern: collection.urlPattern,
949
+ fields
950
+ };
951
+ }
952
+ } catch (error) {
953
+ console.debug("Dineway: Could not load database collections:", error);
954
+ }
955
+ const manifestPlugins = {};
956
+ for (const plugin of this.configuredPlugins) {
957
+ const status = this.pluginStates.get(plugin.id);
958
+ const enabled = status === void 0 || status === "active";
959
+ const hasAdminEntry = !!plugin.admin?.entry;
960
+ const hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0;
961
+ const hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0;
962
+ let adminMode = "none";
963
+ if (hasAdminEntry) adminMode = "react";
964
+ else if (hasAdminPages || hasWidgets) adminMode = "blocks";
965
+ manifestPlugins[plugin.id] = {
966
+ version: plugin.version,
967
+ enabled,
968
+ adminMode,
969
+ adminPages: plugin.admin?.pages ?? [],
970
+ dashboardWidgets: plugin.admin?.widgets ?? [],
971
+ portableTextBlocks: plugin.admin?.portableTextBlocks,
972
+ fieldWidgets: plugin.admin?.fieldWidgets
973
+ };
974
+ }
975
+ for (const entry of this.sandboxedPluginEntries) {
976
+ const status = this.pluginStates.get(entry.id);
977
+ const enabled = status === void 0 || status === "active";
978
+ const hasAdminPages = (entry.adminPages?.length ?? 0) > 0;
979
+ const hasWidgets = (entry.adminWidgets?.length ?? 0) > 0;
980
+ manifestPlugins[entry.id] = {
981
+ version: entry.version,
982
+ enabled,
983
+ sandboxed: true,
984
+ adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
985
+ adminPages: entry.adminPages ?? [],
986
+ dashboardWidgets: entry.adminWidgets ?? []
987
+ };
988
+ }
989
+ for (const [pluginId, meta] of marketplaceManifestCache) {
990
+ if (manifestPlugins[pluginId]) continue;
991
+ const enabled = this.pluginStates.get(pluginId) === "active";
992
+ const pages = meta.admin?.pages;
993
+ const widgets = meta.admin?.widgets;
994
+ const hasAdminPages = (pages?.length ?? 0) > 0;
995
+ const hasWidgets = (widgets?.length ?? 0) > 0;
996
+ manifestPlugins[pluginId] = {
997
+ version: meta.version,
998
+ enabled,
999
+ sandboxed: true,
1000
+ adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
1001
+ adminPages: pages ?? [],
1002
+ dashboardWidgets: widgets ?? []
1003
+ };
1004
+ }
1005
+ let manifestTaxonomies = [];
1006
+ try {
1007
+ manifestTaxonomies = (await this.db.selectFrom("_dineway_taxonomy_defs").selectAll().orderBy("name").execute()).map((row) => ({
1008
+ name: row.name,
1009
+ label: row.label,
1010
+ labelSingular: row.label_singular ?? void 0,
1011
+ hierarchical: row.hierarchical === 1,
1012
+ collections: parseStringArray(row.collections)
1013
+ }));
1014
+ } catch (error) {
1015
+ console.debug("Dineway: Could not load taxonomy definitions:", error);
1016
+ }
1017
+ const manifestHash = await hashString(JSON.stringify(manifestCollections) + JSON.stringify(manifestPlugins) + JSON.stringify(manifestTaxonomies));
1018
+ const authMode = getAuthMode(this.config);
1019
+ const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
1020
+ const i18nConfig = virtualConfig?.i18n;
1021
+ const i18n = i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1 ? {
1022
+ defaultLocale: i18nConfig.defaultLocale,
1023
+ locales: i18nConfig.locales
1024
+ } : void 0;
1025
+ return {
1026
+ version: "0.1.0",
1027
+ hash: manifestHash,
1028
+ collections: manifestCollections,
1029
+ plugins: manifestPlugins,
1030
+ taxonomies: manifestTaxonomies,
1031
+ authMode: authModeValue,
1032
+ i18n,
1033
+ marketplace: !!this.config.marketplace
1034
+ };
1035
+ }
1036
+ /**
1037
+ * Invalidate the cached manifest (no-op now that we don't cache).
1038
+ * Kept for API compatibility.
1039
+ */
1040
+ invalidateManifest() {}
1041
+ async handleContentList(collection, params) {
1042
+ return handleContentList(this.db, collection, params);
1043
+ }
1044
+ async handleContentGet(collection, id, locale) {
1045
+ return handleContentGet(this.db, collection, id, locale);
1046
+ }
1047
+ async handleContentGetIncludingTrashed(collection, id, locale) {
1048
+ return handleContentGetIncludingTrashed(this.db, collection, id, locale);
1049
+ }
1050
+ async handleContentCreate(collection, body) {
1051
+ let processedData = body.data;
1052
+ if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(body.data, collection, true)).content;
1053
+ processedData = await this.runSandboxedBeforeSave(processedData, collection, true);
1054
+ processedData = await this.normalizeMediaFields(collection, processedData);
1055
+ const result = await handleContentCreate(this.db, collection, {
1056
+ ...body,
1057
+ data: processedData,
1058
+ authorId: body.authorId,
1059
+ bylines: body.bylines
1060
+ });
1061
+ if (result.success && result.data) this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, true);
1062
+ return result;
1063
+ }
1064
+ async handleContentUpdate(collection, id, body) {
1065
+ const { ContentRepository } = await import("../content-zSgdNmnt.mjs").then((n) => n.n);
1066
+ const repo = new ContentRepository(this.db);
1067
+ const resolvedItem = await repo.findByIdOrSlug(collection, id);
1068
+ const resolvedId = resolvedItem?.id ?? id;
1069
+ if (body._rev) {
1070
+ if (!resolvedItem) return {
1071
+ success: false,
1072
+ error: {
1073
+ code: "NOT_FOUND",
1074
+ message: `Content item not found: ${id}`
1075
+ }
1076
+ };
1077
+ const revCheck = validateRev(body._rev, resolvedItem);
1078
+ if (!revCheck.valid) return {
1079
+ success: false,
1080
+ error: {
1081
+ code: "CONFLICT",
1082
+ message: revCheck.message
1083
+ }
1084
+ };
1085
+ }
1086
+ const { _rev: _discardedRev, ...bodyWithoutRev } = body;
1087
+ let processedData = bodyWithoutRev.data;
1088
+ if (bodyWithoutRev.data) {
1089
+ if (this.hooks.hasHooks("content:beforeSave")) processedData = (await this.hooks.runContentBeforeSave(bodyWithoutRev.data, collection, false)).content;
1090
+ processedData = await this.runSandboxedBeforeSave(processedData, collection, false);
1091
+ processedData = await this.normalizeMediaFields(collection, processedData);
1092
+ }
1093
+ let usesDraftRevisions = false;
1094
+ if (processedData) try {
1095
+ if ((await this.schemaRegistry.getCollectionWithFields(collection))?.supports?.includes("revisions")) {
1096
+ usesDraftRevisions = true;
1097
+ const revisionRepo = new RevisionRepository(this.db);
1098
+ const existing = await repo.findById(collection, resolvedId);
1099
+ if (existing) {
1100
+ let baseData;
1101
+ if (existing.draftRevisionId) baseData = (await revisionRepo.findById(existing.draftRevisionId))?.data ?? existing.data;
1102
+ else baseData = existing.data;
1103
+ const mergedData = {
1104
+ ...baseData,
1105
+ ...processedData
1106
+ };
1107
+ if (bodyWithoutRev.slug !== void 0) mergedData._slug = bodyWithoutRev.slug;
1108
+ if (bodyWithoutRev.skipRevision && existing.draftRevisionId) await revisionRepo.updateData(existing.draftRevisionId, mergedData);
1109
+ else {
1110
+ const revision = await revisionRepo.create({
1111
+ collection,
1112
+ entryId: resolvedId,
1113
+ data: mergedData,
1114
+ authorId: bodyWithoutRev.authorId ?? void 0
1115
+ });
1116
+ const tableName = `ec_${collection}`;
1117
+ await sql`
1118
+ UPDATE ${sql.ref(tableName)}
1119
+ SET draft_revision_id = ${revision.id},
1120
+ updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
1121
+ WHERE id = ${resolvedId}
1122
+ `.execute(this.db);
1123
+ revisionRepo.pruneOldRevisions(collection, resolvedId, 50).catch(() => {});
1124
+ }
1125
+ }
1126
+ }
1127
+ } catch {}
1128
+ const result = await handleContentUpdate(this.db, collection, resolvedId, {
1129
+ ...bodyWithoutRev,
1130
+ data: usesDraftRevisions ? void 0 : processedData,
1131
+ slug: usesDraftRevisions ? void 0 : bodyWithoutRev.slug,
1132
+ authorId: bodyWithoutRev.authorId,
1133
+ bylines: bodyWithoutRev.bylines
1134
+ });
1135
+ if (result.success && result.data) this.runAfterSaveHooks(contentItemToRecord(result.data.item), collection, false);
1136
+ return result;
1137
+ }
1138
+ async handleContentDelete(collection, id) {
1139
+ if (this.hooks.hasHooks("content:beforeDelete")) {
1140
+ const { allowed } = await this.hooks.runContentBeforeDelete(id, collection);
1141
+ if (!allowed) return {
1142
+ success: false,
1143
+ error: {
1144
+ code: "DELETE_BLOCKED",
1145
+ message: "Delete blocked by plugin hook"
1146
+ }
1147
+ };
1148
+ }
1149
+ if (!await this.runSandboxedBeforeDelete(id, collection)) return {
1150
+ success: false,
1151
+ error: {
1152
+ code: "DELETE_BLOCKED",
1153
+ message: "Delete blocked by sandboxed plugin hook"
1154
+ }
1155
+ };
1156
+ const result = await handleContentDelete(this.db, collection, id);
1157
+ if (result.success) this.runAfterDeleteHooks(id, collection);
1158
+ return result;
1159
+ }
1160
+ async handleContentListTrashed(collection, params = {}) {
1161
+ return handleContentListTrashed(this.db, collection, params);
1162
+ }
1163
+ async handleContentRestore(collection, id) {
1164
+ return handleContentRestore(this.db, collection, id);
1165
+ }
1166
+ async handleContentPermanentDelete(collection, id) {
1167
+ return handleContentPermanentDelete(this.db, collection, id);
1168
+ }
1169
+ async handleContentCountTrashed(collection) {
1170
+ return handleContentCountTrashed(this.db, collection);
1171
+ }
1172
+ async handleContentDuplicate(collection, id, authorId) {
1173
+ return handleContentDuplicate(this.db, collection, id, authorId);
1174
+ }
1175
+ async handleContentPublish(collection, id) {
1176
+ const result = await handleContentPublish(this.db, collection, id);
1177
+ if (result.success && result.data) this.runAfterPublishHooks(contentItemToRecord(result.data.item), collection);
1178
+ return result;
1179
+ }
1180
+ async handleContentUnpublish(collection, id) {
1181
+ const result = await handleContentUnpublish(this.db, collection, id);
1182
+ if (result.success && result.data) this.runAfterUnpublishHooks(contentItemToRecord(result.data.item), collection);
1183
+ return result;
1184
+ }
1185
+ async handleContentSchedule(collection, id, scheduledAt) {
1186
+ return handleContentSchedule(this.db, collection, id, scheduledAt);
1187
+ }
1188
+ async handleContentUnschedule(collection, id) {
1189
+ return handleContentUnschedule(this.db, collection, id);
1190
+ }
1191
+ async handleContentCountScheduled(collection) {
1192
+ return handleContentCountScheduled(this.db, collection);
1193
+ }
1194
+ async handleContentDiscardDraft(collection, id) {
1195
+ return handleContentDiscardDraft(this.db, collection, id);
1196
+ }
1197
+ async handleContentCompare(collection, id) {
1198
+ return handleContentCompare(this.db, collection, id);
1199
+ }
1200
+ async handleContentTranslations(collection, id) {
1201
+ return handleContentTranslations(this.db, collection, id);
1202
+ }
1203
+ async handleMediaList(params) {
1204
+ return handleMediaList(this.db, params);
1205
+ }
1206
+ async handleMediaGet(id) {
1207
+ return handleMediaGet(this.db, id);
1208
+ }
1209
+ async handleMediaCreate(input) {
1210
+ let processedInput = input;
1211
+ if (this.hooks.hasHooks("media:beforeUpload")) {
1212
+ const hookResult = await this.hooks.runMediaBeforeUpload({
1213
+ name: input.filename,
1214
+ type: input.mimeType,
1215
+ size: input.size || 0
1216
+ });
1217
+ processedInput = {
1218
+ ...input,
1219
+ filename: hookResult.file.name,
1220
+ mimeType: hookResult.file.type,
1221
+ size: hookResult.file.size
1222
+ };
1223
+ }
1224
+ const result = await handleMediaCreate(this.db, processedInput);
1225
+ if (result.success && this.hooks.hasHooks("media:afterUpload")) {
1226
+ const item = result.data.item;
1227
+ const mediaItem = {
1228
+ id: item.id,
1229
+ filename: item.filename,
1230
+ mimeType: item.mimeType,
1231
+ size: item.size,
1232
+ url: `/media/${item.id}/${item.filename}`,
1233
+ createdAt: item.createdAt
1234
+ };
1235
+ this.hooks.runMediaAfterUpload(mediaItem).catch((err) => console.error("Dineway afterUpload hook error:", err));
1236
+ }
1237
+ return result;
1238
+ }
1239
+ async handleMediaUpdate(id, input) {
1240
+ return handleMediaUpdate(this.db, id, input);
1241
+ }
1242
+ async handleMediaDelete(id) {
1243
+ return handleMediaDelete(this.db, id);
1244
+ }
1245
+ async handleRevisionList(collection, entryId, params = {}) {
1246
+ return handleRevisionList(this.db, collection, entryId, params);
1247
+ }
1248
+ async handleRevisionGet(revisionId) {
1249
+ return handleRevisionGet(this.db, revisionId);
1250
+ }
1251
+ async handleRevisionRestore(revisionId, callerUserId) {
1252
+ return handleRevisionRestore(this.db, revisionId, callerUserId);
1253
+ }
1254
+ /**
1255
+ * Get route metadata for a plugin route without invoking the handler.
1256
+ * Used by the catch-all route to decide auth before dispatch.
1257
+ * Returns null if the plugin or route doesn't exist.
1258
+ */
1259
+ getPluginRouteMeta(pluginId, path) {
1260
+ if (!this.isPluginEnabled(pluginId)) return null;
1261
+ const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
1262
+ const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
1263
+ if (trustedPlugin) {
1264
+ const route = trustedPlugin.routes[routeKey];
1265
+ if (!route) return null;
1266
+ return { public: route.public === true };
1267
+ }
1268
+ const meta = sandboxedRouteMetaCache.get(pluginId);
1269
+ if (meta) {
1270
+ const routeMeta = meta.get(routeKey);
1271
+ if (routeMeta) return routeMeta;
1272
+ }
1273
+ if (routeKey === "admin") {
1274
+ const manifestMeta = marketplaceManifestCache.get(pluginId);
1275
+ if (manifestMeta?.admin?.pages?.length || manifestMeta?.admin?.widgets?.length) return { public: false };
1276
+ const entry = this.sandboxedPluginEntries.find((e) => e.id === pluginId);
1277
+ if (entry?.adminPages?.length || entry?.adminWidgets?.length) return { public: false };
1278
+ }
1279
+ if (this.findSandboxedPlugin(pluginId)) return { public: false };
1280
+ return null;
1281
+ }
1282
+ async handlePluginApiRoute(pluginId, _method, path, request) {
1283
+ if (!this.isPluginEnabled(pluginId)) return {
1284
+ success: false,
1285
+ error: {
1286
+ code: "NOT_FOUND",
1287
+ message: `Plugin not enabled: ${pluginId}`
1288
+ }
1289
+ };
1290
+ const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
1291
+ if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
1292
+ const routeRegistry = new PluginRouteRegistry({
1293
+ db: this.db,
1294
+ emailPipeline: this.email ?? void 0
1295
+ });
1296
+ routeRegistry.register(trustedPlugin);
1297
+ const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
1298
+ let body = void 0;
1299
+ try {
1300
+ body = await request.json();
1301
+ } catch {}
1302
+ return routeRegistry.invoke(pluginId, routeKey, {
1303
+ request,
1304
+ body
1305
+ });
1306
+ }
1307
+ const sandboxedPlugin = this.findSandboxedPlugin(pluginId);
1308
+ if (sandboxedPlugin) return this.handleSandboxedRoute(sandboxedPlugin, path, request);
1309
+ return {
1310
+ success: false,
1311
+ error: {
1312
+ code: "NOT_FOUND",
1313
+ message: `Plugin not found: ${pluginId}`
1314
+ }
1315
+ };
1316
+ }
1317
+ findSandboxedPlugin(pluginId) {
1318
+ for (const [key, plugin] of this.sandboxedPlugins) if (key.startsWith(pluginId + ":")) return plugin;
1319
+ }
1320
+ /**
1321
+ * Normalize image/file fields in content data.
1322
+ * Fills missing dimensions, storageKey, mimeType, and filename from providers.
1323
+ */
1324
+ async normalizeMediaFields(collection, data) {
1325
+ let collectionInfo;
1326
+ try {
1327
+ collectionInfo = await this.schemaRegistry.getCollectionWithFields(collection);
1328
+ } catch {
1329
+ return data;
1330
+ }
1331
+ if (!collectionInfo?.fields) return data;
1332
+ const imageFields = collectionInfo.fields.filter((f) => f.type === "image" || f.type === "file");
1333
+ if (imageFields.length === 0) return data;
1334
+ const getProvider = (id) => this.getMediaProvider(id);
1335
+ const result = { ...data };
1336
+ for (const field of imageFields) {
1337
+ const value = result[field.slug];
1338
+ if (value == null) continue;
1339
+ try {
1340
+ const normalized = await normalizeMediaValue(value, getProvider);
1341
+ if (normalized) result[field.slug] = normalized;
1342
+ } catch {}
1343
+ }
1344
+ return result;
1345
+ }
1346
+ async runSandboxedBeforeSave(content, collection, isNew) {
1347
+ let result = content;
1348
+ for (const [pluginKey, plugin] of this.sandboxedPlugins) {
1349
+ const [id] = pluginKey.split(":");
1350
+ if (!id || !this.isPluginEnabled(id)) continue;
1351
+ try {
1352
+ const hookResult = await plugin.invokeHook("content:beforeSave", {
1353
+ content: result,
1354
+ collection,
1355
+ isNew
1356
+ });
1357
+ if (hookResult && typeof hookResult === "object" && !Array.isArray(hookResult)) {
1358
+ const record = {};
1359
+ for (const [k, v] of Object.entries(hookResult)) record[k] = v;
1360
+ result = record;
1361
+ }
1362
+ } catch (error) {
1363
+ console.error(`Dineway: Sandboxed plugin ${id} beforeSave hook error:`, error);
1364
+ }
1365
+ }
1366
+ return result;
1367
+ }
1368
+ async runSandboxedBeforeDelete(id, collection) {
1369
+ for (const [pluginKey, plugin] of this.sandboxedPlugins) {
1370
+ const [pluginId] = pluginKey.split(":");
1371
+ if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
1372
+ try {
1373
+ if (await plugin.invokeHook("content:beforeDelete", {
1374
+ id,
1375
+ collection
1376
+ }) === false) return false;
1377
+ } catch (error) {
1378
+ console.error(`Dineway: Sandboxed plugin ${pluginId} beforeDelete hook error:`, error);
1379
+ }
1380
+ }
1381
+ return true;
1382
+ }
1383
+ runAfterSaveHooks(content, collection, isNew) {
1384
+ if (this.hooks.hasHooks("content:afterSave")) this.hooks.runContentAfterSave(content, collection, isNew).catch((err) => console.error("Dineway afterSave hook error:", err));
1385
+ for (const [pluginKey, plugin] of this.sandboxedPlugins) {
1386
+ const [id] = pluginKey.split(":");
1387
+ if (!id || !this.isPluginEnabled(id)) continue;
1388
+ plugin.invokeHook("content:afterSave", {
1389
+ content,
1390
+ collection,
1391
+ isNew
1392
+ }).catch((err) => console.error(`Dineway: Sandboxed plugin ${id} afterSave error:`, err));
1393
+ }
1394
+ }
1395
+ runAfterDeleteHooks(id, collection) {
1396
+ if (this.hooks.hasHooks("content:afterDelete")) this.hooks.runContentAfterDelete(id, collection).catch((err) => console.error("Dineway afterDelete hook error:", err));
1397
+ for (const [pluginKey, plugin] of this.sandboxedPlugins) {
1398
+ const [pluginId] = pluginKey.split(":");
1399
+ if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
1400
+ plugin.invokeHook("content:afterDelete", {
1401
+ id,
1402
+ collection
1403
+ }).catch((err) => console.error(`Dineway: Sandboxed plugin ${pluginId} afterDelete error:`, err));
1404
+ }
1405
+ }
1406
+ runAfterPublishHooks(content, collection) {
1407
+ if (this.hooks.hasHooks("content:afterPublish")) this.hooks.runContentAfterPublish(content, collection).catch((err) => console.error("Dineway afterPublish hook error:", err));
1408
+ for (const [pluginKey, plugin] of this.sandboxedPlugins) {
1409
+ const [pluginId] = pluginKey.split(":");
1410
+ if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
1411
+ plugin.invokeHook("content:afterPublish", {
1412
+ content,
1413
+ collection
1414
+ }).catch((err) => console.error(`Dineway: Sandboxed plugin ${pluginId} afterPublish error:`, err));
1415
+ }
1416
+ }
1417
+ runAfterUnpublishHooks(content, collection) {
1418
+ if (this.hooks.hasHooks("content:afterUnpublish")) this.hooks.runContentAfterUnpublish(content, collection).catch((err) => console.error("Dineway afterUnpublish hook error:", err));
1419
+ for (const [pluginKey, plugin] of this.sandboxedPlugins) {
1420
+ const [pluginId] = pluginKey.split(":");
1421
+ if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
1422
+ plugin.invokeHook("content:afterUnpublish", {
1423
+ content,
1424
+ collection
1425
+ }).catch((err) => console.error(`Dineway: Sandboxed plugin ${pluginId} afterUnpublish error:`, err));
1426
+ }
1427
+ }
1428
+ async handleSandboxedRoute(plugin, path, request) {
1429
+ const routeName = path.replace(LEADING_SLASH_PATTERN, "");
1430
+ let body = void 0;
1431
+ try {
1432
+ body = await request.json();
1433
+ } catch {}
1434
+ try {
1435
+ const headers = sanitizeHeadersForSandbox(request.headers);
1436
+ const meta = extractRequestMeta(request);
1437
+ return {
1438
+ success: true,
1439
+ data: await plugin.invokeRoute(routeName, body, {
1440
+ url: request.url,
1441
+ method: request.method,
1442
+ headers,
1443
+ meta
1444
+ })
1445
+ };
1446
+ } catch (error) {
1447
+ console.error(`Dineway: Sandboxed plugin route error:`, error);
1448
+ return {
1449
+ success: false,
1450
+ error: {
1451
+ code: "ROUTE_ERROR",
1452
+ message: error instanceof Error ? error.message : String(error)
1453
+ }
1454
+ };
1455
+ }
1456
+ }
1457
+ /**
1458
+ * Cache for page contributions. Uses a WeakMap keyed on the PublicPageContext
1459
+ * object so results are collected once per page context per request, even when
1460
+ * multiple render components (DinewayHead, DinewayBodyStart, DinewayBodyEnd)
1461
+ * request contributions from the same page.
1462
+ */
1463
+ pageContributionCache = /* @__PURE__ */ new WeakMap();
1464
+ /**
1465
+ * Collect all page contributions (metadata + fragments) in a single pass.
1466
+ * Results are cached by page context object identity.
1467
+ */
1468
+ async collectPageContributions(page) {
1469
+ const cached = this.pageContributionCache.get(page);
1470
+ if (cached) return cached;
1471
+ const promise = this.doCollectPageContributions(page);
1472
+ this.pageContributionCache.set(page, promise);
1473
+ return promise;
1474
+ }
1475
+ async doCollectPageContributions(page) {
1476
+ const metadata = [];
1477
+ const fragments = [];
1478
+ if (this.hooks.hasHooks("page:metadata")) {
1479
+ const results = await this.hooks.runPageMetadata({ page });
1480
+ for (const r of results) metadata.push(...r.contributions);
1481
+ }
1482
+ if (this.hooks.hasHooks("page:fragments")) {
1483
+ const results = await this.hooks.runPageFragments({ page });
1484
+ for (const r of results) fragments.push(...r.contributions);
1485
+ }
1486
+ for (const [pluginKey, plugin] of this.sandboxedPlugins) {
1487
+ const [id] = pluginKey.split(":");
1488
+ if (!id || !this.isPluginEnabled(id)) continue;
1489
+ try {
1490
+ const result = await plugin.invokeHook("page:metadata", { page });
1491
+ if (result != null) {
1492
+ const items = Array.isArray(result) ? result : [result];
1493
+ for (const item of items) if (isValidMetadataContribution(item)) metadata.push(item);
1494
+ }
1495
+ } catch (error) {
1496
+ console.error(`Dineway: Sandboxed plugin ${id} page:metadata error:`, error);
1497
+ }
1498
+ }
1499
+ return {
1500
+ metadata,
1501
+ fragments
1502
+ };
1503
+ }
1504
+ /**
1505
+ * Collect page metadata contributions from trusted and sandboxed plugins.
1506
+ * Delegates to the single-pass collector and returns the metadata portion.
1507
+ */
1508
+ async collectPageMetadata(page) {
1509
+ const { metadata } = await this.collectPageContributions(page);
1510
+ return metadata;
1511
+ }
1512
+ /**
1513
+ * Collect page fragment contributions from trusted plugins only.
1514
+ * Delegates to the single-pass collector and returns the fragments portion.
1515
+ */
1516
+ async collectPageFragments(page) {
1517
+ const { fragments } = await this.collectPageContributions(page);
1518
+ return fragments;
1519
+ }
1520
+ isPluginEnabled(pluginId) {
1521
+ const status = this.pluginStates.get(pluginId);
1522
+ return status === void 0 || status === "active";
1523
+ }
1524
+ };
1525
+
1526
+ //#endregion
1527
+ //#region src/astro/middleware.ts
1528
+ /**
1529
+ * Dineway middleware
1530
+ *
1531
+ * Thin wrapper that initializes DinewayRuntime and attaches it to locals.
1532
+ * All heavy lifting happens in DinewayRuntime.
1533
+ */
1534
+ let runtimeInstance = null;
1535
+ let runtimeInitializing = false;
1536
+ /** Whether i18n config has been initialized from the virtual module */
1537
+ let i18nInitialized = false;
1538
+ /**
1539
+ * Whether we've verified the database has been set up.
1540
+ * On a fresh deployment the first request may hit a public page, bypassing
1541
+ * runtime init. Without this check, template helpers like getSiteSettings()
1542
+ * would query an empty database and crash. Once verified (or once the runtime
1543
+ * has initialized via an admin/API request), this stays true for the worker's
1544
+ * lifetime.
1545
+ */
1546
+ let setupVerified = false;
1547
+ /**
1548
+ * Get Dineway configuration from virtual module
1549
+ */
1550
+ function getConfig() {
1551
+ if (virtualConfig && typeof virtualConfig === "object") {
1552
+ if (!i18nInitialized) {
1553
+ i18nInitialized = true;
1554
+ const config = virtualConfig;
1555
+ if (config.i18n && typeof config.i18n === "object") setI18nConfig(config.i18n);
1556
+ else setI18nConfig(null);
1557
+ }
1558
+ return virtualConfig;
1559
+ }
1560
+ return null;
1561
+ }
1562
+ /**
1563
+ * Get plugins from virtual module
1564
+ */
1565
+ function getPlugins() {
1566
+ return plugins || [];
1567
+ }
1568
+ /**
1569
+ * Build runtime dependencies from virtual modules
1570
+ */
1571
+ function buildDependencies(config) {
1572
+ return {
1573
+ config,
1574
+ plugins: getPlugins(),
1575
+ createDialect,
1576
+ createStorage,
1577
+ sandboxEnabled,
1578
+ sandboxedPluginEntries: sandboxedPlugins || [],
1579
+ createSandboxRunner,
1580
+ mediaProviderEntries: mediaProviders || []
1581
+ };
1582
+ }
1583
+ /**
1584
+ * Get or create the runtime instance
1585
+ */
1586
+ async function getRuntime(config) {
1587
+ if (runtimeInstance) return runtimeInstance;
1588
+ if (runtimeInitializing) {
1589
+ await new Promise((resolve) => setTimeout(resolve, 50));
1590
+ return getRuntime(config);
1591
+ }
1592
+ runtimeInitializing = true;
1593
+ try {
1594
+ const deps = buildDependencies(config);
1595
+ const runtime = await DinewayRuntime.create(deps);
1596
+ runtimeInstance = runtime;
1597
+ return runtime;
1598
+ } finally {
1599
+ runtimeInitializing = false;
1600
+ }
1601
+ }
1602
+ /**
1603
+ * Baseline security headers applied to all responses.
1604
+ * Admin routes get additional headers (strict CSP) from auth middleware.
1605
+ */
1606
+ function setBaselineSecurityHeaders(response) {
1607
+ response.headers.set("X-Content-Type-Options", "nosniff");
1608
+ response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
1609
+ response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()");
1610
+ if (!response.headers.has("Content-Security-Policy")) response.headers.set("X-Frame-Options", "SAMEORIGIN");
1611
+ }
1612
+ /** Public routes that require the runtime (sitemap, robots.txt, etc.) */
1613
+ const PUBLIC_RUNTIME_ROUTES = new Set(["/sitemap.xml", "/robots.txt"]);
1614
+ const SITEMAP_COLLECTION_RE = /^\/sitemap-[a-z][a-z0-9_]*\.xml$/;
1615
+ const onRequest = defineMiddleware(async (context, next) => {
1616
+ const { locals, cookies } = context;
1617
+ const url = context.url;
1618
+ const isDinewayRoute = url.pathname.startsWith("/_dineway");
1619
+ const isPublicRuntimeRoute = PUBLIC_RUNTIME_ROUTES.has(url.pathname) || SITEMAP_COLLECTION_RE.test(url.pathname);
1620
+ const hasEditCookie = cookies.get("dineway-edit-mode")?.value === "true";
1621
+ const hasPreviewToken = url.searchParams.has("_preview");
1622
+ const requestDb = locals.__dinewayRequestDb;
1623
+ if (!isDinewayRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
1624
+ if (!(context.isPrerendered ? null : await context.session?.get("user")) && !requestDb) {
1625
+ if (!setupVerified) try {
1626
+ const { getDb } = await import("../loader-qKmo0wAY.mjs").then((n) => n.r);
1627
+ await (await getDb()).selectFrom("_dineway_migrations").selectAll().limit(1).execute();
1628
+ setupVerified = true;
1629
+ } catch {
1630
+ return context.redirect("/_dineway/admin/setup");
1631
+ }
1632
+ const config = getConfig();
1633
+ if (config) try {
1634
+ const runtime = await getRuntime(config);
1635
+ setupVerified = true;
1636
+ locals.dineway = {
1637
+ collectPageMetadata: runtime.collectPageMetadata.bind(runtime),
1638
+ collectPageFragments: runtime.collectPageFragments.bind(runtime)
1639
+ };
1640
+ } catch {}
1641
+ const response = await next();
1642
+ setBaselineSecurityHeaders(response);
1643
+ return response;
1644
+ }
1645
+ }
1646
+ const config = getConfig();
1647
+ if (!config) {
1648
+ console.error("Dineway: No configuration found");
1649
+ return next();
1650
+ }
1651
+ const doInit = async () => {
1652
+ try {
1653
+ const runtime = await getRuntime(config);
1654
+ setupVerified = true;
1655
+ locals.dinewayManifest = await runtime.getManifest();
1656
+ locals.dineway = {
1657
+ handleContentList: runtime.handleContentList.bind(runtime),
1658
+ handleContentGet: runtime.handleContentGet.bind(runtime),
1659
+ handleContentCreate: runtime.handleContentCreate.bind(runtime),
1660
+ handleContentUpdate: runtime.handleContentUpdate.bind(runtime),
1661
+ handleContentDelete: runtime.handleContentDelete.bind(runtime),
1662
+ handleContentListTrashed: runtime.handleContentListTrashed.bind(runtime),
1663
+ handleContentRestore: runtime.handleContentRestore.bind(runtime),
1664
+ handleContentPermanentDelete: runtime.handleContentPermanentDelete.bind(runtime),
1665
+ handleContentCountTrashed: runtime.handleContentCountTrashed.bind(runtime),
1666
+ handleContentGetIncludingTrashed: runtime.handleContentGetIncludingTrashed.bind(runtime),
1667
+ handleContentDuplicate: runtime.handleContentDuplicate.bind(runtime),
1668
+ handleContentPublish: runtime.handleContentPublish.bind(runtime),
1669
+ handleContentUnpublish: runtime.handleContentUnpublish.bind(runtime),
1670
+ handleContentSchedule: runtime.handleContentSchedule.bind(runtime),
1671
+ handleContentUnschedule: runtime.handleContentUnschedule.bind(runtime),
1672
+ handleContentCountScheduled: runtime.handleContentCountScheduled.bind(runtime),
1673
+ handleContentDiscardDraft: runtime.handleContentDiscardDraft.bind(runtime),
1674
+ handleContentCompare: runtime.handleContentCompare.bind(runtime),
1675
+ handleContentTranslations: runtime.handleContentTranslations.bind(runtime),
1676
+ handleMediaList: runtime.handleMediaList.bind(runtime),
1677
+ handleMediaGet: runtime.handleMediaGet.bind(runtime),
1678
+ handleMediaCreate: runtime.handleMediaCreate.bind(runtime),
1679
+ handleMediaUpdate: runtime.handleMediaUpdate.bind(runtime),
1680
+ handleMediaDelete: runtime.handleMediaDelete.bind(runtime),
1681
+ handleRevisionList: runtime.handleRevisionList.bind(runtime),
1682
+ handleRevisionGet: runtime.handleRevisionGet.bind(runtime),
1683
+ handleRevisionRestore: runtime.handleRevisionRestore.bind(runtime),
1684
+ handlePluginApiRoute: runtime.handlePluginApiRoute.bind(runtime),
1685
+ getPluginRouteMeta: runtime.getPluginRouteMeta.bind(runtime),
1686
+ getMediaProvider: runtime.getMediaProvider.bind(runtime),
1687
+ getMediaProviderList: runtime.getMediaProviderList.bind(runtime),
1688
+ collectPageMetadata: runtime.collectPageMetadata.bind(runtime),
1689
+ collectPageFragments: runtime.collectPageFragments.bind(runtime),
1690
+ storage: runtime.storage,
1691
+ db: runtime.db,
1692
+ hooks: runtime.hooks,
1693
+ email: runtime.email,
1694
+ configuredPlugins: runtime.configuredPlugins,
1695
+ config,
1696
+ invalidateManifest: runtime.invalidateManifest.bind(runtime),
1697
+ getSandboxRunner: runtime.getSandboxRunner.bind(runtime),
1698
+ syncMarketplacePlugins: runtime.syncMarketplacePlugins.bind(runtime),
1699
+ setPluginStatus: runtime.setPluginStatus.bind(runtime)
1700
+ };
1701
+ } catch (error) {
1702
+ console.error("Dineway middleware error:", error);
1703
+ }
1704
+ const response = await next();
1705
+ setBaselineSecurityHeaders(response);
1706
+ return response;
1707
+ };
1708
+ if (requestDb) return runWithContext({
1709
+ editMode: context.cookies.get("dineway-edit-mode")?.value === "true",
1710
+ db: requestDb
1711
+ }, doInit);
1712
+ return doInit();
1713
+ });
1714
+
1715
+ //#endregion
1716
+ export { onRequest as default, onRequest };