@zoreal/oauth2-js 0.1.5 → 0.1.6
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/README.md +12 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zoreal/oauth2-js
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@zoreal/oauth2-js) [](https://www.npmjs.com/package/@zoreal/oauth2-js) [](https://www.npmjs.com/package/@zoreal/oauth2-js) [](https://github.com/Bynn-Intelligence/zoreal-oauth2-js/actions/workflows/ci.yml) [](./LICENSE)
|
|
4
4
|
|
|
5
5
|
Login with ZOREAL for the browser, framework-free: a ZOREAL Verified
|
|
6
6
|
Proof-of-Human behind every sign-in.
|
|
@@ -611,6 +611,17 @@ treats as attacker-controlled, so nothing it resolves is trusted until your
|
|
|
611
611
|
backend has verified the ID token's signature, `iss`, `aud`, `exp` and `nonce`
|
|
612
612
|
against the JWKS. `unsafeClaims` is named for exactly that reason.
|
|
613
613
|
|
|
614
|
+
## Verifying this release
|
|
615
|
+
|
|
616
|
+
Every version is published from GitHub Actions with [npm provenance](https://docs.npmjs.com/generating-provenance-statements): the package page on npmjs.com carries a **Provenance** panel linking the exact commit and workflow run that built the tarball, signed through [Sigstore](https://www.sigstore.dev/) and recorded in its public transparency log. No long-lived npm token stands behind it — the workflow authenticates by OIDC ([trusted publishing](https://docs.npmjs.com/trusted-publishers)), so a leaked CI secret cannot cut a release.
|
|
617
|
+
|
|
618
|
+
Check the signatures on what you actually installed:
|
|
619
|
+
|
|
620
|
+
```sh
|
|
621
|
+
npm install @zoreal/oauth2-js
|
|
622
|
+
npm audit signatures
|
|
623
|
+
```
|
|
624
|
+
|
|
614
625
|
## The ZOREAL OAuth2 library family
|
|
615
626
|
|
|
616
627
|
| Repository | Package | Role |
|
package/dist/index.cjs
CHANGED
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/jwt.ts","../src/wire.ts","../src/pairing.ts","../src/pkce.ts","../src/login.ts"],"sourcesContent":["export { startLogin } from './login';\nexport {\n FlowAbandonedError,\n OAuthFlowError,\n exchangeCode,\n isMobileUserAgent,\n pollUntilApproved,\n startPairing,\n} from './pairing';\nexport type { StartPairingParams } from './pairing';\nexport { challengeS256, generateState, generateVerifier } from './pkce';\nexport { unsafeClaims } from './jwt';\nexport {\n DEFAULT_ISSUER,\n POLL_INTERVAL_ENROLLING_MS,\n POLL_INTERVAL_MS,\n SDK_NAME,\n SDK_VERSION,\n WIRE_VERSION,\n} from './wire';\nexport type {\n PairCreated,\n PairImmediate,\n PairStartResponse,\n PairStatusResponse,\n TokenResponse,\n} from './wire';\nexport type {\n AcrValue,\n AuthCodeLoginOptions,\n BrowserDirectLoginOptions,\n ErrorCode,\n LoginHandle,\n NonOAuthError,\n PairingState,\n SelectBy,\n StartLoginOptions,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n} from './types';\n","/**\n * Reads claims OUT of an ID token without verifying it.\n *\n * That is not a shortcut, it is the design: this code runs in a browser the\n * threat model assumes is attacker-controlled, so a signature check here\n * proves nothing to anyone. The token is verified where verification means\n * something: server-side against the JWKS. What this parser feeds is\n * convenience fields (acr on the response object) that the types document as\n * convenience, with the token staying the authority.\n */\n\nexport function unsafeClaims(idToken: string): Record<string, unknown> {\n try {\n const payload = idToken.split('.')[1] ?? '';\n const b64 = payload.replace(/-/g, '+').replace(/_/g, '/');\n const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n return JSON.parse(\n new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)))\n );\n } catch {\n return {};\n }\n}\n","/**\n * The wire protocol between this package and the ZOREAL OpenID Provider.\n *\n * VERSIONED: a shipped version keeps working until the provider explicitly\n * refuses it, and when it does, the reason is surfaced verbatim. Both the wire\n * version and the package version travel on every pairing request so a refusal\n * can be precise.\n *\n * Endpoints, all relative to the issuer and all CORS-gated on the client's\n * authorized JavaScript origins (the dashboard):\n *\n * POST /pair start a pairing request. Body carries the\n * authorize parameters plus PKCE challenge.\n * Returns { request_id, pair_url, expires_in }\n * or, for prompt=none with a live consented\n * session, { code } immediately.\n * GET /pair/:id/status poll: pending | claimed |\n * approved (with code) | denied | expired |\n * enrolling. Over-polling cancels the request\n * rather than throttling it, so the cadence\n * below is not a suggestion.\n * GET /pair/:id/qr.svg the QR image for the pairing URL, served by\n * the provider so the pairing surface stays\n * changeable at runtime and\n * this package keeps zero dependencies.\n * POST /token the code exchange. Browser-direct mode uses\n * it directly with PKCE and no client secret;\n * auth-code mode leaves it to the RP backend.\n */\n\nexport const WIRE_VERSION = 1;\nexport const SDK_VERSION = '0.1.5';\nexport const SDK_NAME = '@zoreal/oauth2-js';\nexport const DEFAULT_ISSUER = 'https://id.zoreal.com';\n\n/** Pending TTL is short. Poll gently; over-polling cancels the request. */\nexport const POLL_INTERVAL_MS = 2000;\n/** Enrolling extends the window well beyond a normal login; poll slower. */\nexport const POLL_INTERVAL_ENROLLING_MS = 5000;\n\nexport interface PairCreated {\n request_id: string;\n /** https://zoreal.com/qr/<request_id>. The same URL in QR and app link. */\n pair_url: string;\n expires_in: number;\n}\n\nexport interface PairImmediate {\n /** prompt=none resolved silently: consented sector, live session. */\n code: string;\n}\n\nexport type PairStartResponse = PairCreated | PairImmediate;\n\nexport interface PairStatusResponse {\n status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'cancelled' | 'enrolling';\n code?: string;\n expires_in?: number;\n enrolment_deadline?: number;\n /** The provider's reason on denial or refusal. Surfaced verbatim, never rewritten. */\n error?: string;\n error_description?: string;\n}\n\nexport interface TokenResponse {\n id_token: string;\n access_token?: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n","/**\n * The pairing channel, client side. wire.ts pins the endpoints.\n *\n * The browser polls; the phone never talks to the browser. Everything here is\n * therefore plain fetch against the issuer, CORS-gated on the client's\n * authorized origins, with the poll cadence fixed: the provider cancels an\n * over-polling request rather than throttling it, so a \"retry\n * faster on error\" strategy here would kill the login it is trying to save.\n */\n\nimport {\n POLL_INTERVAL_ENROLLING_MS,\n POLL_INTERVAL_MS,\n SDK_NAME,\n SDK_VERSION,\n WIRE_VERSION,\n type PairStartResponse,\n type PairStatusResponse,\n type TokenResponse,\n} from './wire';\nimport type { ErrorCode, NonOAuthError, PairingState } from './types';\n\nexport class OAuthFlowError extends Error {\n constructor(\n public error: ErrorCode,\n public description?: string\n ) {\n super(description ?? error);\n }\n}\n\nexport class FlowAbandonedError extends Error {\n constructor(public reason: NonOAuthError) {\n super(reason.description ?? reason.type);\n }\n}\n\nexport interface StartPairingParams {\n client_id: string;\n scope: string;\n state: string;\n nonce: string;\n code_challenge: string;\n redirect_uri?: string;\n acr_values?: string;\n max_age?: number;\n prompt?: string;\n locale?: string;\n}\n\nasync function parseJson(response: Response): Promise<Record<string, unknown>> {\n try {\n return (await response.json()) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\nexport async function startPairing(\n issuer: string,\n params: StartPairingParams\n): Promise<PairStartResponse> {\n const response = await fetch(`${issuer}/pair`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n ...params,\n code_challenge_method: 'S256',\n wire_version: WIRE_VERSION,\n sdk: `${SDK_NAME}/${SDK_VERSION}`,\n }),\n });\n\n const body = await parseJson(response);\n if (!response.ok) {\n // The provider's words, verbatim. A refused package version arrives here,\n // and rewriting its reason would hide the only signal telling an integrator\n // to upgrade.\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n (body.error_description as string) ?? `The provider refused the request (${response.status})`\n );\n }\n return body as unknown as PairStartResponse;\n}\n\nconst sleep = (ms: number, signal?: AbortSignal) =>\n new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('aborted', 'AbortError'));\n return;\n }\n const t = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => {\n clearTimeout(t);\n reject(new DOMException('aborted', 'AbortError'));\n });\n });\n\n/**\n * Polls until the request resolves. Returns the authorization code.\n * Throws FlowAbandonedError for the human outcomes (denied, expired,\n * enrolment abandoned) and OAuthFlowError for protocol ones.\n */\nexport async function pollUntilApproved(\n issuer: string,\n requestId: string,\n onState?: (state: PairingState) => void,\n signal?: AbortSignal\n): Promise<string> {\n for (;;) {\n const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {\n signal,\n });\n const body = (await parseJson(response)) as unknown as PairStatusResponse;\n\n if (!response.ok) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Pairing status failed (${response.status})`\n );\n }\n\n onState?.({\n status: body.status,\n expiresIn: body.expires_in,\n enrolmentDeadline: body.enrolment_deadline,\n });\n\n switch (body.status) {\n case 'approved':\n if (!body.code) {\n throw new OAuthFlowError('server_error', 'approved with no authorization code');\n }\n return body.code;\n case 'denied':\n throw new FlowAbandonedError({ type: 'request_denied', description: body.error_description });\n case 'expired':\n throw new FlowAbandonedError({ type: 'request_expired', description: body.error_description });\n case 'cancelled':\n // The provider cancels an over-polled or abandoned request outright\n // (its pairing rows have a real cancelled state); a poll that treats\n // it as unknown spins on a dead request forever.\n throw new FlowAbandonedError({\n type: 'request_expired',\n description: body.error_description ?? 'the provider cancelled the pairing request',\n });\n case 'enrolling':\n await sleep(POLL_INTERVAL_ENROLLING_MS, signal);\n break;\n default:\n await sleep(POLL_INTERVAL_MS, signal);\n }\n }\n}\n\n/**\n * The code exchange, browser-direct mode only: a public client, PKCE and no\n * secret. What comes back can only ever be the pseudonymous tier, by\n * construction rather than by rule: personal data lives at /userinfo behind an\n * access token this mode is never issued, because personal-data scopes are\n * refused for public clients at the pairing step.\n */\nexport async function exchangeCode(\n issuer: string,\n input: { code: string; code_verifier: string; client_id: string }\n): Promise<TokenResponse> {\n const response = await fetch(`${issuer}/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: input.code,\n code_verifier: input.code_verifier,\n client_id: input.client_id,\n }),\n });\n\n const body = (await parseJson(response)) as unknown as TokenResponse;\n if (!response.ok || body.error) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Token exchange failed (${response.status})`\n );\n }\n return body;\n}\n\n/** A mobile user agent gets the app link, not a QR of its own screen. */\nexport function isMobileUserAgent(): boolean {\n if (typeof navigator === 'undefined') return false;\n return /android|iphone|ipad|ipod/i.test(navigator.userAgent);\n}\n","/**\n * PKCE, S256 only: mandatory for every client, confidential ones included.\n * There is no plain fallback and there must never be one; a provider seeing\n * method=plain is seeing a bug or an attack.\n */\n\nconst VERIFIER_BYTES = 32; // 43 base64url chars, the RFC 7636 minimum length\n\nconst base64url = (bytes: Uint8Array): string =>\n btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\nexport function generateVerifier(): string {\n const bytes = new Uint8Array(VERIFIER_BYTES);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n\nexport async function challengeS256(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));\n return base64url(new Uint8Array(digest));\n}\n\nexport function generateState(): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n","/**\n * The one flow, as an imperative handle. This is the same state machine the\n * React SDK's hook runs, without the React: start a pairing, surface it for\n * rendering through onState, poll, and finish per mode. Browser-direct\n * exchanges the code here (public client, PKCE, no secret) and hands over an\n * ID token; auth-code hands the code and the PKCE verifier to the caller,\n * whose backend does the exchange with its client authentication.\n *\n * A framework wrapper owns exactly two things: calling startLogin on the\n * user's gesture, and rendering what onState carries. Everything else -\n * PKCE, state, nonce, cadence, cancellation - lives here.\n */\n\nimport { unsafeClaims } from './jwt';\nimport {\n FlowAbandonedError,\n OAuthFlowError,\n exchangeCode,\n isMobileUserAgent,\n pollUntilApproved,\n startPairing,\n} from './pairing';\nimport { challengeS256, generateState, generateVerifier } from './pkce';\nimport { DEFAULT_ISSUER } from './wire';\nimport type {\n AcrValue,\n AuthCodeLoginOptions,\n BrowserDirectLoginOptions,\n LoginHandle,\n SelectBy,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n} from './types';\n\nexport function startLogin(\n options: BrowserDirectLoginOptions\n): LoginHandle<ZorealCredentialResponse>;\nexport function startLogin(options: AuthCodeLoginOptions): LoginHandle<ZorealCodeResponse>;\nexport function startLogin(\n options: BrowserDirectLoginOptions | AuthCodeLoginOptions\n): LoginHandle<ZorealCredentialResponse> | LoginHandle<ZorealCodeResponse> {\n if ('ux_mode' in options && options.ux_mode === 'redirect') {\n // The popup shape only: the code and PKCE verifier resolve the promise\n // and go from there to your backend over TLS. A redirect would have to\n // carry the verifier in a URL, which is a credential in every access\n // log on the path. Refused loudly rather than implemented badly.\n throw new Error(\n \"@zoreal/oauth2-js: ux_mode 'redirect' is not supported. Use the default \" +\n \"'popup' shape and post the code and code_verifier from the resolved \" +\n 'promise to your backend.'\n );\n }\n\n const flow = options.flow ?? 'browser-direct';\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const controller = new AbortController();\n\n const surface: {\n requestId?: string;\n pairUrl?: string;\n qrUrl?: string;\n appLink?: boolean;\n } = {};\n\n const cancel = () => controller.abort();\n\n const run = async (): Promise<ZorealCredentialResponse | ZorealCodeResponse> => {\n const verifier = generateVerifier();\n const state = generateState();\n const nonce = generateState();\n\n try {\n const started = await startPairing(issuer, {\n client_id: options.clientId,\n scope: options.scope ?? 'openid',\n state,\n nonce,\n code_challenge: await challengeS256(verifier),\n redirect_uri: flow === 'auth-code' ? (options as AuthCodeLoginOptions).redirect_uri : undefined,\n acr_values: Array.isArray(options.acr_values)\n ? options.acr_values.join(' ')\n : options.acr_values,\n max_age: options.max_age,\n prompt: options.prompt,\n locale: options.locale,\n });\n\n let code: string;\n let selectBy: SelectBy = 'device';\n\n if ('code' in started) {\n // prompt=none resolved silently: consented sector, live session.\n code = started.code;\n selectBy = 'session';\n } else {\n const useAppLink =\n options.display === 'link' || (options.display !== 'qr' && isMobileUserAgent());\n selectBy = useAppLink ? 'app_link' : 'qr';\n\n surface.requestId = started.request_id;\n surface.pairUrl = started.pair_url;\n surface.qrUrl = `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`;\n surface.appLink = useAppLink;\n\n // Everything a caller-rendered pairing UI needs, on every state it\n // sees: the QR flow cannot complete unless SOMETHING renders pairUrl,\n // and in this package that something is always the caller.\n const stateSurface = {\n pairUrl: surface.pairUrl,\n qrUrl: surface.qrUrl,\n appLink: useAppLink,\n cancel,\n };\n\n // The initial state, immediately: the first poll response is one\n // round-trip away, and a UI that waits for it opens visibly empty.\n options.onState?.({ status: 'pending', expiresIn: started.expires_in, ...stateSurface });\n\n if (useAppLink && typeof window !== 'undefined') {\n // The universal link, in the same tab: the app claims it, and with\n // no app installed the same URL is the real pairing page, which can\n // enrol. A popup here would be blocked more often than it would\n // help.\n window.location.assign(started.pair_url);\n }\n\n code = await pollUntilApproved(\n issuer,\n started.request_id,\n (s) => options.onState?.({ ...s, ...stateSurface }),\n controller.signal\n );\n }\n\n if (flow === 'auth-code') {\n const response: ZorealCodeResponse = {\n code,\n scope: options.scope ?? 'openid',\n app_state: options.app_state,\n code_verifier: verifier,\n nonce,\n };\n return response;\n }\n\n const tokens = await exchangeCode(issuer, {\n code,\n code_verifier: verifier,\n client_id: options.clientId,\n });\n const claims = unsafeClaims(tokens.id_token);\n const response: ZorealCredentialResponse = {\n credential: tokens.id_token,\n clientId: options.clientId,\n select_by: selectBy,\n acr: (claims.acr as AcrValue) ?? 'zoreal.device',\n };\n return response;\n } catch (e) {\n // The taxonomy the promise rejects with, and nothing else:\n // OAuthFlowError the provider refused; reason verbatim\n // FlowAbandonedError a human outcome, or a failure that never\n // reached the provider (network, unknown)\n // AbortError the caller's own cancel()\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n if (e instanceof OAuthFlowError || e instanceof FlowAbandonedError) throw e;\n throw new FlowAbandonedError({\n type: 'unknown',\n description: e instanceof Error ? e.message : String(e),\n });\n }\n };\n\n const promise = run();\n // A caller driving everything from onState and cancel() may never attach a\n // rejection handler; this no-op one keeps a cancelled login from surfacing\n // as an unhandled rejection. The caller's own catch still sees the error.\n promise.catch(() => {});\n\n return {\n promise: promise as Promise<ZorealCredentialResponse> & Promise<ZorealCodeResponse>,\n cancel,\n get requestId() {\n return surface.requestId;\n },\n get pairUrl() {\n return surface.pairUrl;\n },\n get qrUrl() {\n return surface.qrUrl;\n },\n get appLink() {\n return surface.appLink;\n },\n };\n}\n"],"mappings":"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","names":["response"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/jwt.ts","../src/wire.ts","../src/pairing.ts","../src/pkce.ts","../src/login.ts"],"sourcesContent":["export { startLogin } from './login';\nexport {\n FlowAbandonedError,\n OAuthFlowError,\n exchangeCode,\n isMobileUserAgent,\n pollUntilApproved,\n startPairing,\n} from './pairing';\nexport type { StartPairingParams } from './pairing';\nexport { challengeS256, generateState, generateVerifier } from './pkce';\nexport { unsafeClaims } from './jwt';\nexport {\n DEFAULT_ISSUER,\n POLL_INTERVAL_ENROLLING_MS,\n POLL_INTERVAL_MS,\n SDK_NAME,\n SDK_VERSION,\n WIRE_VERSION,\n} from './wire';\nexport type {\n PairCreated,\n PairImmediate,\n PairStartResponse,\n PairStatusResponse,\n TokenResponse,\n} from './wire';\nexport type {\n AcrValue,\n AuthCodeLoginOptions,\n BrowserDirectLoginOptions,\n ErrorCode,\n LoginHandle,\n NonOAuthError,\n PairingState,\n SelectBy,\n StartLoginOptions,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n} from './types';\n","/**\n * Reads claims OUT of an ID token without verifying it.\n *\n * That is not a shortcut, it is the design: this code runs in a browser the\n * threat model assumes is attacker-controlled, so a signature check here\n * proves nothing to anyone. The token is verified where verification means\n * something: server-side against the JWKS. What this parser feeds is\n * convenience fields (acr on the response object) that the types document as\n * convenience, with the token staying the authority.\n */\n\nexport function unsafeClaims(idToken: string): Record<string, unknown> {\n try {\n const payload = idToken.split('.')[1] ?? '';\n const b64 = payload.replace(/-/g, '+').replace(/_/g, '/');\n const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n return JSON.parse(\n new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)))\n );\n } catch {\n return {};\n }\n}\n","/**\n * The wire protocol between this package and the ZOREAL OpenID Provider.\n *\n * VERSIONED: a shipped version keeps working until the provider explicitly\n * refuses it, and when it does, the reason is surfaced verbatim. Both the wire\n * version and the package version travel on every pairing request so a refusal\n * can be precise.\n *\n * Endpoints, all relative to the issuer and all CORS-gated on the client's\n * authorized JavaScript origins (the dashboard):\n *\n * POST /pair start a pairing request. Body carries the\n * authorize parameters plus PKCE challenge.\n * Returns { request_id, pair_url, expires_in }\n * or, for prompt=none with a live consented\n * session, { code } immediately.\n * GET /pair/:id/status poll: pending | claimed |\n * approved (with code) | denied | expired |\n * enrolling. Over-polling cancels the request\n * rather than throttling it, so the cadence\n * below is not a suggestion.\n * GET /pair/:id/qr.svg the QR image for the pairing URL, served by\n * the provider so the pairing surface stays\n * changeable at runtime and\n * this package keeps zero dependencies.\n * POST /token the code exchange. Browser-direct mode uses\n * it directly with PKCE and no client secret;\n * auth-code mode leaves it to the RP backend.\n */\n\nexport const WIRE_VERSION = 1;\nexport const SDK_VERSION = '0.1.6';\nexport const SDK_NAME = '@zoreal/oauth2-js';\nexport const DEFAULT_ISSUER = 'https://id.zoreal.com';\n\n/** Pending TTL is short. Poll gently; over-polling cancels the request. */\nexport const POLL_INTERVAL_MS = 2000;\n/** Enrolling extends the window well beyond a normal login; poll slower. */\nexport const POLL_INTERVAL_ENROLLING_MS = 5000;\n\nexport interface PairCreated {\n request_id: string;\n /** https://zoreal.com/qr/<request_id>. The same URL in QR and app link. */\n pair_url: string;\n expires_in: number;\n}\n\nexport interface PairImmediate {\n /** prompt=none resolved silently: consented sector, live session. */\n code: string;\n}\n\nexport type PairStartResponse = PairCreated | PairImmediate;\n\nexport interface PairStatusResponse {\n status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'cancelled' | 'enrolling';\n code?: string;\n expires_in?: number;\n enrolment_deadline?: number;\n /** The provider's reason on denial or refusal. Surfaced verbatim, never rewritten. */\n error?: string;\n error_description?: string;\n}\n\nexport interface TokenResponse {\n id_token: string;\n access_token?: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n","/**\n * The pairing channel, client side. wire.ts pins the endpoints.\n *\n * The browser polls; the phone never talks to the browser. Everything here is\n * therefore plain fetch against the issuer, CORS-gated on the client's\n * authorized origins, with the poll cadence fixed: the provider cancels an\n * over-polling request rather than throttling it, so a \"retry\n * faster on error\" strategy here would kill the login it is trying to save.\n */\n\nimport {\n POLL_INTERVAL_ENROLLING_MS,\n POLL_INTERVAL_MS,\n SDK_NAME,\n SDK_VERSION,\n WIRE_VERSION,\n type PairStartResponse,\n type PairStatusResponse,\n type TokenResponse,\n} from './wire';\nimport type { ErrorCode, NonOAuthError, PairingState } from './types';\n\nexport class OAuthFlowError extends Error {\n constructor(\n public error: ErrorCode,\n public description?: string\n ) {\n super(description ?? error);\n }\n}\n\nexport class FlowAbandonedError extends Error {\n constructor(public reason: NonOAuthError) {\n super(reason.description ?? reason.type);\n }\n}\n\nexport interface StartPairingParams {\n client_id: string;\n scope: string;\n state: string;\n nonce: string;\n code_challenge: string;\n redirect_uri?: string;\n acr_values?: string;\n max_age?: number;\n prompt?: string;\n locale?: string;\n}\n\nasync function parseJson(response: Response): Promise<Record<string, unknown>> {\n try {\n return (await response.json()) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\nexport async function startPairing(\n issuer: string,\n params: StartPairingParams\n): Promise<PairStartResponse> {\n const response = await fetch(`${issuer}/pair`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n ...params,\n code_challenge_method: 'S256',\n wire_version: WIRE_VERSION,\n sdk: `${SDK_NAME}/${SDK_VERSION}`,\n }),\n });\n\n const body = await parseJson(response);\n if (!response.ok) {\n // The provider's words, verbatim. A refused package version arrives here,\n // and rewriting its reason would hide the only signal telling an integrator\n // to upgrade.\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n (body.error_description as string) ?? `The provider refused the request (${response.status})`\n );\n }\n return body as unknown as PairStartResponse;\n}\n\nconst sleep = (ms: number, signal?: AbortSignal) =>\n new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('aborted', 'AbortError'));\n return;\n }\n const t = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => {\n clearTimeout(t);\n reject(new DOMException('aborted', 'AbortError'));\n });\n });\n\n/**\n * Polls until the request resolves. Returns the authorization code.\n * Throws FlowAbandonedError for the human outcomes (denied, expired,\n * enrolment abandoned) and OAuthFlowError for protocol ones.\n */\nexport async function pollUntilApproved(\n issuer: string,\n requestId: string,\n onState?: (state: PairingState) => void,\n signal?: AbortSignal\n): Promise<string> {\n for (;;) {\n const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {\n signal,\n });\n const body = (await parseJson(response)) as unknown as PairStatusResponse;\n\n if (!response.ok) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Pairing status failed (${response.status})`\n );\n }\n\n onState?.({\n status: body.status,\n expiresIn: body.expires_in,\n enrolmentDeadline: body.enrolment_deadline,\n });\n\n switch (body.status) {\n case 'approved':\n if (!body.code) {\n throw new OAuthFlowError('server_error', 'approved with no authorization code');\n }\n return body.code;\n case 'denied':\n throw new FlowAbandonedError({ type: 'request_denied', description: body.error_description });\n case 'expired':\n throw new FlowAbandonedError({ type: 'request_expired', description: body.error_description });\n case 'cancelled':\n // The provider cancels an over-polled or abandoned request outright\n // (its pairing rows have a real cancelled state); a poll that treats\n // it as unknown spins on a dead request forever.\n throw new FlowAbandonedError({\n type: 'request_expired',\n description: body.error_description ?? 'the provider cancelled the pairing request',\n });\n case 'enrolling':\n await sleep(POLL_INTERVAL_ENROLLING_MS, signal);\n break;\n default:\n await sleep(POLL_INTERVAL_MS, signal);\n }\n }\n}\n\n/**\n * The code exchange, browser-direct mode only: a public client, PKCE and no\n * secret. What comes back can only ever be the pseudonymous tier, by\n * construction rather than by rule: personal data lives at /userinfo behind an\n * access token this mode is never issued, because personal-data scopes are\n * refused for public clients at the pairing step.\n */\nexport async function exchangeCode(\n issuer: string,\n input: { code: string; code_verifier: string; client_id: string }\n): Promise<TokenResponse> {\n const response = await fetch(`${issuer}/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: input.code,\n code_verifier: input.code_verifier,\n client_id: input.client_id,\n }),\n });\n\n const body = (await parseJson(response)) as unknown as TokenResponse;\n if (!response.ok || body.error) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Token exchange failed (${response.status})`\n );\n }\n return body;\n}\n\n/** A mobile user agent gets the app link, not a QR of its own screen. */\nexport function isMobileUserAgent(): boolean {\n if (typeof navigator === 'undefined') return false;\n return /android|iphone|ipad|ipod/i.test(navigator.userAgent);\n}\n","/**\n * PKCE, S256 only: mandatory for every client, confidential ones included.\n * There is no plain fallback and there must never be one; a provider seeing\n * method=plain is seeing a bug or an attack.\n */\n\nconst VERIFIER_BYTES = 32; // 43 base64url chars, the RFC 7636 minimum length\n\nconst base64url = (bytes: Uint8Array): string =>\n btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\nexport function generateVerifier(): string {\n const bytes = new Uint8Array(VERIFIER_BYTES);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n\nexport async function challengeS256(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));\n return base64url(new Uint8Array(digest));\n}\n\nexport function generateState(): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n","/**\n * The one flow, as an imperative handle. This is the same state machine the\n * React SDK's hook runs, without the React: start a pairing, surface it for\n * rendering through onState, poll, and finish per mode. Browser-direct\n * exchanges the code here (public client, PKCE, no secret) and hands over an\n * ID token; auth-code hands the code and the PKCE verifier to the caller,\n * whose backend does the exchange with its client authentication.\n *\n * A framework wrapper owns exactly two things: calling startLogin on the\n * user's gesture, and rendering what onState carries. Everything else -\n * PKCE, state, nonce, cadence, cancellation - lives here.\n */\n\nimport { unsafeClaims } from './jwt';\nimport {\n FlowAbandonedError,\n OAuthFlowError,\n exchangeCode,\n isMobileUserAgent,\n pollUntilApproved,\n startPairing,\n} from './pairing';\nimport { challengeS256, generateState, generateVerifier } from './pkce';\nimport { DEFAULT_ISSUER } from './wire';\nimport type {\n AcrValue,\n AuthCodeLoginOptions,\n BrowserDirectLoginOptions,\n LoginHandle,\n SelectBy,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n} from './types';\n\nexport function startLogin(\n options: BrowserDirectLoginOptions\n): LoginHandle<ZorealCredentialResponse>;\nexport function startLogin(options: AuthCodeLoginOptions): LoginHandle<ZorealCodeResponse>;\nexport function startLogin(\n options: BrowserDirectLoginOptions | AuthCodeLoginOptions\n): LoginHandle<ZorealCredentialResponse> | LoginHandle<ZorealCodeResponse> {\n if ('ux_mode' in options && options.ux_mode === 'redirect') {\n // The popup shape only: the code and PKCE verifier resolve the promise\n // and go from there to your backend over TLS. A redirect would have to\n // carry the verifier in a URL, which is a credential in every access\n // log on the path. Refused loudly rather than implemented badly.\n throw new Error(\n \"@zoreal/oauth2-js: ux_mode 'redirect' is not supported. Use the default \" +\n \"'popup' shape and post the code and code_verifier from the resolved \" +\n 'promise to your backend.'\n );\n }\n\n const flow = options.flow ?? 'browser-direct';\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const controller = new AbortController();\n\n const surface: {\n requestId?: string;\n pairUrl?: string;\n qrUrl?: string;\n appLink?: boolean;\n } = {};\n\n const cancel = () => controller.abort();\n\n const run = async (): Promise<ZorealCredentialResponse | ZorealCodeResponse> => {\n const verifier = generateVerifier();\n const state = generateState();\n const nonce = generateState();\n\n try {\n const started = await startPairing(issuer, {\n client_id: options.clientId,\n scope: options.scope ?? 'openid',\n state,\n nonce,\n code_challenge: await challengeS256(verifier),\n redirect_uri: flow === 'auth-code' ? (options as AuthCodeLoginOptions).redirect_uri : undefined,\n acr_values: Array.isArray(options.acr_values)\n ? options.acr_values.join(' ')\n : options.acr_values,\n max_age: options.max_age,\n prompt: options.prompt,\n locale: options.locale,\n });\n\n let code: string;\n let selectBy: SelectBy = 'device';\n\n if ('code' in started) {\n // prompt=none resolved silently: consented sector, live session.\n code = started.code;\n selectBy = 'session';\n } else {\n const useAppLink =\n options.display === 'link' || (options.display !== 'qr' && isMobileUserAgent());\n selectBy = useAppLink ? 'app_link' : 'qr';\n\n surface.requestId = started.request_id;\n surface.pairUrl = started.pair_url;\n surface.qrUrl = `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`;\n surface.appLink = useAppLink;\n\n // Everything a caller-rendered pairing UI needs, on every state it\n // sees: the QR flow cannot complete unless SOMETHING renders pairUrl,\n // and in this package that something is always the caller.\n const stateSurface = {\n pairUrl: surface.pairUrl,\n qrUrl: surface.qrUrl,\n appLink: useAppLink,\n cancel,\n };\n\n // The initial state, immediately: the first poll response is one\n // round-trip away, and a UI that waits for it opens visibly empty.\n options.onState?.({ status: 'pending', expiresIn: started.expires_in, ...stateSurface });\n\n if (useAppLink && typeof window !== 'undefined') {\n // The universal link, in the same tab: the app claims it, and with\n // no app installed the same URL is the real pairing page, which can\n // enrol. A popup here would be blocked more often than it would\n // help.\n window.location.assign(started.pair_url);\n }\n\n code = await pollUntilApproved(\n issuer,\n started.request_id,\n (s) => options.onState?.({ ...s, ...stateSurface }),\n controller.signal\n );\n }\n\n if (flow === 'auth-code') {\n const response: ZorealCodeResponse = {\n code,\n scope: options.scope ?? 'openid',\n app_state: options.app_state,\n code_verifier: verifier,\n nonce,\n };\n return response;\n }\n\n const tokens = await exchangeCode(issuer, {\n code,\n code_verifier: verifier,\n client_id: options.clientId,\n });\n const claims = unsafeClaims(tokens.id_token);\n const response: ZorealCredentialResponse = {\n credential: tokens.id_token,\n clientId: options.clientId,\n select_by: selectBy,\n acr: (claims.acr as AcrValue) ?? 'zoreal.device',\n };\n return response;\n } catch (e) {\n // The taxonomy the promise rejects with, and nothing else:\n // OAuthFlowError the provider refused; reason verbatim\n // FlowAbandonedError a human outcome, or a failure that never\n // reached the provider (network, unknown)\n // AbortError the caller's own cancel()\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n if (e instanceof OAuthFlowError || e instanceof FlowAbandonedError) throw e;\n throw new FlowAbandonedError({\n type: 'unknown',\n description: e instanceof Error ? e.message : String(e),\n });\n }\n };\n\n const promise = run();\n // A caller driving everything from onState and cancel() may never attach a\n // rejection handler; this no-op one keeps a cancelled login from surfacing\n // as an unhandled rejection. The caller's own catch still sees the error.\n promise.catch(() => {});\n\n return {\n promise: promise as Promise<ZorealCredentialResponse> & Promise<ZorealCodeResponse>,\n cancel,\n get requestId() {\n return surface.requestId;\n },\n get pairUrl() {\n return surface.pairUrl;\n },\n get qrUrl() {\n return surface.qrUrl;\n },\n get appLink() {\n return surface.appLink;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWO,SAAS,aAAa,SAA0C;AACrE,MAAI;AACF,UAAM,UAAU,QAAQ,MAAM,GAAG,EAAE,CAAC,KAAK;AACzC,UAAM,MAAM,QAAQ,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AACxD,UAAM,SAAS,MAAM,IAAI,QAAQ,IAAK,IAAI,SAAS,KAAM,CAAC;AAC1D,WAAO,KAAK;AAAA,MACV,IAAI,YAAY,EAAE,OAAO,WAAW,KAAK,KAAK,MAAM,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;AAAA,IAChF;AAAA,EACF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ACQO,IAAM,eAAe;AACrB,IAAM,cAAc;AACpB,IAAM,WAAW;AACjB,IAAM,iBAAiB;AAGvB,IAAM,mBAAmB;AAEzB,IAAM,6BAA6B;;;AChBnC,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACxC,YACS,OACA,aACP;AACA,UAAM,eAAe,KAAK;AAHnB;AACA;AAAA,EAGT;AACF;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAmB,QAAuB;AACxC,UAAM,OAAO,eAAe,OAAO,IAAI;AADtB;AAAA,EAEnB;AACF;AAeA,eAAe,UAAU,UAAsD;AAC7E,MAAI;AACF,WAAQ,MAAM,SAAS,KAAK;AAAA,EAC9B,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAsB,aACpB,QACA,QAC4B;AAC5B,QAAM,WAAW,MAAM,MAAM,GAAG,MAAM,SAAS;AAAA,IAC7C,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU;AAAA,MACnB,GAAG;AAAA,MACH,uBAAuB;AAAA,MACvB,cAAc;AAAA,MACd,KAAK,GAAG,QAAQ,IAAI,WAAW;AAAA,IACjC,CAAC;AAAA,EACH,CAAC;AAED,QAAM,OAAO,MAAM,UAAU,QAAQ;AACrC,MAAI,CAAC,SAAS,IAAI;AAIhB,UAAM,IAAI;AAAA,MACP,KAAK,SAAuB;AAAA,MAC5B,KAAK,qBAAgC,qCAAqC,SAAS,MAAM;AAAA,IAC5F;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,QAAQ,CAAC,IAAY,WACzB,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,MAAI,QAAQ,SAAS;AACnB,WAAO,IAAI,aAAa,WAAW,YAAY,CAAC;AAChD;AAAA,EACF;AACA,QAAM,IAAI,WAAW,SAAS,EAAE;AAChC,UAAQ,iBAAiB,SAAS,MAAM;AACtC,iBAAa,CAAC;AACd,WAAO,IAAI,aAAa,WAAW,YAAY,CAAC;AAAA,EAClD,CAAC;AACH,CAAC;AAOH,eAAsB,kBACpB,QACA,WACA,SACA,QACiB;AACjB,aAAS;AACP,UAAM,WAAW,MAAM,MAAM,GAAG,MAAM,SAAS,mBAAmB,SAAS,CAAC,WAAW;AAAA,MACrF;AAAA,IACF,CAAC;AACD,UAAM,OAAQ,MAAM,UAAU,QAAQ;AAEtC,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACP,KAAK,SAAuB;AAAA,QAC7B,KAAK,qBAAqB,0BAA0B,SAAS,MAAM;AAAA,MACrE;AAAA,IACF;AAEA,cAAU;AAAA,MACR,QAAQ,KAAK;AAAA,MACb,WAAW,KAAK;AAAA,MAChB,mBAAmB,KAAK;AAAA,IAC1B,CAAC;AAED,YAAQ,KAAK,QAAQ;AAAA,MACnB,KAAK;AACH,YAAI,CAAC,KAAK,MAAM;AACd,gBAAM,IAAI,eAAe,gBAAgB,qCAAqC;AAAA,QAChF;AACA,eAAO,KAAK;AAAA,MACd,KAAK;AACH,cAAM,IAAI,mBAAmB,EAAE,MAAM,kBAAkB,aAAa,KAAK,kBAAkB,CAAC;AAAA,MAC9F,KAAK;AACH,cAAM,IAAI,mBAAmB,EAAE,MAAM,mBAAmB,aAAa,KAAK,kBAAkB,CAAC;AAAA,MAC/F,KAAK;AAIH,cAAM,IAAI,mBAAmB;AAAA,UAC3B,MAAM;AAAA,UACN,aAAa,KAAK,qBAAqB;AAAA,QACzC,CAAC;AAAA,MACH,KAAK;AACH,cAAM,MAAM,4BAA4B,MAAM;AAC9C;AAAA,MACF;AACE,cAAM,MAAM,kBAAkB,MAAM;AAAA,IACxC;AAAA,EACF;AACF;AASA,eAAsB,aACpB,QACA,OACwB;AACxB,QAAM,WAAW,MAAM,MAAM,GAAG,MAAM,UAAU;AAAA,IAC9C,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,IAC/D,MAAM,IAAI,gBAAgB;AAAA,MACxB,YAAY;AAAA,MACZ,MAAM,MAAM;AAAA,MACZ,eAAe,MAAM;AAAA,MACrB,WAAW,MAAM;AAAA,IACnB,CAAC;AAAA,EACH,CAAC;AAED,QAAM,OAAQ,MAAM,UAAU,QAAQ;AACtC,MAAI,CAAC,SAAS,MAAM,KAAK,OAAO;AAC9B,UAAM,IAAI;AAAA,MACP,KAAK,SAAuB;AAAA,MAC7B,KAAK,qBAAqB,0BAA0B,SAAS,MAAM;AAAA,IACrE;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,oBAA6B;AAC3C,MAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,SAAO,4BAA4B,KAAK,UAAU,SAAS;AAC7D;;;AC1LA,IAAM,iBAAiB;AAEvB,IAAM,YAAY,CAAC,UACjB,KAAK,OAAO,aAAa,GAAG,KAAK,CAAC,EAC/B,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AAEf,SAAS,mBAA2B;AACzC,QAAM,QAAQ,IAAI,WAAW,cAAc;AAC3C,SAAO,gBAAgB,KAAK;AAC5B,SAAO,UAAU,KAAK;AACxB;AAEA,eAAsB,cAAc,UAAmC;AACrE,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC;AACvF,SAAO,UAAU,IAAI,WAAW,MAAM,CAAC;AACzC;AAEO,SAAS,gBAAwB;AACtC,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,UAAU,KAAK;AACxB;;;ACSO,SAAS,WACd,SACyE;AACzE,MAAI,aAAa,WAAW,QAAQ,YAAY,YAAY;AAK1D,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AAEA,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,aAAa,IAAI,gBAAgB;AAEvC,QAAM,UAKF,CAAC;AAEL,QAAM,SAAS,MAAM,WAAW,MAAM;AAEtC,QAAM,MAAM,YAAoE;AAC9E,UAAM,WAAW,iBAAiB;AAClC,UAAM,QAAQ,cAAc;AAC5B,UAAM,QAAQ,cAAc;AAE5B,QAAI;AACF,YAAM,UAAU,MAAM,aAAa,QAAQ;AAAA,QACzC,WAAW,QAAQ;AAAA,QACnB,OAAO,QAAQ,SAAS;AAAA,QACxB;AAAA,QACA;AAAA,QACA,gBAAgB,MAAM,cAAc,QAAQ;AAAA,QAC5C,cAAc,SAAS,cAAe,QAAiC,eAAe;AAAA,QACtF,YAAY,MAAM,QAAQ,QAAQ,UAAU,IACxC,QAAQ,WAAW,KAAK,GAAG,IAC3B,QAAQ;AAAA,QACZ,SAAS,QAAQ;AAAA,QACjB,QAAQ,QAAQ;AAAA,QAChB,QAAQ,QAAQ;AAAA,MAClB,CAAC;AAED,UAAI;AACJ,UAAI,WAAqB;AAEzB,UAAI,UAAU,SAAS;AAErB,eAAO,QAAQ;AACf,mBAAW;AAAA,MACb,OAAO;AACL,cAAM,aACJ,QAAQ,YAAY,UAAW,QAAQ,YAAY,QAAQ,kBAAkB;AAC/E,mBAAW,aAAa,aAAa;AAErC,gBAAQ,YAAY,QAAQ;AAC5B,gBAAQ,UAAU,QAAQ;AAC1B,gBAAQ,QAAQ,GAAG,MAAM,SAAS,mBAAmB,QAAQ,UAAU,CAAC;AACxE,gBAAQ,UAAU;AAKlB,cAAM,eAAe;AAAA,UACnB,SAAS,QAAQ;AAAA,UACjB,OAAO,QAAQ;AAAA,UACf,SAAS;AAAA,UACT;AAAA,QACF;AAIA,gBAAQ,UAAU,EAAE,QAAQ,WAAW,WAAW,QAAQ,YAAY,GAAG,aAAa,CAAC;AAEvF,YAAI,cAAc,OAAO,WAAW,aAAa;AAK/C,iBAAO,SAAS,OAAO,QAAQ,QAAQ;AAAA,QACzC;AAEA,eAAO,MAAM;AAAA,UACX;AAAA,UACA,QAAQ;AAAA,UACR,CAAC,MAAM,QAAQ,UAAU,EAAE,GAAG,GAAG,GAAG,aAAa,CAAC;AAAA,UAClD,WAAW;AAAA,QACb;AAAA,MACF;AAEA,UAAI,SAAS,aAAa;AACxB,cAAMA,YAA+B;AAAA,UACnC;AAAA,UACA,OAAO,QAAQ,SAAS;AAAA,UACxB,WAAW,QAAQ;AAAA,UACnB,eAAe;AAAA,UACf;AAAA,QACF;AACA,eAAOA;AAAA,MACT;AAEA,YAAM,SAAS,MAAM,aAAa,QAAQ;AAAA,QACxC;AAAA,QACA,eAAe;AAAA,QACf,WAAW,QAAQ;AAAA,MACrB,CAAC;AACD,YAAM,SAAS,aAAa,OAAO,QAAQ;AAC3C,YAAM,WAAqC;AAAA,QACzC,YAAY,OAAO;AAAA,QACnB,UAAU,QAAQ;AAAA,QAClB,WAAW;AAAA,QACX,KAAM,OAAO,OAAoB;AAAA,MACnC;AACA,aAAO;AAAA,IACT,SAAS,GAAG;AAMV,UAAI,aAAa,gBAAgB,EAAE,SAAS,aAAc,OAAM;AAChE,UAAI,aAAa,kBAAkB,aAAa,mBAAoB,OAAM;AAC1E,YAAM,IAAI,mBAAmB;AAAA,QAC3B,MAAM;AAAA,QACN,aAAa,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,MACxD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,UAAU,IAAI;AAIpB,UAAQ,MAAM,MAAM;AAAA,EAAC,CAAC;AAEtB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,IAAI,YAAY;AACd,aAAO,QAAQ;AAAA,IACjB;AAAA,IACA,IAAI,UAAU;AACZ,aAAO,QAAQ;AAAA,IACjB;AAAA,IACA,IAAI,QAAQ;AACV,aAAO,QAAQ;AAAA,IACjB;AAAA,IACA,IAAI,UAAU;AACZ,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AACF;","names":["response"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -178,7 +178,7 @@ declare function startLogin(options: AuthCodeLoginOptions): LoginHandle<ZorealCo
|
|
|
178
178
|
* auth-code mode leaves it to the RP backend.
|
|
179
179
|
*/
|
|
180
180
|
declare const WIRE_VERSION = 1;
|
|
181
|
-
declare const SDK_VERSION = "0.1.
|
|
181
|
+
declare const SDK_VERSION = "0.1.6";
|
|
182
182
|
declare const SDK_NAME = "@zoreal/oauth2-js";
|
|
183
183
|
declare const DEFAULT_ISSUER = "https://id.zoreal.com";
|
|
184
184
|
/** Pending TTL is short. Poll gently; over-polling cancels the request. */
|
package/dist/index.d.ts
CHANGED
|
@@ -178,7 +178,7 @@ declare function startLogin(options: AuthCodeLoginOptions): LoginHandle<ZorealCo
|
|
|
178
178
|
* auth-code mode leaves it to the RP backend.
|
|
179
179
|
*/
|
|
180
180
|
declare const WIRE_VERSION = 1;
|
|
181
|
-
declare const SDK_VERSION = "0.1.
|
|
181
|
+
declare const SDK_VERSION = "0.1.6";
|
|
182
182
|
declare const SDK_NAME = "@zoreal/oauth2-js";
|
|
183
183
|
declare const DEFAULT_ISSUER = "https://id.zoreal.com";
|
|
184
184
|
/** Pending TTL is short. Poll gently; over-polling cancels the request. */
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/jwt.ts","../src/wire.ts","../src/pairing.ts","../src/pkce.ts","../src/login.ts"],"sourcesContent":["/**\n * Reads claims OUT of an ID token without verifying it.\n *\n * That is not a shortcut, it is the design: this code runs in a browser the\n * threat model assumes is attacker-controlled, so a signature check here\n * proves nothing to anyone. The token is verified where verification means\n * something: server-side against the JWKS. What this parser feeds is\n * convenience fields (acr on the response object) that the types document as\n * convenience, with the token staying the authority.\n */\n\nexport function unsafeClaims(idToken: string): Record<string, unknown> {\n try {\n const payload = idToken.split('.')[1] ?? '';\n const b64 = payload.replace(/-/g, '+').replace(/_/g, '/');\n const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n return JSON.parse(\n new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)))\n );\n } catch {\n return {};\n }\n}\n","/**\n * The wire protocol between this package and the ZOREAL OpenID Provider.\n *\n * VERSIONED: a shipped version keeps working until the provider explicitly\n * refuses it, and when it does, the reason is surfaced verbatim. Both the wire\n * version and the package version travel on every pairing request so a refusal\n * can be precise.\n *\n * Endpoints, all relative to the issuer and all CORS-gated on the client's\n * authorized JavaScript origins (the dashboard):\n *\n * POST /pair start a pairing request. Body carries the\n * authorize parameters plus PKCE challenge.\n * Returns { request_id, pair_url, expires_in }\n * or, for prompt=none with a live consented\n * session, { code } immediately.\n * GET /pair/:id/status poll: pending | claimed |\n * approved (with code) | denied | expired |\n * enrolling. Over-polling cancels the request\n * rather than throttling it, so the cadence\n * below is not a suggestion.\n * GET /pair/:id/qr.svg the QR image for the pairing URL, served by\n * the provider so the pairing surface stays\n * changeable at runtime and\n * this package keeps zero dependencies.\n * POST /token the code exchange. Browser-direct mode uses\n * it directly with PKCE and no client secret;\n * auth-code mode leaves it to the RP backend.\n */\n\nexport const WIRE_VERSION = 1;\nexport const SDK_VERSION = '0.1.5';\nexport const SDK_NAME = '@zoreal/oauth2-js';\nexport const DEFAULT_ISSUER = 'https://id.zoreal.com';\n\n/** Pending TTL is short. Poll gently; over-polling cancels the request. */\nexport const POLL_INTERVAL_MS = 2000;\n/** Enrolling extends the window well beyond a normal login; poll slower. */\nexport const POLL_INTERVAL_ENROLLING_MS = 5000;\n\nexport interface PairCreated {\n request_id: string;\n /** https://zoreal.com/qr/<request_id>. The same URL in QR and app link. */\n pair_url: string;\n expires_in: number;\n}\n\nexport interface PairImmediate {\n /** prompt=none resolved silently: consented sector, live session. */\n code: string;\n}\n\nexport type PairStartResponse = PairCreated | PairImmediate;\n\nexport interface PairStatusResponse {\n status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'cancelled' | 'enrolling';\n code?: string;\n expires_in?: number;\n enrolment_deadline?: number;\n /** The provider's reason on denial or refusal. Surfaced verbatim, never rewritten. */\n error?: string;\n error_description?: string;\n}\n\nexport interface TokenResponse {\n id_token: string;\n access_token?: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n","/**\n * The pairing channel, client side. wire.ts pins the endpoints.\n *\n * The browser polls; the phone never talks to the browser. Everything here is\n * therefore plain fetch against the issuer, CORS-gated on the client's\n * authorized origins, with the poll cadence fixed: the provider cancels an\n * over-polling request rather than throttling it, so a \"retry\n * faster on error\" strategy here would kill the login it is trying to save.\n */\n\nimport {\n POLL_INTERVAL_ENROLLING_MS,\n POLL_INTERVAL_MS,\n SDK_NAME,\n SDK_VERSION,\n WIRE_VERSION,\n type PairStartResponse,\n type PairStatusResponse,\n type TokenResponse,\n} from './wire';\nimport type { ErrorCode, NonOAuthError, PairingState } from './types';\n\nexport class OAuthFlowError extends Error {\n constructor(\n public error: ErrorCode,\n public description?: string\n ) {\n super(description ?? error);\n }\n}\n\nexport class FlowAbandonedError extends Error {\n constructor(public reason: NonOAuthError) {\n super(reason.description ?? reason.type);\n }\n}\n\nexport interface StartPairingParams {\n client_id: string;\n scope: string;\n state: string;\n nonce: string;\n code_challenge: string;\n redirect_uri?: string;\n acr_values?: string;\n max_age?: number;\n prompt?: string;\n locale?: string;\n}\n\nasync function parseJson(response: Response): Promise<Record<string, unknown>> {\n try {\n return (await response.json()) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\nexport async function startPairing(\n issuer: string,\n params: StartPairingParams\n): Promise<PairStartResponse> {\n const response = await fetch(`${issuer}/pair`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n ...params,\n code_challenge_method: 'S256',\n wire_version: WIRE_VERSION,\n sdk: `${SDK_NAME}/${SDK_VERSION}`,\n }),\n });\n\n const body = await parseJson(response);\n if (!response.ok) {\n // The provider's words, verbatim. A refused package version arrives here,\n // and rewriting its reason would hide the only signal telling an integrator\n // to upgrade.\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n (body.error_description as string) ?? `The provider refused the request (${response.status})`\n );\n }\n return body as unknown as PairStartResponse;\n}\n\nconst sleep = (ms: number, signal?: AbortSignal) =>\n new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('aborted', 'AbortError'));\n return;\n }\n const t = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => {\n clearTimeout(t);\n reject(new DOMException('aborted', 'AbortError'));\n });\n });\n\n/**\n * Polls until the request resolves. Returns the authorization code.\n * Throws FlowAbandonedError for the human outcomes (denied, expired,\n * enrolment abandoned) and OAuthFlowError for protocol ones.\n */\nexport async function pollUntilApproved(\n issuer: string,\n requestId: string,\n onState?: (state: PairingState) => void,\n signal?: AbortSignal\n): Promise<string> {\n for (;;) {\n const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {\n signal,\n });\n const body = (await parseJson(response)) as unknown as PairStatusResponse;\n\n if (!response.ok) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Pairing status failed (${response.status})`\n );\n }\n\n onState?.({\n status: body.status,\n expiresIn: body.expires_in,\n enrolmentDeadline: body.enrolment_deadline,\n });\n\n switch (body.status) {\n case 'approved':\n if (!body.code) {\n throw new OAuthFlowError('server_error', 'approved with no authorization code');\n }\n return body.code;\n case 'denied':\n throw new FlowAbandonedError({ type: 'request_denied', description: body.error_description });\n case 'expired':\n throw new FlowAbandonedError({ type: 'request_expired', description: body.error_description });\n case 'cancelled':\n // The provider cancels an over-polled or abandoned request outright\n // (its pairing rows have a real cancelled state); a poll that treats\n // it as unknown spins on a dead request forever.\n throw new FlowAbandonedError({\n type: 'request_expired',\n description: body.error_description ?? 'the provider cancelled the pairing request',\n });\n case 'enrolling':\n await sleep(POLL_INTERVAL_ENROLLING_MS, signal);\n break;\n default:\n await sleep(POLL_INTERVAL_MS, signal);\n }\n }\n}\n\n/**\n * The code exchange, browser-direct mode only: a public client, PKCE and no\n * secret. What comes back can only ever be the pseudonymous tier, by\n * construction rather than by rule: personal data lives at /userinfo behind an\n * access token this mode is never issued, because personal-data scopes are\n * refused for public clients at the pairing step.\n */\nexport async function exchangeCode(\n issuer: string,\n input: { code: string; code_verifier: string; client_id: string }\n): Promise<TokenResponse> {\n const response = await fetch(`${issuer}/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: input.code,\n code_verifier: input.code_verifier,\n client_id: input.client_id,\n }),\n });\n\n const body = (await parseJson(response)) as unknown as TokenResponse;\n if (!response.ok || body.error) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Token exchange failed (${response.status})`\n );\n }\n return body;\n}\n\n/** A mobile user agent gets the app link, not a QR of its own screen. */\nexport function isMobileUserAgent(): boolean {\n if (typeof navigator === 'undefined') return false;\n return /android|iphone|ipad|ipod/i.test(navigator.userAgent);\n}\n","/**\n * PKCE, S256 only: mandatory for every client, confidential ones included.\n * There is no plain fallback and there must never be one; a provider seeing\n * method=plain is seeing a bug or an attack.\n */\n\nconst VERIFIER_BYTES = 32; // 43 base64url chars, the RFC 7636 minimum length\n\nconst base64url = (bytes: Uint8Array): string =>\n btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\nexport function generateVerifier(): string {\n const bytes = new Uint8Array(VERIFIER_BYTES);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n\nexport async function challengeS256(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));\n return base64url(new Uint8Array(digest));\n}\n\nexport function generateState(): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n","/**\n * The one flow, as an imperative handle. This is the same state machine the\n * React SDK's hook runs, without the React: start a pairing, surface it for\n * rendering through onState, poll, and finish per mode. Browser-direct\n * exchanges the code here (public client, PKCE, no secret) and hands over an\n * ID token; auth-code hands the code and the PKCE verifier to the caller,\n * whose backend does the exchange with its client authentication.\n *\n * A framework wrapper owns exactly two things: calling startLogin on the\n * user's gesture, and rendering what onState carries. Everything else -\n * PKCE, state, nonce, cadence, cancellation - lives here.\n */\n\nimport { unsafeClaims } from './jwt';\nimport {\n FlowAbandonedError,\n OAuthFlowError,\n exchangeCode,\n isMobileUserAgent,\n pollUntilApproved,\n startPairing,\n} from './pairing';\nimport { challengeS256, generateState, generateVerifier } from './pkce';\nimport { DEFAULT_ISSUER } from './wire';\nimport type {\n AcrValue,\n AuthCodeLoginOptions,\n BrowserDirectLoginOptions,\n LoginHandle,\n SelectBy,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n} from './types';\n\nexport function startLogin(\n options: BrowserDirectLoginOptions\n): LoginHandle<ZorealCredentialResponse>;\nexport function startLogin(options: AuthCodeLoginOptions): LoginHandle<ZorealCodeResponse>;\nexport function startLogin(\n options: BrowserDirectLoginOptions | AuthCodeLoginOptions\n): LoginHandle<ZorealCredentialResponse> | LoginHandle<ZorealCodeResponse> {\n if ('ux_mode' in options && options.ux_mode === 'redirect') {\n // The popup shape only: the code and PKCE verifier resolve the promise\n // and go from there to your backend over TLS. A redirect would have to\n // carry the verifier in a URL, which is a credential in every access\n // log on the path. Refused loudly rather than implemented badly.\n throw new Error(\n \"@zoreal/oauth2-js: ux_mode 'redirect' is not supported. Use the default \" +\n \"'popup' shape and post the code and code_verifier from the resolved \" +\n 'promise to your backend.'\n );\n }\n\n const flow = options.flow ?? 'browser-direct';\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const controller = new AbortController();\n\n const surface: {\n requestId?: string;\n pairUrl?: string;\n qrUrl?: string;\n appLink?: boolean;\n } = {};\n\n const cancel = () => controller.abort();\n\n const run = async (): Promise<ZorealCredentialResponse | ZorealCodeResponse> => {\n const verifier = generateVerifier();\n const state = generateState();\n const nonce = generateState();\n\n try {\n const started = await startPairing(issuer, {\n client_id: options.clientId,\n scope: options.scope ?? 'openid',\n state,\n nonce,\n code_challenge: await challengeS256(verifier),\n redirect_uri: flow === 'auth-code' ? (options as AuthCodeLoginOptions).redirect_uri : undefined,\n acr_values: Array.isArray(options.acr_values)\n ? options.acr_values.join(' ')\n : options.acr_values,\n max_age: options.max_age,\n prompt: options.prompt,\n locale: options.locale,\n });\n\n let code: string;\n let selectBy: SelectBy = 'device';\n\n if ('code' in started) {\n // prompt=none resolved silently: consented sector, live session.\n code = started.code;\n selectBy = 'session';\n } else {\n const useAppLink =\n options.display === 'link' || (options.display !== 'qr' && isMobileUserAgent());\n selectBy = useAppLink ? 'app_link' : 'qr';\n\n surface.requestId = started.request_id;\n surface.pairUrl = started.pair_url;\n surface.qrUrl = `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`;\n surface.appLink = useAppLink;\n\n // Everything a caller-rendered pairing UI needs, on every state it\n // sees: the QR flow cannot complete unless SOMETHING renders pairUrl,\n // and in this package that something is always the caller.\n const stateSurface = {\n pairUrl: surface.pairUrl,\n qrUrl: surface.qrUrl,\n appLink: useAppLink,\n cancel,\n };\n\n // The initial state, immediately: the first poll response is one\n // round-trip away, and a UI that waits for it opens visibly empty.\n options.onState?.({ status: 'pending', expiresIn: started.expires_in, ...stateSurface });\n\n if (useAppLink && typeof window !== 'undefined') {\n // The universal link, in the same tab: the app claims it, and with\n // no app installed the same URL is the real pairing page, which can\n // enrol. A popup here would be blocked more often than it would\n // help.\n window.location.assign(started.pair_url);\n }\n\n code = await pollUntilApproved(\n issuer,\n started.request_id,\n (s) => options.onState?.({ ...s, ...stateSurface }),\n controller.signal\n );\n }\n\n if (flow === 'auth-code') {\n const response: ZorealCodeResponse = {\n code,\n scope: options.scope ?? 'openid',\n app_state: options.app_state,\n code_verifier: verifier,\n nonce,\n };\n return response;\n }\n\n const tokens = await exchangeCode(issuer, {\n code,\n code_verifier: verifier,\n client_id: options.clientId,\n });\n const claims = unsafeClaims(tokens.id_token);\n const response: ZorealCredentialResponse = {\n credential: tokens.id_token,\n clientId: options.clientId,\n select_by: selectBy,\n acr: (claims.acr as AcrValue) ?? 'zoreal.device',\n };\n return response;\n } catch (e) {\n // The taxonomy the promise rejects with, and nothing else:\n // OAuthFlowError the provider refused; reason verbatim\n // FlowAbandonedError a human outcome, or a failure that never\n // reached the provider (network, unknown)\n // AbortError the caller's own cancel()\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n if (e instanceof OAuthFlowError || e instanceof FlowAbandonedError) throw e;\n throw new FlowAbandonedError({\n type: 'unknown',\n description: e instanceof Error ? e.message : String(e),\n });\n }\n };\n\n const promise = run();\n // A caller driving everything from onState and cancel() may never attach a\n // rejection handler; this no-op one keeps a cancelled login from surfacing\n // as an unhandled rejection. The caller's own catch still sees the error.\n promise.catch(() => {});\n\n return {\n promise: promise as Promise<ZorealCredentialResponse> & Promise<ZorealCodeResponse>,\n cancel,\n get requestId() {\n return surface.requestId;\n },\n get pairUrl() {\n return surface.pairUrl;\n },\n get qrUrl() {\n return surface.qrUrl;\n },\n get appLink() {\n return surface.appLink;\n },\n };\n}\n"],"mappings":"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","names":["response"]}
|
|
1
|
+
{"version":3,"sources":["../src/jwt.ts","../src/wire.ts","../src/pairing.ts","../src/pkce.ts","../src/login.ts"],"sourcesContent":["/**\n * Reads claims OUT of an ID token without verifying it.\n *\n * That is not a shortcut, it is the design: this code runs in a browser the\n * threat model assumes is attacker-controlled, so a signature check here\n * proves nothing to anyone. The token is verified where verification means\n * something: server-side against the JWKS. What this parser feeds is\n * convenience fields (acr on the response object) that the types document as\n * convenience, with the token staying the authority.\n */\n\nexport function unsafeClaims(idToken: string): Record<string, unknown> {\n try {\n const payload = idToken.split('.')[1] ?? '';\n const b64 = payload.replace(/-/g, '+').replace(/_/g, '/');\n const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n return JSON.parse(\n new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)))\n );\n } catch {\n return {};\n }\n}\n","/**\n * The wire protocol between this package and the ZOREAL OpenID Provider.\n *\n * VERSIONED: a shipped version keeps working until the provider explicitly\n * refuses it, and when it does, the reason is surfaced verbatim. Both the wire\n * version and the package version travel on every pairing request so a refusal\n * can be precise.\n *\n * Endpoints, all relative to the issuer and all CORS-gated on the client's\n * authorized JavaScript origins (the dashboard):\n *\n * POST /pair start a pairing request. Body carries the\n * authorize parameters plus PKCE challenge.\n * Returns { request_id, pair_url, expires_in }\n * or, for prompt=none with a live consented\n * session, { code } immediately.\n * GET /pair/:id/status poll: pending | claimed |\n * approved (with code) | denied | expired |\n * enrolling. Over-polling cancels the request\n * rather than throttling it, so the cadence\n * below is not a suggestion.\n * GET /pair/:id/qr.svg the QR image for the pairing URL, served by\n * the provider so the pairing surface stays\n * changeable at runtime and\n * this package keeps zero dependencies.\n * POST /token the code exchange. Browser-direct mode uses\n * it directly with PKCE and no client secret;\n * auth-code mode leaves it to the RP backend.\n */\n\nexport const WIRE_VERSION = 1;\nexport const SDK_VERSION = '0.1.6';\nexport const SDK_NAME = '@zoreal/oauth2-js';\nexport const DEFAULT_ISSUER = 'https://id.zoreal.com';\n\n/** Pending TTL is short. Poll gently; over-polling cancels the request. */\nexport const POLL_INTERVAL_MS = 2000;\n/** Enrolling extends the window well beyond a normal login; poll slower. */\nexport const POLL_INTERVAL_ENROLLING_MS = 5000;\n\nexport interface PairCreated {\n request_id: string;\n /** https://zoreal.com/qr/<request_id>. The same URL in QR and app link. */\n pair_url: string;\n expires_in: number;\n}\n\nexport interface PairImmediate {\n /** prompt=none resolved silently: consented sector, live session. */\n code: string;\n}\n\nexport type PairStartResponse = PairCreated | PairImmediate;\n\nexport interface PairStatusResponse {\n status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'cancelled' | 'enrolling';\n code?: string;\n expires_in?: number;\n enrolment_deadline?: number;\n /** The provider's reason on denial or refusal. Surfaced verbatim, never rewritten. */\n error?: string;\n error_description?: string;\n}\n\nexport interface TokenResponse {\n id_token: string;\n access_token?: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n","/**\n * The pairing channel, client side. wire.ts pins the endpoints.\n *\n * The browser polls; the phone never talks to the browser. Everything here is\n * therefore plain fetch against the issuer, CORS-gated on the client's\n * authorized origins, with the poll cadence fixed: the provider cancels an\n * over-polling request rather than throttling it, so a \"retry\n * faster on error\" strategy here would kill the login it is trying to save.\n */\n\nimport {\n POLL_INTERVAL_ENROLLING_MS,\n POLL_INTERVAL_MS,\n SDK_NAME,\n SDK_VERSION,\n WIRE_VERSION,\n type PairStartResponse,\n type PairStatusResponse,\n type TokenResponse,\n} from './wire';\nimport type { ErrorCode, NonOAuthError, PairingState } from './types';\n\nexport class OAuthFlowError extends Error {\n constructor(\n public error: ErrorCode,\n public description?: string\n ) {\n super(description ?? error);\n }\n}\n\nexport class FlowAbandonedError extends Error {\n constructor(public reason: NonOAuthError) {\n super(reason.description ?? reason.type);\n }\n}\n\nexport interface StartPairingParams {\n client_id: string;\n scope: string;\n state: string;\n nonce: string;\n code_challenge: string;\n redirect_uri?: string;\n acr_values?: string;\n max_age?: number;\n prompt?: string;\n locale?: string;\n}\n\nasync function parseJson(response: Response): Promise<Record<string, unknown>> {\n try {\n return (await response.json()) as Record<string, unknown>;\n } catch {\n return {};\n }\n}\n\nexport async function startPairing(\n issuer: string,\n params: StartPairingParams\n): Promise<PairStartResponse> {\n const response = await fetch(`${issuer}/pair`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n ...params,\n code_challenge_method: 'S256',\n wire_version: WIRE_VERSION,\n sdk: `${SDK_NAME}/${SDK_VERSION}`,\n }),\n });\n\n const body = await parseJson(response);\n if (!response.ok) {\n // The provider's words, verbatim. A refused package version arrives here,\n // and rewriting its reason would hide the only signal telling an integrator\n // to upgrade.\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n (body.error_description as string) ?? `The provider refused the request (${response.status})`\n );\n }\n return body as unknown as PairStartResponse;\n}\n\nconst sleep = (ms: number, signal?: AbortSignal) =>\n new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('aborted', 'AbortError'));\n return;\n }\n const t = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => {\n clearTimeout(t);\n reject(new DOMException('aborted', 'AbortError'));\n });\n });\n\n/**\n * Polls until the request resolves. Returns the authorization code.\n * Throws FlowAbandonedError for the human outcomes (denied, expired,\n * enrolment abandoned) and OAuthFlowError for protocol ones.\n */\nexport async function pollUntilApproved(\n issuer: string,\n requestId: string,\n onState?: (state: PairingState) => void,\n signal?: AbortSignal\n): Promise<string> {\n for (;;) {\n const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {\n signal,\n });\n const body = (await parseJson(response)) as unknown as PairStatusResponse;\n\n if (!response.ok) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Pairing status failed (${response.status})`\n );\n }\n\n onState?.({\n status: body.status,\n expiresIn: body.expires_in,\n enrolmentDeadline: body.enrolment_deadline,\n });\n\n switch (body.status) {\n case 'approved':\n if (!body.code) {\n throw new OAuthFlowError('server_error', 'approved with no authorization code');\n }\n return body.code;\n case 'denied':\n throw new FlowAbandonedError({ type: 'request_denied', description: body.error_description });\n case 'expired':\n throw new FlowAbandonedError({ type: 'request_expired', description: body.error_description });\n case 'cancelled':\n // The provider cancels an over-polled or abandoned request outright\n // (its pairing rows have a real cancelled state); a poll that treats\n // it as unknown spins on a dead request forever.\n throw new FlowAbandonedError({\n type: 'request_expired',\n description: body.error_description ?? 'the provider cancelled the pairing request',\n });\n case 'enrolling':\n await sleep(POLL_INTERVAL_ENROLLING_MS, signal);\n break;\n default:\n await sleep(POLL_INTERVAL_MS, signal);\n }\n }\n}\n\n/**\n * The code exchange, browser-direct mode only: a public client, PKCE and no\n * secret. What comes back can only ever be the pseudonymous tier, by\n * construction rather than by rule: personal data lives at /userinfo behind an\n * access token this mode is never issued, because personal-data scopes are\n * refused for public clients at the pairing step.\n */\nexport async function exchangeCode(\n issuer: string,\n input: { code: string; code_verifier: string; client_id: string }\n): Promise<TokenResponse> {\n const response = await fetch(`${issuer}/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: input.code,\n code_verifier: input.code_verifier,\n client_id: input.client_id,\n }),\n });\n\n const body = (await parseJson(response)) as unknown as TokenResponse;\n if (!response.ok || body.error) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Token exchange failed (${response.status})`\n );\n }\n return body;\n}\n\n/** A mobile user agent gets the app link, not a QR of its own screen. */\nexport function isMobileUserAgent(): boolean {\n if (typeof navigator === 'undefined') return false;\n return /android|iphone|ipad|ipod/i.test(navigator.userAgent);\n}\n","/**\n * PKCE, S256 only: mandatory for every client, confidential ones included.\n * There is no plain fallback and there must never be one; a provider seeing\n * method=plain is seeing a bug or an attack.\n */\n\nconst VERIFIER_BYTES = 32; // 43 base64url chars, the RFC 7636 minimum length\n\nconst base64url = (bytes: Uint8Array): string =>\n btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\nexport function generateVerifier(): string {\n const bytes = new Uint8Array(VERIFIER_BYTES);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n\nexport async function challengeS256(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));\n return base64url(new Uint8Array(digest));\n}\n\nexport function generateState(): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n","/**\n * The one flow, as an imperative handle. This is the same state machine the\n * React SDK's hook runs, without the React: start a pairing, surface it for\n * rendering through onState, poll, and finish per mode. Browser-direct\n * exchanges the code here (public client, PKCE, no secret) and hands over an\n * ID token; auth-code hands the code and the PKCE verifier to the caller,\n * whose backend does the exchange with its client authentication.\n *\n * A framework wrapper owns exactly two things: calling startLogin on the\n * user's gesture, and rendering what onState carries. Everything else -\n * PKCE, state, nonce, cadence, cancellation - lives here.\n */\n\nimport { unsafeClaims } from './jwt';\nimport {\n FlowAbandonedError,\n OAuthFlowError,\n exchangeCode,\n isMobileUserAgent,\n pollUntilApproved,\n startPairing,\n} from './pairing';\nimport { challengeS256, generateState, generateVerifier } from './pkce';\nimport { DEFAULT_ISSUER } from './wire';\nimport type {\n AcrValue,\n AuthCodeLoginOptions,\n BrowserDirectLoginOptions,\n LoginHandle,\n SelectBy,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n} from './types';\n\nexport function startLogin(\n options: BrowserDirectLoginOptions\n): LoginHandle<ZorealCredentialResponse>;\nexport function startLogin(options: AuthCodeLoginOptions): LoginHandle<ZorealCodeResponse>;\nexport function startLogin(\n options: BrowserDirectLoginOptions | AuthCodeLoginOptions\n): LoginHandle<ZorealCredentialResponse> | LoginHandle<ZorealCodeResponse> {\n if ('ux_mode' in options && options.ux_mode === 'redirect') {\n // The popup shape only: the code and PKCE verifier resolve the promise\n // and go from there to your backend over TLS. A redirect would have to\n // carry the verifier in a URL, which is a credential in every access\n // log on the path. Refused loudly rather than implemented badly.\n throw new Error(\n \"@zoreal/oauth2-js: ux_mode 'redirect' is not supported. Use the default \" +\n \"'popup' shape and post the code and code_verifier from the resolved \" +\n 'promise to your backend.'\n );\n }\n\n const flow = options.flow ?? 'browser-direct';\n const issuer = options.issuer ?? DEFAULT_ISSUER;\n const controller = new AbortController();\n\n const surface: {\n requestId?: string;\n pairUrl?: string;\n qrUrl?: string;\n appLink?: boolean;\n } = {};\n\n const cancel = () => controller.abort();\n\n const run = async (): Promise<ZorealCredentialResponse | ZorealCodeResponse> => {\n const verifier = generateVerifier();\n const state = generateState();\n const nonce = generateState();\n\n try {\n const started = await startPairing(issuer, {\n client_id: options.clientId,\n scope: options.scope ?? 'openid',\n state,\n nonce,\n code_challenge: await challengeS256(verifier),\n redirect_uri: flow === 'auth-code' ? (options as AuthCodeLoginOptions).redirect_uri : undefined,\n acr_values: Array.isArray(options.acr_values)\n ? options.acr_values.join(' ')\n : options.acr_values,\n max_age: options.max_age,\n prompt: options.prompt,\n locale: options.locale,\n });\n\n let code: string;\n let selectBy: SelectBy = 'device';\n\n if ('code' in started) {\n // prompt=none resolved silently: consented sector, live session.\n code = started.code;\n selectBy = 'session';\n } else {\n const useAppLink =\n options.display === 'link' || (options.display !== 'qr' && isMobileUserAgent());\n selectBy = useAppLink ? 'app_link' : 'qr';\n\n surface.requestId = started.request_id;\n surface.pairUrl = started.pair_url;\n surface.qrUrl = `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`;\n surface.appLink = useAppLink;\n\n // Everything a caller-rendered pairing UI needs, on every state it\n // sees: the QR flow cannot complete unless SOMETHING renders pairUrl,\n // and in this package that something is always the caller.\n const stateSurface = {\n pairUrl: surface.pairUrl,\n qrUrl: surface.qrUrl,\n appLink: useAppLink,\n cancel,\n };\n\n // The initial state, immediately: the first poll response is one\n // round-trip away, and a UI that waits for it opens visibly empty.\n options.onState?.({ status: 'pending', expiresIn: started.expires_in, ...stateSurface });\n\n if (useAppLink && typeof window !== 'undefined') {\n // The universal link, in the same tab: the app claims it, and with\n // no app installed the same URL is the real pairing page, which can\n // enrol. A popup here would be blocked more often than it would\n // help.\n window.location.assign(started.pair_url);\n }\n\n code = await pollUntilApproved(\n issuer,\n started.request_id,\n (s) => options.onState?.({ ...s, ...stateSurface }),\n controller.signal\n );\n }\n\n if (flow === 'auth-code') {\n const response: ZorealCodeResponse = {\n code,\n scope: options.scope ?? 'openid',\n app_state: options.app_state,\n code_verifier: verifier,\n nonce,\n };\n return response;\n }\n\n const tokens = await exchangeCode(issuer, {\n code,\n code_verifier: verifier,\n client_id: options.clientId,\n });\n const claims = unsafeClaims(tokens.id_token);\n const response: ZorealCredentialResponse = {\n credential: tokens.id_token,\n clientId: options.clientId,\n select_by: selectBy,\n acr: (claims.acr as AcrValue) ?? 'zoreal.device',\n };\n return response;\n } catch (e) {\n // The taxonomy the promise rejects with, and nothing else:\n // OAuthFlowError the provider refused; reason verbatim\n // FlowAbandonedError a human outcome, or a failure that never\n // reached the provider (network, unknown)\n // AbortError the caller's own cancel()\n if (e instanceof DOMException && e.name === 'AbortError') throw e;\n if (e instanceof OAuthFlowError || e instanceof FlowAbandonedError) throw e;\n throw new FlowAbandonedError({\n type: 'unknown',\n description: e instanceof Error ? e.message : String(e),\n });\n }\n };\n\n const promise = run();\n // A caller driving everything from onState and cancel() may never attach a\n // rejection handler; this no-op one keeps a cancelled login from surfacing\n // as an unhandled rejection. The caller's own catch still sees the error.\n promise.catch(() => {});\n\n return {\n promise: promise as Promise<ZorealCredentialResponse> & Promise<ZorealCodeResponse>,\n cancel,\n get requestId() {\n return surface.requestId;\n },\n get pairUrl() {\n return surface.pairUrl;\n },\n get qrUrl() {\n return surface.qrUrl;\n },\n get appLink() {\n return surface.appLink;\n },\n };\n}\n"],"mappings":"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","names":["response"]}
|
package/package.json
CHANGED