lambder 7.3.1 → 8.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 +933 -3
- package/README.md +41 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +68 -62
- package/dist/api/LambderApiIdempotency.js +214 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +47 -38
- package/dist/api/LambderApiPipeline.js +122 -63
- package/dist/api/LambderApiRateLimits.d.ts +201 -54
- package/dist/api/LambderApiRateLimits.js +185 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +140 -75
- package/dist/core/Lambder.js +347 -227
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +21 -7
- package/dist/core/LambderFiles.js +62 -34
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +15 -8
- package/dist/index.js +5 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
- package/dist/invoke/LambderLambdaEvent.js +40 -22
- package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
- package/dist/invoke/lambderHandlerTransport.js +15 -18
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +1 -1
- package/dist/mock.js +2 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +124 -46
- package/dist/session/LambderSessionManager.js +262 -137
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +79 -33
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +21 -23
- package/dist/testing/LambderTestApp.js +22 -24
- package/dist/testing/LambderTestVisitor.d.ts +10 -12
- package/dist/testing/LambderTestVisitor.js +15 -15
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +12 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
- package/dist/api/LambderApiPolicyEngine.js +0 -85
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ and typed results with no hand-written client:
|
|
|
34
34
|
import { LambderCaller } from "lambder/client";
|
|
35
35
|
import type { ApiContractType } from "./backend/handler";
|
|
36
36
|
|
|
37
|
-
const caller = new LambderCaller<ApiContractType>({ apiPath: "/api"
|
|
37
|
+
const caller = new LambderCaller<ApiContractType>({ apiPath: "/api" });
|
|
38
38
|
const company = await caller.api("getCompany", { slug: "acme" });
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -85,7 +85,7 @@ whatever the code you actually import needs:
|
|
|
85
85
|
| What you import | What to install alongside |
|
|
86
86
|
| --- | --- |
|
|
87
87
|
| `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 |
|
|
88
|
-
| `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 |
|
|
88
|
+
| `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) |
|
|
89
89
|
| `lambder` anywhere else (a long-running server, a container, local tests) | `zod`, plus `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb` when sessions or the DynamoDB stores are used; both are loaded on the first table access, so an app that uses neither needs neither |
|
|
90
90
|
| `LambderS3FileSource` | `@aws-sdk/client-s3`, loaded on first read |
|
|
91
91
|
| `LambderInvokeCaller` | `@aws-sdk/client-lambda`, loaded on the first call |
|
|
@@ -95,11 +95,16 @@ The SDK and its `@smithy` tree are roughly 21MB installed, which is why they are
|
|
|
95
95
|
peers rather than dependencies: a frontend importing only `lambder/client` has
|
|
96
96
|
no use for any of it, and a Lambda deployment package should not ship a second
|
|
97
97
|
copy of what the runtime already loads. The runtime pins its own SDK version,
|
|
98
|
-
so if you need a specific one, install it and bundle it yourself.
|
|
98
|
+
so if you need a specific one, install it and bundle it yourself. One version
|
|
99
|
+
matters to Lambder: `LambderDdbRateLimiter` needs `@aws-sdk/client-dynamodb`
|
|
100
|
+
3.868.0 or later, the first whose throttling errors name their reasons. Under
|
|
101
|
+
an older client it never recognizes a key-range throttle, so a flood on one
|
|
102
|
+
key goes to `failOpen` instead of being refused. Check the version your
|
|
103
|
+
runtime bundles before relying on it, or bundle the client yourself.
|
|
99
104
|
|
|
100
105
|
## Package entry points
|
|
101
106
|
|
|
102
|
-
The package ships
|
|
107
|
+
The package ships five entry points; pick by where the code runs:
|
|
103
108
|
|
|
104
109
|
| Entry | Runs in | Carries |
|
|
105
110
|
| --- | --- | --- |
|
|
@@ -107,6 +112,7 @@ The package ships four entry points; pick by where the code runs:
|
|
|
107
112
|
| `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `html`/`xml` tagged templates, `createLambderI18n` |
|
|
108
113
|
| `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 |
|
|
109
114
|
| `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 |
|
|
115
|
+
| `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance |
|
|
110
116
|
|
|
111
117
|
Frontends and shared isomorphic packages should import from `lambder/client`
|
|
112
118
|
only; the entry's module graph contains no AWS SDK, Node built-ins, or server
|
|
@@ -117,12 +123,15 @@ Source layout mirrors this: `src/api/` (the isomorphic API core: request,
|
|
|
117
123
|
answer, envelope, pipeline, and the declarative policies the pipeline runs),
|
|
118
124
|
`src/core/` (the Lambda server adapter: routes, files, hooks, finalization),
|
|
119
125
|
`src/session/` (the session manager, controller and crypto), `src/stores/`
|
|
120
|
-
(every store implementation, DynamoDB and in-memory alike
|
|
126
|
+
(every store and file-source implementation, DynamoDB and in-memory alike,
|
|
127
|
+
and the helpers the two caches share), `src/client/`,
|
|
121
128
|
`src/invoke/` (the lambda-to-lambda caller and the in-process handler
|
|
122
|
-
transport), `src/mock/` (the mock runtime),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
transport), `src/mock/` (the mock runtime), `src/testing/` (the test app),
|
|
130
|
+
`src/build/` (what a generator script runs at build time), and `src/shared/`
|
|
131
|
+
(isomorphic modules every entry re-exports, grouped into `wire/` for the
|
|
132
|
+
format both sides speak, `contracts/` for the five store and source
|
|
133
|
+
interfaces, `transport/` for the caller-to-server seam, and `util/` for
|
|
134
|
+
helpers).
|
|
126
135
|
Directories are layers and imports only ever point down;
|
|
127
136
|
[docs/api-core.md](./docs/api-core.md#layering) states the order and the test
|
|
128
137
|
that enforces it.
|
|
@@ -137,8 +146,8 @@ guide that matches what you are building. The full index lives in
|
|
|
137
146
|
| --- | --- |
|
|
138
147
|
| [Getting started](./docs/getting-started.md) | The three-step path from a first API to a typed frontend call |
|
|
139
148
|
| [Configuration](./docs/configuration.md) | Every `initLambder().create({...})` option, in one reference |
|
|
140
|
-
| [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, and non-HTTP invocations |
|
|
141
|
-
| [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiRefusal
|
|
149
|
+
| [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, crash reporting, and non-HTTP invocations |
|
|
150
|
+
| [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiRefusal`, the signature file |
|
|
142
151
|
| [Responses](./docs/responses.md) | The render context, resolver methods, cookies, compression, ETag and the size cap |
|
|
143
152
|
| [Sessions](./docs/sessions.md) | Sessions over a store, cookie scope, secrets at rest, `dataRefresh`, the controller API |
|
|
144
153
|
| [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
|
|
@@ -151,7 +160,7 @@ guide that matches what you are building. The full index lives in
|
|
|
151
160
|
| [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, on-demand languages, runtime dictionaries |
|
|
152
161
|
| [The mock runtime](./docs/mock.md) | `LambderMockApp`: the typed contract served from mock handlers over the real pipeline, in the browser and in tests |
|
|
153
162
|
| [DynamoDB tables](./docs/dynamodb-tables.md) | Table shapes, TTL and IAM for sessions, cache, rate limits and idempotency |
|
|
154
|
-
| [Exports reference](./docs/exports.md) | Every name the
|
|
163
|
+
| [Exports reference](./docs/exports.md) | Every name the five entry points export, grouped by purpose |
|
|
155
164
|
|
|
156
165
|
## Standalone modules
|
|
157
166
|
|
|
@@ -162,7 +171,7 @@ framework:
|
|
|
162
171
|
| --- | --- | --- |
|
|
163
172
|
| `html` / `xml` tags + `LambderTemplatingEngine` | [Templating](./docs/templating.md) | Type-safe tagged templates and a comment-only HTML template engine (build-pipeline-safe) |
|
|
164
173
|
| `createLambderI18n` | [Translations](./docs/i18n.md) | Typed translations with enforced/optional languages, component-level extension, auto language detection and on-demand language loading (isomorphic) |
|
|
165
|
-
| `LambderDdbCache` | [DynamoDB cache](./docs/ddb-cache.md) | DynamoDB-backed compressed
|
|
174
|
+
| `LambderDdbCache` / `LambderMemoryCache` | [DynamoDB cache](./docs/ddb-cache.md) | JSON cache behind one `LambderCache` interface: DynamoDB-backed and compressed, with lease-based single-fill and grouped keys (server-only), or in memory for tests |
|
|
166
175
|
| `LambderDdbRateLimiter` / `LambderMemoryRateLimiter` | [Rate limiter](./docs/ddb-rate-limiter.md) | Fixed-window rate limiter, atomic per window, in DynamoDB (server-only) or in memory |
|
|
167
176
|
| `LambderDdbIdempotencyStore` / `LambderMemoryIdempotencyStore` | [Idempotency store](./docs/ddb-idempotency.md) | Idempotency records with owner-checked claims, in DynamoDB (compressed replays, server-only) or in memory |
|
|
168
177
|
| `LambderMockApp` | [The mock runtime](./docs/mock.md) | The typed contract served from mock handlers over the real API pipeline, with failure injection, sessions and a call log (isomorphic) |
|
|
@@ -170,14 +179,25 @@ framework:
|
|
|
170
179
|
## Versioning and changes
|
|
171
180
|
|
|
172
181
|
Released versions and what each one changed are in
|
|
173
|
-
[CHANGELOG.md](./CHANGELOG.md). The current major is
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
[CHANGELOG.md](./CHANGELOG.md). The current major is v8, which came out of a
|
|
183
|
+
review of 7.3.1: session writes that cannot undo a logout, API calls that must
|
|
184
|
+
be JSON, output schemas applied at runtime, rate limits that count IPv6 callers
|
|
185
|
+
by their /64 and custom keys after the guards, idempotency keys bound to the
|
|
186
|
+
request they were first sent with, and the same behavior on every gateway and
|
|
187
|
+
in the mock. Every break and what to do about it is in the 8.0.1 entry. The
|
|
188
|
+
compiler finds most of them. Fourteen it cannot are named there: hand-built
|
|
189
|
+
calls without a JSON Content-Type, hand-built answers without `apiVersion`,
|
|
190
|
+
handlers whose payload does not match their output schema, code that decoded
|
|
191
|
+
`ctx.path` itself, string routes that match case-sensitively, compression
|
|
192
|
+
behind a REST API, two more IAM actions (`UpdateItem` on the session table,
|
|
193
|
+
`GetItem` on the rate-limit table), custom-key limits charged after the
|
|
194
|
+
guards, idempotency keys bound to the request they were first sent with,
|
|
195
|
+
`errorMessage` always being an object, `credentials: true` needing named
|
|
196
|
+
origins, template slots and `html` interpolations refused or checked in
|
|
197
|
+
more attribute positions, `refreshSessionData()` throwing where it answered
|
|
198
|
+
null, and `z.ZodType<T>` annotations leaving a field unchecked. Every live
|
|
199
|
+
session is signed out once by the
|
|
200
|
+
upgrade. An app still on v6 goes through the 7.0.0 entry first.
|
|
181
201
|
|
|
182
202
|
## Contributing
|
|
183
203
|
|
|
@@ -1,40 +1,36 @@
|
|
|
1
1
|
import type { LambderApiHttpAnswer } from "../shared/wire/LambderApiOutcome.js";
|
|
2
2
|
/**
|
|
3
|
-
* The core's output for one API call
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* The core's output for one API call, before any transport-level
|
|
4
|
+
* finalization. The server adapter turns it into a Lambda response
|
|
5
|
+
* (compression, ETag, base64); the mock transport hands it to the caller as
|
|
6
|
+
* it is. The idempotency store persists and replays this shape, which is why
|
|
7
|
+
* it is plain data.
|
|
8
8
|
*
|
|
9
|
-
* Header names keep
|
|
10
|
-
*
|
|
11
|
-
* them.
|
|
9
|
+
* Header names keep their written casing; lookups are case-insensitive (see
|
|
10
|
+
* getAnswerHeader), as in LambderResponse.
|
|
12
11
|
*/
|
|
13
12
|
export type LambderApiAnswer = {
|
|
14
13
|
statusCode: number;
|
|
15
14
|
headers: Record<string, string[]>;
|
|
16
15
|
body: string;
|
|
17
16
|
/**
|
|
18
|
-
* Finalization hints for the server adapter,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* nothing, so it hands the body to the caller as it stands; the stores
|
|
24
|
-
* drop the hints.
|
|
17
|
+
* Finalization hints for the server adapter, so an answer that started as
|
|
18
|
+
* a LambderResponse loses nothing on the way: `isBodyBase64` marks a body
|
|
19
|
+
* that is base64 of binary bytes (never cached by the idempotency engine,
|
|
20
|
+
* never compressed); `compress` and `etag` are the LambderResponse flags.
|
|
21
|
+
* The mock runtime finalizes nothing and the stores drop the hints.
|
|
25
22
|
*/
|
|
26
23
|
isBodyBase64?: boolean;
|
|
27
24
|
compress?: boolean | "auto";
|
|
28
25
|
etag?: boolean | "auto";
|
|
29
26
|
};
|
|
30
27
|
/**
|
|
31
|
-
* An answer in the accessor form resolveApiOutcome() reads
|
|
32
|
-
* fetch Response or a decoded Lambda result
|
|
33
|
-
*
|
|
28
|
+
* An answer in the accessor form resolveApiOutcome() reads (the same view a
|
|
29
|
+
* fetch Response or a decoded Lambda result gets); the mock transport hands
|
|
30
|
+
* this to the caller.
|
|
34
31
|
*
|
|
35
|
-
* The body
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* nothing to do with what it decoded anyway.
|
|
32
|
+
* The body passes through as it stands, base64 hint or not: only the mock
|
|
33
|
+
* runtime's own answers reach here, and those are JSON envelopes. A binary
|
|
34
|
+
* body read through the JSON accessors would be of no use decoded anyway.
|
|
39
35
|
*/
|
|
40
36
|
export declare const toHttpAnswer: (answer: LambderApiAnswer) => LambderApiHttpAnswer;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
2
2
|
/**
|
|
3
|
-
* An answer in the accessor form resolveApiOutcome() reads
|
|
4
|
-
* fetch Response or a decoded Lambda result
|
|
5
|
-
*
|
|
3
|
+
* An answer in the accessor form resolveApiOutcome() reads (the same view a
|
|
4
|
+
* fetch Response or a decoded Lambda result gets); the mock transport hands
|
|
5
|
+
* this to the caller.
|
|
6
6
|
*
|
|
7
|
-
* The body
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* nothing to do with what it decoded anyway.
|
|
7
|
+
* The body passes through as it stands, base64 hint or not: only the mock
|
|
8
|
+
* runtime's own answers reach here, and those are JSON envelopes. A binary
|
|
9
|
+
* body read through the JSON accessors would be of no use decoded anyway.
|
|
11
10
|
*/
|
|
12
11
|
export const toHttpAnswer = (answer) => ({
|
|
13
12
|
status: answer.statusCode,
|
|
@@ -3,9 +3,8 @@ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* The context the API core needs from whoever runs it. The server's render
|
|
5
5
|
* context and the mock runtime's handler context both extend it; the
|
|
6
|
-
* pipeline,
|
|
7
|
-
*
|
|
8
|
-
* under.
|
|
6
|
+
* pipeline, policy engines and session controller touch nothing else, so
|
|
7
|
+
* they never learn which adapter they run under.
|
|
9
8
|
*
|
|
10
9
|
* - `session` is set by the pipeline on session APIs (and by the session
|
|
11
10
|
* controller when a handler creates or ends one).
|
|
@@ -24,11 +23,25 @@ export type LambderApiCallContext<TSessionData = any> = {
|
|
|
24
23
|
/** A fresh call context: no session, no guard data, nothing pending. */
|
|
25
24
|
export declare const createApiCallContext: <TSessionData = any>() => LambderApiCallContext<TSessionData>;
|
|
26
25
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Binds an adapter's tools onto one call context: `getters` run when read
|
|
27
|
+
* (`ctx.sessionController` is built over the object it was read from),
|
|
28
|
+
* `methods` are plain functions, and each is non-enumerable and bound to that
|
|
29
|
+
* object.
|
|
30
|
+
*
|
|
31
|
+
* Non-enumerable keeps a tool on the right object: a copy of the context (a
|
|
32
|
+
* server hook's `{ ...ctx, extra }`) carries none of them, rather than tools
|
|
33
|
+
* still bound to the original and its session. The adapter binds them again
|
|
34
|
+
* on a copy it continues with; configurable lets that replace them.
|
|
35
|
+
*/
|
|
36
|
+
export declare const bindCallTools: (ctx: object, tools: {
|
|
37
|
+
getters?: Record<string, () => unknown>;
|
|
38
|
+
methods?: Record<string, (...args: never[]) => unknown>;
|
|
39
|
+
}) => void;
|
|
40
|
+
/**
|
|
41
|
+
* What one call recorded about itself while it ran, in order. Written as the
|
|
42
|
+
* call goes rather than assembled from each step's return, so a call refused
|
|
43
|
+
* partway still reports the guards that had already run (the mock's call log
|
|
44
|
+
* shows them for exactly the calls a developer is debugging).
|
|
32
45
|
*/
|
|
33
46
|
export type LambderApiCallTrace = {
|
|
34
47
|
/** The guards that ran, in order, including on a call that a later one refused. */
|
|
@@ -2,12 +2,30 @@ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
|
2
2
|
/** A fresh call context: no session, no guard data, nothing pending. */
|
|
3
3
|
export const createApiCallContext = () => ({
|
|
4
4
|
session: null,
|
|
5
|
-
// No prototype
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// for a handler that only wanted to know whether the guard returned
|
|
5
|
+
// No prototype: guard names are the app's to choose, and on a plain
|
|
6
|
+
// object a guard named "toString" or "constructor" would read back as an
|
|
7
|
+
// inherited function to a handler checking whether that guard returned
|
|
9
8
|
// anything.
|
|
10
9
|
guardData: Object.create(null),
|
|
11
10
|
responseHeaders: new LambderAnswerHeaders(),
|
|
12
11
|
logList: [],
|
|
13
12
|
});
|
|
13
|
+
/**
|
|
14
|
+
* Binds an adapter's tools onto one call context: `getters` run when read
|
|
15
|
+
* (`ctx.sessionController` is built over the object it was read from),
|
|
16
|
+
* `methods` are plain functions, and each is non-enumerable and bound to that
|
|
17
|
+
* object.
|
|
18
|
+
*
|
|
19
|
+
* Non-enumerable keeps a tool on the right object: a copy of the context (a
|
|
20
|
+
* server hook's `{ ...ctx, extra }`) carries none of them, rather than tools
|
|
21
|
+
* still bound to the original and its session. The adapter binds them again
|
|
22
|
+
* on a copy it continues with; configurable lets that replace them.
|
|
23
|
+
*/
|
|
24
|
+
export const bindCallTools = (ctx, tools) => {
|
|
25
|
+
for (const [name, get] of Object.entries(tools.getters ?? {})) {
|
|
26
|
+
Object.defineProperty(ctx, name, { get, enumerable: false, configurable: true });
|
|
27
|
+
}
|
|
28
|
+
for (const [name, value] of Object.entries(tools.methods ?? {})) {
|
|
29
|
+
Object.defineProperty(ctx, name, { value, enumerable: false, configurable: true, writable: true });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -6,9 +6,10 @@ import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRate
|
|
|
6
6
|
* addApi/addSessionApi options carry, minus the handler, in a shape the mock
|
|
7
7
|
* runtime can restate from a type-only contract. The schemas are optional
|
|
8
8
|
* because the mock has none; when input is present, validation runs and the
|
|
9
|
-
* handler sees the parsed payload. Output is
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* handler sees the parsed payload. Output is part of the endpoint's
|
|
10
|
+
* signature (apiSignatureOf), which is what a client's build is checked
|
|
11
|
+
* against; the server's resolver also parses every payload a handler
|
|
12
|
+
* answers through it before it is sent.
|
|
12
13
|
*/
|
|
13
14
|
export type LambderApiDefinition = {
|
|
14
15
|
name: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
import type { LambderApiEnvelopeBody, LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
|
|
3
3
|
import { type LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
4
|
+
import type { LambderCrashDetail } from "../shared/wire/LambderCrashDetail.js";
|
|
4
5
|
import type { LambderApiAnswer } from "./LambderApiAnswer.js";
|
|
5
6
|
export declare const API_ANSWER_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
6
7
|
/** The envelope's config plus the logList channel the call accumulated. */
|
|
@@ -40,14 +41,13 @@ export type LambderValidationAnswerBody = {
|
|
|
40
41
|
};
|
|
41
42
|
/**
|
|
42
43
|
* The standard answer for a rejected input: a 422 whose body spells the
|
|
43
|
-
* ZodError out.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* validation handler would receive a ZodError with nothing to branch on.
|
|
44
|
+
* ZodError out. Not serialized as-is: zod 4 keeps `issues` non-enumerable,
|
|
45
|
+
* so JSON.stringify(zodError) would carry the issues only inside the message
|
|
46
|
+
* string, leaving a client's validation handler nothing to branch on.
|
|
47
47
|
*
|
|
48
|
-
* zod's own `message` never ships: it is the whole issue tree re-serialized
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* zod's own `message` never ships: it is the whole issue tree re-serialized
|
|
49
|
+
* and would send every capped byte a second time. A generated summary takes
|
|
50
|
+
* its place on every answer.
|
|
51
51
|
*/
|
|
52
52
|
export declare const validationAnswer: (zodError: z.ZodError, logList?: unknown[]) => LambderApiAnswer;
|
|
53
53
|
/** No API is registered under the requested name: a refusal, not a 404, so a typed caller reads it. */
|
|
@@ -62,6 +62,11 @@ export declare const invalidPayloadAnswer: (apiVersion: string | null | undefine
|
|
|
62
62
|
* The last-resort answer when the call crashed and nothing else could
|
|
63
63
|
* answer: a 500 that is still an envelope, so a caller reads a structured
|
|
64
64
|
* failure rather than a text page. The server sends it only when its global
|
|
65
|
-
* error handler is absent or itself failed.
|
|
65
|
+
* error handler is absent or itself failed. `revealed` (the crash in full,
|
|
66
|
+
* with the call's logList) is passed only for a caller the app's
|
|
67
|
+
* `crashes.reveal` trusts.
|
|
66
68
|
*/
|
|
67
|
-
export declare const crashAnswer: (apiVersion: string | null | undefined
|
|
69
|
+
export declare const crashAnswer: (apiVersion: string | null | undefined, revealed?: {
|
|
70
|
+
crash: LambderCrashDetail;
|
|
71
|
+
logList: unknown[];
|
|
72
|
+
}) => LambderApiAnswer;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { LAMBDER_REFUSAL_CODES } from "../shared/wire/LambderApiRefusal.js";
|
|
1
|
+
import { LAMBDER_REFUSAL_CODES, refusalMessageOf } from "../shared/wire/LambderApiRefusal.js";
|
|
2
2
|
import { setAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
3
3
|
/*
|
|
4
4
|
* The one place the API envelope is written, and the one mapping from each
|
|
5
5
|
* kind of protocol outcome onto an answer: a success, a thrown refusal, a
|
|
6
6
|
* rejected input, an unknown name, a missing session, a stale client, a
|
|
7
7
|
* malformed compressed payload, and the last-resort crash. The server's
|
|
8
|
-
* res.api()
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* wire format. Pure: no Node built-ins, no response classes.
|
|
8
|
+
* res.api(), the mock runtime and the pipeline all build through these
|
|
9
|
+
* functions, so server and mock cannot drift on a single byte of the wire
|
|
10
|
+
* format. Pure: no Node built-ins, no response classes.
|
|
12
11
|
*/
|
|
13
12
|
export const API_ANSWER_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
14
13
|
/**
|
|
@@ -22,12 +21,13 @@ export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionE
|
|
|
22
21
|
...(versionExpired ? { versionExpired } : {}),
|
|
23
22
|
...(sessionExpired ? { sessionExpired } : {}),
|
|
24
23
|
...(notAuthorized ? { notAuthorized } : {}),
|
|
25
|
-
// Presence, not truthiness:
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
24
|
+
// Presence, not truthiness: these channels carry app values, and an app
|
|
25
|
+
// that refuses with errorMessage: "" (or 0) meant to say something. The
|
|
26
|
+
// flags above are booleans, where false and absent mean the same. An
|
|
27
|
+
// errorMessage goes out as a message object whatever form it was written
|
|
28
|
+
// in, so every reader meets one shape.
|
|
29
29
|
...(message !== undefined ? { message } : {}),
|
|
30
|
-
...(errorMessage !== undefined ? { errorMessage } : {}),
|
|
30
|
+
...(errorMessage !== undefined ? { errorMessage: refusalMessageOf(errorMessage) } : {}),
|
|
31
31
|
...(crash !== undefined ? { crash } : {}),
|
|
32
32
|
...(logList?.length ? { logList } : {}),
|
|
33
33
|
});
|
|
@@ -63,11 +63,11 @@ export const refusalAnswer = (err, apiVersion, logList) => envelopeAnswer(buildA
|
|
|
63
63
|
*/
|
|
64
64
|
const MAX_VALIDATION_ISSUES = 50;
|
|
65
65
|
/**
|
|
66
|
-
* What the whole issue list may cost, serialized.
|
|
66
|
+
* What the whole issue list may cost, serialized. A count cap alone bounds
|
|
67
67
|
* the wrong thing: ONE `unrecognized_keys` issue carries every key the client
|
|
68
|
-
* posted, so a strictObject
|
|
69
|
-
* and before any guard ran, and past maxResponseBytes the 422
|
|
70
|
-
*
|
|
68
|
+
* posted, so a strictObject could answer a 1MB body with a 4MB one,
|
|
69
|
+
* unauthenticated and before any guard ran, and past maxResponseBytes the 422
|
|
70
|
+
* would become a 500. The amplification is measured in bytes, so bytes are
|
|
71
71
|
* capped.
|
|
72
72
|
*/
|
|
73
73
|
const MAX_VALIDATION_ISSUES_BYTES = 32_000;
|
|
@@ -78,10 +78,9 @@ const MAX_VALIDATION_TEXT_CHARS = 200;
|
|
|
78
78
|
const utf8Encoder = new TextEncoder();
|
|
79
79
|
const clampText = (value) => value.length > MAX_VALIDATION_TEXT_CHARS ? `${value.slice(0, MAX_VALIDATION_TEXT_CHARS)}...` : value;
|
|
80
80
|
/**
|
|
81
|
-
* One issue with its own strings and lists bounded. Applied
|
|
82
|
-
* rather than to
|
|
83
|
-
*
|
|
84
|
-
* list or a message the client chose the size of.
|
|
81
|
+
* One issue with its own strings and lists bounded. Applied to every field
|
|
82
|
+
* rather than to known ones like `keys`, because any issue a schema authors
|
|
83
|
+
* itself may carry a list or a message whose size the client chose.
|
|
85
84
|
*/
|
|
86
85
|
const clampIssueValue = (value) => {
|
|
87
86
|
if (typeof value === "string")
|
|
@@ -91,10 +90,9 @@ const clampIssueValue = (value) => {
|
|
|
91
90
|
return value;
|
|
92
91
|
};
|
|
93
92
|
const clampIssue = (issue) =>
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// that in the type system.
|
|
93
|
+
// The mapping carries every field through in kind, the union's
|
|
94
|
+
// discriminant included; the cast states that, since a mapped object
|
|
95
|
+
// cannot express it in the type system.
|
|
98
96
|
Object.fromEntries(Object.entries(issue).map(([key, value]) => [key, clampIssueValue(value)]));
|
|
99
97
|
/** The issue list the body may carry: clamped, then cut to the byte budget, whole issues from the end. */
|
|
100
98
|
const boundIssueList = (all) => {
|
|
@@ -108,9 +106,9 @@ const boundIssueList = (all) => {
|
|
|
108
106
|
const size = utf8Encoder.encode(JSON.stringify(clamped)).length;
|
|
109
107
|
if (bytes + size > MAX_VALIDATION_ISSUES_BYTES) {
|
|
110
108
|
trimmed = true;
|
|
111
|
-
// A first issue
|
|
112
|
-
//
|
|
113
|
-
//
|
|
109
|
+
// A first issue over the budget on its own still ships the three
|
|
110
|
+
// fields every issue carries, so a client always has a code and a
|
|
111
|
+
// path to branch on.
|
|
114
112
|
if (issues.length === 0) {
|
|
115
113
|
issues.push({ code: clamped.code, path: clamped.path.slice(0, MAX_VALIDATION_LIST_ENTRIES), message: clampText(clamped.message) });
|
|
116
114
|
}
|
|
@@ -132,14 +130,13 @@ const summarizeIssues = (total, listed, trimmed) => {
|
|
|
132
130
|
};
|
|
133
131
|
/**
|
|
134
132
|
* The standard answer for a rejected input: a 422 whose body spells the
|
|
135
|
-
* ZodError out.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* validation handler would receive a ZodError with nothing to branch on.
|
|
133
|
+
* ZodError out. Not serialized as-is: zod 4 keeps `issues` non-enumerable,
|
|
134
|
+
* so JSON.stringify(zodError) would carry the issues only inside the message
|
|
135
|
+
* string, leaving a client's validation handler nothing to branch on.
|
|
139
136
|
*
|
|
140
|
-
* zod's own `message` never ships: it is the whole issue tree re-serialized
|
|
141
|
-
*
|
|
142
|
-
*
|
|
137
|
+
* zod's own `message` never ships: it is the whole issue tree re-serialized
|
|
138
|
+
* and would send every capped byte a second time. A generated summary takes
|
|
139
|
+
* its place on every answer.
|
|
143
140
|
*/
|
|
144
141
|
export const validationAnswer = (zodError, logList) => {
|
|
145
142
|
const { issues, trimmed } = boundIssueList(zodError.issues);
|
|
@@ -175,6 +172,8 @@ export const invalidPayloadAnswer = (apiVersion, message) => envelopeAnswer(buil
|
|
|
175
172
|
* The last-resort answer when the call crashed and nothing else could
|
|
176
173
|
* answer: a 500 that is still an envelope, so a caller reads a structured
|
|
177
174
|
* failure rather than a text page. The server sends it only when its global
|
|
178
|
-
* error handler is absent or itself failed.
|
|
175
|
+
* error handler is absent or itself failed. `revealed` (the crash in full,
|
|
176
|
+
* with the call's logList) is passed only for a caller the app's
|
|
177
|
+
* `crashes.reveal` trusts.
|
|
179
178
|
*/
|
|
180
|
-
export const crashAnswer = (apiVersion) => envelopeAnswer(buildApiEnvelope(apiVersion, null, { errorMessage: "Internal server error." }), { statusCode: 500 });
|
|
179
|
+
export const crashAnswer = (apiVersion, revealed) => envelopeAnswer(buildApiEnvelope(apiVersion, null, { errorMessage: "Internal server error.", ...revealed }), { statusCode: 500 });
|