@zoreal/oauth2-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bynn Intelligence, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @zoreal/oauth2-react
2
+
3
+ Login with ZOREAL for React: a chip-verified human behind every sign-in.
4
+
5
+ The API mirrors `@react-oauth/google` one to one, renamed, so a team already
6
+ integrated with Google ports by find and replace. The full mapping and every
7
+ design decision live in the specification this package is built against:
8
+ `zoreal/products/oauth2/05-react-sdk.md` in the specification repo.
9
+
10
+ ## Status
11
+
12
+ The package is real; the provider it speaks to is being built. This code pins
13
+ wire protocol v1 and is not yet published to npm. Nothing below works against
14
+ production today, and this section is removed the day that changes.
15
+
16
+ ## Install
17
+
18
+ Not yet published. When it is: `npm install @zoreal/oauth2-react`.
19
+
20
+ ## Quick start: the button (no backend needed)
21
+
22
+ ```tsx
23
+ import { ZorealOAuthProvider, ZorealLogin } from '@zoreal/oauth2-react';
24
+
25
+ <ZorealOAuthProvider clientId="ast_your_asset_id">
26
+ <ZorealLogin
27
+ onSuccess={({ credential }) => {
28
+ // credential is an ID token: a pairwise pseudonymous `sub` plus
29
+ // assurance claims. Verify it server-side against the JWKS before
30
+ // trusting it. It NEVER contains personal data, by construction.
31
+ }}
32
+ onError={(e) => console.warn(e.type, e.description)}
33
+ />
34
+ </ZorealOAuthProvider>
35
+ ```
36
+
37
+ On desktop the button shows a QR; the user scans it with their phone and
38
+ approves in the ZOREAL ID app. On a phone it opens the app directly. Either
39
+ way your page just receives `onSuccess`.
40
+
41
+ ## Quick start: auth-code (personal data, requires your backend)
42
+
43
+ ```tsx
44
+ import { useZorealLogin } from '@zoreal/oauth2-react';
45
+
46
+ const login = useZorealLogin({
47
+ flow: 'auth-code',
48
+ scope: 'openid profile.name',
49
+ onSuccess: async ({ code, code_verifier }) => {
50
+ // Send BOTH to your backend over TLS. Your backend calls POST /token
51
+ // with them plus its client authentication (private_key_jwt, mTLS, or
52
+ // client secret), then reads personal claims from /userinfo.
53
+ await fetch('/api/auth/zoreal', {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({ code, code_verifier }),
57
+ });
58
+ },
59
+ });
60
+ ```
61
+
62
+ `ux_mode: 'redirect'` is not supported in v1: it would put the PKCE verifier
63
+ in a URL, which is a credential in every access log on the path.
64
+
65
+ ## What your page needs to allow
66
+
67
+ The package loads no third-party script, no stylesheet, no font, and has zero
68
+ runtime dependencies. Two things touch the network, both on the ZOREAL origin:
69
+
70
+ | CSP directive | Value | Why |
71
+ |---|---|---|
72
+ | `connect-src` | `https://id.zoreal.com` | starting the pairing, polling it, and (button mode) the code exchange |
73
+ | `img-src` | `https://id.zoreal.com` | the QR image, served by the provider so it stays correct and current |
74
+
75
+ ## The rules this package follows
76
+
77
+ - **No secret has a home here.** The provider takes no `clientSecret` prop and
78
+ never will; a pull request adding one is a security bug regardless of its
79
+ documentation.
80
+ - **The ID token in browser mode carries no personal data.** Personal claims
81
+ exist only at `/userinfo`, behind an access token browser mode is never
82
+ issued for those scopes. There is no configuration that changes this.
83
+ - **Server errors are shown, not rewritten.** Whatever reason the provider
84
+ gives, `description` carries it verbatim.
85
+ - **The button copy is neutral.** "Continue with ZOREAL" and variants; there is
86
+ no "verified human" button text and there will not be one. The assertion
87
+ lives in the token, where it is verifiable.
88
+
89
+ ## License
90
+
91
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,520 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ ZorealLogin: () => ZorealLogin,
25
+ ZorealOAuthProvider: () => ZorealOAuthProvider,
26
+ hasGrantedAllScopesZoreal: () => hasGrantedAllScopesZoreal,
27
+ hasGrantedAnyScopeZoreal: () => hasGrantedAnyScopeZoreal,
28
+ useZorealAutoLogin: () => useZorealAutoLogin,
29
+ useZorealLogin: () => useZorealLogin,
30
+ useZorealOAuth: () => useZorealOAuth,
31
+ zorealLogout: () => zorealLogout
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/context.tsx
36
+ var import_react = require("react");
37
+
38
+ // src/wire.ts
39
+ var WIRE_VERSION = 1;
40
+ var SDK_VERSION = "0.1.0";
41
+ var DEFAULT_ISSUER = "https://id.zoreal.com";
42
+ var POLL_INTERVAL_MS = 2e3;
43
+ var POLL_INTERVAL_ENROLLING_MS = 5e3;
44
+
45
+ // src/context.tsx
46
+ var import_jsx_runtime = require("react/jsx-runtime");
47
+ var ZorealOAuthContext = (0, import_react.createContext)(null);
48
+ function ZorealOAuthProvider({
49
+ clientId,
50
+ issuer = DEFAULT_ISSUER,
51
+ locale,
52
+ children
53
+ }) {
54
+ const value = (0, import_react.useMemo)(
55
+ () => ({ clientId, issuer: issuer.replace(/\/$/, ""), locale }),
56
+ [clientId, issuer, locale]
57
+ );
58
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ZorealOAuthContext.Provider, { value, children });
59
+ }
60
+ function useZorealOAuth() {
61
+ const ctx = (0, import_react.useContext)(ZorealOAuthContext);
62
+ if (!ctx) {
63
+ throw new Error(
64
+ "useZorealOAuth must be used inside <ZorealOAuthProvider clientId=...>. Wrap your app (or the part that logs in) in the provider."
65
+ );
66
+ }
67
+ return ctx;
68
+ }
69
+
70
+ // src/ZorealLogin.tsx
71
+ var import_react3 = require("react");
72
+
73
+ // src/useZorealLogin.ts
74
+ var import_react2 = require("react");
75
+
76
+ // src/jwt.ts
77
+ function unsafeClaims(idToken) {
78
+ try {
79
+ const payload = idToken.split(".")[1] ?? "";
80
+ const b64 = payload.replace(/-/g, "+").replace(/_/g, "/");
81
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
82
+ return JSON.parse(
83
+ new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)))
84
+ );
85
+ } catch {
86
+ return {};
87
+ }
88
+ }
89
+
90
+ // src/pairing.ts
91
+ var OAuthFlowError = class extends Error {
92
+ constructor(error, description) {
93
+ super(description ?? error);
94
+ this.error = error;
95
+ this.description = description;
96
+ }
97
+ };
98
+ var FlowAbandonedError = class extends Error {
99
+ constructor(reason) {
100
+ super(reason.description ?? reason.type);
101
+ this.reason = reason;
102
+ }
103
+ };
104
+ async function parseJson(response) {
105
+ try {
106
+ return await response.json();
107
+ } catch {
108
+ return {};
109
+ }
110
+ }
111
+ async function startPairing(issuer, params) {
112
+ const response = await fetch(`${issuer}/pair`, {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({
116
+ ...params,
117
+ code_challenge_method: "S256",
118
+ wire_version: WIRE_VERSION,
119
+ sdk: `@zoreal/oauth2-react/${SDK_VERSION}`
120
+ })
121
+ });
122
+ const body = await parseJson(response);
123
+ if (!response.ok) {
124
+ throw new OAuthFlowError(
125
+ body.error ?? "server_error",
126
+ body.error_description ?? `The provider refused the request (${response.status})`
127
+ );
128
+ }
129
+ return body;
130
+ }
131
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
132
+ const t = setTimeout(resolve, ms);
133
+ signal?.addEventListener("abort", () => {
134
+ clearTimeout(t);
135
+ reject(new DOMException("aborted", "AbortError"));
136
+ });
137
+ });
138
+ async function pollUntilApproved(issuer, requestId, onState, signal) {
139
+ for (; ; ) {
140
+ const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
141
+ signal
142
+ });
143
+ const body = await parseJson(response);
144
+ if (!response.ok) {
145
+ throw new OAuthFlowError(
146
+ body.error ?? "server_error",
147
+ body.error_description ?? `Pairing status failed (${response.status})`
148
+ );
149
+ }
150
+ onState?.({
151
+ status: body.status,
152
+ expiresIn: body.expires_in,
153
+ enrolmentDeadline: body.enrolment_deadline
154
+ });
155
+ switch (body.status) {
156
+ case "approved":
157
+ if (!body.code) {
158
+ throw new OAuthFlowError("server_error", "approved with no authorization code");
159
+ }
160
+ return body.code;
161
+ case "denied":
162
+ throw new FlowAbandonedError({ type: "request_denied", description: body.error_description });
163
+ case "expired":
164
+ throw new FlowAbandonedError({ type: "request_expired", description: body.error_description });
165
+ case "enrolling":
166
+ await sleep(POLL_INTERVAL_ENROLLING_MS, signal);
167
+ break;
168
+ default:
169
+ await sleep(POLL_INTERVAL_MS, signal);
170
+ }
171
+ }
172
+ }
173
+ async function exchangeCode(issuer, input) {
174
+ const response = await fetch(`${issuer}/token`, {
175
+ method: "POST",
176
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
177
+ body: new URLSearchParams({
178
+ grant_type: "authorization_code",
179
+ code: input.code,
180
+ code_verifier: input.code_verifier,
181
+ client_id: input.client_id
182
+ })
183
+ });
184
+ const body = await parseJson(response);
185
+ if (!response.ok || body.error) {
186
+ throw new OAuthFlowError(
187
+ body.error ?? "server_error",
188
+ body.error_description ?? `Token exchange failed (${response.status})`
189
+ );
190
+ }
191
+ return body;
192
+ }
193
+ function isMobileUserAgent() {
194
+ if (typeof navigator === "undefined") return false;
195
+ return /android|iphone|ipad|ipod/i.test(navigator.userAgent);
196
+ }
197
+
198
+ // src/pkce.ts
199
+ var VERIFIER_BYTES = 32;
200
+ var base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
201
+ function generateVerifier() {
202
+ const bytes = new Uint8Array(VERIFIER_BYTES);
203
+ crypto.getRandomValues(bytes);
204
+ return base64url(bytes);
205
+ }
206
+ async function challengeS256(verifier) {
207
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
208
+ return base64url(new Uint8Array(digest));
209
+ }
210
+ function generateState() {
211
+ const bytes = new Uint8Array(16);
212
+ crypto.getRandomValues(bytes);
213
+ return base64url(bytes);
214
+ }
215
+
216
+ // src/useZorealLogin.ts
217
+ function useZorealFlow(options) {
218
+ const { clientId, issuer, locale } = useZorealOAuth();
219
+ const [pairing, setPairing] = (0, import_react2.useState)(null);
220
+ const abortRef = (0, import_react2.useRef)(null);
221
+ const optionsRef = (0, import_react2.useRef)(options);
222
+ optionsRef.current = options;
223
+ (0, import_react2.useEffect)(() => () => abortRef.current?.abort(), []);
224
+ const login = (0, import_react2.useCallback)(() => {
225
+ const opts = optionsRef.current;
226
+ const run = async () => {
227
+ abortRef.current?.abort();
228
+ const controller = new AbortController();
229
+ abortRef.current = controller;
230
+ const flow = opts.flow;
231
+ const verifier = generateVerifier();
232
+ const state = generateState();
233
+ const nonce = generateState();
234
+ try {
235
+ const started = await startPairing(issuer, {
236
+ client_id: clientId,
237
+ scope: opts.scope ?? "openid",
238
+ state,
239
+ nonce,
240
+ code_challenge: await challengeS256(verifier),
241
+ redirect_uri: flow === "auth-code" ? opts.redirect_uri : void 0,
242
+ acr_values: Array.isArray(opts.acr_values) ? opts.acr_values.join(" ") : opts.acr_values,
243
+ max_age: opts.max_age,
244
+ prompt: opts.prompt,
245
+ locale
246
+ });
247
+ let code;
248
+ let selectBy = "device";
249
+ if ("code" in started) {
250
+ code = started.code;
251
+ selectBy = "session";
252
+ } else {
253
+ const useAppLink = opts.display === "link" || opts.display !== "qr" && isMobileUserAgent();
254
+ selectBy = useAppLink ? "app_link" : "qr";
255
+ const active = {
256
+ requestId: started.request_id,
257
+ pairUrl: started.pair_url,
258
+ qrUrl: `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`,
259
+ state: { status: "pending", expiresIn: started.expires_in },
260
+ appLink: useAppLink,
261
+ cancel: () => {
262
+ controller.abort();
263
+ setPairing(null);
264
+ }
265
+ };
266
+ setPairing(active);
267
+ if (useAppLink) {
268
+ window.location.assign(started.pair_url);
269
+ }
270
+ code = await pollUntilApproved(
271
+ issuer,
272
+ started.request_id,
273
+ (s) => {
274
+ setPairing((p) => p && p.requestId === started.request_id ? { ...p, state: s } : p);
275
+ opts.onPairingStateChange?.(s);
276
+ },
277
+ controller.signal
278
+ );
279
+ }
280
+ setPairing(null);
281
+ if (flow === "auth-code") {
282
+ opts.onCode?.({
283
+ code,
284
+ scope: opts.scope ?? "openid",
285
+ app_state: opts.app_state,
286
+ code_verifier: verifier
287
+ });
288
+ return;
289
+ }
290
+ const tokens = await exchangeCode(issuer, {
291
+ code,
292
+ code_verifier: verifier,
293
+ client_id: clientId
294
+ });
295
+ const claims = unsafeClaims(tokens.id_token);
296
+ const response = {
297
+ credential: tokens.id_token,
298
+ clientId,
299
+ select_by: selectBy,
300
+ acr: claims.acr ?? "zoreal.device"
301
+ };
302
+ opts.onCredential?.(response);
303
+ } catch (e) {
304
+ setPairing(null);
305
+ if (e instanceof DOMException && e.name === "AbortError") return;
306
+ if (e instanceof FlowAbandonedError) {
307
+ opts.onNonOAuthError?.(e.reason);
308
+ return;
309
+ }
310
+ if (e instanceof OAuthFlowError) {
311
+ opts.onError?.({ error: e.error, description: e.description });
312
+ return;
313
+ }
314
+ opts.onNonOAuthError?.({
315
+ type: "unknown",
316
+ description: e instanceof Error ? e.message : String(e)
317
+ });
318
+ }
319
+ };
320
+ void run();
321
+ }, [clientId, issuer, locale]);
322
+ return { login, internals: { pairing } };
323
+ }
324
+ function useZorealLogin(options) {
325
+ if (options.ux_mode === "redirect") {
326
+ throw new Error(
327
+ "@zoreal/oauth2-react: ux_mode 'redirect' is not supported in v1. Use the default 'popup' shape and post the code and code_verifier from onSuccess to your backend."
328
+ );
329
+ }
330
+ const flow = options.flow ?? "browser-direct";
331
+ return useZorealFlow({
332
+ ...options,
333
+ flow,
334
+ onCredential: flow === "browser-direct" ? options.onSuccess : void 0,
335
+ onCode: flow === "auth-code" ? options.onSuccess : void 0
336
+ }).login;
337
+ }
338
+
339
+ // src/ZorealLogin.tsx
340
+ var import_jsx_runtime2 = require("react/jsx-runtime");
341
+ var TEXTS = {
342
+ continue_with: "Continue with ZOREAL",
343
+ signin_with: "Sign in with ZOREAL",
344
+ signup_with: "Sign up with ZOREAL",
345
+ signin: "Sign in"
346
+ };
347
+ var SIZES = {
348
+ large: { height: 44, font: 15, pad: 20 },
349
+ medium: { height: 38, font: 14, pad: 16 },
350
+ small: { height: 32, font: 12, pad: 12 }
351
+ };
352
+ var Mark = ({ size }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: size, height: size, viewBox: "0 0 24 24", "aria-hidden": true, focusable: "false", children: [
353
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "9", fill: "none", stroke: "currentColor", strokeWidth: "2.6" }),
354
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "3.4", fill: "currentColor" })
355
+ ] });
356
+ var PairingPanel = ({ pairing }) => {
357
+ const { status } = pairing.state;
358
+ 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.";
359
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
360
+ "div",
361
+ {
362
+ role: "dialog",
363
+ "aria-label": "Log in with ZOREAL",
364
+ style: {
365
+ marginTop: 8,
366
+ padding: 16,
367
+ width: 232,
368
+ borderRadius: 12,
369
+ border: "1px solid rgba(128,128,128,0.35)",
370
+ background: "Canvas",
371
+ color: "CanvasText",
372
+ textAlign: "center",
373
+ fontFamily: "inherit"
374
+ },
375
+ children: [
376
+ !pairing.appLink && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
377
+ "img",
378
+ {
379
+ src: pairing.qrUrl,
380
+ alt: `QR code for ${pairing.pairUrl}`,
381
+ width: 200,
382
+ height: 200,
383
+ style: { display: "block", margin: "0 auto", borderRadius: 8 }
384
+ }
385
+ ),
386
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { margin: "10px 0 0", fontSize: 12, lineHeight: 1.5 }, children: line }),
387
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
388
+ "button",
389
+ {
390
+ type: "button",
391
+ onClick: pairing.cancel,
392
+ style: {
393
+ marginTop: 10,
394
+ border: "none",
395
+ background: "none",
396
+ color: "inherit",
397
+ opacity: 0.6,
398
+ fontSize: 12,
399
+ cursor: "pointer",
400
+ textDecoration: "underline"
401
+ },
402
+ children: "Cancel"
403
+ }
404
+ )
405
+ ]
406
+ }
407
+ );
408
+ };
409
+ function ZorealLogin(props) {
410
+ const {
411
+ onSuccess,
412
+ onError,
413
+ containerProps,
414
+ type = "standard",
415
+ theme = "filled",
416
+ size = "large",
417
+ text = "continue_with",
418
+ shape = "rectangular",
419
+ logo_alignment = "left",
420
+ width,
421
+ click_listener,
422
+ ...request
423
+ } = props;
424
+ const { login, internals } = useZorealFlow({
425
+ ...request,
426
+ flow: "browser-direct",
427
+ onCredential: onSuccess,
428
+ onError: (e) => onError?.({ type: "unknown", description: e.description ?? e.error }),
429
+ onNonOAuthError: (e) => onError?.(e)
430
+ });
431
+ const s = SIZES[size];
432
+ const style = (0, import_react3.useMemo)(
433
+ () => ({
434
+ display: "inline-flex",
435
+ alignItems: "center",
436
+ justifyContent: logo_alignment === "center" ? "center" : "flex-start",
437
+ gap: 10,
438
+ height: s.height,
439
+ padding: `0 ${s.pad}px`,
440
+ width,
441
+ fontSize: s.font,
442
+ fontFamily: "inherit",
443
+ fontWeight: 500,
444
+ cursor: "pointer",
445
+ borderRadius: shape === "pill" ? s.height / 2 : shape === "square" ? 4 : 8,
446
+ ...theme === "outline" ? { background: "transparent", color: "inherit", border: "1px solid rgba(128,128,128,0.5)" } : theme === "filled_black" ? { background: "#111", color: "#fff", border: "1px solid #111" } : { background: "#00b4d9", color: "#fff", border: "1px solid #00b4d9" }
447
+ }),
448
+ [logo_alignment, s, shape, theme, width]
449
+ );
450
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ...containerProps, children: [
451
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
452
+ "button",
453
+ {
454
+ type: "button",
455
+ style,
456
+ onClick: () => {
457
+ click_listener?.();
458
+ login();
459
+ },
460
+ children: [
461
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Mark, { size: Math.round(s.font * 1.25) }),
462
+ type === "standard" && TEXTS[text]
463
+ ]
464
+ }
465
+ ),
466
+ internals.pairing && !internals.pairing.appLink && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PairingPanel, { pairing: internals.pairing })
467
+ ] });
468
+ }
469
+
470
+ // src/useZorealAutoLogin.ts
471
+ var import_react4 = require("react");
472
+ function useZorealAutoLogin(options) {
473
+ const { login } = useZorealFlow({
474
+ flow: "browser-direct",
475
+ scope: options.scope,
476
+ prompt: "none",
477
+ onCredential: options.onSuccess,
478
+ onError: (e) => {
479
+ const quiet = ["login_required", "consent_required", "interaction_required"];
480
+ if (quiet.includes(e.error)) {
481
+ options.onUnavailable?.();
482
+ } else {
483
+ options.onError?.({ type: "unknown", description: e.description ?? e.error });
484
+ }
485
+ },
486
+ onNonOAuthError: (e) => options.onError?.(e)
487
+ });
488
+ const fired = (0, import_react4.useRef)(false);
489
+ (0, import_react4.useEffect)(() => {
490
+ if (options.disabled || fired.current) return;
491
+ fired.current = true;
492
+ login();
493
+ }, [options.disabled, login]);
494
+ }
495
+
496
+ // src/logout.ts
497
+ function zorealLogout() {
498
+ }
499
+
500
+ // src/scopes.ts
501
+ function hasGrantedAllScopesZoreal(response, firstScope, ...restScopes) {
502
+ const granted = new Set((response.scope ?? "").split(/\s+/).filter(Boolean));
503
+ return [firstScope, ...restScopes].every((s) => granted.has(s));
504
+ }
505
+ function hasGrantedAnyScopeZoreal(response, firstScope, ...restScopes) {
506
+ const granted = new Set((response.scope ?? "").split(/\s+/).filter(Boolean));
507
+ return [firstScope, ...restScopes].some((s) => granted.has(s));
508
+ }
509
+ // Annotate the CommonJS export names for ESM import in node:
510
+ 0 && (module.exports = {
511
+ ZorealLogin,
512
+ ZorealOAuthProvider,
513
+ hasGrantedAllScopesZoreal,
514
+ hasGrantedAnyScopeZoreal,
515
+ useZorealAutoLogin,
516
+ useZorealLogin,
517
+ useZorealOAuth,
518
+ zorealLogout
519
+ });
520
+ //# sourceMappingURL=index.cjs.map