@access-dlsu/leapify 0.260507.1 → 0.260507.4

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 (51) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/{chunk-QARF2YFF.cjs → chunk-BFMJDSDI.cjs} +3 -2
  3. package/dist/chunk-BFMJDSDI.cjs.map +1 -0
  4. package/dist/{chunk-ANNHE3PZ.js → chunk-LJ5BSSYE.js} +3 -2
  5. package/dist/{chunk-QARF2YFF.cjs.map → chunk-LJ5BSSYE.js.map} +1 -1
  6. package/dist/{chunk-63CUZGSZ.js → chunk-MCOLCTFX.js} +3 -2
  7. package/dist/chunk-MCOLCTFX.js.map +1 -0
  8. package/dist/{chunk-YFJBE3AU.cjs → chunk-MKWVLWVJ.cjs} +3 -2
  9. package/dist/chunk-MKWVLWVJ.cjs.map +1 -0
  10. package/dist/client/index.cjs +25 -25
  11. package/dist/client/index.cjs.map +1 -1
  12. package/dist/client/index.d.ts +17 -17
  13. package/dist/client/index.d.ts.map +1 -1
  14. package/dist/client/index.js +25 -25
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/types.d.ts +4 -2
  17. package/dist/client/types.d.ts.map +1 -1
  18. package/dist/db/migrate.d.ts.map +1 -1
  19. package/dist/db/schema/{events.d.ts → classes.d.ts} +3 -3
  20. package/dist/db/schema/classes.d.ts.map +1 -0
  21. package/dist/db/schema/index.d.ts +1 -1
  22. package/dist/db/schema/index.d.ts.map +1 -1
  23. package/dist/db/schema/site-config.d.ts +83 -0
  24. package/dist/db/schema/site-config.d.ts.map +1 -1
  25. package/dist/index.cjs +640 -52
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +616 -33
  30. package/dist/index.js.map +1 -1
  31. package/dist/lib/middleware/pow-challenge.cjs +6 -6
  32. package/dist/lib/middleware/pow-challenge.d.ts.map +1 -1
  33. package/dist/lib/middleware/pow-challenge.js +1 -1
  34. package/dist/routes/classes.d.ts +4 -0
  35. package/dist/routes/classes.d.ts.map +1 -0
  36. package/dist/routes/contentful-sync.d.ts +4 -0
  37. package/dist/routes/contentful-sync.d.ts.map +1 -0
  38. package/dist/services/snapshot.d.ts +1 -1
  39. package/dist/services/snapshot.d.ts.map +1 -1
  40. package/dist/types.d.ts +19 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/worker-handler.d.ts.map +1 -1
  43. package/dist/worker.js +605 -29
  44. package/dist/worker.js.map +1 -1
  45. package/package.json +1 -1
  46. package/dist/chunk-63CUZGSZ.js.map +0 -1
  47. package/dist/chunk-ANNHE3PZ.js.map +0 -1
  48. package/dist/chunk-YFJBE3AU.cjs.map +0 -1
  49. package/dist/db/schema/events.d.ts.map +0 -1
  50. package/dist/routes/events.d.ts +0 -4
  51. package/dist/routes/events.d.ts.map +0 -1
