@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 +21 -0
- package/README.md +91 -0
- package/dist/index.cjs +520 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +178 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +487 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
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
|