@owlmeans/web-oauth 0.1.18-rc.22 → 0.1.18-rc.24

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,3 +1,205 @@
1
+ # @owlmeans/web-oauth
2
+
3
+ The browser half of `@owlmeans/oauth`: the consent, device-code and done screens an OAuth 2.1
4
+ sign-in ends on, the headless `useOAuthConsent` hook behind the consent screen, and
5
+ `oauthEntrypoints()` that binds the three screens into a web app. A web app uses it when its server
6
+ runs `@owlmeans/server-oauth` and points `consentUrl` / `deviceUrl` at these screens. It renders no
7
+ API itself — the consent `load` / `approve` / `deny` handlers come from `@owlmeans/server-oauth`, and
8
+ the protocols, flow and `ConsentView` shape from `@owlmeans/oauth`. An app that does not act as an
9
+ OAuth authorization server (MCP / CLI connectors) does not need it; signing a person in to an
10
+ external OpenID provider is `@owlmeans/web-oidc-rp`.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ bun add @owlmeans/web-oauth@^0.1.18-rc.24
16
+ ```
17
+
18
+ Peer dependencies: `react`, `react-dom`, `tailwindcss`, `@radix-ui/react-label`,
19
+ `@radix-ui/react-slot`, `class-variance-authority`, `clsx`, `tailwind-merge`.
20
+
21
+ ## Concepts
22
+
23
+ - **Screens** — three `RoutedComponent`s: `OAuthConsentScreen` (`/oauth/consent?ref=…`),
24
+ `OAuthDeviceScreen` (`/oauth/device`, RFC 8628 `verification_uri`) and `OAuthDoneScreen`
25
+ (`/oauth/done`). Full-page, no application chrome.
26
+ - **Ref** — the `?ref=` the consent screen reads: a code grant's request id or a device grant's user
27
+ code; the server decides which.
28
+ - **Consent flow** — `oauthFlow` from `@owlmeans/oauth`. The screens transit it with `makeFlowModel`
29
+ instead of hard-coding the next screen (`verify` → `next` → `consent`, `consent` → `approve` /
30
+ `deny` → `done`).
31
+ - **Sign-in leg** — a signed-out visitor is not refused: the flow is suspended at its `sign-in` step
32
+ in `FLOW_STATE` for `OAUTH_SUSPEND_TTL_MS` and the browser goes to the dispatcher; after sign-in the
33
+ landing resumes back on the consent screen with the same `ref`.
34
+ - **Headless hook** — `useOAuthConsent` holds all consent logic; the component is a thin shell over
35
+ the package's private shadcn primitives.
36
+
37
+ ## Usage
38
+
39
+ ### Wire the screens into a web app
40
+
41
+ `appendOAuthScreens` only checks that a `FLOW_STATE` resource is registered, so it runs after
42
+ `appendFlowService` (`@owlmeans/web-flow`). The screens are bound by spreading `oauthEntrypoints()`
43
+ into the app's entrypoint list, with the same `OAuthEntrypointOptions` the server passed to
44
+ `makeOAuthProtocols`.
45
+
46
+ ```tsx
47
+ import { appendFlowService } from '@owlmeans/web-flow'
48
+ import { appendOAuthScreens, oauthEntrypoints } from '@owlmeans/web-oauth'
49
+
50
+ const context = makeContext(cfg)
51
+ appendFlowService(context) // registers FLOW_STATE
52
+ appendOAuthScreens(context) // throws SyntaxError when FLOW_STATE is missing
53
+
54
+ context.registerEntrypoints([
55
+ ...entrypoints,
56
+ ...oauthEntrypoints({ parent: account.base }),
57
+ ])
58
+ ```
59
+
60
+ The web context must also know the consent API protocols so `context.entrypoint(protocols.load)`
61
+ resolves; the harness registers them with `bindAll` from `@owlmeans/client-entrypoint`:
62
+
63
+ ```tsx
64
+ import { bindAll } from '@owlmeans/client-entrypoint'
65
+ import { makeOAuthProtocols } from '@owlmeans/oauth'
66
+
67
+ const consentApi = makeOAuthProtocols({ parent: API_BASE })
68
+
69
+ context.registerEntrypoints(bindAll({
70
+ apiBase,
71
+ oauth: { base: consentApi.base, load: consentApi.load, approve: consentApi.approve, deny: consentApi.deny },
72
+ }))
73
+ ```
74
+
75
+ ### Bind the components yourself
76
+
77
+ An app that builds its entrypoints from shared protocols binds the exported components directly:
78
+
79
+ ```tsx
80
+ import { handler } from '@owlmeans/client'
81
+ import { bindScreen } from '@owlmeans/client-entrypoint'
82
+ import { makeOAuthProtocols } from '@owlmeans/oauth'
83
+ import { OAuthConsentScreen, OAuthDeviceScreen, OAuthDoneScreen } from '@owlmeans/web-oauth'
84
+
85
+ const protocols = makeOAuthProtocols({ parent: account.base })
86
+
87
+ export const screens = [
88
+ bindScreen(protocols.consentScreen, handler(OAuthConsentScreen)),
89
+ bindScreen(protocols.deviceScreen, handler(OAuthDeviceScreen)),
90
+ bindScreen(protocols.doneScreen, handler(OAuthDoneScreen)),
91
+ ]
92
+ ```
93
+
94
+ ### Make Tailwind see the package classes
95
+
96
+ Tailwind skips `node_modules`, so a class used only inside this package never reaches the app
97
+ stylesheet. Every consuming app adds a `@source` line pointing at the shipped `src`:
98
+
99
+ ```css
100
+ @source "../../../node_modules/@owlmeans/web-oauth/src";
101
+ ```
102
+
103
+ ### Build a custom consent screen on the hook
104
+
105
+ ```tsx
106
+ import type { RoutedComponent } from '@owlmeans/client'
107
+ import { useContext } from '@owlmeans/client'
108
+ import { useOAuthConsent } from '@owlmeans/web-oauth'
109
+
110
+ export const MyConsentScreen: RoutedComponent = () => {
111
+ const context = useContext()
112
+ const [query] = context.router().useSearchParams()
113
+ const { stage, view, errorKind, approve, deny, switchAccount } = useOAuthConsent(query.get('ref'))
114
+
115
+ if (stage === 'error') return <p role="alert">{errorKind}</p>
116
+ if (stage !== 'ready' && stage !== 'deciding' || view == null) return <p>…</p>
117
+
118
+ return (
119
+ <div>
120
+ <h1>{view.client.name}</h1>
121
+ <p>{view.scopes.join(', ')}</p>
122
+ <button disabled={stage === 'deciding'} onClick={() => void deny()}>Deny</button>
123
+ <button disabled={stage === 'deciding'} onClick={() => void approve()}>Approve</button>
124
+ <button onClick={() => void switchAccount()}>Use another account</button>
125
+ </div>
126
+ )
127
+ }
128
+ ```
129
+
130
+ `approve()` on a code grant sets `window.location.href` to the server's redirect; on a device grant
131
+ it navigates to the done screen with `kind` / `ref` in the query.
132
+
133
+ ## API
134
+
135
+ | Symbol | Kind | Purpose |
136
+ |---|---|---|
137
+ | `oauthEntrypoints(opts?)` | function | `[consentScreen, deviceScreen, doneScreen]` bound with `bindScreen`; `opts` is `OAuthEntrypointOptions` (`parent?`, `path?`, `guard?`) |
138
+ | `appendOAuthScreens(ctx)` | function | Throws `SyntaxError` unless `FLOW_STATE` is registered; returns the context |
139
+ | `useOAuthConsent(ref, aliases?)` | hook | `UseOAuthConsent` — the consent screen's logic |
140
+ | `OAuthConsentScreen` | component | Reads `?ref=`, signs in if needed, shows the request, approve / deny / switch account |
141
+ | `OAuthDeviceScreen` | component | Code input; with `?user_code=` it proceeds without rendering |
142
+ | `OAuthDoneScreen` | component | Final "you may close this window"; `?kind=device` picks the device message |
143
+ | `OAUTH_I18N` | const | `'oauth'` — the library-tier i18n resource |
144
+ | `OAUTH_SUSPEND_TTL_MS` | const | `600000` — how long a suspended sign-in stays valid |
145
+ | `UseOAuthConsent` | type | `{ stage, view, error, errorKind, approve, deny, switchAccount }` |
146
+ | `ConsentStage` | type | `checking` \| `signing-in` \| `loading` \| `ready` \| `deciding` \| `done` \| `error` |
147
+ | `ConsentErrorKind` | type | `missing` \| `not-found` \| `expired` \| `forbidden` \| `failed` |
148
+
149
+ `error` is the raw message for logging; `errorKind` is derived from the wire markers
150
+ `request-expired`, `request-not-found` and `forbidden`.
151
+
152
+ ## Screen behaviour
153
+
154
+ - **Consent** shows the client name and how it is known (`cimd` host, `dcr` unverified, or known),
155
+ the device code and name, the redirect host, a warning when the client only registered localhost
156
+ addresses, and the scopes. It refuses to render inside a frame (`window.top !== window.self`).
157
+ - Every failure is one translated sentence (`error-missing`, `error-not-found`, `error-expired`,
158
+ `error-forbidden`, or the generic `error`); the wire text is never shown.
159
+ - **Use another account** calls `auth.update(undefined)` and goes through the same suspension.
160
+
161
+ ## i18n
162
+
163
+ Resource `oauth` with sections `consent`, `device` and `done`, shipped in `en`, `pl`, `ru`, `be`,
164
+ `uk`, `es`, `de` and `fr`. Translations register as a side effect of importing the package; an app
165
+ overrides a string at its own tier instead of forking a screen.
166
+
167
+ ## Testing
168
+
169
+ `data-testid` contract: `oauth-consent-card`, `oauth-consent-client`, `oauth-consent-code`,
170
+ `oauth-consent-approve`, `oauth-consent-deny`, `oauth-consent-switch`, `oauth-consent-error`,
171
+ `oauth-device-card`, `oauth-device-input`, `oauth-device-submit`, `oauth-done-card`.
172
+
173
+ `bun test ./tests` drives Chromium against a Vite harness built on the real `@owlmeans/web-client`
174
+ context; the consent API is answered with `page.route` stubs on the same origin.
175
+
176
+ ## Common pitfalls
177
+
178
+ - Missing the `@source` line — the screens render unstyled and nothing in the app's sources explains
179
+ why.
180
+ - Calling `appendOAuthScreens` before `appendFlowService` — it throws by design.
181
+ - Passing different `OAuthEntrypointOptions` to the server and the web app — the screens then call
182
+ consent routes the server never mounted.
183
+ - A sign-in plugin that navigates to `HOME` instead of resuming the flow landing breaks the return to
184
+ the consent screen silently.
185
+ - Renaming a `data-testid` breaks the end-to-end suites that drive sign-in.
186
+ - Importing primitives through the app's `@/` alias inside package source; `tests/package-boundary.spec.ts`
187
+ fails on it.
188
+ - A key added to `en.json` must be added to every language file; `tests/i18n.spec.ts` checks parity.
189
+
190
+ The `web-oauth` skill covers the sign-in leg, the screens and the test harness in more depth; the
191
+ `oauth` and `server-oauth` skills cover the shared contracts and the server.
192
+
193
+ ## Related packages
194
+
195
+ - [`@owlmeans/oauth`](../oauth) — protocols, `oauthFlow`, `ConsentView`, `OAuthEntrypointOptions`
196
+ - [`@owlmeans/server-oauth`](../server-oauth) — authorization server and the consent API handlers
197
+ - [`@owlmeans/web-flow`](../web-flow) — `appendFlowService`, which registers `FLOW_STATE`
198
+ - [`@owlmeans/client-flow`](../client-flow) — `flowLandingOf`, suspend and resume
199
+ - [`@owlmeans/client-entrypoint`](../client-entrypoint) — `bindScreen`, `bindAll`
200
+ - [`@owlmeans/web-client`](../web-client) — the web context the screens run in
201
+ - [`@owlmeans/web-auth-token`](../web-auth-token) — personal access token panel with the same hook/component split
202
+
1
203
  <!-- owlmeans:agent-guidance:start -->
2
204
  ## Agent guidance
3
205
 
@@ -6,7 +208,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
6
208
  your project's skill store (`.agents/skills/`):
7
209
 
8
210
  ```sh
