lambder 8.1.2 → 8.3.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 +136 -0
- package/README.md +8 -7
- package/dist/api/LambderApiGuards.d.ts +2 -17
- 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/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 +21 -0
- package/dist/core/Lambder.js +69 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/mock/LambderMockApp.d.ts +34 -17
- package/dist/mock/LambderMockApp.js +67 -21
- package/dist/mock/LambderMockCreateOptions.d.ts +68 -5
- package/dist/mock/LambderMockTypes.d.ts +29 -10
- 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/assertPlainData.d.ts +9 -0
- package/dist/shared/util/assertPlainData.js +41 -0
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +148 -0
- package/dist/shared/wire/LambderApiOptionEntries.js +35 -0
- 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,142 @@ 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
|
+
## [8.3.1] - 2026-09-27
|
|
13
|
+
|
|
14
|
+
Six additions, each a thing an app otherwise writes for itself once per kind
|
|
15
|
+
of token, secret, retry loop, copied declaration or store: signed claims
|
|
16
|
+
tokens, one-shot secrets over a store that settles their races, the declared
|
|
17
|
+
API options as generated files of plain data, a backoff timer, the digest and
|
|
18
|
+
random secret behind stored secrets, and the store conformance suites for a
|
|
19
|
+
store an app writes over its own database. Nothing changes on the wire, and
|
|
20
|
+
every existing option keeps its meaning.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`writeApiOptions` in `lambder/build`: the declared options as a generated
|
|
25
|
+
file.** The contract carries every API's `guards`, `rateLimit` and
|
|
26
|
+
`idempotency` options as types; code that decides something at runtime with
|
|
27
|
+
them (a mock restating the server's policies, a test walking the public
|
|
28
|
+
surface, a screen asking which permission an endpoint needs) copied them by
|
|
29
|
+
hand and held the copies honest with tests that read the source.
|
|
30
|
+
`writeApiOptions({ module, exportName, file, check })` writes them once, as
|
|
31
|
+
three `as const` tables of plain data (`apiOptions`, `rateLimitPolicies`,
|
|
32
|
+
`guardDeclarations`) from the new `lambder.apiOptionEntries()`, sorted by
|
|
33
|
+
name, and `check: true` fails a stale file naming what moved per table.
|
|
34
|
+
Nothing in the file is code: a guard parameter that is not plain data fails
|
|
35
|
+
the write by API name, a policy keyed by a handler is written as `per:
|
|
36
|
+
"custom"` and no more, and a guard's schema as its input mode alone.
|
|
37
|
+
- Readers in `lambder/client`, typed to the tables' literals:
|
|
38
|
+
`LambderApisWithGuard`, `LambderApisGuardedBy`, `LambderApisWithMode`,
|
|
39
|
+
`LambderGuardParamOf` and `apiGuardParam(apiOptions, name, guard)`.
|
|
40
|
+
- `writeApiGuardParams({ module, exportName, guard, file, check })` writes
|
|
41
|
+
one guard's parameters beside it, as an `as const` table (`guardParams`)
|
|
42
|
+
of the APIs that declare the guard and what each gives it, with nothing
|
|
43
|
+
else about any API and no import: the least a browser gating a screen on
|
|
44
|
+
that guard needs, where importing `apiOptions` as a value would ship
|
|
45
|
+
every endpoint name and every guard's parameter, reasons included.
|
|
46
|
+
- `lambderMockPoliciesFrom(rateLimitPolicies, { keys })` in `lambder/mock`
|
|
47
|
+
rebuilds the policy configs `create()` takes from the table, requiring a
|
|
48
|
+
key handler for exactly the custom-keyed policies; `create()` takes a
|
|
49
|
+
`guardDeclarations` option that holds each mock guard to the server
|
|
50
|
+
guard's input mode and session requirement (`LambderMockGuardShapeOf`);
|
|
51
|
+
and an `apiOptions` option, the table itself, from which every entry's
|
|
52
|
+
guards, rate limit and idempotency are read. Given it, an entry is its
|
|
53
|
+
handler alone, a restated option is a compile error and a throw, the
|
|
54
|
+
table must cover the contract under each endpoint's mode
|
|
55
|
+
(`LambderMockApiOptionsCover`), and a `restNotMocked` answer runs under
|
|
56
|
+
the mode the table gives the name, so an unmocked session endpoint reads
|
|
57
|
+
the session first.
|
|
58
|
+
- The entry types `LambderApiOptionEntries`, `LambderApiOptionEntry`,
|
|
59
|
+
`LambderRateLimitPolicyEntry` and `LambderGuardDeclarationEntry` live in
|
|
60
|
+
`shared/wire`, with `LambderGuardRunAt`, `LambderRateLimitBudget` and
|
|
61
|
+
`LambderRateLimitChargeAt`, which moved down there unchanged.
|
|
62
|
+
|
|
63
|
+
See [the options as a generated file](./docs/apis.md#the-options-as-a-generated-file).
|
|
64
|
+
|
|
65
|
+
- **`LambderSignedClaims`: signed tokens that are their own record.** One
|
|
66
|
+
instance per kind of token, built once with the secret, a version and the
|
|
67
|
+
zod schema of its claims; `sign(claims)` writes `<version>.<base64url
|
|
68
|
+
claims>.<base64url HMAC-SHA256>`, `verify(token, { now? })` answers the
|
|
69
|
+
claims or null for a forged, foreign, malformed, refused or expired token
|
|
70
|
+
alike. An optional `exp` claim in epoch seconds is judged on every verify,
|
|
71
|
+
against an injectable clock. WebCrypto only, exported from `lambder` and
|
|
72
|
+
`lambder/client` for the edge runtimes and shared backend packages that
|
|
73
|
+
verify where the secret is at hand. Beside it, `keyedDigest(secret,
|
|
74
|
+
value)`, the HMAC-SHA256 as base64url a stored secret rests as;
|
|
75
|
+
`randomSecret(bytes?)`; and `constantTimeEquals`.
|
|
76
|
+
|
|
77
|
+
- **`LambderOneShotSecrets`: codes and tokens handed out once and taken back
|
|
78
|
+
once.** The code emailed to an address, the link in an activation mail, the
|
|
79
|
+
code texted before a document opens, the code read out to pair a device:
|
|
80
|
+
one life (minted, sent, stored as a digest, tried against, spent), written
|
|
81
|
+
once, over a `LambderOneShotSecretStore` that settles its races in six
|
|
82
|
+
methods. An app declares its kinds (a `code` of an alphabet and length with
|
|
83
|
+
a ceiling on tries, or a `token` redeemed by value: random bytes, or an
|
|
84
|
+
alphabet's characters for one somebody types) and names, per secret,
|
|
85
|
+
the scope it proves; `issue(kind, scope, { cooldownSeconds?, meta? })`
|
|
86
|
+
answers the plaintext exactly once, `redeem(kind, scope, candidate)` and
|
|
87
|
+
`redeemToken(kind, candidate)` answer `accepted`, `wrong` (with the tries
|
|
88
|
+
left), `expired`, `exhausted` or `none`, and `retire(scope)` ends what the
|
|
89
|
+
scope holds. One record is live per scope; a cooldown is a condition on
|
|
90
|
+
the issuing write; a token's digest is claimed by one scope at a time in
|
|
91
|
+
that same write, and a secret drawn onto a digest another scope holds is
|
|
92
|
+
drawn again (up to five times), so two scopes that drew the same short
|
|
93
|
+
code never redeem each other's; a try is counted in the write that reads
|
|
94
|
+
the digest; a redemption is a conditional consume.
|
|
95
|
+
`LambderDdbOneShotSecretStore` keeps a code as one item under `OTS#` in
|
|
96
|
+
the policy table and a token as two, written in one transaction, and sends
|
|
97
|
+
a write DynamoDB refused for a concurrent transaction on its item again, up
|
|
98
|
+
to three times; `LambderMemoryOneShotSecretStore` keeps the same in a map, and
|
|
99
|
+
`lambderOneShotSecretStoreConformance` holds both, and an app's own store,
|
|
100
|
+
to one set of rules. The class sits in a new `src/secrets/` layer beside
|
|
101
|
+
`session/`.
|
|
102
|
+
|
|
103
|
+
- **`LambderBackoffTimer`: waiting longer after each failure, once.** One
|
|
104
|
+
pending wait at a time: `retry(run)` and `wait(signal?)` climb a jittered
|
|
105
|
+
ladder (`baseMs` the shortest wait, `maxMs` the longest, `factor`,
|
|
106
|
+
`jitter`), `after(ms, run)` waits off
|
|
107
|
+
it, `reset()` and `cancel()`; `wait` rejects with the signal's reason on
|
|
108
|
+
abort and with an Error when dropped, so an await on it always settles.
|
|
109
|
+
Exported from `lambder` and `lambder/client`.
|
|
110
|
+
|
|
111
|
+
See [Secrets and retries](./docs/secrets.md).
|
|
112
|
+
|
|
113
|
+
- **Store conformance suites in `lambder/testing`.** The rules each store
|
|
114
|
+
interface promises its engine were asserted in Lambder's own test suite,
|
|
115
|
+
where only Lambder's stores could meet them; a store an app writes over its
|
|
116
|
+
own database had nothing to hold it to the same rules. They are now
|
|
117
|
+
exported, one suite per interface: `lambderSessionStoreConformance`,
|
|
118
|
+
`lambderIdempotencyStoreConformance`, `lambderRateLimiterConformance` and
|
|
119
|
+
`lambderOneShotSecretStoreConformance`. Each takes the runner's own `it`
|
|
120
|
+
and `expect` (any jest-style `expect`; Lambder imports no runner) and a
|
|
121
|
+
`create` that builds a store for one case, handed the case's clock as
|
|
122
|
+
`now`. The one-shot suite also takes what a store over existing rows needs
|
|
123
|
+
in place of its defaults: two `scopes` it can hold, the kind it keeps for
|
|
124
|
+
each shape it holds (`kinds: { code?, token? }`, which decides whether the
|
|
125
|
+
cases about tries or the ones about digests run), the `meta`, and the
|
|
126
|
+
`lifetimeSeconds` it derives an expiry from. Among its rules: two issues
|
|
127
|
+
racing for one scope leave exactly one live record, two scopes racing for
|
|
128
|
+
one token digest leave it with exactly one, and `attempt` and `consume`
|
|
129
|
+
name a record by its scope and id together, so a record of another scope
|
|
130
|
+
is never the one named. Lambder's memory and DynamoDB stores run through
|
|
131
|
+
the same suites.
|
|
132
|
+
|
|
133
|
+
See [A store of your own](./docs/testing.md#a-store-of-your-own).
|
|
134
|
+
|
|
135
|
+
### Changed
|
|
136
|
+
|
|
137
|
+
- **`LambderUploadRunner` waits on a `LambderBackoffTimer` between tries at
|
|
138
|
+
storage.** The ladder is the timer's: each wait is `baseDelayMs` plus a
|
|
139
|
+
random share of a ceiling that starts at `baseDelayMs` and doubles per
|
|
140
|
+
failed attempt, the whole never past `maxDelayMs`. The first wait is
|
|
141
|
+
unchanged (between the base and twice it); a later one may be shorter than
|
|
142
|
+
before, since the ceiling now counts from the base rather than from twice
|
|
143
|
+
it.
|
|
144
|
+
- **The session crypto shares its HMAC and its constant-time comparison** with
|
|
145
|
+
the new module through `shared/util/LambderTextDigest.ts` rather than
|
|
146
|
+
keeping copies of its own. Behaviour is unchanged.
|
|
147
|
+
|
|
12
148
|
## [8.1.2] - 2026-09-26
|
|
13
149
|
|
|
14
150
|
### Fixed
|
package/README.md
CHANGED
|
@@ -113,10 +113,10 @@ The package ships five entry points; pick by where the code runs:
|
|
|
113
113
|
| Entry | Runs in | Carries |
|
|
114
114
|
| --- | --- | --- |
|
|
115
115
|
| `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` |
|
|
116
|
+
| `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
117
|
| `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 |
|
|
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; the store conformance suites, to hold a store you write to the rules Lambder's own meet |
|
|
119
|
+
| `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
120
|
|
|
121
121
|
Frontends and shared isomorphic packages should import from `lambder/client`
|
|
122
122
|
only; the entry's module graph contains no AWS SDK, Node built-ins, or server
|
|
@@ -126,14 +126,15 @@ tree-shaking.
|
|
|
126
126
|
Source layout mirrors this: `src/api/` (the isomorphic API core: request,
|
|
127
127
|
answer, envelope, pipeline, and the declarative policies the pipeline runs),
|
|
128
128
|
`src/core/` (the Lambda server adapter: routes, files, hooks, finalization),
|
|
129
|
-
`src/session/` (the session manager, controller and crypto), `src/
|
|
129
|
+
`src/session/` (the session manager, controller and crypto), `src/secrets/`
|
|
130
|
+
(one-shot secrets over their store), `src/stores/`
|
|
130
131
|
(every store and file-source implementation, DynamoDB and in-memory alike,
|
|
131
132
|
and the helpers the two caches share), `src/client/`,
|
|
132
133
|
`src/invoke/` (the lambda-to-lambda caller and the in-process handler
|
|
133
|
-
transport), `src/mock/` (the mock runtime), `src/testing/` (the test app),
|
|
134
|
+
transport), `src/mock/` (the mock runtime), `src/testing/` (the test app and the store conformance suites),
|
|
134
135
|
`src/build/` (what a generator script runs at build time), and `src/shared/`
|
|
135
136
|
(isomorphic modules every entry re-exports, grouped into `wire/` for the
|
|
136
|
-
format both sides speak, `contracts/` for the
|
|
137
|
+
format both sides speak, `contracts/` for the six store and source
|
|
137
138
|
interfaces, `transport/` for the caller-to-server seam, and `util/` for
|
|
138
139
|
helpers).
|
|
139
140
|
Directories are layers and imports only ever point down;
|
|
@@ -156,7 +157,7 @@ guide that matches what you are building. The full index lives in
|
|
|
156
157
|
| [Sessions](./docs/sessions.md) | Sessions over a store, cookie scope, secrets at rest, `dataRefresh`, the controller API |
|
|
157
158
|
| [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
|
|
158
159
|
| [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 |
|
|
160
|
+
| [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
161
|
| [The API core](./docs/api-core.md) | `LambderApiPipeline`: the one pipeline the server and the mock runtime run, the store interfaces, the transports |
|
|
161
162
|
| [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression, transports |
|
|
162
163
|
| [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
|
|
@@ -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 {};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { canonicalJson } from "../shared/util/canonicalJson.js";
|
|
2
|
+
import { moduleUrlOf } from "./moduleLocation.js";
|
|
3
|
+
import { writeFileAtomically } from "./writeFileAtomically.js";
|
|
4
|
+
/** Imports the module, finds the instance under its export, and answers what it reports. `generator` names the caller in the errors. */
|
|
5
|
+
export const loadApiOptionEntries = async (location, generator) => {
|
|
6
|
+
const moduleUrl = moduleUrlOf(location.module);
|
|
7
|
+
const exportName = location.exportName ?? "default";
|
|
8
|
+
let namespace;
|
|
9
|
+
try {
|
|
10
|
+
namespace = await import(moduleUrl);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
throw new Error(`${generator} could not load ${moduleUrl}`, { cause: err });
|
|
14
|
+
}
|
|
15
|
+
const source = namespace[exportName];
|
|
16
|
+
if (typeof source?.apiOptionEntries !== "function") {
|
|
17
|
+
throw new Error(`${moduleUrl} has no export "${exportName}" that reports API options: name the export holding the instance in exportName`);
|
|
18
|
+
}
|
|
19
|
+
return source.apiOptionEntries();
|
|
20
|
+
};
|
|
21
|
+
/** One table of a generated file, read back as data, or null for a file that does not hold it as written. */
|
|
22
|
+
export const readGeneratedTable = (contents, exportName) => {
|
|
23
|
+
const match = new RegExp(`export const ${exportName}\\s*=\\s*([\\s\\S]*?)\\s+as const\\b`).exec(contents);
|
|
24
|
+
if (!match)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const table = JSON.parse(match[1]);
|
|
28
|
+
return table !== null && typeof table === "object" && !Array.isArray(table) ? table : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** 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. */
|
|
35
|
+
export const nameChangesOf = (current, previous) => {
|
|
36
|
+
const same = (name) => canonicalJson(current[name]) === canonicalJson(previous[name]);
|
|
37
|
+
return {
|
|
38
|
+
changed: Object.keys(current).filter((name) => name in previous && !same(name)).sort(),
|
|
39
|
+
added: Object.keys(current).filter((name) => !(name in previous)).sort(),
|
|
40
|
+
removed: Object.keys(previous).filter((name) => !(name in current)).sort(),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/** One line per name that moved, marked `~` changed, `+` added or `-` removed, under the table's name. */
|
|
44
|
+
const movedLinesOf = (tableName, changes) => [
|
|
45
|
+
...changes.changed.map((name) => ` ~ ${tableName} ${name}`),
|
|
46
|
+
...changes.added.map((name) => ` + ${tableName} ${name}`),
|
|
47
|
+
...changes.removed.map((name) => ` - ${tableName} ${name}`),
|
|
48
|
+
];
|
|
49
|
+
/** The opening comment of a generated file, one `//` line per line of the header. */
|
|
50
|
+
export const headerLinesOf = (header) => header.split("\n").map((line) => line ? `// ${line}` : "//");
|
|
51
|
+
/**
|
|
52
|
+
* One table as a statement: its doc comment, the `// prettier-ignore` that
|
|
53
|
+
* keeps a formatter off the JSON a check reads back, and the table itself,
|
|
54
|
+
* `as const` with whatever `satisfies` clause the caller gives.
|
|
55
|
+
*/
|
|
56
|
+
export const tableStatementLines = (table) => [
|
|
57
|
+
`/** ${table.doc} */`,
|
|
58
|
+
"// prettier-ignore",
|
|
59
|
+
`export const ${table.name} = ${JSON.stringify(table.value, null, 4)} as const${table.satisfies ? ` satisfies ${table.satisfies}` : ""}${table.semicolon}`,
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* The end of a generator's run. With `check`, it answers whether the file
|
|
63
|
+
* holds what the instance reports and writes nothing. Otherwise it writes
|
|
64
|
+
* the file unless it already holds these tables, however it is formatted, so
|
|
65
|
+
* a watcher or an incremental build sees no change where there is none (a
|
|
66
|
+
* change of header or semicolons shows the next time a table changes).
|
|
67
|
+
* Either way the lines say what moved.
|
|
68
|
+
*/
|
|
69
|
+
export const settleGeneratedFile = (run) => {
|
|
70
|
+
const movedLines = run.tables.flatMap((table) => movedLinesOf(table.name, table.changes));
|
|
71
|
+
const summary = run.tables.map(({ name, count, changes }) => `${name}: ${changes.changed.length} changed, ${changes.added.length} added, ${changes.removed.length} removed (${count - changes.changed.length - changes.added.length} unchanged)`).join("; ");
|
|
72
|
+
const unchanged = run.readBack && movedLines.length === 0;
|
|
73
|
+
if (run.check) {
|
|
74
|
+
if (unchanged)
|
|
75
|
+
return { ok: true, written: false, lines: [`✓ ${run.name} matches the ${run.held}`] };
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
written: false,
|
|
79
|
+
lines: [
|
|
80
|
+
run.previousText !== null && !run.readBack
|
|
81
|
+
? `✗ ${run.name} does not hold ${run.tablesNoun} as written: regenerate it`
|
|
82
|
+
: `✗ ${run.name} is stale: regenerate it`,
|
|
83
|
+
` ${summary}`,
|
|
84
|
+
...movedLines,
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const written = !unchanged;
|
|
89
|
+
if (written)
|
|
90
|
+
writeFileAtomically(run.file, run.render(), run.previousText !== null);
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
written,
|
|
94
|
+
lines: [
|
|
95
|
+
written ? `✓ Wrote ${run.name} (${run.held})` : `✓ ${run.name} is up to date (${run.held})`,
|
|
96
|
+
...(movedLines.length ? [` ${summary}`, ...movedLines] : [` ${run.unmovedLine}`]),
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { LambderModuleLocation } from "./moduleLocation.js";
|
|
2
|
+
import { type LambderNameChanges } from "./generatedTables.js";
|
|
3
|
+
export type LambderApiGuardParamsFileOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* The module that exports the instance, usually the server's entry: a
|
|
6
|
+
* path relative to the working directory, or a file URL. It is imported
|
|
7
|
+
* in this process, so a TypeScript module needs the process's loader
|
|
8
|
+
* (`tsx`, `node --import tsx`), as the generator script itself does.
|
|
9
|
+
*/
|
|
10
|
+
module: LambderModuleLocation;
|
|
11
|
+
/** The export that holds the instance. Default: "default", the module's default export. */
|
|
12
|
+
exportName?: string;
|
|
13
|
+
/** The guard whose parameters the file holds, as the server declares it. A name the server declares no guard under throws. */
|
|
14
|
+
guard: string;
|
|
15
|
+
/** The TypeScript module to write, exporting `guardParams`. Relative to the working directory. */
|
|
16
|
+
file: string;
|
|
17
|
+
/** Write nothing, and answer whether the file on disk holds what the instance reports now. Default: false. */
|
|
18
|
+
check?: boolean;
|
|
19
|
+
/** The comment the file opens with, one `//` line per line. It should name what generates the file. Default: a note naming writeApiGuardParams() and the guard. */
|
|
20
|
+
header?: string;
|
|
21
|
+
/** End the generated statement with a semicolon. Default: true. The table itself is JSON, double quotes included, whatever the project's style: that is what lets a check read it back. */
|
|
22
|
+
semicolons?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type LambderApiGuardParamsFileResult = {
|
|
25
|
+
/** False when a check found the file stale or unreadable. */
|
|
26
|
+
ok: boolean;
|
|
27
|
+
/** The absolute path. */
|
|
28
|
+
file: string;
|
|
29
|
+
/** True when the file was (re)written; a file that already holds this table is left untouched, however it is formatted. */
|
|
30
|
+
written: boolean;
|
|
31
|
+
/** How many APIs declare the guard. */
|
|
32
|
+
count: number;
|
|
33
|
+
/** Which APIs moved against the file that was on disk. */
|
|
34
|
+
changes: LambderNameChanges;
|
|
35
|
+
/** What happened, as lines to print: a summary, then one line per name that moved. */
|
|
36
|
+
lines: string[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Writes one guard's parameters as a module of plain data, or checks the one
|
|
40
|
+
* on disk, and says which APIs moved.
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { writeApiGuardParams } from "lambder/build";
|
|
44
|
+
*
|
|
45
|
+
* const result = await writeApiGuardParams({
|
|
46
|
+
* module: "server/src/index.ts",
|
|
47
|
+
* exportName: "lambder",
|
|
48
|
+
* guard: "store",
|
|
49
|
+
* file: "web/src/generated/storeGuardParams.generated.ts",
|
|
50
|
+
* check: process.argv.includes("--check"),
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* The module exports `guardParams`, `as const`, so a client reads its types
|
|
55
|
+
* straight off it: `keyof typeof guardParams` is the APIs behind the guard,
|
|
56
|
+
* and `(typeof guardParams)[K]` the literal API K declared, `true` for a
|
|
57
|
+
* guard named without a parameter (the string and list forms). A parameter
|
|
58
|
+
* that is not plain data fails the write, as it does for writeApiOptions.
|
|
59
|
+
*/
|
|
60
|
+
export declare const writeApiGuardParams: (options: LambderApiGuardParamsFileOptions) => Promise<LambderApiGuardParamsFileResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { apiGuardParam } from "../shared/wire/LambderApiOptionEntries.js";
|
|
4
|
+
import { headerLinesOf, loadApiOptionEntries, nameChangesOf, readGeneratedTable, settleGeneratedFile, tableStatementLines, } from "./generatedTables.js";
|
|
5
|
+
/*
|
|
6
|
+
* One guard's parameters as a generated module a browser may carry: for each
|
|
7
|
+
* API that declares the guard, the parameter it gives it, and nothing else
|
|
8
|
+
* about any API.
|
|
9
|
+
*
|
|
10
|
+
* The file writeApiOptions writes holds every declaration, because a mock
|
|
11
|
+
* and a test need every declaration. A screen that decides whether to offer
|
|
12
|
+
* a control by the permission an endpoint's guard asks needs one guard's
|
|
13
|
+
* parameter, and a browser that imports the whole table as a value carries
|
|
14
|
+
* every API name, every mode and every guard's parameter, a reviewer's
|
|
15
|
+
* reason beside an open endpoint included. This is the least that screen
|
|
16
|
+
* needs: the names of the APIs behind the guard (which the client calls, so
|
|
17
|
+
* its own code names them already) and what each asks.
|
|
18
|
+
*/
|
|
19
|
+
const TABLE_NAME = "guardParams";
|
|
20
|
+
const defaultHeader = (guard) => [
|
|
21
|
+
"Generated by writeApiGuardParams() from lambder/build. Do not edit.",
|
|
22
|
+
"",
|
|
23
|
+
`The parameter each API the server guards with "${guard}" gives that guard,`,
|
|
24
|
+
"and nothing else about any API: what a client decides with it, without",
|
|
25
|
+
"carrying the server's other declarations.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
/**
|
|
28
|
+
* Writes one guard's parameters as a module of plain data, or checks the one
|
|
29
|
+
* on disk, and says which APIs moved.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { writeApiGuardParams } from "lambder/build";
|
|
33
|
+
*
|
|
34
|
+
* const result = await writeApiGuardParams({
|
|
35
|
+
* module: "server/src/index.ts",
|
|
36
|
+
* exportName: "lambder",
|
|
37
|
+
* guard: "store",
|
|
38
|
+
* file: "web/src/generated/storeGuardParams.generated.ts",
|
|
39
|
+
* check: process.argv.includes("--check"),
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* The module exports `guardParams`, `as const`, so a client reads its types
|
|
44
|
+
* straight off it: `keyof typeof guardParams` is the APIs behind the guard,
|
|
45
|
+
* and `(typeof guardParams)[K]` the literal API K declared, `true` for a
|
|
46
|
+
* guard named without a parameter (the string and list forms). A parameter
|
|
47
|
+
* that is not plain data fails the write, as it does for writeApiOptions.
|
|
48
|
+
*/
|
|
49
|
+
export const writeApiGuardParams = async (options) => {
|
|
50
|
+
const file = resolve(options.file);
|
|
51
|
+
const entries = await loadApiOptionEntries(options, "writeApiGuardParams");
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(entries.guards, options.guard)) {
|
|
53
|
+
const declared = Object.keys(entries.guards);
|
|
54
|
+
throw new Error(`writeApiGuardParams: the server declares no guard "${options.guard}" (it declares ${declared.length ? declared.map((name) => `"${name}"`).join(", ") : "none"}).`);
|
|
55
|
+
}
|
|
56
|
+
const params = {};
|
|
57
|
+
for (const name of Object.keys(entries.apis).sort()) {
|
|
58
|
+
const param = apiGuardParam(entries.apis, name, options.guard);
|
|
59
|
+
if (param !== undefined)
|
|
60
|
+
params[name] = param;
|
|
61
|
+
}
|
|
62
|
+
const previousText = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
63
|
+
const previous = previousText === null ? null : readGeneratedTable(previousText, TABLE_NAME);
|
|
64
|
+
const changes = nameChangesOf(params, previous ?? {});
|
|
65
|
+
const count = Object.keys(params).length;
|
|
66
|
+
const settled = settleGeneratedFile({
|
|
67
|
+
name: options.file, file, check: options.check, previousText, readBack: previous !== null,
|
|
68
|
+
tables: [{ name: TABLE_NAME, count, changes }],
|
|
69
|
+
held: `${count} APIs guarded by "${options.guard}"`,
|
|
70
|
+
tablesNoun: "the table",
|
|
71
|
+
unmovedLine: "no parameters changed",
|
|
72
|
+
render: () => [
|
|
73
|
+
...headerLinesOf(options.header ?? defaultHeader(options.guard)),
|
|
74
|
+
"",
|
|
75
|
+
...tableStatementLines({
|
|
76
|
+
name: TABLE_NAME,
|
|
77
|
+
doc: `The parameter each API guarded by "${options.guard}" gives it: the value as declared, or true for the guard named without one.`,
|
|
78
|
+
value: params,
|
|
79
|
+
semicolon: options.semicolons === false ? "" : ";",
|
|
80
|
+
}),
|
|
81
|
+
"",
|
|
82
|
+
].join("\n"),
|
|
83
|
+
});
|
|
84
|
+
return { ...settled, file, count, changes };
|
|
85
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { LambderApiOptionEntries } from "../shared/wire/LambderApiOptionEntries.js";
|
|
2
|
+
import type { LambderModuleLocation } from "./moduleLocation.js";
|
|
3
|
+
import { type LambderNameChanges } from "./generatedTables.js";
|
|
4
|
+
type TableKey = keyof LambderApiOptionEntries;
|
|
5
|
+
export type LambderApiOptionsFileOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* The module that exports the instance, usually the server's entry: a
|
|
8
|
+
* path relative to the working directory, or a file URL. It is imported
|
|
9
|
+
* in this process, so a TypeScript module needs the process's loader
|
|
10
|
+
* (`tsx`, `node --import tsx`), as the generator script itself does.
|
|
11
|
+
*/
|
|
12
|
+
module: LambderModuleLocation;
|
|
13
|
+
/** The export that holds the instance. Default: "default", the module's default export. */
|
|
14
|
+
exportName?: string;
|
|
15
|
+
/** The TypeScript module to write, exporting `apiOptions`, `rateLimitPolicies` and `guardDeclarations`. Relative to the working directory. */
|
|
16
|
+
file: string;
|
|
17
|
+
/** Write nothing, and answer whether the file on disk holds what the instance reports now. Default: false. */
|
|
18
|
+
check?: boolean;
|
|
19
|
+
/** The comment the file opens with, one `//` line per line. It should name what generates the file. Default: a note naming writeApiOptions(). */
|
|
20
|
+
header?: string;
|
|
21
|
+
/** End the generated statements with semicolons. Default: true. The tables themselves are JSON, double quotes included, whatever the project's style: that is what lets a check read them back. */
|
|
22
|
+
semicolons?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type LambderApiOptionsFileResult = {
|
|
25
|
+
/** False when a check found the file stale or unreadable. */
|
|
26
|
+
ok: boolean;
|
|
27
|
+
/** The absolute path. */
|
|
28
|
+
file: string;
|
|
29
|
+
/** True when the file was (re)written; a file that already holds these tables is left untouched, however it is formatted. */
|
|
30
|
+
written: boolean;
|
|
31
|
+
/** How many APIs, policies and guards the tables hold. */
|
|
32
|
+
counts: Record<TableKey, number>;
|
|
33
|
+
/** What moved, per table, against the file that was on disk. */
|
|
34
|
+
changes: Record<TableKey, LambderNameChanges>;
|
|
35
|
+
/** What happened, as lines to print: a summary, then one line per name that moved. */
|
|
36
|
+
lines: string[];
|
|
37
|
+
};
|
|
38
|
+
/** The tables a generated file holds, read back as data, or null for a file that does not hold all three as written (never written, or rewritten by hand). */
|
|
39
|
+
export declare const readOptionTables: (contents: string) => LambderApiOptionEntries | null;
|
|
40
|
+
/**
|
|
41
|
+
* Writes the declared options of an app's APIs as a module of plain data, or
|
|
42
|
+
* checks the one on disk, and says which APIs, policies and guards moved.
|
|
43
|
+
*
|
|
44
|
+
* Call it from a generator script beside writeApiSignatures, naming the
|
|
45
|
+
* module that exports the app's instance:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { writeApiOptions } from "lambder/build";
|
|
49
|
+
*
|
|
50
|
+
* const result = await writeApiOptions({
|
|
51
|
+
* module: "server/src/index.ts", // export const lambder = initLambder()...
|
|
52
|
+
* exportName: "lambder",
|
|
53
|
+
* file: "shared/generated/apiOptions.generated.ts",
|
|
54
|
+
* check: process.argv.includes("--check"),
|
|
55
|
+
* });
|
|
56
|
+
* console.log(result.lines.join("\n"));
|
|
57
|
+
* process.exit(result.ok ? 0 : 1);
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* The tables are compared as the data the file holds, so a checkout that
|
|
61
|
+
* rewrote its line endings or a formatter that re-indented it is neither
|
|
62
|
+
* stale nor rewritten, and a file whose data is current is left as it is.
|
|
63
|
+
* There is no fresh-process check: nothing here is digested, so nothing can
|
|
64
|
+
* differ per process. A module that does not load, an export that is not an
|
|
65
|
+
* instance, or a guard parameter that is not plain data throws.
|
|
66
|
+
*/
|
|
67
|
+
export declare const writeApiOptions: (options: LambderApiOptionsFileOptions) => Promise<LambderApiOptionsFileResult>;
|
|
68
|
+
export {};
|