@access-dlsu/leapify 0.260601.1 → 0.260602.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-2JEY6TSO.js → chunk-WTA2QGY5.js} +4 -2
  4. package/dist/chunk-WTA2QGY5.js.map +1 -0
  5. package/dist/{chunk-X4OB4DZ3.cjs → chunk-ZV4TIJXI.cjs} +4 -2
  6. package/dist/chunk-ZV4TIJXI.cjs.map +1 -0
  7. package/dist/client/auth.d.ts +41 -41
  8. package/dist/client/index.cjs +7 -2
  9. package/dist/client/index.cjs.map +1 -1
  10. package/dist/client/index.d.ts +10 -1
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +7 -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/cron/reconcile-slots.d.ts.map +1 -1
  17. package/dist/db/schema/themes.d.ts +74 -0
  18. package/dist/db/schema/themes.d.ts.map +1 -1
  19. package/dist/index.cjs +251 -179
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +251 -179
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/middleware/turnstile-challenge.cjs +6 -6
  24. package/dist/lib/middleware/turnstile-challenge.d.ts.map +1 -1
  25. package/dist/lib/middleware/turnstile-challenge.js +1 -1
  26. package/dist/routes/internal/batch-release.d.ts +4 -0
  27. package/dist/routes/internal/batch-release.d.ts.map +1 -0
  28. package/dist/routes/internal/reconcile-slots.d.ts +4 -0
  29. package/dist/routes/internal/reconcile-slots.d.ts.map +1 -0
  30. package/dist/routes/internal/reminder-emails.d.ts +4 -0
  31. package/dist/routes/internal/reminder-emails.d.ts.map +1 -0
  32. package/dist/routes/internal/renew-watches.d.ts +4 -0
  33. package/dist/routes/internal/renew-watches.d.ts.map +1 -0
  34. package/dist/routes/themes.d.ts.map +1 -1
  35. package/dist/services/gforms.d.ts.map +1 -1
  36. package/dist/worker.js +249 -175
  37. package/dist/worker.js.map +1 -1
  38. package/package.json +2 -2
  39. package/dist/chunk-2JEY6TSO.js.map +0 -1
  40. package/dist/chunk-X4OB4DZ3.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { createTurnstileMiddleware, TURNSTILE_VERIFY_PATH, handleTurnstileVerify } from './chunk-2JEY6TSO.js';
1
+ import { createTurnstileMiddleware, TURNSTILE_VERIFY_PATH, handleTurnstileVerify } from './chunk-WTA2QGY5.js';
2
2
  import { __export } from './chunk-PZ5AY32C.js';
3
3
  import { Hono } from 'hono';
4
4
  import { cors } from 'hono/cors';
5
5
  import { drizzle } from 'drizzle-orm/d1';
6
6
  import { sqliteTable, integer, text, index, uniqueIndex } from 'drizzle-orm/sqlite-core';
7
- import { sql, relations, eq, and, count, lte } from 'drizzle-orm';
7
+ import { sql, relations, eq, and, asc, count, isNotNull, lte } from 'drizzle-orm';
8
8
  import { createMiddleware } from 'hono/factory';
9
9
  import { betterAuth } from 'better-auth';
10
10
  import { drizzleAdapter } from 'better-auth/adapters/drizzle';
@@ -20,8 +20,6 @@ var LeapifyError = class extends Error {
20
20
  this.code = code;
21
21
  this.name = "LeapifyError";
22
22
  }
23
- statusCode;
24
- code;
25
23
  };
26
24
  var unauthorized = (message = "Unauthorized") => new LeapifyError(401, "UNAUTHORIZED", message);
