@zintrust/core 2.5.8 → 2.7.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.
Files changed (31) hide show
  1. package/package.json +2 -1
  2. package/src/cli/commands/D1MigrateCommand.d.ts.map +1 -1
  3. package/src/cli/commands/D1MigrateCommand.js +4 -2
  4. package/src/http/middleware/BodyParsingMiddleware.d.ts +28 -0
  5. package/src/http/middleware/BodyParsingMiddleware.d.ts.map +1 -1
  6. package/src/http/middleware/BodyParsingMiddleware.js +61 -21
  7. package/src/http/middleware/FileUploadMiddleware.d.ts.map +1 -1
  8. package/src/http/middleware/FileUploadMiddleware.js +116 -31
  9. package/src/http/parsers/BodyParsers.d.ts.map +1 -1
  10. package/src/http/parsers/BodyParsers.js +49 -18
  11. package/src/http/parsers/MultipartParser.d.ts +3 -1
  12. package/src/http/parsers/MultipartParser.d.ts.map +1 -1
  13. package/src/http/parsers/MultipartParser.js +4 -2
  14. package/src/index.js +3 -3
  15. package/src/proxy/d1/ZintrustD1Proxy.d.ts +7 -0
  16. package/src/proxy/d1/ZintrustD1Proxy.d.ts.map +1 -1
  17. package/src/proxy/d1/ZintrustD1Proxy.js +1 -1
  18. package/src/runtime/adapters/CloudflareAdapter.d.ts.map +1 -1
  19. package/src/runtime/adapters/CloudflareAdapter.js +24 -2
  20. package/src/runtime/adapters/DenoAdapter.js +1 -1
  21. package/src/tools/storage/drivers/R2.d.ts +1 -0
  22. package/src/tools/storage/drivers/R2.d.ts.map +1 -1
  23. package/src/tools/storage/drivers/R2.js +10 -1
  24. package/src/tools/storage/drivers/S3.d.ts +1 -0
  25. package/src/tools/storage/drivers/S3.d.ts.map +1 -1
  26. package/src/tools/storage/drivers/S3.js +5 -0
  27. package/src/tools/storage/index.d.ts.map +1 -1
  28. package/src/tools/storage/index.js +1 -0
  29. package/src/worker-runtime/drain.d.ts +19 -0
  30. package/src/worker-runtime/drain.d.ts.map +1 -1
  31. package/src/worker-runtime/drain.js +21 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/core",
3
- "version": "2.5.8",
3
+ "version": "2.7.1",
4
4
  "description": "Production-grade TypeScript backend framework for JavaScript",
5
5
  "homepage": "https://zintrust.com",
6
6
  "repository": {
@@ -268,6 +268,7 @@
268
268
  "ws": "^8.20.0",
269
269
  "micromatch": "^4.0.8",
270
270
  "brace-expansion": "^5.0.6",
271
+ "esbuild": "0.28.1",
271
272
  "qs": "^6.15.2"
272
273
  },
