@crvouga/mockingbird-service-oauth 1.2.0 → 1.3.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,5 +1,17 @@
1
1
  # Changelog — @crvouga/mockingbird-service-oauth
2
2
 
3
+ ## 1.3.0 (2026-09-29)
4
+
5
+ ### Features
6
+
7
+ - admin read of an account's grant with a client ([90b0279](https://github.com/crvouga/mockingbird/commit/90b0279f53cb4959b18c8992ae64bb00b5de4f96))
8
+
9
+ ## 1.2.1 (2026-09-29)
10
+
11
+ ### Fixes and improvements
12
+
13
+ - sign and retry webhooks on the injected clock ([1091bde](https://github.com/crvouga/mockingbird/commit/1091bde0df69d9cc3eba2062d8523755e6caca14))
14
+
3
15
  ## 1.2.0 (2026-09-29)
4
16
 
5
17
  ### Features
package/README.md CHANGED
@@ -198,6 +198,8 @@ 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 'http://localhost:8810/__admin/grants?clientId=app&accountId=ada'
202
+ curl 'http://localhost:8810/__admin/grants?clientId=app'
201
203
  curl -X POST http://localhost:8810/__admin/accounts/delete -H 'content-type: application/json' \
202
204
  -d '{"accountId":"ada"}'
203
205
  curl -X POST http://localhost:8810/__admin/relay-forwarding -H 'content-type: application/json' \
@@ -206,6 +208,8 @@ curl -X POST http://localhost:8810/__admin/keys/rotate -H 'content-type: applica
206
208
  -d '{"retainPrevious":true}'
207
209
  ```
208
210
 
211
+ `GET /__admin/grants?clientId=<id>&accountId=<id>` (or `&email=<address>`) reads what that client sees for the account, without running a sign-in: `{ subject, granted, scopes, emailChoice, email, isPrivateEmail, userDisclosed }`. `subject` is the id token `sub` the client gets, or would get on a first authorization (it survives a consent revocation). `granted` says whether a consent exists, so whether the consent card is shown next time. `scopes` are the granted scopes, including Google `include_granted_scopes` merges. `emailChoice` is Apple's `"share"` or `"hide"`, and `null` before the first authorization, after a revocation, and on every other profile. `email` is the address the client receives: the relay for Hide My Email, the real address otherwise. `userDisclosed` is whether Apple's first-use `user` payload was already sent. Without `accountId` or `email` it returns `{ "grants": [...] }`, one row (with `accountId`) per account that currently has a grant with the client. An unknown client or account is `404`. Programmatically, `await runtime.instance().grant(clientId, accountId)` returns the same view, or `null`, and `grants(clientId)` the list. The view is read-only and follows namespaces.
212
+
209
213
  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
214
 
211
215
  ### Sign in with Apple server-to-server notifications
@@ -230,14 +234,14 @@ The runtime supplies `/health`, `/__admin/reset`, snapshots, mock clock, request
230
234
 
231
235
  - `createRuntime(options?)`: shared service runtime; `fetch`, `instance`, `reset`, `snapshot`, `restore`, clock, faults and journals.
232
236
  - `createMultiRuntime({ mounts })`: exact-path dispatcher for isolated provider runtimes on one origin, with aggregate health and admin controls.
233
- - `OAuthAPI`: standalone portable handler with `fetch`, `reset`, `seedAccount`, `registerClient`, `accounts`, `clients`, `provider`, `configureBehavior`, `behavior`, `revokeConsent`, `rotateSigningKey`.
237
+ - `OAuthAPI`: standalone portable handler with `fetch`, `reset`, `seedAccount`, `registerClient`, `accounts`, `clients`, `provider`, `configureBehavior`, `behavior`, `revokeConsent`, `rotateSigningKey`, `grant`, `grants`.
234
238
  - `OAUTH_PRESETS`: named transport fault presets.
235
239
  - `OAUTH_SCENARIOS`: named provider-behavior scenarios.
236
240
  - `document`, `operationIds`, `supportedOperationIds`: generated OpenAPI metadata.
237
241
  - `createServer(options?)` from `./server`: Node HTTP adapter, returning `url`, `close` and `runtime`.
238
242
  - `createMultiServer({ mounts })` from `./server`: Node HTTP adapter for a multi-provider runtime.
239
243
  - `DEFAULT_PORT`, `serveTarget` from `./server`: CLI defaults and multi-service launcher integration.
240
- - Types: `Account`, `Client`, `Provider`, `OAuthAPIOptions`, `OAuthRuntimeOptions`, `OAuthRuntime`, `OAuthServerOptions`, `OAuthMount`, `OAuthMultiRuntimeOptions`, `OAuthMultiRuntime`, `OAuthMultiServerOptions`, `OAuthBehavior`, `BehaviorInput`, `OAuthScenario`, `EdgeCase`, `BehaviorEvent`.
244
+ - Types: `Account`, `Client`, `GrantView`, `Provider`, `OAuthAPIOptions`, `OAuthRuntimeOptions`, `OAuthRuntime`, `OAuthServerOptions`, `OAuthMount`, `OAuthMultiRuntimeOptions`, `OAuthMultiRuntime`, `OAuthMultiServerOptions`, `OAuthBehavior`, `BehaviorInput`, `OAuthScenario`, `EdgeCase`, `BehaviorEvent`.
241
245
 
242
246
  ## Verification
243
247
 
@@ -2,7 +2,7 @@ import {
2
2
  OAUTH_SCENARIOS,
3
3
  createMultiRuntime,
4
4
  createRuntime
5
- } from "./chunk-XUUSJ73W.js";
5
+ } from "./chunk-WHI7Y2UA.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-U4WWQUOX.js.map
380
+ //# sourceMappingURL=chunk-MKBPAVYU.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.2.0" : UNRELEASED_VERSION;
1387
+ var PACKAGE_VERSION = true ? "1.3.0" : 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" } });
@@ -2472,6 +2472,24 @@ function createRuntime2(options = {}) {
2472
2472
  );
2473
2473
  return Response.json({ forwarding: input.forwarding, notifications });
2474
2474
  },
2475
+ "GET /grants": async ({ namespace, url }) => {
2476
+ const api = runtime.instance(namespace);
2477
+ const clientId = url.searchParams.get("clientId");
2478
+ const accountId = url.searchParams.get("accountId");
2479
+ const email = url.searchParams.get("email")?.toLowerCase().trim();
2480
+ if (!clientId || accountId !== null && email !== void 0)
2481
+ return Response.json(
2482
+ { error: "clientId is required, with at most one of accountId or email" },
2483
+ { status: 400 }
2484
+ );
2485
+ if (accountId === null && email === void 0) {
2486
+ const grants = await api.grants(clientId);
2487
+ return grants ? Response.json({ grants }) : Response.json({ error: "Unknown client" }, { status: 404 });
2488
+ }
2489
+ const id = accountId ?? api.accounts.list({ where: (a) => a.email.toLowerCase() === email })[0]?.value.id ?? "";
2490
+ const grant = await api.grant(clientId, id);
2491
+ return grant ? Response.json(grant) : Response.json({ error: "Unknown client or account" }, { status: 404 });
2492
+ },
2475
2493
  "POST /keys/rotate": ({ namespace, body }) => {
2476
2494
  const input = body;
2477
2495
  if (input?.retainPrevious !== void 0 && typeof input.retainPrevious !== "boolean")
@@ -2857,6 +2875,39 @@ var OAuthAPI = class {
2857
2875
  }))
2858
2876
  this.tokens.delete(row.id);
2859
2877
  }
2878
+ /**
2879
+ * What `clientId` sees for `accountId` (a test control mirroring private provider state), or
2880
+ * `null` for an unknown client or account. For a client that never authorized the account,
2881
+ * `subject` is the id token `sub` it would receive and `email` the real address.
2882
+ */
2883
+ async grant(clientId, accountId) {
2884
+ const client = this.clients.get(clientId);
2885
+ const account = this.accounts.get(accountId);
2886
+ if (!client || !account) return null;
2887
+ const key = this.identityKey(client, account.id);
2888
+ const identity = this.identities.get(key);
2889
+ const consent = this.consents.get(JSON.stringify([clientId, accountId]));
2890
+ return {
2891
+ subject: identity?.sub ?? (this.provider === "apple" || this.provider === "microsoft" ? await hash(key) : account.id),
2892
+ granted: consent !== void 0,
2893
+ scopes: [...scopes(consent?.scope ?? "")],
2894
+ emailChoice: this.provider === "apple" && identity ? identity.privateEmail ? "hide" : "share" : null,
2895
+ email: identity?.email ?? account.email,
2896
+ isPrivateEmail: identity?.privateEmail ?? false,
2897
+ userDisclosed: this.appleDisclosures.has(key)
2898
+ };
2899
+ }
2900
+ /** The grant view of every account that has authorized `clientId`, oldest account first. */
2901
+ async grants(clientId) {
2902
+ if (!this.clients.has(clientId)) return null;
2903
+ const rows = [];
2904
+ for (const { id, value } of this.accounts.list({ order: "oldest" })) {
2905
+ if (!this.consents.has(JSON.stringify([clientId, id]))) continue;
2906
+ const view = await this.grant(clientId, value.id);
2907
+ if (view) rows.push({ accountId: value.id, ...view });
2908
+ }
2909
+ return rows;
2910
+ }
2860
2911
  paths() {
2861
2912
  const paths = {
2862
2913
  google: {
@@ -3735,4 +3786,4 @@ export {
3735
3786
  createMultiRuntime,
3736
3787
  OAuthAPI
3737
3788
  };
3738
- //# sourceMappingURL=chunk-XUUSJ73W.js.map
3789
+ //# sourceMappingURL=chunk-WHI7Y2UA.js.map