@kanzo-tech/auth 0.29.1 → 0.30.1

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 (89) hide show
  1. package/README.md +36 -23
  2. package/dist/bff-auth.d.ts +1 -2
  3. package/dist/bff-auth.d.ts.map +1 -1
  4. package/dist/bff-auth.js +82 -77
  5. package/dist/bff-auth.js.map +1 -1
  6. package/dist/can.d.ts +13 -9
  7. package/dist/can.d.ts.map +1 -1
  8. package/dist/can.js +6 -6
  9. package/dist/can.js.map +1 -1
  10. package/dist/gate.d.ts +4 -4
  11. package/dist/gate.d.ts.map +1 -1
  12. package/dist/gate.js +8 -9
  13. package/dist/gate.js.map +1 -1
  14. package/dist/index.d.ts +11 -19
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +17 -23
  17. package/dist/index.js.map +1 -1
  18. package/dist/issuer.d.ts +16 -0
  19. package/dist/issuer.d.ts.map +1 -1
  20. package/dist/issuer.js +43 -25
  21. package/dist/issuer.js.map +1 -1
  22. package/dist/next-auth.d.ts +66 -0
  23. package/dist/next-auth.d.ts.map +1 -0
  24. package/dist/next-auth.js +43 -0
  25. package/dist/next-auth.js.map +1 -0
  26. package/dist/next-bound.d.ts +89 -0
  27. package/dist/next-bound.d.ts.map +1 -0
  28. package/dist/next-bound.js +48 -0
  29. package/dist/next-bound.js.map +1 -0
  30. package/dist/next-gate.d.ts +4 -0
  31. package/dist/next-gate.d.ts.map +1 -0
  32. package/dist/next-gate.js +74 -0
  33. package/dist/next-gate.js.map +1 -0
  34. package/dist/next-proxy.d.ts +15 -24
  35. package/dist/next-proxy.d.ts.map +1 -1
  36. package/dist/next-proxy.js +47 -39
  37. package/dist/next-proxy.js.map +1 -1
  38. package/dist/next-routes.d.ts +4 -21
  39. package/dist/next-routes.d.ts.map +1 -1
  40. package/dist/next-routes.js +83 -64
  41. package/dist/next-routes.js.map +1 -1
  42. package/dist/next.d.ts +16 -41
  43. package/dist/next.d.ts.map +1 -1
  44. package/dist/next.js +2 -10
  45. package/dist/next.js.map +1 -1
  46. package/dist/server.d.ts +60 -38
  47. package/dist/server.d.ts.map +1 -1
  48. package/dist/server.js +183 -159
  49. package/dist/server.js.map +1 -1
  50. package/dist/store.d.ts +102 -20
  51. package/dist/store.d.ts.map +1 -1
  52. package/dist/store.js +56 -21
  53. package/dist/store.js.map +1 -1
  54. package/dist/types.d.ts +40 -23
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/use-session.d.ts +6 -1
  58. package/dist/use-session.d.ts.map +1 -1
  59. package/dist/use-session.js +9 -7
  60. package/dist/use-session.js.map +1 -1
  61. package/package.json +11 -30
  62. package/dist/auth-fetch.d.ts +0 -44
  63. package/dist/auth-fetch.d.ts.map +0 -1
  64. package/dist/auth-fetch.js +0 -21
  65. package/dist/auth-fetch.js.map +0 -1
  66. package/dist/browser.d.ts +0 -61
  67. package/dist/browser.d.ts.map +0 -1
  68. package/dist/browser.js +0 -130
  69. package/dist/browser.js.map +0 -1
  70. package/dist/host.d.ts +0 -22
  71. package/dist/host.d.ts.map +0 -1
  72. package/dist/host.js +0 -10
  73. package/dist/host.js.map +0 -1
  74. package/dist/next-middleware.d.ts +0 -23
  75. package/dist/next-middleware.d.ts.map +0 -1
  76. package/dist/next-middleware.js +0 -23
  77. package/dist/next-middleware.js.map +0 -1
  78. package/dist/next-session.d.ts +0 -44
  79. package/dist/next-session.d.ts.map +0 -1
  80. package/dist/next-session.js +0 -11
  81. package/dist/next-session.js.map +0 -1
  82. package/dist/next-token.d.ts +0 -56
  83. package/dist/next-token.d.ts.map +0 -1
  84. package/dist/next-token.js +0 -9
  85. package/dist/next-token.js.map +0 -1
  86. package/dist/use-organization.d.ts +0 -22
  87. package/dist/use-organization.d.ts.map +0 -1
  88. package/dist/use-organization.js +0 -20
  89. package/dist/use-organization.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"use-session.js","sources":["../src/use-session.ts"],"sourcesContent":["\"use client\";\n\nimport { useContext } from \"react\";\nimport { AuthContext, type AuthContextValue } from \"./auth-context\";\n\n/**\n * Who is signed in, and the two things you can do about it.\n *\n * `status` is the field to branch on, not `session === null`: those are the same answer for\n * \"anonymous\" and \"we have not looked yet\", and drawing a sign-in prompt during the second is the\n * flicker every application with a session has shipped at least once. `\"failed\"` is the session\n * that could not be read, with what was thrown on `error`.\n */\nexport function useSession(): AuthContextValue & {\n readonly signIn: AuthContextValue[\"auth\"][\"signIn\"];\n readonly signOut: AuthContextValue[\"auth\"][\"signOut\"];\n} {\n const context = useContext(AuthContext);\n if (context === null) {\n throw new Error(\n \"useSession() was called outside <AuthProvider>. Wrap the application in one, passing the \" +\n \"auth it should use — browserAuth() for a SPA, bffAuth() where there is a server.\",\n );\n }\n\n return {\n ...context,\n signIn: context.auth.signIn,\n signOut: context.auth.signOut,\n };\n}\n"],"names":[],"mappings":";;;AAaO;AAIL;AACA;AACE;AAAU;AACR;AAKJ;AAAO;AACF;AACkB;AACC;AAE1B;;;;"}
1
+ {"version":3,"file":"use-session.js","sources":["../src/use-session.ts"],"sourcesContent":["\"use client\";\n\nimport { useContext } from \"react\";\nimport { AuthContext, type AuthContextValue } from \"./auth-context\";\nimport { can } from \"./can\";\n\n/**\n * Who is signed in, the two things you can do about it, and the role question asked here.\n *\n * `status` is the field to branch on, not `session === null`: those are the same answer for\n * \"anonymous\" and \"we have not looked yet\", and drawing a sign-in prompt during the second is the\n * flicker every application with a session has shipped at least once. `\"failed\"` is the session\n * that could not be read, with what was thrown on `error`.\n *\n * `can(role, organization?)` is {@link can} bound to this session, so it asks inside the current\n * tenant unless told which — Clerk's `useAuth().has()`. It decides what to draw, never what to\n * allow.\n */\nexport function useSession(): AuthContextValue & {\n readonly signIn: AuthContextValue[\"auth\"][\"signIn\"];\n readonly signOut: AuthContextValue[\"auth\"][\"signOut\"];\n readonly can: (role: string, organization?: string) => boolean;\n} {\n const context = useContext(AuthContext);\n if (context === null) {\n throw new Error(\n \"useSession() was called outside <AuthProvider>. Wrap the application in one, passing the \" +\n \"auth it should use — bffAuth() for a product whose server runs kanzoAuth.\",\n );\n }\n\n return {\n ...context,\n signIn: context.auth.signIn,\n signOut: context.auth.signOut,\n can: (role, organization) => can(context.session, role, organization),\n };\n}\n"],"names":[],"mappings":";;;;AAkBO;AAKL;AACA;AACE;AAAU;AACR;AAKJ;AAAO;AACF;AACkB;AACC;AAC8C;AAExE;;;;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kanzo-tech/auth",
3
- "version": "0.29.1",
4
- "description": "Kanzo authentication over Keycloak — the claim vocabulary read into one Session, the role evaluation that knows about organizations, and an authenticated fetch. Sibling of @kanzo-tech/ui, not part of it: the admission rules exclude auth from the generic vocabulary by name.",
3
+ "version": "0.30.1",
4
+ "description": "Kanzo authentication over Keycloak — a Backend For Frontend for Next (kanzoAuth), the claim vocabulary read into one Session, and the role evaluation that knows about organizations. Sibling of @kanzo-tech/ui, not part of it: the admission rules exclude auth from the generic vocabulary by name.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "sideEffects": false,
@@ -16,10 +16,6 @@
16
16
  "types": "./dist/index.d.ts",
17
17
  "import": "./dist/index.js"
18
18
  },
