@broberg/sso 0.16.0 → 0.17.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/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkJGIO7XPR_cjs = require('./chunk-JGIO7XPR.cjs');
3
+ var chunkQYMEISTU_cjs = require('./chunk-QYMEISTU.cjs');
4
4
  var fs = require('fs');
5
5
  var http = require('http');
6
6
  var jose = require('jose');
@@ -48,37 +48,37 @@ async function verifyDpopProof(proof, o) {
48
48
  try {
49
49
  header = jose.decodeProtectedHeader(proof);
50
50
  } catch {
51
- throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof is not a JWS");
51
+ throw new chunkQYMEISTU_cjs.SsoError("DPoP proof is not a JWS");
52
52
  }
53
- if (header.typ !== "dpop+jwt") throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof has typ ${JSON.stringify(header.typ)}, not "dpop+jwt"`);
54
- if (!header.alg || !ASYMMETRIC.has(header.alg)) throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof alg ${JSON.stringify(header.alg)} is not an accepted asymmetric algorithm`);
53
+ if (header.typ !== "dpop+jwt") throw new chunkQYMEISTU_cjs.SsoError(`DPoP proof has typ ${JSON.stringify(header.typ)}, not "dpop+jwt"`);
54
+ if (!header.alg || !ASYMMETRIC.has(header.alg)) throw new chunkQYMEISTU_cjs.SsoError(`DPoP proof alg ${JSON.stringify(header.alg)} is not an accepted asymmetric algorithm`);
55
55
  const jwk = header.jwk;
56
- if (!jwk || typeof jwk !== "object") throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof carries no jwk");
57
- if ("d" in jwk) throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof jwk contains a private key");
56
+ if (!jwk || typeof jwk !== "object") throw new chunkQYMEISTU_cjs.SsoError("DPoP proof carries no jwk");
57
+ if ("d" in jwk) throw new chunkQYMEISTU_cjs.SsoError("DPoP proof jwk contains a private key");
58
58
  let payload;
59
59
  try {
60
60
  const key = await jose.importJWK(jwk, header.alg);
61
61
  ({ payload } = await jose.jwtVerify(proof, key, { typ: "dpop+jwt", algorithms: [header.alg] }));
62
62
  } catch (e) {
63
- throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof signature rejected: ${e instanceof Error ? e.message : String(e)}`);
63
+ throw new chunkQYMEISTU_cjs.SsoError(`DPoP proof signature rejected: ${e instanceof Error ? e.message : String(e)}`);
64
64
  }
65
- if (await jose.calculateJwkThumbprint(jwk) !== o.jkt) throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof key does not match the ticket's cnf.jkt");
66
- if (payload.htm !== o.method.toUpperCase()) throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof is for ${JSON.stringify(payload.htm)}, the request is ${o.method.toUpperCase()}`);
65
+ if (await jose.calculateJwkThumbprint(jwk) !== o.jkt) throw new chunkQYMEISTU_cjs.SsoError("DPoP proof key does not match the ticket's cnf.jkt");
66
+ if (payload.htm !== o.method.toUpperCase()) throw new chunkQYMEISTU_cjs.SsoError(`DPoP proof is for ${JSON.stringify(payload.htm)}, the request is ${o.method.toUpperCase()}`);
67
67
  let htu;
68
68
  try {
69
69
  htu = normalizeHtu(String(payload.htu));
70
70
  } catch {
71
- throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof htu is not a URL");
71
+ throw new chunkQYMEISTU_cjs.SsoError("DPoP proof htu is not a URL");
72
72
  }
73
- if (htu !== normalizeHtu(o.url)) throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof is for ${htu}, the request is ${normalizeHtu(o.url)}`);
73
+ if (htu !== normalizeHtu(o.url)) throw new chunkQYMEISTU_cjs.SsoError(`DPoP proof is for ${htu}, the request is ${normalizeHtu(o.url)}`);
74
74
  const nowSec = Math.floor(o.now() / 1e3);
75
75
  if (typeof payload.iat !== "number" || Math.abs(nowSec - payload.iat) > o.iatWindowSec) {
76
- throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof iat is outside \xB1${o.iatWindowSec}s`);
76
+ throw new chunkQYMEISTU_cjs.SsoError(`DPoP proof iat is outside \xB1${o.iatWindowSec}s`);
77
77
  }
78
- if (payload.ath !== await accessTokenHash(o.ticket)) throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof ath does not match this ticket");
79
- if (typeof payload.jti !== "string" || payload.jti === "") throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof has no jti");
78
+ if (payload.ath !== await accessTokenHash(o.ticket)) throw new chunkQYMEISTU_cjs.SsoError("DPoP proof ath does not match this ticket");
79
+ if (typeof payload.jti !== "string" || payload.jti === "") throw new chunkQYMEISTU_cjs.SsoError("DPoP proof has no jti");
80
80
  if (await o.replay.seen(`${o.jkt}:${payload.jti}`, o.iatWindowSec * 2 * 1e3)) {
81
- throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof was already used (jti replay)");
81
+ throw new chunkQYMEISTU_cjs.SsoError("DPoP proof was already used (jti replay)");
82
82
  }
