@scalemule/nextjs 0.1.28 → 0.1.29

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,333 +0,0 @@
1
- import { LedveryClient } from '@scalemule/ledvery';
2
-
3
- // src/server/ledvery.ts
4
-
5
- // src/server/timing.ts
6
- function constantTimeEqual(a, b) {
7
- const maxLength = Math.max(a.length, b.length);
8
- let mismatch = a.length ^ b.length;
9
- for (let i = 0; i < maxLength; i++) {
10
- const aCode = i < a.length ? a.charCodeAt(i) : 0;
11
- const bCode = i < b.length ? b.charCodeAt(i) : 0;
12
- mismatch |= aCode ^ bCode;
13
- }
14
- return mismatch === 0;
15
- }
16
-
17
- // src/server/ledvery-cookies.ts
18
- var SM_LEDVERY_STATE_COOKIE = "sm_ledvery_state";
19
- var SM_LEDVERY_PKCE_VERIFIER_COOKIE = "sm_ledvery_pkce_verifier";
20
- var SM_LEDVERY_NONCE_COOKIE = "sm_ledvery_nonce";
21
- var SM_LEDVERY_ID_TOKEN_COOKIE = "sm_ledvery_id_token";
22
- var SM_LEDVERY_ACCESS_TOKEN_COOKIE = "sm_ledvery_access_token";
23
- var FLOW_COOKIE_MAX_AGE = 60 * 10;
24
- var SESSION_COOKIE_MAX_AGE = 60 * 60;
25
- function flowCookieOptions() {
26
- return {
27
- httpOnly: true,
28
- secure: process.env.NODE_ENV === "production",
29
- sameSite: "lax",
30
- path: "/",
31
- maxAge: FLOW_COOKIE_MAX_AGE
32
- };
33
- }
34
- function sessionCookieOptions(maxAge) {
35
- return {
36
- httpOnly: true,
37
- secure: process.env.NODE_ENV === "production",
38
- sameSite: "lax",
39
- path: "/",
40
- maxAge: maxAge ? Math.min(maxAge, SESSION_COOKIE_MAX_AGE) : SESSION_COOKIE_MAX_AGE
41
- };
42
- }
43
- function setLedveryFlowCookies(response, params) {
44
- const opts = flowCookieOptions();
45
- setCookie(response, SM_LEDVERY_STATE_COOKIE, params.state, opts);
46
- setCookie(response, SM_LEDVERY_PKCE_VERIFIER_COOKIE, params.codeVerifier, opts);
47
- setCookie(response, SM_LEDVERY_NONCE_COOKIE, params.nonce, opts);
48
- }
49
- function validateAndConsumeLedveryFlowCookies(request, callbackState) {
50
- const cookieState = getCookie(request, SM_LEDVERY_STATE_COOKIE);
51
- const codeVerifier = getCookie(request, SM_LEDVERY_PKCE_VERIFIER_COOKIE);
52
- const nonce = getCookie(request, SM_LEDVERY_NONCE_COOKIE);
53
- if (!cookieState) return "Missing Ledvery state cookie - session may have expired";
54
- if (!callbackState) return "Missing state parameter in callback";
55
- if (!constantTimeEqual(cookieState, callbackState)) return "Ledvery state mismatch - possible CSRF attack";
56
- if (!codeVerifier) return "Missing PKCE verifier cookie";
57
- if (!nonce) return "Missing nonce cookie";
58
- return { codeVerifier, nonce };
59
- }
60
- function setLedverySession(response, session, opts) {
61
- const cookieOpts = sessionCookieOptions(opts?.cookies?.maxAge);
62
- if (opts?.cookies?.domain) cookieOpts.domain = opts.cookies.domain;
63
- if (opts?.cookies?.path) cookieOpts.path = opts.cookies.path;
64
- if (opts?.cookies?.sameSite) cookieOpts.sameSite = opts.cookies.sameSite;
65
- if (opts?.cookies?.secure !== void 0) cookieOpts.secure = opts.cookies.secure;
66
- const sessionPayload = JSON.stringify({
67
- idToken: session.idToken,
68
- claims: session.claims,
69
- expiresAt: session.expiresAt
70
- });
71
- setCookie(response, SM_LEDVERY_ID_TOKEN_COOKIE, sessionPayload, cookieOpts);
72
- if (opts?.storeAccessToken && session.accessToken) {
73
- setCookie(response, SM_LEDVERY_ACCESS_TOKEN_COOKIE, session.accessToken, cookieOpts);
74
- }
75
- }
76
- function getLedverySession(request) {
77
- const raw = getCookie(request, SM_LEDVERY_ID_TOKEN_COOKIE);
78
- if (!raw) return null;
79
- try {
80
- const parsed = JSON.parse(raw);
81
- return {
82
- idToken: parsed.idToken || "",
83
- claims: parsed.claims,
84
- expiresAt: parsed.expiresAt
85
- };
86
- } catch {
87
- return null;
88
- }
89
- }
90
- function clearLedverySession(response) {
91
- deleteCookie(response, SM_LEDVERY_ID_TOKEN_COOKIE);
92
- deleteCookie(response, SM_LEDVERY_ACCESS_TOKEN_COOKIE);
93
- }
94
- function clearLedveryFlowCookies(response) {
95
- deleteCookie(response, SM_LEDVERY_STATE_COOKIE);
96
- deleteCookie(response, SM_LEDVERY_PKCE_VERIFIER_COOKIE);
97
- deleteCookie(response, SM_LEDVERY_NONCE_COOKIE);
98
- }
99
- function getCookie(request, name) {
100
- const header = request.headers.get("cookie") || "";
101
- const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
102
- return match ? decodeURIComponent(match[1]) : void 0;
103
- }
104
- function setCookie(response, name, value, opts) {
105
- const parts = [
106
- `${name}=${encodeURIComponent(value)}`,
107
- `Path=${opts.path}`,
108
- `Max-Age=${opts.maxAge}`,
109
- `SameSite=${opts.sameSite.charAt(0).toUpperCase() + opts.sameSite.slice(1)}`
110
- ];
111
- if (opts.httpOnly) parts.push("HttpOnly");
112
- if (opts.secure) parts.push("Secure");
113
- if (opts.domain) parts.push(`Domain=${opts.domain}`);
114
- response.headers.append("Set-Cookie", parts.join("; "));
115
- }
116
- function deleteCookie(response, name) {
117
- response.headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`);
118
- }
119
-
120
- // src/server/redirect.ts
121
- function normalizeOrigin(origin) {
122
- try {
123
- const u = new URL(origin);
124
- return `${u.protocol}//${u.host}`.toLowerCase();
125
- } catch {
126
- return null;
127
- }
128
- }
129
- function isSafeSchemeRelative(input) {
130
- return !(input.startsWith("//") || input.startsWith("\\"));
131
- }
132
- function validateSafeRedirect(input, opts = {}) {
133
- const defaultPath = opts.defaultPath ?? "/";
134
- if (!input || typeof input !== "string") return defaultPath;
135
- const trimmed = input.trim();
136
- if (!trimmed) return defaultPath;
137
- if (!isSafeSchemeRelative(trimmed)) return defaultPath;
138
- const colon = trimmed.indexOf(":");
139
- if (colon > 0 && colon < 15) {
140
- const scheme = trimmed.slice(0, colon).toLowerCase();
141
- if (/^[a-z][a-z0-9+.-]*$/.test(scheme) && scheme !== "http" && scheme !== "https") {
142
- return defaultPath;
143
- }
144
- }
145
- if (trimmed.startsWith("/")) {
146
- if (trimmed.length > 1 && trimmed[1] === "\\") return defaultPath;
147
- return trimmed;
148
- }
149
- const allowed = (opts.allowedOrigins ?? []).map(normalizeOrigin).filter((o) => o !== null);
150
- try {
151
- const parsed = new URL(trimmed);
152
- const origin = `${parsed.protocol}//${parsed.host}`.toLowerCase();
153
- if (!allowed.includes(origin)) return defaultPath;
154
- if (opts.stripSameOriginHost !== false) {
155
- return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/";
156
- }
157
- return trimmed;
158
- } catch {
159
- return defaultPath;
160
- }
161
- }
162
-
163
- // src/server/ledvery.ts
164
- function createLedveryRoutes(config) {
165
- const client = new LedveryClient({
166
- issuer: config.issuer,
167
- clientId: config.clientId,
168
- clientSecret: config.clientSecret,
169
- redirectUri: config.redirectUri,
170
- fetch: config.fetch
171
- });
172
- const postLoginRedirect = config.postLoginRedirect || "/";
173
- const postLogoutRedirect = config.postLogoutRedirect || "/";
174
- const defaultScope = config.defaultScope || "openid email profile";
175
- async function handleLogin(request) {
176
- const url = new URL(request.url);
177
- const returnTo = url.searchParams.get("returnTo");
178
- const validatedReturnTo = validateSafeRedirect(returnTo, { defaultPath: postLoginRedirect });
179
- const authResult = await client.createAuthorizationUrl({
180
- scope: defaultScope
181
- });
182
- const response = new Response(null, {
183
- status: 302,
184
- headers: { Location: authResult.url }
185
- });
186
- setLedveryFlowCookies(response, {
187
- state: authResult.state,
188
- codeVerifier: authResult.codeVerifier,
189
- nonce: authResult.nonce
190
- });
191
- if (validatedReturnTo !== postLoginRedirect) {
192
- response.headers.append(
193
- "Set-Cookie",
194
- `sm_ledvery_return_to=${encodeURIComponent(validatedReturnTo)}; Path=/; Max-Age=600; HttpOnly; SameSite=Lax${process.env.NODE_ENV === "production" ? "; Secure" : ""}`
195
- );
196
- }
197
- return response;
198
- }
199
- async function handleCallback(request) {
200
- const url = new URL(request.url);
201
- const code = url.searchParams.get("code");
202
- const callbackState = url.searchParams.get("state");
203
- if (!code) {
204
- const error = url.searchParams.get("error");
205
- const errorDesc = url.searchParams.get("error_description");
206
- return new Response(
207
- JSON.stringify({ error: error || "missing_code", message: errorDesc || "No authorization code in callback" }),
208
- { status: 403, headers: { "Content-Type": "application/json" } }
209
- );
210
- }
211
- const flowResult = validateAndConsumeLedveryFlowCookies(request, callbackState);
212
- if (typeof flowResult === "string") {
213
- return new Response(
214
- JSON.stringify({ error: "state_mismatch", message: flowResult }),
215
- { status: 403, headers: { "Content-Type": "application/json" } }
216
- );
217
- }
218
- let session;
219
- try {
220
- session = await client.exchangeCode({
221
- code,
222
- codeVerifier: flowResult.codeVerifier,
223
- receivedState: callbackState,
224
- expectedState: getCookieValue(request, "sm_ledvery_state"),
225
- expectedNonce: flowResult.nonce
226
- });
227
- } catch (err) {
228
- return new Response(
229
- JSON.stringify({ error: "code_exchange_failed", message: err instanceof Error ? err.message : "Code exchange failed" }),
230
- { status: 403, headers: { "Content-Type": "application/json" } }
231
- );
232
- }
233
- const returnTo = getCookieValue(request, "sm_ledvery_return_to");
234
- const redirectTo = validateSafeRedirect(returnTo, { defaultPath: postLoginRedirect });
235
- const response = new Response(null, {
236
- status: 302,
237
- headers: { Location: redirectTo }
238
- });
239
- setLedverySession(response, {
240
- idToken: session.idToken,
241
- accessToken: session.accessToken,
242
- claims: session.claims,
243
- expiresAt: session.expiresAt.toISOString()
244
- }, {
245
- storeAccessToken: config.storeAccessToken,
246
- cookies: config.cookies
247
- });
248
- clearLedveryFlowCookies(response);
249
- response.headers.append("Set-Cookie", "sm_ledvery_return_to=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax");
250
- return response;
251
- }
252
- function handleSession(request) {
253
- const session = getLedverySession(request);
254
- if (!session) {
255
- return new Response(JSON.stringify({ session: null }), {
256
- status: 200,
257
- headers: { "Content-Type": "application/json" }
258
- });
259
- }
260
- const expiresAt = new Date(session.expiresAt);
261
- if (expiresAt <= /* @__PURE__ */ new Date()) {
262
- return new Response(JSON.stringify({ session: null }), {
263
- status: 200,
264
- headers: { "Content-Type": "application/json" }
265
- });
266
- }
267
- return new Response(
268
- JSON.stringify({
269
- session: {
270
- sub: session.claims.sub,
271
- email: session.claims.email,
272
- email_verified: session.claims.email_verified,
273
- name: session.claims.name,
274
- idp: session.claims.idp,
275
- expiresAt: session.expiresAt
276
- }
277
- }),
278
- { status: 200, headers: { "Content-Type": "application/json" } }
279
- );
280
- }
281
- function handleLogout(request) {
282
- const session = getLedverySession(request);
283
- const idToken = session?.idToken;
284
- if (config.gatewayUrl) {
285
- let rpLogoutUrl = `${config.gatewayUrl}/v1/auth/oauth/ledvery/logout?post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirect)}`;
286
- if (idToken) {
287
- rpLogoutUrl += `&id_token_hint=${encodeURIComponent(idToken)}`;
288
- }
289
- const response2 = new Response(null, {
290
- status: 302,
291
- headers: { Location: rpLogoutUrl }
292
- });
293
- clearLedverySession(response2);
294
- return response2;
295
- }
296
- const response = new Response(null, {
297
- status: 302,
298
- headers: { Location: postLogoutRedirect }
299
- });
300
- clearLedverySession(response);
301
- return response;
302
- }
303
- async function handler(request, context) {
304
- const params = await context.params;
305
- const action = params.action?.[0] || "";
306
- switch (action) {
307
- case "login":
308
- return handleLogin(request);
309
- case "callback":
310
- return handleCallback(request);
311
- case "session":
312
- return handleSession(request);
313
- case "logout":
314
- return handleLogout(request);
315
- default:
316
- return new Response(
317
- JSON.stringify({ error: "not_found", message: `Unknown Ledvery route: ${action}` }),
318
- { status: 404, headers: { "Content-Type": "application/json" } }
319
- );
320
- }
321
- }
322
- return {
323
- GET: handler,
324
- POST: handler
325
- };
326
- }
327
- function getCookieValue(request, name) {
328
- const header = request.headers.get("cookie") || "";
329
- const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
330
- return match ? decodeURIComponent(match[1]) : void 0;
331
- }
332
-
333
- export { createLedveryRoutes, getLedverySession };