@access-dlsu/leapify 0.260507.1 → 0.260507.5

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 (57) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/auth/auth.d.ts.map +1 -1
  3. package/dist/auth/middleware.d.ts.map +1 -1
  4. package/dist/{chunk-ANNHE3PZ.js → chunk-5YYVBPAE.js} +21 -5
  5. package/dist/chunk-5YYVBPAE.js.map +1 -0
  6. package/dist/{chunk-QARF2YFF.cjs → chunk-LVKPYSXI.cjs} +21 -5
  7. package/dist/chunk-LVKPYSXI.cjs.map +1 -0
  8. package/dist/{chunk-63CUZGSZ.js → chunk-OZ6HZKR5.js} +21 -5
  9. package/dist/chunk-OZ6HZKR5.js.map +1 -0
  10. package/dist/{chunk-YFJBE3AU.cjs → chunk-S5DBMZVP.cjs} +21 -5
  11. package/dist/chunk-S5DBMZVP.cjs.map +1 -0
  12. package/dist/client/auth.d.ts +1 -13
  13. package/dist/client/auth.d.ts.map +1 -1
  14. package/dist/client/index.cjs +25 -25
  15. package/dist/client/index.cjs.map +1 -1
  16. package/dist/client/index.d.ts +17 -17
  17. package/dist/client/index.d.ts.map +1 -1
  18. package/dist/client/index.js +25 -25
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/client/types.d.ts +4 -2
  21. package/dist/client/types.d.ts.map +1 -1
  22. package/dist/db/migrate.d.ts.map +1 -1
  23. package/dist/db/schema/{events.d.ts → classes.d.ts} +3 -3
  24. package/dist/db/schema/classes.d.ts.map +1 -0
  25. package/dist/db/schema/index.d.ts +1 -1
  26. package/dist/db/schema/index.d.ts.map +1 -1
  27. package/dist/db/schema/site-config.d.ts +83 -0
  28. package/dist/db/schema/site-config.d.ts.map +1 -1
  29. package/dist/index.cjs +679 -59
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +655 -40
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/middleware/cors.d.ts.map +1 -1
  36. package/dist/lib/middleware/pow-challenge.cjs +6 -6
  37. package/dist/lib/middleware/pow-challenge.d.ts.map +1 -1
  38. package/dist/lib/middleware/pow-challenge.js +1 -1
  39. package/dist/routes/classes.d.ts +4 -0
  40. package/dist/routes/classes.d.ts.map +1 -0
  41. package/dist/routes/contentful-sync.d.ts +4 -0
  42. package/dist/routes/contentful-sync.d.ts.map +1 -0
  43. package/dist/services/snapshot.d.ts +1 -1
  44. package/dist/services/snapshot.d.ts.map +1 -1
  45. package/dist/types.d.ts +19 -0
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/worker-handler.d.ts.map +1 -1
  48. package/dist/worker.js +662 -39
  49. package/dist/worker.js.map +1 -1
  50. package/package.json +153 -153
  51. package/dist/chunk-63CUZGSZ.js.map +0 -1
  52. package/dist/chunk-ANNHE3PZ.js.map +0 -1
  53. package/dist/chunk-QARF2YFF.cjs.map +0 -1
  54. package/dist/chunk-YFJBE3AU.cjs.map +0 -1
  55. package/dist/db/schema/events.d.ts.map +0 -1
  56. package/dist/routes/events.d.ts +0 -4
  57. package/dist/routes/events.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createKyselyAdapter, getKyselyDatabaseType } from './chunk-FUCJEA2S.js';
2
2
  import { createDefu, wildcardMatch, getOrigin, getHost, getProtocol, betterFetch, PACKAGE_VERSION, isDynamicBaseURLConfig, getBaseURL, resolveBaseURL, isRequestLike, defu } from './chunk-IQEWVHLM.js';
3
3
  import './chunk-HHNEB7YR.js';
4
- import { createMiddleware, Hono, HTTPException, tryDecode, decodeURIComponent_, createPowChallengeMiddleware, POW_VERIFY_PATH, handlePowVerify } from './chunk-ANNHE3PZ.js';
4
+ import { createMiddleware, Hono, HTTPException, tryDecode, decodeURIComponent_, createPowChallengeMiddleware, POW_VERIFY_PATH, handlePowVerify } from './chunk-5YYVBPAE.js';
5
5
  import { createRandomStringGenerator, createAdapterFactory, withSpan, ATTR_CONTEXT, import_semantic_conventions, ATTR_HOOK_TYPE, ATTR_OPERATION_ID, generateId, safeJSONParse, getAuthTables, initGetModelName, initGetFieldName } from './chunk-GNRL67OU.js';
6
6
  import './chunk-EGRHWZRV.js';
7
7
  import { APIError2, BASE_ERROR_CODES, kAPIErrorHeaderSymbol, BetterCallError, BetterAuthError, logger, ValidationError, APIError, env, isProduction, shouldPublishLog, isDevelopment, createLogger, isTest, getBooleanEnvVar, ENV, getEnvVar } from './chunk-MNEW2V4T.js';
@@ -48,13 +48,28 @@ var errorHandler = (err, c) => {
48
48
  );
49
49
  };
50
50
 
51
+ // src/types.ts
52
+ function parseCmsMode(raw) {
53
+ if (raw === "cloudflare" || raw === "contentful") return raw;
54
+ return "hybrid";
55
+ }
56
+ function shouldPushToContentful(mode) {
57
+ return mode === "hybrid";
58
+ }
59
+ function shouldPullFromContentful(mode) {
60
+ return mode === "contentful" || mode === "hybrid";
61
+ }
62
+
51
63
  // node_modules/hono/dist/middleware/cors/index.js
