@palbase/backend 39.1.0 → 39.1.2

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.
@@ -1398,6 +1398,11 @@ function isDeclarationRefused(e) {
1398
1398
  __name(isDeclarationRefused, "isDeclarationRefused");
1399
1399
 
1400
1400
  // src/engine/auth.ts
1401
+ var KEYSET_RETRY_INTERVAL_MS = 1e3;
1402
+ function authUnavailable() {
1403
+ return markEngineRaised(new HttpError(503, "auth_unavailable", "Authentication is temporarily unavailable"));
1404
+ }
1405
+ __name(authUnavailable, "authUnavailable");
1401
1406
  function b64urlToBytes(s) {
1402
1407
  const pad = s.replace(/-/g, "+").replace(/_/g, "/");
1403
1408
  const full = pad.padEnd(Math.ceil(pad.length / 4) * 4, "=");
@@ -1414,6 +1419,8 @@ var AuthVerifier = class {
1414
1419
  keys = /* @__PURE__ */ new Map();
1415
1420
  fetchedAt = 0;
1416
1421
  inflight = null;
1422
+ nextRefreshAt = 0;
1423
+ refreshFailed = false;
1417
1424
  jwksUrl;
1418
1425
  issuer;
1419
1426
  fetchImpl;
@@ -1424,54 +1431,65 @@ var AuthVerifier = class {
1424
1431
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
1425
1432
  this.ttl = opts.keysetTtlMs ?? 5 * 6e4;
1426
1433
  }
1427
- /** Fetch the keyset at most once per TTL, and at most once concurrently. */
1428
- async refresh() {
1434
+ /** Fetch once concurrently. Known keys use their trust TTL; misses and
1435
+ * failures share a short cooldown so arbitrary kids cannot force a fetch
1436
+ * per request, and recovery does not wait for an entire trust TTL. */
1437
+ refresh() {
1429
1438
  if (this.inflight) return this.inflight;
1430
- this.inflight = (async () => {
1431
- try {
1432
- const res = await this.fetchImpl(this.jwksUrl);
1433
- if (!res.ok) return;
1434
- const body = await res.json();
1435
- const next = /* @__PURE__ */ new Map();
1436
- for (const jwk of body.keys ?? []) {
1437
- if (jwk.kty !== "EC" || jwk.crv !== "P-256") continue;
1438
- try {
1439
- next.set(jwk.kid, await crypto.subtle.importKey("jwk", {
1440
- kty: "EC",
1441
- crv: jwk.crv,
1442
- x: jwk.x,
1443
- y: jwk.y,
1444
- ext: true
1445
- }, {
1446
- name: "ECDSA",
1447
- namedCurve: "P-256"
1448
- }, true, [
1449
- "verify"
1450
- ]));
1451
- } catch {
1452
- }
1453
- }
1454
- if (next.size > 0) {
1455
- this.keys = next;
1456
- this.fetchedAt = Date.now();
1439
+ this.inflight = this.fetchKeyset().finally(() => {
1440
+ this.nextRefreshAt = Date.now() + KEYSET_RETRY_INTERVAL_MS;
1441
+ this.inflight = null;
1442
+ });
1443
+ return this.inflight;
1444
+ }
1445
+ async fetchKeyset() {
1446
+ try {
1447
+ const res = await this.fetchImpl(this.jwksUrl);
1448
+ if (!res.ok) throw authUnavailable();
1449
+ const body = await res.json();
1450
+ if (!body || !Array.isArray(body.keys)) throw authUnavailable();
1451
+ const next = /* @__PURE__ */ new Map();
1452
+ for (const jwk of body.keys) {
1453
+ if (!jwk || jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.kid !== "string" || !jwk.kid) continue;
1454
+ try {
1455
+ next.set(jwk.kid, await crypto.subtle.importKey("jwk", {
1456
+ kty: "EC",
1457
+ crv: jwk.crv,
1458
+ x: jwk.x,
1459
+ y: jwk.y,
1460
+ ext: true
1461
+ }, {
1462
+ name: "ECDSA",
1463
+ namedCurve: "P-256"
1464
+ }, true, [
1465
+ "verify"
1466
+ ]));
1467
+ } catch {
1457
1468
  }
1458
- } finally {
1459
- this.inflight = null;
1460
1469
  }
1461
- })();
1462
- return this.inflight;
1470
+ if (next.size === 0) throw authUnavailable();
1471
+ this.keys = next;
1472
+ this.fetchedAt = Date.now();
1473
+ this.refreshFailed = false;
1474
+ } catch {
1475
+ this.refreshFailed = true;
1476
+ throw authUnavailable();
1477
+ }
1463
1478
  }
1464
1479
  async key(kid) {
1465
1480
  const stale = Date.now() - this.fetchedAt > this.ttl;
1466
- if (!this.keys.has(kid) || stale) await this.refresh();
1481
+ if (this.keys.has(kid) && !stale) return this.keys.get(kid);
1482
+ if (this.inflight || Date.now() >= this.nextRefreshAt) await this.refresh();
1483
+ else if (this.refreshFailed || stale) throw authUnavailable();
1467
1484
  return this.keys.get(kid) ?? null;
1468
1485
  }
1469
1486
  /**
1470
1487
  * Verify an `Authorization` header value.
1471
1488
  *
1472
1489
  * @returns the verified claims, or `null` for absent / malformed / expired /
1473
- * wrong-issuer / bad-signature. One `null` for every failure on purpose:
1474
- * the caller answers 401 either way, and a detailed reason is an oracle.
1490
+ * wrong-issuer / bad-signature. Credential refusals deliberately share one
1491
+ * result. An unavailable keyset throws 503 instead: no authentication
1492
+ * decision was possible, and the caller's session must not be invalidated.
1475
1493
  */
1476
1494
  async verify(authorization) {
1477
1495
  if (!authorization || !authorization.startsWith("Bearer ")) return null;
@@ -1489,7 +1507,10 @@ var AuthVerifier = class {
1489
1507
  } catch {
1490
1508
  return null;
1491
1509
  }
1492
- if (header.alg !== "ES256" || !header.kid) return null;
1510
+ if (!header || typeof header !== "object" || header.alg !== "ES256" || typeof header.kid !== "string" || !header.kid) return null;
1511
+ if (!claims || typeof claims !== "object" || Array.isArray(claims)) return null;
1512
+ if (typeof claims.exp === "number" && claims.exp * 1e3 <= Date.now()) return null;
1513
+ if (this.issuer && claims.iss !== this.issuer) return null;
1493
1514
  const key2 = await this.key(header.kid);
1494
1515
  if (!key2) return null;
1495
1516
  let ok = false;
@@ -1503,7 +1524,6 @@ var AuthVerifier = class {
1503
1524
  }
1504
1525
  if (!ok) return null;
1505
1526
  if (typeof claims.exp === "number" && claims.exp * 1e3 <= Date.now()) return null;
1506
- if (this.issuer && claims.iss !== this.issuer) return null;
1507
1527
  return claims;
1508
1528
  }
1509
1529
  };
@@ -6905,7 +6925,12 @@ async function createApp(opts) {
6905
6925
  }
6906
6926
  const target = matchRoute(routes, body.method ?? "POST", body.path);
6907
6927
  const spec = effectiveAuth(target?.entry.meta.options?.auth, target?.entry.controllerAuth);
6908
- const callerClaims = await auth.verify(req.headers.get("authorization"));
6928
+ let callerClaims;
6929
+ try {
6930
+ callerClaims = await auth.verify(req.headers.get("authorization"));
6931
+ } catch (err) {
6932
+ return errorResponse(err, "upload authentication", requestId);
6933
+ }
6909
6934
  if (spec.required && !callerClaims) {
6910
6935
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
6911
6936
  }
@@ -6955,7 +6980,12 @@ async function createApp(opts) {
6955
6980
  let attestedDevice = null;
6956
6981
  let attestChallengeOut = null;
6957
6982
  const spec = effectiveAuth(meta.options?.auth, hit.entry.controllerAuth);
6958
- const claims = await auth.verify(req.headers.get("authorization"));
6983
+ let claims;
6984
+ try {
6985
+ claims = await auth.verify(req.headers.get("authorization"));
6986
+ } catch (err) {
6987
+ return errorResponse(err, "authentication", requestId);
6988
+ }
6959
6989
  if (spec.required && !claims) {
6960
6990
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
6961
6991
  }