lambder 11.0.1 → 12.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 +154 -0
- package/README.md +18 -13
- package/dist/api/LambderApiDeclarations.d.ts +194 -0
- package/dist/api/LambderApiDeclarations.js +52 -0
- package/dist/api/LambderApiDefinition.d.ts +1 -1
- package/dist/api/LambderApiGuards.d.ts +21 -22
- package/dist/api/LambderApiGuards.js +1 -1
- package/dist/api/LambderApiRateLimits.d.ts +19 -19
- package/dist/api/LambderApiRateLimits.js +1 -1
- package/dist/api/LambderApiRequest.d.ts +15 -6
- package/dist/api/LambderApiRequest.js +8 -9
- package/dist/build/generatedTables.d.ts +2 -1
- package/dist/build/generatedTables.js +1 -1
- package/dist/client/LambderCaller.d.ts +36 -23
- package/dist/client/LambderCaller.js +88 -67
- package/dist/client/lambderFetchTransport.js +2 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -0
- package/dist/core/Lambder.d.ts +203 -87
- package/dist/core/Lambder.js +265 -86
- package/dist/core/LambderContext.d.ts +1 -1
- package/dist/core/LambderContext.js +13 -5
- package/dist/core/LambderCreateOptions.d.ts +28 -39
- package/dist/core/LambderCreateOptions.js +2 -3
- package/dist/core/LambderPublicFiles.d.ts +8 -1
- package/dist/core/LambderPublicFiles.js +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +2 -0
- package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
- package/dist/invoke/LambderInvokeCaller.js +110 -94
- package/dist/invoke/LambderLambdaEvent.d.ts +0 -1
- package/dist/invoke/LambderLambdaEvent.js +0 -1
- package/dist/invoke/lambderHandlerTransport.js +2 -1
- package/dist/mock/LambderMockApp.d.ts +31 -25
- package/dist/mock/LambderMockApp.js +52 -45
- package/dist/mock/LambderMockCreateOptions.d.ts +6 -7
- package/dist/mock/LambderMockTypes.d.ts +44 -7
- package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
- package/dist/mock/lambderMockInvokeTransport.js +15 -7
- package/dist/mock/lambderMockMswHandler.js +8 -4
- package/dist/mock.d.ts +2 -2
- package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
- package/dist/shared/transport/LambderApiTransport.js +2 -3
- package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
- package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
- package/dist/shared/wire/LambderApiContract.d.ts +4 -18
- package/dist/shared/wire/LambderApiContract.js +1 -1
- package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
- package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
- package/dist/shared/wire/LambderApiNames.d.ts +55 -0
- package/dist/shared/wire/LambderApiNames.js +81 -0
- package/dist/shared/wire/LambderUploadSchemas.js +2 -1
- package/dist/testing/LambderTestApp.d.ts +5 -1
- package/dist/testing/LambderTestVisitor.d.ts +16 -8
- package/dist/testing/LambderTestVisitor.js +33 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,160 @@ 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
|
+
## [12.0.1] - 2026-09-30
|
|
13
|
+
|
|
14
|
+
A major: endpoints are values. An app declares each endpoint with its
|
|
15
|
+
instance's `defineApi`, gathers them into named groups with `defineApiGroup`,
|
|
16
|
+
and registers every group in one `registerApiGroups()` call. An endpoint's
|
|
17
|
+
guards decide its mode, its name is `group.action`, a call goes to
|
|
18
|
+
`{apiPath}/{group}/{action}`, and every caller reaches it through its group:
|
|
19
|
+
`caller.orders.place(input)`.
|
|
20
|
+
|
|
21
|
+
### Changed (breaking)
|
|
22
|
+
|
|
23
|
+
- **`addApi` and `addSessionApi` are replaced by `defineApi`,
|
|
24
|
+
`defineApiGroup` and `registerApiGroups`.** `defineApi(options, handler)`
|
|
25
|
+
takes the options and the handler `addApi` took, and registers nothing by
|
|
26
|
+
itself; `defineApiGroup(name, { action: declaration })` gathers a group
|
|
27
|
+
(`defineApiGroup(name, partA, partB)` for one declared across files, an
|
|
28
|
+
action two parts declare being refused); `registerApiGroups(...groups)`
|
|
29
|
+
registers them and returns the instance typed with their contract, one flat
|
|
30
|
+
mapped type. The three builders are arrow properties, so an app hands them
|
|
31
|
+
out with `export const { defineApi, defineApiGroup, lazyApiGroup } =
|
|
32
|
+
lambderApp;`.
|
|
33
|
+
- To move: turn each chain of `addApi`/`addSessionApi` calls into a group of
|
|
34
|
+
`defineApi` declarations and register the groups in one call. Handler
|
|
35
|
+
bodies do not change.
|
|
36
|
+
- **An endpoint's mode is its guards'.** An endpoint declaring a guard that
|
|
37
|
+
needs a session (`session: true`) is a session endpoint: its session is read
|
|
38
|
+
first, a call without one is answered `sessionExpired`, and its handler's
|
|
39
|
+
`ctx.session` is typed present. Every other endpoint is public. A
|
|
40
|
+
per-session rate limit on an endpoint none of whose guards needs a session,
|
|
41
|
+
and a session guard on an instance created without the `session` option,
|
|
42
|
+
are compile errors and registration errors.
|
|
43
|
+
- To move: give an endpoint that was an `addSessionApi` and declares no
|
|
44
|
+
session guard one: a no-op `lambderGuard({ session: true, handler: () => {} })`
|
|
45
|
+
where the session is its whole authorization.
|
|
46
|
+
- **`requireSessionApiGuards` and `requirePublicApiGuards` are replaced by
|
|
47
|
+
`requireApiGuards`**, which makes `guards` a required field of every
|
|
48
|
+
endpoint.
|
|
49
|
+
- **An endpoint is named `group.action`**, both identifiers (a letter, then
|
|
50
|
+
letters, digits or underscores). A group may not take the name of a
|
|
51
|
+
caller's public member or one a runtime asks of any object
|
|
52
|
+
(`LAMBDER_RESERVED_GROUP_NAMES`), nor an action one a function or any
|
|
53
|
+
object answers for (`then`, `call`, `toString`, `valueOf`:
|
|
54
|
+
`LAMBDER_RESERVED_ACTION_NAMES`), so converting a group to a string calls
|
|
55
|
+
no endpoint. The callers keep their state in
|
|
56
|
+
`#private` fields, so a group may take any name their public members leave
|
|
57
|
+
free (`transport`, `client`, `headers`).
|
|
58
|
+
- **A call is `POST {apiPath}/{group}/{action}`**, the envelope in the body
|
|
59
|
+
and no `apiName` in it, so a gateway, a CDN and a log can meter, limit and
|
|
60
|
+
read calls per endpoint. `buildTransportEnvelope` writes no `apiName`, and
|
|
61
|
+
`readApiEnvelope(post, info, apiName)` takes the name from the path. A JSON
|
|
62
|
+
POST to `apiPath` itself naming an endpoint in its body is a page built
|
|
63
|
+
before endpoints had paths, and is answered `versionExpired`, which reloads
|
|
64
|
+
it.
|
|
65
|
+
- To move: an API Gateway route or a CloudFront behavior that matches
|
|
66
|
+
`apiPath` exactly (`POST /api`) now misses every call; widen it to what
|
|
67
|
+
is under it (`/api/*`, `ANY /api/{proxy+}`). A cookie an API handler sets
|
|
68
|
+
with a raw `Set-Cookie` header and no `Path` now defaults to the call's
|
|
69
|
+
directory (`/api/orders`) rather than `/`, in a browser as in the cookie
|
|
70
|
+
jar; `ctx.setCookie` and the session cookies always write `Path=/`.
|
|
71
|
+
- **A request under `apiPath` that nothing matched is the API's**: a GET to a
|
|
72
|
+
call path, or a path of another depth, is answered as an unknown API rather
|
|
73
|
+
than by the public files, the shell or the route fallback. A root `apiPath`
|
|
74
|
+
shares every path with the site, so there only `apiPath` itself and the
|
|
75
|
+
calls are.
|
|
76
|
+
- **The instance takes one type parameter**, `Lambder<TApp, TContract>`,
|
|
77
|
+
where `TApp` is `LambderAppTypes` (the session data, policies, guards,
|
|
78
|
+
idempotency, the guard requirement, sessions and the refusal vocabulary),
|
|
79
|
+
in place of ten positional generics. It is readable as the type-only
|
|
80
|
+
`AppTypes` property beside `ApiContract`.
|
|
81
|
+
- **`use(plugin)` registers routes, hooks and actions**, and hands back the
|
|
82
|
+
same instance: endpoints are registered as groups, so a plugin adds nothing
|
|
83
|
+
to the contract.
|
|
84
|
+
- **A `registerApiGroups()` call's groups take their place in the first-match
|
|
85
|
+
chain where the call stands**, as each `addApi` did: a route registered
|
|
86
|
+
before them sees their calls first, one registered after them never does.
|
|
87
|
+
The beforeRender hooks run before a lazy group loads, and a call to an
|
|
88
|
+
action its group does not have is answered as an unmatched API call, the
|
|
89
|
+
fallback hooks first.
|
|
90
|
+
- **`apiOptionEntries()` is asynchronous**, as `apiSignatures()` and
|
|
91
|
+
`apiSignatureEntries()` are: all three load lazy groups first.
|
|
92
|
+
- **The mock's `publicApi` and `sessionApi` are replaced by `api`, and
|
|
93
|
+
`sessionNotMocked` by `notMocked`.** An entry's mode is the `apiOptions`
|
|
94
|
+
table's when the mock was given it, and otherwise the server's rule applied
|
|
95
|
+
to the guards the entry restates. `LambderMockPublicNames` and
|
|
96
|
+
`LambderMockSessionNames` are removed. Every session endpoint must name at
|
|
97
|
+
least one mock guard declared `session: true`, a compile error at the
|
|
98
|
+
`guards` option otherwise (`LambderMockSessionGuardsCheck`), and a session
|
|
99
|
+
endpoint left unmocked on a mock without the table restates its guards,
|
|
100
|
+
`notMocked(name, { reason, guards })`, so a signed-out call answers
|
|
101
|
+
`sessionExpired` as the server does. `lambderMockInvokeTransport(mockApp,
|
|
102
|
+
{ apiPath })` answers what the callee's server reads as a call, a JSON POST
|
|
103
|
+
to a call path under `apiPath` (default `"/api"`), and 404s anything else.
|
|
104
|
+
- **`LambderMergeContract` is removed**, and `LambderAllowedGuardNames`,
|
|
105
|
+
`LambderParamlessGuardNames`, `LambderGuardsOption`,
|
|
106
|
+
`LambderAllowedPolicyNames` and `LambderRateLimitOption` lose their second
|
|
107
|
+
type parameter: every guard and every policy with a `per` may be declared
|
|
108
|
+
on any endpoint, a session guard making it a session one.
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- **Every caller reaches an endpoint through its group.**
|
|
113
|
+
`caller.orders.place(input)` is `caller.api("orders.place", input)`, and
|
|
114
|
+
`caller.orders.place.outcome(input)` its `apiOutcome`, on `LambderCaller`,
|
|
115
|
+
`LambderInvokeCaller` and the test visitor alike. The groups are read off
|
|
116
|
+
the contract's type, so a client that imports its contract as a type ships
|
|
117
|
+
no endpoint list. A name that is not `group.action` is refused before it is
|
|
118
|
+
sent.
|
|
119
|
+
- **Lazy groups.** `lazyApiGroup(name, () => import(...).then((m) => m.group))`
|
|
120
|
+
registers a group loaded on the first call to one of its endpoints, so a
|
|
121
|
+
cold start parses none of it until then; `loadApiGroups()` loads every one,
|
|
122
|
+
for a build step or a boot check. A hook that answers a call spares it the
|
|
123
|
+
import; one that throws a refusal, or charges a per-API budget, loads the
|
|
124
|
+
group, so the refusal is checked and the budget counted against the
|
|
125
|
+
endpoint called, as for an eager group. A load that fails is forgotten, and
|
|
126
|
+
the next call tries again.
|
|
127
|
+
- **Compile-time registration checks**: a group typed `any`, a lazy group
|
|
128
|
+
loading one, or a group whose endpoints are (its endpoints would be `any` to
|
|
129
|
+
every client), and a group name given twice, are refused where
|
|
130
|
+
`registerApiGroups()` is written, and a group registration is atomic:
|
|
131
|
+
refused anywhere, it leaves the instance as it was. A lazy group written
|
|
132
|
+
where a list of groups is expected (a `satisfies`, an annotated array)
|
|
133
|
+
keeps its loaded group's type, rather than taking the expected `any`.
|
|
134
|
+
- **`beforeCall` on `LambderInvokeCaller`**: a check run before every call,
|
|
135
|
+
`api()`, `apiOutcome()`, a group's call and `request()` alike, before
|
|
136
|
+
anything is built or sent. What it throws is thrown to the caller, from
|
|
137
|
+
`apiOutcome()` too, and `onFailure` is not told, since nothing was sent. It
|
|
138
|
+
is where a rule the calling code must keep goes, such as never calling out
|
|
139
|
+
while a database transaction is open (`LambderInvokeCallCheck`).
|
|
140
|
+
- **What registering endpoints costs the compiler grows linearly**: each
|
|
141
|
+
declaration is typed on its own, and eight hundred endpoints cost under four
|
|
142
|
+
times what two hundred do, which the type-cost suite holds.
|
|
143
|
+
- Exports: `apiCallPath`, `apiNameOfCallPath`, `splitApiName`, `isGroupName`,
|
|
144
|
+
`isActionName`, `LAMBDER_API_NAME_SEGMENT`, `LAMBDER_CALLER_MEMBER_NAMES`,
|
|
145
|
+
`LAMBDER_RESERVED_GROUP_NAMES`, `LAMBDER_RESERVED_ACTION_NAMES`, and the
|
|
146
|
+
types of declarations, groups and group calls (see
|
|
147
|
+
[Exports](./docs/exports.md#endpoints-declarations-groups-and-names)).
|
|
148
|
+
|
|
149
|
+
## [11.1.1] - 2026-09-30
|
|
150
|
+
|
|
151
|
+
A minor for an app whose shell files sit behind a shared cache: a file whose
|
|
152
|
+
name does not change when its bytes do is revalidated rather than kept.
|
|
153
|
+
|
|
154
|
+
### Changed
|
|
155
|
+
|
|
156
|
+
- **`servePublicFiles` answers a file its immutable rule does not match with
|
|
157
|
+
`Cache-Control: no-cache`**, where it answered `public, max-age=3600`. A
|
|
158
|
+
service worker, a web manifest or a favicon keeps its name across a deploy,
|
|
159
|
+
so a CDN or proxy honouring the hour served the old copy to everyone for up
|
|
160
|
+
to an hour afterwards, and an app behind one had to purge it on every
|
|
161
|
+
deploy. Every GET or HEAD 200 already carries an ETag, so a revalidation of
|
|
162
|
+
an unchanged file is a 304 with no body. Content-hashed build output keeps
|
|
163
|
+
`immutableCacheControl`, and an app that wants the old behaviour passes
|
|
164
|
+
`cacheControl: "public, max-age=3600"`.
|
|
165
|
+
|
|
12
166
|
## [11.0.1] - 2026-09-29
|
|
13
167
|
|
|
14
168
|
A major for an app with many APIs: what one API costs the type checker no
|
package/README.md
CHANGED
|
@@ -10,25 +10,30 @@ per-handler boilerplate.
|
|
|
10
10
|
import { initLambder, LambderLocalFileSource, LambderDdbSessionStore } from "lambder";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const app = initLambder<SessionData>().create({
|
|
14
14
|
apiPath: "/api",
|
|
15
15
|
files: new LambderLocalFileSource({ root: "./public" }),
|
|
16
16
|
session: { store: new LambderDdbSessionStore({ tableName: "app-session", region: "us-east-1" }), sessionSalt: process.env.SESSION_SALT! },
|
|
17
|
-
})
|
|
18
|
-
input: z.object({ slug: z.string() }),
|
|
19
|
-
output: z.object({ id: z.string(), name: z.string() }),
|
|
20
|
-
}, async ({ apiPayload }) => await loadCompany(apiPayload.slug));
|
|
17
|
+
});
|
|
21
18
|
|
|
19
|
+
const companyApis = app.defineApiGroup("companies", {
|
|
20
|
+
get: app.defineApi({
|
|
21
|
+
input: z.object({ slug: z.string() }),
|
|
22
|
+
output: z.object({ id: z.string(), name: z.string() }),
|
|
23
|
+
}, async ({ apiPayload }) => await loadCompany(apiPayload.slug)),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const lambder = app.registerApiGroups(companyApis);
|
|
22
27
|
export type ApiContractType = typeof lambder.ApiContract;
|
|
23
28
|
export const handler = lambder.getHandler();
|
|
24
29
|
```
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
An endpoint is a value: declared with `defineApi`, gathered into a named
|
|
32
|
+
group, registered with its groups in one call, and called at
|
|
33
|
+
`/api/companies/get`. A handler returns its output, which is parsed through
|
|
34
|
+
the output schema before it is sent, and says no by throwing a refusal:
|
|
35
|
+
`ctx.refuse()`, typed to the codes the API declares, or `refuse()` from
|
|
36
|
+
anywhere. Its guards say who may call it, and so whether it needs a session.
|
|
32
37
|
|
|
33
38
|
The frontend imports that contract type and gets autocomplete, typed payloads
|
|
34
39
|
and typed results with no hand-written client:
|
|
@@ -38,7 +43,7 @@ import { LambderCaller } from "lambder/client";
|
|
|
38
43
|
import type { ApiContractType } from "./backend/handler";
|
|
39
44
|
|
|
40
45
|
const caller = new LambderCaller<ApiContractType>({ apiPath: "/api" });
|
|
41
|
-
const company = await caller.
|
|
46
|
+
const company = await caller.companies.get({ slug: "acme" });
|
|
42
47
|
```
|
|
43
48
|
|
|
44
49
|
## Features
|
|
@@ -166,7 +171,7 @@ guide that matches what you are building. The full index lives in
|
|
|
166
171
|
| [Getting started](./docs/getting-started.md) | The three-step path from a first API to a typed frontend call |
|
|
167
172
|
| [Configuration](./docs/configuration.md) | Every `initLambder().create({...})` option, in one reference |
|
|
168
173
|
| [Routing and actions](./docs/routing.md) | Routes, matchers, hooks, fallbacks, crash reporting, and non-HTTP invocations |
|
|
169
|
-
| [APIs and refusals](./docs/apis.md) | `
|
|
174
|
+
| [APIs and refusals](./docs/apis.md) | `defineApi`, groups and lazy groups, the mode the guards decide, the inferred contract, `refuse()` and `LambderApiRefusal`, the signature file |
|
|
170
175
|
| [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 |
|
|
171
176
|
| [Sessions](./docs/sessions.md) | Sessions over a store, cookie scope, secrets at rest, `dataRefresh`, the controller API |
|
|
172
177
|
| [API policies](./docs/api-policies.md) | Declarative rate limits, guards and idempotency, and mandatory authorization declarations |
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
3
|
+
import type { LambderApiMode, LambderGuardNamesIn } from "../shared/wire/LambderApiContract.js";
|
|
4
|
+
import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue, LambderRefusalsOptionValue } from "../shared/wire/LambderApiOptionValues.js";
|
|
5
|
+
import type { LambderApiRateLimitPolicyConfig, LambderPolicyPerOf } from "./LambderApiRateLimits.js";
|
|
6
|
+
import type { LambderRefusalVocabulary } from "./LambderApiRefusals.js";
|
|
7
|
+
import { type LambderReservedActionName, type LambderReservedGroupName } from "../shared/wire/LambderApiNames.js";
|
|
8
|
+
/**
|
|
9
|
+
* What an instance's declarations are typed against, fixed at create(): the
|
|
10
|
+
* session data, the rate-limit policies, the guards (as LambderGuardMetaMap
|
|
11
|
+
* records them), whether idempotency is configured, whether every endpoint
|
|
12
|
+
* must declare a guard, whether sessions are configured, and the refusal
|
|
13
|
+
* vocabulary with whether every refusal must name a code.
|
|
14
|
+
*/
|
|
15
|
+
export type LambderAppTypes = {
|
|
16
|
+
session: unknown;
|
|
17
|
+
policies: Record<string, LambderApiRateLimitPolicyConfig>;
|
|
18
|
+
guards: Record<string, any>;
|
|
19
|
+
idempotency: boolean;
|
|
20
|
+
guardsRequired: boolean;
|
|
21
|
+
sessions: boolean;
|
|
22
|
+
refusals: LambderRefusalVocabulary;
|
|
23
|
+
refusalCodesRequired: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** The types of an instance built without create(): no policies, guards or vocabulary, sessions allowed. */
|
|
26
|
+
export type LambderPlainAppTypes<TSession = any> = {
|
|
27
|
+
session: TSession;
|
|
28
|
+
policies: {};
|
|
29
|
+
guards: {};
|
|
30
|
+
idempotency: false;
|
|
31
|
+
guardsRequired: false;
|
|
32
|
+
sessions: true;
|
|
33
|
+
refusals: {};
|
|
34
|
+
refusalCodesRequired: false;
|
|
35
|
+
};
|
|
36
|
+
/** The guards among the names an endpoint declares that need a session (`session: true`). */
|
|
37
|
+
export type LambderSessionGuardNamesIn<TGuards, TGuardsOpt> = {
|
|
38
|
+
[K in LambderGuardNamesIn<TGuardsOpt> & keyof TGuards]: TGuards[K] extends {
|
|
39
|
+
session: true;
|
|
40
|
+
} ? K : never;
|
|
41
|
+
}[LambderGuardNamesIn<TGuardsOpt> & keyof TGuards];
|
|
42
|
+
/**
|
|
43
|
+
* An endpoint's mode, from the guards it declares: "session" when any of
|
|
44
|
+
* them needs a session, "public" otherwise. The guard is the one place the
|
|
45
|
+
* endpoint says who may call it, and a session is part of that answer, so
|
|
46
|
+
* nothing else states the mode and nothing can disagree with it.
|
|
47
|
+
*/
|
|
48
|
+
export type LambderApiModeOf<TGuards, TGuardsOpt> = [
|
|
49
|
+
LambderSessionGuardNamesIn<TGuards, TGuardsOpt>
|
|
50
|
+
] extends [never] ? "public" : "session";
|
|
51
|
+
/** The per-session policies among the names an endpoint's rateLimit option declares. */
|
|
52
|
+
type LambderSessionPolicyNamesIn<TPolicies, TRateOpt> = {
|
|
53
|
+
[K in LambderGuardNamesIn<TRateOpt> & keyof TPolicies]: [LambderPolicyPerOf<TPolicies[K]>] extends ["session"] ? K : never;
|
|
54
|
+
}[LambderGuardNamesIn<TRateOpt> & keyof TPolicies];
|
|
55
|
+
/**
|
|
56
|
+
* A public endpoint's per-session rate limits, refused on the option: a
|
|
57
|
+
* public call carries no session to count against. The property name is the
|
|
58
|
+
* message and its value the policies at fault.
|
|
59
|
+
*/
|
|
60
|
+
export type LambderPublicSessionPolicyCheck<TMode extends LambderApiMode, TPolicies, TRateOpt> = TMode extends "session" ? unknown : [LambderSessionPolicyNamesIn<TPolicies, TRateOpt>] extends [never] ? unknown : {
|
|
61
|
+
rateLimit: {
|
|
62
|
+
readonly "lambder: these rate limits count per session, and none of this API's guards needs a session": LambderSessionPolicyNamesIn<TPolicies, TRateOpt>;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/** What a session endpoint needs of its instance: sessions configured at create(). */
|
|
66
|
+
type LambderSessionsConfigured = {
|
|
67
|
+
readonly "lambder: a guard of this API needs a session, and sessions are not configured on this instance. Pass the session option to create().": never;
|
|
68
|
+
};
|
|
69
|
+
/** A session endpoint on an instance without sessions, refused where it is declared. */
|
|
70
|
+
export type LambderSessionModeCheck<TMode extends LambderApiMode, TSessions extends boolean> = TMode extends "session" ? (TSessions extends true ? unknown : LambderSessionsConfigured) : unknown;
|
|
71
|
+
/** One endpoint's options as registration reads them. */
|
|
72
|
+
export type LambderApiDeclarationOptions = {
|
|
73
|
+
input: z.ZodType;
|
|
74
|
+
output: z.ZodType;
|
|
75
|
+
guards?: LambderGuardsOptionValue;
|
|
76
|
+
rateLimit?: LambderRateLimitOptionValue;
|
|
77
|
+
idempotency?: LambderApiIdempotencyOption;
|
|
78
|
+
refusals?: LambderRefusalsOptionValue;
|
|
79
|
+
compress?: boolean | "auto";
|
|
80
|
+
};
|
|
81
|
+
/** The key the contract entry of a declaration is typed under. It exists only in types: nothing reads or writes it. */
|
|
82
|
+
declare const LAMBDER_CONTRACT_ENTRY: unique symbol;
|
|
83
|
+
/**
|
|
84
|
+
* One endpoint, declared: its options and its handler, and in its type the
|
|
85
|
+
* contract entry a client calls it by. Built by an instance's defineApi(),
|
|
86
|
+
* registered by being put in a group.
|
|
87
|
+
*/
|
|
88
|
+
export type LambderApiDeclaration<TEntry = unknown> = {
|
|
89
|
+
readonly kind: "lambderApi";
|
|
90
|
+
readonly options: LambderApiDeclarationOptions;
|
|
91
|
+
readonly handler: (ctx: never) => MaybePromise<unknown>;
|
|
92
|
+
readonly [LAMBDER_CONTRACT_ENTRY]: TEntry;
|
|
93
|
+
};
|
|
94
|
+
/** A group's endpoints by action name. */
|
|
95
|
+
export type LambderApiDeclarations = Record<string, LambderApiDeclaration<any>>;
|
|
96
|
+
/** A group of endpoints: `name.action` for each of its actions. Built by defineApiGroup(). */
|
|
97
|
+
export type LambderApiGroup<TName extends string = string, TApis extends LambderApiDeclarations = LambderApiDeclarations> = {
|
|
98
|
+
readonly kind: "lambderApiGroup";
|
|
99
|
+
readonly name: TName;
|
|
100
|
+
readonly apis: TApis;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* A group loaded on the first call to one of its endpoints: its name, and
|
|
104
|
+
* how to load it. Built by lazyApiGroup(). What a cold start parses then
|
|
105
|
+
* leaves out every group no request has called yet.
|
|
106
|
+
*/
|
|
107
|
+
export type LambderLazyApiGroup<TName extends string = string, TGroup extends LambderApiGroup<TName, any> = LambderApiGroup<TName, any>> = {
|
|
108
|
+
readonly kind: "lambderLazyApiGroup";
|
|
109
|
+
readonly name: TName;
|
|
110
|
+
readonly load: () => Promise<TGroup>;
|
|
111
|
+
};
|
|
112
|
+
/** Either kind of group registerApiGroups() takes. */
|
|
113
|
+
export type LambderRegistrableApiGroup = LambderApiGroup<string, any> | LambderLazyApiGroup<string, any>;
|
|
114
|
+
/** A declaration's contract entry. */
|
|
115
|
+
export type LambderEntryOf<TDeclaration> = TDeclaration extends {
|
|
116
|
+
readonly [LAMBDER_CONTRACT_ENTRY]: infer TEntry;
|
|
117
|
+
} ? TEntry : never;
|
|
118
|
+
type LambderLoadedGroupOf<TGroup> = TGroup extends LambderLazyApiGroup<string, infer TLoaded> ? TLoaded : TGroup;
|
|
119
|
+
/**
|
|
120
|
+
* One group's part of the contract: `group.action` for each action, its
|
|
121
|
+
* declaration's entry. Distributed over a union of groups (the `TGroup
|
|
122
|
+
* extends unknown`), so each group is read on its own: read as one union,
|
|
123
|
+
* the actions would be only those every group shares.
|
|
124
|
+
*/
|
|
125
|
+
type LambderGroupContract<TGroup> = TGroup extends unknown ? (LambderLoadedGroupOf<TGroup> extends LambderApiGroup<infer TName, infer TApis> ? {
|
|
126
|
+
[TAction in keyof TApis & string as `${TName}.${TAction}`]: LambderEntryOf<TApis[TAction]>;
|
|
127
|
+
} : never) : never;
|
|
128
|
+
type LambderUnionToIntersection<TUnion> = (TUnion extends unknown ? (value: TUnion) => void : never) extends (value: infer TIntersection) => void ? TIntersection : never;
|
|
129
|
+
/** The contract of every group registered together, as one flat object type keyed by endpoint name. */
|
|
130
|
+
export type LambderContractOfGroups<TGroups extends readonly unknown[]> = LambderUnionToIntersection<LambderGroupContract<TGroups[number]>> extends infer TMerged ? {
|
|
131
|
+
[TName in keyof TMerged]: TMerged[TName];
|
|
132
|
+
} : never;
|
|
133
|
+
/**
|
|
134
|
+
* Whether a group, the group a lazy one loads, or that group's endpoints is
|
|
135
|
+
* typed any. A part typed any, merged with the rest, comes out as an index
|
|
136
|
+
* signature rather than as any, and no group declared action by action has
|
|
137
|
+
* one, so that counts too.
|
|
138
|
+
*/
|
|
139
|
+
type LambderGroupTypedAny<TGroup> = 0 extends 1 & TGroup ? true : 0 extends 1 & LambderLoadedGroupOf<TGroup> ? true : LambderLoadedGroupOf<TGroup> extends {
|
|
140
|
+
readonly apis: infer TApis;
|
|
141
|
+
} ? (0 extends 1 & TApis ? true : string extends keyof TApis ? true : false) : false;
|
|
142
|
+
/** The group names one registerApiGroups() call names twice. */
|
|
143
|
+
type LambderRepeatedGroupNames<TGroups extends readonly unknown[], TSeen = never> = TGroups extends readonly [infer THead, ...infer TRest] ? (THead extends {
|
|
144
|
+
name: infer TName;
|
|
145
|
+
} ? ([TName] extends [TSeen] ? TName : never) | LambderRepeatedGroupNames<TRest, TSeen | TName> : never) : never;
|
|
146
|
+
/**
|
|
147
|
+
* What registerApiGroups() asks of its receiver: the instance itself, unless
|
|
148
|
+
* a group, what a lazy group loads or a group's endpoints is typed `any` (a
|
|
149
|
+
* module whose types were lost would otherwise make its endpoints `any` to
|
|
150
|
+
* every client) or a group name is given twice (two
|
|
151
|
+
* groups of one name would be one namespace with two owners). Then it asks
|
|
152
|
+
* for an object no instance is, whose property name is the message, so the
|
|
153
|
+
* call is refused where it is written.
|
|
154
|
+
*/
|
|
155
|
+
export type LambderRegisteredGroupsCheck<TGroups extends readonly unknown[], TSelf> = true extends {
|
|
156
|
+
[TIndex in keyof TGroups]: LambderGroupTypedAny<TGroups[TIndex]>;
|
|
157
|
+
}[number] ? {
|
|
158
|
+
readonly "lambder: a group given to registerApiGroups, what a lazy group loads, or a group's endpoints is typed any, which would make its part of the contract any. Build it with defineApiGroup() and let its type through.": never;
|
|
159
|
+
} : [LambderRepeatedGroupNames<TGroups>] extends [never] ? TSelf : {
|
|
160
|
+
readonly "lambder: these group names are registered twice": LambderRepeatedGroupNames<TGroups>;
|
|
161
|
+
};
|
|
162
|
+
/** A group's name held to the rule a caller's properties need. */
|
|
163
|
+
export type LambderGroupNameCheck<TName extends string> = TName extends LambderReservedGroupName ? {
|
|
164
|
+
readonly "lambder: a caller already has a member of this name, so no group may take it": TName;
|
|
165
|
+
} : unknown;
|
|
166
|
+
/** The actions more than one of a group's parts declare. */
|
|
167
|
+
type LambderRepeatedActions<TParts extends readonly unknown[], TSeen = never> = TParts extends readonly [infer THead, ...infer TRest] ? (Extract<keyof THead, TSeen> | LambderRepeatedActions<TRest, TSeen | keyof THead>) : never;
|
|
168
|
+
/** A group's parts held to declaring each action once: a part would otherwise replace another's endpoint of the same name. */
|
|
169
|
+
export type LambderGroupPartsCheck<TParts extends readonly unknown[]> = [
|
|
170
|
+
LambderRepeatedActions<TParts>
|
|
171
|
+
] extends [never] ? unknown : {
|
|
172
|
+
readonly "lambder: these actions are declared by more than one part of the group": LambderRepeatedActions<TParts>;
|
|
173
|
+
};
|
|
174
|
+
/** One group's endpoints, from the parts it was declared in. */
|
|
175
|
+
export type LambderMergedParts<TParts extends readonly unknown[]> = LambderUnionToIntersection<TParts[number]> extends infer TMerged ? {
|
|
176
|
+
[TAction in keyof TMerged]: TMerged[TAction];
|
|
177
|
+
} : never;
|
|
178
|
+
/** A group's action names held to the rule the functions a caller hands out need. */
|
|
179
|
+
export type LambderActionNamesCheck<TApis> = [
|
|
180
|
+
Extract<keyof TApis, LambderReservedActionName>
|
|
181
|
+
] extends [never] ? unknown : {
|
|
182
|
+
readonly "lambder: a function already has a member of these names, so no action may take them": Extract<keyof TApis, LambderReservedActionName>;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Builds a group from its parts, refusing a name no caller could carry, an
|
|
186
|
+
* action two parts declare, and a value that is not an endpoint. A group is
|
|
187
|
+
* usually one part; one declared across files gives each file's part.
|
|
188
|
+
*/
|
|
189
|
+
export declare const buildApiGroup: <TName extends string, TApis extends LambderApiDeclarations>(name: TName, parts: readonly LambderApiDeclarations[]) => LambderApiGroup<TName, TApis>;
|
|
190
|
+
/** Builds a lazy group; the name is checked here, the group it loads when it loads. */
|
|
191
|
+
export declare const buildLazyApiGroup: <TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, TGroup>;
|
|
192
|
+
/** Whether a value is either kind of group registerApiGroups() takes. */
|
|
193
|
+
export declare const isRegistrableApiGroup: (value: unknown) => value is LambderRegistrableApiGroup;
|
|
194
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isActionName, isGroupName, LAMBDER_API_NAME_SEGMENT } from "../shared/wire/LambderApiNames.js";
|
|
2
|
+
const describeName = (name) => typeof name === "string" ? `"${name}"` : String(name);
|
|
3
|
+
/**
|
|
4
|
+
* Builds a group from its parts, refusing a name no caller could carry, an
|
|
5
|
+
* action two parts declare, and a value that is not an endpoint. A group is
|
|
6
|
+
* usually one part; one declared across files gives each file's part.
|
|
7
|
+
*/
|
|
8
|
+
export const buildApiGroup = (name, parts) => {
|
|
9
|
+
if (typeof name !== "string" || !isGroupName(name)) {
|
|
10
|
+
throw new Error(`Lambder: ${describeName(name)} cannot name a group. A group is a letter then letters, digits or underscores, ` +
|
|
11
|
+
`and not a name a caller already answers for (api, apiOutcome, then, ...).`);
|
|
12
|
+
}
|
|
13
|
+
const apis = {};
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
if (part === null || typeof part !== "object") {
|
|
16
|
+
throw new Error(`Lambder: a part of group "${name}" is not an object of endpoints by action name.`);
|
|
17
|
+
}
|
|
18
|
+
for (const [action, declaration] of Object.entries(part)) {
|
|
19
|
+
if (Object.prototype.hasOwnProperty.call(apis, action)) {
|
|
20
|
+
throw new Error(`Lambder: "${name}.${action}" is declared by two parts of group "${name}". Each action is declared once.`);
|
|
21
|
+
}
|
|
22
|
+
apis[action] = declaration;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (Object.keys(apis).length === 0) {
|
|
26
|
+
throw new Error(`Lambder: group "${name}" declares no endpoints. A group is its endpoints by action name: defineApiGroup("${name}", { action: defineApi(...) }).`);
|
|
27
|
+
}
|
|
28
|
+
for (const [action, declaration] of Object.entries(apis)) {
|
|
29
|
+
if (!isActionName(action)) {
|
|
30
|
+
throw new Error(`Lambder: "${name}.${action}" cannot name an endpoint. An action is a letter then letters, digits or underscores, ` +
|
|
31
|
+
`and not a name a function already answers for (then, outcome, call, ...).`);
|
|
32
|
+
}
|
|
33
|
+
if (declaration?.kind !== "lambderApi") {
|
|
34
|
+
throw new Error(`Lambder: "${name}.${action}" is not an endpoint declaration. Declare it with the instance's defineApi().`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { kind: "lambderApiGroup", name, apis: apis };
|
|
38
|
+
};
|
|
39
|
+
/** Builds a lazy group; the name is checked here, the group it loads when it loads. */
|
|
40
|
+
export const buildLazyApiGroup = (name, load) => {
|
|
41
|
+
if (typeof name !== "string" || !isGroupName(name)) {
|
|
42
|
+
throw new Error(`Lambder: ${describeName(name)} cannot name a group. A group is a letter then letters, digits or underscores, and not a name a caller already answers for.`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof load !== "function")
|
|
45
|
+
throw new Error(`Lambder: lazy group "${name}" has no loader. Pass a function that imports the group: () => import("./orders.js").then((m) => m.orderApis).`);
|
|
46
|
+
return { kind: "lambderLazyApiGroup", name, load };
|
|
47
|
+
};
|
|
48
|
+
/** Whether a value is either kind of group registerApiGroups() takes. */
|
|
49
|
+
export const isRegistrableApiGroup = (value) => {
|
|
50
|
+
const kind = value?.kind;
|
|
51
|
+
return (kind === "lambderApiGroup" || kind === "lambderLazyApiGroup") && typeof value.name === "string" && LAMBDER_API_NAME_SEGMENT.test(value.name);
|
|
52
|
+
};
|
|
@@ -4,7 +4,7 @@ import type { LambderApiMode } from "../shared/wire/LambderApiContract.js";
|
|
|
4
4
|
import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue } from "../shared/wire/LambderApiOptionValues.js";
|
|
5
5
|
/**
|
|
6
6
|
* One endpoint's declaration as the pipeline runs it: what the server's
|
|
7
|
-
*
|
|
7
|
+
* defineApi options carry, minus the handler, in a shape the mock
|
|
8
8
|
* runtime can restate from a type-only contract. The schemas are optional
|
|
9
9
|
* because the mock has none; when input is present, validation runs and the
|
|
10
10
|
* handler sees the parsed payload. Output is part of the endpoint's
|
|
@@ -72,9 +72,11 @@ export type LambderGuardRefusals<TRefusals extends readonly string[] = readonly
|
|
|
72
72
|
*
|
|
73
73
|
* Orthogonally, a guard may also:
|
|
74
74
|
*
|
|
75
|
-
* - declare `session: true`: the guard needs ctx.session, so it
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* - declare `session: true`: the guard needs ctx.session, so it makes every
|
|
76
|
+
* endpoint declaring it a session endpoint (the session is read before the
|
|
77
|
+
* guards run, a call without one is answered sessionExpired, and the
|
|
78
|
+
* endpoint's handler receives the session-typed context), and it receives
|
|
79
|
+
* the session-typed context itself.
|
|
78
80
|
* - take a PARAMETER: annotate a 3rd handler argument
|
|
79
81
|
* (`(ctx, payload, param: YourType) => ...`) and APIs pass the value in
|
|
80
82
|
* their declaration: `guards: { yourGuard: paramValue }`. The value is
|
|
@@ -272,19 +274,16 @@ export type LambderGuardMetaMap<TGuards> = {
|
|
|
272
274
|
[K in keyof TGuards]: LambderGuardMeta<TGuards[K]>;
|
|
273
275
|
};
|
|
274
276
|
/**
|
|
275
|
-
* Guard names an API may declare: every guard the instance holds, session
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* known, by
|
|
277
|
+
* Guard names an API may declare: every guard the instance holds, a session
|
|
278
|
+
* guard included, which makes the API a session one (LambderApiModeOf).
|
|
279
|
+
* Whether the API's input carries the fields an apiInput guard reads is
|
|
280
|
+
* asked of the names it declared, once that input is known, by
|
|
281
|
+
* LambderGuardNamesInputLacks.
|
|
279
282
|
*/
|
|
280
|
-
export type LambderAllowedGuardNames<TGuards
|
|
281
|
-
[K in keyof TGuards]: TGuards[K] extends {
|
|
282
|
-
session: true;
|
|
283
|
-
} ? (TIncludeSession extends true ? K : never) : K;
|
|
284
|
-
}[keyof TGuards] & string;
|
|
283
|
+
export type LambderAllowedGuardNames<TGuards> = keyof TGuards & string;
|
|
285
284
|
/**
|
|
286
285
|
* The apiInput guards among `TNames` whose fields the API's payload does not
|
|
287
|
-
* carry.
|
|
286
|
+
* carry. defineApi asks this once the API's input is known, of
|
|
288
287
|
* the names its guards option holds; see LambderPayloadSliceCheck.
|
|
289
288
|
*
|
|
290
289
|
* The payload is compared whole, not member by member: a bare `TPayload
|
|
@@ -298,14 +297,14 @@ export type LambderGuardNamesInputLacks<TGuards, TNames, TPayload> = {
|
|
|
298
297
|
} ? ([TPayload] extends [R] ? never : K) : never;
|
|
299
298
|
}[TNames & keyof TGuards] & string;
|
|
300
299
|
/** The allowed guard names whose handler takes no param (usable in the string/array forms). */
|
|
301
|
-
export type LambderParamlessGuardNames<TGuards
|
|
302
|
-
[K in LambderAllowedGuardNames<TGuards
|
|
300
|
+
export type LambderParamlessGuardNames<TGuards> = {
|
|
301
|
+
[K in LambderAllowedGuardNames<TGuards> & keyof TGuards]: TGuards[K] extends {
|
|
303
302
|
param: undefined;
|
|
304
303
|
} ? K & string : never;
|
|
305
|
-
}[LambderAllowedGuardNames<TGuards
|
|
304
|
+
}[LambderAllowedGuardNames<TGuards> & keyof TGuards];
|
|
306
305
|
/** The map form's full shape: every declarable guard name, each carrying its own param type. */
|
|
307
|
-
type LambderGuardsMap<TGuards
|
|
308
|
-
readonly [K in LambderAllowedGuardNames<TGuards
|
|
306
|
+
type LambderGuardsMap<TGuards> = {
|
|
307
|
+
readonly [K in LambderAllowedGuardNames<TGuards> & keyof TGuards]?: TGuards[K] extends {
|
|
309
308
|
param: undefined;
|
|
310
309
|
} ? true : TGuards[K] extends {
|
|
311
310
|
param: infer P;
|
|
@@ -324,10 +323,10 @@ type LambderGuardsMap<TGuards, TIncludeSession extends boolean> = {
|
|
|
324
323
|
* compiler builds it once per instance rather than once per API; see
|
|
325
324
|
* LambderPayloadSliceCheck for why, and for where the input is asked.
|
|
326
325
|
*/
|
|
327
|
-
export type LambderGuardsOption<TGuards
|
|
328
|
-
LambderParamlessGuardNames<TGuards
|
|
329
|
-
...LambderParamlessGuardNames<TGuards
|
|
330
|
-
] | LambderNonEmptyOptionMap<LambderGuardsMap<TGuards
|
|
326
|
+
export type LambderGuardsOption<TGuards> = LambderParamlessGuardNames<TGuards> | readonly [
|
|
327
|
+
LambderParamlessGuardNames<TGuards>,
|
|
328
|
+
...LambderParamlessGuardNames<TGuards>[]
|
|
329
|
+
] | LambderNonEmptyOptionMap<LambderGuardsMap<TGuards>>;
|
|
331
330
|
/**
|
|
332
331
|
* The typed ctx.guardData an API's handler sees: declared guards that return
|
|
333
332
|
* a value, keyed by name. Check-only (void) guards never appear.
|
|
@@ -101,7 +101,7 @@ export class LambderApiGuardsEngine {
|
|
|
101
101
|
throw new Error(`Lambder: API "${apiName}" references unknown guard "${name}". Declare it in the guards option at creation.`);
|
|
102
102
|
}
|
|
103
103
|
if (guardDef.session && mode !== "session") {
|
|
104
|
-
throw new Error(`Lambder: API "${apiName}" uses guard "${name}" (session: true)
|
|
104
|
+
throw new Error(`Lambder: API "${apiName}" uses guard "${name}" (session: true) but is registered as public. An endpoint declaring a session guard is a session endpoint.`);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -116,7 +116,7 @@ export type LambderApiRateLimitPolicyConfig<TCtx = any> = LambderRateLimitPolicy
|
|
|
116
116
|
export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderApiRateLimitPolicyConfig<any>>> = {
|
|
117
117
|
/** Your limiter instance (LambderDdbRateLimiter, LambderMemoryRateLimiter, or your own); its table and keyPrefix apply as configured on it. */
|
|
118
118
|
limiter: LambderRateLimiter;
|
|
119
|
-
/** Named policies referenced (typed) from
|
|
119
|
+
/** Named policies referenced (typed) from defineApi. */
|
|
120
120
|
policies: TPolicies;
|
|
121
121
|
/**
|
|
122
122
|
* Let the request through when the limiter itself fails (the table is
|
|
@@ -144,23 +144,23 @@ export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderA
|
|
|
144
144
|
* without one, and a union holding undefined for a policy typed as the
|
|
145
145
|
* general LambderApiRateLimitPolicyConfig, where only registration can tell.
|
|
146
146
|
*/
|
|
147
|
-
type LambderPolicyPerOf<TPolicy> = "per" extends keyof TPolicy ? TPolicy["per" & keyof TPolicy] : undefined;
|
|
147
|
+
export type LambderPolicyPerOf<TPolicy> = "per" extends keyof TPolicy ? TPolicy["per" & keyof TPolicy] : undefined;
|
|
148
148
|
/**
|
|
149
|
-
* Policy names an API may reference:
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* here and
|
|
153
|
-
*
|
|
154
|
-
*
|
|
149
|
+
* Policy names an API may reference: never a policy without `per`, whose key
|
|
150
|
+
* only the code that charges it knows. A policy whose type does not settle
|
|
151
|
+
* its `per` is allowed here and checked at registration. A session-keyed
|
|
152
|
+
* policy is allowed here too, and refused of an API none of whose guards
|
|
153
|
+
* needs a session by LambderPublicSessionPolicyCheck, once its guards are
|
|
154
|
+
* known. Whether the API's input carries the fields an apiInput-keyed policy
|
|
155
|
+
* reads is asked of the names it declared, once that input is known, by
|
|
156
|
+
* LambderPolicyNamesInputLacks.
|
|
155
157
|
*/
|
|
156
|
-
export type LambderAllowedPolicyNames<TPolicies
|
|
157
|
-
[K in keyof TPolicies]: [
|
|
158
|
-
LambderPolicyPerOf<TPolicies[K]>
|
|
159
|
-
] extends [undefined] ? never : [LambderPolicyPerOf<TPolicies[K]>] extends ["session"] ? (TIncludeSession extends true ? K : never) : K;
|
|
158
|
+
export type LambderAllowedPolicyNames<TPolicies> = {
|
|
159
|
+
[K in keyof TPolicies]: [LambderPolicyPerOf<TPolicies[K]>] extends [undefined] ? never : K;
|
|
160
160
|
}[keyof TPolicies] & string;
|
|
161
161
|
/**
|
|
162
162
|
* The apiInput-keyed policies among `TNames` whose key fields the API's
|
|
163
|
-
* payload does not carry.
|
|
163
|
+
* payload does not carry. defineApi asks this once the API's
|
|
164
164
|
* input is known, of the names its rateLimit option holds; see
|
|
165
165
|
* LambderPayloadSliceCheck.
|
|
166
166
|
*
|
|
@@ -235,8 +235,8 @@ type LambderRateLimitOverrideFor<TPolicy> = TPolicy extends {
|
|
|
235
235
|
budget: "perPolicy";
|
|
236
236
|
} ? Pick<LambderRateLimitOverride, "refusal"> : LambderRateLimitOverride;
|
|
237
237
|
/** The map form's full shape: every referable policy name, each carrying its own override. */
|
|
238
|
-
type LambderRateLimitMap<TPolicies
|
|
239
|
-
readonly [K in LambderAllowedPolicyNames<TPolicies
|
|
238
|
+
type LambderRateLimitMap<TPolicies> = {
|
|
239
|
+
readonly [K in LambderAllowedPolicyNames<TPolicies> & keyof TPolicies]?: true | LambderRateLimitOverrideFor<TPolicies[K]>;
|
|
240
240
|
};
|
|
241
241
|
/**
|
|
242
242
|
* The per-API `rateLimit` option: one policy name, a non-empty ordered list
|
|
@@ -251,10 +251,10 @@ type LambderRateLimitMap<TPolicies, TIncludeSession extends boolean> = {
|
|
|
251
251
|
* It depends on the instance alone, never on the API's input, as
|
|
252
252
|
* LambderGuardsOption does; see LambderPayloadSliceCheck.
|
|
253
253
|
*/
|
|
254
|
-
export type LambderRateLimitOption<TPolicies
|
|
255
|
-
LambderAllowedPolicyNames<TPolicies
|
|
256
|
-
...LambderAllowedPolicyNames<TPolicies
|
|
257
|
-
] | LambderNonEmptyOptionMap<LambderRateLimitMap<TPolicies
|
|
254
|
+
export type LambderRateLimitOption<TPolicies> = LambderAllowedPolicyNames<TPolicies> | readonly [
|
|
255
|
+
LambderAllowedPolicyNames<TPolicies>,
|
|
256
|
+
...LambderAllowedPolicyNames<TPolicies>[]
|
|
257
|
+
] | LambderNonEmptyOptionMap<LambderRateLimitMap<TPolicies>>;
|
|
258
258
|
/**
|
|
259
259
|
* When in a call a policy is checked.
|
|
260
260
|
*
|