@access-dlsu/leapify 0.260601.2 → 0.260604.1

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 (40) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/auth/auth.d.ts +2 -2
  3. package/dist/{chunk-X4OB4DZ3.cjs → chunk-NYEPGZMP.cjs} +6 -2
  4. package/dist/chunk-NYEPGZMP.cjs.map +1 -0
  5. package/dist/{chunk-2JEY6TSO.js → chunk-WEW5LGZC.js} +6 -2
  6. package/dist/chunk-WEW5LGZC.js.map +1 -0
  7. package/dist/client/auth.d.ts +41 -41
  8. package/dist/client/index.cjs +0 -2
  9. package/dist/client/index.cjs.map +1 -1
  10. package/dist/client/index.d.ts +3 -1
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +0 -2
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/types.d.ts +4 -0
  15. package/dist/client/types.d.ts.map +1 -1
  16. package/dist/db/schema/themes.d.ts +74 -0
  17. package/dist/db/schema/themes.d.ts.map +1 -1
  18. package/dist/index.cjs +930 -476
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.js +929 -475
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/middleware/turnstile-challenge.cjs +6 -6
  23. package/dist/lib/middleware/turnstile-challenge.d.ts.map +1 -1
  24. package/dist/lib/middleware/turnstile-challenge.js +1 -1
  25. package/dist/routes/health.d.ts.map +1 -1
  26. package/dist/routes/internal/batch-release.d.ts.map +1 -1
  27. package/dist/routes/internal/gforms-webhook.d.ts.map +1 -1
  28. package/dist/routes/internal/reconcile-slots.d.ts.map +1 -1
  29. package/dist/routes/internal/reminder-emails.d.ts.map +1 -1
  30. package/dist/routes/internal/renew-watches.d.ts.map +1 -1
  31. package/dist/routes/site-config.d.ts.map +1 -1
  32. package/dist/routes/themes.d.ts.map +1 -1
  33. package/dist/routes/uploads.d.ts.map +1 -1
  34. package/dist/routes/users.d.ts.map +1 -1
  35. package/dist/worker-handler.d.ts.map +1 -1
  36. package/dist/worker.js +927 -471
  37. package/dist/worker.js.map +1 -1
  38. package/package.json +4 -1
  39. package/dist/chunk-2JEY6TSO.js.map +0 -1
  40. package/dist/chunk-X4OB4DZ3.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,15 +1,16 @@
1
- import { createTurnstileMiddleware, TURNSTILE_VERIFY_PATH, handleTurnstileVerify } from './chunk-2JEY6TSO.js';
1
+ import { createTurnstileMiddleware, TURNSTILE_VERIFY_PATH, handleTurnstileVerify } from './chunk-WEW5LGZC.js';
2
2
  import { __export } from './chunk-PZ5AY32C.js';
3
3
  import { Hono } from 'hono';
4
+ import { describeRoute, validator, openAPIRouteHandler } from 'hono-openapi';
5
+ import { swaggerUI } from '@hono/swagger-ui';
4
6
  import { cors } from 'hono/cors';
5
7
  import { drizzle } from 'drizzle-orm/d1';
6
8
  import { sqliteTable, integer, text, index, uniqueIndex } from 'drizzle-orm/sqlite-core';
7
- import { sql, relations, eq, and, count, isNotNull, lte } from 'drizzle-orm';
9
+ import { sql, relations, eq, and, asc, count, isNotNull, lte } from 'drizzle-orm';
8
10
  import { createMiddleware } from 'hono/factory';
9
11
  import { betterAuth } from 'better-auth';
10
12
  import { drizzleAdapter } from 'better-auth/adapters/drizzle';
11
13
  import { bearer } from 'better-auth/plugins';
12
- import { zValidator } from '@hono/zod-validator';
13
14
  import { z } from 'zod';
14
15
 
15
16
  // src/lib/errors.ts
@@ -20,8 +21,6 @@ var LeapifyError = class extends Error {
20
21
  this.code = code;
21
22
  this.name = "LeapifyError";
22
23
  }
23
- statusCode;
24
- code;
25
24
  };
26
25
  var unauthorized = (message = "Unauthorized") => new LeapifyError(401, "UNAUTHORIZED", message);
