@access-dlsu/leapify 0.260531.1 → 0.260601.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.
@@ -1 +1 @@
1
- {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../src/lib/middleware/cors.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAiDhF"}
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../src/lib/middleware/cors.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAoB7C,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,MAAM,EAAE,GACvB,iBAAiB,CAgEnB"}
@@ -7,7 +7,7 @@ import type { LeapifyBindings } from '../../types';
7
7
  * always allowed through without a Referer check.
8
8
  *
9
9
  * This is a friction layer — it stops naive raw-HTTP clients that don't set Referer.
10
- * Sophisticated clients can spoof it, so this must NOT be relied on as the sole control
10
+ * Sophisticated clients can spook it, so this must NOT be relied on as the sole control
11
11
  * for authenticated mutation endpoints (Firebase JWT is the primary control there).
12
12
  *
13
13
  * Skipped entirely for /health and /internal routes.
@@ -1 +1 @@
1
- {"version":3,"file":"referer-guard.d.ts","sourceRoot":"","sources":["../../../src/lib/middleware/referer-guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAGlD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE;cAIrB,eAAe;yBAsBpD"}
1
+ {"version":3,"file":"referer-guard.d.ts","sourceRoot":"","sources":["../../../src/lib/middleware/referer-guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAqBlD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE;cAIrB,eAAe;yBA2CpD"}
@@ -1,4 +1,4 @@
1
- import { Hono } from "hono";
2
- import type { LeapifyEnv } from "../types";
1
+ import { Hono } from 'hono';
2
+ import type { LeapifyEnv } from '../types';
3
3
  export declare const siteConfigRoute: Hono<LeapifyEnv, import("hono/types").BlankSchema, "/">;
4
4
  //# sourceMappingURL=site-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/routes/site-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAgC,MAAM,UAAU,CAAC;AAMzE,eAAO,MAAM,eAAe,yDAAyB,CAAC"}
1
+ {"version":3,"file":"site-config.d.ts","sourceRoot":"","sources":["../../src/routes/site-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAgC,MAAM,UAAU,CAAA;AAMxE,eAAO,MAAM,eAAe,yDAAyB,CAAA"}
package/dist/worker.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Hono } from 'hono';
2
2
  import { cors } from 'hono/cors';
3
+ import { drizzle } from 'drizzle-orm/d1';
4
+ import { sqliteTable, integer, text, index, uniqueIndex } from 'drizzle-orm/sqlite-core';
5
+ import { sql, relations, eq, and, count, lte } from 'drizzle-orm';
3
6
  import { createMiddleware } from 'hono/factory';
4
7
  import { betterAuth } from 'better-auth';
5
8
  import { drizzleAdapter } from 'better-auth/adapters/drizzle';
6
9
  import { bearer } from 'better-auth/plugins';
7
- import { sql, relations, eq, and, count, lte } from 'drizzle-orm';
8
- import { drizzle } from 'drizzle-orm/d1';
9
- import { sqliteTable, integer, text, index, uniqueIndex } from 'drizzle-orm/sqlite-core';
10
10
  import { zValidator } from '@hono/zod-validator';
11
11
  import { z } from 'zod';
12
12
 
@@ -24,6 +24,8 @@ var LeapifyError = class extends Error {
24
24
  this.code = code;
25
25
  this.name = "LeapifyError";
26
26
  }
27
+ statusCode;
28
+ code;
27
29
  };
28
30
  var unauthorized = (message = "Unauthorized") => new LeapifyError(401, "UNAUTHORIZED", message);
29
31
  var domainRestricted = () => new LeapifyError(
@@ -52,212 +54,6 @@ var errorHandler = (err, c) => {
52
54
  500
53
55
  );
54
56
  };
55
- function createCorsMiddleware(allowedOrigins) {
56
- return async (c, next) => {
57
- const origin = c.req.header("origin");
58
- const dynamicOriginsJson = await c.env.KV.get("config:allowed_origins", "json");
59
- const currentAllowedOrigins = dynamicOriginsJson ?? allowedOrigins;
60
- if (c.req.path.startsWith("/api/uploads/images")) {
61
- c.header("Access-Control-Allow-Origin", "*");
62
- c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
63
- if (c.req.method === "OPTIONS") {
64
- return c.body(null, 204);
65
- }
66
- return next();
67
- }
68
- if (!c.req.path.startsWith("/health") && !c.req.path.startsWith("/api/auth") && !c.req.path.startsWith("/internal") && origin && !currentAllowedOrigins.includes("*") && !currentAllowedOrigins.includes(origin)) {
69
- return c.json(
70
- {
71
- error: {
72
- code: "DOMAIN_RESTRICTED",
73
- message: `Origin ${origin} is not allowed`
74
- }
75
- },
76
- 403
77
- );
78
- }
79
- const honoCors = cors({
80
- origin: currentAllowedOrigins,
81
- allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
82
- allowHeaders: ["Content-Type", "Authorization"],
83
- exposeHeaders: ["ETag", "Last-Modified", "Cache-Control"],
84
- maxAge: 86400,
85
- credentials: true
86
- });
87
- return honoCors(c, next);
88
- };
89
- }
90
- function createRefererGuard(allowedOrigins) {
91
- const MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
92
- const SKIP_PREFIXES = ["/health", "/internal", "/api/auth", "/.well-known"];
93
- return createMiddleware(async (c, next) => {
94
- if (!MUTATION_METHODS.has(c.req.method)) return next();
95
- if (SKIP_PREFIXES.some((p) => c.req.path.startsWith(p))) return next();
96
- const dynamicOriginsJson = await c.env.KV.get("config:allowed_origins", "json");
97
- const currentAllowedOrigins = dynamicOriginsJson ?? allowedOrigins;
98
- if (currentAllowedOrigins.includes("*")) return next();
99
- const referer = c.req.header("referer") ?? "";
100
- const isAllowed = currentAllowedOrigins.some((origin) => referer.startsWith(origin));
101
- if (!isAllowed) {
102
- throw forbidden("Request origin not permitted");
103
- }
104
- return next();
105
- });
106
- }
107
- var TURNSTILE_PATH = "/.well-known/leapify/turnstile";
108
- var TURNSTILE_VERIFY_PATH = `${TURNSTILE_PATH}/verify`;
109
- var TURNSTILE_COOKIE_NAME = "leapify-turnstile";
110
- var VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
111
- var COOKIE_MAX_AGE_SEC = 86400;
112
- var EXEMPT_PATHS = [
113
- "/health",
114
- "/internal",
115
- "/api/auth",
116
- "/api/uploads/images",
117
- "/api/classes",
118
- "/api/faqs",
119
- "/api/config",
120
- TURNSTILE_VERIFY_PATH
121
- ];
122
- function base64urlEncode(bytes) {
123
- let binary = "";
124
- for (const byte of bytes) {
125
- binary += String.fromCharCode(byte);
126
- }
127
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
128
- }
129
- function base64urlDecode(str) {
130
- const padded = str.replace(/-/g, "+").replace(/_/g, "/");
131
- const binary = atob(padded);
132
- const bytes = new Uint8Array(new ArrayBuffer(binary.length));
133
- for (let i = 0; i < binary.length; i++) {
134
- bytes[i] = binary.charCodeAt(i);
135
- }
136
- return bytes;
137
- }
138
- async function importHmacKey(secret) {
139
- return crypto.subtle.importKey(
140
- "raw",
141
- new TextEncoder().encode(secret),
142
- { name: "HMAC", hash: "SHA-256" },
143
- false,
144
- ["sign", "verify"]
145
- );
146
- }
147
- async function signCookie(secret, ip) {
148
- const ts = Date.now();
149
- const nonce = base64urlEncode(crypto.getRandomValues(new Uint8Array(8)));
150
- const payload = `${ip}:${ts}:${nonce}`;
151
- const key = await importHmacKey(secret);
152
- const sig = await crypto.subtle.sign(
153
- "HMAC",
154
- key,
155
- new TextEncoder().encode(payload)
156
- );
157
- const sigB64 = base64urlEncode(new Uint8Array(sig));
158
- return `${base64urlEncode(new TextEncoder().encode(payload))}.${sigB64}`;
159
- }
160
- async function validateCookie(secret, cookie, ip) {
161
- try {
162
- const [payloadB64, sigB64] = cookie.split(".");
163
- if (!payloadB64 || !sigB64) return false;
164
- const payloadBytes = base64urlDecode(payloadB64);
165
- const sigBytes = base64urlDecode(sigB64);
166
- const key = await importHmacKey(secret);
167
- const valid = await crypto.subtle.verify(
168
- "HMAC",
169
- key,
170
- sigBytes,
171
- payloadBytes
172
- );
173
- if (!valid) return false;
174
- const payload = new TextDecoder().decode(payloadBytes);
175
- const [cookieIp, tsStr] = payload.split(":");
176
- if (cookieIp !== ip) return false;
177
- const ts = parseInt(tsStr, 10);
178
- if (isNaN(ts) || Date.now() - ts > COOKIE_MAX_AGE_SEC * 1e3) return false;
179
- return true;
180
- } catch {
181
- return false;
182
- }
183
- }
184
- function getClientIp(c) {
185
- return c.req.header("CF-Connecting-IP") ?? c.req.header("X-Real-IP") ?? c.req.header("X-Forwarded-For")?.split(",")[0]?.trim() ?? "unknown";
186
- }
187
- function isExempt(path) {
188
- const normalized = path.toLowerCase().replace(/\/$/, "");
189
- return EXEMPT_PATHS.some((p) => {
190
- const ep = p.toLowerCase().replace(/\/$/, "");
191
- return normalized === ep || normalized.startsWith(ep + "/");
192
- });
193
- }
194
- function setCookieHeader(c, token) {
195
- const isSecure = c.req.raw.url.startsWith("https") || c.req.header("x-forwarded-proto") === "https";
196
- c.header(
197
- "Set-Cookie",
198
- `${TURNSTILE_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; ${isSecure ? "Secure; " : ""}HttpOnly; SameSite=Lax`
199
- );
200
- }
201
- async function handleTurnstileVerify(c) {
202
- const body = await c.req.json();
203
- const { token } = body;
204
- if (!token) {
205
- return c.json(
206
- { error: { code: "VALIDATION_ERROR", message: "Missing Turnstile token" } },
207
- 422
208
- );
209
- }
210
- const secret = c.env.TURNSTILE_SECRET_KEY;
211
- if (!secret) {
212
- return c.json(
213
- { error: { code: "CONFIG_ERROR", message: "Turnstile not configured" } },
214
- 500
215
- );
216
- }
217
- const ip = getClientIp(c);
218
- const formData = new URLSearchParams();
219
- formData.append("secret", secret);
220
- formData.append("response", token);
221
- if (ip !== "unknown") {
222
- formData.append("remoteip", ip);
223
- }
224
- const res = await fetch(VERIFY_URL, {
225
- method: "POST",
226
- body: formData
227
- });
228
- const outcome = await res.json();
229
- if (!outcome.success) {
230
- return c.json(
231
- { error: { code: "TURNSTILE_FAILED", message: "Turnstile verification failed", details: outcome["error-codes"] } },
232
- 403
233
- );
234
- }
235
- const cookieToken = await signCookie(secret, ip);
236
- setCookieHeader(c, cookieToken);
237
- return c.json({ success: true });
238
- }
239
- function createTurnstileMiddleware() {
240
- return createMiddleware(async (c, next) => {
241
- if (isExempt(c.req.path)) return next();
242
- if (c.req.method === "OPTIONS") return next();
243
- if (c.req.header("Authorization")) return next();
244
- const secret = c.env.TURNSTILE_SECRET_KEY;
245
- if (!secret) return next();
246
- const cookieHeader = c.req.header("Cookie") ?? "";
247
- const cookieMatch = cookieHeader.match(
248
- new RegExp(`${TURNSTILE_COOKIE_NAME}=([^;]+)`)
249
- );
250
- if (cookieMatch) {
251
- const ip = getClientIp(c);
252
- const valid = await validateCookie(secret, cookieMatch[1], ip);
253
- if (valid) return next();
254
- }
255
- return c.json(
256
- { error: { code: "TURNSTILE_REQUIRED", message: "Turnstile verification required" } },
257
- 401
258
- );
259
- });
260
- }
261
57
 
262
58
  // src/db/schema/index.ts
263
59
  var schema_exports = {};
@@ -490,8 +286,266 @@ var authVerification = sqliteTable(
490
286
  function createDb(d1) {
491
287
  return drizzle(d1, { schema: schema_exports });
492
288
  }
493
-
494
- // src/auth/auth.ts
289
+ async function getOriginsFromDb(env) {
290
+ try {
291
+ const db = createDb(env.DB);
292
+ const row = await db.query.siteConfig.findFirst({
293
+ where: eq(siteConfig.key, "allowed_origins")
294
+ });
295
+ if (row) return JSON.parse(row.value);
296
+ } catch {
297
+ }
298
+ return null;
299
+ }
300
+ function createCorsMiddleware(allowedOrigins) {
301
+ return async (c, next) => {
302
+ const origin = c.req.header("origin");
303
+ const dynamicOriginsJson = await c.env.KV.get(
304
+ "config:allowed_origins",
305
+ "json"
306
+ );
307
+ let currentAllowedOrigins = dynamicOriginsJson ?? allowedOrigins;
308
+ if (!dynamicOriginsJson) {
309
+ const dbOrigins = await getOriginsFromDb(c.env);
310
+ if (dbOrigins) {
311
+ currentAllowedOrigins = dbOrigins;
312
+ await c.env.KV.put(
313
+ "config:allowed_origins",
314
+ JSON.stringify(dbOrigins),
315
+ { expirationTtl: 86400 }
316
+ );
317
+ }
318
+ }
319
+ if (c.req.path.startsWith("/api/uploads/images")) {
320
+ c.header("Access-Control-Allow-Origin", "*");
321
+ c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
322
+ if (c.req.method === "OPTIONS") {
323
+ return c.body(null, 204);
324
+ }
325
+ return next();
326
+ }
327
+ if (!c.req.path.startsWith("/health") && !c.req.path.startsWith("/api/auth") && !c.req.path.startsWith("/internal") && origin && !currentAllowedOrigins.includes("*") && !currentAllowedOrigins.includes(origin) && origin !== new URL(c.req.url).origin) {
328
+ return c.json(
329
+ {
330
+ error: {
331
+ code: "DOMAIN_RESTRICTED",
332
+ message: `Origin ${origin} is not allowed`
333
+ }
334
+ },
335
+ 403
336
+ );
337
+ }
338
+ const honoCors = cors({
339
+ origin: currentAllowedOrigins,
340
+ allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
341
+ allowHeaders: ["Content-Type", "Authorization"],
342
+ exposeHeaders: ["ETag", "Last-Modified", "Cache-Control"],
343
+ maxAge: 86400,
344
+ credentials: true
345
+ });
346
+ return honoCors(c, next);
347
+ };
348
+ }
349
+ async function getOriginsFromDb2(env) {
350
+ try {
351
+ const db = createDb(env.DB);
352
+ const row = await db.query.siteConfig.findFirst({
353
+ where: eq(siteConfig.key, "allowed_origins")
354
+ });
355
+ if (row) return JSON.parse(row.value);
356
+ } catch {
357
+ }
358
+ return null;
359
+ }
360
+ function createRefererGuard(allowedOrigins) {
361
+ const MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
362
+ const SKIP_PREFIXES = ["/health", "/internal", "/api/auth", "/.well-known"];
363
+ return createMiddleware(async (c, next) => {
364
+ if (!MUTATION_METHODS.has(c.req.method)) return next();
365
+ if (SKIP_PREFIXES.some((p) => c.req.path.startsWith(p))) return next();
366
+ const dynamicOriginsJson = await c.env.KV.get(
367
+ "config:allowed_origins",
368
+ "json"
369
+ );
370
+ let currentAllowedOrigins = dynamicOriginsJson ?? allowedOrigins;
371
+ if (!dynamicOriginsJson) {
372
+ const dbOrigins = await getOriginsFromDb2(c.env);
373
+ if (dbOrigins) {
374
+ currentAllowedOrigins = dbOrigins;
375
+ await c.env.KV.put(
376
+ "config:allowed_origins",
377
+ JSON.stringify(dbOrigins),
378
+ { expirationTtl: 86400 }
379
+ );
380
+ }
381
+ }
382
+ if (currentAllowedOrigins.includes("*")) return next();
383
+ const referer = c.req.header("referer") ?? "";
384
+ const requestOrigin = new URL(c.req.url).origin;
385
+ if (referer.startsWith(requestOrigin)) return next();
386
+ const isAllowed = currentAllowedOrigins.some(
387
+ (origin) => referer.startsWith(origin)
388
+ );
389
+ if (!isAllowed) {
390
+ throw forbidden("Request origin not permitted");
391
+ }
392
+ return next();
393
+ });
394
+ }
395
+ var TURNSTILE_PATH = "/.well-known/leapify/turnstile";
396
+ var TURNSTILE_VERIFY_PATH = `${TURNSTILE_PATH}/verify`;
397
+ var TURNSTILE_COOKIE_NAME = "leapify-turnstile";
398
+ var VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
399
+ var COOKIE_MAX_AGE_SEC = 86400;
400
+ var EXEMPT_PATHS = [
401
+ "/health",
402
+ "/internal",
403
+ "/api/auth",
404
+ "/api/uploads/images",
405
+ "/api/classes",
406
+ "/api/faqs",
407
+ "/api/config",
408
+ TURNSTILE_VERIFY_PATH
409
+ ];
410
+ function base64urlEncode(bytes) {
411
+ let binary = "";
412
+ for (const byte of bytes) {
413
+ binary += String.fromCharCode(byte);
414
+ }
415
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
416
+ }
417
+ function base64urlDecode(str) {
418
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/");
419
+ const binary = atob(padded);
420
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
421
+ for (let i = 0; i < binary.length; i++) {
422
+ bytes[i] = binary.charCodeAt(i);
423
+ }
424
+ return bytes;
425
+ }
426
+ async function importHmacKey(secret) {
427
+ return crypto.subtle.importKey(
428
+ "raw",
429
+ new TextEncoder().encode(secret),
430
+ { name: "HMAC", hash: "SHA-256" },
431
+ false,
432
+ ["sign", "verify"]
433
+ );
434
+ }
435
+ async function signCookie(secret, ip) {
436
+ const ts = Date.now();
437
+ const nonce = base64urlEncode(crypto.getRandomValues(new Uint8Array(8)));
438
+ const payload = `${ip}:${ts}:${nonce}`;
439
+ const key = await importHmacKey(secret);
440
+ const sig = await crypto.subtle.sign(
441
+ "HMAC",
442
+ key,
443
+ new TextEncoder().encode(payload)
444
+ );
445
+ const sigB64 = base64urlEncode(new Uint8Array(sig));
446
+ return `${base64urlEncode(new TextEncoder().encode(payload))}.${sigB64}`;
447
+ }
448
+ async function validateCookie(secret, cookie, ip) {
449
+ try {
450
+ const [payloadB64, sigB64] = cookie.split(".");
451
+ if (!payloadB64 || !sigB64) return false;
452
+ const payloadBytes = base64urlDecode(payloadB64);
453
+ const sigBytes = base64urlDecode(sigB64);
454
+ const key = await importHmacKey(secret);
455
+ const valid = await crypto.subtle.verify(
456
+ "HMAC",
457
+ key,
458
+ sigBytes,
459
+ payloadBytes
460
+ );
461
+ if (!valid) return false;
462
+ const payload = new TextDecoder().decode(payloadBytes);
463
+ const [cookieIp, tsStr] = payload.split(":");
464
+ if (cookieIp !== ip) return false;
465
+ const ts = parseInt(tsStr, 10);
466
+ if (isNaN(ts) || Date.now() - ts > COOKIE_MAX_AGE_SEC * 1e3) return false;
467
+ return true;
468
+ } catch {
469
+ return false;
470
+ }
471
+ }
472
+ function getClientIp(c) {
473
+ return c.req.header("CF-Connecting-IP") ?? c.req.header("X-Real-IP") ?? c.req.header("X-Forwarded-For")?.split(",")[0]?.trim() ?? "unknown";
474
+ }
475
+ function isExempt(path) {
476
+ const normalized = path.toLowerCase().replace(/\/$/, "");
477
+ return EXEMPT_PATHS.some((p) => {
478
+ const ep = p.toLowerCase().replace(/\/$/, "");
479
+ return normalized === ep || normalized.startsWith(ep + "/");
480
+ });
481
+ }
482
+ function setCookieHeader(c, token) {
483
+ const isSecure = c.req.raw.url.startsWith("https") || c.req.header("x-forwarded-proto") === "https";
484
+ c.header(
485
+ "Set-Cookie",
486
+ `${TURNSTILE_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; ${isSecure ? "Secure; " : ""}HttpOnly; SameSite=Lax`
487
+ );
488
+ }
489
+ async function handleTurnstileVerify(c) {
490
+ const body = await c.req.json();
491
+ const { token } = body;
492
+ if (!token) {
493
+ return c.json(
494
+ { error: { code: "VALIDATION_ERROR", message: "Missing Turnstile token" } },
495
+ 422
496
+ );
497
+ }
498
+ const secret = c.env.TURNSTILE_SECRET_KEY;
499
+ if (!secret) {
500
+ return c.json(
501
+ { error: { code: "CONFIG_ERROR", message: "Turnstile not configured" } },
502
+ 500
503
+ );
504
+ }
505
+ const ip = getClientIp(c);
506
+ const formData = new URLSearchParams();
507
+ formData.append("secret", secret);
508
+ formData.append("response", token);
509
+ if (ip !== "unknown") {
510
+ formData.append("remoteip", ip);
511
+ }
512
+ const res = await fetch(VERIFY_URL, {
513
+ method: "POST",
514
+ body: formData
515
+ });
516
+ const outcome = await res.json();
517
+ if (!outcome.success) {
518
+ return c.json(
519
+ { error: { code: "TURNSTILE_FAILED", message: "Turnstile verification failed", details: outcome["error-codes"] } },
520
+ 403
521
+ );
522
+ }
523
+ const cookieToken = await signCookie(secret, ip);
524
+ setCookieHeader(c, cookieToken);
525
+ return c.json({ success: true });
526
+ }
527
+ function createTurnstileMiddleware() {
528
+ return createMiddleware(async (c, next) => {
529
+ if (isExempt(c.req.path)) return next();
530
+ if (c.req.method === "OPTIONS") return next();
531
+ if (c.req.header("Authorization")) return next();
532
+ const secret = c.env.TURNSTILE_SECRET_KEY;
533
+ if (!secret) return next();
534
+ const cookieHeader = c.req.header("Cookie") ?? "";
535
+ const cookieMatch = cookieHeader.match(
536
+ new RegExp(`${TURNSTILE_COOKIE_NAME}=([^;]+)`)
537
+ );
538
+ if (cookieMatch) {
539
+ const ip = getClientIp(c);
540
+ const valid = await validateCookie(secret, cookieMatch[1], ip);
541
+ if (valid) return next();
542
+ }
543
+ return c.json(
544
+ { error: { code: "TURNSTILE_REQUIRED", message: "Turnstile verification required" } },
545
+ 401
546
+ );
547
+ });
548
+ }
495
549
  var DLSU_DOMAIN = "@dlsu.edu.ph";
496
550
  function createAuth(env) {
497
551
  const db = createDb(env.DB);
@@ -787,7 +841,14 @@ healthRoute.get("/", async (c) => {
787
841
  const env = c.env;
788
842
  const hasSes = Boolean(env.SES_REGION) && Boolean(env.SES_ACCESS_KEY_ID) && Boolean(env.SES_SECRET_ACCESS_KEY);
789
843
  const hasResend = Boolean(env.RESEND_API_KEY);
790
- const hasGForms = Boolean(env.GFORMS_SERVICE_ACCOUNT_JSON);
844
+ let hasGForms = false;
845
+ if (env.GFORMS_SERVICE_ACCOUNT_JSON) {
846
+ try {
847
+ const parsed = JSON.parse(env.GFORMS_SERVICE_ACCOUNT_JSON);
848
+ hasGForms = Boolean(parsed.client_email && parsed.private_key);
849
+ } catch {
850
+ }
851
+ }
791
852
  const probes = [];
792
853
  if (hasSes) {
793
854
  probes.push(
@@ -845,6 +906,7 @@ var CacheService = class {
845
906
  constructor(kv) {
846
907
  this.kv = kv;
847
908
  }
909
+ kv;
848
910
  async get(key) {
849
911
  return this.kv.get(key, "json");
850
912
  }
@@ -890,6 +952,8 @@ var SlotsService = class {
890
952
  this.db = db;
891
953
  this.cache = cache;
892
954
  }
955
+ db;
956
+ cache;
893
957
  kvKey(slug) {
894
958
  return `${SLOT_KV_PREFIX}${slug}`;
895
959
  }
@@ -1504,7 +1568,7 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
1504
1568
  set: { value: JSON.stringify(value), updatedAt: now }
1505
1569
  });
1506
1570
  await c.env.KV.put(`config:${key}`, JSON.stringify(value), {
1507
- expirationTtl: 600
1571
+ expirationTtl: 86400
1508
1572
  });
1509
1573
  return c.json({ data: { key, value } });
1510
1574
  });
@@ -1996,6 +2060,7 @@ var SesError = class extends Error {
1996
2060
  this.status = status;
1997
2061
  this.name = "SesError";
1998
2062
  }
2063
+ status;
1999
2064
  /**
2000
2065
  * True for errors that are permanent (not worth retrying via SES again).
2001
2066
  * 400 BadRequest, 403 Forbidden, 404 NotFound → non-retryable.