9
- npx @owlmeans/agent-skills@^0.1.18-rc.51
211
+ npx @owlmeans/agent-skills@^0.1.18-rc.53
10
212
  ```
11
213
 
12
214
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-oauth",
4
- "version": "0.1.18-rc.22",
5
- "generatedAt": "2026-10-06T11:45:57.678Z",
4
+ "version": "0.1.18-rc.24",
5
+ "generatedAt": "2026-10-09T11:30:42.758Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/web-oauth
9
9
 
10
10
  **Layer:** Web (React, shadcn + Tailwind v4)
11
- **Install:** `"@owlmeans/web-oauth": "^0.1.18-rc.22"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-oauth": "^0.1.18-rc.24"` in `dependencies`
12
12
  **Contracts:** `@owlmeans/oauth` — aliases, `makeOAuthProtocols`, `oauthFlow`, `ConsentView`
13
13
  **Server half:** `@owlmeans/server-oauth` (its `consentUrl`/`deviceUrl` must point at these screens)
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-oauth",
3
- "version": "0.1.18-rc.22",
3
+ "version": "0.1.18-rc.24",
4
4
  "license": "MIT",
5
5
  "description": "The browser half of @owlmeans/oauth — the consent, device-code and done screens an OAuth 2.1 sign-in ends on, driven by the shared consent flow, plus the entrypoints and app-config helper that put them on a web app with no wiring beyond configuration.",
