@zoreal/oauth2-react 0.1.2 → 0.1.5
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 +122 -26
- package/dist/index.cjs +53 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +53 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,25 +34,59 @@ npm install @zoreal/oauth2-react
|
|
|
34
34
|
## Quick start: auth-code (email and name, needs your backend)
|
|
35
35
|
|
|
36
36
|
```tsx
|
|
37
|
+
import { useState } from 'react';
|
|
37
38
|
import { useZorealLogin } from '@zoreal/oauth2-react';
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
function ZorealSignIn() {
|
|
41
|
+
const [pairing, setPairing] = useState(null);
|
|
42
|
+
|
|
43
|
+
// `email` (and profile.name, etc.) are returned from /userinfo on your backend.
|
|
44
|
+
const login = useZorealLogin({
|
|
45
|
+
flow: 'auth-code',
|
|
46
|
+
scope: 'openid email profile.name',
|
|
47
|
+
onPairingStateChange: setPairing,
|
|
48
|
+
onSuccess: async ({ code, code_verifier, nonce }) => {
|
|
49
|
+
setPairing(null);
|
|
50
|
+
// Send ALL THREE to your backend over TLS. It calls POST /token with the
|
|
51
|
+
// code and verifier plus its client authentication, verifies the ID
|
|
52
|
+
// token's nonce is this one, then reads the email and name from
|
|
53
|
+
// /userinfo. That is where personal data is delivered.
|
|
54
|
+
await fetch('/api/auth/zoreal', {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ code, code_verifier, nonce }),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div>
|
|
64
|
+
<button onClick={login}>Continue with ZOREAL</button>
|
|
65
|
+
{pairing && !pairing.appLink && ['pending', 'claimed'].includes(pairing.status) && (
|
|
66
|
+
<div>
|
|
67
|
+
<img src={pairing.qrUrl} alt="Log in with ZOREAL" width={200} height={200} />
|
|
68
|
+
<p>
|
|
69
|
+
{pairing.status === 'claimed'
|
|
70
|
+
? 'Approve the login in your ZOREAL ID app.'
|
|
71
|
+
: 'Scan with your phone camera or the ZOREAL ID app.'}
|
|
72
|
+
</p>
|
|
73
|
+
<button onClick={pairing.cancel}>Cancel</button>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
54
79
|
```
|
|
55
80
|
|
|
81
|
+
**The hook renders nothing, so in this flow the pairing UI is yours.** On
|
|
82
|
+
desktop the login cannot complete unless something shows `pairing.qrUrl` (the
|
|
83
|
+
provider-served QR image) for the holder to scan — that is what
|
|
84
|
+
`onPairingStateChange` is for, and since 0.1.4 it carries `pairUrl`, `qrUrl`,
|
|
85
|
+
`appLink` and `cancel` on every callback, including one fired immediately when
|
|
86
|
+
the pairing starts. On a phone (`appLink: true`) the SDK opens the pairing
|
|
87
|
+
link itself and no panel is needed. Only the drop-in `<ZorealLogin>` button
|
|
88
|
+
below renders its own QR panel, and that component is browser-direct only.
|
|
89
|
+
|
|
56
90
|
## Quick start: the button (no backend, pseudonymous)
|
|
57
91
|
|
|
58
92
|
```tsx
|
|
@@ -77,8 +111,17 @@ way your page just receives `onSuccess`.
|
|
|
77
111
|
|
|
78
112
|
### On your backend
|
|
79
113
|
|
|
80
|
-
|
|
81
|
-
|
|
114
|
+
There is a sibling library for every major backend — use one instead of
|
|
115
|
+
hand-rolling the exchange (the family table at the bottom lists them all).
|
|
116
|
+
With the Ruby one, the whole thing is:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
login = ZOREAL_OAUTH.authenticate(code:, code_verifier:, nonce:)
|
|
120
|
+
login.sub # your stable account key
|
|
121
|
+
login.email # from /userinfo, with the email scope
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
On the wire, what every one of them does:
|
|
82
125
|
|
|
83
126
|
```
|
|
84
127
|
POST https://id.zoreal.com/token
|
|
@@ -86,7 +129,7 @@ POST https://id.zoreal.com/token
|
|
|
86
129
|
code=<code>
|
|
87
130
|
code_verifier=<code_verifier>
|
|
88
131
|
client_id=ast_your_asset_id
|
|
89
|
-
<your client authentication> # client secret, or a private_key_jwt assertion
|
|
132
|
+
<your client authentication> # HTTP Basic client secret, or a private_key_jwt assertion
|
|
90
133
|
|
|
91
134
|
-> { "id_token": "...", "access_token": "...", "expires_in": 600 }
|
|
92
135
|
|
|
@@ -96,9 +139,29 @@ GET https://id.zoreal.com/userinfo
|
|
|
96
139
|
-> { "sub": "...", "email": "...", "email_verified": true, "given_name": "...", ... }
|
|
97
140
|
```
|
|
98
141
|
|
|
99
|
-
Which fields come back depends on the scopes the user consented to.
|
|
100
|
-
|
|
101
|
-
|
|
142
|
+
Which fields come back depends on the scopes the user consented to.
|
|
143
|
+
|
|
144
|
+
Things a backend implementer needs to know, learned the concrete way:
|
|
145
|
+
|
|
146
|
+
- **The `id_token` is ES256, only ever ES256.** Verify against the JWKS at
|
|
147
|
+
`{issuer}/jwks`; a library defaulting to RS256 rejects every real token.
|
|
148
|
+
- **Check `iss` by exact string comparison** against your configured issuer,
|
|
149
|
+
`aud` against your client id, `exp`, and **the `nonce` your frontend posted**
|
|
150
|
+
against the token's nonce claim — without that last check you cannot tell a
|
|
151
|
+
substituted token from the real one, which is why `onSuccess` hands the
|
|
152
|
+
nonce over.
|
|
153
|
+
- **The ID token carries no personal data, by design.** Email and profile
|
|
154
|
+
claims exist only at `/userinfo`. An integration that only decodes the ID
|
|
155
|
+
token and looks for an email finds none, and that is not a bug.
|
|
156
|
+
- **The access token lives 10 minutes.** Call `/userinfo` while handling the
|
|
157
|
+
login; do not store the token.
|
|
158
|
+
- **`sub` is pairwise per verified domain.** It is the right account key, and
|
|
159
|
+
changing your asset's domain rotates every `sub` you have stored — treat a
|
|
160
|
+
domain change as a data migration, not a settings edit.
|
|
161
|
+
- **The provider publishes no `authorization_endpoint`.** The flow starts at
|
|
162
|
+
`/pair` (this SDK's job) and finishes at `/token` (your backend's); a
|
|
163
|
+
generic OIDC relying-party library that wants to build an authorize URL has
|
|
164
|
+
nothing to point at. Use the family libraries.
|
|
102
165
|
|
|
103
166
|
`ux_mode: 'redirect'` is not supported in v1: it would put the PKCE verifier
|
|
104
167
|
in a URL, which is a credential in every access log on the path.
|
|
@@ -110,14 +173,14 @@ Request scopes in the `scope` string, space-separated, always starting with
|
|
|
110
173
|
|
|
111
174
|
| Scope | Returns | Delivered in | Needs |
|
|
112
175
|
|---|---|---|---|
|
|
113
|
-
| `openid` | `sub` (stable per-user id) and a
|
|
176
|
+
| `openid` | `sub` (stable per-user id) and a proof-of-human summary: that this is a live, real, unique person verified by ZOREAL, when they were verified, and how strongly | ID token | nothing |
|
|
114
177
|
| `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
178
|
| `zoreal.nationality` | `nationality` (ISO 3166-1 alpha-3) | ID token | nothing |
|
|
116
179
|
| `email` | `email`, `email_verified` | `/userinfo` | verified domain, confidential client |
|
|
117
180
|
| `profile.name` | `name`, `given_name`, `family_name` | `/userinfo` | verified domain, confidential client |
|
|
118
181
|
| `profile.birthdate` | `birthdate` (full date) | `/userinfo` | verified domain, confidential client |
|
|
119
182
|
| `profile.document` | `document_type`, `document_number`, `issuing_country`, `document_expires_on` | `/userinfo` | verified domain, confidential client |
|
|
120
|
-
| `profile.portrait` | `portrait`, the
|
|
183
|
+
| `profile.portrait` | `portrait`, the person's verified identity photo. Biometric data: requesting it makes you responsible for it under GDPR Article 9 and similar laws | `/userinfo` | verified domain, confidential client |
|
|
121
184
|
|
|
122
185
|
The first three are available to any client and ride in the ID token, so the
|
|
123
186
|
no-backend button can use them. Everything else is personal data: it is served
|
|
@@ -144,9 +207,9 @@ const login = useZorealLogin({
|
|
|
144
207
|
```
|
|
145
208
|
|
|
146
209
|
Your backend then holds the name, date of birth, document type and number,
|
|
147
|
-
issuing country and expiry, all
|
|
148
|
-
|
|
149
|
-
check. You remain the controller of that data and responsible for how you
|
|
210
|
+
issuing country and expiry, all verified at enrolment against a genuine
|
|
211
|
+
government identity document by a live person, plus the portrait if you
|
|
212
|
+
requested it. That is the raw material for a KYC check. You remain the controller of that data and responsible for how you
|
|
150
213
|
store, use, and justify collecting it; ZOREAL provides the verified attributes,
|
|
151
214
|
not a regulatory determination.
|
|
152
215
|
|
|
@@ -174,6 +237,39 @@ runtime dependencies. Two things touch the network, both on the ZOREAL origin:
|
|
|
174
237
|
no "verified human" button text and there will not be one. The assertion
|
|
175
238
|
lives in the token, where it is verifiable.
|
|
176
239
|
|
|
240
|
+
## Registration and development
|
|
241
|
+
|
|
242
|
+
The client id is the asset token from the ZOREAL dashboard (the asset's OAuth2
|
|
243
|
+
tab). Personal-data scopes need a confidential client on a verified domain;
|
|
244
|
+
the pairing start and its poll are checked against the client's authorized
|
|
245
|
+
JavaScript origins, so register the exact origins your pages run on. A
|
|
246
|
+
sandbox-environment client additionally accepts any `http://localhost` origin,
|
|
247
|
+
which is what makes local development work without registering every port.
|
|
248
|
+
|
|
249
|
+
Point the provider at a non-production issuer with the `issuer` prop; the
|
|
250
|
+
value must match the `iss` inside the tokens exactly.
|
|
251
|
+
|
|
252
|
+
## The ZOREAL OAuth2 library family
|
|
253
|
+
|
|
254
|
+
| Repository | Package | Role |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| zoreal-oauth2-react | @zoreal/oauth2-react (npm) | React frontend: the button, the QR, the polling |
|
|
257
|
+
| zoreal-oauth2-js | @zoreal/oauth2-js (npm) | Framework-free browser core |
|
|
258
|
+
| zoreal-oauth2-react-native | @zoreal/oauth2-react-native (npm) | React Native frontend |
|
|
259
|
+
| zoreal-oauth2-node | @zoreal/oauth2-node (npm) | Node.js backend |
|
|
260
|
+
| zoreal-oauth2-ruby | zoreal-oauth2 (RubyGems) | Ruby backend |
|
|
261
|
+
| zoreal-oauth2-python | zoreal-oauth2 (PyPI) | Python backend |
|
|
262
|
+
| zoreal-oauth2-php | zoreal/oauth2 (Packagist) | PHP backend |
|
|
263
|
+
| zoreal-oauth2-go | github.com/Bynn-Intelligence/zoreal-oauth2-go | Go backend |
|
|
264
|
+
| zoreal-oauth2-java | com.zoreal:oauth2 (Maven Central) | JVM backend |
|
|
265
|
+
| zoreal-oauth2-dotnet | Zoreal.OAuth2 (NuGet) | .NET backend |
|
|
266
|
+
|
|
267
|
+
Every backend library supports all four registered client authentication
|
|
268
|
+
methods: `none` (public client, PKCE alone), `client_secret_basic`,
|
|
269
|
+
`private_key_jwt` (a fresh 60-second single-use assertion per exchange), and
|
|
270
|
+
`tls_client_auth` (configurable now; the provider itself does not accept it at
|
|
271
|
+
`/token` yet and answers 501, which the libraries surface rather than hide).
|
|
272
|
+
|
|
177
273
|
## License
|
|
178
274
|
|
|
179
275
|
MIT
|
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.
|
|
40
|
+
var SDK_VERSION = "0.1.5";
|
|
41
41
|
var DEFAULT_ISSUER = "https://id.zoreal.com";
|
|
42
42
|
var POLL_INTERVAL_MS = 2e3;
|
|
43
43
|
var POLL_INTERVAL_ENROLLING_MS = 5e3;
|
|
@@ -129,6 +129,10 @@ async function startPairing(issuer, params) {
|
|
|
129
129
|
return body;
|
|
130
130
|
}
|
|
131
131
|
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
132
|
+
if (signal?.aborted) {
|
|
133
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
132
136
|
const t = setTimeout(resolve, ms);
|
|
133
137
|
signal?.addEventListener("abort", () => {
|
|
134
138
|
clearTimeout(t);
|
|
@@ -162,6 +166,11 @@ async function pollUntilApproved(issuer, requestId, onState, signal) {
|
|
|
162
166
|
throw new FlowAbandonedError({ type: "request_denied", description: body.error_description });
|
|
163
167
|
case "expired":
|
|
164
168
|
throw new FlowAbandonedError({ type: "request_expired", description: body.error_description });
|
|
169
|
+
case "cancelled":
|
|
170
|
+
throw new FlowAbandonedError({
|
|
171
|
+
type: "request_expired",
|
|
172
|
+
description: body.error_description ?? "the provider cancelled the pairing request"
|
|
173
|
+
});
|
|
165
174
|
case "enrolling":
|
|
166
175
|
await sleep(POLL_INTERVAL_ENROLLING_MS, signal);
|
|
167
176
|
break;
|
|
@@ -252,18 +261,26 @@ function useZorealFlow(options) {
|
|
|
252
261
|
} else {
|
|
253
262
|
const useAppLink = opts.display === "link" || opts.display !== "qr" && isMobileUserAgent();
|
|
254
263
|
selectBy = useAppLink ? "app_link" : "qr";
|
|
255
|
-
const
|
|
256
|
-
|
|
264
|
+
const cancel = () => {
|
|
265
|
+
controller.abort();
|
|
266
|
+
setPairing(null);
|
|
267
|
+
};
|
|
268
|
+
const surface = {
|
|
257
269
|
pairUrl: started.pair_url,
|
|
258
270
|
qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,
|
|
259
|
-
state: { status: "pending", expiresIn: started.expires_in },
|
|
260
271
|
appLink: useAppLink,
|
|
261
|
-
cancel
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
272
|
+
cancel
|
|
273
|
+
};
|
|
274
|
+
const active = {
|
|
275
|
+
requestId: started.request_id,
|
|
276
|
+
pairUrl: surface.pairUrl,
|
|
277
|
+
qrUrl: surface.qrUrl,
|
|
278
|
+
state: { status: "pending", expiresIn: started.expires_in, ...surface },
|
|
279
|
+
appLink: useAppLink,
|
|
280
|
+
cancel
|
|
265
281
|
};
|
|
266
282
|
setPairing(active);
|
|
283
|
+
opts.onPairingStateChange?.(active.state);
|
|
267
284
|
if (useAppLink) {
|
|
268
285
|
window.location.assign(started.pair_url);
|
|
269
286
|
}
|
|
@@ -271,8 +288,11 @@ function useZorealFlow(options) {
|
|
|
271
288
|
issuer,
|
|
272
289
|
started.request_id,
|
|
273
290
|
(s) => {
|
|
274
|
-
|
|
275
|
-
|
|
291
|
+
const enriched = { ...s, ...surface };
|
|
292
|
+
setPairing(
|
|
293
|
+
(p) => p && p.requestId === started.request_id ? { ...p, state: enriched } : p
|
|
294
|
+
);
|
|
295
|
+
opts.onPairingStateChange?.(enriched);
|
|
276
296
|
},
|
|
277
297
|
controller.signal
|
|
278
298
|
);
|
|
@@ -283,7 +303,8 @@ function useZorealFlow(options) {
|
|
|
283
303
|
code,
|
|
284
304
|
scope: opts.scope ?? "openid",
|
|
285
305
|
app_state: opts.app_state,
|
|
286
|
-
code_verifier: verifier
|
|
306
|
+
code_verifier: verifier,
|
|
307
|
+
nonce
|
|
287
308
|
});
|
|
288
309
|
return;
|
|
289
310
|
}
|
|
@@ -349,10 +370,27 @@ var SIZES = {
|
|
|
349
370
|
medium: { height: 38, font: 14, pad: 16 },
|
|
350
371
|
small: { height: 32, font: 12, pad: 12 }
|
|
351
372
|
};
|
|
352
|
-
var Mark = ({ size }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
373
|
+
var Mark = ({ size }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
374
|
+
"svg",
|
|
375
|
+
{
|
|
376
|
+
width: size,
|
|
377
|
+
height: size,
|
|
378
|
+
viewBox: "8.4 7.4 62.2 62.2",
|
|
379
|
+
fill: "currentColor",
|
|
380
|
+
fillRule: "evenodd",
|
|
381
|
+
"aria-hidden": true,
|
|
382
|
+
focusable: "false",
|
|
383
|
+
children: [
|
|
384
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M56.1,32.9c.6-3.1-.8-6.4-3.7-8.1l-18-10.4,5.2-3,15.4,8.9c5.4,3.1,7.7,9.6,5.8,15.3-.3.8-.6,1.6-1.1,2.4-.4.7-.9,1.4-1.5,2.1-1.3,1.4-2.9,2.6-4.6,3.3-3.6,1.5-7.9,1.4-11.6-.7l-8.9-5.1c-2.9-1.7-6.5-1.3-8.9.8-.6.6-1.2,1.2-1.7,2-.5.8-.8,1.7-.9,2.5-.6,3.1.9,6.4,3.7,8.1l18,10.4-5.2,3-15.4-8.9c-5.4-3.1-7.7-9.6-5.8-15.3.2-.8.6-1.6,1-2.4.5-.7,1-1.4,1.5-2.1,1.3-1.4,2.9-2.6,4.7-3.3,3.6-1.6,7.8-1.4,11.5.6l8.9,5.2c3,1.7,6.6,1.3,8.9-.8.6-.6,1.2-1.2,1.7-2,.4-.8.7-1.7.9-2.5Z" }),
|
|
385
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M68.7,44.2c-.7,1.2-2.3,1.7-3.5.9-1.3-.7-1.7-2.3-1-3.5.7-1.3,2.3-1.7,3.5-1,1.3.7,1.7,2.3,1,3.6Z" }),
|
|
386
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M25.6,21.3c5.1-.8,10.4,0,15.3,2.9l1.2.7h0c1.2.7,1.6,2.3.9,3.5s-2.3,1.7-3.5.9l-1.2-.7c-4.2-2.4-9.1-3-13.5-1.9-1.6.4-3.1,1.1-4.5,1.9h0c-1.2.7-2.8.3-3.5-1-.7-1.2-.3-2.8.9-3.5,0,0,.1,0,.3-.1.3-.1.6-.4,1-.5,2.1-1.1,4.4-1.7,6.7-2.2Z" }),
|
|
387
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9.6,31.8c.7-1.2,2.4-1.6,3.5-.8,1.2.7,1.6,2.4.8,3.5-.8,1.2-2.4,1.6-3.6.8-1.2-.8-1.5-2.4-.7-3.5Z" }),
|
|
388
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M46.2,30.3c.7-1.3,2.3-1.7,3.5-1,1.2.7,1.7,2.3.9,3.6-.7,1.2-2.3,1.7-3.5.9s-1.7-2.3-.9-3.5Z" }),
|
|
389
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M52.1,54.5c-5,.9-10.4,0-15.3-2.8l-1.2-.7h0c-1.2-.7-1.7-2.3-.9-3.5s2.3-1.7,3.5-.9l1.2.7c4.3,2.4,9.1,3,13.6,1.9,1.6-.4,3.1-1.1,4.5-1.9h0c1.2-.7,2.8-.3,3.5.9.7,1.3.3,2.9-.9,3.6-.1,0-.2,0-.3,0-.4.2-.7.4-1.1.6-2.1,1-4.3,1.7-6.7,2.1Z" }),
|
|
390
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M31.4,45.6c-.7,1.2-2.3,1.7-3.5.9s-1.7-2.3-.9-3.5,2.3-1.7,3.5-.9,1.7,2.3.9,3.5Z" })
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
);
|
|
356
394
|
var PairingPanel = ({ pairing }) => {
|
|
357
395
|
const { status } = pairing.state;
|
|
358
396
|
const line = status === "claimed" ? "Approve the login in your ZOREAL ID app." : status === "enrolling" ? "Finishing enrolment. This screen will continue by itself." : pairing.appLink ? "Continue in the ZOREAL ID app, then return to this tab." : "Scan with your phone camera or the ZOREAL ID app.";
|
package/dist/index.cjs.map
CHANGED
|
@@ -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: 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":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAmE;;;AC8B5D,IAAM,eAAe;AACrB,IAAM,cAAc;AACpB,IAAM,iBAAiB;AAGvB,IAAM,mBAAmB;AAEzB,IAAM,6BAA6B;;;ADNjC;AAZT,IAAM,yBAAqB,4BAA8C,IAAI;AAEtE,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,YAAQ;AAAA,IACZ,OAAO,EAAE,UAAU,QAAQ,OAAO,QAAQ,OAAO,EAAE,GAAG,OAAO;AAAA,IAC7D,CAAC,UAAU,QAAQ,MAAM;AAAA,EAC3B;AACA,SAAO,4CAAC,mBAAmB,UAAnB,EAA4B,OAAe,UAAS;AAC9D;AAEO,SAAS,iBAA0C;AACxD,QAAM,UAAM,yBAAW,kBAAkB;AACzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO;AACT;;;AE3CA,IAAAA,gBAA4C;;;ACA5C,IAAAC,gBAAyD;;;ACWlD,SAAS,aAAa,SAA0C;AACrE,MAAI;AACF,UAAM,UAAU,QAAQ,MAAM,GAAG,EAAE,CAAC,KAAK;AACzC,UAAM,MAAM,QAAQ,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AACxD,UAAM,SAAS,MAAM,IAAI,QAAQ,IAAK,IAAI,SAAS,KAAM,CAAC;AAC1D,WAAO,KAAK;AAAA,MACV,IAAI,YAAY,EAAE,OAAO,WAAW,KAAK,KAAK,MAAM,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;AAAA,IAChF;AAAA,EACF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ACDO,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACxC,YACS,OACA,aACP;AACA,UAAM,eAAe,KAAK;AAHnB;AACA;AAAA,EAGT;AACF;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAmB,QAAuB;AACxC,UAAM,OAAO,eAAe,OAAO,IAAI;AADtB;AAAA,EAEnB;AACF;AAeA,eAAe,UAAU,UAAsD;AAC7E,MAAI;AACF,WAAQ,MAAM,SAAS,KAAK;AAAA,EAC9B,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAsB,aACpB,QACA,QAC4B;AAC5B,QAAM,WAAW,MAAM,MAAM,GAAG,MAAM,SAAS;AAAA,IAC7C,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU;AAAA,MACnB,GAAG;AAAA,MACH,uBAAuB;AAAA,MACvB,cAAc;AAAA,MACd,KAAK,wBAAwB,WAAW;AAAA,IAC1C,CAAC;AAAA,EACH,CAAC;AAED,QAAM,OAAO,MAAM,UAAU,QAAQ;AACrC,MAAI,CAAC,SAAS,IAAI;AAIhB,UAAM,IAAI;AAAA,MACP,KAAK,SAAuB;AAAA,MAC5B,KAAK,qBAAgC,qCAAqC,SAAS,MAAM;AAAA,IAC5F;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,QAAQ,CAAC,IAAY,WACzB,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,QAAM,IAAI,WAAW,SAAS,EAAE;AAChC,UAAQ,iBAAiB,SAAS,MAAM;AACtC,iBAAa,CAAC;AACd,WAAO,IAAI,aAAa,WAAW,YAAY,CAAC;AAAA,EAClD,CAAC;AACH,CAAC;AAOH,eAAsB,kBACpB,QACA,WACA,SACA,QACiB;AACjB,aAAS;AACP,UAAM,WAAW,MAAM,MAAM,GAAG,MAAM,SAAS,mBAAmB,SAAS,CAAC,WAAW;AAAA,MACrF;AAAA,IACF,CAAC;AACD,UAAM,OAAQ,MAAM,UAAU,QAAQ;AAEtC,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACP,KAAK,SAAuB;AAAA,QAC7B,KAAK,qBAAqB,0BAA0B,SAAS,MAAM;AAAA,MACrE;AAAA,IACF;AAEA,cAAU;AAAA,MACR,QAAQ,KAAK;AAAA,MACb,WAAW,KAAK;AAAA,MAChB,mBAAmB,KAAK;AAAA,IAC1B,CAAC;AAED,YAAQ,KAAK,QAAQ;AAAA,MACnB,KAAK;AACH,YAAI,CAAC,KAAK,MAAM;AACd,gBAAM,IAAI,eAAe,gBAAgB,qCAAqC;AAAA,QAChF;AACA,eAAO,KAAK;AAAA,MACd,KAAK;AACH,cAAM,IAAI,mBAAmB,EAAE,MAAM,kBAAkB,aAAa,KAAK,kBAAkB,CAAC;AAAA,MAC9F,KAAK;AACH,cAAM,IAAI,mBAAmB,EAAE,MAAM,mBAAmB,aAAa,KAAK,kBAAkB,CAAC;AAAA,MAC/F,KAAK;AACH,cAAM,MAAM,4BAA4B,MAAM;AAC9C;AAAA,MACF;AACE,cAAM,MAAM,kBAAkB,MAAM;AAAA,IACxC;AAAA,EACF;AACF;AASA,eAAsB,aACpB,QACA,OACwB;AACxB,QAAM,WAAW,MAAM,MAAM,GAAG,MAAM,UAAU;AAAA,IAC9C,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,IAC/D,MAAM,IAAI,gBAAgB;AAAA,MACxB,YAAY;AAAA,MACZ,MAAM,MAAM;AAAA,MACZ,eAAe,MAAM;AAAA,MACrB,WAAW,MAAM;AAAA,IACnB,CAAC;AAAA,EACH,CAAC;AAED,QAAM,OAAQ,MAAM,UAAU,QAAQ;AACtC,MAAI,CAAC,SAAS,MAAM,KAAK,OAAO;AAC9B,UAAM,IAAI;AAAA,MACP,KAAK,SAAuB;AAAA,MAC7B,KAAK,qBAAqB,0BAA0B,SAAS,MAAM;AAAA,IACrE;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,oBAA6B;AAC3C,MAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,SAAO,4BAA4B,KAAK,UAAU,SAAS;AAC7D;;;AC9KA,IAAM,iBAAiB;AAEvB,IAAM,YAAY,CAAC,UACjB,KAAK,OAAO,aAAa,GAAG,KAAK,CAAC,EAC/B,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AAEf,SAAS,mBAA2B;AACzC,QAAM,QAAQ,IAAI,WAAW,cAAc;AAC3C,SAAO,gBAAgB,KAAK;AAC5B,SAAO,UAAU,KAAK;AACxB;AAEA,eAAsB,cAAc,UAAmC;AACrE,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC;AACvF,SAAO,UAAU,IAAI,WAAW,MAAM,CAAC;AACzC;AAEO,SAAS,gBAAwB;AACtC,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,UAAU,KAAK;AACxB;;;AHkCO,SAAS,cAAc,SAG5B;AACA,QAAM,EAAE,UAAU,QAAQ,OAAO,IAAI,eAAe;AACpD,QAAM,CAAC,SAAS,UAAU,QAAI,wBAA+B,IAAI;AACjE,QAAM,eAAW,sBAA+B,IAAI;AACpD,QAAM,iBAAa,sBAAO,OAAO;AACjC,aAAW,UAAU;AAIrB,+BAAU,MAAM,MAAM,SAAS,SAAS,MAAM,GAAG,CAAC,CAAC;AAEnD,QAAM,YAAQ,2BAAY,MAAM;AAC9B,UAAM,OAAO,WAAW;AACxB,UAAM,MAAM,YAAY;AACtB,eAAS,SAAS,MAAM;AACxB,YAAM,aAAa,IAAI,gBAAgB;AACvC,eAAS,UAAU;AAEnB,YAAM,OAAO,KAAK;AAClB,YAAM,WAAW,iBAAiB;AAClC,YAAM,QAAQ,cAAc;AAC5B,YAAM,QAAQ,cAAc;AAE5B,UAAI;AACF,cAAM,UAAU,MAAM,aAAa,QAAQ;AAAA,UACzC,WAAW;AAAA,UACX,OAAO,KAAK,SAAS;AAAA,UACrB;AAAA,UACA;AAAA,UACA,gBAAgB,MAAM,cAAc,QAAQ;AAAA,UAC5C,cAAc,SAAS,cAAc,KAAK,eAAe;AAAA,UACzD,YAAY,MAAM,QAAQ,KAAK,UAAU,IACrC,KAAK,WAAW,KAAK,GAAG,IACxB,KAAK;AAAA,UACT,SAAS,KAAK;AAAA,UACd,QAAQ,KAAK;AAAA,UACb;AAAA,QACF,CAAC;AAED,YAAI;AACJ,YAAI,WAAqB;AAEzB,YAAI,UAAU,SAAS;AAErB,iBAAO,QAAQ;AACf,qBAAW;AAAA,QACb,OAAO;AACL,gBAAM,aACJ,KAAK,YAAY,UAAW,KAAK,YAAY,QAAQ,kBAAkB;AACzE,qBAAW,aAAa,aAAa;AAErC,gBAAM,SAAwB;AAAA,YAC5B,WAAW,QAAQ;AAAA,YACnB,SAAS,QAAQ;AAAA,YACjB,OAAO,GAAG,MAAM,SAAS,mBAAmB,QAAQ,UAAU,CAAC;AAAA,YAC/D,OAAO,EAAE,QAAQ,WAAW,WAAW,QAAQ,WAAW;AAAA,YAC1D,SAAS;AAAA,YACT,QAAQ,MAAM;AACZ,yBAAW,MAAM;AACjB,yBAAW,IAAI;AAAA,YACjB;AAAA,UACF;AACA,qBAAW,MAAM;AAEjB,cAAI,YAAY;AAKd,mBAAO,SAAS,OAAO,QAAQ,QAAQ;AAAA,UACzC;AAEA,iBAAO,MAAM;AAAA,YACX;AAAA,YACA,QAAQ;AAAA,YACR,CAAC,MAAM;AACL,yBAAW,CAAC,MAAO,KAAK,EAAE,cAAc,QAAQ,aAAa,EAAE,GAAG,GAAG,OAAO,EAAE,IAAI,CAAE;AACpF,mBAAK,uBAAuB,CAAC;AAAA,YAC/B;AAAA,YACA,WAAW;AAAA,UACb;AAAA,QACF;AAEA,mBAAW,IAAI;AAEf,YAAI,SAAS,aAAa;AACxB,eAAK,SAAS;AAAA,YACZ;AAAA,YACA,OAAO,KAAK,SAAS;AAAA,YACrB,WAAW,KAAK;AAAA,YAChB,eAAe;AAAA,UACjB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,SAAS,MAAM,aAAa,QAAQ;AAAA,UACxC;AAAA,UACA,eAAe;AAAA,UACf,WAAW;AAAA,QACb,CAAC;AACD,cAAM,SAAS,aAAa,OAAO,QAAQ;AAC3C,cAAM,WAAqC;AAAA,UACzC,YAAY,OAAO;AAAA,UACnB;AAAA,UACA,WAAW;AAAA,UACX,KAAM,OAAO,OAAoB;AAAA,QACnC;AACA,aAAK,eAAe,QAAQ;AAAA,MAC9B,SAAS,GAAG;AACV,mBAAW,IAAI;AACf,YAAI,aAAa,gBAAgB,EAAE,SAAS,aAAc;AAC1D,YAAI,aAAa,oBAAoB;AACnC,eAAK,kBAAkB,EAAE,MAAM;AAC/B;AAAA,QACF;AACA,YAAI,aAAa,gBAAgB;AAC/B,eAAK,UAAU,EAAE,OAAO,EAAE,OAAO,aAAa,EAAE,YAAY,CAAC;AAC7D;AAAA,QACF;AACA,aAAK,kBAAkB;AAAA,UACrB,MAAM;AAAA,UACN,aAAa,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,QACxD,CAAC;AAAA,MACH;AAAA,IACF;AACA,SAAK,IAAI;AAAA,EACX,GAAG,CAAC,UAAU,QAAQ,MAAM,CAAC;AAE7B,SAAO,EAAE,OAAO,WAAW,EAAE,QAAQ,EAAE;AACzC;AAMO,SAAS,eACd,SAMY;AACZ,MAAI,QAAQ,YAAY,YAAY;AAKlC,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,QAAQ;AAC7B,SAAO,cAAc;AAAA,IACnB,GAAG;AAAA,IACH;AAAA,IACA,cACE,SAAS,mBACJ,QAAQ,YACT;AAAA,IACN,QACE,SAAS,cACJ,QAAQ,YACT;AAAA,EACR,CAAC,EAAE;AACL;;;AD3ME,IAAAC,sBAAA;AAfF,IAAM,QAA+D;AAAA,EACnE,eAAe;AAAA,EACf,aAAa;AAAA,EACb,aAAa;AAAA,EACb,QAAQ;AACV;AAEA,IAAM,QAAQ;AAAA,EACZ,OAAO,EAAE,QAAQ,IAAI,MAAM,IAAI,KAAK,GAAG;AAAA,EACvC,QAAQ,EAAE,QAAQ,IAAI,MAAM,IAAI,KAAK,GAAG;AAAA,EACxC,OAAO,EAAE,QAAQ,IAAI,MAAM,IAAI,KAAK,GAAG;AACzC;AAGA,IAAM,OAAO,CAAC,EAAE,KAAK,MACnB,8CAAC,SAAI,OAAO,MAAM,QAAQ,MAAM,SAAQ,aAAY,eAAW,MAAC,WAAU,SACxE;AAAA,+CAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,KAAI,MAAK,QAAO,QAAO,gBAAe,aAAY,OAAM;AAAA,EAClF,6CAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,OAAM,MAAK,gBAAe;AAAA,GACtD;AAGF,IAAM,eAAe,CAAC,EAAE,QAAQ,MAAkC;AAChE,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,OACJ,WAAW,YACP,6CACA,WAAW,cACT,8DACA,QAAQ,UACN,4DACA;AAEV,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAW;AAAA,MACX,OAAO;AAAA,QACL,WAAW;AAAA,QACX,SAAS;AAAA,QACT,OAAO;AAAA,QACP,cAAc;AAAA,QACd,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ,OAAO;AAAA,QACP,WAAW;AAAA,QACX,YAAY;AAAA,MACd;AAAA,MAEC;AAAA,SAAC,QAAQ,WACR;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,QAAQ;AAAA,YACb,KAAK,eAAe,QAAQ,OAAO;AAAA,YACnC,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,OAAO,EAAE,SAAS,SAAS,QAAQ,UAAU,cAAc,EAAE;AAAA;AAAA,QAC/D;AAAA,QAEF,6CAAC,OAAE,OAAO,EAAE,QAAQ,YAAY,UAAU,IAAI,YAAY,IAAI,GAAI,gBAAK;AAAA,QACvE;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,QAAQ;AAAA,YACjB,OAAO;AAAA,cACL,WAAW;AAAA,cACX,QAAQ;AAAA,cACR,YAAY;AAAA,cACZ,OAAO;AAAA,cACP,SAAS;AAAA,cACT,UAAU;AAAA,cACV,QAAQ;AAAA,cACR,gBAAgB;AAAA,YAClB;AAAA,YACD;AAAA;AAAA,QAED;AAAA;AAAA;AAAA,EACF;AAEJ;AAEO,SAAS,YAAY,OAAyB;AACnD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,EAAE,OAAO,UAAU,IAAI,cAAc;AAAA,IACzC,GAAG;AAAA,IACH,MAAM;AAAA,IACN,cAAc;AAAA,IACd,SAAS,CAAC,MAAM,UAAU,EAAE,MAAM,WAAW,aAAa,EAAE,eAAe,EAAE,MAAM,CAAC;AAAA,IACpF,iBAAiB,CAAC,MAAqB,UAAU,CAAC;AAAA,EACpD,CAAC;AAED,QAAM,IAAI,MAAM,IAAI;AACpB,QAAM,YAAuB;AAAA,IAC3B,OAAO;AAAA,MACL,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,gBAAgB,mBAAmB,WAAW,WAAW;AAAA,MACzD,KAAK;AAAA,MACL,QAAQ,EAAE;AAAA,MACV,SAAS,KAAK,EAAE,GAAG;AAAA,MACnB;AAAA,MACA,UAAU,EAAE;AAAA,MACZ,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,cAAc,UAAU,SAAS,EAAE,SAAS,IAAI,UAAU,WAAW,IAAI;AAAA,MACzE,GAAI,UAAU,YACV,EAAE,YAAY,eAAe,OAAO,WAAW,QAAQ,kCAAkC,IACzF,UAAU,iBACR,EAAE,YAAY,QAAQ,OAAO,QAAQ,QAAQ,iBAAiB,IAC9D,EAAE,YAAY,WAAW,OAAO,QAAQ,QAAQ,oBAAoB;AAAA,IAC5E;AAAA,IACA,CAAC,gBAAgB,GAAG,OAAO,OAAO,KAAK;AAAA,EACzC;AAEA,SACE,8CAAC,SAAK,GAAG,gBACP;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL;AAAA,QACA,SAAS,MAAM;AACb,2BAAiB;AACjB,gBAAM;AAAA,QACR;AAAA,QAEA;AAAA,uDAAC,QAAK,MAAM,KAAK,MAAM,EAAE,OAAO,IAAI,GAAG;AAAA,UACtC,SAAS,cAAc,MAAM,IAAI;AAAA;AAAA;AAAA,IACpC;AAAA,IACC,UAAU,WAAW,CAAC,UAAU,QAAQ,WACvC,6CAAC,gBAAa,SAAS,UAAU,SAAS;AAAA,KAE9C;AAEJ;;;AK7JA,IAAAC,gBAAkC;AAe3B,SAAS,mBAAmB,SAA0C;AAC3E,QAAM,EAAE,MAAM,IAAI,cAAc;AAAA,IAC9B,MAAM;AAAA,IACN,OAAO,QAAQ;AAAA,IACf,QAAQ;AAAA,IACR,cAAc,QAAQ;AAAA,IACtB,SAAS,CAAC,MAAM;AAGd,YAAM,QAAQ,CAAC,kBAAkB,oBAAoB,sBAAsB;AAC3E,UAAI,MAAM,SAAS,EAAE,KAAK,GAAG;AAC3B,gBAAQ,gBAAgB;AAAA,MAC1B,OAAO;AACL,gBAAQ,UAAU,EAAE,MAAM,WAAW,aAAa,EAAE,eAAe,EAAE,MAAM,CAAC;AAAA,MAC9E;AAAA,IACF;AAAA,IACA,iBAAiB,CAAC,MAAM,QAAQ,UAAU,CAAC;AAAA,EAC7C,CAAC;AAED,QAAM,YAAQ,sBAAO,KAAK;AAC1B,+BAAU,MAAM;AACd,QAAI,QAAQ,YAAY,MAAM,QAAS;AACvC,UAAM,UAAU;AAChB,UAAM;AAAA,EACR,GAAG,CAAC,QAAQ,UAAU,KAAK,CAAC;AAC9B;;;AC5BO,SAAS,eAAqB;AAErC;;;ACXO,SAAS,0BACd,UACA,eACG,YACM;AACT,QAAM,UAAU,IAAI,KAAK,SAAS,SAAS,IAAI,MAAM,KAAK,EAAE,OAAO,OAAO,CAAC;AAC3E,SAAO,CAAC,YAAY,GAAG,UAAU,EAAE,MAAM,CAAC,MAAM,QAAQ,IAAI,CAAC,CAAC;AAChE;AAEO,SAAS,yBACd,UACA,eACG,YACM;AACT,QAAM,UAAU,IAAI,KAAK,SAAS,SAAS,IAAI,MAAM,KAAK,EAAE,OAAO,OAAO,CAAC;AAC3E,SAAO,CAAC,YAAY,GAAG,UAAU,EAAE,KAAK,CAAC,MAAM,QAAQ,IAAI,CAAC,CAAC;AAC/D;","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.5';\nexport const DEFAULT_ISSUER = 'https://id.zoreal.com';\n\n/** Pending TTL is short. Poll gently; over-polling cancels the request. */\nexport const POLL_INTERVAL_MS = 2000;\n/** Enrolling extends the window well beyond a normal login; poll slower. */\nexport const POLL_INTERVAL_ENROLLING_MS = 5000;\n\nexport interface PairCreated {\n request_id: string;\n /** https://zoreal.com/qr/<request_id>. The same URL in QR and app link. */\n pair_url: string;\n expires_in: number;\n}\n\nexport interface PairImmediate {\n /** prompt=none resolved silently: consented sector, live session. */\n code: string;\n}\n\nexport type PairStartResponse = PairCreated | PairImmediate;\n\nexport interface PairStatusResponse {\n status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'cancelled' | 'enrolling';\n code?: string;\n expires_in?: number;\n enrolment_deadline?: number;\n /** The provider's reason on denial or refusal. Surfaced verbatim, never rewritten. */\n error?: string;\n error_description?: string;\n}\n\nexport interface TokenResponse {\n id_token: string;\n access_token?: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n","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 ZOREAL mark, in currentColor so every theme carries it correctly. */\nconst Mark = ({ size }: { size: number }) => (\n <svg\n width={size}\n height={size}\n viewBox=\"8.4 7.4 62.2 62.2\"\n fill=\"currentColor\"\n fillRule=\"evenodd\"\n aria-hidden\n focusable=\"false\"\n >\n <path d=\"M56.1,32.9c.6-3.1-.8-6.4-3.7-8.1l-18-10.4,5.2-3,15.4,8.9c5.4,3.1,7.7,9.6,5.8,15.3-.3.8-.6,1.6-1.1,2.4-.4.7-.9,1.4-1.5,2.1-1.3,1.4-2.9,2.6-4.6,3.3-3.6,1.5-7.9,1.4-11.6-.7l-8.9-5.1c-2.9-1.7-6.5-1.3-8.9.8-.6.6-1.2,1.2-1.7,2-.5.8-.8,1.7-.9,2.5-.6,3.1.9,6.4,3.7,8.1l18,10.4-5.2,3-15.4-8.9c-5.4-3.1-7.7-9.6-5.8-15.3.2-.8.6-1.6,1-2.4.5-.7,1-1.4,1.5-2.1,1.3-1.4,2.9-2.6,4.7-3.3,3.6-1.6,7.8-1.4,11.5.6l8.9,5.2c3,1.7,6.6,1.3,8.9-.8.6-.6,1.2-1.2,1.7-2,.4-.8.7-1.7.9-2.5Z\" />\n <path d=\"M68.7,44.2c-.7,1.2-2.3,1.7-3.5.9-1.3-.7-1.7-2.3-1-3.5.7-1.3,2.3-1.7,3.5-1,1.3.7,1.7,2.3,1,3.6Z\" />\n <path d=\"M25.6,21.3c5.1-.8,10.4,0,15.3,2.9l1.2.7h0c1.2.7,1.6,2.3.9,3.5s-2.3,1.7-3.5.9l-1.2-.7c-4.2-2.4-9.1-3-13.5-1.9-1.6.4-3.1,1.1-4.5,1.9h0c-1.2.7-2.8.3-3.5-1-.7-1.2-.3-2.8.9-3.5,0,0,.1,0,.3-.1.3-.1.6-.4,1-.5,2.1-1.1,4.4-1.7,6.7-2.2Z\" />\n <path d=\"M9.6,31.8c.7-1.2,2.4-1.6,3.5-.8,1.2.7,1.6,2.4.8,3.5-.8,1.2-2.4,1.6-3.6.8-1.2-.8-1.5-2.4-.7-3.5Z\" />\n <path d=\"M46.2,30.3c.7-1.3,2.3-1.7,3.5-1,1.2.7,1.7,2.3.9,3.6-.7,1.2-2.3,1.7-3.5.9s-1.7-2.3-.9-3.5Z\" />\n <path d=\"M52.1,54.5c-5,.9-10.4,0-15.3-2.8l-1.2-.7h0c-1.2-.7-1.7-2.3-.9-3.5s2.3-1.7,3.5-.9l1.2.7c4.3,2.4,9.1,3,13.6,1.9,1.6-.4,3.1-1.1,4.5-1.9h0c1.2-.7,2.8-.3,3.5.9.7,1.3.3,2.9-.9,3.6-.1,0-.2,0-.3,0-.4.2-.7.4-1.1.6-2.1,1-4.3,1.7-6.7,2.1Z\" />\n <path d=\"M31.4,45.6c-.7,1.2-2.3,1.7-3.5.9s-1.7-2.3-.9-3.5,2.3-1.7,3.5-.9,1.7,2.3.9,3.5Z\" />\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 cancel = () => {\n controller.abort();\n setPairing(null);\n };\n // Everything a caller-rendered pairing UI needs, on every state it\n // sees: the QR flow cannot complete unless SOMETHING renders\n // pairUrl, and for the auth-code flow that something is the caller.\n const surface = {\n pairUrl: started.pair_url,\n qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,\n appLink: useAppLink,\n cancel,\n };\n const active: ActivePairing = {\n requestId: started.request_id,\n pairUrl: surface.pairUrl,\n qrUrl: surface.qrUrl,\n state: { status: 'pending', expiresIn: started.expires_in, ...surface },\n appLink: useAppLink,\n cancel,\n };\n setPairing(active);\n // The initial state, immediately: the first poll response is one\n // round-trip away, and a UI that waits for it opens visibly empty.\n opts.onPairingStateChange?.(active.state);\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 const enriched = { ...s, ...surface };\n setPairing((p) =>\n p && p.requestId === started.request_id ? { ...p, state: enriched } : p\n );\n opts.onPairingStateChange?.(enriched);\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 nonce,\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 // An already-aborted signal never fires its abort event, so check first\n // or the sleep runs to term and the poll takes one extra swing.\n if (signal?.aborted) {\n reject(new DOMException('aborted', 'AbortError'));\n return;\n }\n const t = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => {\n clearTimeout(t);\n reject(new DOMException('aborted', 'AbortError'));\n });\n });\n\n/**\n * Polls until the request resolves. Returns the authorization code.\n * Throws FlowAbandonedError for the human outcomes (denied, expired,\n * enrolment abandoned) and OAuthFlowError for protocol ones.\n */\nexport async function pollUntilApproved(\n issuer: string,\n requestId: string,\n onState?: (state: PairingState) => void,\n signal?: AbortSignal\n): Promise<string> {\n for (;;) {\n const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {\n signal,\n });\n const body = (await parseJson(response)) as unknown as PairStatusResponse;\n\n if (!response.ok) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Pairing status failed (${response.status})`\n );\n }\n\n onState?.({\n status: body.status,\n expiresIn: body.expires_in,\n enrolmentDeadline: body.enrolment_deadline,\n });\n\n switch (body.status) {\n case 'approved':\n if (!body.code) {\n throw new OAuthFlowError('server_error', 'approved with no authorization code');\n }\n return body.code;\n case 'denied':\n throw new FlowAbandonedError({ type: 'request_denied', description: body.error_description });\n case 'expired':\n throw new FlowAbandonedError({ type: 'request_expired', description: body.error_description });\n case 'cancelled':\n // The provider cancels an over-polled or abandoned request outright\n // (its pairing rows have a real cancelled state). Before 0.1.4 this\n // fell through to the default branch and polled a dead request\n // forever.\n throw new FlowAbandonedError({\n type: 'request_expired',\n description: body.error_description ?? 'the provider cancelled the pairing request',\n });\n case 'enrolling':\n await sleep(POLL_INTERVAL_ENROLLING_MS, signal);\n break;\n default:\n await sleep(POLL_INTERVAL_MS, signal);\n }\n }\n}\n\n/**\n * The code exchange, browser-direct mode only: a public client, PKCE and no\n * secret. What comes back can only ever be the pseudonymous tier, by\n * construction rather than by rule: personal data lives at /userinfo behind an\n * access token this mode is never issued, because personal-data scopes are\n * refused for public clients at the pairing step.\n */\nexport async function exchangeCode(\n issuer: string,\n input: { code: string; code_verifier: string; client_id: string }\n): Promise<TokenResponse> {\n const response = await fetch(`${issuer}/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: input.code,\n code_verifier: input.code_verifier,\n client_id: input.client_id,\n }),\n });\n\n const body = (await parseJson(response)) as unknown as TokenResponse;\n if (!response.ok || body.error) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Token exchange failed (${response.status})`\n );\n }\n return body;\n}\n\n/** A mobile user agent gets the app link, not a QR of its own screen. */\nexport function isMobileUserAgent(): boolean {\n if (typeof navigator === 'undefined') return false;\n return /android|iphone|ipad|ipod/i.test(navigator.userAgent);\n}\n","/**\n * PKCE, S256 only: mandatory for every client, confidential ones included. 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
|
@@ -36,11 +36,25 @@ type SelectBy = 'qr' | 'app_link' | 'device' | 'session';
|
|
|
36
36
|
/** How the login was actually authenticated. Describes what happened, never what was requested. */
|
|
37
37
|
type AcrValue = 'zoreal.live' | 'zoreal.device' | 'zoreal.session';
|
|
38
38
|
interface PairingState {
|
|
39
|
-
status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'enrolling';
|
|
39
|
+
status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'cancelled' | 'enrolling';
|
|
40
40
|
/** Present while status is 'pending'. Seconds. */
|
|
41
41
|
expiresIn?: number;
|
|
42
42
|
/** Present while status is 'enrolling'. Enrolment extends the window well beyond a normal login. */
|
|
43
43
|
enrolmentDeadline?: number;
|
|
44
|
+
/**
|
|
45
|
+
* The pairing link and its provider-served QR image. Added in 0.1.4, from the
|
|
46
|
+
* first real integration: `ZorealLogin` renders the QR itself, but it is
|
|
47
|
+
* browser-direct only, so an auth-code integration has to render its own
|
|
48
|
+
* pairing UI — and this callback was the documented place to do that while
|
|
49
|
+
* carrying nothing to render. Present on every callback of a QR/link flow.
|
|
50
|
+
*/
|
|
51
|
+
pairUrl?: string;
|
|
52
|
+
/** The provider-served SVG of pairUrl. Put it in an <img>; do not draw your own. */
|
|
53
|
+
qrUrl?: string;
|
|
54
|
+
/** True when the flow resolved to the app link (mobile) rather than a QR. */
|
|
55
|
+
appLink?: boolean;
|
|
56
|
+
/** Abandons this pairing: stops the poll. Wire it to your UI's cancel control. */
|
|
57
|
+
cancel?: () => void;
|
|
44
58
|
}
|
|
45
59
|
interface ZorealLoginRequestOptions {
|
|
46
60
|
/** Defaults to 'openid'. Scopes that return personal data require flow: 'auth-code'. */
|
|
@@ -93,6 +107,14 @@ interface ZorealCodeResponse {
|
|
|
93
107
|
* to YOUR backend over TLS and nowhere else.
|
|
94
108
|
*/
|
|
95
109
|
code_verifier: string;
|
|
110
|
+
/**
|
|
111
|
+
* The nonce the SDK generated for this flow. Added in 0.1.4: the ID token
|
|
112
|
+
* carries it, the SDK generated it, and without handing it over the backend
|
|
113
|
+
* doing the exchange has no way to check the token it receives was minted for
|
|
114
|
+
* this login rather than substituted. Verify it against the ID token's nonce
|
|
115
|
+
* claim, alongside iss, aud and exp. Same travel rule as code_verifier.
|
|
116
|
+
*/
|
|
117
|
+
nonce: string;
|
|
96
118
|
}
|
|
97
119
|
interface BrowserDirectFlowOptions extends ZorealLoginRequestOptions {
|
|
98
120
|
onSuccess?: (response: ZorealCredentialResponse) => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -36,11 +36,25 @@ type SelectBy = 'qr' | 'app_link' | 'device' | 'session';
|
|
|
36
36
|
/** How the login was actually authenticated. Describes what happened, never what was requested. */
|
|
37
37
|
type AcrValue = 'zoreal.live' | 'zoreal.device' | 'zoreal.session';
|
|
38
38
|
interface PairingState {
|
|
39
|
-
status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'enrolling';
|
|
39
|
+
status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'cancelled' | 'enrolling';
|
|
40
40
|
/** Present while status is 'pending'. Seconds. */
|
|
41
41
|
expiresIn?: number;
|
|
42
42
|
/** Present while status is 'enrolling'. Enrolment extends the window well beyond a normal login. */
|
|
43
43
|
enrolmentDeadline?: number;
|
|
44
|
+
/**
|
|
45
|
+
* The pairing link and its provider-served QR image. Added in 0.1.4, from the
|
|
46
|
+
* first real integration: `ZorealLogin` renders the QR itself, but it is
|
|
47
|
+
* browser-direct only, so an auth-code integration has to render its own
|
|
48
|
+
* pairing UI — and this callback was the documented place to do that while
|
|
49
|
+
* carrying nothing to render. Present on every callback of a QR/link flow.
|
|
50
|
+
*/
|
|
51
|
+
pairUrl?: string;
|
|
52
|
+
/** The provider-served SVG of pairUrl. Put it in an <img>; do not draw your own. */
|
|
53
|
+
qrUrl?: string;
|
|
54
|
+
/** True when the flow resolved to the app link (mobile) rather than a QR. */
|
|
55
|
+
appLink?: boolean;
|
|
56
|
+
/** Abandons this pairing: stops the poll. Wire it to your UI's cancel control. */
|
|
57
|
+
cancel?: () => void;
|
|
44
58
|
}
|
|
45
59
|
interface ZorealLoginRequestOptions {
|
|
46
60
|
/** Defaults to 'openid'. Scopes that return personal data require flow: 'auth-code'. */
|
|
@@ -93,6 +107,14 @@ interface ZorealCodeResponse {
|
|
|
93
107
|
* to YOUR backend over TLS and nowhere else.
|
|
94
108
|
*/
|
|
95
109
|
code_verifier: string;
|
|
110
|
+
/**
|
|
111
|
+
* The nonce the SDK generated for this flow. Added in 0.1.4: the ID token
|
|
112
|
+
* carries it, the SDK generated it, and without handing it over the backend
|
|
113
|
+
* doing the exchange has no way to check the token it receives was minted for
|
|
114
|
+
* this login rather than substituted. Verify it against the ID token's nonce
|
|
115
|
+
* claim, alongside iss, aud and exp. Same travel rule as code_verifier.
|
|
116
|
+
*/
|
|
117
|
+
nonce: string;
|
|
96
118
|
}
|
|
97
119
|
interface BrowserDirectFlowOptions extends ZorealLoginRequestOptions {
|
|
98
120
|
onSuccess?: (response: ZorealCredentialResponse) => void;
|
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.
|
|
8
|
+
var SDK_VERSION = "0.1.5";
|
|
9
9
|
var DEFAULT_ISSUER = "https://id.zoreal.com";
|
|
10
10
|
var POLL_INTERVAL_MS = 2e3;
|
|
11
11
|
var POLL_INTERVAL_ENROLLING_MS = 5e3;
|
|
@@ -97,6 +97,10 @@ async function startPairing(issuer, params) {
|
|
|
97
97
|
return body;
|
|
98
98
|
}
|
|
99
99
|
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
100
|
+
if (signal?.aborted) {
|
|
101
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
100
104
|
const t = setTimeout(resolve, ms);
|
|
101
105
|
signal?.addEventListener("abort", () => {
|
|
102
106
|
clearTimeout(t);
|
|
@@ -130,6 +134,11 @@ async function pollUntilApproved(issuer, requestId, onState, signal) {
|
|
|
130
134
|
throw new FlowAbandonedError({ type: "request_denied", description: body.error_description });
|
|
131
135
|
case "expired":
|
|
132
136
|
throw new FlowAbandonedError({ type: "request_expired", description: body.error_description });
|
|
137
|
+
case "cancelled":
|
|
138
|
+
throw new FlowAbandonedError({
|
|
139
|
+
type: "request_expired",
|
|
140
|
+
description: body.error_description ?? "the provider cancelled the pairing request"
|
|
141
|
+
});
|
|
133
142
|
case "enrolling":
|
|
134
143
|
await sleep(POLL_INTERVAL_ENROLLING_MS, signal);
|
|
135
144
|
break;
|
|
@@ -220,18 +229,26 @@ function useZorealFlow(options) {
|
|
|
220
229
|
} else {
|
|
221
230
|
const useAppLink = opts.display === "link" || opts.display !== "qr" && isMobileUserAgent();
|
|
222
231
|
selectBy = useAppLink ? "app_link" : "qr";
|
|
223
|
-
const
|
|
224
|
-
|
|
232
|
+
const cancel = () => {
|
|
233
|
+
controller.abort();
|
|
234
|
+
setPairing(null);
|
|
235
|
+
};
|
|
236
|
+
const surface = {
|
|
225
237
|
pairUrl: started.pair_url,
|
|
226
238
|
qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,
|
|
227
|
-
state: { status: "pending", expiresIn: started.expires_in },
|
|
228
239
|
appLink: useAppLink,
|
|
229
|
-
cancel
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
240
|
+
cancel
|
|
241
|
+
};
|
|
242
|
+
const active = {
|
|
243
|
+
requestId: started.request_id,
|
|
244
|
+
pairUrl: surface.pairUrl,
|
|
245
|
+
qrUrl: surface.qrUrl,
|
|
246
|
+
state: { status: "pending", expiresIn: started.expires_in, ...surface },
|
|
247
|
+
appLink: useAppLink,
|
|
248
|
+
cancel
|
|
233
249
|
};
|
|
234
250
|
setPairing(active);
|
|
251
|
+
opts.onPairingStateChange?.(active.state);
|
|
235
252
|
if (useAppLink) {
|
|
236
253
|
window.location.assign(started.pair_url);
|
|
237
254
|
}
|
|
@@ -239,8 +256,11 @@ function useZorealFlow(options) {
|
|
|
239
256
|
issuer,
|
|
240
257
|
started.request_id,
|
|
241
258
|
(s) => {
|
|
242
|
-
|
|
243
|
-
|
|
259
|
+
const enriched = { ...s, ...surface };
|
|
260
|
+
setPairing(
|
|
261
|
+
(p) => p && p.requestId === started.request_id ? { ...p, state: enriched } : p
|
|
262
|
+
);
|
|
263
|
+
opts.onPairingStateChange?.(enriched);
|
|
244
264
|
},
|
|
245
265
|
controller.signal
|
|
246
266
|
);
|
|
@@ -251,7 +271,8 @@ function useZorealFlow(options) {
|
|
|
251
271
|
code,
|
|
252
272
|
scope: opts.scope ?? "openid",
|
|
253
273
|
app_state: opts.app_state,
|
|
254
|
-
code_verifier: verifier
|
|
274
|
+
code_verifier: verifier,
|
|
275
|
+
nonce
|
|
255
276
|
});
|
|
256
277
|
return;
|
|
257
278
|
}
|
|
@@ -317,10 +338,27 @@ var SIZES = {
|
|
|
317
338
|
medium: { height: 38, font: 14, pad: 16 },
|
|
318
339
|
small: { height: 32, font: 12, pad: 12 }
|
|
319
340
|
};
|
|
320
|
-
var Mark = ({ size }) => /* @__PURE__ */ jsxs(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
341
|
+
var Mark = ({ size }) => /* @__PURE__ */ jsxs(
|
|
342
|
+
"svg",
|
|
343
|
+
{
|
|
344
|
+
width: size,
|
|
345
|
+
height: size,
|
|
346
|
+
viewBox: "8.4 7.4 62.2 62.2",
|
|
347
|
+
fill: "currentColor",
|
|
348
|
+
fillRule: "evenodd",
|
|
349
|
+
"aria-hidden": true,
|
|
350
|
+
focusable: "false",
|
|
351
|
+
children: [
|
|
352
|
+
/* @__PURE__ */ jsx2("path", { d: "M56.1,32.9c.6-3.1-.8-6.4-3.7-8.1l-18-10.4,5.2-3,15.4,8.9c5.4,3.1,7.7,9.6,5.8,15.3-.3.8-.6,1.6-1.1,2.4-.4.7-.9,1.4-1.5,2.1-1.3,1.4-2.9,2.6-4.6,3.3-3.6,1.5-7.9,1.4-11.6-.7l-8.9-5.1c-2.9-1.7-6.5-1.3-8.9.8-.6.6-1.2,1.2-1.7,2-.5.8-.8,1.7-.9,2.5-.6,3.1.9,6.4,3.7,8.1l18,10.4-5.2,3-15.4-8.9c-5.4-3.1-7.7-9.6-5.8-15.3.2-.8.6-1.6,1-2.4.5-.7,1-1.4,1.5-2.1,1.3-1.4,2.9-2.6,4.7-3.3,3.6-1.6,7.8-1.4,11.5.6l8.9,5.2c3,1.7,6.6,1.3,8.9-.8.6-.6,1.2-1.2,1.7-2,.4-.8.7-1.7.9-2.5Z" }),
|
|
353
|
+
/* @__PURE__ */ jsx2("path", { d: "M68.7,44.2c-.7,1.2-2.3,1.7-3.5.9-1.3-.7-1.7-2.3-1-3.5.7-1.3,2.3-1.7,3.5-1,1.3.7,1.7,2.3,1,3.6Z" }),
|
|
354
|
+
/* @__PURE__ */ jsx2("path", { d: "M25.6,21.3c5.1-.8,10.4,0,15.3,2.9l1.2.7h0c1.2.7,1.6,2.3.9,3.5s-2.3,1.7-3.5.9l-1.2-.7c-4.2-2.4-9.1-3-13.5-1.9-1.6.4-3.1,1.1-4.5,1.9h0c-1.2.7-2.8.3-3.5-1-.7-1.2-.3-2.8.9-3.5,0,0,.1,0,.3-.1.3-.1.6-.4,1-.5,2.1-1.1,4.4-1.7,6.7-2.2Z" }),
|
|
355
|
+
/* @__PURE__ */ jsx2("path", { d: "M9.6,31.8c.7-1.2,2.4-1.6,3.5-.8,1.2.7,1.6,2.4.8,3.5-.8,1.2-2.4,1.6-3.6.8-1.2-.8-1.5-2.4-.7-3.5Z" }),
|
|
356
|
+
/* @__PURE__ */ jsx2("path", { d: "M46.2,30.3c.7-1.3,2.3-1.7,3.5-1,1.2.7,1.7,2.3.9,3.6-.7,1.2-2.3,1.7-3.5.9s-1.7-2.3-.9-3.5Z" }),
|
|
357
|
+
/* @__PURE__ */ jsx2("path", { d: "M52.1,54.5c-5,.9-10.4,0-15.3-2.8l-1.2-.7h0c-1.2-.7-1.7-2.3-.9-3.5s2.3-1.7,3.5-.9l1.2.7c4.3,2.4,9.1,3,13.6,1.9,1.6-.4,3.1-1.1,4.5-1.9h0c1.2-.7,2.8-.3,3.5.9.7,1.3.3,2.9-.9,3.6-.1,0-.2,0-.3,0-.4.2-.7.4-1.1.6-2.1,1-4.3,1.7-6.7,2.1Z" }),
|
|
358
|
+
/* @__PURE__ */ jsx2("path", { d: "M31.4,45.6c-.7,1.2-2.3,1.7-3.5.9s-1.7-2.3-.9-3.5,2.3-1.7,3.5-.9,1.7,2.3.9,3.5Z" })
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
);
|
|
324
362
|
var PairingPanel = ({ pairing }) => {
|
|
325
363
|
const { status } = pairing.state;
|
|
326
364
|
const line = status === "claimed" ? "Approve the login in your ZOREAL ID app." : status === "enrolling" ? "Finishing enrolment. This screen will continue by itself." : pairing.appLink ? "Continue in the ZOREAL ID app, then return to this tab." : "Scan with your phone camera or the ZOREAL ID app.";
|
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: 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"]}
|
|
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.5';\nexport const DEFAULT_ISSUER = 'https://id.zoreal.com';\n\n/** Pending TTL is short. Poll gently; over-polling cancels the request. */\nexport const POLL_INTERVAL_MS = 2000;\n/** Enrolling extends the window well beyond a normal login; poll slower. */\nexport const POLL_INTERVAL_ENROLLING_MS = 5000;\n\nexport interface PairCreated {\n request_id: string;\n /** https://zoreal.com/qr/<request_id>. The same URL in QR and app link. */\n pair_url: string;\n expires_in: number;\n}\n\nexport interface PairImmediate {\n /** prompt=none resolved silently: consented sector, live session. */\n code: string;\n}\n\nexport type PairStartResponse = PairCreated | PairImmediate;\n\nexport interface PairStatusResponse {\n status: 'pending' | 'claimed' | 'approved' | 'denied' | 'expired' | 'cancelled' | 'enrolling';\n code?: string;\n expires_in?: number;\n enrolment_deadline?: number;\n /** The provider's reason on denial or refusal. Surfaced verbatim, never rewritten. */\n error?: string;\n error_description?: string;\n}\n\nexport interface TokenResponse {\n id_token: string;\n access_token?: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n error?: string;\n error_description?: string;\n}\n","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 ZOREAL mark, in currentColor so every theme carries it correctly. */\nconst Mark = ({ size }: { size: number }) => (\n <svg\n width={size}\n height={size}\n viewBox=\"8.4 7.4 62.2 62.2\"\n fill=\"currentColor\"\n fillRule=\"evenodd\"\n aria-hidden\n focusable=\"false\"\n >\n <path d=\"M56.1,32.9c.6-3.1-.8-6.4-3.7-8.1l-18-10.4,5.2-3,15.4,8.9c5.4,3.1,7.7,9.6,5.8,15.3-.3.8-.6,1.6-1.1,2.4-.4.7-.9,1.4-1.5,2.1-1.3,1.4-2.9,2.6-4.6,3.3-3.6,1.5-7.9,1.4-11.6-.7l-8.9-5.1c-2.9-1.7-6.5-1.3-8.9.8-.6.6-1.2,1.2-1.7,2-.5.8-.8,1.7-.9,2.5-.6,3.1.9,6.4,3.7,8.1l18,10.4-5.2,3-15.4-8.9c-5.4-3.1-7.7-9.6-5.8-15.3.2-.8.6-1.6,1-2.4.5-.7,1-1.4,1.5-2.1,1.3-1.4,2.9-2.6,4.7-3.3,3.6-1.6,7.8-1.4,11.5.6l8.9,5.2c3,1.7,6.6,1.3,8.9-.8.6-.6,1.2-1.2,1.7-2,.4-.8.7-1.7.9-2.5Z\" />\n <path d=\"M68.7,44.2c-.7,1.2-2.3,1.7-3.5.9-1.3-.7-1.7-2.3-1-3.5.7-1.3,2.3-1.7,3.5-1,1.3.7,1.7,2.3,1,3.6Z\" />\n <path d=\"M25.6,21.3c5.1-.8,10.4,0,15.3,2.9l1.2.7h0c1.2.7,1.6,2.3.9,3.5s-2.3,1.7-3.5.9l-1.2-.7c-4.2-2.4-9.1-3-13.5-1.9-1.6.4-3.1,1.1-4.5,1.9h0c-1.2.7-2.8.3-3.5-1-.7-1.2-.3-2.8.9-3.5,0,0,.1,0,.3-.1.3-.1.6-.4,1-.5,2.1-1.1,4.4-1.7,6.7-2.2Z\" />\n <path d=\"M9.6,31.8c.7-1.2,2.4-1.6,3.5-.8,1.2.7,1.6,2.4.8,3.5-.8,1.2-2.4,1.6-3.6.8-1.2-.8-1.5-2.4-.7-3.5Z\" />\n <path d=\"M46.2,30.3c.7-1.3,2.3-1.7,3.5-1,1.2.7,1.7,2.3.9,3.6-.7,1.2-2.3,1.7-3.5.9s-1.7-2.3-.9-3.5Z\" />\n <path d=\"M52.1,54.5c-5,.9-10.4,0-15.3-2.8l-1.2-.7h0c-1.2-.7-1.7-2.3-.9-3.5s2.3-1.7,3.5-.9l1.2.7c4.3,2.4,9.1,3,13.6,1.9,1.6-.4,3.1-1.1,4.5-1.9h0c1.2-.7,2.8-.3,3.5.9.7,1.3.3,2.9-.9,3.6-.1,0-.2,0-.3,0-.4.2-.7.4-1.1.6-2.1,1-4.3,1.7-6.7,2.1Z\" />\n <path d=\"M31.4,45.6c-.7,1.2-2.3,1.7-3.5.9s-1.7-2.3-.9-3.5,2.3-1.7,3.5-.9,1.7,2.3.9,3.5Z\" />\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 cancel = () => {\n controller.abort();\n setPairing(null);\n };\n // Everything a caller-rendered pairing UI needs, on every state it\n // sees: the QR flow cannot complete unless SOMETHING renders\n // pairUrl, and for the auth-code flow that something is the caller.\n const surface = {\n pairUrl: started.pair_url,\n qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,\n appLink: useAppLink,\n cancel,\n };\n const active: ActivePairing = {\n requestId: started.request_id,\n pairUrl: surface.pairUrl,\n qrUrl: surface.qrUrl,\n state: { status: 'pending', expiresIn: started.expires_in, ...surface },\n appLink: useAppLink,\n cancel,\n };\n setPairing(active);\n // The initial state, immediately: the first poll response is one\n // round-trip away, and a UI that waits for it opens visibly empty.\n opts.onPairingStateChange?.(active.state);\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 const enriched = { ...s, ...surface };\n setPairing((p) =>\n p && p.requestId === started.request_id ? { ...p, state: enriched } : p\n );\n opts.onPairingStateChange?.(enriched);\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 nonce,\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 // An already-aborted signal never fires its abort event, so check first\n // or the sleep runs to term and the poll takes one extra swing.\n if (signal?.aborted) {\n reject(new DOMException('aborted', 'AbortError'));\n return;\n }\n const t = setTimeout(resolve, ms);\n signal?.addEventListener('abort', () => {\n clearTimeout(t);\n reject(new DOMException('aborted', 'AbortError'));\n });\n });\n\n/**\n * Polls until the request resolves. Returns the authorization code.\n * Throws FlowAbandonedError for the human outcomes (denied, expired,\n * enrolment abandoned) and OAuthFlowError for protocol ones.\n */\nexport async function pollUntilApproved(\n issuer: string,\n requestId: string,\n onState?: (state: PairingState) => void,\n signal?: AbortSignal\n): Promise<string> {\n for (;;) {\n const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {\n signal,\n });\n const body = (await parseJson(response)) as unknown as PairStatusResponse;\n\n if (!response.ok) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Pairing status failed (${response.status})`\n );\n }\n\n onState?.({\n status: body.status,\n expiresIn: body.expires_in,\n enrolmentDeadline: body.enrolment_deadline,\n });\n\n switch (body.status) {\n case 'approved':\n if (!body.code) {\n throw new OAuthFlowError('server_error', 'approved with no authorization code');\n }\n return body.code;\n case 'denied':\n throw new FlowAbandonedError({ type: 'request_denied', description: body.error_description });\n case 'expired':\n throw new FlowAbandonedError({ type: 'request_expired', description: body.error_description });\n case 'cancelled':\n // The provider cancels an over-polled or abandoned request outright\n // (its pairing rows have a real cancelled state). Before 0.1.4 this\n // fell through to the default branch and polled a dead request\n // forever.\n throw new FlowAbandonedError({\n type: 'request_expired',\n description: body.error_description ?? 'the provider cancelled the pairing request',\n });\n case 'enrolling':\n await sleep(POLL_INTERVAL_ENROLLING_MS, signal);\n break;\n default:\n await sleep(POLL_INTERVAL_MS, signal);\n }\n }\n}\n\n/**\n * The code exchange, browser-direct mode only: a public client, PKCE and no\n * secret. What comes back can only ever be the pseudonymous tier, by\n * construction rather than by rule: personal data lives at /userinfo behind an\n * access token this mode is never issued, because personal-data scopes are\n * refused for public clients at the pairing step.\n */\nexport async function exchangeCode(\n issuer: string,\n input: { code: string; code_verifier: string; client_id: string }\n): Promise<TokenResponse> {\n const response = await fetch(`${issuer}/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: input.code,\n code_verifier: input.code_verifier,\n client_id: input.client_id,\n }),\n });\n\n const body = (await parseJson(response)) as unknown as TokenResponse;\n if (!response.ok || body.error) {\n throw new OAuthFlowError(\n (body.error as ErrorCode) ?? 'server_error',\n body.error_description ?? `Token exchange failed (${response.status})`\n );\n }\n return body;\n}\n\n/** A mobile user agent gets the app link, not a QR of its own screen. */\nexport function isMobileUserAgent(): boolean {\n if (typeof navigator === 'undefined') return false;\n return /android|iphone|ipad|ipod/i.test(navigator.userAgent);\n}\n","/**\n * PKCE, S256 only: mandatory for every client, confidential ones included. 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"]}
|