@qrauth/web-components 0.4.0 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @qrauth/web-components
2
2
 
3
+ ## [0.4.2] — 2026-10-13
4
+
5
+ ### Security
6
+ - **Server error text is rendered as text.** All three components put the API's error message into the page with `textContent`. Before, it was interpolated into a template assigned to `innerHTML`, so markup in a message (the API used to echo the `redirect-uri` value back) was parsed in the embedding site's origin.
7
+ - **`<qrauth-login>` and `<qrauth-2fa>` send the PKCE verifier in the `X-QRAuth-Code-Verifier` header** when polling, not in the URL. A query string is written to proxy and server access logs. Requires QRAuth API ≥ 2026-10-13. Against an older API the header is ignored and the approved result comes back without the user; the components now report that as `qrauth:error` instead of a sign-in with no identity.
8
+ - **`<qrauth-login redirect-url>` only follows http(s) and relative URLs.** Any other scheme (`javascript:`, `data:`, ...) is ignored with a console warning. This includes custom app schemes (`myapp://`), which were never documented but did work.
9
+ - **A throwing `on-auth` callback no longer stops the redirect or the modal from closing**, and only an own property of `window` is called (not an inherited one such as `constructor`).
10
+
11
+ ### Docs
12
+ - `on-auth` is documented: it names a function on `window`, which is looked up and called with the session result. Set it only from your own markup. The `qrauth:authenticated` event carries the same data and needs no global.
13
+
14
+ ### Notes
15
+ - No attribute or event changed. Sites that embed the unpinned `components.js` pick this up automatically; sites pinned to a versioned file with an SRI hash need the new file name and hash from `https://cdn.qrauth.io/v1/latest.json`.
16
+ - If your site sets a Content-Security-Policy, `style-src` must allow `'unsafe-inline'` for the components' shadow-DOM styles.
17
+ - Bundle size: 20.8 KB gzipped IIFE / 20.8 KB gzipped ESM (0.4.1: 20.3 KB).
18
+
19
+ ## [0.4.1] — 2026-04-29
20
+
21
+ ### Fixed
22
+ - Removed dependency on `api.qrserver.com` for QR rendering. QR codes are now rendered locally as inline SVG via the `qrcode` library. Required for WP.org plugin directory submission and for any consumer with strict CSP.
23
+ - QR quiet zone increased from 1 to 4 modules per ISO/IEC 18004 — restores the scan reliability of the previous external renderer on stricter readers (iPhone Camera, older Android, low-light captures).
24
+ - Capacity overflow in `qrToSvg()` no longer crashes the component render — `QRCode.create` is now wrapped in try/catch and returns an empty SVG on overflow, letting the calling state render its empty-qr fallback.
25
+
26
+ ### Changed
27
+ - New runtime dependency: `qrcode@1.5.4` (already pinned by `@qrauth/animated-qr`).
28
+ - **`<qrauth-login>` and `<qrauth-2fa>` mobile "Continue with QRAuth" CTA** now appends `?dr=1` to the hosted approval URL. Forward-compatible with an upcoming server-side change at `qrauth.io/a/:token` that will gate the post-approval redirect on this flag — same-device clicks redirect, cross-device scans land on a terminal "Approved, you can close this tab" state. No behavior change until the server gating ships; safe to deploy independently.
29
+
3
30
  ## 0.4.0
4
31
 
