@owlmeans/web-oauth 0.1.18-rc.21 → 0.1.18-rc.23
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 +203 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-oauth/SKILL.md +1 -1
- package/package.json +13 -13
- package/tests/harness/css.d.ts +0 -1
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.23
|
|
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.
|
|
211
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.52
|
|
10
212
|
```
|
|
11
213
|
|
|
12
214
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-oauth",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.23",
|
|
5
|
+
"generatedAt": "2026-10-08T21:27:54.314Z",
|
|
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.
|
|
11
|
+
**Install:** `"@owlmeans/web-oauth": "^0.1.18-rc.23"` 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.
|
|
3
|
+
"version": "0.1.18-rc.23",
|
|
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.
|
|
27
|
-
"@owlmeans/client": "^0.1.18-rc.
|
|
28
|
-
"@owlmeans/client-auth": "^0.1.18-rc.
|
|
29
|
-
"@owlmeans/client-context": "^0.1.18-rc.
|
|
30
|
-
"@owlmeans/client-entrypoint": "^0.1.18-rc.
|
|
31
|
-
"@owlmeans/client-flow": "^0.1.18-rc.
|
|
32
|
-
"@owlmeans/client-i18n": "^0.1.18-rc.
|
|
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.60",
|
|
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.
|
|
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.
|
|
54
|
-
"@owlmeans/test-ui": "^0.1.18-rc.
|
|
55
|
-
"@owlmeans/web-client": "^0.1.18-rc.
|
|
56
|
-
"@owlmeans/web-flow": "^0.1.18-rc.
|
|
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.63",
|
|
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",
|
package/tests/harness/css.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare module '*.css'
|