27
25
  var domainRestricted = () => new LeapifyError(
@@ -87,6 +85,10 @@ var themes = sqliteTable("themes", {
87
85
  name: text("name").notNull().unique(),
88
86
  path: text("path").notNull().unique(),
89
87
  // e.g. "/pirates-cove"
88
+ imageUrl: text("image_url"),
89
+ descriptionEn: text("description_en"),
90
+ descriptionFil: text("description_fil"),
91
+ sortOrder: integer("sort_order").notNull().default(0),
90
92
  createdAt: integer("created_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3)),
91
93
  updatedAt: integer("updated_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3))
92
94
  });
@@ -748,7 +750,6 @@ var CacheService = class {
748
750
  constructor(kv) {
749
751
  this.kv = kv;
750
752
  }
751
- kv;
752
753
  async get(key) {
753
754
  return this.kv.get(key, "json");
754
755
  }
@@ -794,8 +795,6 @@ var SlotsService = class {
794
795
  this.db = db;
795
796
  this.cache = cache;
796
797
  }
797
- db;
798
- cache;
799
798
  kvKey(slug) {
800
799
  return `${SLOT_KV_PREFIX}${slug}`;
801
800
  }
@@ -950,7 +949,10 @@ var GFormsService = class {
950
949
  const response = await fetch(url.toString(), {
951
950
  headers: { Authorization: `Bearer ${token}` }
952
951
  });
953
- if (!response.ok) throw new Error(`Forms API error: ${response.status}`);
952
+ if (!response.ok) {
953
+ const err = await response.text();
954
+ throw new Error(`Forms API error: ${response.status} ${err}`);
955
+ }
954
956
  const data = await response.json();
955
957
  allResponses.push(...data.responses ?? []);
956
958
  pageToken = data.nextPageToken;
@@ -1224,6 +1226,27 @@ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
1224
1226
  c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
1225
1227
  return c.json({ data: info });
1226
1228
  });
1229
+ classesRoute.post("/:slug/reconcile", authMiddleware, adminMiddleware, async (c) => {
1230
+ const { slug } = c.req.param();
1231
+ const db = createDb(c.env.DB);
1232
+ const cache = new CacheService(c.env.KV);
1233
+ const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
1234
+ const slots = new SlotsService(db, cache);
1235
+ const event = await db.query.events.findFirst({
1236
+ where: eq(events.slug, slug),
1237
+ columns: { gformsId: true }
1238
+ });
1239
+ if (!event) throw notFound("Event");
1240
+ if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
1241
+ try {
1242
+ const googleCount = await gforms.getExactResponseCount(event.gformsId);
1243
+ await slots.correctCount(slug, googleCount);
1244
+ return c.json({ data: { registeredSlots: googleCount } });
1245
+ } catch (err) {
1246
+ const message = err?.message ?? "Failed to fetch from Google Forms API";
1247
+ return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
1248
+ }
1249
+ });
1227
1250
  classesRoute.post(
1228
1251
  "/",
1229
1252
  authMiddleware,
@@ -1429,7 +1452,7 @@ faqsRoute.get("/", async (c) => {
1429
1452
  const data = await cache.getOrSet(
1430
1453
  FAQS_KV_KEY,
1431
1454
  () => db.query.faqs.findMany({
1432
- orderBy: (t, { asc }) => [asc(t.sortOrder), asc(t.createdAt)]
1455
+ orderBy: (t, { asc: asc2 }) => [asc2(t.sortOrder), asc2(t.createdAt)]
1433
1456
  }),
1434
1457
  FAQS_TTL
1435
1458
  );
@@ -1531,6 +1554,196 @@ async function verifyGoogSignature(body, signature, secret) {
1531
1554
  return false;
1532
1555
  }
1533
1556
  }
1557
+ var LOCK_KEY = "cron:reconcile-slots:lock";
1558
+ var LOCK_TTL = 300;
1559
+ async function reconcileSlots(env) {
1560
+ const db = createDb(env.DB);
1561
+ const cache = new CacheService(env.KV);
1562
+ const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
1563
+ const slots = new SlotsService(db, cache);
1564
+ const lock = await cache.get(LOCK_KEY);
1565
+ if (lock) {
1566
+ console.log("[reconcile-slots] Lock held, skipping.");
1567
+ return;
1568
+ }
1569
+ await cache.set(LOCK_KEY, "1", LOCK_TTL);
1570
+ try {
1571
+ const publishedEvents = await db.query.events.findMany({
1572
+ where: isNotNull(events.gformsId),
1573
+ columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
1574
+ });
1575
+ const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
1576
+ let corrected = 0;
1577
+ for (const event of eventsWithForms) {
1578
+ try {
1579
+ const googleCount = await gforms.getExactResponseCount(event.gformsId);
1580
+ const localCount = event.registeredSlots;
1581
+ if (googleCount !== localCount) {
1582
+ console.warn(
1583
+ `[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
1584
+ );
1585
+ await slots.correctCount(event.slug, googleCount);
1586
+ corrected++;
1587
+ }
1588
+ } catch (err) {
1589
+ console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
1590
+ }
1591
+ }
1592
+ console.log(
1593
+ `[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
1594
+ );
1595
+ } finally {
1596
+ await cache.del(LOCK_KEY);
1597
+ }
1598
+ }
1599
+
1600
+ // src/routes/internal/reconcile-slots.ts
1601
+ var reconcileSlotsRoute = new Hono();
1602
+ reconcileSlotsRoute.post("/", internalMiddleware, async (c) => {
1603
+ await reconcileSlots(c.env);
1604
+ return c.json({ ok: true });
1605
+ });
1606
+ async function batchRelease(env) {
1607
+ const db = createDb(env.DB);
1608
+ const cache = new CacheService(env.KV);
1609
+ const now = Math.floor(Date.now() / 1e3);
1610
+ const toPublish = await db.query.events.findMany({
1611
+ where: and(eq(events.status, "queued"), lte(events.releaseAt, now)),
1612
+ columns: { id: true, slug: true }
1613
+ });
1614
+ if (toPublish.length === 0) return;
1615
+ const ids = toPublish.map((e) => e.id);
1616
+ await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
1617
+ // Drizzle doesn't have inArray for D1; use raw SQL for batch
1618
+ sql`${events.id} IN (${sql.join(
1619
+ ids.map((id) => sql`${id}`),
1620
+ sql`, `
1621
+ )})`
1622
+ );
1623
+ await cache.del("events:list");
1624
+ await cache.del("events:etag");
1625
+ console.log(
1626
+ `[batch-release] Published ${toPublish.length} events:`,
1627
+ toPublish.map((e) => e.slug).join(", ")
1628
+ );
1629
+ }
1630
+
1631
+ // src/routes/internal/batch-release.ts
1632
+ var batchReleaseRoute = new Hono();
1633
+ batchReleaseRoute.post("/", internalMiddleware, async (c) => {
1634
+ await batchRelease(c.env);
1635
+ return c.json({ ok: true });
1636
+ });
1637
+ function parseStartTimestamp(dateTime, startTime) {
1638
+ if (!dateTime) return null;
1639
+ const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
1640
+ const ms = Date.parse(combined);
1641
+ return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
1642
+ }
1643
+ async function reminderEmails(env) {
1644
+ if (!env.EMAIL_QUEUE) {
1645
+ console.warn(
1646
+ "[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
1647
+ );
1648
+ return;
1649
+ }
1650
+ const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
1651
+ const hasResend = !!env.RESEND_API_KEY;
1652
+ if (!hasSes && !hasResend) {
1653
+ console.warn(
1654
+ "[reminder-emails] No email providers configured. Skipping reminders."
1655
+ );
1656
+ return;
1657
+ }
1658
+ const db = createDb(env.DB);
1659
+ const now = Math.floor(Date.now() / 1e3);
1660
+ const candidates24h = await db.query.events.findMany({
1661
+ where: and(
1662
+ eq(events.status, "published"),
1663
+ eq(events.reminder24hSent, false)
1664
+ ),
1665
+ columns: {
1666
+ id: true,
1667
+ dateTime: true,
1668
+ startTime: true
1669
+ }
1670
+ });
1671
+ for (const event of candidates24h) {
1672
+ const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
1673
+ if (!startsAt) continue;
1674
+ const hoursUntil = (startsAt - now) / 3600;
1675
+ if (hoursUntil <= 25 && hoursUntil >= 23) {
1676
+ await env.EMAIL_QUEUE.send({
1677
+ type: "send_reminder_email",
1678
+ payload: { eventId: event.id, hoursBeforeEvent: 24 }
1679
+ });
1680
+ }
1681
+ }
1682
+ const candidates1h = await db.query.events.findMany({
1683
+ where: and(
1684
+ eq(events.status, "published"),
1685
+ eq(events.reminder1hSent, false)
1686
+ ),
1687
+ columns: {
1688
+ id: true,
1689
+ dateTime: true,
1690
+ startTime: true
1691
+ }
1692
+ });
1693
+ for (const event of candidates1h) {
1694
+ const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
1695
+ if (!startsAt) continue;
1696
+ const hoursUntil = (startsAt - now) / 3600;
1697
+ if (hoursUntil <= 1.5 && hoursUntil >= 0) {
1698
+ await env.EMAIL_QUEUE.send({
1699
+ type: "send_reminder_email",
1700
+ payload: { eventId: event.id, hoursBeforeEvent: 1 }
1701
+ });
1702
+ }
1703
+ }
1704
+ }
1705
+
1706
+ // src/routes/internal/reminder-emails.ts
1707
+ var reminderEmailsRoute = new Hono();
1708
+ reminderEmailsRoute.post("/", internalMiddleware, async (c) => {
1709
+ await reminderEmails(c.env);
1710
+ return c.json({ ok: true });
1711
+ });
1712
+ var RENEWAL_WINDOW = 86400;
1713
+ async function renewWatches(env) {
1714
+ const db = createDb(env.DB);
1715
+ const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
1716
+ const now = Math.floor(Date.now() / 1e3);
1717
+ const threshold = now + RENEWAL_WINDOW;
1718
+ const expiring = await db.query.events.findMany({
1719
+ where: and(
1720
+ eq(events.status, "published"),
1721
+ lte(events.watchExpiresAt, threshold)
1722
+ ),
1723
+ columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
1724
+ });
1725
+ const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
1726
+ let renewed = 0;
1727
+ for (const event of watchEvents) {
1728
+ try {
1729
+ const result = await gforms.renewWatch(event.gformsId, event.watchId);
1730
+ const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
1731
+ await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.id, event.id));
1732
+ renewed++;
1733
+ console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
1734
+ } catch (err) {
1735
+ console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
1736
+ }
1737
+ }
1738
+ console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
1739
+ }
1740
+
1741
+ // src/routes/internal/renew-watches.ts
1742
+ var renewWatchesRoute = new Hono();
1743
+ renewWatchesRoute.post("/", internalMiddleware, async (c) => {
1744
+ await renewWatches(c.env);
1745
+ return c.json({ ok: true });
1746
+ });
1534
1747
  var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
1535
1748
  "image/jpeg",
1536
1749
  "image/png",
@@ -1630,14 +1843,27 @@ function extensionFromMime(mime) {
1630
1843
  };
1631
1844
  return map[mime] ?? "bin";
1632
1845
  }
1846
+ function generatePath(name) {
1847
+ return name.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1848
+ }
1633
1849
  var createThemeSchema = z.object({
1634
1850
  name: z.string().min(1),
1635
- path: z.string().min(1)
1851
+ imageUrl: z.string().url().nullable().optional(),
1852
+ descriptionEn: z.string().nullable().optional(),
1853
+ descriptionFil: z.string().nullable().optional(),
1854
+ sortOrder: z.number().int().default(0)
1855
+ });
1856
+ z.object({
1857
+ name: z.string().min(1).optional(),
1858
+ imageUrl: z.string().url().nullable().optional(),
1859
+ descriptionEn: z.string().nullable().optional(),
1860
+ descriptionFil: z.string().nullable().optional(),
1861
+ sortOrder: z.number().int().optional()
1636
1862
  });
1637
1863
  var themesRoute = new Hono();
1638
1864
  themesRoute.get("/", async (c) => {
1639
1865
  const db = createDb(c.env.DB);
1640
- const data = await db.select().from(themes);
1866
+ const data = await db.select().from(themes).orderBy(asc(themes.sortOrder), asc(themes.createdAt));
1641
1867
  return c.json({ data });
1642
1868
  });
1643
1869
  themesRoute.post(
@@ -1648,8 +1874,9 @@ themesRoute.post(
1648
1874
  async (c) => {
1649
1875
  const body = c.req.valid("json");
1650
1876
  const db = createDb(c.env.DB);
1877
+ const path = generatePath(body.name);
1651
1878
  try {
1652
- const [created] = await db.insert(themes).values(body).returning();
1879
+ const [created] = await db.insert(themes).values({ ...body, path }).returning();
1653
1880
  return c.json({ data: created }, 201);
1654
1881
  } catch (err) {
1655
1882
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -1667,8 +1894,12 @@ themesRoute.patch(
1667
1894
  const { id } = c.req.param();
1668
1895
  const body = await c.req.json();
1669
1896
  const db = createDb(c.env.DB);
1897
+ const update = { ...body };
1898
+ if (body.name) {
1899
+ update.path = generatePath(body.name);
1900
+ }
1670
1901
  try {
1671
- const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
1902
+ const [updated] = await db.update(themes).set(update).where(eq(themes.id, id)).returning();
1672
1903
  if (!updated) throw notFound("Theme");
1673
1904
  return c.json({ data: updated });
1674
1905
  } catch (err) {
@@ -1797,6 +2028,10 @@ function createApp(options = {}) {
1797
2028
  app.route("/api/faqs", faqsRoute);
1798
2029
  app.route("/api/uploads", uploadsRoute);
1799
2030
  app.route("/internal/gforms-webhook", gformsWebhookRoute);
2031
+ app.route("/internal/reconcile-slots", reconcileSlotsRoute);
2032
+ app.route("/internal/batch-release", batchReleaseRoute);
2033
+ app.route("/internal/reminder-emails", reminderEmailsRoute);
2034
+ app.route("/internal/renew-watches", renewWatchesRoute);
1800
2035
  app.onError(errorHandler);
1801
2036
  app.notFound(
1802
2037
  (c) => c.json({ error: { code: "NOT_FOUND", message: "Route not found" } }, 404)
@@ -1902,7 +2137,6 @@ var SesError = class extends Error {
1902
2137
  this.status = status;
1903
2138
  this.name = "SesError";
1904
2139
  }
1905
- status;
1906
2140
  /**
1907
2141
  * True for errors that are permanent (not worth retrying via SES again).
1908
2142
  * 400 BadRequest, 403 Forbidden, 404 NotFound → non-retryable.
@@ -2227,168 +2461,6 @@ async function processJob(job, services) {
2227
2461
  }
2228
2462
  }
2229
2463
  }
2230
- async function batchRelease(env) {
2231
- const db = createDb(env.DB);
2232
- const cache = new CacheService(env.KV);
2233
- const now = Math.floor(Date.now() / 1e3);
2234
- const toPublish = await db.query.events.findMany({
2235
- where: and(eq(events.status, "queued"), lte(events.releaseAt, now)),
2236
- columns: { id: true, slug: true }
2237
- });
2238
- if (toPublish.length === 0) return;
2239
- const ids = toPublish.map((e) => e.id);
2240
- await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
2241
- // Drizzle doesn't have inArray for D1; use raw SQL for batch
2242
- sql`${events.id} IN (${sql.join(
2243
- ids.map((id) => sql`${id}`),
2244
- sql`, `
2245
- )})`
2246
- );
2247
- await cache.del("events:list");
2248
- await cache.del("events:etag");
2249
- console.log(
2250
- `[batch-release] Published ${toPublish.length} events:`,
2251
- toPublish.map((e) => e.slug).join(", ")
2252
- );
2253
- }
2254
- var LOCK_KEY = "cron:reconcile-slots:lock";
2255
- var LOCK_TTL = 300;
2256
- async function reconcileSlots(env) {
2257
- const db = createDb(env.DB);
2258
- const cache = new CacheService(env.KV);
2259
- const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
2260
- const slots = new SlotsService(db, cache);
2261
- const lock = await cache.get(LOCK_KEY);
2262
- if (lock) {
2263
- console.log("[reconcile-slots] Lock held, skipping.");
2264
- return;
2265
- }
2266
- await cache.set(LOCK_KEY, "1", LOCK_TTL);
2267
- try {
2268
- const publishedEvents = await db.query.events.findMany({
2269
- where: eq(events.status, "published"),
2270
- columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
2271
- });
2272
- const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
2273
- let corrected = 0;
2274
- for (const event of eventsWithForms) {
2275
- try {
2276
- const googleCount = await gforms.getExactResponseCount(event.gformsId);
2277
- const localCount = event.registeredSlots;
2278
- if (googleCount !== localCount) {
2279
- console.warn(
2280
- `[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
2281
- );
2282
- await slots.correctCount(event.slug, googleCount);
2283
- corrected++;
2284
- }
2285
- } catch (err) {
2286
- console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
2287
- }
2288
- }
2289
- console.log(
2290
- `[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
2291
- );
2292
- } finally {
2293
- await cache.del(LOCK_KEY);
2294
- }
2295
- }
2296
- function parseStartTimestamp(dateTime, startTime) {
2297
- if (!dateTime) return null;
2298
- const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
2299
- const ms = Date.parse(combined);
2300
- return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
2301
- }
2302
- async function reminderEmails(env) {
2303
- if (!env.EMAIL_QUEUE) {
2304
- console.warn(
2305
- "[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
2306
- );
2307
- return;
2308
- }
2309
- const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
2310
- const hasResend = !!env.RESEND_API_KEY;
2311
- if (!hasSes && !hasResend) {
2312
- console.warn(
2313
- "[reminder-emails] No email providers configured. Skipping reminders."
2314
- );
2315
- return;
2316
- }
2317
- const db = createDb(env.DB);
2318
- const now = Math.floor(Date.now() / 1e3);
2319
- const candidates24h = await db.query.events.findMany({
2320
- where: and(
2321
- eq(events.status, "published"),
2322
- eq(events.reminder24hSent, false)
2323
- ),
2324
- columns: {
2325
- id: true,
2326
- dateTime: true,
2327
- startTime: true
2328
- }
2329
- });
2330
- for (const event of candidates24h) {
2331
- const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
2332
- if (!startsAt) continue;
2333
- const hoursUntil = (startsAt - now) / 3600;
2334
- if (hoursUntil <= 25 && hoursUntil >= 23) {
2335
- await env.EMAIL_QUEUE.send({
2336
- type: "send_reminder_email",
2337
- payload: { eventId: event.id, hoursBeforeEvent: 24 }
2338
- });
2339
- }
2340
- }
2341
- const candidates1h = await db.query.events.findMany({
2342
- where: and(
2343
- eq(events.status, "published"),
2344
- eq(events.reminder1hSent, false)
2345
- ),
2346
- columns: {
2347
- id: true,
2348
- dateTime: true,
2349
- startTime: true
2350
- }
2351
- });
2352
- for (const event of candidates1h) {
2353
- const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
2354
- if (!startsAt) continue;
2355
- const hoursUntil = (startsAt - now) / 3600;
2356
- if (hoursUntil <= 1.5 && hoursUntil >= 0) {
2357
- await env.EMAIL_QUEUE.send({
2358
- type: "send_reminder_email",
2359
- payload: { eventId: event.id, hoursBeforeEvent: 1 }
2360
- });
2361
- }
2362
- }
2363
- }
2364
- var RENEWAL_WINDOW = 86400;
2365
- async function renewWatches(env) {
2366
- const db = createDb(env.DB);
2367
- const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
2368
- const now = Math.floor(Date.now() / 1e3);
2369
- const threshold = now + RENEWAL_WINDOW;
2370
- const expiring = await db.query.events.findMany({
2371
- where: and(
2372
- eq(events.status, "published"),
2373
- lte(events.watchExpiresAt, threshold)
2374
- ),
2375
- columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
2376
- });
2377
- const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
2378
- let renewed = 0;
2379
- for (const event of watchEvents) {
2380
- try {
2381
- const result = await gforms.renewWatch(event.gformsId, event.watchId);
2382
- const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
2383
- await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.id, event.id));
2384
- renewed++;
2385
- console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
2386
- } catch (err) {
2387
- console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
2388
- }
2389
- }
2390
- console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
2391
- }
2392
2464
 
2393
2465
  // src/db/migrate.ts
2394
2466
  var PATCH_STATEMENTS = [
@@ -2595,7 +2667,7 @@ function defaultGetRuntimeConfig(env) {
2595
2667
  };
2596
2668
  }
2597
2669
  function injectConfig(html, config) {
2598
- const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};</script>`;
2670
+ const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
2599
2671
  return html.replace("</head>", `${configScript}</head>`);
2600
2672
  }
2601
2673
  function createWorkerHandler(options) {
@@ -2717,10 +2789,10 @@ function getRuntimeConfig(env) {
2717
2789
  };
2718
2790
  }
2719
2791
  function injectConfig2(html, config) {
2720
- const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};</script>`;
2792
+ const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
2721
2793
  return html.replace("</head>", `${configScript}</head>`);
2722
2794
  }
2723
2795
 
2724
2796
  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 };
2725
2797
  //# sourceMappingURL=index.js.map
2726
- //# sourceMappingURL=index.js.mapap
2798
+ //# sourceMappingURL=index.js.map