lambder 7.2.2 → 7.2.4
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 +45 -0
- package/dist/api/LambderApiSignature.d.ts +6 -4
- package/dist/api/LambderApiSignature.js +23 -8
- package/dist/client.d.ts +2 -2
- package/dist/client.js +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mock/LambderMockCreateOptions.d.ts +71 -10
- package/dist/mock/LambderMockTypes.d.ts +8 -2
- package/dist/shared/wire/LambderApiContract.d.ts +8 -4
- package/dist/shared/wire/LambderApiSignature.d.ts +37 -0
- package/dist/shared/wire/LambderApiSignature.js +36 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,51 @@ 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
|
+
## [7.2.4] - 2026-09-19
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **The mock's options are checked against the contract at compile time.**
|
|
17
|
+
Only the guard map was before, so a mock whose other options no longer fit
|
|
18
|
+
the contract compiled and then threw when its registry loaded, which in a
|
|
19
|
+
dev server is a blank page and an error in the browser console. Now each of
|
|
20
|
+
these is a type error at `create()`:
|
|
21
|
+
- `rateLimits.policies` leaving out a policy an endpoint references (a
|
|
22
|
+
policy added on the server and not to the mock);
|
|
23
|
+
- `sessions`, `idempotency` or `rateLimits` left out (or switched off)
|
|
24
|
+
while the contract has an endpoint that needs it, the way `guards`
|
|
25
|
+
already was;
|
|
26
|
+
- a guard with `session: true`, or a policy keyed `per: "session"`, where a
|
|
27
|
+
public endpoint names it;
|
|
28
|
+
- a `perPolicy` budget on a policy whose windows an endpoint overrides.
|
|
29
|
+
|
|
30
|
+
Each of these was already refused at registration, so a mock that compiles
|
|
31
|
+
now behaves as it did. The runtime checks stay for callers the types do not
|
|
32
|
+
reach.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`LambderContractRateLimitNames<C, M?>`**, from both entries: every
|
|
37
|
+
rate-limit policy name the contract references, the twin of
|
|
38
|
+
`LambderContractGuardNames`. Both now take an optional mode to read only the
|
|
39
|
+
public or only the session endpoints.
|
|
40
|
+
|
|
41
|
+
## [7.2.3] - 2026-09-19
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`extensibleEnum(schema)`**, exported from both entries: marks an enum
|
|
46
|
+
whose readers tolerate values they were not built with, and the signature
|
|
47
|
+
digest leaves its values out wherever it is output. A list that grows with
|
|
48
|
+
the product (roles, permissions, statuses) and rides in a widely returned
|
|
49
|
+
payload changed the signature of every endpoint returning it, so one new
|
|
50
|
+
permission reloaded every open tab. With the mark, the list growing or
|
|
51
|
+
shrinking reloads only the clients of endpoints that take it as input,
|
|
52
|
+
where its values still count because a removed value is a request the
|
|
53
|
+
server now refuses. The schema's type and validation are unchanged; the
|
|
54
|
+
mark is zod metadata, read from zod's shared registry. An unmarked enum
|
|
55
|
+
digests exactly as before, so upgrading changes no signature.
|
|
56
|
+
|
|
12
57
|
## [7.2.2] - 2026-09-18
|
|
13
58
|
|
|
14
59
|
### Added
|
|
@@ -24,9 +24,11 @@ export type LambderApiSignatureEntry = {
|
|
|
24
24
|
*
|
|
25
25
|
* The description is hashed as built, descriptions and titles included: a
|
|
26
26
|
* schema is what the server says it is, and a client built against a
|
|
27
|
-
* different one reloads once
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* the
|
|
27
|
+
* different one reloads once, with one exception the schema declares itself:
|
|
28
|
+
* the values of an extensibleEnum() in an output (see keepShapeOnly). What
|
|
29
|
+
* must hold for the digest to mean anything is that a schema is built from
|
|
30
|
+
* static values: one that reads the clock, a random source or the environment
|
|
31
|
+
* at construction digests differently in the generator's process and on the
|
|
32
|
+
* server.
|
|
31
33
|
*/
|
|
32
34
|
export declare const apiSignatureOf: (definition: LambderApiDefinition, guards: Record<string, LambderApiGuard<any, any, any>> | undefined) => Promise<string>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { toGuardEntries } from "./LambderApiGuards.js";
|
|
3
|
-
import { API_SIGNATURE_HEX_LENGTH } from "../shared/wire/LambderApiSignature.js";
|
|
3
|
+
import { API_SIGNATURE_HEX_LENGTH, EXTENSIBLE_ENUM_META_KEY } from "../shared/wire/LambderApiSignature.js";
|
|
4
4
|
import { sha256HexOf } from "../shared/util/LambderTextDigest.js";
|
|
5
5
|
/*
|
|
6
6
|
* The digest of an endpoint's client-facing shape, computed once, by the
|
|
@@ -30,7 +30,7 @@ const sortKeys = (value) => {
|
|
|
30
30
|
return sorted;
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* Three edits to every node zod emits, before it is hashed.
|
|
34
34
|
*
|
|
35
35
|
* The `default` keyword goes. Its value is server behaviour, not shape: a
|
|
36
36
|
* client never sends it, and its compiled types do not carry it. And for a
|
|
@@ -44,11 +44,24 @@ const sortKeys = (value) => {
|
|
|
44
44
|
*
|
|
45
45
|
* `required` is sorted. It is a set, and the order fields are declared in is
|
|
46
46
|
* not shape either; left as emitted, reordering two fields forced a reload.
|
|
47
|
+
*
|
|
48
|
+
* An enum marked with extensibleEnum() loses its values in an output. Its
|
|
49
|
+
* clients tolerate a value they do not know, so a response carrying one they
|
|
50
|
+
* were not built with, or no longer carrying one they were, changes nothing
|
|
51
|
+
* they can see; the node still says it holds a string. In an input the values
|
|
52
|
+
* stay, since a value dropped from the list is a request an older client may
|
|
53
|
+
* still send and the server now refuses. The mark itself goes in both, so
|
|
54
|
+
* marking an enum changes no input's digest.
|
|
47
55
|
*/
|
|
48
|
-
const keepShapeOnly = (node) => {
|
|
56
|
+
const keepShapeOnly = (node, io) => {
|
|
49
57
|
delete node.default;
|
|
50
58
|
if (Array.isArray(node.required))
|
|
51
59
|
node.required.sort();
|
|
60
|
+
if (node[EXTENSIBLE_ENUM_META_KEY] === true) {
|
|
61
|
+
delete node[EXTENSIBLE_ENUM_META_KEY];
|
|
62
|
+
if (io === "output")
|
|
63
|
+
delete node.enum;
|
|
64
|
+
}
|
|
52
65
|
};
|
|
53
66
|
/**
|
|
54
67
|
* A schema as JSON Schema, as zod emits it minus what keepShapeOnly removes.
|
|
@@ -56,7 +69,7 @@ const keepShapeOnly = (node) => {
|
|
|
56
69
|
* becomes `{}` rather than throwing, because a digest has to exist for every
|
|
57
70
|
* endpoint; what the digest cannot see is documented with it.
|
|
58
71
|
*/
|
|
59
|
-
const jsonSchemaOf = (schema, io) => schema ? z.toJSONSchema(schema, { io, unrepresentable: "any", override: ({ jsonSchema }) => keepShapeOnly(jsonSchema) }) : null;
|
|
72
|
+
const jsonSchemaOf = (schema, io) => schema ? z.toJSONSchema(schema, { io, unrepresentable: "any", override: ({ jsonSchema }) => keepShapeOnly(jsonSchema, io) }) : null;
|
|
60
73
|
const ownGuard = (guards, name) => guards !== undefined && Object.prototype.hasOwnProperty.call(guards, name) ? guards[name] : undefined;
|
|
61
74
|
/**
|
|
62
75
|
* The digest of an endpoint's client-facing shape: its name and mode, its
|
|
@@ -69,10 +82,12 @@ const ownGuard = (guards, name) => guards !== undefined && Object.prototype.hasO
|
|
|
69
82
|
*
|
|
70
83
|
* The description is hashed as built, descriptions and titles included: a
|
|
71
84
|
* schema is what the server says it is, and a client built against a
|
|
72
|
-
* different one reloads once
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* the
|
|
85
|
+
* different one reloads once, with one exception the schema declares itself:
|
|
86
|
+
* the values of an extensibleEnum() in an output (see keepShapeOnly). What
|
|
87
|
+
* must hold for the digest to mean anything is that a schema is built from
|
|
88
|
+
* static values: one that reads the clock, a random source or the environment
|
|
89
|
+
* at construction digests differently in the generator's process and on the
|
|
90
|
+
* server.
|
|
76
91
|
*/
|
|
77
92
|
export const apiSignatureOf = async (definition, guards) => {
|
|
78
93
|
const guardShapes = toGuardEntries(definition.guards).map(({ name }) => {
|
package/dist/client.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type { LambderApiTransport, LambderApiTransportRequest, LambderTransportF
|
|
|
15
15
|
export { LambderCookieJar, parseSetCookie } from "./shared/transport/LambderCookieJar.js";
|
|
16
16
|
export type { LambderStoredCookie } from "./shared/transport/LambderCookieJar.js";
|
|
17
17
|
export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
|
|
18
|
-
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH } from "./shared/wire/LambderApiSignature.js";
|
|
18
|
+
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
|
|
19
19
|
export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignature.js";
|
|
20
20
|
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
21
21
|
export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
|
|
@@ -23,7 +23,7 @@ export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallF
|
|
|
23
23
|
export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderIdempotencyKeyScope, LambderLogListHandler, } from "./client/LambderCaller.js";
|
|
24
24
|
export { LambderApiRefusal, isLambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
|
25
25
|
export type { LambderApiRefusalOptions, LambderRefusalMessage, LambderAppRefusalMessage, LambderRefusalCode, LambderRefuseOptions } from "./shared/wire/LambderApiRefusal.js";
|
|
26
|
-
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
|
|
26
|
+
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
|
|
27
27
|
export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
|
|
28
28
|
export type { LambderCrashDetail, LambderCrashCause } from "./shared/wire/LambderCrashDetail.js";
|
|
29
29
|
export { compressPayloadGzip, isRequestCompressionAvailable, COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from "./shared/wire/LambderRequestPayload.js";
|
package/dist/client.js
CHANGED
|
@@ -14,8 +14,9 @@ export { buildTransportEnvelope, LambderTransportFailure, isLambderTransportFail
|
|
|
14
14
|
export { lambderCookieJarTransport } from "./shared/transport/lambderCookieJarTransport.js";
|
|
15
15
|
export { LambderCookieJar, parseSetCookie } from "./shared/transport/LambderCookieJar.js";
|
|
16
16
|
export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
|
|
17
|
-
// The per-endpoint signature map a build ships with, how a caller reads it,
|
|
18
|
-
|
|
17
|
+
// The per-endpoint signature map a build ships with, how a caller reads it, the
|
|
18
|
+
// mark a shared schema sets on an enum its readers let grow, and the reload-loop window.
|
|
19
|
+
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
|
|
19
20
|
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
20
21
|
export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
|
|
21
22
|
// Typed API refusals (isomorphic: shared code may throw them from anywhere;
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type { LambderApiCallContext, LambderApiCallTrace } from "./api/LambderAp
|
|
|
27
27
|
export type { LambderApiDefinition } from "./api/LambderApiDefinition.js";
|
|
28
28
|
export { apiSignatureOf } from "./api/LambderApiSignature.js";
|
|
29
29
|
export type { LambderApiSignatureEntry } from "./api/LambderApiSignature.js";
|
|
30
|
-
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH } from "./shared/wire/LambderApiSignature.js";
|
|
30
|
+
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
|
|
31
31
|
export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignature.js";
|
|
32
32
|
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
33
33
|
export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
|
|
@@ -104,7 +104,7 @@ export type { LambderApiIdempotencyConfig } from "./api/LambderApiIdempotency.js
|
|
|
104
104
|
export type { LambderGuardsOptionValue, LambderRateLimitOverride, LambderRateLimitOptionValue, LambderApiIdempotencyOption, } from "./shared/wire/LambderApiOptionValues.js";
|
|
105
105
|
export { createLambderI18n } from "./shared/LambderI18n.js";
|
|
106
106
|
export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
|
|
107
|
-
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderApiNullAnswerConfig, LambderContractEntry, LambderMergeContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
|
|
107
|
+
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderApiNullAnswerConfig, LambderContractEntry, LambderMergeContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
|
|
108
108
|
export type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent, LambderHttpEventFormat } from "./core/LambderContext.js";
|
|
109
109
|
export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
|
|
110
110
|
export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export { LambderAnswerHeaders, getAnswerHeader, setAnswerHeader, addAnswerHeader
|
|
|
18
18
|
export { createApiCallContext } from "./api/LambderApiCallContext.js";
|
|
19
19
|
// Per-endpoint signatures: what a client build ships with, digested from the server's own registrations.
|
|
20
20
|
export { apiSignatureOf } from "./api/LambderApiSignature.js";
|
|
21
|
-
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH } from "./shared/wire/LambderApiSignature.js";
|
|
21
|
+
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
|
|
22
22
|
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
23
23
|
export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
|
|
24
24
|
export { buildApiEnvelope, envelopeAnswer, refusalAnswer, validationAnswer, apiNotFoundAnswer, sessionExpiredAnswer, versionExpiredAnswer, invalidPayloadAnswer, crashAnswer, API_ANSWER_CONTENT_TYPE, } from "./api/LambderApiEnvelope.js";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
|
|
2
|
-
import type { LambderContractGuardNames } from "../shared/wire/LambderApiContract.js";
|
|
2
|
+
import type { LambderContractGuardNames, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractRateLimitNames, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
|
|
3
3
|
import type { LambderApiGuard } from "../api/LambderApiGuards.js";
|
|
4
4
|
import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
|
|
5
5
|
import type { LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
6
6
|
import type { LambderApiTransport } from "../shared/transport/LambderApiTransport.js";
|
|
7
7
|
import type { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
8
8
|
import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
|
|
9
|
-
import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.js";
|
|
9
|
+
import type { LambderRateLimiter, LambderRateLimitWindow } from "../shared/contracts/LambderRateLimiter.js";
|
|
10
10
|
import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
11
11
|
import type { LambderSessionDataRefreshConfig } from "../session/LambderSessionManager.js";
|
|
12
12
|
import type { LambderSessionCookieOptions } from "../session/LambderSessionController.js";
|
|
@@ -60,19 +60,52 @@ export type LambderMockIdempotencyOptions<S = any> = {
|
|
|
60
60
|
*/
|
|
61
61
|
callerIdentity?: (ctx: Omit<LambderMockCallContext<S>, "session">, request: LambderApiRequest) => string | null | Promise<string | null>;
|
|
62
62
|
};
|
|
63
|
+
/** Policy names whose windows some endpoint of the contract overrides, in the map form of its rateLimit option. */
|
|
64
|
+
type LambderContractWindowOverrideNames<C> = {
|
|
65
|
+
[K in keyof C]: LambderWindowOverrideNamesIn<LambderContractRateLimitOf<C, K>>;
|
|
66
|
+
}[keyof C] & string;
|
|
67
|
+
type LambderWindowOverrideNamesIn<R> = R extends string | readonly string[] ? never : {
|
|
68
|
+
[N in keyof R]: R[N] extends object ? ([Extract<keyof R[N], LambderRateLimitWindow>] extends [never] ? never : N) : never;
|
|
69
|
+
}[keyof R];
|
|
70
|
+
/** Endpoint names whose idempotency option asks for a store (`false` is an opt-out and asks for none). */
|
|
71
|
+
type LambderContractIdempotentKeys<C> = {
|
|
72
|
+
[K in keyof C]: [LambderContractIdempotencyOf<C, K>] extends [never] ? never : LambderContractIdempotencyOf<C, K> extends false ? never : K;
|
|
73
|
+
}[keyof C];
|
|
63
74
|
/**
|
|
64
75
|
* The rate limits option: the policies endpoints may restate, the limiter
|
|
65
76
|
* they are counted on, and what happens when that limiter throws.
|
|
66
77
|
*
|
|
78
|
+
* The policies are held to what the server's own policies were held to when
|
|
79
|
+
* it registered the same endpoints, because an entry the mock's copy does not
|
|
80
|
+
* fit cannot be registered, and this makes that an error at the option rather
|
|
81
|
+
* than a throw when the registry loads. So the map names every policy the
|
|
82
|
+
* contract references, a policy a public endpoint names is not keyed per
|
|
83
|
+
* session, and a policy whose windows an endpoint overrides keeps a per-API
|
|
84
|
+
* budget. Policies the contract does not reference may be added freely.
|
|
85
|
+
*
|
|
67
86
|
* `failOpen` is the server's own option (see LambderApiRateLimitsConfig) and
|
|
68
87
|
* is here for the reason the idempotency option's twin is: with a limiter of
|
|
69
88
|
* the app's own that fails, a mock that cannot express it always lets the
|
|
70
89
|
* call through, so it answers 200 where a server configured to refuse answers
|
|
71
90
|
* 429.
|
|
72
91
|
*/
|
|
73
|
-
type LambderMockRateLimitsOptions<S, P extends LambderMockRateLimitPolicies<S>> = {
|
|
92
|
+
type LambderMockRateLimitsOptions<C, S, P extends LambderMockRateLimitPolicies<S>> = {
|
|
74
93
|
policies: P & {
|
|
75
94
|
[N in keyof P]: LambderMockSurplusKeys<P[N], LambderApiRateLimitPolicyConfig<LambderMockCallContext<S>>>;
|
|
95
|
+
} & {
|
|
96
|
+
[N in LambderContractRateLimitNames<C>]: LambderApiRateLimitPolicyConfig<LambderMockCallContext<S>>;
|
|
97
|
+
} & {
|
|
98
|
+
[N in keyof P & LambderContractRateLimitNames<C, "public">]: P[N] extends {
|
|
99
|
+
per: "session";
|
|
100
|
+
} ? {
|
|
101
|
+
per: never;
|
|
102
|
+
} : unknown;
|
|
103
|
+
} & {
|
|
104
|
+
[N in keyof P & LambderContractWindowOverrideNames<C>]: P[N] extends {
|
|
105
|
+
budget: "perPolicy";
|
|
106
|
+
} ? {
|
|
107
|
+
budget: never;
|
|
108
|
+
} : unknown;
|
|
76
109
|
};
|
|
77
110
|
/** Where attempts are counted. Default: a fresh LambderMemoryRateLimiter. */
|
|
78
111
|
limiter?: LambderRateLimiter;
|
|
@@ -95,11 +128,45 @@ type LambderMockGuardsOption<C, S, G> = [
|
|
|
95
128
|
} : {
|
|
96
129
|
guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G>;
|
|
97
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* The sessions, idempotency and rateLimits options: each required whenever
|
|
133
|
+
* the contract has an endpoint that needs it, for the guards option's reason.
|
|
134
|
+
* An entry that needs one the mock was created without cannot be registered,
|
|
135
|
+
* so leaving it out is an error here rather than a throw when the registry
|
|
136
|
+
* loads.
|
|
137
|
+
*/
|
|
138
|
+
type LambderMockSessionsOption<C, S> = [
|
|
139
|
+
LambderContractKeysWithMode<C, "session">
|
|
140
|
+
] extends [never] ? {
|
|
141
|
+
/** Sessions over the memory store: `true` for the defaults, or the options. Off by default. */
|
|
142
|
+
sessions?: boolean | LambderMockSessionsOptions<S>;
|
|
143
|
+
} : {
|
|
144
|
+
/** Sessions over the memory store: `true` for the defaults, or the options. Required: the contract has session endpoints. */
|
|
145
|
+
sessions: true | LambderMockSessionsOptions<S>;
|
|
146
|
+
};
|
|
147
|
+
type LambderMockIdempotencyOption<C, S, I> = [
|
|
148
|
+
LambderContractIdempotentKeys<C>
|
|
149
|
+
] extends [never] ? {
|
|
150
|
+
/** Idempotency over a memory store: `true` for the defaults, or the options. Off by default. */
|
|
151
|
+
idempotency?: I & LambderMockSurplusKeys<I, LambderMockIdempotencyOptions<S>>;
|
|
152
|
+
} : {
|
|
153
|
+
/** Idempotency over a memory store: `true` for the defaults, or the options. Required: the contract has idempotent endpoints. */
|
|
154
|
+
idempotency: I & ([I] extends [false] ? never : unknown) & LambderMockSurplusKeys<I, LambderMockIdempotencyOptions<S>>;
|
|
155
|
+
};
|
|
156
|
+
type LambderMockRateLimitsOption<C, S, P extends LambderMockRateLimitPolicies<S>> = [
|
|
157
|
+
LambderContractRateLimitNames<C>
|
|
158
|
+
] extends [never] ? {
|
|
159
|
+
/** The rate-limit policies endpoints may restate, over a memory limiter unless one is given. Off by default. */
|
|
160
|
+
rateLimits?: LambderMockRateLimitsOptions<C, S, P>;
|
|
161
|
+
} : {
|
|
162
|
+
/** The rate-limit policies endpoints may restate, over a memory limiter unless one is given. Required: the contract references policies. */
|
|
163
|
+
rateLimits: LambderMockRateLimitsOptions<C, S, P>;
|
|
164
|
+
};
|
|
98
165
|
/** Each guard checked for surplus keys, so `sesion: true` on an inline guard is an error at the key rather than a guard that silently runs as public. */
|
|
99
166
|
type LambderMockGuardShapes<S, G> = {
|
|
100
167
|
[N in keyof G]: LambderMockSurplusKeys<G[N], LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>>;
|
|
101
168
|
};
|
|
102
|
-
export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicies<S> = LambderMockRateLimitPolicies<S>, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>> = LambderMockGuardsOption<C, S, G> & {
|
|
169
|
+
export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicies<S> = LambderMockRateLimitPolicies<S>, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>> = LambderMockGuardsOption<C, S, G> & LambderMockSessionsOption<C, S> & LambderMockIdempotencyOption<C, S, I> & LambderMockRateLimitsOption<C, S, P> & {
|
|
103
170
|
/** Stamped on every answer's envelope as apiVersion, as the server's option is. */
|
|
104
171
|
apiVersion?: string;
|
|
105
172
|
/** The version floor, as on the server: a call naming a lower `version` answers versionExpired whatever its signature says. */
|
|
@@ -108,12 +175,6 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
|
|
|
108
175
|
apiSignatures?: LambderApiSignatureMap;
|
|
109
176
|
/** Artificial latency per call; off by default. */
|
|
110
177
|
latency?: LambderMockLatency;
|
|
111
|
-
/** Sessions over the memory store: `true` for the defaults, or the options. Off by default: session endpoints then fail at registration. */
|
|
112
|
-
sessions?: boolean | LambderMockSessionsOptions<S>;
|
|
113
|
-
/** The rate-limit policies endpoints may restate, over a memory limiter unless one is given. Off by default. */
|
|
114
|
-
rateLimits?: LambderMockRateLimitsOptions<S, P>;
|
|
115
|
-
/** Idempotency over a memory store: `true` for the defaults, or the options. Off by default. */
|
|
116
|
-
idempotency?: I & LambderMockSurplusKeys<I, LambderMockIdempotencyOptions<S>>;
|
|
117
178
|
/**
|
|
118
179
|
* The host the runtime's cookies belong to: what signIn plants them
|
|
119
180
|
* under, what the direct transport's jar scopes them by, what the MSW
|
|
@@ -90,8 +90,10 @@ export type LambderMockContext<C, K extends keyof C, S, G> = Omit<LambderMockCal
|
|
|
90
90
|
* The guard map a mock app must declare: one guard per name any endpoint of
|
|
91
91
|
* the contract declares, and for every guard the contract knows in
|
|
92
92
|
* guardInput mode, a `guardInput` schema whose output is what the server
|
|
93
|
-
* inferred. A
|
|
94
|
-
*
|
|
93
|
+
* inferred. A guard a public endpoint names may not require a session, since
|
|
94
|
+
* an entry naming one cannot be registered. A missing name, a schema that
|
|
95
|
+
* parses to something else, or a session guard where the contract has a
|
|
96
|
+
* public endpoint fails at the `guards` option.
|
|
95
97
|
*/
|
|
96
98
|
export type LambderMockGuards<C, S = any> = {
|
|
97
99
|
[N in LambderContractGuardNames<C>]: LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>;
|
|
@@ -99,6 +101,10 @@ export type LambderMockGuards<C, S = any> = {
|
|
|
99
101
|
[N in LambderContractGuardInputNames<C>]: {
|
|
100
102
|
guardInput: z.ZodType<LambderContractGuardInput<C, N>, any>;
|
|
101
103
|
};
|
|
104
|
+
} & {
|
|
105
|
+
[N in LambderContractGuardNames<C, "public">]: {
|
|
106
|
+
session?: false;
|
|
107
|
+
};
|
|
102
108
|
};
|
|
103
109
|
export type LambderMockHandler<C, K extends keyof C, S, G> = (ctx: LambderMockContext<C, K, S, G>) => LambderMockOutputOf<C, K> | Promise<LambderMockOutputOf<C, K>>;
|
|
104
110
|
/**
|
|
@@ -99,10 +99,10 @@ export type LambderContractKeysWithMode<C, M extends LambderApiMode> = {
|
|
|
99
99
|
export type LambderContractGuardsOf<C, K extends keyof C> = C[K] extends {
|
|
100
100
|
guards: infer G;
|
|
101
101
|
} ? G : never;
|
|
102
|
-
/** Every guard name any endpoint of the contract declares. */
|
|
103
|
-
export type LambderContractGuardNames<C> = {
|
|
104
|
-
[K in
|
|
105
|
-
}[
|
|
102
|
+
/** Every guard name any endpoint of the contract declares, or only the endpoints of mode M. */
|
|
103
|
+
export type LambderContractGuardNames<C, M extends LambderApiMode = LambderApiMode> = {
|
|
104
|
+
[K in LambderContractKeysWithMode<C, M>]: LambderGuardNamesIn<LambderContractGuardsOf<C, K>>;
|
|
105
|
+
}[LambderContractKeysWithMode<C, M>] & string;
|
|
106
106
|
/** The endpoint's guardInputs requirement, or never when its guards take no client input. */
|
|
107
107
|
export type LambderContractGuardInputsOf<C, K extends keyof C> = C[K] extends {
|
|
108
108
|
guardInputs: infer G;
|
|
@@ -123,6 +123,10 @@ export type LambderContractGuardInputNames<C> = {
|
|
|
123
123
|
export type LambderContractRateLimitOf<C, K extends keyof C> = C[K] extends {
|
|
124
124
|
rateLimit: infer R;
|
|
125
125
|
} ? R : never;
|
|
126
|
+
/** Every rate-limit policy name any endpoint of the contract references, or only the endpoints of mode M. The rateLimit option takes the guards option's three forms, so the names come out the same way. */
|
|
127
|
+
export type LambderContractRateLimitNames<C, M extends LambderApiMode = LambderApiMode> = {
|
|
128
|
+
[K in LambderContractKeysWithMode<C, M>]: LambderGuardNamesIn<LambderContractRateLimitOf<C, K>>;
|
|
129
|
+
}[LambderContractKeysWithMode<C, M>] & string;
|
|
126
130
|
/** The endpoint's idempotency option as written, or never. */
|
|
127
131
|
export type LambderContractIdempotencyOf<C, K extends keyof C> = C[K] extends {
|
|
128
132
|
idempotency: infer I;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
1
2
|
/**
|
|
2
3
|
* The per-endpoint signatures a client carries, generated from the server's
|
|
3
4
|
* own registrations (Lambder.apiSignatures()) and shipped with the client
|
|
@@ -44,3 +45,39 @@ export declare const lookupApiSignature: (signatures: LambderApiSignatureMap, ap
|
|
|
44
45
|
* run instead of saying so.
|
|
45
46
|
*/
|
|
46
47
|
export declare const readApiSignature: (signatures: LambderApiSignatureMap, apiName: string) => Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* The metadata key extensibleEnum() sets and the digest reads. Namespaced,
|
|
50
|
+
* because zod writes metadata into the JSON Schema it emits, so the key shows
|
|
51
|
+
* up in any schema an app converts for itself, where OpenAPI's own
|
|
52
|
+
* `x-extensible-enum` means something else (the values, in place of `enum`).
|
|
53
|
+
*/
|
|
54
|
+
export declare const EXTENSIBLE_ENUM_META_KEY = "x-lambder-extensible-enum";
|
|
55
|
+
/**
|
|
56
|
+
* Marks an enum whose clients tolerate a value they were not built with, so
|
|
57
|
+
* its list of values stays out of the signature of every endpoint that
|
|
58
|
+
* returns it. Adding a role, a status or a locale to a list that rides in a
|
|
59
|
+
* widely returned payload (a session, a profile) then reloads only the
|
|
60
|
+
* clients that send the list back, not every client that reads it.
|
|
61
|
+
*
|
|
62
|
+
* Where the enum is input its values still count: a value dropped from the
|
|
63
|
+
* list is a request an older client may still send and the server now
|
|
64
|
+
* refuses, so that endpoint's clients must reload. Everything else about the
|
|
65
|
+
* schema is untouched: its type, its validation on both sides, and what it
|
|
66
|
+
* is everywhere outside the digest.
|
|
67
|
+
*
|
|
68
|
+
* The mark is a promise the schema makes for its readers, and nothing checks
|
|
69
|
+
* it. A client that switches over every value with no fallback, or indexes a
|
|
70
|
+
* map by one, renders a value it does not know as nothing, or throws. Mark
|
|
71
|
+
* only a list every reader handles that way on purpose.
|
|
72
|
+
*
|
|
73
|
+
* It is zod metadata (`.meta()`), which zod keeps in one registry on
|
|
74
|
+
* globalThis, so an enum marked in a shared package is read by the digest
|
|
75
|
+
* even when the server resolves another copy of zod. A schema derived from a
|
|
76
|
+
* marked enum by rebuilding it (`z.enum(marked.options)`, `.exclude()`)
|
|
77
|
+
* carries no mark and counts in full, which costs a reload, never a missed
|
|
78
|
+
* one.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* export const RoleSchema = extensibleEnum(z.enum(["admin", "member"]));
|
|
82
|
+
*/
|
|
83
|
+
export declare const extensibleEnum: <TSchema extends z.ZodEnum>(schema: TSchema) => TSchema;
|
|
@@ -43,3 +43,39 @@ export const readApiSignature = async (signatures, apiName) => {
|
|
|
43
43
|
}
|
|
44
44
|
return signature;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* The metadata key extensibleEnum() sets and the digest reads. Namespaced,
|
|
48
|
+
* because zod writes metadata into the JSON Schema it emits, so the key shows
|
|
49
|
+
* up in any schema an app converts for itself, where OpenAPI's own
|
|
50
|
+
* `x-extensible-enum` means something else (the values, in place of `enum`).
|
|
51
|
+
*/
|
|
52
|
+
export const EXTENSIBLE_ENUM_META_KEY = "x-lambder-extensible-enum";
|
|
53
|
+
/**
|
|
54
|
+
* Marks an enum whose clients tolerate a value they were not built with, so
|
|
55
|
+
* its list of values stays out of the signature of every endpoint that
|
|
56
|
+
* returns it. Adding a role, a status or a locale to a list that rides in a
|
|
57
|
+
* widely returned payload (a session, a profile) then reloads only the
|
|
58
|
+
* clients that send the list back, not every client that reads it.
|
|
59
|
+
*
|
|
60
|
+
* Where the enum is input its values still count: a value dropped from the
|
|
61
|
+
* list is a request an older client may still send and the server now
|
|
62
|
+
* refuses, so that endpoint's clients must reload. Everything else about the
|
|
63
|
+
* schema is untouched: its type, its validation on both sides, and what it
|
|
64
|
+
* is everywhere outside the digest.
|
|
65
|
+
*
|
|
66
|
+
* The mark is a promise the schema makes for its readers, and nothing checks
|
|
67
|
+
* it. A client that switches over every value with no fallback, or indexes a
|
|
68
|
+
* map by one, renders a value it does not know as nothing, or throws. Mark
|
|
69
|
+
* only a list every reader handles that way on purpose.
|
|
70
|
+
*
|
|
71
|
+
* It is zod metadata (`.meta()`), which zod keeps in one registry on
|
|
72
|
+
* globalThis, so an enum marked in a shared package is read by the digest
|
|
73
|
+
* even when the server resolves another copy of zod. A schema derived from a
|
|
74
|
+
* marked enum by rebuilding it (`z.enum(marked.options)`, `.exclude()`)
|
|
75
|
+
* carries no mark and counts in full, which costs a reload, never a missed
|
|
76
|
+
* one.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* export const RoleSchema = extensibleEnum(z.enum(["admin", "member"]));
|
|
80
|
+
*/
|
|
81
|
+
export const extensibleEnum = (schema) => schema.meta({ [EXTENSIBLE_ENUM_META_KEY]: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.4",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|