@kanzo-tech/auth 0.4.0 → 0.6.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 (45) hide show
  1. package/dist/auth-fetch.d.ts +12 -0
  2. package/dist/auth-fetch.d.ts.map +1 -1
  3. package/dist/auth-fetch.js +2 -1
  4. package/dist/auth-fetch.js.map +1 -1
  5. package/dist/bff-auth.d.ts.map +1 -1
  6. package/dist/bff-auth.js +63 -48
  7. package/dist/bff-auth.js.map +1 -1
  8. package/dist/cookie-session.d.ts.map +1 -1
  9. package/dist/cookie-session.js +8 -8
  10. package/dist/cookie-session.js.map +1 -1
  11. package/dist/next-proxy.d.ts +65 -0
  12. package/dist/next-proxy.d.ts.map +1 -0
  13. package/dist/next-proxy.js +64 -0
  14. package/dist/next-proxy.js.map +1 -0
  15. package/dist/next-routes.d.ts.map +1 -1
  16. package/dist/next-routes.js +56 -39
  17. package/dist/next-routes.js.map +1 -1
  18. package/dist/next-token.d.ts +56 -0
  19. package/dist/next-token.d.ts.map +1 -0
  20. package/dist/next-token.js +9 -0
  21. package/dist/next-token.js.map +1 -0
  22. package/dist/next.d.ts +26 -12
  23. package/dist/next.d.ts.map +1 -1
  24. package/dist/next.js +10 -6
  25. package/dist/next.js.map +1 -1
  26. package/dist/same-site.d.ts +29 -0
  27. package/dist/same-site.d.ts.map +1 -0
  28. package/dist/same-site.js +11 -0
  29. package/dist/same-site.js.map +1 -0
  30. package/dist/server.d.ts +36 -2
  31. package/dist/server.d.ts.map +1 -1
  32. package/dist/server.js +109 -84
  33. package/dist/server.js.map +1 -1
  34. package/dist/single-flight.d.ts +18 -0
  35. package/dist/single-flight.d.ts.map +1 -1
  36. package/dist/single-flight.js +18 -6
  37. package/dist/single-flight.js.map +1 -1
  38. package/dist/store.d.ts +102 -3
  39. package/dist/store.d.ts.map +1 -1
  40. package/dist/store.js +33 -6
  41. package/dist/store.js.map +1 -1
  42. package/dist/types.d.ts +12 -3
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/package.json +4 -4
@@ -20,6 +20,18 @@ export interface TokenSource {
20
20
  current(): Promise<string | null>;
21
21
  renew(): Promise<string | null>;
22
22
  }
23
+ /**
24
+ * Can this request be sent a second time?
25
+ *
26
+ * A body that is a stream can be read once, so a retry would send an empty one — silently, with a
27
+ * misleading error at the far end. Where we cannot prove the body is replayable we do not retry: the
28
+ * 401 reaches the caller, which is honest, rather than a corrupted request reaching the server.
29
+ *
30
+ * Exported for `bff-auth.ts`, which retries for a different reason — a renewed cookie rather than
31
+ * a renewed bearer token — and must not answer the question differently. It is not on the barrel:
32
+ * it is a shared predicate between two implementations, not a thing a consumer holds.
33
+ */
34
+ export declare function isReplayable(input: RequestInfo | URL, init?: RequestInit): boolean;
23
35
  /**
24
36
  * Wrap a `fetch` so every request carries the session, and one stale token does not surface as a
25
37
  * failure the user has to see.
@@ -1 +1 @@
1
- {"version":3,"file":"auth-fetch.d.ts","sourceRoot":"","sources":["../src/auth-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAClC,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACjC;AAgBD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE,OAAO,UAAU,CAAC,KAAwB,GAC/C,OAAO,UAAU,CAAC,KAAK,CAuBzB"}
1
+ {"version":3,"file":"auth-fetch.d.ts","sourceRoot":"","sources":["../src/auth-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAClC,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACjC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAKlF;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE,OAAO,UAAU,CAAC,KAAwB,GAC/C,OAAO,UAAU,CAAC,KAAK,CAuBzB"}
@@ -15,6 +15,7 @@ function h(r, n = globalThis.fetch) {
15
15
  };
16
16
  }
17
17
  export {
18
- h as authFetch
18
+ h as authFetch,
19
+ c as isReplayable
19
20
  };
20
21
  //# sourceMappingURL=auth-fetch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-fetch.js","sources":["../src/auth-fetch.ts"],"sourcesContent":["/**\n * A `fetch` that stays authenticated.\n *\n * This is the seam a product already has. keasy's API client is\n * `createClient({ baseUrl: \"/\" })` with one middleware; the viewer's trace source takes its\n * transport as a parameter. Handing either an authenticated `fetch` changes one line and no call\n * site — which is the point, because the alternative is every call site remembering a header.\n *\n * Auth0's SPA SDK ships the same shape for the same reason: `fetchWithAuth`, *\"a drop-in replacement\n * for the Fetch API's `fetch()` method\"* that builds the headers and handles the retries.\n */\n\n/**\n * Where a token comes from, and how to ask for a fresh one.\n *\n * Both are the implementation's business to make cheap and **single-flight** — see `singleFlight`.\n * `current` is called on every request, so it must answer from cache until the token is near\n * expiry. `null` from both is legitimate: it means there is no bearer token to attach.\n */\nexport interface TokenSource {\n current(): Promise<string | null>;\n renew(): Promise<string | null>;\n}\n\n/**\n * Can this request be sent a second time?\n *\n * A body that is a stream can be read once, so a retry would send an empty one — silently, with a\n * misleading error at the far end. Where we cannot prove the body is replayable we do not retry: the\n * 401 reaches the caller, which is honest, rather than a corrupted request reaching the server.\n */\nfunction isReplayable(input: RequestInfo | URL, init?: RequestInit): boolean {\n if (typeof Request !== \"undefined\" && input instanceof Request && input.body !== null) return false;\n const body = init?.body;\n if (body === undefined || body === null) return true;\n return !(typeof ReadableStream !== \"undefined\" && body instanceof ReadableStream);\n}\n\n/**\n * Wrap a `fetch` so every request carries the session, and one stale token does not surface as a\n * failure the user has to see.\n *\n * The retry is **once**, and only on a 401 we sent a token for. Retrying a 403 would be wrong — that\n * is an answer, not a stale credential — and retrying twice turns an expired session into a loop\n * against the authorization server.\n */\nexport function authFetch(\n source: TokenSource,\n base: typeof globalThis.fetch = globalThis.fetch,\n): typeof globalThis.fetch {\n return async (input, init) => {\n const send = async (token: string | null): Promise<Response> => {\n const inherited =\n init?.headers ??\n (typeof Request !== \"undefined\" && input instanceof Request ? input.headers : undefined);\n const headers = new Headers(inherited);\n if (token !== null) headers.set(\"Authorization\", `Bearer ${token}`);\n return base(input, { ...init, headers });\n };\n\n const token = await source.current();\n const response = await send(token);\n\n // Nothing to renew against, or nothing that says the credential was the problem.\n if (response.status !== 401 || token === null) return response;\n if (!isReplayable(input, init)) return response;\n\n const renewed = await source.renew();\n if (renewed === null || renewed === token) return response;\n\n return send(renewed);\n };\n}\n"],"names":["isReplayable","input","init","body","authFetch","source","base","send","token","inherited","headers","response","renewed"],"mappings":"AA+BA,SAASA,EAAaC,GAA0BC,GAA6B;AAC3E,MAAI,OAAO,UAAY,OAAeD,aAAiB,WAAWA,EAAM,SAAS,KAAM,QAAO;AAC9F,QAAME,IAAOD,KAAA,gBAAAA,EAAM;AACnB,SAA0BC,KAAS,OAAa,KACzC,EAAE,OAAO,iBAAmB,OAAeA,aAAgB;AACpE;AAUO,SAASC,EACdC,GACAC,IAAgC,WAAW,OAClB;AACzB,SAAO,OAAOL,GAAOC,MAAS;AAC5B,UAAMK,IAAO,OAAOC,MAA4C;AAC9D,YAAMC,KACJP,KAAA,gBAAAA,EAAM,aACL,OAAO,UAAY,OAAeD,aAAiB,UAAUA,EAAM,UAAU,SAC1ES,IAAU,IAAI,QAAQD,CAAS;AACrC,aAAID,MAAU,QAAME,EAAQ,IAAI,iBAAiB,UAAUF,CAAK,EAAE,GAC3DF,EAAKL,GAAO,EAAE,GAAGC,GAAM,SAAAQ,GAAS;AAAA,IACzC,GAEMF,IAAQ,MAAMH,EAAO,QAAA,GACrBM,IAAW,MAAMJ,EAAKC,CAAK;AAIjC,QADIG,EAAS,WAAW,OAAOH,MAAU,QACrC,CAACR,EAAaC,GAAOC,CAAI,EAAG,QAAOS;AAEvC,UAAMC,IAAU,MAAMP,EAAO,MAAA;AAC7B,WAAIO,MAAY,QAAQA,MAAYJ,IAAcG,IAE3CJ,EAAKK,CAAO;AAAA,EACrB;AACF;"}
