@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.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  var chunkOK6RVPEH_cjs = require('./chunk-OK6RVPEH.cjs');
4
4
  var chunkRFP2X2FA_cjs = require('./chunk-RFP2X2FA.cjs');
5
5
  require('./chunk-JPVIXCF5.cjs');
6
- var chunkQARF2YFF_cjs = require('./chunk-QARF2YFF.cjs');
6
+ var chunkLVKPYSXI_cjs = require('./chunk-LVKPYSXI.cjs');
7
7
  var chunk5JKLV7IE_cjs = require('./chunk-5JKLV7IE.cjs');
8
8
  require('./chunk-NKIQRCOM.cjs');
9
9
  var chunk4DPT2KQR_cjs = require('./chunk-4DPT2KQR.cjs');
@@ -50,13 +50,28 @@ var errorHandler = (err, c) => {
50
50
  );
51
51
  };
52
52
 
53
+ // src/types.ts
54
+ function parseCmsMode(raw) {
55
+ if (raw === "cloudflare" || raw === "contentful") return raw;
56
+ return "hybrid";
57
+ }
58
+ function shouldPushToContentful(mode) {
59
+ return mode === "hybrid";
60
+ }
61
+ function shouldPullFromContentful(mode) {
62
+ return mode === "contentful" || mode === "hybrid";
63
+ }
64
+
53
65
  // node_modules/hono/dist/middleware/cors/index.js
