@palbase/backend 39.0.0 → 39.1.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.
@@ -1354,6 +1354,11 @@ var DeclarationRefused = class extends Error {
1354
1354
  };
1355
1355
 
1356
1356
  // src/engine/auth.ts
1357
+ var KEYSET_RETRY_INTERVAL_MS = 1e3;
1358
+ function authUnavailable() {
1359
+ return markEngineRaised(new HttpError(503, "auth_unavailable", "Authentication is temporarily unavailable"));
1360
+ }
1361
+ __name(authUnavailable, "authUnavailable");
1357
1362
  function b64urlToBytes(s) {
1358
1363
  const pad = s.replace(/-/g, "+").replace(/_/g, "/");
1359
1364
  const full = pad.padEnd(Math.ceil(pad.length / 4) * 4, "=");
@@ -1370,6 +1375,8 @@ var AuthVerifier = class {
1370
1375
  keys = /* @__PURE__ */ new Map();
1371
1376
  fetchedAt = 0;
1372
1377
  inflight = null;
1378
+ nextRefreshAt = 0;
1379
+ refreshFailed = false;
1373
1380
  jwksUrl;
1374
1381
  issuer;
1375
1382
  fetchImpl;
@@ -1380,54 +1387,65 @@ var AuthVerifier = class {
1380
1387
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
1381
1388
  this.ttl = opts.keysetTtlMs ?? 5 * 6e4;
1382
1389
  }
1383
- /** Fetch the keyset at most once per TTL, and at most once concurrently. */
1384
- async refresh() {
1390
+ /** Fetch once concurrently. Known keys use their trust TTL; misses and
1391
+ * failures share a short cooldown so arbitrary kids cannot force a fetch
1392
+ * per request, and recovery does not wait for an entire trust TTL. */
1393
+ refresh() {
1385
1394
  if (this.inflight) return this.inflight;
1386
- this.inflight = (async () => {
1387
- try {
1388
- const res = await this.fetchImpl(this.jwksUrl);
1389
- if (!res.ok) return;
1390
- const body = await res.json();
1391
- const next = /* @__PURE__ */ new Map();
1392
- for (const jwk of body.keys ?? []) {
1393
- if (jwk.kty !== "EC" || jwk.crv !== "P-256") continue;
1394
- try {
1395
- next.set(jwk.kid, await crypto.subtle.importKey("jwk", {
1396
- kty: "EC",
1397
- crv: jwk.crv,
1398
- x: jwk.x,
1399
- y: jwk.y,
1400
- ext: true
1401
- }, {
1402
- name: "ECDSA",
1403
- namedCurve: "P-256"
1404
- }, true, [
1405
- "verify"
1406
- ]));
1407
- } catch {
1408
- }
1409
- }
1410
- if (next.size > 0) {
1411
- this.keys = next;
1412
- this.fetchedAt = Date.now();
1395
+ this.inflight = this.fetchKeyset().finally(() => {
1396
+ this.nextRefreshAt = Date.now() + KEYSET_RETRY_INTERVAL_MS;
1397
+ this.inflight = null;
1398
+ });
1399
+ return this.inflight;
1400
+ }
1401
+ async fetchKeyset() {
1402
+ try {
1403
+ const res = await this.fetchImpl(this.jwksUrl);
1404
+ if (!res.ok) throw authUnavailable();
1405
+ const body = await res.json();
1406
+ if (!body || !Array.isArray(body.keys)) throw authUnavailable();
1407
+ const next = /* @__PURE__ */ new Map();
1408
+ for (const jwk of body.keys) {
1409
+ if (!jwk || jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.kid !== "string" || !jwk.kid) continue;
1410
+ try {
1411
+ next.set(jwk.kid, await crypto.subtle.importKey("jwk", {
1412
+ kty: "EC",
1413
+ crv: jwk.crv,
1414
+ x: jwk.x,
1415
+ y: jwk.y,
1416
+ ext: true
1417
+ }, {
1418
+ name: "ECDSA",
1419
+ namedCurve: "P-256"
1420
+ }, true, [
1421
+ "verify"
1422
+ ]));
1423
+ } catch {
1413
1424
  }
1414
- } finally {
1415
- this.inflight = null;
1416
1425
  }
1417
- })();
1418
- return this.inflight;
1426
+ if (next.size === 0) throw authUnavailable();
1427
+ this.keys = next;
1428
+ this.fetchedAt = Date.now();
1429
+ this.refreshFailed = false;
1430
+ } catch {
1431
+ this.refreshFailed = true;
1432
+ throw authUnavailable();
1433
+ }
1419
1434
  }
1420
1435
  async key(kid) {
1421
1436
  const stale = Date.now() - this.fetchedAt > this.ttl;
1422
- if (!this.keys.has(kid) || stale) await this.refresh();
1437
+ if (this.keys.has(kid) && !stale) return this.keys.get(kid);
1438
+ if (this.inflight || Date.now() >= this.nextRefreshAt) await this.refresh();
1439
+ else if (this.refreshFailed || stale) throw authUnavailable();
1423
1440
  return this.keys.get(kid) ?? null;
1424
1441
  }
1425
1442
  /**
1426
1443
  * Verify an `Authorization` header value.
1427
1444
  *
1428
1445
  * @returns the verified claims, or `null` for absent / malformed / expired /
1429
- * wrong-issuer / bad-signature. One `null` for every failure on purpose:
1430
- * the caller answers 401 either way, and a detailed reason is an oracle.
1446
+ * wrong-issuer / bad-signature. Credential refusals deliberately share one
1447
+ * result. An unavailable keyset throws 503 instead: no authentication
1448
+ * decision was possible, and the caller's session must not be invalidated.
1431
1449
  */
1432
1450
  async verify(authorization) {
1433
1451
  if (!authorization || !authorization.startsWith("Bearer ")) return null;
@@ -1445,7 +1463,10 @@ var AuthVerifier = class {
1445
1463
  } catch {
1446
1464
  return null;
1447
1465
  }
1448
- if (header.alg !== "ES256" || !header.kid) return null;
1466
+ if (!header || typeof header !== "object" || header.alg !== "ES256" || typeof header.kid !== "string" || !header.kid) return null;
1467
+ if (!claims || typeof claims !== "object" || Array.isArray(claims)) return null;
1468
+ if (typeof claims.exp === "number" && claims.exp * 1e3 <= Date.now()) return null;
1469
+ if (this.issuer && claims.iss !== this.issuer) return null;
1449
1470
  const key2 = await this.key(header.kid);
1450
1471
  if (!key2) return null;
1451
1472
  let ok = false;
@@ -1459,7 +1480,6 @@ var AuthVerifier = class {
1459
1480
  }
1460
1481
  if (!ok) return null;
1461
1482
  if (typeof claims.exp === "number" && claims.exp * 1e3 <= Date.now()) return null;
1462
- if (this.issuer && claims.iss !== this.issuer) return null;
1463
1483
  return claims;
1464
1484
  }
1465
1485
  };
@@ -6807,7 +6827,12 @@ async function createApp(opts) {
6807
6827
  }
6808
6828
  const target = matchRoute(routes, body.method ?? "POST", body.path);
6809
6829
  const spec = effectiveAuth(target?.entry.meta.options?.auth, target?.entry.controllerAuth);
6810
- const callerClaims = await auth.verify(req.headers.get("authorization"));
6830
+ let callerClaims;
6831
+ try {
6832
+ callerClaims = await auth.verify(req.headers.get("authorization"));
6833
+ } catch (err) {
6834
+ return errorResponse(err, "upload authentication", requestId);
6835
+ }
6811
6836
  if (spec.required && !callerClaims) {
6812
6837
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
6813
6838
  }
@@ -6857,7 +6882,12 @@ async function createApp(opts) {
6857
6882
  let attestedDevice = null;
6858
6883
  let attestChallengeOut = null;
6859
6884
  const spec = effectiveAuth(meta.options?.auth, hit.entry.controllerAuth);
6860
- const claims = await auth.verify(req.headers.get("authorization"));
6885
+ let claims;
6886
+ try {
6887
+ claims = await auth.verify(req.headers.get("authorization"));
6888
+ } catch (err) {
6889
+ return errorResponse(err, "authentication", requestId);
6890
+ }
6861
6891
  if (spec.required && !claims) {
6862
6892
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
6863
6893
  }