@fluixi/oauth2 1.0.0-alpha.11 → 1.0.0-alpha.13

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.
Files changed (45) hide show
  1. package/dist/backchannel.cjs +462 -0
  2. package/dist/backchannel.d.ts +39 -0
  3. package/dist/backchannel.d.ts.map +1 -0
  4. package/dist/backchannel.js +97 -0
  5. package/dist/backchannel.mjs +307 -0
  6. package/dist/{browser-N432SMC3.mjs → browser-SQXLVG25.mjs} +2 -2
  7. package/dist/browser.cjs +6 -1
  8. package/dist/browser.mjs +6 -1
  9. package/dist/callback.mjs +1 -1
  10. package/dist/{chunk-ZVDC3JW7.mjs → chunk-CVUD2UAP.mjs} +1 -1
  11. package/dist/chunk-H5KMWPVT.mjs +108 -0
  12. package/dist/{chunk-4TPZVJSR.mjs → chunk-QZFYCRMT.mjs} +1 -1
  13. package/dist/discovery.cjs +6 -1
  14. package/dist/discovery.d.ts.map +1 -1
  15. package/dist/discovery.js +6 -1
  16. package/dist/discovery.mjs +6 -1
  17. package/dist/{dpop-DYUR2CH2.mjs → dpop-7YXQSM5R.mjs} +4 -4
  18. package/dist/dpop-SP7YCDPE.mjs +22 -0
  19. package/dist/index.cjs +107 -7
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.mjs +111 -11
  24. package/dist/server.cjs +649 -137
  25. package/dist/server.d.ts +64 -4
  26. package/dist/server.d.ts.map +1 -1
  27. package/dist/server.js +376 -61
  28. package/dist/server.mjs +532 -139
  29. package/dist/service-client.cjs +7 -2
  30. package/dist/service-client.mjs +9 -4
  31. package/dist/service.cjs +104 -5
  32. package/dist/service.d.ts.map +1 -1
  33. package/dist/service.js +38 -6
  34. package/dist/service.mjs +105 -6
  35. package/dist/session-monitor.cjs +98 -0
  36. package/dist/session-monitor.d.ts +32 -0
  37. package/dist/session-monitor.d.ts.map +1 -0
  38. package/dist/session-monitor.js +104 -0
  39. package/dist/session-monitor.mjs +77 -0
  40. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  41. package/dist/types.d.ts +8 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/verify.cjs +7 -2
  44. package/dist/verify.mjs +12 -7
  45. package/package.json +15 -15
package/dist/server.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @fluixi/oauth2 v1.0.0-alpha.11 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/oauth2 v1.0.0-alpha.13 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  "use strict";
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
@@ -6,6 +6,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
9
12
  var __export = (target, all) => {
10
13
  for (var name in all)
11
14
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -28,11 +31,132 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
31
  ));
29
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
33
 
34
+ // src/types.ts
35
+ var OAuthError;
36
+ var init_types = __esm({
37
+ "src/types.ts"() {
38
+ "use strict";
39
+ OAuthError = class extends Error {
40
+ constructor(message, code, description) {
41
+ super(message);
42
+ this.code = code;
43
+ this.description = description;
44
+ this.name = "OAuthError";
45
+ }
46
+ };
47
+ }
48
+ });
49
+
50
+ // src/dpop.ts
51
+ var dpop_exports = {};
52
+ __export(dpop_exports, {
53
+ boundKeyOf: () => boundKeyOf,
54
+ createDpopKey: () => createDpopKey,
55
+ exportDpopKey: () => exportDpopKey,
56
+ importDpopKey: () => importDpopKey,
57
+ nonceFrom: () => nonceFrom,
58
+ proofFor: () => proofFor,
59
+ thumbprintOf: () => thumbprintOf,
60
+ wantsNonce: () => wantsNonce
61
+ });
62
+ async function loadJose2() {
63
+ try {
64
+ return await import("jose");
65
+ } catch {
66
+ throw new OAuthError(
67
+ "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
68
+ "jose_missing"
69
+ );
70
+ }
71
+ }
72
+ async function createDpopKey(alg = "ES256", options = {}) {
73
+ const jose = await loadJose2();
74
+ const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
75
+ extractable: options.extractable ?? false
76
+ });
77
+ const publicJwk = { ...await jose.exportJWK(publicKey) };
78
+ return {
79
+ privateKey,
80
+ publicJwk,
81
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
82
+ alg
83
+ };
84
+ }
85
+ async function exportDpopKey(key) {
86
+ const jose = await loadJose2();
87
+ return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
88
+ }
89
+ async function importDpopKey(stored) {
90
+ const jose = await loadJose2();
91
+ const privateKey = await jose.importJWK(stored.jwk, stored.alg);
92
+ const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
93
+ return {
94
+ privateKey,
95
+ publicJwk,
96
+ thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
97
+ alg: stored.alg
98
+ };
99
+ }
100
+ async function thumbprintOf(jwk) {
101
+ const jose = await loadJose2();
102
+ return jose.calculateJwkThumbprint(jwk, "sha256");
103
+ }
104
+ async function proofFor(key, options) {
105
+ const jose = await loadJose2();
106
+ const htu = (() => {
107
+ try {
108
+ const url = new URL(options.url);
109
+ url.search = "";
110
+ url.hash = "";
111
+ return url.toString();
112
+ } catch {
113
+ return options.url;
114
+ }
115
+ })();
116
+ const claims = {
117
+ htm: options.method.toUpperCase(),
118
+ htu,
119
+ jti: crypto.randomUUID()
120
+ };
121
+ if (options.accessToken) {
122
+ const digest = await crypto.subtle.digest(
123
+ "SHA-256",
124
+ new TextEncoder().encode(options.accessToken)
125
+ );
126
+ let binary = "";
127
+ for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
128
+ claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
129
+ }
130
+ if (options.nonce) claims.nonce = options.nonce;
131
+ return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
132
+ }
133
+ function nonceFrom(response) {
134
+ return response.headers.get("dpop-nonce");
135
+ }
136
+ function wantsNonce(error, response) {
137
+ const code = typeof error === "string" ? error : error?.error;
138
+ return code === "use_dpop_nonce" || !!response && !!nonceFrom(response);
139
+ }
140
+ function boundKeyOf(claims) {
141
+ const cnf = claims.cnf;
142
+ return typeof cnf?.jkt === "string" ? cnf.jkt : null;
143
+ }
144
+ var PRIVATE_MEMBERS;
145
+ var init_dpop = __esm({
146
+ "src/dpop.ts"() {
147
+ "use strict";
148
+ init_types();
149
+ PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
150
+ }
151
+ });
152
+
31
153
  // src/server.ts
