@access-dlsu/leapify 0.260507.1 → 0.260507.4

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 (51) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/{chunk-QARF2YFF.cjs → chunk-BFMJDSDI.cjs} +3 -2
  3. package/dist/chunk-BFMJDSDI.cjs.map +1 -0
  4. package/dist/{chunk-ANNHE3PZ.js → chunk-LJ5BSSYE.js} +3 -2
  5. package/dist/{chunk-QARF2YFF.cjs.map → chunk-LJ5BSSYE.js.map} +1 -1
  6. package/dist/{chunk-63CUZGSZ.js → chunk-MCOLCTFX.js} +3 -2
  7. package/dist/chunk-MCOLCTFX.js.map +1 -0
  8. package/dist/{chunk-YFJBE3AU.cjs → chunk-MKWVLWVJ.cjs} +3 -2
  9. package/dist/chunk-MKWVLWVJ.cjs.map +1 -0
  10. package/dist/client/index.cjs +25 -25
  11. package/dist/client/index.cjs.map +1 -1
  12. package/dist/client/index.d.ts +17 -17
  13. package/dist/client/index.d.ts.map +1 -1
  14. package/dist/client/index.js +25 -25
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/types.d.ts +4 -2
  17. package/dist/client/types.d.ts.map +1 -1
  18. package/dist/db/migrate.d.ts.map +1 -1
  19. package/dist/db/schema/{events.d.ts → classes.d.ts} +3 -3
  20. package/dist/db/schema/classes.d.ts.map +1 -0
  21. package/dist/db/schema/index.d.ts +1 -1
  22. package/dist/db/schema/index.d.ts.map +1 -1
  23. package/dist/db/schema/site-config.d.ts +83 -0
  24. package/dist/db/schema/site-config.d.ts.map +1 -1
  25. package/dist/index.cjs +640 -52
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +616 -33
  30. package/dist/index.js.map +1 -1
  31. package/dist/lib/middleware/pow-challenge.cjs +6 -6
  32. package/dist/lib/middleware/pow-challenge.d.ts.map +1 -1
  33. package/dist/lib/middleware/pow-challenge.js +1 -1
  34. package/dist/routes/classes.d.ts +4 -0
  35. package/dist/routes/classes.d.ts.map +1 -0
  36. package/dist/routes/contentful-sync.d.ts +4 -0
  37. package/dist/routes/contentful-sync.d.ts.map +1 -0
  38. package/dist/services/snapshot.d.ts +1 -1
  39. package/dist/services/snapshot.d.ts.map +1 -1
  40. package/dist/types.d.ts +19 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/worker-handler.d.ts.map +1 -1
  43. package/dist/worker.js +605 -29
  44. package/dist/worker.js.map +1 -1
  45. package/package.json +1 -1
  46. package/dist/chunk-63CUZGSZ.js.map +0 -1
  47. package/dist/chunk-ANNHE3PZ.js.map +0 -1
  48. package/dist/chunk-YFJBE3AU.cjs.map +0 -1
  49. package/dist/db/schema/events.d.ts.map +0 -1
  50. package/dist/routes/events.d.ts +0 -4
  51. 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 chunkBFMJDSDI_cjs = require('./chunk-BFMJDSDI.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,6 +50,18 @@ 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
67
  const opts = {
@@ -161,7 +173,7 @@ function createCorsMiddleware(allowedOrigins) {
161
173
  function createRefererGuard(allowedOrigins) {
162
174
  const MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
163
175
  const SKIP_PREFIXES = ["/health", "/internal", "/api/auth", "/.well-known"];
164
- return chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
176
+ return chunkBFMJDSDI_cjs.createMiddleware(async (c, next) => {
165
177
  if (!MUTATION_METHODS.has(c.req.method)) return next();
166
178
  if (SKIP_PREFIXES.some((p) => c.req.path.startsWith(p))) return next();
167
179
  if (allowedOrigins.includes("*")) return next();
@@ -36332,12 +36344,14 @@ function drizzle(client, config3 = {}) {
36332
36344
  // src/db/schema/index.ts
36333
36345
  var schema_exports = {};
36334
36346
  chunkEMMSS5I5_cjs.__export(schema_exports, {
36347
+ CONTENTFUL_CONFIG_KEYS: () => CONTENTFUL_CONFIG_KEYS,
36335
36348
  authAccount: () => authAccount,
36336
36349
  authSession: () => authSession,
36337
36350
  authUser: () => authUser,
36338
36351
  authVerification: () => authVerification,
36339
36352
  bookmarks: () => bookmarks,
36340
36353
  bookmarksRelations: () => bookmarksRelations,
36354
+ contentfulConfig: () => contentfulConfig,
36341
36355
  events: () => events,
36342
36356
  eventsRelations: () => eventsRelations,
36343
36357
  faqs: () => faqs,
@@ -36389,7 +36403,7 @@ var organizationsRelations = relations(organizations, ({ many }) => ({
36389
36403
  events: many(events)
36390
36404
  }));
36391
36405
 
36392
- // src/db/schema/events.ts
36406
+ // src/db/schema/classes.ts
36393
36407
  var events = sqliteTable(
36394
36408
  "events",
36395
36409
  {
@@ -36414,7 +36428,7 @@ var events = sqliteTable(
36414
36428
  // start time string
36415
36429
  endTime: text("end_time"),
36416
36430
  // end time string
36417
- isMajor: integer2("is_major", { mode: "boolean" }).notNull().default(false),
36431
+ isSpotlight: integer2("is_spotlight", { mode: "boolean" }).notNull().default(false),
36418
36432
  // Slot tracking (local counter — NOT polled from Google Forms)
36419
36433
  maxSlots: integer2("max_slots").notNull().default(0),
36420
36434
  registeredSlots: integer2("registered_slots").notNull().default(0),
@@ -36471,6 +36485,18 @@ var siteConfig = sqliteTable("site_config", {
36471
36485
  // JSON-serializable string
36472
36486
  updatedAt: integer2("updated_at").notNull().default(sql2`(unixepoch())`)
36473
36487
  });
36488
+ var CONTENTFUL_CONFIG_KEYS = {
36489
+ ENABLED: "contentful.enabled",
36490
+ SPACE_ID: "contentful.spaceId",
36491
+ MANAGEMENT_TOKEN: "contentful.managementToken",
36492
+ DEFAULT_SPACE_ID: "dlsu-events"
36493
+ };
36494
+ var contentfulConfig = sqliteTable("contentful_config", {
36495
+ space_id: text("space_id"),
36496
+ contentful_enabled: integer2("contentful_enabled").notNull().default(0),
36497
+ last_sync_at: integer2("last_sync_at"),
36498
+ updated_at: integer2("updated_at").default(sql2`(unixepoch())`).notNull()
36499
+ });
36474
36500
 
36475
36501
  // src/db/schema/faqs.ts
36476
36502
  var faqs = sqliteTable("faqs", {
@@ -36686,7 +36712,7 @@ async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAu
36686
36712
  emailVerified: betterAuthEmailVerified
36687
36713
  };
36688
36714
  }
36689
- var authMiddleware = chunkQARF2YFF_cjs.createMiddleware(
36715
+ var authMiddleware = chunkBFMJDSDI_cjs.createMiddleware(
36690
36716
  async (c, next) => {
36691
36717
  const rawToken = extractRawToken(c);
36692
36718
  if (rawToken) {
@@ -36728,7 +36754,7 @@ var authMiddleware = chunkQARF2YFF_cjs.createMiddleware(
36728
36754
  return next();
36729
36755
  }
36730
36756
  );
36731
- var optionalAuthMiddleware = chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
36757
+ var optionalAuthMiddleware = chunkBFMJDSDI_cjs.createMiddleware(async (c, next) => {
36732
36758
  const rawToken = extractRawToken(c);
36733
36759
  if (!rawToken) {
36734
36760
  c.set("user", null);
@@ -36736,7 +36762,7 @@ var optionalAuthMiddleware = chunkQARF2YFF_cjs.createMiddleware(async (c, next)
36736
36762
  }
36737
36763
  return authMiddleware(c, next);
36738
36764
  });
36739
- var adminMiddleware = chunkQARF2YFF_cjs.createMiddleware(
36765
+ var adminMiddleware = chunkBFMJDSDI_cjs.createMiddleware(
36740
36766
  async (c, next) => {
36741
36767
  const user = c.get("user");
36742
36768
  if (!user || !["admin", "super_admin"].includes(user.role)) {
@@ -36745,7 +36771,7 @@ var adminMiddleware = chunkQARF2YFF_cjs.createMiddleware(
36745
36771
  return next();
36746
36772
  }
36747
36773
  );
36748
- var internalMiddleware = chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
36774
+ var internalMiddleware = chunkBFMJDSDI_cjs.createMiddleware(async (c, next) => {
36749
36775
  const secret = c.req.header("X-Internal-Secret");
36750
36776
  if (!secret || secret !== c.env.INTERNAL_API_SECRET) {
36751
36777
  throw forbidden("Invalid internal secret");
@@ -36754,7 +36780,7 @@ var internalMiddleware = chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
36754
36780
  });
36755
36781
 
36756
36782
  // src/routes/health.ts
36757
- var healthRoute = new chunkQARF2YFF_cjs.Hono();
36783
+ var healthRoute = new chunkBFMJDSDI_cjs.Hono();
36758
36784
  async function probeResend(apiKey) {
36759
36785
  const start = Date.now();
36760
36786
  try {
@@ -36983,7 +37009,7 @@ var parse4 = (cookie, name) => {
36983
37009
  cookieValue = cookieValue.slice(1, -1);
36984
37010
  }
36985
37011
  if (validCookieValueRegEx.test(cookieValue)) {
36986
- parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? chunkQARF2YFF_cjs.tryDecode(cookieValue, chunkQARF2YFF_cjs.decodeURIComponent_) : cookieValue;
37012
+ parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? chunkBFMJDSDI_cjs.tryDecode(cookieValue, chunkBFMJDSDI_cjs.decodeURIComponent_) : cookieValue;
36987
37013
  }
36988
37014
  }
36989
37015
  return parsedCookie;
@@ -37026,7 +37052,7 @@ var validator = (target, validationFunc) => {
37026
37052
  value = await c.req.json();
37027
37053
  } catch {
37028
37054
  const message2 = "Malformed JSON in request body";
37029
- throw new chunkQARF2YFF_cjs.HTTPException(400, { message: message2 });
37055
+ throw new chunkBFMJDSDI_cjs.HTTPException(400, { message: message2 });
37030
37056
  }
37031
37057
  break;
37032
37058
  case "form": {
@@ -37044,7 +37070,7 @@ var validator = (target, validationFunc) => {
37044
37070
  } catch (e) {
37045
37071
  let message2 = "Malformed FormData request.";
37046
37072
  message2 += e instanceof Error ? ` ${e.message}` : ` ${String(e)}`;
37047
- throw new chunkQARF2YFF_cjs.HTTPException(400, { message: message2 });
37073
+ throw new chunkBFMJDSDI_cjs.HTTPException(400, { message: message2 });
37048
37074
  }
37049
37075
  }
37050
37076
  const form = /* @__PURE__ */ Object.create(null);
@@ -37593,7 +37619,7 @@ var ContentfulManagement = class {
37593
37619
  // src/lib/middleware/rate-limit.ts
37594
37620
  function createRateLimitMiddleware(config3) {
37595
37621
  const { endpoint, limit, windowSec, identifier } = config3;
37596
- return chunkQARF2YFF_cjs.createMiddleware(async (c, next) => {
37622
+ return chunkBFMJDSDI_cjs.createMiddleware(async (c, next) => {
37597
37623
  if (c.req.path === "/.well-known/leapify/pow/verify") return next();
37598
37624
  const id = identifier === "uid" ? c.get("user")?.uid ?? c.req.header("CF-Connecting-IP") ?? "unknown" : c.req.header("CF-Connecting-IP") ?? "unknown";
37599
37625
  const key = `rl:${endpoint}:${id}`;
@@ -37640,7 +37666,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
37640
37666
  identifier: "uid"
37641
37667
  });
37642
37668
 
37643
- // src/routes/events.ts
37669
+ // src/routes/classes.ts
37644
37670
  var EVENTS_LIST_KV_KEY = "events:list";
37645
37671
  var EVENTS_ETAG_KV_KEY = "events:etag";
37646
37672
  var EVENTS_LIST_TTL = 300;
@@ -37656,7 +37682,7 @@ async function pushEventToContentful(env2, event) {
37656
37682
  const fields = {
37657
37683
  title: ContentfulManagement.locale(event.title),
37658
37684
  slug: ContentfulManagement.locale(event.slug),
37659
- isMajor: ContentfulManagement.locale(event.isMajor),
37685
+ isSpotlight: ContentfulManagement.locale(event.isSpotlight),
37660
37686
  maxSlots: ContentfulManagement.locale(event.maxSlots)
37661
37687
  };
37662
37688
  if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
@@ -37719,7 +37745,7 @@ var createEventSchema = external_exports.object({
37719
37745
  startTime: external_exports.string().optional(),
37720
37746
  endTime: external_exports.string().optional(),
37721
37747
  registrationClosesAt: external_exports.number().optional(),
37722
- isMajor: external_exports.boolean().default(false),
37748
+ isSpotlight: external_exports.boolean().default(false),
37723
37749
  maxSlots: external_exports.number().int().min(0).default(0),
37724
37750
  gformsId: external_exports.string().optional(),
37725
37751
  gformsUrl: external_exports.string().url().optional(),
@@ -37728,11 +37754,11 @@ var createEventSchema = external_exports.object({
37728
37754
  contentfulEntryId: external_exports.string().optional(),
37729
37755
  status: external_exports.enum(["draft", "queued", "published"]).default("draft")
37730
37756
  });
37731
- var eventsRoute = new chunkQARF2YFF_cjs.Hono();
37757
+ var classesRoute = new chunkBFMJDSDI_cjs.Hono();
37732
37758
  function generateSlug(title) {
37733
37759
  return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
37734
37760
  }
37735
- eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37761
+ classesRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37736
37762
  const db = createDb(c.env.DB);
37737
37763
  const data = await db.query.events.findMany({
37738
37764
  with: { theme: true, organization: true },
@@ -37740,7 +37766,7 @@ eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
37740
37766
  });
37741
37767
  return c.json({ data });
37742
37768
  });
37743
- eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
37769
+ classesRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
37744
37770
  const body = await c.req.json();
37745
37771
  const db = createDb(c.env.DB);
37746
37772
  const cache3 = new CacheService(c.env.KV);
@@ -37768,7 +37794,7 @@ eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) =>
37768
37794
  ]);
37769
37795
  return c.json({ data: { updated: body.ids.length } });
37770
37796
  });
37771
- eventsRoute.get("/", eventsListRateLimit, async (c) => {
37797
+ classesRoute.get("/", eventsListRateLimit, async (c) => {
37772
37798
  const db = createDb(c.env.DB);
37773
37799
  const cache3 = new CacheService(c.env.KV);
37774
37800
  const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
@@ -37779,7 +37805,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37779
37805
  );
37780
37806
  const ifNoneMatch = c.req.header("If-None-Match");
37781
37807
  if (ifNoneMatch === etag) {
37782
- return c.newResponse(null, 304);
37808
+ return c.body(null, 304);
37783
37809
  }
37784
37810
  const data = await cache3.getOrSet(
37785
37811
  EVENTS_LIST_KV_KEY,
@@ -37803,7 +37829,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37803
37829
  startTime: true,
37804
37830
  endTime: true,
37805
37831
  registrationClosesAt: true,
37806
- isMajor: true,
37832
+ isSpotlight: true,
37807
37833
  maxSlots: true,
37808
37834
  registeredSlots: true,
37809
37835
  gformsUrl: true,
@@ -37820,7 +37846,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
37820
37846
  );
37821
37847
  return c.json({ data });
37822
37848
  });
37823
- eventsRoute.get("/:slug", async (c) => {
37849
+ classesRoute.get("/:slug", async (c) => {
37824
37850
  const { slug } = c.req.param();
37825
37851
  const db = createDb(c.env.DB);
37826
37852
  const event = await db.query.events.findFirst({
@@ -37832,7 +37858,7 @@ eventsRoute.get("/:slug", async (c) => {
37832
37858
  if (!event) throw notFound("Event");
37833
37859
  return c.json({ data: event });
37834
37860
  });
37835
- eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37861
+ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37836
37862
  const { slug } = c.req.param();
37837
37863
  const db = createDb(c.env.DB);
37838
37864
  const cache3 = new CacheService(c.env.KV);
@@ -37842,7 +37868,7 @@ eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
37842
37868
  c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
37843
37869
  return c.json({ data: info2 });
37844
37870
  });
37845
- eventsRoute.post(
37871
+ classesRoute.post(
37846
37872
  "/",
37847
37873
  authMiddleware,
37848
37874
  adminMiddleware,
@@ -37877,11 +37903,13 @@ eventsRoute.post(
37877
37903
  cache3.del(EVENTS_LIST_KV_KEY),
37878
37904
  cache3.del(EVENTS_ETAG_KV_KEY)
37879
37905
  ]);
37880
- c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
37906
+ if (c.get("cmsMode") === "hybrid") {
37907
+ c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
37908
+ }
37881
37909
  return c.json({ data: created }, 201);
37882
37910
  }
37883
37911
  );
37884
- eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37912
+ classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37885
37913
  const { slug } = c.req.param();
37886
37914
  const body = await c.req.json();
37887
37915
  const db = createDb(c.env.DB);
@@ -37896,10 +37924,12 @@ eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
37896
37924
  cache3.del(EVENTS_LIST_KV_KEY),
37897
37925
  cache3.del(EVENTS_ETAG_KV_KEY)
37898
37926
  ]);
37899
- c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
37927
+ if (c.get("cmsMode") === "hybrid") {
37928
+ c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
37929
+ }
37900
37930
  return c.json({ data: updated });
37901
37931
  });
37902
- eventsRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
37932
+ classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
37903
37933
  const { slug } = c.req.param();
37904
37934
  const db = createDb(c.env.DB);
37905
37935
  const cache3 = new CacheService(c.env.KV);
@@ -37914,7 +37944,7 @@ eventsRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
37914
37944
 
37915
37945
  // src/routes/users.ts
37916
37946
  var VALID_ROLES = ["student", "admin", "super_admin"];
37917
- var usersRoute = new chunkQARF2YFF_cjs.Hono();
37947
+ var usersRoute = new chunkBFMJDSDI_cjs.Hono();
37918
37948
  usersRoute.get("/", authMiddleware, adminMiddleware, async (c) => {
37919
37949
  const db = createDb(c.env.DB);
37920
37950
  const data = await db.select().from(users);
@@ -37998,7 +38028,447 @@ usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
37998
38028
  return c.json({ data: { bookmarked: false } });
37999
38029
  });
38000
38030
 
38031
+ // src/services/contentful.ts
38032
+ var CONTENTFUL_CDN = "https://cdn.contentful.com";
38033
+ var ContentfulService = class _ContentfulService {
38034
+ spaceId;
38035
+ accessToken;
38036
+ environment;
38037
+ constructor(spaceId, accessToken, environment = "master") {
38038
+ this.spaceId = spaceId;
38039
+ this.accessToken = accessToken;
38040
+ this.environment = environment;
38041
+ }
38042
+ /**
38043
+ * Returns true if the required Contentful credentials are configured.
38044
+ */
38045
+ static isConfigured(spaceId, accessToken) {
38046
+ return !!(spaceId && accessToken);
38047
+ }
38048
+ // ─── Entries ─────────────────────────────────────────────────────────────
38049
+ /**
38050
+ * Fetch all entries of a given content type.
38051
+ * Handles pagination automatically (100 per page, Contentful max).
38052
+ */
38053
+ async getEntries(contentTypeId) {
38054
+ const allItems = [];
38055
+ let skip = 0;
38056
+ const limit = 100;
38057
+ do {
38058
+ const url2 = this.buildUrl(`/entries`, {
38059
+ content_type: contentTypeId,
38060
+ skip: String(skip),
38061
+ limit: String(limit),
38062
+ include: "2"
38063
+ // resolve up to 2 levels of linked entries/assets
38064
+ });
38065
+ const res = await fetch(url2, { headers: this.headers() });
38066
+ if (!res.ok) {
38067
+ throw new Error(`Contentful entries error: ${res.status} ${await res.text()}`);
38068
+ }
38069
+ const data = await res.json();
38070
+ allItems.push(...data.items);
38071
+ skip += limit;
38072
+ if (allItems.length >= data.total) break;
38073
+ } while (true);
38074
+ return allItems;
38075
+ }
38076
+ /**
38077
+ * Fetch all assets. Handles pagination.
38078
+ */
38079
+ async getAssets() {
38080
+ const allItems = [];
38081
+ let skip = 0;
38082
+ const limit = 100;
38083
+ do {
38084
+ const url2 = this.buildUrl(`/assets`, {
38085
+ skip: String(skip),
38086
+ limit: String(limit)
38087
+ });
38088
+ const res = await fetch(url2, { headers: this.headers() });
38089
+ if (!res.ok) {
38090
+ throw new Error(`Contentful assets error: ${res.status} ${await res.text()}`);
38091
+ }
38092
+ const data = await res.json();
38093
+ allItems.push(...data.items);
38094
+ skip += limit;
38095
+ if (allItems.length >= data.total) break;
38096
+ } while (true);
38097
+ return allItems;
38098
+ }
38099
+ // ─── Asset file download ─────────────────────────────────────────────────
38100
+ /**
38101
+ * Download an asset file from Contentful's CDN.
38102
+ * Returns the raw ArrayBuffer and content type.
38103
+ */
38104
+ async downloadAsset(assetUrl) {
38105
+ const url2 = assetUrl.startsWith("//") ? `https:${assetUrl}` : assetUrl;
38106
+ const res = await fetch(url2);
38107
+ if (!res.ok) {
38108
+ throw new Error(`Failed to download asset: ${res.status}`);
38109
+ }
38110
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
38111
+ const data = await res.arrayBuffer();
38112
+ return { data, contentType };
38113
+ }
38114
+ // ─── Helpers ─────────────────────────────────────────────────────────────
38115
+ /**
38116
+ * Extract a field value from a Contentful entry, handling locale wrapping.
38117
+ * Contentful fields are often `{ "en-US": value }` — this unwraps them.
38118
+ */
38119
+ static getField(entry, fieldName) {
38120
+ const raw = entry.fields[fieldName];
38121
+ if (raw === void 0 || raw === null) return void 0;
38122
+ if (typeof raw === "object" && !Array.isArray(raw) && "en-US" in raw) {
38123
+ return raw["en-US"];
38124
+ }
38125
+ return raw;
38126
+ }
38127
+ /**
38128
+ * Extract a linked entry/sys reference ID from a reference field.
38129
+ */
38130
+ static getRefId(entry, fieldName) {
38131
+ const ref = _ContentfulService.getField(entry, fieldName);
38132
+ return ref?.sys?.id;
38133
+ }
38134
+ /**
38135
+ * Extract an asset URL from a linked asset field.
38136
+ */
38137
+ static getAssetUrl(entry, fieldName) {
38138
+ const asset = _ContentfulService.getField(entry, fieldName);
38139
+ return asset?.sys?.id;
38140
+ }
38141
+ /**
38142
+ * Resolve an asset URL by ID from a list of fetched assets.
38143
+ */
38144
+ static resolveAssetUrl(assets, assetId) {
38145
+ const asset = assets.find((a) => a.sys.id === assetId);
38146
+ return asset?.fields?.file?.url;
38147
+ }
38148
+ // ─── Private ─────────────────────────────────────────────────────────────
38149
+ headers() {
38150
+ return {
38151
+ Authorization: `Bearer ${this.accessToken}`,
38152
+ "Content-Type": "application/json"
38153
+ };
38154
+ }
38155
+ buildUrl(path, params = {}) {
38156
+ const url2 = new URL(
38157
+ `/spaces/${this.spaceId}/environments/${this.environment}${path}`,
38158
+ CONTENTFUL_CDN
38159
+ );
38160
+ for (const [key, value] of Object.entries(params)) {
38161
+ url2.searchParams.set(key, value);
38162
+ }
38163
+ return url2.toString();
38164
+ }
38165
+ };
38166
+
38001
38167
  // src/services/snapshot.ts
38168
+ var DEFAULT_FIELDS = {
38169
+ event: {
38170
+ title: "title",
38171
+ slug: "slug",
38172
+ theme: "theme",
38173
+ organization: "organization",
38174
+ venue: "venue",
38175
+ date: "date",
38176
+ startTime: "startTime",
38177
+ endTime: "endTime",
38178
+ price: "price",
38179
+ image: "image",
38180
+ isSpotlight: "isSpotlight",
38181
+ maxSlots: "maxSlots",
38182
+ gformsUrl: "gformsUrl",
38183
+ gformsEditorUrl: "gformsEditorUrl",
38184
+ registrationClosesAt: "registrationClosesAt",
38185
+ classCode: "classCode"
38186
+ },
38187
+ theme: {
38188
+ name: "name",
38189
+ path: "path"
38190
+ },
38191
+ faq: {
38192
+ question: "question",
38193
+ answer: "answer",
38194
+ category: "category",
38195
+ sortOrder: "sortOrder"
38196
+ },
38197
+ organization: {
38198
+ name: "name",
38199
+ acronym: "acronym",
38200
+ logoUrl: "logoUrl",
38201
+ link: "link"
38202
+ },
38203
+ siteConfig: {
38204
+ key: "key",
38205
+ value: "value"
38206
+ }
38207
+ };
38208
+ var CONTENTFUL_CACHE_PREFIX = "contentful:cache";
38209
+ var CONTENTFUL_CACHE_TTL = 300;
38210
+ async function snapshotAllContent(db, bucket, contentful, config3 = {}, kv) {
38211
+ const mergedConfig = {
38212
+ eventTypeId: config3.eventTypeId ?? "event",
38213
+ themeTypeId: config3.themeTypeId ?? "theme",
38214
+ faqTypeId: config3.faqTypeId ?? "faq",
38215
+ organizationTypeId: config3.organizationTypeId ?? "organization",
38216
+ fields: {
38217
+ event: { ...DEFAULT_FIELDS.event, ...config3.fields?.event },
38218
+ theme: { ...DEFAULT_FIELDS.theme, ...config3.fields?.theme },
38219
+ faq: { ...DEFAULT_FIELDS.faq, ...config3.fields?.faq },
38220
+ organization: { ...DEFAULT_FIELDS.organization, ...config3.fields?.organization },
38221
+ siteConfig: { ...DEFAULT_FIELDS.siteConfig, ...config3.fields?.siteConfig }
38222
+ }
38223
+ };
38224
+ const result = {
38225
+ themesSynced: 0,
38226
+ eventsSynced: 0,
38227
+ faqsSynced: 0,
38228
+ organizationsSynced: 0,
38229
+ imagesUploaded: 0,
38230
+ imagesSkipped: 0,
38231
+ errors: []
38232
+ };
38233
+ let allAssets = [];
38234
+ if (bucket) {
38235
+ try {
38236
+ allAssets = await contentful.getAssets();
38237
+ } catch (err) {
38238
+ result.errors.push(`Failed to fetch assets: ${err}`);
38239
+ }
38240
+ }
38241
+ try {
38242
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:themes`;
38243
+ let themeEntries;
38244
+ if (kv) {
38245
+ const cached2 = await kv.get(cacheKey, "json");
38246
+ if (cached2) {
38247
+ themeEntries = cached2;
38248
+ } else {
38249
+ themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
38250
+ await kv.put(cacheKey, JSON.stringify(themeEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38251
+ }
38252
+ } else {
38253
+ themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
38254
+ }
38255
+ result.themesSynced = await syncThemes(db, themeEntries, mergedConfig);
38256
+ } catch (err) {
38257
+ result.errors.push(`Themes sync failed: ${err}`);
38258
+ }
38259
+ try {
38260
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:organizations`;
38261
+ let orgEntries;
38262
+ if (kv) {
38263
+ const cached2 = await kv.get(cacheKey, "json");
38264
+ if (cached2) {
38265
+ orgEntries = cached2;
38266
+ } else {
38267
+ orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
38268
+ await kv.put(cacheKey, JSON.stringify(orgEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38269
+ }
38270
+ } else {
38271
+ orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
38272
+ }
38273
+ await syncOrganizations(db, orgEntries, mergedConfig);
38274
+ } catch (err) {
38275
+ result.errors.push(`Organizations sync failed: ${err}`);
38276
+ }
38277
+ try {
38278
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:events`;
38279
+ let eventEntries;
38280
+ if (kv) {
38281
+ const cached2 = await kv.get(cacheKey, "json");
38282
+ if (cached2) {
38283
+ eventEntries = cached2;
38284
+ } else {
38285
+ eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
38286
+ await kv.put(cacheKey, JSON.stringify(eventEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38287
+ }
38288
+ } else {
38289
+ eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
38290
+ }
38291
+ result.eventsSynced = await syncEvents(db, bucket, contentful, allAssets, eventEntries, mergedConfig, result);
38292
+ } catch (err) {
38293
+ result.errors.push(`Events sync failed: ${err}`);
38294
+ }
38295
+ try {
38296
+ const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:faqs`;
38297
+ let faqEntries;
38298
+ if (kv) {
38299
+ const cached2 = await kv.get(cacheKey, "json");
38300
+ if (cached2) {
38301
+ faqEntries = cached2;
38302
+ } else {
38303
+ faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
38304
+ await kv.put(cacheKey, JSON.stringify(faqEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
38305
+ }
38306
+ } else {
38307
+ faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
38308
+ }
38309
+ result.faqsSynced = await syncFaqs(db, faqEntries, mergedConfig);
38310
+ } catch (err) {
38311
+ result.errors.push(`FAQs sync failed: ${err}`);
38312
+ }
38313
+ console.log(
38314
+ `[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`
38315
+ );
38316
+ return result;
38317
+ }
38318
+ async function syncThemes(db, entries, config3) {
38319
+ let count2 = 0;
38320
+ for (const entry of entries) {
38321
+ const cfId = entry.sys.id;
38322
+ const name = ContentfulService.getField(entry, config3.fields.theme.name);
38323
+ const path = ContentfulService.getField(entry, config3.fields.theme.path);
38324
+ if (!name || !path) continue;
38325
+ await db.insert(themes).values({ id: cfId, name, path }).onConflictDoUpdate({
38326
+ target: themes.id,
38327
+ set: { name, path }
38328
+ });
38329
+ count2++;
38330
+ }
38331
+ return count2;
38332
+ }
38333
+ async function syncOrganizations(db, entries, config3) {
38334
+ let count2 = 0;
38335
+ for (const entry of entries) {
38336
+ const cfId = entry.sys.id;
38337
+ const f = config3.fields.organization;
38338
+ const name = ContentfulService.getField(entry, f.name);
38339
+ const acronym = ContentfulService.getField(entry, f.acronym);
38340
+ if (!name || !acronym) continue;
38341
+ const logoUrl = ContentfulService.getField(entry, f.logoUrl) ?? null;
38342
+ const link = ContentfulService.getField(entry, f.link) ?? null;
38343
+ await db.insert(organizations).values({ id: cfId, name, acronym, logoUrl, link }).onConflictDoUpdate({
38344
+ target: organizations.id,
38345
+ set: { name, acronym, logoUrl, link }
38346
+ });
38347
+ count2++;
38348
+ }
38349
+ return count2;
38350
+ }
38351
+ async function syncEvents(db, bucket, contentful, allAssets, entries, config3, result) {
38352
+ let count2 = 0;
38353
+ for (const entry of entries) {
38354
+ try {
38355
+ const cfId = entry.sys.id;
38356
+ const f = config3.fields.event;
38357
+ const title = ContentfulService.getField(entry, f.title);
38358
+ if (!title) {
38359
+ result.errors.push(`Event ${cfId}: missing title, skipping`);
38360
+ continue;
38361
+ }
38362
+ const slug = ContentfulService.getField(entry, f.slug) ?? slugify2(title);
38363
+ const themeRef = ContentfulService.getField(entry, f.theme);
38364
+ const themeId = themeRef?.sys?.id ?? null;
38365
+ const orgRef = ContentfulService.getField(entry, f.organization);
38366
+ const organizationId = orgRef?.sys?.id ?? null;
38367
+ let backgroundImageUrl = null;
38368
+ if (bucket) {
38369
+ const imageRef = ContentfulService.getField(entry, f.image);
38370
+ const assetId = imageRef?.sys?.id;
38371
+ if (assetId) {
38372
+ const assetUrl = ContentfulService.resolveAssetUrl(allAssets, assetId);
38373
+ if (assetUrl) {
38374
+ const r2Key = `contentful/${assetId}`;
38375
+ const uploaded = await uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key);
38376
+ if (uploaded.skipped) {
38377
+ result.imagesSkipped++;
38378
+ } else {
38379
+ result.imagesUploaded++;
38380
+ }
38381
+ backgroundImageUrl = `/uploads/images/${r2Key}`;
38382
+ }
38383
+ }
38384
+ }
38385
+ const values = {
38386
+ id: cfId,
38387
+ contentfulEntryId: cfId,
38388
+ title,
38389
+ slug,
38390
+ themeId,
38391
+ organizationId,
38392
+ updatedAt: entry.sys.updatedAt
38393
+ };
38394
+ const venue = ContentfulService.getField(entry, f.venue);
38395
+ if (venue !== void 0) values.venue = venue;
38396
+ const date5 = ContentfulService.getField(entry, f.date);
38397
+ if (date5 !== void 0) values.dateTime = date5;
38398
+ const price = ContentfulService.getField(entry, f.price);
38399
+ if (price !== void 0) values.price = price;
38400
+ if (backgroundImageUrl) values.backgroundImageUrl = backgroundImageUrl;
38401
+ const isSpotlight = ContentfulService.getField(entry, f.isSpotlight);
38402
+ if (isSpotlight !== void 0) values.isSpotlight = isSpotlight;
38403
+ const maxSlots = ContentfulService.getField(entry, f.maxSlots);
38404
+ if (maxSlots !== void 0) values.maxSlots = maxSlots;
38405
+ const gformsUrl = ContentfulService.getField(entry, f.gformsUrl);
38406
+ if (gformsUrl !== void 0) values.gformsUrl = gformsUrl;
38407
+ const gformsEditorUrl = ContentfulService.getField(entry, f.gformsEditorUrl);
38408
+ if (gformsEditorUrl !== void 0) values.gformsEditorUrl = gformsEditorUrl;
38409
+ const classCode = ContentfulService.getField(entry, f.classCode);
38410
+ if (classCode !== void 0) values.classCode = classCode;
38411
+ const startTime = ContentfulService.getField(entry, f.startTime);
38412
+ if (startTime !== void 0) values.startTime = startTime;
38413
+ const endTime = ContentfulService.getField(entry, f.endTime);
38414
+ if (endTime !== void 0) values.endTime = endTime;
38415
+ const regCloseRaw = ContentfulService.getField(entry, f.registrationClosesAt);
38416
+ if (regCloseRaw) {
38417
+ const ms = Date.parse(regCloseRaw);
38418
+ if (!Number.isNaN(ms)) values.registrationClosesAt = Math.floor(ms / 1e3);
38419
+ }
38420
+ await db.insert(events).values(values).onConflictDoUpdate({
38421
+ target: events.id,
38422
+ set: values
38423
+ });
38424
+ count2++;
38425
+ } catch (err) {
38426
+ result.errors.push(`Event ${entry.sys.id}: ${err}`);
38427
+ }
38428
+ }
38429
+ return count2;
38430
+ }
38431
+ async function syncFaqs(db, entries, config3) {
38432
+ let count2 = 0;
38433
+ for (const entry of entries) {
38434
+ const cfId = entry.sys.id;
38435
+ const f = config3.fields.faq;
38436
+ const question = ContentfulService.getField(entry, f.question);
38437
+ const answer = ContentfulService.getField(entry, f.answer);
38438
+ if (!question || !answer) continue;
38439
+ const category = ContentfulService.getField(entry, f.category) ?? null;
38440
+ const sortOrder = ContentfulService.getField(entry, f.sortOrder) ?? 0;
38441
+ await db.insert(faqs).values({
38442
+ id: cfId,
38443
+ question,
38444
+ answer,
38445
+ category,
38446
+ sortOrder
38447
+ }).onConflictDoUpdate({
38448
+ target: faqs.id,
38449
+ set: { question, answer, category, sortOrder }
38450
+ });
38451
+ count2++;
38452
+ }
38453
+ return count2;
38454
+ }
38455
+ async function uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key) {
38456
+ const { data, contentType } = await contentful.downloadAsset(assetUrl);
38457
+ const sha = await computeSha256(data);
38458
+ const existing = await bucket.head(r2Key);
38459
+ if (existing?.customMetadata?.sha256 === sha) {
38460
+ return { skipped: true };
38461
+ }
38462
+ await bucket.put(r2Key, data, {
38463
+ httpMetadata: { contentType },
38464
+ customMetadata: {
38465
+ sha256: sha,
38466
+ source: "contentful",
38467
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
38468
+ }
38469
+ });
38470
+ return { skipped: false };
38471
+ }
38002
38472
  async function batchRun(items, fn, concurrency = 5) {
38003
38473
  const results = [];
38004
38474
  for (let i = 0; i < items.length; i += concurrency) {
@@ -38008,6 +38478,13 @@ async function batchRun(items, fn, concurrency = 5) {
38008
38478
  }
38009
38479
  return results;
38010
38480
  }
38481
+ async function computeSha256(data) {
38482
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
38483
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
38484
+ }
38485
+ function slugify2(text2) {
38486
+ return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
38487
+ }
38011
38488
  async function ensureContentTypes(mgmt, config3 = {}) {
38012
38489
  const eventTypeId = config3.eventTypeId ?? "event";
38013
38490
  const themeTypeId = config3.themeTypeId ?? "theme";
@@ -38028,7 +38505,7 @@ async function ensureContentTypes(mgmt, config3 = {}) {
38028
38505
  { id: "endTime", name: "End Time", type: "Symbol" },
38029
38506
  { id: "price", name: "Price", type: "Symbol" },
38030
38507
  { id: "image", name: "Image", type: "Link", linkType: "Asset" },
38031
- { id: "isMajor", name: "Major Event", type: "Boolean" },
38508
+ { id: "isSpotlight", name: "Spotlight", type: "Boolean" },
38032
38509
  { id: "maxSlots", name: "Max Slots", type: "Integer" },
38033
38510
  { id: "gformsUrl", name: "Google Forms URL", type: "Symbol" },
38034
38511
  { id: "gformsEditorUrl", name: "Google Forms Editor URL", type: "Symbol" },
@@ -38113,7 +38590,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
38113
38590
  const fields = {
38114
38591
  title: ContentfulManagement.locale(event.title),
38115
38592
  slug: ContentfulManagement.locale(event.slug),
38116
- isMajor: ContentfulManagement.locale(event.isMajor),
38593
+ isSpotlight: ContentfulManagement.locale(event.isSpotlight),
38117
38594
  maxSlots: ContentfulManagement.locale(event.maxSlots)
38118
38595
  };
38119
38596
  if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
@@ -38170,7 +38647,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
38170
38647
  }
38171
38648
 
38172
38649
  // src/routes/site-config.ts
38173
- var siteConfigRoute = new chunkQARF2YFF_cjs.Hono();
38650
+ var siteConfigRoute = new chunkBFMJDSDI_cjs.Hono();
38174
38651
  siteConfigRoute.get("/", async (c) => {
38175
38652
  const db = createDb(c.env.DB);
38176
38653
  const rows = await db.query.siteConfig.findMany();
@@ -38184,6 +38661,9 @@ siteConfigRoute.get("/", async (c) => {
38184
38661
  siteName: config3.site_name ?? null,
38185
38662
  registrationGloballyOpen: config3.registration_globally_open ?? true,
38186
38663
  maintenanceMode: config3.maintenance_mode ?? false,
38664
+ // Prefer the D1-persisted value over the env-var default so that
38665
+ // PATCH /config/cms_mode changes are reflected immediately.
38666
+ cmsMode: config3.cms_mode ?? c.get("cmsMode"),
38187
38667
  now: Math.floor(Date.now() / 1e3)
38188
38668
  }
38189
38669
  });
@@ -38205,6 +38685,10 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
38205
38685
  var SYNC_LOCK_KEY = "contentful:sync:lock";
38206
38686
  var SYNC_LOCK_TTL = 60;
38207
38687
  siteConfigRoute.post("/sync-content", authMiddleware, adminMiddleware, async (c) => {
38688
+ const cmsMode = c.get("cmsMode");
38689
+ if (cmsMode === "cloudflare") {
38690
+ throw serviceUnavailable("Contentful sync is not available in Cloudflare-only mode.");
38691
+ }
38208
38692
  if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
38209
38693
  throw serviceUnavailable("Contentful Management API credentials not configured.");
38210
38694
  }
@@ -38238,7 +38722,7 @@ var faqSchema = external_exports.object({
38238
38722
  category: external_exports.string().optional(),
38239
38723
  sortOrder: external_exports.number().int().default(0)
38240
38724
  });
38241
- var faqsRoute = new chunkQARF2YFF_cjs.Hono();
38725
+ var faqsRoute = new chunkBFMJDSDI_cjs.Hono();
38242
38726
  async function pushFaqToContentful(env2, faq) {
38243
38727
  console.log("[Contentful] pushFaqToContentful called for FAQ:", faq.id);
38244
38728
  if (!ContentfulManagement.isConfigured(env2.CONTENTFUL_SPACE_ID, env2.CONTENTFUL_MANAGEMENT_TOKEN)) {
@@ -38291,7 +38775,9 @@ faqsRoute.post(
38291
38775
  const cache3 = new CacheService(c.env.KV);
38292
38776
  const [created] = await db.insert(faqs).values(body).returning();
38293
38777
  await cache3.del(FAQS_KV_KEY);
38294
- c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
38778
+ if (c.get("cmsMode") === "hybrid") {
38779
+ c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
38780
+ }
38295
38781
  return c.json({ data: created }, 201);
38296
38782
  }
38297
38783
  );
@@ -38304,7 +38790,9 @@ faqsRoute.patch("/:id", authMiddleware, adminMiddleware, async (c) => {
38304
38790
  const [updated] = await db.update(faqs).set({ ...body, updatedAt: now2 }).where(eq(faqs.id, id)).returning();
38305
38791
  if (!updated) throw notFound("FAQ");
38306
38792
  await cache3.del(FAQS_KV_KEY);
38307
- c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
38793
+ if (c.get("cmsMode") === "hybrid") {
38794
+ c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
38795
+ }
38308
38796
  return c.json({ data: updated });
38309
38797
  });
38310
38798
  faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
@@ -38318,7 +38806,7 @@ faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
38318
38806
  });
38319
38807
 
38320
38808
  // src/routes/internal/gforms-webhook.ts
38321
- var gformsWebhookRoute = new chunkQARF2YFF_cjs.Hono();
38809
+ var gformsWebhookRoute = new chunkBFMJDSDI_cjs.Hono();
38322
38810
  gformsWebhookRoute.post("/", internalMiddleware, async (c) => {
38323
38811
  const rawBody = await c.req.text();
38324
38812
  const signature = c.req.header("X-Goog-Signature");
@@ -38390,7 +38878,7 @@ var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
38390
38878
  "image/svg+xml"
38391
38879
  ]);
38392
38880
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
38393
- var uploadsRoute = new chunkQARF2YFF_cjs.Hono();
38881
+ var uploadsRoute = new chunkBFMJDSDI_cjs.Hono();
38394
38882
  uploadsRoute.get("/images/*", async (c) => {
38395
38883
  const bucket = c.env.FILES;
38396
38884
  if (!bucket) {
@@ -38564,7 +39052,7 @@ var createThemeSchema = external_exports.object({
38564
39052
  name: external_exports.string().min(1),
38565
39053
  path: external_exports.string().min(1)
38566
39054
  });
38567
- var themesRoute = new chunkQARF2YFF_cjs.Hono();
39055
+ var themesRoute = new chunkBFMJDSDI_cjs.Hono();
38568
39056
  themesRoute.get("/", async (c) => {
38569
39057
  const db = createDb(c.env.DB);
38570
39058
  const data = await db.select().from(themes);
@@ -38580,7 +39068,9 @@ themesRoute.post(
38580
39068
  const db = createDb(c.env.DB);
38581
39069
  try {
38582
39070
  const [created] = await db.insert(themes).values(body).returning();
38583
- c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
39071
+ if (c.get("cmsMode") === "hybrid") {
39072
+ c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
39073
+ }
38584
39074
  return c.json({ data: created }, 201);
38585
39075
  } catch (err) {
38586
39076
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -38601,7 +39091,9 @@ themesRoute.patch(
38601
39091
  try {
38602
39092
  const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
38603
39093
  if (!updated) throw notFound("Theme");
38604
- c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
39094
+ if (c.get("cmsMode") === "hybrid") {
39095
+ c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
39096
+ }
38605
39097
  return c.json({ data: updated });
38606
39098
  } catch (err) {
38607
39099
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -38616,7 +39108,9 @@ themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
38616
39108
  const db = createDb(c.env.DB);
38617
39109
  const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
38618
39110
  if (!deleted) throw notFound("Theme");
38619
- c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
39111
+ if (c.get("cmsMode") === "hybrid") {
39112
+ c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
39113
+ }
38620
39114
  return c.body(null, 204);
38621
39115
  });
38622
39116
 
@@ -38627,7 +39121,7 @@ var createOrganizationSchema = external_exports.object({
38627
39121
  logoUrl: external_exports.string().url().nullable().optional(),
38628
39122
  link: external_exports.string().url().nullable().optional()
38629
39123
  });
38630
- var organizationsRoute = new chunkQARF2YFF_cjs.Hono();
39124
+ var organizationsRoute = new chunkBFMJDSDI_cjs.Hono();
38631
39125
  organizationsRoute.get("/", async (c) => {
38632
39126
  const db = createDb(c.env.DB);
38633
39127
  const data = await db.select().from(organizations);
@@ -38680,9 +39174,86 @@ organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) =>
38680
39174
  return c.body(null, 204);
38681
39175
  });
38682
39176
 
39177
+ // src/routes/contentful-sync.ts
39178
+ var contentfulSyncRoute = new chunkBFMJDSDI_cjs.Hono();
39179
+ contentfulSyncRoute.post(
39180
+ "/trigger",
39181
+ authMiddleware,
39182
+ adminMiddleware,
39183
+ async (c) => {
39184
+ const env2 = c.env;
39185
+ const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
39186
+ if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_ACCESS_TOKEN) {
39187
+ return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful credentials missing" } }, 400);
39188
+ }
39189
+ const db = createDb(env2.DB);
39190
+ const contentful = new ContentfulService(CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_ENVIRONMENT);
39191
+ try {
39192
+ if (CONTENTFUL_MANAGEMENT_TOKEN) {
39193
+ const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
39194
+ await ensureContentTypes(mgmt);
39195
+ }
39196
+ c.executionCtx.waitUntil(
39197
+ 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))
39198
+ );
39199
+ return c.json({ message: "Contentful sync triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
39200
+ } catch (err) {
39201
+ console.error("[Contentful] Sync trigger failed:", err);
39202
+ return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger sync" } }, 500);
39203
+ }
39204
+ }
39205
+ );
39206
+ contentfulSyncRoute.post(
39207
+ "/push",
39208
+ authMiddleware,
39209
+ adminMiddleware,
39210
+ async (c) => {
39211
+ const env2 = c.env;
39212
+ const { CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
39213
+ if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_MANAGEMENT_TOKEN) {
39214
+ return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful management credentials missing" } }, 400);
39215
+ }
39216
+ const db = createDb(env2.DB);
39217
+ const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
39218
+ try {
39219
+ c.executionCtx.waitUntil(
39220
+ pushToContentful(db, mgmt, {}, env2.KV).then((r) => console.log("[Contentful] Push complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Push failed:", err))
39221
+ );
39222
+ return c.json({ message: "Contentful push triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
39223
+ } catch (err) {
39224
+ console.error("[Contentful] Push trigger failed:", err);
39225
+ return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger push" } }, 500);
39226
+ }
39227
+ }
39228
+ );
39229
+ contentfulSyncRoute.get(
39230
+ "/status",
39231
+ authMiddleware,
39232
+ adminMiddleware,
39233
+ async (c) => {
39234
+ const env2 = c.env;
39235
+ const db = createDb(env2.DB);
39236
+ const [[{ themes: themes2 }], [{ events: events2 }], [{ faqs: faqs2 }], [{ orgs }]] = await Promise.all([
39237
+ db.all(sql2`SELECT count(*) as themes FROM themes`),
39238
+ db.all(sql2`SELECT count(*) as events FROM events`),
39239
+ db.all(sql2`SELECT count(*) as faqs FROM faqs`),
39240
+ db.all(sql2`SELECT count(*) as orgs FROM organizations`)
39241
+ ]);
39242
+ const isConfigured = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_ACCESS_TOKEN;
39243
+ const canPush = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_MANAGEMENT_TOKEN;
39244
+ return c.json({
39245
+ isConfigured,
39246
+ canPush,
39247
+ cmsMode: c.get("cmsMode"),
39248
+ spaceId: env2.CONTENTFUL_SPACE_ID ?? null,
39249
+ totals: { themes: themes2, events: events2, faqs: faqs2, organizations: orgs }
39250
+ });
39251
+ }
39252
+ );
39253
+
38683
39254
  // src/app.ts
38684
39255
  function createApp(options = {}) {
38685
- const app = new chunkQARF2YFF_cjs.Hono();
39256
+ const app = new chunkBFMJDSDI_cjs.Hono();
38686
39257
  if (options.gformsWebhookUrl) {
38687
39258
  const webhookUrl = `${options.gformsWebhookUrl.replace(/\/$/, "")}/internal/gforms-webhook`;
38688
39259
  app.use("*", async (c, next) => {
@@ -38691,8 +39262,14 @@ function createApp(options = {}) {
38691
39262
  });
38692
39263
  }
38693
39264
  app.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
38694
- app.use("*", chunkQARF2YFF_cjs.createPowChallengeMiddleware());
39265
+ app.use("*", chunkBFMJDSDI_cjs.createPowChallengeMiddleware());
38695
39266
  app.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
39267
+ app.use("*", async (c, next) => {
39268
+ const overrideRaw = await c.env.KV.get("config:cms_mode").catch(() => null);
39269
+ const override = overrideRaw ? JSON.parse(overrideRaw) : null;
39270
+ c.set("cmsMode", parseCmsMode(override ?? c.env.CMS_MODE));
39271
+ return next();
39272
+ });
38696
39273
  app.on(["POST", "GET"], "/api/auth/*", (c) => {
38697
39274
  const auth = createAuth(c.env);
38698
39275
  const req = c.req.raw;
@@ -38704,7 +39281,7 @@ function createApp(options = {}) {
38704
39281
  return auth.handler(new Request(req.url, {
38705
39282
  method: req.method,
38706
39283
  headers: newHeaders,
38707
- body: req.method === "GET" ? null : req.body,
39284
+ body: req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS" ? null : req.body,
38708
39285
  redirect: req.redirect
38709
39286
  }));
38710
39287
  }
@@ -38722,15 +39299,16 @@ function createApp(options = {}) {
38722
39299
  }
38723
39300
  return next();
38724
39301
  });
38725
- app.post(chunkQARF2YFF_cjs.POW_VERIFY_PATH, chunkQARF2YFF_cjs.handlePowVerify);
39302
+ app.post(chunkBFMJDSDI_cjs.POW_VERIFY_PATH, chunkBFMJDSDI_cjs.handlePowVerify);
38726
39303
  app.route("/health", healthRoute);
38727
39304
  app.route("/api/config", siteConfigRoute);
38728
- app.route("/api/events", eventsRoute);
39305
+ app.route("/api/classes", classesRoute);
38729
39306
  app.route("/api/themes", themesRoute);
38730
39307
  app.route("/api/users", usersRoute);
38731
39308
  app.route("/api/organizations", organizationsRoute);
38732
39309
  app.route("/api/faqs", faqsRoute);
38733
39310
  app.route("/api/uploads", uploadsRoute);
39311
+ app.route("/api/contentful", contentfulSyncRoute);
38734
39312
  app.route("/internal/gforms-webhook", gformsWebhookRoute);
38735
39313
  app.onError(errorHandler);
38736
39314
  app.notFound(
@@ -39364,6 +39942,7 @@ var PATCH_STATEMENTS = [
39364
39942
  `ALTER TABLE "events" ADD COLUMN "class_code" text`,
39365
39943
  `ALTER TABLE "events" ADD COLUMN "start_time" text`,
39366
39944
  `ALTER TABLE "events" ADD COLUMN "end_time" text`,
39945
+ `ALTER TABLE "events" RENAME COLUMN "is_major" TO "is_spotlight"`,
39367
39946
  `CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`
39368
39947
  ];
39369
39948
  var CREATE_STATEMENTS = [
@@ -39467,7 +40046,7 @@ var CREATE_STATEMENTS = [
39467
40046
  "class_code" text,
39468
40047
  "start_time" text,
39469
40048
  "end_time" text,
39470
- "is_major" integer DEFAULT false NOT NULL,
40049
+ "is_spotlight" integer DEFAULT false NOT NULL,
39471
40050
  "max_slots" integer DEFAULT 0 NOT NULL,
39472
40051
  "registered_slots" integer DEFAULT 0 NOT NULL,
39473
40052
  "gforms_id" text,
@@ -39529,7 +40108,9 @@ async function ensureDatabase(d1) {
39529
40108
  try {
39530
40109
  await d1.prepare(sql3).run();
39531
40110
  } catch (err) {
39532
- if (err?.message?.includes("duplicate column")) continue;
40111
+ if (err?.message?.includes("duplicate column") || err?.message?.includes("no such column: is_major")) {
40112
+ continue;
40113
+ }
39533
40114
  throw err;
39534
40115
  }
39535
40116
  }
@@ -39538,12 +40119,14 @@ async function ensureDatabase(d1) {
39538
40119
  // src/worker-handler.ts
39539
40120
  var API_PREFIXES = [
39540
40121
  "/api/auth/",
39541
- "/api/events",
40122
+ "/api/classes",
39542
40123
  "/api/users",
40124
+ "/api/organizations",
39543
40125
  "/api/faqs",
39544
40126
  "/api/themes",
39545
40127
  "/api/config",
39546
40128
  "/api/uploads",
40129
+ "/api/contentful",
39547
40130
  "/health",
39548
40131
  "/internal/",
39549
40132
  "/.well-known/"
@@ -39582,7 +40165,7 @@ function createWorkerHandler(options) {
39582
40165
  return getLeapify(env2).fetch(request, env2, ctx);
39583
40166
  }
39584
40167
  let response = await options.serveFrontend(request, env2, ctx);
39585
- if ((!response || response.status === 404) && !pathname.includes(".")) {
40168
+ if ((!response || response.status === 404) && !pathname.includes(".") && request.method === "GET") {
39586
40169
  const indexRequest = new Request(new URL("/", request.url), request);
39587
40170
  response = await options.serveFrontend(indexRequest, env2, ctx);
39588
40171
  }
@@ -39690,6 +40273,7 @@ function injectConfig2(html2, config3) {
39690
40273
  (*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
39691
40274
  */
39692
40275
 
40276
+ exports.CONTENTFUL_CONFIG_KEYS = CONTENTFUL_CONFIG_KEYS;
39693
40277
  exports.ContentfulManagement = ContentfulManagement;
39694
40278
  exports.authAccount = authAccount;
39695
40279
  exports.authSession = authSession;
@@ -39697,6 +40281,7 @@ exports.authUser = authUser;
39697
40281
  exports.authVerification = authVerification;
39698
40282
  exports.bookmarks = bookmarks;
39699
40283
  exports.bookmarksRelations = bookmarksRelations;
40284
+ exports.contentfulConfig = contentfulConfig;
39700
40285
  exports.createDb = createDb;
39701
40286
  exports.createLeapify = createLeapify;
39702
40287
  exports.createQueueHandler = createQueueHandler;
@@ -39710,6 +40295,9 @@ exports.getRuntimeConfig = getRuntimeConfig;
39710
40295
  exports.injectConfig = injectConfig2;
39711
40296
  exports.organizations = organizations;
39712
40297
  exports.organizationsRelations = organizationsRelations;
40298
+ exports.parseCmsMode = parseCmsMode;
40299
+ exports.shouldPullFromContentful = shouldPullFromContentful;
40300
+ exports.shouldPushToContentful = shouldPushToContentful;
39713
40301
  exports.siteConfig = siteConfig;
39714
40302
  exports.themes = themes;
39715
40303
  exports.themesRelations = themesRelations;