83
83
  }
84
84
 
@@ -241,10 +241,10 @@ function createTicketVerifier(options) {
241
241
  now = () => Date.now()
242
242
  } = options;
243
243
  const replayStore = options.replayStore ?? createMemoryReplayStore(now);
244
- if (!issuer) throw new chunkJGIO7XPR_cjs.SsoError("createTicketVerifier needs an issuer");
245
- if (!audience) throw new chunkJGIO7XPR_cjs.SsoError("createTicketVerifier needs this receiver's audience");
244
+ if (!issuer) throw new chunkQYMEISTU_cjs.SsoError("createTicketVerifier needs an issuer");
245
+ if (!audience) throw new chunkQYMEISTU_cjs.SsoError("createTicketVerifier needs this receiver's audience");
246
246
  let cachePromise = null;
247
- const cacheFor = (jwksUri) => chunkJGIO7XPR_cjs.createJwksCache({
247
+ const cacheFor = (jwksUri) => chunkQYMEISTU_cjs.createJwksCache({
248
248
  jwksUri,
249
249
  fetchImpl,
250
250
  ...options.warmUp ? { warmUp: true } : {},
@@ -260,12 +260,12 @@ function createTicketVerifier(options) {
260
260
  if (!res.ok) throw new Error(`answered ${res.status}`);
261
261
  doc = await res.json();
262
262
  } catch (cause) {
263
- throw new chunkJGIO7XPR_cjs.JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
263
+ throw new chunkQYMEISTU_cjs.JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
264
264
  }
265
265
  if (doc.issuer !== issuer) {
266
- throw new chunkJGIO7XPR_cjs.SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} \u2014 they must match exactly`);
266
+ throw new chunkQYMEISTU_cjs.SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} \u2014 they must match exactly`);
267
267
  }
268
- if (!doc.jwks_uri) throw new chunkJGIO7XPR_cjs.SsoError(`${url} names no jwks_uri`);
268
+ if (!doc.jwks_uri) throw new chunkQYMEISTU_cjs.SsoError(`${url} names no jwks_uri`);
269
269
  return cacheFor(doc.jwks_uri);
270
270
  })().catch((err) => {
271
271
  cachePromise = null;
@@ -281,51 +281,51 @@ function createTicketVerifier(options) {
281
281
  try {
282
282
  header = jose.decodeProtectedHeader(token);
283
283
  } catch {
284
- throw new chunkJGIO7XPR_cjs.SsoError("ticket is not a JWS");
284
+ throw new chunkQYMEISTU_cjs.SsoError("ticket is not a JWS");
285
285
  }
286
- if (!header.kid) throw new chunkJGIO7XPR_cjs.SsoError("ticket has no kid \u2014 cannot pick a signing key");
287
- if (header.typ === "logout+jwt") throw new chunkJGIO7XPR_cjs.SsoError("a logout token is not a ticket");
286
+ if (!header.kid) throw new chunkQYMEISTU_cjs.SsoError("ticket has no kid \u2014 cannot pick a signing key");
287
+ if (header.typ === "logout+jwt") throw new chunkQYMEISTU_cjs.SsoError("a logout token is not a ticket");
288
288
  const cache = await keys();
289
289
  let payload;
290
290
  try {
291
291
  ({ payload } = await jose.jwtVerify(token, async () => cache.getKey(header.kid, header.alg ?? "EdDSA"), {
292
292
  issuer,
293
293
  audience,
294
- algorithms: chunkJGIO7XPR_cjs.ALLOWED_ALGS,
294
+ algorithms: chunkQYMEISTU_cjs.ALLOWED_ALGS,
295
295
  clockTolerance: clockToleranceSec,
296
296
  requiredClaims: ["exp", "iat", "jti", "sub"]
297
297
  }));
298
298
  } catch (e) {
299
- if (e instanceof chunkJGIO7XPR_cjs.JwksUnavailableError) throw e;
300
- throw new chunkJGIO7XPR_cjs.SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);
299
+ if (e instanceof chunkQYMEISTU_cjs.JwksUnavailableError) throw e;
300
+ throw new chunkQYMEISTU_cjs.SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);
301
301
  }
302
- if ("events" in payload) throw new chunkJGIO7XPR_cjs.SsoError("a logout token is not a ticket");
303
- if ("nonce" in payload) throw new chunkJGIO7XPR_cjs.SsoError("an ID token is not a ticket");
302
+ if ("events" in payload) throw new chunkQYMEISTU_cjs.SsoError("a logout token is not a ticket");
303
+ if ("nonce" in payload) throw new chunkQYMEISTU_cjs.SsoError("an ID token is not a ticket");
304
304
  const type = payload.principal_type;
305
305
  if (typeof type !== "string" || !PRINCIPAL_TYPES.includes(type)) {
306
- throw new chunkJGIO7XPR_cjs.SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);
306
+ throw new chunkQYMEISTU_cjs.SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);
307
307
  }
