@access-dlsu/leapify 0.260608.1 → 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.
- package/dist/auth/auth.d.ts +5 -5
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/client/auth.d.ts +64 -60
- package/dist/client/auth.d.ts.map +1 -1
- package/dist/client/index.cjs +541 -446
- package/dist/client/index.js +540 -444
- package/dist/client/types.cjs +0 -4
- package/dist/client/types.js +1 -3
- package/dist/index.cjs +2700 -2972
- package/dist/index.js +2698 -2969
- package/dist/lib/middleware/turnstile-challenge.cjs +145 -29
- package/dist/lib/middleware/turnstile-challenge.js +140 -4
- package/dist/worker.js +2758 -3049
- package/package.json +157 -156
- package/dist/chunk-NYEPGZMP.cjs +0 -171
- package/dist/chunk-NYEPGZMP.cjs.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -9
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-Q7SFCCGT.cjs +0 -11
- package/dist/chunk-Q7SFCCGT.cjs.map +0 -1
- package/dist/chunk-WEW5LGZC.js +0 -165
- package/dist/chunk-WEW5LGZC.js.map +0 -1
- package/dist/client/index.cjs.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/client/types.cjs.map +0 -1
- package/dist/client/types.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/middleware/turnstile-challenge.cjs.map +0 -1
- package/dist/lib/middleware/turnstile-challenge.js.map +0 -1
- package/dist/worker.js.map +0 -1
|
@@ -1,29 +1,145 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 };
|