32
154
  var server_exports = {};
33
155
  __export(server_exports, {
34
156
  authServerModule: () => authServerModule,
157
+ memoryLogouts: () => memoryLogouts,
35
158
  oauthRoutes: () => oauthRoutes,
159
+ requestAuth: () => requestAuth,
36
160
  sessionLocals: () => sessionLocals
37
161
  });
38
162
  module.exports = __toCommonJS(server_exports);
@@ -61,17 +185,8 @@ function safeReturnTo(candidate, options = {}) {
61
185
  return url.toString();
62
186
  }
63
187
 
64
- // src/types.ts
65
- var OAuthError = class extends Error {
66
- constructor(message, code, description) {
67
- super(message);
68
- this.code = code;
69
- this.description = description;
70
- this.name = "OAuthError";
71
- }
72
- };
73
-
74
188
  // src/client-auth.ts
189
+ init_types();
75
190
  var CANONICAL = {
76
191
  basic: "client_secret_basic",
77
192
  body: "client_secret_post",
@@ -125,6 +240,7 @@ async function applyClientAuth(auth, endpoint, headers, form) {
125
240
  }
126
241
 
127
242
  // src/discovery.ts
243
+ init_types();
128
244
  var cache = /* @__PURE__ */ new Map();
129
245
  function documentUrl(issuer) {
130
246
  return `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
@@ -137,8 +253,12 @@ async function discover(issuer, fetcher = fetch) {
137
253
  try {
138
254
  response = await fetcher(documentUrl(issuer));
139
255
  } catch (cause) {
256
+ const why = cause instanceof Error ? cause.message : String(cause);
140
257
  throw new OAuthError(
141
- `Could not read the discovery document at ${documentUrl(issuer)}. In a browser this is usually the provider refusing a cross-origin read: give \`endpoints\` directly, or resolve them on a server. (${cause instanceof Error ? cause.message : String(cause)})`,
258
+ typeof window === "undefined" ? (
259
+ // On a server nothing blocks the read: the provider is down, or the address is wrong.
260
+ `Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
261
+ ) : `Could not read the discovery document at ${documentUrl(issuer)}. In a browser this is usually the provider refusing a cross-origin read: give \`endpoints\` directly, or resolve them on a server. (${why})`,
142
262
  "discovery_unreachable"
143
263
  );
144
264
  }
@@ -163,6 +283,7 @@ async function discover(issuer, fetcher = fetch) {
163
283
  jwks: doc.jwks_uri,
164
284
  introspection: doc.introspection_endpoint,
165
285
  revocation: doc.revocation_endpoint,
286
+ checkSession: doc.check_session_iframe,
166
287
  issuer: doc.issuer ?? issuer
167
288
  };
168
289
  })();
@@ -171,6 +292,252 @@ async function discover(issuer, fetcher = fetch) {
171
292
  return pending;
172
293
  }
173
294
 