52
64
  var cors = (options) => {
53
- const opts = {
65
+ const defaults = {
54
66
  origin: "*",
55
67
  allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
56
68
  allowHeaders: [],
57
- exposeHeaders: [],
69
+ exposeHeaders: []
70
+ };
71
+ const opts = {
72
+ ...defaults,
58
73
  ...options
59
74
  };
60
75
  const findAllowOrigin = ((optsOrigin) => {
@@ -145,9 +160,22 @@ function createCorsMiddleware(allowedOrigins) {
145
160
  });
146
161
  return async (c, next) => {
147
162
  const origin = c.req.header("origin");
163
+ if (c.req.path.startsWith("/api/uploads/images")) {
164
+ c.header("Access-Control-Allow-Origin", "*");
165
+ c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
166
+ if (c.req.method === "OPTIONS") {
167
+ return c.body(null, 204);
168
+ }
169
+ return next();
170
+ }
148
171
  if (!c.req.path.startsWith("/health") && !c.req.path.startsWith("/api/auth") && !c.req.path.startsWith("/internal") && origin && !allowedOrigins.includes("*") && !allowedOrigins.includes(origin)) {
149
172
  return c.json(
150
- { error: { code: "DOMAIN_RESTRICTED", message: `Origin ${origin} is not allowed` } },
173
+ {
174
+ error: {
175
+ code: "DOMAIN_RESTRICTED",
176
+ message: `Origin ${origin} is not allowed`
177
+ }
178
+ },
151
179
  403
152
180
  );
153
181
  }
@@ -36330,12 +36358,14 @@ function drizzle(client, config3 = {}) {
36330
36358
  // src/db/schema/index.ts
36331
36359
  var schema_exports = {};
36332
36360
  __export(schema_exports, {
36361
+ CONTENTFUL_CONFIG_KEYS: () => CONTENTFUL_CONFIG_KEYS,
36333
36362
  authAccount: () => authAccount,
36334
36363
  authSession: () => authSession,
36335
36364
  authUser: () => authUser,
36336
36365
  authVerification: () => authVerification,
36337
36366
  bookmarks: () => bookmarks,
36338
36367
  bookmarksRelations: () => bookmarksRelations,
36368
+ contentfulConfig: () => contentfulConfig,
36339
36369
  events: () => events,
36340
36370
  eventsRelations: () => eventsRelations,
36341
36371
  faqs: () => faqs,
@@ -36387,7 +36417,7 @@ var organizationsRelations = relations(organizations, ({ many }) => ({
36387
36417
  events: many(events)
36388
36418
  }));
36389
36419
 
36390
- // src/db/schema/events.ts
36420
+ // src/db/schema/classes.ts
36391
36421
  var events = sqliteTable(
36392
36422
  "events",
36393
36423
  {
@@ -36412,7 +36442,7 @@ var events = sqliteTable(
36412
36442
  // start time string
36413
36443
  endTime: text("end_time"),
36414
36444
  // end time string
36415
- isMajor: integer2("is_major", { mode: "boolean" }).notNull().default(false),
36445
+ isSpotlight: integer2("is_spotlight", { mode: "boolean" }).notNull().default(false),
36416
36446
  // Slot tracking (local counter — NOT polled from Google Forms)
36417
36447
  maxSlots: integer2("max_slots").notNull().default(0),
36418
36448
  registeredSlots: integer2("registered_slots").notNull().default(0),
@@ -36469,6 +36499,18 @@ var siteConfig = sqliteTable("site_config", {
36469
36499
  // JSON-serializable string
36470
36500
  updatedAt: integer2("updated_at").notNull().default(sql2`(unixepoch())`)
36471
36501
  });
36502
+ var CONTENTFUL_CONFIG_KEYS = {
36503
+ ENABLED: "contentful.enabled",
36504
+ SPACE_ID: "contentful.spaceId",
36505
+ MANAGEMENT_TOKEN: "contentful.managementToken",
36506
+ DEFAULT_SPACE_ID: "dlsu-events"
36507
+ };
36508
+ var contentfulConfig = sqliteTable("contentful_config", {
36509
+ space_id: text("space_id"),
36510
+ contentful_enabled: integer2("contentful_enabled").notNull().default(0),
36511
+ last_sync_at: integer2("last_sync_at"),
36512
+ updated_at: integer2("updated_at").default(sql2`(unixepoch())`).notNull()
36513
+ });
36472
36514
 
36473
36515
  // src/db/schema/faqs.ts
36474
36516
  var faqs = sqliteTable("faqs", {
@@ -36638,11 +36680,21 @@ function createAuth(env2) {
36638
36680
  };
36639
36681
  if (isFirstUser) {
36640
36682
  await db.insert(users).values(base).onConflictDoUpdate({
36641
- target: users.betterAuthId,
36642
- set: { role: "super_admin" }
36683
+ target: users.email,
36684
+ set: {
36685
+ betterAuthId: user.id,
36686
+ role: "super_admin",
36687
+ name: user.name ?? user.email.split("@")[0]
36688
+ }
36643
36689
  });
36644
36690
  } else {
36645
- await db.insert(users).values(base).onConflictDoNothing({ target: users.betterAuthId });
36691
+ await db.insert(users).values(base).onConflictDoUpdate({
36692
+ target: users.email,
36693
+ set: {
36694
+ betterAuthId: user.id,
36695
+ name: user.name ?? user.email.split("@")[0]
36696
+ }
36697
+ });
36646
36698
  }
36647
36699
  }
36648
36700
  }
@@ -36671,7 +36723,13 @@ async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAu
36671
36723
  betterAuthId: betterAuthUserId,
36672
36724
  email: betterAuthUserEmail,
36673
36725
  name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
36674
- }).onConflictDoNothing({ target: users.betterAuthId }).returning();
36726
+ }).onConflictDoUpdate({
36727
+ target: users.email,
36728
+ set: {
36729
+ betterAuthId: betterAuthUserId,
36730
+ name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
36731
+ }
36732
+ }).returning();
36675
36733
  dbUser = created;
36676
36734
  }
36677
36735
  if (!dbUser) throw unauthorized("Failed to resolve user record");
@@ -37638,7 +37696,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
37638
37696
  identifier: "uid"
37639
37697
  });
37640
37698
 
37641
- // src/routes/events.ts
37699
+ // src/routes/classes.ts
37642
37700
  var EVENTS_LIST_KV_KEY = "events:list";
37643
37701
  var EVENTS_ETAG_KV_KEY = "events:etag";
37644
37702
  var EVENTS_LIST_TTL = 300;
@@ -37654,7 +37712,7 @@ async function pushEventToContentful(env2, event) {
37654
37712
  const fields = {
37655
37713
  title: ContentfulManagement.locale(event.title),
37656
37714
  slug: ContentfulManagement.locale(event.slug),
37657
- isMajor: ContentfulManagement.locale(event.isMajor),
37715
+ isSpotlight: ContentfulManagement.locale(event.isSpotlight),
37658
37716
  maxSlots: ContentfulManagement.locale(event.maxSlots)
37659
37717
  };
37660
37718
  if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
@@ -37717,7 +37775,7 @@ var createEventSchema = external_exports.object({
37717
37775
  startTime: external_exports.string().optional(),
37718
37776
  endTime: external_exports.string().optional(),
37719
37777
  registrationClosesAt: external_exports.number().optional(),
37720
- isMajor: external_exports.boolean().default(false),
37778
+ isSpotlight: external_exports.boolean().default(false),
37721
37779
  maxSlots: external_exports.number().int().min(0).default(0),
37722
37780
  gformsId: external_exports.string().optional(),
37723
37781
  gformsUrl: external_exports.string().url().optional(),
@@ -37726,11 +37784,11 @@ var createEventSchema = external_exports.object({
37726
37784
  contentfulEntryId: external_exports.string().optional(),
37727
37785
  status: external_exports.enum(["draft", "queued", "published"]).default("draft")
37728
37786
  });
37729
- var eventsRoute = new Hono();
37787
+ var classesRoute = new Hono();
37730
37788
  function generateSlug(title) {
37731
37789
  return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
37732
37790
  }
37733
- eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37791
+ classesRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37734
37792
  const db = createDb(c.env.DB);
37735
37793
  const data = await db.query.events.findMany({
37736
37794
  with: { theme: true, organization: true },
@@ -37738,7 +37796,7 @@ eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37738
37796
  });
37739
37797
  return c.json({ data });
37740
37798
  });
37741
- eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
37799
+ classesRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
37742
37800
  const body = await c.req.json();
37743
37801
  const db = createDb(c.env.DB);
37744
37802
  const cache3 = new CacheService(c.env.KV);
@@ -37766,7 +37824,7 @@ eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) =>
37766
37824
  ]);
37767
37825
  return c.json({ data: { updated: body.ids.length } });
37768
37826
  });
