@access-dlsu/leapify 0.260605.2 → 0.260608.2

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,29 +1,145 @@
1
- 'use strict';
2
-
3
- var chunkNYEPGZMP_cjs = require('../../chunk-NYEPGZMP.cjs');
4
- require('../../chunk-Q7SFCCGT.cjs');
5
-
6
-
7
-
8
- Object.defineProperty(exports, "TURNSTILE_COOKIE_NAME", {
9
- enumerable: true,
10
- get: function () { return chunkNYEPGZMP_cjs.TURNSTILE_COOKIE_NAME; }
11
- });
12
- Object.defineProperty(exports, "TURNSTILE_PATH", {
13
- enumerable: true,
14
- get: function () { return chunkNYEPGZMP_cjs.TURNSTILE_PATH; }
15
- });
16
- Object.defineProperty(exports, "TURNSTILE_VERIFY_PATH", {
17
- enumerable: true,
18
- get: function () { return chunkNYEPGZMP_cjs.TURNSTILE_VERIFY_PATH; }
19
- });
20
- Object.defineProperty(exports, "createTurnstileMiddleware", {
21
- enumerable: true,
22
- get: function () { return chunkNYEPGZMP_cjs.createTurnstileMiddleware; }
23
- });
24
- Object.defineProperty(exports, "handleTurnstileVerify", {
25
- enumerable: true,
26
- get: function () { return chunkNYEPGZMP_cjs.handleTurnstileVerify; }
27
- });
28
- //# sourceMappingURL=turnstile-challenge.cjs.map
29
- //# sourceMappingURL=turnstile-challenge.cjs.map
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let hono_factory = require("hono/factory");
3
+ //#region src/lib/middleware/turnstile-challenge.ts
4
+ const TURNSTILE_PATH = "/.well-known/leapify/turnstile";
5
+ const TURNSTILE_VERIFY_PATH = `${TURNSTILE_PATH}/verify`;
6
+ const TURNSTILE_COOKIE_NAME = "leapify-turnstile";
7
+ const VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
8
+ const COOKIE_MAX_AGE_SEC = 86400;
9
+ const EXEMPT_PATHS = [
10
+ "/health",
11
+ "/internal",
12
+ "/api/auth",
13
+ "/api/uploads/images",
14
+ "/api/classes",
15
+ "/api/faqs",
16
+ "/api/config",
17
+ "/api/themes",
18
+ "/api/organizations",
19
+ "/api/docs",
20
+ "/api/openapi.json",
21
+ TURNSTILE_VERIFY_PATH
22
+ ];
23
+ function base64urlEncode(bytes) {
24
+ let binary = "";
25
+ for (const byte of bytes) binary += String.fromCharCode(byte);
26
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
27
+ }
28
+ function base64urlDecode(str) {
29
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/");
30
+ const binary = atob(padded);
31
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
32
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
33
+ return bytes;
34
+ }
35
+ async function importHmacKey(secret) {
36
+ return crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
37
+ name: "HMAC",
38
+ hash: "SHA-256"
39
+ }, false, ["sign", "verify"]);
40
+ }
41
+ async function signCookie(secret, ip) {
42
+ const payload = `${ip}:${Date.now()}:${base64urlEncode(crypto.getRandomValues(new Uint8Array(8)))}`;
43
+ const key = await importHmacKey(secret);
44
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
45
+ const sigB64 = base64urlEncode(new Uint8Array(sig));
46
+ return `${base64urlEncode(new TextEncoder().encode(payload))}.${sigB64}`;
47
+ }
48
+ async function validateCookie(secret, cookie, ip) {
49
+ try {
50
+ const [payloadB64, sigB64] = cookie.split(".");
51
+ if (!payloadB64 || !sigB64) return false;
52
+ const payloadBytes = base64urlDecode(payloadB64);
53
+ const sigBytes = base64urlDecode(sigB64);
54
+ const key = await importHmacKey(secret);
55
+ if (!await crypto.subtle.verify("HMAC", key, sigBytes, payloadBytes)) return false;
56
+ const [cookieIp, tsStr] = new TextDecoder().decode(payloadBytes).split(":");
57
+ if (cookieIp !== ip) return false;
58
+ const ts = parseInt(tsStr, 10);
59
+ if (isNaN(ts) || Date.now() - ts > COOKIE_MAX_AGE_SEC * 1e3) return false;
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ function getClientIp(c) {
66
+ return c.req.header("CF-Connecting-IP") ?? c.req.header("X-Real-IP") ?? c.req.header("X-Forwarded-For")?.split(",")[0]?.trim() ?? "unknown";
67
+ }
68
+ function isExempt(path) {
69
+ const normalized = path.toLowerCase().replace(/\/$/, "");
70
+ return EXEMPT_PATHS.some((p) => {
71
+ const ep = p.toLowerCase().replace(/\/$/, "");
72
+ return normalized === ep || normalized.startsWith(ep + "/");
73
+ });
74
+ }
75
+ function setCookieHeader(c, token) {
76
+ const isSecure = c.req.raw.url.startsWith("https") || c.req.header("x-forwarded-proto") === "https";
77
+ c.header("Set-Cookie", `${TURNSTILE_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; ${isSecure ? "Secure; " : ""}HttpOnly; SameSite=Lax`);
78
+ }
79
+ /**
80
+ * POST /.well-known/leapify/turnstile/verify
81
+ *
82
+ * Validates a Turnstile token and issues a signed cookie on success.
83
+ */
84
+ async function handleTurnstileVerify(c) {
85
+ const { token } = await c.req.json();
86
+ if (!token) return c.json({ error: {
87
+ code: "VALIDATION_ERROR",
88
+ message: "Missing Turnstile token"
89
+ } }, 422);
90
+ const secret = c.env.TURNSTILE_SECRET_KEY;
91
+ if (!secret) return c.json({ error: {
92
+ code: "CONFIG_ERROR",
93
+ message: "Turnstile not configured"
94
+ } }, 500);
95
+ const ip = getClientIp(c);
96
+ const formData = new URLSearchParams();
97
+ formData.append("secret", secret);
98
+ formData.append("response", token);
99
+ if (ip !== "unknown") formData.append("remoteip", ip);
100
+ const outcome = await (await fetch(VERIFY_URL, {
101
+ method: "POST",
102
+ body: formData
103
+ })).json();
104
+ if (!outcome.success) return c.json({ error: {
105
+ code: "TURNSTILE_FAILED",
106
+ message: "Turnstile verification failed",
107
+ details: outcome["error-codes"]
108
+ } }, 403);
109
+ setCookieHeader(c, await signCookie(secret, ip));
110
+ return c.json({ success: true });
111
+ }
112
+ /**
113
+ * Turnstile challenge middleware.
114
+ *
115
+ * Requires a valid Turnstile-signed cookie on all non-exempt requests.
116
+ * The client must first solve a Turnstile challenge and POST the token
117
+ * to the verify endpoint to obtain the cookie.
118
+ *
119
+ * Exempt paths: /health, /internal, /api/auth, /api/uploads/images,
120
+ * and the verify endpoint itself.
121
+ */
122
+ function createTurnstileMiddleware() {
123
+ return (0, hono_factory.createMiddleware)(async (c, next) => {
124
+ if (isExempt(c.req.path)) return next();
125
+ if (c.req.method === "OPTIONS") return next();
126
+ if (c.req.header("Authorization")) return next();
127
+ const secret = c.env.TURNSTILE_SECRET_KEY;
128
+ if (!secret) return next();
129
+ const cookieMatch = (c.req.header("Cookie") ?? "").match(new RegExp(`${TURNSTILE_COOKIE_NAME}=([^;]+)`));
130
+ if (cookieMatch) {
131
+ const ip = getClientIp(c);
132
+ if (await validateCookie(secret, cookieMatch[1], ip)) return next();
133
+ }
134
+ return c.json({ error: {
135
+ code: "TURNSTILE_REQUIRED",
136
+ message: "Turnstile verification required"
137
+ } }, 401);
138
+ });
139
+ }
140
+ //#endregion
141
+ exports.TURNSTILE_COOKIE_NAME = TURNSTILE_COOKIE_NAME;
142
+ exports.TURNSTILE_PATH = TURNSTILE_PATH;
143
+ exports.TURNSTILE_VERIFY_PATH = TURNSTILE_VERIFY_PATH;
144
+ exports.createTurnstileMiddleware = createTurnstileMiddleware;
145
+ exports.handleTurnstileVerify = handleTurnstileVerify;
@@ -1,4 +1,140 @@
1
- export { TURNSTILE_COOKIE_NAME, TURNSTILE_PATH, TURNSTILE_VERIFY_PATH, createTurnstileMiddleware, handleTurnstileVerify } from '../../chunk-WEW5LGZC.js';
2
- import '../../chunk-PZ5AY32C.js';
3
- //# sourceMappingURL=turnstile-challenge.js.map
4
- //# sourceMappingURL=turnstile-challenge.js.map
1
+ import { createMiddleware } from "hono/factory";
2
+ //#region src/lib/middleware/turnstile-challenge.ts
3
+ const TURNSTILE_PATH = "/.well-known/leapify/turnstile";
4
+ const TURNSTILE_VERIFY_PATH = `${TURNSTILE_PATH}/verify`;
5
+ const TURNSTILE_COOKIE_NAME = "leapify-turnstile";
6
+ const VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
7
+ const COOKIE_MAX_AGE_SEC = 86400;
8
+ const EXEMPT_PATHS = [
9
+ "/health",
10
+ "/internal",
11
+ "/api/auth",
12
+ "/api/uploads/images",
13
+ "/api/classes",
14
+ "/api/faqs",
15
+ "/api/config",
16
+ "/api/themes",
17
+ "/api/organizations",
18
+ "/api/docs",
19
+ "/api/openapi.json",
20
+ TURNSTILE_VERIFY_PATH
21
+ ];
22
+ function base64urlEncode(bytes) {
23
+ let binary = "";
24
+ for (const byte of bytes) binary += String.fromCharCode(byte);
25
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
26
+ }
27
+ function base64urlDecode(str) {
28
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/");
29
+ const binary = atob(padded);
30
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
31
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
32
+ return bytes;
33
+ }
34
+ async function importHmacKey(secret) {
35
+ return crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
36
+ name: "HMAC",
37
+ hash: "SHA-256"
38
+ }, false, ["sign", "verify"]);
39
+ }
40
+ async function signCookie(secret, ip) {
41
+ const payload = `${ip}:${Date.now()}:${base64urlEncode(crypto.getRandomValues(new Uint8Array(8)))}`;
42
+ const key = await importHmacKey(secret);
43
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
44
+ const sigB64 = base64urlEncode(new Uint8Array(sig));
45
+ return `${base64urlEncode(new TextEncoder().encode(payload))}.${sigB64}`;
46
+ }
47
+ async function validateCookie(secret, cookie, ip) {
48
+ try {
49
+ const [payloadB64, sigB64] = cookie.split(".");
50
+ if (!payloadB64 || !sigB64) return false;
51
+ const payloadBytes = base64urlDecode(payloadB64);
52
+ const sigBytes = base64urlDecode(sigB64);
53
+ const key = await importHmacKey(secret);
54
+ if (!await crypto.subtle.verify("HMAC", key, sigBytes, payloadBytes)) return false;
55
+ const [cookieIp, tsStr] = new TextDecoder().decode(payloadBytes).split(":");
56
+ if (cookieIp !== ip) return false;
57
+ const ts = parseInt(tsStr, 10);
58
+ if (isNaN(ts) || Date.now() - ts > COOKIE_MAX_AGE_SEC * 1e3) return false;
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ function getClientIp(c) {
65
+ return c.req.header("CF-Connecting-IP") ?? c.req.header("X-Real-IP") ?? c.req.header("X-Forwarded-For")?.split(",")[0]?.trim() ?? "unknown";
66
+ }
67
+ function isExempt(path) {
68
+ const normalized = path.toLowerCase().replace(/\/$/, "");
69
+ return EXEMPT_PATHS.some((p) => {
70
+ const ep = p.toLowerCase().replace(/\/$/, "");
71
+ return normalized === ep || normalized.startsWith(ep + "/");
72
+ });
73
+ }
74
+ function setCookieHeader(c, token) {
75
+ const isSecure = c.req.raw.url.startsWith("https") || c.req.header("x-forwarded-proto") === "https";
76
+ c.header("Set-Cookie", `${TURNSTILE_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; ${isSecure ? "Secure; " : ""}HttpOnly; SameSite=Lax`);
77
+ }
78
+ /**
79
+ * POST /.well-known/leapify/turnstile/verify
80
+ *
81
+ * Validates a Turnstile token and issues a signed cookie on success.
82
+ */
83
+ async function handleTurnstileVerify(c) {
84
+ const { token } = await c.req.json();
85
+ if (!token) return c.json({ error: {
86
+ code: "VALIDATION_ERROR",
87
+ message: "Missing Turnstile token"
88
+ } }, 422);
89
+ const secret = c.env.TURNSTILE_SECRET_KEY;
90
+ if (!secret) return c.json({ error: {
91
+ code: "CONFIG_ERROR",
92
+ message: "Turnstile not configured"
93
+ } }, 500);
94
+ const ip = getClientIp(c);
95
+ const formData = new URLSearchParams();
96
+ formData.append("secret", secret);
97
+ formData.append("response", token);
98
+ if (ip !== "unknown") formData.append("remoteip", ip);
99
+ const outcome = await (await fetch(VERIFY_URL, {
100
+ method: "POST",
101
+ body: formData
102
+ })).json();
103
+ if (!outcome.success) return c.json({ error: {
104
+ code: "TURNSTILE_FAILED",
105
+ message: "Turnstile verification failed",
106
+ details: outcome["error-codes"]
107
+ } }, 403);
108
+ setCookieHeader(c, await signCookie(secret, ip));
109
+ return c.json({ success: true });
110
+ }
111
+ /**
112
+ * Turnstile challenge middleware.
113
+ *
114
+ * Requires a valid Turnstile-signed cookie on all non-exempt requests.
115
+ * The client must first solve a Turnstile challenge and POST the token
116
+ * to the verify endpoint to obtain the cookie.
117
+ *
118
+ * Exempt paths: /health, /internal, /api/auth, /api/uploads/images,
119
+ * and the verify endpoint itself.
120
+ */
121
+ function createTurnstileMiddleware() {
122
+ return createMiddleware(async (c, next) => {
123
+ if (isExempt(c.req.path)) return next();
124
+ if (c.req.method === "OPTIONS") return next();
125
+ if (c.req.header("Authorization")) return next();
126
+ const secret = c.env.TURNSTILE_SECRET_KEY;
127
+ if (!secret) return next();
128
+ const cookieMatch = (c.req.header("Cookie") ?? "").match(new RegExp(`${TURNSTILE_COOKIE_NAME}=([^;]+)`));
129
+ if (cookieMatch) {
130
+ const ip = getClientIp(c);
131
+ if (await validateCookie(secret, cookieMatch[1], ip)) return next();
132
+ }
133
+ return c.json({ error: {
134
+ code: "TURNSTILE_REQUIRED",
135
+ message: "Turnstile verification required"
136
+ } }, 401);
137
+ });
138
+ }
139
+ //#endregion
140
+ export { TURNSTILE_COOKIE_NAME, TURNSTILE_PATH, TURNSTILE_VERIFY_PATH, createTurnstileMiddleware, handleTurnstileVerify };
@@ -1 +1 @@
1
- {"version":3,"file":"gforms-webhook.d.ts","sourceRoot":"","sources":["../../../src/routes/internal/gforms-webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAO9C,eAAO,MAAM,kBAAkB,yDAAyB,CAAC"}
1
+ {"version":3,"file":"gforms-webhook.d.ts","sourceRoot":"","sources":["../../../src/routes/internal/gforms-webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAM9C,eAAO,MAAM,kBAAkB,yDAAyB,CAAC"}
@@ -1,34 +1,26 @@
1
1
  import type { LeapifyDb } from '../db';
2
- import type { CacheService } from './cache';
3
2
  export interface SlotInfo {
4
- available: number;
5
3
  total: number;
6
4
  registered: number;
7
- isFull: boolean;
8
5
  }
9
6
  /**
10
- * Manages real-time slot counts using a local D1 counter + KV cache.
11
- * Google Forms Watch webhook increments the counter; reads go through KV.
12
- *
13
- * CF Cache (Cache-Control: public, max-age=5) sits in front of the /slots
14
- * endpoint, so KV is only read once per 5-second window per edge location.
7
+ * Manages real-time slot counts using D1 directly (no KV cache).
8
+ * Google Forms Watch webhook increments the counter.
15
9
  */
16
10
  export declare class SlotsService {
17
11
  private readonly db;
18
- private readonly cache;
19
- constructor(db: LeapifyDb, cache: CacheService);
20
- kvKey(slug: string): string;
12
+ constructor(db: LeapifyDb);
21
13
  /**
22
- * Read current slot info KV first, D1 on miss.
14
+ * Read current slot info from D1.
23
15
  */
24
16
  getSlots(slug: string): Promise<SlotInfo | null>;
25
17
  /**
26
- * Atomically increment registered_slots in D1 and update KV.
18
+ * Atomically increment registered_slots in D1.
27
19
  * Called by the Google Forms Watch webhook handler.
28
20
  */
29
21
  increment(slug: string): Promise<SlotInfo | null>;
30
22
  /**
31
- * Atomically decrement registered_slots in D1 and update KV.
23
+ * Atomically decrement registered_slots in D1.
32
24
  * Used during reconciliation drift correction (not from user actions).
33
25
  */
34
26
  decrement(slug: string): Promise<SlotInfo | null>;
@@ -36,13 +28,5 @@ export declare class SlotsService {
36
28
  * Set registered_slots to a specific value (used by reconciliation cron).
37
29
  */
38
30
  correctCount(slug: string, actualCount: number): Promise<void>;
39
- /**
40
- * Read from D1, write to KV, and return slot info.
41
- */
42
- refreshFromDb(slug: string): Promise<SlotInfo | null>;
43
- /**
44
- * Invalidate the KV cache for a specific event.
45
- */
46
- invalidate(slug: string): Promise<void>;
47
31
  }
48
32
  //# sourceMappingURL=slots.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/services/slots.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAI3C,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,EAAE,EAAE,SAAS,EACb,KAAK,EAAE,YAAY;IAGtC,KAAK,CAAC,IAAI,EAAE,MAAM;IAIlB;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAStD;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IASvD;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAWvD;;OAEG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASpE;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAqB3D;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG9C"}
1
+ {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/services/slots.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,SAAS;IAE1C;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IActD;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IASvD;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAWvD;;OAEG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAMrE"}