@zoreal/oauth2-react 0.1.0 → 0.1.2

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 CHANGED
@@ -1,23 +1,59 @@
1
1
  # @zoreal/oauth2-react
2
2
 
3
- Login with ZOREAL for React: a chip-verified human behind every sign-in.
3
+ Login with ZOREAL for React: a ZOREAL Verified Proof-of-Human behind every sign-in.
4
4
 
5
5
  The API mirrors `@react-oauth/google` one to one, renamed, so a team already
6
- integrated with Google ports by find and replace. The full mapping and every
7
- design decision live in the specification this package is built against:
8
- `zoreal/products/oauth2/05-react-sdk.md` in the specification repo.
6
+ integrated with Google ports by find and replace.
9
7
 
10
8
  ## Status
11
9
 
12
- The package is real; the provider it speaks to is being built. This code pins
13
- wire protocol v1 and is not yet published to npm. Nothing below works against
14
- production today, and this section is removed the day that changes.
10
+ Early release. The package implements wire protocol v1. The hosted ZOREAL
11
+ login service is still rolling out, so treat this as a preview: the API is
12
+ stable, but end-to-end sign-in against production is not available everywhere
13
+ yet. This note is removed once the service is generally available.
15
14
 
16
15
  ## Install
17
16
 
18
- Not yet published. When it is: `npm install @zoreal/oauth2-react`.
17
+ ```sh
18
+ npm install @zoreal/oauth2-react
19
+ ```
20
+
21
+ `react` and `react-dom` (18 or 19) are peer dependencies.
22
+
23
+ ## Two flows: pick by whether you need the user's details
24
+
25
+ - **You have a backend and want the user's email or name** (most apps): use the
26
+ **auth-code flow**. Your backend gets the email, name, and verification
27
+ details from `/userinfo`. Start here.
28
+ - **You have no backend and only need to know "this is a verified, unique human,
29
+ and the same one as last time"**: use the **`<ZorealLogin>` button**. It
30
+ returns a stable per-user identifier and proof of verification, but no email
31
+ or name. Email and other personal details are never placed in a browser-side
32
+ token; that is what the auth-code flow and your backend are for.
33
+
34
+ ## Quick start: auth-code (email and name, needs your backend)
35
+
36
+ ```tsx
37
+ import { useZorealLogin } from '@zoreal/oauth2-react';
38
+
39
+ // `email` (and profile.name, etc.) are returned from /userinfo on your backend.
40
+ const login = useZorealLogin({
41
+ flow: 'auth-code',
42
+ scope: 'openid email profile.name',
43
+ onSuccess: async ({ code, code_verifier }) => {
44
+ // Send BOTH to your backend over TLS. Your backend calls POST /token with
45
+ // them plus its client authentication, then reads the email and name from
46
+ // /userinfo. That is where personal data is delivered.
47
+ const session = await fetch('/api/auth/zoreal', {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({ code, code_verifier }),
51
+ });
52
+ },
53
+ });
54
+ ```
19
55
 
20
- ## Quick start: the button (no backend needed)
56
+ ## Quick start: the button (no backend, pseudonymous)
21
57
 
22
58
  ```tsx
23
59
  import { ZorealOAuthProvider, ZorealLogin } from '@zoreal/oauth2-react';
@@ -25,9 +61,10 @@ import { ZorealOAuthProvider, ZorealLogin } from '@zoreal/oauth2-react';
25
61
  <ZorealOAuthProvider clientId="ast_your_asset_id">
26
62
  <ZorealLogin
27
63
  onSuccess={({ credential }) => {
28
- // credential is an ID token: a pairwise pseudonymous `sub` plus
29
- // assurance claims. Verify it server-side against the JWKS before
30
- // trusting it. It NEVER contains personal data, by construction.
64
+ // `credential` is an ID token carrying a stable per-user identifier (`sub`)
65
+ // and proof the person is a verified, unique human. No email, no name:
66
+ // use the auth-code flow above for those. Verify it on your server against
67
+ // the JWKS before trusting it.
31
68
  }}
32
69
  onError={(e) => console.warn(e.type, e.description)}
33
70
  />
@@ -38,18 +75,65 @@ On desktop the button shows a QR; the user scans it with their phone and
38
75
  approves in the ZOREAL ID app. On a phone it opens the app directly. Either
39
76
  way your page just receives `onSuccess`.
40
77
 
41
- ## Quick start: auth-code (personal data, requires your backend)
78
+ ### On your backend
42
79
 
43
- ```tsx
44
- import { useZorealLogin } from '@zoreal/oauth2-react';
80
+ Exchange the `code` and `code_verifier` your `onSuccess` posted, then read the
81
+ user's details:
82
+
83
+ ```
84
+ POST https://id.zoreal.com/token
85
+ grant_type=authorization_code
86
+ code=<code>
87
+ code_verifier=<code_verifier>
88
+ client_id=ast_your_asset_id
89
+ <your client authentication> # client secret, or a private_key_jwt assertion
90
+
91
+ -> { "id_token": "...", "access_token": "...", "expires_in": 600 }
92
+
93
+ GET https://id.zoreal.com/userinfo
94
+ Authorization: Bearer <access_token>
95
+
96
+ -> { "sub": "...", "email": "...", "email_verified": true, "given_name": "...", ... }
97
+ ```
98
+
99
+ Which fields come back depends on the scopes the user consented to. The
100
+ `id_token` is a JWT you verify against the JWKS at
101
+ `https://id.zoreal.com/jwks`.
102
+
103
+ `ux_mode: 'redirect'` is not supported in v1: it would put the PKCE verifier
104
+ in a URL, which is a credential in every access log on the path.
105
+
106
+ ## Scopes
107
+
108
+ Request scopes in the `scope` string, space-separated, always starting with
109
+ `openid`. What each returns and where:
110
+
111
+ | Scope | Returns | Delivered in | Needs |
112
+ |---|---|---|---|
113
+ | `openid` | `sub` (stable per-user id) and a verification summary: uniqueness, month verified, whether a chip was read live | ID token | nothing |
114
+ | `zoreal.age` | `age_over_N` booleans for the thresholds you registered, e.g. `age_over_18`. Never an age or birthdate | ID token | nothing |
115
+ | `zoreal.nationality` | `nationality` (ISO 3166-1 alpha-3) | ID token | nothing |
116
+ | `email` | `email`, `email_verified` | `/userinfo` | verified domain, confidential client |
117
+ | `profile.name` | `name`, `given_name`, `family_name` | `/userinfo` | verified domain, confidential client |
118
+ | `profile.birthdate` | `birthdate` (full date) | `/userinfo` | verified domain, confidential client |
119
+ | `profile.document` | `document_type`, `document_number`, `issuing_country`, `document_expires_on` | `/userinfo` | verified domain, confidential client |
120
+ | `profile.portrait` | `portrait`, the photo from the document chip. Biometric data: requesting it makes you responsible for it under GDPR Article 9 and similar laws | `/userinfo` | verified domain, confidential client |
121
+
122
+ The first three are available to any client and ride in the ID token, so the
123
+ no-backend button can use them. Everything else is personal data: it is served
124
+ only from `/userinfo` to a confidential client whose domain you have verified in
125
+ the dashboard, and never placed in a browser-side token.
45
126
 
127
+ ### Identity verification (KYC)
128
+
129
+ To receive a person's government-document-verified identity, request the
130
+ document scopes together and read them from `/userinfo`:
131
+
132
+ ```tsx
46
133
  const login = useZorealLogin({
47
134
  flow: 'auth-code',
48
- scope: 'openid profile.name',
135
+ scope: 'openid profile.name profile.birthdate profile.document profile.portrait',
49
136
  onSuccess: async ({ code, code_verifier }) => {
50
- // Send BOTH to your backend over TLS. Your backend calls POST /token
51
- // with them plus its client authentication (private_key_jwt, mTLS, or
52
- // client secret), then reads personal claims from /userinfo.
53
137
  await fetch('/api/auth/zoreal', {
54
138
  method: 'POST',
55
139
  headers: { 'Content-Type': 'application/json' },
@@ -59,8 +143,12 @@ const login = useZorealLogin({
59
143
  });
60
144
  ```
61
145
 
62
- `ux_mode: 'redirect'` is not supported in v1: it would put the PKCE verifier
63
- in a URL, which is a credential in every access log on the path.
146
+ Your backend then holds the name, date of birth, document type and number,
147
+ issuing country and expiry, all read from a chip and verified at enrolment,
148
+ plus the portrait if you requested it. That is the raw material for a KYC
149
+ check. You remain the controller of that data and responsible for how you
150
+ store, use, and justify collecting it; ZOREAL provides the verified attributes,
151
+ not a regulatory determination.
64
152
 
65
153
  ## What your page needs to allow
66
154
 
package/dist/index.cjs CHANGED
@@ -37,7 +37,7 @@ var import_react = require("react");
37
37
 
38
38
  // src/wire.ts
39
39
  var WIRE_VERSION = 1;
40
- var SDK_VERSION = "0.1.0";
40
+ var SDK_VERSION = "0.1.2";
41
41
  var DEFAULT_ISSUER = "https://id.zoreal.com";
42
42
  var POLL_INTERVAL_MS = 2e3;
