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 +11 -8
- package/bin/sporades-host-helper.js +1 -1
- package/bin/sporades.js +18 -12
- package/dist/cli/cli-version.d.ts +1 -1
- package/dist/cli/cli-version.js +1 -1
- package/dist/file-ingress-runtime.d.ts +2 -2
- package/dist/file-ingress-runtime.d.ts.map +1 -1
- package/dist/file-ingress-runtime.js +11 -8
- package/dist/file-ingress-runtime.js.map +1 -1
- package/dist/generated-source-manifest.json +2 -2
- package/dist/server-runtime-source.d.ts.map +1 -1
- package/dist/server-runtime-source.js +13 -3
- package/dist/server-runtime-source.js.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/types/server.d.ts +2 -2
package/CHANGES.md
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased - 2026-09-11
|
|
4
4
|
|
|
5
|
-
Changes since v0.9.
|
|
5
|
+
Changes since v0.9.18.
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### ๐ Features
|
|
8
8
|
|
|
9
|
-
-
|
|
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
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
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.
|
|
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
|
-
|
|
87425
|
-
|
|
87426
|
-
|
|
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)
|
|
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.
|
|
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.
|
|
1
|
+
export declare const CLI_VERSION = "0.9.19";
|
|
2
2
|
//# sourceMappingURL=cli-version.d.ts.map
|
package/dist/cli/cli-version.js
CHANGED
|
@@ -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;;;
|
|
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
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
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)
|