@@ -183,6 +183,7 @@ function createPowChallengeMiddleware() {
183
183
  return createMiddleware(async (c, next) => {
184
184
  if (c.req.path === POW_VERIFY_PATH) return next();
185
185
  if (EXEMPT_PATHS.some((p) => c.req.path.startsWith(p))) return next();
186
+ if (c.req.method === "OPTIONS") return next();
186
187
  if (c.req.header("Authorization")) return next();
187
188
  const cookieHeader = c.req.header("Cookie") ?? "";
188
189
  const cookieMatch = cookieHeader.match(
@@ -208,5 +209,5 @@ function createPowChallengeMiddleware() {
208
209
  }
209
210
 
210
211
  export { POW_COOKIE_NAME, POW_PATH, POW_VERIFY_PATH, createPowChallengeMiddleware, handlePowVerify };
211
- //# sourceMappingURL=chunk-63CUZGSZ.js.map
212
- //# sourceMappingURL=chunk-63CUZGSZ.js.map
212
+ //# sourceMappingURL=chunk-MCOLCTFX.js.map
213
+ //# sourceMappingURL=chunk-MCOLCTFX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/middleware/pow-challenge.ts"],"names":[],"mappings":"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","file":"chunk-MCOLCTFX.js","sourcesContent":["/**\r\n * Proof-of-Work Challenge Middleware (ADR-006, Layer 7).\r\n *\r\n * Anubis-inspired PoW challenge, implemented natively as Hono middleware.\r\n * Requires browsers to solve a SHA-256 PoW puzzle before accessing API endpoints.\r\n * After solving, a signed cookie is issued so subsequent requests bypass the challenge.\r\n *\r\n * Flow:\r\n * 1. Request arrives → no valid PoW cookie → serve challenge HTML page\r\n * 2. Browser runs JS PoW (find nonce where SHA-256(challengeId:nonce) meets difficulty)\r\n * 3. Client POSTs solution to /.well-known/leapify/pow/verify\r\n * 4. Server validates → sets signed cookie → redirects back to original URL\r\n * 5. Subsequent requests include cookie → pass through immediately\r\n *\r\n * Signing key: INTERNAL_API_SECRET (reused — same HMAC purpose as internal route auth)\r\n * Difficulty: POW_DIFFICULTY env var or DEFAULT_POW_DIFFICULTY (leading zero bits)\r\n */\r\n\r\nimport { createMiddleware } from 'hono/factory'\r\nimport type { Context } from 'hono'\r\nimport type { LeapifyBindings } from '../../types'\r\n\r\n// ─── Constants ──────────────────────────────────────────────────────────────────\r\n\r\n/** Base path for PoW challenge routes */\r\nexport const POW_PATH = '/.well-known/leapify/pow'\r\n\r\n/** Challenge verification endpoint */\r\nexport const POW_VERIFY_PATH = `${POW_PATH}/verify`\r\n\r\n/** Cookie name for PoW auth token */\r\nexport const POW_COOKIE_NAME = 'leapify-pow'\r\n\r\n/** KV key prefix for stored challenges */\r\nconst CHALLENGE_KV_PREFIX = 'pow:challenge:'\r\n\r\n/** Default difficulty (leading zero bits required in SHA-256 hash) */\r\nconst DEFAULT_POW_DIFFICULTY = 4\r\n\r\n/** Challenge expiration time in seconds */\r\nconst CHALLENGE_TTL_SEC = 120\r\n\r\n/** Cookie expiration time in seconds (1 hour) */\r\nconst COOKIE_MAX_AGE_SEC = 3600\r\n\r\n/** Paths exempt from PoW challenge */\r\nconst EXEMPT_PATHS = ['/health', '/internal', '/api/auth']\r\n\r\n// ─── Base64url Utilities ────────────────────────────────────────────────────────\r\n\r\nfunction base64urlEncode(bytes: Uint8Array): string {\r\n let binary = ''\r\n for (const byte of bytes) {\r\n binary += String.fromCharCode(byte)\r\n }\r\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\r\n}\r\n\r\nfunction base64urlDecode(str: string): Uint8Array<ArrayBuffer> {\r\n const padded = str.replace(/-/g, '+').replace(/_/g, '/')\r\n const binary = atob(padded)\r\n const bytes = new Uint8Array(new ArrayBuffer(binary.length))\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i)\r\n }\r\n return bytes\r\n}\r\n\r\n// ─── Crypto Helpers ─────────────────────────────────────────────────────────────\r\n\r\nasync function generateChallengeId(): Promise<string> {\r\n const bytes = crypto.getRandomValues(new Uint8Array(16))\r\n return base64urlEncode(bytes)\r\n}\r\n\r\nasync function importHmacKey(secret: string): Promise<CryptoKey> {\r\n return crypto.subtle.importKey(\r\n 'raw',\r\n new TextEncoder().encode(secret),\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n false,\r\n ['sign', 'verify']\r\n )\r\n}\r\n\r\nasync function signCookie(secret: string, ip: string): Promise<string> {\r\n const ts = Date.now()\r\n const nonce = base64urlEncode(crypto.getRandomValues(new Uint8Array(8)))\r\n const payload = `${ip}:${ts}:${nonce}`\r\n const key = await importHmacKey(secret)\r\n const sig = await crypto.subtle.sign(\r\n 'HMAC',\r\n key,\r\n new TextEncoder().encode(payload)\r\n )\r\n const sigB64 = base64urlEncode(new Uint8Array(sig))\r\n return `${base64urlEncode(new TextEncoder().encode(payload))}.${sigB64}`\r\n}\r\n\r\nasync function validateCookie(\r\n secret: string,\r\n cookie: string,\r\n ip: string\r\n): Promise<boolean> {\r\n try {\r\n const [payloadB64, sigB64] = cookie.split('.')\r\n if (!payloadB64 || !sigB64) return false\r\n\r\n const payloadBytes = base64urlDecode(payloadB64)\r\n const sigBytes = base64urlDecode(sigB64)\r\n\r\n // Verify HMAC signature\r\n const key = await importHmacKey(secret)\r\n const valid = await crypto.subtle.verify(\r\n 'HMAC',\r\n key,\r\n sigBytes,\r\n payloadBytes\r\n )\r\n if (!valid) return false\r\n\r\n // Parse payload: ip:ts:nonce\r\n const payload = new TextDecoder().decode(payloadBytes)\r\n const [cookieIp, tsStr] = payload.split(':')\r\n\r\n // Verify IP matches (prevent cookie sharing)\r\n if (cookieIp !== ip) return false\r\n\r\n // Verify not expired\r\n const ts = parseInt(tsStr, 10)\r\n if (isNaN(ts) || Date.now() - ts > COOKIE_MAX_AGE_SEC * 1000) return false\r\n\r\n return true\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\nfunction getClientIp(c: Context<{ Bindings: LeapifyBindings }>): string {\r\n return (\r\n c.req.header('CF-Connecting-IP') ??\r\n c.req.header('X-Real-IP') ??\r\n c.req.header('X-Forwarded-For')?.split(',')[0]?.trim() ??\r\n 'unknown'\r\n )\r\n}\r\n\r\nfunction getDifficulty(env: LeapifyBindings): number {\r\n const raw = env.POW_DIFFICULTY\r\n if (!raw) return DEFAULT_POW_DIFFICULTY\r\n const parsed = parseInt(raw, 10)\r\n return isNaN(parsed)\r\n ? DEFAULT_POW_DIFFICULTY\r\n : Math.max(1, Math.min(parsed, 8))\r\n}\r\n\r\n// ─── Challenge Page HTML ────────────────────────────────────────────────────────\r\n\r\nfunction challengePageHtml(\r\n challengeId: string,\r\n difficulty: number,\r\n originalUrl: string\r\n): string {\r\n return `<!DOCTYPE html>\r\n<html>\r\n<head>\r\n <meta charset=\"utf-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n <title>Verifying your browser</title>\r\n <style>\r\n * { margin: 0; padding: 0; box-sizing: border-box; }\r\n body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f5f5f5; color: #333; }\r\n .card { background: #fff; border-radius: 12px; padding: 2rem; box-shadow: 0 2px 12px rgba(0,0,0,0.08); text-align: center; max-width: 400px; }\r\n .spinner { width: 40px; height: 40px; border: 3px solid #e0e0e0; border-top-color: #333; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 1rem; }\r\n @keyframes spin { to { transform: rotate(360deg); } }\r\n h1 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem; }\r\n p { font-size: 0.9rem; color: #666; }\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"card\">\r\n <div class=\"spinner\"></div>\r\n <h1>Verifying your browser</h1>\r\n <p>This should only take a moment&hellip;</p>\r\n </div>\r\n <script>\r\n (async () => {\r\n const challengeId = ${JSON.stringify(challengeId)};\r\n const difficulty = ${difficulty};\r\n const prefix = '0'.repeat(Math.ceil(difficulty / 4));\r\n let nonce = 0;\r\n const t0 = performance.now();\r\n while (true) {\r\n const input = challengeId + ':' + nonce;\r\n const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));\r\n const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');\r\n if (hex.startsWith(prefix)) {\r\n const elapsed = performance.now() - t0;\r\n const res = await fetch(${JSON.stringify(POW_VERIFY_PATH)}, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ id: challengeId, nonce, elapsed, redir: ${JSON.stringify(originalUrl)} }),\r\n });\r\n const data = await res.json();\r\n if (data.redirect) window.location.href = data.redirect;\r\n else window.location.reload();\r\n break;\r\n }\r\n nonce++;\r\n }\r\n })();\r\n </script>\r\n</body>\r\n</html>`\r\n}\r\n\r\n// ─── Verify Handler ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * POST /.well-known/leapify/pow/verify\r\n *\r\n * Validates a completed PoW challenge and issues a signed cookie.\r\n * Exported for mounting in app.ts.\r\n */\r\nexport async function handlePowVerify(\r\n c: Context<{ Bindings: LeapifyBindings }>\r\n) {\r\n const body = await c.req.json<{\r\n id?: string\r\n nonce?: number\r\n elapsed?: number\r\n redir?: string\r\n }>()\r\n\r\n const { id, nonce, redir } = body\r\n\r\n if (!id || typeof nonce !== 'number') {\r\n return c.json(\r\n {\r\n error: {\r\n code: 'VALIDATION_ERROR',\r\n message: 'Missing challenge id or nonce'\r\n }\r\n },\r\n 422\r\n )\r\n }\r\n\r\n // Retrieve challenge from KV\r\n const challenge = await c.env.KV.get(`${CHALLENGE_KV_PREFIX}${id}`)\r\n if (!challenge) {\r\n return c.json(\r\n { error: { code: 'NOT_FOUND', message: 'Challenge expired or invalid' } },\r\n 404\r\n )\r\n }\r\n\r\n const { difficulty } = JSON.parse(challenge)\r\n\r\n // Verify PoW: SHA-256(challengeId:nonce) must have required leading zeros\r\n const input = new TextEncoder().encode(`${id}:${nonce}`)\r\n const hash = await crypto.subtle.digest('SHA-256', input)\r\n const hex = Array.from(new Uint8Array(hash))\r\n .map((b) => b.toString(16).padStart(2, '0'))\r\n .join('')\r\n\r\n const requiredPrefix = '0'.repeat(Math.ceil(difficulty / 4))\r\n if (!hex.startsWith(requiredPrefix)) {\r\n return c.json(\r\n { error: { code: 'VALIDATION_ERROR', message: 'Invalid proof of work' } },\r\n 422\r\n )\r\n }\r\n\r\n // Invalidate challenge (single-use)\r\n await c.env.KV.delete(`${CHALLENGE_KV_PREFIX}${id}`)\r\n\r\n // Issue signed cookie\r\n const secret = c.env.INTERNAL_API_SECRET\r\n const ip = getClientIp(c)\r\n const token = await signCookie(secret, ip)\r\n\r\n c.header(\r\n 'Set-Cookie',\r\n `${POW_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; Secure; HttpOnly; SameSite=Lax`\r\n )\r\n\r\n return c.json({ redirect: redir || '/' })\r\n}\r\n\r\n// ─── Main Middleware ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * PoW challenge middleware.\r\n *\r\n * Mount AFTER cors, BEFORE everything else:\r\n * app.use('*', createCorsMiddleware(...))\r\n * app.use('*', createPowChallengeMiddleware()) ← here\r\n * app.use('*', createRefererGuard(...))\r\n */\r\nexport function createPowChallengeMiddleware() {\r\n return createMiddleware<{ Bindings: LeapifyBindings }>(async (c, next) => {\r\n // Always pass through the verify endpoint itself\r\n if (c.req.path === POW_VERIFY_PATH) return next()\r\n\r\n // Skip exempt paths (health, internal webhooks)\r\n if (EXEMPT_PATHS.some((p) => c.req.path.startsWith(p))) return next()\r\n\r\n // Skip for OPTIONS requests (preflights should never be challenged)\r\n if (c.req.method === 'OPTIONS') return next()\r\n\r\n // Skip if client has a valid Authorization header (Firebase JWT — auth middleware will handle)\r\n if (c.req.header('Authorization')) return next()\r\n\r\n // Check for valid PoW cookie\r\n const cookieHeader = c.req.header('Cookie') ?? ''\r\n const cookieMatch = cookieHeader.match(\r\n new RegExp(`${POW_COOKIE_NAME}=([^;]+)`)\r\n )\r\n if (cookieMatch) {\r\n const secret = c.env.INTERNAL_API_SECRET\r\n const ip = getClientIp(c)\r\n const valid = await validateCookie(secret, cookieMatch[1], ip)\r\n if (valid) return next()\r\n }\r\n\r\n // ── Issue challenge ──────────────────────────────────────────────────────\r\n\r\n const difficulty = getDifficulty(c.env)\r\n const challengeId = await generateChallengeId()\r\n\r\n // Store challenge in KV with TTL\r\n await c.env.KV.put(\r\n `${CHALLENGE_KV_PREFIX}${challengeId}`,\r\n JSON.stringify({ difficulty, createdAt: Date.now() }),\r\n { expirationTtl: CHALLENGE_TTL_SEC }\r\n )\r\n\r\n // Serve challenge page\r\n const originalUrl = c.req.path + (c.req.query('?') ? c.req.query('?') : '')\r\n const html = challengePageHtml(challengeId, difficulty, originalUrl)\r\n\r\n return c.html(html, 200)\r\n })\r\n}\r\n"]}
@@ -185,6 +185,7 @@ function createPowChallengeMiddleware() {
185
185
  return factory.createMiddleware(async (c, next) => {
186
186
  if (c.req.path === POW_VERIFY_PATH) return next();
187
187
  if (EXEMPT_PATHS.some((p) => c.req.path.startsWith(p))) return next();
188
+ if (c.req.method === "OPTIONS") return next();
188
189
  if (c.req.header("Authorization")) return next();
189
190
  const cookieHeader = c.req.header("Cookie") ?? "";
190
191
  const cookieMatch = cookieHeader.match(
@@ -214,5 +215,5 @@ exports.POW_PATH = POW_PATH;
214
215
  exports.POW_VERIFY_PATH = POW_VERIFY_PATH;
215
216
  exports.createPowChallengeMiddleware = createPowChallengeMiddleware;
216
217
  exports.handlePowVerify = handlePowVerify;
217
- //# sourceMappingURL=chunk-YFJBE3AU.cjs.map
218
- //# sourceMappingURL=chunk-YFJBE3AU.cjs.map
218
+ //# sourceMappingURL=chunk-MKWVLWVJ.cjs.map
219
+ //# sourceMappingURL=chunk-MKWVLWVJ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/middleware/pow-challenge.ts"],"names":["createMiddleware"],"mappings":"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","file":"chunk-MKWVLWVJ.cjs","sourcesContent":["/**\r\n * Proof-of-Work Challenge Middleware (ADR-006, Layer 7).\r\n *\r\n * Anubis-inspired PoW challenge, implemented natively as Hono middleware.\r\n * Requires browsers to solve a SHA-256 PoW puzzle before accessing API endpoints.\r\n * After solving, a signed cookie is issued so subsequent requests bypass the challenge.\r\n *\r\n * Flow:\r\n * 1. Request arrives → no valid PoW cookie → serve challenge HTML page\r\n * 2. Browser runs JS PoW (find nonce where SHA-256(challengeId:nonce) meets difficulty)\r\n * 3. Client POSTs solution to /.well-known/leapify/pow/verify\r\n * 4. Server validates → sets signed cookie → redirects back to original URL\r\n * 5. Subsequent requests include cookie → pass through immediately\r\n *\r\n * Signing key: INTERNAL_API_SECRET (reused — same HMAC purpose as internal route auth)\r\n * Difficulty: POW_DIFFICULTY env var or DEFAULT_POW_DIFFICULTY (leading zero bits)\r\n */\r\n\r\nimport { createMiddleware } from 'hono/factory'\r\nimport type { Context } from 'hono'\r\nimport type { LeapifyBindings } from '../../types'\r\n\r\n// ─── Constants ──────────────────────────────────────────────────────────────────\r\n\r\n/** Base path for PoW challenge routes */\r\nexport const POW_PATH = '/.well-known/leapify/pow'\r\n\r\n/** Challenge verification endpoint */\r\nexport const POW_VERIFY_PATH = `${POW_PATH}/verify`\r\n\r\n/** Cookie name for PoW auth token */\r\nexport const POW_COOKIE_NAME = 'leapify-pow'\r\n\r\n/** KV key prefix for stored challenges */\r\nconst CHALLENGE_KV_PREFIX = 'pow:challenge:'\r\n\r\n/** Default difficulty (leading zero bits required in SHA-256 hash) */\r\nconst DEFAULT_POW_DIFFICULTY = 4\r\n\r\n/** Challenge expiration time in seconds */\r\nconst CHALLENGE_TTL_SEC = 120\r\n\r\n/** Cookie expiration time in seconds (1 hour) */\r\nconst COOKIE_MAX_AGE_SEC = 3600\r\n\r\n/** Paths exempt from PoW challenge */\r\nconst EXEMPT_PATHS = ['/health', '/internal', '/api/auth']\r\n\r\n// ─── Base64url Utilities ────────────────────────────────────────────────────────\r\n\r\nfunction base64urlEncode(bytes: Uint8Array): string {\r\n let binary = ''\r\n for (const byte of bytes) {\r\n binary += String.fromCharCode(byte)\r\n }\r\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\r\n}\r\n\r\nfunction base64urlDecode(str: string): Uint8Array<ArrayBuffer> {\r\n const padded = str.replace(/-/g, '+').replace(/_/g, '/')\r\n const binary = atob(padded)\r\n const bytes = new Uint8Array(new ArrayBuffer(binary.length))\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i)\r\n }\r\n return bytes\r\n}\r\n\r\n// ─── Crypto Helpers ─────────────────────────────────────────────────────────────\r\n\r\nasync function generateChallengeId(): Promise<string> {\r\n const bytes = crypto.getRandomValues(new Uint8Array(16))\r\n return base64urlEncode(bytes)\r\n}\r\n\r\nasync function importHmacKey(secret: string): Promise<CryptoKey> {\r\n return crypto.subtle.importKey(\r\n 'raw',\r\n new TextEncoder().encode(secret),\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n false,\r\n ['sign', 'verify']\r\n )\r\n}\r\n\r\nasync function signCookie(secret: string, ip: string): Promise<string> {\r\n const ts = Date.now()\r\n const nonce = base64urlEncode(crypto.getRandomValues(new Uint8Array(8)))\r\n const payload = `${ip}:${ts}:${nonce}`\r\n const key = await importHmacKey(secret)\r\n const sig = await crypto.subtle.sign(\r\n 'HMAC',\r\n key,\r\n new TextEncoder().encode(payload)\r\n )\r\n const sigB64 = base64urlEncode(new Uint8Array(sig))\r\n return `${base64urlEncode(new TextEncoder().encode(payload))}.${sigB64}`\r\n}\r\n\r\nasync function validateCookie(\r\n secret: string,\r\n cookie: string,\r\n ip: string\r\n): Promise<boolean> {\r\n try {\r\n const [payloadB64, sigB64] = cookie.split('.')\r\n if (!payloadB64 || !sigB64) return false\r\n\r\n const payloadBytes = base64urlDecode(payloadB64)\r\n const sigBytes = base64urlDecode(sigB64)\r\n\r\n // Verify HMAC signature\r\n const key = await importHmacKey(secret)\r\n const valid = await crypto.subtle.verify(\r\n 'HMAC',\r\n key,\r\n sigBytes,\r\n payloadBytes\r\n )\r\n if (!valid) return false\r\n\r\n // Parse payload: ip:ts:nonce\r\n const payload = new TextDecoder().decode(payloadBytes)\r\n const [cookieIp, tsStr] = payload.split(':')\r\n\r\n // Verify IP matches (prevent cookie sharing)\r\n if (cookieIp !== ip) return false\r\n\r\n // Verify not expired\r\n const ts = parseInt(tsStr, 10)\r\n if (isNaN(ts) || Date.now() - ts > COOKIE_MAX_AGE_SEC * 1000) return false\r\n\r\n return true\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\nfunction getClientIp(c: Context<{ Bindings: LeapifyBindings }>): string {\r\n return (\r\n c.req.header('CF-Connecting-IP') ??\r\n c.req.header('X-Real-IP') ??\r\n c.req.header('X-Forwarded-For')?.split(',')[0]?.trim() ??\r\n 'unknown'\r\n )\r\n}\r\n\r\nfunction getDifficulty(env: LeapifyBindings): number {\r\n const raw = env.POW_DIFFICULTY\r\n if (!raw) return DEFAULT_POW_DIFFICULTY\r\n const parsed = parseInt(raw, 10)\r\n return isNaN(parsed)\r\n ? DEFAULT_POW_DIFFICULTY\r\n : Math.max(1, Math.min(parsed, 8))\r\n}\r\n\r\n// ─── Challenge Page HTML ────────────────────────────────────────────────────────\r\n\r\nfunction challengePageHtml(\r\n challengeId: string,\r\n difficulty: number,\r\n originalUrl: string\r\n): string {\r\n return `<!DOCTYPE html>\r\n<html>\r\n<head>\r\n <meta charset=\"utf-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n <title>Verifying your browser</title>\r\n <style>\r\n * { margin: 0; padding: 0; box-sizing: border-box; }\r\n body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f5f5f5; color: #333; }\r\n .card { background: #fff; border-radius: 12px; padding: 2rem; box-shadow: 0 2px 12px rgba(0,0,0,0.08); text-align: center; max-width: 400px; }\r\n .spinner { width: 40px; height: 40px; border: 3px solid #e0e0e0; border-top-color: #333; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 1rem; }\r\n @keyframes spin { to { transform: rotate(360deg); } }\r\n h1 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem; }\r\n p { font-size: 0.9rem; color: #666; }\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"card\">\r\n <div class=\"spinner\"></div>\r\n <h1>Verifying your browser</h1>\r\n <p>This should only take a moment&hellip;</p>\r\n </div>\r\n <script>\r\n (async () => {\r\n const challengeId = ${JSON.stringify(challengeId)};\r\n const difficulty = ${difficulty};\r\n const prefix = '0'.repeat(Math.ceil(difficulty / 4));\r\n let nonce = 0;\r\n const t0 = performance.now();\r\n while (true) {\r\n const input = challengeId + ':' + nonce;\r\n const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));\r\n const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');\r\n if (hex.startsWith(prefix)) {\r\n const elapsed = performance.now() - t0;\r\n const res = await fetch(${JSON.stringify(POW_VERIFY_PATH)}, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ id: challengeId, nonce, elapsed, redir: ${JSON.stringify(originalUrl)} }),\r\n });\r\n const data = await res.json();\r\n if (data.redirect) window.location.href = data.redirect;\r\n else window.location.reload();\r\n break;\r\n }\r\n nonce++;\r\n }\r\n })();\r\n </script>\r\n</body>\r\n</html>`\r\n}\r\n\r\n// ─── Verify Handler ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * POST /.well-known/leapify/pow/verify\r\n *\r\n * Validates a completed PoW challenge and issues a signed cookie.\r\n * Exported for mounting in app.ts.\r\n */\r\nexport async function handlePowVerify(\r\n c: Context<{ Bindings: LeapifyBindings }>\r\n) {\r\n const body = await c.req.json<{\r\n id?: string\r\n nonce?: number\r\n elapsed?: number\r\n redir?: string\r\n }>()\r\n\r\n const { id, nonce, redir } = body\r\n\r\n if (!id || typeof nonce !== 'number') {\r\n return c.json(\r\n {\r\n error: {\r\n code: 'VALIDATION_ERROR',\r\n message: 'Missing challenge id or nonce'\r\n }\r\n },\r\n 422\r\n )\r\n }\r\n\r\n // Retrieve challenge from KV\r\n const challenge = await c.env.KV.get(`${CHALLENGE_KV_PREFIX}${id}`)\r\n if (!challenge) {\r\n return c.json(\r\n { error: { code: 'NOT_FOUND', message: 'Challenge expired or invalid' } },\r\n 404\r\n )\r\n }\r\n\r\n const { difficulty } = JSON.parse(challenge)\r\n\r\n // Verify PoW: SHA-256(challengeId:nonce) must have required leading zeros\r\n const input = new TextEncoder().encode(`${id}:${nonce}`)\r\n const hash = await crypto.subtle.digest('SHA-256', input)\r\n const hex = Array.from(new Uint8Array(hash))\r\n .map((b) => b.toString(16).padStart(2, '0'))\r\n .join('')\r\n\r\n const requiredPrefix = '0'.repeat(Math.ceil(difficulty / 4))\r\n if (!hex.startsWith(requiredPrefix)) {\r\n return c.json(\r\n { error: { code: 'VALIDATION_ERROR', message: 'Invalid proof of work' } },\r\n 422\r\n )\r\n }\r\n\r\n // Invalidate challenge (single-use)\r\n await c.env.KV.delete(`${CHALLENGE_KV_PREFIX}${id}`)\r\n\r\n // Issue signed cookie\r\n const secret = c.env.INTERNAL_API_SECRET\r\n const ip = getClientIp(c)\r\n const token = await signCookie(secret, ip)\r\n\r\n c.header(\r\n 'Set-Cookie',\r\n `${POW_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; Secure; HttpOnly; SameSite=Lax`\r\n )\r\n\r\n return c.json({ redirect: redir || '/' })\r\n}\r\n\r\n// ─── Main Middleware ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * PoW challenge middleware.\r\n *\r\n * Mount AFTER cors, BEFORE everything else:\r\n * app.use('*', createCorsMiddleware(...))\r\n * app.use('*', createPowChallengeMiddleware()) ← here\r\n * app.use('*', createRefererGuard(...))\r\n */\r\nexport function createPowChallengeMiddleware() {\r\n return createMiddleware<{ Bindings: LeapifyBindings }>(async (c, next) => {\r\n // Always pass through the verify endpoint itself\r\n if (c.req.path === POW_VERIFY_PATH) return next()\r\n\r\n // Skip exempt paths (health, internal webhooks)\r\n if (EXEMPT_PATHS.some((p) => c.req.path.startsWith(p))) return next()\r\n\r\n // Skip for OPTIONS requests (preflights should never be challenged)\r\n if (c.req.method === 'OPTIONS') return next()\r\n\r\n // Skip if client has a valid Authorization header (Firebase JWT — auth middleware will handle)\r\n if (c.req.header('Authorization')) return next()\r\n\r\n // Check for valid PoW cookie\r\n const cookieHeader = c.req.header('Cookie') ?? ''\r\n const cookieMatch = cookieHeader.match(\r\n new RegExp(`${POW_COOKIE_NAME}=([^;]+)`)\r\n )\r\n if (cookieMatch) {\r\n const secret = c.env.INTERNAL_API_SECRET\r\n const ip = getClientIp(c)\r\n const valid = await validateCookie(secret, cookieMatch[1], ip)\r\n if (valid) return next()\r\n }\r\n\r\n // ── Issue challenge ──────────────────────────────────────────────────────\r\n\r\n const difficulty = getDifficulty(c.env)\r\n const challengeId = await generateChallengeId()\r\n\r\n // Store challenge in KV with TTL\r\n await c.env.KV.put(\r\n `${CHALLENGE_KV_PREFIX}${challengeId}`,\r\n JSON.stringify({ difficulty, createdAt: Date.now() }),\r\n { expirationTtl: CHALLENGE_TTL_SEC }\r\n )\r\n\r\n // Serve challenge page\r\n const originalUrl = c.req.path + (c.req.query('?') ? c.req.query('?') : '')\r\n const html = challengePageHtml(challengeId, difficulty, originalUrl)\r\n\r\n return c.html(html, 200)\r\n })\r\n}\r\n"]}
@@ -849,7 +849,7 @@ async function solvePowChallenge(baseUrl) {
849
849
  const base = baseUrl?.replace(/\/$/, "") ?? "";
850
850
  let html;
851
851
  try {
852
- const res = await fetch(`${base}/api/events`, { credentials: "include" });
852
+ const res = await fetch(`${base}/api/classes`, { credentials: "include" });
853
853
  const ct = res.headers.get("content-type") || "";
854
854
  if (!ct.includes("text/html")) {
855
855
  return false;
@@ -1010,61 +1010,61 @@ function createLeapifyClient(baseUrl, getToken) {
1010
1010
  },
1011
1011
  // ── Events ─────────────────────────────────────────────────────────────
1012
1012
  /**
1013
- * GET /api/events
1014
- * Returns all published events. Response is ETag-cached for 7 days.
1013
+ * GET /api/classes
1014
+ * Returns all published classes. Response is ETag-cached for 7 days.
1015
1015
  */
1016
1016
  getEvents() {
1017
- return get("/api/events");
1017
+ return get("/api/classes");
1018
1018
  },
1019
1019
  /**
1020
- * GET /api/events/admin — admin only.
1021
- * Returns all events regardless of status.
1020
+ * GET /api/classes/admin — admin only.
1021
+ * Returns all classes regardless of status.
1022
1022
  */
1023
1023
  getAdminEvents() {
1024
- return get("/api/events/admin");
1024
+ return get("/api/classes/admin");
1025
1025
  },
1026
1026
  /**
1027
- * POST /api/events/admin/publish — admin only.
1028
- * Batch publish queued events immediately or schedule them for later.
1027
+ * POST /api/classes/admin/publish — admin only.
1028
+ * Batch publish queued classes immediately or schedule them for later.
1029
1029
  */
1030
1030
  batchPublish(ids, releaseAt) {
1031
- return post("/api/events/admin/publish", { ids, releaseAt });
1031
+ return post("/api/classes/admin/publish", { ids, releaseAt });
1032
1032
  },
1033
1033
  /**
1034
- * GET /api/events/:slug
1035
- * Returns a single published event by slug.
1034
+ * GET /api/classes/:slug
1035
+ * Returns a single published class by slug.
1036
1036
  */
1037
1037
  getEvent(slug) {
1038
- return get(`/api/events/${encodeURIComponent(slug)}`);
1038
+ return get(`/api/classes/${encodeURIComponent(slug)}`);
1039
1039
  },
1040
1040
  /**
1041
- * GET /api/events/:slug/slots
1041
+ * GET /api/classes/:slug/slots
1042
1042
  * Returns real-time slot availability. CF edge caches this for 5 seconds.
1043
- * Poll every 8–10 seconds on event detail pages.
1043
+ * Poll every 8–10 seconds on class detail pages.
1044
1044
  */
1045
1045
  getSlots(slug) {
1046
- return get(`/api/events/${encodeURIComponent(slug)}/slots`);
1046
+ return get(`/api/classes/${encodeURIComponent(slug)}/slots`);
1047
1047
  },
1048
1048
  /**
1049
- * POST /api/events — admin only.
1050
- * Creates a new event. Auto-generates slug from title.
1049
+ * POST /api/classes — admin only.
1050
+ * Creates a new class. Auto-generates slug from title.
1051
1051
  */
1052
1052
  createEvent(data) {
1053
- return post("/api/events", data);
1053
+ return post("/api/classes", data);
1054
1054
  },
1055
1055
  /**
1056
- * PATCH /api/events/:slug — admin only.
1057
- * Updates an existing event by slug.
1056
+ * PATCH /api/classes/:slug — admin only.
1057
+ * Updates an existing class by slug.
1058
1058
  */
1059
1059
  updateEvent(slug, data) {
1060
- return patch(`/api/events/${encodeURIComponent(slug)}`, data);
1060
+ return patch(`/api/classes/${encodeURIComponent(slug)}`, data);
1061
1061
  },
1062
1062
  /**
1063
- * DELETE /api/events/:slug — admin only.
1064
- * Deletes an event.
1063
+ * DELETE /api/classes/:slug — admin only.
1064
+ * Deletes a class.
1065
1065
  */
1066
1066
  deleteEvent(slug) {
1067
- return del(`/api/events/${encodeURIComponent(slug)}`);
1067
+ return del(`/api/classes/${encodeURIComponent(slug)}`);
1068
1068
  },
1069
1069
  // ── Themes ─────────────────────────────────────────────────────────────
1070
1070
  /**