308
308
  const lifetime = payload.exp - payload.iat;
309
309
  if (lifetime > maxLifetimeSec) {
310
- throw new chunkJGIO7XPR_cjs.SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);
310
+ throw new chunkQYMEISTU_cjs.SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);
311
311
  }
312
- if (typeof payload.sub !== "string" || payload.sub === "") throw new chunkJGIO7XPR_cjs.SsoError("ticket has no sub");
312
+ if (typeof payload.sub !== "string" || payload.sub === "") throw new chunkQYMEISTU_cjs.SsoError("ticket has no sub");
313
313
  const scopes = typeof payload.scope === "string" ? payload.scope.split(" ").filter(Boolean) : [];
314
314
  const required = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
315
315
  const missing = required.filter((s) => !scopes.includes(s));
316
- if (missing.length) throw new chunkJGIO7XPR_cjs.SsoError(`ticket lacks scope ${missing.join(", ")}`);
316
+ if (missing.length) throw new chunkQYMEISTU_cjs.SsoError(`ticket lacks scope ${missing.join(", ")}`);
317
317
  const ver = payload.ver === void 0 ? 0 : payload.ver;
318
318
  if (typeof ver !== "number" || !Number.isInteger(ver) || ver < 0) {
319
- throw new chunkJGIO7XPR_cjs.SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);
319
+ throw new chunkQYMEISTU_cjs.SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);
320
320
  }
321
321
  if (ver > TICKET_FORMAT_VERSION) {
322
- throw new chunkJGIO7XPR_cjs.SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) \u2014 upgrade @broberg/sso`);
322
+ throw new chunkQYMEISTU_cjs.SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) \u2014 upgrade @broberg/sso`);
323
323
  }
324
324
  const act = payload.act === void 0 ? null : readActor(payload.act, 1);
325
325
  const cnfRaw = payload.cnf;
326
326
  const cnf = cnfRaw && typeof cnfRaw.jkt === "string" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;
327
327
  if (cnf) {
328
- if (!opts.dpop?.proof) throw new chunkJGIO7XPR_cjs.SsoError("ticket is DPoP-bound (cnf.jkt) but no DPoP proof was presented");
328
+ if (!opts.dpop?.proof) throw new chunkQYMEISTU_cjs.SsoError("ticket is DPoP-bound (cnf.jkt) but no DPoP proof was presented");
329
329
  await verifyDpopProof(opts.dpop.proof, {
330
330
  method: opts.dpop.method,
331
331
  url: opts.dpop.url,
@@ -336,7 +336,7 @@ function createTicketVerifier(options) {
336
336
  now
337
337
  });
338
338
  } else if (requireDpop) {
339
- throw new chunkJGIO7XPR_cjs.SsoError(cnfRaw !== void 0 ? "ticket has a cnf without a string jkt \u2014 DPoP is required here" : "this receiver requires DPoP; the ticket is not bound (no cnf.jkt)");
339
+ throw new chunkQYMEISTU_cjs.SsoError(cnfRaw !== void 0 ? "ticket has a cnf without a string jkt \u2014 DPoP is required here" : "this receiver requires DPoP; the ticket is not bound (no cnf.jkt)");
340
340
  }
341
341
  return {
342
342
  principal: payload.sub,
@@ -354,105 +354,113 @@ function createTicketVerifier(options) {
354
354
  };
355
355
  }
356
356
  function readActor(raw, depth) {
357
- if (depth > MAX_ACT_DEPTH) throw new chunkJGIO7XPR_cjs.SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);
357
+ if (depth > MAX_ACT_DEPTH) throw new chunkQYMEISTU_cjs.SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);
358
358
  const link = raw;
359
359
  if (!link || typeof link !== "object" || typeof link.sub !== "string" || link.sub === "") {
360
- throw new chunkJGIO7XPR_cjs.SsoError(`ticket's act (link ${depth}) has no string sub`);
360
+ throw new chunkQYMEISTU_cjs.SsoError(`ticket's act (link ${depth}) has no string sub`);
361
361
  }
362
362
  return link.act === void 0 ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };
363
363
  }
364
364
 
365
365
  Object.defineProperty(exports, "AVATAR_TYPES", {
366
366
  enumerable: true,
367
- get: function () { return chunkJGIO7XPR_cjs.AVATAR_TYPES; }
367
+ get: function () { return chunkQYMEISTU_cjs.AVATAR_TYPES; }
368
368
  });
369
369
  Object.defineProperty(exports, "DEFAULT_SESSION_MAX_AGE", {
370
370
  enumerable: true,
371
- get: function () { return chunkJGIO7XPR_cjs.DEFAULT_SESSION_MAX_AGE; }
371
+ get: function () { return chunkQYMEISTU_cjs.DEFAULT_SESSION_MAX_AGE; }
372
372
  });
