@crvouga/mockingbird-service-oauth 0.1.0 → 0.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,5 +1,17 @@
1
1
  # Changelog — @crvouga/mockingbird-service-oauth
2
2
 
3
+ ## 0.2.0 (2026-09-24)
4
+
5
+ ### Features
6
+
7
+ - open every live example in a modal window scoped to the example ([c27de26](https://github.com/crvouga/mockingbird/commit/c27de26b677ccb257c3d352090ce905e39f4e470))
8
+
9
+ ## 0.1.1 (2026-09-23)
10
+
11
+ ### Fixes and improvements
12
+
13
+ - let browser clients finish sign-in and redeem codes cross-origin ([c9fe735](https://github.com/crvouga/mockingbird/commit/c9fe735b5c5417ba3c1f6a3bfe7086a61a60639e))
14
+
3
15
  ## 0.1.0 (2026-09-22)
4
16
 
5
17
  Initial release.
package/README.md CHANGED
@@ -211,7 +211,7 @@ This is a ready-to-use local/test identity provider, **not a production authenti
211
211
 
212
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.
213
213
 
214
- No implicit flow, dynamic client registration, wildcard redirect matching, cross-origin browser token CORS policy, 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. 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.
214
+ 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
215
 
216
216
  References: [Google OpenID Connect](https://developers.google.com/identity/openid-connect/openid-connect), [Apple authorization request](https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server.), and [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html).
217
217
 
@@ -1384,7 +1384,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
1384
1384
  };
1385
1385
 
1386
1386
  // ../core/dist/version.js
1387
- var PACKAGE_VERSION = true ? "0.1.0" : UNRELEASED_VERSION;
1387
+ var PACKAGE_VERSION = true ? "0.2.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" } });
@@ -2328,7 +2328,7 @@ function page(title, body, status = 200, action = "'self'", nonce = crypto.rando
2328
2328
  }
2329
2329
  );
2330
2330
  }
