@crvouga/mockingbird-service-oauth 1.1.0 → 1.2.1
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 +13 -0
- package/README.md +21 -5
- package/dist/{chunk-FZKITSDL.js → chunk-2JIADQOV.js} +2 -2
- package/dist/{chunk-JA62N2QU.js → chunk-XP2AEPEJ.js} +152 -12
- package/dist/chunk-XP2AEPEJ.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +58 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +57 -2
- package/dist/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-JA62N2QU.js.map +0 -7
- /package/dist/{chunk-FZKITSDL.js.map → chunk-2JIADQOV.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-oauth
|
|
2
2
|
|
|
3
|
+
## 1.2.1 (2026-09-29)
|
|
4
|
+
|
|
5
|
+
### Fixes and improvements
|
|
6
|
+
|
|
7
|
+
- sign and retry webhooks on the injected clock ([1091bde](https://github.com/crvouga/mockingbird/commit/1091bde0df69d9cc3eba2062d8523755e6caca14))
|
|
8
|
+
|
|
9
|
+
## 1.2.0 (2026-09-29)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- add id token faults for issuer clock skew and unpublished signing key ([7a014f2](https://github.com/crvouga/mockingbird/commit/7a014f2d0e5beeb458a056f092b1b62ccbf504d4))
|
|
14
|
+
- send Sign in with Apple server-to-server notifications ([681348e](https://github.com/crvouga/mockingbird/commit/681348e5ef0b4cf87067f013e3d0703339d608c1))
|
|
15
|
+
|
|
3
16
|
## 1.1.0 (2026-09-29)
|
|
4
17
|
|
|
5
18
|
### Features
|
package/README.md
CHANGED
|
@@ -186,9 +186,9 @@ Identity/consent decisions are sampled once per authorization and kept with the
|
|
|
186
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. |
|
|
187
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. |
|
|
188
188
|
|
|
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"`, 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.
|
|
190
190
|
|
|
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`. 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.
|
|
192
192
|
|
|
193
193
|
```sh
|
|
194
194
|
npx mockingbird-oauth serve --provider apple --seed regression-42 --scenario apple_private_relay
|
|
@@ -198,17 +198,33 @@ curl -X PUT http://localhost:8810/__admin/behavior -H 'content-type: application
|
|
|
198
198
|
curl http://localhost:8810/__admin/behavior
|
|
199
199
|
curl -X POST http://localhost:8810/__admin/consents/revoke -H 'content-type: application/json' \
|
|
200
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}'
|
|
201
205
|
curl -X POST http://localhost:8810/__admin/keys/rotate -H 'content-type: application/json' \
|
|
202
206
|
-d '{"retainPrevious":true}'
|
|
203
207
|
```
|
|
204
208
|
|
|
205
|
-
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.
|
|
206
222
|
|
|
207
223
|
### Shared service controls
|
|
208
224
|
|
|
209
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.
|
|
210
226
|
|
|
211
|
-
`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.
|
|
212
228
|
|
|
213
229
|
## API
|
|
214
230
|
|
|
@@ -238,7 +254,7 @@ references below.
|
|
|
238
254
|
|
|
239
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.
|
|
240
256
|
|
|
241
|
-
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.
|
|
242
258
|
|
|
243
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.
|
|
244
260
|
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
OAUTH_SCENARIOS,
|
|
3
3
|
createMultiRuntime,
|
|
4
4
|
createRuntime
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XP2AEPEJ.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-2JIADQOV.js.map
|
|
@@ -1384,7 +1384,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
|
1384
1384
|
};
|
|
1385
1385
|
|
|
1386
1386
|
// ../core/dist/version.js
|
|
1387
|
-
var PACKAGE_VERSION = true ? "1.1
|
|
1387
|
+
var PACKAGE_VERSION = true ? "1.2.1" : UNRELEASED_VERSION;
|
|
1388
1388
|
|
|
1389
1389
|
// ../core/dist/webhooks.js
|
|
1390
1390
|
var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
@@ -2069,7 +2069,10 @@ var OAUTH_SCENARIOS = {
|
|
|
2069
2069
|
rotating_refresh_tokens: { tokens: { refreshRotation: "rotate" } },
|
|
2070
2070
|
short_lived_tokens: { tokens: { accessTtlSeconds: 5, codeTtlSeconds: 5, refreshTtlSeconds: 30 } },
|
|
2071
2071
|
consent_denied: { consent: { error: "access_denied" } },
|
|
2072
|
-
intermittent_token_failure: { probabilities: { tokenUnavailable: 0.25 } }
|
|
2072
|
+
intermittent_token_failure: { probabilities: { tokenUnavailable: 0.25 } },
|
|
2073
|
+
id_token_clock_ahead: { tokens: { idTokenClockSkewSeconds: 600 } },
|
|
2074
|
+
id_token_stale: { tokens: { idTokenClockSkewSeconds: -7200 } },
|
|
2075
|
+
id_token_unknown_key: { tokens: { idTokenSigningKey: "unpublished" } }
|
|
2073
2076
|
};
|
|
2074
2077
|
var object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
2075
2078
|
var keys = (value, allowed) => {
|
|
@@ -2154,10 +2157,16 @@ function validateBehavior(input) {
|
|
|
2154
2157
|
"codeTtlSeconds",
|
|
2155
2158
|
"refreshTtlSeconds",
|
|
2156
2159
|
"refreshRotation",
|
|
2157
|
-
"refreshError"
|
|
2160
|
+
"refreshError",
|
|
2161
|
+
"idTokenClockSkewSeconds",
|
|
2162
|
+
"idTokenSigningKey"
|
|
2158
2163
|
]);
|
|
2159
2164
|
enumeration(tokens.refreshRotation, ["reuse", "rotate"]);
|
|
2160
2165
|
enumeration(tokens.refreshError, ["invalid_grant", "invalid_rapt"]);
|
|
2166
|
+
enumeration(tokens.idTokenSigningKey, ["published", "unpublished"]);
|
|
2167
|
+
const skew = tokens.idTokenClockSkewSeconds;
|
|
2168
|
+
if (skew !== void 0 && (typeof skew !== "number" || !Number.isSafeInteger(skew) || Math.abs(skew) > 31536e4))
|
|
2169
|
+
throw new Error("idTokenClockSkewSeconds must be an integer in [-315360000,315360000]");
|
|
2161
2170
|
for (const key of ["accessTtlSeconds", "codeTtlSeconds", "refreshTtlSeconds"]) {
|
|
2162
2171
|
const v = tokens[key];
|
|
2163
2172
|
if (v !== void 0 && (typeof v !== "number" || !Number.isSafeInteger(v) || v < 1 || v > 31536e4))
|
|
@@ -2424,12 +2433,44 @@ function createRuntime2(options = {}) {
|
|
|
2424
2433
|
return Response.json({ error: String(error) }, { status: 400 });
|
|
2425
2434
|
}
|
|
2426
2435
|
},
|
|
2427
|
-
"POST /consents/revoke": ({ namespace, body }) => {
|
|
2436
|
+
"POST /consents/revoke": async ({ namespace, body, request }) => {
|
|
2428
2437
|
const input = body;
|
|
2429
2438
|
if (!input || typeof input.clientId !== "string" || typeof input.accountId !== "string")
|
|
2430
2439
|
return Response.json({ error: "clientId and accountId are required" }, { status: 400 });
|
|
2431
|
-
runtime.instance(namespace)
|
|
2432
|
-
|
|
2440
|
+
const api = runtime.instance(namespace);
|
|
2441
|
+
const notifications = await api.revokeConsent(
|
|
2442
|
+
input.clientId,
|
|
2443
|
+
input.accountId,
|
|
2444
|
+
api.issuer(request)
|
|
2445
|
+
);
|
|
2446
|
+
return Response.json({ revoked: true, notifications });
|
|
2447
|
+
},
|
|
2448
|
+
"POST /accounts/delete": async ({ namespace, body, request }) => {
|
|
2449
|
+
const input = body;
|
|
2450
|
+
if (!input || typeof input.accountId !== "string")
|
|
2451
|
+
return Response.json({ error: "accountId is required" }, { status: 400 });
|
|
2452
|
+
const api = runtime.instance(namespace);
|
|
2453
|
+
if (!api.accounts.has(input.accountId))
|
|
2454
|
+
return Response.json({ error: "Unknown account" }, { status: 404 });
|
|
2455
|
+
const notifications = await api.deleteAccount(input.accountId, api.issuer(request));
|
|
2456
|
+
return Response.json({ deleted: true, notifications });
|
|
2457
|
+
},
|
|
2458
|
+
"POST /relay-forwarding": async ({ namespace, body, request }) => {
|
|
2459
|
+
const input = body;
|
|
2460
|
+
if (!input || typeof input.accountId !== "string" || typeof input.forwarding !== "boolean")
|
|
2461
|
+
return Response.json(
|
|
2462
|
+
{ error: "accountId and boolean forwarding are required" },
|
|
2463
|
+
{ status: 400 }
|
|
2464
|
+
);
|
|
2465
|
+
const api = runtime.instance(namespace);
|
|
2466
|
+
if (!api.accounts.has(input.accountId))
|
|
2467
|
+
return Response.json({ error: "Unknown account" }, { status: 404 });
|
|
2468
|
+
const notifications = await api.setRelayForwarding(
|
|
2469
|
+
input.accountId,
|
|
2470
|
+
input.forwarding,
|
|
2471
|
+
api.issuer(request)
|
|
2472
|
+
);
|
|
2473
|
+
return Response.json({ forwarding: input.forwarding, notifications });
|
|
2433
2474
|
},
|
|
2434
2475
|
"POST /keys/rotate": ({ namespace, body }) => {
|
|
2435
2476
|
const input = body;
|
|
@@ -2641,6 +2682,8 @@ var OAuthAPI = class {
|
|
|
2641
2682
|
consents;
|
|
2642
2683
|
signer = new Signer();
|
|
2643
2684
|
previousSigners = [];
|
|
2685
|
+
/** Never published: signs ID tokens for `tokens.idTokenSigningKey: "unpublished"`. */
|
|
2686
|
+
unpublishedSigner;
|
|
2644
2687
|
behavior;
|
|
2645
2688
|
identities;
|
|
2646
2689
|
appleDisclosures;
|
|
@@ -2684,6 +2727,10 @@ var OAuthAPI = class {
|
|
|
2684
2727
|
throw new Error(
|
|
2685
2728
|
"Apple client requires teamId, keyId and a public P-256 JWK, without a static secret"
|
|
2686
2729
|
);
|
|
2730
|
+
if (client.apple?.notificationUrl !== void 0 && !(typeof client.apple.notificationUrl === "string" && safeRedirect(client.apple.notificationUrl) && WEB_SCHEMES.has(new URL(client.apple.notificationUrl).protocol)))
|
|
2731
|
+
throw new Error(
|
|
2732
|
+
"apple.notificationUrl must be an HTTP(S) URL without fragment or credentials"
|
|
2733
|
+
);
|
|
2687
2734
|
this.clients.insert(client.id, structuredClone(client));
|
|
2688
2735
|
return client;
|
|
2689
2736
|
}
|
|
@@ -2704,7 +2751,95 @@ var OAuthAPI = class {
|
|
|
2704
2751
|
this.signer = new Signer();
|
|
2705
2752
|
return { kid: this.signer.kid };
|
|
2706
2753
|
}
|
|
2707
|
-
|
|
2754
|
+
/**
|
|
2755
|
+
* Withdraws one client's consent. When the client registered `apple.notificationUrl` and held
|
|
2756
|
+
* the consent, Apple's `consent-revoked` notification is delivered before this resolves.
|
|
2757
|
+
*/
|
|
2758
|
+
async revokeConsent(clientId, accountId, issuer) {
|
|
2759
|
+
const targets = this.notifiable(accountId).filter((t) => t.client.id === clientId);
|
|
2760
|
+
this.forget(clientId, accountId);
|
|
2761
|
+
return this.notify(targets, "consent-revoked", issuer);
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Deletes an account with its grants and sessions, after telling every client that held a
|
|
2765
|
+
* consent (`account-delete`).
|
|
2766
|
+
*/
|
|
2767
|
+
async deleteAccount(accountId, issuer) {
|
|
2768
|
+
if (!this.accounts.has(accountId)) throw new Error("Unknown account");
|
|
2769
|
+
const targets = this.notifiable(accountId);
|
|
2770
|
+
for (const { value: client } of this.clients.list()) this.forget(client.id, accountId);
|
|
2771
|
+
for (const row of this.codes.list({ where: (c) => c.accountId === accountId }))
|
|
2772
|
+
this.codes.delete(row.id);
|
|
2773
|
+
for (const row of this.tokens.list({ where: (t) => t.accountId === accountId }))
|
|
2774
|
+
this.tokens.delete(row.id);
|
|
2775
|
+
for (const row of this.transactions.list({ where: (t) => t.accountId === accountId }))
|
|
2776
|
+
this.transactions.delete(row.id);
|
|
2777
|
+
for (const row of this.sessions.list({ where: (s) => s.accountId === accountId }))
|
|
2778
|
+
this.sessions.delete(row.id);
|
|
2779
|
+
this.accounts.delete(accountId);
|
|
2780
|
+
return this.notify(targets, "account-delete", issuer);
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* Tells every client whose identity for the account is a private relay address that the user
|
|
2784
|
+
* turned forwarding off (`email-disabled`) or on (`email-enabled`).
|
|
2785
|
+
*/
|
|
2786
|
+
async setRelayForwarding(accountId, forwarding, issuer) {
|
|
2787
|
+
if (!this.accounts.has(accountId)) throw new Error("Unknown account");
|
|
2788
|
+
return this.notify(
|
|
2789
|
+
this.notifiable(accountId).filter((t) => t.relay),
|
|
2790
|
+
forwarding ? "email-enabled" : "email-disabled",
|
|
2791
|
+
issuer,
|
|
2792
|
+
true
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
/** Clients with a notification endpoint that hold a consent for the account. */
|
|
2796
|
+
notifiable(accountId) {
|
|
2797
|
+
return this.clients.list({ order: "oldest" }).flatMap(({ value: client }) => {
|
|
2798
|
+
const identity = this.identities.get(this.identityKey(client, accountId));
|
|
2799
|
+
return client.apple?.notificationUrl && identity && this.consents.has(JSON.stringify([client.id, accountId])) ? [{ client, sub: identity.sub, email: identity.email, relay: identity.privateEmail }] : [];
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
async notify(targets, type, issuer, withEmail = false) {
|
|
2803
|
+
if (!targets.length) return [];
|
|
2804
|
+
const iss = issuer ?? this.options.issuer;
|
|
2805
|
+
if (!iss) throw new Error("A notification needs the issuer option or an issuer argument");
|
|
2806
|
+
const send = this.options.webhooks?.fetch ?? ((r) => fetch(r));
|
|
2807
|
+
const deliveries = [];
|
|
2808
|
+
for (const { client, sub, email } of targets) {
|
|
2809
|
+
const payload = await this.signer.sign({
|
|
2810
|
+
iss: iss.replace(/\/$/, ""),
|
|
2811
|
+
aud: client.id,
|
|
2812
|
+
iat: Math.floor(this.now() / 1e3),
|
|
2813
|
+
jti: random(),
|
|
2814
|
+
events: JSON.stringify({
|
|
2815
|
+
type,
|
|
2816
|
+
sub,
|
|
2817
|
+
...withEmail ? { email, is_private_email: "true" } : {},
|
|
2818
|
+
event_time: this.now()
|
|
2819
|
+
})
|
|
2820
|
+
});
|
|
2821
|
+
try {
|
|
2822
|
+
const response = await send(
|
|
2823
|
+
new Request(client.apple?.notificationUrl ?? "", {
|
|
2824
|
+
method: "POST",
|
|
2825
|
+
headers: { "content-type": "application/json" },
|
|
2826
|
+
body: JSON.stringify({ payload })
|
|
2827
|
+
})
|
|
2828
|
+
);
|
|
2829
|
+
deliveries.push({
|
|
2830
|
+
clientId: client.id,
|
|
2831
|
+
type,
|
|
2832
|
+
delivered: response.ok,
|
|
2833
|
+
status: response.status
|
|
2834
|
+
});
|
|
2835
|
+
} catch (error) {
|
|
2836
|
+
deliveries.push({ clientId: client.id, type, delivered: false, error: String(error) });
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return deliveries;
|
|
2840
|
+
}
|
|
2841
|
+
/** Drops one client's grant for an account: consent, identity, first-use disclosure, codes, tokens. */
|
|
2842
|
+
forget(clientId, accountId) {
|
|
2708
2843
|
const key = JSON.stringify([clientId, accountId]);
|
|
2709
2844
|
this.consents.delete(key);
|
|
2710
2845
|
this.refreshIssued.delete(key);
|
|
@@ -2847,13 +2982,18 @@ var OAuthAPI = class {
|
|
|
2847
2982
|
async idToken(grant, issuer, extra = {}) {
|
|
2848
2983
|
const account = this.accounts.get(grant.accountId);
|
|
2849
2984
|
if (!account) throw new Error("Account no longer exists");
|
|
2850
|
-
|
|
2985
|
+
const tokens = this.behavior.config.tokens;
|
|
2986
|
+
const skew = tokens?.idTokenClockSkewSeconds ?? 0;
|
|
2987
|
+
const iat = Math.floor(this.now() / 1e3) + skew;
|
|
2988
|
+
const unpublished = tokens?.idTokenSigningKey === "unpublished";
|
|
2989
|
+
if (unpublished) this.unpublishedSigner ??= new Signer();
|
|
2990
|
+
return (unpublished && this.unpublishedSigner || this.signer).sign({
|
|
2851
2991
|
...this.profile(account, grant),
|
|
2852
2992
|
iss: issuer,
|
|
2853
2993
|
aud: grant.clientId,
|
|
2854
|
-
iat
|
|
2855
|
-
exp:
|
|
2856
|
-
auth_time: Math.floor(grant.authTime / 1e3),
|
|
2994
|
+
iat,
|
|
2995
|
+
exp: iat + (tokens?.accessTtlSeconds ?? 3600),
|
|
2996
|
+
auth_time: Math.floor(grant.authTime / 1e3) + skew,
|
|
2857
2997
|
...grant.nonce ? { nonce: grant.nonce } : {},
|
|
2858
2998
|
...extra
|
|
2859
2999
|
});
|
|
@@ -3595,4 +3735,4 @@ export {
|
|
|
3595
3735
|
createMultiRuntime,
|
|
3596
3736
|
OAuthAPI
|
|
3597
3737
|
};
|
|
3598
|
-
//# sourceMappingURL=chunk-
|
|
3738
|
+
//# sourceMappingURL=chunk-XP2AEPEJ.js.map
|