295
+ // src/verify.ts
296
+ init_types();
297
+ var TokenError = class extends OAuthError {
298
+ constructor(message, code = "invalid_token") {
299
+ super(message, code);
300
+ this.name = "TokenError";
301
+ }
302
+ };
303
+ function bearerOf(request) {
304
+ const header = request.headers.get("authorization");
305
+ if (!header) return null;
306
+ const match = header.match(/^(?:Bearer|DPoP)\s+(.+)$/i);
307
+ return match ? match[1].trim() : null;
308
+ }
309
+ async function loadJose3() {
310
+ try {
311
+ return await import("jose");
312
+ } catch {
313
+ throw new OAuthError(
314
+ "Token verification needs `jose`. Install it: npm i jose. It is an optional peer so an app that does not verify does not carry it.",
315
+ "jose_missing"
316
+ );
317
+ }
318
+ }
319
+ function createTokenVerifier(options) {
320
+ const issuers = (Array.isArray(options.issuer) ? options.issuer : [options.issuer]).filter(Boolean);
321
+ if (!issuers.length) {
322
+ throw new Error("[fluixi/oauth2] createTokenVerifier needs an issuer to accept tokens from.");
323
+ }
324
+ const discoveryIssuer = issuers[0];
325
+ if (options.audience === void 0) {
326
+ throw new Error(
327
+ "[fluixi/oauth2] createTokenVerifier needs an audience, or `audience: false` to say this service really does accept a token minted for any client of the issuer."
328
+ );
329
+ }
330
+ if (options.audience === false && options.authorizedParty === void 0) {
331
+ console.warn(
332
+ "[fluixi/oauth2] This verifier checks neither `aud` nor `azp`, so a token minted for any client of the issuer is accepted. Set `authorizedParty` if that is not what you meant."
333
+ );
334
+ }
335
+ const algorithms = options.algorithms ?? ["RS256"];
336
+ if (algorithms.some((alg) => !alg || alg.toLowerCase() === "none")) {
337
+ throw new Error("[fluixi/oauth2] `none` is not a signing algorithm and cannot be accepted.");
338
+ }
339
+ let keys = null;
340
+ async function keySet() {
341
+ if (keys) return keys;
342
+ const jose = await loadJose3();
343
+ if (options.jwks) {
344
+ keys = jose.createLocalJWKSet(options.jwks);
345
+ return keys;
346
+ }
347
+ let uri = options.jwksUri;
348
+ if (!uri) {
349
+ const endpoints = await discover(discoveryIssuer);
350
+ uri = endpoints.jwks;
351
+ }
352
+ if (!uri) {
353
+ throw new OAuthError(
354
+ `${discoveryIssuer} published no jwks_uri. Give the verifier one directly.`,
355
+ "no_jwks"
356
+ );
357
+ }
358
+ keys = jose.createRemoteJWKSet(new URL(uri));
359
+ return keys;
360
+ }
361
+ const checkProof = async (claims, token, request) => {
362
+ const jose = await loadJose3();
363
+ const { boundKeyOf: boundKeyOf2, thumbprintOf: thumbprintOf2 } = await Promise.resolve().then(() => (init_dpop(), dpop_exports));
364
+ const bound = boundKeyOf2(claims);
365
+ if (!bound) {
366
+ throw new TokenError(
367
+ "This token is not bound to a key, and this service requires binding.",
368
+ "token_not_bound"
369
+ );
370
+ }
371
+ const proof = request.headers.get("dpop");
372
+ if (!proof) throw new TokenError("The request carried no DPoP proof.", "missing_dpop_proof");
373
+ let payload;
374
+ let header;
375
+ try {
376
+ const result = await jose.jwtVerify(proof, jose.EmbeddedJWK, { typ: "dpop+jwt" });
377
+ payload = result.payload;
378
+ header = result.protectedHeader;
379
+ } catch (cause) {
380
+ throw new TokenError(
381
+ `The DPoP proof did not verify: ${cause instanceof Error ? cause.message : String(cause)}`,
382
+ "invalid_dpop_proof"
383
+ );
384
+ }
385
+ const presented = await thumbprintOf2(header.jwk);
386
+ if (presented !== bound) {
387
+ throw new TokenError(
388
+ "The DPoP proof was signed with a different key from the one this token is bound to.",
389
+ "dpop_key_mismatch"
390
+ );
391
+ }
392
+ const url = new URL(request.url);
393
+ url.search = "";
394
+ url.hash = "";
395
+ if (payload.htm !== request.method.toUpperCase()) {
396
+ throw new TokenError("The DPoP proof was made for a different method.", "invalid_dpop_proof");
397
+ }
398
+ if (payload.htu !== url.toString()) {
399
+ throw new TokenError("The DPoP proof was made for a different URL.", "invalid_dpop_proof");
400
+ }
401
+ if (typeof payload.ath === "string") {
402
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
403
+ let binary = "";
404
+ for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
405
+ const expected = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
406
+ if (payload.ath !== expected) {
407
+ throw new TokenError("The DPoP proof was made for a different token.", "invalid_dpop_proof");
408
+ }
409
+ }
410
+ const age = Math.abs(Date.now() / 1e3 - payload.iat);
411
+ if (!Number.isFinite(age) || age > (options.dpopProofAgeSec ?? 10)) {
412
+ throw new TokenError("The DPoP proof is too old to accept.", "invalid_dpop_proof");
413
+ }
414
+ };
415
+ const verify = async (token, request) => {
416
+ if (!token) throw new TokenError("No token to verify.", "no_token");
417
+ const jose = await loadJose3();
418
+ let payload;
419
+ try {
420
+ ({ payload } = await jose.jwtVerify(token, await keySet(), {
421
+ // jose takes a list and accepts any of them, which is what a provider naming
422
+ // itself two ways needs.
423
+ issuer: issuers,
424
+ ...options.audience === false ? {} : { audience: options.audience },
425
+ algorithms,
426
+ clockTolerance: options.clockToleranceSec ?? 5
427
+ }));
428
+ } catch (cause) {
429
+ throw new TokenError(
430
+ `The token was not accepted: ${cause instanceof Error ? cause.message : String(cause)}`
431
+ );
432
+ }
433
+ if (options.authorizedParty !== void 0) {
434
+ const allowed = Array.isArray(options.authorizedParty) ? options.authorizedParty : [options.authorizedParty];
435
+ const issuedTo = payload.azp ?? payload.client_id;
436
+ if (typeof issuedTo !== "string" || !allowed.includes(issuedTo)) {
437
+ throw new TokenError("The token was issued to a different client.", "invalid_azp");
438
+ }
439
+ }
440
+ if (options.dpop) {
441
+ if (!request) {
442
+ throw new Error(
443
+ "[fluixi/oauth2] `dpop: true` needs the request to read its proof. Use `verify.fromRequest(request)` rather than verifying a bare token."
444
+ );
445
+ }
446
+ await checkProof(payload, token, request);
447
+ }
448
+ if (options.require) {
449
+ const verdict = options.require(payload);
450
+ if (verdict !== true) {
451
+ throw new TokenError(
452
+ typeof verdict === "string" ? verdict : "The token did not meet this service's requirements.",
453
+ "insufficient_claims"
454
+ );
455
+ }
456
+ }
457
+ return payload;
458
+ };
459
+ const verifier = verify;
460
+ verifier.bearerOf = bearerOf;
461
+ verifier.fromRequest = async (request) => {
462
+ const token = verifier.bearerOf(request);
463
+ if (!token) {
464
+ throw new TokenError("The request carried no bearer token.", "no_token");
465
+ }
466
+ return verify(token, request);
467
+ };
468
+ return verifier;
469
+ }
470
+
471
+ // src/backchannel.ts
472
+ var LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
473
+ function memoryLogouts(ttlMs = 14 * 24 * 36e5) {
474
+ const sids = /* @__PURE__ */ new Map();
475
+ const subs = /* @__PURE__ */ new Map();
476
+ const prune = (now) => {
477
+ for (const map of [sids, subs]) for (const [key, at] of map) if (now - at > ttlMs) map.delete(key);
478
+ };
479
+ return {
480
+ add(entry) {
481
+ prune(Date.now());
482
+ if (entry.sid) sids.set(entry.sid, entry.at);
483
+ else if (entry.sub) subs.set(entry.sub, Math.max(entry.at, subs.get(entry.sub) ?? 0));
484
+ },
485
+ ended(session) {
486
+ if (session.sid && sids.has(session.sid)) return true;
487
+ const all = session.sub ? subs.get(session.sub) : void 0;
488
+ return all !== void 0 && session.since <= all;
489
+ }
490
+ };
491
+ }
492
+ function idTokenClaims(idToken) {
493
+ if (!idToken) return {};
494
+ try {
495
+ const part = idToken.split(".")[1] ?? "";
496
+ const json = atob(part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "="));
497
+ const claims = JSON.parse(json);
498
+ return {
499
+ sid: typeof claims.sid === "string" ? claims.sid : void 0,
500
+ sub: typeof claims.sub === "string" ? claims.sub : void 0,
501
+ iat: typeof claims.iat === "number" ? claims.iat : void 0
502
+ };
503
+ } catch {
504
+ return {};
505
+ }
506
+ }
507
+ function headerOf(token) {
508
+ try {
509
+ const part = token.split(".")[0] ?? "";
510
+ return JSON.parse(atob(part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "=")));
511
+ } catch {
512
+ return {};
513
+ }
514
+ }
515
+ function logoutTokenVerifier(options) {
516
+ const verify = createTokenVerifier({
517
+ issuer: options.issuer,
518
+ audience: options.clientId,
519
+ jwksUri: options.jwksUri,
520
+ jwks: options.jwks,
521
+ require: (claims) => {
522
+ const events = claims.events;
523
+ if (!events || typeof events !== "object" || !(LOGOUT_EVENT in events)) return "no logout event";
524
+ if ("nonce" in claims) return "a logout token has no nonce";
525
+ if (typeof claims.sid !== "string" && typeof claims.sub !== "string") return "names no session and no subject";
526
+ return true;
527
+ }
528
+ });
529
+ return async (token) => {
530
+ const typ = headerOf(token).typ;
531
+ if (typ !== void 0 && typ !== "logout+jwt") throw new Error("not a logout token");
532
+ const claims = await verify(token);
533
+ return {
534
+ sid: typeof claims.sid === "string" ? claims.sid : void 0,
535
+ sub: typeof claims.sub === "string" ? claims.sub : void 0,
536
+ at: (typeof claims.iat === "number" ? claims.iat : Math.floor(Date.now() / 1e3)) * 1e3
537
+ };
538
+ };
539
+ }
540
+
174
541
  // src/pkce.ts
