@authara/react 0.0.1-trusted.3 → 0.2.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/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # @authara/react
2
2
 
3
- Official React components for Authara's Google and Apple browser flows.
4
-
5
- The package owns provider SDK loading and React lifecycle integration. HTTP,
6
- cookies, CSRF protection, sessions, and generated Authara API operations remain
7
- in [`@authara/browser`](https://www.npmjs.com/package/@authara/browser).
3
+ Official React buttons for complete Authara Google, Apple, and Microsoft authentication
4
+ flows. The components load the provider SDK, obtain the provider proof, prepare
5
+ CSRF, call the generated Authara browser client, and return a typed result.
8
6
 
9
7
  ## Installation
10
8
 
@@ -12,117 +10,423 @@ in [`@authara/browser`](https://www.npmjs.com/package/@authara/browser).
12
10
  npm install @authara/react @authara/browser react
13
11
  ```
14
12
 
15
- ## Usage
13
+ ## Setup
16
14
 
17
- Create one browser client and place one provider near the root of the page:
15
+ Create one browser client and one provider near the root of the page:
18
16
 
19
17
  ```tsx
20
18
  import { AutharaBrowserClient } from "@authara/browser";
21
- import {
22
- AppleButton,
23
- AutharaProvider,
24
- GoogleButton
25
- } from "@authara/react";
19
+ import { AutharaProvider } from "@authara/react";
26
20
 
27
- const authara = new AutharaBrowserClient();
21
+ const authara = new AutharaBrowserClient({
22
+ automaticRefresh: { audience: "app" },
23
+ });
28
24
 
29
- export function Login() {
25
+ export function App() {
30
26
  return (
31
27
  <AutharaProvider client={authara}>
28
+ <Routes />
29
+ </AutharaProvider>
30
+ );
31
+ }
32
+ ```
33
+
34
+ The provider owns no user or session state. It coordinates Authara's generated
35
+ client and the external-provider SDKs.
36
+
37
+ ## Login
38
+
39
+ ```tsx
40
+ import { AppleButton, GoogleButton, MicrosoftButton } from "@authara/react";
41
+
42
+ export function Login() {
43
+ return (
44
+ <div>
32
45
  <GoogleButton
33
- onCredential={async ({ credential, nonce }) => {
34
- await authara.loginWithGoogle({
35
- audience: "app",
36
- body: { credential, nonce }
37
- });
38
- }}
39
- onError={console.error}
46
+ action="login"
47
+ audience="app"
48
+ variant="text"
49
+ size="large"
50
+ onSuccess={({ session }) => setCurrentUser(session.user)}
51
+ onError={handleAuthError}
40
52
  />
41
53
 
42
54
  <AppleButton
43
- onAuthorization={async ({ code, state }) => {
44
- await authara.loginWithApple({
45
- audience: "app",
46
- body: { code, state }
47
- });
48
- }}
49
- onError={console.error}
55
+ action="login"
56
+ audience="app"
57
+ variant="text"
58
+ size="large"
59
+ onSuccess={({ session }) => setCurrentUser(session.user)}
60
+ onError={handleAuthError}
61
+ onCancel={() => setMessage("Sign in was cancelled.")}
50
62
  />
51
- </AutharaProvider>
63
+
64
+ <MicrosoftButton
65
+ action="login"
66
+ audience="app"
67
+ variant="text"
68
+ size="large"
69
+ onSuccess={({ session }) => setCurrentUser(session.user)}
70
+ onError={handleAuthError}
71
+ onCancel={() => setMessage("Sign in was cancelled.")}
72
+ />
73
+ </div>
52
74
  );
53
75
  }
54
76
  ```
55
77
 
56
- The buttons return provider proof instead of choosing an Authara operation.
57
- The same components therefore work for login, provider linking,
58
- reauthentication, invitations, and account-recovery proof.
78
+ `action="login"` supports the `app`, `admin`, and `operator` audiences. The
79
+ default action is `login` and the default audience is `app`.
59
80
 
60
- `AutharaProvider` is deliberately small. It contains no authentication or
61
- session state. It coordinates the page-global third-party SDK instances so
62
- Google is initialized once and callbacks are routed to the correct button.
81
+ ## Link a provider
82
+
83
+ ```tsx
84
+ <GoogleButton
85
+ action="link"
86
+ onSuccess={() => refreshAccount()}
87
+ onError={handleAuthError}
88
+ />
89
+
90
+ <AppleButton
91
+ action="link"
92
+ onSuccess={() => refreshAccount()}
93
+ onError={handleAuthError}
94
+ />
95
+
96
+ <MicrosoftButton
97
+ action="link"
98
+ onSuccess={() => refreshAccount()}
99
+ onError={handleAuthError}
100
+ />
101
+ ```
63
102
 
64
- ## Provider availability
103
+ Linking uses the current Authara session and therefore does not accept an
104
+ audience. Authara may return `recent_authentication_required` when the session
105
+ needs a fresh proof.
65
106
 
66
- Authara returns `404 Not Found` from an options operation when that provider is
67
- disabled. The corresponding button then renders `fallback` (which defaults to
68
- `null`) and calls `onUnavailable`.
107
+ ## Reauthenticate
69
108
 
70
109
  ```tsx
71
- <GoogleButton
72
- fallback={<p>Google sign-in is not configured.</p>}
73
- onUnavailable={() => reportProviderUnavailable("google")}
74
- onCredential={handleGoogleCredential}
110
+ <AppleButton
111
+ action="reauthenticate"
112
+ authenticationChallengeId={challenge.id}
113
+ onSuccess={() => retrySensitiveAction()}
114
+ onError={handleAuthError}
115
+ />
116
+
117
+ <MicrosoftButton
118
+ action="reauthenticate"
119
+ authenticationChallengeId={challenge.id}
120
+ onSuccess={() => retrySensitiveAction()}
121
+ onError={handleAuthError}
75
122
  />
76
123
  ```
77
124
 
78
- Apple user cancellation calls `onCancel`; it is not reported through
79
- `onError`.
125
+ TypeScript requires `authenticationChallengeId` for reauthentication and
126
+ rejects `audience` for both linking and reauthentication.
127
+
128
+ ## Account collisions
129
+
130
+ A login with an email owned by another sign-in method returns
131
+ `account_link_required`. The error contains the pending provider, a short-lived
132
+ link ID, and the methods that can prove ownership. Render the matching provider
133
+ button with `action="recover"`:
134
+
135
+ ```tsx
136
+ import type {
137
+ AutharaAccountRecoveryLink,
138
+ AutharaButtonError,
139
+ } from "@authara/react";
140
+
141
+ const [recovery, setRecovery] = useState<AutharaAccountRecoveryLink>();
142
+
143
+ function handleLoginError(error: AutharaButtonError) {
144
+ if (error.code === "account_link_required" && error.recovery) {
145
+ setRecovery(error.recovery);
146
+ return;
147
+ }
148
+ showMessage(error.message);
149
+ }
150
+
151
+ {
152
+ recovery?.proof_methods.includes("google") && (
153
+ <GoogleButton
154
+ action="recover"
155
+ recoveryLinkId={recovery.link_id}
156
+ onSuccess={({ session }) => setCurrentUser(session.user)}
157
+ onError={handleLoginError}
158
+ />
159
+ );
160
+ }
161
+
162
+ {
163
+ recovery?.proof_methods.includes("apple") && (
164
+ <AppleButton
165
+ action="recover"
166
+ recoveryLinkId={recovery.link_id}
167
+ onSuccess={({ session }) => setCurrentUser(session.user)}
168
+ onError={handleLoginError}
169
+ />
170
+ );
171
+ }
172
+
173
+ {
174
+ recovery?.proof_methods.includes("microsoft") && (
175
+ <MicrosoftButton
176
+ action="recover"
177
+ recoveryLinkId={recovery.link_id}
178
+ onSuccess={({ session }) => setCurrentUser(session.user)}
179
+ onError={handleLoginError}
180
+ />
181
+ );
182
+ }
183
+ ```
184
+
185
+ The flow is symmetric across external providers: a collision can be proved
186
+ with any existing Google, Apple, or Microsoft identity returned in
187
+ `proof_methods`. Authara never links accounts from an email match alone.
188
+
189
+ ## Errors
190
+
191
+ `onError` receives `AutharaButtonError`. It identifies the failing layer and
192
+ preserves structured Authara metadata:
193
+
194
+ ```tsx
195
+ import type { AutharaButtonError } from "@authara/react";
196
+
197
+ function handleAuthError(error: AutharaButtonError) {
198
+ switch (error.code) {
199
+ case "recent_authentication_required":
200
+ openReauthentication(error.authenticationChallenge);
201
+ return;
202
+ case "auth_method_already_linked":
203
+ showMessage("That provider is already linked.");
204
+ return;
205
+ case "unauthorized":
206
+ navigate("/login");
207
+ return;
208
+ default:
209
+ showMessage(error.message);
210
+ }
211
+ }
212
+ ```
213
+
214
+ Useful fields are:
215
+
216
+ - `source`: `authara`, `provider`, `configuration`, or `consumer`
217
+ - `status`: the HTTP status when Authara responded
218
+ - `code`: the stable Authara error code, or `unknown_authara_error`
219
+ - `serverCode`: an unrecognized server code when `code` is
220
+ `unknown_authara_error`
221
+ - `authenticationChallenge`: the challenge returned with HTTP 428
222
+ - `reauthenticateUrl`: Authara's optional reauthentication URL
223
+ - `recovery`: typed account-collision recovery details returned with HTTP 409
224
+ - `originalError`: the original provider, SDK, or callback failure
225
+
226
+ The selected `action` narrows the error-code type passed to `onError`. These
227
+ unions are generated from Core's `x-authara-error-codes` declarations:
228
+
229
+ - login adds `account_link_required`
230
+ - link adds `auth_method_already_linked` and
231
+ `recent_authentication_required`
232
+ - reauthentication adds `invalid_authentication_challenge`
233
+ - recovery adds the provider-link and invitation completion errors
234
+ - every action includes its declared authorization, request, availability, and
235
+ internal codes, plus the local CSRF and unknown-code fallbacks
236
+
237
+ Apple and Microsoft popup cancellation calls `onCancel`, not `onError`. A provider disabled
238
+ in Authara renders `fallback` and calls `onUnavailable`.
80
239
 
81
240
  ## Appearance
82
241
 
83
- `GoogleButton` exposes the official Google button configuration: `type`,
84
- `theme`, `size`, `text`, `shape`, `logo_alignment`, `width`, and `locale`.
242
+ All three components share the simple properties:
85
243
 
86
- `AppleButton` exposes `color`, `border`, `type`, `mode`, and `locale`. Width and
87
- height are set through the normal React `style` prop. Provider branding remains
88
- rendered by the official provider SDKs.
244
+ ```tsx
245
+ <AppleButton
246
+ action="login"
247
+ variant="icon"
248
+ size="small"
249
+ />
89
250
 
90
- ## Contract synchronization
251
+ <GoogleButton
252
+ action="login"
253
+ variant="text"
254
+ size="large"
255
+ mode="system"
256
+ />
257
+
258
+ <MicrosoftButton action="login" variant="text" size="large" mode="system" />
259
+ ```
260
+
261
+ - `variant`: `text` or `icon`
262
+ - `size`: `small`, `medium`, or `large`
263
+ - `label`: `sign-in`, `sign-up`, or `continue`
264
+
265
+ Text-button heights are `30px`, `36px`, and `44px`. Icon-only buttons are
266
+ larger at `36px`, `44px`, and `52px` for the same three size names.
267
+
268
+ `mode` accepts `"light"`, `"dark"`, or `"system"` and defaults to `"light"`.
269
+ `system` follows live changes to `prefers-color-scheme`. Light mode uses the
270
+ black Apple button and Google's light preset. Dark mode uses the white Apple
271
+ button and Google's dark preset. Apple is borderless in both modes. Explicit
272
+ Apple `color` or `border`, or Google `theme` or `border`, properties take
273
+ precedence over these defaults.
274
+
275
+ Google additionally exposes `theme`, `border`, `shape`, `logoAlignment`, and
276
+ `width`.
277
+ Apple exposes `color`, `border`, `borderRadius`, `align`, and `width`.
278
+ Microsoft exposes `border`, `align`, and `width`. All providers accept
279
+ `buttonText` to localize or otherwise override the
280
+ visible label and the accessible name of icon-only buttons.
281
+ Text and icon variants both default to a `12px` corner radius. Apple's
282
+ `borderRadius` accepts its supported `0`–`50` range; Google's `shape` can still
283
+ select `pill`, `circle`, `square`, or `rectangular` explicitly.
284
+ All providers use an Authara-rendered, centered logo-and-label structure.
285
+ Google's `light`, `dark`, and `neutral` themes use Google's required colors,
286
+ spacing, and typography. Make Google Sans Medium available in your application
287
+ as required by Google's branding guidelines; this package does not inject a
288
+ font download or redistribute Google's font. The provider SDKs still own the
289
+ underlying credential and authorization flows. Google's visible control is a
290
+ real native button; it starts the popup authorization-code flow directly and
291
+ does not place an invisible provider button over Authara's presentation.
292
+
293
+ ## Advanced provider-proof components
294
+
295
+ Applications with a custom multi-step operation can use the lower-level
296
+ components:
297
+
298
+ ```tsx
299
+ <GoogleCredentialButton onCredential={handleGoogleProof} />
300
+ <AppleCredentialButton onAuthorization={handleAppleProof} />
301
+ <MicrosoftCredentialButton onCredential={handleMicrosoftProof} />
302
+ ```
91
303
 
92
- The components never contain Authara route strings. They consume the generated
93
- `AutharaBrowserClient` operation methods:
304
+ Most applications should use `GoogleButton`, `AppleButton`, and
305
+ `MicrosoftButton`; the lower-level components deliberately expose the
306
+ short-lived provider proofs used by Core.
94
307
 
95
- - `getGoogleLoginOptions()`
96
- - `getAppleLoginOptions()`
308
+ ## Real local playground
97
309
 
98
- `.contract/manifest.json` records the exact browser SDK release and its
99
- immutable Authara Core OpenAPI provenance. `npm run check:contract` performs a
100
- TypeScript structural compatibility check against that exact browser SDK.
310
+ The Vite playground uses the real Google, Apple, and Microsoft SDKs and sends real HTTP
311
+ requests to Authara. It has no provider or API mocks.
101
312
 
102
- `.github/workflows/sync-browser.yaml` receives each
103
- `authara-browser-released` event, installs the exact released browser package,
104
- copies its Core provenance, runs the full checks, and opens an automated pull
105
- request. This keeps route and schema changes flowing in this order:
313
+ For fast component development, start an Authara gateway with Google and/or
314
+ Apple configured, then run:
106
315
 
107
- ```text
108
- Authara Core OpenAPI release
109
- -> generated @authara/browser release
110
- -> @authara/react contract check and dependency PR
316
+ ```bash
317
+ npm ci
318
+ npm run dev
111
319
  ```
112
320
 
113
- The initial manifest is intentionally marked `unreleased`. Publishing remains
114
- blocked until a browser SDK release containing the Apple operations has passed
115
- the synchronization workflow.
321
+ Core reads Google configuration from `AUTHARA_OAUTH_GOOGLE_CLIENT_ID` and
322
+ `AUTHARA_OAUTH_GOOGLE_CLIENT_SECRET`. Keep the secret on Core; React never
323
+ receives it. Apple uses `AUTHARA_OAUTH_APPLE_CLIENT_ID`, `AUTHARA_OAUTH_APPLE_TEAM_ID`,
324
+ `AUTHARA_OAUTH_APPLE_KEY_ID`, `AUTHARA_OAUTH_APPLE_PRIVATE_KEY_BASE64`, and the
325
+ Apple token-key settings documented by Authara Core.
326
+ Microsoft uses `AUTHARA_OAUTH_MICROSOFT_CLIENT_ID` and
327
+ `AUTHARA_OAUTH_MICROSOFT_TENANT_ID`; the browser obtains its authority and
328
+ one-time nonce from Core. Register the exact SPA redirect URI
329
+ `https://your-domain/auth/oauth/microsoft/redirect` in Microsoft Entra; Core
330
+ serves the MSAL redirect bridge at that path.
331
+
332
+ By default, Vite proxies `/auth` to `http://localhost:3001`. Override the Core
333
+ or gateway address when needed:
334
+
335
+ ```bash
336
+ AUTHARA_PLAYGROUND_PROXY_TARGET=http://localhost:3000 npm run dev
337
+ ```
338
+
339
+ Alternatively, set `VITE_AUTHARA_BASE_URL` to call an Authara origin directly.
340
+ That origin must allow the playground origin and credentialed browser requests.
341
+ The same-origin proxy is usually simpler.
342
+
343
+ To rehearse the way an operator deploys the packages, use the version-pinned
344
+ Compose stack instead. It builds the package first, builds the playground
345
+ against the package's public `dist` exports, and serves it through Authara
346
+ Gateway alongside released Core, migrations, PostgreSQL, and Redis images. A
347
+ `cloudflared` container publishes that gateway through the named tunnel:
348
+
349
+ ```bash
350
+ cp .env.example .env
351
+ # Paste the tunnel's eyJ... token into CLOUDFLARE_TUNNEL_TOKEN in .env.
352
+ docker compose -f docker-compose.playground.yaml up --build
353
+ ```
354
+
355
+ While testing unreleased Core contract changes from this workspace, add the
356
+ local override. It keeps the operator-style stack but builds the neighboring
357
+ `authara-core` checkout instead of pulling the released Core image:
358
+
359
+ ```bash
360
+ docker compose \
361
+ -f docker-compose.playground.yaml \
362
+ -f docker-compose.playground.local.yaml \
363
+ up --build
364
+ ```
365
+
366
+ The Cloudflare route for `playground.authara.org` must use
367
+ `http://gateway:3000` as its service URL because the connector runs inside the
368
+ Compose network. Open `https://playground.authara.org`; `http://localhost:3000`
369
+ remains available for direct local debugging.
370
+
371
+ After configuring the providers, fill their values in `.env` and set
372
+ `AUTHARA_OAUTH_PROVIDERS=google,apple,microsoft`. Until then, leave the provider list
373
+ empty; the playground displays the disabled-provider fallbacks instead of
374
+ attempting OAuth. Never commit `.env` or the tunnel token.
375
+
376
+ The Compose file defaults to the exact Core and migrations versions recorded
377
+ for this React contract; override `AUTHARA_CORE_VERSION` and
378
+ `AUTHARA_MIGRATIONS_VERSION` only when deliberately testing another released
379
+ pair.
380
+
381
+ For the Compose deployment, configure all providers with the public origin
382
+ `https://playground.authara.org`. The Google redirect URI is
383
+ `https://playground.authara.org/auth/oauth/google/callback`; the Apple domain is
384
+ `playground.authara.org` and its return URL is
385
+ `https://playground.authara.org/auth/oauth/apple/callback`.
386
+ Register
387
+ `https://playground.authara.org/auth/oauth/microsoft/redirect` as the Microsoft
388
+ SPA redirect URI.
389
+
390
+ The playground covers:
391
+
392
+ - real login for `app`, `admin`, and `operator`
393
+ - linking Google, Apple, or Microsoft to the current account
394
+ - signed-out cross-provider account-collision recovery
395
+ - Google, Apple, or Microsoft reauthentication with a challenge UUID
396
+ - text and icon variants in all three sizes
397
+ - disabled-provider fallbacks, cancellation, and structured errors
398
+ - current-session inspection without displaying access or refresh tokens
399
+
400
+ “Request challenge” calls Authara's recent-authentication check. A fresh session
401
+ returns success without a challenge; an old session returns HTTP 428 and the
402
+ playground fills the challenge UUID automatically.
403
+
404
+ To rehearse account-collision recovery, use “Sign out,” select “Recover account
405
+ collision,” and attempt a normal external-provider login with an email already
406
+ owned by another method. Authara's `409 account_link_required` response
407
+ already contains the pending link, so the playground immediately renders the
408
+ available existing-account proof buttons. It creates and consumes Core's real
409
+ ten-minute `link_id`; it does not simulate either provider response.
410
+
411
+ ## Contract synchronization
412
+
413
+ The published components contain no Authara route strings. They call methods on
414
+ the generated `@authara/browser` client. The package's compatibility manifest
415
+ and synchronization workflow bind each React release to an immutable browser
416
+ SDK release and its Authara Core OpenAPI provenance.
116
417
 
117
- ## Design guarantees
418
+ Each Browser release triggers a compatibility PR. The workflow waits for npm
419
+ registry propagation, verifies the Browser and Core commits and OpenAPI hash,
420
+ updates the exact Browser development dependency, and regenerates the action
421
+ error unions from Core. Route/type-compatible updates may auto-merge. A changed
422
+ error union deliberately requires human review so component mechanics and the
423
+ documented application handling can be updated when necessary.
118
424
 
119
- - No hidden login or redirect behavior
120
- - No Authara route duplication
121
- - Provider scripts are loaded once
122
- - Safe effect cleanup for React Strict Mode
123
- - Fresh Apple state and nonce immediately before authorization
124
- - Disabled providers disappear cleanly
125
- - React 18 and 19 peer support
425
+ Provider operations are discovered from Core's `get…LoginOptions` operations,
426
+ so Microsoft error declarations enter the generated unions automatically. The
427
+ Microsoft client methods are optional locally so this package still accepts the
428
+ currently released Browser client and renders `fallback`; they become active
429
+ when Browser is regenerated from the Microsoft-enabled Core OpenAPI contract.
126
430
 
127
431
  ## License
128
432
 
@@ -1,17 +1,36 @@
1
- import { type ComponentPropsWithoutRef, type ReactNode } from "react";
2
- import type { MaybePromise } from "./contract.js";
1
+ import { type ComponentPropsWithoutRef, type CSSProperties, type ReactNode } from "react";
2
+ import type { AutharaActionProps, MaybePromise } from "./contract.js";
3
3
  import type { AppleAuthorization } from "./services.js";
4
- export interface AppleButtonProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onError"> {
4
+ import { type AutharaColorMode } from "./color-mode.js";
5
+ type AppleButtonType = "sign-in" | "continue" | "sign-up";
6
+ export interface AppleCredentialButtonProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onError"> {
5
7
  disabled?: boolean;
6
8
  locale?: string;
7
9
  color?: "black" | "white";
8
10
  border?: boolean;
9
- type?: "sign-in" | "continue" | "sign-up";
11
+ buttonText?: string;
12
+ type?: AppleButtonType;
10
13
  mode?: "center-align" | "left-align" | "logo-only";
14
+ /** Apple supports a border radius from 0 through 50 points. */
15
+ borderRadius?: number;
11
16
  fallback?: ReactNode;
12
17
  onAuthorization: (authorization: AppleAuthorization) => MaybePromise<void>;
13
18
  onCancel?: () => void;
14
19
  onError?: (error: Error) => void;
15
20
  onUnavailable?: () => void;
16
21
  }
17
- export declare function AppleButton({ disabled, locale, color, border, type, mode, fallback, onAuthorization, onCancel, onError, onUnavailable, style, ...divProps }: AppleButtonProps): React.JSX.Element | null;
22
+ export declare function AppleCredentialButton({ disabled, locale, color, border, buttonText, type, mode, borderRadius, fallback, onAuthorization, onCancel, onError, onUnavailable, style, ...divProps }: AppleCredentialButtonProps): React.JSX.Element | null;
23
+ export type AppleButtonVariant = "icon" | "text";
24
+ export type AppleButtonLabel = "continue" | "sign-in" | "sign-up";
25
+ type AppleButtonVisualProps = Omit<AppleCredentialButtonProps, "mode" | "onAuthorization" | "onCancel" | "onError" | "type"> & {
26
+ variant?: AppleButtonVariant;
27
+ size?: "small" | "medium" | "large";
28
+ label?: AppleButtonLabel;
29
+ align?: "center" | "left";
30
+ mode?: AutharaColorMode;
31
+ width?: CSSProperties["width"];
32
+ onCancel?: () => void;
33
+ };
34
+ export type AppleButtonProps = AppleButtonVisualProps & AutharaActionProps<"apple">;
35
+ export declare function AppleButton(props: AppleButtonProps): React.JSX.Element | null;
36
+ export {};