27
26
  var domainRestricted = () => new LeapifyError(
@@ -87,6 +86,10 @@ var themes = sqliteTable("themes", {
87
86
  name: text("name").notNull().unique(),
88
87
  path: text("path").notNull().unique(),
89
88
  // e.g. "/pirates-cove"
89
+ imageUrl: text("image_url"),
90
+ descriptionEn: text("description_en"),
91
+ descriptionFil: text("description_fil"),
92
+ sortOrder: integer("sort_order").notNull().default(0),
90
93
  createdAt: integer("created_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3)),
91
94
  updatedAt: integer("updated_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3))
92
95
  });
@@ -576,8 +579,6 @@ var internalMiddleware = createMiddleware(async (c, next) => {
576
579
  }
577
580
  return next();
578
581
  });
579
-
580
- // src/routes/health.ts
581
582
  var healthRoute = new Hono();
582
583
  async function probeResend(apiKey) {
583
584
  const start = Date.now();
@@ -679,76 +680,93 @@ async function probeGForms(serviceAccountJson) {
679
680
  };
680
681
  }
681
682
  }
682
- healthRoute.get("/", async (c) => {
683
- const env = c.env;
684
- const hasSes = Boolean(env.SES_REGION) && Boolean(env.SES_ACCESS_KEY_ID) && Boolean(env.SES_SECRET_ACCESS_KEY);
685
- const hasResend = Boolean(env.RESEND_API_KEY);
686
- let hasGForms = false;
687
- if (env.GFORMS_SERVICE_ACCOUNT_JSON) {
688
- try {
689
- const parsed = JSON.parse(env.GFORMS_SERVICE_ACCOUNT_JSON);
690
- hasGForms = Boolean(parsed.client_email && parsed.private_key);
691
- } catch {
683
+ healthRoute.get(
684
+ "/",
685
+ describeRoute({
686
+ tags: ["Health"],
687
+ summary: "Service health check",
688
+ responses: { 200: { description: "Health status of configured services" } }
689
+ }),
690
+ async (c) => {
691
+ const env = c.env;
692
+ const hasSes = Boolean(env.SES_REGION) && Boolean(env.SES_ACCESS_KEY_ID) && Boolean(env.SES_SECRET_ACCESS_KEY);
693
+ const hasResend = Boolean(env.RESEND_API_KEY);
694
+ let hasGForms = false;
695
+ if (env.GFORMS_SERVICE_ACCOUNT_JSON) {
696
+ try {
697
+ const parsed = JSON.parse(env.GFORMS_SERVICE_ACCOUNT_JSON);
698
+ hasGForms = Boolean(parsed.client_email && parsed.private_key);
699
+ } catch {
700
+ }
692
701
  }
693
- }
694
- const probes = [];
695
- if (hasSes) {
696
- probes.push(
697
- probeSes(env.SES_REGION, env.SES_ACCESS_KEY_ID, env.SES_SECRET_ACCESS_KEY).then(
698
- (h) => ["ses", h]
699
- )
700
- );
701
- }
702
- if (hasResend) {
703
- probes.push(
704
- probeResend(env.RESEND_API_KEY).then((h) => ["resend", h])
705
- );
706
- }
707
- if (hasGForms) {
708
- probes.push(
709
- probeGForms(env.GFORMS_SERVICE_ACCOUNT_JSON).then(
710
- (h) => ["gforms", h]
711
- )
712
- );
713
- }
714
- const results = await Promise.all(probes);
715
- const services = {};
716
- for (const [name, health] of results) {
717
- services[name] = health;
718
- }
719
- const allOk = results.length === 0 || results.every(([, h]) => h.ok);
720
- return c.json({
721
- data: {
722
- status: allOk ? "OK" : "DEGRADED",
723
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
724
- services
702
+ const probes = [];
703
+ if (hasSes) {
704
+ probes.push(
705
+ probeSes(env.SES_REGION, env.SES_ACCESS_KEY_ID, env.SES_SECRET_ACCESS_KEY).then(
706
+ (h) => ["ses", h]
707
+ )
708
+ );
725
709
  }
726
- });
727
- });
728
- healthRoute.post("/queue-burst", authMiddleware, adminMiddleware, async (c) => {
729
- if (!c.env.EMAIL_QUEUE) {
730
- return c.json({ error: "Queue binding missing" }, 400);
731
- }
732
- const batch = Array.from({ length: 100 }, (_, i) => ({
733
- body: {
734
- type: "audit_log",
735
- payload: {
736
- action: "queue_load_test",
737
- userId: "system",
738
- meta: { index: i, time: Date.now() }
710
+ if (hasResend) {
711
+ probes.push(
712
+ probeResend(env.RESEND_API_KEY).then((h) => ["resend", h])
713
+ );
714
+ }
715
+ if (hasGForms) {
716
+ probes.push(
717
+ probeGForms(env.GFORMS_SERVICE_ACCOUNT_JSON).then(
718
+ (h) => ["gforms", h]
719
+ )
720
+ );
721
+ }
722
+ const results = await Promise.all(probes);
723
+ const services = {};
724
+ for (const [name, health] of results) {
725
+ services[name] = health;
726
+ }
727
+ const allOk = results.length === 0 || results.every(([, h]) => h.ok);
728
+ return c.json({
729
+ data: {
730
+ status: allOk ? "OK" : "DEGRADED",
731
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
732
+ services
739
733
  }
734
+ });
735
+ }
736
+ );
737
+ healthRoute.post(
738
+ "/queue-burst",
739
+ describeRoute({
740
+ tags: ["Health"],
741
+ summary: "Queue load test (admin)",
742
+ responses: { 200: { description: "Items queued" } }
743
+ }),
744
+ authMiddleware,
745
+ adminMiddleware,
746
+ async (c) => {
747
+ if (!c.env.EMAIL_QUEUE) {
748
+ return c.json({ error: "Queue binding missing" }, 400);
740
749
  }
741
- }));
742
- await c.env.EMAIL_QUEUE.sendBatch(batch);
743
- return c.json({ status: "queued", count: 100 });
744
- });
750
+ const batch = Array.from({ length: 100 }, (_, i) => ({
751
+ body: {
752
+ type: "audit_log",
753
+ payload: {
754
+ action: "queue_load_test",
755
+ userId: "system",
756
+ meta: { index: i, time: Date.now() }
757
+ }
758
+ }
759
+ }));
760
+ await c.env.EMAIL_QUEUE.sendBatch(batch);
761
+ return c.json({ status: "queued", count: 100 });
762
+ }
763
+ );
745
764
 
746
765
  // src/services/cache.ts
747
766
  var CacheService = class {
748
767
  constructor(kv) {
749
768
  this.kv = kv;
750
769
  }
751
- kv;
752
770
  async get(key) {
753
771
  return this.kv.get(key, "json");
754
772
  }
@@ -794,8 +812,6 @@ var SlotsService = class {
794
812
  this.db = db;
795
813
  this.cache = cache;
796
814
  }
797
- db;
798
- cache;
799
815
  kvKey(slug) {
800
816
  return `${SLOT_KV_PREFIX}${slug}`;
801
817
  }
@@ -1117,143 +1133,221 @@ var classesRoute = new Hono();
1117
1133
  function generateSlug(title) {
1118
1134
  return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1119
1135
  }
1120
- classesRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
1121
- const db = createDb(c.env.DB);
1122
- const data = await db.query.events.findMany({
1123
- with: { theme: true, organization: true },
1124
- orderBy: (e, { desc }) => [desc(e.createdAt)]
1125
- });
1126
- return c.json({ data });
1127
- });
1128
- classesRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
1129
- const body = await c.req.json();
1130
- const db = createDb(c.env.DB);
1131
- const cache = new CacheService(c.env.KV);
1132
- if (!body.ids?.length) {
1133
- return c.json({ error: "ids are required" }, 400);
1134
- }
1135
- if (body.releaseAt) {
1136
- await db.update(events).set({ releaseAt: body.releaseAt, status: "queued" }).where(
1137
- sql`${events.id} IN (${sql.join(
1138
- body.ids.map((id) => sql`${id}`),
1139
- sql`, `
1140
- )})`
1136
+ classesRoute.get(
1137
+ "/admin",
1138
+ describeRoute({
1139
+ tags: ["Events"],
1140
+ summary: "List all events (admin)",
1141
+ responses: { 200: { description: "List of all events" } }
1142
+ }),
1143
+ authMiddleware,
1144
+ adminMiddleware,
1145
+ async (c) => {
1146
+ const db = createDb(c.env.DB);
1147
+ const data = await db.query.events.findMany({
1148
+ with: { theme: true, organization: true },
1149
+ orderBy: (e, { desc }) => [desc(e.createdAt)]
1150
+ });
1151
+ return c.json({ data });
1152
+ }
1153
+ );
1154
+ classesRoute.post(
1155
+ "/admin/publish",
1156
+ describeRoute({
1157
+ tags: ["Events"],
1158
+ summary: "Batch publish queued events",
1159
+ responses: {
1160
+ 200: { description: "Events published successfully" },
1161
+ 400: { description: "Missing event IDs" }
1162
+ }
1163
+ }),
1164
+ authMiddleware,
1165
+ adminMiddleware,
1166
+ async (c) => {
1167
+ const body = await c.req.json();
1168
+ const db = createDb(c.env.DB);
1169
+ const cache = new CacheService(c.env.KV);
1170
+ if (!body.ids?.length) {
1171
+ return c.json({ error: "ids are required" }, 400);
1172
+ }
1173
+ if (body.releaseAt) {
1174
+ await db.update(events).set({ releaseAt: body.releaseAt, status: "queued" }).where(
1175
+ sql`${events.id} IN (${sql.join(
1176
+ body.ids.map((id) => sql`${id}`),
1177
+ sql`, `
1178
+ )})`
1179
+ );
1180
+ } else {
1181
+ await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
1182
+ sql`${events.id} IN (${sql.join(
1183
+ body.ids.map((id) => sql`${id}`),
1184
+ sql`, `
1185
+ )})`
1186
+ );
1187
+ }
1188
+ await Promise.all([
1189
+ cache.del(EVENTS_LIST_KV_KEY),
1190
+ cache.del(EVENTS_ETAG_KV_KEY)
1191
+ ]);
1192
+ return c.json({ data: { updated: body.ids.length } });
1193
+ }
1194
+ );
1195
+ classesRoute.get(
1196
+ "/",
1197
+ describeRoute({
1198
+ tags: ["Events"],
1199
+ summary: "List published events",
1200
+ responses: { 200: { description: "List of published events with themes" } }
1201
+ }),
1202
+ eventsListRateLimit,
1203
+ async (c) => {
1204
+ const db = createDb(c.env.DB);
1205
+ const cache = new CacheService(c.env.KV);
1206
+ const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
1207
+ const etag = await cache.getOrSet(
1208
+ EVENTS_ETAG_KV_KEY,
1209
+ () => cache.generateETag(String(latest?.max ?? "0")),
1210
+ 300
1211
+ );
1212
+ const ifNoneMatch = c.req.header("If-None-Match");
1213
+ if (ifNoneMatch === etag) {
1214
+ return c.body(null, 304);
1215
+ }
1216
+ const data = await cache.getOrSet(
1217
+ EVENTS_LIST_KV_KEY,
1218
+ () => db.query.events.findMany({
1219
+ where: eq(events.status, "published"),
1220
+ with: {
1221
+ theme: true,
1222
+ organization: true
1223
+ },
1224
+ columns: {
1225
+ id: true,
1226
+ slug: true,
1227
+ themeId: true,
1228
+ organizationId: true,
1229
+ title: true,
1230
+ venue: true,
1231
+ dateTime: true,
1232
+ price: true,
1233
+ backgroundImageUrl: true,
1234
+ classCode: true,
1235
+ startTime: true,
1236
+ endTime: true,
1237
+ registrationClosesAt: true,
1238
+ isSpotlight: true,
1239
+ maxSlots: true,
1240
+ registeredSlots: true,
1241
+ gformsUrl: true,
1242
+ gformsEditorUrl: true,
1243
+ publishedAt: true
1244
+ }
1245
+ }),
1246
+ EVENTS_LIST_TTL
1141
1247
  );
1142
- } else {
1143
- await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
1144
- sql`${events.id} IN (${sql.join(
1145
- body.ids.map((id) => sql`${id}`),
1146
- sql`, `
1147
- )})`
1248
+ c.header("ETag", etag);
1249
+ c.header(
1250
+ "Cache-Control",
1251
+ "public, max-age=604800, stale-while-revalidate=86400"
1148
1252
  );
1253
+ return c.json({ data });
1149
1254
  }
1150
- await Promise.all([
1151
- cache.del(EVENTS_LIST_KV_KEY),
1152
- cache.del(EVENTS_ETAG_KV_KEY)
1153
- ]);
1154
- return c.json({ data: { updated: body.ids.length } });
1155
- });
1156
- classesRoute.get("/", eventsListRateLimit, async (c) => {
1157
- const db = createDb(c.env.DB);
1158
- const cache = new CacheService(c.env.KV);
1159
- const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
1160
- const etag = await cache.getOrSet(
1161
- EVENTS_ETAG_KV_KEY,
1162
- () => cache.generateETag(String(latest?.max ?? "0")),
1163
- 300
1164
- );
1165
- const ifNoneMatch = c.req.header("If-None-Match");
1166
- if (ifNoneMatch === etag) {
1167
- return c.body(null, 304);
1168
- }
1169
- const data = await cache.getOrSet(
1170
- EVENTS_LIST_KV_KEY,
1171
- () => db.query.events.findMany({
1172
- where: eq(events.status, "published"),
1255
+ );
1256
+ classesRoute.get(
1257
+ "/:slug",
1258
+ describeRoute({
1259
+ tags: ["Events"],
1260
+ summary: "Get event by slug",
1261
+ responses: {
1262
+ 200: { description: "Event details" },
1263
+ 404: { description: "Event not found" }
1264
+ }
1265
+ }),
1266
+ async (c) => {
1267
+ const { slug } = c.req.param();
1268
+ const db = createDb(c.env.DB);
1269
+ const event = await db.query.events.findFirst({
1270
+ where: and(eq(events.slug, slug), eq(events.status, "published")),
1173
1271
  with: {
1174
- theme: true,
1175
- organization: true
1176
- },
1177
- columns: {
1178
- id: true,
1179
- slug: true,
1180
- themeId: true,
1181
- organizationId: true,
1182
- title: true,
1183
- venue: true,
1184
- dateTime: true,
1185
- price: true,
1186
- backgroundImageUrl: true,
1187
- classCode: true,
1188
- startTime: true,
1189
- endTime: true,
1190
- registrationClosesAt: true,
1191
- isSpotlight: true,
1192
- maxSlots: true,
1193
- registeredSlots: true,
1194
- gformsUrl: true,
1195
- gformsEditorUrl: true,
1196
- publishedAt: true
1272
+ theme: true
1197
1273
  }
1198
- }),
1199
- EVENTS_LIST_TTL
1200
- );
1201
- c.header("ETag", etag);
1202
- c.header(
1203
- "Cache-Control",
1204
- "public, max-age=604800, stale-while-revalidate=86400"
1205
- );
1206
- return c.json({ data });
1207
- });
1208
- classesRoute.get("/:slug", async (c) => {
1209
- const { slug } = c.req.param();
1210
- const db = createDb(c.env.DB);
1211
- const event = await db.query.events.findFirst({
1212
- where: and(eq(events.slug, slug), eq(events.status, "published")),
1213
- with: {
1214
- theme: true
1274
+ });
1275
+ if (!event) throw notFound("Event");
1276
+ return c.json({ data: event });
1277
+ }
1278
+ );
1279
+ classesRoute.get(
1280
+ "/:slug/slots",
1281
+ describeRoute({
1282
+ tags: ["Events"],
1283
+ summary: "Get event slot availability",
1284
+ responses: {
1285
+ 200: { description: "Slot availability info" },
1286
+ 404: { description: "Event not found" }
1215
1287
  }
1216
- });
1217
- if (!event) throw notFound("Event");
1218
- return c.json({ data: event });
1219
- });
1220
- classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
1221
- const { slug } = c.req.param();
1222
- const db = createDb(c.env.DB);
1223
- const cache = new CacheService(c.env.KV);
1224
- const slotsService = new SlotsService(db, cache);
1225
- const info = await slotsService.getSlots(slug);
1226
- if (!info) throw notFound("Event");
1227
- c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
1228
- return c.json({ data: info });
1229
- });
1230
- classesRoute.post("/:slug/reconcile", authMiddleware, adminMiddleware, async (c) => {
1231
- const { slug } = c.req.param();
1232
- const db = createDb(c.env.DB);
1233
- const cache = new CacheService(c.env.KV);
1234
- const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
1235
- const slots = new SlotsService(db, cache);
1236
- const event = await db.query.events.findFirst({
1237
- where: eq(events.slug, slug),
1238
- columns: { gformsId: true }
1239
- });
1240
- if (!event) throw notFound("Event");
1241
- if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
1242
- try {
1243
- const googleCount = await gforms.getExactResponseCount(event.gformsId);
1244
- await slots.correctCount(slug, googleCount);
1245
- return c.json({ data: { registeredSlots: googleCount } });
1246
- } catch (err) {
1247
- const message = err?.message ?? "Failed to fetch from Google Forms API";
1248
- return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
1288
+ }),
1289
+ eventsSlotsRateLimit,
1290
+ async (c) => {
1291
+ const { slug } = c.req.param();
1292
+ const db = createDb(c.env.DB);
1293
+ const cache = new CacheService(c.env.KV);
1294
+ const slotsService = new SlotsService(db, cache);
1295
+ const info = await slotsService.getSlots(slug);
1296
+ if (!info) throw notFound("Event");
1297
+ c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
1298
+ return c.json({ data: info });
1249
1299
  }
1250
- });
1300
+ );
1301
+ classesRoute.post(
1302
+ "/:slug/reconcile",
1303
+ describeRoute({
1304
+ tags: ["Events"],
1305
+ summary: "Reconcile event slot count with Google Forms",
1306
+ responses: {
1307
+ 200: { description: "Slot count reconciled" },
1308
+ 400: { description: "No gformsId set" },
1309
+ 404: { description: "Event not found" },
1310
+ 502: { description: "Google Forms API error" }
1311
+ }
1312
+ }),
1313
+ authMiddleware,
1314
+ adminMiddleware,
1315
+ async (c) => {
1316
+ const { slug } = c.req.param();
1317
+ const db = createDb(c.env.DB);
1318
+ const cache = new CacheService(c.env.KV);
1319
+ const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
1320
+ const slots = new SlotsService(db, cache);
1321
+ const event = await db.query.events.findFirst({
1322
+ where: eq(events.slug, slug),
1323
+ columns: { gformsId: true }
1324
+ });
1325
+ if (!event) throw notFound("Event");
1326
+ if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
1327
+ try {
1328
+ const googleCount = await gforms.getExactResponseCount(event.gformsId);
1329
+ await slots.correctCount(slug, googleCount);
1330
+ return c.json({ data: { registeredSlots: googleCount } });
1331
+ } catch (err) {
1332
+ const message = err?.message ?? "Failed to fetch from Google Forms API";
1333
+ return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
1334
+ }
1335
+ }
1336
+ );
1251
1337
  classesRoute.post(
1252
1338
  "/",
1339
+ describeRoute({
1340
+ tags: ["Events"],
1341
+ summary: "Create a new event",
1342
+ responses: {
1343
+ 201: { description: "Event created successfully" },
1344
+ 422: { description: "Validation error" }
1345
+ }
1346
+ }),
1253
1347
  authMiddleware,
1254
1348
  adminMiddleware,
1255
1349
  adminEventsRateLimit,
1256
- zValidator("json", createEventSchema),
1350
+ validator("json", createEventSchema),
1257
1351
  async (c) => {
1258
1352
  const body = c.req.valid("json");
1259
1353
  const db = createDb(c.env.DB);
@@ -1286,158 +1380,284 @@ classesRoute.post(
1286
1380
  return c.json({ data: created }, 201);
1287
1381
  }
1288
1382
  );
1289
- classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
1290
- const { slug } = c.req.param();
1291
- const body = await c.req.json();
1292
- const db = createDb(c.env.DB);
1293
- const cache = new CacheService(c.env.KV);
1294
- let newSlug;
1295
- if (body.title) {
1296
- newSlug = generateSlug(body.title);
1297
- }
1298
- const [updated] = await db.update(events).set(newSlug ? { ...body, slug: newSlug } : body).where(eq(events.slug, slug)).returning();
1299
- if (!updated) throw notFound("Event");
1300
- await Promise.all([
1301
- cache.del(EVENTS_LIST_KV_KEY),
1302
- cache.del(EVENTS_ETAG_KV_KEY)
1303
- ]);
1304
- return c.json({ data: updated });
1305
- });
1306
- classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
1307
- const { slug } = c.req.param();
1308
- const db = createDb(c.env.DB);
1309
- const cache = new CacheService(c.env.KV);
1310
- const [deleted] = await db.delete(events).where(eq(events.slug, slug)).returning();
1311
- if (!deleted) throw notFound("Event");
1312
- await Promise.all([
1313
- cache.del(EVENTS_LIST_KV_KEY),
1314
- cache.del(EVENTS_ETAG_KV_KEY)
1315
- ]);
1316
- return c.body(null, 204);
1317
- });
1383
+ classesRoute.patch(
1384
+ "/:slug",
1385
+ describeRoute({
1386
+ tags: ["Events"],
1387
+ summary: "Update an event",
1388
+ responses: {
1389
+ 200: { description: "Event updated successfully" },
1390
+ 404: { description: "Event not found" }
1391
+ }
1392
+ }),
1393
+ authMiddleware,
1394
+ adminMiddleware,
1395
+ async (c) => {
1396
+ const { slug } = c.req.param();
1397
+ const body = await c.req.json();
1398
+ const db = createDb(c.env.DB);
1399
+ const cache = new CacheService(c.env.KV);
1400
+ let newSlug;
1401
+ if (body.title) {
1402
+ newSlug = generateSlug(body.title);
1403
+ }
1404
+ const [updated] = await db.update(events).set(newSlug ? { ...body, slug: newSlug } : body).where(eq(events.slug, slug)).returning();
1405
+ if (!updated) throw notFound("Event");
1406
+ await Promise.all([
1407
+ cache.del(EVENTS_LIST_KV_KEY),
1408
+ cache.del(EVENTS_ETAG_KV_KEY)
1409
+ ]);
1410
+ return c.json({ data: updated });
1411
+ }
1412
+ );
1413
+ classesRoute.delete(
1414
+ "/:slug",
1415
+ describeRoute({
1416
+ tags: ["Events"],
1417
+ summary: "Delete an event",
1418
+ responses: {
1419
+ 204: { description: "Event deleted" },
1420
+ 404: { description: "Event not found" }
1421
+ }
1422
+ }),
1423
+ authMiddleware,
1424
+ adminMiddleware,
1425
+ async (c) => {
1426
+ const { slug } = c.req.param();
1427
+ const db = createDb(c.env.DB);
1428
+ const cache = new CacheService(c.env.KV);
1429
+ const [deleted] = await db.delete(events).where(eq(events.slug, slug)).returning();
1430
+ if (!deleted) throw notFound("Event");
1431
+ await Promise.all([
1432
+ cache.del(EVENTS_LIST_KV_KEY),
1433
+ cache.del(EVENTS_ETAG_KV_KEY)
1434
+ ]);
1435
+ return c.body(null, 204);
1436
+ }
1437
+ );
1318
1438
  var VALID_ROLES = ["student", "admin", "super_admin"];
