lambder 8.1.2 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +208 -0
- package/README.md +23 -31
- package/dist/api/LambderApiCallContext.d.ts +31 -1
- package/dist/api/LambderApiCallContext.js +8 -0
- package/dist/api/LambderApiDefinition.d.ts +2 -2
- package/dist/api/LambderApiEnvelope.d.ts +1 -1
- package/dist/api/LambderApiEnvelope.js +3 -4
- package/dist/api/LambderApiGuards.d.ts +2 -17
- package/dist/api/LambderApiIdempotency.js +5 -7
- package/dist/api/LambderApiRateLimits.d.ts +2 -29
- package/dist/build/generatedTables.d.ts +72 -0
- package/dist/build/generatedTables.js +99 -0
- package/dist/build/writeApiGuardParams.d.ts +60 -0
- package/dist/build/writeApiGuardParams.js +85 -0
- package/dist/build/writeApiOptions.d.ts +68 -0
- package/dist/build/writeApiOptions.js +102 -0
- package/dist/build.d.ts +10 -4
- package/dist/build.js +7 -4
- package/dist/client/LambderCaller.d.ts +0 -4
- package/dist/client/LambderCaller.js +1 -9
- package/dist/client/LambderUploadRunner.d.ts +7 -7
- package/dist/client/LambderUploadRunner.js +12 -21
- package/dist/client.d.ts +7 -0
- package/dist/client.js +11 -0
- package/dist/core/Lambder.d.ts +71 -12
- package/dist/core/Lambder.js +116 -38
- package/dist/core/LambderContext.d.ts +9 -6
- package/dist/core/LambderContext.js +2 -1
- package/dist/core/LambderResolver.d.ts +6 -12
- package/dist/core/LambderResolver.js +2 -14
- package/dist/core/LambderResponseBuilder.d.ts +15 -70
- package/dist/core/LambderResponseBuilder.js +15 -99
- package/dist/index.d.ts +16 -3
- package/dist/index.js +14 -1
- package/dist/invoke/LambderInvokeCaller.js +3 -4
- package/dist/mock/LambderMockApp.d.ts +34 -17
- package/dist/mock/LambderMockApp.js +69 -24
- package/dist/mock/LambderMockCreateOptions.d.ts +70 -7
- package/dist/mock/LambderMockTypes.d.ts +31 -21
- package/dist/mock/lambderMockPoliciesFrom.d.ts +51 -0
- package/dist/mock/lambderMockPoliciesFrom.js +46 -0
- package/dist/mock.d.ts +3 -0
- package/dist/mock.js +3 -0
- package/dist/secrets/LambderOneShotSecrets.d.ts +166 -0
- package/dist/secrets/LambderOneShotSecrets.js +217 -0
- package/dist/session/LambderSessionCrypto.js +6 -16
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +3 -2
- package/dist/shared/contracts/LambderOneShotSecretStore.d.ts +122 -0
- package/dist/shared/contracts/LambderOneShotSecretStore.js +38 -0
- package/dist/shared/util/LambderBackoffTimer.d.ts +82 -0
- package/dist/shared/util/LambderBackoffTimer.js +86 -0
- package/dist/shared/util/LambderBase64.d.ts +14 -0
- package/dist/shared/util/LambderBase64.js +17 -0
- package/dist/shared/util/LambderSignedClaims.d.ts +78 -0
- package/dist/shared/util/LambderSignedClaims.js +109 -0
- package/dist/shared/util/LambderTextDigest.d.ts +19 -5
- package/dist/shared/util/LambderTextDigest.js +30 -5
- package/dist/shared/util/LambderTypeUtilities.d.ts +18 -0
- package/dist/shared/util/assertPlainData.d.ts +9 -0
- package/dist/shared/util/assertPlainData.js +41 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +3 -2
- package/dist/shared/wire/LambderAnswerHeaders.js +3 -2
- package/dist/shared/wire/LambderApiContract.d.ts +9 -14
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +148 -0
- package/dist/shared/wire/LambderApiOptionEntries.js +35 -0
- package/dist/shared/wire/LambderApiRefusal.d.ts +3 -4
- package/dist/shared/wire/LambderApiRefusal.js +3 -4
- package/dist/stores/LambderDdbOneShotSecretStore.d.ts +64 -0
- package/dist/stores/LambderDdbOneShotSecretStore.js +266 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +3 -2
- package/dist/stores/LambderMemoryIdempotencyStore.js +3 -2
- package/dist/stores/LambderMemoryOneShotSecretStore.d.ts +36 -0
- package/dist/stores/LambderMemoryOneShotSecretStore.js +93 -0
- package/dist/testing/LambderConformanceRunner.d.ts +46 -0
- package/dist/testing/LambderConformanceRunner.js +21 -0
- package/dist/testing/lambderIdempotencyStoreConformance.d.ts +33 -0
- package/dist/testing/lambderIdempotencyStoreConformance.js +237 -0
- package/dist/testing/lambderOneShotSecretStoreConformance.d.ts +43 -0
- package/dist/testing/lambderOneShotSecretStoreConformance.js +224 -0
- package/dist/testing/lambderRateLimiterConformance.d.ts +20 -0
- package/dist/testing/lambderRateLimiterConformance.js +72 -0
- package/dist/testing/lambderSessionStoreConformance.d.ts +27 -0
- package/dist/testing/lambderSessionStoreConformance.js +165 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,214 @@ 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
|
+
## [9.0.1] - 2026-09-27
|
|
13
|
+
|
|
14
|
+
A major that gives an API handler one shape. It takes its context and returns
|
|
15
|
+
its output; it says no with `refuse()`; it writes headers, cookies and log
|
|
16
|
+
entries through the context. The response builder stays with routes, hooks,
|
|
17
|
+
fallbacks and error handlers, which build HTTP responses. An API is typed data
|
|
18
|
+
in and typed data out, and a handler that has no response builder cannot
|
|
19
|
+
answer any other way, so the two ways a handler used to refuse (throwing, or
|
|
20
|
+
answering null beside a reason) are one, and the untyped side channels are
|
|
21
|
+
gone. A server handler and its mock twin now read alike.
|
|
22
|
+
|
|
23
|
+
### Changed (breaking)
|
|
24
|
+
|
|
25
|
+
- **An API handler returns its output.** `addApi` and `addSessionApi` take
|
|
26
|
+
`async (ctx) => output` instead of `async (ctx, res) => res.api(output)`.
|
|
27
|
+
The return is checked against the output schema's input form and parsed
|
|
28
|
+
through the schema before it is sent, as `res.api()`'s payload was:
|
|
29
|
+
undeclared fields are stripped, defaults filled, transforms run once, and
|
|
30
|
+
an output the schema rejects is answered as a crash
|
|
31
|
+
(`LambderApiOutputValidationError`). A literal in a returned object keeps
|
|
32
|
+
its type (`{ status: "open" }` is checked as `"open"`, not `string`), which
|
|
33
|
+
is why the handler's return is its own `const` type parameter; arrays in a
|
|
34
|
+
returned literal are read as readonly, which is all an answer needs.
|
|
35
|
+
- To move: drop the second parameter and return what was passed to
|
|
36
|
+
`res.api()`. An answer that was `res.api(null, { errorMessage })`,
|
|
37
|
+
`{ notAuthorized }` or `{ sessionExpired }` becomes `refuse(content, {
|
|
38
|
+
notAuthorized, sessionExpired, code, type })`. A handler that answered
|
|
39
|
+
null where the output does not allow null no longer compiles; it refuses
|
|
40
|
+
instead, or the output becomes nullable.
|
|
41
|
+
- **A refusal is never stored for replay.** Only an answer (a returned
|
|
42
|
+
output) is stored under an idempotency key; a refusal is thrown, the claim
|
|
43
|
+
is released, and a retry runs the handler again, which decides afresh. A
|
|
44
|
+
corrected request after a refusal goes through under the same key instead
|
|
45
|
+
of being refused as a reused key.
|
|
46
|
+
- **Response headers and cookies are written through the context.**
|
|
47
|
+
`ctx.setResponseHeader(key, value)`, `ctx.addResponseHeader(key, value)`,
|
|
48
|
+
`ctx.setCookie(name, value, options?)` and `ctx.clearCookie(name,
|
|
49
|
+
options?)` (the type `LambderResponseTools`) are on every context: routes,
|
|
50
|
+
API handlers, hooks, and mock handlers alike. `res.setHeader`,
|
|
51
|
+
`res.addHeader`, `res.setCookie` and `res.clearCookie` are removed. The
|
|
52
|
+
writers say "Response" because `ctx.header(name)` reads a request header.
|
|
53
|
+
- **The log channel is `ctx.logList`.** `res.logToApiResponse(entry)` is
|
|
54
|
+
removed; push the entry onto `ctx.logList`, which the mock's context has
|
|
55
|
+
always had.
|
|
56
|
+
- **Answer compression is declared per API.** `addApi` and `addSessionApi`
|
|
57
|
+
take `compress?: boolean | "auto"` beside the other options: "auto" (the
|
|
58
|
+
default) compresses for an accepting caller when the body is large enough,
|
|
59
|
+
false never (an answer carrying base64 bytes), true always. It covers
|
|
60
|
+
refusals and replayed answers too, and is a transport setting of the
|
|
61
|
+
server, not part of the contract. `res.apiBinary()` and the per-answer
|
|
62
|
+
response options of an API handler (status code, compression, cache
|
|
63
|
+
headers) are removed; a header goes through `ctx.setResponseHeader`.
|
|
64
|
+
- **`res.api()` writes an envelope by hand, for code outside an API
|
|
65
|
+
handler.** A hook, the input validation handler or a global error handler
|
|
66
|
+
answering an API call still uses it; the payload goes out as given. The
|
|
67
|
+
typed overloads and `LambderResolver`'s output type parameter are removed,
|
|
68
|
+
with the types `LambderResolverApiMethod` and `LambderApiNullAnswerConfig`.
|
|
69
|
+
A binary or file answer is a route's job.
|
|
70
|
+
|
|
71
|
+
### Removed
|
|
72
|
+
|
|
73
|
+
- **The `message` envelope channel.** The untyped `message` field of the
|
|
74
|
+
envelope and of `LambderApiResponseConfig`, `LambderCaller`'s
|
|
75
|
+
`messageHandler` option and per-call override, and the mock's
|
|
76
|
+
`ctx.envelope`. What a success says belongs in the output schema, where it
|
|
77
|
+
is typed and parsed.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- `LambderResponseTools`, the type of the four response writers every
|
|
82
|
+
context carries, exported from `lambder`.
|
|
83
|
+
|
|
84
|
+
## [8.3.1] - 2026-09-27
|
|
85
|
+
|
|
86
|
+
Six additions, each a thing an app otherwise writes for itself once per kind
|
|
87
|
+
of token, secret, retry loop, copied declaration or store: signed claims
|
|
88
|
+
tokens, one-shot secrets over a store that settles their races, the declared
|
|
89
|
+
API options as generated files of plain data, a backoff timer, the digest and
|
|
90
|
+
random secret behind stored secrets, and the store conformance suites for a
|
|
91
|
+
store an app writes over its own database. Nothing changes on the wire, and
|
|
92
|
+
every existing option keeps its meaning.
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
|
|
96
|
+
- **`writeApiOptions` in `lambder/build`: the declared options as a generated
|
|
97
|
+
file.** The contract carries every API's `guards`, `rateLimit` and
|
|
98
|
+
`idempotency` options as types; code that decides something at runtime with
|
|
99
|
+
them (a mock restating the server's policies, a test walking the public
|
|
100
|
+
surface, a screen asking which permission an endpoint needs) copied them by
|
|
101
|
+
hand and held the copies honest with tests that read the source.
|
|
102
|
+
`writeApiOptions({ module, exportName, file, check })` writes them once, as
|
|
103
|
+
three `as const` tables of plain data (`apiOptions`, `rateLimitPolicies`,
|
|
104
|
+
`guardDeclarations`) from the new `lambder.apiOptionEntries()`, sorted by
|
|
105
|
+
name, and `check: true` fails a stale file naming what moved per table.
|
|
106
|
+
Nothing in the file is code: a guard parameter that is not plain data fails
|
|
107
|
+
the write by API name, a policy keyed by a handler is written as `per:
|
|
108
|
+
"custom"` and no more, and a guard's schema as its input mode alone.
|
|
109
|
+
- Readers in `lambder/client`, typed to the tables' literals:
|
|
110
|
+
`LambderApisWithGuard`, `LambderApisGuardedBy`, `LambderApisWithMode`,
|
|
111
|
+
`LambderGuardParamOf` and `apiGuardParam(apiOptions, name, guard)`.
|
|
112
|
+
- `writeApiGuardParams({ module, exportName, guard, file, check })` writes
|
|
113
|
+
one guard's parameters beside it, as an `as const` table (`guardParams`)
|
|
114
|
+
of the APIs that declare the guard and what each gives it, with nothing
|
|
115
|
+
else about any API and no import: the least a browser gating a screen on
|
|
116
|
+
that guard needs, where importing `apiOptions` as a value would ship
|
|
117
|
+
every endpoint name and every guard's parameter, reasons included.
|
|
118
|
+
- `lambderMockPoliciesFrom(rateLimitPolicies, { keys })` in `lambder/mock`
|
|
119
|
+
rebuilds the policy configs `create()` takes from the table, requiring a
|
|
120
|
+
key handler for exactly the custom-keyed policies; `create()` takes a
|
|
121
|
+
`guardDeclarations` option that holds each mock guard to the server
|
|
122
|
+
guard's input mode and session requirement (`LambderMockGuardShapeOf`);
|
|
123
|
+
and an `apiOptions` option, the table itself, from which every entry's
|
|
124
|
+
guards, rate limit and idempotency are read. Given it, an entry is its
|
|
125
|
+
handler alone, a restated option is a compile error and a throw, the
|
|
126
|
+
table must cover the contract under each endpoint's mode
|
|
127
|
+
(`LambderMockApiOptionsCover`), and a `restNotMocked` answer runs under
|
|
128
|
+
the mode the table gives the name, so an unmocked session endpoint reads
|
|
129
|
+
the session first.
|
|
130
|
+
- The entry types `LambderApiOptionEntries`, `LambderApiOptionEntry`,
|
|
131
|
+
`LambderRateLimitPolicyEntry` and `LambderGuardDeclarationEntry` live in
|
|
132
|
+
`shared/wire`, with `LambderGuardRunAt`, `LambderRateLimitBudget` and
|
|
133
|
+
`LambderRateLimitChargeAt`, which moved down there unchanged.
|
|
134
|
+
|
|
135
|
+
See [the options as a generated file](./docs/apis.md#the-options-as-a-generated-file).
|
|
136
|
+
|
|
137
|
+
- **`LambderSignedClaims`: signed tokens that are their own record.** One
|
|
138
|
+
instance per kind of token, built once with the secret, a version and the
|
|
139
|
+
zod schema of its claims; `sign(claims)` writes `<version>.<base64url
|
|
140
|
+
claims>.<base64url HMAC-SHA256>`, `verify(token, { now? })` answers the
|
|
141
|
+
claims or null for a forged, foreign, malformed, refused or expired token
|
|
142
|
+
alike. An optional `exp` claim in epoch seconds is judged on every verify,
|
|
143
|
+
against an injectable clock. WebCrypto only, exported from `lambder` and
|
|
144
|
+
`lambder/client` for the edge runtimes and shared backend packages that
|
|
145
|
+
verify where the secret is at hand. Beside it, `keyedDigest(secret,
|
|
146
|
+
value)`, the HMAC-SHA256 as base64url a stored secret rests as;
|
|
147
|
+
`randomSecret(bytes?)`; and `constantTimeEquals`.
|
|
148
|
+
|
|
149
|
+
- **`LambderOneShotSecrets`: codes and tokens handed out once and taken back
|
|
150
|
+
once.** The code emailed to an address, the link in an activation mail, the
|
|
151
|
+
code texted before a document opens, the code read out to pair a device:
|
|
152
|
+
one life (minted, sent, stored as a digest, tried against, spent), written
|
|
153
|
+
once, over a `LambderOneShotSecretStore` that settles its races in six
|
|
154
|
+
methods. An app declares its kinds (a `code` of an alphabet and length with
|
|
155
|
+
a ceiling on tries, or a `token` redeemed by value: random bytes, or an
|
|
156
|
+
alphabet's characters for one somebody types) and names, per secret,
|
|
157
|
+
the scope it proves; `issue(kind, scope, { cooldownSeconds?, meta? })`
|
|
158
|
+
answers the plaintext exactly once, `redeem(kind, scope, candidate)` and
|
|
159
|
+
`redeemToken(kind, candidate)` answer `accepted`, `wrong` (with the tries
|
|
160
|
+
left), `expired`, `exhausted` or `none`, and `retire(scope)` ends what the
|
|
161
|
+
scope holds. One record is live per scope; a cooldown is a condition on
|
|
162
|
+
the issuing write; a token's digest is claimed by one scope at a time in
|
|
163
|
+
that same write, and a secret drawn onto a digest another scope holds is
|
|
164
|
+
drawn again (up to five times), so two scopes that drew the same short
|
|
165
|
+
code never redeem each other's; a try is counted in the write that reads
|
|
166
|
+
the digest; a redemption is a conditional consume.
|
|
167
|
+
`LambderDdbOneShotSecretStore` keeps a code as one item under `OTS#` in
|
|
168
|
+
the policy table and a token as two, written in one transaction, and sends
|
|
169
|
+
a write DynamoDB refused for a concurrent transaction on its item again, up
|
|
170
|
+
to three times; `LambderMemoryOneShotSecretStore` keeps the same in a map, and
|
|
171
|
+
`lambderOneShotSecretStoreConformance` holds both, and an app's own store,
|
|
172
|
+
to one set of rules. The class sits in a new `src/secrets/` layer beside
|
|
173
|
+
`session/`.
|
|
174
|
+
|
|
175
|
+
- **`LambderBackoffTimer`: waiting longer after each failure, once.** One
|
|
176
|
+
pending wait at a time: `retry(run)` and `wait(signal?)` climb a jittered
|
|
177
|
+
ladder (`baseMs` the shortest wait, `maxMs` the longest, `factor`,
|
|
178
|
+
`jitter`), `after(ms, run)` waits off
|
|
179
|
+
it, `reset()` and `cancel()`; `wait` rejects with the signal's reason on
|
|
180
|
+
abort and with an Error when dropped, so an await on it always settles.
|
|
181
|
+
Exported from `lambder` and `lambder/client`.
|
|
182
|
+
|
|
183
|
+
See [Secrets and retries](./docs/secrets.md).
|
|
184
|
+
|
|
185
|
+
- **Store conformance suites in `lambder/testing`.** The rules each store
|
|
186
|
+
interface promises its engine were asserted in Lambder's own test suite,
|
|
187
|
+
where only Lambder's stores could meet them; a store an app writes over its
|
|
188
|
+
own database had nothing to hold it to the same rules. They are now
|
|
189
|
+
exported, one suite per interface: `lambderSessionStoreConformance`,
|
|
190
|
+
`lambderIdempotencyStoreConformance`, `lambderRateLimiterConformance` and
|
|
191
|
+
`lambderOneShotSecretStoreConformance`. Each takes the runner's own `it`
|
|
192
|
+
and `expect` (any jest-style `expect`; Lambder imports no runner) and a
|
|
193
|
+
`create` that builds a store for one case, handed the case's clock as
|
|
194
|
+
`now`. The one-shot suite also takes what a store over existing rows needs
|
|
195
|
+
in place of its defaults: two `scopes` it can hold, the kind it keeps for
|
|
196
|
+
each shape it holds (`kinds: { code?, token? }`, which decides whether the
|
|
197
|
+
cases about tries or the ones about digests run), the `meta`, and the
|
|
198
|
+
`lifetimeSeconds` it derives an expiry from. Among its rules: two issues
|
|
199
|
+
racing for one scope leave exactly one live record, two scopes racing for
|
|
200
|
+
one token digest leave it with exactly one, and `attempt` and `consume`
|
|
201
|
+
name a record by its scope and id together, so a record of another scope
|
|
202
|
+
is never the one named. Lambder's memory and DynamoDB stores run through
|
|
203
|
+
the same suites.
|
|
204
|
+
|
|
205
|
+
See [A store of your own](./docs/testing.md#a-store-of-your-own).
|
|
206
|
+
|
|
207
|
+
### Changed
|
|
208
|
+
|
|
209
|
+
- **`LambderUploadRunner` waits on a `LambderBackoffTimer` between tries at
|
|
210
|
+
storage.** The ladder is the timer's: each wait is `baseDelayMs` plus a
|
|
211
|
+
random share of a ceiling that starts at `baseDelayMs` and doubles per
|
|
212
|
+
failed attempt, the whole never past `maxDelayMs`. The first wait is
|
|
213
|
+
unchanged (between the base and twice it); a later one may be shorter than
|
|
214
|
+
before, since the ceiling now counts from the base rather than from twice
|
|
215
|
+
it.
|
|
216
|
+
- **The session crypto shares its HMAC and its constant-time comparison** with
|
|
217
|
+
the new module through `shared/util/LambderTextDigest.ts` rather than
|
|
218
|
+
keeping copies of its own. Behaviour is unchanged.
|
|
219
|
+
|
|
12
220
|
## [8.1.2] - 2026-09-26
|
|
13
221
|
|
|
14
222
|
### Fixed
|
package/README.md
CHANGED
|
@@ -17,15 +17,17 @@ const lambder = initLambder<SessionData>().create({
|
|
|
17
17
|
}).addApi("getCompany", {
|
|
18
18
|
input: z.object({ slug: z.string() }),
|
|
19
19
|
output: z.object({ id: z.string(), name: z.string() }),
|
|
20
|
-
}, async ({ apiPayload }
|
|
20
|
+
}, async ({ apiPayload }) => await loadCompany(apiPayload.slug));
|
|
21
21
|
|
|
22
22
|
export type ApiContractType = typeof lambder.ApiContract;
|
|
23
23
|
export const handler = lambder.getHandler();
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`
|
|
26
|
+
A handler returns its output, which is parsed through the output schema before
|
|
27
|
+
it is sent, and says no by throwing `refuse()`. Registration chains onto the
|
|
28
|
+
creation call: every `addApi` returns an instance carrying the contract so
|
|
29
|
+
far, so the whole backend is one declaration and `lambder.ApiContract` is the
|
|
30
|
+
accumulated type.
|
|
29
31
|
|
|
30
32
|
The frontend imports that contract type and gets autocomplete, typed payloads
|
|
31
33
|
and typed results with no hand-written client:
|
|
@@ -113,10 +115,10 @@ The package ships five entry points; pick by where the code runs:
|
|
|
113
115
|
| Entry | Runs in | Carries |
|
|
114
116
|
| --- | --- | --- |
|
|
115
117
|
| `lambder` | Server (Lambda) | The full framework: pipeline, sessions, DDB stores, policies, plus the API core's building blocks and everything from `lambder/client` except the two request-compression helpers, `compressPayloadGzip` and `isRequestCompressionAvailable`, which stay on the client entry where a payload is compressed |
|
|
116
|
-
| `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `html`/`xml` tagged templates, `createLambderI18n` |
|
|
118
|
+
| `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `LambderUploadRunner`, `LambderBackoffTimer`, `LambderSignedClaims`, `html`/`xml` tagged templates, `createLambderI18n` |
|
|
117
119
|
| `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 |
|
|
118
|
-
| `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 |
|
|
119
|
-
| `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiContract`: the contract as plain types a client compiles instead of the server |
|
|
120
|
+
| `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 |
|
|
121
|
+
| `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 and nothing else, what a browser gating on that guard carries; `writeApiContract`: the contract as plain types a client compiles instead of the server |
|
|
120
122
|
|
|
121
123
|
Frontends and shared isomorphic packages should import from `lambder/client`
|
|
122
124
|
only; the entry's module graph contains no AWS SDK, Node built-ins, or server
|
|
@@ -126,14 +128,15 @@ tree-shaking.
|
|
|
126
128
|
Source layout mirrors this: `src/api/` (the isomorphic API core: request,
|
|
127
129
|
answer, envelope, pipeline, and the declarative policies the pipeline runs),
|
|
128
130
|
`src/core/` (the Lambda server adapter: routes, files, hooks, finalization),
|
|
129
|
-
`src/session/` (the session manager, controller and crypto), `src/
|
|
131
|
+
`src/session/` (the session manager, controller and crypto), `src/secrets/`
|
|
132
|
+
(one-shot secrets over their store), `src/stores/`
|
|
130
133
|
(every store and file-source implementation, DynamoDB and in-memory alike,
|
|
131
134
|
and the helpers the two caches share), `src/client/`,
|
|
132
135
|
`src/invoke/` (the lambda-to-lambda caller and the in-process handler
|
|
133
|
-
transport), `src/mock/` (the mock runtime), `src/testing/` (the test app),
|
|
136
|
+
transport), `src/mock/` (the mock runtime), `src/testing/` (the test app and the store conformance suites),
|
|
134
137
|
`src/build/` (what a generator script runs at build time), and `src/shared/`
|
|
135
138
|
(isomorphic modules every entry re-exports, grouped into `wire/` for the
|
|
136
|
-
format both sides speak, `contracts/` for the
|
|
139
|
+
format both sides speak, `contracts/` for the six store and source
|
|
137
140
|
interfaces, `transport/` for the caller-to-server seam, and `util/` for
|
|
138
141
|
helpers).
|
|
139
142
|
Directories are layers and imports only ever point down;
|
|
@@ -152,11 +155,11 @@ guide that matches what you are building. The full index lives in
|
|
|
152
155
|
| [Configuration](./docs/configuration.md) | Every `initLambder().create({...})` option, in one reference |
|
|
153
156
|
| [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, crash reporting, and non-HTTP invocations |
|
|
154
157
|
| [APIs and refusals](./docs/apis.md) | `addApi`/`addSessionApi`, the inferred contract, `refuse()` and `LambderApiRefusal`, the signature file |
|
|
155
|
-
| [Responses](./docs/responses.md) | The render context
|
|
158
|
+
| [Responses](./docs/responses.md) | The render context and its response tools (headers, cookies, log entries), the response builder routes and hooks use, compression, ETag and the size cap |
|
|
156
159
|
| [Sessions](./docs/sessions.md) | Sessions over a store, cookie scope, secrets at rest, `dataRefresh`, the controller API |
|
|
157
160
|
| [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
|
|
158
161
|
| [Calling another lambda](./docs/invoke.md) | `LambderInvokeCaller`: invoking a Lambder app in another function, its contract, failures and compression |
|
|
159
|
-
| [Testing](./docs/testing.md) | `lambderTestApp`: the real instance under test with no HTTP and no AWS, visitors, sign-in without a login endpoint, outcome assertions, crashes, time |
|
|
162
|
+
| [Testing](./docs/testing.md) | `lambderTestApp`: the real instance under test with no HTTP and no AWS, visitors, sign-in without a login endpoint, outcome assertions, crashes, time, store conformance suites |
|
|
160
163
|
| [The API core](./docs/api-core.md) | `LambderApiPipeline`: the one pipeline the server and the mock runtime run, the store interfaces, the transports |
|
|
161
164
|
| [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression, transports |
|
|
162
165
|
| [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
|
|
@@ -184,25 +187,14 @@ framework:
|
|
|
184
187
|
## Versioning and changes
|
|
185
188
|
|
|
186
189
|
Released versions and what each one changed are in
|
|
187
|
-
[CHANGELOG.md](./CHANGELOG.md). The current major is
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
handlers whose payload does not match their output schema, code that decoded
|
|
196
|
-
`ctx.path` itself, string routes that match case-sensitively, compression
|
|
197
|
-
behind a REST API, two more IAM actions (`UpdateItem` on the session table,
|
|
198
|
-
`GetItem` on the rate-limit table), custom-key limits charged after the
|
|
199
|
-
guards, idempotency keys bound to the request they were first sent with,
|
|
200
|
-
`errorMessage` always being an object, `credentials: true` needing named
|
|
201
|
-
origins, template slots and `html` interpolations refused or checked in
|
|
202
|
-
more attribute positions, `refreshSessionData()` throwing where it answered
|
|
203
|
-
null, and `z.ZodType<T>` annotations leaving a field unchecked. Every live
|
|
204
|
-
session is signed out once by the
|
|
205
|
-
upgrade. An app still on v6 goes through the 7.0.0 entry first.
|
|
190
|
+
[CHANGELOG.md](./CHANGELOG.md). The current major is v9, which gives an API
|
|
191
|
+
handler one shape: it takes its context and returns its output, says no with
|
|
192
|
+
`refuse()`, and writes headers, cookies and log entries through the context,
|
|
193
|
+
while the response builder stays with routes, hooks and error handlers. Every
|
|
194
|
+
break and what to do about it is in the 9.0.1 entry, and the compiler finds
|
|
195
|
+
most of them. An app still on v7 goes through the 8.0.2 entry first, which
|
|
196
|
+
names the breaks of v8 the compiler cannot find, and one on v6 through the
|
|
197
|
+
7.0.0 entry before that.
|
|
206
198
|
|
|
207
199
|
## Contributing
|
|
208
200
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
|
|
2
2
|
import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
3
|
+
import { type LambderCookieOptions, type LambderClearCookieOptions } from "../shared/wire/LambderCookie.js";
|
|
3
4
|
/**
|
|
4
5
|
* The context the API core needs from whoever runs it. The server's render
|
|
5
6
|
* context and the mock runtime's handler context both extend it; the
|
|
@@ -12,7 +13,7 @@ import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
|
12
13
|
* - `responseHeaders` collects headers written during the call, applied
|
|
13
14
|
* onto the answer by the pipeline.
|
|
14
15
|
* - `logList` collects entries for the envelope's logList channel
|
|
15
|
-
* (`
|
|
16
|
+
* (`ctx.logList.push(entry)` from a handler).
|
|
16
17
|
*/
|
|
17
18
|
export type LambderApiCallContext<TSessionData = any> = {
|
|
18
19
|
session: LambderSessionRecord<TSessionData> | null;
|
|
@@ -22,6 +23,35 @@ export type LambderApiCallContext<TSessionData = any> = {
|
|
|
22
23
|
};
|
|
23
24
|
/** A fresh call context: no session, no guard data, nothing pending. */
|
|
24
25
|
export declare const createApiCallContext: <TSessionData = any>() => LambderApiCallContext<TSessionData>;
|
|
26
|
+
/**
|
|
27
|
+
* What a handler writes onto its answer beside the body: headers and cookies,
|
|
28
|
+
* collected on `responseHeaders` and applied to whatever answer the request
|
|
29
|
+
* ends with. On every context a handler receives, the server's and the
|
|
30
|
+
* mock's alike, since an API handler returns its output and has no response
|
|
31
|
+
* builder to write them on.
|
|
32
|
+
*/
|
|
33
|
+
export type LambderResponseTools = {
|
|
34
|
+
/** Replaces a response header. */
|
|
35
|
+
setResponseHeader(key: string, value: string | string[]): void;
|
|
36
|
+
/** Appends a response header value (repeatable for one key). */
|
|
37
|
+
addResponseHeader(key: string, value: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Adds a Set-Cookie header. A function-form `domain` is resolved against
|
|
40
|
+
* the request host. Defaults: Path=/, SameSite=Lax, Secure, not HttpOnly,
|
|
41
|
+
* browser-session lifetime.
|
|
42
|
+
*/
|
|
43
|
+
setCookie(name: string, value: string, options?: LambderCookieOptions): void;
|
|
44
|
+
/**
|
|
45
|
+
* Adds a Set-Cookie header that deletes the cookie. Pass the `domain` and
|
|
46
|
+
* `path` it was set with: a cookie's identity is (name, domain, path), and
|
|
47
|
+
* a deletion under another scope deletes nothing.
|
|
48
|
+
*/
|
|
49
|
+
clearCookie(name: string, options?: LambderClearCookieOptions): void;
|
|
50
|
+
};
|
|
51
|
+
/** The response tools of one context, writing into its own responseHeaders; `host` resolves a function-form cookie domain. */
|
|
52
|
+
export declare const responseToolsOf: (ctx: {
|
|
53
|
+
responseHeaders: LambderAnswerHeaders;
|
|
54
|
+
}, host: string) => LambderResponseTools;
|
|
25
55
|
/**
|
|
26
56
|
* Binds an adapter's tools onto one call context: `getters` run when read
|
|
27
57
|
* (`ctx.sessionController` is built over the object it was read from),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LambderAnswerHeaders } from "../shared/wire/LambderAnswerHeaders.js";
|
|
2
|
+
import { serializeCookie, serializeClearCookie } from "../shared/wire/LambderCookie.js";
|
|
2
3
|
/** A fresh call context: no session, no guard data, nothing pending. */
|
|
3
4
|
export const createApiCallContext = () => ({
|
|
4
5
|
session: null,
|
|
@@ -10,6 +11,13 @@ export const createApiCallContext = () => ({
|
|
|
10
11
|
responseHeaders: new LambderAnswerHeaders(),
|
|
11
12
|
logList: [],
|
|
12
13
|
});
|
|
14
|
+
/** The response tools of one context, writing into its own responseHeaders; `host` resolves a function-form cookie domain. */
|
|
15
|
+
export const responseToolsOf = (ctx, host) => ({
|
|
16
|
+
setResponseHeader: (key, value) => { ctx.responseHeaders.set(key, value); },
|
|
17
|
+
addResponseHeader: (key, value) => { ctx.responseHeaders.add(key, value); },
|
|
18
|
+
setCookie: (name, value, options) => { ctx.responseHeaders.add("Set-Cookie", serializeCookie(name, value, options, host)); },
|
|
19
|
+
clearCookie: (name, options) => { ctx.responseHeaders.add("Set-Cookie", serializeClearCookie(name, options, host)); },
|
|
20
|
+
});
|
|
13
21
|
/**
|
|
14
22
|
* Binds an adapter's tools onto one call context: `getters` run when read
|
|
15
23
|
* (`ctx.sessionController` is built over the object it was read from),
|
|
@@ -8,8 +8,8 @@ import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRate
|
|
|
8
8
|
* because the mock has none; when input is present, validation runs and the
|
|
9
9
|
* handler sees the parsed payload. Output is part of the endpoint's
|
|
10
10
|
* signature (apiSignatureOf), which is what a client's build is checked
|
|
11
|
-
* against; the server
|
|
12
|
-
*
|
|
11
|
+
* against; the server also parses every output a handler returns through it
|
|
12
|
+
* before it is sent.
|
|
13
13
|
*/
|
|
14
14
|
export type LambderApiDefinition = {
|
|
15
15
|
name: string;
|
|
@@ -13,7 +13,7 @@ export type LambderApiEnvelopeConfig = LambderApiResponseConfig & {
|
|
|
13
13
|
* plain success is `{ apiVersion, payload }` and nothing else; an empty
|
|
14
14
|
* logList is omitted.
|
|
15
15
|
*/
|
|
16
|
-
export declare const buildApiEnvelope: <T>(apiVersion: string | null | undefined, payload: T | null, { versionExpired, sessionExpired, notAuthorized,
|
|
16
|
+
export declare const buildApiEnvelope: <T>(apiVersion: string | null | undefined, payload: T | null, { versionExpired, sessionExpired, notAuthorized, errorMessage, logList, crash, }?: LambderApiEnvelopeConfig) => LambderApiEnvelopeBody<T>;
|
|
17
17
|
/** An envelope as an answer: JSON body, JSON content type, the status and headers given (200 and none by default). */
|
|
18
18
|
export declare const envelopeAnswer: (envelope: LambderApiEnvelopeBody<unknown>, options?: {
|
|
19
19
|
statusCode?: number;
|
|
@@ -4,8 +4,8 @@ import { setAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
|
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
|
-
* malformed compressed payload, and the last-resort crash. The server's
|
|
8
|
-
* res.api(), the mock runtime and the pipeline all build through these
|
|
7
|
+
* malformed compressed payload, and the last-resort crash. The server's API
|
|
8
|
+
* path and res.api(), the mock runtime and the pipeline all build through these
|
|
9
9
|
* functions, so server and mock cannot drift on a single byte of the wire
|
|
10
10
|
* format. Pure: no Node built-ins, no response classes.
|
|
11
11
|
*/
|
|
@@ -15,7 +15,7 @@ export const API_ANSWER_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
|
15
15
|
* plain success is `{ apiVersion, payload }` and nothing else; an empty
|
|
16
16
|
* logList is omitted.
|
|
17
17
|
*/
|
|
18
|
-
export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionExpired, notAuthorized,
|
|
18
|
+
export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionExpired, notAuthorized, errorMessage, logList, crash, } = {}) => ({
|
|
19
19
|
apiVersion: apiVersion ?? null,
|
|
20
20
|
payload,
|
|
21
21
|
...(versionExpired ? { versionExpired } : {}),
|
|
@@ -26,7 +26,6 @@ export const buildApiEnvelope = (apiVersion, payload, { versionExpired, sessionE
|
|
|
26
26
|
// flags above are booleans, where false and absent mean the same. An
|
|
27
27
|
// errorMessage goes out as a message object whatever form it was written
|
|
28
28
|
// in, so every reader meets one shape.
|
|
29
|
-
...(message !== undefined ? { message } : {}),
|
|
30
29
|
...(errorMessage !== undefined ? { errorMessage: refusalMessageOf(errorMessage) } : {}),
|
|
31
30
|
...(crash !== undefined ? { crash } : {}),
|
|
32
31
|
...(logList?.length ? { logList } : {}),
|
|
@@ -37,22 +37,8 @@ type LambderGuardAnswerCheck<TOutput> = [
|
|
|
37
37
|
type LambderGuardOf<TOutput, TGuard> = LambderGuardAnswerCheck<TOutput> extends LambderGuardMustNotAnswer ? LambderGuardMustNotAnswer : TGuard;
|
|
38
38
|
/** One guard handler: the adapter's context, the validated input slice (undefined in the no-input mode), and the per-API parameter. */
|
|
39
39
|
type LambderGuardHandler<TCtx, TPayload, TParam, TOutput> = (ctx: TCtx, payload: TPayload, param: TParam) => TOutput | Promise<TOutput>;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
* - "beforeInputValidation" (default): an unauthorized caller learns nothing
|
|
44
|
-
* about the input, and no async refinement in the schema runs for it.
|
|
45
|
-
* - "afterInputValidation": for a guard that spends something on the
|
|
46
|
-
* request, such as a single-use captcha token, which a request refused for
|
|
47
|
-
* a mistyped field would otherwise waste. The API's input schema then runs
|
|
48
|
-
* for callers this guard would refuse, so keep lookups (an "email is free"
|
|
49
|
-
* refinement) out of it, in the handler.
|
|
50
|
-
*
|
|
51
|
-
* Guards run in their declared order within each, and the limits keyed by
|
|
52
|
-
* caller data are charged after both unless their policy says otherwise
|
|
53
|
-
* (LambderRateLimitChargeAt).
|
|
54
|
-
*/
|
|
55
|
-
export type LambderGuardRunAt = "beforeInputValidation" | "afterInputValidation";
|
|
40
|
+
import type { LambderGuardRunAt } from "../shared/wire/LambderApiOptionEntries.js";
|
|
41
|
+
export type { LambderGuardRunAt };
|
|
56
42
|
/** Where in the call a guard runs: see LambderGuardRunAt. */
|
|
57
43
|
export type LambderGuardPlacement = {
|
|
58
44
|
/** Default: "beforeInputValidation". */
|
|
@@ -331,4 +317,3 @@ export declare class LambderApiGuardsEngine {
|
|
|
331
317
|
*/
|
|
332
318
|
run(request: LambderApiRequest, ctx: LambderApiCallContext, guardsOption: LambderGuardsOptionValue | undefined, trace: LambderApiCallTrace, runAt: LambderGuardRunAt): Promise<void>;
|
|
333
319
|
}
|
|
334
|
-
export {};
|
|
@@ -386,13 +386,11 @@ export class LambderApiIdempotencyEngine {
|
|
|
386
386
|
}
|
|
387
387
|
// A crash or a thrown refusal (LambderApiRefusal, refuse())
|
|
388
388
|
// releases the claim so a retry retries. The rule is deliberate:
|
|
389
|
-
// ANSWERS
|
|
390
|
-
//
|
|
391
|
-
// on retry and the handler decides afresh.
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
// since a cleanup error in its place would hide why the call
|
|
395
|
-
// failed.
|
|
389
|
+
// ANSWERS (the output a handler returned) are stored and
|
|
390
|
+
// replayed; EXCEPTIONS are not, so a thrown refusal re-executes
|
|
391
|
+
// on retry and the handler decides afresh. The handler's own
|
|
392
|
+
// error is what gets rethrown, since a cleanup error in its place
|
|
393
|
+
// would hide why the call failed.
|
|
396
394
|
try {
|
|
397
395
|
await store.abandon(scopeKey, ownerToken);
|
|
398
396
|
}
|
|
@@ -78,34 +78,8 @@ export type LambderRateLimitKeyBuilder<TCtx> = {
|
|
|
78
78
|
export declare const lambderRateLimitKeyBuilder: <TCtx>() => LambderRateLimitKeyBuilder<TCtx>;
|
|
79
79
|
/** What one rate-limit counter tracks: the client IP, the session identity, or a custom payload-derived key. */
|
|
80
80
|
export type LambderRateLimitPer<TCtx = any> = "ip" | "session" | LambderRateLimitKeyFn<any, TCtx>;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*
|
|
84
|
-
* - "perApi" (default): every API referencing the policy gets its own
|
|
85
|
-
* counter, so the windows are a per-API ceiling (three APIs referencing a
|
|
86
|
-
* 60/min policy allow one subject 180/min in total). An API may tune the
|
|
87
|
-
* windows in its declaration: `rateLimit: { name: { perMin: 20 } }`.
|
|
88
|
-
* - "perPolicy": every API referencing the policy shares ONE counter, so the
|
|
89
|
-
* windows are one combined budget (e.g. one per-email allowance across
|
|
90
|
-
* send, register, and reset). The policy IS the group: to give user APIs
|
|
91
|
-
* and report APIs separate shared budgets, declare two policies.
|
|
92
|
-
*/
|
|
93
|
-
export type LambderRateLimitBudget = "perApi" | "perPolicy";
|
|
94
|
-
/**
|
|
95
|
-
* When a custom-keyed policy is charged, relative to the guards and the input
|
|
96
|
-
* schema.
|
|
97
|
-
*
|
|
98
|
-
* - "afterGuards" (default): after every guard and the input schema passed.
|
|
99
|
-
* The key is a value the caller chose (an email in the payload), so a
|
|
100
|
-
* caller who never passes a captcha guard cannot spend a victim's budget
|
|
101
|
-
* and lock them out of reset, register and send-code.
|
|
102
|
-
* - "beforeGuards": before the guards and the input schema, so an attempt
|
|
103
|
-
* they refuse is counted too. For a limit on guessing a secret a guard or
|
|
104
|
-
* the schema checks (a one-time code checked by a guard, keyed per email):
|
|
105
|
-
* charged after them, a wrong guess is refused before it is ever counted.
|
|
106
|
-
* Pair it with an IP limit, since anyone may spend this budget.
|
|
107
|
-
*/
|
|
108
|
-
export type LambderRateLimitChargeAt = "beforeGuards" | "afterGuards";
|
|
81
|
+
import type { LambderRateLimitBudget, LambderRateLimitChargeAt } from "../shared/wire/LambderApiOptionEntries.js";
|
|
82
|
+
export type { LambderRateLimitBudget, LambderRateLimitChargeAt };
|
|
109
83
|
/**
|
|
110
84
|
* A named rate-limit policy: fixed windows, the key one counter tracks, and
|
|
111
85
|
* what one budget spans.
|
|
@@ -357,4 +331,3 @@ export declare class LambderApiRateLimitsEngine {
|
|
|
357
331
|
*/
|
|
358
332
|
private requestKeyOf;
|
|
359
333
|
}
|
|
360
|
-
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { LambderApiOptionEntries } from "../shared/wire/LambderApiOptionEntries.js";
|
|
2
|
+
import { type LambderModuleLocation } from "./moduleLocation.js";
|
|
3
|
+
/** What the generators read the options from: a Lambder instance, or anything else that reports them the same way. */
|
|
4
|
+
export type LambderApiOptionsSource = {
|
|
5
|
+
apiOptionEntries(): LambderApiOptionEntries;
|
|
6
|
+
};
|
|
7
|
+
/** Which names of one table moved: entries that changed, entries the file did not have, entries the file had and the instance no longer reports. */
|
|
8
|
+
export type LambderNameChanges = {
|
|
9
|
+
changed: string[];
|
|
10
|
+
added: string[];
|
|
11
|
+
removed: string[];
|
|
12
|
+
};
|
|
13
|
+
/** Imports the module, finds the instance under its export, and answers what it reports. `generator` names the caller in the errors. */
|
|
14
|
+
export declare const loadApiOptionEntries: (location: {
|
|
15
|
+
module: LambderModuleLocation;
|
|
16
|
+
exportName?: string;
|
|
17
|
+
}, generator: string) => Promise<LambderApiOptionEntries>;
|
|
18
|
+
/** One table of a generated file, read back as data, or null for a file that does not hold it as written. */
|
|
19
|
+
export declare const readGeneratedTable: (contents: string, exportName: string) => Record<string, unknown> | null;
|
|
20
|
+
/** How one table's entries differ from the ones a file held, by name, compared as canonical JSON so key order is not a change. */
|
|
21
|
+
export declare const nameChangesOf: (current: Record<string, unknown>, previous: Record<string, unknown>) => LambderNameChanges;
|
|
22
|
+
/** The opening comment of a generated file, one `//` line per line of the header. */
|
|
23
|
+
export declare const headerLinesOf: (header: string) => string[];
|
|
24
|
+
/**
|
|
25
|
+
* One table as a statement: its doc comment, the `// prettier-ignore` that
|
|
26
|
+
* keeps a formatter off the JSON a check reads back, and the table itself,
|
|
27
|
+
* `as const` with whatever `satisfies` clause the caller gives.
|
|
28
|
+
*/
|
|
29
|
+
export declare const tableStatementLines: (table: {
|
|
30
|
+
name: string;
|
|
31
|
+
doc: string;
|
|
32
|
+
value: unknown;
|
|
33
|
+
satisfies?: string;
|
|
34
|
+
semicolon: string;
|
|
35
|
+
}) => string[];
|
|
36
|
+
/**
|
|
37
|
+
* The end of a generator's run. With `check`, it answers whether the file
|
|
38
|
+
* holds what the instance reports and writes nothing. Otherwise it writes
|
|
39
|
+
* the file unless it already holds these tables, however it is formatted, so
|
|
40
|
+
* a watcher or an incremental build sees no change where there is none (a
|
|
41
|
+
* change of header or semicolons shows the next time a table changes).
|
|
42
|
+
* Either way the lines say what moved.
|
|
43
|
+
*/
|
|
44
|
+
export declare const settleGeneratedFile: (run: {
|
|
45
|
+
/** The file as the caller named it, for the lines. */
|
|
46
|
+
name: string;
|
|
47
|
+
/** The absolute path. */
|
|
48
|
+
file: string;
|
|
49
|
+
check: boolean | undefined;
|
|
50
|
+
/** The file's text before this run, or null when there was none. */
|
|
51
|
+
previousText: string | null;
|
|
52
|
+
/** Whether that text held the tables as written; false for a file rewritten by hand. */
|
|
53
|
+
readBack: boolean;
|
|
54
|
+
/** Each table the file holds, under its exported name: how many entries it has now, and which of them moved. */
|
|
55
|
+
tables: readonly {
|
|
56
|
+
name: string;
|
|
57
|
+
count: number;
|
|
58
|
+
changes: LambderNameChanges;
|
|
59
|
+
}[];
|
|
60
|
+
/** What the tables hold, for the lines: "6 APIs, 4 policies, 5 guards". */
|
|
61
|
+
held: string;
|
|
62
|
+
/** What the file holds when it is as written, for the line when it is not: "the three tables". */
|
|
63
|
+
tablesNoun: string;
|
|
64
|
+
/** The line when nothing moved: "no options changed". */
|
|
65
|
+
unmovedLine: string;
|
|
66
|
+
/** The file's contents, rendered only when it is written. */
|
|
67
|
+
render: () => string;
|
|
68
|
+
}) => {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
written: boolean;
|
|
71
|
+
lines: string[];
|
|
72
|
+
};
|