@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.
@@ -3,7 +3,7 @@ import {
3
3
  BootRefused,
4
4
  createApp,
5
5
  loadConfig
6
- } from "../chunk-JJSR4BUX.js";
6
+ } from "../chunk-C525N4OW.js";
7
7
  import "../chunk-H7EKL6HC.js";
8
8
  import "../chunk-GHNC2UHS.js";
9
9
  import "../chunk-XZWOMPD3.js";
@@ -148,6 +148,11 @@ ${detail}`);
148
148
  __name(loadConfig, "loadConfig");
149
149
 
150
150
  // src/engine/auth.ts
151
+ var KEYSET_RETRY_INTERVAL_MS = 1e3;
152
+ function authUnavailable() {
153
+ return markEngineRaised(new HttpError(503, "auth_unavailable", "Authentication is temporarily unavailable"));
154
+ }
155
+ __name(authUnavailable, "authUnavailable");
151
156
  function b64urlToBytes(s) {
152
157
  const pad = s.replace(/-/g, "+").replace(/_/g, "/");
153
158
  const full = pad.padEnd(Math.ceil(pad.length / 4) * 4, "=");
@@ -164,6 +169,8 @@ var AuthVerifier = class {
164
169
  keys = /* @__PURE__ */ new Map();
165
170
  fetchedAt = 0;
166
171
  inflight = null;
172
+ nextRefreshAt = 0;
173
+ refreshFailed = false;
167
174
  jwksUrl;
168
175
  issuer;
169
176
  fetchImpl;
@@ -174,54 +181,65 @@ var AuthVerifier = class {
174
181
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
175
182
  this.ttl = opts.keysetTtlMs ?? 5 * 6e4;
176
183
  }
177
- /** Fetch the keyset at most once per TTL, and at most once concurrently. */
178
- async refresh() {
184
+ /** Fetch once concurrently. Known keys use their trust TTL; misses and
185
+ * failures share a short cooldown so arbitrary kids cannot force a fetch
186
+ * per request, and recovery does not wait for an entire trust TTL. */
187
+ refresh() {
179
188
  if (this.inflight) return this.inflight;
180
- this.inflight = (async () => {
181
- try {
182
- const res = await this.fetchImpl(this.jwksUrl);
183
- if (!res.ok) return;
184
- const body = await res.json();
185
- const next = /* @__PURE__ */ new Map();
186
- for (const jwk of body.keys ?? []) {
187
- if (jwk.kty !== "EC" || jwk.crv !== "P-256") continue;
188
- try {
189
- next.set(jwk.kid, await crypto.subtle.importKey("jwk", {
190
- kty: "EC",
191
- crv: jwk.crv,
192
- x: jwk.x,
193
- y: jwk.y,
194
- ext: true
195
- }, {
196
- name: "ECDSA",
197
- namedCurve: "P-256"
198
- }, true, [
199
- "verify"
200
- ]));
201
- } catch {
202
- }
203
- }
204
- if (next.size > 0) {
205
- this.keys = next;
206
- this.fetchedAt = Date.now();
189
+ this.inflight = this.fetchKeyset().finally(() => {
190
+ this.nextRefreshAt = Date.now() + KEYSET_RETRY_INTERVAL_MS;
191
+ this.inflight = null;
192
+ });
193
+ return this.inflight;
194
+ }
195
+ async fetchKeyset() {
196
+ try {
197
+ const res = await this.fetchImpl(this.jwksUrl);
198
+ if (!res.ok) throw authUnavailable();
199
+ const body = await res.json();
200
+ if (!body || !Array.isArray(body.keys)) throw authUnavailable();
201
+ const next = /* @__PURE__ */ new Map();
202
+ for (const jwk of body.keys) {
203
+ if (!jwk || jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.kid !== "string" || !jwk.kid) continue;
204
+ try {
205
+ next.set(jwk.kid, await crypto.subtle.importKey("jwk", {
206
+ kty: "EC",
207
+ crv: jwk.crv,
208
+ x: jwk.x,
209
+ y: jwk.y,
210
+ ext: true
211
+ }, {
212
+ name: "ECDSA",
213
+ namedCurve: "P-256"
214
+ }, true, [
215
+ "verify"
216
+ ]));
217
+ } catch {
207
218
  }
208
- } finally {
209
- this.inflight = null;
210
219
  }
211
- })();
212
- return this.inflight;
220
+ if (next.size === 0) throw authUnavailable();
221
+ this.keys = next;
222
+ this.fetchedAt = Date.now();
223
+ this.refreshFailed = false;
224
+ } catch {
225
+ this.refreshFailed = true;
226
+ throw authUnavailable();
227
+ }
213
228
  }
214
229
  async key(kid) {
215
230
  const stale = Date.now() - this.fetchedAt > this.ttl;
216
- if (!this.keys.has(kid) || stale) await this.refresh();
231
+ if (this.keys.has(kid) && !stale) return this.keys.get(kid);
232
+ if (this.inflight || Date.now() >= this.nextRefreshAt) await this.refresh();
233
+ else if (this.refreshFailed || stale) throw authUnavailable();
217
234
  return this.keys.get(kid) ?? null;
218
235
  }
219
236
  /**
220
237
  * Verify an `Authorization` header value.
221
238
  *
222
239
  * @returns the verified claims, or `null` for absent / malformed / expired /
223
- * wrong-issuer / bad-signature. One `null` for every failure on purpose:
224
- * the caller answers 401 either way, and a detailed reason is an oracle.
240
+ * wrong-issuer / bad-signature. Credential refusals deliberately share one
241
+ * result. An unavailable keyset throws 503 instead: no authentication
242
+ * decision was possible, and the caller's session must not be invalidated.
225
243
  */
226
244
  async verify(authorization) {
227
245
  if (!authorization || !authorization.startsWith("Bearer ")) return null;
@@ -239,7 +257,10 @@ var AuthVerifier = class {
239
257
  } catch {
240
258
  return null;
241
259
  }
242
- if (header.alg !== "ES256" || !header.kid) return null;
260
+ if (!header || typeof header !== "object" || header.alg !== "ES256" || typeof header.kid !== "string" || !header.kid) return null;
261
+ if (!claims || typeof claims !== "object" || Array.isArray(claims)) return null;
262
+ if (typeof claims.exp === "number" && claims.exp * 1e3 <= Date.now()) return null;
263
+ if (this.issuer && claims.iss !== this.issuer) return null;
243
264
  const key2 = await this.key(header.kid);
244
265
  if (!key2) return null;
245
266
  let ok = false;
@@ -253,7 +274,6 @@ var AuthVerifier = class {
253
274
  }
254
275
  if (!ok) return null;
255
276
  if (typeof claims.exp === "number" && claims.exp * 1e3 <= Date.now()) return null;
256
- if (this.issuer && claims.iss !== this.issuer) return null;
257
277
  return claims;
258
278
  }
259
279
  };
@@ -5042,7 +5062,12 @@ async function createApp(opts) {
5042
5062
  }
5043
5063
  const target = matchRoute(routes, body.method ?? "POST", body.path);
5044
5064
  const spec = effectiveAuth(target?.entry.meta.options?.auth, target?.entry.controllerAuth);
5045
- const callerClaims = await auth.verify(req.headers.get("authorization"));
5065
+ let callerClaims;
5066
+ try {
5067
+ callerClaims = await auth.verify(req.headers.get("authorization"));
5068
+ } catch (err) {
5069
+ return errorResponse(err, "upload authentication", requestId);
5070
+ }
5046
5071
  if (spec.required && !callerClaims) {
5047
5072
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
5048
5073
  }
@@ -5092,7 +5117,12 @@ async function createApp(opts) {
5092
5117
  let attestedDevice = null;
5093
5118
  let attestChallengeOut = null;
5094
5119
  const spec = effectiveAuth(meta.options?.auth, hit.entry.controllerAuth);
5095
- const claims = await auth.verify(req.headers.get("authorization"));
5120
+ let claims;
5121
+ try {
5122
+ claims = await auth.verify(req.headers.get("authorization"));
5123
+ } catch (err) {
5124
+ return errorResponse(err, "authentication", requestId);
5125
+ }
5096
5126
  if (spec.required && !claims) {
5097
5127
  return envelope("unauthorized", "A valid access token is required", 401, requestId);
5098
5128
  }
@@ -5563,4 +5593,4 @@ export {
5563
5593
  installEgressFence,
5564
5594
  createApp
5565
5595
  };
5566
- //# sourceMappingURL=chunk-JJSR4BUX.js.map
5596
+ //# sourceMappingURL=chunk-C525N4OW.js.map