@crvouga/mockingbird-service-oauth 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog — @crvouga/mockingbird-service-oauth
2
+
3
+ ## 0.1.0 (2026-09-22)
4
+
5
+ Initial release.
package/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # @crvouga/mockingbird-service-oauth
2
+
3
+ A portable, stateful OAuth 2.0 / OpenID Connect identity sandbox. Google, Apple, Microsoft and GitHub wire profiles share a vendor-neutral account chooser, signup and consent UI. Generic OIDC works with other configurable identity clients. Uses real RS256 signatures, discovery, JWKS, authorization codes, S256 PKCE, refresh tokens and revocation.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @crvouga/mockingbird-service-oauth
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { createRuntime } from "@crvouga/mockingbird-service-oauth"
15
+
16
+ const identity = createRuntime({
17
+ provider: "google", // "apple", "microsoft", "github" or "oidc"
18
+ accounts: [
19
+ { id: "ada", email: "ada@example.test", name: "Ada Lovelace" },
20
+ { id: "grace", email: "grace@example.test", name: "Grace Hopper" },
21
+ ],
22
+ clients: [{
23
+ id: "my-app",
24
+ name: "My application",
25
+ secret: "local-test-client-secret",
26
+ redirectUris: ["http://localhost:3000/auth/callback"],
27
+ }],
28
+ })
29
+
30
+ const discovery = await identity.fetch(
31
+ new Request("http://localhost:8810/.well-known/openid-configuration"),
32
+ )
33
+ console.log(await discovery.json())
34
+
35
+ // The same interface can be mounted in Bun, Deno, a worker or an HTTP adapter.
36
+ // Browser execution needs a secure context for Web Crypto.
37
+ const fetchHandler = (request: Request) => identity.fetch(request)
38
+ void fetchHandler
39
+ ```
40
+
41
+ Serve from Node (also works in Bun):
42
+
43
+ ```ts
44
+ import { createServer } from "@crvouga/mockingbird-service-oauth/server"
45
+
46
+ const server = await createServer({ port: 8810, provider: "apple" })
47
+ console.log(server.url)
48
+ // Register clients and seed accounts through /__admin, or pass them to createServer.
49
+ await server.close()
50
+ ```
51
+
52
+ ```sh
53
+ npx mockingbird-oauth serve --provider google --port 8810
54
+ ```
55
+
56
+ ### Point an app at the mock
57
+
58
+ Override the authorization, token, userinfo and JWKS endpoints in your application's OAuth provider configuration. Set its expected issuer to the mock's public base URL. Discovery is at `/.well-known/openid-configuration` for OIDC profiles; GitHub uses explicit OAuth endpoints and does not issue ID tokens. Register the **exact** callback URL (including scheme, port, path and query); wildcard callbacks are not accepted. No outgoing requests to a vendor occur.
59
+
60
+ | Profile | Authorization | Token | JWKS | Userinfo |
61
+ | --- | --- | --- | --- | --- |
62
+ | `google` | `/o/oauth2/v2/auth` | `/token` | `/oauth2/v3/certs` | `/v1/userinfo` |
63
+ | `apple` | `/auth/authorize` | `/auth/token` | `/auth/keys` | None, as with Apple |
64
+ | `microsoft` | `/oauth2/v2.0/authorize` | `/oauth2/v2.0/token` | `/discovery/v2.0/keys` | `/oidc/userinfo` |
65
+ | `github` | `/login/oauth/authorize` | `/login/oauth/access_token` | Not an OIDC provider | `/user`, `/user/emails` |
66
+ | `oidc` | `/authorize` | `/token` | `/jwks` | `/userinfo` |
67
+
68
+ `/authorize`, `/token`, `/jwks`, `/revoke` are common aliases. Google also accepts `/o/oauth2/auth` and `/oauth2/v3/userinfo`; Apple revocation is `/auth/revoke`. Token and revocation requests use `application/x-www-form-urlencoded`. Token client authentication supports Basic, body credentials, and public clients. Public clients must use S256 PKCE; confidential clients can opt in with `requirePkce: true`. Google mock client secrets are fixture strings configured on the client. Apple clients can use either a fixture string or `apple: { teamId, keyId, publicKey }`, where `publicKey` is an EC P-256 public JWK. In JWT mode the mock verifies the ES256 signature, key ID, team, subject, Apple audience, issue/expiry times and maximum lifetime. The application can keep generating its usual Apple client-secret JWTs with the corresponding test private key.
69
+
70
+ For example, an Auth.js-style OIDC provider can use `type: "oidc"`, `issuer: "http://localhost:8810"`, `clientId`, `clientSecret`, and `checks: ["pkce", "state"]`. For existing Google/Apple presets, override **all** remote endpoints and issuer validation; changing the authorization URL alone is insufficient. In-process HTTP clients can route requests to `identity.fetch`. Browser navigation must reach a served mock or a service worker that routes those requests.
71
+
72
+ The issuer defaults to the incoming origin (and `/ns/<name>` when used). Set `issuer` to the public URL behind a reverse proxy; it may include a mount path. Avoid a fixed issuer shared across namespaces: use the namespace URL and its own discovery/JWKS so each namespace remains an independent issuer.
73
+
74
+ ### Multiple providers on one listener
75
+
76
+ `createMultiRuntime({ mounts })` and `createMultiServer({ mounts })` mount independent providers at exact paths on one origin. Discovery, endpoint URLs, token issuers, signing keys, clients, grants, sessions, faults, journals, and namespace state remain isolated per mount.
77
+
78
+ ```ts
79
+ import { createMultiServer } from "@crvouga/mockingbird-service-oauth/server"
80
+
81
+ const accounts = [{ id: "ada", name: "Ada Lovelace", email: "ada@example.test" }]
82
+ const clients = [
83
+ { id: "app", name: "Example app", redirectUris: ["http://localhost:3000/callback"] },
84
+ ]
85
+ const server = await createMultiServer({
86
+ mounts: [
87
+ { path: "/google", provider: "google", clients, accounts },
88
+ { path: "/apple", provider: "apple", clients, accounts },
89
+ { path: "/oauth2", provider: "microsoft", clients, accounts },
90
+ ],
91
+ })
92
+ ```
93
+
94
+ Use `/ns/<name>/<mount>/…` for URL-selected namespaces. `GET /health` reports every mount; `POST /__admin/reset?all=1` resets them atomically. Other aggregate admin requests select a runtime with `?mount=/google`, while mount-scoped controls are also available at `/google/__admin/*`. Duplicate or unsafe mount paths, duplicate explicit issuers, and duplicate client/key IDs within a mount fail before the listener starts.
95
+
96
+ For the CLI, put the same `mounts` array in a JSON file and run `npx mockingbird-oauth serve --mounts oauth-mounts.json --port 8810`.
97
+
98
+ ### Accounts and signup
99
+
100
+ The chooser displays seeded, enabled test accounts. Choosing an account opens explicit consent; creating an account validates the email/name, rejects duplicate email addresses, persists the identity and opens the same consent flow. This is intentionally passwordless test identity selection; never use real passwords or personal data.
101
+
102
+ ```sh
103
+ curl http://localhost:8810/__admin/clients -H 'content-type: application/json' \
104
+ -d '{"id":"app","name":"Example app","secret":"fixture-secret","redirectUris":["http://localhost:3000/callback"]}'
105
+ curl http://localhost:8810/__admin/accounts -H 'content-type: application/json' \
106
+ -d '{"id":"ada","email":"ada@example.test","name":"Ada Lovelace"}'
107
+ curl http://localhost:8810/__admin/accounts
108
+ ```
109
+
110
+ Set `adminKey` (CLI `--admin-key`) to require `x-mockingbird-admin-key`. Programmatically, `runtime.instance().seedAccount(account)` inserts or updates a stable subject; `registerClient(client)` inserts or updates a client. Accounts support `emailVerified`, `picture`, `givenName`, `familyName`, `locale`, `hostedDomain`, `privateEmail`, `relayEmail`, `omitEmail`, `omitName` and `disabled`. Apple fixtures also accept `realUserStatus` (`0`, `1`, or `2`) and `transferSub` for risk and app-transfer claim tests. Microsoft fixtures accept `preferredUsername`, `tenantId`, `objectId`; GitHub fixtures accept `github: { id, login, publicEmail, emails }`. An email-list entry contains `email`, `primary`, `verified` and `visibility` (`public`, `private` or `null`).
111
+
112
+ ### Fidelity and lifecycle
113
+
114
+ - Authorization-code flow with exact redirect matching, including explicitly registered private-use URI schemes for native apps, state and nonce; duplicate parameters rejected. Invalid clients/callbacks never redirect. Public native clients require S256 PKCE.
115
+ - Real RSA-2048 / RS256 ID tokens and independent public JWKS; correct issuer, audience, expiry, auth time and scope-filtered claims. Keys remain stable until explicitly rotated.
116
+ - Codes expire after 5 minutes and are consumed atomically, including concurrent PKCE redemption. Access/ID tokens last 1 hour. Generic OIDC refresh tokens default to 30 days; Microsoft defaults to 90 days. Apple/Google refresh tokens have no fixed deadline by default; Google inactivity, testing mode and issuance limits still apply. GitHub OAuth app access tokens have no fixed deadline in the mock. The injected mock clock controls expiry.
117
+ - Google `access_type=offline` issues refresh tokens on first consent or `prompt=consent`; generic/Microsoft `offline_access` and Apple issue refresh tokens. Refresh cannot expand scopes. Revocation invalidates related access and refresh tokens; unknown tokens succeed idempotently. Refresh tokens are reusable by default; Microsoft returns a replacement without invalidating the old token. Opt-in strict rotation detects reuse and revokes the token family.
118
+ - `prompt=none` returns `login_required` or `consent_required`; `login`, `consent`, `select_account`, `login_hint` and `max_age` are supported. HttpOnly, SameSite=Lax browser sessions last 24 hours. Cancel returns `access_denied` with state.
119
+ - Apple supports `code id_token`, `c_hash`, `form_post` with an automatic POST and a no-JavaScript Continue button, string `email_verified` / `is_private_email`, no userinfo endpoint, and first-consent-only `user` data. `name` / `email` scopes require `form_post`.
120
+ - Semantic server-rendered HTML needs no frontend framework, hydration, external fonts, images or network assets. Native forms, labelled fields, visible focus rings, a skip link, error announcements, responsive layout, reduced-motion preference and automatic system light/dark colors are included.
121
+
122
+ ### Reproducible provider edge cases
123
+
124
+ Behavioral randomness is **off by default**. Configure exact scenarios or probabilities; these are test frequencies you choose, not estimates of vendor incidence. OAuth credentials, authorization codes and signing keys always use cryptographic randomness.
125
+
126
+ ```ts
127
+ import { createRuntime } from "@crvouga/mockingbird-service-oauth"
128
+
129
+ const identity = createRuntime({
130
+ provider: "apple",
131
+ seed: "signup-regression-42",
132
+ behavior: {
133
+ probabilities: {
134
+ hideEmail: 0.5,
135
+ omitEmail: 0.1,
136
+ omitName: 0.1,
137
+ denyConsent: 0.05,
138
+ tokenUnavailable: 0.1,
139
+ },
140
+ },
141
+ })
142
+
143
+ // Force one case instead. Configuration replaces the old behavior and restarts its sequence.
144
+ identity.instance().configureBehavior({ preset: "apple_private_relay" })
145
+ console.log(identity.instance().behavior.events) // outcomes only; no tokens or account details
146
+ ```
147
+
148
+ Identity/consent decisions are sampled once per authorization and kept with the grant, including refresh. Token failures are sampled per token attempt: a transient 503 preserves the code for retry and includes `Retry-After`. The seed, configuration and same ordered requests reproduce the outcomes. The decision cursor, recent 100 events, identities, consent and token state participate in namespace snapshots; reset returns to constructor configuration. Signup subjects remain random: seed stable account IDs for identical relay addresses across runs.
149
+
150
+ | Provider | Modeled behavior and controls |
151
+ | --- | --- |
152
+ | Apple | First consent offers keyboard-accessible Share/Hide My Email radio buttons. Hidden email becomes a stable `@privaterelay.appleid.com` alias in **both** callback `user` and ID tokens, including refresh. It never merely flips the privacy flag. `account.relayEmail` sets an explicit alias. The choice persists until consent revocation. `apple.emailMode: "hide" / "share"` fixes the initial choice; `"choose"` lets the user choose. |
153
+ | Apple | `user` is returned once; later ID tokens still include email when the email scope was granted. An `openid`-only grant does not leak email or privacy claims. `apple.omitUser` simulates an already-authorized app. `apple.booleanClaims` selects string or boolean verification/privacy claims, including string `"false"`. Subjects and relay addresses are grouped by `client.subjectGroup`, then Apple team ID, then client ID; grouped apps share first-use disclosure state. `realUserStatus` and `transferSub` fixtures cover Apple risk and app-transfer claims. |
154
+ | Google | Refresh tokens normally appear only on first consent or explicit consent. `google.refreshToken` selects `first-consent`, `always`, or `never`. `include_granted_scopes=true` combines prior grants; `consent.deniedScopes` models partial consent. Userinfo scope URL aliases are accepted. Hosted-domain claims remain distinct from an email suffix. |
155
+ | Google | `google.testing=true` expires refresh tokens in seven days **only when non-basic scopes are requested**. `google.maxRefreshTokens` defaults to 100 per account/client and evicts the oldest refresh token. Six calendar months without use expires a refresh token. `tokens.refreshError: "invalid_rapt"` returns the reauthentication error subtype; `invalid_grant` revokes the family. |
156
+ | Microsoft | Client-scoped subject plus `oid`, `tid` and mutable `preferred_username`; fixtures can omit email even when requested. Refresh returns a replacement while retaining the old token. Use `microsoft_spa_expiry` for a 24-hour refresh window. Supply real-shaped tenant/object fixture IDs when the app validates UUIDs. |
157
+ | GitHub | OAuth app endpoints, JSON or form token responses, no ID token, and a nullable `/user.email` even with email scope. `/user/emails` returns primary/secondary and verified/unverified addresses and requires `user:email` or `user`. Resource responses expose `X-OAuth-Scopes` and `X-Accepted-OAuth-Scopes`. An unverified primary account fails token exchange with `unverified_user_email`. Incorrect credentials/code/redirect produce GitHub error names. |
158
+ | Any | Missing/unverified email, missing names, denied consent, partial scopes, configurable token/code expiry, transient token failures, revoked grants, strict refresh rotation/reuse detection, and signing-key rotation. Non-Apple account email changes retain the subject. Additional scopes can be accepted via `additionalScopes`; associated resource APIs are not implied. |
159
+
160
+ The complete typed controls are `OAuthBehavior`. `probabilities` accepts `hideEmail`, `omitEmail`, `omitName`, `unverifiedEmail`, `denyConsent`, `tokenUnavailable`, and `invalidGrant`, each in `[0,1]`. Static `claims` flags force omissions or unverified email. `consent.error` supports `access_denied`, `interaction_required`, or `temporarily_unavailable`. `tokens` accepts positive integer `accessTtlSeconds`, `codeTtlSeconds`, `refreshTtlSeconds`, `refreshRotation: "reuse" | "rotate"`, and `refreshError`. These controls are local testing overrides, not claims that all providers implement every variation.
161
+
162
+ `OAUTH_SCENARIOS` supplies: `apple_private_relay`, `apple_share_email`, `apple_returning_user`, `apple_boolean_claims`, `microsoft_missing_email`, `microsoft_spa_expiry`, `github_unverified_email`, `missing_email`, `missing_name`, `unverified_email`, `google_no_refresh_token`, `google_reauthentication`, `revoked_refresh_token`, `rotating_refresh_tokens`, `short_lived_tokens`, `consent_denied`, `intermittent_token_failure`. Explicit fields override the chosen preset's fields. Unknown keys and invalid values fail validation.
163
+
164
+ ```sh
165
+ npx mockingbird-oauth serve --provider apple --seed regression-42 --scenario apple_private_relay
166
+ curl http://localhost:8810/__admin/scenarios
167
+ curl -X PUT http://localhost:8810/__admin/behavior -H 'content-type: application/json' \
168
+ -d '{"preset":"apple_private_relay","probabilities":{"omitName":0.25}}'
169
+ curl http://localhost:8810/__admin/behavior
170
+ curl -X POST http://localhost:8810/__admin/consents/revoke -H 'content-type: application/json' \
171
+ -d '{"clientId":"app","accountId":"ada"}'
172
+ curl -X POST http://localhost:8810/__admin/keys/rotate -H 'content-type: application/json' \
173
+ -d '{"retainPrevious":true}'
174
+ ```
175
+
176
+ These routes use the shared admin-key and namespace controls. `revokeConsent(clientId, accountId)` removes that client's grants and resets first-use disclosure; it does not disable the account. `rotateSigningKey(true)` retains up to four previous public keys so existing tokens still verify; `false` withdraws them to test stale JWKS caches. Keys themselves are not included in snapshots, so restoring state does not undo a key rotation.
177
+
178
+ ### Shared service controls
179
+
180
+ The runtime supplies `/health`, `/__admin/reset`, snapshots, mock clock, request journal, metrics, fault injection and namespace isolation. Use `x-mockingbird-namespace` for in-process tests or `/ns/<name>/…` for complete browser flows. Header-selected namespaces alone cannot persist across ordinary browser navigation. State, grants, sessions and consent live in the shared SQLite abstraction; there are no filesystem or Node imports in the main entry.
181
+
182
+ `OAUTH_PRESETS` includes `token_unavailable` and `access_denied`. Fault rules can also target a provider-specific path, e.g. `POST /__admin/faults` with `{"pathPrefix":"/auth/token","status":503,"body":{"error":"temporarily_unavailable"}}`. No outbound webhooks are modeled. Journals contain request metadata, never passwords or request bodies.
183
+
184
+ ## API
185
+
186
+ - `createRuntime(options?)`: shared service runtime; `fetch`, `instance`, `reset`, `snapshot`, `restore`, clock, faults and journals.
187
+ - `createMultiRuntime({ mounts })`: exact-path dispatcher for isolated provider runtimes on one origin, with aggregate health and admin controls.
188
+ - `OAuthAPI`: standalone portable handler with `fetch`, `reset`, `seedAccount`, `registerClient`, `accounts`, `clients`, `provider`, `configureBehavior`, `behavior`, `revokeConsent`, `rotateSigningKey`.
189
+ - `OAUTH_PRESETS`: named transport fault presets.
190
+ - `OAUTH_SCENARIOS`: named provider-behavior scenarios.
191
+ - `document`, `operationIds`, `supportedOperationIds`: generated OpenAPI metadata.
192
+ - `createServer(options?)` from `./server`: Node HTTP adapter, returning `url`, `close` and `runtime`.
193
+ - `createMultiServer({ mounts })` from `./server`: Node HTTP adapter for a multi-provider runtime.
194
+ - `DEFAULT_PORT`, `serveTarget` from `./server`: CLI defaults and multi-service launcher integration.
195
+ - Types: `Account`, `Client`, `Provider`, `OAuthAPIOptions`, `OAuthRuntimeOptions`, `OAuthRuntime`, `OAuthServerOptions`, `OAuthMount`, `OAuthMultiRuntimeOptions`, `OAuthMultiRuntime`, `OAuthMultiServerOptions`, `OAuthBehavior`, `BehaviorInput`, `OAuthScenario`, `EdgeCase`, `BehaviorEvent`.
196
+
197
+ ## Verification
198
+
199
+ `bun test` covers protocol security, every published behavior scenario, all provider profiles,
200
+ independent JOSE verification, an unmodified `oauth4webapi` client, the complete in-process app,
201
+ and randomized self-parity across provider, privacy, omission, and verification combinations.
202
+ `bun run parity` safely checks current public discovery and JWKS contracts against Google, Apple,
203
+ and Microsoft, plus GitHub's unauthenticated REST error shape. It needs no credentials and makes
204
+ no grants or account changes. Interactive vendor flows cannot be run unattended without owned
205
+ provider applications, so the focused contract tests use the official behavior documented in the
206
+ references below.
207
+
208
+ ## Deliberately not modelled
209
+
210
+ This is a ready-to-use local/test identity provider, **not a production authentication server or a claim that every proprietary provider feature is implemented**. Its ready tier covers the documented OAuth/OIDC login, identity, consent, token, provider-edge-case, and UI surface. Applications should still run a small final check against each real provider before release.
211
+
212
+ Vendor-hosted Google Identity Services/One Tap, native Apple AuthenticationServices, passkeys, MFA, CAPTCHA, password recovery, email delivery/relay forwarding, app-transfer migration, vendor risk engines, tokeninfo/introspection, logout, GitHub Apps installation/device flows, and Microsoft Graph/tenant administration are not implemented. Other configurable OIDC providers can use the generic profile, but their proprietary scopes and claims are not emulated. Scopes are limited to each profile plus explicitly configured additional scopes. Microsoft uses the configured mock issuer, not real Entra tenant routing. GitHub is the OAuth app login surface, not the full REST API.
213
+
214
+ No implicit flow, dynamic client registration, wildcard redirect matching, cross-origin browser token CORS policy, persistent signing-key import, or distributed-session coordination is provided. Private-use redirect schemes work only when their complete URI is explicitly registered; executable/local schemes such as `javascript:`, `data:` and `file:` are rejected. Snapshot restore is for the same runtime/instance; signing keys are not serialized. The default backing store is in-memory and state disappears when the process exits. A secure browser context and Web Crypto, Fetch and standard Web APIs are required; Node 22+, Bun and modern browsers provide them.
215
+
216
+ References: [Google OpenID Connect](https://developers.google.com/identity/openid-connect/openid-connect), [Apple authorization request](https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server.), and [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html).
217
+
218
+ Provider references for the edge cases: [Apple first-use profile data](https://developer.apple.com/documentation/signinwithapple/configuring-your-webpage-for-sign-in-with-apple), [Apple token response](https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse), [Google consent and refresh](https://developers.google.com/identity/protocols/oauth2/web-server), [Google expiry and limits](https://developers.google.com/identity/protocols/oauth2#expiration), [Microsoft claims](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference), [Microsoft refresh behavior](https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens), [GitHub OAuth](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps), [GitHub token errors](https://docs.github.com/en/apps/oauth-apps/maintaining-oauth-apps/troubleshooting-oauth-app-access-token-request-errors).
219
+
220
+ ## Interactive application example
221
+
222
+ The [OAuth service page](https://mockingbird.chrisvouga.dev/services/oauth#example-google-login)
223
+ includes complete in-process Google-style, Apple-style, Microsoft-style, and GitHub-style login
224
+ profiles.
225
+ Launch the example app, select a seeded account or create one, approve consent, and return to
226
+ a signed-in app. Apple mode exercises real `form_post`, first-use name disclosure, Share My
227
+ Email, and stable Hide My Email relay addresses. Switch scenarios to exercise missing identity
228
+ fields, declined consent, boolean/string claims, returning Apple users, a GitHub account whose
229
+ public email is null, or a failing token endpoint. Reset creates fresh, isolated app and provider
230
+ state.
231
+
232
+ The service-owned source lives in `examples/google-login/`: a real Hono app uses
233
+ `oauth4webapi` to discover OIDC providers, generate PKCE/state/nonce, exchange the code, verify
234
+ JWT signatures against JWKS, fetch identity data, and establish a session. GitHub mode uses its
235
+ explicit OAuth endpoints, numeric account ID, and `/user/emails` fallback. Popup mode keeps the host app visible while a separate sign-in window renders the mock's
236
+ actual HTML response. Redirect mode replaces the preview with the provider document and
237
+ returns to the app on callback. The popup closes on callback and the app updates with the result. Browsers that
238
+ block new windows use a separate modal dialog with its own provider document. Closing the
239
+ popup or pressing Escape returns focus to the app without completing sign-in. Native forms use an
240
+ in-memory Fetch dispatcher. The request trace exposes the protocol without showing credentials.
241
+ Both the app and its cookie/redirect transport run without DOM APIs; only the mounting
242
+ component needs a browser. No authentication request leaves the process.
243
+
244
+ For an entirely browser-hosted Fetch dispatcher, configure
245
+ `cookieHeaders: { request: "x-example-cookie", response: "x-example-set-cookie" }`.
246
+ Browser Fetch strips the standard `Cookie` and `Set-Cookie` headers from synthetic objects;
247
+ this explicit local mapping lets an in-process cookie jar preserve sessions. The example
248
+ uses it for both Hono and the provider. Leave it unset for normal HTTP serving, which uses
249
+ standard cookie headers. This mapping is a transport detail, not a browser cookie-policy emulator.
250
+
251
+
252
+ ### Presentation and account selection
253
+
254
+ The provider UI is neutral and labelled **OAuth Mock**, with no vendor or product branding.
255
+ Its **System / Light / Dark** controls work on standalone HTML pages; the selection persists
256
+ across pages in that browser session. The in-process example bridges the same controls into
257
+ its sandboxed documents. System mode follows the operating system, independently of the
258
+ docs site's selected theme.
259
+
260
+ The example toolbar configures **Popup / Redirect**, appearance, and **Always choose / Reuse
261
+ last account**. Changing these settings does not clear accounts or existing consent. Only
262
+ **Reset example** (or switching a failure scenario) creates a fresh app and provider.
263
+ Initial values can also be passed to the component:
264
+
265
+ ```js
266
+ import { mount } from "./examples/google-login/index.js"
267
+ const dispose = await mount(host, {
268
+ flow: "redirect", // default: "popup"
269
+ theme: "system", // also "light" or "dark"
270
+ reuseLastAccount: false, // default: always show the chooser
271
+ })
272
+ ```
273
+
274
+ Popup versus redirect is an application presentation choice; both use the same authorization
275
+ endpoint and callback validation. The demo uses `prompt=select_account` to force the chooser.
276
+ For any integrating app, session reuse can also be disabled on the mock itself:
277
+
278
+ ```ts
279
+ import { OAuthAPI } from "@crvouga/mockingbird-service-oauth"
280
+
281
+ const api = new OAuthAPI({
282
+ behavior: { session: { reuseLastAccount: false } },
283
+ })
284
+ ```
285
+
286
+ The provider default is `true` to emulate normal social login. `false` prevents automatic
287
+ account selection and makes `prompt=none` return `login_required`. `prompt=select_account`
288
+ always forces interactive choice, regardless of this setting. The same configuration can be
289
+ changed with the behavior admin endpoint and is included in snapshots.