54
66
  var cors = (options) => {
55
- const opts = {
67
+ const defaults = {
56
68
  origin: "*",
57
69
  allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
58
70
  allowHeaders: [],
59
- exposeHeaders: [],
71
+ exposeHeaders: []
72
+ };
73
+ const opts = {
74
+ ...defaults,
60
75
  ...options
61
76
  };
62
77
  const findAllowOrigin = ((optsOrigin) => {
@@ -147,9 +162,22 @@ function createCorsMiddleware(allowedOrigins) {
147
162
  });
148
163
  return async (c, next) => {
149
164
  const origin = c.req.header("origin");
165
+ if (c.req.path.startsWith("/api/uploads/images")) {
166
+ c.header("Access-Control-Allow-Origin", "*");
167
+ c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
168
+ if (c.req.method === "OPTIONS") {
169
+ return c.body(null, 204);
170
+ }
171
+ return next();
172
+ }
150
173
  if (!c.req.path.startsWith("/health") && !c.req.path.startsWith("/api/auth") && !c.req.path.startsWith("/internal") && origin && !allowedOrigins.includes("*") && !allowedOrigins.includes(origin)) {
151
174
  return c.json(
152
- { error: { code: "DOMAIN_RESTRICTED", message: `Origin ${origin} is not allowed` } },
175
+ {
176
+ error: {
177
+ code: "DOMAIN_RESTRICTED",
178
+ message: `Origin ${origin} is not allowed`
179
+ }
180
+ },
153
181
  403
154
182
  );
155
183
  }
@@ -161,7 +189,7 @@ function createCorsMiddleware(allowedOrigins) {
161
189
  function createRefererGuard(allowedOrigins) {
162
190
  const MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
163
191
  const SKIP_PREFIXES = ["/health", "/internal", "/api/auth", "/.well-known"];
164
- return chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
192
+ return chunkLVKPYSXI_cjs.createMiddleware(async (c, next) => {
165
193
  if (!MUTATION_METHODS.has(c.req.method)) return next();
166
194
  if (SKIP_PREFIXES.some((p) => c.req.path.startsWith(p))) return next();
167
195
  if (allowedOrigins.includes("*")) return next();
@@ -36332,12 +36360,14 @@ function drizzle(client, config3 = {}) {
36332
36360
  // src/db/schema/index.ts
36333
36361
  var schema_exports = {};
36334
36362
  chunkEMMSS5I5_cjs.__export(schema_exports, {
36363
+ CONTENTFUL_CONFIG_KEYS: () => CONTENTFUL_CONFIG_KEYS,
36335
36364
  authAccount: () => authAccount,
36336
36365
  authSession: () => authSession,
36337
36366
  authUser: () => authUser,
36338
36367
  authVerification: () => authVerification,
36339
36368
  bookmarks: () => bookmarks,
36340
36369
  bookmarksRelations: () => bookmarksRelations,
36370
+ contentfulConfig: () => contentfulConfig,
36341
36371
  events: () => events,
36342
36372
  eventsRelations: () => eventsRelations,
36343
36373
  faqs: () => faqs,
@@ -36389,7 +36419,7 @@ var organizationsRelations = relations(organizations, ({ many }) => ({
36389
36419
  events: many(events)
36390
36420
  }));
36391
36421
 
36392
- // src/db/schema/events.ts
36422
+ // src/db/schema/classes.ts
36393
36423
  var events = sqliteTable(
36394
36424
  "events",
36395
36425
  {
@@ -36414,7 +36444,7 @@ var events = sqliteTable(
36414
36444
  // start time string
36415
36445
  endTime: text("end_time"),
36416
36446
  // end time string
36417
- isMajor: integer2("is_major", { mode: "boolean" }).notNull().default(false),
36447
+ isSpotlight: integer2("is_spotlight", { mode: "boolean" }).notNull().default(false),
36418
36448
  // Slot tracking (local counter — NOT polled from Google Forms)
36419
36449
  maxSlots: integer2("max_slots").notNull().default(0),
36420
36450
  registeredSlots: integer2("registered_slots").notNull().default(0),
@@ -36471,6 +36501,18 @@ var siteConfig = sqliteTable("site_config", {
36471
36501
  // JSON-serializable string
36472
36502
  updatedAt: integer2("updated_at").notNull().default(sql2`(unixepoch())`)
36473
36503
  });
36504
+ var CONTENTFUL_CONFIG_KEYS = {
36505
+ ENABLED: "contentful.enabled",
36506
+ SPACE_ID: "contentful.spaceId",
36507
+ MANAGEMENT_TOKEN: "contentful.managementToken",
36508
+ DEFAULT_SPACE_ID: "dlsu-events"
36509
+ };
36510
+ var contentfulConfig = sqliteTable("contentful_config", {
36511
+ space_id: text("space_id"),
36512
+ contentful_enabled: integer2("contentful_enabled").notNull().default(0),
36513
+ last_sync_at: integer2("last_sync_at"),
36514
+ updated_at: integer2("updated_at").default(sql2`(unixepoch())`).notNull()
36515
+ });
36474
36516
 
36475
36517
  // src/db/schema/faqs.ts
36476
36518
  var faqs = sqliteTable("faqs", {
@@ -36640,11 +36682,21 @@ function createAuth(env2) {
36640
36682
  };
36641
36683
  if (isFirstUser) {
36642
36684
  await db.insert(users).values(base).onConflictDoUpdate({
36643
- target: users.betterAuthId,
36644
- set: { role: "super_admin" }
36685
+ target: users.email,
36686
+ set: {
36687
+ betterAuthId: user.id,
36688
+ role: "super_admin",
36689
+ name: user.name ?? user.email.split("@")[0]
36690
+ }
36645
36691
  });
36646
36692
  } else {
36647
- await db.insert(users).values(base).onConflictDoNothing({ target: users.betterAuthId });
36693
+ await db.insert(users).values(base).onConflictDoUpdate({
36694
+ target: users.email,
36695
+ set: {
36696
+ betterAuthId: user.id,
36697
+ name: user.name ?? user.email.split("@")[0]
36698
+ }
36699
+ });
36648
36700
  }
36649
36701
  }
36650
36702
  }
@@ -36673,7 +36725,13 @@ async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAu
36673
36725
  betterAuthId: betterAuthUserId,
36674
36726
  email: betterAuthUserEmail,
36675
36727
  name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
36676
- }).onConflictDoNothing({ target: users.betterAuthId }).returning();
36728
+ }).onConflictDoUpdate({
36729
+ target: users.email,
36730
+ set: {
36731
+ betterAuthId: betterAuthUserId,
36732
+ name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
36733
+ }
36734
+ }).returning();
36677
36735
  dbUser = created;
36678
36736
  }
36679
36737
  if (!dbUser) throw unauthorized("Failed to resolve user record");
@@ -36686,7 +36744,7 @@ async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAu
36686
36744
  emailVerified: betterAuthEmailVerified
36687
36745
  };
36688
36746
  }
36689
- var authMiddleware = chunkQARF2YFF_cjs.createMiddleware(
36747
+ var authMiddleware = chunkLVKPYSXI_cjs.createMiddleware(
36690
36748
  async (c, next) => {
36691
36749
  const rawToken = extractRawToken(c);
36692
36750
  if (rawToken) {
@@ -36728,7 +36786,7 @@ var authMiddleware = chunkQARF2YFF_cjs.createMiddleware(
36728
36786
  return next();
36729
36787
  }
36730
36788
  );
36731
- var optionalAuthMiddleware = chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
36789
+ var optionalAuthMiddleware = chunkLVKPYSXI_cjs.createMiddleware(async (c, next) => {
36732
36790
  const rawToken = extractRawToken(c);
36733
36791
  if (!rawToken) {
36734
36792
  c.set("user", null);
@@ -36736,7 +36794,7 @@ var optionalAuthMiddleware = chunkQARF2YFF_cjs.createMiddleware(async (c, next)
36736
36794
  }
36737
36795
  return authMiddleware(c, next);
36738
36796
  });
36739
- var adminMiddleware = chunkQARF2YFF_cjs.createMiddleware(
36797
+ var adminMiddleware = chunkLVKPYSXI_cjs.createMiddleware(
36740
36798
  async (c, next) => {
36741
36799
  const user = c.get("user");
36742
36800
  if (!user || !["admin", "super_admin"].includes(user.role)) {
@@ -36745,7 +36803,7 @@ var adminMiddleware = chunkQARF2YFF_cjs.createMiddleware(
36745
36803
  return next();
36746
36804
  }
36747
36805
  );
36748
- var internalMiddleware = chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
36806
+ var internalMiddleware = chunkLVKPYSXI_cjs.createMiddleware(async (c, next) => {
36749
36807
  const secret = c.req.header("X-Internal-Secret");
36750
36808
  if (!secret || secret !== c.env.INTERNAL_API_SECRET) {
36751
36809
  throw forbidden("Invalid internal secret");
@@ -36754,7 +36812,7 @@ var internalMiddleware = chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
36754
36812
  });
36755
36813
 
36756
36814
  // src/routes/health.ts
36757
- var healthRoute = new chunkQARF2YFF_cjs.Hono();
36815
+ var healthRoute = new chunkLVKPYSXI_cjs.Hono();
36758
36816
  async function probeResend(apiKey) {
36759
36817
  const start = Date.now();
36760
36818
  try {
@@ -36983,7 +37041,7 @@ var parse4 = (cookie, name) => {
36983
37041
  cookieValue = cookieValue.slice(1, -1);
36984
37042
  }
36985
37043
  if (validCookieValueRegEx.test(cookieValue)) {
36986
- parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? chunkQARF2YFF_cjs.tryDecode(cookieValue, chunkQARF2YFF_cjs.decodeURIComponent_) : cookieValue;
37044
+ parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? chunkLVKPYSXI_cjs.tryDecode(cookieValue, chunkLVKPYSXI_cjs.decodeURIComponent_) : cookieValue;
36987
37045
  }
36988
37046
  }
36989
37047
  return parsedCookie;
@@ -37026,7 +37084,7 @@ var validator = (target, validationFunc) => {
37026
37084
  value = await c.req.json();
37027
37085
  } catch {
37028
37086
  const message2 = "Malformed JSON in request body";
37029
- throw new chunkQARF2YFF_cjs.HTTPException(400, { message: message2 });
37087
+ throw new chunkLVKPYSXI_cjs.HTTPException(400, { message: message2 });
37030
37088
  }
37031
37089
  break;
37032
37090
  case "form": {
@@ -37044,7 +37102,7 @@ var validator = (target, validationFunc) => {
37044
37102
  } catch (e) {
37045
37103
  let message2 = "Malformed FormData request.";
37046
37104
  message2 += e instanceof Error ? ` ${e.message}` : ` ${String(e)}`;
37047
- throw new chunkQARF2YFF_cjs.HTTPException(400, { message: message2 });
37105
+ throw new chunkLVKPYSXI_cjs.HTTPException(400, { message: message2 });
37048
37106
  }
37049
37107
  }
37050
37108
  const form = /* @__PURE__ */ Object.create(null);
@@ -37593,7 +37651,7 @@ var ContentfulManagement = class {
37593
37651
  // src/lib/middleware/rate-limit.ts
37594
37652
  function createRateLimitMiddleware(config3) {
37595
37653
  const { endpoint, limit, windowSec, identifier } = config3;
37596
- return chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
37654
+ return chunkLVKPYSXI_cjs.createMiddleware(async (c, next) => {
37597
37655
  if (c.req.path === "/.well-known/leapify/pow/verify") return next();
37598
37656
  const id = identifier === "uid" ? c.get("user")?.uid ?? c.req.header("CF-Connecting-IP") ?? "unknown" : c.req.header("CF-Connecting-IP") ?? "unknown";
37599
37657
  const key = `rl:${endpoint}:${id}`;
@@ -37640,7 +37698,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
37640
37698
  identifier: "uid"
37641
37699
  });
37642
37700
 
37643
- // src/routes/events.ts
37701
+ // src/routes/classes.ts
37644
37702
  var EVENTS_LIST_KV_KEY = "events:list";
37645
37703
  var EVENTS_ETAG_KV_KEY = "events:etag";
37646
37704
  var EVENTS_LIST_TTL = 300;
@@ -37656,7 +37714,7 @@ async function pushEventToContentful(env2, event) {
37656
37714
  const fields = {
37657
37715
  title: ContentfulManagement.locale(event.title),
37658
37716
  slug: ContentfulManagement.locale(event.slug),
37659
- isMajor: ContentfulManagement.locale(event.isMajor),
37717
+ isSpotlight: ContentfulManagement.locale(event.isSpotlight),
37660
37718
  maxSlots: ContentfulManagement.locale(event.maxSlots)
37661
37719
  };
37662
37720
  if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
@@ -37719,7 +37777,7 @@ var createEventSchema = external_exports.object({
37719
37777
  startTime: external_exports.string().optional(),
37720
37778
  endTime: external_exports.string().optional(),
37721
37779
  registrationClosesAt: external_exports.number().optional(),
37722
- isMajor: external_exports.boolean().default(false),
37780
+ isSpotlight: external_exports.boolean().default(false),
37723
37781
  maxSlots: external_exports.number().int().min(0).default(0),
37724
37782
  gformsId: external_exports.string().optional(),
37725
37783
  gformsUrl: external_exports.string().url().optional(),
@@ -37728,11 +37786,11 @@ var createEventSchema = external_exports.object({
37728
37786
  contentfulEntryId: external_exports.string().optional(),
37729
37787
  status: external_exports.enum(["draft", "queued", "published"]).default("draft")
37730
37788
  });
37731
- var eventsRoute = new chunkQARF2YFF_cjs.Hono();
37789
+ var classesRoute = new chunkLVKPYSXI_cjs.Hono();
37732
37790
  function generateSlug(title) {
37733
37791
  return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
37734
37792
  }
37735
- eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37793
+ classesRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37736
37794
  const db = createDb(c.env.DB);
37737
37795
  const data = await db.query.events.findMany({
37738
37796
  with: { theme: true, organization: true },
@@ -37740,7 +37798,7 @@ eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37740
37798
  });
37741
37799
  return c.json({ data });
37742
37800
  });
37743
- eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
37801
+ classesRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
37744
37802
  const body = await c.req.json();
37745
37803
  const db = createDb(c.env.DB);
37746
37804
  const cache3 = new CacheService(c.env.KV);
@@ -37768,7 +37826,7 @@ eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) =>
37768
37826
  ]);
37769
37827
  return c.json({ data: { updated: body.ids.length } });
37770
37828
  });
37771
- eventsRoute.get("/", eventsListRateLimit, async (c) => {
37829
+ classesRoute.get("/", eventsListRateLimit, async (c) => {
37772
37830
  const db = createDb(c.env.DB);
37773
37831
  const cache3 = new CacheService(c.env.KV);
37774
37832
  const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
@@ -37779,7 +37837,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37779
37837
  );
37780
37838
  const ifNoneMatch = c.req.header("If-None-Match");
37781
37839
  if (ifNoneMatch === etag) {
37782
- return c.newResponse(null, 304);
37840
+ return c.body(null, 304);
37783
37841
  }
37784
37842
  const data = await cache3.getOrSet(
37785
37843
  EVENTS_LIST_KV_KEY,
@@ -37803,7 +37861,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37803
37861
  startTime: true,
37804
37862
  endTime: true,
37805
37863
  registrationClosesAt: true,
37806
- isMajor: true,
37864
+ isSpotlight: true,
37807
37865
  maxSlots: true,
37808
37866
  registeredSlots: true,
37809
37867
  gformsUrl: true,
@@ -37820,7 +37878,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37820
37878
  );
37821
37879
  return c.json({ data });
37822
37880
  });
37823
- eventsRoute.get("/:slug", async (c) => {
37881
+ classesRoute.get("/:slug", async (c) => {
37824
37882
  const { slug } = c.req.param();
37825
37883
  const db = createDb(c.env.DB);
37826
37884
  const event = await db.query.events.findFirst({
@@ -37832,7 +37890,7 @@ eventsRoute.get("/:slug", async (c) => {
37832
37890
  if (!event) throw notFound("Event");
37833
37891
  return c.json({ data: event });
37834
37892
  });
37835
- eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37893
+ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37836
37894
  const { slug } = c.req.param();
37837
37895
  const db = createDb(c.env.DB);
37838
37896
  const cache3 = new CacheService(c.env.KV);
@@ -37842,7 +37900,7 @@ eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37842
37900
  c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
37843
37901
  return c.json({ data: info2 });
37844
37902
  });
37845
- eventsRoute.post(
37903
+ classesRoute.post(
37846
37904
  "/",
37847
37905
  authMiddleware,
37848
37906
  adminMiddleware,
@@ -37877,11 +37935,13 @@ eventsRoute.post(
37877
37935
  cache3.del(EVENTS_LIST_KV_KEY),
37878
37936
  cache3.del(EVENTS_ETAG_KV_KEY)
37879
37937
  ]);
37880
- c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
37938
+ if (c.get("cmsMode") === "hybrid") {
37939
+ c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
37940
+ }
37881
37941
  return c.json({ data: created }, 201);
37882
37942
  }
37883
37943
  );
37884
- eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37944
+ classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37885
37945
  const { slug } = c.req.param();
37886
37946
  const body = await c.req.json();
37887
37947
  const db = createDb(c.env.DB);
@@ -37896,10 +37956,12 @@ eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37896
37956
  cache3.del(EVENTS_LIST_KV_KEY),
37897
37957
  cache3.del(EVENTS_ETAG_KV_KEY)
37898
37958
  ]);
37899
- c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
37959
+ if (c.get("cmsMode") === "hybrid") {
37960
+ c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
37961
+ }
37900
37962
  return c.json({ data: updated });
37901
37963
  });
37902
- eventsRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
37964
+ classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
37903
37965
  const { slug } = c.req.param();
37904
37966
  const db = createDb(c.env.DB);
37905
37967
  const cache3 = new CacheService(c.env.KV);
@@ -37914,7 +37976,7 @@ eventsRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
37914
37976
 
37915
37977
  // src/routes/users.ts
37916
37978
  var VALID_ROLES = ["student", "admin", "super_admin"];
37917
- var usersRoute = new chunkQARF2YFF_cjs.Hono();
37979
+ var usersRoute = new chunkLVKPYSXI_cjs.Hono();
37918
37980
  usersRoute.get("/", authMiddleware, adminMiddleware, async (c) => {
37919
37981
  const db = createDb(c.env.DB);
37920
37982
  const data = await db.select().from(users);
@@ -37998,7 +38060,447 @@ usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
37998
38060
  return c.json({ data: { bookmarked: false } });
37999
38061
  });
38000
38062
 
38063
+ // src/services/contentful.ts
38064
+ var CONTENTFUL_CDN = "https://cdn.contentful.com";
38065
+ var ContentfulService = class _ContentfulService {
38066
+ spaceId;
38067
+ accessToken;
38068
+ environment;
38069
+ constructor(spaceId, accessToken, environment = "master") {
38070
+ this.spaceId = spaceId;
38071
+ this.accessToken = accessToken;
38072
+ this.environment = environment;
38073
+ }
38074
+ /**
38075
+ * Returns true if the required Contentful credentials are configured.
38076
+ */
38077
+ static isConfigured(spaceId, accessToken) {
38078
+ return !!(spaceId && accessToken);
38079
+ }
38080
+ // ─── Entries ─────────────────────────────────────────────────────────────
38081
+ /**
38082
+ * Fetch all entries of a given content type.
38083
+ * Handles pagination automatically (100 per page, Contentful max).
38084
+ */
38085
+ async getEntries(contentTypeId) {
38086
+ const allItems = [];
38087
+ let skip = 0;
38088
+ const limit = 100;
38089
+ do {
38090
+ const url2 = this.buildUrl(`/entries`, {
38091
+ content_type: contentTypeId,
38092
+ skip: String(skip),
38093
+ limit: String(limit),
38094
+ include: "2"
38095
+ // resolve up to 2 levels of linked entries/assets
38096
+ });
38097
+ const res = await fetch(url2, { headers: this.headers() });
38098
+ if (!res.ok) {
38099
+ throw new Error(`Contentful entries error: ${res.status} ${await res.text()}`);
38100
+ }
38101
+ const data = await res.json();
38102
+ allItems.push(...data.items);
38103
+ skip += limit;
38104
+ if (allItems.length >= data.total) break;
38105
+ } while (true);
38106
+ return allItems;
38107
+ }
38108
+ /**
38109
+ * Fetch all assets. Handles pagination.
38110
+ */
38111
+ async getAssets() {
38112
+ const allItems = [];
38113
+ let skip = 0;
38114
+ const limit = 100;
38115
+ do {
38116
+ const url2 = this.buildUrl(`/assets`, {
38117
+ skip: String(skip),
38118
+ limit: String(limit)
38119
+ });
38120
+ const res = await fetch(url2, { headers: this.headers() });
38121
+ if (!res.ok) {
38122
+ throw new Error(`Contentful assets error: ${res.status} ${await res.text()}`);
38123
+ }
38124
+ const data = await res.json();
38125
+ allItems.push(...data.items);
38126
+ skip += limit;
38127
+ if (allItems.length >= data.total) break;
38128
+ } while (true);
38129
+ return allItems;
38130
+ }
38131
+ // ─── Asset file download ─────────────────────────────────────────────────
38132
+ /**
38133
+ * Download an asset file from Contentful's CDN.
38134
+ * Returns the raw ArrayBuffer and content type.
38135
+ */
38136
+ async downloadAsset(assetUrl) {
38137
+ const url2 = assetUrl.startsWith("//") ? `https:${assetUrl}` : assetUrl;
38138
+ const res = await fetch(url2);
38139
+ if (!res.ok) {
38140
+ throw new Error(`Failed to download asset: ${res.status}`);
38141
+ }
38142
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
38143
+ const data = await res.arrayBuffer();
38144
+ return { data, contentType };
38145
+ }
38146
+ // ─── Helpers ─────────────────────────────────────────────────────────────
38147
+ /**
38148
+ * Extract a field value from a Contentful entry, handling locale wrapping.
38149
+ * Contentful fields are often `{ "en-US": value }` — this unwraps them.
38150
+ */
38151
+ static getField(entry, fieldName) {
38152
+ const raw = entry.fields[fieldName];
38153
+ if (raw === void 0 || raw === null) return void 0;
38154
+ if (typeof raw === "object" && !Array.isArray(raw) && "en-US" in raw) {
38155
+ return raw["en-US"];
38156
+ }
38157
+ return raw;
38158
+ }
38159
+ /**
38160
+ * Extract a linked entry/sys reference ID from a reference field.
38161
+ */
38162
+ static getRefId(entry, fieldName) {
38163
+ const ref = _ContentfulService.getField(entry, fieldName);
38164
+ return ref?.sys?.id;
38165
+ }
38166
+ /**
38167
+ * Extract an asset URL from a linked asset field.
38168
+ */
38169
+ static getAssetUrl(entry, fieldName) {
38170
+ const asset = _ContentfulService.getField(entry, fieldName);
38171
+ return asset?.sys?.id;
38172
+ }
38173
+ /**
38174
+ * Resolve an asset URL by ID from a list of fetched assets.
38175
+ */
38176
+ static resolveAssetUrl(assets, assetId) {
38177
+ const asset = assets.find((a) => a.sys.id === assetId);
38178
+ return asset?.fields?.file?.url;
38179
+ }
38180
+ // ─── Private ─────────────────────────────────────────────────────────────
38181
+ headers() {
38182
+ return {
38183
+ Authorization: `Bearer ${this.accessToken}`,
38184
+ "Content-Type": "application/json"
38185
+ };
38186
+ }
38187
+ buildUrl(path, params = {}) {
38188
+ const url2 = new URL(
38189
+ `/spaces/${this.spaceId}/environments/${this.environment}${path}`,
38190
+ CONTENTFUL_CDN
38191
+ );
38192
+ for (const [key, value] of Object.entries(params)) {
38193
+ url2.searchParams.set(key, value);
38194
+ }
38195
+ return url2.toString();
38196
+ }
38197
+ };
38198
+
38001
38199
  // src/services/snapshot.ts
38200
+ var DEFAULT_FIELDS = {
38201
+ event: {
38202
+ title: "title",
38203
+ slug: "slug",
38204
+ theme: "theme",
38205
+ organization: "organization",
38206
+ venue: "venue",
38207
+ date: "date",
38208
+ startTime: "startTime",
38209
+ endTime: "endTime",
38210
+ price: "price",
38211
+ image: "image",
38212
+ isSpotlight: "isSpotlight",
38213
+ maxSlots: "maxSlots",
38214
+ gformsUrl: "gformsUrl",
38215
+ gformsEditorUrl: "gformsEditorUrl",
38216
+ registrationClosesAt: "registrationClosesAt",
38217
+ classCode: "classCode"
38218
+ },
38219
+ theme: {
38220
+ name: "name",
38221
+ path: "path"
38222
+ },
38223
+ faq: {
38224
+ question: "question",
38225
+ answer: "answer",
38226
+ category: "category",
38227
+ sortOrder: "sortOrder"
38228
+ },
38229
+ organization: {
38230
+ name: "name",
38231
+ acronym: "acronym",
38232
+ logoUrl: "logoUrl",
38233
+ link: "link"
38234
+ },
38235
+ siteConfig: {
38236
+ key: "key",
38237
+ value: "value"
38238
+ }
38239
+ };
38240
+ var CONTENTFUL_CACHE_PREFIX = "contentful:cache";
38241
+ var CONTENTFUL_CACHE_TTL = 300;
38242
+ async function snapshotAllContent(db, bucket, contentful, config3 = {}, kv) {
38243
+ const mergedConfig = {
38244
+ eventTypeId: config3.eventTypeId ?? "event",
38245
+ themeTypeId: config3.themeTypeId ?? "theme",
38246
+ faqTypeId: config3.faqTypeId ?? "faq",
38247
+ organizationTypeId: config3.organizationTypeId ?? "organization",
38248
+ fields: {
38249
+ event: { ...DEFAULT_FIELDS.event, ...config3.fields?.event },
38250
+ theme: { ...DEFAULT_FIELDS.theme, ...config3.fields?.theme },
38251
+ faq: { ...DEFAULT_FIELDS.faq, ...config3.fields?.faq },
38252
+ organization: { ...DEFAULT_FIELDS.organization, ...config3.fields?.organization },
38253
+ siteConfig: { ...DEFAULT_FIELDS.siteConfig, ...config3.fields?.siteConfig }
38254
+ }
38255
+ };
38256
+ const result = {
38257
+ themesSynced: 0,
38258
+ eventsSynced: 0,
38259
+ faqsSynced: 0,
38260
+ organizationsSynced: 0,
38261
+ imagesUploaded: 0,
38262
+ imagesSkipped: 0,
38263
+ errors: []
38264
+ };
38265
+ let allAssets = [];
38266
+ if (bucket) {
38267
+ try {
38268
+ allAssets = await contentful.getAssets();
38269
+ } catch (err) {
38270
+ result.errors.push(`Failed to fetch assets: ${err}`);
38271
+ }
38272
+ }
38273
+ try {
38274
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:themes`;
38275
+ let themeEntries;
38276
+ if (kv) {
38277
+ const cached2 = await kv.get(cacheKey, "json");
38278
+ if (cached2) {
38279
+ themeEntries = cached2;
38280
+ } else {
38281
+ themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
38282
+ await kv.put(cacheKey, JSON.stringify(themeEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38283
+ }
38284
+ } else {
38285
+ themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
38286
+ }
38287
+ result.themesSynced = await syncThemes(db, themeEntries, mergedConfig);
38288
+ } catch (err) {
38289
+ result.errors.push(`Themes sync failed: ${err}`);
38290
+ }
38291
+ try {
38292
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:organizations`;
38293
+ let orgEntries;
38294
+ if (kv) {
38295
+ const cached2 = await kv.get(cacheKey, "json");
38296
+ if (cached2) {
38297
+ orgEntries = cached2;
38298
+ } else {
38299
+ orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
38300
+ await kv.put(cacheKey, JSON.stringify(orgEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38301
+ }
38302
+ } else {
38303
+ orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
38304
+ }
38305
+ await syncOrganizations(db, orgEntries, mergedConfig);
38306
+ } catch (err) {
38307
+ result.errors.push(`Organizations sync failed: ${err}`);
38308
+ }
38309
+ try {
38310
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:events`;
38311
+ let eventEntries;
38312
+ if (kv) {
38313
+ const cached2 = await kv.get(cacheKey, "json");
38314
+ if (cached2) {
38315
+ eventEntries = cached2;
38316
+ } else {
38317
+ eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
38318
+ await kv.put(cacheKey, JSON.stringify(eventEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38319
+ }
38320
+ } else {
38321
+ eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
38322
+ }
38323
+ result.eventsSynced = await syncEvents(db, bucket, contentful, allAssets, eventEntries, mergedConfig, result);
38324
+ } catch (err) {
38325
+ result.errors.push(`Events sync failed: ${err}`);
38326
+ }
38327
+ try {
38328
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:faqs`;
38329
+ let faqEntries;
38330
+ if (kv) {
38331
+ const cached2 = await kv.get(cacheKey, "json");
38332
+ if (cached2) {
38333
+ faqEntries = cached2;
38334
+ } else {
38335
+ faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
38336
+ await kv.put(cacheKey, JSON.stringify(faqEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38337
+ }
38338
+ } else {
38339
+ faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
38340
+ }
38341
+ result.faqsSynced = await syncFaqs(db, faqEntries, mergedConfig);
38342
+ } catch (err) {
38343
+ result.errors.push(`FAQs sync failed: ${err}`);
38344
+ }
38345
+ console.log(
38346
+ `[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`
38347
+ );
38348
+ return result;
38349
+ }
38350
+ async function syncThemes(db, entries, config3) {
38351
+ let count2 = 0;
38352
+ for (const entry of entries) {
38353
+ const cfId = entry.sys.id;
38354
+ const name = ContentfulService.getField(entry, config3.fields.theme.name);
38355
+ const path = ContentfulService.getField(entry, config3.fields.theme.path);
38356
+ if (!name || !path) continue;
38357
+ await db.insert(themes).values({ id: cfId, name, path }).onConflictDoUpdate({
38358
+ target: themes.id,
38359
+ set: { name, path }
38360
+ });
38361
+ count2++;
38362
+ }
38363
+ return count2;
38364
+ }
38365
+ async function syncOrganizations(db, entries, config3) {
38366
+ let count2 = 0;
38367
+ for (const entry of entries) {
38368
+ const cfId = entry.sys.id;
38369
+ const f = config3.fields.organization;
38370
+ const name = ContentfulService.getField(entry, f.name);
38371
+ const acronym = ContentfulService.getField(entry, f.acronym);
38372
+ if (!name || !acronym) continue;
38373
+ const logoUrl = ContentfulService.getField(entry, f.logoUrl) ?? null;
38374
+ const link = ContentfulService.getField(entry, f.link) ?? null;
38375
+ await db.insert(organizations).values({ id: cfId, name, acronym, logoUrl, link }).onConflictDoUpdate({
38376
+ target: organizations.id,
38377
+ set: { name, acronym, logoUrl, link }
38378
+ });
38379
+ count2++;
38380
+ }
38381
+ return count2;
38382
+ }
38383
+ async function syncEvents(db, bucket, contentful, allAssets, entries, config3, result) {
38384
+ let count2 = 0;
38385
+ for (const entry of entries) {
38386
+ try {
38387
+ const cfId = entry.sys.id;
38388
+ const f = config3.fields.event;
38389
+ const title = ContentfulService.getField(entry, f.title);
38390
+ if (!title) {
38391
+ result.errors.push(`Event ${cfId}: missing title, skipping`);
38392
+ continue;
38393
+ }
38394
+ const slug = ContentfulService.getField(entry, f.slug) ?? slugify2(title);
38395
+ const themeRef = ContentfulService.getField(entry, f.theme);
38396
+ const themeId = themeRef?.sys?.id ?? null;
38397
+ const orgRef = ContentfulService.getField(entry, f.organization);
38398
+ const organizationId = orgRef?.sys?.id ?? null;
38399
+ let backgroundImageUrl = null;
38400
+ if (bucket) {
38401
+ const imageRef = ContentfulService.getField(entry, f.image);
38402
+ const assetId = imageRef?.sys?.id;
38403
+ if (assetId) {
38404
+ const assetUrl = ContentfulService.resolveAssetUrl(allAssets, assetId);
38405
+ if (assetUrl) {
38406
+ const r2Key = `contentful/${assetId}`;
38407
+ const uploaded = await uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key);
38408
+ if (uploaded.skipped) {
38409
+ result.imagesSkipped++;
38410
+ } else {
38411
+ result.imagesUploaded++;
38412
+ }
38413
+ backgroundImageUrl = `/uploads/images/${r2Key}`;
38414
+ }
38415
+ }
38416
+ }
38417
+ const values = {
38418
+ id: cfId,
38419
+ contentfulEntryId: cfId,
38420
+ title,
38421
+ slug,
38422
+ themeId,
38423
+ organizationId,
38424
+ updatedAt: entry.sys.updatedAt
38425
+ };
38426
+ const venue = ContentfulService.getField(entry, f.venue);
38427
+ if (venue !== void 0) values.venue = venue;
38428
+ const date5 = ContentfulService.getField(entry, f.date);
38429
+ if (date5 !== void 0) values.dateTime = date5;
38430
+ const price = ContentfulService.getField(entry, f.price);
38431
+ if (price !== void 0) values.price = price;
38432
+ if (backgroundImageUrl) values.backgroundImageUrl = backgroundImageUrl;
38433
+ const isSpotlight = ContentfulService.getField(entry, f.isSpotlight);
38434
+ if (isSpotlight !== void 0) values.isSpotlight = isSpotlight;
38435
+ const maxSlots = ContentfulService.getField(entry, f.maxSlots);
38436
+ if (maxSlots !== void 0) values.maxSlots = maxSlots;
38437
+ const gformsUrl = ContentfulService.getField(entry, f.gformsUrl);
38438
+ if (gformsUrl !== void 0) values.gformsUrl = gformsUrl;
38439
+ const gformsEditorUrl = ContentfulService.getField(entry, f.gformsEditorUrl);
38440
+ if (gformsEditorUrl !== void 0) values.gformsEditorUrl = gformsEditorUrl;
38441
+ const classCode = ContentfulService.getField(entry, f.classCode);
38442
+ if (classCode !== void 0) values.classCode = classCode;
38443
+ const startTime = ContentfulService.getField(entry, f.startTime);
38444
+ if (startTime !== void 0) values.startTime = startTime;
38445
+ const endTime = ContentfulService.getField(entry, f.endTime);
38446
+ if (endTime !== void 0) values.endTime = endTime;
38447
+ const regCloseRaw = ContentfulService.getField(entry, f.registrationClosesAt);
38448
+ if (regCloseRaw) {
38449
+ const ms = Date.parse(regCloseRaw);
38450
+ if (!Number.isNaN(ms)) values.registrationClosesAt = Math.floor(ms / 1e3);
38451
+ }
38452
+ await db.insert(events).values(values).onConflictDoUpdate({
38453
+ target: events.id,
38454
+ set: values
38455
+ });
38456
+ count2++;
38457
+ } catch (err) {
38458
+ result.errors.push(`Event ${entry.sys.id}: ${err}`);
38459
+ }
38460
+ }
38461
+ return count2;
38462
+ }
38463
+ async function syncFaqs(db, entries, config3) {
38464
+ let count2 = 0;
38465
+ for (const entry of entries) {
38466
+ const cfId = entry.sys.id;
38467
+ const f = config3.fields.faq;
38468
+ const question = ContentfulService.getField(entry, f.question);
38469
+ const answer = ContentfulService.getField(entry, f.answer);
38470
+ if (!question || !answer) continue;
38471
+ const category = ContentfulService.getField(entry, f.category) ?? null;
38472
+ const sortOrder = ContentfulService.getField(entry, f.sortOrder) ?? 0;
38473
+ await db.insert(faqs).values({
38474
+ id: cfId,
38475
+ question,
38476
+ answer,
38477
+ category,
38478
+ sortOrder
38479
+ }).onConflictDoUpdate({
38480
+ target: faqs.id,
38481
+ set: { question, answer, category, sortOrder }
38482
+ });
38483
+ count2++;
38484
+ }
38485
+ return count2;
38486
+ }
38487
+ async function uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key) {
38488
+ const { data, contentType } = await contentful.downloadAsset(assetUrl);
38489
+ const sha = await computeSha256(data);
38490
+ const existing = await bucket.head(r2Key);
38491
+ if (existing?.customMetadata?.sha256 === sha) {
38492
+ return { skipped: true };
38493
+ }
38494
+ await bucket.put(r2Key, data, {
38495
+ httpMetadata: { contentType },
38496
+ customMetadata: {
38497
+ sha256: sha,
38498
+ source: "contentful",
38499
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
38500
+ }
38501
+ });
38502
+ return { skipped: false };
38503
+ }
38002
38504
  async function batchRun(items, fn, concurrency = 5) {
38003
38505
  const results = [];
38004
38506
  for (let i = 0; i < items.length; i += concurrency) {
@@ -38008,6 +38510,13 @@ async function batchRun(items, fn, concurrency = 5) {
38008
38510
  }
38009
38511
  return results;
38010
38512
  }
38513
+ async function computeSha256(data) {
38514
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
38515
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
38516
+ }
38517
+ function slugify2(text2) {
38518
+ return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
38519
+ }
38011
38520
  async function ensureContentTypes(mgmt, config3 = {}) {
38012
38521
  const eventTypeId = config3.eventTypeId ?? "event";
38013
38522
  const themeTypeId = config3.themeTypeId ?? "theme";
@@ -38028,7 +38537,7 @@ async function ensureContentTypes(mgmt, config3 = {}) {
38028
38537
  { id: "endTime", name: "End Time", type: "Symbol" },
38029
38538
  { id: "price", name: "Price", type: "Symbol" },
38030
38539
  { id: "image", name: "Image", type: "Link", linkType: "Asset" },
38031
- { id: "isMajor", name: "Major Event", type: "Boolean" },
38540
+ { id: "isSpotlight", name: "Spotlight", type: "Boolean" },
38032
38541
  { id: "maxSlots", name: "Max Slots", type: "Integer" },
38033
38542
  { id: "gformsUrl", name: "Google Forms URL", type: "Symbol" },
38034
38543
  { id: "gformsEditorUrl", name: "Google Forms Editor URL", type: "Symbol" },
@@ -38113,7 +38622,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
38113
38622
  const fields = {
38114
38623
  title: ContentfulManagement.locale(event.title),
38115
38624
  slug: ContentfulManagement.locale(event.slug),
38116
- isMajor: ContentfulManagement.locale(event.isMajor),
38625
+ isSpotlight: ContentfulManagement.locale(event.isSpotlight),
38117
38626
  maxSlots: ContentfulManagement.locale(event.maxSlots)
38118
38627
  };
38119
38628
  if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
@@ -38170,7 +38679,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
38170
38679
  }
38171
38680
 
38172
38681
  // src/routes/site-config.ts
38173
- var siteConfigRoute = new chunkQARF2YFF_cjs.Hono();
38682
+ var siteConfigRoute = new chunkLVKPYSXI_cjs.Hono();
38174
38683
  siteConfigRoute.get("/", async (c) => {
38175
38684
  const db = createDb(c.env.DB);
38176
38685
  const rows = await db.query.siteConfig.findMany();
@@ -38184,6 +38693,9 @@ siteConfigRoute.get("/", async (c) => {
38184
38693
  siteName: config3.site_name ?? null,
38185
38694
  registrationGloballyOpen: config3.registration_globally_open ?? true,
38186
38695
  maintenanceMode: config3.maintenance_mode ?? false,
38696
+ // Prefer the D1-persisted value over the env-var default so that
38697
+ // PATCH /config/cms_mode changes are reflected immediately.
38698
+ cmsMode: config3.cms_mode ?? c.get("cmsMode"),
38187
38699
  now: Math.floor(Date.now() / 1e3)
38188
38700
  }
38189
38701
  });
@@ -38205,6 +38717,10 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
38205
38717
  var SYNC_LOCK_KEY = "contentful:sync:lock";
38206
38718
  var SYNC_LOCK_TTL = 60;
38207
38719
  siteConfigRoute.post("/sync-content", authMiddleware, adminMiddleware, async (c) => {
38720
+ const cmsMode = c.get("cmsMode");
38721
+ if (cmsMode === "cloudflare") {
38722
+ throw serviceUnavailable("Contentful sync is not available in Cloudflare-only mode.");
38723
+ }
38208
38724
  if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
38209
38725
  throw serviceUnavailable("Contentful Management API credentials not configured.");
38210
38726
  }
@@ -38238,7 +38754,7 @@ var faqSchema = external_exports.object({
38238
38754
  category: external_exports.string().optional(),
38239
38755
  sortOrder: external_exports.number().int().default(0)
38240
38756
  });
38241
- var faqsRoute = new chunkQARF2YFF_cjs.Hono();
38757
+ var faqsRoute = new chunkLVKPYSXI_cjs.Hono();
38242
38758
  async function pushFaqToContentful(env2, faq) {
38243
38759
  console.log("[Contentful] pushFaqToContentful called for FAQ:", faq.id);
38244
38760
  if (!ContentfulManagement.isConfigured(env2.CONTENTFUL_SPACE_ID, env2.CONTENTFUL_MANAGEMENT_TOKEN)) {
@@ -38291,7 +38807,9 @@ faqsRoute.post(
38291
38807
  const cache3 = new CacheService(c.env.KV);
38292
38808
  const [created] = await db.insert(faqs).values(body).returning();
38293
38809
  await cache3.del(FAQS_KV_KEY);
38294
- c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
38810
+ if (c.get("cmsMode") === "hybrid") {
38811
+ c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
38812
+ }
38295
38813
  return c.json({ data: created }, 201);
38296
38814
  }
38297
38815
  );
@@ -38304,7 +38822,9 @@ faqsRoute.patch("/:id", authMiddleware, adminMiddleware, async (c) => {
38304
38822
  const [updated] = await db.update(faqs).set({ ...body, updatedAt: now2 }).where(eq(faqs.id, id)).returning();
38305
38823
  if (!updated) throw notFound("FAQ");
38306
38824
  await cache3.del(FAQS_KV_KEY);
38307
- c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
38825
+ if (c.get("cmsMode") === "hybrid") {
38826
+ c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
38827
+ }
38308
38828
  return c.json({ data: updated });
38309
38829
  });
38310
38830
  faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
@@ -38318,7 +38838,7 @@ faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
38318
38838
  });
38319
38839
 
38320
38840
  // src/routes/internal/gforms-webhook.ts
38321
- var gformsWebhookRoute = new chunkQARF2YFF_cjs.Hono();
38841
+ var gformsWebhookRoute = new chunkLVKPYSXI_cjs.Hono();
38322
38842
  gformsWebhookRoute.post("/", internalMiddleware, async (c) => {
38323
38843
  const rawBody = await c.req.text();
38324
38844
  const signature = c.req.header("X-Goog-Signature");
@@ -38390,7 +38910,7 @@ var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
38390
38910
  "image/svg+xml"
38391
38911
  ]);
38392
38912
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
38393
- var uploadsRoute = new chunkQARF2YFF_cjs.Hono();
38913
+ var uploadsRoute = new chunkLVKPYSXI_cjs.Hono();
38394
38914
  uploadsRoute.get("/images/*", async (c) => {
38395
38915
  const bucket = c.env.FILES;
38396
38916
  if (!bucket) {
@@ -38564,7 +39084,7 @@ var createThemeSchema = external_exports.object({
38564
39084
  name: external_exports.string().min(1),
38565
39085
  path: external_exports.string().min(1)
38566
39086
  });
38567
- var themesRoute = new chunkQARF2YFF_cjs.Hono();
39087
+ var themesRoute = new chunkLVKPYSXI_cjs.Hono();
38568
39088
  themesRoute.get("/", async (c) => {
38569
39089
  const db = createDb(c.env.DB);
38570
39090
  const data = await db.select().from(themes);
@@ -38580,7 +39100,9 @@ themesRoute.post(
38580
39100
  const db = createDb(c.env.DB);
38581
39101
  try {
38582
39102
  const [created] = await db.insert(themes).values(body).returning();
38583
- c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
39103
+ if (c.get("cmsMode") === "hybrid") {
39104
+ c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
39105
+ }
38584
39106
  return c.json({ data: created }, 201);
38585
39107
  } catch (err) {
38586
39108
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -38601,7 +39123,9 @@ themesRoute.patch(
38601
39123
  try {
38602
39124
  const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
38603
39125
  if (!updated) throw notFound("Theme");
38604
- c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
39126
+ if (c.get("cmsMode") === "hybrid") {
39127
+ c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
39128
+ }
38605
39129
  return c.json({ data: updated });
38606
39130
  } catch (err) {
38607
39131
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -38616,7 +39140,9 @@ themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
38616
39140
  const db = createDb(c.env.DB);
38617
39141
  const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
38618
39142
  if (!deleted) throw notFound("Theme");
38619
- c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
39143
+ if (c.get("cmsMode") === "hybrid") {
39144
+ c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
39145
+ }
38620
39146
  return c.body(null, 204);
38621
39147
  });
38622
39148
 
@@ -38627,7 +39153,7 @@ var createOrganizationSchema = external_exports.object({
38627
39153
  logoUrl: external_exports.string().url().nullable().optional(),
38628
39154
  link: external_exports.string().url().nullable().optional()
38629
39155
  });
38630
- var organizationsRoute = new chunkQARF2YFF_cjs.Hono();
39156
+ var organizationsRoute = new chunkLVKPYSXI_cjs.Hono();
38631
39157
  organizationsRoute.get("/", async (c) => {
38632
39158
  const db = createDb(c.env.DB);
38633
39159
  const data = await db.select().from(organizations);
@@ -38680,9 +39206,86 @@ organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) =>
38680
39206
  return c.body(null, 204);
38681
39207
  });
38682
39208
 
39209
+ // src/routes/contentful-sync.ts
39210
+ var contentfulSyncRoute = new chunkLVKPYSXI_cjs.Hono();
39211
+ contentfulSyncRoute.post(
39212
+ "/trigger",
39213
+ authMiddleware,
39214
+ adminMiddleware,
39215
+ async (c) => {
39216
+ const env2 = c.env;
39217
+ const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
39218
+ if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_ACCESS_TOKEN) {
39219
+ return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful credentials missing" } }, 400);
39220
+ }
39221
+ const db = createDb(env2.DB);
39222
+ const contentful = new ContentfulService(CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_ENVIRONMENT);
39223
+ try {
39224
+ if (CONTENTFUL_MANAGEMENT_TOKEN) {
39225
+ const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
39226
+ await ensureContentTypes(mgmt);
39227
+ }
39228
+ c.executionCtx.waitUntil(
39229
+ 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))
39230
+ );
39231
+ return c.json({ message: "Contentful sync triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
39232
+ } catch (err) {
39233
+ console.error("[Contentful] Sync trigger failed:", err);
39234
+ return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger sync" } }, 500);
39235
+ }
39236
+ }
39237
+ );
39238
+ contentfulSyncRoute.post(
39239
+ "/push",
39240
+ authMiddleware,
39241
+ adminMiddleware,
39242
+ async (c) => {
39243
+ const env2 = c.env;
39244
+ const { CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
39245
+ if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_MANAGEMENT_TOKEN) {
39246
+ return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful management credentials missing" } }, 400);
39247
+ }
39248
+ const db = createDb(env2.DB);
39249
+ const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
39250
+ try {
39251
+ c.executionCtx.waitUntil(
39252
+ pushToContentful(db, mgmt, {}, env2.KV).then((r) => console.log("[Contentful] Push complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Push failed:", err))
39253
+ );
39254
+ return c.json({ message: "Contentful push triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
39255
+ } catch (err) {
39256
+ console.error("[Contentful] Push trigger failed:", err);
39257
+ return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger push" } }, 500);
39258
+ }
39259
+ }
39260
+ );
39261
+ contentfulSyncRoute.get(
39262
+ "/status",
39263
+ authMiddleware,
39264
+ adminMiddleware,
39265
+ async (c) => {
39266
+ const env2 = c.env;
39267
+ const db = createDb(env2.DB);
39268
+ const [[{ themes: themes2 }], [{ events: events2 }], [{ faqs: faqs2 }], [{ orgs }]] = await Promise.all([
39269
+ db.all(sql2`SELECT count(*) as themes FROM themes`),
39270
+ db.all(sql2`SELECT count(*) as events FROM events`),
39271
+ db.all(sql2`SELECT count(*) as faqs FROM faqs`),
39272
+ db.all(sql2`SELECT count(*) as orgs FROM organizations`)
39273
+ ]);
39274
+ const isConfigured = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_ACCESS_TOKEN;
39275
+ const canPush = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_MANAGEMENT_TOKEN;
39276
+ return c.json({
39277
+ isConfigured,
39278
+ canPush,
39279
+ cmsMode: c.get("cmsMode"),
39280
+ spaceId: env2.CONTENTFUL_SPACE_ID ?? null,
39281
+ totals: { themes: themes2, events: events2, faqs: faqs2, organizations: orgs }
39282
+ });
39283
+ }
39284
+ );
39285
+
38683
39286
  // src/app.ts
38684
39287
  function createApp(options = {}) {
38685
- const app = new chunkQARF2YFF_cjs.Hono();
39288
+ const app = new chunkLVKPYSXI_cjs.Hono();
38686
39289
  if (options.gformsWebhookUrl) {
38687
39290
  const webhookUrl = `${options.gformsWebhookUrl.replace(/\/$/, "")}/internal/gforms-webhook`;
38688
39291
  app.use("*", async (c, next) => {
@@ -38691,8 +39294,14 @@ function createApp(options = {}) {
38691
39294
  });
38692
39295
  }
38693
39296
  app.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
38694
- app.use("*", chunkQARF2YFF_cjs.createPowChallengeMiddleware());
39297
+ app.use("*", chunkLVKPYSXI_cjs.createPowChallengeMiddleware());
38695
39298
  app.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
39299
+ app.use("*", async (c, next) => {
39300
+ const overrideRaw = await c.env.KV.get("config:cms_mode").catch(() => null);
39301
+ const override = overrideRaw ? JSON.parse(overrideRaw) : null;
39302
+ c.set("cmsMode", parseCmsMode(override ?? c.env.CMS_MODE));
39303
+ return next();
39304
+ });
38696
39305
  app.on(["POST", "GET"], "/api/auth/*", (c) => {
38697
39306
  const auth = createAuth(c.env);
38698
39307
  const req = c.req.raw;
@@ -38704,7 +39313,7 @@ function createApp(options = {}) {
38704
39313
  return auth.handler(new Request(req.url, {
38705
39314
  method: req.method,
38706
39315
  headers: newHeaders,
38707
- body: req.method === "GET" ? null : req.body,
39316
+ body: req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS" ? null : req.body,
38708
39317
  redirect: req.redirect
38709
39318
  }));
38710
39319
  }
@@ -38722,15 +39331,16 @@ function createApp(options = {}) {
38722
39331
  }
38723
39332
  return next();
38724
39333
  });
38725
- app.post(chunkQARF2YFF_cjs.POW_VERIFY_PATH, chunkQARF2YFF_cjs.handlePowVerify);
39334
+ app.post(chunkLVKPYSXI_cjs.POW_VERIFY_PATH, chunkLVKPYSXI_cjs.handlePowVerify);
38726
39335
  app.route("/health", healthRoute);
38727
39336
  app.route("/api/config", siteConfigRoute);
38728
- app.route("/api/events", eventsRoute);
39337
+ app.route("/api/classes", classesRoute);
38729
39338
  app.route("/api/themes", themesRoute);
38730
39339
  app.route("/api/users", usersRoute);
38731
39340
  app.route("/api/organizations", organizationsRoute);
38732
39341
  app.route("/api/faqs", faqsRoute);
38733
39342
  app.route("/api/uploads", uploadsRoute);
39343
+ app.route("/api/contentful", contentfulSyncRoute);
38734
39344
  app.route("/internal/gforms-webhook", gformsWebhookRoute);
38735
39345
  app.onError(errorHandler);
38736
39346
  app.notFound(
@@ -39364,6 +39974,7 @@ var PATCH_STATEMENTS = [
39364
39974
  `ALTER TABLE "events" ADD COLUMN "class_code" text`,
39365
39975
  `ALTER TABLE "events" ADD COLUMN "start_time" text`,
39366
39976
  `ALTER TABLE "events" ADD COLUMN "end_time" text`,
39977
+ `ALTER TABLE "events" RENAME COLUMN "is_major" TO "is_spotlight"`,
39367
39978
  `CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`
39368
39979
  ];
39369
39980
  var CREATE_STATEMENTS = [
@@ -39467,7 +40078,7 @@ var CREATE_STATEMENTS = [
39467
40078
  "class_code" text,
39468
40079
  "start_time" text,
39469
40080
  "end_time" text,
39470
- "is_major" integer DEFAULT false NOT NULL,
40081
+ "is_spotlight" integer DEFAULT false NOT NULL,
39471
40082
  "max_slots" integer DEFAULT 0 NOT NULL,
39472
40083
  "registered_slots" integer DEFAULT 0 NOT NULL,
39473
40084
  "gforms_id" text,
@@ -39529,7 +40140,9 @@ async function ensureDatabase(d1) {
39529
40140
  try {
39530
40141
  await d1.prepare(sql3).run();
39531
40142
  } catch (err) {
39532
- if (err?.message?.includes("duplicate column")) continue;
40143
+ if (err?.message?.includes("duplicate column") || err?.message?.includes("no such column") && err?.message?.includes("is_major")) {
40144
+ continue;
40145
+ }
39533
40146
  throw err;
39534
40147
  }
39535
40148
  }
@@ -39538,12 +40151,14 @@ async function ensureDatabase(d1) {
39538
40151
  // src/worker-handler.ts
39539
40152
  var API_PREFIXES = [
39540
40153
  "/api/auth/",
39541
- "/api/events",
40154
+ "/api/classes",
39542
40155
  "/api/users",
40156
+ "/api/organizations",
39543
40157
  "/api/faqs",
39544
40158
  "/api/themes",
39545
40159
  "/api/config",
39546
40160
  "/api/uploads",
40161
+ "/api/contentful",
39547
40162
  "/health",
39548
40163
  "/internal/",
39549
40164
  "/.well-known/"
@@ -39582,7 +40197,7 @@ function createWorkerHandler(options) {
39582
40197
  return getLeapify(env2).fetch(request, env2, ctx);
39583
40198
  }
39584
40199
  let response = await options.serveFrontend(request, env2, ctx);
39585
- if ((!response || response.status === 404) && !pathname.includes(".")) {
40200
+ if ((!response || response.status === 404) && !pathname.includes(".") && request.method === "GET") {
39586
40201
  const indexRequest = new Request(new URL("/", request.url), request);
39587
40202
  response = await options.serveFrontend(indexRequest, env2, ctx);
39588
40203
  }
@@ -39690,6 +40305,7 @@ function injectConfig2(html2, config3) {
39690
40305
  (*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
39691
40306
  */
39692
40307
 
40308
+ exports.CONTENTFUL_CONFIG_KEYS = CONTENTFUL_CONFIG_KEYS;
39693
40309
  exports.ContentfulManagement = ContentfulManagement;
39694
40310
  exports.authAccount = authAccount;
39695
40311
  exports.authSession = authSession;
@@ -39697,6 +40313,7 @@ exports.authUser = authUser;
39697
40313
  exports.authVerification = authVerification;
39698
40314
  exports.bookmarks = bookmarks;
39699
40315
  exports.bookmarksRelations = bookmarksRelations;
40316
+ exports.contentfulConfig = contentfulConfig;
39700
40317
  exports.createDb = createDb;
39701
40318
  exports.createLeapify = createLeapify;
39702
40319
  exports.createQueueHandler = createQueueHandler;
@@ -39710,6 +40327,9 @@ exports.getRuntimeConfig = getRuntimeConfig;
39710
40327
  exports.injectConfig = injectConfig2;
39711
40328
  exports.organizations = organizations;
39712
40329
  exports.organizationsRelations = organizationsRelations;
40330
+ exports.parseCmsMode = parseCmsMode;
40331
+ exports.shouldPullFromContentful = shouldPullFromContentful;
40332
+ exports.shouldPushToContentful = shouldPushToContentful;
39713
40333
  exports.siteConfig = siteConfig;
39714
40334
  exports.themes = themes;
39715
40335
  exports.themesRelations = themesRelations;