lambder 8.1.1 → 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.
Files changed (73) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/README.md +8 -7
  3. package/dist/api/LambderApiGuards.d.ts +2 -17
  4. package/dist/api/LambderApiRateLimits.d.ts +2 -29
  5. package/dist/build/ContractTypePrinter.d.ts +39 -9
  6. package/dist/build/ContractTypePrinter.js +89 -31
  7. package/dist/build/generatedTables.d.ts +72 -0
  8. package/dist/build/generatedTables.js +99 -0
  9. package/dist/build/writeApiContract.d.ts +2 -1
  10. package/dist/build/writeApiContract.js +2 -1
  11. package/dist/build/writeApiGuardParams.d.ts +60 -0
  12. package/dist/build/writeApiGuardParams.js +85 -0
  13. package/dist/build/writeApiOptions.d.ts +68 -0
  14. package/dist/build/writeApiOptions.js +102 -0
  15. package/dist/build.d.ts +10 -4
  16. package/dist/build.js +7 -4
  17. package/dist/client/LambderUploadRunner.d.ts +7 -7
  18. package/dist/client/LambderUploadRunner.js +23 -32
  19. package/dist/client.d.ts +7 -0
  20. package/dist/client.js +11 -0
  21. package/dist/core/Lambder.d.ts +21 -0
  22. package/dist/core/Lambder.js +69 -0
  23. package/dist/index.d.ts +13 -0
  24. package/dist/index.js +13 -0
  25. package/dist/mock/LambderMockApp.d.ts +34 -17
  26. package/dist/mock/LambderMockApp.js +67 -21
  27. package/dist/mock/LambderMockCreateOptions.d.ts +68 -5
  28. package/dist/mock/LambderMockTypes.d.ts +29 -10
  29. package/dist/mock/lambderMockPoliciesFrom.d.ts +51 -0
  30. package/dist/mock/lambderMockPoliciesFrom.js +46 -0
  31. package/dist/mock.d.ts +3 -0
  32. package/dist/mock.js +3 -0
  33. package/dist/secrets/LambderOneShotSecrets.d.ts +166 -0
  34. package/dist/secrets/LambderOneShotSecrets.js +217 -0
  35. package/dist/session/LambderSessionCrypto.js +6 -16
  36. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +3 -2
  37. package/dist/shared/contracts/LambderOneShotSecretStore.d.ts +122 -0
  38. package/dist/shared/contracts/LambderOneShotSecretStore.js +38 -0
  39. package/dist/shared/util/LambderBackoffTimer.d.ts +82 -0
  40. package/dist/shared/util/LambderBackoffTimer.js +86 -0
  41. package/dist/shared/util/LambderBase64.d.ts +14 -0
  42. package/dist/shared/util/LambderBase64.js +17 -0
  43. package/dist/shared/util/LambderSignedClaims.d.ts +78 -0
  44. package/dist/shared/util/LambderSignedClaims.js +109 -0
  45. package/dist/shared/util/LambderTextDigest.d.ts +19 -5
  46. package/dist/shared/util/LambderTextDigest.js +30 -5
  47. package/dist/shared/util/assertPlainData.d.ts +9 -0
  48. package/dist/shared/util/assertPlainData.js +41 -0
  49. package/dist/shared/util/escapeXmlText.d.ts +8 -0
  50. package/dist/shared/util/escapeXmlText.js +8 -0
  51. package/dist/shared/wire/LambderApiOptionEntries.d.ts +148 -0
  52. package/dist/shared/wire/LambderApiOptionEntries.js +35 -0
  53. package/dist/shared/wire/LambderUploadObjectFields.js +2 -2
  54. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +64 -0
  55. package/dist/stores/LambderDdbOneShotSecretStore.js +266 -0
  56. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +3 -2
  57. package/dist/stores/LambderMemoryIdempotencyStore.js +3 -2
  58. package/dist/stores/LambderMemoryOneShotSecretStore.d.ts +36 -0
  59. package/dist/stores/LambderMemoryOneShotSecretStore.js +93 -0
  60. package/dist/stores/LambderMemoryUploadBucket.js +2 -2
  61. package/dist/testing/LambderConformanceRunner.d.ts +46 -0
  62. package/dist/testing/LambderConformanceRunner.js +21 -0
  63. package/dist/testing/lambderIdempotencyStoreConformance.d.ts +33 -0
  64. package/dist/testing/lambderIdempotencyStoreConformance.js +237 -0
  65. package/dist/testing/lambderOneShotSecretStoreConformance.d.ts +43 -0
  66. package/dist/testing/lambderOneShotSecretStoreConformance.js +224 -0
  67. package/dist/testing/lambderRateLimiterConformance.d.ts +20 -0
  68. package/dist/testing/lambderRateLimiterConformance.js +72 -0
  69. package/dist/testing/lambderSessionStoreConformance.d.ts +27 -0
  70. package/dist/testing/lambderSessionStoreConformance.js +165 -0
  71. package/dist/testing.d.ts +14 -0
  72. package/dist/testing.js +12 -0
  73. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,170 @@ 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