1
+ {"version":3,"file":"auth-fetch.js","sources":["../src/auth-fetch.ts"],"sourcesContent":["/**\n * A `fetch` that stays authenticated.\n *\n * This is the seam a product already has. keasy's API client is\n * `createClient({ baseUrl: \"/\" })` with one middleware; the viewer's trace source takes its\n * transport as a parameter. Handing either an authenticated `fetch` changes one line and no call\n * site — which is the point, because the alternative is every call site remembering a header.\n *\n * Auth0's SPA SDK ships the same shape for the same reason: `fetchWithAuth`, *\"a drop-in replacement\n * for the Fetch API's `fetch()` method\"* that builds the headers and handles the retries.\n */\n\n/**\n * Where a token comes from, and how to ask for a fresh one.\n *\n * Both are the implementation's business to make cheap and **single-flight** — see `singleFlight`.\n * `current` is called on every request, so it must answer from cache until the token is near\n * expiry. `null` from both is legitimate: it means there is no bearer token to attach.\n */\nexport interface TokenSource {\n current(): Promise<string | null>;\n renew(): Promise<string | null>;\n}\n\n/**\n * Can this request be sent a second time?\n *\n * A body that is a stream can be read once, so a retry would send an empty one — silently, with a\n * misleading error at the far end. Where we cannot prove the body is replayable we do not retry: the\n * 401 reaches the caller, which is honest, rather than a corrupted request reaching the server.\n *\n * Exported for `bff-auth.ts`, which retries for a different reason — a renewed cookie rather than\n * a renewed bearer token — and must not answer the question differently. It is not on the barrel:\n * it is a shared predicate between two implementations, not a thing a consumer holds.\n */\nexport function isReplayable(input: RequestInfo | URL, init?: RequestInit): boolean {\n if (typeof Request !== \"undefined\" && input instanceof Request && input.body !== null) return false;\n const body = init?.body;\n if (body === undefined || body === null) return true;\n return !(typeof ReadableStream !== \"undefined\" && body instanceof ReadableStream);\n}\n\n/**\n * Wrap a `fetch` so every request carries the session, and one stale token does not surface as a\n * failure the user has to see.\n *\n * The retry is **once**, and only on a 401 we sent a token for. Retrying a 403 would be wrong — that\n * is an answer, not a stale credential — and retrying twice turns an expired session into a loop\n * against the authorization server.\n */\nexport function authFetch(\n source: TokenSource,\n base: typeof globalThis.fetch = globalThis.fetch,\n): typeof globalThis.fetch {\n return async (input, init) => {\n const send = async (token: string | null): Promise<Response> => {\n const inherited =\n init?.headers ??\n (typeof Request !== \"undefined\" && input instanceof Request ? input.headers : undefined);\n const headers = new Headers(inherited);\n if (token !== null) headers.set(\"Authorization\", `Bearer ${token}`);\n return base(input, { ...init, headers });\n };\n\n const token = await source.current();\n const response = await send(token);\n\n // Nothing to renew against, or nothing that says the credential was the problem.\n if (response.status !== 401 || token === null) return response;\n if (!isReplayable(input, init)) return response;\n\n const renewed = await source.renew();\n if (renewed === null || renewed === token) return response;\n\n return send(renewed);\n };\n}\n"],"names":["isReplayable","input","init","body","authFetch","source","base","send","token","inherited","headers","response","renewed"],"mappings":"AAmCO,SAASA,EAAaC,GAA0BC,GAA6B;AAClF,MAAI,OAAO,UAAY,OAAeD,aAAiB,WAAWA,EAAM,SAAS,KAAM,QAAO;AAC9F,QAAME,IAAOD,KAAA,gBAAAA,EAAM;AACnB,SAA0BC,KAAS,OAAa,KACzC,EAAE,OAAO,iBAAmB,OAAeA,aAAgB;AACpE;AAUO,SAASC,EACdC,GACAC,IAAgC,WAAW,OAClB;AACzB,SAAO,OAAOL,GAAOC,MAAS;AAC5B,UAAMK,IAAO,OAAOC,MAA4C;AAC9D,YAAMC,KACJP,KAAA,gBAAAA,EAAM,aACL,OAAO,UAAY,OAAeD,aAAiB,UAAUA,EAAM,UAAU,SAC1ES,IAAU,IAAI,QAAQD,CAAS;AACrC,aAAID,MAAU,QAAME,EAAQ,IAAI,iBAAiB,UAAUF,CAAK,EAAE,GAC3DF,EAAKL,GAAO,EAAE,GAAGC,GAAM,SAAAQ,GAAS;AAAA,IACzC,GAEMF,IAAQ,MAAMH,EAAO,QAAA,GACrBM,IAAW,MAAMJ,EAAKC,CAAK;AAIjC,QADIG,EAAS,WAAW,OAAOH,MAAU,QACrC,CAACR,EAAaC,GAAOC,CAAI,EAAG,QAAOS;AAEvC,UAAMC,IAAU,MAAMP,EAAO,MAAA;AAC7B,WAAIO,MAAY,QAAQA,MAAYJ,IAAcG,IAE3CJ,EAAKK,CAAO;AAAA,EACrB;AACF;"}
@@ -1 +1 @@
1
- {"version":3,"file":"bff-auth.d.ts","sourceRoot":"","sources":["../src/bff-auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAgB,OAAO,EAAiB,MAAM,SAAS,CAAC;AAE1E;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAgBD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAyB1D;AAED,wBAAgB,OAAO,CAAC,MAAM,GAAE,aAAkB,GAAG,IAAI,CAwExD"}
1
+ {"version":3,"file":"bff-auth.d.ts","sourceRoot":"","sources":["../src/bff-auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAgB,OAAO,EAAiB,MAAM,SAAS,CAAC;AAE1E;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAgBD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAyB1D;AAED,wBAAgB,OAAO,CAAC,MAAM,GAAE,aAAkB,GAAG,IAAI,CA2GxD"}
package/dist/bff-auth.js CHANGED
@@ -1,70 +1,85 @@
1
- import { singleFlight as p } from "./single-flight.js";
2
- function y(e) {
3
- if (typeof e != "object" || e === null) return null;
4
- const t = e, s = t.alias;
5
- return typeof s != "string" || s.length === 0 ? null : {
6
- alias: s,
7
- id: typeof t.id == "string" ? t.id : void 0,
8
- roles: Array.isArray(t.roles) ? t.roles.filter((o) => typeof o == "string") : []
1
+ import { isReplayable as d } from "./auth-fetch.js";
2
+ import { singleFlight as g } from "./single-flight.js";
3
+ function h(s) {
4
+ if (typeof s != "object" || s === null) return null;
5
+ const e = s, t = e.alias;
6
+ return typeof t != "string" || t.length === 0 ? null : {
7
+ alias: t,
8
+ id: typeof e.id == "string" ? e.id : void 0,
9
+ roles: Array.isArray(e.roles) ? e.roles.filter((a) => typeof a == "string") : []
9
10
  };
10
11
  }
11
- function d(e) {
12
- if (typeof e != "object" || e === null) return null;
13
- const t = e, s = t.user;
12
+ function m(s) {
14
13
  if (typeof s != "object" || s === null) return null;
15
- const o = s.id;
16
- if (typeof o != "string" || o.length === 0) return null;
17
- const l = s, u = (r) => typeof l[r] == "string" && l[r] !== "" ? l[r] : void 0;
14
+ const e = s, t = e.user;
15
+ if (typeof t != "object" || t === null) return null;
16
+ const a = t.id;
17
+ if (typeof a != "string" || a.length === 0) return null;
18
+ const l = t, u = (n) => typeof l[n] == "string" && l[n] !== "" ? l[n] : void 0;
18
19
  return {
19
- user: { id: o, email: u("email"), name: u("name"), username: u("username") },
20
- roles: Array.isArray(t.roles) ? t.roles.filter((r) => typeof r == "string") : [],
21
- organizations: Array.isArray(t.organizations) ? t.organizations.map(y).filter((r) => r !== null) : [],
22
- expiresAt: typeof t.expiresAt == "number" ? t.expiresAt : 0
20
+ user: { id: a, email: u("email"), name: u("name"), username: u("username") },
21
+ roles: Array.isArray(e.roles) ? e.roles.filter((n) => typeof n == "string") : [],
22
+ organizations: Array.isArray(e.organizations) ? e.organizations.map(h).filter((n) => n !== null) : [],
23
+ expiresAt: typeof e.expiresAt == "number" ? e.expiresAt : 0
23
24
  };
24
25
  }
25
- function m(e = {}) {
26
- const t = (e.basePath ?? "/api/auth").replace(/\/$/, ""), s = e.fetch ?? ((...n) => globalThis.fetch(...n)), o = e.navigate ?? ((n) => void (globalThis.location.href = n)), l = /* @__PURE__ */ new Set(), u = () => {
27
- for (const n of l) n();
26
+ function A(s = {}) {
27
+ const e = (s.basePath ?? "/api/auth").replace(/\/$/, ""), t = s.fetch ?? ((...r) => globalThis.fetch(...r)), a = s.navigate ?? ((r) => void (globalThis.location.href = r)), l = /* @__PURE__ */ new Set(), u = () => {
28
+ for (const r of l) r();
28
29
  };
29
- let r = null, c = !1;
30
- const g = p(async () => {
31
- const n = await s(`${t}/session`, {
30
+ let n = null, c = !1;
31
+ const p = g(async () => {
32
+ const r = await t(`${e}/session`, {
32
33
  headers: { Accept: "application/json" }
33
34
  });
34
- return n.status === 401 || !n.ok ? null : d(await n.json().catch(() => null));
35
- }), f = async () => {
36
- const n = r;
37
- return r = await g(), c = !0, (n === null != (r === null) || (n == null ? void 0 : n.user.id) !== (r == null ? void 0 : r.user.id)) && u(), r;
35
+ return r.status === 401 || !r.ok ? null : m(await r.json().catch(() => null));
36
+ }), y = g(async () => (await t(`${e}/refresh`, {
37
+ method: "POST",
38
+ headers: { Accept: "application/json" }
39
+ })).ok), f = async () => {
40
+ const r = n;
41
+ return n = await p(), c = !0, (r === null != (n === null) || (r == null ? void 0 : r.user.id) !== (n == null ? void 0 : n.user.id)) && u(), n;
38
42
  };
39
43
  return {
40
44
  async getSession() {
41
- return c ? r : f();
45
+ return c ? n : f();
42
46
  },
43
- subscribe(n) {
44
- return l.add(n), () => l.delete(n);
47
+ subscribe(r) {
48
+ return l.add(r), () => l.delete(r);
45
49
  },
46
- async signIn(n = {}) {
47
- var a;
48
- const i = new URLSearchParams();
49
- i.set("returnTo", n.returnTo ?? ((a = globalThis.location) == null ? void 0 : a.href) ?? "/"), n.organization !== void 0 && i.set("organization", n.organization), o(`${t}/signin?${i.toString()}`);
50
+ async signIn(r = {}) {
51
+ var i;
52
+ const o = new URLSearchParams();
53
+ o.set("returnTo", r.returnTo ?? ((i = globalThis.location) == null ? void 0 : i.href) ?? "/"), r.organization !== void 0 && o.set("organization", r.organization), a(`${e}/signin?${o.toString()}`);
50
54
  },
51
- async signOut(n = {}) {
52
- const i = new URLSearchParams();
53
- n.returnTo !== void 0 && i.set("returnTo", n.returnTo);
54
- const a = i.toString();
55
- o(a ? `${t}/signout?${a}` : `${t}/signout`);
55
+ async signOut(r = {}) {
56
+ const o = new URLSearchParams();
57
+ r.returnTo !== void 0 && o.set("returnTo", r.returnTo);
58
+ const i = o.toString();
59
+ a(i ? `${e}/signout?${i}` : `${e}/signout`);
56
60
  },
57
- // No `Authorization` header, and no retry: the cookie rides along on a same-origin request by
58
- // itself, so there is no stale token to renew. A 401 means the session is *gone* rather than
59
- // stale, and the useful response to that is to tell the tree — which is what re-reading does.
60
- fetch: async (n, i) => {
61
- const a = await s(n, i);
62
- return a.status === 401 && c && (c = !1, await f()), a;
61
+ /**
62
+ * No `Authorization` header — the cookie rides along on a same-origin request by itself — and
63
+ * **one** retry, behind one renewal.
64
+ *
65
+ * A 401 here is ambiguous in a way it is not under `browserAuth`: the cookie was sent and was
66
+ * accepted, so what expired is the access token *behind* the cookie, which this half of the
67
+ * pattern cannot see. So the 401 is taken as "renew and try again" first and as "the session
68
+ * is gone" only when the renewal is refused — at which point re-reading tells the tree, which
69
+ * is what it did before and all it did before.
70
+ *
71
+ * The retry is once, for the reason `authFetch` gives: twice turns an ended session into a
72
+ * loop against the authorization server. A request whose body cannot be replayed is not
73
+ * retried at all, and `isReplayable` is the same predicate the bearer-token path uses.
74
+ */
75
+ fetch: async (r, o) => {
76
+ const i = await t(r, o);
77
+ return i.status !== 401 || !c ? i : await y() ? d(r, o) ? t(r, o) : i : (c = !1, await f(), i);
63
78
  }
64
79
  };
65
80
  }
66
81
  export {
67
- m as bffAuth,
68
- d as readSession
82
+ A as bffAuth,
83
+ m as readSession
69
84
  };
70
85
  //# sourceMappingURL=bff-auth.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bff-auth.js","sources":["../src/bff-auth.ts"],"sourcesContent":["import { singleFlight } from \"./single-flight\";\nimport type { Auth, Organization, Session, SignInOptions } from \"./types\";\n\n/**\n * The Backend-For-Frontend pattern: the token never reaches the browser.\n *\n * RFC 10017 calls this one *\"strongly recommended for business applications, sensitive\n * applications, and applications that handle personal data\"*. The server holds the confidential\n * client and the tokens; the browser holds a cookie it cannot read, and asks the server who it is.\n *\n * So there is no engine on this path — no PKCE, no storage, no renewal — which is why it lives on\n * the root barrel beside the hooks rather than behind a subpath. What it needs from the server is\n * three routes, which `@kanzo-tech/auth/next` provides: a session endpoint, a sign-in and a\n * sign-out.\n */\n\nexport interface BffAuthConfig {\n /** Where the BFF's auth routes are mounted. Default `/api/auth`. */\n readonly basePath?: string;\n /** Injectable for tests. Defaults to the global. */\n readonly fetch?: typeof globalThis.fetch;\n /** Injectable for tests. Defaults to assigning `window.location`. */\n readonly navigate?: (url: string) => void;\n}\n\nfunction readOrganization(value: unknown): Organization | null {\n if (typeof value !== \"object\" || value === null) return null;\n const org = value as Record<string, unknown>;\n const alias = org[\"alias\"];\n if (typeof alias !== \"string\" || alias.length === 0) return null;\n return {\n alias,\n id: typeof org[\"id\"] === \"string\" ? org[\"id\"] : undefined,\n roles: Array.isArray(org[\"roles\"])\n ? org[\"roles\"].filter((r): r is string => typeof r === \"string\")\n : [],\n };\n}\n\n/**\n * The session endpoint's answer, checked rather than cast.\n *\n * It is our own server on the other end, and it is still **data off the wire**: a cast here would\n * make a deploy skew or a proxy's error page arrive as a `Session` whose `user` is undefined, and\n * the failure would surface three components away as a property read on nothing. `null` for\n * anything unrecognisable is the same answer as \"not signed in\", which is the safe reading.\n */\nexport function readSession(value: unknown): Session | null {\n if (typeof value !== \"object\" || value === null) return null;\n const body = value as Record<string, unknown>;\n\n const user = body[\"user\"];\n if (typeof user !== \"object\" || user === null) return null;\n const id = (user as Record<string, unknown>)[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) return null;\n\n const person = user as Record<string, unknown>;\n const str = (key: string) =>\n typeof person[key] === \"string\" && person[key] !== \"\" ? (person[key] as string) : undefined;\n\n return {\n user: { id, email: str(\"email\"), name: str(\"name\"), username: str(\"username\") },\n roles: Array.isArray(body[\"roles\"])\n ? body[\"roles\"].filter((r): r is string => typeof r === \"string\")\n : [],\n organizations: Array.isArray(body[\"organizations\"])\n ? body[\"organizations\"]\n .map(readOrganization)\n .filter((o): o is Organization => o !== null)\n : [],\n expiresAt: typeof body[\"expiresAt\"] === \"number\" ? body[\"expiresAt\"] : 0,\n };\n}\n\nexport function bffAuth(config: BffAuthConfig = {}): Auth {\n const base = (config.basePath ?? \"/api/auth\").replace(/\\/$/, \"\");\n const doFetch = config.fetch ?? ((...args) => globalThis.fetch(...args));\n const go = config.navigate ?? ((url: string) => void (globalThis.location.href = url));\n\n const listeners = new Set<() => void>();\n const announce = () => {\n for (const listener of listeners) listener();\n };\n\n let cached: Session | null = null;\n let known = false;\n\n // Single-flight for the same reason the token path needs it: a page that mounts six components\n // asks six times in one tick, and one answer serves them all. Here it costs a request rather\n // than a revoked token chain, which is a smaller bill for the same mistake.\n const read = singleFlight(async (): Promise<Session | null> => {\n const response = await doFetch(`${base}/session`, {\n headers: { Accept: \"application/json\" },\n });\n // 401 is the documented answer for \"nobody is signed in\", not a failure to report.\n if (response.status === 401) return null;\n if (!response.ok) return null;\n return readSession(await response.json().catch(() => null));\n });\n\n const refresh = async (): Promise<Session | null> => {\n const next = cached;\n cached = await read();\n known = true;\n // Announce only a change, so a poll does not re-render the tree every time.\n if ((next === null) !== (cached === null) || next?.user.id !== cached?.user.id) announce();\n return cached;\n };\n\n return {\n async getSession() {\n if (known) return cached;\n return refresh();\n },\n\n subscribe(onChange) {\n listeners.add(onChange);\n return () => listeners.delete(onChange);\n },\n\n async signIn(options: SignInOptions = {}) {\n const params = new URLSearchParams();\n params.set(\"returnTo\", options.returnTo ?? globalThis.location?.href ?? \"/\");\n if (options.organization !== undefined) params.set(\"organization\", options.organization);\n go(`${base}/signin?${params.toString()}`);\n },\n\n async signOut(options = {}) {\n const params = new URLSearchParams();\n if (options.returnTo !== undefined) params.set(\"returnTo\", options.returnTo);\n const query = params.toString();\n go(query ? `${base}/signout?${query}` : `${base}/signout`);\n },\n\n // No `Authorization` header, and no retry: the cookie rides along on a same-origin request by\n // itself, so there is no stale token to renew. A 401 means the session is *gone* rather than\n // stale, and the useful response to that is to tell the tree — which is what re-reading does.\n fetch: async (input, init) => {\n const response = await doFetch(input, init);\n if (response.status === 401 && known) {\n known = false;\n await refresh();\n }\n return response;\n },\n };\n}\n"],"names":["readOrganization","value","org","alias","r","readSession","body","user","id","person","str","key","o","bffAuth","config","base","doFetch","args","go","url","listeners","announce","listener","cached","known","read","singleFlight","response","refresh","next","onChange","options","params","_a","query","input","init"],"mappings":"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"}
1
+ {"version":3,"file":"bff-auth.js","sources":["../src/bff-auth.ts"],"sourcesContent":["import { isReplayable } from \"./auth-fetch\";\nimport { singleFlight } from \"./single-flight\";\nimport type { Auth, Organization, Session, SignInOptions } from \"./types\";\n\n/**\n * The Backend-For-Frontend pattern: the token never reaches the browser.\n *\n * RFC 10017 calls this one *\"strongly recommended for business applications, sensitive\n * applications, and applications that handle personal data\"*. The server holds the confidential\n * client and the tokens; the browser holds a cookie it cannot read, and asks the server who it is.\n *\n * So there is no engine on this path — no PKCE, no storage, no renewal — which is why it lives on\n * the root barrel beside the hooks rather than behind a subpath. What it needs from the server is\n * three routes, which `@kanzo-tech/auth/next` provides: a session endpoint, a sign-in and a\n * sign-out.\n */\n\nexport interface BffAuthConfig {\n /** Where the BFF's auth routes are mounted. Default `/api/auth`. */\n readonly basePath?: string;\n /** Injectable for tests. Defaults to the global. */\n readonly fetch?: typeof globalThis.fetch;\n /** Injectable for tests. Defaults to assigning `window.location`. */\n readonly navigate?: (url: string) => void;\n}\n\nfunction readOrganization(value: unknown): Organization | null {\n if (typeof value !== \"object\" || value === null) return null;\n const org = value as Record<string, unknown>;\n const alias = org[\"alias\"];\n if (typeof alias !== \"string\" || alias.length === 0) return null;\n return {\n alias,\n id: typeof org[\"id\"] === \"string\" ? org[\"id\"] : undefined,\n roles: Array.isArray(org[\"roles\"])\n ? org[\"roles\"].filter((r): r is string => typeof r === \"string\")\n : [],\n };\n}\n\n/**\n * The session endpoint's answer, checked rather than cast.\n *\n * It is our own server on the other end, and it is still **data off the wire**: a cast here would\n * make a deploy skew or a proxy's error page arrive as a `Session` whose `user` is undefined, and\n * the failure would surface three components away as a property read on nothing. `null` for\n * anything unrecognisable is the same answer as \"not signed in\", which is the safe reading.\n */\nexport function readSession(value: unknown): Session | null {\n if (typeof value !== \"object\" || value === null) return null;\n const body = value as Record<string, unknown>;\n\n const user = body[\"user\"];\n if (typeof user !== \"object\" || user === null) return null;\n const id = (user as Record<string, unknown>)[\"id\"];\n if (typeof id !== \"string\" || id.length === 0) return null;\n\n const person = user as Record<string, unknown>;\n const str = (key: string) =>\n typeof person[key] === \"string\" && person[key] !== \"\" ? (person[key] as string) : undefined;\n\n return {\n user: { id, email: str(\"email\"), name: str(\"name\"), username: str(\"username\") },\n roles: Array.isArray(body[\"roles\"])\n ? body[\"roles\"].filter((r): r is string => typeof r === \"string\")\n : [],\n organizations: Array.isArray(body[\"organizations\"])\n ? body[\"organizations\"]\n .map(readOrganization)\n .filter((o): o is Organization => o !== null)\n : [],\n expiresAt: typeof body[\"expiresAt\"] === \"number\" ? body[\"expiresAt\"] : 0,\n };\n}\n\nexport function bffAuth(config: BffAuthConfig = {}): Auth {\n const base = (config.basePath ?? \"/api/auth\").replace(/\\/$/, \"\");\n const doFetch = config.fetch ?? ((...args) => globalThis.fetch(...args));\n const go = config.navigate ?? ((url: string) => void (globalThis.location.href = url));\n\n const listeners = new Set<() => void>();\n const announce = () => {\n for (const listener of listeners) listener();\n };\n\n let cached: Session | null = null;\n let known = false;\n\n // Single-flight for the same reason the token path needs it: a page that mounts six components\n // asks six times in one tick, and one answer serves them all. Here it costs a request rather\n // than a revoked token chain, which is a smaller bill for the same mistake.\n const read = singleFlight(async (): Promise<Session | null> => {\n const response = await doFetch(`${base}/session`, {\n headers: { Accept: \"application/json\" },\n });\n // 401 is the documented answer for \"nobody is signed in\", not a failure to report.\n if (response.status === 401) return null;\n if (!response.ok) return null;\n return readSession(await response.json().catch(() => null));\n });\n\n /**\n * Ask the BFF to spend the refresh token, once for however many requests noticed at the moment.\n *\n * This is the browser end of the renewal, and without it the session cookie's lifetime and the\n * access token's are two different clocks with nothing between them: a cookie good for eight\n * hours in front of a token good for one produces seven hours in which `/session` answers 200,\n * the whole application draws, and every request for data is a 401 that nothing acts on.\n *\n * `POST`, because the route only answers `POST` — it spends something, and a `GET` that spends\n * something is one prefetch away from spending it unasked.\n */\n const renew = singleFlight(async (): Promise<boolean> => {\n const response = await doFetch(`${base}/refresh`, {\n method: \"POST\",\n headers: { Accept: \"application/json\" },\n });\n return response.ok;\n });\n\n const refresh = async (): Promise<Session | null> => {\n const next = cached;\n cached = await read();\n known = true;\n // Announce only a change, so a poll does not re-render the tree every time.\n if ((next === null) !== (cached === null) || next?.user.id !== cached?.user.id) announce();\n return cached;\n };\n\n return {\n async getSession() {\n if (known) return cached;\n return refresh();\n },\n\n subscribe(onChange) {\n listeners.add(onChange);\n return () => listeners.delete(onChange);\n },\n\n async signIn(options: SignInOptions = {}) {\n const params = new URLSearchParams();\n params.set(\"returnTo\", options.returnTo ?? globalThis.location?.href ?? \"/\");\n if (options.organization !== undefined) params.set(\"organization\", options.organization);\n go(`${base}/signin?${params.toString()}`);\n },\n\n async signOut(options = {}) {\n const params = new URLSearchParams();\n if (options.returnTo !== undefined) params.set(\"returnTo\", options.returnTo);\n const query = params.toString();\n go(query ? `${base}/signout?${query}` : `${base}/signout`);\n },\n\n /**\n * No `Authorization` header — the cookie rides along on a same-origin request by itself — and\n * **one** retry, behind one renewal.\n *\n * A 401 here is ambiguous in a way it is not under `browserAuth`: the cookie was sent and was\n * accepted, so what expired is the access token *behind* the cookie, which this half of the\n * pattern cannot see. So the 401 is taken as \"renew and try again\" first and as \"the session\n * is gone\" only when the renewal is refused — at which point re-reading tells the tree, which\n * is what it did before and all it did before.\n *\n * The retry is once, for the reason `authFetch` gives: twice turns an ended session into a\n * loop against the authorization server. A request whose body cannot be replayed is not\n * retried at all, and `isReplayable` is the same predicate the bearer-token path uses.\n */\n fetch: async (input, init) => {\n const response = await doFetch(input, init);\n if (response.status !== 401 || !known) return response;\n\n if (await renew()) {\n if (!isReplayable(input, init)) return response;\n return doFetch(input, init);\n }\n\n known = false;\n await refresh();\n return response;\n },\n };\n}\n"],"names":["readOrganization","value","org","alias","r","readSession","body","user","id","person","str","key","o","bffAuth","config","base","doFetch","args","go","url","listeners","announce","listener","cached","known","read","singleFlight","response","renew","refresh","next","onChange","options","params","_a","query","input","init","isReplayable"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cookie-session.d.ts","sourceRoot":"","sources":["../src/cookie-session.ts"],"names":[],"mappings":"AAoCA,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IACrC,wGAAwG;IACxG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,uGAAuG;IACvG,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,0DAA0D;IAC1D,KAAK,IAAI,MAAM,CAAC;CACjB;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAQpB;AAaD,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAAC,CAAC,CAAC,CAgD3E"}
1
+ {"version":3,"file":"cookie-session.d.ts","sourceRoot":"","sources":["../src/cookie-session.ts"],"names":[],"mappings":"AAoCA,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IACrC,wGAAwG;IACxG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,uGAAuG;IACvG,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,0DAA0D;IAC1D,KAAK,IAAI,MAAM,CAAC;CACjB;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAQpB;AAaD,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAAC,CAAC,CAAC,CAiD3E"}
@@ -1,6 +1,6 @@
1
1
  import { jwtDecrypt as l, EncryptJWT as u } from "jose";
2
2
  const s = 4096;
3
- function y(e, t) {
3
+ function d(e, t) {
4
4
  if (!(e == null || e.length === 0))
5
5
  for (const n of e.split(";")) {
6
6
  const r = n.indexOf("=");
@@ -8,29 +8,29 @@ function y(e, t) {
8
8
  return n.slice(r + 1).trim();
9
9
  }
10
10
  }
11
- async function d(e) {
11
+ async function p(e) {
12
12
  const t = typeof e == "string" ? new TextEncoder().encode(e) : e;
13
13
  return new Uint8Array(await crypto.subtle.digest("SHA-256", t));
14
14
  }
15
15
  function c(e, t, n) {
16
16
  return `${e}=${t}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${n}`;
17
17
  }
18
- function m(e) {
18
+ function k(e) {
19
19
  const t = `__Host-${e.name}`;
20
20
  let n;
21
- const r = () => n ?? (n = d(e.secret));
21
+ const r = () => n ?? (n = p(e.secret));
22
22
  return {
23
23
  name: t,
24
24
  async seal(a) {
25
25
  const i = await new u({ v: a }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${e.maxAge}s`).encrypt(await r()), o = c(t, i, e.maxAge);
26
26
  if (o.length > s)
27
27
  throw new Error(
28
- `${t} is ${o.length} bytes and a browser is only required to keep ${s}; a cookie over the limit is dropped silently and the session simply never appears. Give relyingParty a SessionStore so the cookie carries a ticket instead of the tokens.`
28
+ `${t} is ${o.length} bytes and a browser is only required to keep ${s}; a cookie over the limit is dropped silently and the session simply never appears. Give relyingParty \`store: ticketStore(adapter)\` so the cookie carries an opaque ticket instead of the tokens: a record holding an access token does not fit here.`
29
29
  );
30
30
  return o;
31
31
  },
32
32
  async read(a) {
33
- const i = y(a, t);
33
+ const i = d(a, t);
34
34
  if (i === void 0) return null;
35
35
  try {
36
36
  const { payload: o } = await l(i, await r());
@@ -45,7 +45,7 @@ function m(e) {
45
45
  };
46
46
  }
47
47
  export {
48
- y as cookieValue,
49
- m as sealedCookie
48
+ d as cookieValue,
49
+ k as sealedCookie
50
50
  };
51
51
  //# sourceMappingURL=cookie-session.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cookie-session.js","sources":["../src/cookie-session.ts"],"sourcesContent":["import { EncryptJWT, jwtDecrypt } from \"jose\";\n\n/**\n * A value sealed into a cookie, and read back out of one.\n *\n * There is **one** mechanism here and it is used twice: for the session that outlives a request,\n * and for the short-lived transaction that carries `state`, `nonce` and the PKCE verifier between\n * the two legs of the authorization code flow. Holding the transaction in a cookie rather than in\n * server memory is what makes the BFF stateless by default — keasy's Rust keeps those three in a\n * SQLite-backed server session, and pays for a session store before anyone has signed in.\n *\n * ## The attributes, and why each one\n *\n * - **`__Host-` prefix**, added here and not declinable. It forces `Secure` and `Path=/`, forbids\n * `Domain`, and — the part that matters — a page on a sibling subdomain cannot write it. Without\n * the prefix, anything that can serve `evil.example.test` can set a cookie that arrives at\n * `app.example.test` looking exactly like ours.\n * - **`HttpOnly`**, so script cannot read it. In a BFF the browser is not supposed to hold the\n * credential at all; this is the enforcement of that sentence.\n * - **`SameSite=Lax`**, not `Strict`. `Strict` withholds the cookie on the top-level navigation\n * *back* from the identity provider, so the callback arrives without the transaction it needs\n * and every sign-in fails. `Lax` sends it on exactly that navigation and on nothing else risky.\n * - **JWE, not a signature.** The payload is a refresh token: signing would authenticate it and\n * leave it readable to anyone who can see the cookie. `dir` + `A256GCM` is authenticated\n * encryption, so a tampered byte fails to decrypt rather than decrypting to something else.\n */\n\n/**\n * The 4 KB a browser is required to keep, and the reason this module has a size guard.\n *\n * A cookie over the limit is not rejected loudly — it is *dropped*, and the symptom is a sign-in\n * that appears to work and a session that is never there. A `SessionStore` is the way out, and the\n * error says so.\n */\nconst COOKIE_LIMIT = 4096;\n\nexport interface SealedCookieConfig {\n /**\n * The name **after** the `__Host-` prefix, which this module adds. A caller cannot decline it:\n * the prefix is the only cookie attribute a browser enforces on our behalf.\n */\n readonly name: string;\n /**\n * The sealing secret. Any length — it is hashed to the 256-bit key — but it is a *secret*, not a\n * password: generate it, do not choose it.\n */\n readonly secret: string | Uint8Array;\n /** Seconds. It is both the cookie's `Max-Age` and the JWE's `exp`, so neither can outlive the other. */\n readonly maxAge: number;\n}\n\nexport interface SealedCookie<T> {\n /** The full cookie name, prefix included. */\n readonly name: string;\n /** The value of a `Set-Cookie` header carrying `value`. */\n seal(value: T): Promise<string>;\n /** Read from a request's `Cookie` header. `null` for absent, tampered, or expired — all one answer. */\n read(header: string | null | undefined): Promise<T | null>;\n /** The value of a `Set-Cookie` header that removes it. */\n clear(): string;\n}\n\n/** One cookie value out of a request's `Cookie` header, or `undefined`. */\nexport function cookieValue(\n header: string | null | undefined,\n name: string,\n): string | undefined {\n if (header === null || header === undefined || header.length === 0) return undefined;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return undefined;\n}\n\nasync function keyFrom(secret: string | Uint8Array): Promise<Uint8Array> {\n const input = typeof secret === \"string\" ? new TextEncoder().encode(secret) : secret;\n return new Uint8Array(await crypto.subtle.digest(\"SHA-256\", input as BufferSource));\n}\n\nfunction setCookie(name: string, value: string, maxAge: number): string {\n // No `Domain`: `__Host-` forbids it, and forbidding it is the point — a cookie without a domain\n // is the one a sibling host cannot reach.\n return `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;\n}\n\nexport function sealedCookie<T>(config: SealedCookieConfig): SealedCookie<T> {\n const name = `__Host-${config.name}`;\n // Derived once, lazily: the digest is cheap but a request path should not pay for it per call.\n let key: Promise<Uint8Array> | undefined;\n const material = () => (key ??= keyFrom(config.secret));\n\n return {\n name,\n\n async seal(value) {\n // The payload is wrapped rather than spread, so a field named `exp` or `iss` in a `Session`\n // could never come to mean the JWT claim of the same name.\n const token = await new EncryptJWT({ v: value })\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(`${config.maxAge}s`)\n .encrypt(await material());\n\n const header = setCookie(name, token, config.maxAge);\n if (header.length > COOKIE_LIMIT) {\n throw new Error(\n `${name} is ${header.length} bytes and a browser is only required to keep ${COOKIE_LIMIT}; ` +\n \"a cookie over the limit is dropped silently and the session simply never appears. \" +\n \"Give relyingParty a SessionStore so the cookie carries a ticket instead of the tokens.\",\n );\n }\n return header;\n },\n\n async read(header) {\n const token = cookieValue(header, name);\n if (token === undefined) return null;\n try {\n const { payload } = await jwtDecrypt(token, await material());\n return (payload[\"v\"] ?? null) as T | null;\n } catch {\n // Forged, re-keyed, truncated by a proxy, or simply expired. None of them is a session,\n // and none of them is worth a different answer to the caller — `bff-auth` reads the same\n // endpoint the same way. A thrown error here would only ever be caught and turned into\n // this.\n return null;\n }\n },\n\n clear() {\n return setCookie(name, \"\", 0);\n },\n };\n}\n"],"names":["COOKIE_LIMIT","cookieValue","header","name","part","eq","keyFrom","secret","input","setCookie","value","maxAge","sealedCookie","config","key","material","token","EncryptJWT","payload","jwtDecrypt"],"mappings":";AAkCA,MAAMA,IAAe;AA6Bd,SAASC,EACdC,GACAC,GACoB;AACpB,MAAI,EAAAD,KAAW,QAAgCA,EAAO,WAAW;AACjE,eAAWE,KAAQF,EAAO,MAAM,GAAG,GAAG;AACpC,YAAMG,IAAKD,EAAK,QAAQ,GAAG;AAC3B,UAAIC,MAAO,MACPD,EAAK,MAAM,GAAGC,CAAE,EAAE,KAAA,MAAWF;AAAM,eAAOC,EAAK,MAAMC,IAAK,CAAC,EAAE,KAAA;AAAA,IACnE;AAEF;AAEA,eAAeC,EAAQC,GAAkD;AACvE,QAAMC,IAAQ,OAAOD,KAAW,WAAW,IAAI,cAAc,OAAOA,CAAM,IAAIA;AAC9E,SAAO,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,WAAWC,CAAqB,CAAC;AACpF;AAEA,SAASC,EAAUN,GAAcO,GAAeC,GAAwB;AAGtE,SAAO,GAAGR,CAAI,IAAIO,CAAK,qDAAqDC,CAAM;AACpF;AAEO,SAASC,EAAgBC,GAA6C;AAC3E,QAAMV,IAAO,UAAUU,EAAO,IAAI;AAElC,MAAIC;AACJ,QAAMC,IAAW,MAAOD,UAAQR,EAAQO,EAAO,MAAM;AAErD,SAAO;AAAA,IACL,MAAAV;AAAA,IAEA,MAAM,KAAKO,GAAO;AAGhB,YAAMM,IAAQ,MAAM,IAAIC,EAAW,EAAE,GAAGP,EAAA,CAAO,EAC5C,mBAAmB,EAAE,KAAK,OAAO,KAAK,UAAA,CAAW,EACjD,YAAA,EACA,kBAAkB,GAAGG,EAAO,MAAM,GAAG,EACrC,QAAQ,MAAME,EAAA,CAAU,GAErBb,IAASO,EAAUN,GAAMa,GAAOH,EAAO,MAAM;AACnD,UAAIX,EAAO,SAASF;AAClB,cAAM,IAAI;AAAA,UACR,GAAGG,CAAI,OAAOD,EAAO,MAAM,iDAAiDF,CAAY;AAAA,QAAA;AAK5F,aAAOE;AAAA,IACT;AAAA,IAEA,MAAM,KAAKA,GAAQ;AACjB,YAAMc,IAAQf,EAAYC,GAAQC,CAAI;AACtC,UAAIa,MAAU,OAAW,QAAO;AAChC,UAAI;AACF,cAAM,EAAE,SAAAE,MAAY,MAAMC,EAAWH,GAAO,MAAMD,GAAU;AAC5D,eAAQG,EAAQ,KAAQ;AAAA,MAC1B,QAAQ;AAKN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,QAAQ;AACN,aAAOT,EAAUN,GAAM,IAAI,CAAC;AAAA,IAC9B;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"cookie-session.js","sources":["../src/cookie-session.ts"],"sourcesContent":["import { EncryptJWT, jwtDecrypt } from \"jose\";\n\n/**\n * A value sealed into a cookie, and read back out of one.\n *\n * There is **one** mechanism here and it is used twice: for the session that outlives a request,\n * and for the short-lived transaction that carries `state`, `nonce` and the PKCE verifier between\n * the two legs of the authorization code flow. Holding the transaction in a cookie rather than in\n * server memory is what makes the BFF stateless by default — keasy's Rust keeps those three in a\n * SQLite-backed server session, and pays for a session store before anyone has signed in.\n *\n * ## The attributes, and why each one\n *\n * - **`__Host-` prefix**, added here and not declinable. It forces `Secure` and `Path=/`, forbids\n * `Domain`, and — the part that matters — a page on a sibling subdomain cannot write it. Without\n * the prefix, anything that can serve `evil.example.test` can set a cookie that arrives at\n * `app.example.test` looking exactly like ours.\n * - **`HttpOnly`**, so script cannot read it. In a BFF the browser is not supposed to hold the\n * credential at all; this is the enforcement of that sentence.\n * - **`SameSite=Lax`**, not `Strict`. `Strict` withholds the cookie on the top-level navigation\n * *back* from the identity provider, so the callback arrives without the transaction it needs\n * and every sign-in fails. `Lax` sends it on exactly that navigation and on nothing else risky.\n * - **JWE, not a signature.** The payload is a refresh token: signing would authenticate it and\n * leave it readable to anyone who can see the cookie. `dir` + `A256GCM` is authenticated\n * encryption, so a tampered byte fails to decrypt rather than decrypting to something else.\n */\n\n/**\n * The 4 KB a browser is required to keep, and the reason this module has a size guard.\n *\n * A cookie over the limit is not rejected loudly — it is *dropped*, and the symptom is a sign-in\n * that appears to work and a session that is never there. A `SessionStore` is the way out, and the\n * error says so.\n */\nconst COOKIE_LIMIT = 4096;\n\nexport interface SealedCookieConfig {\n /**\n * The name **after** the `__Host-` prefix, which this module adds. A caller cannot decline it:\n * the prefix is the only cookie attribute a browser enforces on our behalf.\n */\n readonly name: string;\n /**\n * The sealing secret. Any length — it is hashed to the 256-bit key — but it is a *secret*, not a\n * password: generate it, do not choose it.\n */\n readonly secret: string | Uint8Array;\n /** Seconds. It is both the cookie's `Max-Age` and the JWE's `exp`, so neither can outlive the other. */\n readonly maxAge: number;\n}\n\nexport interface SealedCookie<T> {\n /** The full cookie name, prefix included. */\n readonly name: string;\n /** The value of a `Set-Cookie` header carrying `value`. */\n seal(value: T): Promise<string>;\n /** Read from a request's `Cookie` header. `null` for absent, tampered, or expired — all one answer. */\n read(header: string | null | undefined): Promise<T | null>;\n /** The value of a `Set-Cookie` header that removes it. */\n clear(): string;\n}\n\n/** One cookie value out of a request's `Cookie` header, or `undefined`. */\nexport function cookieValue(\n header: string | null | undefined,\n name: string,\n): string | undefined {\n if (header === null || header === undefined || header.length === 0) return undefined;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq === -1) continue;\n if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return undefined;\n}\n\nasync function keyFrom(secret: string | Uint8Array): Promise<Uint8Array> {\n const input = typeof secret === \"string\" ? new TextEncoder().encode(secret) : secret;\n return new Uint8Array(await crypto.subtle.digest(\"SHA-256\", input as BufferSource));\n}\n\nfunction setCookie(name: string, value: string, maxAge: number): string {\n // No `Domain`: `__Host-` forbids it, and forbidding it is the point — a cookie without a domain\n // is the one a sibling host cannot reach.\n return `${name}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;\n}\n\nexport function sealedCookie<T>(config: SealedCookieConfig): SealedCookie<T> {\n const name = `__Host-${config.name}`;\n // Derived once, lazily: the digest is cheap but a request path should not pay for it per call.\n let key: Promise<Uint8Array> | undefined;\n const material = () => (key ??= keyFrom(config.secret));\n\n return {\n name,\n\n async seal(value) {\n // The payload is wrapped rather than spread, so a field named `exp` or `iss` in a `Session`\n // could never come to mean the JWT claim of the same name.\n const token = await new EncryptJWT({ v: value })\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(`${config.maxAge}s`)\n .encrypt(await material());\n\n const header = setCookie(name, token, config.maxAge);\n if (header.length > COOKIE_LIMIT) {\n throw new Error(\n `${name} is ${header.length} bytes and a browser is only required to keep ${COOKIE_LIMIT}; ` +\n \"a cookie over the limit is dropped silently and the session simply never appears. \" +\n \"Give relyingParty `store: ticketStore(adapter)` so the cookie carries an opaque \" +\n \"ticket instead of the tokens: a record holding an access token does not fit here.\",\n );\n }\n return header;\n },\n\n async read(header) {\n const token = cookieValue(header, name);\n if (token === undefined) return null;\n try {\n const { payload } = await jwtDecrypt(token, await material());\n return (payload[\"v\"] ?? null) as T | null;\n } catch {\n // Forged, re-keyed, truncated by a proxy, or simply expired. None of them is a session,\n // and none of them is worth a different answer to the caller — `bff-auth` reads the same\n // endpoint the same way. A thrown error here would only ever be caught and turned into\n // this.\n return null;\n }\n },\n\n clear() {\n return setCookie(name, \"\", 0);\n },\n };\n}\n"],"names":["COOKIE_LIMIT","cookieValue","header","name","part","eq","keyFrom","secret","input","setCookie","value","maxAge","sealedCookie","config","key","material","token","EncryptJWT","payload","jwtDecrypt"],"mappings":";AAkCA,MAAMA,IAAe;AA6Bd,SAASC,EACdC,GACAC,GACoB;AACpB,MAAI,EAAAD,KAAW,QAAgCA,EAAO,WAAW;AACjE,eAAWE,KAAQF,EAAO,MAAM,GAAG,GAAG;AACpC,YAAMG,IAAKD,EAAK,QAAQ,GAAG;AAC3B,UAAIC,MAAO,MACPD,EAAK,MAAM,GAAGC,CAAE,EAAE,KAAA,MAAWF;AAAM,eAAOC,EAAK,MAAMC,IAAK,CAAC,EAAE,KAAA;AAAA,IACnE;AAEF;AAEA,eAAeC,EAAQC,GAAkD;AACvE,QAAMC,IAAQ,OAAOD,KAAW,WAAW,IAAI,cAAc,OAAOA,CAAM,IAAIA;AAC9E,SAAO,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,WAAWC,CAAqB,CAAC;AACpF;AAEA,SAASC,EAAUN,GAAcO,GAAeC,GAAwB;AAGtE,SAAO,GAAGR,CAAI,IAAIO,CAAK,qDAAqDC,CAAM;AACpF;AAEO,SAASC,EAAgBC,GAA6C;AAC3E,QAAMV,IAAO,UAAUU,EAAO,IAAI;AAElC,MAAIC;AACJ,QAAMC,IAAW,MAAOD,UAAQR,EAAQO,EAAO,MAAM;AAErD,SAAO;AAAA,IACL,MAAAV;AAAA,IAEA,MAAM,KAAKO,GAAO;AAGhB,YAAMM,IAAQ,MAAM,IAAIC,EAAW,EAAE,GAAGP,EAAA,CAAO,EAC5C,mBAAmB,EAAE,KAAK,OAAO,KAAK,UAAA,CAAW,EACjD,YAAA,EACA,kBAAkB,GAAGG,EAAO,MAAM,GAAG,EACrC,QAAQ,MAAME,EAAA,CAAU,GAErBb,IAASO,EAAUN,GAAMa,GAAOH,EAAO,MAAM;AACnD,UAAIX,EAAO,SAASF;AAClB,cAAM,IAAI;AAAA,UACR,GAAGG,CAAI,OAAOD,EAAO,MAAM,iDAAiDF,CAAY;AAAA,QAAA;AAM5F,aAAOE;AAAA,IACT;AAAA,IAEA,MAAM,KAAKA,GAAQ;AACjB,YAAMc,IAAQf,EAAYC,GAAQC,CAAI;AACtC,UAAIa,MAAU,OAAW,QAAO;AAChC,UAAI;AACF,cAAM,EAAE,SAAAE,MAAY,MAAMC,EAAWH,GAAO,MAAMD,GAAU;AAC5D,eAAQG,EAAQ,KAAQ;AAAA,MAC1B,QAAQ;AAKN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,QAAQ;AACN,aAAOT,EAAUN,GAAM,IAAI,CAAC;AAAA,IAC9B;AAAA,EAAA;AAEJ;"}
@@ -0,0 +1,65 @@
1
+ import { AuthSessionConfig } from './next-session';
2
+ /**
3
+ * The BFF half of the *token-mediating backend*: the browser's request goes out again carrying a
4
+ * bearer token, and the cookie that got it here stops at this line.
5
+ *
6
+ * ```ts
7
+ * // app/api/data/[...path]/route.ts
8
+ * export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } = authProxy({
9
+ * issuer, clientId, clientSecret, secret,
10
+ * basePath: "/api/data",
11
+ * target: "https://reports.internal/v1",
12
+ * });
13
+ * ```
14
+ *
15
+ * ## Why this is in the package and not in the product
16
+ *
17
+ * Because it is the same ninety lines every time, and one of them is load-bearing in a way that
18
+ * does not look it. **The `cookie` header must not be forwarded.** Leave it on and the resource
19
+ * server receives a second credential — the session cookie — alongside the bearer token it asked
20
+ * for, which is precisely the confusion the BFF pattern exists to remove: from then on a bug at
21
+ * the far end can act as the person rather than as the token, and the token's scope and lifetime
22
+ * stop being the boundary. Every other line here — the hop-by-hop headers, `duplex: "half"`,
23
+ * `redirect: "manual"`, not buffering the body — is the sort of thing that is either right or
24
+ * produces a symptom three layers away, and none of it is a product's idea of its own domain.
25
+ *
26
+ * ## What it answers without asking upstream
27
+ *
28
+ * - **401** when there is no session, or when the renewal was refused. There is nothing to
29
+ * forward: a request with no credential is not the resource server's to refuse.
30
+ * - **403** when the request is not same-site. `same-site.ts` carries why the package owes this.
31
+ *
32
+ * ## Renewal happens here, because here is somewhere a cookie can be set
33
+ *
34
+ * `authToken` renews when the access token is within a minute of expiry and hands back the
35
+ * `Set-Cookie` that carries the rotated session; this attaches it to the proxied response. That is
36
+ * the whole of the answer to *"the token lives an hour and the cookie lives eight"* — the seven
37
+ * hours in between stop being seven hours of 401s that nothing recovers from.
38
+ */
39
+ export interface AuthProxyConfig extends AuthSessionConfig {
40
+ /** Where the upstream lives. A path here is a prefix: `https://reports.internal/v1`. */
41
+ readonly target: string;
42
+ /** Where this route file is mounted. Stripped from the path before the rest is appended to `target`. */
43
+ readonly basePath: string;
44
+ /** Seconds of remaining lifetime below which the access token is renewed. Default 60. */
45
+ readonly renewWithin?: number;
46
+ }
47
+ /**
48
+ * The upstream is reached with the global `fetch`, and **not** with the inherited `IssuerConfig`
49
+ * one, which is a distinction worth a sentence because it is one field away from being invisible.
50
+ * That `fetch` exists to reach the *identity provider* — it is the seam `internalOrigin` uses to
51
+ * come at Keycloak from inside a cluster. A deployment that set it and found its resource-server
52
+ * traffic going the same way would have every right to be surprised, so there is one name for one
53
+ * transport and the other one is the platform's.
54
+ */
55
+ export interface AuthProxyHandlers {
56
+ GET(request: Request): Promise<Response>;
57
+ POST(request: Request): Promise<Response>;
58
+ PUT(request: Request): Promise<Response>;
59
+ PATCH(request: Request): Promise<Response>;
60
+ DELETE(request: Request): Promise<Response>;
61
+ HEAD(request: Request): Promise<Response>;
62
+ OPTIONS(request: Request): Promise<Response>;
63
+ }
64
+ export declare function authProxy(config: AuthProxyConfig): AuthProxyHandlers;
65
+ //# sourceMappingURL=next-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-proxy.d.ts","sourceRoot":"","sources":["../src/next-proxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAIxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wGAAwG;IACxG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;GAOG;AAEH,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1C,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1C,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9C;AAmDD,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,iBAAiB,CA0EpE"}
@@ -0,0 +1,64 @@
1
+ import { authToken as k } from "./next-token.js";
2
+ import { isSameSite as y } from "./same-site.js";
3
+ import { AuthError as R } from "./types.js";
4
+ const w = [
5
+ "connection",
6
+ "keep-alive",
7
+ "proxy-authenticate",
8
+ "proxy-authorization",
9
+ "te",
10
+ "trailer",
11
+ "transfer-encoding",
12
+ "upgrade"
13
+ ], g = [...w, "cookie", "authorization", "host", "content-length", "accept-encoding"], x = [...w, "set-cookie", "content-encoding", "content-length"];
14
+ function T(n, i) {
15
+ const o = new Headers(n);
16
+ for (const a of i) o.delete(a);
17
+ return o;
18
+ }
19
+ function b(n) {
20
+ const i = k(n), o = (...e) => globalThis.fetch(...e), a = n.basePath.replace(/\/$/, ""), u = new URL(n.target), E = u.pathname.replace(/\/$/, ""), r = (e) => new Response(null, { status: e, headers: { "cache-control": "no-store" } }), t = async (e) => {
21
+ if (!y(e)) return r(403);
22
+ const l = new URL(e.url);
23
+ if (!l.pathname.startsWith(a)) return r(400);
24
+ const d = new URL(u.href);
25
+ d.pathname = `${E}${l.pathname.slice(a.length)}`, d.search = l.search;
26
+ let s;
27
+ try {
28
+ s = await i(e.headers.get("cookie"), { renewWithin: n.renewWithin });
29
+ } catch (h) {
30
+ if (!(h instanceof R)) throw h;
31
+ return r(401);
32
+ }
33
+ if (s === null) return r(401);
34
+ const p = T(e.headers, g);
35
+ p.set("authorization", `Bearer ${s.accessToken}`);
36
+ const m = e.method === "GET" || e.method === "HEAD" ? null : e.body, c = await o(d, {
37
+ method: e.method,
38
+ headers: p,
39
+ body: m,
40
+ // The body is a stream and is forwarded as one: an upload is not read into this server's
41
+ // memory on its way past, and a server-sent event stream is not buffered until it ends —
42
+ // which for an SSE endpoint means never. `duplex` is what Node requires to allow it.
43
+ ...m === null ? {} : { duplex: "half" },
44
+ // A 302 from the upstream is the upstream's answer and belongs to the caller. Following it
45
+ // here would send the bearer token to whatever host the `Location` names.
46
+ redirect: "manual"
47
+ }), f = T(c.headers, x);
48
+ for (const h of s.cookies) f.append("set-cookie", h);
49
+ return new Response(c.body, { status: c.status, statusText: c.statusText, headers: f });
50
+ };
51
+ return {
52
+ GET: t,
53
+ POST: t,
54
+ PUT: t,
55
+ PATCH: t,
56
+ DELETE: t,
57
+ HEAD: t,
58
+ OPTIONS: t
59
+ };
60
+ }
61
+ export {
62
+ b as authProxy
63
+ };
64
+ //# sourceMappingURL=next-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-proxy.js","sources":["../src/next-proxy.ts"],"sourcesContent":["import { authToken } from \"./next-token\";\nimport type { AuthSessionConfig } from \"./next-session\";\nimport { isSameSite } from \"./same-site\";\nimport { AuthError } from \"./types\";\n\n/**\n * The BFF half of the *token-mediating backend*: the browser's request goes out again carrying a\n * bearer token, and the cookie that got it here stops at this line.\n *\n * ```ts\n * // app/api/data/[...path]/route.ts\n * export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } = authProxy({\n * issuer, clientId, clientSecret, secret,\n * basePath: \"/api/data\",\n * target: \"https://reports.internal/v1\",\n * });\n * ```\n *\n * ## Why this is in the package and not in the product\n *\n * Because it is the same ninety lines every time, and one of them is load-bearing in a way that\n * does not look it. **The `cookie` header must not be forwarded.** Leave it on and the resource\n * server receives a second credential — the session cookie — alongside the bearer token it asked\n * for, which is precisely the confusion the BFF pattern exists to remove: from then on a bug at\n * the far end can act as the person rather than as the token, and the token's scope and lifetime\n * stop being the boundary. Every other line here — the hop-by-hop headers, `duplex: \"half\"`,\n * `redirect: \"manual\"`, not buffering the body — is the sort of thing that is either right or\n * produces a symptom three layers away, and none of it is a product's idea of its own domain.\n *\n * ## What it answers without asking upstream\n *\n * - **401** when there is no session, or when the renewal was refused. There is nothing to\n * forward: a request with no credential is not the resource server's to refuse.\n * - **403** when the request is not same-site. `same-site.ts` carries why the package owes this.\n *\n * ## Renewal happens here, because here is somewhere a cookie can be set\n *\n * `authToken` renews when the access token is within a minute of expiry and hands back the\n * `Set-Cookie` that carries the rotated session; this attaches it to the proxied response. That is\n * the whole of the answer to *\"the token lives an hour and the cookie lives eight\"* — the seven\n * hours in between stop being seven hours of 401s that nothing recovers from.\n */\n\nexport interface AuthProxyConfig extends AuthSessionConfig {\n /** Where the upstream lives. A path here is a prefix: `https://reports.internal/v1`. */\n readonly target: string;\n /** Where this route file is mounted. Stripped from the path before the rest is appended to `target`. */\n readonly basePath: string;\n /** Seconds of remaining lifetime below which the access token is renewed. Default 60. */\n readonly renewWithin?: number;\n}\n\n/**\n * The upstream is reached with the global `fetch`, and **not** with the inherited `IssuerConfig`\n * one, which is a distinction worth a sentence because it is one field away from being invisible.\n * That `fetch` exists to reach the *identity provider* — it is the seam `internalOrigin` uses to\n * come at Keycloak from inside a cluster. A deployment that set it and found its resource-server\n * traffic going the same way would have every right to be surprised, so there is one name for one\n * transport and the other one is the platform's.\n */\n\nexport interface AuthProxyHandlers {\n GET(request: Request): Promise<Response>;\n POST(request: Request): Promise<Response>;\n PUT(request: Request): Promise<Response>;\n PATCH(request: Request): Promise<Response>;\n DELETE(request: Request): Promise<Response>;\n HEAD(request: Request): Promise<Response>;\n OPTIONS(request: Request): Promise<Response>;\n}\n\n/**\n * Headers that describe **this** connection and not the message, per RFC 9110 §7.6.1.\n *\n * Forwarding them is how a proxy promises an upstream a connection it does not have. `te` and\n * `trailer` are here for the same reason the others are, and `upgrade` matters most: a forwarded\n * `Upgrade: websocket` invites an answer this handler has no way to complete.\n */\nconst HOP_BY_HOP = [\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n];\n\n/**\n * What is stripped from the browser's request on the way out, beyond the hop-by-hop set.\n *\n * - `cookie` — the whole point, above.\n * - `authorization` — ours is the only one that may be on this request; a caller's own would\n * otherwise decide which of the two the upstream reads.\n * - `host` — it names this server, not the upstream's, and `fetch` sets the right one.\n * - `content-length` — the body is re-streamed, and a length that survived a re-encode would be a\n * lie the transport has to discover.\n * - `accept-encoding` — the client's compression negotiation is with *us*; `fetch` runs its own\n * with the upstream and hands back a decoded body. Passing this on is how a response comes back\n * labelled `gzip` and already decompressed, which no browser recovers from.\n */\nconst NOT_FORWARDED = [...HOP_BY_HOP, \"cookie\", \"authorization\", \"host\", \"content-length\", \"accept-encoding\"];\n\n/**\n * What is stripped from the upstream's answer.\n *\n * `set-cookie` is the one worth the sentence: under this pattern the browser's cookie relationship\n * is with the BFF alone, and a resource server that could set a cookie on this origin could set\n * one named like ours. `content-encoding` and `content-length` go because `fetch` already decoded\n * the body, so both now describe a representation that no longer exists.\n */\nconst NOT_RETURNED = [...HOP_BY_HOP, \"set-cookie\", \"content-encoding\", \"content-length\"];\n\nfunction copyHeaders(from: Headers, without: readonly string[]): Headers {\n const headers = new Headers(from);\n for (const name of without) headers.delete(name);\n return headers;\n}\n\nexport function authProxy(config: AuthProxyConfig): AuthProxyHandlers {\n const token = authToken(config);\n const send = (...args: Parameters<typeof globalThis.fetch>) => globalThis.fetch(...args);\n const base = config.basePath.replace(/\\/$/, \"\");\n const target = new URL(config.target);\n /** `https://api.test` has pathname `/`, and a prefix of `/` would double every separator. */\n const prefix = target.pathname.replace(/\\/$/, \"\");\n\n const refuse = (status: number) =>\n new Response(null, { status, headers: { \"cache-control\": \"no-store\" } });\n\n const handle = async (request: Request): Promise<Response> => {\n if (!isSameSite(request)) return refuse(403);\n\n const url = new URL(request.url);\n\n // A path outside the mount is not one this handler was mounted for, and refusing it *is* the\n // traversal check: the URL parser has already resolved every dot segment — `..` and its\n // percent-encoded spellings alike, which is the parser's job and not a thing to re-implement\n // — so a path that tried to climb has already fallen out of the prefix by the time it is read.\n if (!url.pathname.startsWith(base)) return refuse(400);\n\n // Assigning `pathname` rather than composing a string: a path beginning `//` parsed as a *URL*\n // is protocol-relative and names another host, and `//evil.test/x` is a path a browser will\n // happily send. Set as a component it cannot reach the origin at all.\n const upstream = new URL(target.href);\n upstream.pathname = `${prefix}${url.pathname.slice(base.length)}`;\n upstream.search = url.search;\n\n let held: Awaited<ReturnType<typeof token>>;\n try {\n held = await token(request.headers.get(\"cookie\"), { renewWithin: config.renewWithin });\n } catch (error) {\n // A refused renewal is the end of the session and not an upstream failure. Anything else is\n // a fault this module has no reading of, and hiding it behind a 401 would send a person to\n // sign in again over a misconfiguration that will still be there when they get back.\n if (!(error instanceof AuthError)) throw error;\n return refuse(401);\n }\n if (held === null) return refuse(401);\n\n const headers = copyHeaders(request.headers, NOT_FORWARDED);\n headers.set(\"authorization\", `Bearer ${held.accessToken}`);\n\n const body = request.method === \"GET\" || request.method === \"HEAD\" ? null : request.body;\n const answer = await send(upstream, {\n method: request.method,\n headers,\n body,\n // The body is a stream and is forwarded as one: an upload is not read into this server's\n // memory on its way past, and a server-sent event stream is not buffered until it ends —\n // which for an SSE endpoint means never. `duplex` is what Node requires to allow it.\n ...(body === null ? {} : { duplex: \"half\" }),\n // A 302 from the upstream is the upstream's answer and belongs to the caller. Following it\n // here would send the bearer token to whatever host the `Location` names.\n redirect: \"manual\",\n } as RequestInit);\n\n const out = copyHeaders(answer.headers, NOT_RETURNED);\n // Ours last, so a renewal is never lost to an upstream that had opinions about cookies.\n for (const cookie of held.cookies) out.append(\"set-cookie\", cookie);\n\n return new Response(answer.body, { status: answer.status, statusText: answer.statusText, headers: out });\n };\n\n return {\n GET: handle,\n POST: handle,\n PUT: handle,\n PATCH: handle,\n DELETE: handle,\n HEAD: handle,\n OPTIONS: handle,\n };\n}\n"],"names":["HOP_BY_HOP","NOT_FORWARDED","NOT_RETURNED","copyHeaders","from","without","headers","name","authProxy","config","token","authToken","send","args","base","target","prefix","refuse","status","handle","request","isSameSite","url","upstream","held","error","AuthError","body","answer","out","cookie"],"mappings":";;;AA8EA,MAAMA,IAAa;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAeMC,IAAgB,CAAC,GAAGD,GAAY,UAAU,iBAAiB,QAAQ,kBAAkB,iBAAiB,GAUtGE,IAAe,CAAC,GAAGF,GAAY,cAAc,oBAAoB,gBAAgB;AAEvF,SAASG,EAAYC,GAAeC,GAAqC;AACvE,QAAMC,IAAU,IAAI,QAAQF,CAAI;AAChC,aAAWG,KAAQF,EAAS,CAAAC,EAAQ,OAAOC,CAAI;AAC/C,SAAOD;AACT;AAEO,SAASE,EAAUC,GAA4C;AACpE,QAAMC,IAAQC,EAAUF,CAAM,GACxBG,IAAO,IAAIC,MAA8C,WAAW,MAAM,GAAGA,CAAI,GACjFC,IAAOL,EAAO,SAAS,QAAQ,OAAO,EAAE,GACxCM,IAAS,IAAI,IAAIN,EAAO,MAAM,GAE9BO,IAASD,EAAO,SAAS,QAAQ,OAAO,EAAE,GAE1CE,IAAS,CAACC,MACd,IAAI,SAAS,MAAM,EAAE,QAAAA,GAAQ,SAAS,EAAE,iBAAiB,WAAA,GAAc,GAEnEC,IAAS,OAAOC,MAAwC;AAC5D,QAAI,CAACC,EAAWD,CAAO,EAAG,QAAOH,EAAO,GAAG;AAE3C,UAAMK,IAAM,IAAI,IAAIF,EAAQ,GAAG;AAM/B,QAAI,CAACE,EAAI,SAAS,WAAWR,CAAI,EAAG,QAAOG,EAAO,GAAG;AAKrD,UAAMM,IAAW,IAAI,IAAIR,EAAO,IAAI;AACpC,IAAAQ,EAAS,WAAW,GAAGP,CAAM,GAAGM,EAAI,SAAS,MAAMR,EAAK,MAAM,CAAC,IAC/DS,EAAS,SAASD,EAAI;AAEtB,QAAIE;AACJ,QAAI;AACF,MAAAA,IAAO,MAAMd,EAAMU,EAAQ,QAAQ,IAAI,QAAQ,GAAG,EAAE,aAAaX,EAAO,YAAA,CAAa;AAAA,IACvF,SAASgB,GAAO;AAId,UAAI,EAAEA,aAAiBC,GAAY,OAAMD;AACzC,aAAOR,EAAO,GAAG;AAAA,IACnB;AACA,QAAIO,MAAS,KAAM,QAAOP,EAAO,GAAG;AAEpC,UAAMX,IAAUH,EAAYiB,EAAQ,SAASnB,CAAa;AAC1D,IAAAK,EAAQ,IAAI,iBAAiB,UAAUkB,EAAK,WAAW,EAAE;AAEzD,UAAMG,IAAOP,EAAQ,WAAW,SAASA,EAAQ,WAAW,SAAS,OAAOA,EAAQ,MAC9EQ,IAAS,MAAMhB,EAAKW,GAAU;AAAA,MAClC,QAAQH,EAAQ;AAAA,MAChB,SAAAd;AAAA,MACA,MAAAqB;AAAA;AAAA;AAAA;AAAA,MAIA,GAAIA,MAAS,OAAO,CAAA,IAAK,EAAE,QAAQ,OAAA;AAAA;AAAA;AAAA,MAGnC,UAAU;AAAA,IAAA,CACI,GAEVE,IAAM1B,EAAYyB,EAAO,SAAS1B,CAAY;AAEpD,eAAW4B,KAAUN,EAAK,QAAS,CAAAK,EAAI,OAAO,cAAcC,CAAM;AAElE,WAAO,IAAI,SAASF,EAAO,MAAM,EAAE,QAAQA,EAAO,QAAQ,YAAYA,EAAO,YAAY,SAASC,GAAK;AAAA,EACzG;AAEA,SAAO;AAAA,IACL,KAAKV;AAAA,IACL,MAAMA;AAAA,IACN,KAAKA;AAAA,IACL,OAAOA;AAAA,IACP,QAAQA;AAAA,IACR,MAAMA;AAAA,IACN,SAASA;AAAA,EAAA;AAEb;"}
@@ -1 +1 @@
1
- {"version":3,"file":"next-routes.d.ts","sourceRoot":"","sources":["../src/next-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAkCxD,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC3C;AAwDD,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CA+DtE"}
1
+ {"version":3,"file":"next-routes.d.ts","sourceRoot":"","sources":["../src/next-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AA6CxD,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC3C;AAwDD,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAiGtE"}