373
373
  Object.defineProperty(exports, "JwksError", {
374
374
  enumerable: true,
375
- get: function () { return chunkJGIO7XPR_cjs.JwksError; }
375
+ get: function () { return chunkQYMEISTU_cjs.JwksError; }
376
376
  });
377
377
  Object.defineProperty(exports, "JwksUnavailableError", {
378
378
  enumerable: true,
379
- get: function () { return chunkJGIO7XPR_cjs.JwksUnavailableError; }
379
+ get: function () { return chunkQYMEISTU_cjs.JwksUnavailableError; }
380
380
  });
381
381
  Object.defineProperty(exports, "JwksUnknownKeyError", {
382
382
  enumerable: true,
383
- get: function () { return chunkJGIO7XPR_cjs.JwksUnknownKeyError; }
383
+ get: function () { return chunkQYMEISTU_cjs.JwksUnknownKeyError; }
384
384
  });
385
385
  Object.defineProperty(exports, "MAX_AVATAR_BYTES", {
386
386
  enumerable: true,
387
- get: function () { return chunkJGIO7XPR_cjs.MAX_AVATAR_BYTES; }
387
+ get: function () { return chunkQYMEISTU_cjs.MAX_AVATAR_BYTES; }
388
388
  });
389
389
  Object.defineProperty(exports, "MAX_INVITATIONS", {
390
390
  enumerable: true,
391
- get: function () { return chunkJGIO7XPR_cjs.MAX_INVITATIONS; }
391
+ get: function () { return chunkQYMEISTU_cjs.MAX_INVITATIONS; }
392
392
  });
393
393
  Object.defineProperty(exports, "SsoAppApiError", {
394
394
  enumerable: true,
395
- get: function () { return chunkJGIO7XPR_cjs.SsoAppApiError; }
395
+ get: function () { return chunkQYMEISTU_cjs.SsoAppApiError; }
396
396
  });
397
397
  Object.defineProperty(exports, "SsoAvatarRejectedError", {
398
398
  enumerable: true,
399
- get: function () { return chunkJGIO7XPR_cjs.SsoAvatarRejectedError; }
399
+ get: function () { return chunkQYMEISTU_cjs.SsoAvatarRejectedError; }
400
400
  });
401
401
  Object.defineProperty(exports, "SsoConfigError", {
402
402
  enumerable: true,
403
- get: function () { return chunkJGIO7XPR_cjs.SsoConfigError; }
403
+ get: function () { return chunkQYMEISTU_cjs.SsoConfigError; }
404
404
  });
405
405
  Object.defineProperty(exports, "SsoError", {
406
406
  enumerable: true,
407
- get: function () { return chunkJGIO7XPR_cjs.SsoError; }
407
+ get: function () { return chunkQYMEISTU_cjs.SsoError; }
408
408
  });
409
409
  Object.defineProperty(exports, "SsoInsufficientScopeError", {
410
410
  enumerable: true,
411
- get: function () { return chunkJGIO7XPR_cjs.SsoInsufficientScopeError; }
411
+ get: function () { return chunkQYMEISTU_cjs.SsoInsufficientScopeError; }
412
+ });
413
+ Object.defineProperty(exports, "SsoIntrospectionUnavailableError", {
414
+ enumerable: true,
415
+ get: function () { return chunkQYMEISTU_cjs.SsoIntrospectionUnavailableError; }
412
416
  });
413
417
  Object.defineProperty(exports, "SsoReauthError", {
414
418
  enumerable: true,
415
- get: function () { return chunkJGIO7XPR_cjs.SsoReauthError; }
419
+ get: function () { return chunkQYMEISTU_cjs.SsoReauthError; }
420
+ });
421
+ Object.defineProperty(exports, "SsoTokenInactiveError", {
422
+ enumerable: true,
423
+ get: function () { return chunkQYMEISTU_cjs.SsoTokenInactiveError; }
416
424
  });
417
425
  Object.defineProperty(exports, "cookieHeader", {
418
426
  enumerable: true,
419
- get: function () { return chunkJGIO7XPR_cjs.cookieHeader; }
427
+ get: function () { return chunkQYMEISTU_cjs.cookieHeader; }
420
428
  });
421
429
  Object.defineProperty(exports, "createJwksCache", {
422
430
  enumerable: true,
423
- get: function () { return chunkJGIO7XPR_cjs.createJwksCache; }
431
+ get: function () { return chunkQYMEISTU_cjs.createJwksCache; }
424
432
  });
425
433
  Object.defineProperty(exports, "createSsoClient", {
426
434
  enumerable: true,
427
- get: function () { return chunkJGIO7XPR_cjs.createSsoClient; }
435
+ get: function () { return chunkQYMEISTU_cjs.createSsoClient; }
428
436
  });