1319
1439
  var usersRoute = new Hono();
1320
- usersRoute.get("/", authMiddleware, adminMiddleware, async (c) => {
1321
- const db = createDb(c.env.DB);
1322
- const data = await db.select().from(users);
1323
- return c.json({ data });
1324
- });
1325
- usersRoute.patch("/:id/role", authMiddleware, adminMiddleware, async (c) => {
1326
- const { id } = c.req.param();
1327
- const { role } = await c.req.json();
1328
- if (!role || !VALID_ROLES.includes(role)) {
1329
- throw badRequest("Role must be 'student', 'admin', or 'super_admin'.");
1330
- }
1331
- const db = createDb(c.env.DB);
1332
- const [updated] = await db.update(users).set({ role }).where(eq(users.id, id)).returning();
1333
- if (!updated) throw notFound("User");
1334
- return c.json({ data: updated });
1335
- });
1336
- usersRoute.post("/by-email", authMiddleware, adminMiddleware, async (c) => {
1337
- const { email, role } = await c.req.json();
1338
- if (!email || !role || !VALID_ROLES.includes(role)) {
1339
- throw badRequest("Email and valid role ('student', 'admin', 'super_admin') are required.");
1340
- }
1341
- const db = createDb(c.env.DB);
1342
- const existing = await db.query.users.findFirst({
1343
- where: eq(users.email, email)
1344
- });
1345
- if (existing) {
1346
- const [updated] = await db.update(users).set({ role }).where(eq(users.email, email)).returning();
1440
+ usersRoute.get(
1441
+ "/",
1442
+ describeRoute({
1443
+ tags: ["Users"],
1444
+ summary: "List all users (admin)",
1445
+ responses: { 200: { description: "List of users" } }
1446
+ }),
1447
+ authMiddleware,
1448
+ adminMiddleware,
1449
+ async (c) => {
1450
+ const db = createDb(c.env.DB);
1451
+ const data = await db.select().from(users);
1452
+ return c.json({ data });
1453
+ }
1454
+ );
1455
+ usersRoute.patch(
1456
+ "/:id/role",
1457
+ describeRoute({
1458
+ tags: ["Users"],
1459
+ summary: "Change user role (admin)",
1460
+ responses: {
1461
+ 200: { description: "Role updated" },
1462
+ 400: { description: "Invalid role" },
1463
+ 404: { description: "User not found" }
1464
+ }
1465
+ }),
1466
+ authMiddleware,
1467
+ adminMiddleware,
1468
+ async (c) => {
1469
+ const { id } = c.req.param();
1470
+ const { role } = await c.req.json();
1471
+ if (!role || !VALID_ROLES.includes(role)) {
1472
+ throw badRequest("Role must be 'student', 'admin', or 'super_admin'.");
1473
+ }
1474
+ const db = createDb(c.env.DB);
1475
+ const [updated] = await db.update(users).set({ role }).where(eq(users.id, id)).returning();
1476
+ if (!updated) throw notFound("User");
1347
1477
  return c.json({ data: updated });
1348
1478
  }
1349
- const [created] = await db.insert(users).values({ betterAuthId: `pending:${email}`, email, name: email.split("@")[0], role }).returning();
1350
- return c.json({ data: created }, 201);
1351
- });
1352
- usersRoute.get("/me", optionalAuthMiddleware, async (c) => {
1353
- const user = c.get("user");
1354
- if (!user) return c.json({ data: null });
1355
- const db = createDb(c.env.DB);
1356
- const profile = await db.query.users.findFirst({
1357
- where: eq(users.id, user.dbId)
1358
- });
1359
- if (!profile) return c.json({ data: null });
1360
- const auth = await db.query.authUser.findFirst({
1361
- where: eq(authUser.id, profile.betterAuthId),
1362
- columns: { image: true }
1363
- });
1364
- return c.json({ data: { ...profile, image: auth?.image ?? null } });
1365
- });
1366
- usersRoute.get("/me/bookmarks", optionalAuthMiddleware, async (c) => {
1367
- const user = c.get("user");
1368
- if (!user) return c.json({ data: [] });
1369
- const db = createDb(c.env.DB);
1370
- const rows = await db.query.bookmarks.findMany({
1371
- where: eq(bookmarks.userId, user.dbId),
1372
- with: { event: true }
1373
- });
1374
- const data = rows.map((r) => ({ bookmarkedAt: r.createdAt, event: r.event }));
1375
- return c.json({ data });
1376
- });
1377
- usersRoute.post("/me/bookmarks/:eventId", authMiddleware, bookmarksRateLimit, async (c) => {
1378
- const { eventId } = c.req.param();
1379
- const user = c.get("user");
1380
- const db = createDb(c.env.DB);
1381
- const event = await db.query.events.findFirst({
1382
- where: eq(events.id, eventId),
1383
- columns: { id: true }
1384
- });
1385
- if (!event) throw notFound("Event");
1386
- const inserted = await db.insert(bookmarks).values({ userId: user.dbId, eventId }).onConflictDoNothing({ target: [bookmarks.userId, bookmarks.eventId] }).returning();
1387
- if (inserted.length > 0) {
1388
- return c.json({ data: { bookmarked: true } }, 201);
1479
+ );
1480
+ usersRoute.post(
1481
+ "/by-email",
1482
+ describeRoute({
1483
+ tags: ["Users"],
1484
+ summary: "Find or create user by email (admin)",
1485
+ responses: {
1486
+ 200: { description: "User updated" },
1487
+ 201: { description: "User created" },
1488
+ 400: { description: "Invalid email or role" }
1489
+ }
1490
+ }),
1491
+ authMiddleware,
1492
+ adminMiddleware,
1493
+ async (c) => {
1494
+ const { email, role } = await c.req.json();
1495
+ if (!email || !role || !VALID_ROLES.includes(role)) {
1496
+ throw badRequest("Email and valid role ('student', 'admin', 'super_admin') are required.");
1497
+ }
1498
+ const db = createDb(c.env.DB);
1499
+ const existing = await db.query.users.findFirst({
1500
+ where: eq(users.email, email)
1501
+ });
1502
+ if (existing) {
1503
+ const [updated] = await db.update(users).set({ role }).where(eq(users.email, email)).returning();
1504
+ return c.json({ data: updated });
1505
+ }
1506
+ const [created] = await db.insert(users).values({ betterAuthId: `pending:${email}`, email, name: email.split("@")[0], role }).returning();
1507
+ return c.json({ data: created }, 201);
1389
1508
  }
1390
- await db.delete(bookmarks).where(and(eq(bookmarks.userId, user.dbId), eq(bookmarks.eventId, eventId)));
1391
- return c.json({ data: { bookmarked: false } }, 200);
1392
- });
1393
- usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
1394
- const { eventId } = c.req.param();
1395
- const user = c.get("user");
1396
- const db = createDb(c.env.DB);
1397
- await db.delete(bookmarks).where(
1398
- and(eq(bookmarks.userId, user.dbId), eq(bookmarks.eventId, eventId))
1399
- );
1400
- return c.json({ data: { bookmarked: false } });
1401
- });
1402
- var siteConfigRoute = new Hono();
1403
- siteConfigRoute.get("/", async (c) => {
1404
- const db = createDb(c.env.DB);
1405
- const rows = await db.query.siteConfig.findMany();
1406
- const config = Object.fromEntries(
1407
- rows.map((r) => [r.key, JSON.parse(r.value)])
1408
- );
1409
- return c.json({
1410
- data: {
1411
- comingSoonUntil: config.coming_soon_until ?? null,
1412
- siteEndsAt: config.site_ends_at ?? null,
1413
- siteName: config.site_name ?? null,
1414
- registrationGloballyOpen: config.registration_globally_open ?? true,
1415
- maintenanceMode: config.maintenance_mode ?? false,
1416
- allowedOrigins: config.allowed_origins ?? null,
1417
- now: Math.floor(Date.now() / 1e3)
1509
+ );
1510
+ usersRoute.get(
1511
+ "/me",
1512
+ describeRoute({
1513
+ tags: ["Users"],
1514
+ summary: "Get current user profile",
1515
+ responses: { 200: { description: "Current user profile or null" } }
1516
+ }),
1517
+ optionalAuthMiddleware,
1518
+ async (c) => {
1519
+ const user = c.get("user");
1520
+ if (!user) return c.json({ data: null });
1521
+ const db = createDb(c.env.DB);
1522
+ const profile = await db.query.users.findFirst({
1523
+ where: eq(users.id, user.dbId)
1524
+ });
1525
+ if (!profile) return c.json({ data: null });
1526
+ const auth = await db.query.authUser.findFirst({
1527
+ where: eq(authUser.id, profile.betterAuthId),
1528
+ columns: { image: true }
1529
+ });
1530
+ return c.json({ data: { ...profile, image: auth?.image ?? null } });
1531
+ }
1532
+ );
1533
+ usersRoute.get(
1534
+ "/me/bookmarks",
1535
+ describeRoute({
1536
+ tags: ["Users"],
1537
+ summary: "Get current user's bookmarks",
1538
+ responses: { 200: { description: "List of bookmarked events" } }
1539
+ }),
1540
+ optionalAuthMiddleware,
1541
+ async (c) => {
1542
+ const user = c.get("user");
1543
+ if (!user) return c.json({ data: [] });
1544
+ const db = createDb(c.env.DB);
1545
+ const rows = await db.query.bookmarks.findMany({
1546
+ where: eq(bookmarks.userId, user.dbId),
1547
+ with: { event: true }
1548
+ });
1549
+ const data = rows.map((r) => ({ bookmarkedAt: r.createdAt, event: r.event }));
1550
+ return c.json({ data });
1551
+ }
1552
+ );
1553
+ usersRoute.post(
1554
+ "/me/bookmarks/:eventId",
1555
+ describeRoute({
1556
+ tags: ["Users"],
1557
+ summary: "Toggle bookmark for an event",
1558
+ responses: {
1559
+ 201: { description: "Bookmark created" },
1560
+ 200: { description: "Bookmark removed" },
1561
+ 404: { description: "Event not found" }
1418
1562
  }
1419
- });
1420
- });
1421
- siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
1422
- const key = c.req.param("key");
1423
- const { value } = await c.req.json();
1424
- if (key === "allowed_origins") {
1563
+ }),
1564
+ authMiddleware,
1565
+ bookmarksRateLimit,
1566
+ async (c) => {
1567
+ const { eventId } = c.req.param();
1425
1568
  const user = c.get("user");
1426
- if (!user || user.role !== "super_admin") {
1427
- throw forbidden("Super Admin access required to change allowed origins");
1569
+ const db = createDb(c.env.DB);
1570
+ const event = await db.query.events.findFirst({
1571
+ where: eq(events.id, eventId),
1572
+ columns: { id: true }
1573
+ });
1574
+ if (!event) throw notFound("Event");
1575
+ const inserted = await db.insert(bookmarks).values({ userId: user.dbId, eventId }).onConflictDoNothing({ target: [bookmarks.userId, bookmarks.eventId] }).returning();
1576
+ if (inserted.length > 0) {
1577
+ return c.json({ data: { bookmarked: true } }, 201);
1428
1578
  }
1579
+ await db.delete(bookmarks).where(and(eq(bookmarks.userId, user.dbId), eq(bookmarks.eventId, eventId)));
1580
+ return c.json({ data: { bookmarked: false } }, 200);
1429
1581
  }
1430
- const db = createDb(c.env.DB);
1431
- const now = Math.floor(Date.now() / 1e3);
1432
- await db.insert(siteConfig).values({ key, value: JSON.stringify(value), updatedAt: now }).onConflictDoUpdate({
1433
- target: siteConfig.key,
1434
- set: { value: JSON.stringify(value), updatedAt: now }
1435
- });
1436
- await c.env.KV.put(`config:${key}`, JSON.stringify(value), {
1437
- expirationTtl: 86400
1438
- });
1439
- return c.json({ data: { key, value } });
1440
- });
1582
+ );
1583
+ usersRoute.delete(
1584
+ "/me/bookmarks/:eventId",
1585
+ describeRoute({
1586
+ tags: ["Users"],
1587
+ summary: "Remove bookmark for an event",
1588
+ responses: { 200: { description: "Bookmark removed" } }
1589
+ }),
1590
+ authMiddleware,
1591
+ async (c) => {
1592
+ const { eventId } = c.req.param();
1593
+ const user = c.get("user");
1594
+ const db = createDb(c.env.DB);
1595
+ await db.delete(bookmarks).where(
1596
+ and(eq(bookmarks.userId, user.dbId), eq(bookmarks.eventId, eventId))
1597
+ );
1598
+ return c.json({ data: { bookmarked: false } });
1599
+ }
1600
+ );
1601
+ var siteConfigRoute = new Hono();
1602
+ siteConfigRoute.get(
1603
+ "/",
1604
+ describeRoute({
1605
+ tags: ["Site Config"],
1606
+ summary: "Get public site configuration",
1607
+ responses: { 200: { description: "Site configuration values" } }
1608
+ }),
1609
+ async (c) => {
1610
+ const db = createDb(c.env.DB);
1611
+ const rows = await db.query.siteConfig.findMany();
1612
+ const config = Object.fromEntries(
1613
+ rows.map((r) => [r.key, JSON.parse(r.value)])
1614
+ );
1615
+ return c.json({
1616
+ data: {
1617
+ comingSoonUntil: config.coming_soon_until ?? null,
1618
+ siteEndsAt: config.site_ends_at ?? null,
1619
+ siteName: config.site_name ?? null,
1620
+ registrationGloballyOpen: config.registration_globally_open ?? true,
1621
+ maintenanceMode: config.maintenance_mode ?? false,
1622
+ allowedOrigins: config.allowed_origins ?? null,
1623
+ now: Math.floor(Date.now() / 1e3)
1624
+ }
1625
+ });
1626
+ }
1627
+ );
1628
+ siteConfigRoute.patch(
1629
+ "/:key",
1630
+ describeRoute({
1631
+ tags: ["Site Config"],
1632
+ summary: "Update a site configuration key (admin)",
1633
+ responses: {
1634
+ 200: { description: "Config updated" },
1635
+ 403: { description: "Super admin required for this key" }
1636
+ }
1637
+ }),
1638
+ authMiddleware,
1639
+ adminMiddleware,
1640
+ async (c) => {
1641
+ const key = c.req.param("key");
1642
+ const { value } = await c.req.json();
1643
+ if (key === "allowed_origins") {
1644
+ const user = c.get("user");
1645
+ if (!user || user.role !== "super_admin") {
1646
+ throw forbidden("Super Admin access required to change allowed origins");
1647
+ }
1648
+ }
1649
+ const db = createDb(c.env.DB);
1650
+ const now = Math.floor(Date.now() / 1e3);
1651
+ await db.insert(siteConfig).values({ key, value: JSON.stringify(value), updatedAt: now }).onConflictDoUpdate({
1652
+ target: siteConfig.key,
1653
+ set: { value: JSON.stringify(value), updatedAt: now }
1654
+ });
1655
+ await c.env.KV.put(`config:${key}`, JSON.stringify(value), {
1656
+ expirationTtl: 86400
1657
+ });
1658
+ return c.json({ data: { key, value } });
1659
+ }
1660
+ );
1441
1661
  var FAQS_KV_KEY = "faqs:active";
1442
1662
  var FAQS_TTL = 600;
1443
1663
  var faqSchema = z.object({
@@ -1447,23 +1667,39 @@ var faqSchema = z.object({
1447
1667
  sortOrder: z.number().int().default(0)
1448
1668
  });
1449
1669
  var faqsRoute = new Hono();
1450
- faqsRoute.get("/", async (c) => {
1451
- const db = createDb(c.env.DB);
1452
- const cache = new CacheService(c.env.KV);
1453
- const data = await cache.getOrSet(
1454
- FAQS_KV_KEY,
1455
- () => db.query.faqs.findMany({
1456
- orderBy: (t, { asc }) => [asc(t.sortOrder), asc(t.createdAt)]
1457
- }),
1458
- FAQS_TTL
1459
- );
1460
- return c.json({ data });
1461
- });
1670
+ faqsRoute.get(
1671
+ "/",
1672
+ describeRoute({
1673
+ tags: ["FAQs"],
1674
+ summary: "List all active FAQs",
1675
+ responses: { 200: { description: "List of FAQs" } }
1676
+ }),
1677
+ async (c) => {
1678
+ const db = createDb(c.env.DB);
1679
+ const cache = new CacheService(c.env.KV);
1680
+ const data = await cache.getOrSet(
1681
+ FAQS_KV_KEY,
1682
+ () => db.query.faqs.findMany({
1683
+ orderBy: (t, { asc: asc2 }) => [asc2(t.sortOrder), asc2(t.createdAt)]
1684
+ }),
1685
+ FAQS_TTL
1686
+ );
1687
+ return c.json({ data });
1688
+ }
1689
+ );
1462
1690
  faqsRoute.post(
1463
1691
  "/",
1692
+ describeRoute({
1693
+ tags: ["FAQs"],
1694
+ summary: "Create a new FAQ (admin)",
1695
+ responses: {
1696
+ 201: { description: "FAQ created" },
1697
+ 422: { description: "Validation error" }
1698
+ }
1699
+ }),
1464
1700
  authMiddleware,
1465
1701
  adminMiddleware,
1466
- zValidator("json", faqSchema),
1702
+ validator("json", faqSchema),
1467
1703
  async (c) => {
1468
1704
  const body = c.req.valid("json");
1469
1705
  const db = createDb(c.env.DB);
@@ -1473,65 +1709,105 @@ faqsRoute.post(
1473
1709
  return c.json({ data: created }, 201);
1474
1710
  }
1475
1711
  );
1476
- faqsRoute.patch("/:id", authMiddleware, adminMiddleware, async (c) => {
1477
- const { id } = c.req.param();
1478
- const body = await c.req.json();
1479
- const db = createDb(c.env.DB);
1480
- const cache = new CacheService(c.env.KV);
1481
- const now = Math.floor(Date.now() / 1e3);
1482
- const [updated] = await db.update(faqs).set({ ...body, updatedAt: now }).where(eq(faqs.id, id)).returning();
1483
- if (!updated) throw notFound("FAQ");
1484
- await cache.del(FAQS_KV_KEY);
1485
- return c.json({ data: updated });
1486
- });
1487
- faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
1488
- const { id } = c.req.param();
1489
- const db = createDb(c.env.DB);
1490
- const cache = new CacheService(c.env.KV);
1491
- const [deleted] = await db.delete(faqs).where(eq(faqs.id, id)).returning();
1492
- if (!deleted) throw notFound("FAQ");
1493
- await cache.del(FAQS_KV_KEY);
1494
- return c.json({ data: { deleted: true } });
1495
- });
1496
- var gformsWebhookRoute = new Hono();
1497
- gformsWebhookRoute.post("/", internalMiddleware, async (c) => {
1498
- const rawBody = await c.req.text();
1499
- const signature = c.req.header("X-Goog-Signature");
1500
- if (signature) {
1501
- const isValid = await verifyGoogSignature(
1502
- rawBody,
1503
- signature,
1504
- c.env.GFORMS_WEBHOOK_SECRET
1505
- );
1506
- if (!isValid) {
1507
- return c.json({ error: "Invalid signature" }, 403);
1712
+ faqsRoute.patch(
1713
+ "/:id",
1714
+ describeRoute({
1715
+ tags: ["FAQs"],
1716
+ summary: "Update an FAQ (admin)",
1717
+ responses: {
1718
+ 200: { description: "FAQ updated" },
1719
+ 404: { description: "FAQ not found" }
1508
1720
  }
1721
+ }),
1722
+ authMiddleware,
1723
+ adminMiddleware,
1724
+ async (c) => {
1725
+ const { id } = c.req.param();
1726
+ const body = await c.req.json();
1727
+ const db = createDb(c.env.DB);
1728
+ const cache = new CacheService(c.env.KV);
1729
+ const now = Math.floor(Date.now() / 1e3);
1730
+ const [updated] = await db.update(faqs).set({ ...body, updatedAt: now }).where(eq(faqs.id, id)).returning();
1731
+ if (!updated) throw notFound("FAQ");
1732
+ await cache.del(FAQS_KV_KEY);
1733
+ return c.json({ data: updated });
1509
1734
  }
1510
- let payload;
1511
- try {
1512
- payload = JSON.parse(rawBody);
1513
- } catch {
1514
- return c.json({ error: "Invalid payload" }, 400);
1515
- }
1516
- const { formId } = payload;
1517
- if (!formId) return c.json({ error: "Missing formId" }, 400);
1518
- const db = createDb(c.env.DB);
1519
- const cache = new CacheService(c.env.KV);
1520
- const event = await db.query.events.findFirst({
1521
- where: eq(events.gformsId, formId),
1522
- columns: { slug: true, maxSlots: true, registeredSlots: true }
1523
- });
1524
- if (!event) {
1525
- console.warn(`[gforms-webhook] Unknown formId: ${formId}`);
1735
+ );
1736
+ faqsRoute.delete(
1737
+ "/:id",
1738
+ describeRoute({
1739
+ tags: ["FAQs"],
1740
+ summary: "Delete an FAQ (admin)",
1741
+ responses: {
1742
+ 200: { description: "FAQ deleted" },
1743
+ 404: { description: "FAQ not found" }
1744
+ }
1745
+ }),
1746
+ authMiddleware,
1747
+ adminMiddleware,
1748
+ async (c) => {
1749
+ const { id } = c.req.param();
1750
+ const db = createDb(c.env.DB);
1751
+ const cache = new CacheService(c.env.KV);
1752
+ const [deleted] = await db.delete(faqs).where(eq(faqs.id, id)).returning();
1753
+ if (!deleted) throw notFound("FAQ");
1754
+ await cache.del(FAQS_KV_KEY);
1755
+ return c.json({ data: { deleted: true } });
1756
+ }
1757
+ );
1758
+ var gformsWebhookRoute = new Hono();
1759
+ gformsWebhookRoute.post(
1760
+ "/",
1761
+ describeRoute({
1762
+ tags: ["Internal"],
1763
+ summary: "Google Forms webhook receiver",
1764
+ description: "Receives Google Forms Watch push notifications and increments slot counters.",
1765
+ responses: {
1766
+ 200: { description: "Notification processed" },
1767
+ 400: { description: "Invalid payload" },
1768
+ 403: { description: "Invalid HMAC signature" }
1769
+ }
1770
+ }),
1771
+ internalMiddleware,
1772
+ async (c) => {
1773
+ const rawBody = await c.req.text();
1774
+ const signature = c.req.header("X-Goog-Signature");
1775
+ if (signature) {
1776
+ const isValid = await verifyGoogSignature(
1777
+ rawBody,
1778
+ signature,
1779
+ c.env.GFORMS_WEBHOOK_SECRET
1780
+ );
1781
+ if (!isValid) {
1782
+ return c.json({ error: "Invalid signature" }, 403);
1783
+ }
1784
+ }
1785
+ let payload;
1786
+ try {
1787
+ payload = JSON.parse(rawBody);
1788
+ } catch {
1789
+ return c.json({ error: "Invalid payload" }, 400);
1790
+ }
1791
+ const { formId } = payload;
1792
+ if (!formId) return c.json({ error: "Missing formId" }, 400);
1793
+ const db = createDb(c.env.DB);
1794
+ const cache = new CacheService(c.env.KV);
1795
+ const event = await db.query.events.findFirst({
1796
+ where: eq(events.gformsId, formId),
1797
+ columns: { slug: true, maxSlots: true, registeredSlots: true }
1798
+ });
1799
+ if (!event) {
1800
+ console.warn(`[gforms-webhook] Unknown formId: ${formId}`);
1801
+ return c.json({ ok: true });
1802
+ }
1803
+ const slotsService = new SlotsService(db, cache);
1804
+ const updated = await slotsService.increment(event.slug);
1805
+ console.log(
1806
+ `[gforms-webhook] Incremented "${event.slug}": ${updated?.registered}/${updated?.total}`
1807
+ );
1526
1808
  return c.json({ ok: true });
1527
1809
  }
1528
- const slotsService = new SlotsService(db, cache);
1529
- const updated = await slotsService.increment(event.slug);
1530
- console.log(
1531
- `[gforms-webhook] Incremented "${event.slug}": ${updated?.registered}/${updated?.total}`
1532
- );
1533
- return c.json({ ok: true });
1534
- });
1810
+ );
1535
1811
  async function verifyGoogSignature(body, signature, secret) {
1536
1812
  try {
1537
1813
  const key = await crypto.subtle.importKey(
@@ -1600,10 +1876,20 @@ async function reconcileSlots(env) {
1600
1876
 
1601
1877
  // src/routes/internal/reconcile-slots.ts
1602
1878
  var reconcileSlotsRoute = new Hono();
1603
- reconcileSlotsRoute.post("/", internalMiddleware, async (c) => {
1604
- await reconcileSlots(c.env);
1605
- return c.json({ ok: true });
1606
- });
1879
+ reconcileSlotsRoute.post(
1880
+ "/",
1881
+ describeRoute({
1882
+ tags: ["Internal"],
1883
+ summary: "Reconcile event slot counts",
1884
+ description: "Triggers slot count reconciliation for all events with Google Forms.",
1885
+ responses: { 200: { description: "Reconciliation complete" } }
1886
+ }),
1887
+ internalMiddleware,
1888
+ async (c) => {
1889
+ await reconcileSlots(c.env);
1890
+ return c.json({ ok: true });
1891
+ }
1892
+ );
1607
1893
  async function batchRelease(env) {
1608
1894
  const db = createDb(env.DB);
1609
1895
  const cache = new CacheService(env.KV);
@@ -1631,10 +1917,20 @@ async function batchRelease(env) {
1631
1917
 
1632
1918
  // src/routes/internal/batch-release.ts
1633
1919
  var batchReleaseRoute = new Hono();
1634
- batchReleaseRoute.post("/", internalMiddleware, async (c) => {
1635
- await batchRelease(c.env);
1636
- return c.json({ ok: true });
1637
- });
1920
+ batchReleaseRoute.post(
1921
+ "/",
1922
+ describeRoute({
1923
+ tags: ["Internal"],
1924
+ summary: "Batch release queued events",
1925
+ description: "Triggers batch release of queued events whose releaseAt time has passed.",
1926
+ responses: { 200: { description: "Batch release complete" } }
1927
+ }),
1928
+ internalMiddleware,
1929
+ async (c) => {
1930
+ await batchRelease(c.env);
1931
+ return c.json({ ok: true });
1932
+ }
1933
+ );
1638
1934
  function parseStartTimestamp(dateTime, startTime) {
1639
1935
  if (!dateTime) return null;
1640
1936
  const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
@@ -1706,10 +2002,20 @@ async function reminderEmails(env) {
1706
2002
 
1707
2003
  // src/routes/internal/reminder-emails.ts
1708
2004
  var reminderEmailsRoute = new Hono();
1709
- reminderEmailsRoute.post("/", internalMiddleware, async (c) => {
1710
- await reminderEmails(c.env);
1711
- return c.json({ ok: true });
1712
- });
2005
+ reminderEmailsRoute.post(
2006
+ "/",
2007
+ describeRoute({
2008
+ tags: ["Internal"],
2009
+ summary: "Send reminder emails",
2010
+ description: "Triggers reminder email processing for upcoming events.",
2011
+ responses: { 200: { description: "Reminder emails processed" } }
2012
+ }),
2013
+ internalMiddleware,
2014
+ async (c) => {
2015
+ await reminderEmails(c.env);
2016
+ return c.json({ ok: true });
2017
+ }
2018
+ );
1713
2019
  var RENEWAL_WINDOW = 86400;
1714
2020
  async function renewWatches(env) {
1715
2021
  const db = createDb(env.DB);
@@ -1741,10 +2047,20 @@ async function renewWatches(env) {
1741
2047
 
1742
2048
  // src/routes/internal/renew-watches.ts
1743
2049
  var renewWatchesRoute = new Hono();
1744
- renewWatchesRoute.post("/", internalMiddleware, async (c) => {
1745
- await renewWatches(c.env);
1746
- return c.json({ ok: true });
1747
- });
2050
+ renewWatchesRoute.post(
2051
+ "/",
2052
+ describeRoute({
2053
+ tags: ["Internal"],
2054
+ summary: "Renew Google Forms watches",
2055
+ description: "Triggers renewal of expiring Google Forms Watch subscriptions.",
2056
+ responses: { 200: { description: "Watch renewal complete" } }
2057
+ }),
2058
+ internalMiddleware,
2059
+ async (c) => {
2060
+ await renewWatches(c.env);
2061
+ return c.json({ ok: true });
2062
+ }
2063
+ );
1748
2064
  var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
1749
2065
  "image/jpeg",
1750
2066
  "image/png",
@@ -1754,30 +2070,50 @@ var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
1754
2070
  ]);
1755
2071
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
1756
2072
  var uploadsRoute = new Hono();
1757
- uploadsRoute.get("/images/*", async (c) => {
1758
- const bucket = c.env.FILES;
1759
- if (!bucket) {
1760
- throw serviceUnavailable("File storage (R2) is not configured.");
1761
- }
1762
- const path = c.req.path.split("/uploads/images/")[1];
1763
- if (!path) throw notFound("Image");
1764
- const object = await bucket.get(path);
1765
- if (!object) throw notFound("Image");
1766
- const headers = {
1767
- etag: object.httpEtag,
1768
- // Cache at the edge/browser for 1 month
1769
- "Cache-Control": "public, max-age=2592000, immutable"
1770
- };
1771
- if (object.httpMetadata?.contentType) {
1772
- headers["Content-Type"] = object.httpMetadata.contentType;
1773
- }
1774
- if (object.httpMetadata?.cacheControl) {
1775
- headers["Cache-Control"] = object.httpMetadata.cacheControl;
2073
+ uploadsRoute.get(
2074
+ "/images/*",
2075
+ describeRoute({
2076
+ tags: ["Uploads"],
2077
+ summary: "Serve an image from R2 storage",
2078
+ responses: {
2079
+ 200: { description: "Image file" },
2080
+ 404: { description: "Image not found" }
2081
+ }
2082
+ }),
2083
+ async (c) => {
2084
+ const bucket = c.env.FILES;
2085
+ if (!bucket) {
2086
+ throw serviceUnavailable("File storage (R2) is not configured.");
2087
+ }
2088
+ const path = c.req.path.split("/uploads/images/")[1];
2089
+ if (!path) throw notFound("Image");
2090
+ const object = await bucket.get(path);
2091
+ if (!object) throw notFound("Image");
2092
+ const headers = {
2093
+ etag: object.httpEtag,
2094
+ // Cache at the edge/browser for 1 month
2095
+ "Cache-Control": "public, max-age=2592000, immutable"
2096
+ };
2097
+ if (object.httpMetadata?.contentType) {
2098
+ headers["Content-Type"] = object.httpMetadata.contentType;
2099
+ }
2100
+ if (object.httpMetadata?.cacheControl) {
2101
+ headers["Cache-Control"] = object.httpMetadata.cacheControl;
2102
+ }
2103
+ return c.body(object.body, 200, headers);
1776
2104
  }
1777
- return c.body(object.body, 200, headers);
1778
- });
2105
+ );
1779
2106
  uploadsRoute.post(
1780
2107
  "/images",
2108
+ describeRoute({
2109
+ tags: ["Uploads"],
2110
+ summary: "Upload an image to R2 storage (admin)",
2111
+ responses: {
2112
+ 201: { description: "Image uploaded successfully" },
2113
+ 400: { description: "Invalid file or MIME type" },
2114
+ 503: { description: "R2 storage not configured" }
2115
+ }
2116
+ }),
1781
2117
  authMiddleware,
1782
2118
  adminMiddleware,
1783
2119
  async (c) => {
@@ -1844,26 +2180,57 @@ function extensionFromMime(mime) {
1844
2180
  };
1845
2181
  return map[mime] ?? "bin";
1846
2182
  }
2183
+ function generatePath(name) {
2184
+ return name.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
2185
+ }
1847
2186
  var createThemeSchema = z.object({
1848
2187
  name: z.string().min(1),
1849
- path: z.string().min(1)
2188
+ imageUrl: z.string().url().nullable().optional(),
2189
+ descriptionEn: z.string().nullable().optional(),
2190
+ descriptionFil: z.string().nullable().optional(),
2191
+ sortOrder: z.number().int().default(0)
1850
2192
  });
1851
- var themesRoute = new Hono();
1852
- themesRoute.get("/", async (c) => {
1853
- const db = createDb(c.env.DB);
1854
- const data = await db.select().from(themes);
1855
- return c.json({ data });
2193
+ z.object({
2194
+ name: z.string().min(1).optional(),
2195
+ imageUrl: z.string().url().nullable().optional(),
2196
+ descriptionEn: z.string().nullable().optional(),
2197
+ descriptionFil: z.string().nullable().optional(),
2198
+ sortOrder: z.number().int().optional()
1856
2199
  });
2200
+ var themesRoute = new Hono();
2201
+ themesRoute.get(
2202
+ "/",
2203
+ describeRoute({
2204
+ tags: ["Themes"],
2205
+ summary: "List all themes",
2206
+ responses: { 200: { description: "List of themes" } }
2207
+ }),
2208
+ async (c) => {
2209
+ const db = createDb(c.env.DB);
2210
+ const data = await db.select().from(themes).orderBy(asc(themes.sortOrder), asc(themes.createdAt));
2211
+ return c.json({ data });
2212
+ }
2213
+ );
1857
2214
  themesRoute.post(
1858
2215
  "/",
2216
+ describeRoute({
2217
+ tags: ["Themes"],
2218
+ summary: "Create a new theme (admin)",
2219
+ responses: {
2220
+ 201: { description: "Theme created" },
2221
+ 409: { description: "Theme already exists" },
2222
+ 422: { description: "Validation error" }
2223
+ }
2224
+ }),
1859
2225
  authMiddleware,
1860
2226
  adminMiddleware,
1861
- zValidator("json", createThemeSchema),
2227
+ validator("json", createThemeSchema),
1862
2228
  async (c) => {
1863
2229
  const body = c.req.valid("json");
1864
2230
  const db = createDb(c.env.DB);
2231
+ const path = generatePath(body.name);
1865
2232
  try {
1866
- const [created] = await db.insert(themes).values(body).returning();
2233
+ const [created] = await db.insert(themes).values({ ...body, path }).returning();
1867
2234
  return c.json({ data: created }, 201);
1868
2235
  } catch (err) {
1869
2236
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -1875,14 +2242,27 @@ themesRoute.post(
1875
2242
  );
1876
2243
  themesRoute.patch(
1877
2244
  "/:id",
2245
+ describeRoute({
2246
+ tags: ["Themes"],
2247
+ summary: "Update a theme (admin)",
2248
+ responses: {
2249
+ 200: { description: "Theme updated" },
2250
+ 404: { description: "Theme not found" },
2251
+ 409: { description: "Theme already exists" }
2252
+ }
2253
+ }),
1878
2254
  authMiddleware,
1879
2255
  adminMiddleware,
1880
2256
  async (c) => {
1881
2257
  const { id } = c.req.param();
1882
2258
  const body = await c.req.json();
1883
2259
  const db = createDb(c.env.DB);
2260
+ const update = { ...body };
2261
+ if (body.name) {
2262
+ update.path = generatePath(body.name);
2263
+ }
1884
2264
  try {
1885
- const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
2265
+ const [updated] = await db.update(themes).set(update).where(eq(themes.id, id)).returning();
1886
2266
  if (!updated) throw notFound("Theme");
1887
2267
  return c.json({ data: updated });
1888
2268
  } catch (err) {
@@ -1893,13 +2273,26 @@ themesRoute.patch(
1893
2273
  }
1894
2274
  }
1895
2275
  );
1896
- themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
1897
- const { id } = c.req.param();
1898
- const db = createDb(c.env.DB);
1899
- const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
1900
- if (!deleted) throw notFound("Theme");
1901
- return c.body(null, 204);
1902
- });
2276
+ themesRoute.delete(
2277
+ "/:id",
2278
+ describeRoute({
2279
+ tags: ["Themes"],
2280
+ summary: "Delete a theme (admin)",
2281
+ responses: {
2282
+ 204: { description: "Theme deleted" },
2283
+ 404: { description: "Theme not found" }
2284
+ }
2285
+ }),
2286
+ authMiddleware,
2287
+ adminMiddleware,
2288
+ async (c) => {
2289
+ const { id } = c.req.param();
2290
+ const db = createDb(c.env.DB);
2291
+ const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
2292
+ if (!deleted) throw notFound("Theme");
2293
+ return c.body(null, 204);
2294
+ }
2295
+ );
1903
2296
  var createOrganizationSchema = z.object({
1904
2297
  name: z.string().min(1),
1905
2298
  acronym: z.string().min(1),
@@ -1907,16 +2300,33 @@ var createOrganizationSchema = z.object({
1907
2300
  link: z.string().url().nullable().optional()
1908
2301
  });
1909
2302
  var organizationsRoute = new Hono();
1910
- organizationsRoute.get("/", async (c) => {
1911
- const db = createDb(c.env.DB);
1912
- const data = await db.select().from(organizations);
1913
- return c.json({ data });
1914
- });
2303
+ organizationsRoute.get(
2304
+ "/",
2305
+ describeRoute({
2306
+ tags: ["Organizations"],
2307
+ summary: "List all organizations",
2308
+ responses: { 200: { description: "List of organizations" } }
2309
+ }),
2310
+ async (c) => {
2311
+ const db = createDb(c.env.DB);
2312
+ const data = await db.select().from(organizations);
2313
+ return c.json({ data });
2314
+ }
2315
+ );
1915
2316
  organizationsRoute.post(
1916
2317
  "/",
2318
+ describeRoute({
2319
+ tags: ["Organizations"],
2320
+ summary: "Create a new organization (admin)",
2321
+ responses: {
2322
+ 201: { description: "Organization created" },
2323
+ 409: { description: "Organization already exists" },
2324
+ 422: { description: "Validation error" }
2325
+ }
2326
+ }),
1917
2327
  authMiddleware,
1918
2328
  adminMiddleware,
1919
- zValidator("json", createOrganizationSchema),
2329
+ validator("json", createOrganizationSchema),
1920
2330
  async (c) => {
1921
2331
  const body = c.req.valid("json");
1922
2332
  const db = createDb(c.env.DB);
@@ -1933,6 +2343,15 @@ organizationsRoute.post(
1933
2343
  );
1934
2344
  organizationsRoute.patch(
1935
2345
  "/:id",
2346
+ describeRoute({
2347
+ tags: ["Organizations"],
2348
+ summary: "Update an organization (admin)",
2349
+ responses: {
2350
+ 200: { description: "Organization updated" },
2351
+ 404: { description: "Organization not found" },
2352
+ 409: { description: "Organization already exists" }
2353
+ }
2354
+ }),
1936
2355
  authMiddleware,
1937
2356
  adminMiddleware,
1938
2357
  async (c) => {
@@ -1951,13 +2370,26 @@ organizationsRoute.patch(
1951
2370
  }
1952
2371
  }
1953
2372
  );
1954
- organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
1955
- const { id } = c.req.param();
1956
- const db = createDb(c.env.DB);
1957
- const [deleted] = await db.delete(organizations).where(eq(organizations.id, id)).returning();
1958
- if (!deleted) throw notFound("Organization");
1959
- return c.body(null, 204);
1960
- });
2373
+ organizationsRoute.delete(
2374
+ "/:id",
2375
+ describeRoute({
2376
+ tags: ["Organizations"],
2377
+ summary: "Delete an organization (admin)",
2378
+ responses: {
2379
+ 204: { description: "Organization deleted" },
2380
+ 404: { description: "Organization not found" }
2381
+ }
2382
+ }),
2383
+ authMiddleware,
2384
+ adminMiddleware,
2385
+ async (c) => {
2386
+ const { id } = c.req.param();
2387
+ const db = createDb(c.env.DB);
2388
+ const [deleted] = await db.delete(organizations).where(eq(organizations.id, id)).returning();
2389
+ if (!deleted) throw notFound("Organization");
2390
+ return c.body(null, 204);
2391
+ }
2392
+ );
1961
2393
 
1962
2394
  // src/app.ts
1963
2395
  function createApp(options = {}) {
@@ -2015,6 +2447,27 @@ function createApp(options = {}) {
2015
2447
  app.route("/internal/batch-release", batchReleaseRoute);
2016
2448
  app.route("/internal/reminder-emails", reminderEmailsRoute);
2017
2449
  app.route("/internal/renew-watches", renewWatchesRoute);
2450
+ app.get(
2451
+ "/api/openapi.json",
2452
+ authMiddleware,
2453
+ adminMiddleware,
2454
+ openAPIRouteHandler(app, {
2455
+ documentation: {
2456
+ info: {
2457
+ title: "Leapify API",
2458
+ version: "0.260602.1",
2459
+ description: "DLSU CSO LEAP backend API"
2460
+ },
2461
+ openapi: "3.1.0"
2462
+ }
2463
+ })
2464
+ );
2465
+ app.get(
2466
+ "/api/docs",
2467
+ authMiddleware,
2468
+ adminMiddleware,
2469
+ swaggerUI({ url: "/api/openapi.json" })
2470
+ );
2018
2471
  app.onError(errorHandler);
2019
2472
  app.notFound(
2020
2473
  (c) => c.json({ error: { code: "NOT_FOUND", message: "Route not found" } }, 404)
@@ -2120,7 +2573,6 @@ var SesError = class extends Error {
2120
2573
  this.status = status;
2121
2574
  this.name = "SesError";
2122
2575
  }
2123
- status;
2124
2576
  /**
2125
2577
  * True for errors that are permanent (not worth retrying via SES again).
2126
2578
  * 400 BadRequest, 403 Forbidden, 404 NotFound → non-retryable.
@@ -2636,6 +3088,8 @@ var API_PREFIXES = [
2636
3088
  "/api/themes",
2637
3089
  "/api/config",
2638
3090
  "/api/uploads",
3091
+ "/api/docs",
3092
+ "/api/openapi.json",
2639
3093
  "/health",
2640
3094
  "/internal/",
2641
3095
  "/.well-known/"
@@ -2651,7 +3105,7 @@ function defaultGetRuntimeConfig(env) {
2651
3105
  };
2652
3106
  }
2653
3107
  function injectConfig(html, config) {
2654
- const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
3108
+ const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};</script>`;
2655
3109
  return html.replace("</head>", `${configScript}</head>`);
2656
3110
  }
2657
3111
  function createWorkerHandler(options) {
@@ -2773,10 +3227,10 @@ function getRuntimeConfig(env) {
2773
3227
  };
2774
3228
  }
2775
3229
  function injectConfig2(html, config) {
2776
- const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
3230
+ const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};</script>`;
2777
3231
  return html.replace("</head>", `${configScript}</head>`);
2778
3232
  }
2779
3233
 
2780
3234
  export { authAccount, authSession, authUser, authVerification, bookmarks, bookmarksRelations, createDb, createLeapify, createQueueHandler, createWorkerHandler, ensureDatabase, events, eventsRelations, faqs, getRuntimeConfig, injectConfig2 as injectConfig, organizations, organizationsRelations, siteConfig, themes, themesRelations, users };
2781
3235
  //# sourceMappingURL=index.js.map
2782
- //# sourceMappingURL=index.js.map
3236
+ //# sourceMappingURL=index.js.mapap