@kanzo-tech/auth 0.18.0 → 0.20.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.
Files changed (49) hide show
  1. package/dist/auth-context.d.ts +3 -1
  2. package/dist/auth-context.d.ts.map +1 -1
  3. package/dist/auth-context.js.map +1 -1
  4. package/dist/auth-provider.js +19 -18
  5. package/dist/auth-provider.js.map +1 -1
  6. package/dist/bff-auth.d.ts +2 -1
  7. package/dist/bff-auth.d.ts.map +1 -1
  8. package/dist/bff-auth.js +77 -46
  9. package/dist/bff-auth.js.map +1 -1
  10. package/dist/browser.d.ts.map +1 -1
  11. package/dist/browser.js +65 -42
  12. package/dist/browser.js.map +1 -1
  13. package/dist/claims.js +1 -1
  14. package/dist/claims.js.map +1 -1
  15. package/dist/cookie-session.d.ts.map +1 -1
  16. package/dist/cookie-session.js +1 -1
  17. package/dist/cookie-session.js.map +1 -1
  18. package/dist/deadline.d.ts +14 -0
  19. package/dist/deadline.d.ts.map +1 -0
  20. package/dist/deadline.js +17 -0
  21. package/dist/deadline.js.map +1 -0
  22. package/dist/gate.js +9 -9
  23. package/dist/gate.js.map +1 -1
  24. package/dist/issuer.d.ts +0 -2
  25. package/dist/issuer.d.ts.map +1 -1
  26. package/dist/issuer.js +17 -17
  27. package/dist/issuer.js.map +1 -1
  28. package/dist/next-middleware.d.ts +6 -0
  29. package/dist/next-middleware.d.ts.map +1 -1
  30. package/dist/next-middleware.js +15 -13
  31. package/dist/next-middleware.js.map +1 -1
  32. package/dist/next-proxy.d.ts.map +1 -1
  33. package/dist/next-proxy.js +27 -26
  34. package/dist/next-proxy.js.map +1 -1
  35. package/dist/next-routes.d.ts +9 -0
  36. package/dist/next-routes.d.ts.map +1 -1
  37. package/dist/next-routes.js +66 -46
  38. package/dist/next-routes.js.map +1 -1
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +151 -109
  41. package/dist/server.js.map +1 -1
  42. package/dist/types.d.ts +40 -21
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js +9 -4
  45. package/dist/types.js.map +1 -1
  46. package/dist/use-session.d.ts +2 -1
  47. package/dist/use-session.d.ts.map +1 -1
  48. package/dist/use-session.js.map +1 -1
  49. package/package.json +6 -6