429
437
  Object.defineProperty(exports, "loadSsoConfig", {
430
438
  enumerable: true,
431
- get: function () { return chunkJGIO7XPR_cjs.loadSsoConfig; }
439
+ get: function () { return chunkQYMEISTU_cjs.loadSsoConfig; }
432
440
  });
433
441
  Object.defineProperty(exports, "memoryTokenStore", {
434
442
  enumerable: true,
435
- get: function () { return chunkJGIO7XPR_cjs.memoryTokenStore; }
443
+ get: function () { return chunkQYMEISTU_cjs.memoryTokenStore; }
436
444
  });
437
445
  Object.defineProperty(exports, "readCookie", {
438
446
  enumerable: true,
439
- get: function () { return chunkJGIO7XPR_cjs.readCookie; }
447
+ get: function () { return chunkQYMEISTU_cjs.readCookie; }
440
448
  });
441
449
  Object.defineProperty(exports, "signSession", {
442
450
  enumerable: true,
443
- get: function () { return chunkJGIO7XPR_cjs.signSession; }
451
+ get: function () { return chunkQYMEISTU_cjs.signSession; }
444
452
  });
445
453
  Object.defineProperty(exports, "signValue", {
446
454
  enumerable: true,
447
- get: function () { return chunkJGIO7XPR_cjs.signValue; }
455
+ get: function () { return chunkQYMEISTU_cjs.signValue; }
448
456
  });
449
457
  Object.defineProperty(exports, "verifySession", {
450
458
  enumerable: true,
451
- get: function () { return chunkJGIO7XPR_cjs.verifySession; }
459
+ get: function () { return chunkQYMEISTU_cjs.verifySession; }
452
460
  });
453
461
  Object.defineProperty(exports, "verifyValue", {
454
462
  enumerable: true,
455
- get: function () { return chunkJGIO7XPR_cjs.verifyValue; }
463
+ get: function () { return chunkQYMEISTU_cjs.verifyValue; }
456
464
  });
457
465
  exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
458
466
  exports.MAX_ACT_DEPTH = MAX_ACT_DEPTH;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as InvitationOutcome, f as InvitationResult, g as InviteUsersInput, J as JwksCache, h as JwksCacheOptions, i as JwksError, j as JwksUnavailableError, k as JwksUnknownKeyError, L as LoginResult, l as LoginStart, m as LogoutToken, M as MAX_AVATAR_BYTES, n as MAX_INVITATIONS, o as MigrationState, p as MigrationStatus, b as SessionPayload, q as SsoAppApiError, r as SsoAvatarRejectedError, s as SsoClaims, S as SsoClient, a as SsoConfig, t as SsoConfigError, u as SsoError, v as SsoInsufficientScopeError, w as SsoReauthError, x as TokenSet, T as TokenStore, y as cookieHeader, z as createJwksCache, E as createSsoClient, F as loadSsoConfig, G as memoryTokenStore, H as readCookie, K as signSession, N as signValue, O as verifySession, P as verifyValue } from './session-DsjOyCJb.cjs';
1
+ export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as IntrospectOptions, f as IntrospectedToken, g as InvitationOutcome, h as InvitationResult, i as InviteUsersInput, J as JwksCache, j as JwksCacheOptions, k as JwksError, l as JwksUnavailableError, m as JwksUnknownKeyError, L as LoginResult, n as LoginStart, o as LogoutToken, M as MAX_AVATAR_BYTES, p as MAX_INVITATIONS, q as MigrationState, r as MigrationStatus, b as SessionPayload, s as SsoAppApiError, t as SsoAvatarRejectedError, u as SsoClaims, S as SsoClient, a as SsoConfig, v as SsoConfigError, w as SsoError, x as SsoInsufficientScopeError, y as SsoIntrospectionUnavailableError, z as SsoReauthError, E as SsoTokenInactiveError, F as TokenSet, T as TokenStore, G as cookieHeader, H as createJwksCache, K as createSsoClient, N as loadSsoConfig, O as memoryTokenStore, P as readCookie, Q as signSession, R as signValue, U as verifySession, V as verifyValue } from './session-CIm4_raQ.cjs';
2
2
  import { JWK } from 'jose';
3
3
 
4
4
  declare const DEFAULT_ISSUER = "https://id.broberg.ai";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as InvitationOutcome, f as InvitationResult, g as InviteUsersInput, J as JwksCache, h as JwksCacheOptions, i as JwksError, j as JwksUnavailableError, k as JwksUnknownKeyError, L as LoginResult, l as LoginStart, m as LogoutToken, M as MAX_AVATAR_BYTES, n as MAX_INVITATIONS, o as MigrationState, p as MigrationStatus, b as SessionPayload, q as SsoAppApiError, r as SsoAvatarRejectedError, s as SsoClaims, S as SsoClient, a as SsoConfig, t as SsoConfigError, u as SsoError, v as SsoInsufficientScopeError, w as SsoReauthError, x as TokenSet, T as TokenStore, y as cookieHeader, z as createJwksCache, E as createSsoClient, F as loadSsoConfig, G as memoryTokenStore, H as readCookie, K as signSession, N as signValue, O as verifySession, P as verifyValue } from './session-DsjOyCJb.js';
