@incorta/auth 1.0.0

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/index.cjs ADDED
@@ -0,0 +1,642 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ IncortaAuthError: () => IncortaAuthError,
24
+ createIncortaAuth: () => createIncortaAuth,
25
+ registerClient: () => registerClient
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+
29
+ // src/core/config.ts
30
+ function required(value, name, envVar) {
31
+ if (!value) {
32
+ throw new Error(
33
+ `[incorta-auth] Missing required config "${name}" (or env var ${envVar}).`
34
+ );
35
+ }
36
+ return value;
37
+ }
38
+ function stripTrailingSlash(url) {
39
+ return url.replace(/\/+$/, "");
40
+ }
41
+ function resolveConfig(input = {}) {
42
+ const env = globalThis.process?.env ?? {};
43
+ const incortaUrl = stripTrailingSlash(
44
+ required(input.incortaUrl ?? env.INCORTA_URL, "incortaUrl", "INCORTA_URL")
45
+ );
46
+ const tenant = required(input.tenant ?? env.INCORTA_TENANT, "tenant", "INCORTA_TENANT");
47
+ const clientId = required(
48
+ input.clientId ?? env.INCORTA_CLIENT_ID,
49
+ "clientId",
50
+ "INCORTA_CLIENT_ID"
51
+ );
52
+ const clientSecret = required(
53
+ input.clientSecret ?? env.INCORTA_CLIENT_SECRET,
54
+ "clientSecret",
55
+ "INCORTA_CLIENT_SECRET"
56
+ );
57
+ const secret = required(
58
+ input.secret ?? env.INCORTA_AUTH_SECRET,
59
+ "secret",
60
+ "INCORTA_AUTH_SECRET"
61
+ );
62
+ if (secret.length < 32) {
63
+ throw new Error(
64
+ "[incorta-auth] `secret` must be at least 32 characters. Generate one with: openssl rand -base64 32"
65
+ );
66
+ }
67
+ const basePath = input.basePath ?? "/auth";
68
+ if (!basePath.startsWith("/") || basePath.endsWith("/")) {
69
+ throw new Error(
70
+ `[incorta-auth] basePath must start with "/" and not end with "/" (got "${basePath}").`
71
+ );
72
+ }
73
+ const appUrl = input.appUrl ?? env.INCORTA_APP_URL;
74
+ const cookieName = input.session?.cookieName ?? "incorta_auth_session";
75
+ const issuer = `${incortaUrl}/oauth/${tenant}`;
76
+ return {
77
+ incortaUrl,
78
+ tenant,
79
+ clientId,
80
+ clientSecret,
81
+ secret,
82
+ basePath,
83
+ appUrl: appUrl ? stripTrailingSlash(appUrl) : void 0,
84
+ scope: input.scope ?? "openid profile email",
85
+ cookieName,
86
+ stateCookieName: `${cookieName}_state`,
87
+ sessionMaxAge: input.session?.maxAge ?? 12 * 60 * 60,
88
+ secureCookies: input.cookies?.secure ?? "auto",
89
+ exposeAccessToken: input.exposeAccessToken ?? false,
90
+ issuer,
91
+ discoveryUrl: input.advanced?.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`,
92
+ jwksOverride: input.advanced?.jwks,
93
+ fetch: input.advanced?.fetch ?? fetch
94
+ };
95
+ }
96
+
97
+ // src/core/handler.ts
98
+ var import_node_crypto2 = require("crypto");
99
+
100
+ // src/core/cookies.ts
101
+ function parseCookies(header) {
102
+ const out = {};
103
+ if (!header) return out;
104
+ for (const part of header.split(";")) {
105
+ const eq = part.indexOf("=");
106
+ if (eq === -1) continue;
107
+ const name = part.slice(0, eq).trim();
108
+ if (!name) continue;
109
+ let value = part.slice(eq + 1).trim();
110
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
111
+ try {
112
+ out[name] = decodeURIComponent(value);
113
+ } catch {
114
+ out[name] = value;
115
+ }
116
+ }
117
+ return out;
118
+ }
119
+ function serializeCookie(name, value, options = {}) {
120
+ const parts = [`${name}=${encodeURIComponent(value)}`];
121
+ parts.push(`Path=${options.path ?? "/"}`);
122
+ if (options.maxAge !== void 0) {
123
+ parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
124
+ parts.push(`Expires=${new Date(Date.now() + options.maxAge * 1e3).toUTCString()}`);
125
+ }
126
+ if (options.httpOnly ?? true) parts.push("HttpOnly");
127
+ if (options.secure) parts.push("Secure");
128
+ const sameSite = options.sameSite ?? "lax";
129
+ parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);
130
+ return parts.join("; ");
131
+ }
132
+ function deleteCookie(name, options = {}) {
133
+ return serializeCookie(name, "", { ...options, maxAge: 0 });
134
+ }
135
+
136
+ // src/core/discovery.ts
137
+ var import_jose = require("jose");
138
+ var endpointsCache = /* @__PURE__ */ new Map();
139
+ function getEndpoints(config) {
140
+ const cached = endpointsCache.get(config.issuer);
141
+ if (cached) return cached;
142
+ const promise = resolve(config).catch((error) => {
143
+ endpointsCache.delete(config.issuer);
144
+ throw error;
145
+ });
146
+ endpointsCache.set(config.issuer, promise);
147
+ return promise;
148
+ }
149
+ async function resolve(config) {
150
+ const fallback = {
151
+ issuer: config.issuer,
152
+ authorization_endpoint: `${config.issuer}/authorize`,
153
+ token_endpoint: `${config.issuer}/token`,
154
+ jwks_uri: `${config.issuer}/.well-known/jwks.json`
155
+ };
156
+ let doc = fallback;
157
+ try {
158
+ const response = await config.fetch(config.discoveryUrl, {
159
+ headers: { accept: "application/json" }
160
+ });
161
+ if (response.ok) {
162
+ const body = await response.json();
163
+ doc = {
164
+ issuer: body.issuer ?? fallback.issuer,
165
+ authorization_endpoint: body.authorization_endpoint ?? fallback.authorization_endpoint,
166
+ token_endpoint: body.token_endpoint ?? fallback.token_endpoint,
167
+ jwks_uri: body.jwks_uri ?? fallback.jwks_uri
168
+ };
169
+ }
170
+ } catch {
171
+ }
172
+ const jwks = config.jwksOverride ?? (0, import_jose.createRemoteJWKSet)(new URL(doc.jwks_uri), {
173
+ cacheMaxAge: 10 * 60 * 1e3,
174
+ cooldownDuration: 30 * 1e3
175
+ });
176
+ return {
177
+ issuer: doc.issuer,
178
+ authorizationEndpoint: doc.authorization_endpoint,
179
+ tokenEndpoint: doc.token_endpoint,
180
+ jwks
181
+ };
182
+ }
183
+
184
+ // src/core/session.ts
185
+ var import_node_crypto = require("crypto");
186
+ var import_jose2 = require("jose");
187
+ function sealKey(config) {
188
+ return new Uint8Array(
189
+ (0, import_node_crypto.createHash)("sha256").update(`incorta-auth:${config.secret}`).digest()
190
+ );
191
+ }
192
+ async function seal(config, purpose, data, maxAgeSeconds) {
193
+ return new import_jose2.EncryptJWT({ purpose, data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${maxAgeSeconds}s`).encrypt(sealKey(config));
194
+ }
195
+ async function unseal(config, purpose, cookieValue) {
196
+ if (!cookieValue) return null;
197
+ try {
198
+ const { payload } = await (0, import_jose2.jwtDecrypt)(cookieValue, sealKey(config));
199
+ if (payload.purpose !== purpose) return null;
200
+ return {
201
+ data: payload.data,
202
+ issuedAt: (payload.iat ?? 0) * 1e3,
203
+ expiresAt: (payload.exp ?? 0) * 1e3
204
+ };
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+ function sealSession(config, session) {
210
+ return seal(config, "session", session, config.sessionMaxAge);
211
+ }
212
+ async function unsealSession(config, cookieValue) {
213
+ const result = await unseal(config, "session", cookieValue);
214
+ if (!result) return null;
215
+ const { data, issuedAt, expiresAt } = result;
216
+ if (!data || typeof data !== "object" || !data.user || !data.accessToken) return null;
217
+ return { session: data, issuedAt, expiresAt };
218
+ }
219
+ var STATE_MAX_AGE_SECONDS = 10 * 60;
220
+ function sealLoginState(config, state) {
221
+ return seal(config, "state", state, STATE_MAX_AGE_SECONDS);
222
+ }
223
+ async function unsealLoginState(config, cookieValue) {
224
+ const result = await unseal(config, "state", cookieValue);
225
+ if (!result) return null;
226
+ const { data } = result;
227
+ if (!data || typeof data.state !== "string" || typeof data.nonce !== "string") return null;
228
+ return data;
229
+ }
230
+
231
+ // src/core/tokens.ts
232
+ var import_jose3 = require("jose");
233
+ var IncortaAuthError = class extends Error {
234
+ constructor(code, message) {
235
+ super(`[incorta-auth] ${message}`);
236
+ this.code = code;
237
+ this.name = "IncortaAuthError";
238
+ }
239
+ code;
240
+ };
241
+ function basicAuth(config) {
242
+ const raw = `${config.clientId}:${config.clientSecret}`;
243
+ return `Basic ${Buffer.from(raw, "utf8").toString("base64")}`;
244
+ }
245
+ async function callTokenEndpoint(config, params) {
246
+ const { tokenEndpoint } = await getEndpoints(config);
247
+ let response;
248
+ try {
249
+ response = await config.fetch(tokenEndpoint, {
250
+ method: "POST",
251
+ headers: {
252
+ "content-type": "application/x-www-form-urlencoded",
253
+ accept: "application/json",
254
+ authorization: basicAuth(config)
255
+ },
256
+ body: new URLSearchParams(params).toString()
257
+ });
258
+ } catch (error) {
259
+ throw new IncortaAuthError(
260
+ "incorta_unreachable",
261
+ `Could not reach the Incorta token endpoint (${tokenEndpoint}): ${error.message}`
262
+ );
263
+ }
264
+ let body;
265
+ try {
266
+ body = await response.json();
267
+ } catch {
268
+ throw new IncortaAuthError(
269
+ "invalid_token_response",
270
+ `Token endpoint returned a non-JSON response (HTTP ${response.status}).`
271
+ );
272
+ }
273
+ if (!response.ok || !body.access_token) {
274
+ throw new IncortaAuthError(
275
+ body.error ?? "token_exchange_failed",
276
+ body.error_description ?? `Token exchange failed (HTTP ${response.status}).`
277
+ );
278
+ }
279
+ const { exp } = (0, import_jose3.decodeJwt)(body.access_token);
280
+ return {
281
+ accessToken: body.access_token,
282
+ accessTokenExpiresAt: exp ? exp * 1e3 : Date.now() + 60 * 60 * 1e3,
283
+ idToken: body.id_token,
284
+ refreshToken: body.refresh_token
285
+ };
286
+ }
287
+ function exchangeCode(config, code, redirectUri) {
288
+ return callTokenEndpoint(config, {
289
+ grant_type: "authorization_code",
290
+ code,
291
+ redirect_uri: redirectUri
292
+ });
293
+ }
294
+ function refreshTokens(config, refreshToken) {
295
+ return callTokenEndpoint(config, {
296
+ grant_type: "refresh_token",
297
+ refresh_token: refreshToken
298
+ });
299
+ }
300
+ async function verifyJwt(config, token) {
301
+ const { jwks, issuer } = await getEndpoints(config);
302
+ try {
303
+ const { payload } = await (0, import_jose3.jwtVerify)(token, jwks, {
304
+ issuer,
305
+ audience: config.clientId,
306
+ algorithms: ["RS256"]
307
+ });
308
+ return payload;
309
+ } catch (error) {
310
+ throw new IncortaAuthError(
311
+ "invalid_token",
312
+ `Token verification failed: ${error.message}`
313
+ );
314
+ }
315
+ }
316
+ async function verifyAccessToken(config, accessToken) {
317
+ return verifyJwt(config, accessToken);
318
+ }
319
+ async function verifyIdToken(config, idToken, expectedNonce) {
320
+ const payload = await verifyJwt(config, idToken);
321
+ if (payload.nonce !== expectedNonce) {
322
+ throw new IncortaAuthError("nonce_mismatch", "ID token nonce does not match the login request.");
323
+ }
324
+ if (typeof payload.sub !== "string" || !payload.sub) {
325
+ throw new IncortaAuthError("invalid_token", "ID token is missing the sub claim.");
326
+ }
327
+ const roles = Array.isArray(payload.roles) ? payload.roles.filter((role) => typeof role === "string") : [];
328
+ return {
329
+ sub: payload.sub,
330
+ name: typeof payload.name === "string" ? payload.name : void 0,
331
+ email: typeof payload.email === "string" ? payload.email : void 0,
332
+ roles,
333
+ tenant: typeof payload.tenant === "string" ? payload.tenant : config.tenant
334
+ };
335
+ }
336
+ async function userFromAccessToken(config, accessToken) {
337
+ const payload = await verifyAccessToken(config, accessToken);
338
+ if (typeof payload.sub !== "string" || !payload.sub) {
339
+ throw new IncortaAuthError("invalid_token", "Access token is missing the sub claim.");
340
+ }
341
+ return {
342
+ sub: payload.sub,
343
+ roles: [],
344
+ tenant: typeof payload.tenant === "string" ? payload.tenant : config.tenant
345
+ };
346
+ }
347
+
348
+ // src/core/handler.ts
349
+ var ACCESS_TOKEN_REFRESH_LEEWAY_MS = 60 * 1e3;
350
+ function requestOrigin(config, request) {
351
+ if (config.appUrl) return config.appUrl;
352
+ const url = new URL(request.url);
353
+ const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim() ?? url.protocol.replace(":", "");
354
+ const host = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim() ?? request.headers.get("host") ?? url.host;
355
+ return `${proto}://${host}`;
356
+ }
357
+ function isSecureRequest(config, request) {
358
+ if (config.secureCookies !== "auto") return config.secureCookies;
359
+ return requestOrigin(config, request).startsWith("https://");
360
+ }
361
+ function validateReturnTo(raw, origin) {
362
+ if (!raw) return "/";
363
+ if (raw.startsWith("/") && !raw.startsWith("//")) return raw;
364
+ try {
365
+ const url = new URL(raw);
366
+ if (url.origin === origin) return `${url.pathname}${url.search}${url.hash}` || "/";
367
+ } catch {
368
+ }
369
+ return "/";
370
+ }
371
+ function json(status, body, cookies = []) {
372
+ const headers = new Headers({
373
+ "content-type": "application/json; charset=utf-8",
374
+ "cache-control": "no-store"
375
+ });
376
+ for (const cookie of cookies) headers.append("set-cookie", cookie);
377
+ return new Response(JSON.stringify(body), { status, headers });
378
+ }
379
+ function redirect(location, cookies = []) {
380
+ const headers = new Headers({ location, "cache-control": "no-store" });
381
+ for (const cookie of cookies) headers.append("set-cookie", cookie);
382
+ return new Response(null, { status: 302, headers });
383
+ }
384
+ function withErrorParam(target, code) {
385
+ const separator = target.includes("?") ? "&" : "?";
386
+ return `${target}${separator}incorta_auth_error=${encodeURIComponent(code)}`;
387
+ }
388
+ function randomToken() {
389
+ return (0, import_node_crypto2.randomBytes)(16).toString("base64url");
390
+ }
391
+ function createHandler(config) {
392
+ const sessionCookieOptions = (secure) => ({
393
+ httpOnly: true,
394
+ secure,
395
+ sameSite: "lax",
396
+ path: "/"
397
+ });
398
+ async function login(request, url) {
399
+ const origin = requestOrigin(config, request);
400
+ const secure = isSecureRequest(config, request);
401
+ const returnTo = validateReturnTo(url.searchParams.get("redirect_to"), origin);
402
+ const cookies = parseCookies(request.headers.get("cookie"));
403
+ const existing = await unsealSession(config, cookies[config.cookieName]);
404
+ if (existing) return redirect(returnTo);
405
+ const state = randomToken();
406
+ const nonce = randomToken();
407
+ const stateCookie = serializeCookie(
408
+ config.stateCookieName,
409
+ await sealLoginState(config, { state, nonce, returnTo }),
410
+ { ...sessionCookieOptions(secure), maxAge: 600 }
411
+ );
412
+ const { authorizationEndpoint } = await getEndpoints(config);
413
+ const authorizeUrl = new URL(authorizationEndpoint);
414
+ authorizeUrl.searchParams.set("response_type", "code");
415
+ authorizeUrl.searchParams.set("client_id", config.clientId);
416
+ authorizeUrl.searchParams.set("redirect_uri", `${origin}${config.basePath}/callback`);
417
+ authorizeUrl.searchParams.set("scope", config.scope);
418
+ authorizeUrl.searchParams.set("state", state);
419
+ authorizeUrl.searchParams.set("nonce", nonce);
420
+ return redirect(authorizeUrl.toString(), [stateCookie]);
421
+ }
422
+ async function callback(request, url) {
423
+ const origin = requestOrigin(config, request);
424
+ const secure = isSecureRequest(config, request);
425
+ const cookies = parseCookies(request.headers.get("cookie"));
426
+ const clearState = deleteCookie(config.stateCookieName, sessionCookieOptions(secure));
427
+ const loginState = await unsealLoginState(config, cookies[config.stateCookieName]);
428
+ const returnTo = validateReturnTo(loginState?.returnTo ?? null, origin);
429
+ const fail = (code2) => redirect(withErrorParam(returnTo, code2), [clearState]);
430
+ const oauthError = url.searchParams.get("error");
431
+ if (oauthError) return fail(oauthError);
432
+ const code = url.searchParams.get("code");
433
+ const state = url.searchParams.get("state");
434
+ if (!code || !state) return fail("missing_code");
435
+ if (!loginState) return fail("state_missing");
436
+ if (state !== loginState.state) return fail("state_mismatch");
437
+ try {
438
+ const tokens = await exchangeCode(config, code, `${origin}${config.basePath}/callback`);
439
+ const user = tokens.idToken ? await verifyIdToken(config, tokens.idToken, loginState.nonce) : await userFromAccessToken(config, tokens.accessToken);
440
+ const session = {
441
+ user,
442
+ accessToken: tokens.accessToken,
443
+ accessTokenExpiresAt: tokens.accessTokenExpiresAt,
444
+ refreshToken: tokens.refreshToken,
445
+ createdAt: Date.now()
446
+ };
447
+ const sessionCookie = serializeCookie(
448
+ config.cookieName,
449
+ await sealSession(config, session),
450
+ { ...sessionCookieOptions(secure), maxAge: config.sessionMaxAge }
451
+ );
452
+ return redirect(returnTo, [sessionCookie, clearState]);
453
+ } catch (error) {
454
+ const code2 = error instanceof IncortaAuthError ? error.code : "callback_failed";
455
+ return fail(code2);
456
+ }
457
+ }
458
+ async function loadSession(request) {
459
+ const secure = isSecureRequest(config, request);
460
+ const cookies = parseCookies(request.headers.get("cookie"));
461
+ const unsealed = await unsealSession(config, cookies[config.cookieName]);
462
+ if (!unsealed) return { session: null, sessionExpiresAt: 0, setCookies: [] };
463
+ let { session } = unsealed;
464
+ let mustReseal = false;
465
+ const now = Date.now();
466
+ if (session.accessTokenExpiresAt - now < ACCESS_TOKEN_REFRESH_LEEWAY_MS) {
467
+ if (session.refreshToken) {
468
+ try {
469
+ const tokens = await refreshTokens(config, session.refreshToken);
470
+ session = {
471
+ ...session,
472
+ accessToken: tokens.accessToken,
473
+ accessTokenExpiresAt: tokens.accessTokenExpiresAt,
474
+ refreshToken: tokens.refreshToken ?? session.refreshToken
475
+ };
476
+ mustReseal = true;
477
+ } catch {
478
+ if (session.accessTokenExpiresAt <= now) {
479
+ return {
480
+ session: null,
481
+ sessionExpiresAt: 0,
482
+ setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))]
483
+ };
484
+ }
485
+ }
486
+ } else if (session.accessTokenExpiresAt <= now) {
487
+ return {
488
+ session: null,
489
+ sessionExpiresAt: 0,
490
+ setCookies: [deleteCookie(config.cookieName, sessionCookieOptions(secure))]
491
+ };
492
+ }
493
+ }
494
+ const halfLifeMs = config.sessionMaxAge * 1e3 / 2;
495
+ if (now - unsealed.issuedAt > halfLifeMs) mustReseal = true;
496
+ if (!mustReseal) {
497
+ return { session, sessionExpiresAt: unsealed.expiresAt, setCookies: [] };
498
+ }
499
+ const resealed = serializeCookie(config.cookieName, await sealSession(config, session), {
500
+ ...sessionCookieOptions(secure),
501
+ maxAge: config.sessionMaxAge
502
+ });
503
+ return {
504
+ session,
505
+ sessionExpiresAt: now + config.sessionMaxAge * 1e3,
506
+ setCookies: [resealed]
507
+ };
508
+ }
509
+ async function sessionRoute(request) {
510
+ const { session, sessionExpiresAt, setCookies } = await loadSession(request);
511
+ if (!session) return json(200, { user: null }, setCookies);
512
+ return json(
513
+ 200,
514
+ {
515
+ user: session.user,
516
+ session: {
517
+ expiresAt: sessionExpiresAt,
518
+ accessTokenExpiresAt: session.accessTokenExpiresAt
519
+ }
520
+ },
521
+ setCookies
522
+ );
523
+ }
524
+ async function tokenRoute(request) {
525
+ if (!config.exposeAccessToken) return json(404, { error: "not_found" });
526
+ const { session, setCookies } = await loadSession(request);
527
+ if (!session) return json(401, { error: "unauthenticated" }, setCookies);
528
+ return json(
529
+ 200,
530
+ { accessToken: session.accessToken, expiresAt: session.accessTokenExpiresAt },
531
+ setCookies
532
+ );
533
+ }
534
+ async function logout(request, url) {
535
+ const secure = isSecureRequest(config, request);
536
+ const clearSession = deleteCookie(config.cookieName, sessionCookieOptions(secure));
537
+ if (request.method === "GET") {
538
+ const origin = requestOrigin(config, request);
539
+ const returnTo = validateReturnTo(url.searchParams.get("redirect_to"), origin);
540
+ return redirect(returnTo, [clearSession]);
541
+ }
542
+ return json(200, { ok: true }, [clearSession]);
543
+ }
544
+ async function handler(request) {
545
+ const url = new URL(request.url);
546
+ if (url.pathname !== config.basePath && !url.pathname.startsWith(`${config.basePath}/`)) {
547
+ return json(404, { error: "not_found" });
548
+ }
549
+ const route = url.pathname.slice(config.basePath.length) || "/";
550
+ const method = request.method.toUpperCase();
551
+ try {
552
+ if (route === "/login" && method === "GET") return await login(request, url);
553
+ if (route === "/callback" && method === "GET") return await callback(request, url);
554
+ if (route === "/session" && method === "GET") return await sessionRoute(request);
555
+ if (route === "/token" && method === "GET") return await tokenRoute(request);
556
+ if (route === "/logout" && (method === "POST" || method === "GET")) {
557
+ return await logout(request, url);
558
+ }
559
+ return json(404, { error: "not_found" });
560
+ } catch (error) {
561
+ const code = error instanceof IncortaAuthError ? error.code : "internal_error";
562
+ const message = error instanceof Error ? error.message : "Unexpected incorta-auth handler error";
563
+ console.error(`[incorta-auth] ${route} failed:`, message);
564
+ return json(500, { error: code });
565
+ }
566
+ }
567
+ async function getSession(request) {
568
+ const cookies = parseCookies(request.headers.get("cookie"));
569
+ const unsealed = await unsealSession(config, cookies[config.cookieName]);
570
+ if (!unsealed) return null;
571
+ const { session, expiresAt } = unsealed;
572
+ if (session.accessTokenExpiresAt <= Date.now() && !session.refreshToken) return null;
573
+ return {
574
+ user: session.user,
575
+ accessToken: session.accessToken,
576
+ accessTokenExpiresAt: session.accessTokenExpiresAt,
577
+ expiresAt
578
+ };
579
+ }
580
+ return { handler, getSession };
581
+ }
582
+
583
+ // src/core/register.ts
584
+ async function registerClient(options) {
585
+ const {
586
+ incortaUrl,
587
+ tenant,
588
+ name,
589
+ redirectUris,
590
+ scope = "openid profile email",
591
+ grantTypes = ["authorization_code", "refresh_token"],
592
+ fetchImpl = fetch
593
+ } = options;
594
+ if (!redirectUris.length) {
595
+ throw new Error("[incorta-auth] registerClient requires at least one redirect URI.");
596
+ }
597
+ const endpoint = `${incortaUrl.replace(/\/+$/, "")}/oauth/${tenant}/register`;
598
+ const response = await fetchImpl(endpoint, {
599
+ method: "POST",
600
+ headers: { "content-type": "application/json", accept: "application/json" },
601
+ body: JSON.stringify({
602
+ client_name: name,
603
+ redirect_uris: redirectUris,
604
+ grant_types: grantTypes,
605
+ scope,
606
+ token_endpoint_auth_method: "client_secret_basic"
607
+ })
608
+ });
609
+ let body;
610
+ try {
611
+ body = await response.json();
612
+ } catch {
613
+ throw new Error(
614
+ `[incorta-auth] Client registration returned a non-JSON response (HTTP ${response.status}). Is the OAuth authorization server enabled on this cluster?`
615
+ );
616
+ }
617
+ if (!response.ok || !body.client_id || !body.client_secret) {
618
+ throw new Error(
619
+ `[incorta-auth] Client registration failed (HTTP ${response.status}): ${body.error ?? "unknown_error"} \u2014 ${body.error_description ?? "no description"}`
620
+ );
621
+ }
622
+ return {
623
+ clientId: body.client_id,
624
+ clientSecret: body.client_secret,
625
+ registrationAccessToken: body.registration_access_token,
626
+ registrationClientUri: body.registration_client_uri
627
+ };
628
+ }
629
+
630
+ // src/index.ts
631
+ function createIncortaAuth(config = {}) {
632
+ const resolved = resolveConfig(config);
633
+ const { handler, getSession } = createHandler(resolved);
634
+ return { handler, getSession, config: resolved };
635
+ }
636
+ // Annotate the CommonJS export names for ESM import in node:
637
+ 0 && (module.exports = {
638
+ IncortaAuthError,
639
+ createIncortaAuth,
640
+ registerClient
641
+ });
642
+ //# sourceMappingURL=index.cjs.map