@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.
- package/CHANGELOG.md +90 -0
- package/README.md +37 -1
- package/dist/{AuthClient-Dim7HPRz.d.ts → AuthClient-BGr8L03W.d.mts} +62 -35
- package/dist/{AuthClient-Dim7HPRz.d.mts → AuthClient-D95OMajD.d.ts} +62 -35
- package/dist/TokenResponse-CY1CaU2l.d.mts +59 -0
- package/dist/TokenResponse-CY1CaU2l.d.ts +59 -0
- package/dist/index.d.mts +109 -28
- package/dist/index.d.ts +109 -28
- package/dist/index.js +329 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +322 -20
- package/dist/index.mjs.map +1 -1
- package/dist/oidc/index.d.mts +127 -0
- package/dist/oidc/index.d.ts +127 -0
- package/dist/oidc/index.js +192 -0
- package/dist/oidc/index.js.map +1 -0
- package/dist/oidc/index.mjs +184 -0
- package/dist/oidc/index.mjs.map +1 -0
- package/dist/react.d.mts +2 -1
- package/dist/react.d.ts +2 -1
- package/package.json +12 -2
|
@@ -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":";AAoCA,IAAM,KAAA,uBAAY,GAAA,EAAmC;AAErD,SAAS,gBAAgB,SAAA,EAA2B;AAClD,EAAA,OAAO,SAAA,CAAU,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AACpC;AAEA,SAAS,wBAAwB,IAAA,EAA8C;AAC7E,EAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,EAAU;AAC7C,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,CAAA,GAAI,IAAA;AACV,EAAA,OACE,OAAO,CAAA,CAAE,MAAA,KAAW,YACjB,CAAA,CAAE,MAAA,KAAW,MACb,OAAO,CAAA,CAAE,2BAA2B,QAAA,IACpC,CAAA,CAAE,2BAA2B,EAAA,IAC7B,OAAO,EAAE,cAAA,KAAmB,QAAA,IAC5B,EAAE,cAAA,KAAmB,EAAA;AAE5B;AAUA,eAAsB,uBACpB,KAAA,EACgC;AAChC,EAAA,MAAM,GAAA,GAAM,eAAA,CAAgB,KAAA,CAAM,SAAS,CAAA;AAC3C,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAC5B,EAAA,IAAI,WAAW,MAAA,EAAW;AACxB,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,IAAA,CAAK;AAAA,IAChC,GAAA,EAAK,GAAG,GAAG,CAAA,iCAAA,CAAA;AAAA,IACX,MAAA,EAAQ;AAAA,GACT,CAAA;AACD,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,0BAA0B,MAAA,CAAO,QAAA,CAAS,MAAM,CAAC,QAAQ,GAAG,CAAA;AAAA,KAC9D;AAAA,EACF;AACA,EAAA,IAAI,CAAC,uBAAA,CAAwB,QAAA,CAAS,IAAI,CAAA,EAAG;AAC3C,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,6CAAA,EAAgD,GAAG,CAAA,CAAE,CAAA;AAAA,EACvE;AACA,EAAA,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,QAAA,CAAS,IAAI,CAAA;AAC5B,EAAA,OAAO,QAAA,CAAS,IAAA;AAClB;AAMO,SAAS,mBAAA,GAA4B;AAC1C,EAAA,KAAA,CAAM,KAAA,EAAM;AACd;;;ACtFA,IAAM,mBAAA,GAAsB,EAAA;AAC5B,IAAM,mBAAA,GAAsB,GAAA;AAC5B,IAAM,uBAAA,GAA0B,EAAA;AAChC,IAAM,qBAAA,GAAwB,CAAA;AAE9B,IAAM,gBAAA,GAAmB,oEAAA;AAEzB,SAAS,SAAA,GAAoB;AAC3B,EAAA,MAAM,IAAK,UAAA,CAAmC,MAAA;AAI9C,EAAA,IAAI,CAAA,KAAM,MAAA,IAAa,CAAA,CAAE,MAAA,KAAW,MAAA,EAAW;AAC7C,IAAA,MAAM,IAAI,MAAM,wEAAwE,CAAA;AAAA,EAC1F;AACA,EAAA,OAAO,CAAA;AACT;AAEA,SAAS,qBAAqB,MAAA,EAAsB;AAClD,EAAA,IAAI,MAAA,GAAS,mBAAA,IAAuB,MAAA,GAAS,mBAAA,EAAqB;AAChE,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mCAAA,EAAsC,MAAA,CAAO,mBAAmB,CAAC,CAAA,CAAA,EAAI,MAAA,CAAO,mBAAmB,CAAC,CAAA,iBAAA,CAAmB,CAAA;AAAA,EACrI;AACF;AAOA,SAAS,gBAAgB,MAAA,EAA6B;AACpD,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAM,CAAA;AACnC,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAA,IAAU,MAAA,CAAO,YAAA,CAAa,KAAA,CAAM,CAAC,CAAW,CAAA;AAAA,EAClD;AACA,EAAA,MAAM,GAAA,GAAO,UAAA,CAAgD,IAAA,GAAO,MAAM,CAAA,IACrE,MAAA,CAAO,IAAA,CAAK,MAAA,EAAQ,QAAQ,CAAA,CAAE,QAAA,CAAS,QAAQ,CAAA;AAGpD,EAAA,IAAI,MAAM,GAAA,CAAI,MAAA;AACd,EAAA,OAAO,GAAA,GAAM,CAAA,IAAK,GAAA,CAAI,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA,KAAM,GAAA,CAAI,UAAA,CAAW,CAAC,CAAA,EAAG;AAC/D,IAAA,GAAA,IAAO,CAAA;AAAA,EACT;AACA,EAAA,OAAO,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA;AACjE;AASO,SAAS,oBAAA,CAAqB,SAAiB,uBAAA,EAAiC;AACrF,EAAA,oBAAA,CAAqB,MAAM,CAAA;AAC3B,EAAA,MAAM,SAAS,SAAA,EAAU;AACzB,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAA,GAAS,qBAAqB,CAAA;AAC3D,EAAA,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAC5B,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,EAAQ,CAAA,EAAA,EAAK;AAC/B,IAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AACpB,IAAA,GAAA,IAAO,gBAAA,CAAiB,IAAA,GAAO,gBAAA,CAAiB,MAAM,CAAA;AAAA,EACxD;AACA,EAAA,OAAO,GAAA;AACT;AASA,eAAsB,oBAAoB,QAAA,EAAmC;AAC3E,EAAA,oBAAA,CAAqB,SAAS,MAAM,CAAA;AACpC,EAAA,MAAM,SAAS,SAAA,EAAU;AACzB,EAAA,MAAM,IAAA,GAAO,IAAI,WAAA,EAAY,CAAE,OAAO,QAAQ,CAAA;AAC9C,EAAA,MAAM,SAAS,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,IAAI,CAAA;AACzD,EAAA,OAAO,gBAAgB,MAAM,CAAA;AAC/B;AAWA,eAAsB,iBAAiB,MAAA,EAAoC;AACzE,EAAA,MAAM,YAAA,GAAe,qBAAqB,MAAM,CAAA;AAChD,EAAA,MAAM,aAAA,GAAgB,MAAM,mBAAA,CAAoB,YAAY,CAAA;AAC5D,EAAA,OAAO,EAAE,YAAA,EAAc,aAAA,EAAe,mBAAA,EAAqB,MAAA,EAAO;AACpE;;;AC9FA,IAAM,iBAAA,GAAoB,SAAA;AAC1B,IAAM,aAAA,GAAgB,0BAAA;AAEtB,SAAS,kBAAkB,KAAA,EAAuB;AAChD,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAChC;AAKO,SAAS,cAAA,CAAe,SAAiB,KAAA,EAAuB;AACrE,EAAA,OAAO,CAAA,EAAG,kBAAkB,OAAO,CAAC,GAAG,iBAAiB,CAAA,CAAA,EAAI,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAA;AACvF;AAYO,SAAS,kBAAA,CAAmB,SAAiB,KAAA,EAAuB;AACzE,EAAA,OAAO,GAAG,cAAA,CAAe,OAAA,EAAS,KAAK,CAAC,GAAG,aAAa,CAAA,MAAA,CAAA;AAC1D;;;ACbO,SAAS,2BAA2B,KAAA,EAA2C;AACpF,EAAA,OAAO,IAAI,eAAA,CAAgB;AAAA,IACzB,WAAW,KAAA,CAAM,QAAA;AAAA,IACjB,UAAA,EAAY,oBAAA;AAAA,IACZ,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,cAAc,KAAA,CAAM,WAAA;AAAA,IACpB,eAAe,KAAA,CAAM;AAAA,GACtB,EAAE,QAAA,EAAS;AACd;AAMO,SAAS,sBAAsB,KAAA,EAAsC;AAC1E,EAAA,OAAO,IAAI,eAAA,CAAgB;AAAA,IACzB,WAAW,KAAA,CAAM,QAAA;AAAA,IACjB,UAAA,EAAY,eAAA;AAAA,IACZ,eAAe,KAAA,CAAM;AAAA,GACtB,EAAE,QAAA,EAAS;AACd;;;ACtCA,SAAS,SAAS,KAAA,EAAoC;AACpD,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,KAAK,KAAA,GAAQ,MAAA;AAC7D;AAEA,SAAS,SAAS,KAAA,EAAoC;AACpD,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,QAAA,CAAS,KAAK,IAAI,KAAA,GAAQ,MAAA;AACvE;AAQO,SAAS,uBAAuB,GAAA,EAAsC;AAC3E,EAAA,MAAM,WAAA,GAAc,QAAA,CAAS,GAAA,CAAI,YAAY,CAAA;AAC7C,EAAA,IAAI,gBAAgB,MAAA,EAAW;AAC7B,IAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAAA,EACvD;AACA,EAAA,OAAO;AAAA,IACL,WAAA;AAAA,IACA,YAAA,EAAc,QAAA,CAAS,GAAA,CAAI,aAAa,CAAA;AAAA,IACxC,OAAA,EAAS,QAAA,CAAS,GAAA,CAAI,QAAQ,CAAA;AAAA,IAC9B,SAAA,EAAW,QAAA,CAAS,GAAA,CAAI,UAAU,CAAA;AAAA,IAClC,SAAA,EAAW,QAAA,CAAS,GAAA,CAAI,UAAU,CAAA;AAAA,IAClC,KAAA,EAAO,QAAA,CAAS,GAAA,CAAI,KAAK;AAAA,GAC3B;AACF;;;ACVA,IAAM,YAAA,GAAuC;AAAA,EAC3C,cAAA,EAAgB;AAClB,CAAA;AAoBA,eAAe,iBAAA,CACb,IAAA,EACA,GAAA,EACA,IAAA,EACwB;AACxB,EAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK;AAAA,IAC1B,GAAA;AAAA,IACA,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS,YAAA;AAAA,IACT;AAAA,GACD,CAAA;AACD,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,4BAAA,EAA+B,OAAO,QAAA,CAAS,MAAM,CAAC,CAAA,CAAE,CAAA;AAAA,EAC1E;AACA,EAAA,OAAO,sBAAA,CAAuB,SAAS,IAAwB,CAAA;AACjE;AASA,eAAsB,0BACpB,KAAA,EACwB;AACxB,EAAA,MAAM,GAAA,GAAM,kBAAA,CAAmB,KAAA,CAAM,OAAA,EAAS,MAAM,KAAK,CAAA;AACzD,EAAA,MAAM,OAAO,0BAAA,CAA2B;AAAA,IACtC,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,MAAM,KAAA,CAAM,IAAA;AAAA,IACZ,aAAa,KAAA,CAAM,WAAA;AAAA,IACnB,cAAc,KAAA,CAAM;AAAA,GACrB,CAAA;AACD,EAAA,OAAO,iBAAA,CAAkB,KAAA,CAAM,IAAA,EAAM,GAAA,EAAK,IAAI,CAAA;AAChD;AASA,eAAsB,mBACpB,KAAA,EACwB;AACxB,EAAA,MAAM,GAAA,GAAM,kBAAA,CAAmB,KAAA,CAAM,OAAA,EAAS,MAAM,KAAK,CAAA;AACzD,EAAA,MAAM,OAAO,qBAAA,CAAsB;AAAA,IACjC,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,cAAc,KAAA,CAAM;AAAA,GACrB,CAAA;AACD,EAAA,OAAO,iBAAA,CAAkB,KAAA,CAAM,IAAA,EAAM,GAAA,EAAK,IAAI,CAAA;AAChD","file":"index.mjs","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"]}
|
package/dist/react.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
|
|
2
|
-
import { F as ForgotPasswordRequest,
|
|
2
|
+
import { F as ForgotPasswordRequest, A as AuthApiClient, R as ResetPasswordRequest, a as AuthSessionInfo, b as AuthClient } from './AuthClient-BGr8L03W.mjs';
|
|
3
|
+
import './TokenResponse-CY1CaU2l.mjs';
|
|
3
4
|
|
|
4
5
|
interface UseForgotPasswordOptions extends Omit<UseMutationOptions<void, Error, ForgotPasswordRequest>, 'mutationFn'> {
|
|
5
6
|
api: AuthApiClient;
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
|
|
2
|
-
import { F as ForgotPasswordRequest,
|
|
2
|
+
import { F as ForgotPasswordRequest, A as AuthApiClient, R as ResetPasswordRequest, a as AuthSessionInfo, b as AuthClient } from './AuthClient-D95OMajD.js';
|
|
3
|
+
import './TokenResponse-CY1CaU2l.js';
|
|
3
4
|
|
|
4
5
|
interface UseForgotPasswordOptions extends Omit<UseMutationOptions<void, Error, ForgotPasswordRequest>, 'mutationFn'> {
|
|
5
6
|
api: AuthApiClient;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dloizides/auth-client",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Auth client for the dloizides.com portfolio. v3 adds BffAuthClient — the same-origin client for a per-app Backend-For-Frontend. Also: realm-aware Keycloak/OIDC (PKCE/ROPC), token refresh, storage adapters, hooks for sessions and password reset.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"keycloak",
|
|
7
7
|
"oidc",
|
|
@@ -42,6 +42,16 @@
|
|
|
42
42
|
"types": "./dist/react.d.mts",
|
|
43
43
|
"default": "./dist/react.mjs"
|
|
44
44
|
}
|
|
45
|
+
},
|
|
46
|
+
"./oidc": {
|
|
47
|
+
"require": {
|
|
48
|
+
"types": "./dist/oidc/index.d.ts",
|
|
49
|
+
"default": "./dist/oidc/index.js"
|
|
50
|
+
},
|
|
51
|
+
"import": {
|
|
52
|
+
"types": "./dist/oidc/index.d.mts",
|
|
53
|
+
"default": "./dist/oidc/index.mjs"
|
|
54
|
+
}
|
|
45
55
|
}
|
|
46
56
|
},
|
|
47
57
|
"files": [
|