@zoreal/oauth2-react 0.1.10 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zoreal/oauth2-react
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![npm downloads](https://img.shields.io/npm/dm/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![types](https://img.shields.io/npm/types/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![CI](https://img.shields.io/github/actions/workflow/status/Bynn-Intelligence/zoreal-oauth2-react/ci.yml?branch=main&label=CI)](https://github.com/Bynn-Intelligence/zoreal-oauth2-react/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
3
+ [![npm](https://img.shields.io/npm/v/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![types](https://img.shields.io/npm/types/@zoreal/oauth2-react)](https://www.npmjs.com/package/@zoreal/oauth2-react) [![CI](https://img.shields.io/github/actions/workflow/status/Bynn-Intelligence/zoreal-oauth2-react/ci.yml?branch=main&label=CI)](https://github.com/Bynn-Intelligence/zoreal-oauth2-react/actions/workflows/ci.yml) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Bynn-Intelligence/zoreal-oauth2-react/badge)](https://scorecard.dev/viewer/?uri=github.com/Bynn-Intelligence/zoreal-oauth2-react) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
4
4
 
5
5
  Login with ZOREAL for React: a ZOREAL Verified Proof-of-Human behind every sign-in.
6
6
 
@@ -76,20 +76,28 @@ origin.
76
76
 
77
77
  ## Quick start: auth-code (email and name, needs your backend)
78
78
 
79
+ Two pieces: the provider once, and a button wherever you sign people in. The
80
+ QR, its live status, the countdown and the cancel wiring are the SDK's job.
81
+
79
82
  ```tsx
80
- import { useState } from 'react';
83
+ // app.tsx once, around the part of your app that signs in.
84
+ import { ZorealOAuthProvider } from '@zoreal/oauth2-react';
85
+
86
+ <ZorealOAuthProvider clientId={ZOREAL_CLIENT_ID} locale="en" theme="auto">
87
+ <App />
88
+ </ZorealOAuthProvider>;
89
+ ```
90
+
91
+ ```tsx
92
+ // ZorealSignIn.tsx
81
93
  import { useZorealLogin } from '@zoreal/oauth2-react';
82
94
 
83
95
  function ZorealSignIn() {
84
- const [pairing, setPairing] = useState(null);
85
-
86
96
  // `email` (and profile.name, etc.) are returned from /userinfo on your backend.
87
97
  const login = useZorealLogin({
88
98
  flow: 'auth-code',
89
99
  scope: 'openid email profile.name',
90
- onPairingStateChange: setPairing,
91
100
  onSuccess: async ({ code, code_verifier, nonce }) => {
92
- setPairing(null);
93
101
  // Send ALL THREE to your backend over TLS. It calls POST /token with the
94
102
  // code and verifier plus its client authentication, verifies the ID
95
103
  // token's nonce is this one, then reads the email and name from
@@ -100,35 +108,29 @@ function ZorealSignIn() {
100
108
  body: JSON.stringify({ code, code_verifier, nonce }),
101
109
  });
102
110
  },
111
+ onError: (e) => console.error(e.description ?? e.error),
112
+ onNonOAuthError: (e) => {
113
+ // The holder closing or ignoring the request is not an error to surface.
114
+ if (e.type === 'request_denied' || e.type === 'request_expired') return;
115
+ console.error(e.description ?? e.type);
116
+ },
103
117
  });
104
118
 
105
- return (
106
- <div>
107
- <button onClick={login}>Continue with ZOREAL</button>
108
- {pairing && !pairing.appLink && ['pending', 'claimed'].includes(pairing.status) && (
109
- <div>
110
- <img src={pairing.qrUrl} alt="Log in with ZOREAL" width={200} height={200} />
111
- <p>
112
- {pairing.status === 'claimed'
113
- ? 'Approve the login in your ZOREAL ID app.'
114
- : 'Scan with your phone camera or the ZOREAL ID app.'}
115
- </p>
116
- <button onClick={pairing.cancel}>Cancel</button>
117
- </div>
118
- )}
119
- </div>
120
- );
119
+ return <button onClick={login}>Continue with ZOREAL</button>;
121
120
  }
122
121
  ```
123
122
 
124
- **The hook renders nothing, so in this flow the pairing UI is yours.** On
125
- desktop the login cannot complete unless something shows `pairing.qrUrl` (the
126
- provider-served QR image) for the holder to scan that is what
127
- `onPairingStateChange` is for, and since 0.1.4 it carries `pairUrl`, `qrUrl`,
128
- `appLink` and `cancel` on every callback, including one fired immediately when
129
- the pairing starts. On a phone (`appLink: true`) the SDK opens the pairing
130
- link itself and no panel is needed. Only the drop-in `<ZorealLogin>` button
131
- below renders its own QR panel, and that component is browser-direct only.
123
+ That is the whole integration. When `login()` runs, the provider puts the
124
+ pairing modal on screen; on a phone it skips the QR and opens the ZOREAL ID
125
+ app instead. See [The pairing modal](#the-pairing-modal) for what it does and
126
+ how to theme, translate, time out or replace it.
127
+
128
+ > **Upgrading from 0.1.x?** Nothing breaks, but you can delete code. If you
129
+ > rendered your own panel from `onPairingStateChange`, that panel and the
130
+ > `useState` behind it are now redundant remove them and the SDK's modal
131
+ > takes over. To keep yours instead, set `pairingUI="none"` on the provider and
132
+ > nothing changes. `<ZorealLogin>` no longer draws its own inline QR panel; it
133
+ > uses the same modal.
132
134
 
133
135
  ## Quick start: the button (no backend, pseudonymous)
134
136
 
@@ -148,9 +150,10 @@ import { ZorealOAuthProvider, ZorealLogin } from '@zoreal/oauth2-react';
148
150
  </ZorealOAuthProvider>
149
151
  ```
150
152
 
151
- On desktop the button shows a QR; the user scans it with their phone and
152
- approves in the ZOREAL ID app. On a phone it opens the app directly. Either
153
- way your page just receives `onSuccess`.
153
+ On desktop the provider opens the [pairing modal](#the-pairing-modal); the user
154
+ scans the QR with their phone and approves in the ZOREAL ID app. On a phone it
155
+ skips the QR and opens the app directly. Either way your page just receives
156
+ `onSuccess`.
154
157
 
155
158
  ### On your backend
156
159
 
@@ -209,6 +212,57 @@ Things a backend implementer needs to know, learned the concrete way:
209
212
  `ux_mode: 'redirect'` is not supported in v1: it would put the PKCE verifier
210
213
  in a URL, which is a credential in every access log on the path.
211
214
 
215
+ ## The pairing modal
216
+
217
+ On desktop, a QR sign-in cannot complete unless something puts the pairing code
218
+ on screen. Since 0.2.0 that something is this package: `ZorealOAuthProvider`
219
+ renders the modal for **both** flows, so `useZorealLogin` integrators get the
220
+ whole pairing UI without writing (or styling, or translating) a line of it.
221
+
222
+ ```jsx
223
+ <ZorealOAuthProvider clientId={CLIENT_ID} locale={i18n.language} theme="auto">
224
+ <App />
225
+ </ZorealOAuthProvider>
226
+ ```
227
+
228
+ That is the entire integration. Your button stays a button:
229
+
230
+ ```jsx
231
+ const login = useZorealLogin({ flow: 'auth-code', scope: 'openid email', onSuccess });
232
+ return <button onClick={login}>Continue with ZOREAL</button>;
233
+ ```
234
+
235
+ What the modal does:
236
+
237
+ | | |
238
+ | --- | --- |
239
+ | **Mobile** | No QR. The SDK opens the pairing link, which the ZOREAL ID app claims; the modal never appears. Force one or the other with `display: 'qr'` / `'link'`. |
240
+ | **Live status** | The copy and the title follow the pairing: waiting for a scan, then waiting for approval once the holder has claimed the code (the spent QR blurs out behind a phone glyph). |
241
+ | **Countdown** | Counts down to expiry, turning amber under 20s. Reads the clock each tick rather than decrementing, so a backgrounded tab comes back honest. |
242
+ | **Timeout** | Closes and cancels at zero. Defaults to 120s; override with `pairingTimeoutMs`. The provider's own expiry wins when it is shorter. |
243
+ | **Cancel** | The X, the Cancel button, `Escape`, clicking outside and the timeout are one behaviour: abort the poll, close the modal. An orphaned poll is exactly how a request gets cancelled for over-polling. |
244
+ | **No ZOREAL ID yet** | A footer says the same code also installs the app. Without it the panel reads as "scan this with something I do not have", and the flow dead-ends at the one moment it can still be recovered. |
245
+ | **Themes** | `theme="auto"` (default) follows `prefers-color-scheme`; `"light"` and `"dark"` force it. The QR well stays white in dark mode on purpose — an inverted QR fails on a good number of phone cameras. |
246
+ | **Language** | Ships its own copy in 13 locales. Pass `locale` and the modal matches the page it opened on; omit it and it follows the browser's preference list, English as the floor. Arabic flips the dialog to RTL. |
247
+ | **Accessibility** | `role="dialog"`, `aria-modal`, labelled by its title, focus moved in on open, scroll locked, visible focus rings, and a `prefers-reduced-motion` fallback. |
248
+
249
+ Styling is a single `<style>` tag injected once, every selector prefixed `zrl-`
250
+ and scoped to the dialog. There is no CSS file to import and nothing to add to
251
+ your build, because a required import step is a required support ticket.
252
+
253
+ ### Rendering it yourself
254
+
255
+ If you already have a design system you want the QR to live inside, opt out:
256
+
257
+ ```jsx
258
+ <ZorealOAuthProvider clientId={CLIENT_ID} pairingUI="none">
259
+ ```
260
+
261
+ `onPairingStateChange` then carries everything you need on every state:
262
+ `qrUrl`, `pairUrl`, `status`, `expiresIn` and `cancel`. Render `qrUrl` in an
263
+ `<img>`; do not draw your own. `PairingModal` is also exported if you want the
264
+ real dialog but on your own terms.
265
+
212
266
  ## Scopes and claims
213
267
 
214
268
  Request scopes in the `scope` string, space-separated, always starting with
@@ -622,6 +676,17 @@ runtime dependencies. Two things touch the network, both on the ZOREAL origin:
622
676
  no "verified human" button text and there will not be one. The assertion
623
677
  lives in the token, where it is verifiable.
624
678
 
679
+ ## Verifying this release
680
+
681
+ Every version is published from GitHub Actions with [npm provenance](https://docs.npmjs.com/generating-provenance-statements): the package page on npmjs.com carries a **Provenance** panel linking the exact commit and workflow run that built the tarball, signed through [Sigstore](https://www.sigstore.dev/) and recorded in its public transparency log. No long-lived npm token stands behind it — the workflow authenticates by OIDC ([trusted publishing](https://docs.npmjs.com/trusted-publishers)), so a leaked CI secret cannot cut a release.
682
+
683
+ Check the signatures on what you actually installed:
684
+
685
+ ```sh
686
+ npm install @zoreal/oauth2-react
687
+ npm audit signatures
688
+ ```
689
+
625
690
  ## The ZOREAL OAuth2 library family
626
691
 
627
692
  | Repository | Package | Role |