@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.
- package/dist/app.d.ts.map +1 -1
- package/dist/auth/auth.d.ts +2 -2
- package/dist/{chunk-2JEY6TSO.js → chunk-WTA2QGY5.js} +4 -2
- package/dist/chunk-WTA2QGY5.js.map +1 -0
- package/dist/{chunk-X4OB4DZ3.cjs → chunk-ZV4TIJXI.cjs} +4 -2
- package/dist/chunk-ZV4TIJXI.cjs.map +1 -0
- package/dist/client/auth.d.ts +41 -41
- package/dist/client/index.cjs +7 -2
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.ts +10 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +7 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +4 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/cron/reconcile-slots.d.ts.map +1 -1
- package/dist/db/schema/themes.d.ts +74 -0
- package/dist/db/schema/themes.d.ts.map +1 -1
- package/dist/index.cjs +251 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +251 -179
- package/dist/index.js.map +1 -1
- package/dist/lib/middleware/turnstile-challenge.cjs +6 -6
- package/dist/lib/middleware/turnstile-challenge.d.ts.map +1 -1
- package/dist/lib/middleware/turnstile-challenge.js +1 -1
- package/dist/routes/internal/batch-release.d.ts +4 -0
- package/dist/routes/internal/batch-release.d.ts.map +1 -0
- package/dist/routes/internal/reconcile-slots.d.ts +4 -0
- package/dist/routes/internal/reconcile-slots.d.ts.map +1 -0
- package/dist/routes/internal/reminder-emails.d.ts +4 -0
- package/dist/routes/internal/reminder-emails.d.ts.map +1 -0
- package/dist/routes/internal/renew-watches.d.ts +4 -0
- package/dist/routes/internal/renew-watches.d.ts.map +1 -0
- package/dist/routes/themes.d.ts.map +1 -1
- package/dist/services/gforms.d.ts.map +1 -1
- package/dist/worker.js +249 -175
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-2JEY6TSO.js.map +0 -1
- 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-
|
|
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)
|
|
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 }) => [
|
|
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
|
-
|
|
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(
|
|
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)}
|
|
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)}
|
|
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.
|
|
2798
|
+
//# sourceMappingURL=index.js.map
|