@crvouga/mockingbird-service-oauth 1.2.1 → 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 +6 -0
- package/README.md +6 -2
- package/dist/{chunk-2JIADQOV.js → chunk-MKBPAVYU.js} +2 -2
- package/dist/{chunk-XP2AEPEJ.js → chunk-WHI7Y2UA.js} +53 -2
- package/dist/{chunk-XP2AEPEJ.js.map → chunk-WHI7Y2UA.js.map} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +21 -1
- package/dist/index.js +1 -1
- package/dist/server.d.ts +20 -0
- package/dist/server.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-2JIADQOV.js.map → chunk-MKBPAVYU.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 1.2.1 (2026-09-29)
|
|
4
10
|
|
|
5
11
|
### Fixes and improvements
|
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-
|
|
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-
|
|
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.
|
|
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-
|
|
3789
|
+
//# sourceMappingURL=chunk-WHI7Y2UA.js.map
|