@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.js CHANGED
@@ -22,10 +22,12 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  CauthyApiError: () => CauthyApiError,
24
24
  CauthyConfigError: () => CauthyConfigError,
25
+ CauthyKeyError: () => CauthyKeyError,
25
26
  CauthyTokenError: () => CauthyTokenError,
26
27
  CauthyUnavailableError: () => CauthyUnavailableError,
27
28
  CauthyWebhookError: () => CauthyWebhookError,
28
29
  DEFAULT_PASSWORD_POLICY: () => DEFAULT_PASSWORD_POLICY,
30
+ KEY_REPLACES: () => KEY_REPLACES,
29
31
  MIN_PASSWORD: () => MIN_PASSWORD,
30
32
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
31
33
  STANDARD_VARIABLES: () => STANDARD_VARIABLES,
@@ -35,15 +37,18 @@ __export(src_exports, {
35
37
  checkName: () => checkName,
36
38
  checkNewPassword: () => checkNewPassword,
37
39
  checkPassword: () => checkPassword,
40
+ composeKey: () => composeKey,
38
41
  connectSystem: () => connectSystem,
39
42
  createCauthy: () => createCauthy,
40
43
  createSystem: () => createSystem,
41
44
  createTokenVerifier: () => createTokenVerifier,
45
+ createVerifierFromEnv: () => createVerifierFromEnv,
42
46
  definePolicy: () => definePolicy,
43
47
  describePasswordPolicy: () => describePasswordPolicy,
44
48
  explainPasswordProblems: () => explainPasswordProblems,
45
49
  failClosed: () => failClosed,
46
50
  memoryReplayStore: () => memoryReplayStore,
51
+ parseKey: () => parseKey,
47
52
  passwordPolicyOf: () => passwordPolicyOf,
48
53
  passwordProblems: () => passwordProblems,
49
54
  policySource: () => policySource,
@@ -131,6 +136,14 @@ var CauthyConfigError = class extends Error {
131
136
  this.name = "CauthyConfigError";
132
137
  }
133
138
  };
139
+ var CauthyKeyError = class extends CauthyConfigError {
140
+ constructor(reason, message) {
141
+ super(`CAUTHY_KEY: ${message} (${reason})`);
142
+ this.reason = reason;
143
+ this.name = "CauthyKeyError";
144
+ }
145
+ reason;
146
+ };
134
147
 
135
148
  // src/url.ts
136
149
  function withoutTrailingSlashes(s) {
@@ -1041,6 +1054,7 @@ function createSystem(given = {}) {
1041
1054
  };
1042
1055
  const tokenEndpoint = `${issuer}/v1/oauth/token`;
1043
1056
  function configured() {
1057
+ if (given.signer) return;
1044
1058
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
1045
1059
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
1046
1060
  if (given.keys) {
@@ -1053,7 +1067,7 @@ function createSystem(given = {}) {
1053
1067
  }
1054
1068
  let imported;
1055
1069
  const signingKey = () => imported ??= loadKey(key);
1056
- async function fail(res, what) {
1070
+ async function fail2(res, what) {
1057
1071
  const body = await res.json().catch(() => null);
1058
1072
  const first = body?.errors?.[0];
1059
1073
  const code = typeof body?.error === "string" ? body.error : first?.code;
@@ -1064,9 +1078,13 @@ function createSystem(given = {}) {
1064
1078
  let asking;
1065
1079
  async function ask() {
1066
1080
  configured();
1067
- const self = given.keys ? given.keysClientId : key?.clientId || clientId;
1081
+ const self = given.signer ? given.signer.clientId : given.keys ? given.keysClientId : key?.clientId || clientId;
1068
1082
  const form = new URLSearchParams({ grant_type: "client_credentials", client_id: self });
1069
- if (given.keys || key) {
1083
+ if (given.signer) {
1084
+ const s = await given.signer.signer;
1085
+ form.set("client_assertion_type", ASSERTION_TYPE);
1086
+ form.set("client_assertion", await s.assertion(self, tokenEndpoint));
1087
+ } else if (given.keys || key) {
1070
1088
  const signer = given.keys ? await given.keys.current() : { kid: key.keyId, privateKey: await signingKey() };
1071
1089
  const assertion = await new import_jose3.SignJWT({}).setProtectedHeader({ alg: "RS256", kid: signer.kid }).setIssuer(self).setSubject(self).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(signer.privateKey);
1072
1090
  form.set("client_assertion_type", ASSERTION_TYPE);
@@ -1075,7 +1093,7 @@ function createSystem(given = {}) {
1075
1093
  form.set("client_secret", clientSecret);
1076
1094
  }
1077
1095
  const res = await doFetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
1078
- if (!res.ok) await fail(res, "the application's own token");
1096
+ if (!res.ok) await fail2(res, "the application's own token");
1079
1097
  const t = await res.json();
1080
1098
  held = { value: t.access_token, until: Date.now() + t.expires_in * 1e3 - SKEW_MS };
1081
1099
  return t.access_token;
@@ -1097,7 +1115,7 @@ function createSystem(given = {}) {
1097
1115
  held = void 0;
1098
1116
  res = await send();
1099
1117
  }
1100
- if (!res.ok) await fail(res, `${method} ${path}`);
1118
+ if (!res.ok) await fail2(res, `${method} ${path}`);
1101
1119
  return res.status === 204 ? void 0 : res.json();
1102
1120
  }
1103
1121
  const data = async (method, path, body, extra) => (await call(method, path, body, extra))?.data;
@@ -1171,7 +1189,7 @@ function createSystem(given = {}) {
1171
1189
  }
1172
1190
  },
1173
1191
  async check(q) {
1174
- const system = q.system ?? ownSystem;
1192
+ const system = q.system ?? (ownSystem || given.currentSystem?.() || "");
1175
1193
  if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
1176
1194
  return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
1177
1195
  },
@@ -1341,6 +1359,219 @@ function firstProblem(...checks) {
1341
1359
 
1342
1360
  // src/webhook.ts
1343
1361
  var import_jose4 = require("jose");
1362
+
1363
+ // src/sha256.ts
1364
+ var K = new Uint32Array([
1365
+ 1116352408,
1366
+ 1899447441,
1367
+ 3049323471,
1368
+ 3921009573,
1369
+ 961987163,
1370
+ 1508970993,
1371
+ 2453635748,
1372
+ 2870763221,
1373
+ 3624381080,
1374
+ 310598401,
1375
+ 607225278,
1376
+ 1426881987,
1377
+ 1925078388,
1378
+ 2162078206,
1379
+ 2614888103,
1380
+ 3248222580,
1381
+ 3835390401,
1382
+ 4022224774,
1383
+ 264347078,
1384
+ 604807628,
1385
+ 770255983,
1386
+ 1249150122,
1387
+ 1555081692,
1388
+ 1996064986,
1389
+ 2554220882,
1390
+ 2821834349,
1391
+ 2952996808,
1392
+ 3210313671,
1393
+ 3336571891,
1394
+ 3584528711,
1395
+ 113926993,
1396
+ 338241895,
1397
+ 666307205,
1398
+ 773529912,
1399
+ 1294757372,
1400
+ 1396182291,
1401
+ 1695183700,
1402
+ 1986661051,
1403
+ 2177026350,
1404
+ 2456956037,
1405
+ 2730485921,
1406
+ 2820302411,
1407
+ 3259730800,
1408
+ 3345764771,
1409
+ 3516065817,
1410
+ 3600352804,
1411
+ 4094571909,
1412
+ 275423344,
1413
+ 430227734,
1414
+ 506948616,
1415
+ 659060556,
1416
+ 883997877,
1417
+ 958139571,
1418
+ 1322822218,
1419
+ 1537002063,
1420
+ 1747873779,
1421
+ 1955562222,
1422
+ 2024104815,
1423
+ 2227730452,
1424
+ 2361852424,
1425
+ 2428436474,
1426
+ 2756734187,
1427
+ 3204031479,
1428
+ 3329325298
1429
+ ]);
1430
+ function sha256(data) {
1431
+ const h = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
1432
+ const len = data.length;
1433
+ const padded = new Uint8Array(len + 9 + 63 >> 6 << 6);
1434
+ padded.set(data);
1435
+ padded[len] = 128;
1436
+ const view = new DataView(padded.buffer);
1437
+ view.setUint32(padded.length - 8, Math.floor(len / 536870912));
1438
+ view.setUint32(padded.length - 4, len << 3 >>> 0);
1439
+ const w = new Uint32Array(64);
1440
+ const rotr = (x, n) => x >>> n | x << 32 - n;
1441
+ for (let off = 0; off < padded.length; off += 64) {
1442
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
1443
+ for (let i = 16; i < 64; i++) {
1444
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
1445
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
1446
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
1447
+ }
1448
+ let [a, b, c, d, e, f, g, hh] = h;
1449
+ for (let i = 0; i < 64; i++) {
1450
+ const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] >>> 0;
1451
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
1452
+ hh = g;
1453
+ g = f;
1454
+ f = e;
1455
+ e = d + t1 >>> 0;
1456
+ d = c;
1457
+ c = b;
1458
+ b = a;
1459
+ a = t1 + t2 >>> 0;
1460
+ }
1461
+ h[0] += a;
1462
+ h[1] += b;
1463
+ h[2] += c;
1464
+ h[3] += d;
1465
+ h[4] += e;
1466
+ h[5] += f;
1467
+ h[6] += g;
1468
+ h[7] += hh;
1469
+ }
1470
+ const out2 = new Uint8Array(32);
1471
+ const ov = new DataView(out2.buffer);
1472
+ for (let i = 0; i < 8; i++) ov.setUint32(i * 4, h[i]);
1473
+ return out2;
1474
+ }
1475
+
1476
+ // src/cauthykey.ts
1477
+ var KEY_PREFIX = "cauthy1";
1478
+ var KEY_REPLACES = [
1479
+ "CAUTHY_ISSUER",
1480
+ "CAUTHY_CLIENT_ID",
1481
+ "CAUTHY_CLIENT_SECRET",
1482
+ "CAUTHY_SYSTEM",
1483
+ "CAUTHY_TENANT",
1484
+ "CAUTHY_SYSTEM_KEY",
1485
+ "CAUTHY_SYSTEM_KEY_ID",
1486
+ "CAUTHY_BASE_URL",
1487
+ "CAUTHY_AUDIENCE",
1488
+ "CAUTHY_TENANT_ID"
1489
+ ];
1490
+ function refuseBesideKey(environment) {
1491
+ const set = KEY_REPLACES.filter((n) => (environment?.[n] ?? "") !== "");
1492
+ if (set.length > 0) {
1493
+ 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.`);
1494
+ }
1495
+ }
1496
+ var MAX_LEN = 512;
1497
+ var enc = new TextEncoder();
1498
+ var dec = new TextDecoder("utf-8", { fatal: true });
1499
+ function b64uEncode(b) {
1500
+ let bin = "";
1501
+ for (const x of b) bin += String.fromCharCode(x);
1502
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1503
+ }
1504
+ function b64uDecode(s) {
1505
+ if (!/^[A-Za-z0-9_-]*$/.test(s) || s.length % 4 === 1) return null;
1506
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
1507
+ const out2 = Uint8Array.from(bin, (c) => c.charCodeAt(0));
1508
+ return b64uEncode(out2) === s ? out2 : null;
1509
+ }
1510
+ function checksum(body) {
1511
+ return b64uEncode(sha256(enc.encode(body)).slice(0, 6));
1512
+ }
1513
+ function printableAscii(s) {
1514
+ return /^[\x21-\x7e]*$/.test(s);
1515
+ }
1516
+ function validIssuer(s) {
1517
+ if (s === "" || !printableAscii(s)) return false;
1518
+ let u;
1519
+ try {
1520
+ u = new URL(s);
1521
+ } catch {
1522
+ return false;
1523
+ }
1524
+ if (u.origin !== s) return false;
1525
+ if (/^\[::ffff:/i.test(u.host)) return false;
1526
+ if (u.protocol === "https:") return true;
1527
+ return u.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname);
1528
+ }
1529
+ var fail = (reason, message) => {
1530
+ throw new CauthyKeyError(reason, message);
1531
+ };
1532
+ var NOT_A_KEY = "not a Cauthy key (copy it again from the dashboard)";
1533
+ function parseKey(text2) {
1534
+ const s = text2.replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, "");
1535
+ if (s === "" || s.length > MAX_LEN || !printableAscii(s)) fail("key_format", NOT_A_KEY);
1536
+ const head = s.split(".", 1)[0];
1537
+ if (head !== KEY_PREFIX) {
1538
+ if (/^cauthy[0-9]+$/.test(head)) fail("key_version", `this key is for a newer SDK (${head}): update @cauthyhq/node`);
1539
+ fail("key_format", NOT_A_KEY);
1540
+ }
1541
+ const parts = s.split(".");
1542
+ if (parts.length !== 5 || parts.some((p) => p === "")) fail("key_format", NOT_A_KEY);
1543
+ if (!constantTimeEqual(checksum(parts.slice(0, 4).join(".")), parts[4])) {
1544
+ fail("key_checksum", "the key is damaged (part of it is missing or changed): copy it again from the dashboard");
1545
+ }
1546
+ const issuerBytes = b64uDecode(parts[1]);
1547
+ let issuer = "";
1548
+ try {
1549
+ issuer = issuerBytes ? dec.decode(issuerBytes) : "";
1550
+ } catch {
1551
+ issuer = "";
1552
+ }
1553
+ if (!validIssuer(issuer)) fail("key_issuer", "the key names no valid Cauthy address");
1554
+ const clientBytes = b64uDecode(parts[2]);
1555
+ const clientId = clientBytes ? String.fromCharCode(...clientBytes) : "";
1556
+ if (!/^[A-Za-z0-9._-]{1,128}$/.test(clientId)) fail("key_client", "the key names no valid client");
1557
+ const secret = b64uDecode(parts[3]);
1558
+ if (parts[3].length !== 43 || !secret || secret.length !== 32) fail("key_secret", "the key's secret is not 32 bytes");
1559
+ return { issuer, clientId, secret: parts[3] };
1560
+ }
1561
+ function composeKey(issuer, clientId, secret) {
1562
+ const body = `${KEY_PREFIX}.${b64uEncode(enc.encode(issuer))}.${b64uEncode(enc.encode(clientId))}.${secret}`;
1563
+ const key = `${body}.${checksum(body)}`;
1564
+ parseKey(key);
1565
+ return key;
1566
+ }
1567
+ function constantTimeEqual(a, b) {
1568
+ if (a.length !== b.length) return false;
1569
+ let d = 0;
1570
+ for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
1571
+ return d === 0;
1572
+ }
1573
+
1574
+ // src/webhook.ts
1344
1575
  var CauthyWebhookError = class extends Error {
1345
1576
  constructor(reason) {
1346
1577
  super(`@cauthyhq/node: webhook refused: ${reason}`);
@@ -1375,7 +1606,8 @@ function keysOf(issuer) {
1375
1606
  }
1376
1607
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
1377
1608
  async function verifyWebhook(request, options = {}) {
1378
- const env4 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
1609
+ const key = typeof process !== "undefined" ? process.env?.CAUTHY_KEY : void 0;
1610
+ const env4 = key ? parseKey(key).issuer : typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
1379
1611
  const issuer = withoutTrailingSlashes(options.issuer ?? env4 ?? "");
1380
1612
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
1381
1613
  const signature = request.headers.get("x-cauthy-signature-jwt");
@@ -1504,7 +1736,7 @@ var LIMIT = 2n ** 53n;
1504
1736
  function parseStrict(text2, integerMembers = []) {
1505
1737
  let i = 0;
1506
1738
  let depth = 0;
1507
- const fail = (what) => {
1739
+ const fail2 = (what) => {
1508
1740
  throw new SyntaxError(`${what} at ${i}`);
1509
1741
  };
1510
1742
  const skip = () => {
@@ -1523,19 +1755,19 @@ function parseStrict(text2, integerMembers = []) {
1523
1755
  return v;
1524
1756
  }
1525
1757
  }
1526
- return fail("unexpected character");
1758
+ return fail2("unexpected character");
1527
1759
  };
1528
1760
  const string = () => {
1529
1761
  const start = i;
1530
1762
  i++;
1531
1763
  while (i < text2.length && text2[i] !== '"') i += text2[i] === "\\" ? 2 : 1;
1532
- if (text2[i] !== '"') fail("unterminated string");
1764
+ if (text2[i] !== '"') fail2("unterminated string");
1533
1765
  i++;
1534
1766
  return JSON.parse(text2.slice(start, i));
1535
1767
  };
1536
1768
  const number = (integer) => {
1537
1769
  const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text2.slice(i));
1538
- if (!m) return fail("bad number");
1770
+ if (!m) return fail2("bad number");
1539
1771
  i += m[0].length;
1540
1772
  if (integer && (m[2] !== void 0 || m[3] !== void 0)) return Number.NaN;
1541
1773
  if (m[2] === void 0 && m[3] === void 0) {
@@ -1562,7 +1794,7 @@ function parseStrict(text2, integerMembers = []) {
1562
1794
  i++;
1563
1795
  depth--;
1564
1796
  return out2;
1565
- } else fail("expected , or ]");
1797
+ } else fail2("expected , or ]");
1566
1798
  }
1567
1799
  };
1568
1800
  const object = () => {
@@ -1578,12 +1810,12 @@ function parseStrict(text2, integerMembers = []) {
1578
1810
  }
1579
1811
  for (; ; ) {
1580
1812
  skip();
1581
- if (text2[i] !== '"') fail("expected a name");
1813
+ if (text2[i] !== '"') fail2("expected a name");
1582
1814
  const name = string();
1583
- if (seen.has(name)) fail(`repeated name ${JSON.stringify(name)}`);
1815
+ if (seen.has(name)) fail2(`repeated name ${JSON.stringify(name)}`);
1584
1816
  seen.add(name);
1585
1817
  skip();
1586
- if (text2[i] !== ":") fail("expected :");
1818
+ if (text2[i] !== ":") fail2("expected :");
1587
1819
  i++;
1588
1820
  Object.defineProperty(out2, name, { value: value(depth === 1 && integerMembers.includes(name)), enumerable: true, writable: true, configurable: true });
1589
1821
  skip();
@@ -1592,15 +1824,175 @@ function parseStrict(text2, integerMembers = []) {
1592
1824
  i++;
1593
1825
  depth--;
1594
1826
  return out2;
1595
- } else fail("expected , or }");
1827
+ } else fail2("expected , or }");
1596
1828
  }
1597
1829
  };
1598
1830
  const result = value();
1599
1831
  skip();
1600
- if (i < text2.length) fail("trailing characters");
1832
+ if (i < text2.length) fail2("trailing characters");
1601
1833
  return result;
1602
1834
  }
1603
1835
 
1836
+ // src/deprecate.ts
1837
+ var told = /* @__PURE__ */ new Set();
1838
+ function deprecated(what, instead) {
1839
+ if (told.has(what)) return;
1840
+ told.add(what);
1841
+ const message = `@cauthyhq/node: ${what} is deprecated and leaves in 2.0. ${instead}`;
1842
+ if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
1843
+ process.emitWarning(message, { type: "DeprecationWarning", code: "CAUTHY_DEPRECATED" });
1844
+ } else {
1845
+ console.warn(message);
1846
+ }
1847
+ }
1848
+
1849
+ // src/keyderive.ts
1850
+ var enc2 = new TextEncoder();
1851
+ var PKCS8_ED25519 = Uint8Array.from([48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]);
1852
+ var ASSERTION_TYPE2 = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
1853
+ async function seedOf(secret) {
1854
+ const ikm = await crypto.subtle.importKey("raw", enc2.encode(secret), "HKDF", false, ["deriveBits"]);
1855
+ return new Uint8Array(await crypto.subtle.deriveBits(
1856
+ { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: enc2.encode("cauthy/sign/v1") },
1857
+ ikm,
1858
+ 256
1859
+ ));
1860
+ }
1861
+ async function thumbprint(x) {
1862
+ return b64uEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", enc2.encode(`{"crv":"Ed25519","kty":"OKP","x":"${x}"}`))));
1863
+ }
1864
+ async function signerOf(secret) {
1865
+ const seed = await seedOf(secret);
1866
+ const der = new Uint8Array(PKCS8_ED25519.length + 32);
1867
+ der.set(PKCS8_ED25519);
1868
+ der.set(seed, PKCS8_ED25519.length);
1869
+ const readable = await crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, true, ["sign"]);
1870
+ const { x } = await crypto.subtle.exportKey("jwk", readable);
1871
+ const signing = await crypto.subtle.importKey("pkcs8", der, { name: "Ed25519" }, false, ["sign"]);
1872
+ der.fill(0);
1873
+ seed.fill(0);
1874
+ const publicJwk = { kty: "OKP", crv: "Ed25519", x };
1875
+ const kid = await thumbprint(publicJwk.x);
1876
+ const header = b64uEncode(enc2.encode(JSON.stringify({ alg: "EdDSA", kid })));
1877
+ return {
1878
+ kid,
1879
+ publicJwk,
1880
+ async assertion(clientId, tokenEndpoint, jti = crypto.randomUUID(), iat = Math.floor(Date.now() / 1e3)) {
1881
+ const claims = { iss: clientId, sub: clientId, aud: tokenEndpoint, jti, iat, exp: iat + 120 };
1882
+ const input = `${header}.${b64uEncode(enc2.encode(JSON.stringify(claims)))}`;
1883
+ const sig = new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, signing, enc2.encode(input)));
1884
+ return `${input}.${b64uEncode(sig)}`;
1885
+ }
1886
+ };
1887
+ }
1888
+ function deriveFromKey(key, previous) {
1889
+ const other = previous && previous.secret !== key.secret ? previous : void 0;
1890
+ const rings = deriveKeys(key.secret, other?.secret);
1891
+ return {
1892
+ cookie: rings.cookie,
1893
+ flow: rings.flow,
1894
+ signer: signerOf(key.secret),
1895
+ ...other ? { previousJwk: signerOf(other.secret).then((s) => ({ ...s.publicJwk, kid: s.kid })) } : {}
1896
+ };
1897
+ }
1898
+
1899
+ // src/identity.ts
1900
+ var REVALIDATE_MS = 3e5;
1901
+ var sleep = (ms) => new Promise((resolve2) => {
1902
+ const t = setTimeout(resolve2, ms);
1903
+ t.unref?.();
1904
+ });
1905
+ function createIdentity(o) {
1906
+ const doFetch = o.fetch ?? globalThis.fetch;
1907
+ const now = o.now ?? Date.now;
1908
+ const retry = o.retryMs ?? [1e3, 2e3, 5e3, 1e4, 3e4];
1909
+ let held;
1910
+ let etag;
1911
+ let checkedAt = 0;
1912
+ let first;
1913
+ let refused;
1914
+ let refreshing;
1915
+ let failed = false;
1916
+ let attemptEnded;
1917
+ let attemptOut;
1918
+ const kid = () => (typeof o.kid === "function" ? o.kid() : o.kid) ?? "?";
1919
+ async function ask() {
1920
+ const headers = { authorization: `Bearer ${await o.token()}`, accept: "application/json" };
1921
+ if (etag && held) headers["if-none-match"] = etag;
1922
+ const res = await doFetch(`${o.issuer}/v1/connect/self`, { headers });
1923
+ if (res.status === 304 && held) return "same";
1924
+ if (res.status === 410) throw Object.assign(new Error("client_inactive"), { code: "client_inactive" });
1925
+ if (!res.ok) throw new CauthyApiError(503, "unavailable", `Cauthy answered ${res.status} to /v1/connect/self`);
1926
+ const body = await res.json();
1927
+ if (!body?.data || typeof body.data.client_id !== "string") throw new CauthyApiError(503, "unavailable", "Cauthy's answer to /v1/connect/self has no identity");
1928
+ etag = res.headers.get("etag") ?? void 0;
1929
+ return body.data;
1930
+ }
1931
+ function refusalOf(e) {
1932
+ const code = e?.code;
1933
+ if (code !== "invalid_client" && code !== "client_inactive") return void 0;
1934
+ 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.`;
1935
+ return new CauthyApiError(503, "misconfigured", message);
1936
+ }
1937
+ function refuse(err) {
1938
+ if (refused === void 0) o.log?.(err.message.replace(/^@cauthyhq\/node: /, ""));
1939
+ refused = err;
1940
+ checkedAt = now();
1941
+ return err;
1942
+ }
1943
+ async function firstAnswer() {
1944
+ for (let attempt = 0; ; attempt++) {
1945
+ attemptOut = new Promise((resolve2) => {
1946
+ attemptEnded = resolve2;
1947
+ });
1948
+ try {
1949
+ const got = await ask();
1950
+ if (got !== "same") held = got;
1951
+ checkedAt = now();
1952
+ failed = false;
1953
+ attemptEnded?.(true);
1954
+ return held;
1955
+ } catch (e) {
1956
+ const r = refusalOf(e);
1957
+ failed = true;
1958
+ attemptEnded?.(false);
1959
+ if (r) throw refuse(r);
1960
+ await sleep(retry[Math.min(attempt, retry.length - 1)]);
1961
+ }
1962
+ }
1963
+ }
1964
+ async function refresh() {
1965
+ checkedAt = now();
1966
+ try {
1967
+ const got = await ask();
1968
+ if (got !== "same") held = got;
1969
+ refused = void 0;
1970
+ } catch (e) {
1971
+ const r = refusalOf(e);
1972
+ if (r) refuse(r);
1973
+ }
1974
+ }
1975
+ return {
1976
+ ready() {
1977
+ if ((held || refused) && now() - checkedAt >= REVALIDATE_MS) {
1978
+ refreshing ??= refresh().finally(() => {
1979
+ refreshing = void 0;
1980
+ });
1981
+ }
1982
+ if (refused) return Promise.reject(refused);
1983
+ if (held) return Promise.resolve(held);
1984
+ first ??= firstAnswer();
1985
+ return first;
1986
+ },
1987
+ current: () => held,
1988
+ refresh: () => refreshing ??= refresh().finally(() => {
1989
+ refreshing = void 0;
1990
+ }),
1991
+ failing: () => held === void 0 && failed && refused === void 0,
1992
+ attempt: () => held !== void 0 ? Promise.resolve(true) : attemptOut ?? Promise.resolve(false)
1993
+ };
1994
+ }
1995
+
1604
1996
  // src/verify.ts
1605
1997
  var CauthyUnavailableError = class extends Error {
1606
1998
  constructor(detail) {
@@ -1743,6 +2135,10 @@ function decodeJson(segment) {
1743
2135
  return parseStrict(Buffer.from(padded, "base64").toString("utf8"));
1744
2136
  }
1745
2137
  function createTokenVerifier(options = {}) {
2138
+ const fromEnv = ["CAUTHY_ISSUER", "CAUTHY_AUDIENCE", "CAUTHY_TENANT_ID"].filter((name, i) => [options.issuer, options.audience, options.tenantId][i] === void 0 && (env2(name) ?? "") !== "");
2139
+ if (fromEnv.length > 0) {
2140
+ deprecated("Reading CAUTHY_ISSUER, CAUTHY_AUDIENCE and CAUTHY_TENANT_ID to verify tokens", "Set CAUTHY_KEY and use createVerifierFromEnv().");
2141
+ }
1746
2142
  const issuerGiven = options.issuer ?? env2("CAUTHY_ISSUER") ?? "";
1747
2143
  if (issuerGiven === "") throw new CauthyConfigError("issuer (CAUTHY_ISSUER) is not set");
1748
2144
  const issuer = issuerAddress(issuerGiven, "issuer");
@@ -1838,18 +2234,18 @@ function createTokenVerifier(options = {}) {
1838
2234
  async function verifyToken(from, options = {}) {
1839
2235
  return createTokenVerifier(options)(from);
1840
2236
  }
1841
-
1842
- // src/deprecate.ts
1843
- var told = /* @__PURE__ */ new Set();
1844
- function deprecated(what, instead) {
1845
- if (told.has(what)) return;
1846
- told.add(what);
1847
- const message = `@cauthyhq/node: ${what} is deprecated and leaves in 2.0. ${instead}`;
1848
- if (typeof process !== "undefined" && typeof process.emitWarning === "function") {
1849
- process.emitWarning(message, { type: "DeprecationWarning", code: "CAUTHY_DEPRECATED" });
1850
- } else {
1851
- console.warn(message);
2237
+ async function createVerifierFromEnv(options = {}) {
2238
+ const text2 = options.key ?? env2("CAUTHY_KEY");
2239
+ if (text2 === void 0 || text2 === "") return createTokenVerifier(options);
2240
+ refuseBesideKey(typeof process !== "undefined" ? process.env : void 0);
2241
+ for (const name of ["issuer", "audience", "tenantId"]) {
2242
+ if (options[name] !== void 0) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`);
1852
2243
  }
2244
+ const key = parseKey(text2);
2245
+ const system = createSystem({ issuer: key.issuer, clientId: key.clientId, signer: { clientId: key.clientId, signer: deriveFromKey(key).signer }, fetch: options.fetch });
2246
+ const id = await createIdentity({ issuer: key.issuer, token: () => system.token(), fetch: deadline(options.fetch, KEYS_TIMEOUT_MS) }).ready();
2247
+ const { key: _, ...rest } = options;
2248
+ return createTokenVerifier({ ...rest, issuer: id.issuer, audience: id.audience, tenantId: id.tenant.id });
1853
2249
  }
1854
2250
 
1855
2251
  // src/mount.ts
@@ -1942,6 +2338,63 @@ function redirect(location, cookies = []) {
1942
2338
  function conditionalOf(c) {
1943
2339
  return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
1944
2340
  }
2341
+ function byKey(given) {
2342
+ const text2 = given.key ?? env3("CAUTHY_KEY");
2343
+ if (text2 === void 0 || text2 === "") return void 0;
2344
+ refuseBesideKey(typeof process !== "undefined" ? process.env : void 0);
2345
+ for (const [name, value] of [
2346
+ ["issuer", given.issuer],
2347
+ ["clientId", given.clientId],
2348
+ ["clientSecret", given.clientSecret],
2349
+ ["secret", given.secret],
2350
+ ["system", given.system],
2351
+ ["tenant", given.tenant],
2352
+ ["audience", given.audience],
2353
+ ["tenantId", given.tenantId],
2354
+ ["systemKey", given.systemKey],
2355
+ ["rootKey", given.rootKey]
2356
+ ]) {
2357
+ if (value !== void 0) throw new CauthyConfigError(`with CAUTHY_KEY, ${name} comes from CAUTHY_KEY: do not pass it`);
2358
+ }
2359
+ const key = parseKey(text2);
2360
+ const prevText = given.keyPrevious ?? env3("CAUTHY_KEY_PREVIOUS");
2361
+ let previous;
2362
+ if (prevText) {
2363
+ try {
2364
+ previous = parseKey(prevText);
2365
+ } catch (e) {
2366
+ 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_]+\)$/, "")}`);
2367
+ throw e;
2368
+ }
2369
+ }
2370
+ if (previous && (previous.issuer !== key.issuer || previous.clientId !== key.clientId)) {
2371
+ throw new CauthyKeyError("key_previous_mismatch", "CAUTHY_KEY_PREVIOUS names another Cauthy or client than CAUTHY_KEY");
2372
+ }
2373
+ const legacySecret = env3("CAUTHY_SECRET") || void 0;
2374
+ const legacyRoot = env3("PACT_ROOT_KEY") || void 0;
2375
+ if (legacySecret) deprecated("CAUTHY_SECRET beside CAUTHY_KEY", "It is read only for sessions made before the move: remove it after the rotation window.");
2376
+ 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.");
2377
+ if (given.keys && !(legacyRoot && given.contract)) {
2378
+ 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)");
2379
+ }
2380
+ return { key, previous, legacySecret, legacyRoot };
2381
+ }
2382
+ var UnknownOriginError = class extends Error {
2383
+ constructor(origin, registered) {
2384
+ 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`);
2385
+ this.origin = origin;
2386
+ this.registered = registered;
2387
+ }
2388
+ origin;
2389
+ registered;
2390
+ };
2391
+ function contractOnKey(given, keyed) {
2392
+ const contract = readContract(given.contract);
2393
+ if (contract.issuer !== keyed.key.issuer) {
2394
+ 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`);
2395
+ }
2396
+ return contract;
2397
+ }
1945
2398
  function byContract(given) {
1946
2399
  failClosed(typeof process !== "undefined" ? process.env : void 0);
1947
2400
  for (const [name, value] of [
@@ -1960,27 +2413,48 @@ function byContract(given) {
1960
2413
  if (contract.serviceClientId && !given.keys) {
1961
2414
  throw new CauthyConfigError(`the link ${contract.link} gives ${contract.system} its own identity: pass keys, where its key is kept (@cauthyhq/node/keys)`);
1962
2415
  }
2416
+ 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.");
1963
2417
  return { contract, root, previous: given.rootKeyPrevious ?? env3("PACT_ROOT_KEY_PREVIOUS") };
1964
2418
  }
1965
2419
  var toldOfHeader = false;
1966
2420
  function createCauthy(given = {}) {
1967
- const pacted = given.contract ? byContract(given) : void 0;
1968
- const contract = pacted?.contract;
1969
- const issuer = withoutTrailingSlashes(given.issuer ?? contract?.issuer ?? env3("CAUTHY_ISSUER") ?? "");
1970
- const clientId = contract?.clientId ?? given.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
1971
- const clientSecret = contract ? void 0 : given.clientSecret ?? env3("CAUTHY_CLIENT_SECRET");
1972
- const secret = contract ? "" : given.secret ?? env3("CAUTHY_SECRET") ?? "";
1973
- const system = contract?.system ?? given.system ?? env3("CAUTHY_SYSTEM") ?? "";
1974
- const tenant = contract?.tenant ?? given.tenant ?? env3("CAUTHY_TENANT") ?? "";
2421
+ const keyed = byKey(given);
2422
+ const pacted = given.contract && !keyed ? byContract(given) : void 0;
2423
+ const contract = keyed && given.contract ? contractOnKey(given, keyed) : pacted?.contract;
2424
+ const issuer = withoutTrailingSlashes(keyed?.key.issuer ?? given.issuer ?? contract?.issuer ?? env3("CAUTHY_ISSUER") ?? "");
2425
+ const clientId = contract?.clientId ?? keyed?.key.clientId ?? given.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
2426
+ const clientSecret = contract || keyed ? void 0 : given.clientSecret ?? env3("CAUTHY_CLIENT_SECRET");
2427
+ const secret = contract || keyed ? "" : given.secret ?? env3("CAUTHY_SECRET") ?? "";
2428
+ const askSelf = keyed !== void 0 && contract === void 0;
2429
+ const system = askSelf ? "" : contract?.system ?? given.system ?? env3("CAUTHY_SYSTEM") ?? "";
2430
+ const tenant = askSelf ? "" : contract?.tenant ?? given.tenant ?? env3("CAUTHY_TENANT") ?? "";
1975
2431
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
1976
2432
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
1977
- if (!contract && secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
2433
+ if (!contract && !keyed && secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
2434
+ const fromKey = keyed ? deriveFromKey(keyed.key, keyed.previous) : void 0;
2435
+ let signingKid;
2436
+ fromKey?.signer.then((s) => {
2437
+ signingKid = s.kid;
2438
+ }, () => void 0);
1978
2439
  const derived = pacted ? deriveKeys(pacted.root, pacted.previous) : void 0;
1979
- const single = derived ? void 0 : keyOf(secret).then((k) => [k]);
1980
- const cookieKeys = derived?.cookie ?? single;
1981
- const flowKeys = derived?.flow ?? single;
1982
- const ownKeys = derived && given.keys ? pactKeys(given.keys, derived.seal) : void 0;
1983
- const baseURL = given.baseURL ?? env3("CAUTHY_BASE_URL");
2440
+ const single = derived || fromKey ? void 0 : keyOf(secret).then((k) => [k]);
2441
+ const legacyRings = keyed?.legacyRoot ? deriveKeys(keyed.legacyRoot) : void 0;
2442
+ const legacySingle = keyed?.legacySecret ? keyOf(keyed.legacySecret) : void 0;
2443
+ const withLegacy = async (own, purpose) => [
2444
+ ...await own,
2445
+ ...legacyRings ? [(await legacyRings[purpose])[0]] : [],
2446
+ ...legacySingle ? [await legacySingle] : []
2447
+ ];
2448
+ const cookieKeys = fromKey ? withLegacy(fromKey.cookie, "cookie") : derived?.cookie ?? single;
2449
+ const flowKeys = fromKey ? withLegacy(fromKey.flow, "flow") : derived?.flow ?? single;
2450
+ if (!keyed && !pacted) {
2451
+ deprecated(
2452
+ "Configuring with CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_CLIENT_SECRET, CAUTHY_SECRET and the other CAUTHY_* variables (or their options)",
2453
+ "Connect the application in Cauthy's dashboard and use the one variable CAUTHY_KEY."
2454
+ );
2455
+ }
2456
+ const ownKeys = derived && given.keys ? pactKeys(given.keys, derived.seal) : legacyRings && contract && given.keys ? pactKeys(given.keys, legacyRings.seal) : void 0;
2457
+ const baseURL = given.baseURL ?? (keyed ? void 0 : env3("CAUTHY_BASE_URL"));
1984
2458
  if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
1985
2459
  throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
1986
2460
  }
@@ -2016,18 +2490,65 @@ function createCauthy(given = {}) {
2016
2490
  acceptInviteUrl,
2017
2491
  fetch: given.fetch,
2018
2492
  timeoutMs: systemTimeout,
2019
- ...ownKeys ? { keys: ownKeys, keysClientId: contract?.serviceClientId } : {}
2493
+ ...ownKeys ? { keys: ownKeys, keysClientId: contract?.serviceClientId } : {},
2494
+ // With a contract the key proves the link's service client; without, the key's own client.
2495
+ ...fromKey ? {
2496
+ signer: { clientId: contract ? contract.serviceClientId ?? keyed.key.clientId : clientId, signer: fromKey.signer },
2497
+ currentSystem: () => identity?.current()?.system ?? ""
2498
+ } : {}
2020
2499
  });
2021
- const originOf = (request) => {
2022
- if (baseURL) return withoutTrailingSlashes(baseURL);
2500
+ const selfOptions = () => ({
2501
+ issuer,
2502
+ token: () => ownSystem.token(),
2503
+ fetch: deadline(given.fetch, requestTimeout),
2504
+ kid: () => signingKid,
2505
+ log: (m) => console.error(`@cauthyhq/node: ${m}`)
2506
+ });
2507
+ const identity = askSelf ? createIdentity({
2508
+ issuer,
2509
+ token: () => ownSystem.token(),
2510
+ fetch: deadline(given.fetch, requestTimeout),
2511
+ kid: () => signingKid,
2512
+ log: (m) => console.error(`@cauthyhq/node: ${m}`)
2513
+ }) : void 0;
2514
+ identity?.ready().catch(() => {
2515
+ });
2516
+ const tenantNow = () => identity?.current()?.tenant.slug ?? tenant;
2517
+ const systemNow = () => identity?.current()?.system ?? system;
2518
+ const requestOrigin = (request) => {
2023
2519
  const url = new URL(request.url);
2024
2520
  const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase();
2025
2521
  return proto === "https" || proto === "http" ? `${proto}://${url.host}` : url.origin;
2026
2522
  };
2027
- const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
2523
+ const originOf = (request) => {
2524
+ if (baseURL) return withoutTrailingSlashes(baseURL);
2525
+ const asked = requestOrigin(request);
2526
+ if (!identity) return asked;
2527
+ const urls2 = identity.current()?.app_urls ?? [];
2528
+ if (urls2.includes(asked)) return asked;
2529
+ if (urls2.length === 1) return urls2[0];
2530
+ throw new UnknownOriginError(asked, urls2);
2531
+ };
2532
+ const safeOrigin = (request) => {
2533
+ try {
2534
+ return originOf(request);
2535
+ } catch {
2536
+ return requestOrigin(request);
2537
+ }
2538
+ };
2539
+ const attrs = (request, maxAge) => ({ secure: safeOrigin(request).startsWith("https://"), maxAge });
2540
+ async function clientAuth(form) {
2541
+ if (fromKey && !contract) {
2542
+ const s = await fromKey.signer;
2543
+ form.set("client_assertion_type", ASSERTION_TYPE2);
2544
+ form.set("client_assertion", await s.assertion(clientId, `${issuer}/v1/oauth/token`));
2545
+ } else if (clientSecret) {
2546
+ form.set("client_secret", clientSecret);
2547
+ }
2548
+ return form;
2549
+ }
2028
2550
  async function token(form, proof = {}) {
2029
- const body2 = new URLSearchParams({ client_id: clientId, ...form });
2030
- if (clientSecret) body2.set("client_secret", clientSecret);
2551
+ const body2 = await clientAuth(new URLSearchParams({ client_id: clientId, ...form }));
2031
2552
  try {
2032
2553
  const res = await doFetch(`${issuer}/v1/oauth/token`, {
2033
2554
  method: "POST",
@@ -2066,8 +2587,7 @@ function createCauthy(given = {}) {
2066
2587
  }
2067
2588
  const kindOf = (body2, was) => (typeof body2?.persistent === "boolean" ? body2.persistent : was?.p === 1) ? { p: 1 } : {};
2068
2589
  async function revoke(refreshToken) {
2069
- const form = new URLSearchParams({ client_id: clientId, token: refreshToken });
2070
- if (clientSecret) form.set("client_secret", clientSecret);
2590
+ const form = await clientAuth(new URLSearchParams({ client_id: clientId, token: refreshToken }));
2071
2591
  await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
2072
2592
  }
2073
2593
  const unverified = (u) => contract?.requireVerifiedEmail === true && !u.emailVerified;
@@ -2159,7 +2679,8 @@ function createCauthy(given = {}) {
2159
2679
  async function organizations(session) {
2160
2680
  const hit = orgCache.get(session.accessToken);
2161
2681
  if (hit && Date.now() - hit.at < ORGANIZATIONS_TTL_MS) return hit.value;
2162
- const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
2682
+ const sys = systemNow();
2683
+ const url = `${issuer}/v1/oauth/organizations${sys ? `?system=${encodeURIComponent(sys)}` : ""}`;
2163
2684
  const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
2164
2685
  if (!res.ok) throw new Error(`@cauthyhq/node: reading organizations: ${res.status}`);
2165
2686
  const body2 = await res.json();
@@ -2313,8 +2834,7 @@ function createCauthy(given = {}) {
2313
2834
  async function logout(request) {
2314
2835
  const { had, value } = await read(request);
2315
2836
  if (value !== null) {
2316
- const body2 = new URLSearchParams({ client_id: clientId, token: value.rt });
2317
- if (clientSecret) body2.set("client_secret", clientSecret);
2837
+ const body2 = await clientAuth(new URLSearchParams({ client_id: clientId, token: value.rt }));
2318
2838
  await doFetch(`${issuer}/v1/oauth/revoke`, {
2319
2839
  method: "POST",
2320
2840
  headers: { "content-type": "application/x-www-form-urlencoded" },
@@ -2438,7 +2958,7 @@ function createCauthy(given = {}) {
2438
2958
  const res = await doFetch(`${issuer}${path}`, {
2439
2959
  method,
2440
2960
  headers: {
2441
- "x-tenant-key": tenant,
2961
+ "x-tenant-key": tenantNow(),
2442
2962
  accept: "application/json",
2443
2963
  // A link Cauthy mails opens the application's page for it when it
2444
2964
  // has one; otherwise Cauthy's own. Either way in the reader's language.
@@ -2584,7 +3104,7 @@ function createCauthy(given = {}) {
2584
3104
  }
2585
3105
  if (route === "/sign-in/anonymous" || route === "/me/upgrade") deprecated(`POST ${route}`, "Guest sessions leave the auth-bff protocol in version 2.");
2586
3106
  if (route.startsWith("/me/upgrade/social/")) deprecated("GET /me/upgrade/social/{provider}", "Guest sessions leave the auth-bff protocol in version 2.");
2587
- if (tenant === "") {
3107
+ if (tenantNow() === "") {
2588
3108
  return problem(500, "not_configured", "@cauthyhq/node: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
2589
3109
  }
2590
3110
  if (route === "/look" && !post) {
@@ -2819,7 +3339,7 @@ function createCauthy(given = {}) {
2819
3339
  let upstream;
2820
3340
  try {
2821
3341
  upstream = await doFetch(`${issuer}/v1/auth/mfa/push/events`, {
2822
- headers: { "x-tenant-key": tenant, "x-mfa-token": step.token, accept: "text/event-stream" },
3342
+ headers: { "x-tenant-key": tenantNow(), "x-mfa-token": step.token, accept: "text/event-stream" },
2823
3343
  signal: request.signal
2824
3344
  });
2825
3345
  } catch {
@@ -2839,8 +3359,7 @@ function createCauthy(given = {}) {
2839
3359
  if (route === "/sign-out") {
2840
3360
  const { had, value } = await read(request);
2841
3361
  if (value !== null) {
2842
- const form = new URLSearchParams({ client_id: clientId, token: value.rt });
2843
- if (clientSecret) form.set("client_secret", clientSecret);
3362
+ const form = await clientAuth(new URLSearchParams({ client_id: clientId, token: value.rt }));
2844
3363
  await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
2845
3364
  orgCache.delete(value.at);
2846
3365
  }
@@ -3239,14 +3758,38 @@ function createCauthy(given = {}) {
3239
3758
  const inside = url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
3240
3759
  const route = inside ? withoutTrailingSlashes(url.pathname.slice(basePath.length)) || "/" : null;
3241
3760
  const published3 = pact !== void 0 && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD");
3761
+ if (identity && route !== null) {
3762
+ const unready = await identityGate();
3763
+ if (unready) return finish(unready, route);
3764
+ }
3242
3765
  try {
3243
3766
  return finish(await serve(request), route, published3);
3244
3767
  } catch (err) {
3768
+ if (err instanceof UnknownOriginError) return finish(problem(400, "unknown_origin", err.message), route);
3245
3769
  if (!(err instanceof CauthyApiError && err.code === "email_not_confirmed")) throw err;
3246
3770
  const wantsPage = request.method === "GET" && (request.headers.get("sec-fetch-mode") ?? "navigate") === "navigate";
3247
3771
  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);
3248
3772
  }
3249
3773
  }
3774
+ async function identityGate() {
3775
+ if (!identity) return null;
3776
+ const unavailable = () => problem(503, "unavailable", "Cauthy cannot be reached yet. Try again in a moment.");
3777
+ if (identity.failing()) return unavailable();
3778
+ let timer;
3779
+ const waited = new Promise((resolve3) => {
3780
+ timer = setTimeout(() => resolve3("timeout"), requestTimeout);
3781
+ });
3782
+ try {
3783
+ const ready = identity.ready();
3784
+ const got = await Promise.race([ready.then(() => "ok"), identity.attempt().then((ok) => ok ? "ok" : "failed"), waited]);
3785
+ if (got === "failed") await Promise.race([ready, Promise.resolve()]);
3786
+ return got === "ok" ? null : unavailable();
3787
+ } catch (e) {
3788
+ return e.code === "misconfigured" ? problem(503, "misconfigured", "Cauthy refuses this application's key: make a new one in Cauthy's dashboard.") : unavailable();
3789
+ } finally {
3790
+ clearTimeout(timer);
3791
+ }
3792
+ }
3250
3793
  async function serve(request) {
3251
3794
  const url = new URL(request.url);
3252
3795
  if (pact && url.pathname === contract?.jwksPath && (request.method === "GET" || request.method === "HEAD")) return pact.jwks();
@@ -3329,7 +3872,28 @@ function createCauthy(given = {}) {
3329
3872
  return { response: redirect(to) };
3330
3873
  }
3331
3874
  const replay = given.webhookReplay ?? memoryReplayStore();
3875
+ let lateSelf;
3332
3876
  const tokenVerifier = (o = {}) => {
3877
+ if (keyed) {
3878
+ if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
3879
+ throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
3880
+ }
3881
+ let built;
3882
+ return async (from) => {
3883
+ const id = await (identity ?? (lateSelf ??= createIdentity(selfOptions()))).ready();
3884
+ if (built?.from !== id) {
3885
+ built = { from: id, verify: createTokenVerifier({
3886
+ issuer,
3887
+ tenantId: contract?.tenantId ?? id.tenant.id,
3888
+ audience: id.audience,
3889
+ fetch: given.fetch,
3890
+ ...contract ? { clientId: contract.clientId } : {},
3891
+ ...o
3892
+ }) };
3893
+ }
3894
+ return built.verify(from);
3895
+ };
3896
+ }
3333
3897
  if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
3334
3898
  throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
3335
3899
  }
@@ -3343,7 +3907,30 @@ function createCauthy(given = {}) {
3343
3907
  });
3344
3908
  };
3345
3909
  const webhook = (request, o = {}) => verifyWebhook(request, { issuer, url: o.url ?? `${originOf(request)}${new URL(request.url).pathname}`, jwks: o.jwks, now: o.now, replay });
3346
- const pact = contract && ownKeys ? {
3910
+ async function keyJwks() {
3911
+ const s = await fromKey.signer;
3912
+ const keys2 = [{ ...s.publicJwk, kid: s.kid, alg: "EdDSA", use: "sig" }];
3913
+ if (fromKey.previousJwk) keys2.push({ ...await fromKey.previousJwk, alg: "EdDSA", use: "sig" });
3914
+ if (ownKeys) keys2.push(...(await ownKeys.published()).keys);
3915
+ return new Response(JSON.stringify({ keys: keys2 }), {
3916
+ headers: { "content-type": "application/json; charset=utf-8", "cache-control": `public, max-age=${JWKS_MAX_AGE_S}` }
3917
+ });
3918
+ }
3919
+ const noStore = () => {
3920
+ 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");
3921
+ };
3922
+ const keyOnly = {
3923
+ current: async () => noStore(),
3924
+ published: async () => ({ keys: [] }),
3925
+ rotate: async () => noStore(),
3926
+ reseal: async () => 0
3927
+ };
3928
+ const pact = contract && fromKey ? {
3929
+ contract,
3930
+ keys: ownKeys ?? keyOnly,
3931
+ jwks: keyJwks,
3932
+ status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
3933
+ } : contract && ownKeys ? {
3347
3934
  contract,
3348
3935
  keys: ownKeys,
3349
3936
  async jwks() {
@@ -3353,8 +3940,25 @@ function createCauthy(given = {}) {
3353
3940
  },
3354
3941
  status: () => ownSystem.request("GET", `/v1/pact/status?system=${encodeURIComponent(contract.system)}`)
3355
3942
  } : void 0;
3943
+ const configured = () => ({
3944
+ issuer,
3945
+ audience: given.audience ?? env3("CAUTHY_AUDIENCE") ?? "",
3946
+ tenant: { id: contract?.tenantId ?? given.tenantId ?? env3("CAUTHY_TENANT_ID") ?? "", slug: tenant },
3947
+ system,
3948
+ client_id: clientId,
3949
+ sign_in_client_id: clientId,
3950
+ app_urls: baseURL ? [withoutTrailingSlashes(baseURL)] : [],
3951
+ callback_path: `${basePath}/callback`,
3952
+ key: null,
3953
+ contract: contract ? { link: contract.link, system_proof: contract.systemProof, require_verified_email: contract.requireVerifiedEmail } : null
3954
+ });
3356
3955
  return {
3357
3956
  handler,
3957
+ ready: async () => {
3958
+ if (identity) return identity.ready();
3959
+ const id = configured();
3960
+ return fromKey ? { ...id, key: { kid: (await fromKey.signer).kid, retires_at: null } } : id;
3961
+ },
3358
3962
  checkMount: (target) => checkMount(basePath, target),
3359
3963
  webhook,
3360
3964
  tokenVerifier,
@@ -3445,10 +4049,12 @@ async function connectSystem(o) {
3445
4049
  0 && (module.exports = {
3446
4050
  CauthyApiError,
3447
4051
  CauthyConfigError,
4052
+ CauthyKeyError,
3448
4053
  CauthyTokenError,
3449
4054
  CauthyUnavailableError,
3450
4055
  CauthyWebhookError,
3451
4056
  DEFAULT_PASSWORD_POLICY,
4057
+ KEY_REPLACES,
3452
4058
  MIN_PASSWORD,
3453
4059
  PROTOCOL_VERSION,
3454
4060
  STANDARD_VARIABLES,
@@ -3458,15 +4064,18 @@ async function connectSystem(o) {
3458
4064
  checkName,
3459
4065
  checkNewPassword,
3460
4066
  checkPassword,
4067
+ composeKey,
3461
4068
  connectSystem,
3462
4069
  createCauthy,
3463
4070
  createSystem,
3464
4071
  createTokenVerifier,
4072
+ createVerifierFromEnv,
3465
4073
  definePolicy,
3466
4074
  describePasswordPolicy,
3467
4075
  explainPasswordProblems,
3468
4076
  failClosed,
3469
4077
  memoryReplayStore,
4078
+ parseKey,
3470
4079
  passwordPolicyOf,
3471
4080
  passwordProblems,
3472
4081
  policySource,