37769
- eventsRoute.get("/", eventsListRateLimit, async (c) => {
37827
+ classesRoute.get("/", eventsListRateLimit, async (c) => {
37770
37828
  const db = createDb(c.env.DB);
37771
37829
  const cache3 = new CacheService(c.env.KV);
37772
37830
  const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
@@ -37777,7 +37835,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37777
37835
  );
37778
37836
  const ifNoneMatch = c.req.header("If-None-Match");
37779
37837
  if (ifNoneMatch === etag) {
37780
- return c.newResponse(null, 304);
37838
+ return c.body(null, 304);
37781
37839
  }
37782
37840
  const data = await cache3.getOrSet(
37783
37841
  EVENTS_LIST_KV_KEY,
@@ -37801,7 +37859,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37801
37859
  startTime: true,
37802
37860
  endTime: true,
37803
37861
  registrationClosesAt: true,
37804
- isMajor: true,
37862
+ isSpotlight: true,
37805
37863
  maxSlots: true,
37806
37864
  registeredSlots: true,
37807
37865
  gformsUrl: true,
@@ -37818,7 +37876,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37818
37876
  );
37819
37877
  return c.json({ data });
37820
37878
  });
37821
- eventsRoute.get("/:slug", async (c) => {
37879
+ classesRoute.get("/:slug", async (c) => {
37822
37880
  const { slug } = c.req.param();
37823
37881
  const db = createDb(c.env.DB);
37824
37882
  const event = await db.query.events.findFirst({
@@ -37830,7 +37888,7 @@ eventsRoute.get("/:slug", async (c) => {
37830
37888
  if (!event) throw notFound("Event");
37831
37889
  return c.json({ data: event });
37832
37890
  });
37833
- eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37891
+ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37834
37892
  const { slug } = c.req.param();
37835
37893
  const db = createDb(c.env.DB);
37836
37894
  const cache3 = new CacheService(c.env.KV);
@@ -37840,7 +37898,7 @@ eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37840
37898
  c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
37841
37899
  return c.json({ data: info2 });
37842
37900
  });
37843
- eventsRoute.post(
37901
+ classesRoute.post(
37844
37902
  "/",
37845
37903
  authMiddleware,
37846
37904
  adminMiddleware,
@@ -37875,11 +37933,13 @@ eventsRoute.post(
37875
37933
  cache3.del(EVENTS_LIST_KV_KEY),
37876
37934
  cache3.del(EVENTS_ETAG_KV_KEY)
37877
37935
  ]);
37878
- c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
37936
+ if (c.get("cmsMode") === "hybrid") {
37937
+ c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
37938
+ }
37879
37939
  return c.json({ data: created }, 201);
37880
37940
  }
37881
37941
  );
37882
- eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37942
+ classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37883
37943
  const { slug } = c.req.param();
37884
37944
  const body = await c.req.json();
37885
37945
  const db = createDb(c.env.DB);
@@ -37894,10 +37954,12 @@ eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37894
37954
  cache3.del(EVENTS_LIST_KV_KEY),
37895
37955
  cache3.del(EVENTS_ETAG_KV_KEY)
37896
37956
  ]);
37897
- c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
37957
+ if (c.get("cmsMode") === "hybrid") {
37958
+ c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
37959
+ }
37898
37960
  return c.json({ data: updated });
37899
37961
  });
37900
- eventsRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
37962
+ classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
37901
37963
  const { slug } = c.req.param();
37902
37964
  const db = createDb(c.env.DB);
37903
37965
  const cache3 = new CacheService(c.env.KV);
@@ -37996,7 +38058,447 @@ usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
37996
38058
  return c.json({ data: { bookmarked: false } });
37997
38059
  });
37998
38060
 