6
6
  "type": "module",
@@ -23,19 +23,19 @@
23
23
  }
24
24
  },
25
25
  "dependencies": {
26
- "@owlmeans/auth-common": "^0.1.18-rc.48",
27
- "@owlmeans/client": "^0.1.18-rc.52",
28
- "@owlmeans/client-auth": "^0.1.18-rc.59",
29
- "@owlmeans/client-context": "^0.1.18-rc.49",
30
- "@owlmeans/client-entrypoint": "^0.1.18-rc.48",
31
- "@owlmeans/client-flow": "^0.1.18-rc.55",
32
- "@owlmeans/client-i18n": "^0.1.18-rc.51",
26
+ "@owlmeans/auth-common": "^0.1.18-rc.49",
27
+ "@owlmeans/client": "^0.1.18-rc.53",
28
+ "@owlmeans/client-auth": "^0.1.18-rc.61",
29
+ "@owlmeans/client-context": "^0.1.18-rc.50",
30
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.49",
31
+ "@owlmeans/client-flow": "^0.1.18-rc.56",
32
+ "@owlmeans/client-i18n": "^0.1.18-rc.52",
33
33
  "@owlmeans/context": "^0.1.18-rc.34",
34
34
  "@owlmeans/entrypoint": "^0.1.18-rc.41",
35
35
  "@owlmeans/error": "^0.1.18-rc.38",
36
36
  "@owlmeans/flow": "^0.1.18-rc.45",
37
37
  "@owlmeans/i18n": "^0.1.18-rc.39",
38
- "@owlmeans/oauth": "^0.1.18-rc.16",
38
+ "@owlmeans/oauth": "^0.1.18-rc.17",
39
39
  "@owlmeans/route": "^0.1.18-rc.35"
40
40
  },
41
41
  "peerDependencies": {
@@ -50,10 +50,10 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@owlmeans/dep-config": "workspace:*",
53
- "@owlmeans/test-auth": "^0.1.18-rc.47",
54
- "@owlmeans/test-ui": "^0.1.18-rc.50",
55
- "@owlmeans/web-client": "^0.1.18-rc.62",
56
- "@owlmeans/web-flow": "^0.1.18-rc.54",
53
+ "@owlmeans/test-auth": "^0.1.18-rc.48",
54
+ "@owlmeans/test-ui": "^0.1.18-rc.51",
55
+ "@owlmeans/web-client": "^0.1.18-rc.64",
56
+ "@owlmeans/web-flow": "^0.1.18-rc.55",
57
57
  "@radix-ui/react-label": "^2.1.15",
58
58
  "@radix-ui/react-slot": "^1.3.3",
59
59
  "@tailwindcss/vite": "^4.3.3",
@@ -0,0 +1 @@
1
+ declare module '*.css'