@authu/react 1.0.66 → 1.0.67

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
@@ -128,6 +128,30 @@ function ApiComponent() {
128
128
  }
129
129
  ```
130
130
 
131
+ ### 5. Sign In Another Device with a QR Code
132
+
133
+ `DeviceLinkQrCode` shows a QR code that signs the scanning phone into the
134
+ current account, with no password:
135
+
136
+ ```tsx
137
+ import {DeviceLinkQrCode} from '@authu/react';
138
+
139
+ function OpenOnMobile() {
140
+ return <DeviceLinkQrCode size={220} />;
141
+ }
142
+ ```
143
+
144
+ The code encodes a one-shot AuthU link that expires after two minutes, and the
145
+ component renews it on its own while mounted. Opening the link gives the phone
146
+ its own AuthU session cookie, then redirects to the tenant `homeUrl`, where the
147
+ silent renewal signs the app in. Since anyone photographing the screen within
148
+ that window gets the session, only mount the component when the user asks for
149
+ it.
150
+
151
+ For a custom rendering, `useDeviceLink()` returns `{link, isLoading, error,
152
+ renew}` with the same auto-renewal, and `createDeviceLink()` on `useAuthU()`
153
+ creates a single link.
154
+
131
155
  ## Token Storage
132
156
 
133
157
  **Tokens never touch `localStorage`, `sessionStorage` or any other persistent
@@ -201,6 +225,7 @@ Two behaviour changes to expect in an app that upgrades:
201
225
  | `login(options?)` | `function` | Redirect to login |
202
226
  | `logout(options?)` | `function` | Log out user |
203
227
  | `getAccessToken()` | `function` | Get current access token |
228
+ | `createDeviceLink()` | `function` | One-shot sign-in link for another device (`{url, expiresAt} \| null`) |
204
229
 
205
230
  ## Development
206
231
 
@@ -1 +1 @@
1
- {"version":3,"file":"AuthUProvider.d.ts","sourceRoot":"","sources":["../src/AuthUProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAEV,WAAW,EACX,iBAAiB,EAIlB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,YAAY,mDAAgD,CAAC;AAO1E,UAAU,kBAAkB;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAuDD,wBAAgB,aAAa,CAAC,EAAC,MAAM,EAAE,QAAQ,EAAC,EAAE,kBAAkB,2CAwYnE"}
1
+ {"version":3,"file":"AuthUProvider.d.ts","sourceRoot":"","sources":["../src/AuthUProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAEV,WAAW,EACX,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,YAAY,mDAAgD,CAAC;AAO1E,UAAU,kBAAkB;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAuDD,wBAAgB,aAAa,CAAC,EAAC,MAAM,EAAE,QAAQ,EAAC,EAAE,kBAAkB,2CA6ZnE"}
@@ -344,6 +344,24 @@ export function AuthUProvider({ config, children }) {
344
344
  return false;
345
345
  }
346
346
  }, [config.clientId, config.domain]);
347
+ const createDeviceLink = useCallback(async () => {
348
+ const accessToken = await getAccessToken();
349
+ if (!accessToken)
350
+ return null;
351
+ try {
352
+ const response = await fetch(`${buildBaseUrl(config.domain)}/api/auth/device-link`, {
353
+ method: 'POST',
354
+ headers: { Authorization: `Bearer ${accessToken}` }
355
+ });
356
+ if (!response.ok)
357
+ return null;
358
+ const data = await response.json();
359
+ return { url: data.url, expiresAt: new Date(data.expiresAt).getTime() };
360
+ }
361
+ catch {
362
+ return null;
363
+ }
364
+ }, [config.domain, getAccessToken]);
347
365
  const contextValue = {
348
366
  isLoading: state.isLoading,
349
367
  isAuthenticated: state.isAuthenticated,
@@ -352,7 +370,8 @@ export function AuthUProvider({ config, children }) {
352
370
  login,
353
371
  logout,
354
372
  getAccessToken,
355
- checkEmailExists
373
+ checkEmailExists,
374
+ createDeviceLink
356
375
  };
357
376
  return (_jsx(AuthUContext.Provider, { value: contextValue, children: children }));
358
377
  }
@@ -0,0 +1,16 @@
1
+ import { type CSSProperties } from 'react';
2
+ interface DeviceLinkQrCodeProps {
3
+ /** Rendered width and height in pixels */
4
+ size?: number;
5
+ color?: string;
6
+ background?: string;
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ }
10
+ /**
11
+ * QR code that signs the scanning device into the current account. The link
12
+ * renews itself while mounted; nothing is rendered when signed out.
13
+ */
14
+ export declare function DeviceLinkQrCode({ size, color, background, className, style }: DeviceLinkQrCodeProps): import("react/jsx-runtime").JSX.Element | null;
15
+ export {};
16
+ //# sourceMappingURL=DeviceLinkQrCode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeviceLinkQrCode.d.ts","sourceRoot":"","sources":["../src/DeviceLinkQrCode.tsx"],"names":[],"mappings":"AACA,OAAO,EAAC,KAAK,aAAa,EAAU,MAAM,OAAO,CAAC;AAGlD,UAAU,qBAAqB;IAC7B,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAKD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAU,EACV,KAAiB,EACjB,UAAsB,EACtB,SAAS,EACT,KAAK,EACN,EAAE,qBAAqB,kDAsCvB"}
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import qrcode from 'qrcode-generator';
3
+ import { useMemo } from 'react';
4
+ import { useDeviceLink } from './useDeviceLink.js';
5
+ /** Quiet zone around the code, in modules, required by scanners */
6
+ const MARGIN = 2;
7
+ /**
8
+ * QR code that signs the scanning device into the current account. The link
9
+ * renews itself while mounted; nothing is rendered when signed out.
10
+ */
11
+ export function DeviceLinkQrCode({ size = 200, color = '#000000', background = '#ffffff', className, style }) {
12
+ const { link } = useDeviceLink();
13
+ const qr = useMemo(() => {
14
+ if (!link)
15
+ return null;
16
+ const code = qrcode(0, 'M');
17
+ code.addData(link.url);
18
+ code.make();
19
+ const count = code.getModuleCount();
20
+ let path = '';
21
+ for (let row = 0; row < count; row++) {
22
+ for (let col = 0; col < count; col++) {
23
+ if (code.isDark(row, col)) {
24
+ path += `M${col + MARGIN} ${row + MARGIN}h1v1h-1z`;
25
+ }
26
+ }
27
+ }
28
+ return { path, viewBox: count + MARGIN * 2 };
29
+ }, [link]);
30
+ if (!qr)
31
+ return null;
32
+ return (_jsxs("svg", { role: "img", "aria-label": "Sign-in QR code", width: size, height: size, viewBox: `0 0 ${qr.viewBox} ${qr.viewBox}`, shapeRendering: "crispEdges", className: className, style: style, children: [_jsx("rect", { width: qr.viewBox, height: qr.viewBox, fill: background }), _jsx("path", { d: qr.path, fill: color })] }));
33
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { AuthUProvider } from './AuthUProvider.js';
2
+ export { DeviceLinkQrCode } from './DeviceLinkQrCode.js';
2
3
  export { PrivateRoute } from './PrivateRoute.js';
3
4
  export * from './pkce.js';
4
- export type { AuthUConfig, AuthUContextValue, AuthUUser } from './types.js';
5
+ export type { AuthUConfig, AuthUContextValue, AuthUUser, DeviceLink } from './types.js';
5
6
  export { useApiToken } from './useApiToken.js';
6
7
  export { useAuthU } from './useAuthU.js';
8
+ export { useDeviceLink } from './useDeviceLink.js';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,cAAc,WAAW,CAAC;AAC1B,YAAY,EAAC,WAAW,EAAE,iBAAiB,EAAE,SAAS,EAAC,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAC/C,cAAc,WAAW,CAAC;AAC1B,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,UAAU,EACX,MAAM,YAAY,CAAC;AACpB,OAAO,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AACvC,OAAO,EAAC,aAAa,EAAC,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { AuthUProvider } from './AuthUProvider.js';
2
+ export { DeviceLinkQrCode } from './DeviceLinkQrCode.js';
2
3
  export { PrivateRoute } from './PrivateRoute.js';
3
4
  export * from './pkce.js';
4
5
  export { useApiToken } from './useApiToken.js';
5
6
  export { useAuthU } from './useAuthU.js';
7
+ export { useDeviceLink } from './useDeviceLink.js';
package/dist/types.d.ts CHANGED
@@ -28,6 +28,16 @@ export interface AuthUContextValue {
28
28
  logout: (options?: LogoutOptions) => void;
29
29
  getAccessToken: () => Promise<string | null>;
30
30
  checkEmailExists: (email: string) => Promise<boolean>;
31
+ /**
32
+ * One-shot link that signs another device into the same account, meant to
33
+ * be shown as a QR code. Expires after two minutes; null when signed out.
34
+ */
35
+ createDeviceLink: () => Promise<DeviceLink | null>;
36
+ }
37
+ export interface DeviceLink {
38
+ url: string;
39
+ /** Epoch milliseconds */
40
+ expiresAt: number;
31
41
  }
32
42
  export interface LoginOptions {
33
43
  returnTo?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD;;;OAGG;IACH,gBAAgB,EAAE,MAAM,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB"}
@@ -0,0 +1,14 @@
1
+ import type { DeviceLink } from './types.js';
2
+ interface UseDeviceLinkReturn {
3
+ link: DeviceLink | null;
4
+ isLoading: boolean;
5
+ error: Error | null;
6
+ renew: () => Promise<void>;
7
+ }
8
+ /**
9
+ * Keeps a fresh device link while the component is mounted (and `enabled`),
10
+ * so a QR code can stay on screen without ever showing an expired link.
11
+ */
12
+ export declare function useDeviceLink(enabled?: boolean): UseDeviceLinkReturn;
13
+ export {};
14
+ //# sourceMappingURL=useDeviceLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDeviceLink.d.ts","sourceRoot":"","sources":["../src/useDeviceLink.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,YAAY,CAAC;AAS3C,UAAU,mBAAmB;IAC3B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,UAAO,GAAG,mBAAmB,CA8BjE"}
@@ -0,0 +1,39 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { useAuthU } from './useAuthU.js';
3
+ /**
4
+ * Renewed this long before it expires, so a phone scanning at the last second
5
+ * still lands on a valid link.
6
+ */
7
+ const RENEW_MARGIN_MS = 15_000;
8
+ /**
9
+ * Keeps a fresh device link while the component is mounted (and `enabled`),
10
+ * so a QR code can stay on screen without ever showing an expired link.
11
+ */
12
+ export function useDeviceLink(enabled = true) {
13
+ const { createDeviceLink, isAuthenticated } = useAuthU();
14
+ const [link, setLink] = useState(null);
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const [error, setError] = useState(null);
17
+ const renew = useCallback(async () => {
18
+ setIsLoading(true);
19
+ const created = await createDeviceLink();
20
+ setLink(created);
21
+ setError(created ? null : new Error('Could not create a device link'));
22
+ setIsLoading(false);
23
+ }, [createDeviceLink]);
24
+ useEffect(() => {
25
+ if (!enabled || !isAuthenticated) {
26
+ setLink(null);
27
+ return;
28
+ }
29
+ renew();
30
+ }, [enabled, isAuthenticated, renew]);
31
+ useEffect(() => {
32
+ if (!enabled || !link)
33
+ return;
34
+ const delay = Math.max(link.expiresAt - Date.now() - RENEW_MARGIN_MS, 0);
35
+ const timer = setTimeout(renew, delay);
36
+ return () => clearTimeout(timer);
37
+ }, [enabled, link, renew]);
38
+ return { link, isLoading, error, renew };
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authu/react",
3
- "version": "1.0.66",
3
+ "version": "1.0.67",
4
4
  "description": "React SDK for AuthU - Centralized Multi-Tenant Authentication Service",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -43,5 +43,8 @@
43
43
  "@types/react": "^19.0.4",
44
44
  "react": "^19.0.0",
45
45
  "typescript": "^5.7.3"
46
+ },
47
+ "dependencies": {
48
+ "qrcode-generator": "^2.0.4"
46
49
  }
47
50
  }