38061
+ // src/services/contentful.ts
38062
+ var CONTENTFUL_CDN = "https://cdn.contentful.com";
38063
+ var ContentfulService = class _ContentfulService {
38064
+ spaceId;
38065
+ accessToken;
38066
+ environment;
38067
+ constructor(spaceId, accessToken, environment = "master") {
38068
+ this.spaceId = spaceId;
38069
+ this.accessToken = accessToken;
38070
+ this.environment = environment;
38071
+ }
38072
+ /**
38073
+ * Returns true if the required Contentful credentials are configured.
38074
+ */
38075
+ static isConfigured(spaceId, accessToken) {
38076
+ return !!(spaceId && accessToken);
38077
+ }
38078
+ // ─── Entries ─────────────────────────────────────────────────────────────
38079
+ /**
38080
+ * Fetch all entries of a given content type.
38081
+ * Handles pagination automatically (100 per page, Contentful max).
38082
+ */
38083
+ async getEntries(contentTypeId) {
38084
+ const allItems = [];
38085
+ let skip = 0;
38086
+ const limit = 100;
38087
+ do {
38088
+ const url2 = this.buildUrl(`/entries`, {
38089
+ content_type: contentTypeId,
38090
+ skip: String(skip),
38091
+ limit: String(limit),
38092
+ include: "2"
38093
+ // resolve up to 2 levels of linked entries/assets
38094
+ });
38095
+ const res = await fetch(url2, { headers: this.headers() });
38096
+ if (!res.ok) {
38097
+ throw new Error(`Contentful entries error: ${res.status} ${await res.text()}`);
38098
+ }
38099
+ const data = await res.json();
38100
+ allItems.push(...data.items);
38101
+ skip += limit;
38102
+ if (allItems.length >= data.total) break;
38103
+ } while (true);
38104
+ return allItems;
38105
+ }
38106
+ /**
38107
+ * Fetch all assets. Handles pagination.
38108
+ */
38109
+ async getAssets() {
38110
+ const allItems = [];
38111
+ let skip = 0;
38112
+ const limit = 100;
38113
+ do {
38114
+ const url2 = this.buildUrl(`/assets`, {
38115
+ skip: String(skip),
38116
+ limit: String(limit)
38117
+ });
38118
+ const res = await fetch(url2, { headers: this.headers() });
38119
+ if (!res.ok) {
38120
+ throw new Error(`Contentful assets error: ${res.status} ${await res.text()}`);
38121
+ }
38122
+ const data = await res.json();
38123
+ allItems.push(...data.items);
38124
+ skip += limit;
38125
+ if (allItems.length >= data.total) break;
38126
+ } while (true);
38127
+ return allItems;
38128
+ }
38129
+ // ─── Asset file download ─────────────────────────────────────────────────
38130
+ /**
38131
+ * Download an asset file from Contentful's CDN.
38132
+ * Returns the raw ArrayBuffer and content type.
38133
+ */
38134
+ async downloadAsset(assetUrl) {
38135
+ const url2 = assetUrl.startsWith("//") ? `https:${assetUrl}` : assetUrl;
38136
+ const res = await fetch(url2);
38137
+ if (!res.ok) {
38138
+ throw new Error(`Failed to download asset: ${res.status}`);
38139
+ }
38140
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
38141
+ const data = await res.arrayBuffer();
38142
+ return { data, contentType };
38143
+ }
38144
+ // ─── Helpers ─────────────────────────────────────────────────────────────
38145
+ /**
38146
+ * Extract a field value from a Contentful entry, handling locale wrapping.
38147
+ * Contentful fields are often `{ "en-US": value }` — this unwraps them.
38148
+ */
38149
+ static getField(entry, fieldName) {
38150
+ const raw = entry.fields[fieldName];
38151
+ if (raw === void 0 || raw === null) return void 0;
38152
+ if (typeof raw === "object" && !Array.isArray(raw) && "en-US" in raw) {
38153
+ return raw["en-US"];
38154
+ }
38155
+ return raw;
38156
+ }
38157
+ /**
38158
+ * Extract a linked entry/sys reference ID from a reference field.
38159
+ */
38160
+ static getRefId(entry, fieldName) {
38161
+ const ref = _ContentfulService.getField(entry, fieldName);
38162
+ return ref?.sys?.id;
38163
+ }
38164
+ /**
38165
+ * Extract an asset URL from a linked asset field.
38166
+ */
38167
+ static getAssetUrl(entry, fieldName) {
38168
+ const asset = _ContentfulService.getField(entry, fieldName);
38169
+ return asset?.sys?.id;
38170
+ }
38171
+ /**
38172
+ * Resolve an asset URL by ID from a list of fetched assets.
38173
+ */
38174
+ static resolveAssetUrl(assets, assetId) {
38175
+ const asset = assets.find((a) => a.sys.id === assetId);
38176
+ return asset?.fields?.file?.url;
38177
+ }
38178
+ // ─── Private ─────────────────────────────────────────────────────────────
38179
+ headers() {
38180
+ return {
38181
+ Authorization: `Bearer ${this.accessToken}`,
38182
+ "Content-Type": "application/json"
38183
+ };
38184
+ }
38185
+ buildUrl(path, params = {}) {
38186
+ const url2 = new URL(
38187
+ `/spaces/${this.spaceId}/environments/${this.environment}${path}`,
38188
+ CONTENTFUL_CDN
38189
+ );
38190
+ for (const [key, value] of Object.entries(params)) {
38191
+ url2.searchParams.set(key, value);
38192
+ }
38193
+ return url2.toString();
38194
+ }
38195
+ };
38196
+
37999
38197
  // src/services/snapshot.ts