1
+ export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as IntrospectOptions, f as IntrospectedToken, g as InvitationOutcome, h as InvitationResult, i as InviteUsersInput, J as JwksCache, j as JwksCacheOptions, k as JwksError, l as JwksUnavailableError, m as JwksUnknownKeyError, L as LoginResult, n as LoginStart, o as LogoutToken, M as MAX_AVATAR_BYTES, p as MAX_INVITATIONS, q as MigrationState, r as MigrationStatus, b as SessionPayload, s as SsoAppApiError, t as SsoAvatarRejectedError, u as SsoClaims, S as SsoClient, a as SsoConfig, v as SsoConfigError, w as SsoError, x as SsoInsufficientScopeError, y as SsoIntrospectionUnavailableError, z as SsoReauthError, E as SsoTokenInactiveError, F as TokenSet, T as TokenStore, G as cookieHeader, H as createJwksCache, K as createSsoClient, N as loadSsoConfig, O as memoryTokenStore, P as readCookie, Q as signSession, R as signValue, U as verifySession, V as verifyValue } from './session-CIm4_raQ.js';
2
2
  import { JWK } from 'jose';
3
3
 
4
4
  declare const DEFAULT_ISSUER = "https://id.broberg.ai";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { SsoError, ALLOWED_ALGS, JwksUnavailableError, createJwksCache } from './chunk-ZPRWYUFG.js';
2
- export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue } from './chunk-ZPRWYUFG.js';
1
+ import { SsoError, ALLOWED_ALGS, JwksUnavailableError, createJwksCache } from './chunk-AVXQLEYK.js';
2
+ export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoIntrospectionUnavailableError, SsoReauthError, SsoTokenInactiveError, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue } from './chunk-AVXQLEYK.js';
3
3
  import { existsSync } from 'fs';
4
4
  import { request } from 'http';
5
5
  import { generateKeyPair, exportJWK, calculateJwkThumbprint, SignJWT, decodeProtectedHeader, importJWK, jwtVerify } from 'jose';
@@ -197,6 +197,8 @@ interface Discovery {
197
197
  jwks_uri: string;
198
198
  userinfo_endpoint?: string;
199
199
  end_session_endpoint?: string;
200
+ /** RFC 7662 — where a resource server checks an access token (F084.158). */
201
+ introspection_endpoint?: string;
200
202
  }
