sporades 0.9.42 → 0.9.43

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/CHANGES.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changes
2
2
 
3
+ ## v0.9.43 - 2026-11-01
4
+
5
+ Changes since v0.9.42.
6
+
7
+ ### 🚀 Features
8
+
9
+ - Allow registry propagation before verifying npm releases (#324) (15bdab53).
10
+
11
+ ### 🐛 Bug Fixes
12
+
13
+ - Fix dependency audit vulnerabilities (#328) (cd70c8de).
14
+
15
+ ### 🔧 Improvements
16
+
17
+ - Regenerate dist and bin [skip ci] (de2841c9).
18
+ - Regenerate dist and bin [skip ci] (57a49c25).
19
+
20
+ ### 📝 Documentation
21
+
22
+ - Preserve endpoint security on rollback and record ingress refusals (#327) (1a7e2185).
23
+ - Trigger npm releases from labelled Release issues (#325) (83a9c292).
24
+
3
25
  ## v0.9.42 - 2026-10-30
4
26
 
5
27
  Changes since v0.9.41.
@@ -46189,7 +46189,7 @@ var mutationExecution = new MutationExecutionStorage();
46189
46189
  init_cli_support();
46190
46190
 
46191
46191
  // src/cli/cli-version.ts
46192
- var CLI_VERSION = "0.9.42";
46192
+ var CLI_VERSION = "0.9.43";
46193
46193
 
46194
46194
  // src/cli/schedule-inspection-envelope.ts
46195
46195
  var SCHEDULE_DIAGNOSTIC_FIELDS = /* @__PURE__ */ new Set([
package/bin/sporades.js CHANGED
@@ -95491,6 +95491,38 @@ function flushTeamSecurityEvents(database, context2, options = {}) {
95491
95491
  }
95492
95492
  }
95493
95493
 
95494
+ // src/endpoint-error.ts
95495
+ function endpointError(headers) {
95496
+ const invalid4 = () => new Error("Invalid endpoint error headers.");
95497
+ if (!headers || typeof headers !== "object" || Array.isArray(headers) || ![Object.prototype, null].includes(Object.getPrototypeOf(headers))) throw invalid4();
95498
+ const result = {};
95499
+ let size = 0;
95500
+ for (const key of Reflect.ownKeys(headers)) {
95501
+ if (typeof key !== "string") throw invalid4();
95502
+ const name2 = key.toLowerCase();
95503
+ if (!["cache-control", "pragma", "x-robots-tag", "content-security-policy", "referrer-policy"].includes(name2) || Object.hasOwn(result, name2)) throw invalid4();
95504
+ const property = Object.getOwnPropertyDescriptor(headers, key);
95505
+ const value = property?.value;
95506
+ if (!property || !Object.hasOwn(property, "value") || typeof value !== "string" || !value.trim() || /[^\x20-\x7e]/.test(value)) throw invalid4();
95507
+ size += name2.length + value.length;
95508
+ if (size > 8192) throw invalid4();
95509
+ result[name2] = value;
95510
+ }
95511
+ const error = new Error("Endpoint handler failed.");
95512
+ Object.defineProperty(error, Symbol.for("sporades.endpoint.errorHeaders"), { value: Object.freeze(result) });
95513
+ return error;
95514
+ }
95515
+ function endpointErrorResponseHeaders(error) {
95516
+ try {
95517
+ const descriptor = error && typeof error === "object" ? Object.getOwnPropertyDescriptor(error, Symbol.for("sporades.endpoint.errorHeaders")) : void 0;
95518
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) return {};
95519
+ const checked = endpointError(descriptor.value);
95520
+ return checked[Symbol.for("sporades.endpoint.errorHeaders")];
95521
+ } catch {
95522
+ return {};
95523
+ }
95524
+ }
95525
+
95494
95526
  // src/http-runtime.ts
95495
95527
  import { ServerResponse } from "node:http";
95496
95528
  import { randomBytes as randomBytes6 } from "node:crypto";
@@ -129726,8 +129758,9 @@ function prepareHttpSecurity(database, request, response, admitPreflight) {
129726
129758
  response.writeHead = ((statusCode, statusMessageOrHeaders, maybeHeaders) => {
129727
129759
  const statusMessage = typeof statusMessageOrHeaders === "string" ? statusMessageOrHeaders : void 0;
129728
129760
  const inputHeaders = statusMessage ? maybeHeaders : typeof statusMessageOrHeaders === "string" ? {} : statusMessageOrHeaders;
129761
+ const supplied = sanitizeResponseHeaders({ ...response.getHeaders?.() ?? {}, ...inputHeaders });
129729
129762
  const headers = {
129730
- ...sanitizeResponseHeaders(inputHeaders ?? {}),
129763
+ ...supplied,
129731
129764
  "x-content-type-options": "nosniff",
129732
129765
  "referrer-policy": "no-referrer",
129733
129766
  "x-frame-options": "DENY",
@@ -129736,6 +129769,11 @@ function prepareHttpSecurity(database, request, response, admitPreflight) {
129736
129769
  [policy.csp.header]: serializeCspDirectives(directives),
129737
129770
  ...policy.hsts ? { "strict-transport-security": policy.hsts } : {}
129738
129771
  };
129772
+ const suppliedCsp = supplied[policy.csp.header];
129773
+ if (suppliedCsp) headers[policy.csp.header] = [
129774
+ ...Array.isArray(suppliedCsp) ? suppliedCsp.map(String) : [String(suppliedCsp)],
129775
+ serializeCspDirectives(directives)
129776
+ ];
129739
129777
  if (isGuardedAttachmentHttpResponse(response)) {
129740
129778
  delete headers["content-security-policy-report-only"];
129741
129779
  delete headers["access-control-allow-origin"];
@@ -130015,7 +130053,7 @@ function sanitizeResponseHeaders(headers) {
130015
130053
  [...entries].filter(([name2]) => {
130016
130054
  const normalized = String(name2).toLowerCase();
130017
130055
  return normalized !== "x-powered-by" && normalized !== "server";
130018
- })
130056
+ }).map(([name2, value]) => [String(name2).toLowerCase(), value])
130019
130057
  );
130020
130058
  }
130021
130059
  async function handleFileHttpRoute(database, request, response, websocketHub = null) {
@@ -130282,7 +130320,7 @@ function mergeEndpointResponseHeaders(handlerHeaders, runtimeHeaders) {
130282
130320
  return { ...headers, ...runtimeHeaders };
130283
130321
  }
130284
130322
  function writeEndpointError(response, error) {
130285
- const headers = { "content-type": "application/json; charset=utf-8" };
130323
+ const headers = { ...endpointErrorResponseHeaders(error), "content-type": "application/json; charset=utf-8" };
130286
130324
  if (error?.code === "UNAUTHENTICATED" && error?.sporadesAccessKeyFailure) {
130287
130325
  headers["www-authenticate"] = error?.sporadesAccessKeyFailure === "invalid" && error?.sporadesAccessKeyReason !== "missing" ? 'Bearer realm="sporades", error="invalid_token"' : 'Bearer realm="sporades"';
130288
130326
  }
@@ -130290,6 +130328,10 @@ function writeEndpointError(response, error) {
130290
130328
  headers["cache-control"] = "no-store";
130291
130329
  headers.pragma = "no-cache";
130292
130330
  }
130331
+ if (String(response.getHeader?.("cache-control") ?? "").includes("no-store")) {
130332
+ headers["cache-control"] = response.getHeader("cache-control");
130333
+ headers.pragma = "no-cache";
130334
+ }
130293
130335
  const multipartDetails = safeMultipartLimitDetails(error);
130294
130336
  response.writeHead(endpointErrorStatus(error), headers);
130295
130337
  response.end(
@@ -130314,6 +130356,7 @@ function safeMultipartLimitDetails(error) {
130314
130356
  return { partType: details.partType, limitKind: details.limitKind, limit: details.limit };
130315
130357
  }
130316
130358
  function endpointErrorStatus(error) {
130359
+ if (error?.code === "FILE_INGRESS_REFUSAL_FAILED") return 503;
130317
130360
  if (error?.code === "UNAUTHENTICATED") return 401;
130318
130361
  if (error?.code === "FORBIDDEN") return 403;
130319
130362
  if (error?.code === "RATE_LIMITED") return 429;
@@ -142565,6 +142608,7 @@ function privilegedOnlyTableAcl() {
142565
142608
  }
142566
142609
  function serverRuntimeModuleSource() {
142567
142610
  return `export { escapeHtml, html } from "sporades:html";
142611
+ export const endpointError = ${endpointError.toString()};
142568
142612
  const validateEndpointSessionDeclaration = ${validateEndpointSessionDeclaration.toString()};
142569
142613
  const AUTH_REQUIREMENTS = Symbol.for("sporades.auth.requirements");
142570
142614
  const ATOMIC_STRIPE_EVENT_DEFINITION = Symbol.for("sporades.stripeEvent.atomicDefinition");
@@ -145115,6 +145159,49 @@ function restartPolicyStatus(mode, overrides2 = {}) {
145115
145159
  };
145116
145160
  }
145117
145161
 
145162
+ // src/file-ingress-refusal.ts
145163
+ function snapshotIngressDecision(value) {
145164
+ let nodes = 0;
145165
+ let bytes = 0;
145166
+ const invalid4 = () => {
145167
+ throw new Error("Invalid File ingress decision.");
145168
+ };
145169
+ const visit = (input, depth2) => {
145170
+ if (++nodes > 1024 || depth2 > 16) return invalid4();
145171
+ if (input === null || typeof input === "boolean") return input;
145172
+ if (typeof input === "number") return Number.isFinite(input) ? input : invalid4();
145173
+ if (typeof input === "string") {
145174
+ bytes += Buffer.byteLength(input, "utf8");
145175
+ return bytes <= 8192 ? input : invalid4();
145176
+ }
145177
+ if (!input || typeof input !== "object" || !Array.isArray(input) && ![Object.prototype, null].includes(Object.getPrototypeOf(input))) return invalid4();
145178
+ const output = Array.isArray(input) ? [] : /* @__PURE__ */ Object.create(null);
145179
+ for (const key of Reflect.ownKeys(input)) {
145180
+ if (Array.isArray(input) && key === "length") continue;
145181
+ if (typeof key !== "string") return invalid4();
145182
+ bytes += Buffer.byteLength(key, "utf8");
145183
+ if (bytes > 8192) return invalid4();
145184
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
145185
+ if (!descriptor || !Object.hasOwn(descriptor, "value") || !descriptor.enumerable) return invalid4();
145186
+ if (depth2 === 0 && (key === "allowFiles" || key === "refusalJob") && descriptor.value === void 0) continue;
145187
+ if (Array.isArray(input) && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= 1024)) return invalid4();
145188
+ Object.defineProperty(output, key, { value: visit(descriptor.value, depth2 + 1), enumerable: true });
145189
+ }
145190
+ if (Array.isArray(input) && (input.length > 1024 || Object.keys(output).length !== input.length)) return invalid4();
145191
+ return Object.freeze(output);
145192
+ };
145193
+ const result = visit(value, 0);
145194
+ if (Buffer.byteLength(JSON.stringify(result) ?? "", "utf8") > 8192) invalid4();
145195
+ return result;
145196
+ }
145197
+ function ingressRefusalJob(decision) {
145198
+ if (!decision || decision.allow !== false || Object.keys(decision).some((key) => !["allow", "refusalJob"].includes(key))) throw new Error("Invalid File ingress refusal.");
145199
+ if (!Object.hasOwn(decision, "refusalJob")) return null;
145200
+ const job = decision.refusalJob;
145201
+ if (!job || typeof job !== "object" || Array.isArray(job) || Object.keys(job).length !== 3 || Object.keys(job).some((key) => !["name", "payload", "idempotencyKey"].includes(key)) || typeof job.name !== "string" || !/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(job.name) || typeof job.idempotencyKey !== "string" || !job.idempotencyKey || Buffer.byteLength(job.idempotencyKey, "utf8") > 256 || /[\x00-\x1f\x7f]/.test(job.idempotencyKey) || !Object.hasOwn(job, "payload") || Buffer.byteLength(JSON.stringify(job.payload), "utf8") > 4096) throw new Error("Invalid File ingress refusal.");
145202
+ return job;
145203
+ }
145204
+
145118
145205
  // src/services-context.ts
145119
145206
  import { inspect } from "node:util";
145120
145207
  var hidden = { toJSON: { value: () => void 0 }, [inspect.custom]: { value: () => "[Capsule services]" } };
@@ -151471,25 +151558,47 @@ function multipartAdmissionFilePermission(decision, principalMode = false) {
151471
151558
  if (keys.some((key) => key !== "allow" && key !== "allowFiles" && !(principalMode && key === "principal")) || !allow || !Object.prototype.hasOwnProperty.call(allow, "value") || allow.value !== true || principalMode && (!principal || !Object.prototype.hasOwnProperty.call(principal, "value")) || allowFiles && (!Object.prototype.hasOwnProperty.call(allowFiles, "value") || allowFiles.value !== void 0 && typeof allowFiles.value !== "boolean")) throw multipartAdmissionDenied();
151472
151559
  return allowFiles?.value !== false;
151473
151560
  }
151474
- async function admitCapsuleIngressPrincipal(database, endpoint, endpointRequest, signal) {
151561
+ async function admitCapsuleIngressPrincipal(database, endpoint, endpointRequest, requestSignal) {
151562
+ const signal = requestSignal ? AbortSignal.any([requestSignal, AbortSignal.timeout(5e3)]) : AbortSignal.timeout(5e3);
151475
151563
  const definition = database.fileIngressDefinition;
151476
151564
  if (!definition) throw commandError("Unauthenticated.", "Provide valid ingress authority and retry.", "UNAUTHENTICATED");
151477
- const decision = await database.adapter.withTransaction((transaction) => withTrustedRead(database, {
151478
- transaction,
151479
- purpose: "files.ingress-admission",
151480
- subject: { method: endpoint.options.method, path: endpoint.options.path },
151481
- signal
151482
- }, (db, assertActive) => definition.admit(Object.freeze({
151483
- db,
151484
- app: database.app,
151485
- env: database.serverEnv,
151486
- signal,
151487
- rateLimit: (key2, options) => {
151565
+ let decision;
151566
+ try {
151567
+ decision = await database.adapter.withTransaction((transaction) => withTrustedRead(database, {
151568
+ transaction,
151569
+ purpose: "files.ingress-admission",
151570
+ subject: { method: endpoint.options.method, path: endpoint.options.path },
151571
+ signal
151572
+ }, async (db, assertActive) => {
151573
+ const request = Object.freeze({ method: endpointRequest.method, path: endpointRequest.path, headers: Object.freeze({ ...endpointRequest.headers }), query: Object.freeze({ ...endpointRequest.query }) });
151574
+ const result = await raceIngressAdmission(() => definition.admit(Object.freeze({
151575
+ db,
151576
+ app: database.app,
151577
+ env: database.serverEnv,
151578
+ signal,
151579
+ request,
151580
+ rateLimit: (key2, options) => {
151581
+ assertActive();
151582
+ return database.capsuleRateLimiter.consume(key2, options);
151583
+ }
151584
+ }), request), signal);
151488
151585
  assertActive();
151489
- return database.capsuleRateLimiter.consume(key2, options);
151490
- },
151491
- request: Object.freeze({ method: endpointRequest.method, path: endpointRequest.path, headers: Object.freeze({ ...endpointRequest.headers }), query: Object.freeze({ ...endpointRequest.query }) })
151492
- }), Object.freeze({ method: endpointRequest.method, path: endpointRequest.path, headers: Object.freeze({ ...endpointRequest.headers }), query: Object.freeze({ ...endpointRequest.query }) }))));
151586
+ return snapshotIngressDecision(result);
151587
+ }), { signal });
151588
+ if (signal?.aborted) throw new Error("Aborted");
151589
+ } catch {
151590
+ throw capsuleIngressDenied();
151591
+ }
151592
+ if (decision?.allow === false) {
151593
+ let refusal;
151594
+ try {
151595
+ refusal = ingressRefusalJob(decision);
151596
+ } catch {
151597
+ throw capsuleIngressDenied();
151598
+ }
151599
+ if (refusal) await enqueueCapsuleIngressRefusal(database, endpointRequest, refusal, requestSignal);
151600
+ throw capsuleIngressDenied();
151601
+ }
151493
151602
  let allowFiles;
151494
151603
  try {
151495
151604
  allowFiles = multipartAdmissionFilePermission(decision, true);
@@ -151510,6 +151619,58 @@ async function admitCapsuleIngressPrincipal(database, endpoint, endpointRequest,
151510
151619
  }
151511
151620
  return Object.freeze({ allowFiles, authority: Object.freeze({ kind: "capsule-principal", namespace, key, keyDigest: createHash15("sha256").update(`${namespace}\0${key}`, "utf8").digest("hex"), ownerId: database.capsuleIngressOwnerId }) });
151512
151621
  }
151622
+ async function raceIngressAdmission(evaluate, signal) {
151623
+ if (signal.aborted) throw capsuleIngressDenied();
151624
+ let removeAbort = () => {
151625
+ };
151626
+ try {
151627
+ const abort = new Promise((_2, reject) => {
151628
+ const onAbort = () => reject(capsuleIngressDenied());
151629
+ signal.addEventListener("abort", onAbort, { once: true });
151630
+ removeAbort = () => signal.removeEventListener("abort", onAbort);
151631
+ });
151632
+ return await Promise.race([Promise.resolve().then(evaluate), abort]);
151633
+ } finally {
151634
+ removeAbort();
151635
+ }
151636
+ }
151637
+ function capsuleIngressDenied() {
151638
+ return commandError("Unauthenticated.", "Provide valid ingress authority and retry.", "UNAUTHENTICATED");
151639
+ }
151640
+ async function enqueueCapsuleIngressRefusal(database, request, refusal, signal) {
151641
+ let context2;
151642
+ const failure2 = () => commandError("File ingress refusal could not be recorded.", "Retry the request later.", "FILE_INGRESS_REFUSAL_FAILED");
151643
+ try {
151644
+ if (signal?.aborted || !database.jobs?.some((job) => job.name === refusal.name) || refusal.name.toLowerCase().startsWith(RESERVED_JOB_NAME_PREFIX)) throw failure2();
151645
+ const idempotencyKey = "file-ingress-refusal:" + createHash15("sha256").update(JSON.stringify([request.method, request.path, refusal.name, refusal.idempotencyKey]), "utf8").digest("hex");
151646
+ await database.adapter.withTransaction(async (adapter) => {
151647
+ const scoped = createTransactionDatabase(database, adapter);
151648
+ const checks = Symbol.for("sporades.database.transactionBeforeCommitChecks");
151649
+ (adapter[checks] ??= []).push(() => {
151650
+ if (signal?.aborted) throw failure2();
151651
+ });
151652
+ let failed = false;
151653
+ try {
151654
+ if (signal?.aborted) throw failure2();
151655
+ context2 = createMutationContext(scoped, { userId: database.capsuleIngressOwnerId, displayName: "File ingress", email: null, picture: null, isAuthenticated: false, isGuest: false, provider: "file-ingress" }, { ordinaryCredential: false });
151656
+ await context2.privileged.run(
151657
+ { operation: "files.ingress-refusal", targetResourceKind: "job-queue", signal },
151658
+ (privileged) => privileged.jobs.enqueue(refusal.name, refusal.payload, { idempotencyKey })
151659
+ );
151660
+ if (signal?.aborted) throw failure2();
151661
+ } catch (error) {
151662
+ failed = true;
151663
+ throw error;
151664
+ } finally {
151665
+ await cleanupTransactionHandler(scoped, context2, failed);
151666
+ }
151667
+ }, { signal });
151668
+ await dispatchPendingJobs(context2);
151669
+ } catch {
151670
+ dropPendingJobDispatch(context2);
151671
+ throw failure2();
151672
+ }
151673
+ }
151513
151674
  var endpointMultipartAdmissionTimeoutMs = 5e3;
151514
151675
  function multipartAdmissionDenied() {
151515
151676
  return commandError("Multipart request was not admitted.", "Check the request conditions and retry.", "MULTIPART_ADMISSION_DENIED");
@@ -151632,8 +151793,10 @@ async function runEndpoint(database, endpoint, requestUrl, request, requestPath
151632
151793
  let allowFiles = true;
151633
151794
  if (claimAuthority === "capsule-principal") {
151634
151795
  const endpointSignal = request.__sporadesEndpointSignal ?? request.signal;
151796
+ request.__sporadesCapsuleIngressAdmissionDenied = true;
151635
151797
  const admission = await admitCapsuleIngressPrincipal(database, endpoint, endpointRequest, endpointSignal);
151636
151798
  if (endpointSignal?.aborted) throw commandError("Unauthenticated.", "Provide valid ingress authority and retry.", "UNAUTHENTICATED");
151799
+ delete request.__sporadesCapsuleIngressAdmissionDenied;
151637
151800
  ingressAuthority = admission.authority;
151638
151801
  allowFiles = admission.allowFiles;
151639
151802
  } else {
@@ -164411,7 +164574,7 @@ jobs:
164411
164574
  }
164412
164575
 
164413
164576
  // src/cli/cli-version.ts
164414
- var CLI_VERSION = "0.9.42";
164577
+ var CLI_VERSION = "0.9.43";
164415
164578
 
164416
164579
  // src/cli/sporades.ts
164417
164580
  var containerSecretResolutions = /* @__PURE__ */ new WeakMap();
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.9.42";
1
+ export declare const CLI_VERSION = "0.9.43";
2
2
  //# sourceMappingURL=cli-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Generated by `npm run build` from package.json. Do not edit by hand.
2
- export const CLI_VERSION = "0.9.42";
2
+ export const CLI_VERSION = "0.9.43";
3
3
  //# sourceMappingURL=cli-version.js.map
@@ -0,0 +1,7 @@
1
+ /** The only response headers available on a thrown endpoint failure. */
2
+ export type EndpointErrorHeaders = Readonly<Partial<Record<"Cache-Control" | "Pragma" | "X-Robots-Tag" | "Content-Security-Policy" | "Referrer-Policy" | "cache-control" | "pragma" | "x-robots-tag" | "content-security-policy" | "referrer-policy", string>>>;
3
+ /** Self-contained so the public helper and generated Capsule helper use identical validation. */
4
+ export declare function endpointError(headers: EndpointErrorHeaders): Error;
5
+ /** Revalidate the public helper's snapshot at the response boundary, including forged errors. */
6
+ export declare function endpointErrorResponseHeaders(error: unknown): Record<string, string>;
7
+ //# sourceMappingURL=endpoint-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-error.d.ts","sourceRoot":"","sources":["../src/endpoint-error.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CACxD,eAAe,GAAG,QAAQ,GAAG,cAAc,GAAG,yBAAyB,GAAG,iBAAiB,GAC3F,eAAe,GAAG,QAAQ,GAAG,cAAc,GAAG,yBAAyB,GAAG,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AAEzG,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,KAAK,CAqBlE;AAED,iGAAiG;AACjG,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOnF"}
@@ -0,0 +1,42 @@
1
+ /** Self-contained so the public helper and generated Capsule helper use identical validation. */
2
+ export function endpointError(headers) {
3
+ const invalid = () => new Error("Invalid endpoint error headers.");
4
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)
5
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(headers)))
6
+ throw invalid();
7
+ const result = {};
8
+ let size = 0;
9
+ for (const key of Reflect.ownKeys(headers)) {
10
+ if (typeof key !== "string")
11
+ throw invalid();
12
+ const name = key.toLowerCase();
13
+ if (!["cache-control", "pragma", "x-robots-tag", "content-security-policy", "referrer-policy"].includes(name) || Object.hasOwn(result, name))
14
+ throw invalid();
15
+ const property = Object.getOwnPropertyDescriptor(headers, key);
16
+ const value = property?.value;
17
+ // ASCII only, with a total 8 KiB budget; never invoke accessors or coerce values.
18
+ if (!property || !Object.hasOwn(property, "value") || typeof value !== "string" || !value.trim() || /[^\x20-\x7e]/.test(value))
19
+ throw invalid();
20
+ size += name.length + value.length;
21
+ if (size > 8192)
22
+ throw invalid();
23
+ result[name] = value;
24
+ }
25
+ const error = new Error("Endpoint handler failed.");
26
+ Object.defineProperty(error, Symbol.for("sporades.endpoint.errorHeaders"), { value: Object.freeze(result) });
27
+ return error;
28
+ }
29
+ /** Revalidate the public helper's snapshot at the response boundary, including forged errors. */
30
+ export function endpointErrorResponseHeaders(error) {
31
+ try {
32
+ const descriptor = error && typeof error === "object" ? Object.getOwnPropertyDescriptor(error, Symbol.for("sporades.endpoint.errorHeaders")) : undefined;
33
+ if (!descriptor || !Object.hasOwn(descriptor, "value"))
34
+ return {};
35
+ const checked = endpointError(descriptor.value);
36
+ return checked[Symbol.for("sporades.endpoint.errorHeaders")];
37
+ }
38
+ catch {
39
+ return {};
40
+ }
41
+ }
42
+ //# sourceMappingURL=endpoint-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-error.js","sourceRoot":"","sources":["../src/endpoint-error.ts"],"names":[],"mappings":"AAKA,iGAAiG;AACjG,MAAM,UAAU,aAAa,CAAC,OAA6B;IACzD,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACnE,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;WAChE,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QAAE,MAAM,OAAO,EAAE,CAAC;IACzF,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3C,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,MAAM,OAAO,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAC/B,IAAI,CAAC,CAAC,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,yBAAyB,EAAE,iBAAiB,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC;YAAE,MAAM,OAAO,EAAE,CAAC;QAC9J,MAAM,QAAQ,GAAG,MAAM,CAAC,wBAAwB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,QAAQ,EAAE,KAAK,CAAC;QAC9B,kFAAkF;QAClF,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,MAAM,OAAO,EAAE,CAAC;QAChJ,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QACnC,IAAI,IAAI,GAAG,IAAI;YAAE,MAAM,OAAO,EAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACvB,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IACpD,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7G,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,4BAA4B,CAAC,KAAc;IACzD,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzJ,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QAClE,MAAM,OAAO,GAAG,aAAa,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAChD,OAAQ,OAAe,CAAC,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;AACxB,CAAC"}
@@ -0,0 +1,4 @@
1
+ /** Snapshot untrusted policy output without getters/toJSON or retained mutable references. */
2
+ export declare function snapshotIngressDecision(value: unknown): any;
3
+ export declare function ingressRefusalJob(decision: any): any;
4
+ //# sourceMappingURL=file-ingress-refusal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-ingress-refusal.d.ts","sourceRoot":"","sources":["../src/file-ingress-refusal.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,GAAG,CAkC3D;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,GAAG,OAW9C"}
@@ -0,0 +1,64 @@
1
+ /** Snapshot untrusted policy output without getters/toJSON or retained mutable references. */
2
+ export function snapshotIngressDecision(value) {
3
+ let nodes = 0;
4
+ let bytes = 0;
5
+ const invalid = () => { throw new Error("Invalid File ingress decision."); };
6
+ const visit = (input, depth) => {
7
+ if (++nodes > 1024 || depth > 16)
8
+ return invalid();
9
+ if (input === null || typeof input === "boolean")
10
+ return input;
11
+ if (typeof input === "number")
12
+ return Number.isFinite(input) ? input : invalid();
13
+ if (typeof input === "string") {
14
+ bytes += Buffer.byteLength(input, "utf8");
15
+ return bytes <= 8192 ? input : invalid();
16
+ }
17
+ if (!input || typeof input !== "object" || (!Array.isArray(input) && ![Object.prototype, null].includes(Object.getPrototypeOf(input))))
18
+ return invalid();
19
+ const output = Array.isArray(input) ? [] : Object.create(null);
20
+ for (const key of Reflect.ownKeys(input)) {
21
+ if (Array.isArray(input) && key === "length")
22
+ continue;
23
+ if (typeof key !== "string")
24
+ return invalid();
25
+ bytes += Buffer.byteLength(key, "utf8");
26
+ if (bytes > 8192)
27
+ return invalid();
28
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
29
+ if (!descriptor || !Object.hasOwn(descriptor, "value") || !descriptor.enumerable)
30
+ return invalid();
31
+ // Preserve the established optional allowFiles contract while keeping
32
+ // refusal payloads strict JSON. Explicitly undefined optional fields
33
+ // have the same meaning as omission, only at the decision root.
34
+ if (depth === 0 && (key === "allowFiles" || key === "refusalJob") && descriptor.value === undefined)
35
+ continue;
36
+ if (Array.isArray(input) && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= 1024))
37
+ return invalid();
38
+ Object.defineProperty(output, key, { value: visit(descriptor.value, depth + 1), enumerable: true });
39
+ }
40
+ if (Array.isArray(input) && (input.length > 1024 || Object.keys(output).length !== input.length))
41
+ return invalid();
42
+ return Object.freeze(output);
43
+ };
44
+ const result = visit(value, 0);
45
+ if (Buffer.byteLength(JSON.stringify(result) ?? "", "utf8") > 8192)
46
+ invalid();
47
+ return result;
48
+ }
49
+ export function ingressRefusalJob(decision) {
50
+ if (!decision || decision.allow !== false || Object.keys(decision).some(key => !["allow", "refusalJob"].includes(key)))
51
+ throw new Error("Invalid File ingress refusal.");
52
+ if (!Object.hasOwn(decision, "refusalJob"))
53
+ return null;
54
+ const job = decision.refusalJob;
55
+ if (!job || typeof job !== "object" || Array.isArray(job) || Object.keys(job).length !== 3
56
+ || Object.keys(job).some(key => !["name", "payload", "idempotencyKey"].includes(key))
57
+ || typeof job.name !== "string" || !/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(job.name)
58
+ || typeof job.idempotencyKey !== "string" || !job.idempotencyKey || Buffer.byteLength(job.idempotencyKey, "utf8") > 256
59
+ || /[\x00-\x1f\x7f]/.test(job.idempotencyKey)
60
+ || !Object.hasOwn(job, "payload") || Buffer.byteLength(JSON.stringify(job.payload), "utf8") > 4096)
61
+ throw new Error("Invalid File ingress refusal.");
62
+ return job;
63
+ }
64
+ //# sourceMappingURL=file-ingress-refusal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-ingress-refusal.js","sourceRoot":"","sources":["../src/file-ingress-refusal.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,OAAO,GAAG,GAAG,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,CAAC,KAAU,EAAE,KAAa,EAAO,EAAE;QAC/C,IAAI,EAAE,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE;YAAE,OAAO,OAAO,EAAE,CAAC;QACnD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAC/D,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QACjF,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC1C,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;YAAE,OAAO,OAAO,EAAE,CAAC;QACzJ,MAAM,MAAM,GAAQ,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpE,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,QAAQ;gBAAE,SAAS;YACvD,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,OAAO,OAAO,EAAE,CAAC;YAC9C,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACxC,IAAI,KAAK,GAAG,IAAI;gBAAE,OAAO,OAAO,EAAE,CAAC;YACnC,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC/D,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU;gBAAE,OAAO,OAAO,EAAE,CAAC;YACnG,sEAAsE;YACtE,qEAAqE;YACrE,gEAAgE;YAChE,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,YAAY,IAAI,GAAG,KAAK,YAAY,CAAC,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS;gBAAE,SAAS;YAC9G,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;gBAAE,OAAO,OAAO,EAAE,CAAC;YACtG,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QACtG,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC;YAAE,OAAO,OAAO,EAAE,CAAC;QACnH,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC/B,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,GAAG,IAAI;QAAE,OAAO,EAAE,CAAC;IAC9E,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAa;IAC7C,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,KAAK,KAAK,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACzK,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IACxD,MAAM,GAAG,GAAG,QAAQ,CAAC,UAAU,CAAC;IAChC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC;WACrF,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;WAClF,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,gCAAgC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;WAChF,OAAO,GAAG,CAAC,cAAc,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,cAAc,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG,GAAG;WACpH,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC;WAC1C,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,GAAG,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACvJ,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -80,8 +80,10 @@
80
80
  "src/dev-clamav-sidecar.ts",
81
81
  "src/email-events-config.ts",
82
82
  "src/email-events-runtime.ts",
83
+ "src/endpoint-error.ts",
83
84
  "src/endpoint-file-response.ts",
84
85
  "src/endpoint-session.ts",
86
+ "src/file-ingress-refusal.ts",
85
87
  "src/file-ingress-runtime.ts",
86
88
  "src/file-policy.ts",
87
89
  "src/file-storage-runtime.ts",
@@ -158,7 +160,7 @@
158
160
  "tsconfig.json",
159
161
  "tsconfig.runtime.json"
160
162
  ],
161
- "digest": "03b9f54d552fd4b2040d645f572c76cad26f639f762eb0046bb67e6cc10031a0"
163
+ "digest": "e705886424917bce68edceb78f8f45184c1055abfd7699836596e6e4e364a7e5"
162
164
  },
163
165
  "outputs": {
164
166
  "files": [
@@ -448,6 +450,10 @@
448
450
  "dist/email-events-runtime.d.ts.map",
449
451
  "dist/email-events-runtime.js",
450
452
  "dist/email-events-runtime.js.map",
453
+ "dist/endpoint-error.d.ts",
454
+ "dist/endpoint-error.d.ts.map",
455
+ "dist/endpoint-error.js",
456
+ "dist/endpoint-error.js.map",
451
457
  "dist/endpoint-file-response.d.ts",
452
458
  "dist/endpoint-file-response.d.ts.map",
453
459
  "dist/endpoint-file-response.js",
@@ -456,6 +462,10 @@
456
462
  "dist/endpoint-session.d.ts.map",
457
463
  "dist/endpoint-session.js",
458
464
  "dist/endpoint-session.js.map",
465
+ "dist/file-ingress-refusal.d.ts",
466
+ "dist/file-ingress-refusal.d.ts.map",
467
+ "dist/file-ingress-refusal.js",
468
+ "dist/file-ingress-refusal.js.map",
459
469
  "dist/file-ingress-runtime.d.ts",
460
470
  "dist/file-ingress-runtime.d.ts.map",
461
471
  "dist/file-ingress-runtime.js",
@@ -706,6 +716,6 @@
706
716
  "dist/user-preferences-runtime.js.map",
707
717
  "monitoring/trace/inventory-contract.mjs"
708
718
  ],
709
- "digest": "b7b91be6ef8746a346a6d48d53da2d3c6db34b177f1e8c78aede76da59f18c9b"
719
+ "digest": "2beff51c6581530f9cc54446a881f896db899362e19c06d4d883e0fbaf54a9ff"
710
720
  }
711
721
  }
@@ -1 +1 @@
1
- {"version":3,"file":"http-runtime.d.ts","sourceRoot":"","sources":["../src/http-runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http-runtime.d.ts","sourceRoot":"","sources":["../src/http-runtime.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { endpointErrorResponseHeaders } from "./endpoint-error.js";
1
2
  // The Capsule runtime's HTTP and security policy domain: the CORS and CSP posture every response
2
3
  // carries, the origin and host-header validation behind it, the request body reader and its size
3
4
  // limit, the generic response writers, and the two routes the runtime owns outright — the health
@@ -405,8 +406,9 @@ export function prepareHttpSecurity(database, request, response, admitPreflight)
405
406
  response.writeHead = ((statusCode, statusMessageOrHeaders, maybeHeaders) => {
406
407
  const statusMessage = typeof statusMessageOrHeaders === "string" ? statusMessageOrHeaders : undefined;
407
408
  const inputHeaders = statusMessage ? maybeHeaders : typeof statusMessageOrHeaders === "string" ? {} : statusMessageOrHeaders;
409
+ const supplied = sanitizeResponseHeaders({ ...(response.getHeaders?.() ?? {}), ...inputHeaders });
408
410
  const headers = {
409
- ...sanitizeResponseHeaders(inputHeaders ?? {}),
411
+ ...supplied,
410
412
  "x-content-type-options": "nosniff",
411
413
  "referrer-policy": "no-referrer",
412
414
  "x-frame-options": "DENY",
@@ -415,6 +417,14 @@ export function prepareHttpSecurity(database, request, response, admitPreflight)
415
417
  [policy.csp.header]: serializeCspDirectives(directives),
416
418
  ...(policy.hsts ? { "strict-transport-security": policy.hsts } : {}),
417
419
  };
420
+ // Multiple CSP policies are enforced independently (intersection), never merged
421
+ // by directive: neither the endpoint nor the platform can weaken the other.
422
+ const suppliedCsp = supplied[policy.csp.header];
423
+ if (suppliedCsp)
424
+ headers[policy.csp.header] = [
425
+ ...(Array.isArray(suppliedCsp) ? suppliedCsp.map(String) : [String(suppliedCsp)]),
426
+ serializeCspDirectives(directives),
427
+ ];
418
428
  if (isGuardedAttachmentHttpResponse(response)) {
419
429
  delete headers["content-security-policy-report-only"];
420
430
  delete headers["access-control-allow-origin"];
@@ -731,7 +741,7 @@ function sanitizeResponseHeaders(headers) {
731
741
  return Object.fromEntries([...entries].filter(([name]) => {
732
742
  const normalized = String(name).toLowerCase();
733
743
  return normalized !== "x-powered-by" && normalized !== "server";
734
- }));
744
+ }).map(([name, value]) => [String(name).toLowerCase(), value]));
735
745
  }
736
746
  export async function handleFileHttpRoute(database, request, response, websocketHub = null) {
737
747
  const target = requestTarget(request);
@@ -1013,7 +1023,7 @@ function mergeEndpointResponseHeaders(handlerHeaders, runtimeHeaders) {
1013
1023
  return { ...headers, ...runtimeHeaders };
1014
1024
  }
1015
1025
  export function writeEndpointError(response, error) {
1016
- const headers = { "content-type": "application/json; charset=utf-8" };
1026
+ const headers = { ...endpointErrorResponseHeaders(error), "content-type": "application/json; charset=utf-8" };
1017
1027
  if (error?.code === "UNAUTHENTICATED" && error?.sporadesAccessKeyFailure) {
1018
1028
  headers["www-authenticate"] = error?.sporadesAccessKeyFailure === "invalid" && error?.sporadesAccessKeyReason !== "missing"
1019
1029
  ? 'Bearer realm="sporades", error="invalid_token"'
@@ -1023,6 +1033,10 @@ export function writeEndpointError(response, error) {
1023
1033
  headers["cache-control"] = "no-store";
1024
1034
  headers.pragma = "no-cache";
1025
1035
  }
1036
+ if (String(response.getHeader?.("cache-control") ?? "").includes("no-store")) {
1037
+ headers["cache-control"] = response.getHeader("cache-control");
1038
+ headers.pragma = "no-cache";
1039
+ }
1026
1040
  const multipartDetails = safeMultipartLimitDetails(error);
1027
1041
  response.writeHead(endpointErrorStatus(error), headers);
1028
1042
  response.end(`${JSON.stringify({
@@ -1058,6 +1072,8 @@ function safeMultipartLimitDetails(error) {
1058
1072
  return { partType: details.partType, limitKind: details.limitKind, limit: details.limit };
1059
1073
  }
1060
1074
  function endpointErrorStatus(error) {
1075
+ if (error?.code === "FILE_INGRESS_REFUSAL_FAILED")
1076
+ return 503;
1061
1077
  if (error?.code === "UNAUTHENTICATED")
1062
1078
  return 401;
1063
1079
  if (error?.code === "FORBIDDEN")