@palbase/backend 39.1.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.
- package/dist/bin/palbase-backend.cjs +70 -40
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +1 -1
- package/dist/{chunk-JJSR4BUX.js → chunk-C525N4OW.js} +71 -41
- package/dist/chunk-C525N4OW.js.map +1 -0
- package/dist/engine/index.cjs +70 -40
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +1 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/{index-B0mjUnxy.d.cts → index-Brhp8cxj.d.cts} +9 -15
- package/dist/{index-yDno9Ju9.d.ts → index-Dlu2b9-f.d.ts} +9 -15
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/docs/auth.md +20 -0
- package/docs/llms-full.txt +20 -0
- package/package.json +1 -1
- package/dist/chunk-JJSR4BUX.js.map +0 -1
|
@@ -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
|
|
178
|
-
|
|
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 = (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
224
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
5596
|
+
//# sourceMappingURL=chunk-C525N4OW.js.map
|