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