lambder 15.0.1 → 15.1.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.
- package/CHANGELOG.md +48 -0
- package/README.md +4 -4
- package/dist/build/checkApiRefusals.d.ts +1 -1
- package/dist/build/checkApiRefusals.js +24 -0
- package/dist/core/Lambder.d.ts +6 -6
- package/dist/core/Lambder.js +42 -14
- package/dist/core/LambderCallSummary.d.ts +11 -9
- package/dist/core/LambderCreateOptions.d.ts +9 -8
- package/dist/core/LambderInvocationScope.d.ts +9 -3
- package/dist/core/LambderRouting.d.ts +9 -0
- package/dist/core/LambderRouting.js +20 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +16 -7
- package/dist/testing/LambderTestApp.d.ts +4 -3
- package/dist/testing/LambderTestApp.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,54 @@ sit on its first published patch, and later patches list only what they changed.
|
|
|
9
9
|
Releases up to 3.2.6 carry git tags; the ones after it were published without
|
|
10
10
|
one, so versions are not cross-linked to tag comparisons here.
|
|
11
11
|
|
|
12
|
+
## [15.1.1] - 2026-10-03
|
|
13
|
+
|
|
14
|
+
A minor: every request a route answers writes a call summary line, named by
|
|
15
|
+
the route as registered, and a matcher object can name its route.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **A call summary line for every route.** A request a route answered
|
|
20
|
+
(`addRoute`, `addSessionRoute`, or an `addAction` on its HTTP side) writes
|
|
21
|
+
the same `lambder.call` line an API call does, so a webhook or a page route
|
|
22
|
+
is counted, timed and queried beside the endpoints. The line's `route`
|
|
23
|
+
names the route as it was registered, its matcher's `name` or its method
|
|
24
|
+
and path pattern, and carries nothing of the request's own path; its
|
|
25
|
+
`outcome` is read from its status (`success` below 400, `crash` from 500,
|
|
26
|
+
`other` between) and `handlerMs` is the route handler's own time. A line
|
|
27
|
+
a hook's answer ends in is the matched route's too. Files, the index page,
|
|
28
|
+
the route fallback and non-HTTP events still write none.
|
|
29
|
+
- **`name` on a matcher object**, what the line names the route by, which a
|
|
30
|
+
bare predicate route needs to be named at all.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **`LambderCallSummary.api` is null for a route, and the line gains
|
|
35
|
+
`route`**, null for an API call. A `callSummary` function that read `api`
|
|
36
|
+
as a string narrows it first.
|
|
37
|
+
|
|
38
|
+
## [15.0.2] - 2026-10-02
|
|
39
|
+
|
|
40
|
+
A patch: two fixes for apps with a large contract.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **Reading a failure compiles for a contract of hundreds of refusal codes.**
|
|
45
|
+
`failure.reason`, `failure.refusal`, `outcome.ok` and every other read of a
|
|
46
|
+
`LambderApiFailure` or `LambderApiOutcome` over a contract's refusal
|
|
47
|
+
messages failed with TS2590 ("union type too complex to represent") once
|
|
48
|
+
the vocabulary held a few hundred codes, and so did a `LambderCaller` whose
|
|
49
|
+
`fetchEndedHandler` or `errorHandler` read its argument: the refusal arm
|
|
50
|
+
declared its message both on the fields every failure shares and on the arm
|
|
51
|
+
itself, and a read of the intersection crossed the message union with
|
|
52
|
+
itself. Each arm now declares it once. The types describe the same values.
|
|
53
|
+
- **`checkApiRefusals` no longer reports a mock guard's unused codes.** A mock
|
|
54
|
+
guard declares exactly its server guard's codes, which `create()` holds it
|
|
55
|
+
to against `guardDeclarations`, so one that raises fewer (a captcha stand-in
|
|
56
|
+
letting every token through) got an `unused` finding it could not act on.
|
|
57
|
+
Its codes are still checked the other way: one it raises and may not send
|
|
58
|
+
is `undeclared`.
|
|
59
|
+
|
|
12
60
|
## [15.0.1] - 2026-10-02
|
|
13
61
|
|
|
14
62
|
A major. Fixes for crashes, misconfigurations that passed silently, and places
|
package/README.md
CHANGED
|
@@ -80,10 +80,10 @@ const company = await caller.companies.get({ slug: "acme" });
|
|
|
80
80
|
cookies, and a guard against Lambda's response size cap.
|
|
81
81
|
- **Hooks and actions.** Lifecycle hooks, plus `addAction()` for the non-HTTP
|
|
82
82
|
invocations (EventBridge, SQS, custom events) the same function receives.
|
|
83
|
-
- **A summary line per call.** Every API call
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
caller.
|
|
83
|
+
- **A summary line per call.** Every API call, and every request a route
|
|
84
|
+
answers, is logged once, as JSON, with its endpoint or route, outcome,
|
|
85
|
+
refusal code, status and timings, and the request ids that join it to the
|
|
86
|
+
calls it made; nothing from its input, its path or its caller.
|
|
87
87
|
- **Lambda to lambda calls.** `LambderInvokeCaller` invokes a Lambder app in
|
|
88
88
|
another function directly, with no API Gateway in between, typed from the
|
|
89
89
|
callee's own contract and carrying its refusals, crash detail and logs back.
|
|
@@ -16,7 +16,7 @@ export type LambderRefusalCheckFinding = {
|
|
|
16
16
|
at: string;
|
|
17
17
|
/**
|
|
18
18
|
* - `undeclared`: a code the handler can reach that it may not send.
|
|
19
|
-
* - `unused`: a code the handler's own `refusals` option names that nothing it reaches raises.
|
|
19
|
+
* - `unused`: a code the handler's own `refusals` option names that nothing it reaches raises. Never a mock guard's, whose codes are its server guard's.
|
|
20
20
|
* - `uncoded`: a refusal with no code it can reach, where codes are required.
|
|
21
21
|
* - `unreadable`: a refusal it can reach whose code is not a string literal type.
|
|
22
22
|
* - `untraced`: a handler handed a typed refuse whose function cannot be
|
|
@@ -67,6 +67,24 @@ export const checkApiRefusals = async (options) => {
|
|
|
67
67
|
return true;
|
|
68
68
|
return isInside(path, lambderRoot) && !isInside(path, resolve(lambderRoot, "tests")) && !parts.slice(lambderRoot.split(sep).length).includes("node_modules");
|
|
69
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Whether a registration is a mock guard: `guard` read off a mock init,
|
|
72
|
+
* whose builder Lambder declares in its mock entry, where a server init's
|
|
73
|
+
* is declared in its own.
|
|
74
|
+
*/
|
|
75
|
+
const isMockGuard = (call, registration) => {
|
|
76
|
+
if (registration !== "guard" || !ts.isPropertyAccessExpression(call.expression))
|
|
77
|
+
return false;
|
|
78
|
+
const declaration = checker.getSymbolAtLocation(call.expression.name)?.declarations?.[0];
|
|
79
|
+
if (!declaration || !isLambderDeclaration(declaration))
|
|
80
|
+
return false;
|
|
81
|
+
const path = realPathOf(ts, declaration.getSourceFile().fileName);
|
|
82
|
+
const parts = path.split(sep);
|
|
83
|
+
const packageAt = parts.findIndex((part, index) => part === "lambder" && parts[index - 1] === "node_modules");
|
|
84
|
+
// The path inside the package: dist/mock/... installed, src/mock/... over its own sources.
|
|
85
|
+
const inside = packageAt >= 0 ? parts.slice(packageAt + 1) : relative(lambderRoot, path).split(sep);
|
|
86
|
+
return inside[1] === "mock";
|
|
87
|
+
};
|
|
70
88
|
const where = (node) => {
|
|
71
89
|
const sourceFile = node.getSourceFile();
|
|
72
90
|
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
@@ -390,6 +408,12 @@ export const checkApiRefusals = async (options) => {
|
|
|
390
408
|
findings.push({ handler: name, at, problem: "unreadable", raisedAt: raise.at });
|
|
391
409
|
}
|
|
392
410
|
}
|
|
411
|
+
// A mock guard declares its server guard's codes, which create() holds
|
|
412
|
+
// it to against guardDeclarations. Standing in for that guard it may
|
|
413
|
+
// raise fewer of them, which only lets more through, so a code it
|
|
414
|
+
// never raises is no declaration to trim.
|
|
415
|
+
if (isMockGuard(call, registration))
|
|
416
|
+
return;
|
|
393
417
|
for (const code of ownRefusalsOf(optionsNode) ?? []) {
|
|
394
418
|
if (!reached.has(code))
|
|
395
419
|
findings.push({ handler: name, at, problem: "unused", code });
|
package/dist/core/Lambder.d.ts
CHANGED
|
@@ -150,7 +150,7 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
150
150
|
private readonly trustedClientIpHeaders;
|
|
151
151
|
private readonly trustedHostHeaders;
|
|
152
152
|
private readonly originProof;
|
|
153
|
-
/** Where each
|
|
153
|
+
/** Where each call's summary goes, an API call's or a route's (the callSummary option); null writes none. Replaceable through the backend swap alone. */
|
|
154
154
|
private callSummaryWriter;
|
|
155
155
|
constructor(given?: LambderCreateOptions<TApp["session"]>);
|
|
156
156
|
setRouteFallbackHandler(routeFallbackHandler: LambderFallbackHandler): this;
|
|
@@ -464,8 +464,8 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
464
464
|
private resolveRequest;
|
|
465
465
|
/**
|
|
466
466
|
* One HTTP invocation, from the event to the finalized response, under
|
|
467
|
-
* an invocation record of its own (see LambderInvocationScope): what
|
|
468
|
-
*
|
|
467
|
+
* an invocation record of its own (see LambderInvocationScope): what its
|
|
468
|
+
* call summary line is written from once the response is final.
|
|
469
469
|
*/
|
|
470
470
|
render(event: LambderHttpEvent, lambdaContext: Context): Promise<LambderHttpResponse>;
|
|
471
471
|
private renderRequest;
|
|
@@ -496,9 +496,9 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
496
496
|
*/
|
|
497
497
|
private answerCrash;
|
|
498
498
|
/**
|
|
499
|
-
* The summary line of an API call
|
|
500
|
-
* for an invocation that served
|
|
501
|
-
* the call its line and nothing else.
|
|
499
|
+
* The summary line of an API call or of a request a route answered, once
|
|
500
|
+
* its response is final: nothing for an invocation that served neither. A
|
|
501
|
+
* writer that throws costs the call its line and nothing else.
|
|
502
502
|
*/
|
|
503
503
|
private writeCallSummary;
|
|
504
504
|
/**
|
package/dist/core/Lambder.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import LambderResolver from "./LambderResolver.js";
|
|
2
2
|
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
3
3
|
import { LambderResponse, finalizeResponse, answerFromResponse, responseFromAnswer, emitResponse, DEFAULT_FINALIZE_OPTIONS, DEFAULT_RESPONSE_COMPRESSION_SETTINGS, } from "./LambderResponse.js";
|
|
4
|
-
import { compileRouteMatcher } from "./LambderRouting.js";
|
|
4
|
+
import { compileRouteMatcher, routeNameOf } from "./LambderRouting.js";
|
|
5
5
|
import { allowedCorsOriginOf, applyCorsHeaders } from "./LambderCors.js";
|
|
6
6
|
import LambderSessionManager from "../session/LambderSessionManager.js";
|
|
7
7
|
import { resolveCompressionOption } from "../shared/wire/LambderCompressionOption.js";
|
|
@@ -37,7 +37,7 @@ import { assertCreateOptions, mergeNamedMaps, } from "./LambderCreateOptions.js"
|
|
|
37
37
|
/** The refusals of a name no API is registered under: no code, and none required. */
|
|
38
38
|
const NO_DECLARED_REFUSALS = { codes: new Map(), codeRequired: false };
|
|
39
39
|
/** Longest endpoint name a call summary carries: a page built before paths posts the name in its body, and a line is no place for a long one. */
|
|
40
|
-
const
|
|
40
|
+
const MAX_SUMMARY_NAME_CHARS = 200;
|
|
41
41
|
/** A request id as the parent header may carry one: what Lambda's own ids look like, and no longer than any of them. */
|
|
42
42
|
const REQUEST_ID_PATTERN = /^[A-Za-z0-9-]{1,128}$/;
|
|
43
43
|
/**
|
|
@@ -52,7 +52,11 @@ const parentRequestIdOf = (event) => {
|
|
|
52
52
|
return typeof value === "string" && REQUEST_ID_PATTERN.test(value) ? value : null;
|
|
53
53
|
};
|
|
54
54
|
/** The API call a request is, as its invocation records it; null for a request that is no API call. */
|
|
55
|
-
const apiCallOf = (ctx, event) => ctx.api ? { api: ctx.api.apiName, parentRequestId: parentRequestIdOf(event) } : null;
|
|
55
|
+
const apiCallOf = (ctx, event) => ctx.api ? { api: ctx.api.apiName, route: null, parentRequestId: parentRequestIdOf(event) } : null;
|
|
56
|
+
/** The call a request a route answered is: the route by its registered name, never by the path asked for. */
|
|
57
|
+
const routeCallOf = (ctx, route) => ({ api: null, route, parentRequestId: parentRequestIdOf(ctx.event) });
|
|
58
|
+
/** A route's outcome, which no envelope names: read from the status it answered with, as an API call's 5xx reads. */
|
|
59
|
+
const outcomeOfStatus = (status) => ({ outcome: status >= 500 ? "crash" : status >= 400 ? "other" : "success", code: null });
|
|
56
60
|
/**
|
|
57
61
|
* Main Lambder class for building type-safe serverless APIs. Create
|
|
58
62
|
* instances with initLambder<SessionData>().create({...}) (see below): the
|
|
@@ -157,7 +161,7 @@ export default class Lambder {
|
|
|
157
161
|
trustedClientIpHeaders;
|
|
158
162
|
trustedHostHeaders;
|
|
159
163
|
originProof;
|
|
160
|
-
/** Where each
|
|
164
|
+
/** Where each call's summary goes, an API call's or a route's (the callSummary option); null writes none. Replaceable through the backend swap alone. */
|
|
161
165
|
callSummaryWriter;
|
|
162
166
|
constructor(given = {}) {
|
|
163
167
|
// The guards and the rate-limit policies as one map each, whether
|
|
@@ -314,6 +318,7 @@ export default class Lambder {
|
|
|
314
318
|
addRoute(condition, actionFn) {
|
|
315
319
|
this.actionList.push({
|
|
316
320
|
match: compileRouteMatcher(condition),
|
|
321
|
+
name: routeNameOf(condition),
|
|
317
322
|
actionFn: (ctx, resolver) => actionFn(ctx, resolver),
|
|
318
323
|
});
|
|
319
324
|
return this;
|
|
@@ -321,6 +326,7 @@ export default class Lambder {
|
|
|
321
326
|
addSessionRoute(condition, actionFn) {
|
|
322
327
|
this.actionList.push({
|
|
323
328
|
match: compileRouteMatcher(condition),
|
|
329
|
+
name: routeNameOf(condition),
|
|
324
330
|
actionFn: async (ctx, resolver) => {
|
|
325
331
|
await this.requireSession(ctx, resolver);
|
|
326
332
|
// requireSession answered already if there was no session, so
|
|
@@ -429,6 +435,7 @@ export default class Lambder {
|
|
|
429
435
|
const group = ctx.api ? splitApiName(ctx.api.apiName)?.group : undefined;
|
|
430
436
|
return group !== undefined && names.has(group) ? {} : false;
|
|
431
437
|
},
|
|
438
|
+
name: null,
|
|
432
439
|
actionFn: (ctx, resolver) => instance.dispatchApiCall(ctx, resolver),
|
|
433
440
|
});
|
|
434
441
|
return instance;
|
|
@@ -556,6 +563,7 @@ export default class Lambder {
|
|
|
556
563
|
// HTTP side: joins the route/API chain in registration order.
|
|
557
564
|
this.actionList.push({
|
|
558
565
|
match: (ctx) => filter(ctx.event, ctx) ? {} : false,
|
|
566
|
+
name: null,
|
|
559
567
|
actionFn: async (ctx, resolver) => {
|
|
560
568
|
const result = await actionFn(ctx.event, { ctx, res: resolver, lambdaContext: ctx.lambdaContext });
|
|
561
569
|
if (!(result instanceof LambderResponse)) {
|
|
@@ -919,15 +927,32 @@ export default class Lambder {
|
|
|
919
927
|
// Set before the hooks run, so a beforeRender hook on a matched route
|
|
920
928
|
// sees the route's own path params.
|
|
921
929
|
ctx.pathParams = matched.params;
|
|
930
|
+
// A route's call is named as soon as it has matched, before the hooks
|
|
931
|
+
// run, so the line a hook's answer ends in is the route's too. An API
|
|
932
|
+
// call keeps the name its path gave it, whatever action answers it.
|
|
933
|
+
const invocation = currentInvocation();
|
|
934
|
+
if (invocation && !invocation.call)
|
|
935
|
+
invocation.call = routeCallOf(ctx, matched.action.name);
|
|
922
936
|
const beforeRenderResult = await this.runBeforeRenderHooks(ctx, resolver, onContextReplaced);
|
|
923
937
|
if (beforeRenderResult instanceof LambderResponse)
|
|
924
938
|
return beforeRenderResult;
|
|
925
|
-
|
|
939
|
+
// An API call's handler time is the pipeline's to measure, inside its
|
|
940
|
+
// own work; a route's is the whole action.
|
|
941
|
+
if (ctx.api)
|
|
942
|
+
return await matched.action.actionFn(beforeRenderResult, resolver);
|
|
943
|
+
const handlerStarted = performance.now();
|
|
944
|
+
try {
|
|
945
|
+
return await matched.action.actionFn(beforeRenderResult, resolver);
|
|
946
|
+
}
|
|
947
|
+
finally {
|
|
948
|
+
if (invocation)
|
|
949
|
+
invocation.handlerMs = performance.now() - handlerStarted;
|
|
950
|
+
}
|
|
926
951
|
}
|
|
927
952
|
/**
|
|
928
953
|
* One HTTP invocation, from the event to the finalized response, under
|
|
929
|
-
* an invocation record of its own (see LambderInvocationScope): what
|
|
930
|
-
*
|
|
954
|
+
* an invocation record of its own (see LambderInvocationScope): what its
|
|
955
|
+
* call summary line is written from once the response is final.
|
|
931
956
|
*/
|
|
932
957
|
async render(event, lambdaContext) {
|
|
933
958
|
return await runInvocation(lambdaContext, async (invocation) => {
|
|
@@ -995,7 +1020,8 @@ export default class Lambder {
|
|
|
995
1020
|
// reach it.
|
|
996
1021
|
ctx.responseHeaders.applyTo(response, response === responseIntoHooks ? headersAppliedIntoHooks : 0);
|
|
997
1022
|
this.applyCors(allowedOrigin, response, this.isCorsPreflight(ctx));
|
|
998
|
-
|
|
1023
|
+
// An API answer names its outcome; a route's answer names none, and reads from its status.
|
|
1024
|
+
invocation.outcome = response.callOutcome ?? (ctx.api ? { outcome: "other", code: null } : outcomeOfStatus(response.statusCode));
|
|
999
1025
|
return await finalizeResponse(ctx, response, this.finalizeOptions, ctx.eventFormat);
|
|
1000
1026
|
}
|
|
1001
1027
|
catch (err) {
|
|
@@ -1101,9 +1127,9 @@ export default class Lambder {
|
|
|
1101
1127
|
return emitResponse(eventFormat, crashResponse.statusCode, crashResponse.headers, typeof crashResponse.body === "string" ? crashResponse.body : "", false);
|
|
1102
1128
|
}
|
|
1103
1129
|
/**
|
|
1104
|
-
* The summary line of an API call
|
|
1105
|
-
* for an invocation that served
|
|
1106
|
-
* the call its line and nothing else.
|
|
1130
|
+
* The summary line of an API call or of a request a route answered, once
|
|
1131
|
+
* its response is final: nothing for an invocation that served neither. A
|
|
1132
|
+
* writer that throws costs the call its line and nothing else.
|
|
1107
1133
|
*/
|
|
1108
1134
|
writeCallSummary(invocation, status) {
|
|
1109
1135
|
const { call } = invocation;
|
|
@@ -1112,8 +1138,10 @@ export default class Lambder {
|
|
|
1112
1138
|
const summary = {
|
|
1113
1139
|
kind: "lambder.call",
|
|
1114
1140
|
// The path form is two identifiers; a page built before paths
|
|
1115
|
-
// posts the name in its body, which is whatever it sent.
|
|
1116
|
-
|
|
1141
|
+
// posts the name in its body, which is whatever it sent. A route's
|
|
1142
|
+
// name is the app's own, cut the same way.
|
|
1143
|
+
api: call.api === null ? null : call.api.slice(0, MAX_SUMMARY_NAME_CHARS),
|
|
1144
|
+
route: call.route === null ? null : call.route.slice(0, MAX_SUMMARY_NAME_CHARS),
|
|
1117
1145
|
outcome: invocation.outcome?.outcome ?? "other",
|
|
1118
1146
|
code: invocation.outcome?.code ?? null,
|
|
1119
1147
|
status,
|
|
@@ -1128,7 +1156,7 @@ export default class Lambder {
|
|
|
1128
1156
|
this.callSummaryWriter(summary);
|
|
1129
1157
|
}
|
|
1130
1158
|
catch (err) {
|
|
1131
|
-
console.error(`Lambder: the callSummary writer threw, so the summary of "${summary.api}" was not written. ${coerceToError(err).message}`);
|
|
1159
|
+
console.error(`Lambder: the callSummary writer threw, so the summary of "${summary.api ?? summary.route ?? "a route"}" was not written. ${coerceToError(err).message}`);
|
|
1132
1160
|
}
|
|
1133
1161
|
}
|
|
1134
1162
|
/**
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { LambderCallOutcome } from "../shared/wire/LambderCallOutcome.js";
|
|
2
2
|
/**
|
|
3
|
-
* One API call
|
|
4
|
-
* ended, how long it took, and the ids that join
|
|
5
|
-
* logs. Nothing from the call's input, its
|
|
6
|
-
* caller's address is in it, so the line can be
|
|
7
|
-
* the app likes.
|
|
3
|
+
* One API call, or one request a route answered, as its summary line records
|
|
4
|
+
* it: what was called, how it ended, how long it took, and the ids that join
|
|
5
|
+
* it to the rest of the logs. Nothing from the call's input, its path, its
|
|
6
|
+
* session, its cookies or its caller's address is in it, so the line can be
|
|
7
|
+
* kept and queried as long as the app likes.
|
|
8
8
|
*/
|
|
9
9
|
export type LambderCallSummary = {
|
|
10
10
|
/** Always "lambder.call": what a log query picks the lines out by. */
|
|
11
11
|
kind: "lambder.call";
|
|
12
|
-
/** The endpoint
|
|
13
|
-
api: string;
|
|
14
|
-
/**
|
|
12
|
+
/** The endpoint an API call's path named (`group.action`), whether or not one is registered under it; null for a route. */
|
|
13
|
+
api: string | null;
|
|
14
|
+
/** The route that answered, as it was registered: its matcher's name, or its method and path pattern; null for an API call, for a predicate route given no name, and for an addAction that answered an HTTP request. */
|
|
15
|
+
route: string | null;
|
|
16
|
+
/** How the call ended; see LambderCallOutcome. A route's is read from its status: success below 400, crash from 500, other between. */
|
|
15
17
|
outcome: LambderCallOutcome;
|
|
16
18
|
/** The refusal's code, the framework's (`lambder/rate-limited`) or the app's; null when the answer carries none. */
|
|
17
19
|
code: string | null;
|
|
@@ -19,7 +21,7 @@ export type LambderCallSummary = {
|
|
|
19
21
|
status: number;
|
|
20
22
|
/** From the invocation's start to the answer, in milliseconds. */
|
|
21
23
|
durationMs: number;
|
|
22
|
-
/** How long the API handler itself ran, in milliseconds; null when it did not run (refused before it, or
|
|
24
|
+
/** How long the API handler, or the route's handler, itself ran, in milliseconds; null when it did not run (refused before it, replayed, or answered by a hook). */
|
|
23
25
|
handlerMs: number | null;
|
|
24
26
|
/** True when a stored idempotent answer was replayed and no handler ran. */
|
|
25
27
|
replayed: boolean;
|
|
@@ -292,14 +292,15 @@ export type LambderCreateOptions<TSessionData = any> = {
|
|
|
292
292
|
*/
|
|
293
293
|
originProof?: LambderOriginProof;
|
|
294
294
|
/**
|
|
295
|
-
* One summary per API call,
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* the
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
295
|
+
* One summary per API call, and per request a route answered, written
|
|
296
|
+
* when it is answered: the endpoint or the route as registered, how it
|
|
297
|
+
* ended (success, a refusal and its code, a flag, a validation refusal, a
|
|
298
|
+
* crash; a route's read from its status), the status, the duration and
|
|
299
|
+
* the handler's own time, whether an idempotent answer was replayed,
|
|
300
|
+
* whether the invocation was the process's first, and its request id
|
|
301
|
+
* with the id of the invocation that called it over invoke. Nothing from
|
|
302
|
+
* the input, the path, the session, the cookies or the caller's address.
|
|
303
|
+
* See LambderCallSummary.
|
|
303
304
|
*
|
|
304
305
|
* Default: each summary as one JSON line on stdout, which a Lambda
|
|
305
306
|
* function's log group keeps and CloudWatch Logs Insights reads field by
|
|
@@ -7,13 +7,19 @@ export type LambderInvocationRecord = LambderInvocation & {
|
|
|
7
7
|
readonly startedAt: number;
|
|
8
8
|
/** Whether this is the first invocation this process has run. */
|
|
9
9
|
readonly coldStart: boolean;
|
|
10
|
-
/** How long the API handler itself ran, in milliseconds; null until it has. */
|
|
10
|
+
/** How long the API handler, or the route's handler, itself ran, in milliseconds; null until it has. */
|
|
11
11
|
handlerMs: number | null;
|
|
12
12
|
/** Whether an idempotent answer was replayed, so no handler ran. */
|
|
13
13
|
replayed: boolean;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* What the invocation serves, named: an API call by the endpoint its path
|
|
16
|
+
* named, or a request a route answered by the route as registered (null
|
|
17
|
+
* for a route with no name), with the invocation that invoked it, when
|
|
18
|
+
* one did. Null for anything else: a file, the index page, an event.
|
|
19
|
+
*/
|
|
15
20
|
call: {
|
|
16
|
-
api: string;
|
|
21
|
+
api: string | null;
|
|
22
|
+
route: string | null;
|
|
17
23
|
parentRequestId: string | null;
|
|
18
24
|
} | null;
|
|
19
25
|
/** What the answer is, once the call has one. */
|
|
@@ -15,6 +15,8 @@ export type LambderRouteMatcher = {
|
|
|
15
15
|
/** One or more HTTP methods; HEAD requests also match GET routes. */
|
|
16
16
|
method?: string | string[];
|
|
17
17
|
condition?: LambderRouteConditionFn;
|
|
18
|
+
/** What the route's call summary line names it by; by default its method and path pattern, which a route with neither has none of. */
|
|
19
|
+
name?: string;
|
|
18
20
|
};
|
|
19
21
|
export type LambderRouteCondition = LambderRoutePath | RegExp | LambderRouteConditionFn | LambderRouteMatcher;
|
|
20
22
|
/** Returns matched path params, or false when the route doesn't match. */
|
|
@@ -28,6 +30,13 @@ export type CompiledMatcher = (ctx: LambderRenderContext) => false | Record<stri
|
|
|
28
30
|
* this rule, so neighbouring slots cannot disagree about what a method means.
|
|
29
31
|
*/
|
|
30
32
|
export declare const allowsRequestMethod: (methods: ReadonlySet<string>, requestMethod: string) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* What a route's call summary line names it by: the name its matcher gives,
|
|
35
|
+
* or its method and path pattern, both fixed at registration, so the line
|
|
36
|
+
* carries nothing of the request's own path. Null for a route with neither,
|
|
37
|
+
* a bare predicate, which the line still counts.
|
|
38
|
+
*/
|
|
39
|
+
export declare const routeNameOf: (condition: LambderRouteCondition) => string | null;
|
|
31
40
|
/** Compile a route condition once at registration time. */
|
|
32
41
|
export declare const compileRouteMatcher: (condition: LambderRouteCondition) => CompiledMatcher;
|
|
33
42
|
export {};
|
|
@@ -57,6 +57,26 @@ export const allowsRequestMethod = (methods, requestMethod) => {
|
|
|
57
57
|
return true;
|
|
58
58
|
return method === "HEAD" && methods.has("GET");
|
|
59
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* What a route's call summary line names it by: the name its matcher gives,
|
|
62
|
+
* or its method and path pattern, both fixed at registration, so the line
|
|
63
|
+
* carries nothing of the request's own path. Null for a route with neither,
|
|
64
|
+
* a bare predicate, which the line still counts.
|
|
65
|
+
*/
|
|
66
|
+
export const routeNameOf = (condition) => {
|
|
67
|
+
if (typeof condition === "string")
|
|
68
|
+
return condition;
|
|
69
|
+
if (condition instanceof RegExp)
|
|
70
|
+
return condition.toString();
|
|
71
|
+
if (typeof condition === "function")
|
|
72
|
+
return null;
|
|
73
|
+
if (condition.name !== undefined)
|
|
74
|
+
return condition.name;
|
|
75
|
+
const methods = condition.method === undefined ? [] : (Array.isArray(condition.method) ? condition.method : [condition.method]).map((m) => m.toUpperCase());
|
|
76
|
+
const path = condition.path === undefined ? null : typeof condition.path === "string" ? condition.path : condition.path.toString();
|
|
77
|
+
const name = [methods.join(","), path].filter((part) => part).join(" ");
|
|
78
|
+
return name || null;
|
|
79
|
+
};
|
|
60
80
|
/** Compile a route condition once at registration time. */
|
|
61
81
|
export const compileRouteMatcher = (condition) => {
|
|
62
82
|
if (typeof condition === "string" || condition instanceof RegExp) {
|
|
@@ -42,13 +42,17 @@ export type LambderApiSuccessOutcome<T> = {
|
|
|
42
42
|
* message the endpoint can answer with (LambderContractRefusalMessage), on
|
|
43
43
|
* every arm: a refusal flagged notAuthorized arrives as that reason and still
|
|
44
44
|
* carries its message.
|
|
45
|
+
*
|
|
46
|
+
* The message is declared once per arm, never here and again on the arm that
|
|
47
|
+
* requires it: an intersection reading `refusal` from both would intersect
|
|
48
|
+
* TMessage with itself, a union of every code a contract declares crossed
|
|
49
|
+
* with itself, which for a contract of a few hundred codes is past what the
|
|
50
|
+
* compiler can represent (TS2590 on any read of a failure).
|
|
45
51
|
*/
|
|
46
|
-
type LambderApiFailureFields
|
|
52
|
+
type LambderApiFailureFields = {
|
|
47
53
|
ok: false;
|
|
48
54
|
/** HTTP status, when a response was received. */
|
|
49
55
|
status?: number;
|
|
50
|
-
/** The envelope's refusal, when the server provided one: always the message object, a plain string having been read as one (refusalMessageOf). */
|
|
51
|
-
refusal?: TMessage;
|
|
52
56
|
/** Seconds to wait before retrying, from the response's Retry-After header (rate-limit refusals send it, and so may a 503). */
|
|
53
57
|
retryAfterSeconds?: number;
|
|
54
58
|
/**
|
|
@@ -69,21 +73,26 @@ type LambderApiFailureFields<TMessage extends LambderUncheckedRefusalMessage> =
|
|
|
69
73
|
* `response` when the server answered with Lambder's own envelope, which is
|
|
70
74
|
* how a crash detail and a logList arrive with it.
|
|
71
75
|
*/
|
|
72
|
-
export type LambderApiCallFailure<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = LambderApiFailureFields
|
|
76
|
+
export type LambderApiCallFailure<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = LambderApiFailureFields & {
|
|
73
77
|
reason: 'network' | 'timeout' | 'aborted' | 'server' | 'unknown';
|
|
78
|
+
/** The envelope's refusal, when the server provided one: always the message object, a plain string having been read as one (refusalMessageOf). */
|
|
79
|
+
refusal?: TMessage;
|
|
74
80
|
error: Error;
|
|
75
81
|
response?: LambderApiRefusalEnvelope;
|
|
76
82
|
};
|
|
77
83
|
/** HTTP 422: the server rejected the input against the API's schema. Always carries the issues. */
|
|
78
|
-
export type LambderApiValidationFailure<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = LambderApiFailureFields
|
|
84
|
+
export type LambderApiValidationFailure<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = LambderApiFailureFields & {
|
|
79
85
|
reason: 'validation';
|
|
86
|
+
/** As on a call failure. */
|
|
87
|
+
refusal?: TMessage;
|
|
80
88
|
zodError: LambderValidationError;
|
|
81
89
|
};
|
|
82
90
|
/** The server answered, and the envelope itself says the call is refused. Always carries that envelope, and a `refusal` failure always carries the refusal. */
|
|
83
|
-
export type LambderApiEnvelopeFailure<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = LambderApiFailureFields
|
|
91
|
+
export type LambderApiEnvelopeFailure<TMessage extends LambderUncheckedRefusalMessage = LambderUncheckedRefusalMessage> = LambderApiFailureFields & {
|
|
84
92
|
response: LambderApiRefusalEnvelope;
|
|
85
93
|
} & ({
|
|
86
|
-
reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized';
|
|
94
|
+
reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized'; /** As on a call failure. */
|
|
95
|
+
refusal?: TMessage;
|
|
87
96
|
} | {
|
|
88
97
|
reason: 'refusal';
|
|
89
98
|
refusal: TMessage;
|
|
@@ -153,9 +153,10 @@ export declare class LambderTestApp<TContract extends LambderApiContractShape =
|
|
|
153
153
|
*/
|
|
154
154
|
get crashes(): readonly Error[];
|
|
155
155
|
/**
|
|
156
|
-
* The summary of every API call
|
|
157
|
-
* in order (see LambderCallSummary): what
|
|
158
|
-
* have been handed, collected here instead of
|
|
156
|
+
* The summary of every API call, and every request a route answered,
|
|
157
|
+
* since the last reset, in order (see LambderCallSummary): what the app's
|
|
158
|
+
* callSummary option would have been handed, collected here instead of
|
|
159
|
+
* written to stdout.
|
|
159
160
|
*/
|
|
160
161
|
get callSummaries(): readonly LambderCallSummary[];
|
|
161
162
|
/**
|
|
@@ -183,9 +183,10 @@ export class LambderTestApp {
|
|
|
183
183
|
return this.crashList;
|
|
184
184
|
}
|
|
185
185
|
/**
|
|
186
|
-
* The summary of every API call
|
|
187
|
-
* in order (see LambderCallSummary): what
|
|
188
|
-
* have been handed, collected here instead of
|
|
186
|
+
* The summary of every API call, and every request a route answered,
|
|
187
|
+
* since the last reset, in order (see LambderCallSummary): what the app's
|
|
188
|
+
* callSummary option would have been handed, collected here instead of
|
|
189
|
+
* written to stdout.
|
|
189
190
|
*/
|
|
190
191
|
get callSummaries() {
|
|
191
192
|
return this.callSummaryList;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.1.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|