@cauthyhq/node 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -68,6 +68,14 @@ var CauthyConfigError = class extends Error {
68
68
  this.name = "CauthyConfigError";
69
69
  }
70
70
  };
71
+ var CauthyKeyError = class extends CauthyConfigError {
72
+ constructor(reason, message) {
73
+ super(`CAUTHY_KEY: ${message} (${reason})`);
74
+ this.reason = reason;
75
+ this.name = "CauthyKeyError";
76
+ }
77
+ reason;
78
+ };
71
79
 
72
80
  // src/url.ts
73
81
  function withoutTrailingSlashes(s) {
@@ -978,6 +986,7 @@ function createSystem(given = {}) {
978
986
  };
979
987
  const tokenEndpoint = `${issuer}/v1/oauth/token`;
980
988
  function configured() {
989
+ if (given.signer) return;
981
990
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
982
991
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
983
992
  if (given.keys) {
@@ -990,7 +999,7 @@ function createSystem(given = {}) {
990
999
  }
991
1000
  let imported;
992
1001
  const signingKey = () => imported ??= loadKey(key);
993
- async function fail(res, what) {
1002
+ async function fail2(res, what) {
994
1003
  const body = await res.json().catch(() => null);
995
1004
  const first = body?.errors?.[0];
996
1005
  const code = typeof body?.error === "string" ? body.error : first?.code;
@@ -1001,9 +1010,13 @@ function createSystem(given = {}) {
1001
1010
  let asking;
1002
1011
  async function ask() {
1003
1012
  configured();
1004
- const self = given.keys ? given.keysClientId : key?.clientId || clientId;
1013
+ const self = given.signer ? given.signer.clientId : given.keys ? given.keysClientId : key?.clientId || clientId;
1005
1014
  const form = new URLSearchParams({ grant_type: "client_credentials", client_id: self });
1006
- if (given.keys || key) {
1015
+ if (given.signer) {
1016
+ const s = await given.signer.signer;
1017
+ form.set("client_assertion_type", ASSERTION_TYPE);
1018
+ form.set("client_assertion", await s.assertion(self, tokenEndpoint));
1019
+ } else if (given.keys || key) {
1007
1020
  const signer = given.keys ? await given.keys.current() : { kid: key.keyId, privateKey: await signingKey() };
1008
1021
  const assertion = await new SignJWT({}).setProtectedHeader({ alg: "RS256", kid: signer.kid }).setIssuer(self).setSubject(self).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(signer.privateKey);
1009
1022
  form.set("client_assertion_type", ASSERTION_TYPE);
@@ -1012,7 +1025,7 @@ function createSystem(given = {}) {
1012
1025
  form.set("client_secret", clientSecret);
1013
1026
  }
1014
1027
  const res = await doFetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
1015
- if (!res.ok) await fail(res, "the application's own token");
1028
+ if (!res.ok) await fail2(res, "the application's own token");
1016
1029
  const t = await res.json();
1017
1030
  held = { value: t.access_token, until: Date.now() + t.expires_in * 1e3 - SKEW_MS };
1018
1031
  return t.access_token;
@@ -1034,7 +1047,7 @@ function createSystem(given = {}) {
1034
1047
  held = void 0;
1035
1048
  res = await send();
1036
1049
  }
1037
- if (!res.ok) await fail(res, `${method} ${path}`);
1050
+ if (!res.ok) await fail2(res, `${method} ${path}`);
1038
1051
  return res.status === 204 ? void 0 : res.json();
1039
1052
  }
1040
1053
  const data = async (method, path, body, extra) => (await call(method, path, body, extra))?.data;
@@ -1108,7 +1121,7 @@ function createSystem(given = {}) {
1108
1121
  }
1109
1122
  },
1110
1123
  async check(q) {
1111
- const system = q.system ?? ownSystem;
1124
+ const system = q.system ?? (ownSystem || given.currentSystem?.() || "");
1112
1125
  if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
1113
1126
  return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
1114
1127
  },
@@ -1278,6 +1291,219 @@ function firstProblem(...checks) {
1278
1291
 
1279
1292
  // src/webhook.ts
1280
1293
  import { createRemoteJWKSet, jwtVerify } from "jose";
1294
+
1295
+ // src/sha256.ts
1296
+ var K = new Uint32Array([
1297
+ 1116352408,
1298
+ 1899447441,
1299
+ 3049323471,
1300
+ 3921009573,
1301
+ 961987163,
1302
+ 1508970993,
1303
+ 2453635748,
1304
+ 2870763221,
1305
+ 3624381080,
1306
+ 310598401,
1307
+ 607225278,
1308
+ 1426881987,
1309
+ 1925078388,
1310
+ 2162078206,
1311
+ 2614888103,
1312
+ 3248222580,
1313
+ 3835390401,
1314
+ 4022224774,
1315
+ 264347078,
1316
+ 604807628,
1317
+ 770255983,
1318
+ 1249150122,
1319
+ 1555081692,
1320
+ 1996064986,
1321
+ 2554220882,
1322
+ 2821834349,
1323
+ 2952996808,
1324
+ 3210313671,
1325
+ 3336571891,
1326
+ 3584528711,
1327
+ 113926993,
1328
+ 338241895,
1329
+ 666307205,
1330
+ 773529912,
1331
+ 1294757372,
1332
+ 1396182291,
1333
+ 1695183700,
1334
+ 1986661051,
1335
+ 2177026350,
1336
+ 2456956037,
1337
+ 2730485921,
1338
+ 2820302411,
1339
+ 3259730800,
1340
+ 3345764771,
1341
+ 3516065817,
1342
+ 3600352804,
1343
+ 4094571909,
1344
+ 275423344,
1345
+ 430227734,
1346
+ 506948616,
1347
+ 659060556,
1348
+ 883997877,
1349
+ 958139571,
1350
+ 1322822218,
1351
+ 1537002063,
1352
+ 1747873779,
1353
+ 1955562222,
1354
+ 2024104815,
1355
+ 2227730452,
1356
+ 2361852424,
1357
+ 2428436474,
1358
+ 2756734187,
1359
+ 3204031479,
1360
+ 3329325298
1361
+ ]);
1362
+ function sha256(data) {
1363
+ const h = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
1364
+ const len = data.length;
1365
+ const padded = new Uint8Array(len + 9 + 63 >> 6 << 6);
1366
+ padded.set(data);
1367
+ padded[len] = 128;
1368
+ const view = new DataView(padded.buffer);
1369
+ view.setUint32(padded.length - 8, Math.floor(len / 536870912));
1370
+ view.setUint32(padded.length - 4, len << 3 >>> 0);
1371
+ const w = new Uint32Array(64);
1372
+ const rotr = (x, n) => x >>> n | x << 32 - n;
1373
+ for (let off = 0; off < padded.length; off += 64) {
1374
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
1375
+ for (let i = 16; i < 64; i++) {
1376
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
1377
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
1378
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
1379
+ }
1380
+ let [a, b, c, d, e, f, g, hh] = h;
1381
+ for (let i = 0; i < 64; i++) {
1382
+ const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] >>> 0;
1383
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
1384
+ hh = g;
1385
+ g = f;
1386
+ f = e;
1387
+ e = d + t1 >>> 0;
1388
+ d = c;
1389
+ c = b;
1390
+ b = a;
1391
+ a = t1 + t2 >>> 0;
1392
+ }
1393
+ h[0] += a;
1394
+ h[1] += b;
1395
+ h[2] += c;
1396
+ h[3] += d;
1397
+ h[4] += e;
1398
+ h[5] += f;
1399
+ h[6] += g;
1400
+ h[7] += hh;
1401
+ }
1402
+ const out2 = new Uint8Array(32);
1403
+ const ov = new DataView(out2.buffer);
1404
+ for (let i = 0; i < 8; i++) ov.setUint32(i * 4, h[i]);
1405
+ return out2;
1406
+ }
1407
+
1408
+ // src/cauthykey.ts
1409
+ var KEY_PREFIX = "cauthy1";
1410
+ var KEY_REPLACES = [
1411
+ "CAUTHY_ISSUER",
1412
+ "CAUTHY_CLIENT_ID",
1413
+ "CAUTHY_CLIENT_SECRET",
1414
+ "CAUTHY_SYSTEM",
1415
+ "CAUTHY_TENANT",
1416
+ "CAUTHY_SYSTEM_KEY",
1417
+ "CAUTHY_SYSTEM_KEY_ID",
1418
+ "CAUTHY_BASE_URL",
1419
+ "CAUTHY_AUDIENCE",
1420
+ "CAUTHY_TENANT_ID"
1421
+ ];
1422
+ function refuseBesideKey(environment) {
1423
+ const set = KEY_REPLACES.filter((n) => (environment?.[n] ?? "") !== "");
1424
+ if (set.length > 0) {
1425
+ throw new CauthyConfigError(`the application runs on CAUTHY_KEY, and ${set.join(", ")} ${set.length === 1 ? "is" : "are"} also set: remove ${set.length === 1 ? "it" : "them"} from the environment. Everything comes from the key.`);
1426
+ }
1427
+ }
1428
+ var MAX_LEN = 512;
1429
+ var enc = new TextEncoder();
1430
+ var dec = new TextDecoder("utf-8", { fatal: true });
1431
+ function b64uEncode(b) {
1432
+ let bin = "";
1433
+ for (const x of b) bin += String.fromCharCode(x);
1434
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1435
+ }
1436
+ function b64uDecode(s) {
1437
+ if (!/^[A-Za-z0-9_-]*$/.test(s) || s.length % 4 === 1) return null;
1438
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
1439
+ const out2 = Uint8Array.from(bin, (c) => c.charCodeAt(0));
1440
+ return b64uEncode(out2) === s ? out2 : null;
1441
+ }
1442
+ function checksum(body) {
1443
+ return b64uEncode(sha256(enc.encode(body)).slice(0, 6));
1444
+ }
1445
+ function printableAscii(s) {
1446
+ return /^[\x21-\x7e]*$/.test(s);
1447
+ }
1448
+ function validIssuer(s) {
1449
+ if (s === "" || !printableAscii(s)) return false;
1450
+ let u;
1451
+ try {
1452
+ u = new URL(s);
1453
+ } catch {
1454
+ return false;
1455
+ }
1456
+ if (u.origin !== s) return false;
1457
+ if (/^\[::ffff:/i.test(u.host)) return false;
1458
+ if (u.protocol === "https:") return true;
1459
+ return u.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname);
1460
+ }
1461
+ var fail = (reason, message) => {
1462
+ throw new CauthyKeyError(reason, message);
1463
+ };
1464
+ var NOT_A_KEY = "not a Cauthy key (copy it again from the dashboard)";
1465
+ function parseKey(text2) {
1466
+ const s = text2.replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, "");
1467
+ if (s === "" || s.length > MAX_LEN || !printableAscii(s)) fail("key_format", NOT_A_KEY);
1468
+ const head = s.split(".", 1)[0];
1469
+ if (head !== KEY_PREFIX) {
1470
+ if (/^cauthy[0-9]+$/.test(head)) fail("key_version", `this key is for a newer SDK (${head}): update @cauthyhq/node`);
1471
+ fail("key_format", NOT_A_KEY);
1472
+ }
1473
+ const parts = s.split(".");
1474
+ if (parts.length !== 5 || parts.some((p) => p === "")) fail("key_format", NOT_A_KEY);
1475
+ if (!constantTimeEqual(checksum(parts.slice(0, 4).join(".")), parts[4])) {
1476
+ fail("key_checksum", "the key is damaged (part of it is missing or changed): copy it again from the dashboard");
1477
+ }
1478
+ const issuerBytes = b64uDecode(parts[1]);
1479
+ let issuer = "";
1480
+ try {
1481
+ issuer = issuerBytes ? dec.decode(issuerBytes) : "";
1482
+ } catch {
1483
+ issuer = "";
1484
+ }
1485
+ if (!validIssuer(issuer)) fail("key_issuer", "the key names no valid Cauthy address");
1486
+ const clientBytes = b64uDecode(parts[2]);
1487
+ const clientId = clientBytes ? String.fromCharCode(...clientBytes) : "";
1488
+ if (!/^[A-Za-z0-9._-]{1,128}$/.test(clientId)) fail("key_client", "the key names no valid client");
1489
+ const secret = b64uDecode(parts[3]);
1490
+ if (parts[3].length !== 43 || !secret || secret.length !== 32) fail("key_secret", "the key's secret is not 32 bytes");
1491
+ return { issuer, clientId, secret: parts[3] };
1492
+ }
1493
+ function composeKey(issuer, clientId, secret) {
1494
+ const body = `${KEY_PREFIX}.${b64uEncode(enc.encode(issuer))}.${b64uEncode(enc.encode(clientId))}.${secret}`;
1495
+ const key = `${body}.${checksum(body)}`;
1496
+ parseKey(key);
1497
+ return key;
1498
+ }
1499
+ function constantTimeEqual(a, b) {
1500
+ if (a.length !== b.length) return false;
1501
+ let d = 0;
1502
+ for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
1503
+ return d === 0;
1504
+ }
1505
+
1506
+ // src/webhook.ts
1281
1507
  var CauthyWebhookError = class extends Error {
1282
1508
  constructor(reason) {
1283
1509
  super(`@cauthyhq/node: webhook refused: ${reason}`);
@@ -1312,7 +1538,8 @@ function keysOf(issuer) {
1312
1538
  }
1313
1539
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
1314
1540
  async function verifyWebhook(request, options = {}) {
1315
- const env4 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
1541
+ const key = typeof process !== "undefined" ? process.env?.CAUTHY_KEY : void 0;
1542
+ const env4 = key ? parseKey(key).issuer : typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
1316
1543
  const issuer = withoutTrailingSlashes(options.issuer ?? env4 ?? "");
1317
1544
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
1318
1545
  const signature = request.headers.get("x-cauthy-signature-jwt");
@@ -1441,7 +1668,7 @@ var LIMIT = 2n ** 53n;
1441
1668
  function parseStrict(text2, integerMembers = []) {
1442
1669
  let i = 0;
1443
1670
  let depth = 0;
1444
- const fail = (what) => {
1671
+ const fail2 = (what) => {
1445
1672
  throw new SyntaxError(`${what} at ${i}`);
1446
1673
  };
1447
1674
  const skip = () => {
@@ -1460,19 +1687,19 @@ function parseStrict(text2, integerMembers = []) {
1460
1687
  return v;
1461
1688
  }
1462
1689
  }
1463
- return fail("unexpected character");
1690
+ return fail2("unexpected character");
1464
1691
  };
1465
1692
  const string = () => {
1466
1693
  const start = i;
1467
1694
  i++;
1468
1695
  while (i < text2.length && text2[i] !== '"') i += text2[i] === "\\" ? 2 : 1;
1469
- if (text2[i] !== '"') fail("unterminated string");
1696
+ if (text2[i] !== '"') fail2("unterminated string");
1470
1697
  i++;
1471
1698
  return JSON.parse(text2.slice(start, i));
1472
1699
  };
1473
1700
  const number = (integer) => {
1474
1701
  const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text2.slice(i));
1475
- if (!m) return fail("bad number");
1702
+ if (!m) return fail2("bad number");
1476
1703
  i += m[0].length;
1477
1704
  if (integer && (m[2] !== void 0 || m[3] !== void 0)) return Number.NaN;
1478
1705
  if (m[2] === void 0 && m[3] === void 0) {
@@ -1499,7 +1726,7 @@ function parseStrict(text2, integerMembers = []) {
1499
1726
  i++;
1500
1727
  depth--;
1501
1728
  return out2;
1502
- } else fail("expected , or ]");
1729
+ } else fail2("expected , or ]");
1503
1730
  }
1504
1731
  };
1505
1732
  const object = () => {
@@ -1515,12 +1742,12 @@ function parseStrict(text2, integerMembers = []) {
1515
1742
  }
1516
1743
  for (; ; ) {
1517
1744
  skip();
1518
- if (text2[i] !== '"') fail("expected a name");
1745
+ if (text2[i] !== '"') fail2("expected a name");
1519
1746
  const name = string();
1520
- if (seen.has(name)) fail(`repeated name ${JSON.stringify(name)}`);
1747
+ if (seen.has(name)) fail2(`repeated name ${JSON.stringify(name)}`);
1521
1748
  seen.add(name);
1522
1749
  skip();
1523
- if (text2[i] !== ":") fail("expected :");
1750
+ if (text2[i] !== ":") fail2("expected :");
1524
1751
  i++;
1525
1752
  Object.defineProperty(out2, name, { value: value(depth === 1 && integerMembers.includes(name)), enumerable: true, writable: true, configurable: true });
1526
1753
  skip();
@@ -1529,15 +1756,175 @@ function parseStrict(text2, integerMembers = []) {
1529
1756
  i++;
1530
1757
  depth--;
1531
1758
  return out2;
1532
- } else fail("expected , or }");
1759
+ } else fail2("expected , or }");
1533
1760
  }
1534
1761
  };
1535
1762
  const result = value();
1536
1763
  skip();
1537
- if (i < text2.length) fail("trailing characters");
1764
+ if (i < text2.length) fail2("trailing characters");
1538
1765
  return result;
1539
1766
  }
1540
1767
 
1768
+ // src/deprecate.ts
1769
+ var told = /* @__PURE__ */ new Set();
1770
+ function deprecated(what, instead) {
1771
+ if (told.has(what)) return;
1772
+ told.add(what);
1773
+ const message = `@cauthyhq/node: ${what} is deprecated and leaves in 2.0. ${instead}`;
1774
+ if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
1775
+ process.emitWarning(message, { type: "DeprecationWarning", code: "CAUTHY_DEPRECATED" });
1776
+ } else {
1777
+ console.warn(message);
1778
+ }
1779
+ }
1780
+
1781
+ // src/keyderive.ts
1782
+ var enc2 = new TextEncoder();
1783
+ var PKCS8_ED25519 = Uint8Array.from([48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]);
1784
+ var ASSERTION_TYPE2 = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
1785
+ async function seedOf(secret) {
1786
+ const ikm = await crypto.subtle.importKey("raw", enc2.encode(secret), "HKDF", false, ["deriveBits"]);
1787
+ return new Uint8Array(await crypto.subtle.deriveBits(
1788
+ { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: enc2.encode("cauthy/sign/v1") },
1789
+ ikm,
1790
+ 256
1791
+ ));
1792
+ }
1793
+ async function thumbprint(x) {
1794
+ return b64uEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", enc2.encode(`{"crv":"Ed25519","kty":"OKP","x":"${x}"}`))));
1795
+ }
1796
+ async function signerOf(secret) {
1797
+ const seed = await seedOf(secret);
1798
+ const der = new Uint8Array(PKCS8_ED25519.length + 32);
1799
+ der.set(PKCS8_ED25519);
1800
+ der.set(seed, PKCS8_ED25519.length);
1801
+ const readable = await crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, true, ["sign"]);
1802
+ const { x } = await crypto.subtle.exportKey("jwk", readable);
1803
+ const signing = await crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, false, ["sign"]);
1804
+ der.fill(0);
1805
+ seed.fill(0);
1806
+ const publicJwk = { kty: "OKP", crv: "Ed25519", x };
1807
+ const kid = await thumbprint(publicJwk.x);
1808
+ const header = b64uEncode(enc2.encode(JSON.stringify({ alg: "EdDSA", kid })));
1809
+ return {
1810
+ kid,
1811
+ publicJwk,
1812
+ async assertion(clientId, tokenEndpoint, jti = crypto.randomUUID(), iat = Math.floor(Date.now() / 1e3)) {
1813
+ const claims = { iss: clientId, sub: clientId, aud: tokenEndpoint, jti, iat, exp: iat + 120 };
1814
+ const input = `${header}.${b64uEncode(enc2.encode(JSON.stringify(claims)))}`;
1815
+ const sig = new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, signing, enc2.encode(input)));
1816
+ return `${input}.${b64uEncode(sig)}`;
1817
+ }
1818
+ };
1819
+ }
1820
+ function deriveFromKey(key, previous) {
1821
+ const other = previous && previous.secret !== key.secret ? previous : void 0;
1822
+ const rings = deriveKeys(key.secret, other?.secret);
1823
+ return {
1824
+ cookie: rings.cookie,
1825
+ flow: rings.flow,
1826
+ signer: signerOf(key.secret),
1827
+ ...other ? { previousJwk: signerOf(other.secret).then((s) => ({ ...s.publicJwk, kid: s.kid })) } : {}
1828
+ };
1829
+ }
1830
+
1831
+ // src/identity.ts
1832
+ var REVALIDATE_MS = 3e5;
1833
+ var sleep = (ms) => new Promise((resolve2) => {
1834
+ const t = setTimeout(resolve2, ms);
1835
+ t.unref?.();
1836
+ });
1837
+ function createIdentity(o) {
1838
+ const doFetch = o.fetch ?? globalThis.fetch;
1839
+ const now = o.now ?? Date.now;
1840
+ const retry = o.retryMs ?? [1e3, 2e3, 5e3, 1e4, 3e4];
1841
+ let held;
1842
+ let etag;
1843
+ let checkedAt = 0;
1844
+ let first;
1845
+ let refused;
1846
+ let refreshing;
1847
+ let failed = false;
1848
+ let attemptEnded;
1849
+ let attemptOut;
1850
+ const kid = () => (typeof o.kid === "function" ? o.kid() : o.kid) ?? "?";
1851
+ async function ask() {
1852
+ const headers = { authorization: `Bearer ${await o.token()}`, accept: "application/json" };
1853
+ if (etag && held) headers["if-none-match"] = etag;
1854
+ const res = await doFetch(`${o.issuer}/v1/connect/self`, { headers });
1855
+ if (res.status === 304 && held) return "same";
1856
+ if (res.status === 410) throw Object.assign(new Error("client_inactive"), { code: "client_inactive" });
1857
+ if (!res.ok) throw new CauthyApiError(503, "unavailable", `Cauthy answered ${res.status} to /v1/connect/self`);
1858
+ const body = await res.json();
1859
+ if (!body?.data || typeof body.data.client_id !== "string") throw new CauthyApiError(503, "unavailable", "Cauthy's answer to /v1/connect/self has no identity");
1860
+ etag = res.headers.get("etag") ?? void 0;
1861
+ return body.data;
1862
+ }
1863
+ function refusalOf(e) {
1864
+ const code = e?.code;
1865
+ if (code !== "invalid_client" && code !== "client_inactive") return void 0;
1866
+ const message = code === "client_inactive" ? `Cauthy says this application's client is no longer active (kid ${kid()}). Connect it again in Cauthy's dashboard.` : `Cauthy refuses this application's key (kid ${kid()}): it was revoked or never registered. Make a new key in Cauthy's dashboard.`;
1867
+ return new CauthyApiError(503, "misconfigured", message);
1868
+ }
1869
+ function refuse(err) {
1870
+ if (refused === void 0) o.log?.(err.message.replace(/^@cauthyhq\/node: /, ""));
1871
+ refused = err;
1872
+ checkedAt = now();
1873
+ return err;
1874
+ }
1875
+ async function firstAnswer() {
1876
+ for (let attempt = 0; ; attempt++) {
1877
+ attemptOut = new Promise((resolve2) => {
1878
+ attemptEnded = resolve2;
1879
+ });
1880
+ try {
1881
+ const got = await ask();
1882
+ if (got !== "same") held = got;
1883
+ checkedAt = now();
1884
+ failed = false;
1885
+ attemptEnded?.(true);
1886
+ return held;
1887
+ } catch (e) {
1888
+ const r = refusalOf(e);
1889
+ failed = true;
1890
+ attemptEnded?.(false);
1891
+ if (r) throw refuse(r);
1892
+ await sleep(retry[Math.min(attempt, retry.length - 1)]);
1893
+ }
1894
+ }
1895
+ }
1896
+ async function refresh() {
1897
+ checkedAt = now();
1898
+ try {
1899
+ const got = await ask();
1900
+ if (got !== "same") held = got;
1901
+ refused = void 0;
1902
+ } catch (e) {
1903
+ const r = refusalOf(e);
1904
+ if (r) refuse(r);
1905
+ }
1906
+ }
1907
+ return {
1908
+ ready() {
1909
+ if ((held || refused) && now() - checkedAt >= REVALIDATE_MS) {
1910
+ refreshing ??= refresh().finally(() => {
1911
+ refreshing = void 0;
1912
+ });
1913
+ }
1914
+ if (refused) return Promise.reject(refused);
1915
+ if (held) return Promise.resolve(held);
1916
+ first ??= firstAnswer();
1917
+ return first;
1918
+ },
1919
+ current: () => held,
1920
+ refresh: () => refreshing ??= refresh().finally(() => {
1921
+ refreshing = void 0;
1922
+ }),
1923
+ failing: () => held === void 0 && failed && refused === void 0,
1924
+ attempt: () => held !== void 0 ? Promise.resolve(true) : attemptOut ?? Promise.resolve(false)
1925
+ };
1926
+ }
1927
+
1541
1928
  // src/verify.ts
1542
1929
  var CauthyUnavailableError = class extends Error {
1543
1930
  constructor(detail) {
@@ -1680,6 +2067,10 @@ function decodeJson(segment) {
1680
2067
  return parseStrict(Buffer.from(padded, "base64").toString("utf8"));
1681
2068
  }
1682
2069
  function createTokenVerifier(options = {}) {
2070
+ const fromEnv = ["CAUTHY_ISSUER", "CAUTHY_AUDIENCE", "CAUTHY_TENANT_ID"].filter((name, i) => [options.issuer, options.audience, options.tenantId][i] === void 0 && (env2(name) ?? "") !== "");
2071
+ if (fromEnv.length > 0) {
2072
+ deprecated("Reading CAUTHY_ISSUER, CAUTHY_AUDIENCE and CAUTHY_TENANT_ID to verify tokens", "Set CAUTHY_KEY and use createVerifierFromEnv().");
2073
+ }
1683
2074
  const issuerGiven = options.issuer ?? env2("CAUTHY_ISSUER") ?? "";
1684
2075
  if (issuerGiven === "") throw new CauthyConfigError("issuer (CAUTHY_ISSUER) is not set");
1685
2076
  const issuer = issuerAddress(issuerGiven, "issuer");
@@ -1775,18 +2166,18 @@ function createTokenVerifier(options = {}) {
1775
2166
  async function verifyToken(from, options = {}) {
1776
2167
  return createTokenVerifier(options)(from);
1777
2168
  }
1778
-
1779
- // src/deprecate.ts
1780
- var told = /* @__PURE__ */ new Set();
1781
- function deprecated(what, instead) {
1782
- if (told.has(what)) return;
1783
- told.add(what);
1784
- const message = `@cauthyhq/node: ${what} is deprecated and leaves in 2.0. ${instead}`;
1785
- if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
1786
- process.emitWarning(message, { type: "DeprecationWarning", code: "CAUTHY_DEPRECATED" });
1787
- } else {
1788
- console.warn(message);
2169
+ async function createVerifierFromEnv(options = {}) {
2170
+ const text2 = options.key ?? env2("CAUTHY_KEY");
2171
+ if (text2 === void 0 || text2 === "") return createTokenVerifier(options);
2172
+ refuseBesideKey(typeof process !== "undefined" ? process.env : void 0);
2173
+ for (const name of ["issuer", "audience", "tenantId"]) {
2174
+ if (options[name] !== void 0) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`);
1789
2175
  }
2176
+ const key = parseKey(text2);
2177
+ const system = createSystem({ issuer: key.issuer, clientId: key.clientId, signer: { clientId: key.clientId, signer: deriveFromKey(key).signer }, fetch: options.fetch });
2178
+ const id = await createIdentity({ issuer: key.issuer, token: () => system.token(), fetch: deadline(options.fetch, KEYS_TIMEOUT_MS) }).ready();
2179
+ const { key: _, ...rest } = options;
2180
+ return createTokenVerifier({ ...rest, issuer: id.issuer, audience: id.audience, tenantId: id.tenant.id });
1790
2181
  }
1791
2182
 
1792
2183
  // src/mount.ts
@@ -1879,6 +2270,63 @@ function redirect(location, cookies = []) {
1879
2270
  function conditionalOf(c) {
1880
2271
  return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
1881
2272
  }
2273
+ function byKey(given) {
2274
+ const text2 = given.key ?? env3("CAUTHY_KEY");
2275
+ if (text2 === void 0 || text2 === "") return void 0;
2276
+ refuseBesideKey(typeof process !== "undefined" ? process.env : void 0);
2277
+ for (const [name, value] of [
2278
+ ["issuer", given.issuer],
2279
+ ["clientId", given.clientId],
2280
+ ["clientSecret", given.clientSecret],
2281
+ ["secret", given.secret],
2282
+ ["system", given.system],
2283
+ ["tenant", given.tenant],
2284
+ ["audience", given.audience],
2285
+ ["tenantId", given.tenantId],
2286
+ ["systemKey", given.systemKey],
2287
+ ["rootKey", given.rootKey]
2288
+ ]) {
2289
+ if (value !== void 0) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`);
2290
+ }
2291
+ const key = parseKey(text2);
2292
+ const prevText = given.keyPrevious ?? env3("CAUTHY_KEY_PREVIOUS");
2293
+ let previous;
2294
+ if (prevText) {
2295
+ try {
2296
+ previous = parseKey(prevText);
2297
+ } catch (e) {
2298
+ if (e instanceof CauthyKeyError) throw new CauthyKeyError(e.reason, `CAUTHY_KEY_PREVIOUS is refused: ${e.message.replace(/^@cauthyhq\/node: CAUTHY_KEY: /, "").replace(/ \(key_[a-z_]+\)$/, "")}`);
2299
+ throw e;
2300
+ }
2301
+ }
2302
+ if (previous && (previous.issuer !== key.issuer || previous.clientId !== key.clientId)) {
2303
+ throw new CauthyKeyError("key_previous_mismatch", "CAUTHY_KEY_PREVIOUS names another Cauthy or client than CAUTHY_KEY");
2304
+ }
2305
+ const legacySecret = env3("CAUTHY_SECRET") || void 0;
2306
+ const legacyRoot = env3("PACT_ROOT_KEY") || void 0;
2307
+ if (legacySecret) deprecated("CAUTHY_SECRET beside CAUTHY_KEY", "It is read only for sessions made before the move: remove it after the rotation window.");
2308
+ if (legacyRoot) deprecated("PACT_ROOT_KEY beside CAUTHY_KEY", "It is read only for sessions made before the move: remove it, the keys option and its table after the rotation window.");
2309
+ if (given.keys && !(legacyRoot && given.contract)) {
2310
+ throw new CauthyConfigError("with CAUTHY_KEY the application keeps no key store: remove keys (it is read only beside PACT_ROOT_KEY and a contract, while moving)");
2311
+ }
2312
+ return { key, previous, legacySecret, legacyRoot };
2313
+ }
2314
+ var UnknownOriginError = class extends Error {
2315
+ constructor(origin, registered) {
2316
+ super(`${origin} is not one of the application's addresses in Cauthy (${registered.join(", ") || "none"}): add it on the application's card, or set baseURL`);
2317
+ this.origin = origin;
2318
+ this.registered = registered;
2319
+ }
2320
+ origin;
2321
+ registered;
2322
+ };
2323
+ function contractOnKey(given, keyed) {
2324
+ const contract = readContract(given.contract);
2325
+ if (contract.issuer !== keyed.key.issuer) {
2326
+ throw new CauthyKeyError("key_contract_issuer", `the key names ${keyed.key.issuer}, and the contract's Cauthy is ${contract.issuer}: make the key in that Cauthy's dashboard`);
2327
+ }
2328
+ return contract;
2329
+ }
1882
2330
  function byContract(given) {
1883
2331
  failClosed(typeof process !== "undefined" ? process.env : void 0);
1884
2332
  for (const [name, value] of [
@@ -1897,27 +2345,48 @@ function byContract(given) {
1897
2345
  if (contract.serviceClientId && !given.keys) {
1898
2346
  throw new CauthyConfigError(`the link ${contract.link} gives ${contract.system} its own identity: pass keys, where its key is kept (@cauthyhq/node/keys)`);
1899
2347
  }
2348
+ deprecated("PACT_ROOT_KEY", "Make the application's CAUTHY_KEY in Cauthy's dashboard (the application's card), then remove PACT_ROOT_KEY, the keys option and its table.");
1900
2349
  return { contract, root, previous: given.rootKeyPrevious ?? env3("PACT_ROOT_KEY_PREVIOUS") };
1901
2350
  }
1902
2351
  var toldOfHeader = false;
1903
2352
  function createCauthy(given = {}) {
1904
- const pacted = given.contract ? byContract(given) : void 0;
1905
- const contract = pacted?.contract;
1906
- const issuer = withoutTrailingSlashes(given.issuer ?? contract?.issuer ?? env3("CAUTHY_ISSUER") ?? "");
1907
- const clientId = contract?.clientId ?? given.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
1908
- const clientSecret = contract ? void 0 : given.clientSecret ?? env3("CAUTHY_CLIENT_SECRET");
1909
- const secret = contract ? "" : given.secret ?? env3("CAUTHY_SECRET") ?? "";
1910
- const system = contract?.system ?? given.system ?? env3("CAUTHY_SYSTEM") ?? "";
1911
- const tenant = contract?.tenant ?? given.tenant ?? env3("CAUTHY_TENANT") ?? "";
2353
+ const keyed = byKey(given);
2354
+ const pacted = given.contract && !keyed ? byContract(given) : void 0;
2355
+ const contract = keyed && given.contract ? contractOnKey(given, keyed) : pacted?.contract;
2356
+ const issuer = withoutTrailingSlashes(keyed?.key.issuer ?? given.issuer ?? contract?.issuer ?? env3("CAUTHY_ISSUER") ?? "");
2357
+ const clientId = contract?.clientId ?? keyed?.key.clientId ?? given.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
2358
+ const clientSecret = contract || keyed ? void 0 : given.clientSecret ?? env3("CAUTHY_CLIENT_SECRET");
2359
+ const secret = contract || keyed ? "" : given.secret ?? env3("CAUTHY_SECRET") ?? "";
2360
+ const askSelf = keyed !== void 0 && contract === void 0;
2361
+ const system = askSelf ? "" : contract?.system ?? given.system ?? env3("CAUTHY_SYSTEM") ?? "";
2362
+ const tenant = askSelf ? "" : contract?.tenant ?? given.tenant ?? env3("CAUTHY_TENANT") ?? "";
1912
2363
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
1913
2364
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
1914
- if (!contract && secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
2365
+ if (!contract && !keyed && secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
2366
+ const fromKey = keyed ? deriveFromKey(keyed.key, keyed.previous) : void 0;
2367
+ let signingKid;
2368
+ fromKey?.signer.then((s) => {
2369
+ signingKid = s.kid;
2370
+ }, () => void 0);
1915
2371
  const derived = pacted ? deriveKeys(pacted.root, pacted.previous) : void 0;
1916
- const single = derived ? void 0 : keyOf(secret).then((k) => [k]);
1917
- const cookieKeys = derived?.cookie ?? single;
1918
- const flowKeys = derived?.flow ?? single;
1919
- const ownKeys = derived && given.keys ? pactKeys(given.keys, derived.seal) : void 0;
1920
- const baseURL = given.baseURL ?? env3("CAUTHY_BASE_URL");
2372
+ const single = derived || fromKey ? void 0 : keyOf(secret).then((k) => [k]);
2373
+ const legacyRings = keyed?.legacyRoot ? deriveKeys(keyed.legacyRoot) : void 0;
2374
+ const legacySingle = keyed?.legacySecret ? keyOf(keyed.legacySecret) : void 0;
2375
+ const withLegacy = async (own, purpose) => [
2376
+ ...await own,
2377
+ ...legacyRings ? [(await legacyRings[purpose])[0]] : [],
2378
+ ...legacySingle ? [await legacySingle] : []
2379
+ ];
2380
+ const cookieKeys = fromKey ? withLegacy(fromKey.cookie, "cookie") : derived?.cookie ?? single;
2381
+ const flowKeys = fromKey ? withLegacy(fromKey.flow, "flow") : derived?.flow ?? single;
2382
+ if (!keyed && !pacted) {
2383
+ deprecated(
2384
+ "Configuring with CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_CLIENT_SECRET, CAUTHY_SECRET and the other CAUTHY_* variables (or their options)",
2385
+ "Connect the application in Cauthy's dashboard and use the one variable CAUTHY_KEY."
2386
+ );
2387
+ }
2388
+ const ownKeys = derived && given.keys ? pactKeys(given.keys, derived.seal) : legacyRings && contract && given.keys ? pactKeys(given.keys, legacyRings.seal) : void 0;
2389
+ const baseURL = given.baseURL ?? (keyed ? void 0 : env3("CAUTHY_BASE_URL"));
1921
2390
  if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
1922
2391
  throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
1923
2392
  }
@@ -1953,18 +2422,65 @@ function createCauthy(given = {}) {
1953
2422
  acceptInviteUrl,
1954
2423
  fetch: given.fetch,
1955
2424
  timeoutMs: systemTimeout,
1956
- ...ownKeys ? { keys: ownKeys, keysClientId: contract?.serviceClientId } : {}
2425
+ ...ownKeys ? { keys: ownKeys, keysClientId: contract?.serviceClientId } : {},
2426
+ // With a contract the key proves the link's service client; without, the key's own client.
2427
+ ...fromKey ? {
2428
+ signer: { clientId: contract ? contract.serviceClientId ?? keyed.key.clientId : clientId, signer: fromKey.signer },
2429
+ currentSystem: () => identity?.current()?.system ?? ""
2430
+ } : {}
1957
2431
  });
1958
- const originOf = (request) => {
1959
- if (baseURL) return withoutTrailingSlashes(baseURL);
2432
+ const selfOptions = () => ({
2433
+ issuer,
2434
+ token: () => ownSystem.token(),
2435
+ fetch: deadline(given.fetch, requestTimeout),
2436
+ kid: () => signingKid,
2437
+ log: (m) => console.error(`@cauthyhq/node: ${m}`)
2438
+ });
2439
+ const identity = askSelf ? createIdentity({
2440
+ issuer,
2441
+ token: () => ownSystem.token(),
2442
+ fetch: deadline(given.fetch, requestTimeout),
2443
+ kid: () => signingKid,
2444
+ log: (m) => console.error(`@cauthyhq/node: ${m}`)
2445
+ }) : void 0;
2446
+ identity?.ready().catch(() => {
2447
+ });
2448
+ const tenantNow = () => identity?.current()?.tenant.slug ?? tenant;
2449
+ const systemNow = () => identity?.current()?.system ?? system;
2450
+ const requestOrigin = (request) => {
1960
2451
  const url = new URL(request.url);
1961
2452
  const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase();
1962
2453
  return proto === "https" || proto === "http" ? `${proto}://${url.host}` : url.origin;
1963
2454
  };
1964
- const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
2455
+ const originOf = (request) => {
2456
+ if (baseURL) return withoutTrailingSlashes(baseURL);
2457
+ const asked = requestOrigin(request);
2458
+ if (!identity) return asked;
2459
+ const urls2 = identity.current()?.app_urls ?? [];
2460
+ if (urls2.includes(asked)) return asked;
2461
+ if (urls2.length === 1) return urls2[0];
2462
+ throw new UnknownOriginError(asked, urls2);
2463
+ };
2464
+ const safeOrigin = (request) => {
2465
+ try {
2466
+ return originOf(request);
2467
+ } catch {
2468
+ return requestOrigin(request);
2469
+ }
2470
+ };
2471
+ const attrs = (request, maxAge) => ({ secure: safeOrigin(request).startsWith("https://"), maxAge });
2472
+ async function clientAuth(form) {
2473
+ if (fromKey && !contract) {
2474
+ const s = await fromKey.signer;
2475
+ form.set("client_assertion_type", ASSERTION_TYPE2);
2476
+ form.set("client_assertion", await s.assertion(clientId, `${issuer}/v1/oauth/token`));
2477
+ } else if (clientSecret) {
2478
+ form.set("client_secret", clientSecret);
2479
+ }
2480
+ return form;
2481
+ }
1965
2482
  async function token(form, proof = {}) {
1966
- const body2 = new URLSearchParams({ client_id: clientId, ...form });
1967
- if (clientSecret) body2.set("client_secret", clientSecret);
2483
+ const body2 = await clientAuth(new URLSearchParams({ client_id: clientId, ...form }));
1968
2484
  try {
1969
2485
  const res = await doFetch(`${issuer}/v1/oauth/token`, {
1970
2486
  method: "POST",
@@ -2003,8 +2519,7 @@ function createCauthy(given = {}) {
2003
2519
  }
2004
2520
  const kindOf = (body2, was) => (typeof body2?.persistent === "boolean" ? body2.persistent : was?.p === 1) ? { p: 1 } : {};
2005
2521
  async function revoke(refreshToken) {
2006
- const form = new URLSearchParams({ client_id: clientId, token: refreshToken });
2007
- if (clientSecret) form.set("client_secret", clientSecret);
2522
+ const form = await clientAuth(new URLSearchParams({ client_id: clientId, token: refreshToken }));
2008
2523
  await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
2009
2524
  }
2010
2525
  const unverified = (u) => contract?.requireVerifiedEmail === true && !u.emailVerified;
@@ -2096,7 +2611,8 @@ function createCauthy(given = {}) {
2096
2611
  async function organizations(session) {
2097
2612
  const hit = orgCache.get(session.accessToken);
2098
2613
  if (hit && Date.now() - hit.at < ORGANIZATIONS_TTL_MS) return hit.value;
2099
- const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
2614
+ const sys = systemNow();
2615
+ const url = `${issuer}/v1/oauth/organizations${sys ? `?system=${encodeURIComponent(sys)}` : ""}`;
2100
2616
  const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
2101
2617
  if (!res.ok) throw new Error(`@cauthyhq/node: reading organizations: ${res.status}`);
2102
2618
  const body2 = await res.json();
@@ -2250,8 +2766,7 @@ function createCauthy(given = {}) {
2250
2766
  async function logout(request) {
2251
2767
  const { had, value } = await read(request);
2252
2768
  if (value !== null) {
2253
- const body2 = new URLSearchParams({ client_id: clientId, token: value.rt });
2254
- if (clientSecret) body2.set("client_secret", clientSecret);
2769
+ const body2 = await clientAuth(new URLSearchParams({ client_id: clientId, token: value.rt }));
2255
2770
  await doFetch(`${issuer}/v1/oauth/revoke`, {
2256
2771
  method: "POST",
2257
2772
  headers: { "content-type": "application/x-www-form-urlencoded" },
@@ -2375,7 +2890,7 @@ function createCauthy(given = {}) {
2375
2890
  const res = await doFetch(`${issuer}${path}`, {
2376
2891
  method,
2377
2892
  headers: {
2378
- "x-tenant-key": tenant,
2893
+ "x-tenant-key": tenantNow(),
2379
2894
  accept: "application/json",
2380
2895
  // A link Cauthy mails opens the application's page for it when it
2381
2896
  // has one; otherwise Cauthy's own. Either way in the reader's language.
@@ -2521,7 +3036,7 @@ function createCauthy(given = {}) {
2521
3036
  }
2522
3037
  if (route === "/sign-in/anonymous" || route === "/me/upgrade") deprecated(`POST ${route}`, "Guest sessions leave the auth-bff protocol in version 2.");
2523
3038
  if (route.startsWith("/me/upgrade/social/")) deprecated("GET /me/upgrade/social/{provider}", "Guest sessions leave the auth-bff protocol in version 2.");
2524
- if (tenant === "") {
3039
+ if (tenantNow() === "") {
2525
3040
  return problem(500, "not_configured", "@cauthyhq/node: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
2526
3041
  }
2527
3042
  if (route === "/look" && !post) {
@@ -2756,7 +3271,7 @@ function createCauthy(given = {}) {
2756
3271
  let upstream;
2757
3272
  try {
2758
3273
  upstream = await doFetch(`${issuer}/v1/auth/mfa/push/events`, {
2759
- headers: { "x-tenant-key": tenant, "x-mfa-token": step.token, accept: "text/event-stream" },
3274
+ headers: { "x-tenant-key": tenantNow(), "x-mfa-token": step.token, accept: "text/event-stream" },
2760
3275
  signal: request.signal
2761
3276
  });
2762
3277
  } catch {
@@ -2776,8 +3291,7 @@ function createCauthy(given = {}) {
2776
3291
  if (route === "/sign-out") {
2777
3292
  const { had, value } = await read(request);
2778
3293
  if (value !== null) {
2779
- const form = new URLSearchParams({ client_id: clientId, token: value.rt });
2780
- if (clientSecret) form.set("client_secret", clientSecret);
3294
+ const form = await clientAuth(new URLSearchParams({ client_id: clientId, token: value.rt }));
2781
3295
  await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
2782
3296
  orgCache.delete(value.at);
2783
3297
  }
@@ -3176,14 +3690,38 @@ function createCauthy(given = {}) {
3176
3690
  const inside = url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
3177
3691
  const route = inside ? withoutTrailingSlashes(url.pathname.slice(basePath.length)) || "/" : null;
3178
3692
  const published3 = pact !== void 0 && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD");
3693
+ if (identity && route !== null) {
3694
+ const unready = await identityGate();
3695
+ if (unready) return finish(unready, route);
3696
+ }
3179
3697
  try {
3180
3698
  return finish(await serve(request), route, published3);
3181
3699
  } catch (err) {
3700
+ if (err instanceof UnknownOriginError) return finish(problem(400, "unknown_origin", err.message), route);
3182
3701
  if (!(err instanceof CauthyApiError && err.code === "email_not_confirmed")) throw err;
3183
3702
  const wantsPage = request.method === "GET" && (request.headers.get("sec-fetch-mode") ?? "navigate") === "navigate";
3184
3703
  return finish(wantsPage ? new Response("Sign-in refused (email_not_confirmed). Confirm your email address first.", { status: 403, headers: { "content-type": "text/plain; charset=utf-8" } }) : problem(403, "email_not_confirmed", "Confirm your email address first."), route);
3185
3704
  }
3186
3705
  }
3706
+ async function identityGate() {
3707
+ if (!identity) return null;
3708
+ const unavailable = () => problem(503, "unavailable", "Cauthy cannot be reached yet. Try again in a moment.");
3709
+ if (identity.failing()) return unavailable();
3710
+ let timer;
3711
+ const waited = new Promise((resolve3) => {
3712
+ timer = setTimeout(() => resolve3("timeout"), requestTimeout);
3713
+ });
3714
+ try {
3715
+ const ready = identity.ready();
3716
+ const got = await Promise.race([ready.then(() => "ok"), identity.attempt().then((ok) => ok ? "ok" : "failed"), waited]);
3717
+ if (got === "failed") await Promise.race([ready, Promise.resolve()]);
3718
+ return got === "ok" ? null : unavailable();
3719
+ } catch (e) {
3720
+ return e.code === "misconfigured" ? problem(503, "misconfigured", "Cauthy refuses this application's key: make a new one in Cauthy's dashboard.") : unavailable();
3721
+ } finally {
3722
+ clearTimeout(timer);
3723
+ }
3724
+ }
3187
3725
  async function serve(request) {
3188
3726
  const url = new URL(request.url);
3189
3727
  if (pact && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD")) return pact.jwks();
@@ -3266,7 +3804,28 @@ function createCauthy(given = {}) {
3266
3804
  return { response: redirect(to) };
3267
3805
  }
3268
3806
  const replay = given.webhookReplay ?? memoryReplayStore();
3807
+ let lateSelf;
3269
3808
  const tokenVerifier = (o = {}) => {
3809
+ if (keyed) {
3810
+ if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
3811
+ throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
3812
+ }
3813
+ let built;
3814
+ return async (from) => {
3815
+ const id = await (identity ?? (lateSelf ??= createIdentity(selfOptions()))).ready();
3816
+ if (built?.from !== id) {
3817
+ built = { from: id, verify: createTokenVerifier({
3818
+ issuer,
3819
+ tenantId: contract?.tenantId ?? id.tenant.id,
3820
+ audience: id.audience,
3821
+ fetch: given.fetch,
3822
+ ...contract ? { clientId: contract.clientId } : {},
3823
+ ...o
3824
+ }) };
3825
+ }
3826
+ return built.verify(from);
3827
+ };
3828
+ }
3270
3829
  if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
3271
3830
  throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
3272
3831
  }
@@ -3280,7 +3839,30 @@ function createCauthy(given = {}) {
3280
3839
  });
3281
3840
  };
3282
3841
  const webhook = (request, o = {}) => verifyWebhook(request, { issuer, url: o.url ?? `${originOf(request)}${new URL(request.url).pathname}`, jwks: o.jwks, now: o.now, replay });
3283
- const pact = contract && ownKeys ? {
3842
+ async function keyJwks() {
3843
+ const s = await fromKey.signer;
3844
+ const keys2 = [{ ...s.publicJwk, kid: s.kid, alg: "EdDSA", use: "sig" }];
3845
+ if (fromKey.previousJwk) keys2.push({ ...await fromKey.previousJwk, alg: "EdDSA", use: "sig" });
3846
+ if (ownKeys) keys2.push(...(await ownKeys.published()).keys);
3847
+ return new Response(JSON.stringify({ keys: keys2 }), {
3848
+ headers: { "content-type": "application/json; charset=utf-8", "cache-control": `public, max-age=${JWKS_MAX_AGE_S}` }
3849
+ });
3850
+ }
3851
+ const noStore = () => {
3852
+ throw new CauthyConfigError("on CAUTHY_KEY the application keeps no key of its own: make a new key on the application's card in Cauthy's dashboard");
3853
+ };
3854
+ const keyOnly = {
3855
+ current: async () => noStore(),
3856
+ published: async () => ({ keys: [] }),
3857
+ rotate: async () => noStore(),
3858
+ reseal: async () => 0
3859
+ };
3860
+ const pact = contract && fromKey ? {
3861
+ contract,
3862
+ keys: ownKeys ?? keyOnly,
3863
+ jwks: keyJwks,
3864
+ status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
3865
+ } : contract && ownKeys ? {
3284
3866
  contract,
3285
3867
  keys: ownKeys,
3286
3868
  async jwks() {
@@ -3290,8 +3872,25 @@ function createCauthy(given = {}) {
3290
3872
  },
3291
3873
  status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
3292
3874
  } : void 0;
3875
+ const configured = () => ({
3876
+ issuer,
3877
+ audience: given.audience ?? env3("CAUTHY_AUDIENCE") ?? "",
3878
+ tenant: { id: contract?.tenantId ?? given.tenantId ?? env3("CAUTHY_TENANT_ID") ?? "", slug: tenant },
3879
+ system,
3880
+ client_id: clientId,
3881
+ sign_in_client_id: clientId,
3882
+ app_urls: baseURL ? [withoutTrailingSlashes(baseURL)] : [],
3883
+ callback_path: `${basePath}/callback`,
3884
+ key: null,
3885
+ contract: contract ? { link: contract.link, system_proof: contract.systemProof, require_verified_email: contract.requireVerifiedEmail } : null
3886
+ });
3293
3887
  return {
3294
3888
  handler,
3889
+ ready: async () => {
3890
+ if (identity) return identity.ready();
3891
+ const id = configured();
3892
+ return fromKey ? { ...id, key: { kid: (await fromKey.signer).kid, retires_at: null } } : id;
3893
+ },
3295
3894
  checkMount: (target) => checkMount(basePath, target),
3296
3895
  webhook,
3297
3896
  tokenVerifier,
@@ -3381,10 +3980,12 @@ async function connectSystem(o) {
3381
3980
  export {
3382
3981
  CauthyApiError,
3383
3982
  CauthyConfigError,
3983
+ CauthyKeyError,
3384
3984
  CauthyTokenError,
3385
3985
  CauthyUnavailableError,
3386
3986
  CauthyWebhookError,
3387
3987
  DEFAULT_PASSWORD_POLICY,
3988
+ KEY_REPLACES,
3388
3989
  MIN_PASSWORD,
3389
3990
  PROTOCOL_VERSION,
3390
3991
  STANDARD_VARIABLES,
@@ -3394,15 +3995,18 @@ export {
3394
3995
  checkName,
3395
3996
  checkNewPassword,
3396
3997
  checkPassword,
3998
+ composeKey,
3397
3999
  connectSystem,
3398
4000
  createCauthy,
3399
4001
  createSystem,
3400
4002
  createTokenVerifier,
4003
+ createVerifierFromEnv,
3401
4004
  definePolicy,
3402
4005
  describePasswordPolicy,
3403
4006
  explainPasswordProblems,
3404
4007
  failClosed,
3405
4008
  memoryReplayStore,
4009
+ parseKey,
3406
4010
  passwordPolicyOf,
3407
4011
  passwordProblems,
3408
4012
  policySource,