175
542
  function base64url(bytes) {
176
543
  let binary = "";
@@ -207,81 +574,62 @@ function expiryFrom(expiresIn, now = Date.now()) {
207
574
  return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : void 0;
208
575
  }
209
576
 
210
- // src/dpop.ts
211
- async function loadJose2() {
212
- try {
213
- return await import("jose");
214
- } catch {
215
- throw new OAuthError(
216
- "DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
217
- "jose_missing"
218
- );
219
- }
220
- }
221
- async function createDpopKey(alg = "ES256", options = {}) {
222
- const jose = await loadJose2();
223
- const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
224
- extractable: options.extractable ?? false
225
- });
226
- const publicJwk = { ...await jose.exportJWK(publicKey) };
227
- return {
228
- privateKey,
229
- publicJwk,
230
- thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
231
- alg
232
- };
577
+ // src/server.ts
578
+ init_types();
579
+ init_dpop();
580
+ var import_app = require("@fluixi/start/app");
581
+ var RETRY_MARK = ".retry";
582
+ var PROMPTS = /* @__PURE__ */ new Set(["none", "login", "consent", "select_account"]);
583
+ var SILENT_REFUSALS = /* @__PURE__ */ new Set([
584
+ "login_required",
585
+ "consent_required",
586
+ "interaction_required",
587
+ "account_selection_required"
588
+ ]);
589
+ function checkedCookie(config, sessionState) {
590
+ const name = `${config.cookieName ?? "fx_session"}_checked`;
591
+ const secure = config.secureCookie ?? true;
592
+ const maxAge = Math.max(0, Math.floor(config.silentRecheck ?? 60));
593
+ const value = sessionState ?? "1";
594
+ return { name, header: `${name}=${value}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}` };
233
595
  }
