@web-ts-toolkit/express-runtime 0.43.0 → 0.44.0
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/README.md +80 -50
- package/{chunk-UPFG3S34.mjs → chunk-KBNC4WIR.mjs} +378 -101
- package/{chunk-VPFBKM2K.mjs → chunk-QNRHWPTO.mjs} +13 -10
- package/cli-api.d.mts +166 -18
- package/cli-api.d.ts +166 -18
- package/cli-api.js +385 -109
- package/cli-api.mjs +4 -4
- package/{cli-utils-4POUMJN7.mjs → cli-utils-IN67EHOM.mjs} +2 -2
- package/cli.js +385 -109
- package/index.d.mts +16 -6
- package/index.d.ts +16 -6
- package/index.js +11 -10
- package/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import serverless from "serverless-http";
|
|
|
5
5
|
|
|
6
6
|
// src/numeric-validation.ts
|
|
7
7
|
var MAX_INTEGER_OPTION_VALUE = Number.MAX_SAFE_INTEGER;
|
|
8
|
+
var MAX_TIMER_DURATION_MS = 2147483647;
|
|
8
9
|
function validateFiniteInteger(value, options) {
|
|
9
10
|
const min = options.min ?? Number.MIN_SAFE_INTEGER;
|
|
10
11
|
const max = options.max ?? MAX_INTEGER_OPTION_VALUE;
|
|
@@ -35,6 +36,9 @@ function parsePortValue(value, name) {
|
|
|
35
36
|
}
|
|
36
37
|
return value;
|
|
37
38
|
}
|
|
39
|
+
function validateTimerDuration(value, name) {
|
|
40
|
+
return validateFiniteInteger(value, { name, min: 0, max: MAX_TIMER_DURATION_MS });
|
|
41
|
+
}
|
|
38
42
|
|
|
39
43
|
// src/index.ts
|
|
40
44
|
var defaultLogger = {
|
|
@@ -103,20 +107,19 @@ function defaultRequestHook(req, maxBodyBytes = 1024 * 1024, logger = defaultLog
|
|
|
103
107
|
logger.debug?.(" Skipping oversized serverless body for content-type parsing");
|
|
104
108
|
return;
|
|
105
109
|
}
|
|
106
|
-
const bodyStr = req.body.toString("utf8");
|
|
107
110
|
const contentType = getHeaderValue(req.headers, "content-type");
|
|
108
111
|
if (isJsonMediaType(contentType)) {
|
|
109
112
|
if (isReadableRequest(req)) {
|
|
110
113
|
return;
|
|
111
114
|
}
|
|
112
115
|
try {
|
|
113
|
-
req.body = JSON.parse(
|
|
116
|
+
req.body = JSON.parse(req.body.toString("utf8"));
|
|
114
117
|
} catch (_error) {
|
|
115
118
|
void _error;
|
|
116
119
|
}
|
|
117
120
|
return;
|
|
118
121
|
}
|
|
119
|
-
req.body =
|
|
122
|
+
req.body = req.body.toString("utf8");
|
|
120
123
|
}
|
|
121
124
|
function getHeaderValue(headers, name) {
|
|
122
125
|
if (!headers) return "";
|
|
@@ -216,11 +219,7 @@ function startLocalServer(app, options = {}) {
|
|
|
216
219
|
const logger = options.logger ?? defaultLogger;
|
|
217
220
|
const port = normalizePort(options.port);
|
|
218
221
|
const host = options.host ?? process.env.HOST ?? "0.0.0.0";
|
|
219
|
-
const shutdownTimeout =
|
|
220
|
-
name: "shutdownTimeout",
|
|
221
|
-
min: 0,
|
|
222
|
-
max: MAX_INTEGER_OPTION_VALUE
|
|
223
|
-
});
|
|
222
|
+
const shutdownTimeout = validateTimerDuration(options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT, "shutdownTimeout");
|
|
224
223
|
const server = http.createServer(app);
|
|
225
224
|
app.set("port", port);
|
|
226
225
|
let state = "initializing";
|
|
@@ -392,6 +391,7 @@ function startLocalServer(app, options = {}) {
|
|
|
392
391
|
done();
|
|
393
392
|
}
|
|
394
393
|
});
|
|
394
|
+
let shutdownFailed = false;
|
|
395
395
|
let shutdownError;
|
|
396
396
|
try {
|
|
397
397
|
if (options.onShutdown) {
|
|
@@ -399,9 +399,10 @@ function startLocalServer(app, options = {}) {
|
|
|
399
399
|
}
|
|
400
400
|
} catch (err) {
|
|
401
401
|
logger.error("onShutdown hook failed:", err);
|
|
402
|
+
shutdownFailed = true;
|
|
402
403
|
shutdownError = err;
|
|
403
404
|
}
|
|
404
|
-
if (
|
|
405
|
+
if (shutdownFailed) {
|
|
405
406
|
state = "failed";
|
|
406
407
|
if (options.exitAfterShutdown) {
|
|
407
408
|
process.exit(1);
|
|
@@ -420,7 +421,7 @@ function startLocalServer(app, options = {}) {
|
|
|
420
421
|
};
|
|
421
422
|
if (options.signals !== false) {
|
|
422
423
|
const list = options.signals === void 0 || options.signals === true ? DEFAULT_SIGNALS : options.signals;
|
|
423
|
-
for (const sig of list) {
|
|
424
|
+
for (const sig of new Set(list)) {
|
|
424
425
|
const handler = () => {
|
|
425
426
|
void shutdown().catch(() => {
|
|
426
427
|
});
|
|
@@ -477,8 +478,10 @@ function startLocalServer(app, options = {}) {
|
|
|
477
478
|
|
|
478
479
|
export {
|
|
479
480
|
MAX_INTEGER_OPTION_VALUE,
|
|
481
|
+
MAX_TIMER_DURATION_MS,
|
|
480
482
|
validateFiniteInteger,
|
|
481
483
|
parsePortValue,
|
|
484
|
+
validateTimerDuration,
|
|
482
485
|
createExpressApp,
|
|
483
486
|
defaultRequestHook,
|
|
484
487
|
createServerlessHandler,
|
package/cli-api.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LocalServerOptions, LocalServer } from './index.mjs';
|
|
2
|
-
import { watch, existsSync } from 'node:fs';
|
|
2
|
+
import { watch, mkdtempSync, lstatSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
3
3
|
import { ChildProcess, fork } from 'node:child_process';
|
|
4
4
|
import { Response, Request, Express } from 'express';
|
|
5
5
|
import 'node:http';
|
|
@@ -25,7 +25,7 @@ interface DevArgs {
|
|
|
25
25
|
watch: string[];
|
|
26
26
|
/** File extensions to watch (default: ts,js,mjs,cjs,json). */
|
|
27
27
|
watchExt: string[];
|
|
28
|
-
/** Debounce delay (ms) before restarting on file change (default: 500). */
|
|
28
|
+
/** Debounce delay (ms) before restarting on file change (default: 500). Must be a finite integer in `0..2147483647` (Node timer limit); `0` restarts without debouncing. */
|
|
29
29
|
watchDelay: number;
|
|
30
30
|
}
|
|
31
31
|
interface BuildArgs {
|
|
@@ -125,6 +125,13 @@ declare function loadEnvFiles(paths: string[]): void;
|
|
|
125
125
|
* loading the app module. Each module is `require()`-ed, running its
|
|
126
126
|
* side effects (registering hooks, loading configs, etc.).
|
|
127
127
|
*
|
|
128
|
+
* Resolution uses the current working directory captured when preloading
|
|
129
|
+
* starts, so programmatic consumers that change cwd between invocations
|
|
130
|
+
* resolve relative preloads (and bare dependencies) against the current
|
|
131
|
+
* invocation — consistent with call-time `loadEnvFiles`/`loadApp` — rather
|
|
132
|
+
* than the directory that was current when this module was first evaluated.
|
|
133
|
+
* Preloads run sequentially in list order. No sandbox is applied.
|
|
134
|
+
*
|
|
128
135
|
* Public helper for programmatic CLI integrations that need the same preload
|
|
129
136
|
* behavior as the binary before loading an app or handler module.
|
|
130
137
|
*/
|
|
@@ -139,6 +146,7 @@ interface WatchSupervisorDeps {
|
|
|
139
146
|
watch?: typeof watch;
|
|
140
147
|
existsSync?: typeof existsSync;
|
|
141
148
|
logger?: Pick<Console, 'error'>;
|
|
149
|
+
/** Ms before SIGTERM escalates to SIGKILL. Must be a finite integer in `0..2147483647` (Node timer limit). */
|
|
142
150
|
killTimeoutMs?: number;
|
|
143
151
|
setTimeout?: typeof setTimeout;
|
|
144
152
|
clearTimeout?: typeof clearTimeout;
|
|
@@ -163,6 +171,11 @@ interface WatchSupervisorController {
|
|
|
163
171
|
* Reconstruct the argv for the child process, stripping --watch/--ext/--delay
|
|
164
172
|
* flags (the child runs without watch mode).
|
|
165
173
|
*
|
|
174
|
+
* Generated options are placed before `--`, with the positional app module
|
|
175
|
+
* after it, so leading-dash module paths (e.g. `--app.js`, `--help`) parsed
|
|
176
|
+
* via `dev --watch ./src -- --app.js` keep their `--` protection and are not
|
|
177
|
+
* reinterpreted as flags (or help/version requests) by the child parser.
|
|
178
|
+
*
|
|
166
179
|
* Public helper for CLI wrappers that supervise watch mode themselves and need
|
|
167
180
|
* the same child argv reconstruction as `runWithWatch`.
|
|
168
181
|
*/
|
|
@@ -208,11 +221,53 @@ declare function generateRuntimeEntry(appPath: string, initPath?: string): strin
|
|
|
208
221
|
* Public safety check for programmatic build integrations before invoking
|
|
209
222
|
* `buildBundleFromEntryContent()` with `clean: true`.
|
|
210
223
|
*/
|
|
224
|
+
/**
|
|
225
|
+
* Validate that `outDir` is safe to clean before invoking tsup.
|
|
226
|
+
* Prevents destructive `clean: true` combinations:
|
|
227
|
+
* - filesystem root (physical)
|
|
228
|
+
* - project cwd itself (physical)
|
|
229
|
+
* - ancestors of the project cwd (physical, through symlinked aliases)
|
|
230
|
+
* - symlinked output directories
|
|
231
|
+
* - output that physically contains input files (appPath/initPath), or is
|
|
232
|
+
* physically nested inside an input path
|
|
233
|
+
*
|
|
234
|
+
* Physical comparison canonicalizes cwd, outDir (via its nearest existing
|
|
235
|
+
* ancestor when it does not exist yet), and supplied input paths with
|
|
236
|
+
* `realpath`. Unexpected filesystem errors fail closed. Only `clean: false`
|
|
237
|
+
* skips validation.
|
|
238
|
+
*
|
|
239
|
+
* Public safety check for programmatic build integrations before invoking
|
|
240
|
+
* `buildBundleFromEntryContent()` with `clean: true`.
|
|
241
|
+
*/
|
|
211
242
|
declare function validateOutDirForClean(outDir: string, clean: boolean, appPath?: string, initPath?: string): void;
|
|
212
|
-
|
|
243
|
+
/** Injectable filesystem/build seams for deterministic staging-failure tests. */
|
|
244
|
+
interface BuildStagingDeps {
|
|
245
|
+
mkdtempSyncImpl?: typeof mkdtempSync;
|
|
246
|
+
lstatSyncImpl?: typeof lstatSync;
|
|
247
|
+
writeFileSyncImpl?: typeof writeFileSync;
|
|
248
|
+
rmSyncImpl?: typeof rmSync;
|
|
249
|
+
buildImpl?: (options: {
|
|
250
|
+
config: false;
|
|
251
|
+
entry: Record<string, string>;
|
|
252
|
+
tsconfig?: string;
|
|
253
|
+
format: string[];
|
|
254
|
+
target: string;
|
|
255
|
+
outDir: string;
|
|
256
|
+
clean: boolean;
|
|
257
|
+
external: string[];
|
|
258
|
+
sourcemap: boolean;
|
|
259
|
+
dts: boolean;
|
|
260
|
+
splitting: boolean;
|
|
261
|
+
}) => Promise<void>;
|
|
262
|
+
}
|
|
263
|
+
declare function buildBundleFromEntryContent(args: BuildEntryContentArgs, deps?: BuildStagingDeps): Promise<void>;
|
|
213
264
|
/**
|
|
214
265
|
* Bundle an Express app as a local runtime module. The output default-exports
|
|
215
266
|
* the app and may additionally export an `init` hook for the `start` command.
|
|
267
|
+
*
|
|
268
|
+
* `express` and `@web-ts-toolkit/express-runtime` are always external, so the
|
|
269
|
+
* bundle must be deployed with both packages installed (`express` is a peer
|
|
270
|
+
* dependency). Additional externals can be passed via `BuildArgs.external`.
|
|
216
271
|
*/
|
|
217
272
|
declare function buildRuntime(args: BuildArgs): Promise<void>;
|
|
218
273
|
/**
|
|
@@ -220,17 +275,36 @@ declare function buildRuntime(args: BuildArgs): Promise<void>;
|
|
|
220
275
|
* to the user's cwd (for node_modules resolution), lazy-loads the bundled
|
|
221
276
|
* build tool, then cleans up.
|
|
222
277
|
*
|
|
223
|
-
* `express`
|
|
224
|
-
*
|
|
278
|
+
* `express` and `@web-ts-toolkit/express-runtime` (imported by the generated
|
|
279
|
+
* entry) are always external; additional externals can be passed via
|
|
280
|
+
* `BuildArgs.external`. Deploy the bundle together with both packages
|
|
281
|
+
* installed (`express` is a peer dependency; `serverless-http` ships with the
|
|
282
|
+
* runtime package).
|
|
225
283
|
*/
|
|
226
284
|
declare function buildServerless(args: BuildArgs): Promise<void>;
|
|
227
285
|
/**
|
|
228
|
-
* A
|
|
229
|
-
*
|
|
286
|
+
* A serverless handler callable as invoked by the local `start-serverless`
|
|
287
|
+
* adapter. The adapter supplies an AWS API Gateway REST API v1 event (see
|
|
288
|
+
* `ApiGatewayRestEvent`) and an empty record context (`{}`), then validates
|
|
289
|
+
* the unknown result via `applyServerlessResult`.
|
|
290
|
+
*
|
|
291
|
+
* Keep the parameters narrow: handlers requiring provider-specific event
|
|
292
|
+
* fields or rich Lambda-like contexts must not typecheck here, since the
|
|
293
|
+
* local adapter cannot supply them. The default
|
|
294
|
+
* `ServerlessHandler<Record<string, unknown>, Record<string, unknown>>` from
|
|
295
|
+
* `createServerlessHandler(app)` remains assignable, so cast-free
|
|
296
|
+
* `createServerlessAdapterApp(createServerlessHandler(app))` composition
|
|
297
|
+
* compiles.
|
|
298
|
+
*/
|
|
299
|
+
type GenericHandler = (event: ApiGatewayRestEvent, context: Record<string, unknown>) => Promise<unknown>;
|
|
300
|
+
/**
|
|
301
|
+
* AWS API Gateway REST API v1 / Lambda proxy event shape emitted by the local adapter.
|
|
302
|
+
*
|
|
303
|
+
* Declared as a type alias (not an interface) so the implicit index signature
|
|
304
|
+
* lets the default provider-generic `ServerlessHandler` (`Record<string,
|
|
305
|
+
* unknown>` event) accept it without casts.
|
|
230
306
|
*/
|
|
231
|
-
type
|
|
232
|
-
/** AWS API Gateway REST API v1 / Lambda proxy event shape emitted by the local adapter. */
|
|
233
|
-
interface ApiGatewayRestEvent {
|
|
307
|
+
type ApiGatewayRestEvent = {
|
|
234
308
|
httpMethod: string;
|
|
235
309
|
path: string;
|
|
236
310
|
headers: Record<string, string>;
|
|
@@ -244,7 +318,7 @@ interface ApiGatewayRestEvent {
|
|
|
244
318
|
sourceIp: string;
|
|
245
319
|
};
|
|
246
320
|
};
|
|
247
|
-
}
|
|
321
|
+
};
|
|
248
322
|
/**
|
|
249
323
|
* AWS API Gateway REST API v1 / Lambda proxy result shape returned by `serverless-http`.
|
|
250
324
|
*/
|
|
@@ -261,7 +335,12 @@ interface ServerlessAdapterOptions {
|
|
|
261
335
|
* Maximum bytes to buffer for a single request body.
|
|
262
336
|
* Default: 1048576 (1 MiB). Must be a finite non-negative integer.
|
|
263
337
|
* When `0`, no body is allowed — any non-empty body receives `413`.
|
|
264
|
-
*
|
|
338
|
+
* Collection retains O(limit) chunk bytes: appending stops once the running
|
|
339
|
+
* total would exceed the limit (at most one chunk over the limit is observed
|
|
340
|
+
* before rejection). `Buffer.concat` then holds the chunks plus one output
|
|
341
|
+
* Buffer, and event translation adds a transient base64 copy (~4/3 of the
|
|
342
|
+
* body), so peak transient memory is a small multiple of the limit rather
|
|
343
|
+
* than an exact limit-plus-chunk ceiling.
|
|
265
344
|
*/
|
|
266
345
|
maxBodyBytes?: number;
|
|
267
346
|
}
|
|
@@ -277,19 +356,80 @@ declare function validateMaxBodyBytes(value: unknown): number;
|
|
|
277
356
|
* bodies with a `LIMIT_EXCEEDED` error (413), stops retaining chunks after the limit,
|
|
278
357
|
* removes owned listeners, and drains the request.
|
|
279
358
|
* Distinguishes client aborts (`CLIENT_ABORT`) and stream errors from oversize.
|
|
359
|
+
*
|
|
360
|
+
* Memory phases (no unmeasured total-memory ceiling is claimed): chunk
|
|
361
|
+
* retention is O(limit) — appending stops once the running total would exceed
|
|
362
|
+
* `maxBytes`, so at most one chunk over the limit is observed before
|
|
363
|
+
* rejection; `Buffer.concat` then retains the chunks plus one output Buffer of
|
|
364
|
+
* the accepted size; `toServerlessEvent` adds a transient base64 copy (~4/3 of
|
|
365
|
+
* the body). Peak transient memory is therefore a small multiple of the limit.
|
|
280
366
|
*/
|
|
281
367
|
declare function collectBody(req: Request, maxBytes: number): Promise<Buffer>;
|
|
282
368
|
/**
|
|
283
369
|
* Build an AWS API Gateway REST API v1 / Lambda proxy event from HTTP request components.
|
|
284
370
|
*
|
|
371
|
+
* Path contract (origin-form request targets are preserved verbatim):
|
|
372
|
+
*
|
|
373
|
+
* - `url` is normally the raw origin-form target from `req.url`
|
|
374
|
+
* (`/path?query`). The path is everything before the first literal `?`
|
|
375
|
+
* (or `#`); it is never dot-segment-resolved, slash-collapsed, or
|
|
376
|
+
* percent-decoded. `//admin/users`, `/a/../private`, `/a/./b`,
|
|
377
|
+
* `/%2E%2E/private`, and `/a%2Fb` all reach the handler unchanged, so
|
|
378
|
+
* wrapped routing sees the same target the client sent. Query splitting
|
|
379
|
+
* and single-decode semantics are unchanged, and only a literal `?`
|
|
380
|
+
* starts the query (an encoded `%3F` stays in the path).
|
|
381
|
+
* - A `#fragment`, never part of a real HTTP request target, is stripped
|
|
382
|
+
* when present.
|
|
383
|
+
* - Absolute-form targets (`scheme://authority/path?query`, as sent to
|
|
384
|
+
* proxies) are supported by stripping the scheme and authority and
|
|
385
|
+
* preserving the raw path remainder (`http://h//a/../b?x=1` yields path
|
|
386
|
+
* `//a/../b`); a missing remainder maps to `/`. Userinfo, host, and port
|
|
387
|
+
* are ignored, not validated.
|
|
388
|
+
* - Asterisk-form (`*`, used by `OPTIONS *`) is supported with path `*`.
|
|
389
|
+
* - The empty string maps to `/` for backwards compatibility. Any other
|
|
390
|
+
* target that is neither origin-form, absolute-form, nor asterisk-form
|
|
391
|
+
* (e.g. `foo/bar`) is rejected with an `Error` (the local adapter turns
|
|
392
|
+
* this into a 500 without invoking the handler).
|
|
393
|
+
*
|
|
394
|
+
* Intentional behavior change: this helper previously split the target via
|
|
395
|
+
* the WHATWG `URL` parser, which rewrote origin-form paths before routing
|
|
396
|
+
* (`//admin/users` was parsed as host `admin` plus path `/users`,
|
|
397
|
+
* `/a/../private` resolved to `/private`, and even encoded `%2E%2E` was
|
|
398
|
+
* decoded and then resolved). Those rewrites silently selected a different
|
|
399
|
+
* route; they are no longer performed.
|
|
400
|
+
*
|
|
401
|
+
* Header contract (HTTP boundary fidelity):
|
|
402
|
+
*
|
|
403
|
+
* - When the Node `rawHeaders` list (`[name, value, ...]` from
|
|
404
|
+
* `IncomingMessage.rawHeaders`, optionally passed as the fifth argument)
|
|
405
|
+
* is available, both header maps are derived from it. Names are
|
|
406
|
+
* lowercased (HTTP names are case-insensitive, so `X-Repeat` and
|
|
407
|
+
* `x-repeat` merge); values are preserved verbatim in wire order and
|
|
408
|
+
* never split on commas (`"one, with comma"` stays one entry).
|
|
409
|
+
* - Otherwise the `headers` map (e.g. `req.headers` or `headersDistinct`)
|
|
410
|
+
* is used as-is: array values are preserved entry-wise, string values
|
|
411
|
+
* become single entries, and commas inside values are never split. Note
|
|
412
|
+
* that `req.headers` has already joined duplicates (`"one, two"`), so
|
|
413
|
+
* callers at the real HTTP boundary should pass `rawHeaders` (the local
|
|
414
|
+
* adapter does) to keep repeated headers distinct.
|
|
415
|
+
* - The single-value map joins each multi-value entry with `", "`; the
|
|
416
|
+
* multi-value map keeps every value in order. Response `set-cookie`
|
|
417
|
+
* handling is unchanged (`applyServerlessResult` still emits each
|
|
418
|
+
* `set-cookie` value as its own header and joins other multi-values
|
|
419
|
+
* with `","`).
|
|
420
|
+
*
|
|
285
421
|
* Public helper for adapters that need the same AWS REST API v1 event shape as
|
|
286
422
|
* the `start-serverless` command.
|
|
287
423
|
*/
|
|
288
|
-
declare function toServerlessEvent(method: string, url: string, headers: Record<string, string | string[] | undefined>, body: Buffer): ApiGatewayRestEvent;
|
|
424
|
+
declare function toServerlessEvent(method: string, url: string, headers: Record<string, string | string[] | undefined>, body: Buffer, rawHeaders?: unknown): ApiGatewayRestEvent;
|
|
289
425
|
/**
|
|
290
426
|
* Write a serverless handler result to an Express response.
|
|
291
427
|
* Validates the complete AWS API Gateway REST API v1 / Lambda proxy result before writing anything.
|
|
292
428
|
* `multiValueHeaders` wins over `headers` when the same header appears in both maps.
|
|
429
|
+
* An empty `multiValueHeaders` array is omitted (no header is emitted), but its
|
|
430
|
+
* name is still validated so invalid names fail closed even with zero values.
|
|
431
|
+
* If header/body application fails before headers are sent, any headers staged
|
|
432
|
+
* by this call are removed before the error propagates so a fallback 500 stays clean.
|
|
293
433
|
*
|
|
294
434
|
* Public helper for adapters that need the same AWS REST API v1 result-to-HTTP
|
|
295
435
|
* translation as the `start-serverless` command.
|
|
@@ -298,14 +438,22 @@ declare function applyServerlessResult(result: unknown, res: Response): void;
|
|
|
298
438
|
/**
|
|
299
439
|
* Create an Express app that proxies all requests to a serverless handler.
|
|
300
440
|
* Each HTTP request is translated into a serverless event, the handler is
|
|
301
|
-
* invoked,
|
|
441
|
+
* invoked as `handler(event, {})` where `event` is an `ApiGatewayRestEvent`
|
|
442
|
+
* and the context is an empty record, and the result is written back to the
|
|
443
|
+
* response.
|
|
302
444
|
*
|
|
303
|
-
* Express body parsers are disabled; the raw request body is
|
|
304
|
-
*
|
|
305
|
-
*
|
|
445
|
+
* Express body parsers are disabled; the raw request body is buffered with
|
|
446
|
+
* `collectBody` and then base64-encoded into the AWS v1 string `body` field
|
|
447
|
+
* (`isBase64Encoded` is true for non-empty bodies, false with `body: ''` for
|
|
448
|
+
* empty ones), so `serverless-http` replays the decoded bytes through the
|
|
449
|
+
* Express request stream identically to production.
|
|
306
450
|
* Bodies exceeding `maxBodyBytes` (default 1 MiB, 0 = empty bodies only) receive
|
|
307
451
|
* `413 Payload Too Large` without invoking the handler; the request is drained
|
|
308
|
-
* and
|
|
452
|
+
* and chunk retention is O(limit) — appending stops once the running total
|
|
453
|
+
* would exceed the limit, so at most one chunk over the limit is observed
|
|
454
|
+
* before rejection. `Buffer.concat` retains the chunks plus one output Buffer
|
|
455
|
+
* and event translation adds a transient base64 copy (~4/3 of the body), so
|
|
456
|
+
* peak transient memory is a small multiple of the limit.
|
|
309
457
|
*/
|
|
310
458
|
declare function createServerlessAdapterApp(handler: GenericHandler, options?: ServerlessAdapterOptions): Express;
|
|
311
459
|
/**
|
package/cli-api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LocalServerOptions, LocalServer } from './index.js';
|
|
2
|
-
import { watch, existsSync } from 'node:fs';
|
|
2
|
+
import { watch, mkdtempSync, lstatSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
3
3
|
import { ChildProcess, fork } from 'node:child_process';
|
|
4
4
|
import { Response, Request, Express } from 'express';
|
|
5
5
|
import 'node:http';
|
|
@@ -25,7 +25,7 @@ interface DevArgs {
|
|
|
25
25
|
watch: string[];
|
|
26
26
|
/** File extensions to watch (default: ts,js,mjs,cjs,json). */
|
|
27
27
|
watchExt: string[];
|
|
28
|
-
/** Debounce delay (ms) before restarting on file change (default: 500). */
|
|
28
|
+
/** Debounce delay (ms) before restarting on file change (default: 500). Must be a finite integer in `0..2147483647` (Node timer limit); `0` restarts without debouncing. */
|
|
29
29
|
watchDelay: number;
|
|
30
30
|
}
|
|
31
31
|
interface BuildArgs {
|
|
@@ -125,6 +125,13 @@ declare function loadEnvFiles(paths: string[]): void;
|
|
|
125
125
|
* loading the app module. Each module is `require()`-ed, running its
|
|
126
126
|
* side effects (registering hooks, loading configs, etc.).
|
|
127
127
|
*
|
|
128
|
+
* Resolution uses the current working directory captured when preloading
|
|
129
|
+
* starts, so programmatic consumers that change cwd between invocations
|
|
130
|
+
* resolve relative preloads (and bare dependencies) against the current
|
|
131
|
+
* invocation — consistent with call-time `loadEnvFiles`/`loadApp` — rather
|
|
132
|
+
* than the directory that was current when this module was first evaluated.
|
|
133
|
+
* Preloads run sequentially in list order. No sandbox is applied.
|
|
134
|
+
*
|
|
128
135
|
* Public helper for programmatic CLI integrations that need the same preload
|
|
129
136
|
* behavior as the binary before loading an app or handler module.
|
|
130
137
|
*/
|
|
@@ -139,6 +146,7 @@ interface WatchSupervisorDeps {
|
|
|
139
146
|
watch?: typeof watch;
|
|
140
147
|
existsSync?: typeof existsSync;
|
|
141
148
|
logger?: Pick<Console, 'error'>;
|
|
149
|
+
/** Ms before SIGTERM escalates to SIGKILL. Must be a finite integer in `0..2147483647` (Node timer limit). */
|
|
142
150
|
killTimeoutMs?: number;
|
|
143
151
|
setTimeout?: typeof setTimeout;
|
|
144
152
|
clearTimeout?: typeof clearTimeout;
|
|
@@ -163,6 +171,11 @@ interface WatchSupervisorController {
|
|
|
163
171
|
* Reconstruct the argv for the child process, stripping --watch/--ext/--delay
|
|
164
172
|
* flags (the child runs without watch mode).
|
|
165
173
|
*
|
|
174
|
+
* Generated options are placed before `--`, with the positional app module
|
|
175
|
+
* after it, so leading-dash module paths (e.g. `--app.js`, `--help`) parsed
|
|
176
|
+
* via `dev --watch ./src -- --app.js` keep their `--` protection and are not
|
|
177
|
+
* reinterpreted as flags (or help/version requests) by the child parser.
|
|
178
|
+
*
|
|
166
179
|
* Public helper for CLI wrappers that supervise watch mode themselves and need
|
|
167
180
|
* the same child argv reconstruction as `runWithWatch`.
|
|
168
181
|
*/
|
|
@@ -208,11 +221,53 @@ declare function generateRuntimeEntry(appPath: string, initPath?: string): strin
|
|
|
208
221
|
* Public safety check for programmatic build integrations before invoking
|
|
209
222
|
* `buildBundleFromEntryContent()` with `clean: true`.
|
|
210
223
|
*/
|
|
224
|
+
/**
|
|
225
|
+
* Validate that `outDir` is safe to clean before invoking tsup.
|
|
226
|
+
* Prevents destructive `clean: true` combinations:
|
|
227
|
+
* - filesystem root (physical)
|
|
228
|
+
* - project cwd itself (physical)
|
|
229
|
+
* - ancestors of the project cwd (physical, through symlinked aliases)
|
|
230
|
+
* - symlinked output directories
|
|
231
|
+
* - output that physically contains input files (appPath/initPath), or is
|
|
232
|
+
* physically nested inside an input path
|
|
233
|
+
*
|
|
234
|
+
* Physical comparison canonicalizes cwd, outDir (via its nearest existing
|
|
235
|
+
* ancestor when it does not exist yet), and supplied input paths with
|
|
236
|
+
* `realpath`. Unexpected filesystem errors fail closed. Only `clean: false`
|
|
237
|
+
* skips validation.
|
|
238
|
+
*
|
|
239
|
+
* Public safety check for programmatic build integrations before invoking
|
|
240
|
+
* `buildBundleFromEntryContent()` with `clean: true`.
|
|
241
|
+
*/
|
|
211
242
|
declare function validateOutDirForClean(outDir: string, clean: boolean, appPath?: string, initPath?: string): void;
|
|
212
|
-
|
|
243
|
+
/** Injectable filesystem/build seams for deterministic staging-failure tests. */
|
|
244
|
+
interface BuildStagingDeps {
|
|
245
|
+
mkdtempSyncImpl?: typeof mkdtempSync;
|
|
246
|
+
lstatSyncImpl?: typeof lstatSync;
|
|
247
|
+
writeFileSyncImpl?: typeof writeFileSync;
|
|
248
|
+
rmSyncImpl?: typeof rmSync;
|
|
249
|
+
buildImpl?: (options: {
|
|
250
|
+
config: false;
|
|
251
|
+
entry: Record<string, string>;
|
|
252
|
+
tsconfig?: string;
|
|
253
|
+
format: string[];
|
|
254
|
+
target: string;
|
|
255
|
+
outDir: string;
|
|
256
|
+
clean: boolean;
|
|
257
|
+
external: string[];
|
|
258
|
+
sourcemap: boolean;
|
|
259
|
+
dts: boolean;
|
|
260
|
+
splitting: boolean;
|
|
261
|
+
}) => Promise<void>;
|
|
262
|
+
}
|
|
263
|
+
declare function buildBundleFromEntryContent(args: BuildEntryContentArgs, deps?: BuildStagingDeps): Promise<void>;
|
|
213
264
|
/**
|
|
214
265
|
* Bundle an Express app as a local runtime module. The output default-exports
|
|
215
266
|
* the app and may additionally export an `init` hook for the `start` command.
|
|
267
|
+
*
|
|
268
|
+
* `express` and `@web-ts-toolkit/express-runtime` are always external, so the
|
|
269
|
+
* bundle must be deployed with both packages installed (`express` is a peer
|
|
270
|
+
* dependency). Additional externals can be passed via `BuildArgs.external`.
|
|
216
271
|
*/
|
|
217
272
|
declare function buildRuntime(args: BuildArgs): Promise<void>;
|
|
218
273
|
/**
|
|
@@ -220,17 +275,36 @@ declare function buildRuntime(args: BuildArgs): Promise<void>;
|
|
|
220
275
|
* to the user's cwd (for node_modules resolution), lazy-loads the bundled
|
|
221
276
|
* build tool, then cleans up.
|
|
222
277
|
*
|
|
223
|
-
* `express`
|
|
224
|
-
*
|
|
278
|
+
* `express` and `@web-ts-toolkit/express-runtime` (imported by the generated
|
|
279
|
+
* entry) are always external; additional externals can be passed via
|
|
280
|
+
* `BuildArgs.external`. Deploy the bundle together with both packages
|
|
281
|
+
* installed (`express` is a peer dependency; `serverless-http` ships with the
|
|
282
|
+
* runtime package).
|
|
225
283
|
*/
|
|
226
284
|
declare function buildServerless(args: BuildArgs): Promise<void>;
|
|
227
285
|
/**
|
|
228
|
-
* A
|
|
229
|
-
*
|
|
286
|
+
* A serverless handler callable as invoked by the local `start-serverless`
|
|
287
|
+
* adapter. The adapter supplies an AWS API Gateway REST API v1 event (see
|
|
288
|
+
* `ApiGatewayRestEvent`) and an empty record context (`{}`), then validates
|
|
289
|
+
* the unknown result via `applyServerlessResult`.
|
|
290
|
+
*
|
|
291
|
+
* Keep the parameters narrow: handlers requiring provider-specific event
|
|
292
|
+
* fields or rich Lambda-like contexts must not typecheck here, since the
|
|
293
|
+
* local adapter cannot supply them. The default
|
|
294
|
+
* `ServerlessHandler<Record<string, unknown>, Record<string, unknown>>` from
|
|
295
|
+
* `createServerlessHandler(app)` remains assignable, so cast-free
|
|
296
|
+
* `createServerlessAdapterApp(createServerlessHandler(app))` composition
|
|
297
|
+
* compiles.
|
|
298
|
+
*/
|
|
299
|
+
type GenericHandler = (event: ApiGatewayRestEvent, context: Record<string, unknown>) => Promise<unknown>;
|
|
300
|
+
/**
|
|
301
|
+
* AWS API Gateway REST API v1 / Lambda proxy event shape emitted by the local adapter.
|
|
302
|
+
*
|
|
303
|
+
* Declared as a type alias (not an interface) so the implicit index signature
|
|
304
|
+
* lets the default provider-generic `ServerlessHandler` (`Record<string,
|
|
305
|
+
* unknown>` event) accept it without casts.
|
|
230
306
|
*/
|
|
231
|
-
type
|
|
232
|
-
/** AWS API Gateway REST API v1 / Lambda proxy event shape emitted by the local adapter. */
|
|
233
|
-
interface ApiGatewayRestEvent {
|
|
307
|
+
type ApiGatewayRestEvent = {
|
|
234
308
|
httpMethod: string;
|
|
235
309
|
path: string;
|
|
236
310
|
headers: Record<string, string>;
|
|
@@ -244,7 +318,7 @@ interface ApiGatewayRestEvent {
|
|
|
244
318
|
sourceIp: string;
|
|
245
319
|
};
|
|
246
320
|
};
|
|
247
|
-
}
|
|
321
|
+
};
|
|
248
322
|
/**
|
|
249
323
|
* AWS API Gateway REST API v1 / Lambda proxy result shape returned by `serverless-http`.
|
|
250
324
|
*/
|
|
@@ -261,7 +335,12 @@ interface ServerlessAdapterOptions {
|
|
|
261
335
|
* Maximum bytes to buffer for a single request body.
|
|
262
336
|
* Default: 1048576 (1 MiB). Must be a finite non-negative integer.
|
|
263
337
|
* When `0`, no body is allowed — any non-empty body receives `413`.
|
|
264
|
-
*
|
|
338
|
+
* Collection retains O(limit) chunk bytes: appending stops once the running
|
|
339
|
+
* total would exceed the limit (at most one chunk over the limit is observed
|
|
340
|
+
* before rejection). `Buffer.concat` then holds the chunks plus one output
|
|
341
|
+
* Buffer, and event translation adds a transient base64 copy (~4/3 of the
|
|
342
|
+
* body), so peak transient memory is a small multiple of the limit rather
|
|
343
|
+
* than an exact limit-plus-chunk ceiling.
|
|
265
344
|
*/
|
|
266
345
|
maxBodyBytes?: number;
|
|
267
346
|
}
|
|
@@ -277,19 +356,80 @@ declare function validateMaxBodyBytes(value: unknown): number;
|
|
|
277
356
|
* bodies with a `LIMIT_EXCEEDED` error (413), stops retaining chunks after the limit,
|
|
278
357
|
* removes owned listeners, and drains the request.
|
|
279
358
|
* Distinguishes client aborts (`CLIENT_ABORT`) and stream errors from oversize.
|
|
359
|
+
*
|
|
360
|
+
* Memory phases (no unmeasured total-memory ceiling is claimed): chunk
|
|
361
|
+
* retention is O(limit) — appending stops once the running total would exceed
|
|
362
|
+
* `maxBytes`, so at most one chunk over the limit is observed before
|
|
363
|
+
* rejection; `Buffer.concat` then retains the chunks plus one output Buffer of
|
|
364
|
+
* the accepted size; `toServerlessEvent` adds a transient base64 copy (~4/3 of
|
|
365
|
+
* the body). Peak transient memory is therefore a small multiple of the limit.
|
|
280
366
|
*/
|
|
281
367
|
declare function collectBody(req: Request, maxBytes: number): Promise<Buffer>;
|
|
282
368
|
/**
|
|
283
369
|
* Build an AWS API Gateway REST API v1 / Lambda proxy event from HTTP request components.
|
|
284
370
|
*
|
|
371
|
+
* Path contract (origin-form request targets are preserved verbatim):
|
|
372
|
+
*
|
|
373
|
+
* - `url` is normally the raw origin-form target from `req.url`
|
|
374
|
+
* (`/path?query`). The path is everything before the first literal `?`
|
|
375
|
+
* (or `#`); it is never dot-segment-resolved, slash-collapsed, or
|
|
376
|
+
* percent-decoded. `//admin/users`, `/a/../private`, `/a/./b`,
|
|
377
|
+
* `/%2E%2E/private`, and `/a%2Fb` all reach the handler unchanged, so
|
|
378
|
+
* wrapped routing sees the same target the client sent. Query splitting
|
|
379
|
+
* and single-decode semantics are unchanged, and only a literal `?`
|
|
380
|
+
* starts the query (an encoded `%3F` stays in the path).
|
|
381
|
+
* - A `#fragment`, never part of a real HTTP request target, is stripped
|
|
382
|
+
* when present.
|
|
383
|
+
* - Absolute-form targets (`scheme://authority/path?query`, as sent to
|
|
384
|
+
* proxies) are supported by stripping the scheme and authority and
|
|
385
|
+
* preserving the raw path remainder (`http://h//a/../b?x=1` yields path
|
|
386
|
+
* `//a/../b`); a missing remainder maps to `/`. Userinfo, host, and port
|
|
387
|
+
* are ignored, not validated.
|
|
388
|
+
* - Asterisk-form (`*`, used by `OPTIONS *`) is supported with path `*`.
|
|
389
|
+
* - The empty string maps to `/` for backwards compatibility. Any other
|
|
390
|
+
* target that is neither origin-form, absolute-form, nor asterisk-form
|
|
391
|
+
* (e.g. `foo/bar`) is rejected with an `Error` (the local adapter turns
|
|
392
|
+
* this into a 500 without invoking the handler).
|
|
393
|
+
*
|
|
394
|
+
* Intentional behavior change: this helper previously split the target via
|
|
395
|
+
* the WHATWG `URL` parser, which rewrote origin-form paths before routing
|
|
396
|
+
* (`//admin/users` was parsed as host `admin` plus path `/users`,
|
|
397
|
+
* `/a/../private` resolved to `/private`, and even encoded `%2E%2E` was
|
|
398
|
+
* decoded and then resolved). Those rewrites silently selected a different
|
|
399
|
+
* route; they are no longer performed.
|
|
400
|
+
*
|
|
401
|
+
* Header contract (HTTP boundary fidelity):
|
|
402
|
+
*
|
|
403
|
+
* - When the Node `rawHeaders` list (`[name, value, ...]` from
|
|
404
|
+
* `IncomingMessage.rawHeaders`, optionally passed as the fifth argument)
|
|
405
|
+
* is available, both header maps are derived from it. Names are
|
|
406
|
+
* lowercased (HTTP names are case-insensitive, so `X-Repeat` and
|
|
407
|
+
* `x-repeat` merge); values are preserved verbatim in wire order and
|
|
408
|
+
* never split on commas (`"one, with comma"` stays one entry).
|
|
409
|
+
* - Otherwise the `headers` map (e.g. `req.headers` or `headersDistinct`)
|
|
410
|
+
* is used as-is: array values are preserved entry-wise, string values
|
|
411
|
+
* become single entries, and commas inside values are never split. Note
|
|
412
|
+
* that `req.headers` has already joined duplicates (`"one, two"`), so
|
|
413
|
+
* callers at the real HTTP boundary should pass `rawHeaders` (the local
|
|
414
|
+
* adapter does) to keep repeated headers distinct.
|
|
415
|
+
* - The single-value map joins each multi-value entry with `", "`; the
|
|
416
|
+
* multi-value map keeps every value in order. Response `set-cookie`
|
|
417
|
+
* handling is unchanged (`applyServerlessResult` still emits each
|
|
418
|
+
* `set-cookie` value as its own header and joins other multi-values
|
|
419
|
+
* with `","`).
|
|
420
|
+
*
|
|
285
421
|
* Public helper for adapters that need the same AWS REST API v1 event shape as
|
|
286
422
|
* the `start-serverless` command.
|
|
287
423
|
*/
|
|
288
|
-
declare function toServerlessEvent(method: string, url: string, headers: Record<string, string | string[] | undefined>, body: Buffer): ApiGatewayRestEvent;
|
|
424
|
+
declare function toServerlessEvent(method: string, url: string, headers: Record<string, string | string[] | undefined>, body: Buffer, rawHeaders?: unknown): ApiGatewayRestEvent;
|
|
289
425
|
/**
|
|
290
426
|
* Write a serverless handler result to an Express response.
|
|
291
427
|
* Validates the complete AWS API Gateway REST API v1 / Lambda proxy result before writing anything.
|
|
292
428
|
* `multiValueHeaders` wins over `headers` when the same header appears in both maps.
|
|
429
|
+
* An empty `multiValueHeaders` array is omitted (no header is emitted), but its
|
|
430
|
+
* name is still validated so invalid names fail closed even with zero values.
|
|
431
|
+
* If header/body application fails before headers are sent, any headers staged
|
|
432
|
+
* by this call are removed before the error propagates so a fallback 500 stays clean.
|
|
293
433
|
*
|
|
294
434
|
* Public helper for adapters that need the same AWS REST API v1 result-to-HTTP
|
|
295
435
|
* translation as the `start-serverless` command.
|
|
@@ -298,14 +438,22 @@ declare function applyServerlessResult(result: unknown, res: Response): void;
|
|
|
298
438
|
/**
|
|
299
439
|
* Create an Express app that proxies all requests to a serverless handler.
|
|
300
440
|
* Each HTTP request is translated into a serverless event, the handler is
|
|
301
|
-
* invoked,
|
|
441
|
+
* invoked as `handler(event, {})` where `event` is an `ApiGatewayRestEvent`
|
|
442
|
+
* and the context is an empty record, and the result is written back to the
|
|
443
|
+
* response.
|
|
302
444
|
*
|
|
303
|
-
* Express body parsers are disabled; the raw request body is
|
|
304
|
-
*
|
|
305
|
-
*
|
|
445
|
+
* Express body parsers are disabled; the raw request body is buffered with
|
|
446
|
+
* `collectBody` and then base64-encoded into the AWS v1 string `body` field
|
|
447
|
+
* (`isBase64Encoded` is true for non-empty bodies, false with `body: ''` for
|
|
448
|
+
* empty ones), so `serverless-http` replays the decoded bytes through the
|
|
449
|
+
* Express request stream identically to production.
|
|
306
450
|
* Bodies exceeding `maxBodyBytes` (default 1 MiB, 0 = empty bodies only) receive
|
|
307
451
|
* `413 Payload Too Large` without invoking the handler; the request is drained
|
|
308
|
-
* and
|
|
452
|
+
* and chunk retention is O(limit) — appending stops once the running total
|
|
453
|
+
* would exceed the limit, so at most one chunk over the limit is observed
|
|
454
|
+
* before rejection. `Buffer.concat` retains the chunks plus one output Buffer
|
|
455
|
+
* and event translation adds a transient base64 copy (~4/3 of the body), so
|
|
456
|
+
* peak transient memory is a small multiple of the limit.
|
|
309
457
|
*/
|
|
310
458
|
declare function createServerlessAdapterApp(handler: GenericHandler, options?: ServerlessAdapterOptions): Express;
|
|
311
459
|
/**
|