lambder 14.1.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 +429 -0
- package/README.md +14 -17
- package/dist/api/LambderApiDeclarations.d.ts +11 -5
- package/dist/api/LambderApiDefinition.d.ts +2 -1
- package/dist/api/LambderApiEnvelope.d.ts +2 -2
- package/dist/api/LambderApiEnvelope.js +8 -2
- package/dist/api/LambderApiGuards.d.ts +44 -5
- package/dist/api/LambderApiGuards.js +27 -0
- package/dist/api/LambderApiIdempotency.d.ts +14 -7
- package/dist/api/LambderApiIdempotency.js +39 -27
- package/dist/api/LambderApiPipeline.d.ts +25 -11
- package/dist/api/LambderApiPipeline.js +34 -17
- package/dist/api/LambderApiRateLimits.d.ts +28 -21
- package/dist/api/LambderApiRateLimits.js +55 -35
- package/dist/api/LambderApiRefusals.d.ts +10 -4
- package/dist/api/LambderApiRefusals.js +31 -5
- package/dist/api/LambderApiRequest.d.ts +21 -1
- package/dist/api/LambderApiRequest.js +31 -2
- package/dist/build/checkApiRefusals.d.ts +8 -3
- package/dist/build/checkApiRefusals.js +80 -29
- package/dist/build/contractPrinterProcess.d.ts +11 -0
- package/dist/build/contractPrinterProcess.js +17 -0
- package/dist/build/generateApiFiles.d.ts +26 -5
- package/dist/build/generateApiFiles.js +68 -7
- package/dist/build/writeApiSchemas.d.ts +109 -0
- package/dist/build/writeApiSchemas.js +308 -0
- package/dist/build.d.ts +3 -1
- package/dist/build.js +2 -0
- package/dist/client/LambderCaller.d.ts +25 -11
- package/dist/client/LambderCaller.js +57 -49
- package/dist/client/LambderUploadRunner.d.ts +55 -27
- package/dist/client/LambderUploadRunner.js +89 -34
- package/dist/client.d.ts +6 -6
- package/dist/client.js +2 -2
- package/dist/core/Lambder.d.ts +31 -21
- package/dist/core/Lambder.js +79 -23
- package/dist/core/LambderCallSummary.d.ts +1 -1
- package/dist/core/LambderContext.d.ts +59 -7
- package/dist/core/LambderContext.js +31 -15
- package/dist/core/LambderCrashHandling.d.ts +5 -0
- package/dist/core/LambderCrashHandling.js +11 -1
- package/dist/core/LambderCreateOptions.d.ts +28 -17
- package/dist/core/LambderCreateOptions.js +14 -7
- package/dist/core/LambderResolver.d.ts +3 -2
- package/dist/core/LambderResponseBuilder.d.ts +20 -9
- package/dist/core/LambderResponseBuilder.js +39 -8
- package/dist/core/LambderTemplatingEngine.d.ts +24 -6
- package/dist/core/LambderTemplatingEngine.js +23 -3
- package/dist/index.d.ts +15 -12
- package/dist/index.js +7 -5
- package/dist/invoke/LambderInvokeCaller.d.ts +25 -4
- package/dist/invoke/LambderInvokeCaller.js +47 -9
- package/dist/invoke/LambderInvokeOutcome.d.ts +7 -6
- package/dist/mock/LambderMockAdapterDoor.d.ts +40 -0
- package/dist/mock/LambderMockAdapterDoor.js +10 -0
- package/dist/mock/LambderMockApiSchemas.d.ts +28 -0
- package/dist/mock/LambderMockApiSchemas.js +150 -0
- package/dist/mock/LambderMockApp.d.ts +56 -48
- package/dist/mock/LambderMockApp.js +107 -62
- package/dist/mock/LambderMockBrowserCookies.js +18 -5
- package/dist/mock/LambderMockCreateOptions.d.ts +51 -4
- package/dist/mock/LambderMockFailureInjector.d.ts +12 -3
- package/dist/mock/LambderMockFailureInjector.js +12 -8
- package/dist/mock/LambderMockTypes.d.ts +8 -4
- package/dist/mock/lambderMockInvokeTransport.js +2 -9
- package/dist/mock/lambderMockMswHandler.d.ts +19 -13
- package/dist/mock/lambderMockMswHandler.js +25 -22
- package/dist/mock.d.ts +1 -0
- package/dist/secrets/LambderOneShotSecrets.d.ts +1 -1
- package/dist/secrets/LambderOneShotSecrets.js +6 -36
- package/dist/secrets/LambderPasswordHasher.d.ts +56 -0
- package/dist/secrets/LambderPasswordHasher.js +168 -0
- package/dist/session/LambderSessionController.d.ts +37 -4
- package/dist/session/LambderSessionController.js +36 -6
- package/dist/session/LambderSessionManager.d.ts +33 -3
- package/dist/session/LambderSessionManager.js +55 -16
- package/dist/shared/LambderHtml.d.ts +23 -4
- package/dist/shared/LambderHtml.js +17 -5
- package/dist/shared/LambderI18n.d.ts +106 -16
- package/dist/shared/LambderI18n.js +259 -74
- package/dist/shared/contracts/LambderCache.d.ts +14 -1
- package/dist/shared/contracts/LambderRateLimiter.d.ts +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
- package/dist/shared/util/LambderBackoffTimer.d.ts +24 -13
- package/dist/shared/util/LambderBackoffTimer.js +40 -12
- package/dist/shared/util/LambderCallAbort.d.ts +22 -6
- package/dist/shared/util/LambderCallAbort.js +37 -9
- package/dist/shared/util/LambderErrorReportMark.d.ts +4 -0
- package/dist/shared/util/LambderErrorReportMark.js +22 -0
- package/dist/shared/util/LambderKeyFieldDigest.d.ts +46 -0
- package/dist/shared/util/LambderKeyFieldDigest.js +75 -0
- package/dist/shared/util/LambderSignedClaims.d.ts +19 -0
- package/dist/shared/util/LambderSignedClaims.js +54 -0
- package/dist/shared/util/LambderSwappableInstances.d.ts +29 -0
- package/dist/shared/util/LambderSwappableInstances.js +54 -0
- package/dist/shared/util/LambderTestingDoors.d.ts +6 -2
- package/dist/shared/util/LambderTestingDoors.js +6 -2
- package/dist/shared/util/LambderTextDigest.d.ts +5 -5
- package/dist/shared/util/LambderTextDigest.js +5 -5
- package/dist/shared/wire/LambderApiContract.d.ts +1 -1
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +9 -1
- package/dist/shared/wire/LambderApiOptionEntries.js +15 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +3 -2
- package/dist/shared/wire/LambderApiOutcome.d.ts +37 -19
- package/dist/shared/wire/LambderApiOutcome.js +1 -1
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -12
- package/dist/shared/wire/LambderApiRefusal.js +3 -3
- package/dist/shared/wire/LambderApiSchemaEntries.d.ts +21 -0
- package/dist/shared/wire/LambderApiSchemaEntries.js +18 -0
- package/dist/shared/wire/LambderCallOptions.d.ts +3 -3
- package/dist/shared/wire/LambderCallOutcome.d.ts +2 -1
- package/dist/shared/wire/LambderCallOutcome.js +3 -2
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +5 -3
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -3
- package/dist/shared/wire/LambderInvokeApiId.d.ts +3 -3
- package/dist/shared/wire/LambderInvokeApiId.js +3 -3
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +7 -6
- package/dist/shared/wire/LambderOutcomeAssertions.js +1 -1
- package/dist/stores/LambderCacheValues.d.ts +2 -5
- package/dist/stores/LambderDdbCache.d.ts +15 -9
- package/dist/stores/LambderDdbCache.js +25 -6
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +6 -4
- package/dist/stores/LambderDdbIdempotencyStore.js +32 -52
- package/dist/stores/LambderDdbOneShotSecretStore.d.ts +11 -0
- package/dist/stores/LambderDdbOneShotSecretStore.js +17 -5
- package/dist/stores/LambderDdbSdk.d.ts +59 -29
- package/dist/stores/LambderDdbSdk.js +116 -44
- package/dist/stores/LambderDdbSessionStore.d.ts +26 -11
- package/dist/stores/LambderDdbSessionStore.js +133 -68
- package/dist/stores/LambderMemoryCache.d.ts +2 -2
- package/dist/stores/LambderS3FileSource.d.ts +3 -2
- package/dist/stores/LambderS3FileSource.js +11 -13
- package/dist/stores/LambderS3UploadBucket.d.ts +12 -0
- package/dist/stores/LambderS3UploadBucket.js +23 -0
- package/dist/stores/LambderStorageBackedCache.d.ts +2 -2
- package/dist/stores/LambderStoredText.d.ts +26 -0
- package/dist/stores/LambderStoredText.js +25 -0
- package/dist/testing/LambderConformanceRunner.d.ts +9 -2
- package/dist/testing/LambderConformanceRunner.js +9 -2
- package/dist/testing/LambderTestApp.d.ts +65 -7
- package/dist/testing/LambderTestApp.js +101 -6
- package/dist/testing/LambderTestVisitor.d.ts +43 -12
- package/dist/testing/LambderTestVisitor.js +35 -11
- package/dist/testing/bootLambdaPackage.d.ts +105 -0
- package/dist/testing/bootLambdaPackage.js +265 -0
- package/dist/testing/lambdaPackageBootProcess.d.ts +39 -0
- package/dist/testing/lambdaPackageBootProcess.js +130 -0
- package/dist/testing/lambderCacheConformance.d.ts +26 -0
- package/dist/testing/lambderCacheConformance.js +287 -0
- package/dist/testing/lambderCacheStorageConformance.d.ts +25 -0
- package/dist/testing/lambderCacheStorageConformance.js +159 -0
- package/dist/testing/lambderSessionStoreConformance.d.ts +3 -3
- package/dist/testing/lambderSessionStoreConformance.js +3 -3
- package/dist/testing.d.ts +7 -1
- package/dist/testing.js +5 -0
- package/package.json +1 -6
- package/dist/shared/util/boundKeyField.d.ts +0 -20
- package/dist/shared/util/boundKeyField.js +0 -34
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,435 @@ 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
|
+
|
|
34
|
+
## [15.0.1] - 2026-10-02
|
|
35
|
+
|
|
36
|
+
A major. Fixes for crashes, misconfigurations that passed silently, and places
|
|
37
|
+
where the mock and the test kit drifted from the server; and capabilities apps
|
|
38
|
+
were writing for themselves: an `aborted` outcome for a call the page cancels,
|
|
39
|
+
upload retries that keep their ticket, request provenance under the origin
|
|
40
|
+
proof, digested key fields at rest, guard inputs in the idempotency identity,
|
|
41
|
+
plural forms, typed template data, dev-only schemas the mock validates against,
|
|
42
|
+
a test app that keeps an app's own caches, one-shot secret stores, upload
|
|
43
|
+
buckets and invoke callers off AWS by default, a boot check for
|
|
44
|
+
a built deployment package, and argon2id password hashing. Every break is
|
|
45
|
+
named under Changed.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **`randomCode(alphabet, length)`**, from `lambder` and `lambder/client`
|
|
50
|
+
beside `randomSecret`: a code a person types or reads out, drawn evenly from
|
|
51
|
+
the alphabet with the cryptographic random source. Bytes past the largest
|
|
52
|
+
multiple of the alphabet's size are discarded rather than folded in with a
|
|
53
|
+
modulo, which favors the first characters. An alphabet that is not 2 to 256
|
|
54
|
+
distinct characters, or a length that is not a positive integer, throws.
|
|
55
|
+
`LambderOneShotSecrets` draws its codes with it.
|
|
56
|
+
- **`LambderPasswordHasher`**, from `lambder`: passwords at rest as argon2id
|
|
57
|
+
through node's own `crypto.argon2` (Node 24.7 and later), in the PHC string
|
|
58
|
+
every argon2 library reads and writes, so no native addon ships in the
|
|
59
|
+
deployment package. `hash(password)` writes argon2id under the instance's
|
|
60
|
+
cost (`memoryKib`, `passes`, `parallelism`; default 64 MiB, 3, 4) with a
|
|
61
|
+
fresh salt. `verify(stored, password)` reads the variant and cost from the
|
|
62
|
+
stored string, so it verifies argon2id, argon2i and argon2d strings other
|
|
63
|
+
libraries wrote, parameters in any order, and answers false rather than
|
|
64
|
+
throwing for anything that is not one (`null` for an account with no
|
|
65
|
+
password, `undefined` for one that does not exist), still computing one hash
|
|
66
|
+
under the instance's cost so the time taken does not say which accounts
|
|
67
|
+
exist. A cost has ceilings, for what an instance writes and what it
|
|
68
|
+
verifies alike: 2 GiB of memory, 4 GiB of memory over all passes, 255 lanes;
|
|
69
|
+
a stored string over them matches no password rather than stalling every
|
|
70
|
+
sign-in on its account. `needsRehash(stored)` says when a stored hash was
|
|
71
|
+
written under another variant or cost, so a raised cost reaches old
|
|
72
|
+
passwords at their next sign-in. A runtime without argon2 throws at
|
|
73
|
+
construction rather than failing every sign-in. New type:
|
|
74
|
+
`LambderPasswordHasherOptions`.
|
|
75
|
+
- **`fetchSession({ refreshData: true })` and
|
|
76
|
+
`fetchSessionIfExists({ refreshData: true })`** on the session controller,
|
|
77
|
+
and `renewSession(session, { refreshData: true })` on the manager: renew the
|
|
78
|
+
session's data on that read whatever its deadline, running the `dataRefresh`
|
|
79
|
+
callback and the store write once. A read followed by `refreshSessionData()`
|
|
80
|
+
ran both twice whenever the data was due. Without `dataRefresh` they throw.
|
|
81
|
+
- **`LambderRenderContextOf<typeof app>` and
|
|
82
|
+
`LambderSessionRenderContextOf<typeof app>`** name one instance's render
|
|
83
|
+
context, with its session data and rate-limit policy names, for typing a
|
|
84
|
+
helper in another file without writing out `LambderRenderContext`'s
|
|
85
|
+
parameters. Contexts from API handlers, routes, hooks and guards are all
|
|
86
|
+
assignable to them.
|
|
87
|
+
- **`apisWithGuard(apiOptions, guard)`**, from `lambder` and `lambder/client`:
|
|
88
|
+
the names of the APIs whose guards option names a guard, typed as
|
|
89
|
+
`LambderApisWithGuard` and in the table's order, so a test can loop over the
|
|
90
|
+
APIs behind a guard without casting the generated table.
|
|
91
|
+
- **`lambderCacheConformance` and `lambderCacheStorageConformance`**
|
|
92
|
+
(`lambder/testing`), with their options types: the rules every `LambderCache`
|
|
93
|
+
keeps, and the rules for the `LambderCacheStorage` under
|
|
94
|
+
`LambderStorageBackedCache`. An app that writes its own cache or storage
|
|
95
|
+
checks it against the rules Lambder's own meet. The storage suite targets
|
|
96
|
+
the usual database slips: jsonb reordering keys, case-insensitive or
|
|
97
|
+
space-padding collations, LIKE wildcards in a listing prefix, partitions
|
|
98
|
+
matched by prefix.
|
|
99
|
+
- **`LambderI18nReadonlyInstance<TContract>`**: the reading members of any
|
|
100
|
+
LambderI18n instance of one contract, whatever its language set, without the
|
|
101
|
+
members that switch languages, load or register dictionaries, touch the
|
|
102
|
+
document or build extensions. Code that only reads translations takes any
|
|
103
|
+
such instance without restating its members.
|
|
104
|
+
- **`jsonScript({ type: "application/ld+json" }, data)`** writes JSON-LD with
|
|
105
|
+
the same escaping (`<`, U+2028, U+2029) as the hydration form.
|
|
106
|
+
`jsonScript(id, data)` is unchanged, the options form `{ id, type? }` writes
|
|
107
|
+
`application/json`, and any other type is refused. New type:
|
|
108
|
+
`LambderJsonScriptOptions`.
|
|
109
|
+
- **`LambderBackoffTimer.retries`**: the retries `retry` and `wait` have
|
|
110
|
+
scheduled since the last `reset()`, so a loop that gives up after so many
|
|
111
|
+
reads the timer instead of keeping a counter. `LambderUploadRunner` counts
|
|
112
|
+
its storage retries this way.
|
|
113
|
+
- **`LambderMockApp.pageCookieJar`**: the page's cookie jar, owned by the
|
|
114
|
+
runtime and emptied by `reset()` (see Changed).
|
|
115
|
+
- **`readApiEnvelopeText(text, info, apiName)`**: `readApiEnvelope` for an
|
|
116
|
+
adapter that holds only the body's text. An empty text is an empty
|
|
117
|
+
envelope; text that is not JSON is flagged as no envelope (see Fixed).
|
|
118
|
+
- **`ctx.arrivedVia`** (`LambderRequestArrival`): how the request reached the
|
|
119
|
+
function. `"proxy"` with a valid origin proof, `"direct"` when the proof is
|
|
120
|
+
configured and missing or wrong, `"invoke"` for a Lambda invoke (read from
|
|
121
|
+
the event's `requestContext.apiId`, which no HTTP client can set), and
|
|
122
|
+
`"unverified"` when no proof is configured. An invoke-only endpoint checks
|
|
123
|
+
it rather than the `x-lambder-invoke` header, which any client can send.
|
|
124
|
+
- **`originProof.proxyHeaders`**: headers only the proxy may write. On a
|
|
125
|
+
request without a valid proof they are removed from `ctx.headers`,
|
|
126
|
+
`ctx.header()` and the API request's headers, with the trusted client-IP and
|
|
127
|
+
host headers, so a handler reads the proxy's value or nothing. An origin
|
|
128
|
+
proof with no trusted headers is accepted, since `arrivedVia` is useful on
|
|
129
|
+
its own.
|
|
130
|
+
- **`lambder/rate-limited` carries typed data**, `{ policy, retryAfterSeconds }`
|
|
131
|
+
(`LambderRateLimitRefusalData`), from the server and the mock alike, as its
|
|
132
|
+
own arm of `LambderRefusalMessage`, so a client narrows `message.data` and
|
|
133
|
+
words each limit its own way. The policy's name reaches every client, so
|
|
134
|
+
name policies fit to show. The mock's injected rate limit takes `policy`.
|
|
135
|
+
- **Plural forms in LambderI18n.** An entry may be a plural entry keyed by
|
|
136
|
+
`Intl.PluralRules` category with `other` required,
|
|
137
|
+
`{ one: "{count} item", other: "{count} items" }`, and `t(key, { count })`
|
|
138
|
+
picks the form under the rules of the text's language, so a language with
|
|
139
|
+
six categories (Arabic) translates as well as English. A plural key needs a
|
|
140
|
+
numeric `count` at compile time beside its tokens. The default language's
|
|
141
|
+
entry decides a key's kind: a text key takes texts, a plural key takes
|
|
142
|
+
plural entries or a plain text standing for `other`. Malformed entries are
|
|
143
|
+
refused where a dictionary arrives. Category coverage depends on each
|
|
144
|
+
runtime's CLDR data, so it never fails at runtime: a count whose category an
|
|
145
|
+
entry lacks shows its `other` form. **`checkPluralCoverage()`** is the strict
|
|
146
|
+
check for tests and builds: it loads every language and rejects with every
|
|
147
|
+
gap by key, language and missing categories. New types:
|
|
148
|
+
`LambderI18nDictionaryEntry`, `LambderI18nPluralEntry`,
|
|
149
|
+
`LambderI18nPluralCategory`.
|
|
150
|
+
- **Typed template data.** `LambderTemplatingEngine<TNames>`,
|
|
151
|
+
`fromFile<TNames>` and `res.templateFile<TNames>(...)` (and on `res.die`)
|
|
152
|
+
type the data by the template's slot and condition names, so a misspelled
|
|
153
|
+
key is a compile error; `LambderTemplateData` takes the names. New type
|
|
154
|
+
`LambderTemplateFileOptions`. The runtime check is under Changed.
|
|
155
|
+
- **`writeApiSchemas`** (`lambder/build`, and `schemas` in `generateApiFiles`):
|
|
156
|
+
every API's input schema as a client posts it and output schema as a client
|
|
157
|
+
receives it, as JSON Schema in a module of plain data, for the mock in
|
|
158
|
+
development. It lists every refinement, transform, pipe, computed default and
|
|
159
|
+
string check the file cannot carry (a regex's flags, a URL's protocol or
|
|
160
|
+
hostname rule), never writes a check the mock would read back stricter than
|
|
161
|
+
the server (a guid as a uuid, an email under its own pattern), and throws,
|
|
162
|
+
naming the API and field, for anything JSON Schema cannot represent. The schemas come from the new instance method
|
|
163
|
+
`apiSchemaEntries()`. New types: `LambderApiSchemasSource`,
|
|
164
|
+
`LambderApiSchemasFileOptions`, `LambderApiSchemasFileResult`,
|
|
165
|
+
`LambderApiSchemaLoss`.
|
|
166
|
+
- **The mock's `apiSchemas` option** takes that module. The mock then validates
|
|
167
|
+
every call's input against the server's schema and refuses it as the server
|
|
168
|
+
does (422, or what `onInvalidInput` answers), hands the handler the payload
|
|
169
|
+
with undeclared keys dropped and defaults filled, and parses every answer
|
|
170
|
+
through the output schema, so fields the server strips no longer reach the
|
|
171
|
+
client in development. Each schema is rebuilt with `z.fromJSONSchema` the
|
|
172
|
+
first time it is needed; an entry's own `input` schema applies after it. The
|
|
173
|
+
module names every endpoint, so import it only from the mock's setup. New
|
|
174
|
+
types (`lambder/mock`): `LambderApiSchemaEntries`, `LambderApiSchemaEntry`,
|
|
175
|
+
`LambderJsonSchema`.
|
|
176
|
+
- **`bootLambdaPackage(options)`** (`lambder/testing`): boots a built
|
|
177
|
+
deployment package in a fresh node process as Lambda does, hands the handler
|
|
178
|
+
API calls (`{ api, payload }`, turned into gateway or invoke events) or raw
|
|
179
|
+
events, and reports each answer (an outcome for an API call), the import
|
|
180
|
+
time and memory, and the phase and call that failed. Imports the package
|
|
181
|
+
makes are held to the package, so a dependency it does not carry fails here
|
|
182
|
+
rather than after a deploy; what the runtime supplies (the AWS SDK by
|
|
183
|
+
default) resolves from an install outside it. The process sees only `PATH`
|
|
184
|
+
and the given `env`. Needs Node 22.15 or later. New types:
|
|
185
|
+
`LambderPackageBootOptions`, `LambderPackageBootCall`,
|
|
186
|
+
`LambderPackageBootResult`, `LambderPackageBootCallResult`,
|
|
187
|
+
`LambderPackageBootMeasurements`.
|
|
188
|
+
- **`testApp.assertNoCrashesAfterEach(afterEach)`** fails any test the app
|
|
189
|
+
crashed in, listing each crash with its stack, in place of a crash check
|
|
190
|
+
repeated in every test file. **`testApp.memoryTwinOf(store)`** hands back the
|
|
191
|
+
in-memory copy a swapped store answers from, to seed or inspect it. New
|
|
192
|
+
types `LambderTestVisitorEndpoint` and `LambderTestVisitorGroupCalls`.
|
|
193
|
+
- **`LambderApiFailure`**, from `lambder/client` and `lambder`: the failure
|
|
194
|
+
side of an API outcome, which `errorHandler` now receives (see Changed).
|
|
195
|
+
|
|
196
|
+
### Changed
|
|
197
|
+
|
|
198
|
+
- **Breaking: a `minApiVersion` above `apiVersion` throws at creation**, on
|
|
199
|
+
the server and in the mock, like every other misconfiguration. It was
|
|
200
|
+
clamped to `apiVersion` with a console warning.
|
|
201
|
+
- **Breaking (types): `lambder.apiPath`, `lambder.apiVersion` and
|
|
202
|
+
`lambder.files` are `readonly`.** The pipeline, crash handling and public
|
|
203
|
+
files keep what they were built with, so a later write left the instance
|
|
204
|
+
disagreeing with itself.
|
|
205
|
+
- **Breaking (types): `LambderCache.getOrSet` takes
|
|
206
|
+
`LambderCacheGetOrSetOptions`**, which adds `leaseSeconds` and
|
|
207
|
+
`waitForFillMs` to `ttlSeconds`, so code typed against the interface can tune
|
|
208
|
+
the DynamoDB cache's fill lease. The type was `LambderDdbCacheGetOrSetOptions`;
|
|
209
|
+
that name is gone. Every cache already checked these options at runtime.
|
|
210
|
+
- **Breaking (types): `LambderSessionManager.expireSessionDataAllByKey()`
|
|
211
|
+
returns `Promise<void>`**, as the controller's does, in place of a boolean
|
|
212
|
+
that was always `true`.
|
|
213
|
+
- **Breaking (API core): `readApiEnvelope` takes the parsed body as
|
|
214
|
+
`unknown`** and a `bodyNotJson` flag, and flags a body that is not a plain
|
|
215
|
+
object on the request's new `invalidEnvelope` field. A `null` body is
|
|
216
|
+
flagged where it read as an empty envelope; an absent body still reads as
|
|
217
|
+
an empty one.
|
|
218
|
+
- **The session controller's `endSessionAll()` and `deleteSessionAllByKey()`
|
|
219
|
+
return `Promise<boolean>`**, passing on the manager's answer: false means a
|
|
220
|
+
session rotating in a tight loop raced every pass of the delete and may
|
|
221
|
+
still stand, so run `deleteSessionAllByKey()` again. The password-change
|
|
222
|
+
recipe in the sessions docs checks it.
|
|
223
|
+
- **Breaking (mock): with no jar, `signIn` and `signOut` use
|
|
224
|
+
`pageCookieJar`, and `lambderMockMswHandler` uses it unless given
|
|
225
|
+
`cookieJar`.** `signIn(key, data)` alone now signs the page in behind the
|
|
226
|
+
MSW worker; the session used to go to no jar and every call answered
|
|
227
|
+
`sessionExpired`.
|
|
228
|
+
- **Breaking (mock): `mock.guard` from a mock that declared a refusal
|
|
229
|
+
vocabulary is bound to it**, as the server init's `guard` is: `ctx.refuse` is
|
|
230
|
+
typed to the guard's own `refusals`, and a code outside the vocabulary fails
|
|
231
|
+
to compile and throws when the guard is built. `checkApiRefusals` checks
|
|
232
|
+
these guards rather than listing them as unchecked.
|
|
233
|
+
- **Breaking (testing): `lambderTestApp` throws when `session.store`,
|
|
234
|
+
`rateLimits.limiter` or `idempotency.store` is given for an app created
|
|
235
|
+
without that subsystem**, as it already did for `files`. The store was
|
|
236
|
+
silently dropped and the suite ran over nothing.
|
|
237
|
+
- **Breaking: `LambderBackoffTimer` refuses at construction** a `baseMs` of 0
|
|
238
|
+
or less (every wait was 0 ms), a `baseMs` or `maxMs` above 2147483647 ms
|
|
239
|
+
(which `setTimeout` fires at once), and an unknown `jitter`.
|
|
240
|
+
`LambderUploadRunner` therefore refuses a `storageRetry.baseDelayMs` of 0
|
|
241
|
+
when it is built.
|
|
242
|
+
- **LambderI18n compares language codes case-insensitively**, as BCP 47 tags
|
|
243
|
+
are, and always answers with the code as registered: a browser's `pt-br`, a
|
|
244
|
+
detector's `PT-BR` and `setLanguage("pt-br")` select a registered `pt-BR`,
|
|
245
|
+
as do `forLanguage`, `loadLanguage` and `registerDictionary`.
|
|
246
|
+
`isLanguageCode` stays exact, since it narrows its argument. Breaking only
|
|
247
|
+
for a config that registers two codes differing only in case, which
|
|
248
|
+
`createLambderI18n` refuses.
|
|
249
|
+
- **`LambderCaller`'s `apiPath` is optional** and defaults to `"/api"`, the
|
|
250
|
+
server's own default, from the constant `LambderInvokeCaller` uses.
|
|
251
|
+
- **`LambderDdbIdempotencyStore.complete()` answers `"too-large"` for a body
|
|
252
|
+
over 32 MB**, however small it compresses. Such a body was stored and then
|
|
253
|
+
refused on every replay.
|
|
254
|
+
- **One-shot secret alphabets are counted in code points**, so an alphabet
|
|
255
|
+
of characters outside the basic plane is accepted, where it was refused as
|
|
256
|
+
not distinct.
|
|
257
|
+
- **`addHook("created", fn, priority)` honors the priority** like the other
|
|
258
|
+
hooks: lower runs first, and hooks of one priority run in the order they
|
|
259
|
+
were added.
|
|
260
|
+
|
|
261
|
+
- **Breaking: `res.html`, `res.status`, `res.status404` and `res.xml` (and
|
|
262
|
+
`res.die.*`) take safe markup only**, what `html`/`xml`, `raw()` and
|
|
263
|
+
`jsonScript()` build. A plain string is a compile error and throws, naming
|
|
264
|
+
the tag, `raw()` and `res.text`, so a route cannot reflect request text into
|
|
265
|
+
a page unescaped. Plain text goes out with `res.text(body, { statusCode })`.
|
|
266
|
+
The framework's own 401 for a session route without a session and
|
|
267
|
+
`res.file`'s 404 are `text/plain`.
|
|
268
|
+
- **Breaking: rendering a template with a data key it has no slot or
|
|
269
|
+
condition for throws**, naming the key and the template's names;
|
|
270
|
+
`res.templateFile` also names the file. A slot renamed in the HTML dropped
|
|
271
|
+
the server's content silently.
|
|
272
|
+
- **Breaking: `finalizeResponse`, `answerFromResponse`, `responseFromAnswer`
|
|
273
|
+
and `LambderFinalizeOptions` are no longer exported.** They take internal
|
|
274
|
+
options no consumer could build.
|
|
275
|
+
- **Breaking: a call aborted through its own `signal` fails with the new reason
|
|
276
|
+
`aborted`**, on `LambderCaller` and `LambderInvokeCaller`, in place of
|
|
277
|
+
`network`, and is never handed to `errorHandler` or `onFailure`, so a page
|
|
278
|
+
can cancel a superseded read without a "could not reach the server" alert.
|
|
279
|
+
`fetchEndedHandler` still hears it, a `timeout` is still reported, and
|
|
280
|
+
whichever aborted the call first names it. A signal that aborts with a
|
|
281
|
+
`TimeoutError` (`AbortSignal.timeout()`, alone or inside `AbortSignal.any()`)
|
|
282
|
+
carries a deadline rather than the page's choice, and fails the call as
|
|
283
|
+
`timeout`. A key scope treats an aborted call as not sent before sending and
|
|
284
|
+
possibly used after. The upload runner's `cancelled` reason is `aborted` too.
|
|
285
|
+
- **`errorHandler` is called as `(error, failure)`**, `failure` being the
|
|
286
|
+
call's typed failure (`LambderApiFailure`), the reload-loop and
|
|
287
|
+
missing-handler cases included; a one-argument handler keeps working.
|
|
288
|
+
**Breaking:** `fetchEndedHandler`'s `fetchResult` is the call's outcome, the
|
|
289
|
+
object `apiOutcome()` resolves to, in place of an envelope, an Error or null.
|
|
290
|
+
- **Breaking: `LambderUploadRunner`'s `requestTicket` and `confirmUpload`
|
|
291
|
+
return the caller's outcome** (`caller.x.y.outcome()`), and the runner
|
|
292
|
+
retries their `network`, `timeout` and `server` failures on its backoff timer
|
|
293
|
+
with the same ticket, so a dropped confirm after the bytes are stored no
|
|
294
|
+
longer ends the upload or leaves an orphan object. Each step gets every
|
|
295
|
+
attempt. The ticket endpoint's output, which carries `ticket`, is handed to
|
|
296
|
+
`confirmUpload` in place of `{ ticket, reference }`, and `LambderUploadError`
|
|
297
|
+
carries the failed call's outcome as `callFailure`. A retried call may
|
|
298
|
+
follow one that ran and lost its answer, so a confirm with an effect beyond
|
|
299
|
+
marking its record is declared idempotent and called with one key per
|
|
300
|
+
upload (`confirm-${invoiceId}`); the uploads docs show it.
|
|
301
|
+
- **A `LambderInvokeError` whose failure `onFailure` already received is not
|
|
302
|
+
reported again** by the calling instance's `crashes.report`; the request is
|
|
303
|
+
still answered as a crash. Apps drop their `isLambderInvokeError` skip.
|
|
304
|
+
- **Breaking: `rateLimitRefusal(detail, { policy, retryAfterSeconds }, words?)`**
|
|
305
|
+
replaces `(detail, retryAfterSeconds, message?)`, and
|
|
306
|
+
`DEFAULT_RATE_LIMIT_REFUSAL` is the default words alone. A refusal carrying
|
|
307
|
+
`lambder/rate-limited` without its data is a crash, and `res.apiRefusal`
|
|
308
|
+
refuses the code. An exhaustive switch over `LambderRefusalMessage` asserts
|
|
309
|
+
`never` on the message rather than on `message.code`.
|
|
310
|
+
- **Breaking: caller-controlled key fields are digests at rest.** Every
|
|
311
|
+
caller-controlled field of a rate-limit tracker key and an idempotency scope
|
|
312
|
+
(the address, session key, custom key, `callerIdentity`, posted idempotency
|
|
313
|
+
key) is written as a fixed-length digest at any length; it was readable up
|
|
314
|
+
to 1,024 bytes, so emails and tokens sat in partition keys. With sessions
|
|
315
|
+
configured it is an HMAC under an HKDF subkey of the `sessionSalt`; without
|
|
316
|
+
them a plain SHA-256, which a table reader can test guesses against. API and
|
|
317
|
+
policy names stay readable. Counters and idempotency records start afresh
|
|
318
|
+
once on upgrade, and again whenever `sessionSalt` changes.
|
|
319
|
+
- **Breaking: a `guardInput` guard's value counts in an idempotency key's
|
|
320
|
+
request**, beside the payload: the same key with another store or tenant is
|
|
321
|
+
409 `lambder/idempotency-key-reused`, where it replayed the first answer. A
|
|
322
|
+
guard whose input can differ between attempts of one operation declares
|
|
323
|
+
`singleUseInput: true`, so a retry still replays: a single-use proof (a fresh
|
|
324
|
+
captcha token, a one-time code) or a credential refreshed between attempts
|
|
325
|
+
(a short-lived token). Apps with such guards add it; without it the retry is
|
|
326
|
+
refused and the person's next attempt runs the operation again.
|
|
327
|
+
- **Breaking: `LambderDdbSessionStore` takes a `DynamoDBClient`**, like every
|
|
328
|
+
other DynamoDB store, and `@aws-sdk/lib-dynamodb` is no longer a peer
|
|
329
|
+
dependency. The store writes and reads attribute values itself, in the shape
|
|
330
|
+
the document client wrote, so existing tables read unchanged; without a
|
|
331
|
+
client it shares the per-region default client with the other stores. Pass
|
|
332
|
+
the `DynamoDBClient` a document client was built from.
|
|
333
|
+
- **Breaking: `checkApiRefusals` fails on a handler it could not check**, a
|
|
334
|
+
finding of the new kind `unchecked`, where it was printed and passed. The
|
|
335
|
+
line says to declare the vocabulary on that init or give the guard its
|
|
336
|
+
codes; `requireTypedRefuse: false` lists such handlers without failing. A
|
|
337
|
+
guard with an explicit `refusals: []` is checked against no codes.
|
|
338
|
+
- **Breaking (mock): the MSW adapter's bookkeeping is off `LambderMockApp`.**
|
|
339
|
+
`hasRegisteredEntry`, `notePassthrough` and `mirrorCookiesIntoDocument` sit
|
|
340
|
+
behind a door only the package's adapter names, `adoptCookieJar` is gone, and
|
|
341
|
+
`lambderMockMswHandler` takes a `LambderMockApp` and refuses anything else.
|
|
342
|
+
- **Breaking (mock): a mock guard declares exactly the refusal codes the
|
|
343
|
+
server's guard of its name declares in `guardDeclarations`**: a different
|
|
344
|
+
list is a compile error at the `guards` option, naming the codes, and
|
|
345
|
+
`create()` throws for one the compiler could not compare.
|
|
346
|
+
- **Breaking (testing): `lambderTestApp` swaps the Lambder classes an app
|
|
347
|
+
builds itself**, built before the test app or after it. Each
|
|
348
|
+
`LambderDdbCache`, `LambderDdbOneShotSecretStore` and `LambderS3UploadBucket`
|
|
349
|
+
answers from an in-memory copy built with its own settings, and each
|
|
350
|
+
`LambderInvokeCaller` built without its own transport is answered by the
|
|
351
|
+
mock app the new `invokeMocks` option gives for its function; with none,
|
|
352
|
+
every call fails naming the function, so a test never reaches AWS without a
|
|
353
|
+
hand-written fake. `reset()` empties the copies.
|
|
354
|
+
- **Breaking (testing): a visitor's plain call returns the output and throws
|
|
355
|
+
on any failure**, naming the endpoint and describing what came back as
|
|
356
|
+
`assertApiSuccess` does, the app's crash included; it returned `undefined`,
|
|
357
|
+
which surfaced later as a read of `undefined`. `.outcome()` is for a test
|
|
358
|
+
that expects a failure.
|
|
359
|
+
- **`generateApiFiles` prints each contract in a Node process of its own**,
|
|
360
|
+
with a heap of `contractHeapMegabytes` (default 8192) and none of the
|
|
361
|
+
script's command-line flags, so one call serves a large app whatever loader
|
|
362
|
+
its script runs under. A process that runs out of heap fails its contract
|
|
363
|
+
and says to raise the option. New type: `LambderApiFilesOptions`.
|
|
364
|
+
|
|
365
|
+
### Fixed
|
|
366
|
+
|
|
367
|
+
- **A JSON body that is not an object no longer crashes an API call.** A body
|
|
368
|
+
of `5`, `"x"`, `true`, `null`, an array, or no JSON at all is answered with
|
|
369
|
+
the `lambder/invalid-request-payload` refusal (400), the same way by the
|
|
370
|
+
server and by every mock adapter that reads a raw body (the MSW handler and
|
|
371
|
+
the mock invoke transport). The server crashed on it; the mock answered 422,
|
|
372
|
+
or the MSW handler passed a non-JSON body on to the network. The rule lives
|
|
373
|
+
once in the API core: `readApiEnvelope` flags such a body on the request as
|
|
374
|
+
`invalidEnvelope`, and the pipeline's `prepare()` refuses it before the
|
|
375
|
+
version floor, the signature gate or anything else reads the call. On the
|
|
376
|
+
server `ctx.post` is always a plain object: a JSON body that is not an
|
|
377
|
+
object leaves it empty, a body that is not JSON fills it with its form
|
|
378
|
+
fields, and either stays readable as `ctx.rawBody`.
|
|
379
|
+
- **With `apiPath: "/"`, a request to the site root walks the fallback
|
|
380
|
+
chain** (public files, the index shell, the route fallback) rather than
|
|
381
|
+
getting the API not-found envelope; only an actual API call is the API's. A
|
|
382
|
+
non-root `apiPath` is unchanged.
|
|
383
|
+
- **The framework's plain-text 500 for a crashed route carries
|
|
384
|
+
`Content-Type: text/plain; charset=utf-8`**, as its 404 does.
|
|
385
|
+
- **The call summary records the code of a `sessionExpired` refusal** raised
|
|
386
|
+
with a declared code, as it does for `notAuthorized`.
|
|
387
|
+
- **The compile-time checks for a group name given twice and an action
|
|
388
|
+
declared by two parts of a group evaluate as loops**, so an app of many
|
|
389
|
+
small groups (150 or more) compiles; they hit TypeScript's instantiation
|
|
390
|
+
depth limit.
|
|
391
|
+
- **`LambderDdbSessionStore` caps session data at 32 MB of JSON and refuses
|
|
392
|
+
data too large for a DynamoDB item**, on create and update, before writing,
|
|
393
|
+
with an error naming the size and the limit, in place of DynamoDB's raw
|
|
394
|
+
`ValidationException` (a 500 at sign-in with nothing to act on). A stored
|
|
395
|
+
record declaring more than 32 MB reads as no session and is never
|
|
396
|
+
decompressed. Existing records read as before; the session, idempotency and
|
|
397
|
+
cache stores share one implementation of the compressed-or-plain attribute.
|
|
398
|
+
- **`regenerateSession()` compares stored data with stored data.** It
|
|
399
|
+
compared the request's schema output with the record as stored, so with a
|
|
400
|
+
schema that strips keys every rotation saw a change that never happened,
|
|
401
|
+
wrote it back and handed the request unstripped data, and with a schema
|
|
402
|
+
that transforms it threw. It reads the stored record first (one more store
|
|
403
|
+
read per rotation), carries it over as stored, and gives the request the
|
|
404
|
+
schema's output.
|
|
405
|
+
- **`LambderInvokeCaller` leaves the key of a call whose signal had already
|
|
406
|
+
aborted before it was sent unused**, as `LambderCaller` does and as it
|
|
407
|
+
already did for an event over the invoke cap: a refusal after it moves the
|
|
408
|
+
scope on to a new key.
|
|
409
|
+
- **`stopWaitingWhenAborted` rejects at once for a signal that had already
|
|
410
|
+
aborted**, and still handles a later rejection of the work.
|
|
411
|
+
- **A call's timeout firing after its signal had already aborted it no longer
|
|
412
|
+
reports the call as `timeout`.**
|
|
413
|
+
- **The conformance suites' clock starts at 2100-01-01**, so a store whose
|
|
414
|
+
database deletes expired rows itself (a DynamoDB TTL, Redis `EXPIREAT`) runs
|
|
415
|
+
the suites with that deletion on.
|
|
416
|
+
- **`LambderS3FileSource` does not remember a failed `@aws-sdk/client-s3`
|
|
417
|
+
load**: a read after the package is installed in the same process succeeds,
|
|
418
|
+
as it does for the other stores and buckets.
|
|
419
|
+
- **`checkApiRefusals` recognizes a raise by the refuse function the call
|
|
420
|
+
resolves to**, including the forms written out in installed declaration
|
|
421
|
+
files. A refuse reached under a renamed import, a variable, a destructured
|
|
422
|
+
name or a typed parameter is checked; it was skipped, so an undeclared code
|
|
423
|
+
passed and a declared one read as unused. A project that passed may now
|
|
424
|
+
report findings.
|
|
425
|
+
- **An injected `sessionExpired`, or an injected refusal whose code is
|
|
426
|
+
declared `sessionExpired`, ends the session the call carries**, as on the
|
|
427
|
+
server. The next call was still signed in.
|
|
428
|
+
- **The mock's `reset()` expires cookies mirrored into `document.cookie` at
|
|
429
|
+
the Domain and Path each was set with**; a cookie set under a cookie domain
|
|
430
|
+
survived it.
|
|
431
|
+
- **Docs:** the templating and frontend-hosting examples work as raw files in
|
|
432
|
+
a development browser (the title through `htmlVirtualSlots`, the if/else
|
|
433
|
+
example without two `<body>` tags); the mock docs say refusal data is parsed
|
|
434
|
+
under `declareRefusals` and that the server's hooks do not run in the mock;
|
|
435
|
+
the testing docs name the one-shot secret store and the upload bucket among
|
|
436
|
+
the classes an app swaps itself; `docs/invoke.md`'s reporter example
|
|
437
|
+
compiles; the i18n docs import from `lambder/client`; `LambderBackoffTimer`
|
|
438
|
+
is documented with the client; the `initLambder` documentation is one block
|
|
439
|
+
on the declaration, so editors show it on hover.
|
|
440
|
+
|
|
12
441
|
## [14.1.1] - 2026-09-30
|
|
13
442
|
|
|
14
443
|
A minor: a refusal code can declare the `sessionExpired` flag, and
|
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ npm install lambder zod
|
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
`zod` is a required peer dependency: the published declarations name its types,
|
|
109
|
-
so npm installs it alongside lambder. The
|
|
109
|
+
so npm installs it alongside lambder. The AWS SDK packages and `msw` are
|
|
110
110
|
optional peers, so installing lambder never drags them into your tree. Add
|
|
111
111
|
whatever the code you actually import needs:
|
|
112
112
|
|
|
@@ -114,7 +114,7 @@ whatever the code you actually import needs:
|
|
|
114
114
|
| --- | --- |
|
|
115
115
|
| `lambder/client` (browser, shared isomorphic code) | `zod`. `LambderCookieJar` pulls in `tough-cookie` and its public suffix list, so a bundle that never imports the jar never carries either |
|
|
116
116
|
| `lambder` on AWS Lambda (any current Node.js runtime; the package needs Node 20 or later) | `zod`. The runtime already provides the AWS SDK v3, so mark the SDK packages as dev dependencies and keep them out of the deployment package, as long as the runtime's `@aws-sdk/client-dynamodb` is new enough for `LambderDdbRateLimiter` (3.868.0, see below) |
|
|
117
|
-
| `lambder` anywhere else (a long-running server, a container, local tests) | `zod`, plus `@aws-sdk/client-dynamodb`
|
|
117
|
+
| `lambder` anywhere else (a long-running server, a container, local tests) | `zod`, plus `@aws-sdk/client-dynamodb` when sessions or the DynamoDB stores are used; it is loaded on the first table access, so an app that uses neither does not need it |
|
|
118
118
|
| `LambderS3FileSource` | `@aws-sdk/client-s3`, loaded on first read |
|
|
119
119
|
| `LambderInvokeCaller` | `@aws-sdk/client-lambda`, loaded on the first call |
|
|
120
120
|
| `lambder/mock` | nothing; `msw` only for the optional network-panel adapter |
|
|
@@ -140,7 +140,7 @@ The package ships five entry points; pick by where the code runs:
|
|
|
140
140
|
| `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `LambderUploadRunner`, `LambderBackoffTimer`, `LambderSignedClaims`, `html`/`xml` tagged templates, `createLambderI18n` |
|
|
141
141
|
| `lambder/mock` | Browser and Node, in development and tests | `LambderMockApp`, the mock runtime: your typed contract served from mock handlers over the real API pipeline and memory stores |
|
|
142
142
|
| `lambder/testing` | Node, in tests | `lambderTestApp`: your real instance under test in this process, memory stores put under it in place, simulated browsers with typed callers in front of it, and the outcome assertions; the store conformance suites, to hold a store you write to the rules Lambder's own meet |
|
|
143
|
-
| `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiOptions`: every API's declared options, policies and guard declarations as plain data, for the code that decides with them; `writeApiGuardParams`: one guard's parameters, one export per API and nothing else, so a browser gating on that guard carries only the ones it imports; `writeApiContract`: the contract as plain types a client compiles instead of the server; `generateApiFiles`: every one of them, for every app a script names, in one call |
|
|
143
|
+
| `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiOptions`: every API's declared options, policies and guard declarations as plain data, for the code that decides with them; `writeApiGuardParams`: one guard's parameters, one export per API and nothing else, so a browser gating on that guard carries only the ones it imports; `writeApiContract`: the contract as plain types a client compiles instead of the server; `generateApiFiles`: every one of them, for every app a script names, in one call; `checkApiRefusals`: every refusal a handler can reach, held to the codes its endpoint may send |
|
|
144
144
|
|
|
145
145
|
Frontends and shared isomorphic packages should import from `lambder/client`
|
|
146
146
|
only; the entry's module graph contains no AWS SDK, Node built-ins, or server
|
|
@@ -158,7 +158,7 @@ and the helpers the two caches share), `src/client/`,
|
|
|
158
158
|
transport), `src/mock/` (the mock runtime), `src/testing/` (the test app and the store conformance suites),
|
|
159
159
|
`src/build/` (what a generator script runs at build time), and `src/shared/`
|
|
160
160
|
(isomorphic modules every entry re-exports, grouped into `wire/` for the
|
|
161
|
-
format both sides speak, `contracts/` for the
|
|
161
|
+
format both sides speak, `contracts/` for the seven store and source
|
|
162
162
|
interfaces, `transport/` for the caller-to-server seam, and `util/` for
|
|
163
163
|
helpers).
|
|
164
164
|
Directories are layers and imports only ever point down;
|
|
@@ -209,19 +209,16 @@ framework:
|
|
|
209
209
|
## Versioning and changes
|
|
210
210
|
|
|
211
211
|
Released versions and what each one changed are in
|
|
212
|
-
[CHANGELOG.md](./CHANGELOG.md).
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
and what to do about it
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
whose payload is not an object or an array now reads as a server failure. An
|
|
223
|
-
app still on v8 goes through the 9.0.1 entry first, and one on an older
|
|
224
|
-
major through the entries before it.
|
|
212
|
+
[CHANGELOG.md](./CHANGELOG.md). Both sides of an answer are exact. A success
|
|
213
|
+
is only ever the handler's parsed output, and an output is an object or an
|
|
214
|
+
array, so `caller.api()` is truthy exactly on success and a success's payload
|
|
215
|
+
is typed as exactly the output; a hook, a fallback or an error handler
|
|
216
|
+
answers an API call with `res.apiRefusal()`, never with a payload. A refusal
|
|
217
|
+
names a code the app declared, which the contract carries and a caller
|
|
218
|
+
narrows on, with its data typed. Each major's first entry lists every break
|
|
219
|
+
it made and what to do about it, and the compiler finds most of them; the
|
|
220
|
+
entry names the ones it cannot, which are changes of behavior. An app on an
|
|
221
|
+
older major goes through each major's entry in turn.
|
|
225
222
|
|
|
226
223
|
## Contributing
|
|
227
224
|
|
|
@@ -139,10 +139,16 @@ export type LambderContractOfGroups<TGroups extends readonly unknown[]> = Lambde
|
|
|
139
139
|
type LambderGroupTypedAny<TGroup> = 0 extends 1 & TGroup ? true : 0 extends 1 & LambderLoadedGroupOf<TGroup> ? true : LambderLoadedGroupOf<TGroup> extends {
|
|
140
140
|
readonly apis: infer TApis;
|
|
141
141
|
} ? (0 extends 1 & TApis ? true : string extends keyof TApis ? true : false) : false;
|
|
142
|
-
/**
|
|
143
|
-
|
|
142
|
+
/**
|
|
143
|
+
* The group names one registerApiGroups() call names twice. Written as an
|
|
144
|
+
* accumulator, the recursive reference standing alone in its branch, so the
|
|
145
|
+
* compiler evaluates it as a loop: an app registers a group per feature,
|
|
146
|
+
* and recursion nested in a union runs into the compiler's instantiation
|
|
147
|
+
* depth limit at a group count a large app reaches.
|
|
148
|
+
*/
|
|
149
|
+
type LambderRepeatedGroupNames<TGroups extends readonly unknown[], TSeen = never, TRepeated = never> = TGroups extends readonly [infer THead, ...infer TRest] ? THead extends {
|
|
144
150
|
name: infer TName;
|
|
145
|
-
} ? ([TName] extends [TSeen] ? TName : never)
|
|
151
|
+
} ? LambderRepeatedGroupNames<TRest, TSeen | TName, TRepeated | ([TName] extends [TSeen] ? TName : never)> : TRepeated : TRepeated;
|
|
146
152
|
/**
|
|
147
153
|
* What registerApiGroups() asks of its receiver: the instance itself, unless
|
|
148
154
|
* a group, what a lazy group loads or a group's endpoints is typed `any` (a
|
|
@@ -163,8 +169,8 @@ export type LambderRegisteredGroupsCheck<TGroups extends readonly unknown[], TSe
|
|
|
163
169
|
export type LambderGroupNameCheck<TName extends string> = TName extends LambderReservedGroupName ? {
|
|
164
170
|
readonly "lambder: a caller already has a member of this name, so no group may take it": TName;
|
|
165
171
|
} : unknown;
|
|
166
|
-
/** The actions more than one of a group's parts declare. */
|
|
167
|
-
type LambderRepeatedActions<TParts extends readonly unknown[], TSeen = never> = TParts extends readonly [infer THead, ...infer TRest] ?
|
|
172
|
+
/** The actions more than one of a group's parts declare; an accumulator, as LambderRepeatedGroupNames is, for the same reason. */
|
|
173
|
+
type LambderRepeatedActions<TParts extends readonly unknown[], TSeen = never, TRepeated = never> = TParts extends readonly [infer THead, ...infer TRest] ? LambderRepeatedActions<TRest, TSeen | keyof THead, TRepeated | Extract<keyof THead, TSeen>> : TRepeated;
|
|
168
174
|
/** A group's parts held to declaring each action once: a part would otherwise replace another's endpoint of the same name. */
|
|
169
175
|
export type LambderGroupPartsCheck<TParts extends readonly unknown[]> = [
|
|
170
176
|
LambderRepeatedActions<TParts>
|
|
@@ -6,7 +6,8 @@ import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRate
|
|
|
6
6
|
* One endpoint's declaration as the pipeline runs it: what the server's
|
|
7
7
|
* defineApi options carry, minus the handler, in a shape the mock
|
|
8
8
|
* runtime can restate from a type-only contract. The schemas are optional
|
|
9
|
-
* because
|
|
9
|
+
* because a mock may have none (its input schema is the generated table's,
|
|
10
|
+
* an entry's own, or neither); when input is present, validation runs and the
|
|
10
11
|
* handler sees the parsed payload. Output is part of the endpoint's
|
|
11
12
|
* signature (apiSignatureOf), which is what a client's build is checked
|
|
12
13
|
* against; the server also parses every output a handler returns through it
|
|
@@ -21,8 +21,8 @@ export declare const refusalEnvelope: (apiVersion: string | null | undefined, {
|
|
|
21
21
|
* server, onInvalidInput in the mock) as its envelope, once the config is
|
|
22
22
|
* known to be a refusal: a refusal message or one of the three flags, so a
|
|
23
23
|
* reader never takes it for the handler's output, and a message with a
|
|
24
|
-
* framework code
|
|
25
|
-
* endpoint's declared refusals. `writer` names the call in the error, and
|
|
24
|
+
* framework code that carries no data, or none, since it answers outside any
|
|
25
|
+
* one endpoint's declared refusals. `writer` names the call in the error, and
|
|
26
26
|
* the logList is the config's own or the one given.
|
|
27
27
|
*/
|
|
28
28
|
export declare const plainRefusalEnvelope: (apiVersion: string | null | undefined, config: LambderApiRefusalConfig, writer: string, logList?: unknown[]) => LambderApiRefusalEnvelope;
|
|
@@ -46,8 +46,8 @@ export const refusalEnvelope = (apiVersion, { versionExpired, sessionExpired, no
|
|
|
46
46
|
* server, onInvalidInput in the mock) as its envelope, once the config is
|
|
47
47
|
* known to be a refusal: a refusal message or one of the three flags, so a
|
|
48
48
|
* reader never takes it for the handler's output, and a message with a
|
|
49
|
-
* framework code
|
|
50
|
-
* endpoint's declared refusals. `writer` names the call in the error, and
|
|
49
|
+
* framework code that carries no data, or none, since it answers outside any
|
|
50
|
+
* one endpoint's declared refusals. `writer` names the call in the error, and
|
|
51
51
|
* the logList is the config's own or the one given.
|
|
52
52
|
*/
|
|
53
53
|
export const plainRefusalEnvelope = (apiVersion, config, writer, logList) => {
|
|
@@ -60,6 +60,12 @@ export const plainRefusalEnvelope = (apiVersion, config, writer, logList) => {
|
|
|
60
60
|
if (message.code !== undefined && !isLambderRefusalCode(message.code)) {
|
|
61
61
|
throw new Error(`Lambder: ${writer} was given the code "${message.code}". It answers outside any one API's declared refusals, so its message carries a framework code or none.`);
|
|
62
62
|
}
|
|
63
|
+
// The one framework code that carries data names the policy that
|
|
64
|
+
// refused, which only the rate-limit engine knows, so a rate limit
|
|
65
|
+
// is answered by charging the policy (ctx.rateLimit), never by hand.
|
|
66
|
+
if (message.code === LAMBDER_REFUSAL_CODES.rateLimited) {
|
|
67
|
+
throw new Error(`Lambder: ${writer} was given the code "${message.code}". A rate limit refuses with the policy that refused and the seconds to wait as its data; charge the policy with ctx.rateLimit, which refuses the call itself.`);
|
|
68
|
+
}
|
|
63
69
|
if (message.data !== undefined) {
|
|
64
70
|
throw new Error(`Lambder: ${writer} was given a message with data. Only a code an API declares carries data, and this answer is outside any one API's declaration.`);
|
|
65
71
|
}
|