@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/worker.js CHANGED
@@ -25089,13 +25089,22 @@ var errorHandler2 = (err, c) => {
25089
25089
  );
25090
25090
  };
25091
25091
 
25092
+ // src/types.ts
25093
+ function parseCmsMode(raw2) {
25094
+ if (raw2 === "cloudflare" || raw2 === "contentful") return raw2;
25095
+ return "hybrid";
25096
+ }
25097
+
25092
25098
  // node_modules/hono/dist/middleware/cors/index.js
25093
25099
  var cors = (options) => {
25094
- const opts = {
25100
+ const defaults = {
25095
25101
  origin: "*",
25096
25102
  allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
25097
25103
  allowHeaders: [],
25098
- exposeHeaders: [],
25104
+ exposeHeaders: []
25105
+ };
25106
+ const opts = {
25107
+ ...defaults,
25099
25108
  ...options
25100
25109
  };
25101
25110
  const findAllowOrigin = ((optsOrigin) => {
@@ -25186,9 +25195,22 @@ function createCorsMiddleware(allowedOrigins) {
25186
25195
  });
25187
25196
  return async (c, next) => {
25188
25197
  const origin = c.req.header("origin");
25198
+ if (c.req.path.startsWith("/api/uploads/images")) {
25199
+ c.header("Access-Control-Allow-Origin", "*");
25200
+ c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
25201
+ if (c.req.method === "OPTIONS") {
25202
+ return c.body(null, 204);
25203
+ }
25204
+ return next();
25205
+ }
25189
25206
  if (!c.req.path.startsWith("/health") && !c.req.path.startsWith("/api/auth") && !c.req.path.startsWith("/internal") && origin && !allowedOrigins.includes("*") && !allowedOrigins.includes(origin)) {
25190
25207
  return c.json(
25191
- { error: { code: "DOMAIN_RESTRICTED", message: `Origin ${origin} is not allowed` } },
25208
+ {
25209
+ error: {
25210
+ code: "DOMAIN_RESTRICTED",
25211
+ message: `Origin ${origin} is not allowed`
25212
+ }
25213
+ },
25192
25214
  403
25193
25215
  );
25194
25216
  }
@@ -25224,7 +25246,15 @@ var CHALLENGE_KV_PREFIX = "pow:challenge:";
25224
25246
  var DEFAULT_POW_DIFFICULTY = 4;
25225
25247
  var CHALLENGE_TTL_SEC = 120;
25226
25248
  var COOKIE_MAX_AGE_SEC = 3600;
25227
- var EXEMPT_PATHS = ["/health", "/internal", "/api/auth"];
25249
+ var EXEMPT_PATHS = [
25250
+ "/health",
25251
+ "/internal",
25252
+ "/api/auth",
25253
+ "/api/uploads/images",
25254
+ "/api/classes",
25255
+ "/api/faqs",
25256
+ "/api/config"
25257
+ ];
25228
25258
  function base64urlEncode(bytes) {
25229
25259
  let binary2 = "";
25230
25260
  for (const byte of bytes) {
@@ -25389,16 +25419,24 @@ async function handlePowVerify(c) {
25389
25419
  const secret = c.env.INTERNAL_API_SECRET;
25390
25420
  const ip = getClientIp(c);
25391
25421
  const token = await signCookie(secret, ip);
25422
+ const isSecure = c.req.raw.url.startsWith("https") || c.req.header("x-forwarded-proto") === "https";
25392
25423
  c.header(
25393
25424
  "Set-Cookie",
25394
- `${POW_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; Secure; HttpOnly; SameSite=Lax`
25425
+ `${POW_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; ${isSecure ? "Secure; " : ""}HttpOnly; SameSite=Lax`
25395
25426
  );
25396
25427
  return c.json({ redirect: redir || "/" });
25397
25428
  }
25398
25429
  function createPowChallengeMiddleware() {
25399
25430
  return createMiddleware(async (c, next) => {
25400
25431
  if (c.req.path === POW_VERIFY_PATH) return next();
25401
- if (EXEMPT_PATHS.some((p) => c.req.path.startsWith(p))) return next();
25432
+ const normalizedPath = c.req.path.toLowerCase().replace(/\/$/, "");
25433
+ const isExempt = EXEMPT_PATHS.some((p) => {
25434
+ const ep = p.toLowerCase().replace(/\/$/, "");
25435
+ return normalizedPath === ep || normalizedPath.startsWith(ep + "/");
25436
+ });
25437
+ console.log(`[pow] path=${c.req.path} normalized=${normalizedPath} exempt=${isExempt}`);
25438
+ if (isExempt) return next();
25439
+ if (c.req.method === "OPTIONS") return next();
25402
25440
  if (c.req.header("Authorization")) return next();
25403
25441
  const cookieHeader = c.req.header("Cookie") ?? "";
25404
25442
  const cookieMatch = cookieHeader.match(
@@ -62511,12 +62549,14 @@ function drizzle(client, config3 = {}) {
62511
62549
  // src/db/schema/index.ts
62512
62550
  var schema_exports = {};
62513
62551
  __export(schema_exports, {
62552
+ CONTENTFUL_CONFIG_KEYS: () => CONTENTFUL_CONFIG_KEYS,
62514
62553
  authAccount: () => authAccount,
62515
62554
  authSession: () => authSession,
62516
62555
  authUser: () => authUser,
62517
62556
  authVerification: () => authVerification,
62518
62557
  bookmarks: () => bookmarks,
62519
62558
  bookmarksRelations: () => bookmarksRelations,
62559
+ contentfulConfig: () => contentfulConfig,
62520
62560
  events: () => events,
62521
62561
  eventsRelations: () => eventsRelations,
62522
62562
  faqs: () => faqs,
@@ -62568,7 +62608,7 @@ var organizationsRelations = relations(organizations, ({ many }) => ({
62568
62608
  events: many(events)
62569
62609
  }));
62570
62610
 
62571
- // src/db/schema/events.ts
62611
+ // src/db/schema/classes.ts
62572
62612
  var events = sqliteTable(
62573
62613
  "events",
62574
62614
  {
@@ -62593,7 +62633,7 @@ var events = sqliteTable(
62593
62633
  // start time string
62594
62634
  endTime: text("end_time"),
62595
62635
  // end time string
62596
- isMajor: integer2("is_major", { mode: "boolean" }).notNull().default(false),
62636
+ isSpotlight: integer2("is_spotlight", { mode: "boolean" }).notNull().default(false),
62597
62637
  // Slot tracking (local counter — NOT polled from Google Forms)
62598
62638
  maxSlots: integer2("max_slots").notNull().default(0),
62599
62639
  registeredSlots: integer2("registered_slots").notNull().default(0),
@@ -62650,6 +62690,18 @@ var siteConfig = sqliteTable("site_config", {
62650
62690
  // JSON-serializable string
62651
62691
  updatedAt: integer2("updated_at").notNull().default(sql2`(unixepoch())`)
62652
62692
  });
62693
+ var CONTENTFUL_CONFIG_KEYS = {
62694
+ ENABLED: "contentful.enabled",
62695
+ SPACE_ID: "contentful.spaceId",
62696
+ MANAGEMENT_TOKEN: "contentful.managementToken",
62697
+ DEFAULT_SPACE_ID: "dlsu-events"
62698
+ };
62699
+ var contentfulConfig = sqliteTable("contentful_config", {
62700
+ space_id: text("space_id"),
62701
+ contentful_enabled: integer2("contentful_enabled").notNull().default(0),
62702
+ last_sync_at: integer2("last_sync_at"),
62703
+ updated_at: integer2("updated_at").default(sql2`(unixepoch())`).notNull()
62704
+ });
62653
62705
 
62654
62706
  // src/db/schema/faqs.ts
62655
62707
  var faqs = sqliteTable("faqs", {
@@ -62819,11 +62871,21 @@ function createAuth(env2) {
62819
62871
  };
62820
62872
  if (isFirstUser) {
62821
62873
  await db.insert(users).values(base).onConflictDoUpdate({
62822
- target: users.betterAuthId,
62823
- set: { role: "super_admin" }
62874
+ target: users.email,
62875
+ set: {
62876
+ betterAuthId: user.id,
62877
+ role: "super_admin",
62878
+ name: user.name ?? user.email.split("@")[0]
62879
+ }
62824
62880
  });
62825
62881
  } else {
62826
- await db.insert(users).values(base).onConflictDoNothing({ target: users.betterAuthId });
62882
+ await db.insert(users).values(base).onConflictDoUpdate({
62883
+ target: users.email,
62884
+ set: {
62885
+ betterAuthId: user.id,
62886
+ name: user.name ?? user.email.split("@")[0]
62887
+ }
62888
+ });
62827
62889
  }
62828
62890
  }
62829
62891
  }
@@ -62852,7 +62914,13 @@ async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAu
62852
62914
  betterAuthId: betterAuthUserId,
62853
62915
  email: betterAuthUserEmail,
62854
62916
  name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
62855
- }).onConflictDoNothing({ target: users.betterAuthId }).returning();
62917
+ }).onConflictDoUpdate({
62918
+ target: users.email,
62919
+ set: {
62920
+ betterAuthId: betterAuthUserId,
62921
+ name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
62922
+ }
62923
+ }).returning();
62856
62924
  dbUser = created;
62857
62925
  }
62858
62926
  if (!dbUser) throw unauthorized("Failed to resolve user record");
@@ -63819,7 +63887,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
63819
63887
  identifier: "uid"
63820
63888
  });
63821
63889
 
63822
- // src/routes/events.ts
63890
+ // src/routes/classes.ts
63823
63891
  var EVENTS_LIST_KV_KEY = "events:list";
63824
63892
  var EVENTS_ETAG_KV_KEY = "events:etag";
63825
63893
  var EVENTS_LIST_TTL = 300;
@@ -63835,7 +63903,7 @@ async function pushEventToContentful(env2, event) {
63835
63903
  const fields = {
63836
63904
  title: ContentfulManagement.locale(event.title),
63837
63905
  slug: ContentfulManagement.locale(event.slug),
63838
- isMajor: ContentfulManagement.locale(event.isMajor),
63906
+ isSpotlight: ContentfulManagement.locale(event.isSpotlight),
63839
63907
  maxSlots: ContentfulManagement.locale(event.maxSlots)
63840
63908
  };
63841
63909
  if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
@@ -63898,7 +63966,7 @@ var createEventSchema = external_exports.object({
63898
63966
  startTime: external_exports.string().optional(),
63899
63967
  endTime: external_exports.string().optional(),
63900
63968
  registrationClosesAt: external_exports.number().optional(),
63901
- isMajor: external_exports.boolean().default(false),
63969
+ isSpotlight: external_exports.boolean().default(false),
63902
63970
  maxSlots: external_exports.number().int().min(0).default(0),
63903
63971
  gformsId: external_exports.string().optional(),
63904
63972
  gformsUrl: external_exports.string().url().optional(),
@@ -63907,11 +63975,11 @@ var createEventSchema = external_exports.object({
63907
63975
  contentfulEntryId: external_exports.string().optional(),
63908
63976
  status: external_exports.enum(["draft", "queued", "published"]).default("draft")
63909
63977
  });
63910
- var eventsRoute = new Hono2();
63978
+ var classesRoute = new Hono2();
63911
63979
  function generateSlug(title) {
63912
63980
  return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
63913
63981
  }
63914
- eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
63982
+ classesRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
63915
63983
  const db = createDb(c.env.DB);
63916
63984
  const data = await db.query.events.findMany({
63917
63985
  with: { theme: true, organization: true },
@@ -63919,7 +63987,7 @@ eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
63919
63987
  });
63920
63988
  return c.json({ data });
63921
63989
  });
63922
- eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
63990
+ classesRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
63923
63991
  const body = await c.req.json();
63924
63992
  const db = createDb(c.env.DB);
63925
63993
  const cache3 = new CacheService(c.env.KV);
@@ -63947,7 +64015,7 @@ eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) =>
63947
64015
  ]);
63948
64016
  return c.json({ data: { updated: body.ids.length } });
63949
64017
  });
63950
- eventsRoute.get("/", eventsListRateLimit, async (c) => {
64018
+ classesRoute.get("/", eventsListRateLimit, async (c) => {
63951
64019
  const db = createDb(c.env.DB);
63952
64020
  const cache3 = new CacheService(c.env.KV);
63953
64021
  const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
@@ -63958,7 +64026,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
63958
64026
  );
63959
64027
  const ifNoneMatch = c.req.header("If-None-Match");
63960
64028
  if (ifNoneMatch === etag) {
63961
- return c.newResponse(null, 304);
64029
+ return c.body(null, 304);
63962
64030
  }
63963
64031
  const data = await cache3.getOrSet(
63964
64032
  EVENTS_LIST_KV_KEY,
@@ -63982,7 +64050,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
63982
64050
  startTime: true,
63983
64051
  endTime: true,
63984
64052
  registrationClosesAt: true,
63985
- isMajor: true,
64053
+ isSpotlight: true,
63986
64054
  maxSlots: true,
63987
64055
  registeredSlots: true,
63988
64056
  gformsUrl: true,
@@ -63999,7 +64067,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
63999
64067
  );
64000
64068
  return c.json({ data });
64001
64069
  });
64002
- eventsRoute.get("/:slug", async (c) => {
64070
+ classesRoute.get("/:slug", async (c) => {
64003
64071
  const { slug } = c.req.param();
64004
64072
  const db = createDb(c.env.DB);
64005
64073
  const event = await db.query.events.findFirst({
@@ -64011,7 +64079,7 @@ eventsRoute.get("/:slug", async (c) => {
64011
64079
  if (!event) throw notFound("Event");
64012
64080
  return c.json({ data: event });
64013
64081
  });
64014
- eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
64082
+ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
64015
64083
  const { slug } = c.req.param();
64016
64084
  const db = createDb(c.env.DB);
64017
64085
  const cache3 = new CacheService(c.env.KV);
@@ -64021,7 +64089,7 @@ eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
64021
64089
  c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
64022
64090
  return c.json({ data: info2 });
64023
64091
  });
64024
- eventsRoute.post(
64092
+ classesRoute.post(
64025
64093
  "/",
64026
64094
  authMiddleware,
64027
64095
  adminMiddleware,
@@ -64056,11 +64124,13 @@ eventsRoute.post(
64056
64124
  cache3.del(EVENTS_LIST_KV_KEY),
64057
64125
  cache3.del(EVENTS_ETAG_KV_KEY)
64058
64126
  ]);
64059
- c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
64127
+ if (c.get("cmsMode") === "hybrid") {
64128
+ c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
64129
+ }
64060
64130
  return c.json({ data: created }, 201);
64061
64131
  }
64062
64132
  );
64063
- eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
64133
+ classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
64064
64134
  const { slug } = c.req.param();
64065
64135
  const body = await c.req.json();
64066
64136
  const db = createDb(c.env.DB);
@@ -64075,10 +64145,12 @@ eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
64075
64145
  cache3.del(EVENTS_LIST_KV_KEY),
64076
64146
  cache3.del(EVENTS_ETAG_KV_KEY)
64077
64147
  ]);
64078
- c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
64148
+ if (c.get("cmsMode") === "hybrid") {
64149
+ c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
64150
+ }
64079
64151
  return c.json({ data: updated });
64080
64152
  });
64081
- eventsRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
64153
+ classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
64082
64154
  const { slug } = c.req.param();
64083
64155
  const db = createDb(c.env.DB);
64084
64156
  const cache3 = new CacheService(c.env.KV);
@@ -64177,7 +64249,447 @@ usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
64177
64249
  return c.json({ data: { bookmarked: false } });
64178
64250
  });
64179
64251
 
64252
+ // src/services/contentful.ts
64253
+ var CONTENTFUL_CDN = "https://cdn.contentful.com";
64254
+ var ContentfulService = class _ContentfulService {
64255
+ spaceId;
64256
+ accessToken;
64257
+ environment;
64258
+ constructor(spaceId, accessToken, environment = "master") {
64259
+ this.spaceId = spaceId;
64260
+ this.accessToken = accessToken;
64261
+ this.environment = environment;
64262
+ }
64263
+ /**
64264
+ * Returns true if the required Contentful credentials are configured.
64265
+ */
64266
+ static isConfigured(spaceId, accessToken) {
64267
+ return !!(spaceId && accessToken);
64268
+ }
64269
+ // ─── Entries ─────────────────────────────────────────────────────────────
64270
+ /**
64271
+ * Fetch all entries of a given content type.
64272
+ * Handles pagination automatically (100 per page, Contentful max).
64273
+ */
64274
+ async getEntries(contentTypeId) {
64275
+ const allItems = [];
64276
+ let skip = 0;
64277
+ const limit = 100;
64278
+ do {
64279
+ const url2 = this.buildUrl(`/entries`, {
64280
+ content_type: contentTypeId,
64281
+ skip: String(skip),
64282
+ limit: String(limit),
64283
+ include: "2"
64284
+ // resolve up to 2 levels of linked entries/assets
64285
+ });
64286
+ const res = await fetch(url2, { headers: this.headers() });
64287
+ if (!res.ok) {
64288
+ throw new Error(`Contentful entries error: ${res.status} ${await res.text()}`);
64289
+ }
64290
+ const data = await res.json();
64291
+ allItems.push(...data.items);
64292
+ skip += limit;
64293
+ if (allItems.length >= data.total) break;
64294
+ } while (true);
64295
+ return allItems;
64296
+ }
64297
+ /**
64298
+ * Fetch all assets. Handles pagination.
64299
+ */
64300
+ async getAssets() {
64301
+ const allItems = [];
64302
+ let skip = 0;
64303
+ const limit = 100;
64304
+ do {
64305
+ const url2 = this.buildUrl(`/assets`, {
64306
+ skip: String(skip),
64307
+ limit: String(limit)
64308
+ });
64309
+ const res = await fetch(url2, { headers: this.headers() });
64310
+ if (!res.ok) {
64311
+ throw new Error(`Contentful assets error: ${res.status} ${await res.text()}`);
64312
+ }
64313
+ const data = await res.json();
64314
+ allItems.push(...data.items);
64315
+ skip += limit;
64316
+ if (allItems.length >= data.total) break;
64317
+ } while (true);
64318
+ return allItems;
64319
+ }
64320
+ // ─── Asset file download ─────────────────────────────────────────────────
64321
+ /**
64322
+ * Download an asset file from Contentful's CDN.
64323
+ * Returns the raw ArrayBuffer and content type.
64324
+ */
64325
+ async downloadAsset(assetUrl) {
64326
+ const url2 = assetUrl.startsWith("//") ? `https:${assetUrl}` : assetUrl;
64327
+ const res = await fetch(url2);
64328
+ if (!res.ok) {
64329
+ throw new Error(`Failed to download asset: ${res.status}`);
64330
+ }
64331
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
64332
+ const data = await res.arrayBuffer();
64333
+ return { data, contentType };
64334
+ }
64335
+ // ─── Helpers ─────────────────────────────────────────────────────────────
64336
+ /**
64337
+ * Extract a field value from a Contentful entry, handling locale wrapping.
64338
+ * Contentful fields are often `{ "en-US": value }` — this unwraps them.
64339
+ */
64340
+ static getField(entry, fieldName) {
64341
+ const raw2 = entry.fields[fieldName];
64342
+ if (raw2 === void 0 || raw2 === null) return void 0;
64343
+ if (typeof raw2 === "object" && !Array.isArray(raw2) && "en-US" in raw2) {
64344
+ return raw2["en-US"];
64345
+ }
64346
+ return raw2;
64347
+ }
64348
+ /**
64349
+ * Extract a linked entry/sys reference ID from a reference field.
64350
+ */
64351
+ static getRefId(entry, fieldName) {
64352
+ const ref = _ContentfulService.getField(entry, fieldName);
64353
+ return ref?.sys?.id;
64354
+ }
64355
+ /**
64356
+ * Extract an asset URL from a linked asset field.
64357
+ */
64358
+ static getAssetUrl(entry, fieldName) {
64359
+ const asset = _ContentfulService.getField(entry, fieldName);
64360
+ return asset?.sys?.id;
64361
+ }
64362
+ /**
64363
+ * Resolve an asset URL by ID from a list of fetched assets.
64364
+ */
64365
+ static resolveAssetUrl(assets, assetId) {
64366
+ const asset = assets.find((a) => a.sys.id === assetId);
64367
+ return asset?.fields?.file?.url;
64368
+ }
64369
+ // ─── Private ─────────────────────────────────────────────────────────────
64370
+ headers() {
64371
+ return {
64372
+ Authorization: `Bearer ${this.accessToken}`,
64373
+ "Content-Type": "application/json"
64374
+ };
64375
+ }
64376
+ buildUrl(path, params = {}) {
64377
+ const url2 = new URL(
64378
+ `/spaces/${this.spaceId}/environments/${this.environment}${path}`,
64379
+ CONTENTFUL_CDN
64380
+ );
64381
+ for (const [key, value] of Object.entries(params)) {
64382
+ url2.searchParams.set(key, value);
64383
+ }
64384
+ return url2.toString();
64385
+ }
64386
+ };
64387
+
64180
64388
  // src/services/snapshot.ts
64389
+ var DEFAULT_FIELDS = {
64390
+ event: {
64391
+ title: "title",
64392
+ slug: "slug",
64393
+ theme: "theme",
64394
+ organization: "organization",
64395
+ venue: "venue",
64396
+ date: "date",
64397
+ startTime: "startTime",
64398
+ endTime: "endTime",
64399
+ price: "price",
64400
+ image: "image",
64401
+ isSpotlight: "isSpotlight",
64402
+ maxSlots: "maxSlots",
64403
+ gformsUrl: "gformsUrl",
64404
+ gformsEditorUrl: "gformsEditorUrl",
64405
+ registrationClosesAt: "registrationClosesAt",
64406
+ classCode: "classCode"
64407
+ },
64408
+ theme: {
64409
+ name: "name",
64410
+ path: "path"
64411
+ },
64412
+ faq: {
64413
+ question: "question",
64414
+ answer: "answer",
64415
+ category: "category",
64416
+ sortOrder: "sortOrder"
64417
+ },
64418
+ organization: {
64419
+ name: "name",
64420
+ acronym: "acronym",
64421
+ logoUrl: "logoUrl",
64422
+ link: "link"
64423
+ },
64424
+ siteConfig: {
64425
+ key: "key",
64426
+ value: "value"
64427
+ }
64428
+ };
64429
+ var CONTENTFUL_CACHE_PREFIX = "contentful:cache";
64430
+ var CONTENTFUL_CACHE_TTL = 300;
64431
+ async function snapshotAllContent(db, bucket, contentful, config3 = {}, kv) {
64432
+ const mergedConfig = {
64433
+ eventTypeId: config3.eventTypeId ?? "event",
64434
+ themeTypeId: config3.themeTypeId ?? "theme",
64435
+ faqTypeId: config3.faqTypeId ?? "faq",
64436
+ organizationTypeId: config3.organizationTypeId ?? "organization",
64437
+ fields: {
64438
+ event: { ...DEFAULT_FIELDS.event, ...config3.fields?.event },
64439
+ theme: { ...DEFAULT_FIELDS.theme, ...config3.fields?.theme },
64440
+ faq: { ...DEFAULT_FIELDS.faq, ...config3.fields?.faq },
64441
+ organization: { ...DEFAULT_FIELDS.organization, ...config3.fields?.organization },
64442
+ siteConfig: { ...DEFAULT_FIELDS.siteConfig, ...config3.fields?.siteConfig }
64443
+ }
64444
+ };
64445
+ const result = {
64446
+ themesSynced: 0,
64447
+ eventsSynced: 0,
64448
+ faqsSynced: 0,
64449
+ organizationsSynced: 0,
64450
+ imagesUploaded: 0,
64451
+ imagesSkipped: 0,
64452
+ errors: []
64453
+ };
64454
+ let allAssets = [];
64455
+ if (bucket) {
64456
+ try {
64457
+ allAssets = await contentful.getAssets();
64458
+ } catch (err) {
64459
+ result.errors.push(`Failed to fetch assets: ${err}`);
64460
+ }
64461
+ }
64462
+ try {
64463
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:themes`;
64464
+ let themeEntries;
64465
+ if (kv) {
64466
+ const cached2 = await kv.get(cacheKey, "json");
64467
+ if (cached2) {
64468
+ themeEntries = cached2;
64469
+ } else {
64470
+ themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
64471
+ await kv.put(cacheKey, JSON.stringify(themeEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
64472
+ }
64473
+ } else {
64474
+ themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
64475
+ }
64476
+ result.themesSynced = await syncThemes(db, themeEntries, mergedConfig);
64477
+ } catch (err) {
64478
+ result.errors.push(`Themes sync failed: ${err}`);
64479
+ }
64480
+ try {
64481
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:organizations`;
64482
+ let orgEntries;
64483
+ if (kv) {
64484
+ const cached2 = await kv.get(cacheKey, "json");
64485
+ if (cached2) {
64486
+ orgEntries = cached2;
64487
+ } else {
64488
+ orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
64489
+ await kv.put(cacheKey, JSON.stringify(orgEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
64490
+ }
64491
+ } else {
64492
+ orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
64493
+ }
64494
+ await syncOrganizations(db, orgEntries, mergedConfig);
64495
+ } catch (err) {
64496
+ result.errors.push(`Organizations sync failed: ${err}`);
64497
+ }
64498
+ try {
64499
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:events`;
64500
+ let eventEntries;
64501
+ if (kv) {
64502
+ const cached2 = await kv.get(cacheKey, "json");
64503
+ if (cached2) {
64504
+ eventEntries = cached2;
64505
+ } else {
64506
+ eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
64507
+ await kv.put(cacheKey, JSON.stringify(eventEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
64508
+ }
64509
+ } else {
64510
+ eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
64511
+ }
64512
+ result.eventsSynced = await syncEvents(db, bucket, contentful, allAssets, eventEntries, mergedConfig, result);
64513
+ } catch (err) {
64514
+ result.errors.push(`Events sync failed: ${err}`);
64515
+ }
64516
+ try {
64517
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:faqs`;
64518
+ let faqEntries;
64519
+ if (kv) {
64520
+ const cached2 = await kv.get(cacheKey, "json");
64521
+ if (cached2) {
64522
+ faqEntries = cached2;
64523
+ } else {
64524
+ faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
64525
+ await kv.put(cacheKey, JSON.stringify(faqEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
64526
+ }
64527
+ } else {
64528
+ faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
64529
+ }
64530
+ result.faqsSynced = await syncFaqs(db, faqEntries, mergedConfig);
64531
+ } catch (err) {
64532
+ result.errors.push(`FAQs sync failed: ${err}`);
64533
+ }
64534
+ console.log(
64535
+ `[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`
64536
+ );
64537
+ return result;
64538
+ }
64539
+ async function syncThemes(db, entries, config3) {
64540
+ let count2 = 0;
64541
+ for (const entry of entries) {
64542
+ const cfId = entry.sys.id;
64543
+ const name = ContentfulService.getField(entry, config3.fields.theme.name);
64544
+ const path = ContentfulService.getField(entry, config3.fields.theme.path);
64545
+ if (!name || !path) continue;
64546
+ await db.insert(themes).values({ id: cfId, name, path }).onConflictDoUpdate({
64547
+ target: themes.id,
64548
+ set: { name, path }
64549
+ });
64550
+ count2++;
64551
+ }
64552
+ return count2;
64553
+ }
64554
+ async function syncOrganizations(db, entries, config3) {
64555
+ let count2 = 0;
64556
+ for (const entry of entries) {
64557
+ const cfId = entry.sys.id;
64558
+ const f = config3.fields.organization;
64559
+ const name = ContentfulService.getField(entry, f.name);
64560
+ const acronym = ContentfulService.getField(entry, f.acronym);
64561
+ if (!name || !acronym) continue;
64562
+ const logoUrl = ContentfulService.getField(entry, f.logoUrl) ?? null;
64563
+ const link = ContentfulService.getField(entry, f.link) ?? null;
64564
+ await db.insert(organizations).values({ id: cfId, name, acronym, logoUrl, link }).onConflictDoUpdate({
64565
+ target: organizations.id,
64566
+ set: { name, acronym, logoUrl, link }
64567
+ });
64568
+ count2++;
64569
+ }
64570
+ return count2;
64571
+ }
64572
+ async function syncEvents(db, bucket, contentful, allAssets, entries, config3, result) {
64573
+ let count2 = 0;
64574
+ for (const entry of entries) {
64575
+ try {
64576
+ const cfId = entry.sys.id;
64577
+ const f = config3.fields.event;
64578
+ const title = ContentfulService.getField(entry, f.title);
64579
+ if (!title) {
64580
+ result.errors.push(`Event ${cfId}: missing title, skipping`);
64581
+ continue;
64582
+ }
64583
+ const slug = ContentfulService.getField(entry, f.slug) ?? slugify2(title);
64584
+ const themeRef = ContentfulService.getField(entry, f.theme);
64585
+ const themeId = themeRef?.sys?.id ?? null;
64586
+ const orgRef = ContentfulService.getField(entry, f.organization);
64587
+ const organizationId = orgRef?.sys?.id ?? null;
64588
+ let backgroundImageUrl = null;
64589
+ if (bucket) {
64590
+ const imageRef = ContentfulService.getField(entry, f.image);
64591
+ const assetId = imageRef?.sys?.id;
64592
+ if (assetId) {
64593
+ const assetUrl = ContentfulService.resolveAssetUrl(allAssets, assetId);
64594
+ if (assetUrl) {
64595
+ const r2Key = `contentful/${assetId}`;
64596
+ const uploaded = await uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key);
64597
+ if (uploaded.skipped) {
64598
+ result.imagesSkipped++;
64599
+ } else {
64600
+ result.imagesUploaded++;
64601
+ }
64602
+ backgroundImageUrl = `/uploads/images/${r2Key}`;
64603
+ }
64604
+ }
64605
+ }
64606
+ const values = {
64607
+ id: cfId,
64608
+ contentfulEntryId: cfId,
64609
+ title,
64610
+ slug,
64611
+ themeId,
64612
+ organizationId,
64613
+ updatedAt: entry.sys.updatedAt
64614
+ };
64615
+ const venue = ContentfulService.getField(entry, f.venue);
64616
+ if (venue !== void 0) values.venue = venue;
64617
+ const date5 = ContentfulService.getField(entry, f.date);
64618
+ if (date5 !== void 0) values.dateTime = date5;
64619
+ const price = ContentfulService.getField(entry, f.price);
64620
+ if (price !== void 0) values.price = price;
64621
+ if (backgroundImageUrl) values.backgroundImageUrl = backgroundImageUrl;
64622
+ const isSpotlight = ContentfulService.getField(entry, f.isSpotlight);
64623
+ if (isSpotlight !== void 0) values.isSpotlight = isSpotlight;
64624
+ const maxSlots = ContentfulService.getField(entry, f.maxSlots);
64625
+ if (maxSlots !== void 0) values.maxSlots = maxSlots;
64626
+ const gformsUrl = ContentfulService.getField(entry, f.gformsUrl);
64627
+ if (gformsUrl !== void 0) values.gformsUrl = gformsUrl;
64628
+ const gformsEditorUrl = ContentfulService.getField(entry, f.gformsEditorUrl);
64629
+ if (gformsEditorUrl !== void 0) values.gformsEditorUrl = gformsEditorUrl;
64630
+ const classCode = ContentfulService.getField(entry, f.classCode);
64631
+ if (classCode !== void 0) values.classCode = classCode;
64632
+ const startTime = ContentfulService.getField(entry, f.startTime);
64633
+ if (startTime !== void 0) values.startTime = startTime;
64634
+ const endTime = ContentfulService.getField(entry, f.endTime);
64635
+ if (endTime !== void 0) values.endTime = endTime;
64636
+ const regCloseRaw = ContentfulService.getField(entry, f.registrationClosesAt);
64637
+ if (regCloseRaw) {
64638
+ const ms = Date.parse(regCloseRaw);
64639
+ if (!Number.isNaN(ms)) values.registrationClosesAt = Math.floor(ms / 1e3);
64640
+ }
64641
+ await db.insert(events).values(values).onConflictDoUpdate({
64642
+ target: events.id,
64643
+ set: values
64644
+ });
64645
+ count2++;
64646
+ } catch (err) {
64647
+ result.errors.push(`Event ${entry.sys.id}: ${err}`);
64648
+ }
64649
+ }
64650
+ return count2;
64651
+ }
64652
+ async function syncFaqs(db, entries, config3) {
64653
+ let count2 = 0;
64654
+ for (const entry of entries) {
64655
+ const cfId = entry.sys.id;
64656
+ const f = config3.fields.faq;
64657
+ const question = ContentfulService.getField(entry, f.question);
64658
+ const answer = ContentfulService.getField(entry, f.answer);
64659
+ if (!question || !answer) continue;
64660
+ const category = ContentfulService.getField(entry, f.category) ?? null;
64661
+ const sortOrder = ContentfulService.getField(entry, f.sortOrder) ?? 0;
64662
+ await db.insert(faqs).values({
64663
+ id: cfId,
64664
+ question,
64665
+ answer,
64666
+ category,
64667
+ sortOrder
64668
+ }).onConflictDoUpdate({
64669
+ target: faqs.id,
64670
+ set: { question, answer, category, sortOrder }
64671
+ });
64672
+ count2++;
64673
+ }
64674
+ return count2;
64675
+ }
64676
+ async function uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key) {
64677
+ const { data, contentType } = await contentful.downloadAsset(assetUrl);
64678
+ const sha = await computeSha256(data);
64679
+ const existing = await bucket.head(r2Key);
64680
+ if (existing?.customMetadata?.sha256 === sha) {
64681
+ return { skipped: true };
64682
+ }
64683
+ await bucket.put(r2Key, data, {
64684
+ httpMetadata: { contentType },
64685
+ customMetadata: {
64686
+ sha256: sha,
64687
+ source: "contentful",
64688
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
64689
+ }
64690
+ });
64691
+ return { skipped: false };
64692
+ }
64181
64693
  async function batchRun(items, fn, concurrency = 5) {
64182
64694
  const results = [];
64183
64695
  for (let i = 0; i < items.length; i += concurrency) {
@@ -64187,6 +64699,13 @@ async function batchRun(items, fn, concurrency = 5) {
64187
64699
  }
64188
64700
  return results;
64189
64701
  }
64702
+ async function computeSha256(data) {
64703
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
64704
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
64705
+ }
64706
+ function slugify2(text2) {
64707
+ return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
64708
+ }
64190
64709
  async function ensureContentTypes(mgmt, config3 = {}) {
64191
64710
  const eventTypeId = config3.eventTypeId ?? "event";
64192
64711
  const themeTypeId = config3.themeTypeId ?? "theme";
@@ -64207,7 +64726,7 @@ async function ensureContentTypes(mgmt, config3 = {}) {
64207
64726
  { id: "endTime", name: "End Time", type: "Symbol" },
64208
64727
  { id: "price", name: "Price", type: "Symbol" },
64209
64728
  { id: "image", name: "Image", type: "Link", linkType: "Asset" },
64210
- { id: "isMajor", name: "Major Event", type: "Boolean" },
64729
+ { id: "isSpotlight", name: "Spotlight", type: "Boolean" },
64211
64730
  { id: "maxSlots", name: "Max Slots", type: "Integer" },
64212
64731
  { id: "gformsUrl", name: "Google Forms URL", type: "Symbol" },
64213
64732
  { id: "gformsEditorUrl", name: "Google Forms Editor URL", type: "Symbol" },
@@ -64292,7 +64811,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
64292
64811
  const fields = {
64293
64812
  title: ContentfulManagement.locale(event.title),
64294
64813
  slug: ContentfulManagement.locale(event.slug),
64295
- isMajor: ContentfulManagement.locale(event.isMajor),
64814
+ isSpotlight: ContentfulManagement.locale(event.isSpotlight),
64296
64815
  maxSlots: ContentfulManagement.locale(event.maxSlots)
64297
64816
  };
64298
64817
  if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
@@ -64363,6 +64882,9 @@ siteConfigRoute.get("/", async (c) => {
64363
64882
  siteName: config3.site_name ?? null,
64364
64883
  registrationGloballyOpen: config3.registration_globally_open ?? true,
64365
64884
  maintenanceMode: config3.maintenance_mode ?? false,
64885
+ // Prefer the D1-persisted value over the env-var default so that
64886
+ // PATCH /config/cms_mode changes are reflected immediately.
64887
+ cmsMode: config3.cms_mode ?? c.get("cmsMode"),
64366
64888
  now: Math.floor(Date.now() / 1e3)
64367
64889
  }
64368
64890
  });
@@ -64384,6 +64906,10 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
64384
64906
  var SYNC_LOCK_KEY = "contentful:sync:lock";
64385
64907
  var SYNC_LOCK_TTL = 60;
64386
64908
  siteConfigRoute.post("/sync-content", authMiddleware, adminMiddleware, async (c) => {
64909
+ const cmsMode = c.get("cmsMode");
64910
+ if (cmsMode === "cloudflare") {
64911
+ throw serviceUnavailable("Contentful sync is not available in Cloudflare-only mode.");
64912
+ }
64387
64913
  if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
64388
64914
  throw serviceUnavailable("Contentful Management API credentials not configured.");
64389
64915
  }
@@ -64470,7 +64996,9 @@ faqsRoute.post(
64470
64996
  const cache3 = new CacheService(c.env.KV);
64471
64997
  const [created] = await db.insert(faqs).values(body).returning();
64472
64998
  await cache3.del(FAQS_KV_KEY);
64473
- c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
64999
+ if (c.get("cmsMode") === "hybrid") {
65000
+ c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
65001
+ }
64474
65002
  return c.json({ data: created }, 201);
64475
65003
  }
64476
65004
  );
@@ -64483,7 +65011,9 @@ faqsRoute.patch("/:id", authMiddleware, adminMiddleware, async (c) => {
64483
65011
  const [updated] = await db.update(faqs).set({ ...body, updatedAt: now2 }).where(eq(faqs.id, id)).returning();
64484
65012
  if (!updated) throw notFound("FAQ");
64485
65013
  await cache3.del(FAQS_KV_KEY);
64486
- c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
65014
+ if (c.get("cmsMode") === "hybrid") {
65015
+ c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
65016
+ }
64487
65017
  return c.json({ data: updated });
64488
65018
  });
64489
65019
  faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
@@ -64759,7 +65289,9 @@ themesRoute.post(
64759
65289
  const db = createDb(c.env.DB);
64760
65290
  try {
64761
65291
  const [created] = await db.insert(themes).values(body).returning();
64762
- c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
65292
+ if (c.get("cmsMode") === "hybrid") {
65293
+ c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
65294
+ }
64763
65295
  return c.json({ data: created }, 201);
64764
65296
  } catch (err) {
64765
65297
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -64780,7 +65312,9 @@ themesRoute.patch(
64780
65312
  try {
64781
65313
  const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
64782
65314
  if (!updated) throw notFound("Theme");
64783
- c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
65315
+ if (c.get("cmsMode") === "hybrid") {
65316
+ c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
65317
+ }
64784
65318
  return c.json({ data: updated });
64785
65319
  } catch (err) {
64786
65320
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -64795,7 +65329,9 @@ themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
64795
65329
  const db = createDb(c.env.DB);
64796
65330
  const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
64797
65331
  if (!deleted) throw notFound("Theme");
64798
- c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
65332
+ if (c.get("cmsMode") === "hybrid") {
65333
+ c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
65334
+ }
64799
65335
  return c.body(null, 204);
64800
65336
  });
64801
65337
 
@@ -64859,6 +65395,83 @@ organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) =>
64859
65395
  return c.body(null, 204);
64860
65396
  });
64861
65397
 
65398
+ // src/routes/contentful-sync.ts
65399
+ var contentfulSyncRoute = new Hono2();
65400
+ contentfulSyncRoute.post(
65401
+ "/trigger",
65402
+ authMiddleware,
65403
+ adminMiddleware,
65404
+ async (c) => {
65405
+ const env2 = c.env;
65406
+ const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
65407
+ if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_ACCESS_TOKEN) {
65408
+ return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful credentials missing" } }, 400);
65409
+ }
65410
+ const db = createDb(env2.DB);
65411
+ const contentful = new ContentfulService(CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_ENVIRONMENT);
65412
+ try {
65413
+ if (CONTENTFUL_MANAGEMENT_TOKEN) {
65414
+ const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
65415
+ await ensureContentTypes(mgmt);
65416
+ }
65417
+ c.executionCtx.waitUntil(
65418
+ 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))
65419
+ );
65420
+ return c.json({ message: "Contentful sync triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
65421
+ } catch (err) {
65422
+ console.error("[Contentful] Sync trigger failed:", err);
65423
+ return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger sync" } }, 500);
65424
+ }
65425
+ }
65426
+ );
65427
+ contentfulSyncRoute.post(
65428
+ "/push",
65429
+ authMiddleware,
65430
+ adminMiddleware,
65431
+ async (c) => {
65432
+ const env2 = c.env;
65433
+ const { CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
65434
+ if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_MANAGEMENT_TOKEN) {
65435
+ return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful management credentials missing" } }, 400);
65436
+ }
65437
+ const db = createDb(env2.DB);
65438
+ const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
65439
+ try {
65440
+ c.executionCtx.waitUntil(
65441
+ pushToContentful(db, mgmt, {}, env2.KV).then((r) => console.log("[Contentful] Push complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Push failed:", err))
65442
+ );
65443
+ return c.json({ message: "Contentful push triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
65444
+ } catch (err) {
65445
+ console.error("[Contentful] Push trigger failed:", err);
65446
+ return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger push" } }, 500);
65447
+ }
65448
+ }
65449
+ );
65450
+ contentfulSyncRoute.get(
65451
+ "/status",
65452
+ authMiddleware,
65453
+ adminMiddleware,
65454
+ async (c) => {
65455
+ const env2 = c.env;
65456
+ const db = createDb(env2.DB);
65457
+ const [[{ themes: themes2 }], [{ events: events2 }], [{ faqs: faqs2 }], [{ orgs }]] = await Promise.all([
65458
+ db.all(sql2`SELECT count(*) as themes FROM themes`),
65459
+ db.all(sql2`SELECT count(*) as events FROM events`),
65460
+ db.all(sql2`SELECT count(*) as faqs FROM faqs`),
65461
+ db.all(sql2`SELECT count(*) as orgs FROM organizations`)
65462
+ ]);
65463
+ const isConfigured = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_ACCESS_TOKEN;
65464
+ const canPush = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_MANAGEMENT_TOKEN;
65465
+ return c.json({
65466
+ isConfigured,
65467
+ canPush,
65468
+ cmsMode: c.get("cmsMode"),
65469
+ spaceId: env2.CONTENTFUL_SPACE_ID ?? null,
65470
+ totals: { themes: themes2, events: events2, faqs: faqs2, organizations: orgs }
65471
+ });
65472
+ }
65473
+ );
65474
+
64862
65475
  // src/app.ts
64863
65476
  function createApp(options = {}) {
64864
65477
  const app2 = new Hono2();
@@ -64872,6 +65485,12 @@ function createApp(options = {}) {
64872
65485
  app2.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
64873
65486
  app2.use("*", createPowChallengeMiddleware());
64874
65487
  app2.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
65488
+ app2.use("*", async (c, next) => {
65489
+ const overrideRaw = await c.env.KV.get("config:cms_mode").catch(() => null);
65490
+ const override = overrideRaw ? JSON.parse(overrideRaw) : null;
65491
+ c.set("cmsMode", parseCmsMode(override ?? c.env.CMS_MODE));
65492
+ return next();
65493
+ });
64875
65494
  app2.on(["POST", "GET"], "/api/auth/*", (c) => {
64876
65495
  const auth = createAuth(c.env);
64877
65496
  const req = c.req.raw;
@@ -64883,7 +65502,7 @@ function createApp(options = {}) {
64883
65502
  return auth.handler(new Request(req.url, {
64884
65503
  method: req.method,
64885
65504
  headers: newHeaders,
64886
- body: req.method === "GET" ? null : req.body,
65505
+ body: req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS" ? null : req.body,
64887
65506
  redirect: req.redirect
64888
65507
  }));
64889
65508
  }
@@ -64904,12 +65523,13 @@ function createApp(options = {}) {
64904
65523
  app2.post(POW_VERIFY_PATH, handlePowVerify);
64905
65524
  app2.route("/health", healthRoute);
64906
65525
  app2.route("/api/config", siteConfigRoute);
64907
- app2.route("/api/events", eventsRoute);
65526
+ app2.route("/api/classes", classesRoute);
64908
65527
  app2.route("/api/themes", themesRoute);
64909
65528
  app2.route("/api/users", usersRoute);
64910
65529
  app2.route("/api/organizations", organizationsRoute);
64911
65530
  app2.route("/api/faqs", faqsRoute);
64912
65531
  app2.route("/api/uploads", uploadsRoute);
65532
+ app2.route("/api/contentful", contentfulSyncRoute);
64913
65533
  app2.route("/internal/gforms-webhook", gformsWebhookRoute);
64914
65534
  app2.onError(errorHandler2);
64915
65535
  app2.notFound(
@@ -65543,6 +66163,7 @@ var PATCH_STATEMENTS = [
65543
66163
  `ALTER TABLE "events" ADD COLUMN "class_code" text`,
65544
66164
  `ALTER TABLE "events" ADD COLUMN "start_time" text`,
65545
66165
  `ALTER TABLE "events" ADD COLUMN "end_time" text`,
66166
+ `ALTER TABLE "events" RENAME COLUMN "is_major" TO "is_spotlight"`,
65546
66167
  `CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`
65547
66168
  ];
65548
66169
  var CREATE_STATEMENTS = [
@@ -65646,7 +66267,7 @@ var CREATE_STATEMENTS = [
65646
66267
  "class_code" text,
65647
66268
  "start_time" text,
65648
66269
  "end_time" text,
65649
- "is_major" integer DEFAULT false NOT NULL,
66270
+ "is_spotlight" integer DEFAULT false NOT NULL,
65650
66271
  "max_slots" integer DEFAULT 0 NOT NULL,
65651
66272
  "registered_slots" integer DEFAULT 0 NOT NULL,
65652
66273
  "gforms_id" text,
@@ -65708,7 +66329,9 @@ async function ensureDatabase(d1) {
65708
66329
  try {
65709
66330
  await d1.prepare(sql3).run();
65710
66331
  } catch (err) {
65711
- if (err?.message?.includes("duplicate column")) continue;
66332
+ if (err?.message?.includes("duplicate column") || err?.message?.includes("no such column") && err?.message?.includes("is_major")) {
66333
+ continue;
66334
+ }
65712
66335
  throw err;
65713
66336
  }
65714
66337
  }