5
32
  - **`<qrauth-login>` and `<qrauth-2fa>`: new `redirect-uri` attribute.** After the user taps Approve on the mobile CTA path, the QRAuth hosted approval page now navigates back to the consumer site instead of landing on a "you can close this page" dead end. The attribute is forwarded to `POST /api/v1/auth-sessions` as `redirectUrl` and validated server-side against the app's registered `redirectUrls` allowlist (same security model as OAuth 2.0 redirect URIs — strips trailing slash and ignores query + fragment before comparison). No-op on the desktop QR flow since polling in the original tab already handles the transition.
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Drop-in Web Components for [QRAuth](https://qrauth.io) — passwordless QR authentication, 2FA, and ephemeral delegated access. Shadow DOM, framework-free, ~10 KB gzipped.
4
4
 
5
+ <a href="https://openid.net/certification/certified-openid-providers-profiles/"><img src="https://qrauth.io/assets/certifications/openid-certified.png" alt="OpenID Certified" width="180" /></a>
6
+
7
+ QRAuth is an **OpenID Certified™** OpenID Provider (Basic OP + Config OP profiles).
8
+
5
9
  - `<qrauth-login>` — passwordless sign-in, button or inline. Mobile-aware pending state (tap-to-continue CTA with QR demoted under a "Use another device" expander).
6
10
  - `<qrauth-2fa>` — drop-in second-factor step with session-token binding.
7
11
  - `<qrauth-ephemeral>` — time-limited, device-bound access QRs.
@@ -14,12 +18,12 @@ All three components bubble `qrauth:*` events across the Shadow DOM boundary (`c
14
18
  npm install @qrauth/web-components
15
19
  ```
16
20
 
17
- Or load the CDN IIFE directly in any HTML page:
21
+ Or load the CDN IIFE directly in any HTML page (latest hashes at [`https://cdn.qrauth.io/v1/latest.json`](https://cdn.qrauth.io/v1/latest.json)):
18
22
 
19
23
  ```html
20
24
  <script
21
- src="https://cdn.qrauth.io/v1/components.js"
22
- integrity="sha384-..."
25
+ src="https://cdn.qrauth.io/v1/components-<version>.js"
26
+ integrity="<sha384 for that file, from latest.json>"
23
27
  crossorigin="anonymous"
24
28
  ></script>
25
29
 
@@ -51,12 +55,13 @@ Or load the CDN IIFE directly in any HTML page:
51
55
  | `theme` | `light` \| `dark` | `light` | Colour scheme. |
52
56
  | `base-url` | URL | `https://qrauth.io` | API host. Override for staging / local dev. |
53
57
  | `scopes` | space-separated | `identity` | OAuth-style scopes requested. |
54
- | `redirect-url` | URL | — | Auto-redirect after success. |
55
- | `on-auth` | string | — | Name of a global function to invoke on success. |
58
+ | `redirect-url` | URL | — | Auto-redirect after success. Only an http(s) URL or a relative URL is followed; any other scheme is ignored with a console warning. |
59
+ | `on-auth` | string | — | Name of a function on `window`, called with the session result on success. It is looked up by name, so set it only from your own markup, never from user input. The `qrauth:authenticated` event carries the same data and needs no global. |
56
60
  | `display` | `button` \| `inline` | `button` | Modal trigger vs. embedded widget. |
57
61
  | `animated` | boolean | off | Enables the pulse animation on the QR. |
58
62
  | `force-mode` | `mobile` \| `desktop` \| `auto` | `auto` | Override automatic mobile detection. |
59
63
  | `mobile-fallback-only` | boolean | off | Keep the QR-first body on every device; disables the mobile CTA path. |
64
+ | `redirect-uri` | URL | — | Where the hosted approval page sends the user after Approve. Required for the mobile flow to feel complete. Must match the app's registered `redirectUrls` allowlist. See [docs.qrauth.io/guide/web-components](https://docs.qrauth.io/guide/web-components#the-mobile-flow) for the URL-callback handler your app needs. |
60
65
 
61
66
  ## Events
62
67
 
@@ -68,6 +73,9 @@ All events bubble across the Shadow DOM (`composed: true`):
68
73
  - `qrauth:denied` — user denied approval on their device
69
74
  - `qrauth:error` — `detail: { message }`
70
75
 
76
+ `qrauth:error` carries `detail.message`, which can be text from the API. Show
77
+ it with `textContent`, never `innerHTML`.
78
+
71
79
  ## Theming
72
80
 
73
81
  Override any of the following CSS custom properties on the host page or on the element itself:
@@ -89,6 +97,13 @@ qrauth-login {
89
97
 
90
98
  Or flip the bundled dark palette via `theme="dark"`.
91
99
 
100
+ ## Content Security Policy
101
+
102
+ The components style themselves inside their shadow root with inline
103
+ `<style>`. If the embedding page sends a Content-Security-Policy, `style-src`
104
+ must allow `'unsafe-inline'`. `connect-src` must allow the QRAuth API origin
105
+ (`https://qrauth.io` unless you set `base-url`).
106
+
92
107
  ## Framework integration
93
108
 
94
109
  The components are framework-free native Custom Elements. They work in React, Vue, Svelte, Angular, plain HTML — wherever the browser exposes `customElements`. For React, wrap event listeners in `useEffect` because React's synthetic event system doesn't observe custom events directly.
package/dist/base.d.ts CHANGED
@@ -8,6 +8,16 @@
8
8
  * Exported for documentation and test mocks.
9
9
  */
10
10
  export declare const MOBILE_MEDIA_QUERY = "(pointer: coarse) and (hover: none)";
11
+ /** Header the API reads the PKCE verifier from when an auth session is polled. */
12
+ export declare const CODE_VERIFIER_HEADER = "X-QRAuth-Code-Verifier";
13
+ /**
14
+ * Where `redirect-url` may send the browser: an http(s) URL, or a relative
15
+ * one resolved against the current page. Anything that does not resolve to
16
+ * http(s), `javascript:` and `data:` in particular, is refused. Returns the
17
+ * absolute URL to navigate to, or null. This is a scheme check, not an
18
+ * origin check: an absolute URL on another site is allowed, as documented.
19
+ */
20
+ export declare function safeRedirectTarget(raw: string | null | undefined, pageUrl: string): string | null;
11
21
  export declare abstract class QRAuthElement extends HTMLElement {
12
22
  protected shadow: ShadowRoot;
13
23
  private _sseConnection;
@@ -43,6 +53,31 @@ export declare abstract class QRAuthElement extends HTMLElement {
43
53
  protected startPolling(url: string, intervalMs: number, callback: (data: unknown) => void, headers?: Record<string, string>): void;
44
54
  protected stopPolling(): void;
45
55
  protected cleanup(): void;
56
+ /**
57
+ * Put a message into the rendered error state as text. The message can come
58
+ * from the API, so it is never parsed as HTML; the template only supplies
59
+ * the element and its default wording.
60
+ */
61
+ protected setErrorText(message: string | null | undefined): void;
62
+ /**
63
+ * True when a PKCE-bound session came back APPROVED without the user: the
64
+ * API did not accept the verifier (an API older than the verifier header
65
+ * ignores it). There is no identity to hand to the page, so this must not
66
+ * be reported as a successful sign-in.
67
+ */
68
+ protected isApprovalWithoutIdentity(sentVerifier: boolean, data: {
69
+ user?: unknown;
70
+ } | undefined): boolean;
71
+ /**
72
+ * Call the function named by an `on-*` attribute. Only an own property of
73
+ * `window` is called, not an inherited one (`constructor`, `toString`), and
74
+ * a throwing callback does not stop the component. Built-ins such as
75
+ * `alert` are own properties of `window` in browsers, so the attribute must
76
+ * still come from the embedding page's own markup, as documented.
77
+ */
78
+ protected callGlobal(name: string | null, payload: unknown): void;
79
+ /** Leave the page. Separate so it can be observed in tests. */
80
+ protected navigate(url: string): void;
46
81
  protected emit(name: string, detail?: unknown): void;
47
82
  protected generateCodeVerifier(): Promise<string>;
48
83
  protected computeCodeChallenge(verifier: string): Promise<string>;
package/dist/login.d.ts CHANGED
@@ -7,8 +7,14 @@ import { QRAuthElement } from './base.js';
7
7
  * theme — "light" (default) | "dark"
8
8
  * base-url — API base URL (default: https://qrauth.io)
9
9
  * scopes — Space-separated scopes (default: "identity")
10
- * redirect-url — Optional redirect after successful auth
11
- * on-auth — Name of a global callback function to call on success
10
+ * redirect-url — Optional redirect after successful auth. Only an
11
+ * http(s) URL or a relative URL is followed; any
12
+ * other scheme is ignored with a console warning.
13
+ * on-auth — Name of a function on `window` to call on success
14
+ * with the session result. It is looked up by name
15
+ * and called, so set it only from your own markup,
16
+ * never from user input. The `qrauth:authenticated`
17
+ * event carries the same data and needs no global.
12
18
  * display — "button" (default) | "inline"
13
19
  * animated — If present, enables subtle QR pulse animation
14
20
  * force-mode — "mobile" | "desktop" | "auto" (default "auto").
@@ -53,6 +59,7 @@ export declare class QRAuthLogin extends QRAuthElement {
53
59
  private _codeVerifier;
54
60
  private _status;
55
61
  private _expiresAt;
62
+ private _currentQrUrl;
56
63
  private _timerInterval;
57
64
  private _pollTimeout;
58
65
  get scopes(): string;
@@ -97,11 +104,18 @@ export declare class QRAuthLogin extends QRAuthElement {
97
104
  * The existing polling loop in _beginPolling picks up SCANNED / APPROVED
98
105
  * transitions from the server once the user completes the flow there,
99
106
  * so qrauth:scanned and qrauth:authenticated fire unchanged.
107
+ *
108
+ * The `dr=1` query flag tells the hosted approval page that this is a
109
+ * same-device click (not a cross-device QR scan), and that the
110
+ * post-approval redirect to redirectUrl should fire. The QR matrix
111
+ * encodes the bare /a/:token URL, so a scanner cannot carry this flag.
100
112
  */
101
113
  private _openApprovalPage;
102
114
  private _openModal;
103
115
  private _closeModal;
104
116
  private _startAuth;
117
+ /** The scan was approved but the API returned no identity for it. */
118
+ private _failWithoutIdentity;
105
119
  private _retry;
106
120
  private _beginPolling;
107
121
  private _handleStatusChange;