@broberg/sso 0.14.2 → 0.15.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
@@ -540,7 +540,7 @@ const tickets = createTicketVerifier({
540
540
 
541
541
  try {
542
542
  const who = await tickets.verify(bearer, { scope: "discovery:read-fleet" });
543
- // { principal: "svc-trail", type: "service", clientId, org, act, scopes, exp, jti }
543
+ // { principal: "svc-trail", type: "service", clientId, org, act, version, cnf, scopes, exp, jti }
544
544
  } catch (e) {
545
545
  if (e instanceof JwksUnavailableError) return c.text("try again", 503); // could not ask BID
546
546
  if (e instanceof SsoError) return c.text("forbidden", 401); // the ticket is not acceptable
@@ -553,6 +553,12 @@ try {
553
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
554
  - Not a login client: a service that only receives tickets needs no `client_id`, no redirect and no session.
555
555
 
556
+ **Since 0.15.0 (F084.156, broberg-id-F087.15) — read for the future, never shortened:**
557
+
558
+ - `act` is the **whole delegation chain** (RFC 8693): agent-b acting for agent-a acting for user-1 is `{ sub: "agent-a", act: { sub: "user-1" } }`. `act.sub` is the same as before. A chain deeper than 5 links, or a link without a string `sub`, is **rejected** — 0.14.x kept only the first link and read a malformed `act` as "no delegation", both in the direction where the caller looks like it acts for fewer people.
559
+ - `version` is the ticket format (`ver`): 0 for a ticket from before BID set one, 1 today. A `ver` newer than this package understands is **rejected by name** ("upgrade @broberg/sso"), never read with today's rules.
560
+ - `cnf` is `{ jkt }` for a DPoP-bound ticket, else `null`. **It is not checked here**: until your service verifies a DPoP proof against it (F087.16), a bound ticket is accepted exactly like a bearer ticket, and a stolen one works. Do not treat `cnf` as protection yet.
561
+
556
562
  ## Getting a ticket — `fetchTicket` (since 0.14.1 — 0.14.0 was tagged but never reached npm)
557
563
 
558
564
  The sending half: a workload on **Fly** or in **GitHub Actions** gets a Broberg ID ticket with no key at all. It proves who it is with the identity its platform already gives it, and BID exchanges that (RFC 8693). Nothing secret is stored anywhere.
package/dist/index.cjs CHANGED
@@ -142,6 +142,8 @@ function fetchTicket(opts) {
142
142
  return shared.get(opts);
143
143
  }
144
144
  var PRINCIPAL_TYPES = ["human", "service", "agent"];
145
+ var TICKET_FORMAT_VERSION = 1;
146
+ var MAX_ACT_DEPTH = 5;
145
147
  function createTicketVerifier(options) {
146
148
  const {
147
149
  issuer,
@@ -223,14 +225,24 @@ function createTicketVerifier(options) {
223
225
  const required = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
224
226
  const missing = required.filter((s) => !scopes.includes(s));
225
227
  if (missing.length) throw new chunkJGIO7XPR_cjs.SsoError(`ticket lacks scope ${missing.join(", ")}`);
226
- const actRaw = payload.act;
227
- const act = actRaw && typeof actRaw.sub === "string" && actRaw.sub ? { sub: actRaw.sub } : null;
228
+ const ver = payload.ver === void 0 ? 0 : payload.ver;
229
+ if (typeof ver !== "number" || !Number.isInteger(ver) || ver < 0) {
230
+ throw new chunkJGIO7XPR_cjs.SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);
231
+ }
232
+ if (ver > TICKET_FORMAT_VERSION) {
233
+ throw new chunkJGIO7XPR_cjs.SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) \u2014 upgrade @broberg/sso`);
234
+ }
235
+ const act = payload.act === void 0 ? null : readActor(payload.act, 1);
236
+ const cnfRaw = payload.cnf;
237
+ const cnf = cnfRaw && typeof cnfRaw.jkt === "string" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;
228
238
  return {
229
239
  principal: payload.sub,
230
240
  type,
231
241
  clientId: typeof payload.client_id === "string" && payload.client_id ? payload.client_id : payload.sub,
232
242
  org: typeof payload.org === "string" && payload.org ? payload.org : null,
233
243
  act,
244
+ version: ver,
245
+ cnf,
234
246
  scopes,
235
247
  exp: payload.exp,
236
248
  jti: String(payload.jti)
@@ -238,6 +250,14 @@ function createTicketVerifier(options) {
238
250
  }
239
251
  };
240
252
  }
253
+ function readActor(raw, depth) {
254
+ if (depth > MAX_ACT_DEPTH) throw new chunkJGIO7XPR_cjs.SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);
255
+ const link = raw;
256
+ if (!link || typeof link !== "object" || typeof link.sub !== "string" || link.sub === "") {
257
+ throw new chunkJGIO7XPR_cjs.SsoError(`ticket's act (link ${depth}) has no string sub`);
258
+ }
259
+ return link.act === void 0 ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };
260
+ }
241
261
 
242
262
  Object.defineProperty(exports, "AVATAR_TYPES", {
243
263
  enumerable: true,
@@ -332,7 +352,9 @@ Object.defineProperty(exports, "verifyValue", {
332
352
  get: function () { return chunkJGIO7XPR_cjs.verifyValue; }
333
353
  });
334
354
  exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
355
+ exports.MAX_ACT_DEPTH = MAX_ACT_DEPTH;
335
356
  exports.NoWorkloadIdentityError = NoWorkloadIdentityError;
357
+ exports.TICKET_FORMAT_VERSION = TICKET_FORMAT_VERSION;
336
358
  exports.TicketExchangeError = TicketExchangeError;
337
359
  exports.TicketUnavailableError = TicketUnavailableError;
338
360
  exports.createTicketClient = createTicketClient;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fetch-ticket.ts","../src/ticket.ts"],"names":["request","existsSync","SsoError","createJwksCache","JwksUnavailableError","decodeProtectedHeader","jwtVerify","ALLOWED_ALGS"],"mappings":";;;;;;;AAkBO,IAAM,cAAA,GAAiB;AAC9B,IAAM,UAAA,GAAa,WAAA;AAGZ,IAAM,uBAAA,GAAN,cAAsC,KAAA,CAAM;AAAA,EACjD,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,yBAAA;AAAA,EACd;AACF;AAOO,IAAM,mBAAA,GAAN,cAAkC,KAAA,CAAM;AAAA,EAC7C,WAAA,CAAY,OAAA,EAA0B,IAAA,EAAuB,MAAA,EAAgB;AAC3E,IAAA,KAAA,CAAM,OAAO,CAAA;AADuB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAuB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAE3D,IAAA,IAAA,CAAK,IAAA,GAAO,qBAAA;AAAA,EACd;AAAA,EAHsC,IAAA;AAAA,EAAuB,MAAA;AAI/D;AAGO,IAAM,sBAAA,GAAN,cAAqC,KAAA,CAAM;AAAA,EAChD,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,wBAAA;AAAA,EACd;AACF;AAqBA,SAAS,kBAAkB,GAAA,EAA8B;AACvD,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,EAAE,KAAK,CAAA;AACnC,IAAA,MAAM,GAAA,GAAMA,YAAA;AAAA,MACV,EAAE,UAAA,EAAY,UAAA,EAAY,IAAA,EAAM,iBAAA,EAAmB,QAAQ,MAAA,EAAQ,OAAA,EAAS,EAAE,cAAA,EAAgB,oBAAoB,gBAAA,EAAkB,MAAA,CAAO,UAAA,CAAW,IAAI,GAAE,EAAE;AAAA,MAC9J,CAAC,GAAA,KAAQ;AACP,QAAA,IAAI,IAAA,GAAO,EAAA;AACX,QAAA,GAAA,CAAI,YAAY,MAAM,CAAA;AACtB,QAAA,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAO,QAAQ,CAAE,CAAA;AACjC,QAAA,GAAA,CAAI,EAAA,CAAG,OAAO,MAAM;AAClB,UAAA,IAAA,CAAK,GAAA,CAAI,UAAA,IAAc,CAAA,KAAM,GAAA,IAAA,CAAQ,GAAA,CAAI,UAAA,IAAc,CAAA,IAAK,GAAA,EAAK,OAAA,CAAQ,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,sBACxE,IAAI,sBAAA,CAAuB,qBAAqB,GAAA,CAAI,UAAU,EAAE,CAAC,CAAA;AAAA,QAC/E,CAAC,CAAA;AAAA,MACH;AAAA,KACF;AACA,IAAA,GAAA,CAAI,EAAA,CAAG,OAAA,EAAS,CAAC,CAAA,KAAM,MAAA,CAAO,IAAI,sBAAA,CAAuB,CAAA,iBAAA,EAAoB,CAAA,CAAE,OAAO,CAAA,CAAE,CAAC,CAAC,CAAA;AAC1F,IAAA,GAAA,CAAI,IAAI,IAAI,CAAA;AAAA,EACd,CAAC,CAAA;AACH;AAGA,SAAS,OAAO,KAAA,EAA8B;AAC5C,EAAA,IAAI;AACF,IAAA,MAAM,UAAU,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,IAAA,CAAK,MAAM,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,KAAK,EAAA,EAAI,WAAW,CAAA,CAAE,QAAA,CAAS,MAAM,CAAC,CAAA;AAC/F,IAAA,OAAO,OAAO,OAAA,CAAQ,GAAA,KAAQ,QAAA,GAAW,QAAQ,GAAA,GAAM,IAAA;AAAA,EACzD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEO,SAAS,kBAAA,CAAmB,OAAA,GAA+B,EAAC,EAAiB;AAClF,EAAA,MAAM,UAAU,OAAA,CAAQ,MAAA,IAAU,cAAA,EAAgB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACpE,EAAA,MAAM,cAAA,GAAiB,QAAQ,cAAA,IAAkB,EAAA;AACjD,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,IAAO,OAAA,CAAQ,GAAA;AACnC,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,KAAA;AACvC,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,KAAQ,MAAM,KAAK,GAAA,EAAI,CAAA;AAC3C,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,IAAW,iBAAA;AACnC,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,KAAU,MAAM,CAAC,CAAC,GAAA,CAAI,YAAA,IAAgBC,aAAA,CAAW,UAAU,CAAA,CAAA;AAEjF,EAAA,MAAM,KAAA,uBAAY,GAAA,EAA4C;AAC9D,EAAA,MAAM,QAAA,uBAAe,GAAA,EAA6B;AAElD,EAAA,eAAe,aAAA,GAAiC;AAC9C,IAAA,IAAI,KAAA,EAAM,EAAG,OAAO,OAAA,CAAQ,MAAM,CAAA;AAClC,IAAA,MAAM,MAAM,GAAA,CAAI,4BAAA;AAChB,IAAA,MAAM,SAAS,GAAA,CAAI,8BAAA;AACnB,IAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AACF,QAAA,GAAA,GAAM,MAAM,SAAA,CAAU,CAAA,EAAG,GAAG,CAAA,UAAA,EAAa,mBAAmB,MAAM,CAAC,CAAA,CAAA,EAAI,EAAE,SAAS,EAAE,aAAA,EAAe,UAAU,MAAM,CAAA,CAAA,IAAM,CAAA;AAAA,MAC3H,SAAS,CAAA,EAAG;AACV,QAAA,MAAM,IAAI,sBAAA,CAAuB,CAAA,kCAAA,EAAqC,CAAA,YAAa,KAAA,GAAQ,EAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAE,CAAA;AAAA,MACpH;AACA,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI,MAAM,IAAI,sBAAA,CAAuB,CAAA,8BAAA,EAAiC,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAC3F,MAAA,MAAM,EAAE,KAAA,EAAM,GAAK,MAAM,IAAI,IAAA,EAAK;AAClC,MAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,uBAAuB,wCAAwC,CAAA;AACrF,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,MAAM,IAAI,uBAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AAEA,EAAA,eAAe,QAAA,CAAS,UAAkB,KAAA,EAAoE;AAC5G,IAAA,MAAM,OAAA,GAAU,MAAM,aAAA,EAAc;AACpC,IAAA,MAAM,IAAA,GAAO,IAAI,eAAA,CAAgB;AAAA,MAC/B,UAAA,EAAY,iDAAA;AAAA,MACZ,aAAA,EAAe,OAAA;AAAA,MACf,kBAAA,EAAoB,sCAAA;AAAA,MACpB;AAAA,KACD,CAAA;AACD,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,KAAK,CAAA;AAClC,IAAA,IAAI,GAAA;AACJ,IAAA,IAAI;AACF,MAAA,GAAA,GAAM,MAAM,SAAA,CAAU,CAAA,EAAG,MAAM,CAAA,aAAA,CAAA,EAAiB;AAAA,QAC9C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,mCAAA,EAAoC;AAAA,QAC/D,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH,SAAS,CAAA,EAAG;AACV,MAAA,MAAM,IAAI,sBAAA,CAAuB,CAAA,EAAG,MAAM,CAAA,cAAA,EAAiB,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAE,CAAA;AAAA,IACzG;AACA,IAAA,IAAI,OAAmG,EAAC;AACxG,IAAA,IAAI;AACF,MAAA,IAAA,GAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,IACzB,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,IAAI,GAAA,CAAI,MAAA,IAAU,GAAA,EAAK,MAAM,IAAI,sBAAA,CAAuB,CAAA,EAAG,MAAM,CAAA,UAAA,EAAa,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAC1F,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,IAAM,CAAC,KAAK,YAAA,EAAc;AACjC,MAAA,MAAM,IAAA,GAAO,KAAK,iBAAA,IAAqB,EAAA;AACvC,MAAA,MAAM,OAAO,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,GAAI,KAAK,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAC,CAAA,CAAE,IAAA,EAAK,GAAI,KAAK,KAAA,IAAS,kBAAA;AAC1F,MAAA,MAAM,IAAI,mBAAA,CAAoB,CAAA,kCAAA,EAAqC,QAAQ,CAAA,EAAA,EAAK,IAAA,IAAQ,IAAA,CAAK,KAAA,IAAS,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,IAAA,EAAM,IAAI,MAAM,CAAA;AAAA,IACtI;AACA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAA,KAAQ,GAAI,CAAA;AACtC,IAAA,MAAM,MAAM,MAAA,CAAO,IAAA,CAAK,YAAY,CAAA,IAAK,MAAA,IAAU,KAAK,UAAA,IAAc,GAAA,CAAA;AACtE,IAAA,OAAO,EAAE,KAAA,EAAO,IAAA,CAAK,YAAA,EAAc,GAAA,EAAI;AAAA,EACzC;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,GAAA,CAAI,EAAE,QAAA,EAAU,OAAM,EAAG;AAC7B,MAAA,MAAM,GAAA,GAAM,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,SAAS,EAAE,CAAA,CAAA;AACtC,MAAA,MAAM,GAAA,GAAM,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AACzB,MAAA,IAAI,GAAA,IAAO,IAAI,GAAA,GAAM,cAAA,GAAiB,KAAI,GAAI,GAAA,SAAa,GAAA,CAAI,KAAA;AAC/D,MAAA,MAAM,OAAA,GAAU,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AAChC,MAAA,IAAI,SAAS,OAAO,OAAA;AACpB,MAAA,MAAM,IAAI,QAAA,CAAS,QAAA,EAAU,KAAK,CAAA,CAC/B,IAAA,CAAK,CAAC,CAAA,KAAM;AACX,QAAA,KAAA,CAAM,GAAA,CAAI,KAAK,CAAC,CAAA;AAChB,QAAA,OAAO,CAAA,CAAE,KAAA;AAAA,MACX,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM,QAAA,CAAS,MAAA,CAAO,GAAG,CAAC,CAAA;AACrC,MAAA,QAAA,CAAS,GAAA,CAAI,KAAK,CAAC,CAAA;AACnB,MAAA,OAAO,CAAA;AAAA,IACT;AAAA,GACF;AACF;AAEA,IAAI,MAAA,GAA8B,IAAA;AAE3B,SAAS,YAAY,IAAA,EAA6D;AACvF,EAAA,MAAA,KAAW,kBAAA,EAAmB;AAC9B,EAAA,OAAO,MAAA,CAAO,IAAI,IAAI,CAAA;AACxB;ACpKA,IAAM,eAAA,GAA4C,CAAC,OAAA,EAAS,SAAA,EAAW,OAAO,CAAA;AA6CvE,SAAS,qBAAqB,OAAA,EAAgD;AACnF,EAAA,MAAM;AAAA,IACJ,MAAA;AAAA,IACA,QAAA;AAAA,IACA,iBAAiB,EAAA,GAAK,EAAA;AAAA,IACtB,iBAAA,GAAoB,EAAA;AAAA,IACpB,SAAA,GAAY;AAAA,GACd,GAAI,OAAA;AACJ,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAIC,2BAAS,sCAAsC,CAAA;AACtE,EAAA,IAAI,CAAC,QAAA,EAAU,MAAM,IAAIA,2BAAS,qDAAqD,CAAA;AAEvF,EAAA,IAAI,YAAA,GAA0C,IAAA;AAC9C,EAAA,MAAM,QAAA,GAAW,CAAC,OAAA,KAChBC,iCAAA,CAAgB;AAAA,IACd,OAAA;AAAA,IACA,SAAA;AAAA,IACA,GAAI,OAAA,CAAQ,MAAA,GAAS,EAAE,MAAA,EAAQ,IAAA,KAAS,EAAC;AAAA,IACzC,GAAI,QAAQ,oBAAA,KAAyB,MAAA,GAAY,EAAE,oBAAA,EAAsB,OAAA,CAAQ,oBAAA,EAAqB,GAAI;AAAC,GAC5G,CAAA;AAEH,EAAA,eAAe,IAAA,GAA2B;AACxC,IAAA,YAAA,KAAA,CAAkB,YAAY;AAC5B,MAAA,IAAI,OAAA,CAAQ,OAAA,EAAS,OAAO,QAAA,CAAS,QAAQ,OAAO,CAAA;AACpD,MAAA,MAAM,GAAA,GAAM,GAAG,MAAM,CAAA,iCAAA,CAAA;AACrB,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAG,CAAA;AAC/B,QAAA,IAAI,CAAC,IAAI,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,SAAA,EAAY,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AACrD,QAAA,GAAA,GAAO,MAAM,IAAI,IAAA,EAAK;AAAA,MACxB,SAAS,KAAA,EAAO;AAEd,QAAA,MAAM,IAAIC,sCAAA,CAAqB,CAAA,EAAG,GAAG,CAAA,EAAA,EAAK,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,MAAA,CAAO,KAAK,CAAC,CAAA,CAAE,CAAA;AAAA,MACpG;AACA,MAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,QAAA,MAAM,IAAIF,2BAAS,CAAA,UAAA,EAAa,MAAM,QAAQ,GAAG,CAAA,MAAA,EAAS,GAAA,CAAI,MAAM,CAAA,+BAAA,CAA4B,CAAA;AAAA,MAClG;AACA,MAAA,IAAI,CAAC,IAAI,QAAA,EAAU,MAAM,IAAIA,0BAAA,CAAS,CAAA,EAAG,GAAG,CAAA,kBAAA,CAAoB,CAAA;AAChE,MAAA,OAAO,QAAA,CAAS,IAAI,QAAQ,CAAA;AAAA,IAC9B,CAAA,GAAG,CAAE,KAAA,CAAM,CAAC,GAAA,KAAQ;AAClB,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,MAAM,GAAA;AAAA,IACR,CAAC,CAAA;AACD,IAAA,OAAO,YAAA;AAAA,EACT;AACA,EAAA,IAAI,QAAQ,MAAA,EAAQ,KAAK,IAAA,EAAK,CAAE,MAAM,MAAM;AAAA,EAAC,CAAC,CAAA;AAE9C,EAAA,OAAO;AAAA,IACL,MAAM,MAAA,CAAO,KAAA,EAAO,IAAA,GAAO,EAAC,EAAG;AAC7B,MAAA,IAAI,MAAA;AACJ,MAAA,IAAI;AACF,QAAA,MAAA,GAASG,2BAAsB,KAAK,CAAA;AAAA,MACtC,CAAA,CAAA,MAAQ;AACN,QAAA,MAAM,IAAIH,2BAAS,qBAAqB,CAAA;AAAA,MAC1C;AACA,MAAA,IAAI,CAAC,MAAA,CAAO,GAAA,EAAK,MAAM,IAAIA,2BAAS,oDAA+C,CAAA;AACnF,MAAA,IAAI,OAAO,GAAA,KAAQ,YAAA,EAAc,MAAM,IAAIA,2BAAS,gCAAgC,CAAA;AAEpF,MAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,EAAK;AACzB,MAAA,IAAI,OAAA;AACJ,MAAA,IAAI;AACF,QAAA,CAAC,EAAE,OAAA,EAAQ,GAAI,MAAMI,eAAU,KAAA,EAAO,YAAY,KAAA,CAAM,MAAA,CAAO,MAAA,CAAO,GAAA,EAAM,MAAA,CAAO,GAAA,IAAO,OAAO,CAAA,EAAG;AAAA,UAClG,MAAA;AAAA,UACA,QAAA;AAAA,UACA,UAAA,EAAYC,8BAAA;AAAA,UACZ,cAAA,EAAgB,iBAAA;AAAA,UAChB,cAAA,EAAgB,CAAC,KAAA,EAAO,KAAA,EAAO,OAAO,KAAK;AAAA,SAC5C,CAAA;AAAA,MACH,SAAS,CAAA,EAAG;AAEV,QAAA,IAAI,CAAA,YAAaH,wCAAsB,MAAM,CAAA;AAC7C,QAAA,MAAM,IAAIF,0BAAA,CAAS,CAAA,iBAAA,EAAoB,CAAA,YAAa,KAAA,GAAQ,EAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAE,CAAA;AAAA,MACrF;AAIA,MAAA,IAAI,QAAA,IAAY,OAAA,EAAS,MAAM,IAAIA,2BAAS,gCAAgC,CAAA;AAC5E,MAAA,IAAI,OAAA,IAAW,OAAA,EAAS,MAAM,IAAIA,2BAAS,6BAA6B,CAAA;AAExE,MAAA,MAAM,OAAO,OAAA,CAAQ,cAAA;AACrB,MAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,CAAC,eAAA,CAAgB,QAAA,CAAS,IAAqB,CAAA,EAAG;AAChF,QAAA,MAAM,IAAIA,0BAAA,CAAS,CAAA,wCAAA,EAA2C,KAAK,SAAA,CAAU,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,MACvF;AACA,MAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,GAAO,OAAA,CAAQ,GAAA;AACxC,MAAA,IAAI,WAAW,cAAA,EAAgB;AAC7B,QAAA,MAAM,IAAIA,0BAAA,CAAS,CAAA,gBAAA,EAAmB,QAAQ,CAAA,oBAAA,EAAuB,cAAc,CAAA,aAAA,CAAe,CAAA;AAAA,MACpG;AACA,MAAA,IAAI,OAAO,OAAA,CAAQ,GAAA,KAAQ,QAAA,IAAY,OAAA,CAAQ,QAAQ,EAAA,EAAI,MAAM,IAAIA,0BAAA,CAAS,mBAAmB,CAAA;AAEjG,MAAA,MAAM,MAAA,GAAS,OAAO,OAAA,CAAQ,KAAA,KAAU,QAAA,GAAW,OAAA,CAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,IAAI,EAAC;AAC/F,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,KAAU,MAAA,GAAY,EAAC,GAAI,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA,CAAK,KAAA,GAAQ,CAAC,KAAK,KAAK,CAAA;AACrG,MAAA,MAAM,OAAA,GAAU,SAAS,MAAA,CAAO,CAAC,MAAM,CAAC,MAAA,CAAO,QAAA,CAAS,CAAC,CAAC,CAAA;AAC1D,MAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,MAAM,IAAIA,0BAAA,CAAS,sBAAsB,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAEjF,MAAA,MAAM,SAAS,OAAA,CAAQ,GAAA;AACvB,MAAA,MAAM,GAAA,GAAM,MAAA,IAAU,OAAO,MAAA,CAAO,GAAA,KAAQ,QAAA,IAAY,MAAA,CAAO,GAAA,GAAM,EAAE,GAAA,EAAK,MAAA,CAAO,GAAA,EAAI,GAAI,IAAA;AAC3F,MAAA,OAAO;AAAA,QACL,WAAW,OAAA,CAAQ,GAAA;AAAA,QACnB,IAAA;AAAA,QACA,QAAA,EAAU,OAAO,OAAA,CAAQ,SAAA,KAAc,YAAY,OAAA,CAAQ,SAAA,GAAY,OAAA,CAAQ,SAAA,GAAY,OAAA,CAAQ,GAAA;AAAA,QACnG,GAAA,EAAK,OAAO,OAAA,CAAQ,GAAA,KAAQ,YAAY,OAAA,CAAQ,GAAA,GAAM,QAAQ,GAAA,GAAM,IAAA;AAAA,QACpE,GAAA;AAAA,QACA,MAAA;AAAA,QACA,KAAK,OAAA,CAAQ,GAAA;AAAA,QACb,GAAA,EAAK,MAAA,CAAO,OAAA,CAAQ,GAAG;AAAA,OACzB;AAAA,IACF;AAAA,GACF;AACF","file":"index.cjs","sourcesContent":["/**\n * F084.154 — fetch a Broberg ID ticket WITHOUT a key (broberg-id-F087.10/.11).\n *\n * The sending half of F084.153: a workload proves who it is with the identity its\n * platform already gives it, and BID exchanges that for a short-lived ticket\n * (RFC 8693 token exchange). No secret is stored anywhere.\n *\n * Fly.io POST unix:/.fly/api /v1/tokens/oidc {aud: issuer}\n * GitHub Actions GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=issuer\n * (needs `permissions: id-token: write` in the workflow)\n * then POST {issuer}/oauth2/token grant_type=…token-exchange\n *\n * Reference implementation: broberg-id/.github/workflows/ticket-probe.yml.\n * The ticket itself is never logged or put in an error message.\n */\nimport { existsSync } from \"node:fs\";\nimport { request } from \"node:http\";\n\nexport const DEFAULT_ISSUER = \"https://id.broberg.ai\";\nconst FLY_SOCKET = \"/.fly/api\";\n\n/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */\nexport class NoWorkloadIdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"NoWorkloadIdentityError\";\n }\n}\n\n/**\n * BID refused the exchange. `code` is BID's own error name (the part of\n * error_description before the colon, e.g. `no_rule_for_audience`), `status` the\n * HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.\n */\nexport class TicketExchangeError extends Error {\n constructor(message: string, readonly code: string, readonly status: number) {\n super(message);\n this.name = \"TicketExchangeError\";\n }\n}\n\n/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */\nexport class TicketUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TicketUnavailableError\";\n }\n}\n\nexport interface TicketClientOptions {\n issuer?: string;\n /** Renew this many seconds before exp. Default 30. */\n renewBeforeSec?: number;\n /** Injectable for tests. */\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */\n flyOidc?: (aud: string) => Promise<string>;\n /** Whether the Fly socket is present. Default: checks /.fly/api. */\n onFly?: () => boolean;\n}\n\nexport interface TicketClient {\n /** A ticket for `audience` (BID's short name, e.g. \"discovery\"), from cache while it is fresh. */\n get(opts: { audience: string; scope?: string }): Promise<string>;\n}\n\nfunction flyOidcOverSocket(aud: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ aud });\n const req = request(\n { socketPath: FLY_SOCKET, path: \"/v1/tokens/oidc\", method: \"POST\", headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) } },\n (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (c) => (data += c));\n res.on(\"end\", () => {\n if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());\n else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));\n });\n },\n );\n req.on(\"error\", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));\n req.end(body);\n });\n}\n\n/** Seconds since the epoch at which a JWT expires, or null if it cannot be read. */\nfunction jwtExp(token: string): number | null {\n try {\n const payload = JSON.parse(Buffer.from(token.split(\".\")[1] ?? \"\", \"base64url\").toString(\"utf8\"));\n return typeof payload.exp === \"number\" ? payload.exp : null;\n } catch {\n return null;\n }\n}\n\nexport function createTicketClient(options: TicketClientOptions = {}): TicketClient {\n const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\\/+$/, \"\");\n const renewBeforeSec = options.renewBeforeSec ?? 30;\n const env = options.env ?? process.env;\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = options.now ?? (() => Date.now());\n const flyOidc = options.flyOidc ?? flyOidcOverSocket;\n const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));\n\n const cache = new Map<string, { token: string; exp: number }>();\n const inFlight = new Map<string, Promise<string>>();\n\n async function platformToken(): Promise<string> {\n if (onFly()) return flyOidc(issuer);\n const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (url && bearer) {\n let res: Response;\n try {\n res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });\n } catch (e) {\n throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);\n const { value } = (await res.json()) as { value?: string };\n if (!value) throw new TicketUnavailableError(\"GitHub OIDC endpoint returned no token\");\n return value;\n }\n throw new NoWorkloadIdentityError(\n \"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job \" +\n \"(ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN — add `permissions: id-token: write`). A Mac session keeps using its session key.\",\n );\n }\n\n async function exchange(audience: string, scope: string | undefined): Promise<{ token: string; exp: number }> {\n const subject = await platformToken();\n const form = new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:token-exchange\",\n subject_token: subject,\n subject_token_type: \"urn:ietf:params:oauth:token-type:jwt\",\n audience,\n });\n if (scope) form.set(\"scope\", scope);\n let res: Response;\n try {\n res = await fetchImpl(`${issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: form,\n });\n } catch (e) {\n throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n let body: { access_token?: string; expires_in?: number; error?: string; error_description?: string } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n /* a non-JSON answer is handled by the status checks below */\n }\n if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);\n if (!res.ok || !body.access_token) {\n const desc = body.error_description ?? \"\";\n const code = desc.includes(\":\") ? desc.slice(0, desc.indexOf(\":\")).trim() : body.error ?? \"exchange_refused\";\n throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);\n }\n const nowSec = Math.floor(now() / 1000);\n const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);\n return { token: body.access_token, exp };\n }\n\n return {\n async get({ audience, scope }) {\n const key = `${audience} ${scope ?? \"\"}`;\n const hit = cache.get(key);\n if (hit && hit.exp - renewBeforeSec > now() / 1000) return hit.token;\n const pending = inFlight.get(key);\n if (pending) return pending;\n const p = exchange(audience, scope)\n .then((t) => {\n cache.set(key, t);\n return t.token;\n })\n .finally(() => inFlight.delete(key));\n inFlight.set(key, p);\n return p;\n },\n };\n}\n\nlet shared: TicketClient | null = null;\n/** A ticket from the process-wide client (one cache per process). */\nexport function fetchTicket(opts: { audience: string; scope?: string }): Promise<string> {\n shared ??= createTicketClient();\n return shared.get(opts);\n}\n","/**\n * F084.153 — verify a Broberg ID ticket LOCALLY (broberg-id-F087.4).\n *\n * A ticket is how one service or agent proves who it is to another without a\n * shared secret: Broberg ID signs a short-lived JWT, the receiver checks it\n * against BID's PUBLIC keys. Nothing secret lives at the receiver.\n *\n * Format: broberg-id docs/features/F087.1-identitetsmodel.md §3 (draft, 8/10).\n * iss = the issuer · aud = the receiver's URL · scope = space-separated\n * sub = client_id = the caller · principal_type = human | service | agent\n * org · act {sub} only on delegation (RFC 8693) · exp/iat/jti\n *\n * This is deliberately NOT a method on createSsoClient: a service that only\n * RECEIVES tickets is not a login client — it has no client_id, no redirect,\n * no session. It needs the issuer, its own audience, and the key set.\n *\n * TWO FAILURES, TWO ANSWERS — keep them apart in the caller:\n * JwksUnavailableError → we could not ask BID right now. Answer 503 and let\n * the caller retry; the ticket may be perfectly valid.\n * SsoError → the ticket is not acceptable. Answer 401/403.\n * A known key is served from cache, so a ticket signed with it verifies while\n * BID is briefly unreachable.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport { ALLOWED_ALGS, SsoError } from \"./client.js\";\nimport { createJwksCache, JwksUnavailableError, type JwksCache } from \"./jwks.js\";\n\nexport type PrincipalType = \"human\" | \"service\" | \"agent\";\nconst PRINCIPAL_TYPES: readonly PrincipalType[] = [\"human\", \"service\", \"agent\"];\n\n/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */\nexport interface TicketPrincipal {\n /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */\n principal: string;\n type: PrincipalType;\n /** The client the ticket was issued to (`client_id`, falling back to `sub`). */\n clientId: string;\n /** The organisation, when BID stated one. */\n org: string | null;\n /** On delegation: who the caller is acting for (RFC 8693 `act.sub`). */\n act: { sub: string } | null;\n scopes: string[];\n /** Seconds since the epoch. */\n exp: number;\n jti: string;\n}\n\nexport interface TicketVerifierOptions {\n /** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */\n issuer: string;\n /** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */\n audience: string;\n /** The key set URL. Omitted: read once from the issuer's discovery document. */\n jwksUri?: string;\n /**\n * The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min\n * (service) and 15 min (agent); anything longer was not minted by the policy\n * we were told about, so it is refused rather than trusted for longer.\n */\n maxLifetimeSec?: number;\n /** Seconds of clock skew to allow on exp/iat. Default 30. */\n clockToleranceSec?: number;\n /** Fetch the key set at construction, so the first caller does not wait. */\n warmUp?: boolean;\n fetchImpl?: typeof fetch;\n minRefetchIntervalMs?: number;\n}\n\nexport interface TicketVerifier {\n /** Verify a ticket; with `scope`, also require that scope to be granted. */\n verify(token: string, options?: { scope?: string | string[] }): Promise<TicketPrincipal>;\n}\n\nexport function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier {\n const {\n issuer,\n audience,\n maxLifetimeSec = 15 * 60,\n clockToleranceSec = 30,\n fetchImpl = fetch,\n } = options;\n if (!issuer) throw new SsoError(\"createTicketVerifier needs an issuer\");\n if (!audience) throw new SsoError(\"createTicketVerifier needs this receiver's audience\");\n\n let cachePromise: Promise<JwksCache> | null = null;\n const cacheFor = (jwksUri: string) =>\n createJwksCache({\n jwksUri,\n fetchImpl,\n ...(options.warmUp ? { warmUp: true } : {}),\n ...(options.minRefetchIntervalMs !== undefined ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}),\n });\n\n async function keys(): Promise<JwksCache> {\n cachePromise ??= (async () => {\n if (options.jwksUri) return cacheFor(options.jwksUri);\n const url = `${issuer}/.well-known/openid-configuration`;\n let doc: { issuer?: string; jwks_uri?: string };\n try {\n const res = await fetchImpl(url);\n if (!res.ok) throw new Error(`answered ${res.status}`);\n doc = (await res.json()) as typeof doc;\n } catch (cause) {\n // Reached nothing usable: transient, the same class a key-set outage gets.\n throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n if (doc.issuer !== issuer) {\n throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} — they must match exactly`);\n }\n if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);\n return cacheFor(doc.jwks_uri);\n })().catch((err) => {\n cachePromise = null; // a later call retries instead of caching the failure\n throw err;\n });\n return cachePromise;\n }\n if (options.warmUp) void keys().catch(() => {});\n\n return {\n async verify(token, opts = {}) {\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"ticket is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"ticket has no kid — cannot pick a signing key\");\n if (header.typ === \"logout+jwt\") throw new SsoError(\"a logout token is not a ticket\");\n\n const cache = await keys();\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"EdDSA\"), {\n issuer,\n audience,\n algorithms: ALLOWED_ALGS,\n clockTolerance: clockToleranceSec,\n requiredClaims: [\"exp\", \"iat\", \"jti\", \"sub\"],\n }));\n } catch (e) {\n // A key-set outage must surface as itself (→ 503), never as \"rejected\".\n if (e instanceof JwksUnavailableError) throw e;\n throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // An ID token or a logout token must never pass as a ticket. They carry\n // no principal_type; a logout token carries events, an ID token a nonce.\n if (\"events\" in payload) throw new SsoError(\"a logout token is not a ticket\");\n if (\"nonce\" in payload) throw new SsoError(\"an ID token is not a ticket\");\n\n const type = payload.principal_type;\n if (typeof type !== \"string\" || !PRINCIPAL_TYPES.includes(type as PrincipalType)) {\n throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);\n }\n const lifetime = payload.exp! - payload.iat!;\n if (lifetime > maxLifetimeSec) {\n throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"ticket has no sub\");\n\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : [];\n const required = opts.scope === undefined ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n const missing = required.filter((s) => !scopes.includes(s));\n if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(\", \")}`);\n\n const actRaw = payload.act as { sub?: unknown } | undefined;\n const act = actRaw && typeof actRaw.sub === \"string\" && actRaw.sub ? { sub: actRaw.sub } : null;\n return {\n principal: payload.sub,\n type: type as PrincipalType,\n clientId: typeof payload.client_id === \"string\" && payload.client_id ? payload.client_id : payload.sub,\n org: typeof payload.org === \"string\" && payload.org ? payload.org : null,\n act,\n scopes,\n exp: payload.exp!,\n jti: String(payload.jti),\n };\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/fetch-ticket.ts","../src/ticket.ts"],"names":["request","existsSync","SsoError","createJwksCache","JwksUnavailableError","decodeProtectedHeader","jwtVerify","ALLOWED_ALGS"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * F084.154 — fetch a Broberg ID ticket WITHOUT a key (broberg-id-F087.10/.11).\n *\n * The sending half of F084.153: a workload proves who it is with the identity its\n * platform already gives it, and BID exchanges that for a short-lived ticket\n * (RFC 8693 token exchange). No secret is stored anywhere.\n *\n * Fly.io POST unix:/.fly/api /v1/tokens/oidc {aud: issuer}\n * GitHub Actions GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=issuer\n * (needs `permissions: id-token: write` in the workflow)\n * then POST {issuer}/oauth2/token grant_type=…token-exchange\n *\n * Reference implementation: broberg-id/.github/workflows/ticket-probe.yml.\n * The ticket itself is never logged or put in an error message.\n */\nimport { existsSync } from \"node:fs\";\nimport { request } from \"node:http\";\n\nexport const DEFAULT_ISSUER = \"https://id.broberg.ai\";\nconst FLY_SOCKET = \"/.fly/api\";\n\n/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */\nexport class NoWorkloadIdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"NoWorkloadIdentityError\";\n }\n}\n\n/**\n * BID refused the exchange. `code` is BID's own error name (the part of\n * error_description before the colon, e.g. `no_rule_for_audience`), `status` the\n * HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.\n */\nexport class TicketExchangeError extends Error {\n constructor(message: string, readonly code: string, readonly status: number) {\n super(message);\n this.name = \"TicketExchangeError\";\n }\n}\n\n/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */\nexport class TicketUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TicketUnavailableError\";\n }\n}\n\nexport interface TicketClientOptions {\n issuer?: string;\n /** Renew this many seconds before exp. Default 30. */\n renewBeforeSec?: number;\n /** Injectable for tests. */\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */\n flyOidc?: (aud: string) => Promise<string>;\n /** Whether the Fly socket is present. Default: checks /.fly/api. */\n onFly?: () => boolean;\n}\n\nexport interface TicketClient {\n /** A ticket for `audience` (BID's short name, e.g. \"discovery\"), from cache while it is fresh. */\n get(opts: { audience: string; scope?: string }): Promise<string>;\n}\n\nfunction flyOidcOverSocket(aud: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ aud });\n const req = request(\n { socketPath: FLY_SOCKET, path: \"/v1/tokens/oidc\", method: \"POST\", headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) } },\n (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (c) => (data += c));\n res.on(\"end\", () => {\n if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());\n else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));\n });\n },\n );\n req.on(\"error\", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));\n req.end(body);\n });\n}\n\n/** Seconds since the epoch at which a JWT expires, or null if it cannot be read. */\nfunction jwtExp(token: string): number | null {\n try {\n const payload = JSON.parse(Buffer.from(token.split(\".\")[1] ?? \"\", \"base64url\").toString(\"utf8\"));\n return typeof payload.exp === \"number\" ? payload.exp : null;\n } catch {\n return null;\n }\n}\n\nexport function createTicketClient(options: TicketClientOptions = {}): TicketClient {\n const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\\/+$/, \"\");\n const renewBeforeSec = options.renewBeforeSec ?? 30;\n const env = options.env ?? process.env;\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = options.now ?? (() => Date.now());\n const flyOidc = options.flyOidc ?? flyOidcOverSocket;\n const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));\n\n const cache = new Map<string, { token: string; exp: number }>();\n const inFlight = new Map<string, Promise<string>>();\n\n async function platformToken(): Promise<string> {\n if (onFly()) return flyOidc(issuer);\n const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (url && bearer) {\n let res: Response;\n try {\n res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });\n } catch (e) {\n throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);\n const { value } = (await res.json()) as { value?: string };\n if (!value) throw new TicketUnavailableError(\"GitHub OIDC endpoint returned no token\");\n return value;\n }\n throw new NoWorkloadIdentityError(\n \"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job \" +\n \"(ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN — add `permissions: id-token: write`). A Mac session keeps using its session key.\",\n );\n }\n\n async function exchange(audience: string, scope: string | undefined): Promise<{ token: string; exp: number }> {\n const subject = await platformToken();\n const form = new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:token-exchange\",\n subject_token: subject,\n subject_token_type: \"urn:ietf:params:oauth:token-type:jwt\",\n audience,\n });\n if (scope) form.set(\"scope\", scope);\n let res: Response;\n try {\n res = await fetchImpl(`${issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: form,\n });\n } catch (e) {\n throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n let body: { access_token?: string; expires_in?: number; error?: string; error_description?: string } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n /* a non-JSON answer is handled by the status checks below */\n }\n if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);\n if (!res.ok || !body.access_token) {\n const desc = body.error_description ?? \"\";\n const code = desc.includes(\":\") ? desc.slice(0, desc.indexOf(\":\")).trim() : body.error ?? \"exchange_refused\";\n throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);\n }\n const nowSec = Math.floor(now() / 1000);\n const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);\n return { token: body.access_token, exp };\n }\n\n return {\n async get({ audience, scope }) {\n const key = `${audience} ${scope ?? \"\"}`;\n const hit = cache.get(key);\n if (hit && hit.exp - renewBeforeSec > now() / 1000) return hit.token;\n const pending = inFlight.get(key);\n if (pending) return pending;\n const p = exchange(audience, scope)\n .then((t) => {\n cache.set(key, t);\n return t.token;\n })\n .finally(() => inFlight.delete(key));\n inFlight.set(key, p);\n return p;\n },\n };\n}\n\nlet shared: TicketClient | null = null;\n/** A ticket from the process-wide client (one cache per process). */\nexport function fetchTicket(opts: { audience: string; scope?: string }): Promise<string> {\n shared ??= createTicketClient();\n return shared.get(opts);\n}\n","/**\n * F084.153 — verify a Broberg ID ticket LOCALLY (broberg-id-F087.4).\n *\n * A ticket is how one service or agent proves who it is to another without a\n * shared secret: Broberg ID signs a short-lived JWT, the receiver checks it\n * against BID's PUBLIC keys. Nothing secret lives at the receiver.\n *\n * Format: broberg-id docs/features/F087.1-identitetsmodel.md §3 (draft, 8/10).\n * iss = the issuer · aud = the receiver's URL · scope = space-separated\n * sub = client_id = the caller · principal_type = human | service | agent\n * org · act {sub} only on delegation (RFC 8693) · exp/iat/jti\n *\n * This is deliberately NOT a method on createSsoClient: a service that only\n * RECEIVES tickets is not a login client — it has no client_id, no redirect,\n * no session. It needs the issuer, its own audience, and the key set.\n *\n * TWO FAILURES, TWO ANSWERS — keep them apart in the caller:\n * JwksUnavailableError → we could not ask BID right now. Answer 503 and let\n * the caller retry; the ticket may be perfectly valid.\n * SsoError → the ticket is not acceptable. Answer 401/403.\n * A known key is served from cache, so a ticket signed with it verifies while\n * BID is briefly unreachable.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport { ALLOWED_ALGS, SsoError } from \"./client.js\";\nimport { createJwksCache, JwksUnavailableError, type JwksCache } from \"./jwks.js\";\n\nexport type PrincipalType = \"human\" | \"service\" | \"agent\";\nconst PRINCIPAL_TYPES: readonly PrincipalType[] = [\"human\", \"service\", \"agent\"];\n\n/**\n * F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link\n * outward: agent-b acting for agent-a acting for user-1 reads\n * `{ sub: \"agent-a\", act: { sub: \"user-1\" } }`. `act.sub` alone is the old shape.\n */\nexport interface TicketActor {\n sub: string;\n act?: TicketActor;\n}\n\n/** The newest ticket format version this package understands (BID sets `ver`). */\nexport const TICKET_FORMAT_VERSION = 1;\n/** Delegation deeper than this is refused rather than shortened. */\nexport const MAX_ACT_DEPTH = 5;\n\n/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */\nexport interface TicketPrincipal {\n /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */\n principal: string;\n type: PrincipalType;\n /** The client the ticket was issued to (`client_id`, falling back to `sub`). */\n clientId: string;\n /** The organisation, when BID stated one. */\n org: string | null;\n /** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */\n act: TicketActor | null;\n /** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */\n version: number;\n /**\n * F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.\n * NOT checked here: until the receiver verifies a DPoP proof against it\n * (F087.16), a bound ticket is accepted exactly like a bearer ticket.\n */\n cnf: { jkt: string } | null;\n scopes: string[];\n /** Seconds since the epoch. */\n exp: number;\n jti: string;\n}\n\nexport interface TicketVerifierOptions {\n /** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */\n issuer: string;\n /** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */\n audience: string;\n /** The key set URL. Omitted: read once from the issuer's discovery document. */\n jwksUri?: string;\n /**\n * The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min\n * (service) and 15 min (agent); anything longer was not minted by the policy\n * we were told about, so it is refused rather than trusted for longer.\n */\n maxLifetimeSec?: number;\n /** Seconds of clock skew to allow on exp/iat. Default 30. */\n clockToleranceSec?: number;\n /** Fetch the key set at construction, so the first caller does not wait. */\n warmUp?: boolean;\n fetchImpl?: typeof fetch;\n minRefetchIntervalMs?: number;\n}\n\nexport interface TicketVerifier {\n /** Verify a ticket; with `scope`, also require that scope to be granted. */\n verify(token: string, options?: { scope?: string | string[] }): Promise<TicketPrincipal>;\n}\n\nexport function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier {\n const {\n issuer,\n audience,\n maxLifetimeSec = 15 * 60,\n clockToleranceSec = 30,\n fetchImpl = fetch,\n } = options;\n if (!issuer) throw new SsoError(\"createTicketVerifier needs an issuer\");\n if (!audience) throw new SsoError(\"createTicketVerifier needs this receiver's audience\");\n\n let cachePromise: Promise<JwksCache> | null = null;\n const cacheFor = (jwksUri: string) =>\n createJwksCache({\n jwksUri,\n fetchImpl,\n ...(options.warmUp ? { warmUp: true } : {}),\n ...(options.minRefetchIntervalMs !== undefined ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}),\n });\n\n async function keys(): Promise<JwksCache> {\n cachePromise ??= (async () => {\n if (options.jwksUri) return cacheFor(options.jwksUri);\n const url = `${issuer}/.well-known/openid-configuration`;\n let doc: { issuer?: string; jwks_uri?: string };\n try {\n const res = await fetchImpl(url);\n if (!res.ok) throw new Error(`answered ${res.status}`);\n doc = (await res.json()) as typeof doc;\n } catch (cause) {\n // Reached nothing usable: transient, the same class a key-set outage gets.\n throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n if (doc.issuer !== issuer) {\n throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} — they must match exactly`);\n }\n if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);\n return cacheFor(doc.jwks_uri);\n })().catch((err) => {\n cachePromise = null; // a later call retries instead of caching the failure\n throw err;\n });\n return cachePromise;\n }\n if (options.warmUp) void keys().catch(() => {});\n\n return {\n async verify(token, opts = {}) {\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"ticket is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"ticket has no kid — cannot pick a signing key\");\n if (header.typ === \"logout+jwt\") throw new SsoError(\"a logout token is not a ticket\");\n\n const cache = await keys();\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"EdDSA\"), {\n issuer,\n audience,\n algorithms: ALLOWED_ALGS,\n clockTolerance: clockToleranceSec,\n requiredClaims: [\"exp\", \"iat\", \"jti\", \"sub\"],\n }));\n } catch (e) {\n // A key-set outage must surface as itself (→ 503), never as \"rejected\".\n if (e instanceof JwksUnavailableError) throw e;\n throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // An ID token or a logout token must never pass as a ticket. They carry\n // no principal_type; a logout token carries events, an ID token a nonce.\n if (\"events\" in payload) throw new SsoError(\"a logout token is not a ticket\");\n if (\"nonce\" in payload) throw new SsoError(\"an ID token is not a ticket\");\n\n const type = payload.principal_type;\n if (typeof type !== \"string\" || !PRINCIPAL_TYPES.includes(type as PrincipalType)) {\n throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);\n }\n const lifetime = payload.exp! - payload.iat!;\n if (lifetime > maxLifetimeSec) {\n throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"ticket has no sub\");\n\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : [];\n const required = opts.scope === undefined ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n const missing = required.filter((s) => !scopes.includes(s));\n if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(\", \")}`);\n\n // F084.156 — ver: unknown future formats are refused by name, never read\n // with today's rules.\n const ver = payload.ver === undefined ? 0 : payload.ver;\n if (typeof ver !== \"number\" || !Number.isInteger(ver) || ver < 0) {\n throw new SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);\n }\n if (ver > TICKET_FORMAT_VERSION) {\n throw new SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) — upgrade @broberg/sso`);\n }\n // The whole chain, or a refusal. 0.14.2 kept only act.sub, cutting a chain\n // to its first link, and read a malformed act as \"no delegation\" — both in\n // the direction where the caller looks like it acts for fewer people.\n const act = payload.act === undefined ? null : readActor(payload.act, 1);\n const cnfRaw = payload.cnf as { jkt?: unknown } | undefined;\n const cnf = cnfRaw && typeof cnfRaw.jkt === \"string\" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;\n return {\n principal: payload.sub,\n type: type as PrincipalType,\n clientId: typeof payload.client_id === \"string\" && payload.client_id ? payload.client_id : payload.sub,\n org: typeof payload.org === \"string\" && payload.org ? payload.org : null,\n act,\n version: ver,\n cnf,\n scopes,\n exp: payload.exp!,\n jti: String(payload.jti),\n };\n },\n };\n}\n\nfunction readActor(raw: unknown, depth: number): TicketActor {\n if (depth > MAX_ACT_DEPTH) throw new SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);\n const link = raw as { sub?: unknown; act?: unknown } | null;\n if (!link || typeof link !== \"object\" || typeof link.sub !== \"string\" || link.sub === \"\") {\n throw new SsoError(`ticket's act (link ${depth}) has no string sub`);\n }\n return link.act === undefined ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -48,6 +48,19 @@ declare function fetchTicket(opts: {
48
48
  }): Promise<string>;
49
49
 
50
50
  type PrincipalType = "human" | "service" | "agent";
51
+ /**
52
+ * F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link
53
+ * outward: agent-b acting for agent-a acting for user-1 reads
54
+ * `{ sub: "agent-a", act: { sub: "user-1" } }`. `act.sub` alone is the old shape.
55
+ */
56
+ interface TicketActor {
57
+ sub: string;
58
+ act?: TicketActor;
59
+ }
60
+ /** The newest ticket format version this package understands (BID sets `ver`). */
61
+ declare const TICKET_FORMAT_VERSION = 1;
62
+ /** Delegation deeper than this is refused rather than shortened. */
63
+ declare const MAX_ACT_DEPTH = 5;
51
64
  /** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
52
65
  interface TicketPrincipal {
53
66
  /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */
@@ -57,9 +70,17 @@ interface TicketPrincipal {
57
70
  clientId: string;
58
71
  /** The organisation, when BID stated one. */
59
72
  org: string | null;
60
- /** On delegation: who the caller is acting for (RFC 8693 `act.sub`). */
61
- act: {
62
- sub: string;
73
+ /** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */
74
+ act: TicketActor | null;
75
+ /** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */
76
+ version: number;
77
+ /**
78
+ * F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.
79
+ * NOT checked here: until the receiver verifies a DPoP proof against it
80
+ * (F087.16), a bound ticket is accepted exactly like a bearer ticket.
81
+ */
82
+ cnf: {
83
+ jkt: string;
63
84
  } | null;
64
85
  scopes: string[];
65
86
  /** Seconds since the epoch. */
@@ -94,4 +115,4 @@ interface TicketVerifier {
94
115
  }
95
116
  declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
96
117
 
97
- export { DEFAULT_ISSUER, NoWorkloadIdentityError, type PrincipalType, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
118
+ export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, TICKET_FORMAT_VERSION, type TicketActor, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
package/dist/index.d.ts CHANGED
@@ -48,6 +48,19 @@ declare function fetchTicket(opts: {
48
48
  }): Promise<string>;
49
49
 
50
50
  type PrincipalType = "human" | "service" | "agent";
51
+ /**
52
+ * F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link
53
+ * outward: agent-b acting for agent-a acting for user-1 reads
54
+ * `{ sub: "agent-a", act: { sub: "user-1" } }`. `act.sub` alone is the old shape.
55
+ */
56
+ interface TicketActor {
57
+ sub: string;
58
+ act?: TicketActor;
59
+ }
60
+ /** The newest ticket format version this package understands (BID sets `ver`). */
61
+ declare const TICKET_FORMAT_VERSION = 1;
62
+ /** Delegation deeper than this is refused rather than shortened. */
63
+ declare const MAX_ACT_DEPTH = 5;
51
64
  /** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
52
65
  interface TicketPrincipal {
53
66
  /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */
@@ -57,9 +70,17 @@ interface TicketPrincipal {
57
70
  clientId: string;
58
71
  /** The organisation, when BID stated one. */
59
72
  org: string | null;
60
- /** On delegation: who the caller is acting for (RFC 8693 `act.sub`). */
61
- act: {
62
- sub: string;
73
+ /** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */
74
+ act: TicketActor | null;
75
+ /** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */
76
+ version: number;
77
+ /**
78
+ * F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.
79
+ * NOT checked here: until the receiver verifies a DPoP proof against it
80
+ * (F087.16), a bound ticket is accepted exactly like a bearer ticket.
81
+ */
82
+ cnf: {
83
+ jkt: string;
63
84
  } | null;
64
85
  scopes: string[];
65
86
  /** Seconds since the epoch. */
@@ -94,4 +115,4 @@ interface TicketVerifier {
94
115
  }
95
116
  declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
96
117
 
97
- export { DEFAULT_ISSUER, NoWorkloadIdentityError, type PrincipalType, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
118
+ export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, TICKET_FORMAT_VERSION, type TicketActor, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
package/dist/index.js CHANGED
@@ -141,6 +141,8 @@ function fetchTicket(opts) {
141
141
  return shared.get(opts);
142
142
  }
143
143
  var PRINCIPAL_TYPES = ["human", "service", "agent"];
144
+ var TICKET_FORMAT_VERSION = 1;
145
+ var MAX_ACT_DEPTH = 5;
144
146
  function createTicketVerifier(options) {
145
147
  const {
146
148
  issuer,
@@ -222,14 +224,24 @@ function createTicketVerifier(options) {
222
224
  const required = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
223
225
  const missing = required.filter((s) => !scopes.includes(s));
224
226
  if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(", ")}`);
225
- const actRaw = payload.act;
226
- const act = actRaw && typeof actRaw.sub === "string" && actRaw.sub ? { sub: actRaw.sub } : null;
227
+ const ver = payload.ver === void 0 ? 0 : payload.ver;
228
+ if (typeof ver !== "number" || !Number.isInteger(ver) || ver < 0) {
229
+ throw new SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);
230
+ }
231
+ if (ver > TICKET_FORMAT_VERSION) {
232
+ throw new SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) \u2014 upgrade @broberg/sso`);
233
+ }
234
+ const act = payload.act === void 0 ? null : readActor(payload.act, 1);
235
+ const cnfRaw = payload.cnf;
236
+ const cnf = cnfRaw && typeof cnfRaw.jkt === "string" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;
227
237
  return {
228
238
  principal: payload.sub,
229
239
  type,
230
240
  clientId: typeof payload.client_id === "string" && payload.client_id ? payload.client_id : payload.sub,
231
241
  org: typeof payload.org === "string" && payload.org ? payload.org : null,
232
242
  act,
243
+ version: ver,
244
+ cnf,
233
245
  scopes,
234
246
  exp: payload.exp,
235
247
  jti: String(payload.jti)
@@ -237,7 +249,15 @@ function createTicketVerifier(options) {
237
249
  }
238
250
  };
239
251
  }
252
+ function readActor(raw, depth) {
253
+ if (depth > MAX_ACT_DEPTH) throw new SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);
254
+ const link = raw;
255
+ if (!link || typeof link !== "object" || typeof link.sub !== "string" || link.sub === "") {
256
+ throw new SsoError(`ticket's act (link ${depth}) has no string sub`);
257
+ }
258
+ return link.act === void 0 ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };
259
+ }
240
260
 
241
- export { DEFAULT_ISSUER, NoWorkloadIdentityError, TicketExchangeError, TicketUnavailableError, createTicketClient, createTicketVerifier, fetchTicket };
261
+ export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, TICKET_FORMAT_VERSION, TicketExchangeError, TicketUnavailableError, createTicketClient, createTicketVerifier, fetchTicket };
242
262
  //# sourceMappingURL=index.js.map
243
263
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fetch-ticket.ts","../src/ticket.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * F084.154 — fetch a Broberg ID ticket WITHOUT a key (broberg-id-F087.10/.11).\n *\n * The sending half of F084.153: a workload proves who it is with the identity its\n * platform already gives it, and BID exchanges that for a short-lived ticket\n * (RFC 8693 token exchange). No secret is stored anywhere.\n *\n * Fly.io POST unix:/.fly/api /v1/tokens/oidc {aud: issuer}\n * GitHub Actions GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=issuer\n * (needs `permissions: id-token: write` in the workflow)\n * then POST {issuer}/oauth2/token grant_type=…token-exchange\n *\n * Reference implementation: broberg-id/.github/workflows/ticket-probe.yml.\n * The ticket itself is never logged or put in an error message.\n */\nimport { existsSync } from \"node:fs\";\nimport { request } from \"node:http\";\n\nexport const DEFAULT_ISSUER = \"https://id.broberg.ai\";\nconst FLY_SOCKET = \"/.fly/api\";\n\n/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */\nexport class NoWorkloadIdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"NoWorkloadIdentityError\";\n }\n}\n\n/**\n * BID refused the exchange. `code` is BID's own error name (the part of\n * error_description before the colon, e.g. `no_rule_for_audience`), `status` the\n * HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.\n */\nexport class TicketExchangeError extends Error {\n constructor(message: string, readonly code: string, readonly status: number) {\n super(message);\n this.name = \"TicketExchangeError\";\n }\n}\n\n/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */\nexport class TicketUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TicketUnavailableError\";\n }\n}\n\nexport interface TicketClientOptions {\n issuer?: string;\n /** Renew this many seconds before exp. Default 30. */\n renewBeforeSec?: number;\n /** Injectable for tests. */\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */\n flyOidc?: (aud: string) => Promise<string>;\n /** Whether the Fly socket is present. Default: checks /.fly/api. */\n onFly?: () => boolean;\n}\n\nexport interface TicketClient {\n /** A ticket for `audience` (BID's short name, e.g. \"discovery\"), from cache while it is fresh. */\n get(opts: { audience: string; scope?: string }): Promise<string>;\n}\n\nfunction flyOidcOverSocket(aud: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ aud });\n const req = request(\n { socketPath: FLY_SOCKET, path: \"/v1/tokens/oidc\", method: \"POST\", headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) } },\n (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (c) => (data += c));\n res.on(\"end\", () => {\n if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());\n else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));\n });\n },\n );\n req.on(\"error\", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));\n req.end(body);\n });\n}\n\n/** Seconds since the epoch at which a JWT expires, or null if it cannot be read. */\nfunction jwtExp(token: string): number | null {\n try {\n const payload = JSON.parse(Buffer.from(token.split(\".\")[1] ?? \"\", \"base64url\").toString(\"utf8\"));\n return typeof payload.exp === \"number\" ? payload.exp : null;\n } catch {\n return null;\n }\n}\n\nexport function createTicketClient(options: TicketClientOptions = {}): TicketClient {\n const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\\/+$/, \"\");\n const renewBeforeSec = options.renewBeforeSec ?? 30;\n const env = options.env ?? process.env;\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = options.now ?? (() => Date.now());\n const flyOidc = options.flyOidc ?? flyOidcOverSocket;\n const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));\n\n const cache = new Map<string, { token: string; exp: number }>();\n const inFlight = new Map<string, Promise<string>>();\n\n async function platformToken(): Promise<string> {\n if (onFly()) return flyOidc(issuer);\n const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (url && bearer) {\n let res: Response;\n try {\n res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });\n } catch (e) {\n throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);\n const { value } = (await res.json()) as { value?: string };\n if (!value) throw new TicketUnavailableError(\"GitHub OIDC endpoint returned no token\");\n return value;\n }\n throw new NoWorkloadIdentityError(\n \"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job \" +\n \"(ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN — add `permissions: id-token: write`). A Mac session keeps using its session key.\",\n );\n }\n\n async function exchange(audience: string, scope: string | undefined): Promise<{ token: string; exp: number }> {\n const subject = await platformToken();\n const form = new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:token-exchange\",\n subject_token: subject,\n subject_token_type: \"urn:ietf:params:oauth:token-type:jwt\",\n audience,\n });\n if (scope) form.set(\"scope\", scope);\n let res: Response;\n try {\n res = await fetchImpl(`${issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: form,\n });\n } catch (e) {\n throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n let body: { access_token?: string; expires_in?: number; error?: string; error_description?: string } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n /* a non-JSON answer is handled by the status checks below */\n }\n if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);\n if (!res.ok || !body.access_token) {\n const desc = body.error_description ?? \"\";\n const code = desc.includes(\":\") ? desc.slice(0, desc.indexOf(\":\")).trim() : body.error ?? \"exchange_refused\";\n throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);\n }\n const nowSec = Math.floor(now() / 1000);\n const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);\n return { token: body.access_token, exp };\n }\n\n return {\n async get({ audience, scope }) {\n const key = `${audience} ${scope ?? \"\"}`;\n const hit = cache.get(key);\n if (hit && hit.exp - renewBeforeSec > now() / 1000) return hit.token;\n const pending = inFlight.get(key);\n if (pending) return pending;\n const p = exchange(audience, scope)\n .then((t) => {\n cache.set(key, t);\n return t.token;\n })\n .finally(() => inFlight.delete(key));\n inFlight.set(key, p);\n return p;\n },\n };\n}\n\nlet shared: TicketClient | null = null;\n/** A ticket from the process-wide client (one cache per process). */\nexport function fetchTicket(opts: { audience: string; scope?: string }): Promise<string> {\n shared ??= createTicketClient();\n return shared.get(opts);\n}\n","/**\n * F084.153 — verify a Broberg ID ticket LOCALLY (broberg-id-F087.4).\n *\n * A ticket is how one service or agent proves who it is to another without a\n * shared secret: Broberg ID signs a short-lived JWT, the receiver checks it\n * against BID's PUBLIC keys. Nothing secret lives at the receiver.\n *\n * Format: broberg-id docs/features/F087.1-identitetsmodel.md §3 (draft, 8/10).\n * iss = the issuer · aud = the receiver's URL · scope = space-separated\n * sub = client_id = the caller · principal_type = human | service | agent\n * org · act {sub} only on delegation (RFC 8693) · exp/iat/jti\n *\n * This is deliberately NOT a method on createSsoClient: a service that only\n * RECEIVES tickets is not a login client — it has no client_id, no redirect,\n * no session. It needs the issuer, its own audience, and the key set.\n *\n * TWO FAILURES, TWO ANSWERS — keep them apart in the caller:\n * JwksUnavailableError → we could not ask BID right now. Answer 503 and let\n * the caller retry; the ticket may be perfectly valid.\n * SsoError → the ticket is not acceptable. Answer 401/403.\n * A known key is served from cache, so a ticket signed with it verifies while\n * BID is briefly unreachable.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport { ALLOWED_ALGS, SsoError } from \"./client.js\";\nimport { createJwksCache, JwksUnavailableError, type JwksCache } from \"./jwks.js\";\n\nexport type PrincipalType = \"human\" | \"service\" | \"agent\";\nconst PRINCIPAL_TYPES: readonly PrincipalType[] = [\"human\", \"service\", \"agent\"];\n\n/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */\nexport interface TicketPrincipal {\n /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */\n principal: string;\n type: PrincipalType;\n /** The client the ticket was issued to (`client_id`, falling back to `sub`). */\n clientId: string;\n /** The organisation, when BID stated one. */\n org: string | null;\n /** On delegation: who the caller is acting for (RFC 8693 `act.sub`). */\n act: { sub: string } | null;\n scopes: string[];\n /** Seconds since the epoch. */\n exp: number;\n jti: string;\n}\n\nexport interface TicketVerifierOptions {\n /** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */\n issuer: string;\n /** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */\n audience: string;\n /** The key set URL. Omitted: read once from the issuer's discovery document. */\n jwksUri?: string;\n /**\n * The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min\n * (service) and 15 min (agent); anything longer was not minted by the policy\n * we were told about, so it is refused rather than trusted for longer.\n */\n maxLifetimeSec?: number;\n /** Seconds of clock skew to allow on exp/iat. Default 30. */\n clockToleranceSec?: number;\n /** Fetch the key set at construction, so the first caller does not wait. */\n warmUp?: boolean;\n fetchImpl?: typeof fetch;\n minRefetchIntervalMs?: number;\n}\n\nexport interface TicketVerifier {\n /** Verify a ticket; with `scope`, also require that scope to be granted. */\n verify(token: string, options?: { scope?: string | string[] }): Promise<TicketPrincipal>;\n}\n\nexport function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier {\n const {\n issuer,\n audience,\n maxLifetimeSec = 15 * 60,\n clockToleranceSec = 30,\n fetchImpl = fetch,\n } = options;\n if (!issuer) throw new SsoError(\"createTicketVerifier needs an issuer\");\n if (!audience) throw new SsoError(\"createTicketVerifier needs this receiver's audience\");\n\n let cachePromise: Promise<JwksCache> | null = null;\n const cacheFor = (jwksUri: string) =>\n createJwksCache({\n jwksUri,\n fetchImpl,\n ...(options.warmUp ? { warmUp: true } : {}),\n ...(options.minRefetchIntervalMs !== undefined ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}),\n });\n\n async function keys(): Promise<JwksCache> {\n cachePromise ??= (async () => {\n if (options.jwksUri) return cacheFor(options.jwksUri);\n const url = `${issuer}/.well-known/openid-configuration`;\n let doc: { issuer?: string; jwks_uri?: string };\n try {\n const res = await fetchImpl(url);\n if (!res.ok) throw new Error(`answered ${res.status}`);\n doc = (await res.json()) as typeof doc;\n } catch (cause) {\n // Reached nothing usable: transient, the same class a key-set outage gets.\n throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n if (doc.issuer !== issuer) {\n throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} — they must match exactly`);\n }\n if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);\n return cacheFor(doc.jwks_uri);\n })().catch((err) => {\n cachePromise = null; // a later call retries instead of caching the failure\n throw err;\n });\n return cachePromise;\n }\n if (options.warmUp) void keys().catch(() => {});\n\n return {\n async verify(token, opts = {}) {\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"ticket is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"ticket has no kid — cannot pick a signing key\");\n if (header.typ === \"logout+jwt\") throw new SsoError(\"a logout token is not a ticket\");\n\n const cache = await keys();\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"EdDSA\"), {\n issuer,\n audience,\n algorithms: ALLOWED_ALGS,\n clockTolerance: clockToleranceSec,\n requiredClaims: [\"exp\", \"iat\", \"jti\", \"sub\"],\n }));\n } catch (e) {\n // A key-set outage must surface as itself (→ 503), never as \"rejected\".\n if (e instanceof JwksUnavailableError) throw e;\n throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // An ID token or a logout token must never pass as a ticket. They carry\n // no principal_type; a logout token carries events, an ID token a nonce.\n if (\"events\" in payload) throw new SsoError(\"a logout token is not a ticket\");\n if (\"nonce\" in payload) throw new SsoError(\"an ID token is not a ticket\");\n\n const type = payload.principal_type;\n if (typeof type !== \"string\" || !PRINCIPAL_TYPES.includes(type as PrincipalType)) {\n throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);\n }\n const lifetime = payload.exp! - payload.iat!;\n if (lifetime > maxLifetimeSec) {\n throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"ticket has no sub\");\n\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : [];\n const required = opts.scope === undefined ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n const missing = required.filter((s) => !scopes.includes(s));\n if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(\", \")}`);\n\n const actRaw = payload.act as { sub?: unknown } | undefined;\n const act = actRaw && typeof actRaw.sub === \"string\" && actRaw.sub ? { sub: actRaw.sub } : null;\n return {\n principal: payload.sub,\n type: type as PrincipalType,\n clientId: typeof payload.client_id === \"string\" && payload.client_id ? payload.client_id : payload.sub,\n org: typeof payload.org === \"string\" && payload.org ? payload.org : null,\n act,\n scopes,\n exp: payload.exp!,\n jti: String(payload.jti),\n };\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/fetch-ticket.ts","../src/ticket.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * F084.154 — fetch a Broberg ID ticket WITHOUT a key (broberg-id-F087.10/.11).\n *\n * The sending half of F084.153: a workload proves who it is with the identity its\n * platform already gives it, and BID exchanges that for a short-lived ticket\n * (RFC 8693 token exchange). No secret is stored anywhere.\n *\n * Fly.io POST unix:/.fly/api /v1/tokens/oidc {aud: issuer}\n * GitHub Actions GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=issuer\n * (needs `permissions: id-token: write` in the workflow)\n * then POST {issuer}/oauth2/token grant_type=…token-exchange\n *\n * Reference implementation: broberg-id/.github/workflows/ticket-probe.yml.\n * The ticket itself is never logged or put in an error message.\n */\nimport { existsSync } from \"node:fs\";\nimport { request } from \"node:http\";\n\nexport const DEFAULT_ISSUER = \"https://id.broberg.ai\";\nconst FLY_SOCKET = \"/.fly/api\";\n\n/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */\nexport class NoWorkloadIdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"NoWorkloadIdentityError\";\n }\n}\n\n/**\n * BID refused the exchange. `code` is BID's own error name (the part of\n * error_description before the colon, e.g. `no_rule_for_audience`), `status` the\n * HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.\n */\nexport class TicketExchangeError extends Error {\n constructor(message: string, readonly code: string, readonly status: number) {\n super(message);\n this.name = \"TicketExchangeError\";\n }\n}\n\n/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */\nexport class TicketUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TicketUnavailableError\";\n }\n}\n\nexport interface TicketClientOptions {\n issuer?: string;\n /** Renew this many seconds before exp. Default 30. */\n renewBeforeSec?: number;\n /** Injectable for tests. */\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */\n flyOidc?: (aud: string) => Promise<string>;\n /** Whether the Fly socket is present. Default: checks /.fly/api. */\n onFly?: () => boolean;\n}\n\nexport interface TicketClient {\n /** A ticket for `audience` (BID's short name, e.g. \"discovery\"), from cache while it is fresh. */\n get(opts: { audience: string; scope?: string }): Promise<string>;\n}\n\nfunction flyOidcOverSocket(aud: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ aud });\n const req = request(\n { socketPath: FLY_SOCKET, path: \"/v1/tokens/oidc\", method: \"POST\", headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) } },\n (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (c) => (data += c));\n res.on(\"end\", () => {\n if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());\n else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));\n });\n },\n );\n req.on(\"error\", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));\n req.end(body);\n });\n}\n\n/** Seconds since the epoch at which a JWT expires, or null if it cannot be read. */\nfunction jwtExp(token: string): number | null {\n try {\n const payload = JSON.parse(Buffer.from(token.split(\".\")[1] ?? \"\", \"base64url\").toString(\"utf8\"));\n return typeof payload.exp === \"number\" ? payload.exp : null;\n } catch {\n return null;\n }\n}\n\nexport function createTicketClient(options: TicketClientOptions = {}): TicketClient {\n const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\\/+$/, \"\");\n const renewBeforeSec = options.renewBeforeSec ?? 30;\n const env = options.env ?? process.env;\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = options.now ?? (() => Date.now());\n const flyOidc = options.flyOidc ?? flyOidcOverSocket;\n const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));\n\n const cache = new Map<string, { token: string; exp: number }>();\n const inFlight = new Map<string, Promise<string>>();\n\n async function platformToken(): Promise<string> {\n if (onFly()) return flyOidc(issuer);\n const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (url && bearer) {\n let res: Response;\n try {\n res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });\n } catch (e) {\n throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);\n const { value } = (await res.json()) as { value?: string };\n if (!value) throw new TicketUnavailableError(\"GitHub OIDC endpoint returned no token\");\n return value;\n }\n throw new NoWorkloadIdentityError(\n \"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job \" +\n \"(ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN — add `permissions: id-token: write`). A Mac session keeps using its session key.\",\n );\n }\n\n async function exchange(audience: string, scope: string | undefined): Promise<{ token: string; exp: number }> {\n const subject = await platformToken();\n const form = new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:token-exchange\",\n subject_token: subject,\n subject_token_type: \"urn:ietf:params:oauth:token-type:jwt\",\n audience,\n });\n if (scope) form.set(\"scope\", scope);\n let res: Response;\n try {\n res = await fetchImpl(`${issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: form,\n });\n } catch (e) {\n throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n let body: { access_token?: string; expires_in?: number; error?: string; error_description?: string } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n /* a non-JSON answer is handled by the status checks below */\n }\n if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);\n if (!res.ok || !body.access_token) {\n const desc = body.error_description ?? \"\";\n const code = desc.includes(\":\") ? desc.slice(0, desc.indexOf(\":\")).trim() : body.error ?? \"exchange_refused\";\n throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);\n }\n const nowSec = Math.floor(now() / 1000);\n const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);\n return { token: body.access_token, exp };\n }\n\n return {\n async get({ audience, scope }) {\n const key = `${audience} ${scope ?? \"\"}`;\n const hit = cache.get(key);\n if (hit && hit.exp - renewBeforeSec > now() / 1000) return hit.token;\n const pending = inFlight.get(key);\n if (pending) return pending;\n const p = exchange(audience, scope)\n .then((t) => {\n cache.set(key, t);\n return t.token;\n })\n .finally(() => inFlight.delete(key));\n inFlight.set(key, p);\n return p;\n },\n };\n}\n\nlet shared: TicketClient | null = null;\n/** A ticket from the process-wide client (one cache per process). */\nexport function fetchTicket(opts: { audience: string; scope?: string }): Promise<string> {\n shared ??= createTicketClient();\n return shared.get(opts);\n}\n","/**\n * F084.153 — verify a Broberg ID ticket LOCALLY (broberg-id-F087.4).\n *\n * A ticket is how one service or agent proves who it is to another without a\n * shared secret: Broberg ID signs a short-lived JWT, the receiver checks it\n * against BID's PUBLIC keys. Nothing secret lives at the receiver.\n *\n * Format: broberg-id docs/features/F087.1-identitetsmodel.md §3 (draft, 8/10).\n * iss = the issuer · aud = the receiver's URL · scope = space-separated\n * sub = client_id = the caller · principal_type = human | service | agent\n * org · act {sub} only on delegation (RFC 8693) · exp/iat/jti\n *\n * This is deliberately NOT a method on createSsoClient: a service that only\n * RECEIVES tickets is not a login client — it has no client_id, no redirect,\n * no session. It needs the issuer, its own audience, and the key set.\n *\n * TWO FAILURES, TWO ANSWERS — keep them apart in the caller:\n * JwksUnavailableError → we could not ask BID right now. Answer 503 and let\n * the caller retry; the ticket may be perfectly valid.\n * SsoError → the ticket is not acceptable. Answer 401/403.\n * A known key is served from cache, so a ticket signed with it verifies while\n * BID is briefly unreachable.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport { ALLOWED_ALGS, SsoError } from \"./client.js\";\nimport { createJwksCache, JwksUnavailableError, type JwksCache } from \"./jwks.js\";\n\nexport type PrincipalType = \"human\" | \"service\" | \"agent\";\nconst PRINCIPAL_TYPES: readonly PrincipalType[] = [\"human\", \"service\", \"agent\"];\n\n/**\n * F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link\n * outward: agent-b acting for agent-a acting for user-1 reads\n * `{ sub: \"agent-a\", act: { sub: \"user-1\" } }`. `act.sub` alone is the old shape.\n */\nexport interface TicketActor {\n sub: string;\n act?: TicketActor;\n}\n\n/** The newest ticket format version this package understands (BID sets `ver`). */\nexport const TICKET_FORMAT_VERSION = 1;\n/** Delegation deeper than this is refused rather than shortened. */\nexport const MAX_ACT_DEPTH = 5;\n\n/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */\nexport interface TicketPrincipal {\n /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */\n principal: string;\n type: PrincipalType;\n /** The client the ticket was issued to (`client_id`, falling back to `sub`). */\n clientId: string;\n /** The organisation, when BID stated one. */\n org: string | null;\n /** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */\n act: TicketActor | null;\n /** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */\n version: number;\n /**\n * F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.\n * NOT checked here: until the receiver verifies a DPoP proof against it\n * (F087.16), a bound ticket is accepted exactly like a bearer ticket.\n */\n cnf: { jkt: string } | null;\n scopes: string[];\n /** Seconds since the epoch. */\n exp: number;\n jti: string;\n}\n\nexport interface TicketVerifierOptions {\n /** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */\n issuer: string;\n /** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */\n audience: string;\n /** The key set URL. Omitted: read once from the issuer's discovery document. */\n jwksUri?: string;\n /**\n * The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min\n * (service) and 15 min (agent); anything longer was not minted by the policy\n * we were told about, so it is refused rather than trusted for longer.\n */\n maxLifetimeSec?: number;\n /** Seconds of clock skew to allow on exp/iat. Default 30. */\n clockToleranceSec?: number;\n /** Fetch the key set at construction, so the first caller does not wait. */\n warmUp?: boolean;\n fetchImpl?: typeof fetch;\n minRefetchIntervalMs?: number;\n}\n\nexport interface TicketVerifier {\n /** Verify a ticket; with `scope`, also require that scope to be granted. */\n verify(token: string, options?: { scope?: string | string[] }): Promise<TicketPrincipal>;\n}\n\nexport function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier {\n const {\n issuer,\n audience,\n maxLifetimeSec = 15 * 60,\n clockToleranceSec = 30,\n fetchImpl = fetch,\n } = options;\n if (!issuer) throw new SsoError(\"createTicketVerifier needs an issuer\");\n if (!audience) throw new SsoError(\"createTicketVerifier needs this receiver's audience\");\n\n let cachePromise: Promise<JwksCache> | null = null;\n const cacheFor = (jwksUri: string) =>\n createJwksCache({\n jwksUri,\n fetchImpl,\n ...(options.warmUp ? { warmUp: true } : {}),\n ...(options.minRefetchIntervalMs !== undefined ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}),\n });\n\n async function keys(): Promise<JwksCache> {\n cachePromise ??= (async () => {\n if (options.jwksUri) return cacheFor(options.jwksUri);\n const url = `${issuer}/.well-known/openid-configuration`;\n let doc: { issuer?: string; jwks_uri?: string };\n try {\n const res = await fetchImpl(url);\n if (!res.ok) throw new Error(`answered ${res.status}`);\n doc = (await res.json()) as typeof doc;\n } catch (cause) {\n // Reached nothing usable: transient, the same class a key-set outage gets.\n throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n if (doc.issuer !== issuer) {\n throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} — they must match exactly`);\n }\n if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);\n return cacheFor(doc.jwks_uri);\n })().catch((err) => {\n cachePromise = null; // a later call retries instead of caching the failure\n throw err;\n });\n return cachePromise;\n }\n if (options.warmUp) void keys().catch(() => {});\n\n return {\n async verify(token, opts = {}) {\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"ticket is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"ticket has no kid — cannot pick a signing key\");\n if (header.typ === \"logout+jwt\") throw new SsoError(\"a logout token is not a ticket\");\n\n const cache = await keys();\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"EdDSA\"), {\n issuer,\n audience,\n algorithms: ALLOWED_ALGS,\n clockTolerance: clockToleranceSec,\n requiredClaims: [\"exp\", \"iat\", \"jti\", \"sub\"],\n }));\n } catch (e) {\n // A key-set outage must surface as itself (→ 503), never as \"rejected\".\n if (e instanceof JwksUnavailableError) throw e;\n throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // An ID token or a logout token must never pass as a ticket. They carry\n // no principal_type; a logout token carries events, an ID token a nonce.\n if (\"events\" in payload) throw new SsoError(\"a logout token is not a ticket\");\n if (\"nonce\" in payload) throw new SsoError(\"an ID token is not a ticket\");\n\n const type = payload.principal_type;\n if (typeof type !== \"string\" || !PRINCIPAL_TYPES.includes(type as PrincipalType)) {\n throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);\n }\n const lifetime = payload.exp! - payload.iat!;\n if (lifetime > maxLifetimeSec) {\n throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"ticket has no sub\");\n\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : [];\n const required = opts.scope === undefined ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n const missing = required.filter((s) => !scopes.includes(s));\n if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(\", \")}`);\n\n // F084.156 — ver: unknown future formats are refused by name, never read\n // with today's rules.\n const ver = payload.ver === undefined ? 0 : payload.ver;\n if (typeof ver !== \"number\" || !Number.isInteger(ver) || ver < 0) {\n throw new SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);\n }\n if (ver > TICKET_FORMAT_VERSION) {\n throw new SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) — upgrade @broberg/sso`);\n }\n // The whole chain, or a refusal. 0.14.2 kept only act.sub, cutting a chain\n // to its first link, and read a malformed act as \"no delegation\" — both in\n // the direction where the caller looks like it acts for fewer people.\n const act = payload.act === undefined ? null : readActor(payload.act, 1);\n const cnfRaw = payload.cnf as { jkt?: unknown } | undefined;\n const cnf = cnfRaw && typeof cnfRaw.jkt === \"string\" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;\n return {\n principal: payload.sub,\n type: type as PrincipalType,\n clientId: typeof payload.client_id === \"string\" && payload.client_id ? payload.client_id : payload.sub,\n org: typeof payload.org === \"string\" && payload.org ? payload.org : null,\n act,\n version: ver,\n cnf,\n scopes,\n exp: payload.exp!,\n jti: String(payload.jti),\n };\n },\n };\n}\n\nfunction readActor(raw: unknown, depth: number): TicketActor {\n if (depth > MAX_ACT_DEPTH) throw new SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);\n const link = raw as { sub?: unknown; act?: unknown } | null;\n if (!link || typeof link !== \"object\" || typeof link.sub !== \"string\" || link.sub === \"\") {\n throw new SsoError(`ticket's act (link ${depth}) has no string sub`);\n }\n return link.act === undefined ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broberg/sso",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "The thin client for Broberg ID — send a user to central login, verify the ID token against JWKS, keep a local session. Public client + PKCE only: no passwords, no passkeys, no client secret can live in an app.",
5
5
  "type": "module",
6
6
  "license": "MIT",