lambder 15.0.1 → 15.0.2
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
CHANGED
|
@@ -9,6 +9,28 @@ 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.0.2] - 2026-10-02
|
|
13
|
+
|
|
14
|
+
A patch: two fixes for apps with a large contract.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Reading a failure compiles for a contract of hundreds of refusal codes.**
|
|
19
|
+
`failure.reason`, `failure.refusal`, `outcome.ok` and every other read of a
|
|
20
|
+
`LambderApiFailure` or `LambderApiOutcome` over a contract's refusal
|
|
21
|
+
messages failed with TS2590 ("union type too complex to represent") once
|
|
22
|
+
the vocabulary held a few hundred codes, and so did a `LambderCaller` whose
|
|
23
|
+
`fetchEndedHandler` or `errorHandler` read its argument: the refusal arm
|
|
24
|
+
declared its message both on the fields every failure shares and on the arm
|
|
25
|
+
itself, and a read of the intersection crossed the message union with
|
|
26
|
+
itself. Each arm now declares it once. The types describe the same values.
|
|
27
|
+
- **`checkApiRefusals` no longer reports a mock guard's unused codes.** A mock
|
|
28
|
+
guard declares exactly its server guard's codes, which `create()` holds it
|
|
29
|
+
to against `guardDeclarations`, so one that raises fewer (a captcha stand-in
|
|
30
|
+
letting every token through) got an `unused` finding it could not act on.
|
|
31
|
+
Its codes are still checked the other way: one it raises and may not send
|
|
32
|
+
is `undeclared`.
|
|
33
|
+
|
|
12
34
|
## [15.0.1] - 2026-10-02
|
|
13
35
|
|
|
14
36
|
A major. Fixes for crashes, misconfigurations that passed silently, and places
|
|
@@ -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 });
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "15.0.
|
|
3
|
+
"version": "15.0.2",
|
|
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": [
|