@@ -1,82 +1,102 @@
1
- import { isSameSite as m } from "./same-site.js";
2
- import { relyingParty as k } from "./server.js";
3
- import { AuthError as R } from "./types.js";
4
- const d = { "content-type": "application/json", "cache-control": "no-store" };
5
- function y(t) {
6
- const n = t.lastIndexOf("/");
7
- return { action: t.slice(n + 1), base: n <= 0 ? "" : t.slice(0, n) };
1
+ import { isSameSite as y } from "./same-site.js";
2
+ import { relyingParty as b } from "./server.js";
3
+ import { AuthError as w } from "./types.js";
4
+ const f = { "content-type": "application/json", "cache-control": "no-store" }, k = "/auth/problem";
5
+ function P(e) {
6
+ const n = e.lastIndexOf("/");
7
+ return { action: e.slice(n + 1), base: n <= 0 ? "" : e.slice(0, n) };
8
8
  }
9
- function g(t, n) {
10
- if (!(t === null || t.length === 0))
9
+ function g(e, n) {
10
+ if (!(e === null || e.length === 0))
11
11
  try {
12
- const r = new URL(t, n);
13
- return r.origin === n ? r.href : void 0;
12
+ const a = new URL(e, n);
13
+ return a.origin === n ? a.href : void 0;
14
14
  } catch {
15
15
  return;
16
16
  }
17
17
  }
18
- function h(t, n) {
19
- const r = new Headers({ location: t, "cache-control": "no-store" });
20
- for (const i of n) r.append("set-cookie", i);
21
- return new Response(null, { status: 302, headers: r });
18
+ function h(e, n) {
19
+ const a = new Headers({ location: e, "cache-control": "no-store" });
20
+ for (const c of n) a.append("set-cookie", c);
21
+ return new Response(null, { status: 302, headers: a });
22
22
  }
23
- function f(t) {
24
- if (!(t instanceof R)) throw t;
25
- return new Response(JSON.stringify({ error: t.code, message: t.message }), {
26
- status: t.code === "session.absent" ? 401 : 400,
27
- headers: d
23
+ function T(e) {
24
+ if (e === "idp/silent" || e === "session/silent") return 504;
25
+ if (e === "idp/unreachable") return 502;
26
+ if (e === "session/unavailable") return 503;
27
+ }
28
+ function p(e) {
29
+ if (!(e instanceof w)) throw e;
30
+ return new Response(JSON.stringify({ error: e.code, message: e.message }), {
31
+ status: T(e.code) ?? (e.code === "session/absent" ? 401 : 400),
32
+ headers: f
28
33
  });
29
34
  }
30
- function S(t) {
35
+ function E(e) {
31
36
  let n;
32
- const r = (s) => ((n == null ? void 0 : n.redirectUri) !== s && (n = { redirectUri: s, auth: k({ ...t, redirectUri: s }) }), n.auth), i = async (s) => {
33
- const o = new URL(s.url), { action: l, base: w } = y(o.pathname), u = s.headers.get("cookie"), a = r(t.redirectUri ?? `${o.origin}${w}/callback`);
34
- if (l !== "callback" && l !== "signin" && !m(s))
37
+ const a = (s) => ((n == null ? void 0 : n.redirectUri) !== s && (n = { redirectUri: s, auth: b({ ...e, redirectUri: s }) }), n.auth), c = (s, r) => {
38
+ if (!(s instanceof w)) throw s;
39
+ const i = new URL(e.problemPage ?? k, r);
40
+ return i.searchParams.set("code", s.code), h(i.href, []);
41
+ }, d = async (s) => {
42
+ const r = new URL(s.url), { action: i, base: m } = P(r.pathname), l = s.headers.get("cookie"), u = a(e.redirectUri ?? `${r.origin}${m}/callback`);
43
+ if (i !== "callback" && i !== "signin" && !y(s))
35
44
  return new Response(null, { status: 403, headers: { "cache-control": "no-store" } });
36
- switch (l) {
45
+ switch (i) {
37
46
  case "signin":
38
47
  try {
39
- const e = await a.begin({
40
- returnTo: g(o.searchParams.get("returnTo"), o.origin) ?? "/",
41
- organization: o.searchParams.get("organization") ?? void 0
48
+ const t = await u.begin({
49
+ returnTo: g(r.searchParams.get("returnTo"), r.origin) ?? "/",
50
+ organization: r.searchParams.get("organization") ?? void 0
42
51
  });
43
- return h(e.url, e.cookies);
44
- } catch (e) {
45
- return f(e);
52
+ return h(t.url, t.cookies);
53
+ } catch (t) {
54
+ return c(t, r.origin);
46
55
  }
47
56
  case "callback":
48
57
  try {
49
- const e = await a.complete({ url: o, cookie: u });
50
- return h(e.returnTo, e.cookies);
51
- } catch (e) {
52
- return f(e);
58
+ const t = await u.complete({ url: r, cookie: l });
59
+ return h(t.returnTo, t.cookies);
60
+ } catch (t) {
61
+ return c(t, r.origin);
53
62
  }
54
63
  case "signout": {
55
- const e = g(o.searchParams.get("returnTo"), o.origin), c = await a.end(u, { returnTo: e });
56
- return h(c.url, c.cookies);
64
+ const t = g(r.searchParams.get("returnTo"), r.origin);
65
+ try {
66
+ const o = await u.end(l, { returnTo: t });
67
+ return h(o.url, o.cookies);
68
+ } catch (o) {
69
+ return c(o, r.origin);
70
+ }
57
71
  }
58
72
  case "session": {
59
- const e = await a.read(u);
60
- return e === null ? new Response(null, { status: 401, headers: { "cache-control": "no-store" } }) : new Response(JSON.stringify(e), { status: 200, headers: d });
73
+ let t;
74
+ try {
75
+ t = await u.read(l);
76
+ } catch (o) {
77
+ return p(o);
78
+ }
79
+ return t === null ? new Response(null, { status: 401, headers: { "cache-control": "no-store" } }) : new Response(JSON.stringify(t), { status: 200, headers: f });
61
80
  }
62
81
  case "refresh": {
63
82
  if (s.method !== "POST")
64
83
  return new Response(null, { status: 405, headers: { allow: "POST" } });
65
84
  try {
66
- const e = await a.refresh(u), c = new Headers(d);
67
- for (const p of e.cookies) c.append("set-cookie", p);
68
- return new Response(JSON.stringify(e.session), { status: 200, headers: c });
69
- } catch (e) {
70
- return f(e);
85
+ const t = await u.refresh(l), o = new Headers(f);
86
+ for (const R of t.cookies) o.append("set-cookie", R);
87
+ return new Response(JSON.stringify(t.session), { status: 200, headers: o });
88
+ } catch (t) {
89
+ return p(t);
71
90
  }
72
91
  }
73
92
  default:
74
93
  return new Response(null, { status: 404 });
75
94
  }
76
95
  };
77
- return { GET: i, POST: i };
96
+ return { GET: d, POST: d };
78
97
  }
79
98
  export {
80
- S as authRoutes
99
+ E as authRoutes,
100
+ T as outage
81
101
  };
82
102
  //# sourceMappingURL=next-routes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"next-routes.js","sources":["../src/next-routes.ts"],"sourcesContent":["import { isSameSite } from \"./same-site\";\nimport type { AuthSessionConfig } from \"./next-session\";\nimport { relyingParty, type RelyingParty } from \"./server\";\nimport { AuthError } from \"./types\";\n\n/**\n * The five routes a Backend For Frontend needs, as one App Router catch-all.\n *\n * ```ts\n * // app/api/auth/[...auth]/route.ts\n * export const { GET, POST } = authRoutes({ issuer, clientId, clientSecret, secret });\n * ```\n *\n * `relyingParty` already does the whole flow in strings — a URL and a `Cookie` header in, a URL and\n * `Set-Cookie` values out — so the only thing written here is the translation into `Request` and\n * `Response`, plus the two decisions that translation forces: what a route answers when there is\n * no session, and where it is willing to send a browser afterwards.\n *\n * **Nothing in this module imports `next`.** An App Router route handler is handed a standard\n * `Request` and may answer with a standard `Response`, so the framework's own types would buy\n * nothing and would make this half untestable without it. The door earns its subpath through\n * `next-session.ts` and `next-middleware.ts`, which genuinely cannot be written without `next`.\n *\n * ## The other end of the contract\n *\n * `bffAuth` in the root barrel is the browser half, and it is specific: it `GET`s\n * `${basePath}/session` and reads **401 as \"nobody is signed in\"**, not as a failure; it `POST`s\n * `${basePath}/refresh` when a request of its own comes back 401, and retries that request once if\n * the renewal worked; it navigates to `${basePath}/signin?returnTo=…&organization=…` and\n * `${basePath}/signout?returnTo=…`. Those five paths and those status codes are the contract, and\n * `next-routes.test.ts` drives a real `bffAuth` against these handlers rather than trusting the\n * two descriptions to agree.\n *\n * ## Which routes a cross-site request may reach\n *\n * `callback` and `signin` must be reachable from anywhere — one *is* a navigation from the\n * identity provider, and the other is a link somebody is allowed to put on another page. The other\n * three are not: `signout` reached cross-site is a logout anyone can cause, `refresh` is a\n * rotation anyone can cause, and `session` is a person's identity read from a page that is not\n * ours. `same-site.ts` carries the check and the reason the package rather than the product owes\n * it.\n */\n\n/** The session endpoint answers about a person; no cache may ever hold that answer. */\nconst PRIVATE = { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" } as const;\n\nexport interface AuthRoutesConfig extends AuthSessionConfig {\n /**\n * Override the callback URL. Absent, it is derived from the incoming request: the origin it\n * arrived at, the path this route file sits on, and `/callback`.\n *\n * Deriving it trusts the `Host` header, which is chosen by whoever made the request. That is a\n * bounded trust — a forged host produces a `redirect_uri` Keycloak has not registered, and\n * Keycloak refuses it — but a deployment behind a proxy that rewrites the host should say the\n * URL out loud here rather than discover this.\n */\n readonly redirectUri?: string;\n}\n\nexport interface AuthRouteHandlers {\n GET(request: Request): Promise<Response>;\n POST(request: Request): Promise<Response>;\n}\n\n/** The last path segment, and everything before it. `/api/auth/signin` → `signin`, `/api/auth`. */\nfunction split(pathname: string): { readonly action: string; readonly base: string } {\n const cut = pathname.lastIndexOf(\"/\");\n return { action: pathname.slice(cut + 1), base: cut <= 0 ? \"\" : pathname.slice(0, cut) };\n}\n\n/**\n * A `returnTo` confined to this origin, or `undefined`.\n *\n * Without this the sign-in route is an open redirect: `?returnTo=https://evil.test` is carried\n * through the whole flow and spent on a browser that has just proved who it is, which is the most\n * valuable moment to hijack. The check is here, where the string arrives from a query parameter,\n * and deliberately *not* repeated on the callback — what comes back out at the callback was\n * sealed into a cookie by us, so re-checking it would only make it unclear which check is the\n * real one.\n */\nfunction sameOrigin(candidate: string | null, origin: string): string | undefined {\n if (candidate === null || candidate.length === 0) return undefined;\n try {\n const target = new URL(candidate, origin);\n return target.origin === origin ? target.href : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * A redirect that can carry cookies.\n *\n * `Response.redirect()` cannot: its headers are immutable, and a callback that cannot set a cookie\n * is a sign-in that never completes. And `append`, never `set` — the callback answers with **two**\n * `Set-Cookie` values, the session it just issued and the transaction it just spent, and `set`\n * would silently keep only the last of them.\n */\nfunction redirect(url: string, cookies: readonly string[]): Response {\n const headers = new Headers({ location: url, \"cache-control\": \"no-store\" });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(null, { status: 302, headers });\n}\n\n/**\n * An `AuthError` as a status and a code a product can route on.\n *\n * Anything else is rethrown: a failure this module has no reading of is Next's to report, and\n * flattening it into a 400 here would hide a misconfiguration behind a message about credentials.\n */\nfunction failure(error: unknown): Response {\n if (!(error instanceof AuthError)) throw error;\n return new Response(JSON.stringify({ error: error.code, message: error.message }), {\n status: error.code === \"session.absent\" ? 401 : 400,\n headers: PRIVATE,\n });\n}\n\nexport function authRoutes(config: AuthRoutesConfig): AuthRouteHandlers {\n // One instance, rebuilt only when the callback URL it was built for changes. A `Map` keyed by\n // origin would grow without bound on a stream of forged `Host` headers; a single slot cannot,\n // and it self-heals, because the next genuine request derives its own URL again. What it costs\n // in that case is a re-discovery, which is the right price for a request that lied.\n let current: { readonly redirectUri: string; readonly auth: RelyingParty } | undefined;\n const authFor = (redirectUri: string): RelyingParty => {\n if (current?.redirectUri !== redirectUri) {\n current = { redirectUri, auth: relyingParty({ ...config, redirectUri }) };\n }\n return current.auth;\n };\n\n const handle = async (request: Request): Promise<Response> => {\n const url = new URL(request.url);\n const { action, base } = split(url.pathname);\n const cookie = request.headers.get(\"cookie\");\n const auth = authFor(config.redirectUri ?? `${url.origin}${base}/callback`);\n\n if (action !== \"callback\" && action !== \"signin\" && !isSameSite(request)) {\n return new Response(null, { status: 403, headers: { \"cache-control\": \"no-store\" } });\n }\n\n switch (action) {\n case \"signin\": {\n try {\n const started = await auth.begin({\n returnTo: sameOrigin(url.searchParams.get(\"returnTo\"), url.origin) ?? \"/\",\n organization: url.searchParams.get(\"organization\") ?? undefined,\n });\n return redirect(started.url, started.cookies);\n } catch (error) {\n // `organization` arrives from a query parameter and `begin` refuses one that is not an\n // alias, because a space in it injects scopes. Without this `catch` that refusal is a\n // 500 on a link somebody typed wrong.\n return failure(error);\n }\n }\n\n case \"callback\": {\n try {\n const done = await auth.complete({ url, cookie });\n return redirect(done.returnTo, done.cookies);\n } catch (error) {\n return failure(error);\n }\n }\n\n case \"signout\": {\n const returnTo = sameOrigin(url.searchParams.get(\"returnTo\"), url.origin);\n const ended = await auth.end(cookie, { returnTo });\n return redirect(ended.url, ended.cookies);\n }\n\n case \"session\": {\n const session = await auth.read(cookie);\n // 401 and no body at all. `bffAuth` reads this status as \"nobody is signed in\" and stops;\n // a body would be parsed by something eventually, and an error shape arriving where a\n // `Session` is expected is the failure `readSession` exists to refuse.\n if (session === null) {\n return new Response(null, { status: 401, headers: { \"cache-control\": \"no-store\" } });\n }\n return new Response(JSON.stringify(session), { status: 200, headers: PRIVATE });\n }\n\n case \"refresh\": {\n // `POST` only, and it is the one route here that constrains its verb. The others are\n // reached by navigation, where the verb is the browser's to choose; this one spends a\n // refresh token, and a `GET` that spends something is a link, a prefetch and a preview\n // pane away from spending it. The 405 says so rather than answering 404 for a route that\n // is plainly there.\n if (request.method !== \"POST\") {\n return new Response(null, { status: 405, headers: { allow: \"POST\" } });\n }\n try {\n const renewed = await auth.refresh(cookie);\n const headers = new Headers(PRIVATE);\n for (const value of renewed.cookies) headers.append(\"set-cookie\", value);\n return new Response(JSON.stringify(renewed.session), { status: 200, headers });\n } catch (error) {\n // A refused refresh is the end of the session, and `failure` already answers 401 for\n // `session.absent`. `token.exchange-failed` is a 400 and means the same thing to the\n // browser: there is nothing left to renew, go and sign in.\n return failure(error);\n }\n }\n\n default:\n return new Response(null, { status: 404 });\n }\n };\n\n // One handler behind both verbs. Every route here is reached by navigation or by `fetch`, and\n // which verb a product uses for sign-out — a link or a form — is its choice, not ours to\n // constrain with a second table that could drift from this one. `refresh` is the exception and\n // checks its own method, because it is the only one that spends something.\n return { GET: handle, POST: handle };\n}\n"],"names":["PRIVATE","split","pathname","cut","sameOrigin","candidate","origin","target","redirect","url","cookies","headers","cookie","failure","error","AuthError","authRoutes","config","current","authFor","redirectUri","relyingParty","handle","request","action","base","auth","isSameSite","started","done","returnTo","ended","session","renewed","value"],"mappings":";;;AA4CA,MAAMA,IAAU,EAAE,gBAAgB,oBAAoB,iBAAiB,WAAA;AAqBvE,SAASC,EAAMC,GAAsE;AACnF,QAAMC,IAAMD,EAAS,YAAY,GAAG;AACpC,SAAO,EAAE,QAAQA,EAAS,MAAMC,IAAM,CAAC,GAAG,MAAMA,KAAO,IAAI,KAAKD,EAAS,MAAM,GAAGC,CAAG,EAAA;AACvF;AAYA,SAASC,EAAWC,GAA0BC,GAAoC;AAChF,MAAI,EAAAD,MAAc,QAAQA,EAAU,WAAW;AAC/C,QAAI;AACF,YAAME,IAAS,IAAI,IAAIF,GAAWC,CAAM;AACxC,aAAOC,EAAO,WAAWD,IAASC,EAAO,OAAO;AAAA,IAClD,QAAQ;AACN;AAAA,IACF;AACF;AAUA,SAASC,EAASC,GAAaC,GAAsC;AACnE,QAAMC,IAAU,IAAI,QAAQ,EAAE,UAAUF,GAAK,iBAAiB,YAAY;AAC1E,aAAWG,KAAUF,EAAS,CAAAC,EAAQ,OAAO,cAAcC,CAAM;AACjE,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAAD,GAAS;AACpD;AAQA,SAASE,EAAQC,GAA0B;AACzC,MAAI,EAAEA,aAAiBC,GAAY,OAAMD;AACzC,SAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAOA,EAAM,MAAM,SAASA,EAAM,QAAA,CAAS,GAAG;AAAA,IACjF,QAAQA,EAAM,SAAS,mBAAmB,MAAM;AAAA,IAChD,SAASd;AAAA,EAAA,CACV;AACH;AAEO,SAASgB,EAAWC,GAA6C;AAKtE,MAAIC;AACJ,QAAMC,IAAU,CAACC,QACXF,KAAA,gBAAAA,EAAS,iBAAgBE,MAC3BF,IAAU,EAAE,aAAAE,GAAa,MAAMC,EAAa,EAAE,GAAGJ,GAAQ,aAAAG,EAAA,CAAa,EAAA,IAEjEF,EAAQ,OAGXI,IAAS,OAAOC,MAAwC;AAC5D,UAAMd,IAAM,IAAI,IAAIc,EAAQ,GAAG,GACzB,EAAE,QAAAC,GAAQ,MAAAC,EAAA,IAASxB,EAAMQ,EAAI,QAAQ,GACrCG,IAASW,EAAQ,QAAQ,IAAI,QAAQ,GACrCG,IAAOP,EAAQF,EAAO,eAAe,GAAGR,EAAI,MAAM,GAAGgB,CAAI,WAAW;AAE1E,QAAID,MAAW,cAAcA,MAAW,YAAY,CAACG,EAAWJ,CAAO;AACrE,aAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,iBAAiB,WAAA,GAAc;AAGrF,YAAQC,GAAA;AAAA,MACN,KAAK;AACH,YAAI;AACF,gBAAMI,IAAU,MAAMF,EAAK,MAAM;AAAA,YAC/B,UAAUtB,EAAWK,EAAI,aAAa,IAAI,UAAU,GAAGA,EAAI,MAAM,KAAK;AAAA,YACtE,cAAcA,EAAI,aAAa,IAAI,cAAc,KAAK;AAAA,UAAA,CACvD;AACD,iBAAOD,EAASoB,EAAQ,KAAKA,EAAQ,OAAO;AAAA,QAC9C,SAASd,GAAO;AAId,iBAAOD,EAAQC,CAAK;AAAA,QACtB;AAAA,MAGF,KAAK;AACH,YAAI;AACF,gBAAMe,IAAO,MAAMH,EAAK,SAAS,EAAE,KAAAjB,GAAK,QAAAG,GAAQ;AAChD,iBAAOJ,EAASqB,EAAK,UAAUA,EAAK,OAAO;AAAA,QAC7C,SAASf,GAAO;AACd,iBAAOD,EAAQC,CAAK;AAAA,QACtB;AAAA,MAGF,KAAK,WAAW;AACd,cAAMgB,IAAW1B,EAAWK,EAAI,aAAa,IAAI,UAAU,GAAGA,EAAI,MAAM,GAClEsB,IAAQ,MAAML,EAAK,IAAId,GAAQ,EAAE,UAAAkB,GAAU;AACjD,eAAOtB,EAASuB,EAAM,KAAKA,EAAM,OAAO;AAAA,MAC1C;AAAA,MAEA,KAAK,WAAW;AACd,cAAMC,IAAU,MAAMN,EAAK,KAAKd,CAAM;AAItC,eAAIoB,MAAY,OACP,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,iBAAiB,WAAA,GAAc,IAE9E,IAAI,SAAS,KAAK,UAAUA,CAAO,GAAG,EAAE,QAAQ,KAAK,SAAShC,GAAS;AAAA,MAChF;AAAA,MAEA,KAAK,WAAW;AAMd,YAAIuB,EAAQ,WAAW;AACrB,iBAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,OAAO,OAAA,GAAU;AAEvE,YAAI;AACF,gBAAMU,IAAU,MAAMP,EAAK,QAAQd,CAAM,GACnCD,IAAU,IAAI,QAAQX,CAAO;AACnC,qBAAWkC,KAASD,EAAQ,QAAS,CAAAtB,EAAQ,OAAO,cAAcuB,CAAK;AACvE,iBAAO,IAAI,SAAS,KAAK,UAAUD,EAAQ,OAAO,GAAG,EAAE,QAAQ,KAAK,SAAAtB,GAAS;AAAA,QAC/E,SAASG,GAAO;AAId,iBAAOD,EAAQC,CAAK;AAAA,QACtB;AAAA,MACF;AAAA,MAEA;AACE,eAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK;AAAA,IAAA;AAAA,EAE/C;AAMA,SAAO,EAAE,KAAKQ,GAAQ,MAAMA,EAAA;AAC9B;"}
1
+ {"version":3,"file":"next-routes.js","sources":["../src/next-routes.ts"],"sourcesContent":["import { isSameSite } from \"./same-site\";\nimport type { AuthSessionConfig } from \"./next-session\";\nimport { relyingParty, type RelyingParty } from \"./server\";\nimport { AuthError, type AuthErrorCode } from \"./types\";\n\n/**\n * The five routes a Backend For Frontend needs, as one App Router catch-all.\n *\n * ```ts\n * // app/api/auth/[...auth]/route.ts\n * export const { GET, POST } = authRoutes({ issuer, clientId, clientSecret, secret });\n * ```\n *\n * `relyingParty` already does the whole flow in strings — a URL and a `Cookie` header in, a URL and\n * `Set-Cookie` values out — so the only thing written here is the translation into `Request` and\n * `Response`, plus the two decisions that translation forces: what a route answers when there is\n * no session, and where it is willing to send a browser afterwards.\n *\n * **Nothing in this module imports `next`.** An App Router route handler is handed a standard\n * `Request` and may answer with a standard `Response`, so the framework's own types would buy\n * nothing and would make this half untestable without it. The door earns its subpath through\n * `next-session.ts` and `next-middleware.ts`, which genuinely cannot be written without `next`.\n *\n * ## The other end of the contract\n *\n * `bffAuth` in the root barrel is the browser half, and it is specific: it `GET`s\n * `${basePath}/session` and reads **401 as \"nobody is signed in\"**, not as a failure; it `POST`s\n * `${basePath}/refresh` when a request of its own comes back 401, and retries that request once if\n * the renewal worked; it navigates to `${basePath}/signin?returnTo=…&organization=…` and\n * `${basePath}/signout?returnTo=…`. Those five paths and those status codes are the contract, and\n * `next-routes.test.ts` drives a real `bffAuth` against these handlers rather than trusting the\n * two descriptions to agree.\n *\n * ## A failure on a navigation is a page, not a body\n *\n * `signin`, `callback` and `signout` are reached by the browser's address bar, so an `AuthError`\n * there answers with a redirect to the product's {@link AuthRoutesConfig.problemPage}, the code in\n * `?code=`, where the product renders it in its own words. `session` and `refresh` are reached by\n * `fetch`, and answer with a status and `{ error, message }`.\n *\n * ## Which routes a cross-site request may reach\n *\n * `callback` and `signin` must be reachable from anywhere — one *is* a navigation from the\n * identity provider, and the other is a link somebody is allowed to put on another page. The other\n * three are not: `signout` reached cross-site is a logout anyone can cause, `refresh` is a\n * rotation anyone can cause, and `session` is a person's identity read from a page that is not\n * ours. `same-site.ts` carries the check and the reason the package rather than the product owes\n * it.\n */\n\n/** The session endpoint answers about a person; no cache may ever hold that answer. */\nconst PRIVATE = { \"content-type\": \"application/json\", \"cache-control\": \"no-store\" } as const;\n\nexport interface AuthRoutesConfig extends AuthSessionConfig {\n /**\n * Override the callback URL. Absent, it is derived from the incoming request: the origin it\n * arrived at, the path this route file sits on, and `/callback`.\n *\n * Deriving it trusts the `Host` header, which is chosen by whoever made the request. That is a\n * bounded trust — a forged host produces a `redirect_uri` Keycloak has not registered, and\n * Keycloak refuses it — but a deployment behind a proxy that rewrites the host should say the\n * URL out loud here rather than discover this.\n */\n readonly redirectUri?: string;\n /**\n * The product's page that renders a failed sign-in, sign-in callback or sign-out, reached as\n * `?code=<AuthErrorCode>`. Default `/auth/problem`. `authMiddleware` keeps the same default\n * public: whoever lands here has, by definition, no session.\n */\n readonly problemPage?: string;\n}\n\n/** Where a failed navigation is sent; `authMiddleware` holds the same default. */\nconst PROBLEM_PAGE = \"/auth/problem\";\n\nexport interface AuthRouteHandlers {\n GET(request: Request): Promise<Response>;\n POST(request: Request): Promise<Response>;\n}\n\n/** The last path segment, and everything before it. `/api/auth/signin` → `signin`, `/api/auth`. */\nfunction split(pathname: string): { readonly action: string; readonly base: string } {\n const cut = pathname.lastIndexOf(\"/\");\n return { action: pathname.slice(cut + 1), base: cut <= 0 ? \"\" : pathname.slice(0, cut) };\n}\n\n/**\n * A `returnTo` confined to this origin, or `undefined`.\n *\n * Without this the sign-in route is an open redirect: `?returnTo=https://evil.test` is carried\n * through the whole flow and spent on a browser that has just proved who it is, which is the most\n * valuable moment to hijack. The check is here, where the string arrives from a query parameter,\n * and deliberately *not* repeated on the callback — what comes back out at the callback was\n * sealed into a cookie by us, so re-checking it would only make it unclear which check is the\n * real one.\n */\nfunction sameOrigin(candidate: string | null, origin: string): string | undefined {\n if (candidate === null || candidate.length === 0) return undefined;\n try {\n const target = new URL(candidate, origin);\n return target.origin === origin ? target.href : undefined;\n } catch {\n return undefined; // not a URL at all: dropped like a foreign one, and the sign-in goes to `/`\n }\n}\n\n/**\n * A redirect that can carry cookies.\n *\n * `Response.redirect()` cannot: its headers are immutable, and a callback that cannot set a cookie\n * is a sign-in that never completes. And `append`, never `set` — the callback answers with **two**\n * `Set-Cookie` values, the session it just issued and the transaction it just spent, and `set`\n * would silently keep only the last of them.\n */\nfunction redirect(url: string, cookies: readonly string[]): Response {\n const headers = new Headers({ location: url, \"cache-control\": \"no-store\" });\n for (const cookie of cookies) headers.append(\"set-cookie\", cookie);\n return new Response(null, { status: 302, headers });\n}\n\n/** The status for a party that did not answer, or `undefined` for a refusal. */\nexport function outage(code: AuthErrorCode): number | undefined {\n if (code === \"idp/silent\" || code === \"session/silent\") return 504;\n if (code === \"idp/unreachable\") return 502;\n if (code === \"session/unavailable\") return 503;\n return undefined;\n}\n\n/**\n * An `AuthError` as a status and a code a product can route on.\n *\n * Anything else is rethrown: a failure this module has no reading of is Next's to report, and\n * flattening it into a 400 here would hide a misconfiguration behind a message about credentials.\n */\nfunction failure(error: unknown): Response {\n if (!(error instanceof AuthError)) throw error;\n return new Response(JSON.stringify({ error: error.code, message: error.message }), {\n status: outage(error.code) ?? (error.code === \"session/absent\" ? 401 : 400),\n headers: PRIVATE,\n });\n}\n\nexport function authRoutes(config: AuthRoutesConfig): AuthRouteHandlers {\n // One instance, rebuilt only when the callback URL it was built for changes. A `Map` keyed by\n // origin would grow without bound on a stream of forged `Host` headers; a single slot cannot,\n // and it self-heals, because the next genuine request derives its own URL again. What it costs\n // in that case is a re-discovery, which is the right price for a request that lied.\n let current: { readonly redirectUri: string; readonly auth: RelyingParty } | undefined;\n const authFor = (redirectUri: string): RelyingParty => {\n if (current?.redirectUri !== redirectUri) {\n current = { redirectUri, auth: relyingParty({ ...config, redirectUri }) };\n }\n return current.auth;\n };\n\n /** An `AuthError` on a navigation, as the product's page; anything else rethrown, as in `failure`. */\n const problem = (error: unknown, origin: string): Response => {\n if (!(error instanceof AuthError)) throw error;\n const page = new URL(config.problemPage ?? PROBLEM_PAGE, origin);\n page.searchParams.set(\"code\", error.code);\n return redirect(page.href, []);\n };\n\n const handle = async (request: Request): Promise<Response> => {\n const url = new URL(request.url);\n const { action, base } = split(url.pathname);\n const cookie = request.headers.get(\"cookie\");\n const auth = authFor(config.redirectUri ?? `${url.origin}${base}/callback`);\n\n if (action !== \"callback\" && action !== \"signin\" && !isSameSite(request)) {\n return new Response(null, { status: 403, headers: { \"cache-control\": \"no-store\" } });\n }\n\n switch (action) {\n case \"signin\": {\n try {\n const started = await auth.begin({\n returnTo: sameOrigin(url.searchParams.get(\"returnTo\"), url.origin) ?? \"/\",\n organization: url.searchParams.get(\"organization\") ?? undefined,\n });\n return redirect(started.url, started.cookies);\n } catch (error) {\n // `organization` arrives from a query parameter and `begin` refuses one that is not an\n // alias, because a space in it injects scopes; and the IdP may be down. Without this\n // `catch` either is a 500 on a link.\n return problem(error, url.origin);\n }\n }\n\n case \"callback\": {\n try {\n const done = await auth.complete({ url, cookie });\n return redirect(done.returnTo, done.cookies);\n } catch (error) {\n return problem(error, url.origin);\n }\n }\n\n case \"signout\": {\n const returnTo = sameOrigin(url.searchParams.get(\"returnTo\"), url.origin);\n try {\n const ended = await auth.end(cookie, { returnTo });\n return redirect(ended.url, ended.cookies);\n } catch (error) {\n return problem(error, url.origin);\n }\n }\n\n case \"session\": {\n let session: Awaited<ReturnType<RelyingParty[\"read\"]>>;\n try {\n session = await auth.read(cookie);\n } catch (error) {\n return failure(error);\n }\n // 401 and no body at all. `bffAuth` reads this status as \"nobody is signed in\" and stops;\n // a body would be parsed by something eventually, and an error shape arriving where a\n // `Session` is expected is the failure `readSession` exists to refuse.\n if (session === null) {\n return new Response(null, { status: 401, headers: { \"cache-control\": \"no-store\" } });\n }\n return new Response(JSON.stringify(session), { status: 200, headers: PRIVATE });\n }\n\n case \"refresh\": {\n // `POST` only, and it is the one route here that constrains its verb. The others are\n // reached by navigation, where the verb is the browser's to choose; this one spends a\n // refresh token, and a `GET` that spends something is a link, a prefetch and a preview\n // pane away from spending it. The 405 says so rather than answering 404 for a route that\n // is plainly there.\n if (request.method !== \"POST\") {\n return new Response(null, { status: 405, headers: { allow: \"POST\" } });\n }\n try {\n const renewed = await auth.refresh(cookie);\n const headers = new Headers(PRIVATE);\n for (const value of renewed.cookies) headers.append(\"set-cookie\", value);\n return new Response(JSON.stringify(renewed.session), { status: 200, headers });\n } catch (error) {\n // A refused refresh is the end of the session, and `failure` already answers 401 for\n // `session/absent`. `token/exchange-failed` is a 400 and means the same thing to the\n // browser: there is nothing left to renew, go and sign in. An outage is a 5xx, which\n // `bffAuth` does not read as an ended session.\n return failure(error);\n }\n }\n\n default:\n return new Response(null, { status: 404 });\n }\n };\n\n // One handler behind both verbs. Every route here is reached by navigation or by `fetch`, and\n // which verb a product uses for sign-out — a link or a form — is its choice, not ours to\n // constrain with a second table that could drift from this one. `refresh` is the exception and\n // checks its own method, because it is the only one that spends something.\n return { GET: handle, POST: handle };\n}\n"],"names":["PRIVATE","PROBLEM_PAGE","split","pathname","cut","sameOrigin","candidate","origin","target","redirect","url","cookies","headers","cookie","outage","code","failure","error","AuthError","authRoutes","config","current","authFor","redirectUri","relyingParty","problem","page","handle","request","action","base","auth","isSameSite","started","done","returnTo","ended","session","renewed","value"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -1,182 +1,224 @@
1
- import { buildEndSessionUrl as p, randomPKCECodeVerifier as E, randomState as x, randomNonce as I, calculatePKCECodeChallenge as b, buildAuthorizationUrl as z, refreshTokenGrant as N, authorizationCodeGrant as O } from "openid-client";
2
- import { claims as C } from "./claims.js";
3
- import { sealedCookie as g } from "./cookie-session.js";
4
- import { cookieValue as B } from "./cookie-session.js";
5
- import { issuer as S } from "./issuer.js";
6
- import { rewriteOrigin as q } from "./issuer.js";
7
- import { keyedSingleFlight as D } from "./single-flight.js";
8
- import { statelessStore as U } from "./store.js";
9
- import { ticketStore as te } from "./store.js";
10
- import { AuthError as v } from "./types.js";
11
- const A = "openid profile email", L = 480 * 60, R = 600, F = 60;
12
- function l(n, c, i) {
13
- const d = new v(n, c);
14
- throw i !== void 0 && (d.cause = i), d;
1
+ import { buildEndSessionUrl as O, randomPKCECodeVerifier as x, randomState as N, randomNonce as S, calculatePKCECodeChallenge as v, buildAuthorizationUrl as z, refreshTokenGrant as C, authorizationCodeGrant as D } from "openid-client";
2
+ import { claims as U } from "./claims.js";
3
+ import { sealedCookie as E } from "./cookie-session.js";
4
+ import { cookieValue as ae } from "./cookie-session.js";
5
+ import { DEADLINE as L } from "./deadline.js";
6
+ import { issuer as R } from "./issuer.js";
7
+ import { rewriteOrigin as se } from "./issuer.js";
8
+ import { keyedSingleFlight as P } from "./single-flight.js";
9
+ import { statelessStore as F } from "./store.js";
10
+ import { ticketStore as ce } from "./store.js";
11
+ import { AuthError as m } from "./types.js";
12
+ const I = "openid profile email", M = 480 * 60, G = 600, H = 60;
13
+ function h(n, s, a) {
14
+ throw new m(n, s, {}, a === void 0 ? void 0 : { cause: a });
15
15
  }
16
16
  function y(n) {
17
17
  if (typeof n != "object" || n === null || !("code" in n)) return;
18
- const c = n.code;
19
- return typeof c == "string" ? c : void 0;
18
+ const s = n.code;
19
+ return typeof s == "string" ? s : void 0;
20
20
  }
21
- function _(n) {
22
- const c = y(n);
23
- if (c === "OAUTH_JWT_CLAIM_COMPARISON_FAILED") {
24
- let i = n;
25
- for (let d = 0; d < 4 && typeof i == "object" && i !== null; d++) {
26
- if (i.claim === "nonce") return !0;
27
- i = i.cause;
21
+ function b(n) {
22
+ const s = y(n);
23
+ if (s === "OAUTH_JWT_CLAIM_COMPARISON_FAILED") {
24
+ let a = n;
25
+ for (let l = 0; l < 4 && typeof a == "object" && a !== null; l++) {
26
+ if (a.claim === "nonce") return !0;
27
+ a = a.cause;
28
28
  }
29
29
  return !1;
30
30
  }
31
- return c === "OAUTH_INVALID_RESPONSE" && n instanceof Error && n.cause instanceof Error && n.cause.message.includes('"nonce"');
31
+ return s === "OAUTH_INVALID_RESPONSE" && n instanceof Error && n.cause instanceof Error && n.cause.message.includes('"nonce"');
32
32
  }
33
- function P(n) {
33
+ function Z(n) {
34
34
  return y(n) === "OAUTH_KEY_SELECTION_FAILED";
35
35
  }
36
- const G = /^(\*|[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?)$/, M = D();
37
- function X(n) {
38
- const c = S(n), i = n.store ?? U(), d = g({
36
+ function K(n) {
37
+ const s = y(n);
38
+ if (s === "OAUTH_TIMEOUT") return "idp/silent";
39
+ if (s === "OAUTH_RESPONSE_IS_NOT_CONFORM" || n instanceof TypeError && s === void 0) return "idp/unreachable";
40
+ }
41
+ const V = /^(\*|[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?)$/, W = P();
42
+ function $(n) {
43
+ const s = (a) => h("session/unavailable", "the session store did not answer", a);
44
+ return {
45
+ async put(a) {
46
+ try {
47
+ return await n.put(a);
48
+ } catch (l) {
49
+ return s(l);
50
+ }
51
+ },
52
+ async get(a) {
53
+ try {
54
+ return await n.get(a);
55
+ } catch (l) {
56
+ return s(l);
57
+ }
58
+ },
59
+ async drop(a) {
60
+ try {
61
+ await n.drop(a);
62
+ } catch (l) {
63
+ s(l);
64
+ }
65
+ }
66
+ };
67
+ }
68
+ function te(n) {
69
+ const s = R(n), a = $(n.store ?? F()), l = L, k = (t, e, o) => {
70
+ const r = K(t) ?? e;
71
+ return new m(r, o, r === "idp/silent" ? { after: l } : {}, { cause: t });
72
+ }, w = async () => {
73
+ try {
74
+ return await s.configuration();
75
+ } catch (t) {
76
+ throw k(t, "idp/unreachable", "the IdP's discovery document could not be read");
77
+ }
78
+ }, f = E({
39
79
  name: "kanzo-session",
40
80
  secret: n.secret,
41
- maxAge: n.maxAge ?? L
42
- }), k = g({
81
+ maxAge: n.maxAge ?? M
82
+ }), T = E({
43
83
  name: "kanzo-auth",
44
84
  secret: n.secret,
45
- maxAge: R
46
- }), m = async (t) => {
47
- const e = await d.read(t);
48
- return e === null ? null : i.get(e.ticket);
49
- }, w = async (t, e) => {
50
- const o = t.claims(), a = o === void 0 ? e == null ? void 0 : e.session : C(o, n);
51
- a === void 0 && l("token.exchange-failed", "the token response carried no ID token, so it names nobody");
52
- const s = t.expiresIn(), r = {
53
- session: a,
85
+ maxAge: G
86
+ }), A = async (t) => {
87
+ const e = await f.read(t);
88
+ return e === null ? null : a.get(e.ticket);
89
+ }, _ = async (t, e) => {
90
+ const o = t.claims(), r = o === void 0 ? e == null ? void 0 : e.session : U(o, n);
91
+ r === void 0 && h("token/exchange-failed", "the token response carried no ID token, so it names nobody");
92
+ const i = t.expiresIn(), c = {
93
+ session: r,
54
94
  accessToken: t.access_token,
55
- accessTokenExpiresAt: s === void 0 ? void 0 : Date.now() + s * 1e3,
95
+ accessTokenExpiresAt: i === void 0 ? void 0 : Date.now() + i * 1e3,
56
96
  // RFC 10017 requires rotation, so the newly issued token is the only one still valid. An
57
97
  // authorization server that did not rotate returns none, and the one we hold stays good.
58
98
  refreshToken: t.refresh_token ?? (e == null ? void 0 : e.refreshToken),
59
99
  idToken: t.id_token ?? (e == null ? void 0 : e.idToken)
60
- }, f = await i.put(r);
61
- return { renewed: { session: a, cookies: [await d.seal({ ticket: f })] }, record: r };
62
- }, T = async (t) => {
63
- const e = await d.read(t), o = e === null ? null : await i.get(e.ticket);
64
- (e === null || o === null) && l("session.absent", "there is no session cookie to refresh");
65
- const a = o.refreshToken;
66
- return a === void 0 && l("session.absent", "the session holds no refresh token, so it cannot be renewed"), M(e.ticket, async () => {
67
- let s;
100
+ }, u = await a.put(c);
101
+ return { renewed: { session: r, cookies: [await f.seal({ ticket: u })] }, record: c };
102
+ }, g = async (t) => {
103
+ const e = await f.read(t), o = e === null ? null : await a.get(e.ticket);
104
+ (e === null || o === null) && h("session/absent", "there is no session cookie to refresh");
105
+ const r = o.refreshToken;
106
+ return r === void 0 && h("session/absent", "the session holds no refresh token, so it cannot be renewed"), W(e.ticket, async () => {
107
+ let i;
68
108
  try {
69
- s = await N(await c.configuration(), a);
70
- } catch (r) {
71
- l("token.exchange-failed", "the refresh token was refused", r);
109
+ i = await C(await w(), r);
110
+ } catch (c) {
111
+ throw c instanceof m ? c : k(c, "token/exchange-failed", "the refresh token was refused");
72
112
  }
73
- return await i.drop(e.ticket), w(s, o);
113
+ return await a.drop(e.ticket), _(i, o);
74
114
  });
75
115
  };
76
116
  return {
77
117
  async begin(t = {}) {
78
- const e = await c.configuration(), o = E(), a = x(), s = I();
79
- t.organization !== void 0 && !G.test(t.organization) && l(
80
- "organization.invalid",
118
+ const e = await w(), o = x(), r = N(), i = S();
119
+ t.organization !== void 0 && !V.test(t.organization) && h(
120
+ "organization/invalid",
81
121
  `\`${t.organization}\` is not an organization alias, and a scope is a space-delimited list: see ORGANIZATION`
82
122
  );
83
- const r = {
123
+ const c = {
84
124
  redirect_uri: n.redirectUri,
85
125
  // `organization:<alias>` asks Keycloak for one; a product with many asks for
86
126
  // `organization:*` through `scope`, because plain `organization` prompts for a choice.
87
- scope: t.organization === void 0 ? n.scope ?? A : `${n.scope ?? A} organization:${t.organization}`,
88
- code_challenge: await b(o),
127
+ scope: t.organization === void 0 ? n.scope ?? I : `${n.scope ?? I} organization:${t.organization}`,
128
+ code_challenge: await v(o),
89
129
  code_challenge_method: "S256",
90
- state: a,
91
- nonce: s
130
+ state: r,
131
+ nonce: i
92
132
  };
93
133
  return {
94
- url: z(e, r).href,
134
+ url: z(e, c).href,
95
135
  cookies: [
96
- await k.seal({ state: a, nonce: s, verifier: o, returnTo: t.returnTo ?? "/" })
136
+ await T.seal({ state: r, nonce: i, verifier: o, returnTo: t.returnTo ?? "/" })
97
137
  ]
98
138
  };
99
139
  },
100
140
  async complete(t) {
101
- const e = await k.read(t.cookie);
102
- e === null && l(
103
- "callback.state-mismatch",
141
+ const e = await T.read(t.cookie);
142
+ e === null && h(
143
+ "callback/state-mismatch",
104
144
  "the callback arrived with no transaction cookie, so there is nothing to match its `state` against"
105
145
  );
106
146
  const o = new URL(t.url);
107
- o.searchParams.get("state") !== e.state && l(
108
- "callback.state-mismatch",
147
+ o.searchParams.get("state") !== e.state && h(
148
+ "callback/state-mismatch",
109
149
  "the callback's `state` is not the one this browser was sent with"
110
150
  );
111
- const a = {
151
+ const r = {
112
152
  pkceCodeVerifier: e.verifier,
113
153
  expectedState: e.state,
114
154
  expectedNonce: e.nonce
115
- }, s = (h) => O(h, o, a);
116
- let r;
155
+ }, i = (d) => D(d, o, r);
156
+ let c;
117
157
  try {
118
- r = await s(await c.configuration());
119
- } catch (h) {
120
- _(h) && l(
121
- "callback.nonce-mismatch",
158
+ c = await i(await w());
159
+ } catch (d) {
160
+ if (d instanceof m) throw d;
161
+ if (b(d) && h(
162
+ "callback/nonce-mismatch",
122
163
  "the ID token's `nonce` is not the one this transaction sent",
123
- h
124
- ), P(h) || l("token.exchange-failed", "the authorization code could not be exchanged", h);
164
+ d
165
+ ), !Z(d))
166
+ throw k(d, "token/exchange-failed", "the authorization code could not be exchanged");
125
167
  try {
126
- r = await s(await c.rediscover());
127
- } catch (u) {
128
- _(u) && l(
129
- "callback.nonce-mismatch",
168
+ c = await i(await s.rediscover());
169
+ } catch (p) {
170
+ throw b(p) && h(
171
+ "callback/nonce-mismatch",
130
172
  "the ID token's `nonce` is not the one this transaction sent",
131
- u
132
- ), l(
133
- "token.exchange-failed",
134
- "the ID token did not verify, and did not verify against freshly discovered keys either",
135
- u
173
+ p
174
+ ), k(
175
+ p,
176
+ "token/exchange-failed",
177
+ "the ID token did not verify, and did not verify against freshly discovered keys either"
136
178
  );
137
179
  }
138
180
  }
139
- const { renewed: f } = await w(r, null);
181
+ const { renewed: u } = await _(c, null);
140
182
  return {
141
- ...f,
142
- cookies: [...f.cookies, k.clear()],
183
+ ...u,
184
+ cookies: [...u.cookies, T.clear()],
143
185
  returnTo: e.returnTo
144
186
  };
145
187
  },
146
188
  async read(t) {
147
189
  var e;
148
- return ((e = await m(t)) == null ? void 0 : e.session) ?? null;
190
+ return ((e = await A(t)) == null ? void 0 : e.session) ?? null;
149
191
  },
150
192
  async token(t, e = {}) {
151
- const o = await m(t);
193
+ const o = await A(t);
152
194
  if (o === null) return null;
153
- const a = (e.renewWithin ?? F) * 1e3, s = o.accessToken, r = o.accessTokenExpiresAt !== void 0 && o.accessTokenExpiresAt - Date.now() <= a;
154
- if (s !== void 0 && !r)
155
- return { accessToken: s, session: o.session, cookies: [] };
156
- const { renewed: f, record: h } = await T(t);
157
- return h.accessToken === void 0 && l("token.exchange-failed", "the token response carried no access token"), { accessToken: h.accessToken, session: f.session, cookies: f.cookies };
195
+ const r = (e.renewWithin ?? H) * 1e3, i = o.accessToken, c = o.accessTokenExpiresAt !== void 0 && o.accessTokenExpiresAt - Date.now() <= r;
196
+ if (i !== void 0 && !c)
197
+ return { accessToken: i, session: o.session, cookies: [] };
198
+ const { renewed: u, record: d } = await g(t);
199
+ return d.accessToken === void 0 && h("token/exchange-failed", "the token response carried no access token"), { accessToken: d.accessToken, session: u.session, cookies: u.cookies };
158
200
  },
159
201
  async refresh(t) {
160
- return (await T(t)).renewed;
202
+ return (await g(t)).renewed;
161
203
  },
162
204
  async end(t, e = {}) {
163
- const o = await d.read(t), a = o === null ? null : await i.get(o.ticket);
164
- o !== null && await i.drop(o.ticket);
165
- const s = {}, r = e.returnTo ?? n.postLogoutRedirectUri;
166
- return r !== void 0 && (s.post_logout_redirect_uri = r), (a == null ? void 0 : a.idToken) !== void 0 && (s.id_token_hint = a.idToken), {
167
- url: p(await c.configuration(), s).href,
168
- cookies: [d.clear()]
205
+ const o = await f.read(t), r = o === null ? null : await a.get(o.ticket);
206
+ o !== null && await a.drop(o.ticket);
207
+ const i = {}, c = e.returnTo ?? n.postLogoutRedirectUri;
208
+ return c !== void 0 && (i.post_logout_redirect_uri = c), (r == null ? void 0 : r.idToken) !== void 0 && (i.id_token_hint = r.idToken), {
209
+ url: O(await w(), i).href,
210
+ cookies: [f.clear()]
169
211
  };
170
212
  }
171
213
  };
172
214
  }
173
215
  export {
174
- B as cookieValue,
175
- S as issuer,
176
- X as relyingParty,
177
- q as rewriteOrigin,
178
- g as sealedCookie,
179
- U as statelessStore,
180
- te as ticketStore
216
+ ae as cookieValue,
217
+ R as issuer,
218
+ te as relyingParty,
219
+ se as rewriteOrigin,
220
+ E as sealedCookie,
221
+ F as statelessStore,
222
+ ce as ticketStore
181
223
  };
182
224
  //# sourceMappingURL=server.js.map