@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 +22 -1
- package/README.md +51 -6
- package/dist/{chunk-X5QMVVR5.js → chunk-U4WWQUOX.js} +2 -2
- package/dist/{chunk-I24F66FL.js → chunk-XUUSJ73W.js} +197 -36
- package/dist/chunk-XUUSJ73W.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +60 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +60 -4
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-I24F66FL.js.map +0 -7
- /package/dist/{chunk-X5QMVVR5.js.map → chunk-U4WWQUOX.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-oauth
|
|
2
2
|
|
|
3
|
-
## 1.0
|
|
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 `
|
|
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"}}`.
|
|
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-
|
|
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-
|
|
380
|
+
//# sourceMappingURL=chunk-U4WWQUOX.js.map
|