+
148
+ ## [8.1.2] - 2026-09-26
149
+
150
+ ### Fixed
151
+
152
+ - **`writeApiContract` names no longer follow the order APIs are registered
153
+ in.** Two types that want one name (an interface `Row` in two modules) were
154
+ told apart by a number given in the order the printer met them, so
155
+ reordering registrations, or adding an API that reaches one of them first,
156
+ swapped `Row` and `Row2` and reported every API using either as changed.
157
+ The printer now finds every declaration first and numbers them by where
158
+ each is declared, the one declared first (by file, then position) keeping
159
+ the name, and a number never takes the name another type is declared
160
+ under. A file already written may be renamed once, the first time it is
161
+ written with this version.
162
+ - **`LambderUploadRunner` randomises its first wait too.** The ceiling of the
163
+ first wait before trying storage again was the base itself, so browsers
164
+ dropped together all came back exactly `baseDelayMs` later. Each wait is
165
+ now between `baseDelayMs` and twice it, doubling with every failed attempt
166
+ up to `maxDelayMs`.
167
+ - **`LambderUploadRunner` renews a ticket on `ExpiredToken`,** which S3
168
+ answers when the temporary credentials that signed the ticket ran out
169
+ before the ticket did. It is renewed like an expired ticket, at once and
170
+ spending no attempt; before, the upload failed as `storageRejected`.
171
+ - **`LambderUploadRunner` no longer holds the file's bytes through the
172
+ upload.** The buffer it hashes, as large as the file, was kept in a local
173
+ for the whole post and every retry; it is now read straight into the
174
+ digest.
175
+
12
176
  ## [8.1.1] - 2026-09-26
13
177
 