43
43
  var POLL_INTERVAL_ENROLLING_MS = 5e3;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/context.tsx","../src/wire.ts","../src/ZorealLogin.tsx","../src/useZorealLogin.ts","../src/jwt.ts","../src/pairing.ts","../src/pkce.ts","../src/useZorealAutoLogin.ts","../src/logout.ts","../src/scopes.ts"],"sourcesContent":["export { ZorealOAuthProvider, useZorealOAuth } from './context';\nexport type { ZorealOAuthProviderProps, ZorealOAuthContextProps } from './context';\nexport { ZorealLogin } from './ZorealLogin';\nexport { useZorealLogin } from './useZorealLogin';\nexport { useZorealAutoLogin } from './useZorealAutoLogin';\nexport { zorealLogout } from './logout';\nexport { hasGrantedAllScopesZoreal, hasGrantedAnyScopeZoreal } from './scopes';\nexport type {\n AcrValue,\n AuthCodeFlowOptions,\n BrowserDirectFlowOptions,\n ErrorCode,\n NonOAuthError,\n PairingState,\n SelectBy,\n UseZorealAutoLoginOptions,\n ZorealButtonConfiguration,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n ZorealLoginProps,\n ZorealLoginRequestOptions,\n} from './types';\n","import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { DEFAULT_ISSUER } from './wire';\n\nexport interface ZorealOAuthProviderProps {\n /** From the zoreal-web dashboard: the asset ID. */\n clientId: string;\n /** Override the provider origin. Sandbox and self-hosted testing only. */\n issuer?: string;\n /** BCP 47. Drives button text and the pairing page. */\n locale?: string;\n children: ReactNode;\n}\n\nexport interface ZorealOAuthContextProps {\n clientId: string;\n issuer: string;\n locale?: string;\n}\n\nconst ZorealOAuthContext = createContext<ZorealOAuthContextProps | null>(null);\n\nexport function ZorealOAuthProvider({\n clientId,\n issuer = DEFAULT_ISSUER,\n locale,\n children,\n}: ZorealOAuthProviderProps) {\n const value = useMemo(\n () => ({ clientId, issuer: issuer.replace(/\\/$/, ''), locale }),\n [clientId, issuer, locale]\n );\n return <ZorealOAuthContext.Provider value={value}>{children}</ZorealOAuthContext.Provider>;\n}\n\nexport function useZorealOAuth(): ZorealOAuthContextProps {\n const ctx = useContext(ZorealOAuthContext);\n if (!ctx) {\n throw new Error(\n 'useZorealOAuth must be used inside <ZorealOAuthProvider clientId=...>. ' +\n 'Wrap your app (or the part that logs in) in the provider.'\n );\n }\n return ctx;\n}\n","/**\n * The wire protocol between this package and the ZOREAL OpenID Provider.\n *\n * VERSIONED, because the package is self-contained by decision (01 section 4):\n * a shipped version keeps working until the provider explicitly refuses it,\n * and the provider CAN refuse it, with a reason this package surfaces verbatim\n * (06 section 2: the compensating control for the npm supply chain). Both the\n * wire version and the package version travel on every pairing request so the\n * refusal can be precise.\n *\n * Endpoints, all relative to the issuer and all CORS-gated on the client's\n * authorized JavaScript origins (04 section 1.1):\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. 02 section 1: 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 (01 section 4) 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.0';\nexport const DEFAULT_ISSUER = 'https://id.zoreal.com';\n\n/** 02 section 1: pending TTL 120s. Poll gently; over-polling cancels. */\nexport const POLL_INTERVAL_MS = 2000;\n/** 02 section 4: enrolling extends the window to 30 minutes; 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' | '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","import { useMemo, type CSSProperties } from 'react';\nimport { useZorealFlow, type ActivePairing } from './useZorealLogin';\nimport type { NonOAuthError, ZorealLoginProps } from './types';\n\n/**\n * The drop-in button, browser-direct and therefore Tier A only (03 section 1:\n * it receives no access token, so there is nothing for a personal-data claim\n * to arrive on).\n *\n * The copy is neutral by decision (01 section 5): the button asserts nothing\n * about a person who has not yet authenticated. Styling is inline and\n * self-contained; no stylesheet, no font, no external asset, because this\n * renders on the most attacked page the integrator owns (01 section 4).\n */\n\nconst TEXTS: Record<NonNullable<ZorealLoginProps['text']>, string> = {\n continue_with: 'Continue with ZOREAL',\n signin_with: 'Sign in with ZOREAL',\n signup_with: 'Sign up with ZOREAL',\n signin: 'Sign in',\n};\n\nconst SIZES = {\n large: { height: 44, font: 15, pad: 20 },\n medium: { height: 38, font: 14, pad: 16 },\n small: { height: 32, font: 12, pad: 12 },\n} as const;\n\n/** The mark: a filled ring, the brand geometry reduced to currentColor. */\nconst Mark = ({ size }: { size: number }) => (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" aria-hidden focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.6\" />\n <circle cx=\"12\" cy=\"12\" r=\"3.4\" fill=\"currentColor\" />\n </svg>\n);\n\nconst PairingPanel = ({ pairing }: { pairing: ActivePairing }) => {\n const { status } = pairing.state;\n const line =\n status === 'claimed'\n ? 'Approve the login in your ZOREAL ID app.'\n : status === 'enrolling'\n ? 'Finishing enrolment. This screen will continue by itself.'\n : pairing.appLink\n ? 'Continue in the ZOREAL ID app, then return to this tab.'\n : 'Scan with your phone camera or the ZOREAL ID app.';\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Log in with ZOREAL\"\n style={{\n marginTop: 8,\n padding: 16,\n width: 232,\n borderRadius: 12,\n border: '1px solid rgba(128,128,128,0.35)',\n background: 'Canvas',\n color: 'CanvasText',\n textAlign: 'center',\n fontFamily: 'inherit',\n }}\n >\n {!pairing.appLink && (\n <img\n src={pairing.qrUrl}\n alt={`QR code for ${pairing.pairUrl}`}\n width={200}\n height={200}\n style={{ display: 'block', margin: '0 auto', borderRadius: 8 }}\n />\n )}\n <p style={{ margin: '10px 0 0', fontSize: 12, lineHeight: 1.5 }}>{line}</p>\n <button\n type=\"button\"\n onClick={pairing.cancel}\n style={{\n marginTop: 10,\n border: 'none',\n background: 'none',\n color: 'inherit',\n opacity: 0.6,\n fontSize: 12,\n cursor: 'pointer',\n textDecoration: 'underline',\n }}\n >\n Cancel\n </button>\n </div>\n );\n};\n\nexport function ZorealLogin(props: ZorealLoginProps) {\n const {\n onSuccess,\n onError,\n containerProps,\n type = 'standard',\n theme = 'filled',\n size = 'large',\n text = 'continue_with',\n shape = 'rectangular',\n logo_alignment = 'left',\n width,\n click_listener,\n ...request\n } = props;\n\n const { login, internals } = useZorealFlow({\n ...request,\n flow: 'browser-direct',\n onCredential: onSuccess,\n onError: (e) => onError?.({ type: 'unknown', description: e.description ?? e.error }),\n onNonOAuthError: (e: NonOAuthError) => onError?.(e),\n });\n\n const s = SIZES[size];\n const style: CSSProperties = useMemo(\n () => ({\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: logo_alignment === 'center' ? 'center' : 'flex-start',\n gap: 10,\n height: s.height,\n padding: `0 ${s.pad}px`,\n width,\n fontSize: s.font,\n fontFamily: 'inherit',\n fontWeight: 500,\n cursor: 'pointer',\n borderRadius: shape === 'pill' ? s.height / 2 : shape === 'square' ? 4 : 8,\n ...(theme === 'outline'\n ? { background: 'transparent', color: 'inherit', border: '1px solid rgba(128,128,128,0.5)' }\n : theme === 'filled_black'\n ? { background: '#111', color: '#fff', border: '1px solid #111' }\n : { background: '#00b4d9', color: '#fff', border: '1px solid #00b4d9' }),\n }),\n [logo_alignment, s, shape, theme, width]\n );\n\n return (\n <div {...containerProps}>\n <button\n type=\"button\"\n style={style}\n onClick={() => {\n click_listener?.();\n login();\n }}\n >\n <Mark size={Math.round(s.font * 1.25)} />\n {type === 'standard' && TEXTS[text]}\n </button>\n {internals.pairing && !internals.pairing.appLink && (\n <PairingPanel pairing={internals.pairing} />\n )}\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useZorealOAuth } from './context';\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 type {\n AcrValue,\n AuthCodeFlowOptions,\n BrowserDirectFlowOptions,\n ErrorCode,\n NonOAuthError,\n PairingState,\n SelectBy,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n ZorealLoginRequestOptions,\n} from './types';\n\nexport interface ActivePairing {\n requestId: string;\n pairUrl: string;\n qrUrl: string;\n state: PairingState;\n /** True when display resolved to the app link rather than the QR. */\n appLink: boolean;\n cancel: () => void;\n}\n\ninterface FlowInternals {\n /** Non-null while a pairing is on screen. ZorealLogin renders from this. */\n pairing: ActivePairing | null;\n}\n\n/**\n * The internal option shape: one flow discriminator, one success callback per\n * mode. The public API keeps Google's single overloaded onSuccess; this type\n * exists because an intersection of those two signatures is uninhabitable, and\n * the mapping from public to internal happens once, in useZorealLogin.\n */\nexport interface InternalFlowOptions extends ZorealLoginRequestOptions {\n flow: 'browser-direct' | 'auth-code';\n redirect_uri?: string;\n onCredential?: (response: ZorealCredentialResponse) => void;\n onCode?: (response: ZorealCodeResponse) => void;\n onError?: (error: Pick<NonOAuthError, 'description'> & { error: ErrorCode }) => void;\n onNonOAuthError?: (error: NonOAuthError) => void;\n}\n\n/**\n * The one flow, shared by the hook and the button. Starts a pairing, exposes\n * it for rendering, polls, and finishes per mode: browser-direct exchanges the\n * code here (public client, PKCE, no secret) and hands over an ID token;\n * auth-code hands the code and the PKCE verifier to the caller, whose backend\n * does the exchange with its client authentication.\n */\nexport function useZorealFlow(options: InternalFlowOptions): {\n login: () => void;\n internals: FlowInternals;\n} {\n const { clientId, issuer, locale } = useZorealOAuth();\n const [pairing, setPairing] = useState<ActivePairing | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // A component unmounting mid-login must stop the poll: the provider cancels\n // over-polled requests, and an orphaned interval is exactly how one happens.\n useEffect(() => () => abortRef.current?.abort(), []);\n\n const login = useCallback(() => {\n const opts = optionsRef.current;\n const run = async () => {\n abortRef.current?.abort();\n const controller = new AbortController();\n abortRef.current = controller;\n\n const flow = opts.flow;\n const verifier = generateVerifier();\n const state = generateState();\n const nonce = generateState();\n\n try {\n const started = await startPairing(issuer, {\n client_id: clientId,\n scope: opts.scope ?? 'openid',\n state,\n nonce,\n code_challenge: await challengeS256(verifier),\n redirect_uri: flow === 'auth-code' ? opts.redirect_uri : undefined,\n acr_values: Array.isArray(opts.acr_values)\n ? opts.acr_values.join(' ')\n : opts.acr_values,\n max_age: opts.max_age,\n prompt: opts.prompt,\n 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 opts.display === 'link' || (opts.display !== 'qr' && isMobileUserAgent());\n selectBy = useAppLink ? 'app_link' : 'qr';\n\n const active: ActivePairing = {\n requestId: started.request_id,\n pairUrl: started.pair_url,\n qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,\n state: { status: 'pending', expiresIn: started.expires_in },\n appLink: useAppLink,\n cancel: () => {\n controller.abort();\n setPairing(null);\n },\n };\n setPairing(active);\n\n if (useAppLink) {\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 (02 sections 3 and 4). A popup here would be blocked more\n // often than it would help.\n window.location.assign(started.pair_url);\n }\n\n code = await pollUntilApproved(\n issuer,\n started.request_id,\n (s) => {\n setPairing((p) => (p && p.requestId === started.request_id ? { ...p, state: s } : p));\n opts.onPairingStateChange?.(s);\n },\n controller.signal\n );\n }\n\n setPairing(null);\n\n if (flow === 'auth-code') {\n opts.onCode?.({\n code,\n scope: opts.scope ?? 'openid',\n app_state: opts.app_state,\n code_verifier: verifier,\n });\n return;\n }\n\n const tokens = await exchangeCode(issuer, {\n code,\n code_verifier: verifier,\n client_id: clientId,\n });\n const claims = unsafeClaims(tokens.id_token);\n const response: ZorealCredentialResponse = {\n credential: tokens.id_token,\n clientId,\n select_by: selectBy,\n acr: (claims.acr as AcrValue) ?? 'zoreal.device',\n };\n opts.onCredential?.(response);\n } catch (e) {\n setPairing(null);\n if (e instanceof DOMException && e.name === 'AbortError') return;\n if (e instanceof FlowAbandonedError) {\n opts.onNonOAuthError?.(e.reason);\n return;\n }\n if (e instanceof OAuthFlowError) {\n opts.onError?.({ error: e.error, description: e.description });\n return;\n }\n opts.onNonOAuthError?.({\n type: 'unknown',\n description: e instanceof Error ? e.message : String(e),\n });\n }\n };\n void run();\n }, [clientId, issuer, locale]);\n\n return { login, internals: { pairing } };\n}\n\nexport function useZorealLogin(\n options: { flow?: 'browser-direct' } & BrowserDirectFlowOptions\n): () => void;\nexport function useZorealLogin(options: { flow: 'auth-code' } & AuthCodeFlowOptions): () => void;\nexport function useZorealLogin(\n options: ({ flow?: 'browser-direct' | 'auth-code' } & ZorealLoginRequestOptions) &\n Partial<Pick<AuthCodeFlowOptions, 'redirect_uri' | 'ux_mode'>> & {\n onSuccess?: (response: never) => void;\n onError?: (error: Pick<NonOAuthError, 'description'> & { error: ErrorCode }) => void;\n onNonOAuthError?: (error: NonOAuthError) => void;\n }\n): () => void {\n if (options.ux_mode === 'redirect') {\n // v1 supports the popup shape only: the code and PKCE verifier go to your\n // onSuccess and from there to your backend over TLS. A redirect would have\n // to 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-react: ux_mode 'redirect' is not supported in v1. Use the default \" +\n \"'popup' shape and post the code and code_verifier from onSuccess to your backend.\"\n );\n }\n const flow = options.flow ?? 'browser-direct';\n return useZorealFlow({\n ...options,\n flow,\n onCredential:\n flow === 'browser-direct'\n ? (options.onSuccess as unknown as (r: ZorealCredentialResponse) => void)\n : undefined,\n onCode:\n flow === 'auth-code'\n ? (options.onSuccess as unknown as (r: ZorealCodeResponse) => void)\n : undefined,\n }).login;\n}\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 (02), 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 pairing channel, client side. 02 section 1 is the authority for the\n * object; 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 (02 section 1), 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_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: `@zoreal/oauth2-react/${SDK_VERSION}`,\n }),\n });\n\n const body = await parseJson(response);\n if (!response.ok) {\n // The provider's words, verbatim. 06 section 2: a refused package version\n // arrives here, and rewriting its reason would disable the one remediation\n // path that does not depend on integrators upgrading.\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 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 '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 (03 section 1): personal data lives at\n * /userinfo behind an access token this mode is never issued for Tier B\n * scopes, because those are 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/** 02 section 3: 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, per docs/14 section 3: mandatory for every client,\n * confidential ones included. There is no plain fallback and there must never\n * be one; a provider seeing 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","import { useEffect, useRef } from 'react';\nimport { useZorealFlow } from './useZorealLogin';\nimport type { UseZorealAutoLoginOptions } from './types';\n\n/**\n * Silent re-auth with prompt=none. NOT One Tap and never could be: there is no\n * ZOREAL session cookie in the browser to read, the credential is on a phone.\n * This succeeds only for a returning user at a consented sector with a live\n * session, the resulting acr is zoreal.session with an empty amr, and a\n * relying party that needs a live human must not build on this hook.\n *\n * PRIVACY STATUS, stated because the spec leaves it open (06 section 7, O5):\n * mounting this on a page sends a request to ZOREAL on page load, before the\n * user does anything. Until that question is decided, `disabled` defaults are\n * conservative: the hook fires once per mount, never retries, and does nothing\n * at all when `disabled` is true.\n */\nexport function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void {\n const { login } = useZorealFlow({\n flow: 'browser-direct',\n scope: options.scope,\n prompt: 'none',\n onCredential: options.onSuccess,\n onError: (e) => {\n // The provider's honest answer when no silent session exists (the\n // common case): unavailable, not an error, and never surfaced.\n const quiet = ['login_required', 'consent_required', 'interaction_required'];\n if (quiet.includes(e.error)) {\n options.onUnavailable?.();\n } else {\n options.onError?.({ type: 'unknown', description: e.description ?? e.error });\n }\n },\n onNonOAuthError: (e) => options.onError?.(e),\n });\n\n const fired = useRef(false);\n useEffect(() => {\n if (options.disabled || fired.current) return;\n fired.current = true;\n login();\n }, [options.disabled, login]);\n}\n","/**\n * Clears SDK-held local state. Named for parity with googleLogout and, like\n * it, LOCAL ONLY: it does not end the holder's ZOREAL session, which lives on\n * their phone and at the provider. A relying party that believes this signs\n * the user out of ZOREAL has a security misunderstanding, not a naming\n * complaint (05 section 8). The relying party's own session is the relying\n * party's to end.\n *\n * The SDK deliberately persists nothing (no localStorage, no cookies), so\n * today this has nothing to clear and exists as the stable API surface for a\n * future that does.\n */\nexport function zorealLogout(): void {\n // Intentionally empty until the SDK holds state worth clearing.\n}\n","import type { ZorealCodeResponse } from './types';\n\n/** Mirrors hasGrantedAllScopesGoogle: name-for-name portability (05 section 1). */\nexport function hasGrantedAllScopesZoreal(\n response: Pick<ZorealCodeResponse, 'scope'>,\n firstScope: string,\n ...restScopes: string[]\n): boolean {\n const granted = new Set((response.scope ?? '').split(/\\s+/).filter(Boolean));\n return [firstScope, ...restScopes].every((s) => granted.has(s));\n}\n\nexport function hasGrantedAnyScopeZoreal(\n response: Pick<ZorealCodeResponse, 'scope'>,\n firstScope: string,\n ...restScopes: string[]\n): boolean {\n const granted = new Set((response.scope ?? '').split(/\\s+/).filter(Boolean));\n return [firstScope, ...restScopes].some((s) => granted.has(s));\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","import_react"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/context.tsx","../src/wire.ts","../src/ZorealLogin.tsx","../src/useZorealLogin.ts","../src/jwt.ts","../src/pairing.ts","../src/pkce.ts","../src/useZorealAutoLogin.ts","../src/logout.ts","../src/scopes.ts"],"sourcesContent":["export { ZorealOAuthProvider, useZorealOAuth } from './context';\nexport type { ZorealOAuthProviderProps, ZorealOAuthContextProps } from './context';\nexport { ZorealLogin } from './ZorealLogin';\nexport { useZorealLogin } from './useZorealLogin';\nexport { useZorealAutoLogin } from './useZorealAutoLogin';\nexport { zorealLogout } from './logout';\nexport { hasGrantedAllScopesZoreal, hasGrantedAnyScopeZoreal } from './scopes';\nexport type {\n AcrValue,\n AuthCodeFlowOptions,\n BrowserDirectFlowOptions,\n ErrorCode,\n NonOAuthError,\n PairingState,\n SelectBy,\n UseZorealAutoLoginOptions,\n ZorealButtonConfiguration,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n ZorealLoginProps,\n ZorealLoginRequestOptions,\n} from './types';\n","import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { DEFAULT_ISSUER } from './wire';\n\nexport interface ZorealOAuthProviderProps {\n /** From the zoreal-web dashboard: the asset ID. */\n clientId: string;\n /** Override the provider origin. Sandbox and self-hosted testing only. */\n issuer?: string;\n /** BCP 47. Drives button text and the pairing page. */\n locale?: string;\n children: ReactNode;\n}\n\nexport interface ZorealOAuthContextProps {\n clientId: string;\n issuer: string;\n locale?: string;\n}\n\nconst ZorealOAuthContext = createContext<ZorealOAuthContextProps | null>(null);\n\nexport function ZorealOAuthProvider({\n clientId,\n issuer = DEFAULT_ISSUER,\n locale,\n children,\n}: ZorealOAuthProviderProps) {\n const value = useMemo(\n () => ({ clientId, issuer: issuer.replace(/\\/$/, ''), locale }),\n [clientId, issuer, locale]\n );\n return <ZorealOAuthContext.Provider value={value}>{children}</ZorealOAuthContext.Provider>;\n}\n\nexport function useZorealOAuth(): ZorealOAuthContextProps {\n const ctx = useContext(ZorealOAuthContext);\n if (!ctx) {\n throw new Error(\n 'useZorealOAuth must be used inside <ZorealOAuthProvider clientId=...>. ' +\n 'Wrap your app (or the part that logs in) in the provider.'\n );\n }\n return ctx;\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.2';\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' | '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","import { useMemo, type CSSProperties } from 'react';\nimport { useZorealFlow, type ActivePairing } from './useZorealLogin';\nimport type { NonOAuthError, ZorealLoginProps } from './types';\n\n/**\n * The drop-in button. It receives no access token, so it returns the\n * pseudonymous identity only; personal data needs the auth-code flow.\n *\n * The copy is neutral: the button asserts nothing about a person who has not\n * yet authenticated. Styling is inline and self-contained; no stylesheet, no\n * font, no external asset, because this renders on a sign-in page.\n */\n\nconst TEXTS: Record<NonNullable<ZorealLoginProps['text']>, string> = {\n continue_with: 'Continue with ZOREAL',\n signin_with: 'Sign in with ZOREAL',\n signup_with: 'Sign up with ZOREAL',\n signin: 'Sign in',\n};\n\nconst SIZES = {\n large: { height: 44, font: 15, pad: 20 },\n medium: { height: 38, font: 14, pad: 16 },\n small: { height: 32, font: 12, pad: 12 },\n} as const;\n\n/** The mark: a filled ring, the brand geometry reduced to currentColor. */\nconst Mark = ({ size }: { size: number }) => (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" aria-hidden focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.6\" />\n <circle cx=\"12\" cy=\"12\" r=\"3.4\" fill=\"currentColor\" />\n </svg>\n);\n\nconst PairingPanel = ({ pairing }: { pairing: ActivePairing }) => {\n const { status } = pairing.state;\n const line =\n status === 'claimed'\n ? 'Approve the login in your ZOREAL ID app.'\n : status === 'enrolling'\n ? 'Finishing enrolment. This screen will continue by itself.'\n : pairing.appLink\n ? 'Continue in the ZOREAL ID app, then return to this tab.'\n : 'Scan with your phone camera or the ZOREAL ID app.';\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Log in with ZOREAL\"\n style={{\n marginTop: 8,\n padding: 16,\n width: 232,\n borderRadius: 12,\n border: '1px solid rgba(128,128,128,0.35)',\n background: 'Canvas',\n color: 'CanvasText',\n textAlign: 'center',\n fontFamily: 'inherit',\n }}\n >\n {!pairing.appLink && (\n <img\n src={pairing.qrUrl}\n alt={`QR code for ${pairing.pairUrl}`}\n width={200}\n height={200}\n style={{ display: 'block', margin: '0 auto', borderRadius: 8 }}\n />\n )}\n <p style={{ margin: '10px 0 0', fontSize: 12, lineHeight: 1.5 }}>{line}</p>\n <button\n type=\"button\"\n onClick={pairing.cancel}\n style={{\n marginTop: 10,\n border: 'none',\n background: 'none',\n color: 'inherit',\n opacity: 0.6,\n fontSize: 12,\n cursor: 'pointer',\n textDecoration: 'underline',\n }}\n >\n Cancel\n </button>\n </div>\n );\n};\n\nexport function ZorealLogin(props: ZorealLoginProps) {\n const {\n onSuccess,\n onError,\n containerProps,\n type = 'standard',\n theme = 'filled',\n size = 'large',\n text = 'continue_with',\n shape = 'rectangular',\n logo_alignment = 'left',\n width,\n click_listener,\n ...request\n } = props;\n\n const { login, internals } = useZorealFlow({\n ...request,\n flow: 'browser-direct',\n onCredential: onSuccess,\n onError: (e) => onError?.({ type: 'unknown', description: e.description ?? e.error }),\n onNonOAuthError: (e: NonOAuthError) => onError?.(e),\n });\n\n const s = SIZES[size];\n const style: CSSProperties = useMemo(\n () => ({\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: logo_alignment === 'center' ? 'center' : 'flex-start',\n gap: 10,\n height: s.height,\n padding: `0 ${s.pad}px`,\n width,\n fontSize: s.font,\n fontFamily: 'inherit',\n fontWeight: 500,\n cursor: 'pointer',\n borderRadius: shape === 'pill' ? s.height / 2 : shape === 'square' ? 4 : 8,\n ...(theme === 'outline'\n ? { background: 'transparent', color: 'inherit', border: '1px solid rgba(128,128,128,0.5)' }\n : theme === 'filled_black'\n ? { background: '#111', color: '#fff', border: '1px solid #111' }\n : { background: '#00b4d9', color: '#fff', border: '1px solid #00b4d9' }),\n }),\n [logo_alignment, s, shape, theme, width]\n );\n\n return (\n <div {...containerProps}>\n <button\n type=\"button\"\n style={style}\n onClick={() => {\n click_listener?.();\n login();\n }}\n >\n <Mark size={Math.round(s.font * 1.25)} />\n {type === 'standard' && TEXTS[text]}\n </button>\n {internals.pairing && !internals.pairing.appLink && (\n <PairingPanel pairing={internals.pairing} />\n )}\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useZorealOAuth } from './context';\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 type {\n AcrValue,\n AuthCodeFlowOptions,\n BrowserDirectFlowOptions,\n ErrorCode,\n NonOAuthError,\n PairingState,\n SelectBy,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n ZorealLoginRequestOptions,\n} from './types';\n\nexport interface ActivePairing {\n requestId: string;\n pairUrl: string;\n qrUrl: string;\n state: PairingState;\n /** True when display resolved to the app link rather than the QR. */\n appLink: boolean;\n cancel: () => void;\n}\n\ninterface FlowInternals {\n /** Non-null while a pairing is on screen. ZorealLogin renders from this. */\n pairing: ActivePairing | null;\n}\n\n/**\n * The internal option shape: one flow discriminator, one success callback per\n * mode. The public API keeps Google's single overloaded onSuccess; this type\n * exists because an intersection of those two signatures is uninhabitable, and\n * the mapping from public to internal happens once, in useZorealLogin.\n */\nexport interface InternalFlowOptions extends ZorealLoginRequestOptions {\n flow: 'browser-direct' | 'auth-code';\n redirect_uri?: string;\n onCredential?: (response: ZorealCredentialResponse) => void;\n onCode?: (response: ZorealCodeResponse) => void;\n onError?: (error: Pick<NonOAuthError, 'description'> & { error: ErrorCode }) => void;\n onNonOAuthError?: (error: NonOAuthError) => void;\n}\n\n/**\n * The one flow, shared by the hook and the button. Starts a pairing, exposes\n * it for rendering, polls, and finishes per mode: browser-direct exchanges the\n * code here (public client, PKCE, no secret) and hands over an ID token;\n * auth-code hands the code and the PKCE verifier to the caller, whose backend\n * does the exchange with its client authentication.\n */\nexport function useZorealFlow(options: InternalFlowOptions): {\n login: () => void;\n internals: FlowInternals;\n} {\n const { clientId, issuer, locale } = useZorealOAuth();\n const [pairing, setPairing] = useState<ActivePairing | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // A component unmounting mid-login must stop the poll: the provider cancels\n // over-polled requests, and an orphaned interval is exactly how one happens.\n useEffect(() => () => abortRef.current?.abort(), []);\n\n const login = useCallback(() => {\n const opts = optionsRef.current;\n const run = async () => {\n abortRef.current?.abort();\n const controller = new AbortController();\n abortRef.current = controller;\n\n const flow = opts.flow;\n const verifier = generateVerifier();\n const state = generateState();\n const nonce = generateState();\n\n try {\n const started = await startPairing(issuer, {\n client_id: clientId,\n scope: opts.scope ?? 'openid',\n state,\n nonce,\n code_challenge: await challengeS256(verifier),\n redirect_uri: flow === 'auth-code' ? opts.redirect_uri : undefined,\n acr_values: Array.isArray(opts.acr_values)\n ? opts.acr_values.join(' ')\n : opts.acr_values,\n max_age: opts.max_age,\n prompt: opts.prompt,\n 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 opts.display === 'link' || (opts.display !== 'qr' && isMobileUserAgent());\n selectBy = useAppLink ? 'app_link' : 'qr';\n\n const active: ActivePairing = {\n requestId: started.request_id,\n pairUrl: started.pair_url,\n qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,\n state: { status: 'pending', expiresIn: started.expires_in },\n appLink: useAppLink,\n cancel: () => {\n controller.abort();\n setPairing(null);\n },\n };\n setPairing(active);\n\n if (useAppLink) {\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 (02 sections 3 and 4). A popup here would be blocked more\n // often than it would help.\n window.location.assign(started.pair_url);\n }\n\n code = await pollUntilApproved(\n issuer,\n started.request_id,\n (s) => {\n setPairing((p) => (p && p.requestId === started.request_id ? { ...p, state: s } : p));\n opts.onPairingStateChange?.(s);\n },\n controller.signal\n );\n }\n\n setPairing(null);\n\n if (flow === 'auth-code') {\n opts.onCode?.({\n code,\n scope: opts.scope ?? 'openid',\n app_state: opts.app_state,\n code_verifier: verifier,\n });\n return;\n }\n\n const tokens = await exchangeCode(issuer, {\n code,\n code_verifier: verifier,\n client_id: clientId,\n });\n const claims = unsafeClaims(tokens.id_token);\n const response: ZorealCredentialResponse = {\n credential: tokens.id_token,\n clientId,\n select_by: selectBy,\n acr: (claims.acr as AcrValue) ?? 'zoreal.device',\n };\n opts.onCredential?.(response);\n } catch (e) {\n setPairing(null);\n if (e instanceof DOMException && e.name === 'AbortError') return;\n if (e instanceof FlowAbandonedError) {\n opts.onNonOAuthError?.(e.reason);\n return;\n }\n if (e instanceof OAuthFlowError) {\n opts.onError?.({ error: e.error, description: e.description });\n return;\n }\n opts.onNonOAuthError?.({\n type: 'unknown',\n description: e instanceof Error ? e.message : String(e),\n });\n }\n };\n void run();\n }, [clientId, issuer, locale]);\n\n return { login, internals: { pairing } };\n}\n\nexport function useZorealLogin(\n options: { flow?: 'browser-direct' } & BrowserDirectFlowOptions\n): () => void;\nexport function useZorealLogin(options: { flow: 'auth-code' } & AuthCodeFlowOptions): () => void;\nexport function useZorealLogin(\n options: ({ flow?: 'browser-direct' | 'auth-code' } & ZorealLoginRequestOptions) &\n Partial<Pick<AuthCodeFlowOptions, 'redirect_uri' | 'ux_mode'>> & {\n onSuccess?: (response: never) => void;\n onError?: (error: Pick<NonOAuthError, 'description'> & { error: ErrorCode }) => void;\n onNonOAuthError?: (error: NonOAuthError) => void;\n }\n): () => void {\n if (options.ux_mode === 'redirect') {\n // v1 supports the popup shape only: the code and PKCE verifier go to your\n // onSuccess and from there to your backend over TLS. A redirect would have\n // to 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-react: ux_mode 'redirect' is not supported in v1. Use the default \" +\n \"'popup' shape and post the code and code_verifier from onSuccess to your backend.\"\n );\n }\n const flow = options.flow ?? 'browser-direct';\n return useZorealFlow({\n ...options,\n flow,\n onCredential:\n flow === 'browser-direct'\n ? (options.onSuccess as unknown as (r: ZorealCredentialResponse) => void)\n : undefined,\n onCode:\n flow === 'auth-code'\n ? (options.onSuccess as unknown as (r: ZorealCodeResponse) => void)\n : undefined,\n }).login;\n}\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 (02), 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 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_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: `@zoreal/oauth2-react/${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 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 '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. There is no plain fallback and there must never\n * be one; a provider seeing 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","import { useEffect, useRef } from 'react';\nimport { useZorealFlow } from './useZorealLogin';\nimport type { UseZorealAutoLoginOptions } from './types';\n\n/**\n * Silent re-auth with prompt=none. NOT One Tap and never could be: there is no\n * ZOREAL session cookie in the browser to read, the credential is on a phone.\n * This succeeds only for a returning user at a consented sector with a live\n * session, the resulting acr is zoreal.session with an empty amr, and a\n * relying party that needs a live human must not build on this hook.\n *\n * PRIVACY NOTE: mounting this on a page sends a request to ZOREAL on page\n * load, before the user does anything. The hook is therefore conservative: it\n * fires once per mount, never retries, and does nothing when `disabled`.\n */\nexport function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void {\n const { login } = useZorealFlow({\n flow: 'browser-direct',\n scope: options.scope,\n prompt: 'none',\n onCredential: options.onSuccess,\n onError: (e) => {\n // The provider's honest answer when no silent session exists (the\n // common case): unavailable, not an error, and never surfaced.\n const quiet = ['login_required', 'consent_required', 'interaction_required'];\n if (quiet.includes(e.error)) {\n options.onUnavailable?.();\n } else {\n options.onError?.({ type: 'unknown', description: e.description ?? e.error });\n }\n },\n onNonOAuthError: (e) => options.onError?.(e),\n });\n\n const fired = useRef(false);\n useEffect(() => {\n if (options.disabled || fired.current) return;\n fired.current = true;\n login();\n }, [options.disabled, login]);\n}\n","/**\n * Clears SDK-held local state. Named for parity with googleLogout and, like\n * it, LOCAL ONLY: it does not end the holder's ZOREAL session, which lives on\n * their phone and at the provider. A relying party that believes this signs\n * the user out of ZOREAL has a security misunderstanding, not a naming\n * complaint. The relying party's own session is the relying\n * party's to end.\n *\n * The SDK deliberately persists nothing (no localStorage, no cookies), so\n * today this has nothing to clear and exists as the stable API surface for a\n * future that does.\n */\nexport function zorealLogout(): void {\n // Intentionally empty until the SDK holds state worth clearing.\n}\n","import type { ZorealCodeResponse } from './types';\n\n/** Mirrors hasGrantedAllScopesGoogle for name-for-name portability. */\nexport function hasGrantedAllScopesZoreal(\n response: Pick<ZorealCodeResponse, 'scope'>,\n firstScope: string,\n ...restScopes: string[]\n): boolean {\n const granted = new Set((response.scope ?? '').split(/\\s+/).filter(Boolean));\n return [firstScope, ...restScopes].every((s) => granted.has(s));\n}\n\nexport function hasGrantedAnyScopeZoreal(\n response: Pick<ZorealCodeResponse, 'scope'>,\n firstScope: string,\n ...restScopes: string[]\n): boolean {\n const granted = new Set((response.scope ?? '').split(/\\s+/).filter(Boolean));\n return [firstScope, ...restScopes].some((s) => granted.has(s));\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","import_react"]}
package/dist/index.d.cts CHANGED
@@ -21,10 +21,8 @@ declare function useZorealOAuth(): ZorealOAuthContextProps;
21
21
  /**
22
22
  * The public types of @zoreal/oauth2-react.
23
23
  *
24
- * The authority for every shape here is zoreal/products/oauth2/05-react-sdk.md,
25
- * and the mapping to @react-oauth/google is one to one so a team already
26
- * integrated with Google ports by renaming. Where this file and that document
27
- * disagree, the document wins and this file has a bug.
24
+ * The API mirrors @react-oauth/google one to one, renamed, so a team already
25
+ * integrated with Google ports by renaming their imports.
28
26
  */
29
27
  type ErrorCode = 'invalid_request' | 'access_denied' | 'unauthorized_client' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable' | 'login_required' | 'consent_required' | 'interaction_required';
30
28
  /** Failures that are not OAuth errors, because the flow never reached the provider. */
@@ -35,17 +33,17 @@ type NonOAuthError = {
35
33
  };
36
34
  /** How the holder reached this login. Analogous to Google's select_by. */
37
35
  type SelectBy = 'qr' | 'app_link' | 'device' | 'session';
38
- /** 02 section 5. Describes what happened, never what was requested. */
36
+ /** How the login was actually authenticated. Describes what happened, never what was requested. */
39
37
  type AcrValue = 'zoreal.live' | 'zoreal.device' | 'zoreal.session';
40
38
  interface PairingState {
41
39
  status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'enrolling';
42
40
  /** Present while status is 'pending'. Seconds. */
43
41
  expiresIn?: number;
44
- /** Present while status is 'enrolling'. 02 section 4 extends the window to 30 minutes. */
42
+ /** Present while status is 'enrolling'. Enrolment extends the window well beyond a normal login. */
45
43
  enrolmentDeadline?: number;
46
44
  }
47
45
  interface ZorealLoginRequestOptions {
48
- /** Defaults to 'openid'. 03 section 2. Tier B scopes require flow: 'auth-code'. */
46
+ /** Defaults to 'openid'. Scopes that return personal data require flow: 'auth-code'. */
49
47
  scope?: string;
50
48
  /** Ask for a specific assurance. Omit to accept the default, zoreal.device. */
51
49
  acr_values?: AcrValue | AcrValue[];
@@ -56,7 +54,6 @@ interface ZorealLoginRequestOptions {
56
54
  app_state?: string;
57
55
  /**
58
56
  * 'auto' renders a QR on desktop and an app link on mobile, which is what you want.
59
- * 02 sections 2 and 3.
60
57
  */
61
58
  display?: 'auto' | 'qr' | 'link';
62
59
  /** Called on each pairing state change. Drive your own UI from this if you render one. */
@@ -67,9 +64,8 @@ interface ZorealButtonConfiguration {
67
64
  theme?: 'outline' | 'filled' | 'filled_black';
68
65
  size?: 'large' | 'medium' | 'small';
69
66
  /**
70
- * All four are neutral, by decision. 01 section 5: the button asserts nothing about a
71
- * person who has not yet authenticated. There is no 'verified_human' variant and there
72
- * will not be one.
67
+ * All four are neutral. The button asserts nothing about a person who has not yet
68
+ * authenticated; there is no 'verified_human' variant.
73
69
  */
74
70
  text?: 'continue_with' | 'signin_with' | 'signup_with' | 'signin';
75
71
  shape?: 'rectangular' | 'pill' | 'square';
@@ -92,9 +88,9 @@ interface ZorealCodeResponse {
92
88
  /**
93
89
  * The PKCE verifier for this code. Post it to your backend with the code;
94
90
  * the backend sends both to /token along with its client authentication.
95
- * Added 2026-08-26: PKCE is mandatory for every client (docs/14 section 3),
96
- * and the verifier is generated here, so your server can only present it if
97
- * this hands it over. It travels to YOUR backend over TLS and nowhere else.
91
+ * PKCE is mandatory for every client, and the verifier is generated here, so
92
+ * your server can only complete the exchange if this hands it over. It travels
93
+ * to YOUR backend over TLS and nowhere else.
98
94
  */
99
95
  code_verifier: string;
100
96
  }
@@ -111,7 +107,7 @@ interface AuthCodeFlowOptions extends ZorealLoginRequestOptions {
111
107
  error: ErrorCode;
112
108
  }) => void;
113
109
  onNonOAuthError?: (error: NonOAuthError) => void;
114
- /** Must be registered. 04 section 1. */
110
+ /** Must be registered for this client in the ZOREAL dashboard. */
115
111
  redirect_uri?: string;
116
112
  ux_mode?: 'popup' | 'redirect';
117
113
  }
@@ -149,11 +145,9 @@ declare function useZorealLogin(options: {
149
145
  * session, the resulting acr is zoreal.session with an empty amr, and a
150
146
  * relying party that needs a live human must not build on this hook.
151
147
  *
152
- * PRIVACY STATUS, stated because the spec leaves it open (06 section 7, O5):
153
- * mounting this on a page sends a request to ZOREAL on page load, before the
154
- * user does anything. Until that question is decided, `disabled` defaults are
155
- * conservative: the hook fires once per mount, never retries, and does nothing
156
- * at all when `disabled` is true.
148
+ * PRIVACY NOTE: mounting this on a page sends a request to ZOREAL on page
149
+ * load, before the user does anything. The hook is therefore conservative: it
150
+ * fires once per mount, never retries, and does nothing when `disabled`.
157
151
  */
158
152
  declare function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void;
159
153
 
@@ -162,7 +156,7 @@ declare function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void;
162
156
  * it, LOCAL ONLY: it does not end the holder's ZOREAL session, which lives on
163
157
  * their phone and at the provider. A relying party that believes this signs
164
158
  * the user out of ZOREAL has a security misunderstanding, not a naming
165
- * complaint (05 section 8). The relying party's own session is the relying
159
+ * complaint. The relying party's own session is the relying
166
160
  * party's to end.
167
161
  *
168
162
  * The SDK deliberately persists nothing (no localStorage, no cookies), so
@@ -171,7 +165,7 @@ declare function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void;
171
165
  */
172
166
  declare function zorealLogout(): void;
173
167
 
174
- /** Mirrors hasGrantedAllScopesGoogle: name-for-name portability (05 section 1). */
168
+ /** Mirrors hasGrantedAllScopesGoogle for name-for-name portability. */
175
169
  declare function hasGrantedAllScopesZoreal(response: Pick<ZorealCodeResponse, 'scope'>, firstScope: string, ...restScopes: string[]): boolean;
176
170
  declare function hasGrantedAnyScopeZoreal(response: Pick<ZorealCodeResponse, 'scope'>, firstScope: string, ...restScopes: string[]): boolean;
177
171
 
package/dist/index.d.ts CHANGED
@@ -21,10 +21,8 @@ declare function useZorealOAuth(): ZorealOAuthContextProps;
21
21
  /**
22
22
  * The public types of @zoreal/oauth2-react.
23
23
  *
24
- * The authority for every shape here is zoreal/products/oauth2/05-react-sdk.md,
25
- * and the mapping to @react-oauth/google is one to one so a team already
26
- * integrated with Google ports by renaming. Where this file and that document
27
- * disagree, the document wins and this file has a bug.
24
+ * The API mirrors @react-oauth/google one to one, renamed, so a team already
25
+ * integrated with Google ports by renaming their imports.
28
26
  */
29
27
  type ErrorCode = 'invalid_request' | 'access_denied' | 'unauthorized_client' | 'unsupported_response_type' | 'invalid_scope' | 'server_error' | 'temporarily_unavailable' | 'login_required' | 'consent_required' | 'interaction_required';
30
28
  /** Failures that are not OAuth errors, because the flow never reached the provider. */
@@ -35,17 +33,17 @@ type NonOAuthError = {
35
33
  };
36
34
  /** How the holder reached this login. Analogous to Google's select_by. */
37
35
  type SelectBy = 'qr' | 'app_link' | 'device' | 'session';
38
- /** 02 section 5. Describes what happened, never what was requested. */
36
+ /** How the login was actually authenticated. Describes what happened, never what was requested. */
39
37
  type AcrValue = 'zoreal.live' | 'zoreal.device' | 'zoreal.session';
40
38
  interface PairingState {
41
39
  status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'enrolling';
42
40
  /** Present while status is 'pending'. Seconds. */
43
41
  expiresIn?: number;
44
- /** Present while status is 'enrolling'. 02 section 4 extends the window to 30 minutes. */
42
+ /** Present while status is 'enrolling'. Enrolment extends the window well beyond a normal login. */
45
43
  enrolmentDeadline?: number;
46
44
  }
47
45
  interface ZorealLoginRequestOptions {
48
- /** Defaults to 'openid'. 03 section 2. Tier B scopes require flow: 'auth-code'. */
46
+ /** Defaults to 'openid'. Scopes that return personal data require flow: 'auth-code'. */
49
47
  scope?: string;
50
48
  /** Ask for a specific assurance. Omit to accept the default, zoreal.device. */
51
49
  acr_values?: AcrValue | AcrValue[];
@@ -56,7 +54,6 @@ interface ZorealLoginRequestOptions {
56
54
  app_state?: string;
57
55
  /**
58
56
  * 'auto' renders a QR on desktop and an app link on mobile, which is what you want.
59
- * 02 sections 2 and 3.
60
57
  */
61
58
  display?: 'auto' | 'qr' | 'link';
62
59
  /** Called on each pairing state change. Drive your own UI from this if you render one. */
@@ -67,9 +64,8 @@ interface ZorealButtonConfiguration {
67
64
  theme?: 'outline' | 'filled' | 'filled_black';
68
65
  size?: 'large' | 'medium' | 'small';
69
66
  /**
70
- * All four are neutral, by decision. 01 section 5: the button asserts nothing about a
71
- * person who has not yet authenticated. There is no 'verified_human' variant and there
72
- * will not be one.
67
+ * All four are neutral. The button asserts nothing about a person who has not yet
68
+ * authenticated; there is no 'verified_human' variant.
73
69
  */
74
70
  text?: 'continue_with' | 'signin_with' | 'signup_with' | 'signin';
75
71
  shape?: 'rectangular' | 'pill' | 'square';
@@ -92,9 +88,9 @@ interface ZorealCodeResponse {
92
88
  /**
93
89
  * The PKCE verifier for this code. Post it to your backend with the code;
94
90
  * the backend sends both to /token along with its client authentication.
95
- * Added 2026-08-26: PKCE is mandatory for every client (docs/14 section 3),
96
- * and the verifier is generated here, so your server can only present it if
97
- * this hands it over. It travels to YOUR backend over TLS and nowhere else.
91
+ * PKCE is mandatory for every client, and the verifier is generated here, so
92
+ * your server can only complete the exchange if this hands it over. It travels
93
+ * to YOUR backend over TLS and nowhere else.
98
94
  */
99
95
  code_verifier: string;
100
96
  }
@@ -111,7 +107,7 @@ interface AuthCodeFlowOptions extends ZorealLoginRequestOptions {
111
107
  error: ErrorCode;
112
108
  }) => void;
113
109
  onNonOAuthError?: (error: NonOAuthError) => void;
114
- /** Must be registered. 04 section 1. */
110
+ /** Must be registered for this client in the ZOREAL dashboard. */
115
111
  redirect_uri?: string;
116
112
  ux_mode?: 'popup' | 'redirect';
117
113
  }
@@ -149,11 +145,9 @@ declare function useZorealLogin(options: {
149
145
  * session, the resulting acr is zoreal.session with an empty amr, and a
150
146
  * relying party that needs a live human must not build on this hook.
151
147
  *
152
- * PRIVACY STATUS, stated because the spec leaves it open (06 section 7, O5):
153
- * mounting this on a page sends a request to ZOREAL on page load, before the
154
- * user does anything. Until that question is decided, `disabled` defaults are
155
- * conservative: the hook fires once per mount, never retries, and does nothing
156
- * at all when `disabled` is true.
148
+ * PRIVACY NOTE: mounting this on a page sends a request to ZOREAL on page
149
+ * load, before the user does anything. The hook is therefore conservative: it
150
+ * fires once per mount, never retries, and does nothing when `disabled`.
157
151
  */
158
152
  declare function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void;
159
153
 
@@ -162,7 +156,7 @@ declare function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void;
162
156
  * it, LOCAL ONLY: it does not end the holder's ZOREAL session, which lives on
163
157
  * their phone and at the provider. A relying party that believes this signs
164
158
  * the user out of ZOREAL has a security misunderstanding, not a naming
165
- * complaint (05 section 8). The relying party's own session is the relying
159
+ * complaint. The relying party's own session is the relying
166
160
  * party's to end.
167
161
  *
168
162
  * The SDK deliberately persists nothing (no localStorage, no cookies), so
@@ -171,7 +165,7 @@ declare function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void;
171
165
  */
172
166
  declare function zorealLogout(): void;
173
167
 
174
- /** Mirrors hasGrantedAllScopesGoogle: name-for-name portability (05 section 1). */
168
+ /** Mirrors hasGrantedAllScopesGoogle for name-for-name portability. */
175
169
  declare function hasGrantedAllScopesZoreal(response: Pick<ZorealCodeResponse, 'scope'>, firstScope: string, ...restScopes: string[]): boolean;
176
170
  declare function hasGrantedAnyScopeZoreal(response: Pick<ZorealCodeResponse, 'scope'>, firstScope: string, ...restScopes: string[]): boolean;
177
171
 
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { createContext, useContext, useMemo } from "react";
5
5
 
6
6
  // src/wire.ts
7
7
  var WIRE_VERSION = 1;
8
- var SDK_VERSION = "0.1.0";
8
+ var SDK_VERSION = "0.1.2";
9
9
  var DEFAULT_ISSUER = "https://id.zoreal.com";
10
10
  var POLL_INTERVAL_MS = 2e3;
11
11
  var POLL_INTERVAL_ENROLLING_MS = 5e3;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/context.tsx","../src/wire.ts","../src/ZorealLogin.tsx","../src/useZorealLogin.ts","../src/jwt.ts","../src/pairing.ts","../src/pkce.ts","../src/useZorealAutoLogin.ts","../src/logout.ts","../src/scopes.ts"],"sourcesContent":["import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { DEFAULT_ISSUER } from './wire';\n\nexport interface ZorealOAuthProviderProps {\n /** From the zoreal-web dashboard: the asset ID. */\n clientId: string;\n /** Override the provider origin. Sandbox and self-hosted testing only. */\n issuer?: string;\n /** BCP 47. Drives button text and the pairing page. */\n locale?: string;\n children: ReactNode;\n}\n\nexport interface ZorealOAuthContextProps {\n clientId: string;\n issuer: string;\n locale?: string;\n}\n\nconst ZorealOAuthContext = createContext<ZorealOAuthContextProps | null>(null);\n\nexport function ZorealOAuthProvider({\n clientId,\n issuer = DEFAULT_ISSUER,\n locale,\n children,\n}: ZorealOAuthProviderProps) {\n const value = useMemo(\n () => ({ clientId, issuer: issuer.replace(/\\/$/, ''), locale }),\n [clientId, issuer, locale]\n );\n return <ZorealOAuthContext.Provider value={value}>{children}</ZorealOAuthContext.Provider>;\n}\n\nexport function useZorealOAuth(): ZorealOAuthContextProps {\n const ctx = useContext(ZorealOAuthContext);\n if (!ctx) {\n throw new Error(\n 'useZorealOAuth must be used inside <ZorealOAuthProvider clientId=...>. ' +\n 'Wrap your app (or the part that logs in) in the provider.'\n );\n }\n return ctx;\n}\n","/**\n * The wire protocol between this package and the ZOREAL OpenID Provider.\n *\n * VERSIONED, because the package is self-contained by decision (01 section 4):\n * a shipped version keeps working until the provider explicitly refuses it,\n * and the provider CAN refuse it, with a reason this package surfaces verbatim\n * (06 section 2: the compensating control for the npm supply chain). Both the\n * wire version and the package version travel on every pairing request so the\n * refusal can be precise.\n *\n * Endpoints, all relative to the issuer and all CORS-gated on the client's\n * authorized JavaScript origins (04 section 1.1):\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. 02 section 1: 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 (01 section 4) 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.0';\nexport const DEFAULT_ISSUER = 'https://id.zoreal.com';\n\n/** 02 section 1: pending TTL 120s. Poll gently; over-polling cancels. */\nexport const POLL_INTERVAL_MS = 2000;\n/** 02 section 4: enrolling extends the window to 30 minutes; 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' | '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","import { useMemo, type CSSProperties } from 'react';\nimport { useZorealFlow, type ActivePairing } from './useZorealLogin';\nimport type { NonOAuthError, ZorealLoginProps } from './types';\n\n/**\n * The drop-in button, browser-direct and therefore Tier A only (03 section 1:\n * it receives no access token, so there is nothing for a personal-data claim\n * to arrive on).\n *\n * The copy is neutral by decision (01 section 5): the button asserts nothing\n * about a person who has not yet authenticated. Styling is inline and\n * self-contained; no stylesheet, no font, no external asset, because this\n * renders on the most attacked page the integrator owns (01 section 4).\n */\n\nconst TEXTS: Record<NonNullable<ZorealLoginProps['text']>, string> = {\n continue_with: 'Continue with ZOREAL',\n signin_with: 'Sign in with ZOREAL',\n signup_with: 'Sign up with ZOREAL',\n signin: 'Sign in',\n};\n\nconst SIZES = {\n large: { height: 44, font: 15, pad: 20 },\n medium: { height: 38, font: 14, pad: 16 },\n small: { height: 32, font: 12, pad: 12 },\n} as const;\n\n/** The mark: a filled ring, the brand geometry reduced to currentColor. */\nconst Mark = ({ size }: { size: number }) => (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" aria-hidden focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.6\" />\n <circle cx=\"12\" cy=\"12\" r=\"3.4\" fill=\"currentColor\" />\n </svg>\n);\n\nconst PairingPanel = ({ pairing }: { pairing: ActivePairing }) => {\n const { status } = pairing.state;\n const line =\n status === 'claimed'\n ? 'Approve the login in your ZOREAL ID app.'\n : status === 'enrolling'\n ? 'Finishing enrolment. This screen will continue by itself.'\n : pairing.appLink\n ? 'Continue in the ZOREAL ID app, then return to this tab.'\n : 'Scan with your phone camera or the ZOREAL ID app.';\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Log in with ZOREAL\"\n style={{\n marginTop: 8,\n padding: 16,\n width: 232,\n borderRadius: 12,\n border: '1px solid rgba(128,128,128,0.35)',\n background: 'Canvas',\n color: 'CanvasText',\n textAlign: 'center',\n fontFamily: 'inherit',\n }}\n >\n {!pairing.appLink && (\n <img\n src={pairing.qrUrl}\n alt={`QR code for ${pairing.pairUrl}`}\n width={200}\n height={200}\n style={{ display: 'block', margin: '0 auto', borderRadius: 8 }}\n />\n )}\n <p style={{ margin: '10px 0 0', fontSize: 12, lineHeight: 1.5 }}>{line}</p>\n <button\n type=\"button\"\n onClick={pairing.cancel}\n style={{\n marginTop: 10,\n border: 'none',\n background: 'none',\n color: 'inherit',\n opacity: 0.6,\n fontSize: 12,\n cursor: 'pointer',\n textDecoration: 'underline',\n }}\n >\n Cancel\n </button>\n </div>\n );\n};\n\nexport function ZorealLogin(props: ZorealLoginProps) {\n const {\n onSuccess,\n onError,\n containerProps,\n type = 'standard',\n theme = 'filled',\n size = 'large',\n text = 'continue_with',\n shape = 'rectangular',\n logo_alignment = 'left',\n width,\n click_listener,\n ...request\n } = props;\n\n const { login, internals } = useZorealFlow({\n ...request,\n flow: 'browser-direct',\n onCredential: onSuccess,\n onError: (e) => onError?.({ type: 'unknown', description: e.description ?? e.error }),\n onNonOAuthError: (e: NonOAuthError) => onError?.(e),\n });\n\n const s = SIZES[size];\n const style: CSSProperties = useMemo(\n () => ({\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: logo_alignment === 'center' ? 'center' : 'flex-start',\n gap: 10,\n height: s.height,\n padding: `0 ${s.pad}px`,\n width,\n fontSize: s.font,\n fontFamily: 'inherit',\n fontWeight: 500,\n cursor: 'pointer',\n borderRadius: shape === 'pill' ? s.height / 2 : shape === 'square' ? 4 : 8,\n ...(theme === 'outline'\n ? { background: 'transparent', color: 'inherit', border: '1px solid rgba(128,128,128,0.5)' }\n : theme === 'filled_black'\n ? { background: '#111', color: '#fff', border: '1px solid #111' }\n : { background: '#00b4d9', color: '#fff', border: '1px solid #00b4d9' }),\n }),\n [logo_alignment, s, shape, theme, width]\n );\n\n return (\n <div {...containerProps}>\n <button\n type=\"button\"\n style={style}\n onClick={() => {\n click_listener?.();\n login();\n }}\n >\n <Mark size={Math.round(s.font * 1.25)} />\n {type === 'standard' && TEXTS[text]}\n </button>\n {internals.pairing && !internals.pairing.appLink && (\n <PairingPanel pairing={internals.pairing} />\n )}\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useZorealOAuth } from './context';\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 type {\n AcrValue,\n AuthCodeFlowOptions,\n BrowserDirectFlowOptions,\n ErrorCode,\n NonOAuthError,\n PairingState,\n SelectBy,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n ZorealLoginRequestOptions,\n} from './types';\n\nexport interface ActivePairing {\n requestId: string;\n pairUrl: string;\n qrUrl: string;\n state: PairingState;\n /** True when display resolved to the app link rather than the QR. */\n appLink: boolean;\n cancel: () => void;\n}\n\ninterface FlowInternals {\n /** Non-null while a pairing is on screen. ZorealLogin renders from this. */\n pairing: ActivePairing | null;\n}\n\n/**\n * The internal option shape: one flow discriminator, one success callback per\n * mode. The public API keeps Google's single overloaded onSuccess; this type\n * exists because an intersection of those two signatures is uninhabitable, and\n * the mapping from public to internal happens once, in useZorealLogin.\n */\nexport interface InternalFlowOptions extends ZorealLoginRequestOptions {\n flow: 'browser-direct' | 'auth-code';\n redirect_uri?: string;\n onCredential?: (response: ZorealCredentialResponse) => void;\n onCode?: (response: ZorealCodeResponse) => void;\n onError?: (error: Pick<NonOAuthError, 'description'> & { error: ErrorCode }) => void;\n onNonOAuthError?: (error: NonOAuthError) => void;\n}\n\n/**\n * The one flow, shared by the hook and the button. Starts a pairing, exposes\n * it for rendering, polls, and finishes per mode: browser-direct exchanges the\n * code here (public client, PKCE, no secret) and hands over an ID token;\n * auth-code hands the code and the PKCE verifier to the caller, whose backend\n * does the exchange with its client authentication.\n */\nexport function useZorealFlow(options: InternalFlowOptions): {\n login: () => void;\n internals: FlowInternals;\n} {\n const { clientId, issuer, locale } = useZorealOAuth();\n const [pairing, setPairing] = useState<ActivePairing | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // A component unmounting mid-login must stop the poll: the provider cancels\n // over-polled requests, and an orphaned interval is exactly how one happens.\n useEffect(() => () => abortRef.current?.abort(), []);\n\n const login = useCallback(() => {\n const opts = optionsRef.current;\n const run = async () => {\n abortRef.current?.abort();\n const controller = new AbortController();\n abortRef.current = controller;\n\n const flow = opts.flow;\n const verifier = generateVerifier();\n const state = generateState();\n const nonce = generateState();\n\n try {\n const started = await startPairing(issuer, {\n client_id: clientId,\n scope: opts.scope ?? 'openid',\n state,\n nonce,\n code_challenge: await challengeS256(verifier),\n redirect_uri: flow === 'auth-code' ? opts.redirect_uri : undefined,\n acr_values: Array.isArray(opts.acr_values)\n ? opts.acr_values.join(' ')\n : opts.acr_values,\n max_age: opts.max_age,\n prompt: opts.prompt,\n 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 opts.display === 'link' || (opts.display !== 'qr' && isMobileUserAgent());\n selectBy = useAppLink ? 'app_link' : 'qr';\n\n const active: ActivePairing = {\n requestId: started.request_id,\n pairUrl: started.pair_url,\n qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,\n state: { status: 'pending', expiresIn: started.expires_in },\n appLink: useAppLink,\n cancel: () => {\n controller.abort();\n setPairing(null);\n },\n };\n setPairing(active);\n\n if (useAppLink) {\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 (02 sections 3 and 4). A popup here would be blocked more\n // often than it would help.\n window.location.assign(started.pair_url);\n }\n\n code = await pollUntilApproved(\n issuer,\n started.request_id,\n (s) => {\n setPairing((p) => (p && p.requestId === started.request_id ? { ...p, state: s } : p));\n opts.onPairingStateChange?.(s);\n },\n controller.signal\n );\n }\n\n setPairing(null);\n\n if (flow === 'auth-code') {\n opts.onCode?.({\n code,\n scope: opts.scope ?? 'openid',\n app_state: opts.app_state,\n code_verifier: verifier,\n });\n return;\n }\n\n const tokens = await exchangeCode(issuer, {\n code,\n code_verifier: verifier,\n client_id: clientId,\n });\n const claims = unsafeClaims(tokens.id_token);\n const response: ZorealCredentialResponse = {\n credential: tokens.id_token,\n clientId,\n select_by: selectBy,\n acr: (claims.acr as AcrValue) ?? 'zoreal.device',\n };\n opts.onCredential?.(response);\n } catch (e) {\n setPairing(null);\n if (e instanceof DOMException && e.name === 'AbortError') return;\n if (e instanceof FlowAbandonedError) {\n opts.onNonOAuthError?.(e.reason);\n return;\n }\n if (e instanceof OAuthFlowError) {\n opts.onError?.({ error: e.error, description: e.description });\n return;\n }\n opts.onNonOAuthError?.({\n type: 'unknown',\n description: e instanceof Error ? e.message : String(e),\n });\n }\n };\n void run();\n }, [clientId, issuer, locale]);\n\n return { login, internals: { pairing } };\n}\n\nexport function useZorealLogin(\n options: { flow?: 'browser-direct' } & BrowserDirectFlowOptions\n): () => void;\nexport function useZorealLogin(options: { flow: 'auth-code' } & AuthCodeFlowOptions): () => void;\nexport function useZorealLogin(\n options: ({ flow?: 'browser-direct' | 'auth-code' } & ZorealLoginRequestOptions) &\n Partial<Pick<AuthCodeFlowOptions, 'redirect_uri' | 'ux_mode'>> & {\n onSuccess?: (response: never) => void;\n onError?: (error: Pick<NonOAuthError, 'description'> & { error: ErrorCode }) => void;\n onNonOAuthError?: (error: NonOAuthError) => void;\n }\n): () => void {\n if (options.ux_mode === 'redirect') {\n // v1 supports the popup shape only: the code and PKCE verifier go to your\n // onSuccess and from there to your backend over TLS. A redirect would have\n // to 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-react: ux_mode 'redirect' is not supported in v1. Use the default \" +\n \"'popup' shape and post the code and code_verifier from onSuccess to your backend.\"\n );\n }\n const flow = options.flow ?? 'browser-direct';\n return useZorealFlow({\n ...options,\n flow,\n onCredential:\n flow === 'browser-direct'\n ? (options.onSuccess as unknown as (r: ZorealCredentialResponse) => void)\n : undefined,\n onCode:\n flow === 'auth-code'\n ? (options.onSuccess as unknown as (r: ZorealCodeResponse) => void)\n : undefined,\n }).login;\n}\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 (02), 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 pairing channel, client side. 02 section 1 is the authority for the\n * object; 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 (02 section 1), 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_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: `@zoreal/oauth2-react/${SDK_VERSION}`,\n }),\n });\n\n const body = await parseJson(response);\n if (!response.ok) {\n // The provider's words, verbatim. 06 section 2: a refused package version\n // arrives here, and rewriting its reason would disable the one remediation\n // path that does not depend on integrators upgrading.\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 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 '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 (03 section 1): personal data lives at\n * /userinfo behind an access token this mode is never issued for Tier B\n * scopes, because those are 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/** 02 section 3: 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, per docs/14 section 3: mandatory for every client,\n * confidential ones included. There is no plain fallback and there must never\n * be one; a provider seeing 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","import { useEffect, useRef } from 'react';\nimport { useZorealFlow } from './useZorealLogin';\nimport type { UseZorealAutoLoginOptions } from './types';\n\n/**\n * Silent re-auth with prompt=none. NOT One Tap and never could be: there is no\n * ZOREAL session cookie in the browser to read, the credential is on a phone.\n * This succeeds only for a returning user at a consented sector with a live\n * session, the resulting acr is zoreal.session with an empty amr, and a\n * relying party that needs a live human must not build on this hook.\n *\n * PRIVACY STATUS, stated because the spec leaves it open (06 section 7, O5):\n * mounting this on a page sends a request to ZOREAL on page load, before the\n * user does anything. Until that question is decided, `disabled` defaults are\n * conservative: the hook fires once per mount, never retries, and does nothing\n * at all when `disabled` is true.\n */\nexport function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void {\n const { login } = useZorealFlow({\n flow: 'browser-direct',\n scope: options.scope,\n prompt: 'none',\n onCredential: options.onSuccess,\n onError: (e) => {\n // The provider's honest answer when no silent session exists (the\n // common case): unavailable, not an error, and never surfaced.\n const quiet = ['login_required', 'consent_required', 'interaction_required'];\n if (quiet.includes(e.error)) {\n options.onUnavailable?.();\n } else {\n options.onError?.({ type: 'unknown', description: e.description ?? e.error });\n }\n },\n onNonOAuthError: (e) => options.onError?.(e),\n });\n\n const fired = useRef(false);\n useEffect(() => {\n if (options.disabled || fired.current) return;\n fired.current = true;\n login();\n }, [options.disabled, login]);\n}\n","/**\n * Clears SDK-held local state. Named for parity with googleLogout and, like\n * it, LOCAL ONLY: it does not end the holder's ZOREAL session, which lives on\n * their phone and at the provider. A relying party that believes this signs\n * the user out of ZOREAL has a security misunderstanding, not a naming\n * complaint (05 section 8). The relying party's own session is the relying\n * party's to end.\n *\n * The SDK deliberately persists nothing (no localStorage, no cookies), so\n * today this has nothing to clear and exists as the stable API surface for a\n * future that does.\n */\nexport function zorealLogout(): void {\n // Intentionally empty until the SDK holds state worth clearing.\n}\n","import type { ZorealCodeResponse } from './types';\n\n/** Mirrors hasGrantedAllScopesGoogle: name-for-name portability (05 section 1). */\nexport function hasGrantedAllScopesZoreal(\n response: Pick<ZorealCodeResponse, 'scope'>,\n firstScope: string,\n ...restScopes: string[]\n): boolean {\n const granted = new Set((response.scope ?? '').split(/\\s+/).filter(Boolean));\n return [firstScope, ...restScopes].every((s) => granted.has(s));\n}\n\nexport function hasGrantedAnyScopeZoreal(\n response: Pick<ZorealCodeResponse, 'scope'>,\n firstScope: string,\n ...restScopes: string[]\n): boolean {\n const granted = new Set((response.scope ?? '').split(/\\s+/).filter(Boolean));\n return [firstScope, ...restScopes].some((s) => granted.has(s));\n}\n"],"mappings":"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","names":["useMemo","jsx","useMemo","useEffect","useRef","useRef","useEffect"]}
1
+ {"version":3,"sources":["../src/context.tsx","../src/wire.ts","../src/ZorealLogin.tsx","../src/useZorealLogin.ts","../src/jwt.ts","../src/pairing.ts","../src/pkce.ts","../src/useZorealAutoLogin.ts","../src/logout.ts","../src/scopes.ts"],"sourcesContent":["import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { DEFAULT_ISSUER } from './wire';\n\nexport interface ZorealOAuthProviderProps {\n /** From the zoreal-web dashboard: the asset ID. */\n clientId: string;\n /** Override the provider origin. Sandbox and self-hosted testing only. */\n issuer?: string;\n /** BCP 47. Drives button text and the pairing page. */\n locale?: string;\n children: ReactNode;\n}\n\nexport interface ZorealOAuthContextProps {\n clientId: string;\n issuer: string;\n locale?: string;\n}\n\nconst ZorealOAuthContext = createContext<ZorealOAuthContextProps | null>(null);\n\nexport function ZorealOAuthProvider({\n clientId,\n issuer = DEFAULT_ISSUER,\n locale,\n children,\n}: ZorealOAuthProviderProps) {\n const value = useMemo(\n () => ({ clientId, issuer: issuer.replace(/\\/$/, ''), locale }),\n [clientId, issuer, locale]\n );\n return <ZorealOAuthContext.Provider value={value}>{children}</ZorealOAuthContext.Provider>;\n}\n\nexport function useZorealOAuth(): ZorealOAuthContextProps {\n const ctx = useContext(ZorealOAuthContext);\n if (!ctx) {\n throw new Error(\n 'useZorealOAuth must be used inside <ZorealOAuthProvider clientId=...>. ' +\n 'Wrap your app (or the part that logs in) in the provider.'\n );\n }\n return ctx;\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.2';\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' | '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","import { useMemo, type CSSProperties } from 'react';\nimport { useZorealFlow, type ActivePairing } from './useZorealLogin';\nimport type { NonOAuthError, ZorealLoginProps } from './types';\n\n/**\n * The drop-in button. It receives no access token, so it returns the\n * pseudonymous identity only; personal data needs the auth-code flow.\n *\n * The copy is neutral: the button asserts nothing about a person who has not\n * yet authenticated. Styling is inline and self-contained; no stylesheet, no\n * font, no external asset, because this renders on a sign-in page.\n */\n\nconst TEXTS: Record<NonNullable<ZorealLoginProps['text']>, string> = {\n continue_with: 'Continue with ZOREAL',\n signin_with: 'Sign in with ZOREAL',\n signup_with: 'Sign up with ZOREAL',\n signin: 'Sign in',\n};\n\nconst SIZES = {\n large: { height: 44, font: 15, pad: 20 },\n medium: { height: 38, font: 14, pad: 16 },\n small: { height: 32, font: 12, pad: 12 },\n} as const;\n\n/** The mark: a filled ring, the brand geometry reduced to currentColor. */\nconst Mark = ({ size }: { size: number }) => (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" aria-hidden focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.6\" />\n <circle cx=\"12\" cy=\"12\" r=\"3.4\" fill=\"currentColor\" />\n </svg>\n);\n\nconst PairingPanel = ({ pairing }: { pairing: ActivePairing }) => {\n const { status } = pairing.state;\n const line =\n status === 'claimed'\n ? 'Approve the login in your ZOREAL ID app.'\n : status === 'enrolling'\n ? 'Finishing enrolment. This screen will continue by itself.'\n : pairing.appLink\n ? 'Continue in the ZOREAL ID app, then return to this tab.'\n : 'Scan with your phone camera or the ZOREAL ID app.';\n\n return (\n <div\n role=\"dialog\"\n aria-label=\"Log in with ZOREAL\"\n style={{\n marginTop: 8,\n padding: 16,\n width: 232,\n borderRadius: 12,\n border: '1px solid rgba(128,128,128,0.35)',\n background: 'Canvas',\n color: 'CanvasText',\n textAlign: 'center',\n fontFamily: 'inherit',\n }}\n >\n {!pairing.appLink && (\n <img\n src={pairing.qrUrl}\n alt={`QR code for ${pairing.pairUrl}`}\n width={200}\n height={200}\n style={{ display: 'block', margin: '0 auto', borderRadius: 8 }}\n />\n )}\n <p style={{ margin: '10px 0 0', fontSize: 12, lineHeight: 1.5 }}>{line}</p>\n <button\n type=\"button\"\n onClick={pairing.cancel}\n style={{\n marginTop: 10,\n border: 'none',\n background: 'none',\n color: 'inherit',\n opacity: 0.6,\n fontSize: 12,\n cursor: 'pointer',\n textDecoration: 'underline',\n }}\n >\n Cancel\n </button>\n </div>\n );\n};\n\nexport function ZorealLogin(props: ZorealLoginProps) {\n const {\n onSuccess,\n onError,\n containerProps,\n type = 'standard',\n theme = 'filled',\n size = 'large',\n text = 'continue_with',\n shape = 'rectangular',\n logo_alignment = 'left',\n width,\n click_listener,\n ...request\n } = props;\n\n const { login, internals } = useZorealFlow({\n ...request,\n flow: 'browser-direct',\n onCredential: onSuccess,\n onError: (e) => onError?.({ type: 'unknown', description: e.description ?? e.error }),\n onNonOAuthError: (e: NonOAuthError) => onError?.(e),\n });\n\n const s = SIZES[size];\n const style: CSSProperties = useMemo(\n () => ({\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: logo_alignment === 'center' ? 'center' : 'flex-start',\n gap: 10,\n height: s.height,\n padding: `0 ${s.pad}px`,\n width,\n fontSize: s.font,\n fontFamily: 'inherit',\n fontWeight: 500,\n cursor: 'pointer',\n borderRadius: shape === 'pill' ? s.height / 2 : shape === 'square' ? 4 : 8,\n ...(theme === 'outline'\n ? { background: 'transparent', color: 'inherit', border: '1px solid rgba(128,128,128,0.5)' }\n : theme === 'filled_black'\n ? { background: '#111', color: '#fff', border: '1px solid #111' }\n : { background: '#00b4d9', color: '#fff', border: '1px solid #00b4d9' }),\n }),\n [logo_alignment, s, shape, theme, width]\n );\n\n return (\n <div {...containerProps}>\n <button\n type=\"button\"\n style={style}\n onClick={() => {\n click_listener?.();\n login();\n }}\n >\n <Mark size={Math.round(s.font * 1.25)} />\n {type === 'standard' && TEXTS[text]}\n </button>\n {internals.pairing && !internals.pairing.appLink && (\n <PairingPanel pairing={internals.pairing} />\n )}\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useZorealOAuth } from './context';\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 type {\n AcrValue,\n AuthCodeFlowOptions,\n BrowserDirectFlowOptions,\n ErrorCode,\n NonOAuthError,\n PairingState,\n SelectBy,\n ZorealCodeResponse,\n ZorealCredentialResponse,\n ZorealLoginRequestOptions,\n} from './types';\n\nexport interface ActivePairing {\n requestId: string;\n pairUrl: string;\n qrUrl: string;\n state: PairingState;\n /** True when display resolved to the app link rather than the QR. */\n appLink: boolean;\n cancel: () => void;\n}\n\ninterface FlowInternals {\n /** Non-null while a pairing is on screen. ZorealLogin renders from this. */\n pairing: ActivePairing | null;\n}\n\n/**\n * The internal option shape: one flow discriminator, one success callback per\n * mode. The public API keeps Google's single overloaded onSuccess; this type\n * exists because an intersection of those two signatures is uninhabitable, and\n * the mapping from public to internal happens once, in useZorealLogin.\n */\nexport interface InternalFlowOptions extends ZorealLoginRequestOptions {\n flow: 'browser-direct' | 'auth-code';\n redirect_uri?: string;\n onCredential?: (response: ZorealCredentialResponse) => void;\n onCode?: (response: ZorealCodeResponse) => void;\n onError?: (error: Pick<NonOAuthError, 'description'> & { error: ErrorCode }) => void;\n onNonOAuthError?: (error: NonOAuthError) => void;\n}\n\n/**\n * The one flow, shared by the hook and the button. Starts a pairing, exposes\n * it for rendering, polls, and finishes per mode: browser-direct exchanges the\n * code here (public client, PKCE, no secret) and hands over an ID token;\n * auth-code hands the code and the PKCE verifier to the caller, whose backend\n * does the exchange with its client authentication.\n */\nexport function useZorealFlow(options: InternalFlowOptions): {\n login: () => void;\n internals: FlowInternals;\n} {\n const { clientId, issuer, locale } = useZorealOAuth();\n const [pairing, setPairing] = useState<ActivePairing | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n // A component unmounting mid-login must stop the poll: the provider cancels\n // over-polled requests, and an orphaned interval is exactly how one happens.\n useEffect(() => () => abortRef.current?.abort(), []);\n\n const login = useCallback(() => {\n const opts = optionsRef.current;\n const run = async () => {\n abortRef.current?.abort();\n const controller = new AbortController();\n abortRef.current = controller;\n\n const flow = opts.flow;\n const verifier = generateVerifier();\n const state = generateState();\n const nonce = generateState();\n\n try {\n const started = await startPairing(issuer, {\n client_id: clientId,\n scope: opts.scope ?? 'openid',\n state,\n nonce,\n code_challenge: await challengeS256(verifier),\n redirect_uri: flow === 'auth-code' ? opts.redirect_uri : undefined,\n acr_values: Array.isArray(opts.acr_values)\n ? opts.acr_values.join(' ')\n : opts.acr_values,\n max_age: opts.max_age,\n prompt: opts.prompt,\n 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 opts.display === 'link' || (opts.display !== 'qr' && isMobileUserAgent());\n selectBy = useAppLink ? 'app_link' : 'qr';\n\n const active: ActivePairing = {\n requestId: started.request_id,\n pairUrl: started.pair_url,\n qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,\n state: { status: 'pending', expiresIn: started.expires_in },\n appLink: useAppLink,\n cancel: () => {\n controller.abort();\n setPairing(null);\n },\n };\n setPairing(active);\n\n if (useAppLink) {\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 (02 sections 3 and 4). A popup here would be blocked more\n // often than it would help.\n window.location.assign(started.pair_url);\n }\n\n code = await pollUntilApproved(\n issuer,\n started.request_id,\n (s) => {\n setPairing((p) => (p && p.requestId === started.request_id ? { ...p, state: s } : p));\n opts.onPairingStateChange?.(s);\n },\n controller.signal\n );\n }\n\n setPairing(null);\n\n if (flow === 'auth-code') {\n opts.onCode?.({\n code,\n scope: opts.scope ?? 'openid',\n app_state: opts.app_state,\n code_verifier: verifier,\n });\n return;\n }\n\n const tokens = await exchangeCode(issuer, {\n code,\n code_verifier: verifier,\n client_id: clientId,\n });\n const claims = unsafeClaims(tokens.id_token);\n const response: ZorealCredentialResponse = {\n credential: tokens.id_token,\n clientId,\n select_by: selectBy,\n acr: (claims.acr as AcrValue) ?? 'zoreal.device',\n };\n opts.onCredential?.(response);\n } catch (e) {\n setPairing(null);\n if (e instanceof DOMException && e.name === 'AbortError') return;\n if (e instanceof FlowAbandonedError) {\n opts.onNonOAuthError?.(e.reason);\n return;\n }\n if (e instanceof OAuthFlowError) {\n opts.onError?.({ error: e.error, description: e.description });\n return;\n }\n opts.onNonOAuthError?.({\n type: 'unknown',\n description: e instanceof Error ? e.message : String(e),\n });\n }\n };\n void run();\n }, [clientId, issuer, locale]);\n\n return { login, internals: { pairing } };\n}\n\nexport function useZorealLogin(\n options: { flow?: 'browser-direct' } & BrowserDirectFlowOptions\n): () => void;\nexport function useZorealLogin(options: { flow: 'auth-code' } & AuthCodeFlowOptions): () => void;\nexport function useZorealLogin(\n options: ({ flow?: 'browser-direct' | 'auth-code' } & ZorealLoginRequestOptions) &\n Partial<Pick<AuthCodeFlowOptions, 'redirect_uri' | 'ux_mode'>> & {\n onSuccess?: (response: never) => void;\n onError?: (error: Pick<NonOAuthError, 'description'> & { error: ErrorCode }) => void;\n onNonOAuthError?: (error: NonOAuthError) => void;\n }\n): () => void {\n if (options.ux_mode === 'redirect') {\n // v1 supports the popup shape only: the code and PKCE verifier go to your\n // onSuccess and from there to your backend over TLS. A redirect would have\n // to 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-react: ux_mode 'redirect' is not supported in v1. Use the default \" +\n \"'popup' shape and post the code and code_verifier from onSuccess to your backend.\"\n );\n }\n const flow = options.flow ?? 'browser-direct';\n return useZorealFlow({\n ...options,\n flow,\n onCredential:\n flow === 'browser-direct'\n ? (options.onSuccess as unknown as (r: ZorealCredentialResponse) => void)\n : undefined,\n onCode:\n flow === 'auth-code'\n ? (options.onSuccess as unknown as (r: ZorealCodeResponse) => void)\n : undefined,\n }).login;\n}\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 (02), 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 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_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: `@zoreal/oauth2-react/${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 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 '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. There is no plain fallback and there must never\n * be one; a provider seeing 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","import { useEffect, useRef } from 'react';\nimport { useZorealFlow } from './useZorealLogin';\nimport type { UseZorealAutoLoginOptions } from './types';\n\n/**\n * Silent re-auth with prompt=none. NOT One Tap and never could be: there is no\n * ZOREAL session cookie in the browser to read, the credential is on a phone.\n * This succeeds only for a returning user at a consented sector with a live\n * session, the resulting acr is zoreal.session with an empty amr, and a\n * relying party that needs a live human must not build on this hook.\n *\n * PRIVACY NOTE: mounting this on a page sends a request to ZOREAL on page\n * load, before the user does anything. The hook is therefore conservative: it\n * fires once per mount, never retries, and does nothing when `disabled`.\n */\nexport function useZorealAutoLogin(options: UseZorealAutoLoginOptions): void {\n const { login } = useZorealFlow({\n flow: 'browser-direct',\n scope: options.scope,\n prompt: 'none',\n onCredential: options.onSuccess,\n onError: (e) => {\n // The provider's honest answer when no silent session exists (the\n // common case): unavailable, not an error, and never surfaced.\n const quiet = ['login_required', 'consent_required', 'interaction_required'];\n if (quiet.includes(e.error)) {\n options.onUnavailable?.();\n } else {\n options.onError?.({ type: 'unknown', description: e.description ?? e.error });\n }\n },\n onNonOAuthError: (e) => options.onError?.(e),\n });\n\n const fired = useRef(false);\n useEffect(() => {\n if (options.disabled || fired.current) return;\n fired.current = true;\n login();\n }, [options.disabled, login]);\n}\n","/**\n * Clears SDK-held local state. Named for parity with googleLogout and, like\n * it, LOCAL ONLY: it does not end the holder's ZOREAL session, which lives on\n * their phone and at the provider. A relying party that believes this signs\n * the user out of ZOREAL has a security misunderstanding, not a naming\n * complaint. The relying party's own session is the relying\n * party's to end.\n *\n * The SDK deliberately persists nothing (no localStorage, no cookies), so\n * today this has nothing to clear and exists as the stable API surface for a\n * future that does.\n */\nexport function zorealLogout(): void {\n // Intentionally empty until the SDK holds state worth clearing.\n}\n","import type { ZorealCodeResponse } from './types';\n\n/** Mirrors hasGrantedAllScopesGoogle for name-for-name portability. */\nexport function hasGrantedAllScopesZoreal(\n response: Pick<ZorealCodeResponse, 'scope'>,\n firstScope: string,\n ...restScopes: string[]\n): boolean {\n const granted = new Set((response.scope ?? '').split(/\\s+/).filter(Boolean));\n return [firstScope, ...restScopes].every((s) => granted.has(s));\n}\n\nexport function hasGrantedAnyScopeZoreal(\n response: Pick<ZorealCodeResponse, 'scope'>,\n firstScope: string,\n ...restScopes: string[]\n): boolean {\n const granted = new Set((response.scope ?? '').split(/\\s+/).filter(Boolean));\n return [firstScope, ...restScopes].some((s) => granted.has(s));\n}\n"],"mappings":"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","names":["useMemo","jsx","useMemo","useEffect","useRef","useRef","useEffect"]}
package/package.json CHANGED
@@ -1,14 +1,22 @@
1
1
  {
2
2
  "name": "@zoreal/oauth2-react",
3
- "version": "0.1.0",
4
- "description": "Login with ZOREAL for React. A chip-verified human behind every sign-in.",
3
+ "version": "0.1.2",
4
+ "description": "Login with ZOREAL for React. A ZOREAL Verified Proof-of-Human behind every sign-in.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/Bynn-Intelligence/zoreal-oauth2-react.git"
9
9
  },
10
10
  "homepage": "https://zoreal.com",
11
- "keywords": ["zoreal", "oauth2", "oidc", "react", "login", "identity", "liveness"],
11
+ "keywords": [
12
+ "zoreal",
13
+ "oauth2",
14
+ "oidc",
15
+ "react",
16
+ "login",
17
+ "identity",
18
+ "liveness"
19
+ ],
12
20
  "type": "module",
13
21
  "main": "./dist/index.cjs",
14
22
  "module": "./dist/index.js",
@@ -20,7 +28,9 @@
20
28
  "require": "./dist/index.cjs"
21
29
  }
22
30
  },
23
- "files": ["dist"],
31
+ "files": [
32
+ "dist"
33
+ ],
24
34
  "sideEffects": false,
25
35
  "scripts": {
26
36
  "build": "tsup",