@broberg/sso 0.12.0 → 0.13.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/README.md CHANGED
@@ -510,7 +510,7 @@ fact the server gave away for free. Reported by helpdesk, 22 September 2026.
510
510
  **The operator still gets all three.** `onCallbackRefused(cause, c)` receives the
511
511
  precise cause on every refusal; without it the adapter writes
512
512
  `[@broberg/sso] /callback refused: <cause>` with `console.warn`. Watch for
513
- `unreadable` on real users: that is what a rotated `SSO_COOKIE_SECRET` looks like.
513
+ `unreadable` on real users: that is what a rotated `SSO_COOKIE_SECRET` looks like. A hook that throws is reported with `console.error` and the refusal still stands (0.12.1); it never becomes a 500.
514
514
 
515
515
  ```ts
516
516
  ssoRoutes({
@@ -524,3 +524,31 @@ three codes back, so it can say *"your login expired, try again"*. The price is
524
524
  the one bit above, for anyone with a found cookie. Fine for an internal app behind
525
525
  a VPN; think twice on the open internet.
526
526
 
527
+
528
+ ## Trusting another service — BID tickets (since 0.13.0)
529
+
530
+ One service proves who it is to another with a short-lived **ticket** from Broberg ID (a JWT signed with BID's keys). The receiver checks it locally against BID's *public* keys — no shared secret anywhere. Format: broberg-id `docs/features/F087.1-identitetsmodel.md` §3 (draft until the organisation question is settled; only `org`'s mapping would move).
531
+
532
+ ```ts
533
+ import { createTicketVerifier, JwksUnavailableError, SsoError } from "@broberg/sso";
534
+
535
+ const tickets = createTicketVerifier({
536
+ issuer: "https://id.broberg.ai",
537
+ audience: "https://discovery.broberg.ai", // THIS service's own URL
538
+ warmUp: true,
539
+ });
540
+
541
+ try {
542
+ const who = await tickets.verify(bearer, { scope: "discovery:read-fleet" });
543
+ // { principal: "svc-trail", type: "service", clientId, org, act, scopes, exp, jti }
544
+ } catch (e) {
545
+ if (e instanceof JwksUnavailableError) return c.text("try again", 503); // could not ask BID
546
+ if (e instanceof SsoError) return c.text("forbidden", 401); // the ticket is not acceptable
547
+ throw e;
548
+ }
549
+ ```
550
+
551
+ - **Checked:** signature against BID's key set (same algorithm allowlist as ID tokens, EdDSA first), `iss`, `aud` = your audience, `exp`/`iat` with 30 s skew, a `jti`, a known `principal_type` (human | service | agent), every scope you ask for, and a lifetime no longer than 15 minutes (BID issues 5 min for services, 15 for agents).
552
+ - **Never accepted as a ticket:** an ID token or a logout token, even though BID signed both.
553
+ - **BID briefly down:** a key already in the cache still verifies, with no network call. An unknown key while BID is down throws `JwksUnavailableError` — answer 503, do not reject the caller.
554
+ - Not a login client: a service that only receives tickets needs no `client_id`, no redirect and no session.
package/dist/hono.cjs CHANGED
@@ -909,7 +909,11 @@ function ssoRoutes(options = {}) {
909
909
  message: "This browser's login cookie could not be read. Begin again from the login page."
910
910
  }
911
911
  };
912
- await onCallbackRefused(parsed.reason, c);
912
+ try {
913
+ await onCallbackRefused(parsed.reason, c);
914
+ } catch (err) {
915
+ console.error("[@broberg/sso] onCallbackRefused threw; the refusal stands", err);
916
+ }
913
917
  const shown = callbackErrors === "single" && parsed.reason !== "absent" ? {
914
918
  status: 400,
915
919
  error: "login_failed",