@dloizides/auth-client 2.0.0 → 3.0.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.
@@ -0,0 +1,127 @@
1
+ import { H as HttpClient, T as TokenResponse } from '../TokenResponse-CY1CaU2l.mjs';
2
+
3
+ /**
4
+ * OIDC discovery document fetcher.
5
+ *
6
+ * Fetches `{issuer}/.well-known/openid-configuration` and caches the result
7
+ * per-issuer for the lifetime of the process. Discovery responses are stable
8
+ * for hours; the cache prevents the auth flow from hitting KC on every login.
9
+ *
10
+ * Pure (no React, no hooks). Consumed by app-side hooks that orchestrate the
11
+ * PKCE flow.
12
+ */
13
+
14
+ /**
15
+ * Subset of the OIDC Discovery 1.0 metadata the auth-client needs.
16
+ *
17
+ * The KC discovery doc carries many more fields; we type only what the PKCE
18
+ * flow consumes to keep the surface small and to fail loudly when KC ever
19
+ * stops returning one of these.
20
+ */
21
+ interface OidcDiscoveryDocument {
22
+ issuer: string;
23
+ authorization_endpoint: string;
24
+ token_endpoint: string;
25
+ end_session_endpoint?: string;
26
+ userinfo_endpoint?: string;
27
+ jwks_uri?: string;
28
+ }
29
+ interface FetchDiscoveryDocumentInput {
30
+ /** Issuer URL — `{baseUrl}/realms/{realm}`. Trailing slash tolerated. */
31
+ issuerUrl: string;
32
+ /** Transport. Pass `createFetchHttpClient(fetch)` in browser/Node18+. */
33
+ http: HttpClient;
34
+ }
35
+ /**
36
+ * Fetch + cache the OIDC discovery document for an issuer.
37
+ *
38
+ * Cache key = normalized issuer URL (trailing slash stripped).
39
+ *
40
+ * @throws Error when the HTTP call fails, returns non-2xx, or returns a body
41
+ * missing required OIDC metadata fields.
42
+ */
43
+ declare function fetchDiscoveryDocument(input: FetchDiscoveryDocumentInput): Promise<OidcDiscoveryDocument>;
44
+ /**
45
+ * Clear the per-issuer discovery cache. Test-only — production code does not
46
+ * call this. Useful when a test mocks different metadata across cases.
47
+ */
48
+ declare function clearDiscoveryCache(): void;
49
+
50
+ /**
51
+ * PKCE (RFC 7636) primitives for the OIDC authorization-code flow.
52
+ *
53
+ * Pure (no React). Browser-compatible — uses `crypto.subtle` for SHA-256 and
54
+ * `crypto.getRandomValues` for the verifier. Node 16+ exposes both via
55
+ * `globalThis.crypto`.
56
+ */
57
+ /**
58
+ * Generate a cryptographically random PKCE code_verifier.
59
+ *
60
+ * Default length 64 sits well inside the RFC 7636 43..128 band.
61
+ *
62
+ * @throws Error when `length` falls outside the RFC band.
63
+ */
64
+ declare function generateCodeVerifier(length?: number): string;
65
+ /**
66
+ * Derive the S256 code_challenge from a code_verifier.
67
+ *
68
+ * `code_challenge = BASE64URL(SHA256(code_verifier))` — RFC 7636 §4.2.
69
+ *
70
+ * @throws Error when `verifier` is shorter than 43 or longer than 128 chars.
71
+ */
72
+ declare function deriveCodeChallenge(verifier: string): Promise<string>;
73
+ interface PkcePair {
74
+ codeVerifier: string;
75
+ codeChallenge: string;
76
+ codeChallengeMethod: 'S256';
77
+ }
78
+ /**
79
+ * Convenience: produce a fresh verifier + matching challenge in one call.
80
+ */
81
+ declare function generatePkcePair(length?: number): Promise<PkcePair>;
82
+
83
+ /**
84
+ * OIDC token-endpoint helpers.
85
+ *
86
+ * Pure (no React, no hooks). Wraps the realm-aware token endpoint with the
87
+ * PKCE `authorization_code` and `refresh_token` grants. The transport is
88
+ * injected so callers can use the HTTP client of their choice.
89
+ *
90
+ * Use these from app-side hooks (e.g. `useKeycloakExchange`) instead of
91
+ * duplicating the body-builder + POST + normalise dance.
92
+ */
93
+
94
+ interface ExchangeAuthorizationCodeInput {
95
+ http: HttpClient;
96
+ baseUrl: string;
97
+ realm: string;
98
+ clientId: string;
99
+ code: string;
100
+ redirectUri: string;
101
+ codeVerifier: string;
102
+ }
103
+ interface RefreshAccessTokenInput {
104
+ http: HttpClient;
105
+ baseUrl: string;
106
+ realm: string;
107
+ clientId: string;
108
+ refreshToken: string;
109
+ }
110
+ /**
111
+ * Exchange a PKCE authorization `code` for tokens via the realm's token
112
+ * endpoint (`grant_type=authorization_code`).
113
+ *
114
+ * @throws Error when the HTTP call returns non-2xx or the body is missing
115
+ * `access_token`.
116
+ */
117
+ declare function exchangeAuthorizationCode(input: ExchangeAuthorizationCodeInput): Promise<TokenResponse>;
118
+ /**
119
+ * Swap a refresh token for a fresh access/refresh-token pair via the realm's
120
+ * token endpoint (`grant_type=refresh_token`).
121
+ *
122
+ * @throws Error when the HTTP call returns non-2xx or the body is missing
123
+ * `access_token`.
124
+ */
125
+ declare function refreshAccessToken(input: RefreshAccessTokenInput): Promise<TokenResponse>;
126
+
127
+ export { type ExchangeAuthorizationCodeInput, type FetchDiscoveryDocumentInput, type OidcDiscoveryDocument, type PkcePair, type RefreshAccessTokenInput, clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken };
@@ -0,0 +1,127 @@
1
+ import { H as HttpClient, T as TokenResponse } from '../TokenResponse-CY1CaU2l.js';
2
+
3
+ /**
4
+ * OIDC discovery document fetcher.
5
+ *
6
+ * Fetches `{issuer}/.well-known/openid-configuration` and caches the result
7
+ * per-issuer for the lifetime of the process. Discovery responses are stable
8
+ * for hours; the cache prevents the auth flow from hitting KC on every login.
9
+ *
10
+ * Pure (no React, no hooks). Consumed by app-side hooks that orchestrate the
11
+ * PKCE flow.
12
+ */
13
+
14
+ /**
15
+ * Subset of the OIDC Discovery 1.0 metadata the auth-client needs.
16
+ *
17
+ * The KC discovery doc carries many more fields; we type only what the PKCE
18
+ * flow consumes to keep the surface small and to fail loudly when KC ever
19
+ * stops returning one of these.
20
+ */
21
+ interface OidcDiscoveryDocument {
22
+ issuer: string;
23
+ authorization_endpoint: string;
24
+ token_endpoint: string;
25
+ end_session_endpoint?: string;
26
+ userinfo_endpoint?: string;
27
+ jwks_uri?: string;
28
+ }
29
+ interface FetchDiscoveryDocumentInput {
30
+ /** Issuer URL — `{baseUrl}/realms/{realm}`. Trailing slash tolerated. */
31
+ issuerUrl: string;
32
+ /** Transport. Pass `createFetchHttpClient(fetch)` in browser/Node18+. */
33
+ http: HttpClient;
34
+ }
35
+ /**
36
+ * Fetch + cache the OIDC discovery document for an issuer.
37
+ *
38
+ * Cache key = normalized issuer URL (trailing slash stripped).
39
+ *
40
+ * @throws Error when the HTTP call fails, returns non-2xx, or returns a body
41
+ * missing required OIDC metadata fields.
42
+ */
43
+ declare function fetchDiscoveryDocument(input: FetchDiscoveryDocumentInput): Promise<OidcDiscoveryDocument>;
44
+ /**
45
+ * Clear the per-issuer discovery cache. Test-only — production code does not
46
+ * call this. Useful when a test mocks different metadata across cases.
47
+ */
48
+ declare function clearDiscoveryCache(): void;
49
+
50
+ /**
51
+ * PKCE (RFC 7636) primitives for the OIDC authorization-code flow.
52
+ *
53
+ * Pure (no React). Browser-compatible — uses `crypto.subtle` for SHA-256 and
54
+ * `crypto.getRandomValues` for the verifier. Node 16+ exposes both via
55
+ * `globalThis.crypto`.
56
+ */
57
+ /**
58
+ * Generate a cryptographically random PKCE code_verifier.
59
+ *
60
+ * Default length 64 sits well inside the RFC 7636 43..128 band.
61
+ *
62
+ * @throws Error when `length` falls outside the RFC band.
63
+ */
64
+ declare function generateCodeVerifier(length?: number): string;
65
+ /**
66
+ * Derive the S256 code_challenge from a code_verifier.
67
+ *
68
+ * `code_challenge = BASE64URL(SHA256(code_verifier))` — RFC 7636 §4.2.
69
+ *
70
+ * @throws Error when `verifier` is shorter than 43 or longer than 128 chars.
71
+ */
72
+ declare function deriveCodeChallenge(verifier: string): Promise<string>;
73
+ interface PkcePair {
74
+ codeVerifier: string;
75
+ codeChallenge: string;
76
+ codeChallengeMethod: 'S256';
77
+ }
78
+ /**
79
+ * Convenience: produce a fresh verifier + matching challenge in one call.
80
+ */
81
+ declare function generatePkcePair(length?: number): Promise<PkcePair>;
82
+
83
+ /**
84
+ * OIDC token-endpoint helpers.
85
+ *
86
+ * Pure (no React, no hooks). Wraps the realm-aware token endpoint with the
87
+ * PKCE `authorization_code` and `refresh_token` grants. The transport is
88
+ * injected so callers can use the HTTP client of their choice.
89
+ *
90
+ * Use these from app-side hooks (e.g. `useKeycloakExchange`) instead of
91
+ * duplicating the body-builder + POST + normalise dance.
92
+ */
93
+
94
+ interface ExchangeAuthorizationCodeInput {
95
+ http: HttpClient;
96
+ baseUrl: string;
97
+ realm: string;
98
+ clientId: string;
99
+ code: string;
100
+ redirectUri: string;
101
+ codeVerifier: string;
102
+ }
103
+ interface RefreshAccessTokenInput {
104
+ http: HttpClient;
105
+ baseUrl: string;
106
+ realm: string;
107
+ clientId: string;
108
+ refreshToken: string;
109
+ }
110
+ /**
111
+ * Exchange a PKCE authorization `code` for tokens via the realm's token
112
+ * endpoint (`grant_type=authorization_code`).
113
+ *
114
+ * @throws Error when the HTTP call returns non-2xx or the body is missing
115
+ * `access_token`.
116
+ */
117
+ declare function exchangeAuthorizationCode(input: ExchangeAuthorizationCodeInput): Promise<TokenResponse>;
118
+ /**
119
+ * Swap a refresh token for a fresh access/refresh-token pair via the realm's
120
+ * token endpoint (`grant_type=refresh_token`).
121
+ *
122
+ * @throws Error when the HTTP call returns non-2xx or the body is missing
123
+ * `access_token`.
124
+ */
125
+ declare function refreshAccessToken(input: RefreshAccessTokenInput): Promise<TokenResponse>;
126
+
127
+ export { type ExchangeAuthorizationCodeInput, type FetchDiscoveryDocumentInput, type OidcDiscoveryDocument, type PkcePair, type RefreshAccessTokenInput, clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken };
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ // src/oidc/discovery.ts
4
+ var cache = /* @__PURE__ */ new Map();
5
+ function normalizeIssuer(issuerUrl) {
6
+ return issuerUrl.replace(/\/$/, "");
7
+ }
8
+ function isOidcDiscoveryDocument(data) {
9
+ if (data === null || typeof data !== "object") {
10
+ return false;
11
+ }
12
+ const d = data;
13
+ return typeof d.issuer === "string" && d.issuer !== "" && typeof d.authorization_endpoint === "string" && d.authorization_endpoint !== "" && typeof d.token_endpoint === "string" && d.token_endpoint !== "";
14
+ }
15
+ async function fetchDiscoveryDocument(input) {
16
+ const key = normalizeIssuer(input.issuerUrl);
17
+ const cached = cache.get(key);
18
+ if (cached !== void 0) {
19
+ return cached;
20
+ }
21
+ const response = await input.http({
22
+ url: `${key}/.well-known/openid-configuration`,
23
+ method: "GET"
24
+ });
25
+ if (!response.ok) {
26
+ throw new Error(
27
+ `OIDC discovery failed: ${String(response.status)} for ${key}`
28
+ );
29
+ }
30
+ if (!isOidcDiscoveryDocument(response.data)) {
31
+ throw new Error(`OIDC discovery returned invalid metadata for ${key}`);
32
+ }
33
+ cache.set(key, response.data);
34
+ return response.data;
35
+ }
36
+ function clearDiscoveryCache() {
37
+ cache.clear();
38
+ }
39
+
40
+ // src/oidc/pkce.ts
41
+ var VERIFIER_MIN_LENGTH = 43;
42
+ var VERIFIER_MAX_LENGTH = 128;
43
+ var DEFAULT_VERIFIER_LENGTH = 64;
44
+ var RANDOM_BYTES_PER_CHAR = 1;
45
+ var UNRESERVED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
46
+ function getCrypto() {
47
+ const c = globalThis.crypto;
48
+ if (c === void 0 || c.subtle === void 0) {
49
+ throw new Error("pkce: globalThis.crypto.subtle is required (Node 16+ / modern browser)");
50
+ }
51
+ return c;
52
+ }
53
+ function assertVerifierLength(length) {
54
+ if (length < VERIFIER_MIN_LENGTH || length > VERIFIER_MAX_LENGTH) {
55
+ throw new Error(`pkce: code_verifier length must be ${String(VERIFIER_MIN_LENGTH)}-${String(VERIFIER_MAX_LENGTH)} chars (RFC 7636)`);
56
+ }
57
+ }
58
+ function base64UrlEncode(buffer) {
59
+ const bytes = new Uint8Array(buffer);
60
+ let binary = "";
61
+ for (let i = 0; i < bytes.length; i++) {
62
+ binary += String.fromCharCode(bytes[i]);
63
+ }
64
+ const b64 = globalThis.btoa?.(binary) ?? Buffer.from(binary, "binary").toString("base64");
65
+ let end = b64.length;
66
+ while (end > 0 && b64.charCodeAt(end - 1) === "=".charCodeAt(0)) {
67
+ end -= 1;
68
+ }
69
+ return b64.slice(0, end).replace(/\+/g, "-").replace(/\//g, "_");
70
+ }
71
+ function generateCodeVerifier(length = DEFAULT_VERIFIER_LENGTH) {
72
+ assertVerifierLength(length);
73
+ const crypto = getCrypto();
74
+ const bytes = new Uint8Array(length * RANDOM_BYTES_PER_CHAR);
75
+ crypto.getRandomValues(bytes);
76
+ let out = "";
77
+ for (let i = 0; i < length; i++) {
78
+ const byte = bytes[i];
79
+ out += UNRESERVED_CHARS[byte % UNRESERVED_CHARS.length];
80
+ }
81
+ return out;
82
+ }
83
+ async function deriveCodeChallenge(verifier) {
84
+ assertVerifierLength(verifier.length);
85
+ const crypto = getCrypto();
86
+ const data = new TextEncoder().encode(verifier);
87
+ const digest = await crypto.subtle.digest("SHA-256", data);
88
+ return base64UrlEncode(digest);
89
+ }
90
+ async function generatePkcePair(length) {
91
+ const codeVerifier = generateCodeVerifier(length);
92
+ const codeChallenge = await deriveCodeChallenge(codeVerifier);
93
+ return { codeVerifier, codeChallenge, codeChallengeMethod: "S256" };
94
+ }
95
+
96
+ // src/utils/buildKeycloakUrls.ts
97
+ var REALM_PATH_PREFIX = "/realms";
98
+ var PROTOCOL_PATH = "/protocol/openid-connect";
99
+ function trimTrailingSlash(value) {
100
+ return value.replace(/\/$/, "");
101
+ }
102
+ function buildIssuerUrl(baseUrl, realm) {
103
+ return `${trimTrailingSlash(baseUrl)}${REALM_PATH_PREFIX}/${encodeURIComponent(realm)}`;
104
+ }
105
+ function buildTokenEndpoint(baseUrl, realm) {
106
+ return `${buildIssuerUrl(baseUrl, realm)}${PROTOCOL_PATH}/token`;
107
+ }
108
+
109
+ // src/utils/buildTokenRequestBody.ts
110
+ function buildAuthorizationCodeBody(input) {
111
+ return new URLSearchParams({
112
+ client_id: input.clientId,
113
+ grant_type: "authorization_code",
114
+ code: input.code,
115
+ redirect_uri: input.redirectUri,
116
+ code_verifier: input.codeVerifier
117
+ }).toString();
118
+ }
119
+ function buildRefreshTokenBody(input) {
120
+ return new URLSearchParams({
121
+ client_id: input.clientId,
122
+ grant_type: "refresh_token",
123
+ refresh_token: input.refreshToken
124
+ }).toString();
125
+ }
126
+
127
+ // src/utils/normalizeTokenResponse.ts
128
+ function asString(value) {
129
+ return typeof value === "string" && value !== "" ? value : void 0;
130
+ }
131
+ function asNumber(value) {
132
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
133
+ }
134
+ function normalizeTokenResponse(raw) {
135
+ const accessToken = asString(raw.access_token);
136
+ if (accessToken === void 0) {
137
+ throw new Error("Token response missing access_token");
138
+ }
139
+ return {
140
+ accessToken,
141
+ refreshToken: asString(raw.refresh_token),
142
+ idToken: asString(raw.id_token),
143
+ expiresIn: asNumber(raw.expires_in),
144
+ tokenType: asString(raw.token_type),
145
+ scope: asString(raw.scope)
146
+ };
147
+ }
148
+
149
+ // src/oidc/tokenExchange.ts
150
+ var FORM_HEADERS = {
151
+ "Content-Type": "application/x-www-form-urlencoded"
152
+ };
153
+ async function postTokenEndpoint(http, url, body) {
154
+ const response = await http({
155
+ url,
156
+ method: "POST",
157
+ headers: FORM_HEADERS,
158
+ body
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`token endpoint POST failed: ${String(response.status)}`);
162
+ }
163
+ return normalizeTokenResponse(response.data);
164
+ }
165
+ async function exchangeAuthorizationCode(input) {
166
+ const url = buildTokenEndpoint(input.baseUrl, input.realm);
167
+ const body = buildAuthorizationCodeBody({
168
+ clientId: input.clientId,
169
+ code: input.code,
170
+ redirectUri: input.redirectUri,
171
+ codeVerifier: input.codeVerifier
172
+ });
173
+ return postTokenEndpoint(input.http, url, body);
174
+ }
175
+ async function refreshAccessToken(input) {
176
+ const url = buildTokenEndpoint(input.baseUrl, input.realm);
177
+ const body = buildRefreshTokenBody({
178
+ clientId: input.clientId,
179
+ refreshToken: input.refreshToken
180
+ });
181
+ return postTokenEndpoint(input.http, url, body);
182
+ }
183
+
184
+ exports.clearDiscoveryCache = clearDiscoveryCache;
185
+ exports.deriveCodeChallenge = deriveCodeChallenge;
186
+ exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
187
+ exports.fetchDiscoveryDocument = fetchDiscoveryDocument;
188
+ exports.generateCodeVerifier = generateCodeVerifier;
189
+ exports.generatePkcePair = generatePkcePair;
190
+ exports.refreshAccessToken = refreshAccessToken;
191
+ //# sourceMappingURL=index.js.map
192
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/oidc/discovery.ts","../../src/oidc/pkce.ts","../../src/utils/buildKeycloakUrls.ts","../../src/utils/buildTokenRequestBody.ts","../../src/utils/normalizeTokenResponse.ts","../../src/oidc/tokenExchange.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * OIDC discovery document fetcher.\n *\n * Fetches `{issuer}/.well-known/openid-configuration` and caches the result\n * per-issuer for the lifetime of the process. Discovery responses are stable\n * for hours; the cache prevents the auth flow from hitting KC on every login.\n *\n * Pure (no React, no hooks). Consumed by app-side hooks that orchestrate the\n * PKCE flow.\n */\n\nimport type { HttpClient } from '../http/HttpClient';\n\n/**\n * Subset of the OIDC Discovery 1.0 metadata the auth-client needs.\n *\n * The KC discovery doc carries many more fields; we type only what the PKCE\n * flow consumes to keep the surface small and to fail loudly when KC ever\n * stops returning one of these.\n */\nexport interface OidcDiscoveryDocument {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n end_session_endpoint?: string;\n userinfo_endpoint?: string;\n jwks_uri?: string;\n}\n\nexport interface FetchDiscoveryDocumentInput {\n /** Issuer URL — `{baseUrl}/realms/{realm}`. Trailing slash tolerated. */\n issuerUrl: string;\n /** Transport. Pass `createFetchHttpClient(fetch)` in browser/Node18+. */\n http: HttpClient;\n}\n\nconst cache = new Map<string, OidcDiscoveryDocument>();\n\nfunction normalizeIssuer(issuerUrl: string): string {\n return issuerUrl.replace(/\\/$/, '');\n}\n\nfunction isOidcDiscoveryDocument(data: unknown): data is OidcDiscoveryDocument {\n if (data === null || typeof data !== 'object') {\n return false;\n }\n const d = data as Record<string, unknown>;\n return (\n typeof d.issuer === 'string'\n && d.issuer !== ''\n && typeof d.authorization_endpoint === 'string'\n && d.authorization_endpoint !== ''\n && typeof d.token_endpoint === 'string'\n && d.token_endpoint !== ''\n );\n}\n\n/**\n * Fetch + cache the OIDC discovery document for an issuer.\n *\n * Cache key = normalized issuer URL (trailing slash stripped).\n *\n * @throws Error when the HTTP call fails, returns non-2xx, or returns a body\n * missing required OIDC metadata fields.\n */\nexport async function fetchDiscoveryDocument(\n input: FetchDiscoveryDocumentInput,\n): Promise<OidcDiscoveryDocument> {\n const key = normalizeIssuer(input.issuerUrl);\n const cached = cache.get(key);\n if (cached !== undefined) {\n return cached;\n }\n const response = await input.http({\n url: `${key}/.well-known/openid-configuration`,\n method: 'GET',\n });\n if (!response.ok) {\n throw new Error(\n `OIDC discovery failed: ${String(response.status)} for ${key}`,\n );\n }\n if (!isOidcDiscoveryDocument(response.data)) {\n throw new Error(`OIDC discovery returned invalid metadata for ${key}`);\n }\n cache.set(key, response.data);\n return response.data;\n}\n\n/**\n * Clear the per-issuer discovery cache. Test-only — production code does not\n * call this. Useful when a test mocks different metadata across cases.\n */\nexport function clearDiscoveryCache(): void {\n cache.clear();\n}\n","/**\n * PKCE (RFC 7636) primitives for the OIDC authorization-code flow.\n *\n * Pure (no React). Browser-compatible — uses `crypto.subtle` for SHA-256 and\n * `crypto.getRandomValues` for the verifier. Node 16+ exposes both via\n * `globalThis.crypto`.\n */\n\n/** RFC 7636 §4.1: code_verifier MUST be 43..128 chars from the unreserved set. */\nconst VERIFIER_MIN_LENGTH = 43;\nconst VERIFIER_MAX_LENGTH = 128;\nconst DEFAULT_VERIFIER_LENGTH = 64;\nconst RANDOM_BYTES_PER_CHAR = 1;\n\nconst UNRESERVED_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';\n\nfunction getCrypto(): Crypto {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n // Runtime check: in some Node test environments `crypto.subtle` may not\n // exist even though the TS lib types mark it as non-optional.\n // eslint-disable-next-line sonarjs/different-types-comparison, @typescript-eslint/no-unnecessary-condition\n if (c === undefined || c.subtle === undefined) {\n throw new Error('pkce: globalThis.crypto.subtle is required (Node 16+ / modern browser)');\n }\n return c;\n}\n\nfunction assertVerifierLength(length: number): void {\n if (length < VERIFIER_MIN_LENGTH || length > VERIFIER_MAX_LENGTH) {\n throw new Error(`pkce: code_verifier length must be ${String(VERIFIER_MIN_LENGTH)}-${String(VERIFIER_MAX_LENGTH)} chars (RFC 7636)`);\n }\n}\n\n/**\n * Base64-URL encode an ArrayBuffer (no padding, `-` and `_` substitutions).\n *\n * Required for the S256 challenge — RFC 7636 §4.2.\n */\nfunction base64UrlEncode(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] as number);\n }\n const b64 = (globalThis as { btoa?: (s: string) => string }).btoa?.(binary)\n ?? Buffer.from(binary, 'binary').toString('base64');\n // Strip trailing '=' padding by slicing — avoids the sonarjs/slow-regex\n // warning on /=+$/ even though base64 padding is bounded to 0..2 chars.\n let end = b64.length;\n while (end > 0 && b64.charCodeAt(end - 1) === '='.charCodeAt(0)) {\n end -= 1;\n }\n return b64.slice(0, end).replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\n/**\n * Generate a cryptographically random PKCE code_verifier.\n *\n * Default length 64 sits well inside the RFC 7636 43..128 band.\n *\n * @throws Error when `length` falls outside the RFC band.\n */\nexport function generateCodeVerifier(length: number = DEFAULT_VERIFIER_LENGTH): string {\n assertVerifierLength(length);\n const crypto = getCrypto();\n const bytes = new Uint8Array(length * RANDOM_BYTES_PER_CHAR);\n crypto.getRandomValues(bytes);\n let out = '';\n for (let i = 0; i < length; i++) {\n const byte = bytes[i] as number;\n out += UNRESERVED_CHARS[byte % UNRESERVED_CHARS.length];\n }\n return out;\n}\n\n/**\n * Derive the S256 code_challenge from a code_verifier.\n *\n * `code_challenge = BASE64URL(SHA256(code_verifier))` — RFC 7636 §4.2.\n *\n * @throws Error when `verifier` is shorter than 43 or longer than 128 chars.\n */\nexport async function deriveCodeChallenge(verifier: string): Promise<string> {\n assertVerifierLength(verifier.length);\n const crypto = getCrypto();\n const data = new TextEncoder().encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(digest);\n}\n\nexport interface PkcePair {\n codeVerifier: string;\n codeChallenge: string;\n codeChallengeMethod: 'S256';\n}\n\n/**\n * Convenience: produce a fresh verifier + matching challenge in one call.\n */\nexport async function generatePkcePair(length?: number): Promise<PkcePair> {\n const codeVerifier = generateCodeVerifier(length);\n const codeChallenge = await deriveCodeChallenge(codeVerifier);\n return { codeVerifier, codeChallenge, codeChallengeMethod: 'S256' };\n}\n","/**\n * URL builders for the realm-aware Keycloak surface area.\n *\n * Every helper takes `baseUrl` and `realm` explicitly — no hardcoded realm\n * names. This is the contract that Phase 2 of the product split relies on:\n * the same package serves the future Questioner-realm app and OnlineMenu-realm\n * app without code change.\n */\n\nconst REALM_PATH_PREFIX = '/realms';\nconst PROTOCOL_PATH = '/protocol/openid-connect';\n\nfunction trimTrailingSlash(value: string): string {\n return value.replace(/\\/$/, '');\n}\n\n/**\n * Compute the issuer URL: `{baseUrl}/realms/{realm}`.\n */\nexport function buildIssuerUrl(baseUrl: string, realm: string): string {\n return `${trimTrailingSlash(baseUrl)}${REALM_PATH_PREFIX}/${encodeURIComponent(realm)}`;\n}\n\n/**\n * Compute the authorization endpoint URL.\n */\nexport function buildAuthorizationEndpoint(baseUrl: string, realm: string): string {\n return `${buildIssuerUrl(baseUrl, realm)}${PROTOCOL_PATH}/auth`;\n}\n\n/**\n * Compute the token endpoint URL.\n */\nexport function buildTokenEndpoint(baseUrl: string, realm: string): string {\n return `${buildIssuerUrl(baseUrl, realm)}${PROTOCOL_PATH}/token`;\n}\n\n/**\n * Compute the userinfo endpoint URL.\n */\nexport function buildUserInfoEndpoint(baseUrl: string, realm: string): string {\n return `${buildIssuerUrl(baseUrl, realm)}${PROTOCOL_PATH}/userinfo`;\n}\n\n/**\n * Compute the logout endpoint URL.\n */\nexport function buildLogoutEndpoint(baseUrl: string, realm: string): string {\n return `${buildIssuerUrl(baseUrl, realm)}${PROTOCOL_PATH}/logout`;\n}\n\nexport interface AuthorizationUrlInput {\n baseUrl: string;\n realm: string;\n clientId: string;\n redirectUri: string;\n scope?: string;\n state?: string;\n codeChallenge?: string;\n codeChallengeMethod?: 'S256' | 'plain';\n}\n\n/**\n * Build a complete authorization URL the user agent can navigate to.\n *\n * All PKCE-related fields are optional so this helper also serves\n * non-PKCE flows (e.g. confidential server-side clients) — but PKCE is\n * the recommended path for SPA / native consumers.\n */\nexport function buildAuthorizationUrl(input: AuthorizationUrlInput): string {\n const params = new URLSearchParams({\n client_id: input.clientId,\n redirect_uri: input.redirectUri,\n response_type: 'code',\n });\n if (typeof input.scope === 'string' && input.scope !== '') {\n params.set('scope', input.scope);\n }\n if (typeof input.state === 'string' && input.state !== '') {\n params.set('state', input.state);\n }\n if (typeof input.codeChallenge === 'string' && input.codeChallenge !== '') {\n params.set('code_challenge', input.codeChallenge);\n params.set('code_challenge_method', input.codeChallengeMethod ?? 'S256');\n }\n return `${buildAuthorizationEndpoint(input.baseUrl, input.realm)}?${params.toString()}`;\n}\n","/**\n * Inputs for the OAuth `authorization_code` token request.\n */\nexport interface AuthorizationCodeBodyInput {\n clientId: string;\n code: string;\n redirectUri: string;\n codeVerifier: string;\n}\n\n/**\n * Inputs for the OAuth `refresh_token` token request.\n */\nexport interface RefreshTokenBodyInput {\n clientId: string;\n refreshToken: string;\n}\n\n/**\n * Build the `application/x-www-form-urlencoded` body for the\n * `grant_type=authorization_code` token endpoint call (PKCE flow).\n */\nexport function buildAuthorizationCodeBody(input: AuthorizationCodeBodyInput): string {\n return new URLSearchParams({\n client_id: input.clientId,\n grant_type: 'authorization_code',\n code: input.code,\n redirect_uri: input.redirectUri,\n code_verifier: input.codeVerifier,\n }).toString();\n}\n\n/**\n * Build the `application/x-www-form-urlencoded` body for the\n * `grant_type=refresh_token` token endpoint call.\n */\nexport function buildRefreshTokenBody(input: RefreshTokenBodyInput): string {\n return new URLSearchParams({\n client_id: input.clientId,\n grant_type: 'refresh_token',\n refresh_token: input.refreshToken,\n }).toString();\n}\n","import type { AuthTokens } from '../types/AuthTokens';\nimport type { RawTokenResponse, TokenResponse } from '../types/TokenResponse';\nimport { computeExpiresAt } from './isTokenExpired';\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * Map a raw OIDC token endpoint response (snake_case) to camelCase.\n *\n * Throws when `access_token` is missing or empty — callers should let this\n * propagate to the auth state machine, which treats it as a login failure.\n */\nexport function normalizeTokenResponse(raw: RawTokenResponse): TokenResponse {\n const accessToken = asString(raw.access_token);\n if (accessToken === undefined) {\n throw new Error('Token response missing access_token');\n }\n return {\n accessToken,\n refreshToken: asString(raw.refresh_token),\n idToken: asString(raw.id_token),\n expiresIn: asNumber(raw.expires_in),\n tokenType: asString(raw.token_type),\n scope: asString(raw.scope),\n };\n}\n\n/**\n * Convert a normalized {@link TokenResponse} into a persistable\n * {@link AuthTokens} bundle by computing `expiresAt` from `expiresIn`.\n */\nexport function tokenResponseToAuthTokens(\n response: TokenResponse,\n now: number = Date.now(),\n): AuthTokens {\n return {\n accessToken: response.accessToken,\n refreshToken: response.refreshToken,\n idToken: response.idToken,\n expiresAt: computeExpiresAt(response.expiresIn, now),\n };\n}\n","/**\n * OIDC token-endpoint helpers.\n *\n * Pure (no React, no hooks). Wraps the realm-aware token endpoint with the\n * PKCE `authorization_code` and `refresh_token` grants. The transport is\n * injected so callers can use the HTTP client of their choice.\n *\n * Use these from app-side hooks (e.g. `useKeycloakExchange`) instead of\n * duplicating the body-builder + POST + normalise dance.\n */\n\nimport { buildTokenEndpoint } from '../utils/buildKeycloakUrls';\nimport {\n buildAuthorizationCodeBody,\n buildRefreshTokenBody,\n} from '../utils/buildTokenRequestBody';\nimport { normalizeTokenResponse } from '../utils/normalizeTokenResponse';\n\nimport type { HttpClient } from '../http/HttpClient';\nimport type { RawTokenResponse, TokenResponse } from '../types/TokenResponse';\n\nconst FORM_HEADERS: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n};\n\nexport interface ExchangeAuthorizationCodeInput {\n http: HttpClient;\n baseUrl: string;\n realm: string;\n clientId: string;\n code: string;\n redirectUri: string;\n codeVerifier: string;\n}\n\nexport interface RefreshAccessTokenInput {\n http: HttpClient;\n baseUrl: string;\n realm: string;\n clientId: string;\n refreshToken: string;\n}\n\nasync function postTokenEndpoint(\n http: HttpClient,\n url: string,\n body: string,\n): Promise<TokenResponse> {\n const response = await http({\n url,\n method: 'POST',\n headers: FORM_HEADERS,\n body,\n });\n if (!response.ok) {\n throw new Error(`token endpoint POST failed: ${String(response.status)}`);\n }\n return normalizeTokenResponse(response.data as RawTokenResponse);\n}\n\n/**\n * Exchange a PKCE authorization `code` for tokens via the realm's token\n * endpoint (`grant_type=authorization_code`).\n *\n * @throws Error when the HTTP call returns non-2xx or the body is missing\n * `access_token`.\n */\nexport async function exchangeAuthorizationCode(\n input: ExchangeAuthorizationCodeInput,\n): Promise<TokenResponse> {\n const url = buildTokenEndpoint(input.baseUrl, input.realm);\n const body = buildAuthorizationCodeBody({\n clientId: input.clientId,\n code: input.code,\n redirectUri: input.redirectUri,\n codeVerifier: input.codeVerifier,\n });\n return postTokenEndpoint(input.http, url, body);\n}\n\n/**\n * Swap a refresh token for a fresh access/refresh-token pair via the realm's\n * token endpoint (`grant_type=refresh_token`).\n *\n * @throws Error when the HTTP call returns non-2xx or the body is missing\n * `access_token`.\n */\nexport async function refreshAccessToken(\n input: RefreshAccessTokenInput,\n): Promise<TokenResponse> {\n const url = buildTokenEndpoint(input.baseUrl, input.realm);\n const body = buildRefreshTokenBody({\n clientId: input.clientId,\n refreshToken: input.refreshToken,\n });\n return postTokenEndpoint(input.http, url, body);\n}\n"]}
@@ -0,0 +1,184 @@
1
+ // src/oidc/discovery.ts
2
+ var cache = /* @__PURE__ */ new Map();
3
+ function normalizeIssuer(issuerUrl) {
4
+ return issuerUrl.replace(/\/$/, "");
5
+ }
6
+ function isOidcDiscoveryDocument(data) {
7
+ if (data === null || typeof data !== "object") {
8
+ return false;
9
+ }
10
+ const d = data;
11
+ return typeof d.issuer === "string" && d.issuer !== "" && typeof d.authorization_endpoint === "string" && d.authorization_endpoint !== "" && typeof d.token_endpoint === "string" && d.token_endpoint !== "";
12
+ }
13
+ async function fetchDiscoveryDocument(input) {
14
+ const key = normalizeIssuer(input.issuerUrl);
15
+ const cached = cache.get(key);
16
+ if (cached !== void 0) {
17
+ return cached;
18
+ }
19
+ const response = await input.http({
20
+ url: `${key}/.well-known/openid-configuration`,
21
+ method: "GET"
22
+ });
23
+ if (!response.ok) {
24
+ throw new Error(
25
+ `OIDC discovery failed: ${String(response.status)} for ${key}`
26
+ );
27
+ }
28
+ if (!isOidcDiscoveryDocument(response.data)) {
29
+ throw new Error(`OIDC discovery returned invalid metadata for ${key}`);
30
+ }
31
+ cache.set(key, response.data);
32
+ return response.data;
33
+ }
34
+ function clearDiscoveryCache() {
35
+ cache.clear();
36
+ }
37
+
38
+ // src/oidc/pkce.ts
39
+ var VERIFIER_MIN_LENGTH = 43;
40
+ var VERIFIER_MAX_LENGTH = 128;
41
+ var DEFAULT_VERIFIER_LENGTH = 64;
42
+ var RANDOM_BYTES_PER_CHAR = 1;
43
+ var UNRESERVED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
44
+ function getCrypto() {
45
+ const c = globalThis.crypto;
46
+ if (c === void 0 || c.subtle === void 0) {
47
+ throw new Error("pkce: globalThis.crypto.subtle is required (Node 16+ / modern browser)");
48
+ }
49
+ return c;
50
+ }
51
+ function assertVerifierLength(length) {
52
+ if (length < VERIFIER_MIN_LENGTH || length > VERIFIER_MAX_LENGTH) {
53
+ throw new Error(`pkce: code_verifier length must be ${String(VERIFIER_MIN_LENGTH)}-${String(VERIFIER_MAX_LENGTH)} chars (RFC 7636)`);
54
+ }
55
+ }
56
+ function base64UrlEncode(buffer) {
57
+ const bytes = new Uint8Array(buffer);
58
+ let binary = "";
59
+ for (let i = 0; i < bytes.length; i++) {
60
+ binary += String.fromCharCode(bytes[i]);
61
+ }
62
+ const b64 = globalThis.btoa?.(binary) ?? Buffer.from(binary, "binary").toString("base64");
63
+ let end = b64.length;
64
+ while (end > 0 && b64.charCodeAt(end - 1) === "=".charCodeAt(0)) {
65
+ end -= 1;
66
+ }
67
+ return b64.slice(0, end).replace(/\+/g, "-").replace(/\//g, "_");
68
+ }
69
+ function generateCodeVerifier(length = DEFAULT_VERIFIER_LENGTH) {
70
+ assertVerifierLength(length);
71
+ const crypto = getCrypto();
72
+ const bytes = new Uint8Array(length * RANDOM_BYTES_PER_CHAR);
73
+ crypto.getRandomValues(bytes);
74
+ let out = "";
75
+ for (let i = 0; i < length; i++) {
76
+ const byte = bytes[i];
77
+ out += UNRESERVED_CHARS[byte % UNRESERVED_CHARS.length];
78
+ }
79
+ return out;
80
+ }
81
+ async function deriveCodeChallenge(verifier) {
82
+ assertVerifierLength(verifier.length);
83
+ const crypto = getCrypto();
84
+ const data = new TextEncoder().encode(verifier);
85
+ const digest = await crypto.subtle.digest("SHA-256", data);
86
+ return base64UrlEncode(digest);
87
+ }
88
+ async function generatePkcePair(length) {
89
+ const codeVerifier = generateCodeVerifier(length);
90
+ const codeChallenge = await deriveCodeChallenge(codeVerifier);
91
+ return { codeVerifier, codeChallenge, codeChallengeMethod: "S256" };
92
+ }
93
+
94
+ // src/utils/buildKeycloakUrls.ts
95
+ var REALM_PATH_PREFIX = "/realms";
96
+ var PROTOCOL_PATH = "/protocol/openid-connect";
97
+ function trimTrailingSlash(value) {
98
+ return value.replace(/\/$/, "");
99
+ }
100
+ function buildIssuerUrl(baseUrl, realm) {
101
+ return `${trimTrailingSlash(baseUrl)}${REALM_PATH_PREFIX}/${encodeURIComponent(realm)}`;
102
+ }
103
+ function buildTokenEndpoint(baseUrl, realm) {
104
+ return `${buildIssuerUrl(baseUrl, realm)}${PROTOCOL_PATH}/token`;
105
+ }
106
+
107
+ // src/utils/buildTokenRequestBody.ts
108
+ function buildAuthorizationCodeBody(input) {
109
+ return new URLSearchParams({
110
+ client_id: input.clientId,
111
+ grant_type: "authorization_code",
112
+ code: input.code,
113
+ redirect_uri: input.redirectUri,
114
+ code_verifier: input.codeVerifier
115
+ }).toString();
116
+ }
117
+ function buildRefreshTokenBody(input) {
118
+ return new URLSearchParams({
119
+ client_id: input.clientId,
120
+ grant_type: "refresh_token",
121
+ refresh_token: input.refreshToken
122
+ }).toString();
123
+ }
124
+
125
+ // src/utils/normalizeTokenResponse.ts
126
+ function asString(value) {
127
+ return typeof value === "string" && value !== "" ? value : void 0;
128
+ }
129
+ function asNumber(value) {
130
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
131
+ }
132
+ function normalizeTokenResponse(raw) {
133
+ const accessToken = asString(raw.access_token);
134
+ if (accessToken === void 0) {
135
+ throw new Error("Token response missing access_token");
136
+ }
137
+ return {
138
+ accessToken,
139
+ refreshToken: asString(raw.refresh_token),
140
+ idToken: asString(raw.id_token),
141
+ expiresIn: asNumber(raw.expires_in),
142
+ tokenType: asString(raw.token_type),
143
+ scope: asString(raw.scope)
144
+ };
145
+ }
146
+
147
+ // src/oidc/tokenExchange.ts
148
+ var FORM_HEADERS = {
149
+ "Content-Type": "application/x-www-form-urlencoded"
150
+ };
151
+ async function postTokenEndpoint(http, url, body) {
152
+ const response = await http({
153
+ url,
154
+ method: "POST",
155
+ headers: FORM_HEADERS,
156
+ body
157
+ });
158
+ if (!response.ok) {
159
+ throw new Error(`token endpoint POST failed: ${String(response.status)}`);
160
+ }
161
+ return normalizeTokenResponse(response.data);
162
+ }
163
+ async function exchangeAuthorizationCode(input) {
164
+ const url = buildTokenEndpoint(input.baseUrl, input.realm);
165
+ const body = buildAuthorizationCodeBody({
166
+ clientId: input.clientId,
167
+ code: input.code,
168
+ redirectUri: input.redirectUri,
169
+ codeVerifier: input.codeVerifier
170
+ });
171
+ return postTokenEndpoint(input.http, url, body);
172
+ }
173
+ async function refreshAccessToken(input) {
174
+ const url = buildTokenEndpoint(input.baseUrl, input.realm);
175
+ const body = buildRefreshTokenBody({
176
+ clientId: input.clientId,
177
+ refreshToken: input.refreshToken
178
+ });
179
+ return postTokenEndpoint(input.http, url, body);
180
+ }
181
+
182
+ export { clearDiscoveryCache, deriveCodeChallenge, exchangeAuthorizationCode, fetchDiscoveryDocument, generateCodeVerifier, generatePkcePair, refreshAccessToken };
183
+ //# sourceMappingURL=index.mjs.map
184
+ //# sourceMappingURL=index.mjs.map