201
203
  declare class SsoError extends Error {
202
204
  constructor(message: string);
@@ -233,6 +235,43 @@ declare class SsoInsufficientScopeError extends SsoAppApiError {
233
235
  constructor(message: string, scope: string);
234
236
  }
235
237
  /** F095.2 — an avatar refused BEFORE it was sent: too big, or not PNG/JPEG/WebP. */
238
+ /**
239
+ * F084.158 — the token is not usable HERE: inactive, expired, or issued to a
240
+ * different client than the one required. Answer 401 — the user has to log in
241
+ * again. Never thrown because BID could not be asked.
242
+ */
243
+ declare class SsoTokenInactiveError extends SsoError {
244
+ /** "inactive" | "wrong_client" | "expired" */
245
+ readonly reason: "inactive" | "wrong_client" | "expired";
246
+ constructor(message: string,
247
+ /** "inactive" | "wrong_client" | "expired" */
248
+ reason: "inactive" | "wrong_client" | "expired");
249
+ }
250
+ /**
251
+ * F084.158 — BID could not answer whether the token is active (network, timeout,
252
+ * 5xx, an unreadable body, or our own client credentials refused). Answer 503 —
253
+ * a BID hiccup must never look like an expired login.
254
+ */
255
+ declare class SsoIntrospectionUnavailableError extends SsoError {
256
+ readonly status: number | null;
257
+ constructor(message: string, status: number | null);
258
+ }
259
+ interface IntrospectOptions {
260
+ /** The client the token must have been issued to (e.g. "trail-clipper"). */
261
+ requireClientId: string;
262
+ /** Reuse an ACTIVE answer for up to this long, never past the token's exp. Off by default. */
263
+ cacheMs?: number;
264
+ /** For tests. */
265
+ now?: () => number;
266
+ }
267
+ interface IntrospectedToken {
268
+ active: true;
269
+ sub: string;
270
+ clientId: string;
271
+ scope: string;
272
+ /** Seconds since epoch. */
273
+ exp: number;
274
+ }
236
275
  declare class SsoAvatarRejectedError extends SsoError {
237
276
  readonly reason: "too_large" | "unsupported_type";
238
277
  constructor(message: string, reason: "too_large" | "unsupported_type");
@@ -370,6 +409,13 @@ interface SsoClient {
370
409
  uploadAvatar(accessToken: string, bytes: Uint8Array | ArrayBuffer, contentType: string): Promise<BidProfile>;
371
410
  /** F095.2 — remove the user's picture (scope `profile:write`). */
372
411
  removeAvatar(accessToken: string): Promise<BidProfile>;
412
+ /**
413
+ * F084.158 — ask BID (RFC 7662) whether an access token is active, with THIS
414
+ * app's client credentials, and require it was issued to `requireClientId`.
415
+ * Throws SsoTokenInactiveError (→ 401) or SsoIntrospectionUnavailableError
416
+ * (→ 503). The token never appears in an error message.
417
+ */
418
+ introspectToken(token: string, options: IntrospectOptions): Promise<IntrospectedToken>;
373
419
  /** Exposed for tests and for a health check; not needed in normal use. */
374
420
  readonly jwks: JwksCache;
375
421
  }
@@ -577,4 +623,4 @@ declare function cookieHeader(name: string, value: string, opts: {
577
623
  }): string;
578
624
  declare function readCookie(header: string | null | undefined, name: string): string | undefined;
579
625
 
580
- export { AVATAR_TYPES as A, type BeginLoginOptions as B, type CreateSsoClientOptions as C, DEFAULT_SESSION_MAX_AGE as D, createSsoClient as E, loadSsoConfig as F, memoryTokenStore as G, readCookie as H, type InvitationOutcome as I, type JwksCache as J, signSession as K, type LoginResult as L, MAX_AVATAR_BYTES as M, signValue as N, verifySession as O, verifyValue as P, type SsoClient as S, type TokenStore as T, type SsoConfig as a, type SessionPayload as b, type AddressOwnership as c, type BidProfile as d, type Discovery as e, type InvitationResult as f, type InviteUsersInput as g, type JwksCacheOptions as h, JwksError as i, JwksUnavailableError as j, JwksUnknownKeyError as k, type LoginStart as l, type LogoutToken as m, MAX_INVITATIONS as n, type MigrationState as o, type MigrationStatus as p, SsoAppApiError as q, SsoAvatarRejectedError as r, type SsoClaims as s, SsoConfigError as t, SsoError as u, SsoInsufficientScopeError as v, SsoReauthError as w, type TokenSet as x, cookieHeader as y, createJwksCache as z };
626
+ export { AVATAR_TYPES as A, type BeginLoginOptions as B, type CreateSsoClientOptions as C, DEFAULT_SESSION_MAX_AGE as D, SsoTokenInactiveError as E, type TokenSet as F, cookieHeader as G, createJwksCache as H, type IntrospectOptions as I, type JwksCache as J, createSsoClient as K, type LoginResult as L, MAX_AVATAR_BYTES as M, loadSsoConfig as N, memoryTokenStore as O, readCookie as P, signSession as Q, signValue as R, type SsoClient as S, type TokenStore as T, verifySession as U, verifyValue as V, type SsoConfig as a, type SessionPayload as b, type AddressOwnership as c, type BidProfile as d, type Discovery as e, type IntrospectedToken as f, type InvitationOutcome as g, type InvitationResult as h, type InviteUsersInput as i, type JwksCacheOptions as j, JwksError as k, JwksUnavailableError as l, JwksUnknownKeyError as m, type LoginStart as n, type LogoutToken as o, MAX_INVITATIONS as p, type MigrationState as q, type MigrationStatus as r, SsoAppApiError as s, SsoAvatarRejectedError as t, type SsoClaims as u, SsoConfigError as v, SsoError as w, SsoInsufficientScopeError as x, SsoIntrospectionUnavailableError as y, SsoReauthError as z };
@@ -197,6 +197,8 @@ interface Discovery {
197
197
  jwks_uri: string;
198
198
  userinfo_endpoint?: string;
199
199
  end_session_endpoint?: string;
200
+ /** RFC 7662 — where a resource server checks an access token (F084.158). */
201
+ introspection_endpoint?: string;
200
202
  }
201
203
  declare class SsoError extends Error {
202
204
  constructor(message: string);
@@ -233,6 +235,43 @@ declare class SsoInsufficientScopeError extends SsoAppApiError {
233
235
  constructor(message: string, scope: string);
234
236
  }
235
237
  /** F095.2 — an avatar refused BEFORE it was sent: too big, or not PNG/JPEG/WebP. */
238
+ /**
239
+ * F084.158 — the token is not usable HERE: inactive, expired, or issued to a
240
+ * different client than the one required. Answer 401 — the user has to log in
241
+ * again. Never thrown because BID could not be asked.
242
+ */
243
+ declare class SsoTokenInactiveError extends SsoError {
244
+ /** "inactive" | "wrong_client" | "expired" */
245
+ readonly reason: "inactive" | "wrong_client" | "expired";
246
+ constructor(message: string,
247
+ /** "inactive" | "wrong_client" | "expired" */
248
+ reason: "inactive" | "wrong_client" | "expired");
249
+ }
250
+ /**
251
+ * F084.158 — BID could not answer whether the token is active (network, timeout,
252
+ * 5xx, an unreadable body, or our own client credentials refused). Answer 503 —
253
+ * a BID hiccup must never look like an expired login.
254
+ */
255
+ declare class SsoIntrospectionUnavailableError extends SsoError {
256
+ readonly status: number | null;
257
+ constructor(message: string, status: number | null);
258
+ }
259
+ interface IntrospectOptions {
260
+ /** The client the token must have been issued to (e.g. "trail-clipper"). */
261
+ requireClientId: string;
262
+ /** Reuse an ACTIVE answer for up to this long, never past the token's exp. Off by default. */
263
+ cacheMs?: number;
264
+ /** For tests. */
265
+ now?: () => number;
266
+ }
267
+ interface IntrospectedToken {
268
+ active: true;
269
+ sub: string;
270
+ clientId: string;
271
+ scope: string;
272
+ /** Seconds since epoch. */
273
+ exp: number;
274
+ }
236
275
  declare class SsoAvatarRejectedError extends SsoError {
237
276
  readonly reason: "too_large" | "unsupported_type";
238
277
  constructor(message: string, reason: "too_large" | "unsupported_type");
@@ -370,6 +409,13 @@ interface SsoClient {
370
409
  uploadAvatar(accessToken: string, bytes: Uint8Array | ArrayBuffer, contentType: string): Promise<BidProfile>;
371
410
  /** F095.2 — remove the user's picture (scope `profile:write`). */
372
411
  removeAvatar(accessToken: string): Promise<BidProfile>;
412
+ /**
413
+ * F084.158 — ask BID (RFC 7662) whether an access token is active, with THIS
414
+ * app's client credentials, and require it was issued to `requireClientId`.
415
+ * Throws SsoTokenInactiveError (→ 401) or SsoIntrospectionUnavailableError
416
+ * (→ 503). The token never appears in an error message.
417
+ */
418
+ introspectToken(token: string, options: IntrospectOptions): Promise<IntrospectedToken>;
373
419
  /** Exposed for tests and for a health check; not needed in normal use. */
374
420
  readonly jwks: JwksCache;
375
421
  }
@@ -577,4 +623,4 @@ declare function cookieHeader(name: string, value: string, opts: {
577
623
  }): string;
578
624
  declare function readCookie(header: string | null | undefined, name: string): string | undefined;
579
625
 
580
- export { AVATAR_TYPES as A, type BeginLoginOptions as B, type CreateSsoClientOptions as C, DEFAULT_SESSION_MAX_AGE as D, createSsoClient as E, loadSsoConfig as F, memoryTokenStore as G, readCookie as H, type InvitationOutcome as I, type JwksCache as J, signSession as K, type LoginResult as L, MAX_AVATAR_BYTES as M, signValue as N, verifySession as O, verifyValue as P, type SsoClient as S, type TokenStore as T, type SsoConfig as a, type SessionPayload as b, type AddressOwnership as c, type BidProfile as d, type Discovery as e, type InvitationResult as f, type InviteUsersInput as g, type JwksCacheOptions as h, JwksError as i, JwksUnavailableError as j, JwksUnknownKeyError as k, type LoginStart as l, type LogoutToken as m, MAX_INVITATIONS as n, type MigrationState as o, type MigrationStatus as p, SsoAppApiError as q, SsoAvatarRejectedError as r, type SsoClaims as s, SsoConfigError as t, SsoError as u, SsoInsufficientScopeError as v, SsoReauthError as w, type TokenSet as x, cookieHeader as y, createJwksCache as z };
626
+ export { AVATAR_TYPES as A, type BeginLoginOptions as B, type CreateSsoClientOptions as C, DEFAULT_SESSION_MAX_AGE as D, SsoTokenInactiveError as E, type TokenSet as F, cookieHeader as G, createJwksCache as H, type IntrospectOptions as I, type JwksCache as J, createSsoClient as K, type LoginResult as L, MAX_AVATAR_BYTES as M, loadSsoConfig as N, memoryTokenStore as O, readCookie as P, signSession as Q, signValue as R, type SsoClient as S, type TokenStore as T, verifySession as U, verifyValue as V, type SsoConfig as a, type SessionPayload as b, type AddressOwnership as c, type BidProfile as d, type Discovery as e, type IntrospectedToken as f, type InvitationOutcome as g, type InvitationResult as h, type InviteUsersInput as i, type JwksCacheOptions as j, JwksError as k, JwksUnavailableError as l, JwksUnknownKeyError as m, type LoginStart as n, type LogoutToken as o, MAX_INVITATIONS as p, type MigrationState as q, type MigrationStatus as r, SsoAppApiError as s, SsoAvatarRejectedError as t, type SsoClaims as u, SsoConfigError as v, SsoError as w, SsoInsufficientScopeError as x, SsoIntrospectionUnavailableError as y, SsoReauthError as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broberg/sso",
3
- "version": "0.16.0",
3
+ "version": "0.17.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",