2331
- function loginPage(transaction, client, accounts, base, signup = false, error = "") {
2331
+ function loginPage(transaction, client, accounts, base, action, signup = false, error = "") {
2332
2332
  const hidden = `<input type="hidden" name="transaction" value="${escapeHtml(transaction)}">`;
2333
2333
  const endpoint = escapeHtml(`${base}/interaction`);
2334
2334
  const intro = `<span class="eyebrow">Sign in</span><h1 id="title">${signup ? "Create your account" : "Choose an account"}</h1><p>${signup ? "Create an account to continue to" : "Choose an account to continue to"}<br><span class="app">${escapeHtml(client)}</span></p>${error ? `<div class="error" role="alert">${escapeHtml(error)}</div>` : ""}`;
@@ -2336,10 +2336,11 @@ function loginPage(transaction, client, accounts, base, signup = false, error =
2336
2336
  return page(
2337
2337
  signup ? "Create account" : "Choose an account",
2338
2338
  `${intro}${body}<div class="divider"></div><form method="post" action="${endpoint}">${hidden}<button class="secondary" name="action" value="deny">Cancel sign-in</button></form>`,
2339
- error ? 400 : 200
2339
+ error ? 400 : 200,
2340
+ action
2340
2341
  );
2341
2342
  }
2342
- function consentPage(transaction, client, account, scopes2, base, privacy) {
2343
+ function consentPage(transaction, client, account, scopes2, base, action, privacy) {
2343
2344
  const labels = {
2344
2345
  openid: "Confirm your identity",
2345
2346
  email: "View your email address",
@@ -2354,7 +2355,9 @@ function consentPage(transaction, client, account, scopes2, base, privacy) {
2354
2355
  (s) => `<li><span class="check" aria-hidden="true">\u2713</span>${escapeHtml(labels[s] ?? s)}</li>`
2355
2356
  ).join(
2356
2357
  ""
2357
- )}</ul><p>You can cancel now without sharing anything.</p><form method="post" action="${escapeHtml(base)}/interaction"><input type="hidden" name="transaction" value="${escapeHtml(transaction)}">${privacyFields}<button class="primary" name="action" value="allow">Allow &amp; continue</button><button class="secondary" name="action" value="deny">Cancel</button></form>`
2358
+ )}</ul><p>You can cancel now without sharing anything.</p><form method="post" action="${escapeHtml(base)}/interaction"><input type="hidden" name="transaction" value="${escapeHtml(transaction)}">${privacyFields}<button class="primary" name="action" value="allow">Allow &amp; continue</button><button class="secondary" name="action" value="deny">Cancel</button></form>`,
2359
+ 200,
2360
+ action
2358
2361
  );
2359
2362
  }
2360
2363
 
@@ -2571,6 +2574,11 @@ var fail = (error, description, status = 400) => json4({ error, error_descriptio
2571
2574
  var scopes = (value) => new Set(value.split(/\s+/).filter(Boolean));
2572
2575
  var validEmail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) && s.length <= 254;
2573
2576
  var WEB_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
2577
+ var formTarget = (redirectUri) => {
2578
+ const u = new URL(redirectUri);
2579
+ return WEB_SCHEMES.has(u.protocol) ? u.origin : u.protocol;
2580
+ };
2581
+ var formAction = (redirectUri) => `'self' ${formTarget(redirectUri)}`;
2574
2582
  var FORBIDDEN_REDIRECT_SCHEMES = /* @__PURE__ */ new Set(["blob:", "data:", "file:", "javascript:", "vbscript:"]);
2575
2583
  var safeRedirect = (s) => {
2576
2584
  try {
@@ -2842,6 +2850,38 @@ var OAuthAPI = class {
2842
2850
  const issuer = this.issuer(request);
2843
2851
  const mount = new URL(issuer).pathname.replace(/\/$/, "");
2844
2852
  const path = mount && url.pathname.startsWith(`${mount}/`) ? url.pathname.slice(mount.length) : url.pathname;
2853
+ const paths = this.paths();
2854
+ const cors = [
2855
+ "/.well-known/openid-configuration",
2856
+ paths.jwks,
2857
+ "/jwks",
2858
+ paths.token,
2859
+ "/token",
2860
+ paths.userinfo,
2861
+ "/userinfo",
2862
+ "/oauth2/v3/userinfo",
2863
+ "/user/emails",
2864
+ paths.revoke,
2865
+ "/revoke"
2866
+ ].includes(path);
2867
+ if (!cors) return this.route(request, url, issuer, path);
2868
+ const origin = request.headers.get("origin");
2869
+ const response = request.method === "OPTIONS" ? new Response(null, {
2870
+ status: 204,
2871
+ headers: {
2872
+ "access-control-allow-methods": "GET, POST, OPTIONS",
2873
+ "access-control-allow-headers": request.headers.get("access-control-request-headers") ?? "authorization, content-type",
2874
+ "access-control-max-age": "600"
2875
+ }
2876
+ }) : await this.route(request, url, issuer, path);
2877
+ if (origin) {
2878
+ response.headers.set("access-control-allow-origin", origin);
2879
+ response.headers.set("access-control-expose-headers", "www-authenticate");
2880
+ response.headers.append("vary", "Origin");
2881
+ }
2882
+ return response;
2883
+ }
2884
+ async route(request, url, issuer, path) {
2845
2885
  const paths = this.paths();
2846
2886
  if (request.method === "GET" && path === "/.well-known/openid-configuration" && this.provider !== "github")
2847
2887
  return json4({
@@ -3107,7 +3147,7 @@ var OAuthAPI = class {
3107
3147
  accounts.sort(
3108
3148
  (a, b) => Number(b.email === hint || b.id === hint) - Number(a.email === hint || a.id === hint)
3109
3149
  );
3110
- return loginPage(id, client.name, accounts, issuer);
3150
+ return loginPage(id, client.name, accounts, issuer, formAction(auth.redirectUri));
3111
3151
  }
3112
3152
  consent(id, client, account, auth, issuer) {
3113
3153
  const existing = this.identities.get(this.identityKey(client, account.id));
@@ -3119,6 +3159,7 @@ var OAuthAPI = class {
3119
3159
  account,
3120
3160
  allowed,
3121
3161
  issuer,
3162
+ formAction(auth.redirectUri),
3122
3163
  this.provider === "apple" && scopes(auth.scope).has("email") && !existing ? {
3123
3164
  hideEmail: account.privateEmail ?? (this.behavior.config.apple?.emailMode === "hide" || (this.behavior.config.apple?.emailMode ?? "choose") === "choose" && auth.decisions.hideEmail),
3124
3165
  choice: (this.behavior.config.apple?.emailMode ?? "choose") === "choose"
@@ -3141,7 +3182,14 @@ var OAuthAPI = class {
3141
3182
  );
3142
3183
  const accounts = () => this.accounts.list({ order: "oldest", where: (a) => !a.disabled }).map((a) => a.value);
3143
3184
  if (request.method === "GET")
3144
- return loginPage(id, client.name, accounts(), issuer, p.get("screen") === "signup");
3185
+ return loginPage(
3186
+ id,
3187
+ client.name,
3188
+ accounts(),
3189
+ issuer,
3190
+ formAction(auth.redirectUri),
3191
+ p.get("screen") === "signup"
3192
+ );
3145
3193
  const action = p.get("action");
3146
3194
  if (action === "deny") {
3147
3195
  this.transactions.delete(id);
@@ -3158,6 +3206,7 @@ var OAuthAPI = class {
3158
3206
  client.name,
3159
3207
  [],
3160
3208
  issuer,
3209
+ formAction(auth.redirectUri),
3161
3210
  true,
3162
3211
  "Enter a full name and a valid email address."
3163
3212
  );
@@ -3167,6 +3216,7 @@ var OAuthAPI = class {
3167
3216
  client.name,
3168
3217
  [],
3169
3218
  issuer,
3219
+ formAction(auth.redirectUri),
3170
3220
  true,
3171
3221
  "This email already has an account. Go back to choose it."
3172
3222
  );
@@ -3179,7 +3229,15 @@ var OAuthAPI = class {
3179
3229
  if (action === "select") {
3180
3230
  const account = this.accounts.get(p.get("account") ?? "");
3181
3231
  if (!account || account.disabled)
3182
- return loginPage(id, client.name, accounts(), issuer, false, "Choose an available account.");
3232
+ return loginPage(
3233
+ id,
3234
+ client.name,
3235
+ accounts(),
3236
+ issuer,
3237
+ formAction(auth.redirectUri),
3238
+ false,
3239
+ "Choose an available account."
3240
+ );
3183
3241
  auth.accountId = account.id;
3184
3242
  auth.authTime = this.now();
3185
3243
  this.transactions.update(id, auth);
@@ -3283,7 +3341,7 @@ var OAuthAPI = class {
3283
3341
  ""
3284
3342
  )}<button class="primary">Continue</button></form><script nonce="${nonce}">document.getElementById('callback').submit()</script>`,
3285
3343
  200,
3286
- WEB_SCHEMES.has(redirect.protocol) ? redirect.origin : redirect.protocol,
3344
+ formTarget(auth.redirectUri),
3287
3345
  nonce
3288
3346
  );
3289
3347
  return result;
@@ -3507,4 +3565,4 @@ export {
3507
3565
  createMultiRuntime,
3508
3566
  OAuthAPI
3509
3567
  };
3510
- //# sourceMappingURL=chunk-X6FYBOEA.js.map
3568
+ //# sourceMappingURL=chunk-PCHI2SDJ.js.map