19
- "./browser": {
20
- "types": "./dist/browser.d.ts",
21
- "import": "./dist/browser.js"
22
- },
23
19
  "./server": {
24
20
  "types": "./dist/server.d.ts",
25
21
  "import": "./dist/server.js"
@@ -30,13 +26,12 @@
30
26
  },
31
27
  "./package.json": "./package.json"
32
28
  },
33
- "//peers": "react is REQUIRED: the root barrel exports a provider and hooks, and two copies of React are two context trees. Everything else is optional and lives behind a subpath, because the two deployment patterns in RFC 10017 need different engines and no consumer needs both — oidc-client-ts is the browser protocol behind `./browser`, openid-client and jose are the confidential client behind `./server`, and next is behind `./next`. A SPA installs this package alone. Deliberately absent: tailwind-variants, because nothing here draws — which is also what keeps this package out of the appearance guards, whose corpus in packages/ui/src/guard-corpus.ts selects on exactly that dependency. The day this package grows a screen the dependency arrives with it and the guards pick it up unasked; no list anywhere needs editing.",
29
+ "//peers": "react is REQUIRED: the root barrel exports a provider and hooks, and two copies of React are two context trees. Everything else is optional and lives behind a subpath: openid-client and jose are the confidential client behind `./server`, and next (16, whose proxy runs on Node and can therefore read and renew a session) is behind `./next`. The browser half, `bffAuth`, needs no engine at all. The public browser client and its oidc-client-ts peer were removed: RFC 10017 recommends the BFF for business applications and nothing consumed the other. Deliberately absent: tailwind-variants, because nothing here draws — which is also what keeps this package out of the appearance guards, whose corpus in packages/ui/src/guard-corpus.ts selects on exactly that dependency. The day this package grows a screen the dependency arrives with it and the guards pick it up unasked; no list anywhere needs editing.",
34
30
  "peerDependencies": {
35
31
  "jose": "^6",
36
- "next": ">=15",
37
- "oidc-client-ts": "^3",
32
+ "next": "^16.0.0",
38
33
  "openid-client": "^6",
39
- "react": ">=19"
34
+ "react": "^19.0.0"
40
35
  },
