@crvouga/mockingbird-service-oauth 1.0.2 → 1.2.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 CHANGED
@@ -1,6 +1,27 @@
1
1
  # Changelog — @crvouga/mockingbird-service-oauth
2
2
 
3
- ## 1.0.2 (2026-09-26)
3
+ ## 1.2.0 (2026-09-29)
4
+
5
+ ### Features
6
+
7
+ - add id token faults for issuer clock skew and unpublished signing key ([7a014f2](https://github.com/crvouga/mockingbird/commit/7a014f2d0e5beeb458a056f092b1b62ccbf504d4))
8
+ - send Sign in with Apple server-to-server notifications ([681348e](https://github.com/crvouga/mockingbird/commit/681348e5ef0b4cf87067f013e3d0703339d608c1))
9
+
10
+ ## 1.1.0 (2026-09-29)
11
+
12
+ ### Features
13
+
14
+ - stable data-testid hooks on the chooser, signup, consent and form_post pages ([43f687a](https://github.com/crvouga/mockingbird/commit/43f687afc421c9f9e96d1b60760622261ae110df))
15
+ - let a scalar naming a later branch's enum value pick that branch ([30ade3a](https://github.com/crvouga/mockingbird/commit/30ade3a28a5b6b273ffbf261216b821acac8e961))
16
+
17
+ ### Fixes and improvements
18
+
19
+ - skip Google consent after the account chooser when scopes were already granted ([1b0d62a](https://github.com/crvouga/mockingbird/commit/1b0d62a7b14e870138952f17c40ba2b6e1384ddf))
20
+ - stop cleared webhook deliveries from rescheduling forever ([424b78f](https://github.com/crvouga/mockingbird/commit/424b78f29317b162e3bf7d0049cc19be5b977026))
21
+ - serialize immediate webhook attempts to fix reorder flake ([625c82b](https://github.com/crvouga/mockingbird/commit/625c82b5a8a7992c1cf93e9df68e77af08b353e3))
22
+ - flush drains webhook retries scheduled while it runs ([c58cede](https://github.com/crvouga/mockingbird/commit/c58cede1b6ba7ae5ed3f8721ad9b7cd7bc0b1ca7))
23
+
24
+ ## 1.0.2 (2026-09-25)
4
25
 
5
26
  ### Fixes and improvements
6
27
 
package/README.md CHANGED
@@ -97,7 +97,7 @@ For the CLI, put the same `mounts` array in a JSON file and run `npx mockingbird
97
97
 
98
98
  ### Accounts and signup
99
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.
100
+ The chooser displays seeded, enabled test accounts. Choosing an account opens explicit consent (Google skips it when that account already granted every requested scope to the client; `prompt=consent` or a new scope asks again); 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
101
 
102
102
  ```sh
103
103
  curl http://localhost:8810/__admin/clients -H 'content-type: application/json' \
@@ -109,6 +109,35 @@ curl http://localhost:8810/__admin/accounts
109
109
 
110
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
111
 
112
+ ### Test hooks on the interaction pages
113
+
114
+ The chooser, signup, consent, `form_post` and error pages carry stable `data-testid` hooks, so a UI
115
+ suite (Playwright's `getByTestId`, a driver that selects only by test id) can drive them without CSS
116
+ classes or visible copy. The ids are part of the documented surface and do not change between versions.
117
+ Each page has exactly one root (the page's card), and every interactive element has its own id.
118
+
119
+ | Page | Element | `data-testid` |
120
+ | --- | --- | --- |
121
+ | chooser | page root | `oauth-mock-chooser` |
122
+ | chooser | one wrapper per account (shared id, with `data-account-id` and `data-email`) | `oauth-mock-account` |
123
+ | chooser | each account's button, unique per account | `oauth-mock-account-<account id>` |
124
+ | chooser | "Create a new account" | `oauth-mock-create-account` |
125
+ | chooser | "Choose an available account" error (`role="alert"`) | `oauth-mock-chooser-error` |
126
+ | chooser, signup, consent | cancel / deny button | `oauth-mock-deny` |
127
+ | signup | page root | `oauth-mock-signup` |
128
+ | signup | full name / email inputs | `oauth-mock-signup-name`, `oauth-mock-signup-email` |
129
+ | signup | submit button | `oauth-mock-signup-submit` |
130
+ | signup | validation or duplicate-email error (`role="alert"`) | `oauth-mock-signup-error` |
131
+ | consent | page root | `oauth-mock-consent` |
132
+ | consent | Allow button | `oauth-mock-allow` |
133
+ | consent (Apple, default `emailMode: "choose"`) | Share / Hide My Email radios | `oauth-mock-share-email`, `oauth-mock-hide-email` |
134
+ | `form_post` | page root (the hand-off page) | `oauth-mock-form-post` |
135
+ | `form_post` | no-JavaScript Continue button inside the `#callback` form | `oauth-mock-form-post-continue` |
136
+ | expired or unknown sign-in | page root | `oauth-mock-error` |
137
+
138
+ `<account id>` is HTML-escaped. A driver that runs no page scripts finishes Sign in with Apple by
139
+ tapping `oauth-mock-form-post-continue`; with scripts, the page submits the form itself.
140
+
112
141
  ### Fidelity and lifecycle
113
142
 
114
143
  - 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.
@@ -157,9 +186,9 @@ Identity/consent decisions are sampled once per authorization and kept with the
157
186
  | 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
187
  | 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
188
 
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.
189
+ 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"`, `refreshError`, and two ID-token faults for testing a relying party's verification: `idTokenClockSkewSeconds` (an integer, forward or backward) sets `iat = now + skew`, `exp = iat + accessTtlSeconds` and offsets `auth_time` by the same skew, and `idTokenSigningKey: "unpublished"` signs ID tokens with a key whose `kid` no JWKS response ever contains, even after `rotateSigningKey`. Both apply to authorization-code, refresh-grant and `code id_token` ID tokens and compose with each other. These controls are local testing overrides, not claims that all providers implement every variation.
161
190
 
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.
191
+ `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`, `id_token_clock_ahead` (+600 s), `id_token_stale` (-7200 s, expired an hour ago), `id_token_unknown_key`. Explicit fields override the chosen preset's fields. Unknown keys and invalid values fail validation.
163
192
 
164
193
  ```sh
165
194
  npx mockingbird-oauth serve --provider apple --seed regression-42 --scenario apple_private_relay
@@ -169,17 +198,33 @@ curl -X PUT http://localhost:8810/__admin/behavior -H 'content-type: application
169
198
  curl http://localhost:8810/__admin/behavior
170
199
  curl -X POST http://localhost:8810/__admin/consents/revoke -H 'content-type: application/json' \
171
200
  -d '{"clientId":"app","accountId":"ada"}'
201
+ curl -X POST http://localhost:8810/__admin/accounts/delete -H 'content-type: application/json' \
202
+ -d '{"accountId":"ada"}'
203
+ curl -X POST http://localhost:8810/__admin/relay-forwarding -H 'content-type: application/json' \
204
+ -d '{"accountId":"ada","forwarding":false}'
172
205
  curl -X POST http://localhost:8810/__admin/keys/rotate -H 'content-type: application/json' \
173
206
  -d '{"retainPrevious":true}'
174
207
  ```
175
208
 
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.
209
+ 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. It is async because it can deliver a [server-to-server notification](#sign-in-with-apple-server-to-server-notifications). `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.
210
+
211
+ ### Sign in with Apple server-to-server notifications
212
+
213
+ Register an Apple client with `apple.notificationUrl` (an HTTP(S) URL; requires the JWT `apple` client form) and the mock behaves like Apple's [server-to-server notification endpoint](https://developer.apple.com/documentation/signinwithapplerestapi/processing-changes-for-sign-in-with-apple-accounts): it POSTs `{"payload": "<JWS>"}` as JSON. The JWS is signed by the current signing key (verify it against the JWKS) with `iss` = the issuer, `aud` = the client id, `iat` from the mock clock and a random `jti`. `events` is a JSON string of `{ type, sub, event_time }`, where `sub` is the client's identity for the account and `event_time` is milliseconds from the mock clock.
214
+
215
+ | Trigger | Event `type` | Sent to |
216
+ | --- | --- | --- |
217
+ | `revokeConsent(clientId, accountId)` / `POST /__admin/consents/revoke` | `consent-revoked` | that client, when it held the consent |
218
+ | `deleteAccount(accountId)` / `POST /__admin/accounts/delete {"accountId"}` | `account-delete` | every client holding a consent; then the account, its grants, sessions and pending sign-ins are removed |
219
+ | `setRelayForwarding(accountId, forwarding)` / `POST /__admin/relay-forwarding {"accountId","forwarding"}` | `email-disabled` / `email-enabled`, with `email` (the relay address) and `is_private_email: "true"` | every client holding a consent whose identity is a private relay address |
220
+
221
+ Delivery is awaited by the call that caused it, through `webhooks.fetch` (`createRuntime({ webhooks: { fetch } })`, default global `fetch`), so an in-process suite can route it through its own network model. The admin routes and methods return one `{ clientId, type, delivered, status?, error? }` per notification; a non-2xx response or a thrown error is reported there and never fails the call, and Apple's retries are not modelled. Without an `issuer` option, direct method calls need an `issuer` argument (admin routes derive it from the request). Unknown accounts return 404 from the admin routes and reject from the methods.
177
222
 
178
223
  ### Shared service controls
179
224
 
180
225
  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
226
 
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.
227
+ `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"}}`. The only outbound requests are the Apple notifications above. Journals contain request metadata, never passwords or request bodies.
183
228
 
184
229
  ## API
185
230
 
@@ -209,7 +254,7 @@ references below.
209
254
 
210
255
  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
256
 
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.
257
+ Vendor-hosted Google Identity Services/One Tap, native Apple AuthenticationServices, passkeys, MFA, CAPTCHA, password recovery, email delivery/relay forwarding (only Apple's notification of a forwarding change is sent), 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
258
 
214
259
  No implicit flow, dynamic client registration, wildcard redirect matching, 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. Sign-in and consent pages extend CSP `form-action` with the transaction's redirect origin (or custom scheme) so browsers follow the redirect back to the client. Discovery, JWKS, token, userinfo and revoke answer `OPTIONS` preflights and reflect the request `Origin`, so browser (SPA + PKCE) clients can redeem codes directly; the interaction pages send no CORS headers. 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
260
 
@@ -2,7 +2,7 @@ import {
2
2
  OAUTH_SCENARIOS,
3
3
  createMultiRuntime,
4
4
  createRuntime
5
- } from "./chunk-I24F66FL.js";
5
+ } from "./chunk-XUUSJ73W.js";
6
6
 
7
7
  // src/server.ts
8
8
  import { readFile as readFile2 } from "node:fs/promises";
@@ -377,4 +377,4 @@ export {
377
377
  createMultiServer,
378
378
  serveTarget
379
379
  };
380
- //# sourceMappingURL=chunk-X5QMVVR5.js.map
380
+ //# sourceMappingURL=chunk-U4WWQUOX.js.map