14
178
  Two additions, and three breaking changes a minor line carries here on
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/stores/`
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 five store and source
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
- * When a guard runs, relative to the API's input validation.
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
- * What one budget spans:
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 {};
@@ -22,18 +22,40 @@ export declare class ContractTypePrinter {
22
22
  private readonly program;
23
23
  private readonly checker;
24
24
  private readonly style;
25
- private readonly failures;
26
- /** Every named declaration by the type it stands for; its text is null while it is being printed. */
27
- private readonly declarations;
25
+ private failures;
26
+ /** Every declaration by the type it stands for. */
27
+ private declarations;
28
+ /** The name each declaration is printed under, settled from all of them; empty on the pass that finds them. */
29
+ private settledNames;
28
30
  /** Names a declaration may not take: the default library's, and the contract's own. */
29
- private readonly takenNames;
31
+ private readonly reservedNames;
30
32
  /** The anonymous types being printed: meeting one again inside itself is recursion, and it needs a name. */
31
33
  private readonly inProgress;
32
34
  /** Under exactOptionalPropertyTypes an optional member's type carries the compiler's own "missing" undefined, which its source never wrote. */
33
35
  private readonly exactOptionalProperties;
34
36
  constructor(ts: typeof import("typescript"), program: ts.Program, checker: ts.TypeChecker, style: ContractPrintStyle, contractName: string);
35
- /** Prints each member of the contract type, sorted by name, and every declaration they refer to. */
37
+ /**
38
+ * Prints each member of the contract type, sorted by name, and every
39
+ * declaration they refer to.
40
+ *
41
+ * In two passes: the first finds every type that needs a declaration,
42
+ * and the second prints with their names settled from all of them. Named
43
+ * as the printer met them, two types wanting one name would trade it
44
+ * whenever the APIs were registered in another order, or a union's
45
+ * members created in another, and the file would move with no API
46
+ * changed.
47
+ */
36
48
  printContract(contract: ts.Type): PrintedContract;
49
+ /**
50
+ * A name for every declaration the first pass found. Of the types that
51
+ * want one name, the one declared first (by file, then position) keeps
52
+ * it, and the others take the lowest free number after it once every
53
+ * type has claimed its own name, so a number never takes the name
54
+ * another type is declared under. Types declared at one place (two
55
+ * instantiations of a generic) keep the order the entries reached them
56
+ * in, by entry name.
57
+ */
58
+ private settleNames;
37
59
  /**
38
60
  * `label value`, with a union too long for one line starting on the next
39
61
  * line, one member per line: what a property, an index signature and a
@@ -43,16 +65,25 @@ export declare class ContractTypePrinter {
43
65
  private print;
44
66
  /** A union, an intersection or an object: printed in place, or as a reference to a declaration of its own. */
45
67
  private printComposite;
46
- /** The name a type is declared under, when it is one to print as a declaration: non-generic, and not the default library's. */
47
- private declaredNameOf;
68
+ /** A type's declaration, under the name it is settled to once the first pass has settled them. */
69
+ private declare;
70
+ /** The name a type is declared under and where, when it is one to print as a declaration: non-generic, and not the default library's. */
71
+ private ownDeclarationOf;
72
+ /** A symbol's name to declare a type under, and where the symbol is declared. */
73
+ private declaredAs;
74
+ /** Where a symbol is declared, as text that orders by file and then position; empty for one declared nowhere. */
75
+ private originOf;
48
76
  /** The name a symbol is declared under, read off its declaration, so `export default interface Customer` is Customer; undefined when it has none to print. */
49
77
  private nameOf;
50
78
  /**
51
79
  * A name for a type that refers to itself and has none of its own: what it
52
80
  * instantiates followed by its arguments (a JSON mapping of a Tree is
53
81
  * `JsonOfTree`, a `Tree<string>` is `TreeString`), or `RecursiveType`.
82
+ * Its origin is where what it instantiates is declared, then where each
83
+ * argument is, so two instantiations named alike are told apart by their
84
+ * arguments.
54
85
  */
55
- private recursiveNameOf;
86
+ private recursiveDeclarationOf;
56
87
  private printStructure;
57
88
  private printUnion;
58
89
  private printUnionOf;
@@ -80,6 +111,5 @@ export declare class ContractTypePrinter {
80
111
  private wrapped;
81
112
  keyOf(name: string): string;
82
113
  private quoted;
83
- private takeName;
84
114
  private fail;
85
115
  }
@@ -37,10 +37,12 @@ export class ContractTypePrinter {
37
37
  checker;
38
38
  style;
39
39
  failures = [];
40
- /** Every named declaration by the type it stands for; its text is null while it is being printed. */
40
+ /** Every declaration by the type it stands for. */
41
41
  declarations = new Map();
42
+ /** The name each declaration is printed under, settled from all of them; empty on the pass that finds them. */
43
+ settledNames = new Map();
42
44
  /** Names a declaration may not take: the default library's, and the contract's own. */
43
- takenNames = new Set();
45
+ reservedNames = new Set();
44
46
  /** The anonymous types being printed: meeting one again inside itself is recursion, and it needs a name. */
45
47
  inProgress = new Set();
46
48
  /** Under exactOptionalPropertyTypes an optional member's type carries the compiler's own "missing" undefined, which its source never wrote. */
@@ -50,7 +52,7 @@ export class ContractTypePrinter {
50
52
  this.program = program;
51
53
  this.checker = checker;
52
54
  this.style = style;
53
- this.takenNames.add(contractName);
55
+ this.reservedNames.add(contractName);
54
56
  this.exactOptionalProperties = !!program.getCompilerOptions().exactOptionalPropertyTypes;
55
57
  // A declaration named after a global the printed text refers to by
56
58
  // name (Date) would shadow it.
@@ -60,20 +62,65 @@ export class ContractTypePrinter {
60
62
  for (const statement of sourceFile.statements) {
61
63
  if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isModuleDeclaration(statement))
62
64
  && statement.name && ts.isIdentifier(statement.name))
63
- this.takenNames.add(statement.name.text);
65
+ this.reservedNames.add(statement.name.text);
64
66
  }
65
67
  }
66
68
  }
67
- /** Prints each member of the contract type, sorted by name, and every declaration they refer to. */
69
+ /**
70
+ * Prints each member of the contract type, sorted by name, and every
71
+ * declaration they refer to.
72
+ *
73
+ * In two passes: the first finds every type that needs a declaration,
74
+ * and the second prints with their names settled from all of them. Named
75
+ * as the printer met them, two types wanting one name would trade it
76
+ * whenever the APIs were registered in another order, or a union's
77
+ * members created in another, and the file would move with no API
78
+ * changed.
79
+ */
68
80
  printContract(contract) {
69
- const entries = this.checker.getPropertiesOfType(contract)
70
- .map((entry) => ({ name: entry.name, text: this.print(this.checker.getTypeOfSymbol(entry), this.keyOf(entry.name)).text }))
71
- .sort((a, b) => byCodeUnits(a.name, b.name));
81
+ const members = [...this.checker.getPropertiesOfType(contract)].sort((a, b) => byCodeUnits(a.name, b.name));
82
+ const printEntries = () => members.map((entry) => ({ name: entry.name, text: this.print(this.checker.getTypeOfSymbol(entry), this.keyOf(entry.name)).text }));
83
+ printEntries();
84
+ this.settledNames = this.settleNames();
85
+ this.failures = [];
86
+ this.declarations = new Map();
87
+ const entries = printEntries();
72
88
  const declarations = [...this.declarations.values()]
73
89
  .map(({ name, text }) => ({ name, text: text ?? "never" }))
74
90
  .sort((a, b) => byCodeUnits(a.name, b.name));
75
91
  return { entries, declarations, failures: this.failures };
76
92
  }
93
+ /**
94
+ * A name for every declaration the first pass found. Of the types that
95
+ * want one name, the one declared first (by file, then position) keeps
96
+ * it, and the others take the lowest free number after it once every
97
+ * type has claimed its own name, so a number never takes the name
98
+ * another type is declared under. Types declared at one place (two
99
+ * instantiations of a generic) keep the order the entries reached them
100
+ * in, by entry name.
101
+ */
102
+ settleNames() {
103
+ const wanting = [...this.declarations].sort(([, a], [, b]) => byCodeUnits(a.base, b.base) || byCodeUnits(a.origin, b.origin));
104
+ const taken = new Set(this.reservedNames);
105
+ const settled = new Map();
106
+ const numbered = [];
107
+ for (const [type, { base }] of wanting) {
108
+ if (taken.has(base)) {
109
+ numbered.push([type, base]);
110
+ continue;
111
+ }
112
+ taken.add(base);
113
+ settled.set(type, base);
114
+ }
115
+ for (const [type, base] of numbered) {
116
+ let suffix = 2;
117
+ while (taken.has(`${base}${suffix}`))
118
+ suffix++;
119
+ taken.add(`${base}${suffix}`);
120
+ settled.set(type, `${base}${suffix}`);
121
+ }
122
+ return settled;
123
+ }
77
124
  /**
78
125
  * `label value`, with a union too long for one line starting on the next
79
126
  * line, one member per line: what a property, an index signature and a
@@ -142,20 +189,16 @@ export class ContractTypePrinter {
142
189
  const known = this.declarations.get(type);
143
190
  if (known)
144
191
  return atom(known.name);
145
- const ownName = this.declaredNameOf(type);
146
- if (ownName !== undefined) {
192
+ const own = this.ownDeclarationOf(type);
193
+ if (own) {
147
194
  // Registered before the body is printed, so a reference to itself
148
195
  // inside the body finds the name.
149
- const declaration = { name: this.takeName(ownName), text: null };
150
- this.declarations.set(type, declaration);
196
+ const declaration = this.declare(type, own);
151
197
  declaration.text = this.printStructure(type, path).text;
152
198
  return atom(declaration.name);
153
199
  }
154
- if (this.inProgress.has(type)) {
155
- const declaration = { name: this.takeName(this.recursiveNameOf(type)), text: null };
156
- this.declarations.set(type, declaration);
157
- return atom(declaration.name);
158
- }
200
+ if (this.inProgress.has(type))
201
+ return atom(this.declare(type, this.recursiveDeclarationOf(type)).name);
159
202
  this.inProgress.add(type);
160
203
  const printed = this.printStructure(type, path);
161
204
  this.inProgress.delete(type);
@@ -166,11 +209,17 @@ export class ContractTypePrinter {
166
209
  recursive.text = printed.text;
167
210
  return atom(recursive.name);
168
211
  }
169
- /** The name a type is declared under, when it is one to print as a declaration: non-generic, and not the default library's. */
170
- declaredNameOf(type) {
212
+ /** A type's declaration, under the name it is settled to once the first pass has settled them. */
213
+ declare(type, { base, origin }) {
214
+ const declaration = { base, origin, name: this.settledNames.get(type) ?? base, text: null };
215
+ this.declarations.set(type, declaration);
216
+ return declaration;
217
+ }
218
+ /** The name a type is declared under and where, when it is one to print as a declaration: non-generic, and not the default library's. */
219
+ ownDeclarationOf(type) {
171
220
  const { ObjectFlags, TypeFlags } = this.ts;
172
221
  if (type.aliasSymbol) {
173
- return type.aliasTypeArguments?.length || this.isDefaultLibrary(type.aliasSymbol) ? undefined : this.nameOf(type.aliasSymbol);
222
+ return type.aliasTypeArguments?.length || this.isDefaultLibrary(type.aliasSymbol) ? undefined : this.declaredAs(type.aliasSymbol);
174
223
  }
175
224
  if (!(type.flags & TypeFlags.Object))
176
225
  return undefined;
@@ -182,7 +231,17 @@ export class ContractTypePrinter {
182
231
  // it instead, and are printed in place.
183
232
  if (objectFlags & ObjectFlags.Reference && (type.target !== type || type.typeParameters?.length))
184
233
  return undefined;
185
- return this.isDefaultLibrary(type.symbol) ? undefined : this.nameOf(type.symbol);
234
+ return this.isDefaultLibrary(type.symbol) ? undefined : this.declaredAs(type.symbol);
235
+ }
236
+ /** A symbol's name to declare a type under, and where the symbol is declared. */
237
+ declaredAs(symbol) {
238
+ const base = this.nameOf(symbol);
239
+ return base === undefined ? undefined : { base, origin: this.originOf(symbol) };
240
+ }
241
+ /** Where a symbol is declared, as text that orders by file and then position; empty for one declared nowhere. */
242
+ originOf(symbol) {
243
+ const declaration = symbol?.declarations?.[0];
244
+ return declaration ? `${declaration.getSourceFile().fileName}\0${String(declaration.pos).padStart(10, "0")}` : "";
186
245
  }
187
246
  /** The name a symbol is declared under, read off its declaration, so `export default interface Customer` is Customer; undefined when it has none to print. */
188
247
  nameOf(symbol) {
@@ -195,8 +254,11 @@ export class ContractTypePrinter {
195
254
  * A name for a type that refers to itself and has none of its own: what it
196
255
  * instantiates followed by its arguments (a JSON mapping of a Tree is
197
256
  * `JsonOfTree`, a `Tree<string>` is `TreeString`), or `RecursiveType`.
257
+ * Its origin is where what it instantiates is declared, then where each
258
+ * argument is, so two instantiations named alike are told apart by their
259
+ * arguments.
198
260
  */
199
- recursiveNameOf(type) {
261
+ recursiveDeclarationOf(type) {
200
262
  const { ObjectFlags, TypeFlags } = this.ts;
201
263
  let instantiated;
202
264
  if (type.aliasSymbol) {
@@ -209,11 +271,14 @@ export class ContractTypePrinter {
209
271
  }
210
272
  const base = instantiated && this.nameOf(instantiated.symbol);
211
273
  if (!instantiated || !base)
212
- return "RecursiveType";
274
+ return { base: "RecursiveType", origin: this.originOf(type.symbol) };
213
275
  const argumentNames = instantiated.typeArguments
214
276
  .map((argument) => (argument.aliasSymbol && this.nameOf(argument.aliasSymbol)) ?? (argument.symbol && this.nameOf(argument.symbol)) ?? this.checker.typeToString(argument))
215
277
  .filter((name) => IDENTIFIER.test(name));
216
- return `${base}${argumentNames.map((name) => name[0].toUpperCase() + name.slice(1)).join("")}`;
278
+ return {
279
+ base: `${base}${argumentNames.map((name) => name[0].toUpperCase() + name.slice(1)).join("")}`,
280
+ origin: [instantiated.symbol, ...instantiated.typeArguments.map((argument) => argument.aliasSymbol ?? argument.symbol)].map((symbol) => this.originOf(symbol)).join("\n"),
281
+ };
217
282
  }
218
283
  printStructure(type, path) {
219
284
  const { TypeFlags } = this.ts;
@@ -388,13 +453,6 @@ export class ContractTypePrinter {
388
453
  // never the tail of an escaped backslash.
389
454
  return `'${doubleQuoted.slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
390
455
  }
391
- takeName(base) {
392
- let name = base;
393
- for (let suffix = 2; this.takenNames.has(name); suffix++)
394
- name = `${base}${suffix}`;
395
- this.takenNames.add(name);
396
- return name;
397
- }
398
456
  fail(path, reason) {
399
457
  this.failures.push(`${path}: ${reason}`);
400
458
  return atom("unknown");
@@ -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
+ };