234
- async function exportDpopKey(key) {
235
- const jose = await loadJose2();
236
- return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
596
+ function sessionStateFrom(url) {
597
+ const value = url.searchParams.get("session_state");
598
+ return value && /^[\w-]{1,128}\.[\w-]{1,64}$/.test(value) ? value : void 0;
237
599
  }
238
- var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
239
- async function importDpopKey(stored) {
240
- const jose = await loadJose2();
241
- const privateKey = await jose.importJWK(stored.jwk, stored.alg);
242
- const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
243
- return {
244
- privateKey,
245
- publicJwk,
246
- thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
247
- alg: stored.alg
248
- };
600
+ function isPageRequest(request) {
601
+ if (request.method !== "GET") return false;
602
+ const dest = request.headers.get("sec-fetch-dest");
603
+ if (dest) return dest === "document";
604
+ return (request.headers.get("accept") ?? "").includes("text/html");
249
605
  }
250
- async function proofFor(key, options) {
251
- const jose = await loadJose2();
252
- const htu = (() => {
253
- try {
254
- const url = new URL(options.url);
255
- url.search = "";
256
- url.hash = "";
257
- return url.toString();
258
- } catch {
259
- return options.url;
260
- }
261
- })();
262
- const claims = {
263
- htm: options.method.toUpperCase(),
264
- htu,
265
- jti: crypto.randomUUID()
266
- };
267
- if (options.accessToken) {
268
- const digest = await crypto.subtle.digest(
269
- "SHA-256",
270
- new TextEncoder().encode(options.accessToken)
271
- );
272
- let binary = "";
273
- for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
274
- claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
606
+ function cookieValue(request, name) {
607
+ for (const part of (request.headers.get("cookie") ?? "").split(";")) {
608
+ const [key, ...rest] = part.trim().split("=");
609
+ if (key === name) return rest.join("=");
275
610
  }
276
- if (options.nonce) claims.nonce = options.nonce;
277
- return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
611
+ return void 0;
278
612
  }
279
- function nonceFrom(response) {
280
- return response.headers.get("dpop-nonce");
613
+ function hasCookie(request, name) {
614
+ return cookieValue(request, name) !== void 0;
615
+ }
616
+ function under(pathname, path) {
617
+ return pathname === path || pathname.startsWith(path.endsWith("/") ? path : `${path}/`);
618
+ }
619
+ function signInRedirect(request, config) {
620
+ if (!isPageRequest(request)) return null;
621
+ const url = new URL(request.url);
622
+ const base = (config.basePath ?? "/auth").replace(/\/$/, "");
623
+ const guarded = (config.protectedPaths ?? []).some((path) => under(url.pathname, path));
624
+ const mode = guarded ? "required" : config.signIn ?? "manual";
625
+ if (mode === "manual") return null;
626
+ if ([base, ...config.publicPaths ?? []].some((path) => under(url.pathname, path))) return null;
627
+ if (url.searchParams.has("auth_error")) return null;
628
+ if (mode === "silent" && hasCookie(request, checkedCookie(config).name)) return null;
629
+ const login = new URLSearchParams({ returnTo: url.pathname + url.search });
630
+ if (mode === "silent") login.set("prompt", "none");
631
+ return new Response(null, { status: 302, headers: { location: `${base}/login?${login}` } });
281
632
  }
282
-
283
- // src/server.ts
284
- var import_app = require("@fluixi/start/app");
285
633
  var dpopNonces = /* @__PURE__ */ new Map();
286
634
  async function prove(headers, key, method, url, accessToken) {
287
635
  headers.dpop = await proofFor(key, { method, url, accessToken, nonce: dpopNonces.get(new URL(url).origin) });
@@ -294,27 +642,25 @@ function learnNonce(url, response) {
294
642
  dpopNonces.set(origin, offered);
295
643
  return fresh && !response.ok;
296
644
  }
297
- async function withToken(url, session) {
645
+ async function sendAs(session, input, init = {}) {
646
+ const url = String(input);
647
+ const method = (init.method ?? "GET").toUpperCase();
298
648
  const key = session.dpop && session.tokenType?.toLowerCase() === "dpop" ? await importDpopKey(session.dpop) : null;
299
649
  const send = async () => {
300
- const headers = {
650
+ const credentials = {
301
651
  authorization: `${key ? "DPoP" : "Bearer"} ${session.accessToken}`
302
652
  };
303
- if (key) await prove(headers, key, "GET", url, session.accessToken);
304
- return fetch(url, { headers });
653
+ if (key) await prove(credentials, key, method, url, session.accessToken);
654
+ const headers = new Headers(init.headers);
655
+ for (const [name, value] of Object.entries(credentials)) headers.set(name, value);
656
+ return fetch(url, { ...init, method, headers });
305
657
  };
306
658
  let response = await send();
307
659
  if (key && learnNonce(url, response)) response = await send();
308
660
  return response;
309
661
  }
310
- function oauthRoutes(config) {
311
- const base = (config.basePath ?? "/auth").replace(/\/$/, "");
312
- const scopes = config.scopes ?? ["openid", "profile", "email"];
313
- const cookie = (0, import_server.createSessionCookie)({
314
- secret: config.cookieSecret,
315
- name: config.cookieName,
316
- secure: config.secureCookie
317
- });
662
+ var withToken = (url, session) => sendAs(session, url);
663
+ function tokenClient(config) {
318
664
  let endpoints = config.endpoints ?? null;
319
665
  const resolve = async () => {
320
666
  if (endpoints) return endpoints;
@@ -324,10 +670,6 @@ function oauthRoutes(config) {
324
670
  endpoints = await discover(config.issuer);
325
671
  return endpoints;
326
672
  };
327
- const json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), {
328
- status,
329
- headers: { "content-type": "application/json", ...headers }
330
- });
331
673
  const clientAuth = {
332
674
  clientId: config.clientId,
333
675
  // `client_secret_post` by default here, which is what this half has always sent, so
@@ -360,17 +702,111 @@ function oauthRoutes(config) {
360
702
  }
361
703
  return payload;
362
704
  }
705
+ return { resolve, tokenRequest, clientAuth };
706
+ }
707
+ var renewals = /* @__PURE__ */ new Map();
708
+ var RENEWAL_REUSE_MS = 3e4;
709
+ function renew(current, tokenRequest) {
710
+ const refreshToken = current.refreshToken;
711
+ const now = Date.now();
712
+ for (const [key, entry] of renewals) if (entry.until <= now) renewals.delete(key);
713
+ const known = renewals.get(refreshToken);
714
+ if (known) return known.result;
715
+ const result = (async () => {
716
+ const key = current.dpop ? await importDpopKey(current.dpop) : null;
717
+ const payload = await tokenRequest({ grant_type: "refresh_token", refresh_token: refreshToken }, key);
718
+ return {
719
+ accessToken: String(payload.access_token ?? ""),
720
+ // A provider that rotates refresh tokens sends a new one; one that does not
721
+ // expects the old one to keep working.
722
+ refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : refreshToken,
723
+ idToken: typeof payload.id_token === "string" ? payload.id_token : current.idToken,
724
+ expiresAt: expiryFrom(payload.expires_in),
725
+ tokenType: typeof payload.token_type === "string" ? payload.token_type : current.tokenType,
726
+ dpop: current.dpop,
727
+ sessionState: current.sessionState
728
+ };
729
+ })().catch((error) => {
730
+ if (error instanceof OAuthError) return null;
731
+ renewals.delete(refreshToken);
732
+ throw error;
733
+ });
734
+ renewals.set(refreshToken, { until: now + RENEWAL_REUSE_MS, result });
735
+ return result;
736
+ }
737
+ var EXPIRY_SKEW_MS = 3e4;
738
+ var expired = (session) => session.expiresAt !== void 0 && session.expiresAt - EXPIRY_SKEW_MS <= Date.now();
739
+ var defaultLogouts = /* @__PURE__ */ new WeakMap();
740
+ function logoutsOf(config) {
741
+ if (config.logouts) return config.logouts;
742
+ let store = defaultLogouts.get(config);
743
+ if (!store) defaultLogouts.set(config, store = memoryLogouts());
744
+ return store;
745
+ }
746
+ async function endedAtProvider(config, session) {
747
+ const id = idTokenClaims(session.idToken);
748
+ if (!id.sid && !id.sub) return false;
749
+ return logoutsOf(config).ended({ sid: id.sid, sub: id.sub, since: (id.iat ?? 0) * 1e3 });
750
+ }
751
+ function oauthRoutes(config) {
752
+ const { resolve, tokenRequest, clientAuth } = tokenClient(config);
753
+ const json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), {
754
+ status,
755
+ headers: { "content-type": "application/json", ...headers }
756
+ });
757
+ const base = (config.basePath ?? "/auth").replace(/\/$/, "");
758
+ const scopes = config.scopes ?? ["openid", "profile", "email"];
759
+ const cookie = (0, import_server.createSessionCookie)({
760
+ secret: config.cookieSecret,
761
+ name: config.cookieName,
762
+ secure: config.secureCookie
763
+ });
363
764
  return async function middleware(request, next) {
364
765
  const url = new URL(request.url);
365
766
  if (!url.pathname.startsWith(`${base}/`)) return next();
366
767
  const route = url.pathname.slice(base.length);
367
768
  const session = async () => {
368
- return cookie.read(request);
769
+ const current = await cookie.read(request);
770
+ return current && await endedAtProvider(config, current) ? null : current;
369
771
  };
772
+ if (route === "/backchannel-logout") {
773
+ const headers = { "cache-control": "no-store" };
774
+ if (request.method !== "POST") return json({ error: "invalid_request" }, 405, headers);
775
+ try {
776
+ const form = await request.formData();
777
+ const token = form.get("logout_token");
778
+ if (typeof token !== "string" || !token) throw new Error("no logout_token");
779
+ const endpoints = await resolve();
780
+ const issuer = endpoints.issuer ?? config.issuer;
781
+ if (!issuer) throw new Error("no issuer");
782
+ if (!endpoints.jwks) throw new Error("no jwks_uri");
783
+ const jwks = await (await fetch(endpoints.jwks)).json();
784
+ const entry = await logoutTokenVerifier({ issuer, clientId: config.clientId, jwks })(token);
785
+ await logoutsOf(config).add(entry);
786
+ return new Response(null, { status: 200, headers });
787
+ } catch {
788
+ return json({ error: "invalid_request" }, 400, headers);
789
+ }
790
+ }
370
791
  if (route === "/login") {
371
- const { authorization } = await resolve();
792
+ let authorization;
793
+ try {
794
+ ({ authorization } = await resolve());
795
+ } catch (error) {
796
+ if (!(error instanceof OAuthError) || error.code !== "discovery_unreachable") throw error;
797
+ const back = safeReturnTo(url.searchParams.get("returnTo"), {
798
+ fallback: config.defaultReturnTo ?? "/",
799
+ origin: url.origin,
800
+ allowedOrigins: config.allowedReturnOrigins
801
+ });
802
+ const target2 = new URL(back, url.origin);
803
+ target2.searchParams.set("auth_error", "provider_unreachable");
804
+ const location2 = target2.origin === url.origin ? target2.pathname + target2.search : target2.toString();
805
+ return new Response(null, { status: 302, headers: { location: location2 } });
806
+ }
372
807
  const { verifier, challenge } = await createPkcePair();
373
- const state = createStateValue();
808
+ const retry = url.searchParams.get("retry") === "1";
809
+ const state = createStateValue() + (retry ? RETRY_MARK : "");
374
810
  const target = new URL(authorization);
375
811
  target.searchParams.set("response_type", "code");
376
812
  target.searchParams.set("client_id", config.clientId);
@@ -379,6 +815,8 @@ function oauthRoutes(config) {
379
815
  target.searchParams.set("state", state);
380
816
  target.searchParams.set("code_challenge", challenge);
381
817
  target.searchParams.set("code_challenge_method", "S256");
818
+ const prompt = url.searchParams.get("prompt");
819
+ if (prompt && PROMPTS.has(prompt)) target.searchParams.set("prompt", prompt);
382
820
  const pendingCookie = await cookie.write(
383
821
  {
384
822
  accessToken: "",
@@ -391,7 +829,8 @@ function oauthRoutes(config) {
391
829
  fallback: "",
392
830
  origin: url.origin,
393
831
  allowedOrigins: config.allowedReturnOrigins
394
- }) || void 0
832
+ }) || void 0,
833
+ silent: prompt === "none" || void 0
395
834
  }
396
835
  },
397
836
  // Ten minutes: long enough to sign in, short enough that an abandoned attempt
@@ -410,12 +849,43 @@ function oauthRoutes(config) {
410
849
  const current = await session();
411
850
  const pending = current?.pending;
412
851
  const error = url.searchParams.get("error");
413
- if (error) return json({ error }, 400);
852
+ if (error) {
853
+ const ours = !!pending && timingSafeEqual(pending.state, url.searchParams.get("state") ?? "");
854
+ let location2 = safeReturnTo(ours ? pending.returnTo ?? config.defaultReturnTo : config.defaultReturnTo, {
855
+ fallback: "/",
856
+ origin: url.origin,
857
+ allowedOrigins: config.allowedReturnOrigins
858
+ });
859
+ const headers = new Headers();
860
+ headers.append("set-cookie", cookie.clear());
861
+ if (ours && pending.silent && SILENT_REFUSALS.has(error)) {
862
+ headers.append("set-cookie", checkedCookie(config, sessionStateFrom(url)).header);
863
+ } else {
864
+ const target = new URL(location2, url.origin);
865
+ target.searchParams.set("auth_error", error);
866
+ location2 = target.origin === url.origin ? target.pathname + target.search : target.toString();
867
+ }
868
+ headers.set("location", location2);
869
+ return new Response(null, { status: 302, headers });
870
+ }
414
871
  const code = url.searchParams.get("code");
415
872
  const state = url.searchParams.get("state");
416
873
  if (!code) return json({ error: "no_code" }, 400);
417
874
  if (!pending || !state || !timingSafeEqual(pending.state, state)) {
418
- return json({ error: "state_mismatch" }, 400);
875
+ const home = safeReturnTo(config.defaultReturnTo, { fallback: "/", origin: url.origin, allowedOrigins: config.allowedReturnOrigins });
876
+ if (current?.accessToken) return new Response(null, { status: 302, headers: { location: home } });
877
+ if (!state?.endsWith(RETRY_MARK)) {
878
+ return new Response(null, { status: 302, headers: { location: `${base}/login?retry=1` } });
879
+ }
880
+ const target = new URL(home, url.origin);
881
+ target.searchParams.set("auth_error", "state_mismatch");
882
+ return new Response(null, {
883
+ status: 302,
884
+ headers: {
885
+ location: target.origin === url.origin ? target.pathname + target.search : target.toString(),
886
+ "set-cookie": cookie.clear()
887
+ }
888
+ });
419
889
  }
420
890
  const key = config.dpop ? await createDpopKey(config.dpopAlgorithm, { extractable: true }) : null;
421
891
  const payload = await tokenRequest(
@@ -433,7 +903,8 @@ function oauthRoutes(config) {
433
903
  idToken: typeof payload.id_token === "string" ? payload.id_token : void 0,
434
904
  expiresAt: expiryFrom(payload.expires_in),
435
905
  tokenType: typeof payload.token_type === "string" ? payload.token_type : void 0,
436
- dpop: key ? await exportDpopKey(key) : void 0
906
+ dpop: key ? await exportDpopKey(key) : void 0,
907
+ sessionState: sessionStateFrom(url)
437
908
  });
438
909
  return new Response(null, {
439
910
  status: 302,
@@ -453,22 +924,9 @@ function oauthRoutes(config) {
453
924
  if (route === "/refresh") {
454
925
  const current = await session();
455
926
  if (!current?.refreshToken) return json({ error: "no_session" }, 401);
456
- const key = current.dpop ? await importDpopKey(current.dpop) : null;
457
- const payload = await tokenRequest(
458
- { grant_type: "refresh_token", refresh_token: current.refreshToken },
459
- key
460
- );
461
- const refreshed = await cookie.write({
462
- accessToken: String(payload.access_token ?? ""),
463
- // A provider that rotates refresh tokens sends a new one; one that does not
464
- // expects the old one to keep working.
465
- refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : current.refreshToken,
466
- idToken: current.idToken,
467
- expiresAt: expiryFrom(payload.expires_in),
468
- tokenType: typeof payload.token_type === "string" ? payload.token_type : current.tokenType,
469
- dpop: current.dpop
470
- });
471
- return json({ ok: true }, 200, { "set-cookie": refreshed });
927
+ const renewed = await renew(current, tokenRequest);
928
+ if (!renewed) return json({ error: "refresh_refused" }, 401, { "set-cookie": cookie.clear() });
929
+ return json({ ok: true }, 200, { "set-cookie": await cookie.write(renewed) });
472
930
  }
473
931
  if (route === "/me") {
474
932
  const current = await session();
@@ -480,6 +938,13 @@ function oauthRoutes(config) {
480
938
  const claims = await response.json();
481
939
  return json(config.mapUser ? config.mapUser(claims, current.accessToken) : claims);
482
940
  }
941
+ if (route === "/session-check") {
942
+ const { checkSession } = await resolve().catch(() => ({ checkSession: void 0 }));
943
+ const current = await session();
944
+ const checked = cookieValue(request, checkedCookie(config).name);
945
+ const state = current?.accessToken ? current.sessionState : checked !== "1" ? checked : void 0;
946
+ return json({ frame: checkSession ?? null, clientId: config.clientId, state: state ?? null });
947
+ }
483
948
  if (route === "/revoke") {
484
949
  const current = await session();
485
950
  const hint = url.searchParams.get("hint") === "access_token" ? "access_token" : "refresh_token";
@@ -502,31 +967,33 @@ function oauthRoutes(config) {
502
967
  const { endSession } = await resolve().catch(() => ({ endSession: void 0 }));
503
968
  const { revocation } = await resolve().catch(() => ({ revocation: void 0 }));
504
969
  if (revocation && current?.refreshToken) {
505
- const headers = {
970
+ const headers2 = {
506
971
  "content-type": "application/x-www-form-urlencoded"
507
972
  };
508
973
  const form = new URLSearchParams({
509
974
  token: current.refreshToken,
510
975
  token_type_hint: "refresh_token"
511
976
  });
512
- await applyClientAuth(clientAuth, revocation, headers, form);
513
- await fetch(revocation, { method: "POST", headers, body: form.toString() }).catch(
977
+ await applyClientAuth(clientAuth, revocation, headers2, form);
978
+ await fetch(revocation, { method: "POST", headers: headers2, body: form.toString() }).catch(
514
979
  () => void 0
515
980
  );
516
981
  }
517
982
  const cleared = cookie.clear();
983
+ const headers = new Headers();
984
+ headers.append("set-cookie", cleared);
985
+ headers.append("set-cookie", checkedCookie(config).header);
518
986
  if (endSession && current?.idToken) {
519
987
  const target = new URL(endSession);
520
988
  target.searchParams.set("id_token_hint", current.idToken);
521
989
  if (config.defaultReturnTo) {
522
990
  target.searchParams.set("post_logout_redirect_uri", config.defaultReturnTo);
523
991
  }
524
- return new Response(null, {
525
- status: 302,
526
- headers: { location: target.toString(), "set-cookie": cleared }
527
- });
992
+ headers.set("location", target.toString());
993
+ return new Response(null, { status: 302, headers });
528
994
  }
529
- return json({ ok: true }, 200, { "set-cookie": cleared });
995
+ headers.set("content-type", "application/json");
996
+ return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
530
997
  }
531
998
  return next();
532
999
  };
@@ -537,6 +1004,7 @@ function sessionLocals(config) {
537
1004
  name: config.cookieName,
538
1005
  secure: config.secureCookie
539
1006
  });
1007
+ const { tokenRequest } = tokenClient(config);
540
1008
  const ttl = config.userCacheMs ?? 3e4;
541
1009
  const cache2 = /* @__PURE__ */ new Map();
542
1010
  const remember = (token, user) => {
@@ -551,21 +1019,56 @@ function sessionLocals(config) {
551
1019
  return async function middleware(request, next) {
552
1020
  const { getRequestLocals } = await import("@fluixi/server");
553
1021
  const locals = getRequestLocals(request);
554
- const session = await cookie.read(request);
555
- if (!session?.accessToken) {
1022
+ let session = await cookie.read(request);
1023
+ let setCookie;
1024
+ const respond = async (response) => {
1025
+ if (!setCookie) return response;
1026
+ const copy = new Response(response.body, response);
1027
+ copy.headers.append("set-cookie", setCookie);
1028
+ return copy;
1029
+ };
1030
+ const renewOrDrop = async (current2) => {
1031
+ const renewed = current2.refreshToken ? await renew(current2, tokenRequest) : null;
1032
+ setCookie = renewed ? await cookie.write(renewed) : cookie.clear();
1033
+ return renewed;
1034
+ };
1035
+ if (session?.accessToken && expired(session)) {
1036
+ session = await renewOrDrop(session).catch(() => session);
1037
+ }
1038
+ const signedOut = async () => {
1039
+ const redirect = signInRedirect(request, config);
1040
+ if (redirect) return respond(redirect);
556
1041
  locals.auth = { user: null, session: null };
557
- return next();
1042
+ return respond(await next());
1043
+ };
1044
+ if (!session?.accessToken) return signedOut();
1045
+ if (await endedAtProvider(config, session)) {
1046
+ setCookie = cookie.clear();
1047
+ session = null;
1048
+ return signedOut();
558
1049
  }
559
- const cached = cache2.get(session.accessToken);
1050
+ const live = session;
1051
+ const fetchAs = (input, init) => sendAs(live, input, init);
1052
+ const cached = cache2.get(live.accessToken);
560
1053
  if (cached && cached.until > Date.now()) {
561
- locals.auth = { user: cached.user, session: { expiresAt: session.expiresAt } };
562
- return next();
1054
+ locals.auth = { user: cached.user, session: { expiresAt: live.expiresAt }, fetch: fetchAs };
1055
+ return respond(await next());
563
1056
  }
564
1057
  let user = null;
1058
+ let unreachable = false;
565
1059
  try {
566
1060
  const endpoints = config.endpoints ?? (config.issuer ? await discover(config.issuer) : null);
567
1061
  if (endpoints?.userinfo) {
568
- const response = await withToken(endpoints.userinfo, session);
1062
+ let response = await withToken(endpoints.userinfo, session);
1063
+ if (response.status === 401 && !setCookie) {
1064
+ session = await renewOrDrop(session).catch(() => session);
1065
+ if (!session) return signedOut();
1066
+ response = await withToken(endpoints.userinfo, session);
1067
+ }
1068
+ if (response.status === 401) {
1069
+ setCookie = cookie.clear();
1070
+ return signedOut();
1071
+ }
569
1072
  if (response.ok) {
570
1073
  const claims = await response.json();
571
1074
  user = config.mapUser ? config.mapUser(claims, session.accessToken) : claims;
@@ -573,11 +1076,20 @@ function sessionLocals(config) {
573
1076
  }
574
1077
  }
575
1078
  } catch {
1079
+ unreachable = true;
576
1080
  }
577
- locals.auth = { user, session: user ? { expiresAt: session.expiresAt } : null };
578
- return next();
1081
+ if (!session) return signedOut();
1082
+ const current = session;
1083
+ const fetchCurrent = (input, init) => sendAs(current, input, init);
1084
+ locals.auth = user ? { user, session: { expiresAt: current.expiresAt }, fetch: fetchCurrent } : { user: null, session: null, ...unreachable ? { unreachable: true } : {} };
1085
+ return respond(await next());
579
1086
  };
580
1087
  }
1088
+ async function requestAuth(request) {
1089
+ const { getLocals, getRequestLocals } = await import("@fluixi/server");
1090
+ const locals = request ? getRequestLocals(request) : getLocals();
1091
+ return locals.auth ?? { user: null, session: null };
1092
+ }
581
1093
  function authServerModule(config) {
582
1094
  return (0, import_app.defineModule)({ name: "oauth2-server", middleware: [oauthRoutes(config), sessionLocals(config)] });
583
1095
  }