38198
+ var DEFAULT_FIELDS = {
38199
+ event: {
38200
+ title: "title",
38201
+ slug: "slug",
38202
+ theme: "theme",
38203
+ organization: "organization",
38204
+ venue: "venue",
38205
+ date: "date",
38206
+ startTime: "startTime",
38207
+ endTime: "endTime",
38208
+ price: "price",
38209
+ image: "image",
38210
+ isSpotlight: "isSpotlight",
38211
+ maxSlots: "maxSlots",
38212
+ gformsUrl: "gformsUrl",
38213
+ gformsEditorUrl: "gformsEditorUrl",
38214
+ registrationClosesAt: "registrationClosesAt",
38215
+ classCode: "classCode"
38216
+ },
38217
+ theme: {
38218
+ name: "name",
38219
+ path: "path"
38220
+ },
38221
+ faq: {
38222
+ question: "question",
38223
+ answer: "answer",
38224
+ category: "category",
38225
+ sortOrder: "sortOrder"
38226
+ },
38227
+ organization: {
38228
+ name: "name",
38229
+ acronym: "acronym",
38230
+ logoUrl: "logoUrl",
38231
+ link: "link"
38232
+ },
38233
+ siteConfig: {
38234
+ key: "key",
38235
+ value: "value"
38236
+ }
38237
+ };
38238
+ var CONTENTFUL_CACHE_PREFIX = "contentful:cache";
38239
+ var CONTENTFUL_CACHE_TTL = 300;
38240
+ async function snapshotAllContent(db, bucket, contentful, config3 = {}, kv) {
38241
+ const mergedConfig = {
38242
+ eventTypeId: config3.eventTypeId ?? "event",
38243
+ themeTypeId: config3.themeTypeId ?? "theme",
38244
+ faqTypeId: config3.faqTypeId ?? "faq",
38245
+ organizationTypeId: config3.organizationTypeId ?? "organization",
38246
+ fields: {
38247
+ event: { ...DEFAULT_FIELDS.event, ...config3.fields?.event },
38248
+ theme: { ...DEFAULT_FIELDS.theme, ...config3.fields?.theme },
38249
+ faq: { ...DEFAULT_FIELDS.faq, ...config3.fields?.faq },
38250
+ organization: { ...DEFAULT_FIELDS.organization, ...config3.fields?.organization },
38251
+ siteConfig: { ...DEFAULT_FIELDS.siteConfig, ...config3.fields?.siteConfig }
38252
+ }
38253
+ };
38254
+ const result = {
38255
+ themesSynced: 0,
38256
+ eventsSynced: 0,
38257
+ faqsSynced: 0,
38258
+ organizationsSynced: 0,
38259
+ imagesUploaded: 0,
38260
+ imagesSkipped: 0,
38261
+ errors: []
38262
+ };
38263
+ let allAssets = [];
38264
+ if (bucket) {
38265
+ try {
38266
+ allAssets = await contentful.getAssets();
38267
+ } catch (err) {
38268
+ result.errors.push(`Failed to fetch assets: ${err}`);
38269
+ }
38270
+ }
38271
+ try {
38272
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:themes`;
38273
+ let themeEntries;
38274
+ if (kv) {
38275
+ const cached2 = await kv.get(cacheKey, "json");
38276
+ if (cached2) {
38277
+ themeEntries = cached2;
38278
+ } else {
38279
+ themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
38280
+ await kv.put(cacheKey, JSON.stringify(themeEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38281
+ }
38282
+ } else {
38283
+ themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
38284
+ }
38285
+ result.themesSynced = await syncThemes(db, themeEntries, mergedConfig);
38286
+ } catch (err) {
38287
+ result.errors.push(`Themes sync failed: ${err}`);
38288
+ }
38289
+ try {
38290
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:organizations`;
38291
+ let orgEntries;
38292
+ if (kv) {
38293
+ const cached2 = await kv.get(cacheKey, "json");
38294
+ if (cached2) {
38295
+ orgEntries = cached2;
38296
+ } else {
38297
+ orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
38298
+ await kv.put(cacheKey, JSON.stringify(orgEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38299
+ }
38300
+ } else {
38301
+ orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
38302
+ }
38303
+ await syncOrganizations(db, orgEntries, mergedConfig);
38304
+ } catch (err) {
38305
+ result.errors.push(`Organizations sync failed: ${err}`);
38306
+ }
38307
+ try {
38308
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:events`;
38309
+ let eventEntries;
38310
+ if (kv) {
38311
+ const cached2 = await kv.get(cacheKey, "json");
38312
+ if (cached2) {
38313
+ eventEntries = cached2;
38314
+ } else {
38315
+ eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
38316
+ await kv.put(cacheKey, JSON.stringify(eventEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38317
+ }
38318
+ } else {
38319
+ eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
38320
+ }
38321
+ result.eventsSynced = await syncEvents(db, bucket, contentful, allAssets, eventEntries, mergedConfig, result);
38322
+ } catch (err) {
38323
+ result.errors.push(`Events sync failed: ${err}`);
38324
+ }
38325
+ try {
38326
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:faqs`;
38327
+ let faqEntries;
38328
+ if (kv) {
38329
+ const cached2 = await kv.get(cacheKey, "json");
38330
+ if (cached2) {
38331
+ faqEntries = cached2;
38332
+ } else {
38333
+ faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
38334
+ await kv.put(cacheKey, JSON.stringify(faqEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38335
+ }
38336
+ } else {
38337
+ faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
38338
+ }
38339
+ result.faqsSynced = await syncFaqs(db, faqEntries, mergedConfig);
38340
+ } catch (err) {
38341
+ result.errors.push(`FAQs sync failed: ${err}`);
38342
+ }
38343
+ console.log(
38344
+ `[Snapshot] Complete: ${result.themesSynced} themes, ${result.organizationsSynced} organizations, ${result.eventsSynced} events, ${result.faqsSynced} FAQs, ${result.imagesUploaded} images uploaded, ${result.imagesSkipped} images skipped, ${result.errors.length} errors`
38345
+ );
38346
+ return result;
38347
+ }
38348
+ async function syncThemes(db, entries, config3) {
38349
+ let count2 = 0;
38350
+ for (const entry of entries) {
38351
+ const cfId = entry.sys.id;
38352
+ const name = ContentfulService.getField(entry, config3.fields.theme.name);
38353
+ const path = ContentfulService.getField(entry, config3.fields.theme.path);
38354
+ if (!name || !path) continue;
38355
+ await db.insert(themes).values({ id: cfId, name, path }).onConflictDoUpdate({
38356
+ target: themes.id,
38357
+ set: { name, path }
38358
+ });
38359
+ count2++;
38360
+ }
38361
+ return count2;
38362
+ }
38363
+ async function syncOrganizations(db, entries, config3) {
38364
+ let count2 = 0;
38365
+ for (const entry of entries) {
38366
+ const cfId = entry.sys.id;
38367
+ const f = config3.fields.organization;
38368
+ const name = ContentfulService.getField(entry, f.name);
38369
+ const acronym = ContentfulService.getField(entry, f.acronym);
38370
+ if (!name || !acronym) continue;
38371
+ const logoUrl = ContentfulService.getField(entry, f.logoUrl) ?? null;
38372
+ const link = ContentfulService.getField(entry, f.link) ?? null;
38373
+ await db.insert(organizations).values({ id: cfId, name, acronym, logoUrl, link }).onConflictDoUpdate({
38374
+ target: organizations.id,
38375
+ set: { name, acronym, logoUrl, link }
38376
+ });
38377
+ count2++;
38378
+ }
38379
+ return count2;
38380
+ }
38381
+ async function syncEvents(db, bucket, contentful, allAssets, entries, config3, result) {
38382
+ let count2 = 0;
38383
+ for (const entry of entries) {
38384
+ try {
38385
+ const cfId = entry.sys.id;
38386
+ const f = config3.fields.event;
38387
+ const title = ContentfulService.getField(entry, f.title);
38388
+ if (!title) {
38389
+ result.errors.push(`Event ${cfId}: missing title, skipping`);
38390
+ continue;
38391
+ }
38392
+ const slug = ContentfulService.getField(entry, f.slug) ?? slugify2(title);
38393
+ const themeRef = ContentfulService.getField(entry, f.theme);
38394
+ const themeId = themeRef?.sys?.id ?? null;
38395
+ const orgRef = ContentfulService.getField(entry, f.organization);
38396
+ const organizationId = orgRef?.sys?.id ?? null;
38397
+ let backgroundImageUrl = null;
38398
+ if (bucket) {
38399
+ const imageRef = ContentfulService.getField(entry, f.image);
38400
+ const assetId = imageRef?.sys?.id;
38401
+ if (assetId) {
38402
+ const assetUrl = ContentfulService.resolveAssetUrl(allAssets, assetId);
38403
+ if (assetUrl) {
38404
+ const r2Key = `contentful/${assetId}`;
38405
+ const uploaded = await uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key);
38406
+ if (uploaded.skipped) {
38407
+ result.imagesSkipped++;
38408
+ } else {
38409
+ result.imagesUploaded++;
38410
+ }
38411
+ backgroundImageUrl = `/uploads/images/${r2Key}`;
38412
+ }
38413
+ }
38414
+ }
38415
+ const values = {
38416
+ id: cfId,
38417
+ contentfulEntryId: cfId,
38418
+ title,
38419
+ slug,
38420
+ themeId,
38421
+ organizationId,
38422
+ updatedAt: entry.sys.updatedAt
38423
+ };
38424
+ const venue = ContentfulService.getField(entry, f.venue);
38425
+ if (venue !== void 0) values.venue = venue;
38426
+ const date5 = ContentfulService.getField(entry, f.date);
38427
+ if (date5 !== void 0) values.dateTime = date5;
38428
+ const price = ContentfulService.getField(entry, f.price);
38429
+ if (price !== void 0) values.price = price;
38430
+ if (backgroundImageUrl) values.backgroundImageUrl = backgroundImageUrl;
38431
+ const isSpotlight = ContentfulService.getField(entry, f.isSpotlight);
38432
+ if (isSpotlight !== void 0) values.isSpotlight = isSpotlight;
38433
+ const maxSlots = ContentfulService.getField(entry, f.maxSlots);
38434
+ if (maxSlots !== void 0) values.maxSlots = maxSlots;
38435
+ const gformsUrl = ContentfulService.getField(entry, f.gformsUrl);
38436
+ if (gformsUrl !== void 0) values.gformsUrl = gformsUrl;
38437
+ const gformsEditorUrl = ContentfulService.getField(entry, f.gformsEditorUrl);
38438
+ if (gformsEditorUrl !== void 0) values.gformsEditorUrl = gformsEditorUrl;
38439
+ const classCode = ContentfulService.getField(entry, f.classCode);
38440
+ if (classCode !== void 0) values.classCode = classCode;
38441
+ const startTime = ContentfulService.getField(entry, f.startTime);
38442
+ if (startTime !== void 0) values.startTime = startTime;
38443
+ const endTime = ContentfulService.getField(entry, f.endTime);
38444
+ if (endTime !== void 0) values.endTime = endTime;
38445
+ const regCloseRaw = ContentfulService.getField(entry, f.registrationClosesAt);
38446
+ if (regCloseRaw) {
38447
+ const ms = Date.parse(regCloseRaw);
38448
+ if (!Number.isNaN(ms)) values.registrationClosesAt = Math.floor(ms / 1e3);
38449
+ }
38450
+ await db.insert(events).values(values).onConflictDoUpdate({
38451
+ target: events.id,
38452
+ set: values
38453
+ });
38454
+ count2++;
38455
+ } catch (err) {
38456
+ result.errors.push(`Event ${entry.sys.id}: ${err}`);
38457
+ }
38458
+ }
38459
+ return count2;
38460
+ }
38461
+ async function syncFaqs(db, entries, config3) {
38462
+ let count2 = 0;
38463
+ for (const entry of entries) {
38464
+ const cfId = entry.sys.id;
38465
+ const f = config3.fields.faq;
38466
+ const question = ContentfulService.getField(entry, f.question);
38467
+ const answer = ContentfulService.getField(entry, f.answer);
38468
+ if (!question || !answer) continue;
38469
+ const category = ContentfulService.getField(entry, f.category) ?? null;
38470
+ const sortOrder = ContentfulService.getField(entry, f.sortOrder) ?? 0;
38471
+ await db.insert(faqs).values({
38472
+ id: cfId,
38473
+ question,
38474
+ answer,
38475
+ category,
38476
+ sortOrder
38477
+ }).onConflictDoUpdate({
38478
+ target: faqs.id,
38479
+ set: { question, answer, category, sortOrder }
38480
+ });
38481
+ count2++;
38482
+ }
38483
+ return count2;
38484
+ }
38485
+ async function uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key) {
38486
+ const { data, contentType } = await contentful.downloadAsset(assetUrl);
38487
+ const sha = await computeSha256(data);
38488
+ const existing = await bucket.head(r2Key);
38489
+ if (existing?.customMetadata?.sha256 === sha) {
38490
+ return { skipped: true };
38491
+ }
38492
+ await bucket.put(r2Key, data, {
38493
+ httpMetadata: { contentType },
38494
+ customMetadata: {
38495
+ sha256: sha,
38496
+ source: "contentful",
38497
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
38498
+ }
38499
+ });
38500
+ return { skipped: false };
38501
+ }
38000
38502
  async function batchRun(items, fn, concurrency = 5) {
38001
38503
  const results = [];
38002
38504
  for (let i = 0; i < items.length; i += concurrency) {
@@ -38006,6 +38508,13 @@ async function batchRun(items, fn, concurrency = 5) {
38006
38508
  }
38007
38509
  return results;
38008
38510
  }
38511
+ async function computeSha256(data) {
38512
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
38513
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
38514
+ }
38515
+ function slugify2(text2) {
38516
+ return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
38517
+ }
38009
38518
  async function ensureContentTypes(mgmt, config3 = {}) {
38010
38519
  const eventTypeId = config3.eventTypeId ?? "event";
38011
38520
  const themeTypeId = config3.themeTypeId ?? "theme";
@@ -38026,7 +38535,7 @@ async function ensureContentTypes(mgmt, config3 = {}) {
38026
38535
  { id: "endTime", name: "End Time", type: "Symbol" },
38027
38536
  { id: "price", name: "Price", type: "Symbol" },
38028
38537
  { id: "image", name: "Image", type: "Link", linkType: "Asset" },
38029
- { id: "isMajor", name: "Major Event", type: "Boolean" },
38538
+ { id: "isSpotlight", name: "Spotlight", type: "Boolean" },
38030
38539
  { id: "maxSlots", name: "Max Slots", type: "Integer" },
38031
38540
  { id: "gformsUrl", name: "Google Forms URL", type: "Symbol" },
38032
38541
  { id: "gformsEditorUrl", name: "Google Forms Editor URL", type: "Symbol" },
@@ -38111,7 +38620,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
38111
38620
  const fields = {
38112
38621
  title: ContentfulManagement.locale(event.title),
38113
38622
  slug: ContentfulManagement.locale(event.slug),
38114
- isMajor: ContentfulManagement.locale(event.isMajor),
38623
+ isSpotlight: ContentfulManagement.locale(event.isSpotlight),
38115
38624
  maxSlots: ContentfulManagement.locale(event.maxSlots)
38116
38625
  };
38117
38626
  if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
@@ -38182,6 +38691,9 @@ siteConfigRoute.get("/", async (c) => {
38182
38691
  siteName: config3.site_name ?? null,
38183
38692
  registrationGloballyOpen: config3.registration_globally_open ?? true,
38184
38693
  maintenanceMode: config3.maintenance_mode ?? false,
38694
+ // Prefer the D1-persisted value over the env-var default so that
38695
+ // PATCH /config/cms_mode changes are reflected immediately.
38696
+ cmsMode: config3.cms_mode ?? c.get("cmsMode"),
38185
38697
  now: Math.floor(Date.now() / 1e3)
38186
38698
  }
38187
38699
  });
@@ -38203,6 +38715,10 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
38203
38715
  var SYNC_LOCK_KEY = "contentful:sync:lock";
38204
38716
  var SYNC_LOCK_TTL = 60;
38205
38717
  siteConfigRoute.post("/sync-content", authMiddleware, adminMiddleware, async (c) => {
38718
+ const cmsMode = c.get("cmsMode");
38719
+ if (cmsMode === "cloudflare") {
38720
+ throw serviceUnavailable("Contentful sync is not available in Cloudflare-only mode.");
38721
+ }
38206
38722
  if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
38207
38723
  throw serviceUnavailable("Contentful Management API credentials not configured.");
38208
38724
  }
@@ -38289,7 +38805,9 @@ faqsRoute.post(
38289
38805
  const cache3 = new CacheService(c.env.KV);
38290
38806
  const [created] = await db.insert(faqs).values(body).returning();
38291
38807
  await cache3.del(FAQS_KV_KEY);
38292
- c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
38808
+ if (c.get("cmsMode") === "hybrid") {
38809
+ c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
38810
+ }
38293
38811
  return c.json({ data: created }, 201);
38294
38812
  }
38295
38813
  );
@@ -38302,7 +38820,9 @@ faqsRoute.patch("/:id", authMiddleware, adminMiddleware, async (c) => {
38302
38820
  const [updated] = await db.update(faqs).set({ ...body, updatedAt: now2 }).where(eq(faqs.id, id)).returning();
38303
38821
  if (!updated) throw notFound("FAQ");
38304
38822
  await cache3.del(FAQS_KV_KEY);
38305
- c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
38823
+ if (c.get("cmsMode") === "hybrid") {
38824
+ c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
38825
+ }
38306
38826
  return c.json({ data: updated });
38307
38827
  });
38308
38828
  faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
@@ -38578,7 +39098,9 @@ themesRoute.post(
38578
39098
  const db = createDb(c.env.DB);
38579
39099
  try {
38580
39100
  const [created] = await db.insert(themes).values(body).returning();
38581
- c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
39101
+ if (c.get("cmsMode") === "hybrid") {
39102
+ c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
39103
+ }
38582
39104
  return c.json({ data: created }, 201);
38583
39105
  } catch (err) {
38584
39106
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -38599,7 +39121,9 @@ themesRoute.patch(
38599
39121
  try {
38600
39122
  const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
38601
39123
  if (!updated) throw notFound("Theme");
38602
- c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
39124
+ if (c.get("cmsMode") === "hybrid") {
39125
+ c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
39126
+ }
38603
39127
  return c.json({ data: updated });
38604
39128
  } catch (err) {
38605
39129
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -38614,7 +39138,9 @@ themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
38614
39138
  const db = createDb(c.env.DB);
38615
39139
  const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
38616
39140
  if (!deleted) throw notFound("Theme");
38617
- c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
39141
+ if (c.get("cmsMode") === "hybrid") {
39142
+ c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
39143
+ }
38618
39144
  return c.body(null, 204);
38619
39145
  });
38620
39146
 
@@ -38678,6 +39204,83 @@ organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) =>
38678
39204
  return c.body(null, 204);
38679
39205
  });
38680
39206
 
39207
+ // src/routes/contentful-sync.ts
39208
+ var contentfulSyncRoute = new Hono();
39209
+ contentfulSyncRoute.post(
39210
+ "/trigger",
39211
+ authMiddleware,
39212
+ adminMiddleware,
39213
+ async (c) => {
39214
+ const env2 = c.env;
39215
+ const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
39216
+ if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_ACCESS_TOKEN) {
39217
+ return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful credentials missing" } }, 400);
39218
+ }
39219
+ const db = createDb(env2.DB);
39220
+ const contentful = new ContentfulService(CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_ENVIRONMENT);
39221
+ try {
39222
+ if (CONTENTFUL_MANAGEMENT_TOKEN) {
39223
+ const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
39224
+ await ensureContentTypes(mgmt);
39225
+ }
39226
+ c.executionCtx.waitUntil(
39227
+ snapshotAllContent(db, env2.FILES, contentful, {}, env2.KV).then((r) => console.log("[Contentful] Snapshot complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Snapshot failed:", err))
39228
+ );
39229
+ return c.json({ message: "Contentful sync triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
39230
+ } catch (err) {
39231
+ console.error("[Contentful] Sync trigger failed:", err);
39232
+ return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger sync" } }, 500);
39233
+ }
39234
+ }
39235
+ );
39236
+ contentfulSyncRoute.post(
39237
+ "/push",
39238
+ authMiddleware,
39239
+ adminMiddleware,
39240
+ async (c) => {
39241
+ const env2 = c.env;
39242
+ const { CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
39243
+ if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_MANAGEMENT_TOKEN) {
39244
+ return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful management credentials missing" } }, 400);
39245
+ }
39246
+ const db = createDb(env2.DB);
39247
+ const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
39248
+ try {
39249
+ c.executionCtx.waitUntil(
39250
+ pushToContentful(db, mgmt, {}, env2.KV).then((r) => console.log("[Contentful] Push complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Push failed:", err))
39251
+ );
39252
+ return c.json({ message: "Contentful push triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
39253
+ } catch (err) {
39254
+ console.error("[Contentful] Push trigger failed:", err);
39255
+ return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger push" } }, 500);
39256
+ }
39257
+ }
39258
+ );
39259
+ contentfulSyncRoute.get(
39260
+ "/status",
39261
+ authMiddleware,
39262
+ adminMiddleware,
39263
+ async (c) => {
39264
+ const env2 = c.env;
39265
+ const db = createDb(env2.DB);
39266
+ const [[{ themes: themes2 }], [{ events: events2 }], [{ faqs: faqs2 }], [{ orgs }]] = await Promise.all([
39267
+ db.all(sql2`SELECT count(*) as themes FROM themes`),
39268
+ db.all(sql2`SELECT count(*) as events FROM events`),
39269
+ db.all(sql2`SELECT count(*) as faqs FROM faqs`),
39270
+ db.all(sql2`SELECT count(*) as orgs FROM organizations`)
39271
+ ]);
39272
+ const isConfigured = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_ACCESS_TOKEN;
39273
+ const canPush = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_MANAGEMENT_TOKEN;
39274
+ return c.json({
39275
+ isConfigured,
39276
+ canPush,
39277
+ cmsMode: c.get("cmsMode"),
39278
+ spaceId: env2.CONTENTFUL_SPACE_ID ?? null,
39279
+ totals: { themes: themes2, events: events2, faqs: faqs2, organizations: orgs }
39280
+ });
39281
+ }
39282
+ );
39283
+
38681
39284
  // src/app.ts
38682
39285
  function createApp(options = {}) {
38683
39286
  const app = new Hono();
@@ -38691,6 +39294,12 @@ function createApp(options = {}) {
38691
39294
  app.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
38692
39295
  app.use("*", createPowChallengeMiddleware());
38693
39296
  app.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
39297
+ app.use("*", async (c, next) => {
39298
+ const overrideRaw = await c.env.KV.get("config:cms_mode").catch(() => null);
39299
+ const override = overrideRaw ? JSON.parse(overrideRaw) : null;
39300
+ c.set("cmsMode", parseCmsMode(override ?? c.env.CMS_MODE));
39301
+ return next();
39302
+ });
38694
39303
  app.on(["POST", "GET"], "/api/auth/*", (c) => {
38695
39304
  const auth = createAuth(c.env);
38696
39305
  const req = c.req.raw;
@@ -38702,7 +39311,7 @@ function createApp(options = {}) {
38702
39311
  return auth.handler(new Request(req.url, {
38703
39312
  method: req.method,
38704
39313
  headers: newHeaders,
38705
- body: req.method === "GET" ? null : req.body,
39314
+ body: req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS" ? null : req.body,
38706
39315
  redirect: req.redirect
38707
39316
  }));
38708
39317
  }
@@ -38723,12 +39332,13 @@ function createApp(options = {}) {
38723
39332
  app.post(POW_VERIFY_PATH, handlePowVerify);
38724
39333
  app.route("/health", healthRoute);
38725
39334
  app.route("/api/config", siteConfigRoute);
38726
- app.route("/api/events", eventsRoute);
39335
+ app.route("/api/classes", classesRoute);
38727
39336
  app.route("/api/themes", themesRoute);
38728
39337
  app.route("/api/users", usersRoute);
38729
39338
  app.route("/api/organizations", organizationsRoute);
38730
39339
  app.route("/api/faqs", faqsRoute);
38731
39340
  app.route("/api/uploads", uploadsRoute);
39341
+ app.route("/api/contentful", contentfulSyncRoute);
38732
39342
  app.route("/internal/gforms-webhook", gformsWebhookRoute);
38733
39343
  app.onError(errorHandler);
38734
39344
  app.notFound(
@@ -39362,6 +39972,7 @@ var PATCH_STATEMENTS = [
39362
39972
  `ALTER TABLE "events" ADD COLUMN "class_code" text`,
39363
39973
  `ALTER TABLE "events" ADD COLUMN "start_time" text`,
39364
39974
  `ALTER TABLE "events" ADD COLUMN "end_time" text`,
39975
+ `ALTER TABLE "events" RENAME COLUMN "is_major" TO "is_spotlight"`,
39365
39976
  `CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`
39366
39977
  ];
39367
39978
  var CREATE_STATEMENTS = [
@@ -39465,7 +40076,7 @@ var CREATE_STATEMENTS = [
39465
40076
  "class_code" text,
39466
40077
  "start_time" text,
39467
40078
  "end_time" text,
39468
- "is_major" integer DEFAULT false NOT NULL,
40079
+ "is_spotlight" integer DEFAULT false NOT NULL,
39469
40080
  "max_slots" integer DEFAULT 0 NOT NULL,
39470
40081
  "registered_slots" integer DEFAULT 0 NOT NULL,
39471
40082
  "gforms_id" text,
@@ -39527,7 +40138,9 @@ async function ensureDatabase(d1) {
39527
40138
  try {
39528
40139
  await d1.prepare(sql3).run();
39529
40140
  } catch (err) {
39530
- if (err?.message?.includes("duplicate column")) continue;
40141
+ if (err?.message?.includes("duplicate column") || err?.message?.includes("no such column") && err?.message?.includes("is_major")) {
40142
+ continue;
40143
+ }
39531
40144
  throw err;
39532
40145
  }
39533
40146
  }
@@ -39536,12 +40149,14 @@ async function ensureDatabase(d1) {
39536
40149
  // src/worker-handler.ts
39537
40150
  var API_PREFIXES = [
39538
40151
  "/api/auth/",
39539
- "/api/events",
40152
+ "/api/classes",
39540
40153
  "/api/users",
40154
+ "/api/organizations",
39541
40155
  "/api/faqs",
39542
40156
  "/api/themes",
39543
40157
  "/api/config",
39544
40158
  "/api/uploads",
40159
+ "/api/contentful",
39545
40160
  "/health",
39546
40161
  "/internal/",
39547
40162
  "/.well-known/"
@@ -39580,7 +40195,7 @@ function createWorkerHandler(options) {
39580
40195
  return getLeapify(env2).fetch(request, env2, ctx);
39581
40196
  }
39582
40197
  let response = await options.serveFrontend(request, env2, ctx);
39583
- if ((!response || response.status === 404) && !pathname.includes(".")) {
40198
+ if ((!response || response.status === 404) && !pathname.includes(".") && request.method === "GET") {
39584
40199
  const indexRequest = new Request(new URL("/", request.url), request);
39585
40200
  response = await options.serveFrontend(indexRequest, env2, ctx);
39586
40201
  }
@@ -39688,6 +40303,6 @@ function injectConfig2(html2, config3) {
39688
40303
  (*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
39689
40304
  */
39690
40305
 
39691
- export { ContentfulManagement, authAccount, authSession, authUser, authVerification, bookmarks, bookmarksRelations, createDb, createLeapify, createQueueHandler, createWorkerHandler, ensureContentTypes, ensureDatabase, events, eventsRelations, faqs, getRuntimeConfig, injectConfig2 as injectConfig, organizations, organizationsRelations, siteConfig, themes, themesRelations, users };
40306
+ export { CONTENTFUL_CONFIG_KEYS, ContentfulManagement, authAccount, authSession, authUser, authVerification, bookmarks, bookmarksRelations, contentfulConfig, createDb, createLeapify, createQueueHandler, createWorkerHandler, ensureContentTypes, ensureDatabase, events, eventsRelations, faqs, getRuntimeConfig, injectConfig2 as injectConfig, organizations, organizationsRelations, parseCmsMode, shouldPullFromContentful, shouldPushToContentful, siteConfig, themes, themesRelations, users };
39692
40307
  //# sourceMappingURL=index.js.map
39693
40308
  //# sourceMappingURL=index.js.map