@hyper.authio/bff 0.0.0-stage → 0.7.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,950 @@
1
+ import { randomBytes, createDecipheriv, createCipheriv, timingSafeEqual, createHash, hkdfSync, createHmac } from 'crypto';
2
+ import { AuthioClaimTypes, AuthioResourceAccessClaims, systemClock, createAuthioClient, createAuthioTokenDecrypter, encryptionKeyFromFile, AuthioPrompts, AuthioProtocolError, AuthioTokenTypes, AuthioError, AuthioConfigurationError, AuthioDefaults, resourceAccessSourceOf, parseResourceAccess, AuthioOAuthErrors, isAuthioTokenEncrypted } from '@hyper.authio/client';
3
+ import { Readable } from 'stream';
4
+ import { pipeline } from 'stream/promises';
5
+
6
+ // src/bff.ts
7
+ var AuthioBffClaimTypes = {
8
+ /** Seconds until the BFF session expires. */
9
+ SessionExpiresIn: "bff:session_expires_in",
10
+ /** The URL the SPA navigates to in order to log out; it carries the session's `sid`. */
11
+ LogoutUrl: "bff:logout_url",
12
+ /** One Authio role the person holds (flattened from the `*_resource_access` claims). */
13
+ Role: "role",
14
+ /** One permission the person holds, as `{resource}:{method}` (flattened from the `*_resource_access` claims). */
15
+ Permission: "permission"
16
+ };
17
+ var accessTokenClaimTypes = /* @__PURE__ */ new Set([
18
+ ...Object.keys(AuthioResourceAccessClaims),
19
+ AuthioClaimTypes.OrganizationId,
20
+ AuthioClaimTypes.OrganizationAlias,
21
+ AuthioClaimTypes.AccountId
22
+ ]);
23
+ var protocolClaimTypes = /* @__PURE__ */ new Set(["nonce", "aud", "azp", "iss", "iat", "nbf", "exp", "at_hash", "c_hash", "ipaddr", "platf", "ver"]);
24
+ function toSessionClaims(idToken, fallbackSubject) {
25
+ const claims = {};
26
+ for (const [type, value] of Object.entries(idToken ?? {})) if (!protocolClaimTypes.has(type) && value !== void 0 && value !== null) claims[type] = value;
27
+ if (claims.sub === void 0 && fallbackSubject) claims.sub = fallbackSubject;
28
+ return claims;
29
+ }
30
+ function mergeUserInfo(claims, userInfo) {
31
+ for (const [type, value] of Object.entries(userInfo)) if (claims[type] === void 0 && value !== void 0 && value !== null) claims[type] = value;
32
+ }
33
+ async function replaceAccessTokenClaims(claims, accessToken, decrypt) {
34
+ const payload = await readPayload(accessToken, decrypt);
35
+ if (!payload) return;
36
+ for (const type of Object.keys(claims)) if (accessTokenClaimTypes.has(type)) delete claims[type];
37
+ for (const [type, value] of Object.entries(payload)) if (accessTokenClaimTypes.has(type) && value !== void 0 && value !== null) claims[type] = value;
38
+ }
39
+ async function readAccessTokenSubject(accessToken, decrypt) {
40
+ const subject = (await readPayload(accessToken, decrypt))?.sub;
41
+ return typeof subject === "string" && subject ? subject : void 0;
42
+ }
43
+ function toAuthioBffClaims(claims, sessionExpiresIn, logoutUrl) {
44
+ const entries = [];
45
+ const seen = /* @__PURE__ */ new Set();
46
+ const add = (type, value) => {
47
+ const key = `${type}\0${value}`;
48
+ if (seen.has(key)) return;
49
+ seen.add(key);
50
+ entries.push({ type, value });
51
+ };
52
+ for (const [type, value] of Object.entries(claims)) {
53
+ if (resourceAccessSourceOf(type)) continue;
54
+ for (const item of Array.isArray(value) ? value : [value]) if (item !== void 0 && item !== null) add(type, toText(item));
55
+ }
56
+ const access = parseResourceAccess(claims);
57
+ for (const role of access.roles) add(AuthioBffClaimTypes.Role, role);
58
+ for (const [resource, methods] of Object.entries(access.permissions)) for (const method of methods) add(AuthioBffClaimTypes.Permission, `${resource}:${method}`);
59
+ entries.push({ type: AuthioBffClaimTypes.SessionExpiresIn, value: sessionExpiresIn });
60
+ entries.push({ type: AuthioBffClaimTypes.LogoutUrl, value: logoutUrl });
61
+ return entries;
62
+ }
63
+ async function readPayload(token, decrypt) {
64
+ if (!token) return void 0;
65
+ try {
66
+ const signed = isAuthioTokenEncrypted(token) ? await decrypt(token) : token;
67
+ const parts = signed.split(".");
68
+ if (parts.length !== 3 || !parts[1]) return void 0;
69
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
70
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : void 0;
71
+ } catch {
72
+ return void 0;
73
+ }
74
+ }
75
+ function toText(value) {
76
+ return typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value);
77
+ }
78
+
79
+ // src/http.ts
80
+ function readCookies(request) {
81
+ const cookies = /* @__PURE__ */ new Map();
82
+ for (const part of (request.headers.cookie ?? "").split(";")) {
83
+ const separator = part.indexOf("=");
84
+ if (separator <= 0) continue;
85
+ const name = part.slice(0, separator).trim();
86
+ if (!name || cookies.has(name)) continue;
87
+ let value = part.slice(separator + 1).trim();
88
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) value = value.slice(1, -1);
89
+ try {
90
+ cookies.set(name, decodeURIComponent(value));
91
+ } catch {
92
+ cookies.set(name, value);
93
+ }
94
+ }
95
+ return cookies;
96
+ }
97
+ function setCookie(response, name, value, sameSite, maxAge) {
98
+ if (response.headersSent) return;
99
+ const attributes = [
100
+ `${name}=${encodeURIComponent(value)}`,
101
+ "Path=/",
102
+ ...maxAge === void 0 ? [] : [`Max-Age=${Math.max(0, Math.floor(maxAge))}`],
103
+ ...maxAge === 0 ? ["Expires=Thu, 01 Jan 1970 00:00:00 GMT"] : [],
104
+ "HttpOnly",
105
+ "Secure",
106
+ `SameSite=${sameSite === "strict" ? "Strict" : "Lax"}`
107
+ ];
108
+ response.append("Set-Cookie", attributes.join("; "));
109
+ }
110
+ function sendError(response, status, error, description) {
111
+ if (response.headersSent) return;
112
+ response.status(status).set("Cache-Control", "no-store").json(description ? { error, error_description: description } : { error });
113
+ }
114
+ function hasAntiForgeryHeader(request, name, value) {
115
+ const lower = name.toLowerCase();
116
+ let count = 0;
117
+ let matched = false;
118
+ for (let index = 0; index < request.rawHeaders.length; index += 2) {
119
+ if (request.rawHeaders[index]?.toLowerCase() !== lower) continue;
120
+ count += 1;
121
+ matched = request.rawHeaders[index + 1] === value;
122
+ }
123
+ return count === 1 && matched;
124
+ }
125
+ function readQuery(request) {
126
+ return new URL(request.originalUrl || request.url, "http://localhost").searchParams;
127
+ }
128
+ function requestBase(request) {
129
+ return `${request.protocol}://${request.get("host") ?? "localhost"}${request.baseUrl ?? ""}`;
130
+ }
131
+ function withQueryParameter(url, name, value) {
132
+ const hash = url.indexOf("#");
133
+ const base = hash >= 0 ? url.slice(0, hash) : url;
134
+ const fragment = hash >= 0 ? url.slice(hash) : "";
135
+ return `${base}${base.includes("?") ? "&" : "?"}${encodeURIComponent(name)}=${encodeURIComponent(value)}${fragment}`;
136
+ }
137
+ async function readFormField(request, field, maxBytes = 64 * 1024) {
138
+ const parsed = request.body;
139
+ if (parsed && typeof parsed === "object" && !Buffer.isBuffer(parsed)) {
140
+ const value = parsed[field];
141
+ if (typeof value === "string") return value;
142
+ }
143
+ if (!(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/x-www-form-urlencoded")) return void 0;
144
+ const text = Buffer.isBuffer(parsed) ? parsed.toString("utf8") : typeof parsed === "string" ? parsed : request.readableEnded ? void 0 : await readText(request, maxBytes);
145
+ return text === void 0 ? void 0 : new URLSearchParams(text).get(field) ?? void 0;
146
+ }
147
+ function readText(request, maxBytes) {
148
+ return new Promise((resolve, reject) => {
149
+ const chunks = [];
150
+ let size = 0;
151
+ let done = false;
152
+ request.on("data", (chunk) => {
153
+ if (done) return;
154
+ size += chunk.byteLength;
155
+ if (size > maxBytes) {
156
+ done = true;
157
+ request.pause();
158
+ resolve(void 0);
159
+ return;
160
+ }
161
+ chunks.push(chunk);
162
+ });
163
+ request.on("end", () => {
164
+ if (!done) resolve(Buffer.concat(chunks).toString("utf8"));
165
+ });
166
+ request.on("error", reject);
167
+ });
168
+ }
169
+ var AuthioBffDefaults = {
170
+ /** The session cookie name. `__Host-` makes browsers refuse it unless it is `Secure`, has `Path=/` and no `Domain`. */
171
+ CookieName: "__Host-authio",
172
+ /** The route prefix of the BFF endpoints. */
173
+ EndpointPrefix: "/bff",
174
+ /** The anti-forgery header the SPA sends on every API call. */
175
+ AntiForgeryHeaderName: "X-CSRF",
176
+ /** The value the anti-forgery header carries. */
177
+ AntiForgeryHeaderValue: "1",
178
+ /** The redirect path Authio sends the person back to after sign-in. */
179
+ CallbackPath: "/signin-oidc",
180
+ /** The redirect path Authio sends the person back to after sign-out. */
181
+ SignedOutCallbackPath: "/signout-callback-oidc",
182
+ /** The `source` of the message the silent-login page posts to its parent window. */
183
+ SilentLoginMessageSource: "authio-bff-silent-login",
184
+ /** The prefix of the keys sessions are stored under. */
185
+ SessionKeyPrefix: "authio-bff:",
186
+ /** Seconds a session lasts without activity (8 hours). */
187
+ SessionLifetime: 8 * 60 * 60,
188
+ /** Seconds between two checks of the Authio session (5 minutes). */
189
+ SessionValidationInterval: 5 * 60,
190
+ /** Seconds a sign-in may take between the login request and the callback (15 minutes). */
191
+ LoginLifetime: 15 * 60,
192
+ /** The scopes asked for at sign-in; `offline_access` gives the BFF a refresh token. */
193
+ Scopes: Object.freeze(["openid", "profile", "email", "offline_access"])
194
+ };
195
+ function resolveBffSettings(options) {
196
+ const problems = [];
197
+ const cookieSecrets = (typeof options.cookieSecret === "string" ? [options.cookieSecret] : [...options.cookieSecret ?? []]).filter(
198
+ (secret) => typeof secret === "string"
199
+ );
200
+ const settings = {
201
+ cookieSecrets,
202
+ endpointPrefix: `/${(options.endpointPrefix ?? AuthioBffDefaults.EndpointPrefix).replace(/^\/+|\/+$/g, "")}`,
203
+ scopes: withOpenId([...options.scopes ?? AuthioBffDefaults.Scopes, ...options.requestRoles ? ["roles"] : []]),
204
+ getClaimsFromUserInfoEndpoint: options.getClaimsFromUserInfoEndpoint ?? false,
205
+ organization: nonEmpty(options.organization),
206
+ resource: nonEmpty(options.resource),
207
+ allowedResources: [...options.allowedResources ?? []],
208
+ prompt: options.prompt || void 0,
209
+ uiLocales: nonEmpty(options.uiLocales),
210
+ maxAge: options.maxAge,
211
+ additionalAuthorizationParameters: { ...options.additionalAuthorizationParameters },
212
+ requireIssuerParameter: options.requireIssuerParameter ?? true,
213
+ callbackPath: options.callbackPath ?? AuthioBffDefaults.CallbackPath,
214
+ signedOutCallbackPath: options.signedOutCallbackPath ?? AuthioBffDefaults.SignedOutCallbackPath,
215
+ defaultReturnUrl: options.defaultReturnUrl ?? "/",
216
+ cookieName: options.cookieName ?? AuthioBffDefaults.CookieName,
217
+ cookieSameSite: options.cookieSameSite ?? "lax",
218
+ sessionLifetimeMs: (options.sessionLifetime ?? AuthioBffDefaults.SessionLifetime) * 1e3,
219
+ slidingExpiration: options.slidingExpiration ?? true,
220
+ sessionValidation: options.sessionValidation ?? "introspection",
221
+ sessionValidationIntervalMs: (options.sessionValidationInterval ?? AuthioBffDefaults.SessionValidationInterval) * 1e3,
222
+ revokeRefreshTokenOnLogout: options.revokeRefreshTokenOnLogout ?? true,
223
+ antiForgeryHeaderName: options.antiForgeryHeaderName ?? AuthioBffDefaults.AntiForgeryHeaderName,
224
+ antiForgeryHeaderValue: options.antiForgeryHeaderValue ?? AuthioBffDefaults.AntiForgeryHeaderValue,
225
+ sessionKeyPrefix: options.sessionKeyPrefix ?? AuthioBffDefaults.SessionKeyPrefix,
226
+ backChannelLogout: options.backChannelLogout ?? true,
227
+ tokenRefreshSkewMs: (options.tokenRefreshSkew ?? 60) * 1e3,
228
+ redirectUri: nonEmpty(options.redirectUri),
229
+ postLogoutRedirectUri: nonEmpty(options.postLogoutRedirectUri),
230
+ onError: options.onError ?? (() => void 0)
231
+ };
232
+ if (cookieSecrets.length === 0) problems.push("cookieSecret is required: the session cookie is signed with it.");
233
+ else if (cookieSecrets.some((secret) => secret.length < 32)) problems.push("Every cookieSecret must be at least 32 characters long.");
234
+ if (!settings.cookieName.trim()) problems.push("cookieName is required.");
235
+ else if (!settings.cookieName.startsWith("__Host-") && !settings.cookieName.startsWith("__Secure-"))
236
+ problems.push("cookieName must start with __Host- (or __Secure-) so browsers enforce a Secure, host-only cookie.");
237
+ else if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(settings.cookieName)) problems.push("cookieName holds characters a cookie name cannot.");
238
+ if (settings.cookieSameSite !== "lax" && settings.cookieSameSite !== "strict") problems.push("cookieSameSite must be lax or strict.");
239
+ if (!isPositive(settings.sessionLifetimeMs)) problems.push("sessionLifetime must be a positive number of seconds.");
240
+ if (settings.sessionValidation !== "none" && settings.sessionValidation !== "introspection")
241
+ problems.push("sessionValidation must be none or introspection.");
242
+ else if (settings.sessionValidation === "introspection" && !isPositive(settings.sessionValidationIntervalMs))
243
+ problems.push("sessionValidationInterval must be a positive number of seconds.");
244
+ if (!settings.antiForgeryHeaderName.trim() || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(settings.antiForgeryHeaderName))
245
+ problems.push("antiForgeryHeaderName must be a header name.");
246
+ if (!settings.antiForgeryHeaderValue) problems.push("antiForgeryHeaderValue is required.");
247
+ if (!isLocalUrl(settings.defaultReturnUrl)) problems.push("defaultReturnUrl must be a local URL starting with a single '/'.");
248
+ if (!isLocalUrl(settings.callbackPath)) problems.push("callbackPath must start with '/'.");
249
+ if (!isLocalUrl(settings.signedOutCallbackPath)) problems.push("signedOutCallbackPath must start with '/'.");
250
+ if (settings.endpointPrefix === "/") problems.push("endpointPrefix cannot be empty.");
251
+ if (settings.prompt !== void 0 && !AuthioPrompts.includes(settings.prompt)) problems.push("prompt must be none, login, select_account or consent.");
252
+ if (settings.maxAge !== void 0 && (!Number.isInteger(settings.maxAge) || settings.maxAge < 0))
253
+ problems.push("maxAge must be a non-negative whole number of seconds.");
254
+ if (!settings.sessionKeyPrefix.trim()) problems.push("sessionKeyPrefix is required.");
255
+ if (!Number.isFinite(settings.tokenRefreshSkewMs) || settings.tokenRefreshSkewMs < 0) problems.push("tokenRefreshSkew must be a non-negative number of seconds.");
256
+ if (problems.length > 0) throw new AuthioConfigurationError(`The Authio BFF options are not valid:
257
+ - ${problems.join("\n- ")}`);
258
+ return settings;
259
+ }
260
+ function isLocalUrl(url) {
261
+ if (typeof url !== "string" || !url || /[\u0000-\u001f\u007f-\u009f]/.test(url)) return false;
262
+ return url[0] === "/" && (url.length === 1 || url[1] !== "/" && url[1] !== "\\");
263
+ }
264
+ function withOpenId(scopes) {
265
+ const distinct = [...new Set(scopes.map((scope) => scope.trim()).filter(Boolean))];
266
+ return Object.freeze(distinct.includes("openid") ? distinct : ["openid", ...distinct]);
267
+ }
268
+ function nonEmpty(value) {
269
+ return value?.trim() ? value : void 0;
270
+ }
271
+ function isPositive(value) {
272
+ return Number.isFinite(value) && value > 0;
273
+ }
274
+ function createAuthioBffProtector(secrets) {
275
+ const keys = /* @__PURE__ */ new Map();
276
+ const keysFor = (purpose) => {
277
+ let derived = keys.get(purpose);
278
+ if (!derived) {
279
+ derived = secrets.map((secret) => Buffer.from(hkdfSync("sha256", secret, "authio-bff", `authio-bff:${purpose}`, 32)));
280
+ keys.set(purpose, derived);
281
+ }
282
+ return derived;
283
+ };
284
+ const mac = (key, value) => createHmac("sha256", key).update(value, "utf8").digest();
285
+ return {
286
+ sign(value) {
287
+ return `${value}.${mac(keysFor("session-cookie")[0], value).toString("base64url")}`;
288
+ },
289
+ unsign(signed) {
290
+ const dot = signed.lastIndexOf(".");
291
+ if (dot <= 0) return void 0;
292
+ const value = signed.slice(0, dot);
293
+ const signature = Buffer.from(signed.slice(dot + 1), "base64url");
294
+ for (const key of keysFor("session-cookie")) {
295
+ const expected = mac(key, value);
296
+ if (signature.length === expected.length && timingSafeEqual(signature, expected)) return value;
297
+ }
298
+ return void 0;
299
+ },
300
+ seal(purpose, value) {
301
+ const iv = randomBytes(12);
302
+ const cipher = createCipheriv("aes-256-gcm", keysFor(purpose)[0], iv);
303
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final()]);
304
+ return Buffer.concat([iv, ciphertext, cipher.getAuthTag()]).toString("base64url");
305
+ },
306
+ open(purpose, sealed) {
307
+ const bytes = Buffer.from(sealed, "base64url");
308
+ if (bytes.length < 29) return void 0;
309
+ for (const key of keysFor(purpose)) {
310
+ try {
311
+ const decipher = createDecipheriv("aes-256-gcm", key, bytes.subarray(0, 12));
312
+ decipher.setAuthTag(bytes.subarray(bytes.length - 16));
313
+ const plaintext = Buffer.concat([decipher.update(bytes.subarray(12, bytes.length - 16)), decipher.final()]).toString("utf8");
314
+ return JSON.parse(plaintext);
315
+ } catch {
316
+ continue;
317
+ }
318
+ }
319
+ return void 0;
320
+ }
321
+ };
322
+ }
323
+ var tokenModes = ["user", "userOrNone", "clientCredentials", "userOrClientCredentials", "none"];
324
+ var hopByHop = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade"]);
325
+ var decodedEncodings = /* @__PURE__ */ new Set(["gzip", "x-gzip", "deflate", "br"]);
326
+ function resolveRemoteApi(destination, options) {
327
+ let url;
328
+ try {
329
+ url = new URL(destination);
330
+ } catch {
331
+ throw new AuthioConfigurationError(`The remote API destination '${destination}' must be an absolute HTTP(S) URL.`);
332
+ }
333
+ if (url.protocol !== "https:" && url.protocol !== "http:")
334
+ throw new AuthioConfigurationError(`The remote API destination '${destination}' must be an absolute HTTP(S) URL.`);
335
+ const tokenMode = options.tokenMode ?? "user";
336
+ if (!tokenModes.includes(tokenMode)) throw new AuthioConfigurationError(`The remote API tokenMode must be one of ${tokenModes.join(", ")}.`);
337
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
338
+ if (typeof fetchImplementation !== "function") throw new AuthioConfigurationError("No fetch implementation is available: pass fetch to remoteApi.");
339
+ return {
340
+ destination: destination.replace(/\/+$/, ""),
341
+ tokenMode,
342
+ resource: options.resource?.trim() || void 0,
343
+ scopes: [...options.scopes ?? []],
344
+ requireAntiForgery: options.requireAntiForgery ?? true,
345
+ validateSessionOnUnauthorized: options.validateSessionOnUnauthorized ?? true,
346
+ fetch: fetchImplementation
347
+ };
348
+ }
349
+ async function forwardToRemoteApi(request, response, remote, context) {
350
+ if (remote.requireAntiForgery && !context.antiForgery())
351
+ return sendError(response, 401, "antiforgery_required", `The ${context.antiForgeryHeaderName} header is required.`);
352
+ const token = await tokenFor(remote, context);
353
+ if (token.refused) return sendError(response, 401, "unauthorized", "Sign in first.");
354
+ const headers = outgoingHeaders(request, context.antiForgeryHeaderName);
355
+ if (token.accessToken) headers.set("authorization", `Bearer ${token.accessToken}`);
356
+ let upstream;
357
+ try {
358
+ upstream = await remote.fetch(remote.destination + remainingUrl(request), {
359
+ method: request.method,
360
+ headers,
361
+ body: outgoingBody(request),
362
+ redirect: "manual",
363
+ duplex: "half"
364
+ });
365
+ } catch {
366
+ return sendError(response, 502, "bad_gateway", "The remote API could not be reached.");
367
+ }
368
+ if (upstream.status === 401 && token.isUserToken && remote.validateSessionOnUnauthorized) await context.endIfInactive();
369
+ if (response.headersSent) return;
370
+ const encoding = upstream.headers.get("content-encoding")?.trim().toLowerCase();
371
+ const decoded = encoding !== void 0 && decodedEncodings.has(encoding);
372
+ response.status(upstream.status);
373
+ upstream.headers.forEach((value, name) => {
374
+ if (hopByHop.has(name) || name === "set-cookie" || decoded && (name === "content-encoding" || name === "content-length")) return;
375
+ response.setHeader(name, value);
376
+ });
377
+ if (!upstream.body || request.method === "HEAD") {
378
+ response.end();
379
+ return;
380
+ }
381
+ await pipeline(Readable.fromWeb(upstream.body), response);
382
+ }
383
+ async function tokenFor(remote, context) {
384
+ switch (remote.tokenMode) {
385
+ case "none":
386
+ return { refused: false };
387
+ case "clientCredentials":
388
+ return { refused: false, accessToken: await context.clientToken() };
389
+ default: {
390
+ const userToken = await context.hasSession() ? await context.userToken() : void 0;
391
+ if (userToken) return { refused: false, accessToken: userToken, isUserToken: true };
392
+ if (remote.tokenMode === "userOrNone") return { refused: false };
393
+ if (remote.tokenMode === "userOrClientCredentials") return { refused: false, accessToken: await context.clientToken() };
394
+ return { refused: true };
395
+ }
396
+ }
397
+ }
398
+ function outgoingHeaders(request, antiForgeryHeaderName) {
399
+ const headers = new Headers();
400
+ const skipped = /* @__PURE__ */ new Set([
401
+ ...hopByHop,
402
+ "host",
403
+ "cookie",
404
+ "authorization",
405
+ "content-length",
406
+ antiForgeryHeaderName.toLowerCase(),
407
+ ...(request.headers.connection ?? "").split(",").map((name) => name.trim().toLowerCase())
408
+ ]);
409
+ for (const [name, value] of Object.entries(request.headers)) {
410
+ if (value === void 0 || skipped.has(name)) continue;
411
+ for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item);
412
+ }
413
+ headers.set("accept-encoding", "identity");
414
+ return headers;
415
+ }
416
+ function outgoingBody(request) {
417
+ const declaresBody = request.headers["transfer-encoding"] !== void 0 || Number(request.headers["content-length"] ?? 0) > 0;
418
+ if (request.method === "GET" || request.method === "HEAD" || !declaresBody) return void 0;
419
+ const parsed = request.body;
420
+ if (!request.readableEnded) return Readable.toWeb(request);
421
+ if (parsed === void 0 || parsed === null) return void 0;
422
+ if (Buffer.isBuffer(parsed)) return new Uint8Array(parsed);
423
+ if (typeof parsed === "string") return parsed;
424
+ if ((request.headers["content-type"] ?? "").toLowerCase().startsWith("application/x-www-form-urlencoded"))
425
+ return new URLSearchParams(parsed).toString();
426
+ return JSON.stringify(parsed);
427
+ }
428
+ function remainingUrl(request) {
429
+ const url = request.url || "/";
430
+ const originalPath = new URL(request.originalUrl || url, "http://localhost").pathname;
431
+ if ((url === "/" || url.startsWith("/?")) && !originalPath.endsWith("/")) return url.slice(1);
432
+ return url;
433
+ }
434
+
435
+ // src/session-locks.ts
436
+ var AuthioBffSessionLocks = class {
437
+ tails = /* @__PURE__ */ new Map();
438
+ /** The number of keys with a holder or a waiter. */
439
+ get size() {
440
+ return this.tails.size;
441
+ }
442
+ /**
443
+ * Runs `work` once every earlier holder of `key` released it, and releases it afterwards, even when `work` throws.
444
+ */
445
+ async run(key, work) {
446
+ const previous = this.tails.get(key);
447
+ let release;
448
+ const turn = new Promise((resolve) => {
449
+ release = resolve;
450
+ });
451
+ const entry = { promise: (previous?.promise ?? Promise.resolve()).then(() => turn), references: (previous?.references ?? 0) + 1 };
452
+ this.tails.set(key, entry);
453
+ try {
454
+ await previous?.promise;
455
+ return await work();
456
+ } finally {
457
+ release();
458
+ const current = this.tails.get(key);
459
+ if (current) {
460
+ current.references -= 1;
461
+ if (current.references === 0) this.tails.delete(key);
462
+ }
463
+ }
464
+ }
465
+ };
466
+ function createAuthioBffMemoryStore(options = {}) {
467
+ const clock = options.clock ?? systemClock;
468
+ const sessions = /* @__PURE__ */ new Map();
469
+ const bySessionId = /* @__PURE__ */ new Map();
470
+ const bySubject = /* @__PURE__ */ new Map();
471
+ let nextSweep = 0;
472
+ const index = (lookup, value, key) => {
473
+ if (!value) return;
474
+ const keys = lookup.get(value) ?? /* @__PURE__ */ new Set();
475
+ keys.add(key);
476
+ lookup.set(value, keys);
477
+ };
478
+ const unindex = (lookup, value, key) => {
479
+ if (!value) return;
480
+ const keys = lookup.get(value);
481
+ keys?.delete(key);
482
+ if (keys?.size === 0) lookup.delete(value);
483
+ };
484
+ const remove = (key) => {
485
+ const session = sessions.get(key);
486
+ if (!session) return;
487
+ sessions.delete(key);
488
+ unindex(bySessionId, session.sessionId, key);
489
+ unindex(bySubject, session.subject, key);
490
+ };
491
+ const sweep = () => {
492
+ const now = clock.now();
493
+ if (now < nextSweep) return;
494
+ nextSweep = now + 6e4;
495
+ for (const [key, session] of sessions) if (session.expiresAt <= now) remove(key);
496
+ };
497
+ return {
498
+ get size() {
499
+ return sessions.size;
500
+ },
501
+ get(key) {
502
+ const session = sessions.get(key);
503
+ if (!session) return void 0;
504
+ if (session.expiresAt > clock.now()) return session;
505
+ remove(key);
506
+ return void 0;
507
+ },
508
+ set(session) {
509
+ sweep();
510
+ remove(session.key);
511
+ sessions.set(session.key, session);
512
+ index(bySessionId, session.sessionId, session.key);
513
+ index(bySubject, session.subject, session.key);
514
+ },
515
+ delete: remove,
516
+ deleteBySessionId(sessionId) {
517
+ for (const key of [...bySessionId.get(sessionId) ?? []]) remove(key);
518
+ },
519
+ deleteBySubject(subject) {
520
+ for (const key of [...bySubject.get(subject) ?? []]) remove(key);
521
+ }
522
+ };
523
+ }
524
+
525
+ // src/bff.ts
526
+ var sessionSlot = /* @__PURE__ */ Symbol.for("authio.bff.session");
527
+ function authioBff(options) {
528
+ const settings = resolveBffSettings(options);
529
+ const client = options.client ?? createAuthioClient(options);
530
+ const clock = client.connection.clock;
531
+ const store = options.store ?? createAuthioBffMemoryStore({ clock });
532
+ const protector = createAuthioBffProtector(settings.cookieSecrets);
533
+ const locks = new AuthioBffSessionLocks();
534
+ const exchanges = /* @__PURE__ */ new Map();
535
+ const decrypt = createAuthioTokenDecrypter(
536
+ options.clientEncryptionKey ?? (options.clientEncryptionKeyPath ? encryptionKeyFromFile(options.clientEncryptionKeyPath) : void 0)
537
+ );
538
+ const prefix = settings.endpointPrefix;
539
+ const loginLifetimeMs = AuthioBffDefaults.LoginLifetime * 1e3;
540
+ const loginCookieName = (state) => `${settings.cookieName}.login.${createHash("sha256").update(state).digest("base64url").slice(0, 16)}`;
541
+ const toInfo = (session) => Object.freeze({
542
+ subject: textOf(session.data.claims.sub),
543
+ sessionId: textOf(session.data.claims.sid),
544
+ claims: Object.freeze({ ...session.data.claims }),
545
+ expiresAt: session.expiresAt
546
+ });
547
+ const expose = (request, session) => {
548
+ request[sessionSlot] = Promise.resolve(session);
549
+ request.authioBff = session ? toInfo(session) : void 0;
550
+ };
551
+ const read = async (key) => {
552
+ const stored = await store.get(key);
553
+ if (!stored || stored.expiresAt <= clock.now()) return void 0;
554
+ const data = protector.open("session-data", stored.data);
555
+ return data ? { key, expiresAt: stored.expiresAt, data } : void 0;
556
+ };
557
+ const write = async (key, data, expiresAt) => {
558
+ await store.set({ key, sessionId: textOf(data.claims.sid), subject: textOf(data.claims.sub), expiresAt, data: protector.seal("session-data", data) });
559
+ return { key, expiresAt, data };
560
+ };
561
+ const update = (key, change) => locks.run(key, async () => {
562
+ const current = await read(key);
563
+ if (!current) return void 0;
564
+ const next = change(current);
565
+ return write(key, next.data, next.expiresAt);
566
+ });
567
+ const dropExchanges = (key) => {
568
+ for (const [cacheKey, entry] of exchanges) if (entry.sessionKey === key) exchanges.delete(cacheKey);
569
+ };
570
+ const clearCookie = (response) => setCookie(response, settings.cookieName, "", settings.cookieSameSite, 0);
571
+ const endSessionByKey = async (request, response, key) => {
572
+ await store.delete(key);
573
+ dropExchanges(key);
574
+ clearCookie(response);
575
+ expose(request, void 0);
576
+ };
577
+ const report = (error) => {
578
+ try {
579
+ settings.onError(error);
580
+ } catch {
581
+ return;
582
+ }
583
+ };
584
+ const isActive = async (data) => {
585
+ const token = data.refreshToken ?? data.accessToken;
586
+ if (!token) return void 0;
587
+ try {
588
+ const result = await client.introspect(token, { tokenTypeHint: data.refreshToken ? AuthioTokenTypes.RefreshToken : AuthioTokenTypes.AccessToken });
589
+ return result.active === true;
590
+ } catch (error) {
591
+ report(error);
592
+ return void 0;
593
+ }
594
+ };
595
+ const maintain = async (request, response, session) => {
596
+ const now = clock.now();
597
+ const validate = settings.sessionValidation === "introspection" && now - session.data.validatedAt >= settings.sessionValidationIntervalMs;
598
+ const slide = settings.slidingExpiration && session.expiresAt - now < settings.sessionLifetimeMs / 2;
599
+ let validated = false;
600
+ if (validate) {
601
+ const active = await isActive(session.data);
602
+ if (active === false) {
603
+ await endSessionByKey(request, response, session.key);
604
+ return void 0;
605
+ }
606
+ validated = active === true;
607
+ }
608
+ if (!validated && !slide) return session;
609
+ const renewed = await update(session.key, (current) => ({
610
+ data: validated ? { ...current.data, validatedAt: now } : current.data,
611
+ expiresAt: slide ? now + settings.sessionLifetimeMs : current.expiresAt
612
+ }));
613
+ return renewed;
614
+ };
615
+ const loadSession = (request, response) => {
616
+ const holder = request;
617
+ const cached = holder[sessionSlot];
618
+ if (cached) return cached;
619
+ const loading = (async () => {
620
+ const cookie = readCookies(request).get(settings.cookieName);
621
+ if (!cookie) return void 0;
622
+ const id = protector.unsign(cookie);
623
+ const found = id ? await read(settings.sessionKeyPrefix + id) : void 0;
624
+ const session = found ? await maintain(request, response, found) : void 0;
625
+ if (!session) {
626
+ clearCookie(response);
627
+ request.authioBff = void 0;
628
+ return void 0;
629
+ }
630
+ request.authioBff = toInfo(session);
631
+ return session;
632
+ })();
633
+ holder[sessionSlot] = loading;
634
+ return loading;
635
+ };
636
+ const isFresh = (data, skewMs) => Boolean(data.accessToken) && (data.accessTokenExpiresAt === void 0 || clock.now() < data.accessTokenExpiresAt - skewMs);
637
+ const refresh = async (request, response, session) => {
638
+ const data = { ...session.data, claims: { ...session.data.claims } };
639
+ const unexpired = isFresh(data, 0) ? data.accessToken : void 0;
640
+ if (!data.refreshToken) return unexpired;
641
+ try {
642
+ const issuedAt = clock.now();
643
+ const answer = await client.tokens.refresh({ refreshToken: data.refreshToken });
644
+ data.accessToken = answer.access_token;
645
+ data.accessTokenExpiresAt = issuedAt + lifetimeOf(answer.expires_in);
646
+ data.tokenType = answer.token_type ?? data.tokenType;
647
+ if (answer.refresh_token) data.refreshToken = answer.refresh_token;
648
+ if (answer.id_token) data.idToken = answer.id_token;
649
+ await replaceAccessTokenClaims(data.claims, answer.access_token, decrypt);
650
+ if (!await store.get(session.key)) {
651
+ await endSessionByKey(request, response, session.key);
652
+ return void 0;
653
+ }
654
+ expose(request, await write(session.key, data, session.expiresAt));
655
+ return answer.access_token;
656
+ } catch (error) {
657
+ report(error);
658
+ if (error instanceof AuthioProtocolError && error.error === AuthioOAuthErrors.InvalidGrant) {
659
+ await endSessionByKey(request, response, session.key);
660
+ return void 0;
661
+ }
662
+ return unexpired;
663
+ }
664
+ };
665
+ const sessionAccessToken = async (request, response) => {
666
+ const session = await loadSession(request, response);
667
+ if (!session) return void 0;
668
+ if (isFresh(session.data, settings.tokenRefreshSkewMs)) return session.data.accessToken;
669
+ return locks.run(session.key, async () => {
670
+ const current = await read(session.key);
671
+ if (!current) {
672
+ await endSessionByKey(request, response, session.key);
673
+ return void 0;
674
+ }
675
+ if (isFresh(current.data, settings.tokenRefreshSkewMs)) {
676
+ expose(request, current);
677
+ return current.data.accessToken;
678
+ }
679
+ return refresh(request, response, current);
680
+ });
681
+ };
682
+ const getUserAccessToken = async (request, response, tokenOptions = {}) => {
683
+ const scopes = [...new Set((tokenOptions.scopes ?? []).map((scope) => scope.trim()).filter(Boolean))].sort();
684
+ const resource = tokenOptions.resource?.trim() || void 0;
685
+ const subjectToken = await sessionAccessToken(request, response);
686
+ if (!subjectToken || !resource && scopes.length === 0) return subjectToken;
687
+ const session = await loadSession(request, response);
688
+ if (!session) return void 0;
689
+ const cacheKey = `${session.key}|${resource ?? ""}|${scopes.join(" ")}`;
690
+ const cached = exchanges.get(cacheKey);
691
+ if (cached && clock.now() < cached.refreshAt) return cached.accessToken;
692
+ return locks.run(cacheKey, async () => {
693
+ const again = exchanges.get(cacheKey);
694
+ if (again && clock.now() < again.refreshAt) return again.accessToken;
695
+ const issuedAt = clock.now();
696
+ const answer = await client.tokens.exchange({ subjectToken, scopes: scopes.length > 0 ? scopes : void 0, resource });
697
+ const lifetime = lifetimeOf(answer.expires_in);
698
+ for (const [key, entry] of exchanges) if (entry.expiresAt <= issuedAt) exchanges.delete(key);
699
+ exchanges.set(cacheKey, {
700
+ sessionKey: session.key,
701
+ accessToken: answer.access_token,
702
+ refreshAt: issuedAt + lifetime - Math.min(settings.tokenRefreshSkewMs, lifetime / 2),
703
+ expiresAt: issuedAt + lifetime
704
+ });
705
+ return answer.access_token;
706
+ });
707
+ };
708
+ const endIfInactive = async (request, response) => {
709
+ const session = await loadSession(request, response);
710
+ if (session && await isActive(session.data) === false) await endSessionByKey(request, response, session.key);
711
+ };
712
+ const antiForgery = (request) => hasAntiForgeryHeader(request, settings.antiForgeryHeaderName, settings.antiForgeryHeaderValue);
713
+ const antiForgeryRequired = (response) => sendError(response, 401, "antiforgery_required", `The ${settings.antiForgeryHeaderName} header is required.`);
714
+ const startLogin = async (request, response, parameters) => {
715
+ const { url, transaction } = await client.createAuthorizationUrl({
716
+ redirectUri: settings.redirectUri ?? requestBase(request) + settings.callbackPath,
717
+ scopes: settings.scopes,
718
+ resource: parameters.resource ?? settings.resource,
719
+ organization: parameters.organization ?? settings.organization,
720
+ prompt: parameters.prompt ?? settings.prompt,
721
+ loginHint: parameters.loginHint,
722
+ uiLocales: parameters.uiLocales ?? settings.uiLocales,
723
+ maxAge: parameters.maxAge ?? settings.maxAge,
724
+ extraParameters: settings.additionalAuthorizationParameters
725
+ });
726
+ const state = { transaction, returnUrl: parameters.returnUrl, expiresAt: clock.now() + loginLifetimeMs };
727
+ setCookie(response, loginCookieName(transaction.state), protector.seal("login", state), "lax", AuthioBffDefaults.LoginLifetime);
728
+ response.set("Cache-Control", "no-store").redirect(302, url);
729
+ };
730
+ const login = async (request, response) => {
731
+ const query = readQuery(request);
732
+ const returnUrl = query.get("returnUrl") ?? "";
733
+ const prompt = query.get("prompt") ?? "";
734
+ const maxAge = query.get("max_age") ?? "";
735
+ const resource = query.get("resource") ?? "";
736
+ if (returnUrl && !isLocalUrl(returnUrl)) return sendError(response, 400, "invalid_request", "returnUrl must be a local URL.");
737
+ if (prompt && !AuthioPrompts.includes(prompt))
738
+ return sendError(response, 400, "invalid_request", "prompt must be none, login, select_account or consent.");
739
+ if (maxAge && !/^\d+$/.test(maxAge)) return sendError(response, 400, "invalid_request", "max_age must be a number of seconds.");
740
+ if (resource && resource !== settings.resource && !settings.allowedResources.includes(resource))
741
+ return sendError(response, 400, "invalid_target", "The resource is not allowed by allowedResources.");
742
+ await startLogin(request, response, {
743
+ returnUrl: returnUrl || settings.defaultReturnUrl,
744
+ prompt: prompt || void 0,
745
+ loginHint: query.get("login_hint") || void 0,
746
+ uiLocales: query.get("ui_locales") || void 0,
747
+ maxAge: maxAge ? Number(maxAge) : void 0,
748
+ organization: query.get("organization") || void 0,
749
+ resource: resource || void 0
750
+ });
751
+ };
752
+ const silentLogin = (request, response) => startLogin(request, response, { returnUrl: `${request.baseUrl ?? ""}${prefix}/silent-login-callback`, prompt: "none" });
753
+ const callback = async (request, response) => {
754
+ const query = readQuery(request);
755
+ const state = query.get("state") ?? "";
756
+ const cookieName = state ? loginCookieName(state) : void 0;
757
+ const sealed = cookieName ? readCookies(request).get(cookieName) : void 0;
758
+ const pending = sealed ? protector.open("login", sealed) : void 0;
759
+ if (cookieName && sealed) setCookie(response, cookieName, "", "lax", 0);
760
+ response.set("Cache-Control", "no-store");
761
+ const fail = (target2, error) => response.redirect(302, withQueryParameter(target2, "error", error));
762
+ if (!pending || pending.transaction?.state !== state || pending.expiresAt <= clock.now()) return fail(settings.defaultReturnUrl, "login_failed");
763
+ const target = isLocalUrl(pending.returnUrl) ? pending.returnUrl : settings.defaultReturnUrl;
764
+ if (settings.requireIssuerParameter && !query.has("iss") && !query.has("error")) return fail(target, "login_failed");
765
+ let data;
766
+ try {
767
+ const { tokens, idToken } = await client.handleAuthorizationCallback(query, pending.transaction);
768
+ const now = clock.now();
769
+ const claims = toSessionClaims(idToken, await readAccessTokenSubject(tokens.access_token, decrypt));
770
+ await replaceAccessTokenClaims(claims, tokens.access_token, decrypt);
771
+ if (settings.getClaimsFromUserInfoEndpoint) mergeUserInfo(claims, await client.userInfo(tokens.access_token));
772
+ data = {
773
+ publicId: randomBytes(16).toString("hex"),
774
+ claims,
775
+ accessToken: tokens.access_token,
776
+ accessTokenExpiresAt: now + lifetimeOf(tokens.expires_in),
777
+ refreshToken: tokens.refresh_token,
778
+ idToken: tokens.id_token,
779
+ tokenType: tokens.token_type,
780
+ resource: pending.transaction.resource,
781
+ createdAt: now,
782
+ validatedAt: now
783
+ };
784
+ } catch (error) {
785
+ report(error);
786
+ return fail(target, error instanceof AuthioProtocolError ? error.error : "login_failed");
787
+ }
788
+ const previous = readCookies(request).get(settings.cookieName);
789
+ const previousId = previous ? protector.unsign(previous) : void 0;
790
+ if (previousId) {
791
+ await store.delete(settings.sessionKeyPrefix + previousId);
792
+ dropExchanges(settings.sessionKeyPrefix + previousId);
793
+ }
794
+ const id = randomBytes(32).toString("hex");
795
+ expose(request, await write(settings.sessionKeyPrefix + id, data, clock.now() + settings.sessionLifetimeMs));
796
+ setCookie(response, settings.cookieName, protector.sign(id), settings.cookieSameSite);
797
+ response.redirect(302, target);
798
+ };
799
+ const user = async (request, response) => {
800
+ if (!antiForgery(request)) return antiForgeryRequired(response);
801
+ const session = await loadSession(request, response);
802
+ if (!session) return sendError(response, 401, "unauthorized", "Nobody is signed in.");
803
+ const expiresIn = Math.max(0, Math.floor((session.expiresAt - clock.now()) / 1e3));
804
+ const logoutUrl = `${request.baseUrl ?? ""}${prefix}/logout?sid=${encodeURIComponent(session.data.publicId)}`;
805
+ response.set("Cache-Control", "no-store").json(toAuthioBffClaims(session.data.claims, expiresIn, logoutUrl));
806
+ };
807
+ const logout = async (request, response) => {
808
+ const query = readQuery(request);
809
+ const returnUrl = query.get("returnUrl") ?? "";
810
+ if (returnUrl && !isLocalUrl(returnUrl)) return sendError(response, 400, "invalid_request", "returnUrl must be a local URL.");
811
+ const target = returnUrl || settings.defaultReturnUrl;
812
+ const session = await loadSession(request, response);
813
+ response.set("Cache-Control", "no-store");
814
+ if (!session) {
815
+ clearCookie(response);
816
+ return response.redirect(302, target);
817
+ }
818
+ if (!antiForgery(request) && !fixedTimeEquals(query.get("sid") ?? "", session.data.publicId))
819
+ return sendError(response, 400, "invalid_request", `Logging out requires the ${settings.antiForgeryHeaderName} header or the session's sid parameter.`);
820
+ if (settings.revokeRefreshTokenOnLogout && session.data.refreshToken) {
821
+ try {
822
+ await client.revoke(session.data.refreshToken, { tokenTypeHint: AuthioTokenTypes.RefreshToken });
823
+ } catch (error) {
824
+ report(error);
825
+ }
826
+ }
827
+ await endSessionByKey(request, response, session.key);
828
+ const document = await client.discovery.get();
829
+ if (!document.end_session_endpoint) return response.redirect(302, target);
830
+ const endSessionUrl = await client.createEndSessionUrl({
831
+ idTokenHint: session.data.idToken,
832
+ postLogoutRedirectUri: settings.postLogoutRedirectUri ?? requestBase(request) + settings.signedOutCallbackPath,
833
+ state: protector.seal("logout-state", { returnUrl: target, expiresAt: clock.now() + loginLifetimeMs }),
834
+ uiLocales: settings.uiLocales
835
+ });
836
+ response.redirect(302, endSessionUrl);
837
+ };
838
+ const signedOutCallback = (request, response) => {
839
+ const state = readQuery(request).get("state");
840
+ const value = state ? protector.open("logout-state", state) : void 0;
841
+ const target = value && value.expiresAt > clock.now() && isLocalUrl(value.returnUrl) ? value.returnUrl : settings.defaultReturnUrl;
842
+ response.set("Cache-Control", "no-store").redirect(302, target);
843
+ };
844
+ const silentLoginCallback = async (request, response) => {
845
+ const session = await loadSession(request, response);
846
+ const error = readQuery(request).get("error") || null;
847
+ const message = JSON.stringify({ source: AuthioBffDefaults.SilentLoginMessageSource, isLoggedIn: Boolean(session) && !error, error }).replace(
848
+ /[^ -~]|[<>&]/g,
849
+ (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
850
+ );
851
+ response.set("Cache-Control", "no-store").type("text/html; charset=utf-8").send(
852
+ `<!doctype html><html><head><meta charset="utf-8"><title>Authio</title></head><body><script>(function(){var m=${message};(window.parent!==window?window.parent:window.opener||window).postMessage(m,window.location.origin);})();</script></body></html>`
853
+ );
854
+ };
855
+ const backChannelLogout = async (request, response) => {
856
+ response.set("Cache-Control", "no-store");
857
+ if (!settings.backChannelLogout) return void response.status(404).end();
858
+ const token = await readFormField(request, "logout_token");
859
+ if (!token?.trim()) return sendError(response, 400, "invalid_request", "The logout_token form field is required.");
860
+ let logout2;
861
+ try {
862
+ logout2 = await client.validateLogoutToken(token);
863
+ } catch (error) {
864
+ report(error);
865
+ if (!(error instanceof AuthioError)) throw error;
866
+ return sendError(response, 400, "invalid_request", "The logout token is not valid.");
867
+ }
868
+ if (logout2.sessionId) await store.deleteBySessionId(logout2.sessionId);
869
+ else if (logout2.subject) await store.deleteBySubject(logout2.subject);
870
+ response.status(200).end();
871
+ };
872
+ const routes = /* @__PURE__ */ new Map([
873
+ [settings.callbackPath, { methods: ["GET"], run: callback }],
874
+ [settings.signedOutCallbackPath, { methods: ["GET"], run: signedOutCallback }],
875
+ [`${prefix}/login`, { methods: ["GET"], run: login }],
876
+ [`${prefix}/logout`, { methods: ["GET", "POST"], run: logout }],
877
+ [`${prefix}/user`, { methods: ["GET"], run: user }],
878
+ [`${prefix}/silent-login`, { methods: ["GET"], run: silentLogin }],
879
+ [`${prefix}/silent-login-callback`, { methods: ["GET"], run: silentLoginCallback }],
880
+ [`${prefix}/backchannel-logout`, { methods: ["POST"], run: backChannelLogout }]
881
+ ]);
882
+ const middleware = (request, response, next) => {
883
+ const route = routes.get(request.path.replace(/(.)\/+$/, "$1"));
884
+ if (route && route.methods.includes(request.method)) {
885
+ Promise.resolve().then(() => route.run(request, response)).catch(next);
886
+ return;
887
+ }
888
+ loadSession(request, response).then(() => next(), next);
889
+ };
890
+ const bff = middleware;
891
+ return Object.assign(bff, {
892
+ client,
893
+ store,
894
+ api(apiOptions = {}) {
895
+ return (request, response, next) => {
896
+ if (apiOptions.requireAntiForgery !== false && !antiForgery(request)) return antiForgeryRequired(response);
897
+ loadSession(request, response).then((session) => {
898
+ if (!session && !apiOptions.optional) return sendError(response, 401, "unauthorized", "Sign in first.");
899
+ next();
900
+ }, next);
901
+ };
902
+ },
903
+ remoteApi(destination, remoteOptions = {}) {
904
+ const remote = resolveRemoteApi(destination, remoteOptions);
905
+ return (request, response, next) => {
906
+ forwardToRemoteApi(request, response, remote, {
907
+ antiForgery: () => antiForgery(request),
908
+ antiForgeryHeaderName: settings.antiForgeryHeaderName,
909
+ hasSession: async () => Boolean(await loadSession(request, response)),
910
+ userToken: () => getUserAccessToken(request, response, { resource: remote.resource, scopes: remote.scopes }),
911
+ clientToken: async () => (await client.clientCredentials.getToken({ scopes: remote.scopes, resource: remote.resource })).accessToken,
912
+ endIfInactive: () => endIfInactive(request, response)
913
+ }).catch(next);
914
+ };
915
+ },
916
+ async getSession(request, response) {
917
+ const session = await loadSession(request, response);
918
+ return session ? toInfo(session) : void 0;
919
+ },
920
+ getUserAccessToken,
921
+ userFetch(request, response, fetchOptions = {}) {
922
+ const inner = fetchOptions.fetch ?? client.connection.fetch;
923
+ return async (input, init) => {
924
+ const token = await getUserAccessToken(request, response, fetchOptions);
925
+ const headers = new Headers(init?.headers ?? (input instanceof globalThis.Request ? input.headers : void 0));
926
+ if (token) headers.set("authorization", `Bearer ${token}`);
927
+ return inner(input, { ...init, headers });
928
+ };
929
+ },
930
+ async endSession(request, response) {
931
+ const session = await loadSession(request, response);
932
+ if (session) await endSessionByKey(request, response, session.key);
933
+ }
934
+ });
935
+ }
936
+ function lifetimeOf(expiresIn) {
937
+ return (expiresIn && expiresIn > 0 ? expiresIn : AuthioDefaults.AssumedTokenLifetime) * 1e3;
938
+ }
939
+ function textOf(value) {
940
+ return typeof value === "string" && value ? value : void 0;
941
+ }
942
+ function fixedTimeEquals(left, right) {
943
+ const a = Buffer.from(left, "utf8");
944
+ const b = Buffer.from(right, "utf8");
945
+ return a.length > 0 && a.length === b.length && timingSafeEqual(a, b);
946
+ }
947
+
948
+ export { AuthioBffClaimTypes, AuthioBffDefaults, authioBff, createAuthioBffMemoryStore, isLocalUrl };
949
+ //# sourceMappingURL=index.js.map
950
+ //# sourceMappingURL=index.js.map