@cauthyhq/node 1.1.0 → 1.2.1

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