273
274
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"D1MigrateCommand.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/D1MigrateCommand.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,kBAAkB,CAAC;AA0IrE;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;OAEG;cACO,YAAY;EAgCtB,CAAC"}
1
+ {"version":3,"file":"D1MigrateCommand.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/D1MigrateCommand.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,kBAAkB,CAAC;AA4IrE;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;OAEG;cACO,YAAY;EAgCtB,CAAC"}
@@ -37,8 +37,10 @@ const buildExecutionContext = (options) => {
37
37
  const dbName = getDbName(projectRoot, options);
38
38
  const env = typeof options['env'] === 'string' ? options['env'].trim() : undefined;
39
39
  const config = (typeof options['config'] === 'string' ? options['config'].trim() : undefined) ??
40
- (typeof options.wranglerConfig === 'string' ? options.wranglerConfig.trim() : undefined) ??
41
- (typeof options.wc === 'string' ? options.wc.trim() : undefined);
40
+ (typeof options['wranglerConfig'] === 'string'
41
+ ? options['wranglerConfig'].trim()
42
+ : undefined) ??
43
+ (typeof options['wc'] === 'string' ? options['wc'].trim() : undefined);
42
44
  const migrationsRelDir = isWorkerCommand
43
45
  ? path.join('database', 'migrations', 'd1')
44
46
  : WranglerConfig.getD1MigrationsDir(projectRoot, dbName);
@@ -2,7 +2,35 @@
2
2
  * Content Type Detection & Body Parsing Middleware
3
3
  * Detects content-type and parses non-JSON request bodies
4
4
  */
5
+ import type { IRequest } from '../Request';
5
6
  import type { Middleware } from '../../middleware/MiddlewareStack';
7
+ type ReadBodyResult = {
8
+ ok: true;
9
+ bytes: Buffer;
10
+ text?: string;
11
+ } | {
12
+ ok: false;
13
+ statusCode: 400 | 413;
14
+ message: string;
15
+ };
16
+ /**
17
+ * Attempt to recover a JSON string that was stored as raw text or bytes
18
+ * due to a missing or incorrect Content-Type header.
19
+ *
20
+ * Only plain objects `{...}` are considered for recovery — arrays, strings,
21
+ * and primitives are left unchanged. This minimizes false positives for
22
+ * legitimate text or binary payloads that happen to start with `{`.
23
+ */
24
+ export declare const tryRecoverTextJsonBody: (body: string) => string | Record<string, unknown>;
25
+ export declare const setRequestBody: (req: IRequest, rawResult: ReadBodyResult & {
26
+ ok: true;
27
+ }, contentType: string) => void;
28
+ /**
29
+ * Attempt to parse a string as a JSON object.
30
+ * Returns the parsed object on success, or null if the string is not
31
+ * a valid JSON object literal (plain object `{...}`, not array or primitive).
32
+ */
33
+ export declare const tryParseJsonObject: (text: string) => Record<string, unknown> | null;
6
34
  /**
7
35
  * Body parsing middleware
8
36
  * Automatically detects content-type and parses non-JSON bodies
@@ -1 +1 @@
1
- {"version":3,"file":"BodyParsingMiddleware.d.ts","sourceRoot":"","sources":["../../../../src/http/middleware/BodyParsingMiddleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAwV9D;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,UA4CnC,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"BodyParsingMiddleware.d.ts","sourceRoot":"","sources":["../../../../src/http/middleware/BodyParsingMiddleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,KAAK,cAAc,GACf;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AA+J1D;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,MAAM,KAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAapF,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,KAAK,QAAQ,EACb,WAAW,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,EACxC,aAAa,MAAM,KAClB,IA4BF,CAAC;AA+CF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAY3E,CAAC;AAwGF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,UA4CnC,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -164,7 +164,7 @@ const parseJsonBody = (text, contentType, res) => {
164
164
  * and primitives are left unchanged. This minimizes false positives for
165
165
  * legitimate text or binary payloads that happen to start with `{`.
166
166
  */
167
- const tryRecoverTextJsonBody = (body) => {
167
+ export const tryRecoverTextJsonBody = (body) => {
168
168
  const trimmed = body.trim();
169
169
  if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
170
170
  try {
@@ -179,7 +179,7 @@ const tryRecoverTextJsonBody = (body) => {
179
179
  }
180
180
  return body;
181
181
  };
182
- const setRequestBody = (req, rawResult, contentType) => {
182
+ export const setRequestBody = (req, rawResult, contentType) => {
183
183
  const isUrlEncoded = contentType.includes('application/x-www-form-urlencoded');
184
184
  const isText = contentType.startsWith('text/') || contentType.includes('application/xml');
185
185
  const text = getTextFromRaw(rawResult);
@@ -238,39 +238,79 @@ const reuseExistingRawBody = (req, res, contentType, existingBytes, existingText
238
238
  }
239
239
  return applyParsedRequestBody(req, res, rawResult, contentType);
240
240
  };
241
- const parseUrlEncodedBody = (text) => {
241
+ /**
242
+ * Attempt to parse a string as a JSON object.
243
+ * Returns the parsed object on success, or null if the string is not
244
+ * a valid JSON object literal (plain object `{...}`, not array or primitive).
245
+ */
246
+ export const tryParseJsonObject = (text) => {
247
+ const trimmed = text.trim();
248
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
249
+ return null;
250
+ try {
251
+ const parsed = JSON.parse(trimmed);
252
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
253
+ return parsed;
254
+ }
255
+ }
256
+ catch {
257
+ // Not valid JSON
258
+ }
259
+ return null;
260
+ };
261
+ /**
262
+ * Parse URL-encoded form data into a key-value record.
263
+ * Multiple values for the same key are collected into an array.
264
+ */
265
+ const parseUrlEncodedParams = (text) => {
242
266
  const out = {};
243
267
  const params = new URLSearchParams(text);
244
268
  for (const [key, value] of params.entries()) {
245
269
  const existing = out[key];
246
270
  if (existing === undefined) {
247
271
  out[key] = value;
248
- continue;
249
272
  }
250
- if (Array.isArray(existing)) {
273
+ else if (Array.isArray(existing)) {
251
274
  existing.push(value);
252
- continue;
253
275
  }
254
- out[key] = [existing, value];
276
+ else {
277
+ out[key] = [existing, value];
278
+ }
279
+ }
280
+ return out;
281
+ };
282
+ /**
283
+ * Detect and recover from a degenerate case where a JSON payload was sent
284
+ * with a url-encoded Content-Type header. URLSearchParams treats the entire
285
+ * JSON string as a single key with an empty value, so we try parsing that key.
286
+ */
287
+ const tryRecoverDegenerateJson = (out) => {
288
+ const keys = Object.keys(out);
289
+ if (keys.length !== 1 || out[keys[0]] !== '')
290
+ return null;
291
+ const maybeJson = keys[0].trim();
292
+ const parsed = tryParseJsonObject(maybeJson);
293
+ if (parsed !== null) {
294
+ return parsed;
255
295
  }
296
+ return null;
297
+ };
298
+ const parseUrlEncodedBody = (text) => {
299
+ // JSON-first recovery: parse raw text before URLSearchParams touches it.
300
+ // URLSearchParams splits on the first '=' (corrupting base64-padded values)
301
+ // and converts '+' to spaces, making encrypted/base64 fields unrecoverable.
302
+ const jsonResult = tryParseJsonObject(text);
303
+ if (jsonResult !== null) {
304
+ return jsonResult;
305
+ }
306
+ const out = parseUrlEncodedParams(text);
256
307
  // Recovery: if a JSON payload is sent with the wrong Content-Type header
257
308
  // (e.g. application/x-www-form-urlencoded instead of application/json),
258
309
  // URLSearchParams will treat the entire JSON string as a single key with
259
310
  // an empty value. Detect this degenerate case and fall back to JSON.parse.
260
- const keys = Object.keys(out);
261
- if (keys.length === 1 && out[keys[0]] === '') {
262
- const maybeJson = keys[0].trim();
263
- if (maybeJson.startsWith('{') && maybeJson.endsWith('}')) {
264
- try {
265
- const parsed = JSON.parse(maybeJson);
266
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
267
- return parsed;
268
- }
269
- }
270
- catch {
271
- // Not valid JSON — leave original result unchanged
272
- }
273
- }
311
+ const recovered = tryRecoverDegenerateJson(out);
312
+ if (recovered !== null) {
313
+ return recovered;
274
314
  }
275
315
  return out;
276
316
  };
@@ -1 +1 @@
1
- {"version":3,"file":"FileUploadMiddleware.d.ts","sourceRoot":"","sources":["../../../../src/http/middleware/FileUploadMiddleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAW9D;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,UA8DlC,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"FileUploadMiddleware.d.ts","sourceRoot":"","sources":["../../../../src/http/middleware/FileUploadMiddleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AA0H9D;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,UAsDlC,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -6,69 +6,154 @@ import { Env } from '../../config/env.js';
6
6
  import { Logger } from '../../config/logger.js';
7
7
  import { ErrorFactory } from '../../exceptions/ZintrustError.js';
8
8
  import { MultipartParser } from '../parsers/MultipartParser.js';
9
- import { MultipartParserRegistry } from '../parsers/MultipartParserRegistry.js';
9
+ import { MultipartParserRegistry, } from '../parsers/MultipartParserRegistry.js';
10
10
  /**
11
- * Extract content-type header safely
11
+ * Extract the original content-type header value.
12
+ *
13
+ * The value is NOT lowercased: multipart boundaries are case-sensitive, so a
14
+ * browser boundary containing uppercase characters must be preserved verbatim.
15
+ * Only the multipart check (MultipartParser.isMultipart) is case-insensitive.
12
16
  */
13
17
  const getContentType = (req) => {
14
18
  const contentType = req.getHeader('content-type');
15
19
  if (typeof contentType === 'string')
16
- return contentType.toLowerCase().trim();
20
+ return contentType.trim();
21
+ if (Array.isArray(contentType) && typeof contentType[0] === 'string') {
22
+ return contentType[0].trim();
23
+ }
17
24
  return '';
18
25
  };
26
+ /**
27
+ * Convert a buffered request body (Buffer or string) to a Buffer, if present.
28
+ * Worker adapters often expose a buffered body rather than a Node stream.
29
+ */
30
+ const getBufferedBody = (req) => {
31
+ const raw = req.getRaw();
32
+ const body = raw.body;
33
+ if (Buffer.isBuffer(body))
34
+ return body;
35
+ if (typeof body === 'string')
36
+ return Buffer.from(body);
37
+ return undefined;
38
+ };
39
+ /**
40
+ * A raw request is pipeable when it exposes a Node-style readable stream, which
41
+ * the registered streaming parser (e.g. busboy) consumes via `.pipe(...)`.
42
+ */
43
+ const isPipeable = (req) => {
44
+ const raw = req.getRaw();
45
+ return typeof raw.pipe === 'function';
46
+ };
47
+ /**
48
+ * Convert a bracket-notation field name to dotted notation.
49
+ * e.g. `doc[reg_cer]` -> `doc.reg_cer`, `doc[reg_cer][file]` -> `doc.reg_cer.file`.
50
+ * Empty brackets (`files[]`) collapse to the base name (`files`).
51
+ */
52
+ const toDottedName = (name) => name.replaceAll(/\[([^\]]*)\]/g, (_match, inner) => (inner === '' ? '' : `.${inner}`));
53
+ /**
54
+ * Add dotted-notation aliases for any bracket-notation keys, without removing
55
+ * the original field names. Existing keys are never overwritten.
56
+ */
57
+ const addDottedAliases = (record) => {
58
+ const out = { ...record };
59
+ for (const [key, value] of Object.entries(record)) {
60
+ if (!key.includes('['))
61
+ continue;
62
+ const dotted = toDottedName(key);
63
+ if (dotted !== '' && dotted !== key && !(dotted in out)) {
64
+ out[dotted] = value;
65
+ }
66
+ }
67
+ return out;
68
+ };
69
+ /**
70
+ * Parse a buffered multipart body using the built-in parser and the original
71
+ * (case-sensitive) boundary from the content-type header.
72
+ */
73
+ const parseBufferedMultipart = (body, contentType) => {
74
+ const boundary = MultipartParser.getBoundary(contentType);
75
+ if (boundary === undefined || boundary === '') {
76
+ throw ErrorFactory.createValidationError('Multipart upload is missing a boundary.', {
77
+ contentType,
78
+ });
79
+ }
80
+ return MultipartParser.parse(body, boundary);
81
+ };
82
+ /**
83
+ * Resolve parsed multipart data from the request.
84
+ *
85
+ * Prefers a buffered binary body (Worker adapters) parsed with the built-in
86
+ * parser. Falls back to a registered streaming parser only when the raw request
87
+ * is pipeable (Node-style request streams).
88
+ */
89
+ const resolveParsedMultipart = async (req, contentType, limits) => {
90
+ const bufferedBody = getBufferedBody(req);
91
+ if (bufferedBody !== undefined) {
92
+ return parseBufferedMultipart(bufferedBody, contentType);
93
+ }
94
+ if (isPipeable(req)) {
95
+ const provider = MultipartParserRegistry.get();
96
+ if (provider === null) {
97
+ throw ErrorFactory.createConfigError('Multipart upload parser is not configured.', {
98
+ contentType,
99
+ hint: 'Install @zintrust/storage to enable multipart/form-data uploads.',
100
+ });
101
+ }
102
+ return provider({ req: req.getRaw(), contentType, limits });
103
+ }
104
+ throw ErrorFactory.createValidationError('Multipart upload has no readable body.', {
105
+ contentType,
106
+ });
107
+ };
108
+ const isConfigError = (error) => typeof error === 'object' &&
109
+ error !== null &&
110
+ error.code === 'CONFIG_ERROR';
19
111
  /**
20
112
  * File upload middleware
21
113
  * Automatically parses multipart/form-data and makes files available
22
114
  */
23
115
  export const fileUploadMiddleware = async (req, _res, next) => {
24
116
  const contentType = getContentType(req);
25
- // Only process multipart/form-data requests
117
+ // Only process multipart/form-data requests (case-insensitive check).
26
118
  if (!MultipartParser.isMultipart(contentType)) {
27
119
  await next();
28
120
  return;
29
121
  }
30
- // Phase 4 default behavior: multipart requires external streaming parser.
31
- const provider = MultipartParserRegistry.get();
32
- if (provider === null) {
33
- throw ErrorFactory.createConfigError('Multipart upload parser is not configured.', {
34
- contentType,
35
- hint: 'Install @zintrust/storage to enable multipart/form-data uploads.',
36
- });
37
- }
38
122
  try {
39
- const maxFileSizeBytes = Env.getInt('MAX_FILE_SIZE', 50 * 1024 * 1024);
40
- const maxFiles = Env.getInt('MAX_FILES', 20);
41
- const maxFields = Env.getInt('MAX_FIELDS', 200);
42
- const maxFieldSizeBytes = Env.getInt('MAX_FIELD_SIZE', 128 * 1024);
43
- const parsed = await provider({
44
- req: req.getRaw(),
45
- contentType,
46
- limits: {
47
- maxFileSizeBytes,
48
- maxFiles,
49
- maxFields,
50
- maxFieldSizeBytes,
51
- },
52
- });
123
+ const limits = {
124
+ maxFileSizeBytes: Env.getInt('MAX_FILE_SIZE', 50 * 1024 * 1024),
125
+ maxFiles: Env.getInt('MAX_FILES', 20),
126
+ maxFields: Env.getInt('MAX_FIELDS', 200),
127
+ maxFieldSizeBytes: Env.getInt('MAX_FIELD_SIZE', 128 * 1024),
128
+ };
129
+ const parsed = await resolveParsedMultipart(req, contentType, limits);
130
+ // Add dotted aliases for bracket-notation names without dropping originals,
131
+ // so `doc[reg_cer]` is also addressable as `doc.reg_cer`.
132
+ const fields = addDottedAliases(parsed.fields);
133
+ const files = addDottedAliases(parsed.files);
53
134
  const currentBody = req.getBody?.();
54
135
  const updatedBody = typeof currentBody === 'object' && currentBody !== null ? currentBody : {};
55
136
  // Merge fields directly into the request body for ergonomics.
56
137
  // Files remain under __files for FileUpload helper compatibility.
57
138
  req.setBody({
58
139
  ...updatedBody,
59
- ...parsed.fields,
60
- __files: parsed.files,
140
+ ...fields,
141
+ __files: files,
61
142
  });
62
143
  if (Env.getBool('ZIN_DEBUG_FILE_UPLOAD', false)) {
63
144
  Logger.debug('[File Upload] Successfully parsed multipart data', {
64
- fieldsCount: Object.keys(parsed.fields).length,
65
- filesCount: Object.keys(parsed.files).length,
145
+ fieldsCount: Object.keys(fields).length,
146
+ filesCount: Object.keys(files).length,
66
147
  });
67
148
  }
68
149
  }
69
150
  catch (error) {
151
+ // A missing streaming parser is a hard configuration error (e.g. multipart
152
+ // arrives as a Node stream but @zintrust/storage is not installed): surface it.
153
+ if (isConfigError(error))
154
+ throw error;
155
+ // Other parse failures are non-fatal: log and continue with the unparsed body.
70
156
  Logger.error('[File Upload] Error parsing multipart data', error);
71
- // Continue with unparsed body
72
157
  }
73
158
  await next();
74
159
  };
@@ -1 +1 @@
1
- {"version":3,"file":"BodyParsers.d.ts","sourceRoot":"","sources":["../../../../src/http/parsers/BodyParsers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,UAAU,gBAAgB;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,OAAO,EAAE,CAAC;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAAC;CAChD;AAiLD,eAAO,MAAM,WAAW;;;;IAKtB;;OAEG;cACO,WAAW,EAAE;IAIvB;;OAEG;4BACqB,MAAM,GAAG,WAAW,GAAG,SAAS;IAIxD;;OAEG;uBACgB,MAAM,QAAQ,MAAM,GAAG,MAAM,GAAG,gBAAgB;EAQnE,CAAC;AAEH,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"BodyParsers.d.ts","sourceRoot":"","sources":["../../../../src/http/parsers/BodyParsers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,UAAU,gBAAgB;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,OAAO,EAAE,CAAC;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAAC;CAChD;AAoND,eAAO,MAAM,WAAW;;;;IAKtB;;OAEG;cACO,WAAW,EAAE;IAIvB;;OAEG;4BACqB,MAAM,GAAG,WAAW,GAAG,SAAS;IAIxD;;OAEG;uBACgB,MAAM,QAAQ,MAAM,GAAG,MAAM,GAAG,gBAAgB;EAQnE,CAAC;AAEH,eAAe,WAAW,CAAC"}
@@ -27,6 +27,48 @@ const tryRecoverJsonBody = (result) => {
27
27
  }
28
28
  return undefined;
29
29
  };
30
+ /**
31
+ * Try to parse raw text as JSON before URLSearchParams touches it.
32
+ * This handles cases where JSON is sent with application/x-www-form-urlencoded Content-Type,
33
+ * which would be corrupted by URLSearchParams (splits on '=', converts '+' to spaces).
34
+ */
35
+ const tryParseJsonRaw = (text) => {
36
+ const trimmed = text.trim();
37
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
38
+ return undefined;
39
+ try {
40
+ const parsed = JSON.parse(trimmed);
41
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
42
+ return parsed;
43
+ }
44
+ }
45
+ catch {
46
+ // Not valid JSON
47
+ }
48
+ return undefined;
49
+ };
50
+ /**
51
+ * Parse URL-encoded string into key-value pairs, handling duplicate keys as arrays.
52
+ */
53
+ const parseUrlEncodedParams = (text) => {
54
+ const params = new URLSearchParams(text);
55
+ const result = {};
56
+ for (const [key, value] of params.entries()) {
57
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
58
+ const existing = result[key];
59
+ if (Array.isArray(existing)) {
60
+ existing.push(value);
61
+ }
62
+ else {
63
+ result[key] = [existing, value];
64
+ }
65
+ }
66
+ else {
67
+ result[key] = value;
68
+ }
69
+ }
70
+ return result;
71
+ };
30
72
  /**
31
73
  * URL-encoded form data parser
32
74
  * Content-Type: application/x-www-form-urlencoded
@@ -38,25 +80,14 @@ const FormDataParser = {
38
80
  const text = typeof body === 'string' ? body : body.toString('utf-8');
39
81
  if (!text.trim())
40
82
  return { ok: true, data: {} };
41
- const params = new URLSearchParams(text);
42
- const result = {};
43
- for (const [key, value] of params.entries()) {
44
- if (Object.prototype.hasOwnProperty.call(result, key)) {
45
- // Handle multiple values with same key
46
- const existing = result[key];
47
- if (Array.isArray(existing)) {
48
- existing.push(value);
49
- }
50
- else {
51
- result[key] = [existing, value];
52
- }
53
- }
54
- else {
55
- result[key] = value;
56
- }
83
+ // JSON-first recovery: parse raw text before URLSearchParams touches it.
84
+ const rawJsonResult = tryParseJsonRaw(text);
85
+ if (rawJsonResult !== undefined) {
86
+ return { ok: true, data: rawJsonResult };
57
87
  }
58
- // Recovery: if a JSON payload is sent with the wrong Content-Type header,
59
- // URLSearchParams will treat the entire JSON string as a single key with
88
+ const result = parseUrlEncodedParams(text);
89
+ // Recovery: if a JSON payload was sent with wrong Content-Type,
90
+ // URLSearchParams treats the whole JSON string as a single key with
60
91
  // an empty value. Detect this degenerate case and fall back to JSON.parse.
61
92
  const recovered = tryRecoverJsonBody(result);
62
93
  if (recovered !== undefined) {
@@ -17,7 +17,9 @@ export declare const parseMultipartFormData: (body: Buffer | string, boundary: s
17
17
  */
18
18
  export declare const MultipartParser: Readonly<{
19
19
  /**
20
- * Check if content-type is multipart/form-data
20
+ * Check if content-type is multipart/form-data.
21
+ * The check is case-insensitive so callers can preserve the original
22
+ * content-type value (and its case-sensitive boundary) for parsing.
21
23
  */
22
24
  isMultipart(contentType: string): boolean;
23
25
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"MultipartParser.d.ts","sourceRoot":"","sources":["../../../../src/http/parsers/MultipartParser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;CACvC;AAwJD;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,GAAG,MAAM,EACrB,UAAU,MAAM,KACf,mBAaF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe;IAC1B;;OAEG;6BACsB,MAAM,GAAG,OAAO;IAIzC;;OAEG;6BACsB,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpD;;OAEG;gBACS,MAAM,GAAG,MAAM,YAAY,MAAM,GAAG,mBAAmB;EAGnE,CAAC;AAEH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"MultipartParser.d.ts","sourceRoot":"","sources":["../../../../src/http/parsers/MultipartParser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;CACvC;AAwJD;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,GAAG,MAAM,EACrB,UAAU,MAAM,KACf,mBAaF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;6BACsB,MAAM,GAAG,OAAO;IAIzC;;OAEG;6BACsB,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpD;;OAEG;gBACS,MAAM,GAAG,MAAM,YAAY,MAAM,GAAG,mBAAmB;EAGnE,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -135,10 +135,12 @@ export const parseMultipartFormData = (body, boundary) => {
135
135
  */
136
136
  export const MultipartParser = Object.freeze({
137
137
  /**
138
- * Check if content-type is multipart/form-data
138
+ * Check if content-type is multipart/form-data.
139
+ * The check is case-insensitive so callers can preserve the original
140
+ * content-type value (and its case-sensitive boundary) for parsing.
139
141
  */
140
142
  isMultipart(contentType) {
141
- return contentType.includes('multipart/form-data');
143
+ return contentType.toLowerCase().includes('multipart/form-data');
142
144
  },
143
145
  /**
144
146
  * Get boundary from content-type header
package/src/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @zintrust/core v2.5.8
2
+ * @zintrust/core v2.7.1
3
3
  *
4
4
  * ZinTrust Framework - Production-Grade TypeScript Backend
5
5
  * Built for performance, type safety, and exceptional developer experience
6
6
  *
7
7
  * Build Information:
8
- * Built: 2026-06-12T18:44:49.274Z
8
+ * Built: 2026-06-15T10:44:07.685Z
9
9
  * Node: >=20.0.0
10
10
  * License: MIT
11
11
  *
@@ -21,7 +21,7 @@
21
21
  * Available at runtime for debugging and health checks
22
22
  */
23
23
  export const ZINTRUST_VERSION = '0.1.41';
24
- export const ZINTRUST_BUILD_DATE = '2026-06-12T18:44:49.232Z'; // Replaced during build
24
+ export const ZINTRUST_BUILD_DATE = '2026-06-15T10:44:07.641Z'; // Replaced during build
25
25
  export { Application } from './boot/Application.js';
26
26
  export { AwsSigV4 } from './common/index.js';
27
27
  export { SignedRequest } from './security/SignedRequest.js';
@@ -39,6 +39,13 @@ type D1Env = {
39
39
  ZT_MAX_PARAMS?: string;
40
40
  ZT_D1_STATEMENTS_JSON?: string;
41
41
  };
42
+ /**
43
+ * Normalize bind parameters before passing to D1's bind().
44
+ *
45
+ * D1 rejects JS objects and needs integer-valued numbers serialized for TEXT
46
+ * column compatibility, Dates as ISO strings, while blobs must be untouched.
47
+ */
48
+ export declare const normalizeParams: (params: unknown[]) => unknown[];
42
49
  export declare const ZintrustD1Proxy: Readonly<{
43
50
  _ZINTRUST_CLOUDFLARE_D1_PROXY_VERSION: "0.1.15";
44
51
  _ZINTRUST_CLOUDFLARE_D1_PROXY_BUILD_DATE: "__BUILD_DATE__";
@@ -1 +1 @@
1
- {"version":3,"file":"ZintrustD1Proxy.d.ts","sourceRoot":"","sources":["../../../../src/proxy/d1/ZintrustD1Proxy.ts"],"names":[],"mappings":"AAUA,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC;AAEjD,KAAK,qBAAqB,GAAG;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,GAAG,EAAE;QACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACtC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;QACrE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;QAChE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC;KAChG,CAAC;IACF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF,CAAC;AAEF,KAAK,WAAW,CAAC,CAAC,IAAI;IACpB,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;CACf,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,mBAAmB,CAAC;IACpD,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,KAAK,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5C,GAAG,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;CACjC,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,mBAAmB,CAAC;CAC/C,CAAC;AAEF,KAAK,KAAK,GAAG;IACX,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAsWF,eAAO,MAAM,eAAe;;;mBAGL,OAAO,OAAO,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC;EAqB5D,CAAC;AAEH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"ZintrustD1Proxy.d.ts","sourceRoot":"","sources":["../../../../src/proxy/d1/ZintrustD1Proxy.ts"],"names":[],"mappings":"AAUA,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC;AAEjD,KAAK,qBAAqB,GAAG;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,GAAG,EAAE;QACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACtC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;QACrE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;QAChE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC;KAChG,CAAC;IACF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF,CAAC;AAEF,KAAK,WAAW,CAAC,CAAC,IAAI;IACpB,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;CACf,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,mBAAmB,CAAC;IACpD,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,KAAK,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5C,GAAG,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;CACjC,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,mBAAmB,CAAC;CAC/C,CAAC;AAEF,KAAK,KAAK,GAAG;IACX,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AA2LF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,OAAO,EAAE,KAAG,OAAO,EAe1D,CAAC;AAsJF,eAAO,MAAM,eAAe;;;mBAGL,OAAO,OAAO,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC;EAqB5D,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -157,7 +157,7 @@ const resolveSqlRequest = async (request, env) => {
157
157
  * D1 rejects JS objects and needs integer-valued numbers serialized for TEXT
158
158
  * column compatibility, Dates as ISO strings, while blobs must be untouched.
159
159
  */
160
- const normalizeParams = (params) => {
160
+ export const normalizeParams = (params) => {
161
161
  if (!Array.isArray(params))
162
162
  return params;
163
163
  return params.map((value) => {
@@ -1 +1 @@
1
- {"version":3,"file":"CloudflareAdapter.d.ts","sourceRoot":"","sources":["../../../../src/runtime/adapters/CloudflareAdapter.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,aAAa,EAGb,cAAc,EACf,MAAM,yBAAyB,CAAC;AAIjC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;IAC5B;;OAEG;mBACY,aAAa,GAAG,cAAc;IA8D7C;;;OAGG;qBACc,OAAO;IAKxB;;;OAGG;qBACc,MAAM,GAAG,OAAO;EAIjC,CAAC;AA8KH;;;;GAIG;AACH,MAAM,WAAW,iBAAkB,SAAQ,OAAO;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACjC,EAAE,CAAC,EAAE;QACH,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH"}
1
+ {"version":3,"file":"CloudflareAdapter.d.ts","sourceRoot":"","sources":["../../../../src/runtime/adapters/CloudflareAdapter.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,aAAa,EAGb,cAAc,EACf,MAAM,yBAAyB,CAAC;AAIjC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;IAC5B;;OAEG;mBACY,aAAa,GAAG,cAAc;IA8D7C;;;OAGG;qBACc,OAAO;IAKxB;;;OAGG;qBACc,MAAM,GAAG,OAAO;EAIjC,CAAC;AAyMH;;;;GAIG;AACH,MAAM,WAAW,iBAAkB,SAAQ,OAAO;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACjC,EAAE,CAAC,EAAE;QACH,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH"}
@@ -74,6 +74,26 @@ export const CloudflareAdapter = Object.freeze({
74
74
  return globalThis.env?.[namespace] ?? null;
75
75
  },
76
76
  });
77
+ /**
78
+ * Read the Cloudflare request body in a content-type aware way.
79
+ *
80
+ * Multipart bodies are read as a binary Buffer (via arrayBuffer) so uploaded
81
+ * files are not corrupted by text decoding. All other bodies are read as text,
82
+ * preserving the previous behavior for JSON / urlencoded / text payloads.
83
+ */
84
+ async function readCloudflareRequestBody(request, platformRequest) {
85
+ if (request.method === 'GET' || request.method === 'HEAD')
86
+ return null;
87
+ const rawContentType = platformRequest.headers['content-type'];
88
+ const contentType = Array.isArray(rawContentType)
89
+ ? (rawContentType[0] ?? '')
90
+ : (rawContentType ?? '');
91
+ if (contentType.toLowerCase().includes('multipart/form-data')) {
92
+ const arrayBuffer = await request.arrayBuffer();
93
+ return Buffer.from(arrayBuffer);
94
+ }
95
+ return request.text();
96
+ }
77
97
  /**
78
98
  * Handle Cloudflare request
79
99
  */
@@ -82,8 +102,10 @@ async function handleCloudflareRequest(adapter, config, logger, event) {
82
102
  const request = event;
83
103
  // Parse incoming request
84
104
  const platformRequest = adapter.parseRequest(request);
85
- // Read request body
86
- const body = request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : null;
105
+ // Read request body. Multipart bodies may contain binary bytes, so read them
106
+ // as an ArrayBuffer and keep a binary Buffer — converting to text first would
107
+ // corrupt uploaded files. Other content types remain text for downstream parsing.
108
+ const body = await readCloudflareRequestBody(request, platformRequest);
87
109
  platformRequest.body = body;
88
110
  // Create mock Node.js request/response objects
89
111
  const { req, res, responseData } = createMockHttpObjects(platformRequest);
@@ -175,7 +175,7 @@ function formatDenoResponse(response) {
175
175
  */
176
176
  function getDenoEnvironment() {
177
177
  // @ts-ignore - Deno.env is available in Deno runtime
178
- const env = (typeof Deno === 'undefined' ? {} : Deno.env.toObject?.() ?? {});
178
+ const env = (typeof Deno === 'undefined' ? {} : (Deno.env.toObject?.() ?? {}));
179
179
  return {
180
180
  nodeEnv: (env['DENO_ENV'] ?? 'production'),
181
181
  runtime: 'deno',
@@ -4,6 +4,7 @@ export type R2Config = {
4
4
  accessKeyId: string;
5
5
  secretAccessKey: string;
6
6
  endpoint?: string;
7
+ url?: string;
7
8
  binding?: string;
8
9
  };
9
10
  export type R2MultipartUploadInfo = {
@@ -1 +1 @@
1
- {"version":3,"file":"R2.d.ts","sourceRoot":"","sources":["../../../../../src/tools/storage/drivers/R2.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AA6EF,eAAO,MAAM,QAAQ;kCAET,QAAQ,OACX,MAAM,YACD,OAAO,GAChB,OAAO,CAAC,qBAAqB,CAAC;uBAOvB,QAAQ,OACX,MAAM,YACD,MAAM,cACJ,MAAM,SACX,OAAO,YACJ,OAAO,GAChB,OAAO,CAAC,cAAc,CAAC;oCAOhB,QAAQ,OACX,MAAM,YACD,MAAM,iBACD,cAAc,EAAE,GAC9B,OAAO,CAAC,MAAM,CAAC;iCAOiB,QAAQ,OAAO,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBAMxE,QAAQ,OAAO,MAAM,WAAW,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;gBAyBjE,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;mBAmBpC,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAmBxC,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBAmB9C,QAAQ,OAAO,MAAM,GAAG,MAAM;oBAQhC,QAAQ,OACX,MAAM,YACD;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE,GACvD,MAAM;EAgBT,CAAC;AAEH,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"R2.d.ts","sourceRoot":"","sources":["../../../../../src/tools/storage/drivers/R2.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AA6EF,eAAO,MAAM,QAAQ;kCAET,QAAQ,OACX,MAAM,YACD,OAAO,GAChB,OAAO,CAAC,qBAAqB,CAAC;uBAOvB,QAAQ,OACX,MAAM,YACD,MAAM,cACJ,MAAM,SACX,OAAO,YACJ,OAAO,GAChB,OAAO,CAAC,cAAc,CAAC;oCAOhB,QAAQ,OACX,MAAM,YACD,MAAM,iBACD,cAAc,EAAE,GAC9B,OAAO,CAAC,MAAM,CAAC;iCAOiB,QAAQ,OAAO,MAAM,YAAY,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBAMxE,QAAQ,OAAO,MAAM,WAAW,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;gBA4BjE,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;mBAmBpC,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAmBxC,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBAmB9C,QAAQ,OAAO,MAAM,GAAG,MAAM;oBAchC,QAAQ,OACX,MAAM,YACD;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE,GACvD,MAAM;EAgBT,CAAC;AAEH,eAAe,QAAQ,CAAC"}
@@ -77,7 +77,10 @@ export const R2Driver = Object.freeze({
77
77
  endpoint: config.endpoint,
78
78
  usePathStyle: true,
79
79
  };
80
- return S3Driver.put(s3Config, key, content);
80
+ await S3Driver.put(s3Config, key, content);
81
+ // Return the public-facing URL (custom domain when configured), not the
82
+ // path-style signing URL produced by S3Driver.put.
83
+ return R2Driver.url(config, key);
81
84
  },
82
85
  async get(config, key) {
83
86
  if (hasWorkersBucketBinding(config)) {
@@ -128,6 +131,12 @@ export const R2Driver = Object.freeze({
128
131
  return S3Driver.delete(s3Config, key);
129
132
  },
130
133
  url(config, key) {
134
+ // Prefer an explicit public base URL (custom domain CNAME'd to the bucket root):
135
+ // no bucket segment, since the domain already resolves to the bucket. The signing
136
+ // `endpoint` (used by tempUrl/put/get) stays path-style and independent.
137
+ const publicBase = config.url?.trim() ?? '';
138
+ if (publicBase !== '')
139
+ return `${publicBase.replace(/\/$/, '')}/${key}`;
131
140
  if (config.endpoint !== undefined && config.endpoint.trim() !== '') {
132
141
  return `${config.endpoint.replace(/\/$/, '')}/${config.bucket}/${key}`;
133
142
  }
@@ -5,6 +5,7 @@ export type S3Config = {
5
5
  secretAccessKey: string;
6
6
  endpoint?: string;
7
7
  usePathStyle?: boolean;
8
+ url?: string;
8
9
  };
9
10
  export type GS3Cred = {
10
11
  accessKeyId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"S3.d.ts","sourceRoot":"","sources":["../../../../../src/tools/storage/drivers/S3.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,CAAC;AAwGF,eAAO,MAAM,QAAQ;gBACD,QAAQ,OAAO,MAAM,WAAW,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;gBAkDjE,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;mBA8CpC,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAsCxC,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBAwC9C,QAAQ,OAAO,MAAM,GAAG,MAAM;oBAOhC,QAAQ,OACX,MAAM,YACD;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE,GACvD,MAAM;EAqDT,CAAC;AAEH,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"S3.d.ts","sourceRoot":"","sources":["../../../../../src/tools/storage/drivers/S3.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IAIvB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,CAAC;AAwGF,eAAO,MAAM,QAAQ;gBACD,QAAQ,OAAO,MAAM,WAAW,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;gBAkDjE,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;mBA8CpC,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAsCxC,QAAQ,OAAO,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;gBAwC9C,QAAQ,OAAO,MAAM,GAAG,MAAM;oBAYhC,QAAQ,OACX,MAAM,YACD;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE,GACvD,MAAM;EAqDT,CAAC;AAEH,eAAe,QAAQ,CAAC"}
@@ -209,6 +209,11 @@ export const S3Driver = Object.freeze({
209
209
  }
210
210
  },
211
211
  url(config, key) {
212
+ // Prefer an explicit public base URL (custom domain mapped to the bucket root):
213
+ // no bucket segment, since the domain already resolves to the bucket.
214
+ const publicBase = config.url?.trim() ?? '';
215
+ if (publicBase !== '')
216
+ return `${publicBase.replace(/\/$/, '')}/${key}`;
212
217
  const endpoint = config.endpoint?.trim() ?? '';
213
218
  if (endpoint !== '')
214
219
  return `${endpoint.replace(/\/$/, '')}/${config.bucket}/${key}`;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/tools/storage/index.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC;AAErD,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,cAAc,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;CAAE,CAAC;AAuFrE,eAAO,MAAM,OAAO;mBACH,MAAM,GAAG,WAAW;cAuCnB,MAAM,GAAG,SAAS,QAAQ,MAAM,YAAY,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;cAW7E,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;iBAW/C,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;iBASnD,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cASzD,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,MAAM;kBAY/B,MAAM,GAAG,SAAS,QAAQ,MAAM,YAAY,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;EAqBhG,CAAC;AAEH,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/tools/storage/index.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC;AAErD,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,cAAc,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;CAAE,CAAC;AAwFrE,eAAO,MAAM,OAAO;mBACH,MAAM,GAAG,WAAW;cAuCnB,MAAM,GAAG,SAAS,QAAQ,MAAM,YAAY,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;cAW7E,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;iBAW/C,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;iBASnD,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cASzD,MAAM,GAAG,SAAS,QAAQ,MAAM,GAAG,MAAM;kBAY/B,MAAM,GAAG,SAAS,QAAQ,MAAM,YAAY,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;EAqBhG,CAAC;AAEH,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC"}
@@ -14,6 +14,7 @@ const normalizers = {
14
14
  accessKeyId: String(raw['accessKeyId'] ?? raw['key'] ?? ''),
15
15
  secretAccessKey: String(raw['secretAccessKey'] ?? raw['secret'] ?? ''),
16
16
  endpoint: typeof raw['endpoint'] === 'string' ? raw['endpoint'] : undefined,
17
+ url: typeof raw['url'] === 'string' ? raw['url'] : undefined,
17
18
  usePathStyle: Boolean(raw['usePathStyle'] ?? raw['usePathStyleUrl'] ?? false),
18
19
  }),
19
20
  r2: (raw) => ({
@@ -1,4 +1,15 @@
1
1
  import type { WorkerModule } from './processor-registry';
2
+ export declare const csvEnvSet: (key: string) => Set<string>;
3
+ type DrainTarget = {
4
+ queueName: string;
5
+ processorSpec: string;
6
+ };
7
+ /**
8
+ * Narrow drain targets by env allowlist/denylist:
9
+ * - `WORKER_DRAIN_QUEUES`: if set, only these queues are drained.
10
+ * - `WORKER_DRAIN_EXCLUDE_QUEUES`: these queues are always skipped.
11
+ */
12
+ export declare const filterDrainTargetsByEnv: (targets: DrainTarget[]) => DrainTarget[];
2
13
  export type AppWorkerDefinition = Readonly<{
3
14
  name: string;
4
15
  queueName: string;
@@ -8,6 +19,13 @@ export type AppWorkerDefinition = Readonly<{
8
19
  concurrency: number;
9
20
  processorSpec: string;
10
21
  }>;
22
+ /**
23
+ * Resolve which queues this Worker should drain: app manifest workers that have a bundled
24
+ * processor and are not disabled. The backend lifecycle registry is authoritative for
25
+ * start/stop — a worker explicitly `stopped` there is skipped; otherwise the manifest's
26
+ * `autoStart` decides.
27
+ */
28
+ export declare const resolveDrainTargets: (appWorkerDefinitions: AppWorkerDefinition[], workerModules: ReadonlyArray<WorkerModule>) => Promise<DrainTarget[]>;
11
29
  /**
12
30
  * Ensure a drain loop is running. Returns immediately. If a loop is already active this is
13
31
  * a no-op (concurrent-PING guard). The returned promise resolves when the active loop ends
@@ -15,4 +33,5 @@ export type AppWorkerDefinition = Readonly<{
15
33
  */
16
34
  export declare const ensureDraining: (appWorkerDefinitions: AppWorkerDefinition[], workerModules: ReadonlyArray<WorkerModule>) => Promise<void>;
17
35
  export declare const isDraining: () => boolean;
36
+ export {};
18
37
  //# sourceMappingURL=drain.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drain.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/drain.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAgCvE,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AAsKH;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,sBAAsB,mBAAmB,EAAE,EAC3C,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,OAAO,CAAC,IAAI,CAUd,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,OAA+B,CAAC"}
1
+ {"version":3,"file":"drain.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/drain.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAoBvE,eAAO,MAAM,SAAS,GAAI,KAAK,MAAM,KAAG,GAAG,CAAC,MAAM,CAM/C,CAAC;AAEJ,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,WAAW,EAAE,KAAG,WAAW,EAW3E,CAAC;AASF,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,sBAAsB,mBAAmB,EAAE,EAC3C,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,OAAO,CAAC,WAAW,EAAE,CA6BvB,CAAC;AA8HF;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,sBAAsB,mBAAmB,EAAE,EAC3C,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,OAAO,CAAC,IAAI,CAUd,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,OAA+B,CAAC"}
@@ -21,13 +21,32 @@ const intEnv = (key, fallback) => {
21
21
  const parsed = Number.parseInt(Env.get(key, '').trim(), 10);
22
22
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
23
23
  };
24
+ export const csvEnvSet = (key) => new Set(Env.get(key, '')
25
+ .split(',')
26
+ .map((value) => value.trim())
27
+ .filter((value) => value.length > 0));
28
+ /**
29
+ * Narrow drain targets by env allowlist/denylist:
30
+ * - `WORKER_DRAIN_QUEUES`: if set, only these queues are drained.
31
+ * - `WORKER_DRAIN_EXCLUDE_QUEUES`: these queues are always skipped.
32
+ */
33
+ export const filterDrainTargetsByEnv = (targets) => {
34
+ const onlyQueues = csvEnvSet('WORKER_DRAIN_QUEUES');
35
+ const excludeQueues = csvEnvSet('WORKER_DRAIN_EXCLUDE_QUEUES');
36
+ return targets.filter((target) => {
37
+ if (onlyQueues.size > 0 && !onlyQueues.has(target.queueName)) {
38
+ return false;
39
+ }
40
+ return !excludeQueues.has(target.queueName);
41
+ });
42
+ };
24
43
  /**
25
44
  * Resolve which queues this Worker should drain: app manifest workers that have a bundled
26
45
  * processor and are not disabled. The backend lifecycle registry is authoritative for
27
46
  * start/stop — a worker explicitly `stopped` there is skipped; otherwise the manifest's
28
47
  * `autoStart` decides.
29
48
  */
30
- const resolveDrainTargets = async (appWorkerDefinitions, workerModules) => {
49
+ export const resolveDrainTargets = async (appWorkerDefinitions, workerModules) => {
31
50
  let statusByQueue = new Map();
32
51
  try {
33
52
  const registered = await listWorkers();
@@ -50,7 +69,7 @@ const resolveDrainTargets = async (appWorkerDefinitions, workerModules) => {
50
69
  targets.push({ queueName: definition.queueName, processorSpec: definition.processorSpec });
51
70
  }
52
71
  }
53
- return targets;
72
+ return filterDrainTargetsByEnv(targets);
54
73
  };
55
74
  const toJob = (queueName, pulled) => {
56
75
  return {