sporades 0.9.18 โ†’ 0.9.19

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
@@ -2,19 +2,21 @@
2
2
 
3
3
  ## Unreleased - 2026-09-11
4
4
 
5
- Changes since v0.9.17.
5
+ Changes since v0.9.18.
6
6
 
7
- ### ๐Ÿ› Bug Fixes
7
+ ### ๐Ÿš€ Features
8
8
 
9
- - Reject caps below the configured envelope floor (bfb6bce1).
10
- - Fix query arguments containing multi-element arrays (#37) (8cfef157).
9
+ - Allow trusted multipart admission to prohibit files (#39) (47602ddf).
11
10
 
12
11
  ### ๐Ÿ“ Documentation
13
12
 
14
- - Remove personal infrastructure from documentation and host fixture (628363b1).
15
- - Link log cap implementation PR (a0b2fb88).
16
- - Record log cap validation completion (675226cd).
17
- - Document shell quote handlers in SQL lexer census (#38) (125355ab).
13
+ - Require explicit own data for multipart admission authority (3b118d0d).
14
+ - Treat undefined multipart file permission as omitted (5cfe8aca).
15
+ - Regenerate multipart admission API documentation (ac92dcf8).
16
+
17
+ ### ๐Ÿงช Tests
18
+
19
+ - Verify file permission in packed admission declarations (0465c97f).
18
20
 
19
21
  ## v0.9.11 - 2026-09-02
20
22
 
@@ -109,3 +111,4 @@ generated artifacts, and documentation.
109
111
 
110
112
 
111
113
 
114
+
@@ -42587,7 +42587,7 @@ function writeEnvelope(result, failed = false) {
42587
42587
  }
42588
42588
 
42589
42589
  // src/cli/cli-version.ts
42590
- var CLI_VERSION = "0.9.18";
42590
+ var CLI_VERSION = "0.9.19";
42591
42591
 
42592
42592
  // src/cli/schedule-inspection-envelope.ts
42593
42593
  var SCHEDULE_DIAGNOSTIC_FIELDS = /* @__PURE__ */ new Set([
package/bin/sporades.js CHANGED
@@ -87363,7 +87363,7 @@ function idempotencyConflict(message = "Ingress claim conflicts with the complet
87363
87363
  function ingressAuthorityDenied() {
87364
87364
  return Object.assign(new Error("File ingress authority is unavailable."), { code: "INGRESS_AUTHORITY_DENIED" });
87365
87365
  }
87366
- var ingressAuditCodes = /* @__PURE__ */ new Set(["INVALID_MULTIPART", "MULTIPART_LIMIT_EXCEEDED", "INVALID_MULTIPART_REQUEST_KEY", "INVALID_MULTIPART_PART_KEY", "INGRESS_AUTHORITY_DENIED", "INGRESS_LEASE_EXPIRED", "INGRESS_PATH_DENIED", "INGRESS_DESCRIPTOR_CONFLICT", "INGRESS_STAGING_INCOMPLETE", "INGRESS_ORPHAN_CLEANUP_FAILED", "FILE_PATH_EXISTS"]);
87366
+ var ingressAuditCodes = /* @__PURE__ */ new Set(["MULTIPART_ADMISSION_DENIED", "INVALID_MULTIPART", "MULTIPART_LIMIT_EXCEEDED", "INVALID_MULTIPART_REQUEST_KEY", "INVALID_MULTIPART_PART_KEY", "INGRESS_AUTHORITY_DENIED", "INGRESS_LEASE_EXPIRED", "INGRESS_PATH_DENIED", "INGRESS_DESCRIPTOR_CONFLICT", "INGRESS_STAGING_INCOMPLETE", "INGRESS_ORPHAN_CLEANUP_FAILED", "FILE_PATH_EXISTS"]);
87367
87367
  async function emitIngressAudit(database, event, data2) {
87368
87368
  try {
87369
87369
  await database.log?.emit?.({ category: "platform", event: `file.ingress.${event}`, level: event === "failed" || event === "cleanup-failed" ? "warn" : "info", message: "Multipart ingress lifecycle event", data: { schema: "v1", ...data2 } });
@@ -87391,7 +87391,7 @@ function multipartBoundary(contentType) {
87391
87391
  const validToken = /^[0-9A-Za-z'+_.-]+$/.test(value);
87392
87392
  return value.length <= 70 && validBchars && (quoted || validToken) ? value : null;
87393
87393
  }
87394
- async function* multipartParts(request, boundaryText, maxWireBytes, maxPartBytes) {
87394
+ async function* multipartParts(request, boundaryText, maxWireBytes, maxPartBytes, allowFiles = true) {
87395
87395
  const boundary = Buffer.from(`--${boundaryText}`);
87396
87396
  const marker = Buffer.from(`\r
87397
87397
  --${boundaryText}`);
@@ -87421,10 +87421,10 @@ async function* multipartParts(request, boundaryText, maxWireBytes, maxPartBytes
87421
87421
  break;
87422
87422
  }
87423
87423
  rawHeaders = pending.subarray(0, headerEnd).toString("latin1");
87424
- if (typeof maxPartBytes !== "number") {
87425
- const disposition = /^content-disposition:\s*form-data;\s*name="[^"]+"(?:;\s*filename="([^"]*)")?/im.exec(rawHeaders);
87426
- partLimit = disposition?.[1] !== void 0 ? maxPartBytes.file : maxPartBytes.field;
87427
- }
87424
+ const disposition = /^content-disposition:\s*form-data;\s*name="[^"]+"(?:;\s*filename="([^"]*)")?/im.exec(rawHeaders);
87425
+ const isFile = disposition?.[1] !== void 0;
87426
+ if (isFile && !allowFiles) throw Object.assign(new Error("Multipart request was not admitted."), { code: "MULTIPART_ADMISSION_DENIED" });
87427
+ if (typeof maxPartBytes !== "number") partLimit = isFile ? maxPartBytes.file : maxPartBytes.field;
87428
87428
  pending = pending.subarray(headerEnd + 4);
87429
87429
  pieces = [];
87430
87430
  size = 0;
@@ -87492,7 +87492,7 @@ async function* multipartParts(request, boundaryText, maxWireBytes, maxPartBytes
87492
87492
  if (state === "closing" && pending.length === 0) return;
87493
87493
  throw Object.assign(new Error("Truncated multipart request."), { code: "INVALID_MULTIPART" });
87494
87494
  }
87495
- async function stageMultipartIngress(database, endpoint, request, endpointRequest, actor, admittedAuthority) {
87495
+ async function stageMultipartIngress(database, endpoint, request, endpointRequest, actor, admittedAuthority, allowFiles = true) {
87496
87496
  let policy;
87497
87497
  try {
87498
87498
  policy = validateMultipartIngressPolicy(endpoint.options.body.multipart);
@@ -87525,7 +87525,7 @@ async function stageMultipartIngress(database, endpoint, request, endpointReques
87525
87525
  const wonReceipts = [];
87526
87526
  const streamingFileLimit = Math.min(Number(policy.maxFileBytes), Number(database.fileMaxSizeBytes));
87527
87527
  try {
87528
- for await (const part of multipartParts(request, boundary, maxBytes, { file: streamingFileLimit, field: policy.maxFieldBytes })) {
87528
+ for await (const part of multipartParts(request, boundary, maxBytes, { file: streamingFileLimit, field: policy.maxFieldBytes }, allowFiles)) {
87529
87529
  const rawHeaders = part.rawHeaders;
87530
87530
  const body = part.body;
87531
87531
  if (rawHeaders.length > 16384) throw Object.assign(new Error("Multipart headers exceed limit."), { code: "MULTIPART_LIMIT_EXCEEDED" });
@@ -100638,7 +100638,12 @@ async function admitEndpointMultipart(database, endpoint, endpointRequest, admis
100638
100638
  }
100639
100639
  });
100640
100640
  const decision = await Promise.race([transaction, settlementAbort]).finally(removeSettlementAbort);
100641
- if (controller.signal.aborted || Date.now() >= deadline || !decision || typeof decision !== "object" || Array.isArray(decision) || Object.keys(decision).length !== 1 || typeof decision.allow !== "boolean" || decision.allow !== true) throw multipartAdmissionDenied();
100641
+ if (controller.signal.aborted || Date.now() >= deadline || !decision || typeof decision !== "object" || Array.isArray(decision)) throw multipartAdmissionDenied();
100642
+ const keys = Reflect.ownKeys(decision);
100643
+ const allow = Object.getOwnPropertyDescriptor(decision, "allow");
100644
+ const allowFiles = Object.getOwnPropertyDescriptor(decision, "allowFiles");
100645
+ if (keys.some((key) => key !== "allow" && key !== "allowFiles") || !allow || !Object.prototype.hasOwnProperty.call(allow, "value") || allow.value !== true || allowFiles && (!Object.prototype.hasOwnProperty.call(allowFiles, "value") || allowFiles.value !== void 0 && typeof allowFiles.value !== "boolean")) throw multipartAdmissionDenied();
100646
+ return allowFiles?.value !== false;
100642
100647
  } catch {
100643
100648
  throw multipartAdmissionDenied();
100644
100649
  } finally {
@@ -100693,16 +100698,17 @@ async function runEndpoint(database, endpoint, requestUrl, request) {
100693
100698
  const claimAuthority = endpointIngressClaimAuthority(endpoint);
100694
100699
  const admitted = accessKeyAdmission ?? session;
100695
100700
  let ingressAuthority;
100701
+ let allowFiles = true;
100696
100702
  if (claimAuthority === "capsule-principal") {
100697
100703
  ingressAuthority = await admitCapsuleIngressPrincipal(database, endpoint, endpointRequest, request.signal);
100698
100704
  } else {
100699
100705
  if (!admitted?.auth?.isAuthenticated || admitted.auth.isGuest || isReservedAuthUserId(admitted.auth.userId)) throw commandError("Unauthenticated.", "Sign in with a linked human or service User and retry.", "UNAUTHENTICATED");
100700
100706
  const endpointSignal = request.signal ?? request.__sporadesEndpointSignal;
100701
- await admitEndpointMultipart(database, endpoint, endpointRequest, admitted, endpointSignal);
100707
+ allowFiles = await admitEndpointMultipart(database, endpoint, endpointRequest, admitted, endpointSignal) !== false;
100702
100708
  if (endpointSignal?.aborted) throw multipartAdmissionDenied();
100703
100709
  ingressAuthority = Object.freeze({ kind: "actor", actorId: String(admitted.auth.userId), ownerId: String(admitted.auth.userId) });
100704
100710
  }
100705
- const payload = await stageMultipartIngress(database, endpoint, request, endpointRequest, admitted.auth, ingressAuthority);
100711
+ const payload = await stageMultipartIngress(database, endpoint, request, endpointRequest, admitted.auth, ingressAuthority, allowFiles);
100706
100712
  endpointRequest = { ...endpointRequest, ...payload };
100707
100713
  } catch (error) {
100708
100714
  if (error?.code === "UNAUTHENTICATED") {
@@ -110759,7 +110765,7 @@ jobs:
110759
110765
  }
110760
110766
 
110761
110767
  // src/cli/cli-version.ts
110762
- var CLI_VERSION = "0.9.18";
110768
+ var CLI_VERSION = "0.9.19";
110763
110769
 
110764
110770
  // src/cli/sporades.ts
110765
110771
  var SUPPORTED_TEMPLATES = new Set(CLIENT_TEMPLATES);
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.9.18";
1
+ export declare const CLI_VERSION = "0.9.19";
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.18";
2
+ export const CLI_VERSION = "0.9.19";
3
3
  //# sourceMappingURL=cli-version.js.map
@@ -21,12 +21,12 @@ export declare function checkClamavRuntime(database: RecordLike): Promise<{
21
21
  export declare function multipartParts(request: AsyncIterable<Uint8Array>, boundaryText: string, maxWireBytes: number, maxPartBytes: number | {
22
22
  file: number;
23
23
  field: number;
24
- }): AsyncGenerator<{
24
+ }, allowFiles?: boolean): AsyncGenerator<{
25
25
  rawHeaders: string;
26
26
  body: Buffer<ArrayBuffer>;
27
27
  }, void, unknown>;
28
28
  /** Parse only after endpoint credential admission. The bounded body is never exposed as an ordinary endpoint body. */
29
- export declare function stageMultipartIngress(database: RecordLike, endpoint: RecordLike, request: any, endpointRequest: RecordLike, actor: RecordLike, admittedAuthority?: RecordLike): Promise<{
29
+ export declare function stageMultipartIngress(database: RecordLike, endpoint: RecordLike, request: any, endpointRequest: RecordLike, actor: RecordLike, admittedAuthority?: RecordLike, allowFiles?: boolean): Promise<{
30
30
  body: null;
31
31
  bodyBytes: Readonly<{
32
32
  byteLength: 0;
@@ -1 +1 @@
1
- {"version":3,"file":"file-ingress-runtime.d.ts","sourceRoot":"","sources":["../src/file-ingress-runtime.ts"],"names":[],"mappings":"AAWA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAWtC,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,MAAM,WAAoN;AAgrBpR,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,oBAoF/E;AAoCD,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,WAW5D;AAmCD,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,MAAM,WAE/D;AACD,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,WAwB5D;AAMD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,WAmBxD;AAaD,eAAO,MAAM,uBAAuB,mBAElC,CAAC;AAgOH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,WAgDvD;AAmBD,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,UAAU,GAAG,IAAI,EAAE,GAAG,SAAa,WAAgQ;AACvV,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,SAAO;QAA6B,OAAO;YAAU,MAAM;GAAy2B;AA4Hx/B,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,oBASnH;AAOD,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,UAAU,gBAkBjE;AACD,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,UAAU,iBAAuR;AACvV,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,UAAU;;GAA+jB;AAoD5nB,wBAAuB,cAAc,CAAC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;;;kBAqB3K;AAED,sHAAsH;AACtH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,iBAAiB,CAAC,EAAE,UAAU;;;;;;;;;;;;;;;;;;;GA2FnL;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,UAAU,cAiBhE;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;iBAU9G,UAAU,WAAW,UAAU;;;;;;;;;sBAyD1B,UAAU;;;;6BASH,MAAM,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBzD;AAED,wGAAwG;AACxG,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,QAEpF;AAED,6FAA6F;AAC7F,wBAAsB,8BAA8B,CAAC,QAAQ,EAAE,UAAU,oBAQxE;AAED,4EAA4E;AAC5E,wBAAsB,4BAA4B,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAE,UAAe,iBAsChG;AAwCD,wGAAwG;AACxG,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAE,UAAe;;;;;;;;;;IAuB3F"}
1
+ {"version":3,"file":"file-ingress-runtime.d.ts","sourceRoot":"","sources":["../src/file-ingress-runtime.ts"],"names":[],"mappings":"AAWA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAWtC,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,MAAM,WAAoN;AAgrBpR,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,oBAoF/E;AAoCD,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,WAW5D;AAmCD,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,MAAM,WAE/D;AACD,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,WAwB5D;AAMD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,WAmBxD;AAaD,eAAO,MAAM,uBAAuB,mBAElC,CAAC;AAgOH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,WAgDvD;AAmBD,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,UAAU,GAAG,IAAI,EAAE,GAAG,SAAa,WAAgQ;AACvV,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,SAAO;QAA6B,OAAO;YAAU,MAAM;GAAy2B;AA4Hx/B,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,oBASnH;AAOD,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,UAAU,gBAkBjE;AACD,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,UAAU,iBAAuR;AACvV,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,UAAU;;GAA+jB;AAoD5nB,wBAAuB,cAAc,CAAC,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,UAAU,UAAO;;;kBA+B9L;AAED,sHAAsH;AACtH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,iBAAiB,CAAC,EAAE,UAAU,EAAE,UAAU,UAAO;;;;;;;;;;;;;;;;;;;GA2FtM;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,UAAU,cAiBhE;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;iBAU9G,UAAU,WAAW,UAAU;;;;;;;;;sBAyD1B,UAAU;;;;6BASH,MAAM,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBzD;AAED,wGAAwG;AACxG,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,QAEpF;AAED,6FAA6F;AAC7F,wBAAsB,8BAA8B,CAAC,QAAQ,EAAE,UAAU,oBAQxE;AAED,4EAA4E;AAC5E,wBAAsB,4BAA4B,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAE,UAAe,iBAsChG;AAwCD,wGAAwG;AACxG,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAE,UAAe;;;;;;;;;;IAuB3F"}
@@ -2797,7 +2797,7 @@ function legacyDelimitedKeyFor(endpoint, requestKey, partKey, actor) { return `$
2797
2797
  function publicLease(row) { return Object.freeze({ leaseId: row.leaseId, partId: row.partId, fieldName: row.fieldName, name: row.name, type: row.type, declaredSize: null, size: row.size, expiresAt: row.expiresAt }); }
2798
2798
  function idempotencyConflict(message = "Ingress claim conflicts with the completed request.") { return Object.assign(new Error(message), { code: "IDEMPOTENCY_CONFLICT" }); }
2799
2799
  function ingressAuthorityDenied() { return Object.assign(new Error("File ingress authority is unavailable."), { code: "INGRESS_AUTHORITY_DENIED" }); }
2800
- const ingressAuditCodes = new Set(["INVALID_MULTIPART", "MULTIPART_LIMIT_EXCEEDED", "INVALID_MULTIPART_REQUEST_KEY", "INVALID_MULTIPART_PART_KEY", "INGRESS_AUTHORITY_DENIED", "INGRESS_LEASE_EXPIRED", "INGRESS_PATH_DENIED", "INGRESS_DESCRIPTOR_CONFLICT", "INGRESS_STAGING_INCOMPLETE", "INGRESS_ORPHAN_CLEANUP_FAILED", "FILE_PATH_EXISTS"]);
2800
+ const ingressAuditCodes = new Set(["MULTIPART_ADMISSION_DENIED", "INVALID_MULTIPART", "MULTIPART_LIMIT_EXCEEDED", "INVALID_MULTIPART_REQUEST_KEY", "INVALID_MULTIPART_PART_KEY", "INGRESS_AUTHORITY_DENIED", "INGRESS_LEASE_EXPIRED", "INGRESS_PATH_DENIED", "INGRESS_DESCRIPTOR_CONFLICT", "INGRESS_STAGING_INCOMPLETE", "INGRESS_ORPHAN_CLEANUP_FAILED", "FILE_PATH_EXISTS"]);
2801
2801
  async function emitIngressAudit(database, event, data) {
2802
2802
  try {
2803
2803
  await database.log?.emit?.({ category: "platform", event: `file.ingress.${event}`, level: event === "failed" || event === "cleanup-failed" ? "warn" : "info", message: "Multipart ingress lifecycle event", data: { schema: "v1", ...data } });
@@ -2826,7 +2826,7 @@ function multipartBoundary(contentType) {
2826
2826
  }
2827
2827
  // Keeps only one completed part (never the aggregate request) in memory. The storage adapter
2828
2828
  // currently accepts complete bounded bytes; this is deliberately the narrowest streaming seam.
2829
- export async function* multipartParts(request, boundaryText, maxWireBytes, maxPartBytes) {
2829
+ export async function* multipartParts(request, boundaryText, maxWireBytes, maxPartBytes, allowFiles = true) {
2830
2830
  const boundary = Buffer.from(`--${boundaryText}`);
2831
2831
  const marker = Buffer.from(`\r\n--${boundaryText}`);
2832
2832
  let pending = Buffer.alloc(0);
@@ -2859,10 +2859,13 @@ export async function* multipartParts(request, boundaryText, maxWireBytes, maxPa
2859
2859
  break;
2860
2860
  }
2861
2861
  rawHeaders = pending.subarray(0, headerEnd).toString("latin1");
2862
- if (typeof maxPartBytes !== "number") {
2863
- const disposition = /^content-disposition:\s*form-data;\s*name="[^"]+"(?:;\s*filename="([^"]*)")?/im.exec(rawHeaders);
2864
- partLimit = disposition?.[1] !== undefined ? maxPartBytes.file : maxPartBytes.field;
2865
- }
2862
+ const disposition = /^content-disposition:\s*form-data;\s*name="[^"]+"(?:;\s*filename="([^"]*)")?/im.exec(rawHeaders);
2863
+ const isFile = disposition?.[1] !== undefined;
2864
+ // Admission applies at recognition, before buffering even an empty file.
2865
+ if (isFile && !allowFiles)
2866
+ throw Object.assign(new Error("Multipart request was not admitted."), { code: "MULTIPART_ADMISSION_DENIED" });
2867
+ if (typeof maxPartBytes !== "number")
2868
+ partLimit = isFile ? maxPartBytes.file : maxPartBytes.field;
2866
2869
  pending = pending.subarray(headerEnd + 4);
2867
2870
  pieces = [];
2868
2871
  size = 0;
@@ -2941,7 +2944,7 @@ export async function* multipartParts(request, boundaryText, maxWireBytes, maxPa
2941
2944
  throw Object.assign(new Error("Truncated multipart request."), { code: "INVALID_MULTIPART" });
2942
2945
  }
2943
2946
  /** Parse only after endpoint credential admission. The bounded body is never exposed as an ordinary endpoint body. */
2944
- export async function stageMultipartIngress(database, endpoint, request, endpointRequest, actor, admittedAuthority) {
2947
+ export async function stageMultipartIngress(database, endpoint, request, endpointRequest, actor, admittedAuthority, allowFiles = true) {
2945
2948
  let policy;
2946
2949
  try {
2947
2950
  policy = validateMultipartIngressPolicy(endpoint.options.body.multipart);
@@ -2978,7 +2981,7 @@ export async function stageMultipartIngress(database, endpoint, request, endpoin
2978
2981
  const wonReceipts = [];
2979
2982
  const streamingFileLimit = Math.min(Number(policy.maxFileBytes), Number(database.fileMaxSizeBytes));
2980
2983
  try {
2981
- for await (const part of multipartParts(request, boundary, maxBytes, { file: streamingFileLimit, field: policy.maxFieldBytes })) {
2984
+ for await (const part of multipartParts(request, boundary, maxBytes, { file: streamingFileLimit, field: policy.maxFieldBytes }, allowFiles)) {
2982
2985
  const rawHeaders = part.rawHeaders;
2983
2986
  const body = part.body;
2984
2987
  if (rawHeaders.length > 16384)