41
36
  "peerDependenciesMeta": {
42
37
  "jose": {
@@ -45,9 +40,6 @@
45
40
  "next": {
46
41
  "optional": true
47
42
  },
48
- "oidc-client-ts": {
49
- "optional": true
50
- },
51
43
  "openid-client": {
52
44
  "optional": true
53
45
  }
@@ -60,39 +52,27 @@
60
52
  "jose": "^6.2.12",
61
53
  "jsdom": "^29.1.1",
62
54
  "next": "^16.3.7",
63
- "oidc-client-ts": "^3.5.0",
64
55
  "openid-client": "^6.8.8",
65
56
  "react": "^19.0.0",
66
57
  "react-dom": "^19.0.0",
67
58
  "rollup-plugin-preserve-directives": "^0.4.0"
68
59
  },
69
- "//size-limit": "The root barrel was 677 B at the first commit — the claim reader, the role predicate and the types — against a 1 kB budget set deliberately tight, with a note saying the raise would be a line in the commit that landed the hooks rather than a quiet edit. This is that line: 677 B -> 2.19 kB, and the budget goes to 2.5 kB. What grew is the provider, three hooks, `Gate` and `bffAuth`, which is the whole of what a consumer imports to have a session. Every budget here is set just above its first measurement for the same reason: one with 70% headroom detects nothing. Each subpath ignores its own engine, because the consumer installs that explicitly and what is being measured is our adapter over it. What these numbers are really watching for is a door reaching through another: the root barrel must never learn a protocol, and the two Node doors must never learn React. A jump of kilobytes on any of them means one of those happened. Second raise, and it is a decision rather than a quiet edit: the session lifecycle — a refresh route, `authToken`, `authProxy`, `ticketStore` and the same-site check — moved the two Node doors, `server` 3.2 kB -> 3.4 kB (budget 3.6) and `next` 4 kB -> 4.59 kB (budget 5). What is being watched for is unchanged and still holds: neither door learned React and the barrel learned no protocol, which is why the other two numbers barely moved. Third raise, 2026-10-01, as a decision (Ángel; branch `feat/failure-guarantees`): the failure guarantees — a `failed` status carrying the thrown value, a 30 s deadline on every wait this package makes, the coded IdP and session failures and the problem-page redirect — moved all four: barrel 2.22 -> 2.6 kB (budget 2.75), `browser` 1.77 -> 2.1 kB (2.25), `server` 3.4 -> 3.73 kB (3.9), `next` 4.59 -> 5.03 kB (5.25). Same watch, still holding: no door learned another's engine; the growth is code paths that used to collapse into `anonymous` or a bare 500.",
60
+ "//size-limit": "The root barrel was 677 B at the first commit — the claim reader, the role predicate and the types — against a 1 kB budget set deliberately tight, with a note saying the raise would be a line in the commit that landed the hooks rather than a quiet edit. This is that line: 677 B -> 2.19 kB, and the budget goes to 2.5 kB. What grew is the provider, three hooks, `Gate` and `bffAuth`, which is the whole of what a consumer imports to have a session. Every budget here is set just above its first measurement for the same reason: one with 70% headroom detects nothing. Each subpath ignores its own engine, because the consumer installs that explicitly and what is being measured is our adapter over it. What these numbers are really watching for is a door reaching through another: the root barrel must never learn a protocol, and the two Node doors must never learn React. A jump of kilobytes on any of them means one of those happened. Second raise, and it is a decision rather than a quiet edit: the session lifecycle — a refresh route, `authToken`, `authProxy`, `ticketStore` and the same-site check — moved the two Node doors, `server` 3.2 kB -> 3.4 kB (budget 3.6) and `next` 4 kB -> 4.59 kB (budget 5). What is being watched for is unchanged and still holds: neither door learned React and the barrel learned no protocol, which is why the other two numbers barely moved. Third raise, 2026-10-01, as a decision (Ángel; branch `feat/failure-guarantees`): the failure guarantees — a `failed` status carrying the thrown value, a 30 s deadline on every wait this package makes, the coded IdP and session failures and the problem-page redirect — moved all four: barrel 2.22 -> 2.6 kB (budget 2.75), `browser` 1.77 -> 2.1 kB (2.25), `server` 3.4 -> 3.73 kB (3.9), `next` 4.59 -> 5.03 kB (5.25). Same watch, still holding: no door learned another's engine; the growth is code paths that used to collapse into `anonymous` or a bare 500. Fourth change, 2026-10-05, as a decision (branch `fix/auth-return-to`, `kanzoAuth`): `./browser` is gone with its peer, and `authFetch` and `useOrganization` left the barrel, so the barrel went down, 2.6 -> 2.37 kB (budget lowered to 2.5). The two Node doors went up: `server` 3.73 -> 4.75 kB (5) for back-channel logout, a transaction cookie per `state`, renewal in place with the cross-process re-read, and the store's `update`/`dropAll`; `next` 5.03 -> 6.65 kB (7) for that plus the proxy as session authority, which replaced a middleware that only looked for a cookie. Same watch, still holding: neither Node door learned React, and the barrel learned no protocol.",
70
61
  "size-limit": [
71
62
  {
72
63
  "name": "root barrel (JS)",
73
64
  "path": "dist/index.js",
74
- "limit": "2.75 kB",
65
+ "limit": "2.5 kB",
75
66
  "ignore": [
76
67
  "react",
77
68
  "react-dom",
78
69
  "react/jsx-runtime"
79
70
  ]
80
71
  },
81
- {
82
- "name": "browser subpath (JS)",
83
- "path": "dist/browser.js",
84
- "limit": "2.25 kB",
85
- "ignore": [
86
- "react",
87
- "react-dom",
88
- "react/jsx-runtime",
89
- "oidc-client-ts"
90
- ]
91
- },
92
72
  {
93
73
  "name": "server subpath (JS)",
94
74
  "path": "dist/server.js",
95
- "limit": "3.9 kB",
75
+ "limit": "5 kB",
96
76
  "ignore": [
97
77
  "react",
98
78
  "react-dom",
@@ -104,7 +84,7 @@
104
84
  {
105
85
  "name": "next subpath (JS)",
106
86
  "path": "dist/next.js",
107
- "limit": "5.25 kB",
87
+ "limit": "7 kB",
108
88
  "ignore": [
109
89
  "react",
110
90
  "react-dom",
@@ -113,7 +93,8 @@
113
93
  "jose",
114
94
  "next",
115
95
  "next/server",
116
- "next/headers"
96
+ "next/headers",
97
+ "next/navigation"
117
98
  ]
118
99
  }
119
100
  ],
@@ -1,44 +0,0 @@
1
- /**
2
- * A `fetch` that stays authenticated.
3
- *
4
- * This is the seam a product already has. A typed API client is often
5
- * `createClient({ baseUrl: "/" })` with one middleware; the viewer's trace source takes its
6
- * transport as a parameter. Handing either an authenticated `fetch` changes one line and no call
7
- * site — which is the point, because the alternative is every call site remembering a header.
8
- *
9
- * Auth0's SPA SDK ships the same shape for the same reason: `fetchWithAuth`, *"a drop-in replacement
10
- * for the Fetch API's `fetch()` method"* that builds the headers and handles the retries.
11
- */
12
- /**
13
- * Where a token comes from, and how to ask for a fresh one.
14
- *
15
- * Both are the implementation's business to make cheap and **single-flight** — see `singleFlight`.
16
- * `current` is called on every request, so it must answer from cache until the token is near
17
- * expiry. `null` from both is legitimate: it means there is no bearer token to attach.
18
- */
19
- export interface TokenSource {
20
- current(): Promise<string | null>;
21
- renew(): Promise<string | null>;
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;
35
- /**
36
- * Wrap a `fetch` so every request carries the session, and one stale token does not surface as a
37
- * failure the user has to see.
38
- *
39
- * The retry is **once**, and only on a 401 we sent a token for. Retrying a 403 would be wrong — that
40
- * is an answer, not a stale credential — and retrying twice turns an expired session into a loop
41
- * against the authorization server.
42
- */
43
- export declare function authFetch(source: TokenSource, base?: typeof globalThis.fetch): typeof globalThis.fetch;
44
- //# sourceMappingURL=auth-fetch.d.ts.map
@@ -1 +0,0 @@
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"}
@@ -1,21 +0,0 @@
1
- function c(r, n) {
2
- if (typeof Request < "u" && r instanceof Request && r.body !== null) return !1;
3
- const e = n == null ? void 0 : n.body;
4
- return e == null ? !0 : !(typeof ReadableStream < "u" && e instanceof ReadableStream);
5
- }
6
- function h(r, n = globalThis.fetch) {
7
- return async (e, t) => {
8
- const u = async (d) => {
9
- const l = (t == null ? void 0 : t.headers) ?? (typeof Request < "u" && e instanceof Request ? e.headers : void 0), f = new Headers(l);
10
- return d !== null && f.set("Authorization", `Bearer ${d}`), n(e, { ...t, headers: f });
11
- }, s = await r.current(), a = await u(s);
12
- if (a.status !== 401 || s === null || !c(e, t)) return a;
13
- const o = await r.renew();
14
- return o === null || o === s ? a : u(o);
15
- };
16
- }
17
- export {
18
- h as authFetch,
19
- c as isReplayable
20
- };
21
- //# sourceMappingURL=auth-fetch.js.map
@@ -1 +0,0 @@
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. A typed API client is often\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;"}
package/dist/browser.d.ts DELETED
@@ -1,61 +0,0 @@
1
- import { UserManagerSettings } from 'oidc-client-ts';
2
- import { Auth } from './types';
3
- /** What this adapter reads off the library's `User`. The access token is carried, never opened. */
4
- export interface OidcUser {
5
- readonly profile: unknown;
6
- readonly access_token: string;
7
- readonly expires_at?: number;
8
- readonly url_state?: string;
9
- }
10
- /**
11
- * The slice of `UserManager` this file uses.
12
- *
13
- * It exists so the manager can be injected, which is what lets everything below be tested with no
14
- * network, no IdP and no iframe. It is deliberately structural and deliberately small — and the
15
- * default factory returns a real `UserManager`, so the compiler is what keeps the slice honest.
16
- */
17
- export interface OidcUserManager {
18
- getUser(): Promise<OidcUser | null>;
19
- signinRedirect(args?: {
20
- scope?: string;
21
- url_state?: string;
22
- }): Promise<void>;
23
- signinCallback(url?: string): Promise<OidcUser | undefined>;
24
- signinSilent(): Promise<OidcUser | null>;
25
- signoutRedirect(args?: {
26
- post_logout_redirect_uri?: string;
27
- }): Promise<void>;
28
- readonly events: {
29
- addUserLoaded(cb: () => void): () => void;
30
- addUserUnloaded(cb: () => void): () => void;
31
- addAccessTokenExpired(cb: () => void): () => void;
32
- addUserSessionChanged(cb: () => void): () => void;
33
- };
34
- }
35
- export interface BrowserAuthConfig {
36
- /** The realm's issuer, e.g. `https://id.kanzo.tech/realms/kanzo`. */
37
- readonly issuer: string;
38
- /**
39
- * This application's Keycloak client id. It selects the credential and, through {@link claims},
40
- * which of the token's roles are this application's.
41
- */
42
- readonly clientId: string;
43
- /** Must be registered with the IdP. Defaults to the current origin. */
44
- readonly redirectUri?: string;
45
- /** Must be registered with the IdP. Defaults to the current origin. */
46
- readonly postLogoutRedirectUri?: string;
47
- /** Defaults to `openid profile email organization:*`. */
48
- readonly scope?: string;
49
- /**
50
- * Poll the OP's session state, so a sign-out performed elsewhere reaches this tab.
51
- *
52
- * Off by default because it cannot be relied on: it runs a hidden cross-site iframe, and a
53
- * browser that partitions third-party cookies — which is now most of them — makes it report
54
- * nothing rather than fail loudly. It is a knob and not a default for exactly that reason.
55
- */
56
- readonly monitorSession?: boolean;
57
- /** The seam the tests inject through. Defaults to the real `UserManager`. */
58
- readonly createManager?: (settings: UserManagerSettings) => OidcUserManager;
59
- }
60
- export declare function browserAuth(config: BrowserAuthConfig): Auth;
61
- //# sourceMappingURL=browser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EAAa,KAAK,IAAI,EAAoC,MAAM,SAAS,CAAC;AAiCjF,mGAAmG;AACnG,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACpC,cAAc,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,cAAc,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IAC5D,YAAY,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACzC,eAAe,CAAC,IAAI,CAAC,EAAE;QAAE,wBAAwB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,QAAQ,CAAC,MAAM,EAAE;QACf,aAAa,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;QAC1C,eAAe,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;QAC5C,qBAAqB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;QAClD,qBAAqB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;KACnD,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,yDAAyD;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,eAAe,CAAC;CAC7E;AA8CD,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CA6K3D"}
package/dist/browser.js DELETED
@@ -1,130 +0,0 @@
1
- import { WebStorageStateStore as g, InMemoryWebStorage as A, UserManager as I, ErrorResponse as _, ErrorTimeout as R } from "oidc-client-ts";
2
- import { authFetch as v } from "./auth-fetch.js";
3
- import { claims as x } from "./claims.js";
4
- import { DEADLINE as S } from "./deadline.js";
5
- import { singleFlight as m } from "./single-flight.js";
6
- import { AuthError as u } from "./types.js";
7
- const L = "openid profile email organization:*", P = ["code", "state", "session_state", "iss", "error", "error_description"], $ = /* @__PURE__ */ new Set([
8
- "login_required",
9
- "interaction_required",
10
- "consent_required",
11
- "account_selection_required",
12
- "invalid_grant"
13
- ]), k = "No matching state found in storage";
14
- function w(t, r) {
15
- return t instanceof R ? new u("idp/silent", `${r}: the IdP did not answer`, { after: S }, { cause: t }) : t instanceof _ ? new u("token/exchange-failed", `${r}: the IdP refused (${t.error})`, {}, { cause: t }) : new u("idp/unreachable", `${r}: the IdP could not be reached`, {}, { cause: t });
16
- }
17
- function q(t) {
18
- const r = new URLSearchParams(t);
19
- return r.has("state") && (r.has("code") || r.has("error"));
20
- }
21
- function N(t, r) {
22
- return r === void 0 ? t : [...t.split(" ").filter((s) => s.length > 0 && !s.startsWith("organization:")), `organization:${r}`].join(" ");
23
- }
24
- function B(t) {
25
- var p;
26
- const r = ((p = globalThis.location) == null ? void 0 : p.origin) ?? "", a = t.scope ?? L, s = {
27
- authority: t.issuer,
28
- client_id: t.clientId,
29
- redirect_uri: t.redirectUri ?? r,
30
- post_logout_redirect_uri: t.postLogoutRedirectUri ?? r,
31
- scope: a,
32
- response_type: "code",
33
- monitorSession: t.monitorSession ?? !1,
34
- // Unset, the library waits on discovery, the token endpoint and the JWKS forever; this also
35
- // becomes the silent renewal's iframe timeout.
36
- requestTimeoutInSeconds: S / 1e3,
37
- // **Tokens in memory only.** The library's default is `sessionStorage`, and RFC 10017 is
38
- // explicit that under a public client anything script can read is something an XSS can steal;
39
- // a token on disk outlives the page that was compromised to get it.
40
- //
41
- // The cost is real and is the whole of the trade: a full page load starts with nothing, so the
42
- // session is re-established by a silent renewal or a redirect rather than read off disk. That
43
- // is one round trip on every load, and a signed-out first paint for any application that draws
44
- // before `getSession()` settles.
45
- userStore: new g({ store: new A() }),
46
- // The PKCE verifier, `state` and `nonce` are **not** tokens and cannot be in memory: the page
47
- // is unloaded by the navigation to the IdP, and memory goes with it. `sessionStorage` rather
48
- // than the library's `localStorage` default — per tab, gone when the tab closes, and never
49
- // visible to a second tab racing the same sign-in.
50
- stateStore: new g({ store: globalThis.sessionStorage })
51
- }, o = (t.createManager ?? ((e) => new I(e)))(s), d = (e) => {
52
- const n = new URL(globalThis.location.href);
53
- for (const U of P) n.searchParams.delete(U);
54
- const i = e === void 0 ? n : new URL(e, n.origin), l = i.origin === n.origin ? i : n;
55
- globalThis.history.replaceState(null, "", `${l.pathname}${l.search}${l.hash}`);
56
- }, b = async () => {
57
- if (!q(globalThis.location.search)) return null;
58
- try {
59
- const e = await o.signinCallback(globalThis.location.href) ?? null;
60
- return d(e == null ? void 0 : e.url_state), e;
61
- } catch (e) {
62
- if (d(void 0), e instanceof Error && e.message === k) return null;
63
- throw w(e, "the sign-in could not be completed");
64
- }
65
- };
66
- let h = !1;
67
- const y = m(async () => {
68
- if (!h) {
69
- const e = await b();
70
- if (h = !0, e !== null) return e;
71
- }
72
- return o.getUser();
73
- }), c = m(async () => {
74
- try {
75
- return await o.signinSilent();
76
- } catch (e) {
77
- if (e instanceof _ && e.error !== null && $.has(e.error))
78
- return null;
79
- throw w(e, "the session could not be renewed");
80
- }
81
- }), E = (e) => e.expires_at !== void 0 && e.expires_at * 1e3 <= Date.now(), f = async () => {
82
- const e = await y();
83
- return e === null || E(e) ? c() : e;
84
- }, T = (e) => {
85
- const n = x(e.profile, { clientId: t.clientId });
86
- return e.expires_at === void 0 ? n : { ...n, expiresAt: e.expires_at * 1e3 };
87
- };
88
- return {
89
- async getSession() {
90
- const e = await f();
91
- return e === null ? null : T(e);
92
- },
93
- subscribe(e) {
94
- const n = [
95
- o.events.addUserLoaded(e),
96
- o.events.addUserUnloaded(e),
97
- o.events.addAccessTokenExpired(e),
98
- o.events.addUserSessionChanged(e)
99
- ];
100
- return () => {
101
- for (const i of n) i();
102
- };
103
- },
104
- async signIn(e = {}) {
105
- await o.signinRedirect({
106
- scope: N(a, e.organization),
107
- url_state: e.returnTo ?? globalThis.location.href
108
- });
109
- },
110
- async signOut(e = {}) {
111
- await o.signoutRedirect({
112
- post_logout_redirect_uri: e.returnTo ?? t.postLogoutRedirectUri
113
- });
114
- },
115
- fetch: v({
116
- async current() {
117
- var e;
118
- return ((e = await f()) == null ? void 0 : e.access_token) ?? null;
119
- },
120
- async renew() {
121
- var e;
122
- return ((e = await c()) == null ? void 0 : e.access_token) ?? null;
123
- }
124
- })
125
- };
126
- }
127
- export {
128
- B as browserAuth
129
- };
130
- //# sourceMappingURL=browser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"browser.js","sources":["../src/browser.ts"],"sourcesContent":["import {\n ErrorResponse,\n ErrorTimeout,\n InMemoryWebStorage,\n UserManager,\n WebStorageStateStore,\n type UserManagerSettings,\n} from \"oidc-client-ts\";\nimport { authFetch, type TokenSource } from \"./auth-fetch\";\nimport { claims } from \"./claims\";\nimport { DEADLINE } from \"./deadline\";\nimport { singleFlight } from \"./single-flight\";\nimport { AuthError, type Auth, type Session, type SignInOptions } from \"./types\";\n\n/**\n * `./browser` — RFC 10017's *browser-based OAuth 2.0 client*: a public client with PKCE, for the\n * SPA that has no server to hold a confidential one. `agents/viewer` and `hub/browser` are both\n * that shape, and for them it is the only architecture available.\n *\n * **This file is an adapter, not an implementation.** PKCE, silent renewal, storage, the callback\n * and RP-initiated logout are `oidc-client-ts`'s; a `UserManager` goes in and our `Auth` comes out.\n * The reference evaluation picked that library precisely so none of the protocol would be written\n * here, because a hand-written flow is where a mistake stops being a bug and becomes a\n * vulnerability. What is genuinely ours is the four things no library can have: Keycloak's claim\n * vocabulary read into one `Session`, the organization scope, the single-flight renewal, and one\n * `Session` across both deployment patterns.\n *\n * There is no `\"use client\"` here, and that is the rule rather than an oversight: nothing in this\n * module is stateful and no browser global is read at module scope. The listeners are registered\n * inside `subscribe`, and `location` inside the factory.\n */\n\n/**\n * `organization:*`, not `organization`.\n *\n * Plain `organization` returns the single organization when there is one and **prompts for a\n * choice** when there are several — which is the documented behaviour behind the reports of the\n * claim \"disappearing\" for multi-organization users. It never disappeared; the selection was\n * missing. A single-tenant product pays nothing for the star: the claim is simply absent.\n */\nconst DEFAULT_SCOPE = \"openid profile email organization:*\";\n\n/** The query parameters an authorization response puts in the URL, and that must not stay there. */\nconst RESPONSE_PARAMS = [\"code\", \"state\", \"session_state\", \"iss\", \"error\", \"error_description\"];\n\n/** What this adapter reads off the library's `User`. The access token is carried, never opened. */\nexport interface OidcUser {\n readonly profile: unknown;\n readonly access_token: string;\n readonly expires_at?: number;\n readonly url_state?: string;\n}\n\n/**\n * The slice of `UserManager` this file uses.\n *\n * It exists so the manager can be injected, which is what lets everything below be tested with no\n * network, no IdP and no iframe. It is deliberately structural and deliberately small — and the\n * default factory returns a real `UserManager`, so the compiler is what keeps the slice honest.\n */\nexport interface OidcUserManager {\n getUser(): Promise<OidcUser | null>;\n signinRedirect(args?: { scope?: string; url_state?: string }): Promise<void>;\n signinCallback(url?: string): Promise<OidcUser | undefined>;\n signinSilent(): Promise<OidcUser | null>;\n signoutRedirect(args?: { post_logout_redirect_uri?: string }): Promise<void>;\n readonly events: {\n addUserLoaded(cb: () => void): () => void;\n addUserUnloaded(cb: () => void): () => void;\n addAccessTokenExpired(cb: () => void): () => void;\n addUserSessionChanged(cb: () => void): () => void;\n };\n}\n\nexport interface BrowserAuthConfig {\n /** The realm's issuer, e.g. `https://id.kanzo.tech/realms/kanzo`. */\n readonly issuer: string;\n /**\n * This application's Keycloak client id. It selects the credential and, through {@link claims},\n * which of the token's roles are this application's.\n */\n readonly clientId: string;\n /** Must be registered with the IdP. Defaults to the current origin. */\n readonly redirectUri?: string;\n /** Must be registered with the IdP. Defaults to the current origin. */\n readonly postLogoutRedirectUri?: string;\n /** Defaults to `openid profile email organization:*`. */\n readonly scope?: string;\n /**\n * Poll the OP's session state, so a sign-out performed elsewhere reaches this tab.\n *\n * Off by default because it cannot be relied on: it runs a hidden cross-site iframe, and a\n * browser that partitions third-party cookies — which is now most of them — makes it report\n * nothing rather than fail loudly. It is a knob and not a default for exactly that reason.\n */\n readonly monitorSession?: boolean;\n /** The seam the tests inject through. Defaults to the real `UserManager`. */\n readonly createManager?: (settings: UserManagerSettings) => OidcUserManager;\n}\n\n/**\n * The answers to `prompt=none` and to a refresh-token grant that mean \"there is no session here\":\n * the IdP holds none, or the refresh token is gone or rotated out. Every other failure is the IdP\n * refusing the client, not answering, or not being reached, and is thrown.\n */\nconst NO_SESSION = new Set([\n \"login_required\",\n \"interaction_required\",\n \"consent_required\",\n \"account_selection_required\",\n \"invalid_grant\",\n]);\n\n/**\n * `oidc-client-ts`'s own words for a callback whose transaction this tab does not hold — a reloaded\n * callback URL, or a `state` that expired. Pinned against the real library in `browser.test.ts`.\n */\nconst STALE_STATE = \"No matching state found in storage\";\n\n/** A failure from the IdP, coded by what happened: no answer, a refusal, or no reaching it. */\nfunction failure(error: unknown, doing: string): AuthError {\n if (error instanceof ErrorTimeout) {\n return new AuthError(\"idp/silent\", `${doing}: the IdP did not answer`, { after: DEADLINE }, { cause: error });\n }\n if (error instanceof ErrorResponse) {\n return new AuthError(\"token/exchange-failed\", `${doing}: the IdP refused (${error.error})`, {}, { cause: error });\n }\n return new AuthError(\"idp/unreachable\", `${doing}: the IdP could not be reached`, {}, { cause: error });\n}\n\n/** `code` or `error`, together with `state`. Nothing else in a URL is an authorization response. */\nfunction isAuthorizationResponse(search: string): boolean {\n const params = new URLSearchParams(search);\n return params.has(\"state\") && (params.has(\"code\") || params.has(\"error\"));\n}\n\nfunction scopeFor(scope: string, organization: string | undefined): string {\n if (organization === undefined) return scope;\n // Replace rather than append: `organization:* organization:acme` asks for both, and the point of\n // naming one is to get one.\n const rest = scope.split(\" \").filter((s) => s.length > 0 && !s.startsWith(\"organization:\"));\n return [...rest, `organization:${organization}`].join(\" \");\n}\n\nexport function browserAuth(config: BrowserAuthConfig): Auth {\n const origin = globalThis.location?.origin ?? \"\";\n const scope = config.scope ?? DEFAULT_SCOPE;\n\n const settings: UserManagerSettings = {\n authority: config.issuer,\n client_id: config.clientId,\n redirect_uri: config.redirectUri ?? origin,\n post_logout_redirect_uri: config.postLogoutRedirectUri ?? origin,\n scope,\n response_type: \"code\",\n monitorSession: config.monitorSession ?? false,\n // Unset, the library waits on discovery, the token endpoint and the JWKS forever; this also\n // becomes the silent renewal's iframe timeout.\n requestTimeoutInSeconds: DEADLINE / 1000,\n\n // **Tokens in memory only.** The library's default is `sessionStorage`, and RFC 10017 is\n // explicit that under a public client anything script can read is something an XSS can steal;\n // a token on disk outlives the page that was compromised to get it.\n //\n // The cost is real and is the whole of the trade: a full page load starts with nothing, so the\n // session is re-established by a silent renewal or a redirect rather than read off disk. That\n // is one round trip on every load, and a signed-out first paint for any application that draws\n // before `getSession()` settles.\n userStore: new WebStorageStateStore({ store: new InMemoryWebStorage() }),\n\n // The PKCE verifier, `state` and `nonce` are **not** tokens and cannot be in memory: the page\n // is unloaded by the navigation to the IdP, and memory goes with it. `sessionStorage` rather\n // than the library's `localStorage` default — per tab, gone when the tab closes, and never\n // visible to a second tab racing the same sign-in.\n stateStore: new WebStorageStateStore({ store: globalThis.sessionStorage }),\n };\n\n const manager = (config.createManager ?? ((s) => new UserManager(s)))(settings);\n\n /** Take the spent response out of the URL, and honour `returnTo` if it is ours to honour. */\n const restore = (returnTo: string | undefined): void => {\n const here = new URL(globalThis.location.href);\n for (const key of RESPONSE_PARAMS) here.searchParams.delete(key);\n const wanted = returnTo === undefined ? here : new URL(returnTo, here.origin);\n // `url_state` round-trips through the IdP, so following it anywhere would make our own callback\n // the bounce in an open redirect. Same origin, or we stay where we are.\n const url = wanted.origin === here.origin ? wanted : here;\n globalThis.history.replaceState(null, \"\", `${url.pathname}${url.search}${url.hash}`);\n };\n\n /**\n * Finish a redirect the IdP has just sent back.\n *\n * The URL is cleaned either way. An authorization code is single-use, and one left in the address\n * bar reaches history, bookmarks and the `Referer` — and replays as an error on the next reload.\n * Cleaning it is also what makes the next call after a failure find no response to finish.\n */\n const complete = async (): Promise<OidcUser | null> => {\n if (!isAuthorizationResponse(globalThis.location.search)) return null;\n try {\n const user = (await manager.signinCallback(globalThis.location.href)) ?? null;\n restore(user?.url_state);\n return user;\n } catch (error) {\n restore(undefined);\n // A stale state entry or a reloaded callback URL is \"not signed in\", not an exception for\n // every caller of `getSession` to handle. Anything else is the IdP's failure and is thrown.\n if (error instanceof Error && error.message === STALE_STATE) return null;\n throw failure(error, \"the sign-in could not be completed\");\n }\n };\n\n let completed = false;\n\n const load = singleFlight(async (): Promise<OidcUser | null> => {\n if (!completed) {\n const user = await complete();\n completed = true;\n if (user !== null) return user;\n }\n return manager.getUser();\n });\n\n /**\n * One renewal at a time, shared by every caller — the most load-bearing line in this file, and\n * the failure `singleFlight` exists for: under the rotation RFC 10017 requires, a second renewal\n * replays a token the first already spent.\n */\n const renew = singleFlight(async (): Promise<OidcUser | null> => {\n try {\n return await manager.signinSilent();\n } catch (error) {\n // The refresh token gone or rotated out, or the IdP session ended, all mean \"no session\", and\n // the answer is the same one `getUser` gives for it.\n if (error instanceof ErrorResponse && error.error !== null && NO_SESSION.has(error.error)) {\n return null;\n }\n throw failure(error, \"the session could not be renewed\");\n }\n });\n\n const isExpired = (user: OidcUser): boolean =>\n user.expires_at !== undefined && user.expires_at * 1000 <= Date.now();\n\n /**\n * The user — renewed when the token it carries has expired, and **asked for when there is no\n * user at all**.\n *\n * That second case is the whole of what makes memory-only storage liveable, and it was missing:\n * this returned `null` the moment `load()` did, so every reload signed the person out while the\n * comment on `userStore` promised a silent renewal would pick them back up. Memory-only is a\n * defensible trade against XSS *because* the IdP still holds the session and a `prompt=none`\n * round trip recovers it; without that round trip it is just a session that dies on F5, which no\n * product would accept and which no amount of XSS resistance would buy back.\n *\n * The cost is the one the `userStore` comment already names: an anonymous cold load spends a\n * failed `prompt=none` too. `renew()` answers `null` for the IdP's `login_required` rather than\n * throwing, so the caller sees the same \"not signed in\" it saw before — one round trip later.\n */\n const fresh = async (): Promise<OidcUser | null> => {\n const user = await load();\n if (user === null) return renew();\n return isExpired(user) ? renew() : user;\n };\n\n const toSession = (user: OidcUser): Session => {\n const session = claims(user.profile, { clientId: config.clientId });\n if (user.expires_at === undefined) return session;\n // The access token's expiry comes from the token *response*, not from opening the token —\n // which stays opaque. It is the credential we attach, so it is the one worth counting down.\n return { ...session, expiresAt: user.expires_at * 1000 };\n };\n\n const source: TokenSource = {\n async current() {\n return (await fresh())?.access_token ?? null;\n },\n async renew() {\n return (await renew())?.access_token ?? null;\n },\n };\n\n return {\n async getSession() {\n const user = await fresh();\n return user === null ? null : toSession(user);\n },\n\n subscribe(onChange) {\n const detach = [\n manager.events.addUserLoaded(onChange),\n manager.events.addUserUnloaded(onChange),\n manager.events.addAccessTokenExpired(onChange),\n manager.events.addUserSessionChanged(onChange),\n ];\n return () => {\n for (const off of detach) off();\n };\n },\n\n async signIn(options: SignInOptions = {}) {\n await manager.signinRedirect({\n scope: scopeFor(scope, options.organization),\n url_state: options.returnTo ?? globalThis.location.href,\n });\n },\n\n async signOut(options = {}) {\n // RP-initiated logout through the library, which discovers `end_session_endpoint` and sends\n // the `id_token_hint`. A hand-built URL gets both wrong and neither failure is visible.\n await manager.signoutRedirect({\n post_logout_redirect_uri: options.returnTo ?? config.postLogoutRedirectUri,\n });\n },\n\n fetch: authFetch(source),\n };\n}\n"],"names":["DEFAULT_SCOPE","RESPONSE_PARAMS","NO_SESSION","STALE_STATE","failure","error","doing","ErrorTimeout","AuthError","DEADLINE","ErrorResponse","isAuthorizationResponse","search","params","scopeFor","scope","organization","browserAuth","config","origin","_a","settings","WebStorageStateStore","InMemoryWebStorage","manager","s","UserManager","restore","returnTo","here","key","wanted","url","complete","user","completed","load","singleFlight","renew","isExpired","fresh","toSession","session","claims","onChange","detach","off","options","authFetch"],"mappings":"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"}
package/dist/host.d.ts DELETED
@@ -1,22 +0,0 @@
1
- /**
2
- * The organization a hostname addresses — a **hint**, never an authority.
3
- *
4
- * This file has no `"use client"` and takes no globals, and both are deliberate: it is a string
5
- * function, so it is testable without a browser and readable on a server.
6
- *
7
- * The rule it must not be mistaken for: **the host is chosen by whoever made the request.** Anyone
8
- * can send `Host: acme.kanzo.tech`. What makes an organization real for a session is the
9
- * `organization` claim in a token Keycloak signed, so every consumer of this resolves the answer
10
- * against `session.organizations` and treats a miss as "not a member" — which is what
11
- * `useOrganization` does and what the server's own scope extractor must do before it opens a
12
- * database.
13
- */
14
- /**
15
- * The leading label, when the hostname has one to spare.
16
- *
17
- * `acme.kanzo.tech` → `acme`, and `kanzo.tech` → `undefined`, because a two-label host is the site
18
- * itself rather than a tenant of it. `acme.localhost` → `acme` as the one exception, since that is
19
- * how a developer reaches a tenant without editing DNS. An IP address yields nothing.
20
- */
21
- export declare function organizationFromHost(hostname: string | undefined): string | undefined;
22
- //# sourceMappingURL=host.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAYrF"}
package/dist/host.js DELETED
@@ -1,10 +0,0 @@
1
- function o(t) {
2
- if (t === void 0 || t.length === 0 || /^\[|^\d+\.\d+\.\d+\.\d+$/.test(t)) return;
3
- const r = t.split("."), [i, n] = r;
4
- if (!(i === void 0 || i.length === 0) && (r.length >= 3 || r.length === 2 && n === "localhost"))
5
- return i;
6
- }
7
- export {
8
- o as organizationFromHost
9
- };
10
- //# sourceMappingURL=host.js.map
package/dist/host.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"host.js","sources":["../src/host.ts"],"sourcesContent":["/**\n * The organization a hostname addresses — a **hint**, never an authority.\n *\n * This file has no `\"use client\"` and takes no globals, and both are deliberate: it is a string\n * function, so it is testable without a browser and readable on a server.\n *\n * The rule it must not be mistaken for: **the host is chosen by whoever made the request.** Anyone\n * can send `Host: acme.kanzo.tech`. What makes an organization real for a session is the\n * `organization` claim in a token Keycloak signed, so every consumer of this resolves the answer\n * against `session.organizations` and treats a miss as \"not a member\" — which is what\n * `useOrganization` does and what the server's own scope extractor must do before it opens a\n * database.\n */\n\n/**\n * The leading label, when the hostname has one to spare.\n *\n * `acme.kanzo.tech` → `acme`, and `kanzo.tech` → `undefined`, because a two-label host is the site\n * itself rather than a tenant of it. `acme.localhost` → `acme` as the one exception, since that is\n * how a developer reaches a tenant without editing DNS. An IP address yields nothing.\n */\nexport function organizationFromHost(hostname: string | undefined): string | undefined {\n if (hostname === undefined || hostname.length === 0) return undefined;\n // An IPv4 literal or a bracketed IPv6 has no labels to read.\n if (/^\\[|^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(hostname)) return undefined;\n\n const labels = hostname.split(\".\");\n const [first, second] = labels;\n if (first === undefined || first.length === 0) return undefined;\n\n if (labels.length >= 3) return first;\n if (labels.length === 2 && second === \"localhost\") return first;\n return undefined;\n}\n"],"names":["organizationFromHost","hostname","labels","first","second"],"mappings":"AAqBO,SAASA,EAAqBC,GAAkD;AAGrF,MAFIA,MAAa,UAAaA,EAAS,WAAW,KAE9C,2BAA2B,KAAKA,CAAQ,EAAG;AAE/C,QAAMC,IAASD,EAAS,MAAM,GAAG,GAC3B,CAACE,GAAOC,CAAM,IAAIF;AACxB,MAAI,EAAAC,MAAU,UAAaA,EAAM,WAAW,OAExCD,EAAO,UAAU,KACjBA,EAAO,WAAW,KAAKE,MAAW;AAAa,WAAOD;AAE5D;"}
@@ -1,23 +0,0 @@
1
- import { NextResponse, NextRequest } from 'next/server';
2
- export interface AuthMiddlewareConfig {
3
- /**
4
- * Path prefixes that need no session — a health probe, a marketing page, a legal notice.
5
- *
6
- * Matched on segment boundaries, so `/health` exempts `/health` and `/health/live` and does
7
- * **not** exempt `/healthcare`. A plain `startsWith` is one keystroke away from opening a route
8
- * nobody meant to open.
9
- */
10
- readonly public?: readonly string[];
11
- /** Where `authRoutes` is mounted. Always public — it is how a person signs in. Default `/api/auth`. */
12
- readonly basePath?: string;
13
- /**
14
- * The `problemPage` given to `authRoutes`. Always public — whoever is sent there has no session,
15
- * and sending them to sign in instead is a loop when signing in is what failed. Default
16
- * `/auth/problem`.
17
- */
18
- readonly problemPage?: string;
19
- /** Default `__Host-kanzo-session`. Set it only if `relyingParty` was given a different cookie. */
20
- readonly cookieName?: string;
21
- }
22
- export declare function authMiddleware(config?: AuthMiddlewareConfig): (request: NextRequest) => NextResponse;
23
- //# sourceMappingURL=next-middleware.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"next-middleware.d.ts","sourceRoot":"","sources":["../src/next-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAoD7D,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,uGAAuG;IACvG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kGAAkG;IAClG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAOD,wBAAgB,cAAc,CAC5B,MAAM,GAAE,oBAAyB,GAChC,CAAC,OAAO,EAAE,WAAW,KAAK,YAAY,CAsBxC"}
@@ -1,23 +0,0 @@
1
- import { NextResponse as r } from "next/server";
2
- const u = "__Host-kanzo-session", p = "/auth/problem";
3
- function m(e) {
4
- return e.slice(e.lastIndexOf("/") + 1).includes(".");
5
- }
6
- function x(e = {}) {
7
- const s = (e.basePath ?? "/api/auth").replace(/\/$/, ""), c = e.cookieName ?? u, i = [s, e.problemPage ?? p, ...e.public ?? []].map(
8
- (t) => t.replace(/\/$/, "")
9
- );
10
- return (t) => {
11
- const { pathname: n, search: l } = t.nextUrl;
12
- if (m(n)) return r.next();
13
- if (i.some((o) => n === o || n.startsWith(`${o}/`)))
14
- return r.next();
15
- if (t.cookies.has(c)) return r.next();
16
- const a = new URL(`${s}/signin`, t.nextUrl);
17
- return a.searchParams.set("returnTo", `${n}${l}`), r.redirect(a);
18
- };
19
- }
20
- export {
21
- x as authMiddleware
22
- };
23
- //# sourceMappingURL=next-middleware.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"next-middleware.js","sources":["../src/next-middleware.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from \"next/server\";\n\n/**\n * The edge middleware that sends an anonymous browser to the sign-in route.\n *\n * ```ts\n * // middleware.ts\n * export const middleware = authMiddleware({ public: [\"/health\"] });\n * ```\n *\n * ## It checks for presence, and nothing else\n *\n * The cookie is sealed with JWE, and this file never opens it — no key, no `jose`, no store. Two\n * reasons, and the second is the one that matters:\n *\n * 1. Middleware runs on every request, including the ones that are about to be answered from a\n * cache. Decrypting there buys a redirect decision that a route handler and `authSession` are\n * both going to make again, properly, a millisecond later.\n * 2. **A redirect is not an authorization.** Letting a request past here grants nothing: the page\n * behind it reads the session itself, and the resource server behind *that* validates an access\n * token. A forged cookie gets someone as far as a page that will find no session and say so.\n * Treating this as the check is how a middleware becomes load-bearing and then gets edited by\n * someone who does not know it is.\n *\n * So this module imports nothing from the package. That is deliberate and it has a cost: the\n * cookie's name is written here a second time, and `next-middleware.test.ts` ties the two together\n * by signing in through a fake realm and asserting the default is the name `relyingParty` actually\n * emitted. A transcribed constant with no test is how the number in a generated fixture goes stale.\n *\n * ## The matcher\n *\n * A `middleware.ts` also exports its own `config.matcher`, and what belongs in it was learned the\n * expensive way: a matcher that missed static files sent `/fossil/fossil_wasm_bg.wasm` to the\n * sign-in page, and the app loaded without its WebAssembly. The exemption is applied *here* as\n * well, on the path, so it holds whatever matcher a consumer writes — a fix for the class rather\n * than for the regexp. The matcher a product starts from:\n *\n * ```ts\n * export const config = { matcher: [\"/((?!_next/static|_next/image|favicon.ico|.*\\\\..*).*)\"] };\n * ```\n */\n\n/**\n * The cookie `relyingParty` issues: `sealedCookie` prefixes every name with `__Host-`.\n *\n * Held by \"defaults to the cookie name relyingParty actually issues\" in `next-middleware.test.ts`.\n */\nconst SESSION_COOKIE = \"__Host-kanzo-session\";\n\n/** `authRoutes`'s default `problemPage`, repeated because importing it would put `openid-client` on the edge. */\nconst PROBLEM_PAGE = \"/auth/problem\";\n\nexport interface AuthMiddlewareConfig {\n /**\n * Path prefixes that need no session — a health probe, a marketing page, a legal notice.\n *\n * Matched on segment boundaries, so `/health` exempts `/health` and `/health/live` and does\n * **not** exempt `/healthcare`. A plain `startsWith` is one keystroke away from opening a route\n * nobody meant to open.\n */\n readonly public?: readonly string[];\n /** Where `authRoutes` is mounted. Always public — it is how a person signs in. Default `/api/auth`. */\n readonly basePath?: string;\n /**\n * The `problemPage` given to `authRoutes`. Always public — whoever is sent there has no session,\n * and sending them to sign in instead is a loop when signing in is what failed. Default\n * `/auth/problem`.\n */\n readonly problemPage?: string;\n /** Default `__Host-kanzo-session`. Set it only if `relyingParty` was given a different cookie. */\n readonly cookieName?: string;\n}\n\n/** A path whose last segment carries a dot: a static file, not a page. */\nfunction isFile(pathname: string): boolean {\n return pathname.slice(pathname.lastIndexOf(\"/\") + 1).includes(\".\");\n}\n\nexport function authMiddleware(\n config: AuthMiddlewareConfig = {},\n): (request: NextRequest) => NextResponse {\n const base = (config.basePath ?? \"/api/auth\").replace(/\\/$/, \"\");\n const cookieName = config.cookieName ?? SESSION_COOKIE;\n const open = [base, config.problemPage ?? PROBLEM_PAGE, ...(config.public ?? [])].map((prefix) =>\n prefix.replace(/\\/$/, \"\"),\n );\n\n return (request) => {\n const { pathname, search } = request.nextUrl;\n\n if (isFile(pathname)) return NextResponse.next();\n if (open.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) {\n return NextResponse.next();\n }\n if (request.cookies.has(cookieName)) return NextResponse.next();\n\n const away = new URL(`${base}/signin`, request.nextUrl);\n // Where they were going, so the callback can put them back. `authRoutes` confines it to this\n // origin before sealing it, which is the check this line is relying on rather than repeating.\n away.searchParams.set(\"returnTo\", `${pathname}${search}`);\n return NextResponse.redirect(away);\n };\n}\n"],"names":["SESSION_COOKIE","PROBLEM_PAGE","isFile","pathname","authMiddleware","config","base","cookieName","open","prefix","request","search","NextResponse","away"],"mappings":";AA+CA,MAAMA,IAAiB,wBAGjBC,IAAe;AAwBrB,SAASC,EAAOC,GAA2B;AACzC,SAAOA,EAAS,MAAMA,EAAS,YAAY,GAAG,IAAI,CAAC,EAAE,SAAS,GAAG;AACnE;AAEO,SAASC,EACdC,IAA+B,IACS;AACxC,QAAMC,KAAQD,EAAO,YAAY,aAAa,QAAQ,OAAO,EAAE,GACzDE,IAAaF,EAAO,cAAcL,GAClCQ,IAAO,CAACF,GAAMD,EAAO,eAAeJ,GAAc,GAAII,EAAO,UAAU,CAAA,CAAG,EAAE;AAAA,IAAI,CAACI,MACrFA,EAAO,QAAQ,OAAO,EAAE;AAAA,EAAA;AAG1B,SAAO,CAACC,MAAY;AAClB,UAAM,EAAE,UAAAP,GAAU,QAAAQ,EAAA,IAAWD,EAAQ;AAErC,QAAIR,EAAOC,CAAQ,EAAG,QAAOS,EAAa,KAAA;AAC1C,QAAIJ,EAAK,KAAK,CAACC,MAAWN,MAAaM,KAAUN,EAAS,WAAW,GAAGM,CAAM,GAAG,CAAC;AAChF,aAAOG,EAAa,KAAA;AAEtB,QAAIF,EAAQ,QAAQ,IAAIH,CAAU,EAAG,QAAOK,EAAa,KAAA;AAEzD,UAAMC,IAAO,IAAI,IAAI,GAAGP,CAAI,WAAWI,EAAQ,OAAO;AAGtD,WAAAG,EAAK,aAAa,IAAI,YAAY,GAAGV,CAAQ,GAAGQ,CAAM,EAAE,GACjDC,EAAa,SAASC,CAAI;AAAA,EACnC;AACF;"}
@@ -1,44 +0,0 @@
1
- import { RelyingPartyConfig } from './server';
2
- import { Session } from './types';
3
- /**
4
- * The session a React Server Component can read, without a request object in hand.
5
- *
6
- * An RSC is handed no `Request`; `next/headers` is how it reaches the one it is rendering for, and
7
- * that import is what makes this module — and only this module of the three — Node-only. It is
8
- * also what earns the whole door its place on a subpath: *a part belongs on a subpath only if it
9
- * imports that subpath's engine.*
10
- *
11
- * ## Why a factory, when the call site is `await getSession()`
12
- *
13
- * The call site is preserved exactly; what moved is where the binding is made. A
14
- * zero-argument import would have to find its secret somewhere ambient — an environment variable
15
- * this package would then be naming and documenting forever — and it could never be given a
16
- * {@link SessionStore}, which is an object and not a string. So the consumer binds it once, in the
17
- * same module that already holds the config it hands {@link authRoutes}:
18
- *
19
- * ```ts
20
- * // auth.ts
21
- * export const getSession = authSession({ issuer, clientId, clientSecret, secret });
22
- * ```
23
- *
24
- * and every server component writes `await getSession()`. That also makes the three names of this
25
- * door one shape — `authRoutes`, `authSession`, `authMiddleware` are all factories over a config —
26
- * rather than two factories and an exception.
27
- *
28
- * ## `cache`, and what it is for
29
- *
30
- * React's `cache` scopes memoization to one request, so a page that asks in a layout, in a
31
- * breadcrumb and in a menu unseals the cookie once. A host that wraps its own reader does it for
32
- * exactly this reason. **It is dormant outside a React request scope** — `cache`
33
- * with no dispatcher simply calls through — which is why the test beside this file asserts the
34
- * answers and not the number of reads.
35
- */
36
- /**
37
- * What reading a session needs, which is strictly less than signing one in.
38
- *
39
- * `redirectUri` is absent because no authorization request is built here: {@link RelyingParty.read}
40
- * unseals a cookie and asks the store, and neither of those has a browser to send anywhere.
41
- */
42
- export type AuthSessionConfig = Omit<RelyingPartyConfig, "redirectUri">;
43
- export declare function authSession(config: AuthSessionConfig): () => Promise<Session | null>;
44
- //# sourceMappingURL=next-session.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"next-session.d.ts","sourceRoot":"","sources":["../src/next-session.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,CAAC;AAExE,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAOpF"}