lambder 8.1.2 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +208 -0
- package/README.md +23 -31
- package/dist/api/LambderApiCallContext.d.ts +31 -1
- package/dist/api/LambderApiCallContext.js +8 -0
- package/dist/api/LambderApiDefinition.d.ts +2 -2
- package/dist/api/LambderApiEnvelope.d.ts +1 -1
- package/dist/api/LambderApiEnvelope.js +3 -4
- package/dist/api/LambderApiGuards.d.ts +2 -17
- package/dist/api/LambderApiIdempotency.js +5 -7
- package/dist/api/LambderApiRateLimits.d.ts +2 -29
- package/dist/build/generatedTables.d.ts +72 -0
- package/dist/build/generatedTables.js +99 -0
- package/dist/build/writeApiGuardParams.d.ts +60 -0
- package/dist/build/writeApiGuardParams.js +85 -0
- package/dist/build/writeApiOptions.d.ts +68 -0
- package/dist/build/writeApiOptions.js +102 -0
- package/dist/build.d.ts +10 -4
- package/dist/build.js +7 -4
- package/dist/client/LambderCaller.d.ts +0 -4
- package/dist/client/LambderCaller.js +1 -9
- package/dist/client/LambderUploadRunner.d.ts +7 -7
- package/dist/client/LambderUploadRunner.js +12 -21
- package/dist/client.d.ts +7 -0
- package/dist/client.js +11 -0
- package/dist/core/Lambder.d.ts +71 -12
- package/dist/core/Lambder.js +116 -38
- package/dist/core/LambderContext.d.ts +9 -6
- package/dist/core/LambderContext.js +2 -1
- package/dist/core/LambderResolver.d.ts +6 -12
- package/dist/core/LambderResolver.js +2 -14
- package/dist/core/LambderResponseBuilder.d.ts +15 -70
- package/dist/core/LambderResponseBuilder.js +15 -99
- package/dist/index.d.ts +16 -3
- package/dist/index.js +14 -1
- package/dist/invoke/LambderInvokeCaller.js +3 -4
- package/dist/mock/LambderMockApp.d.ts +34 -17
- package/dist/mock/LambderMockApp.js +69 -24
- package/dist/mock/LambderMockCreateOptions.d.ts +70 -7
- package/dist/mock/LambderMockTypes.d.ts +31 -21
- package/dist/mock/lambderMockPoliciesFrom.d.ts +51 -0
- package/dist/mock/lambderMockPoliciesFrom.js +46 -0
- package/dist/mock.d.ts +3 -0
- package/dist/mock.js +3 -0
- package/dist/secrets/LambderOneShotSecrets.d.ts +166 -0
- package/dist/secrets/LambderOneShotSecrets.js +217 -0
- package/dist/session/LambderSessionCrypto.js +6 -16
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +3 -2
- package/dist/shared/contracts/LambderOneShotSecretStore.d.ts +122 -0
- package/dist/shared/contracts/LambderOneShotSecretStore.js +38 -0
- package/dist/shared/util/LambderBackoffTimer.d.ts +82 -0
- package/dist/shared/util/LambderBackoffTimer.js +86 -0
- package/dist/shared/util/LambderBase64.d.ts +14 -0
- package/dist/shared/util/LambderBase64.js +17 -0
- package/dist/shared/util/LambderSignedClaims.d.ts +78 -0
- package/dist/shared/util/LambderSignedClaims.js +109 -0
- package/dist/shared/util/LambderTextDigest.d.ts +19 -5
- package/dist/shared/util/LambderTextDigest.js +30 -5
- package/dist/shared/util/LambderTypeUtilities.d.ts +18 -0
- package/dist/shared/util/assertPlainData.d.ts +9 -0
- package/dist/shared/util/assertPlainData.js +41 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +3 -2
- package/dist/shared/wire/LambderAnswerHeaders.js +3 -2
- package/dist/shared/wire/LambderApiContract.d.ts +9 -14
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +148 -0
- package/dist/shared/wire/LambderApiOptionEntries.js +35 -0
- package/dist/shared/wire/LambderApiRefusal.d.ts +3 -4
- package/dist/shared/wire/LambderApiRefusal.js +3 -4
- package/dist/stores/LambderDdbOneShotSecretStore.d.ts +64 -0
- package/dist/stores/LambderDdbOneShotSecretStore.js +266 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +3 -2
- package/dist/stores/LambderMemoryIdempotencyStore.js +3 -2
- package/dist/stores/LambderMemoryOneShotSecretStore.d.ts +36 -0
- package/dist/stores/LambderMemoryOneShotSecretStore.js +93 -0
- package/dist/testing/LambderConformanceRunner.d.ts +46 -0
- package/dist/testing/LambderConformanceRunner.js +21 -0
- package/dist/testing/lambderIdempotencyStoreConformance.d.ts +33 -0
- package/dist/testing/lambderIdempotencyStoreConformance.js +237 -0
- package/dist/testing/lambderOneShotSecretStoreConformance.d.ts +43 -0
- package/dist/testing/lambderOneShotSecretStoreConformance.js +224 -0
- package/dist/testing/lambderRateLimiterConformance.d.ts +20 -0
- package/dist/testing/lambderRateLimiterConformance.js +72 -0
- package/dist/testing/lambderSessionStoreConformance.d.ts +27 -0
- package/dist/testing/lambderSessionStoreConformance.js +165 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +12 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
|
|
2
2
|
import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
3
|
-
import { bindCallTools, createApiCallContext } from "../api/LambderApiCallContext.js";
|
|
3
|
+
import { bindCallTools, createApiCallContext, responseToolsOf } from "../api/LambderApiCallContext.js";
|
|
4
4
|
import { toHttpAnswer } from "../api/LambderApiAnswer.js";
|
|
5
5
|
import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
6
6
|
import { buildApiEnvelope, envelopeAnswer, crashAnswer, } from "../api/LambderApiEnvelope.js";
|
|
@@ -47,7 +47,8 @@ const defaultCookieHost = () => globalThis.location?.host || "localhost";
|
|
|
47
47
|
*
|
|
48
48
|
* Create one with initLambderMock<Contract, SessionData>().create(...),
|
|
49
49
|
* which fixes the contract and session types first so everything else is
|
|
50
|
-
* inferred from the options.
|
|
50
|
+
* inferred from the options. `TDerived` is true for a mock created with the
|
|
51
|
+
* generated `apiOptions` table, whose entries are their handlers alone.
|
|
51
52
|
*/
|
|
52
53
|
export class LambderMockApp {
|
|
53
54
|
apiVersion;
|
|
@@ -85,10 +86,13 @@ export class LambderMockApp {
|
|
|
85
86
|
recorder;
|
|
86
87
|
/** Registered entries and the overrides over them (see LambderMockEntryRegistry). */
|
|
87
88
|
registry = new LambderMockEntryRegistry();
|
|
89
|
+
/** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
|
|
90
|
+
apiOptions;
|
|
88
91
|
/** The jars the runtime owns and what it planted in document.cookie (see LambderMockBrowserCookies). */
|
|
89
92
|
browserCookies = new LambderMockBrowserCookies();
|
|
90
93
|
constructor(options) {
|
|
91
94
|
this.apiVersion = options.apiVersion ?? null;
|
|
95
|
+
this.apiOptions = options.apiOptions ?? null;
|
|
92
96
|
this.failures = new LambderMockFailureInjector({ apiVersion: this.apiVersion, latency: options.latency ?? 0 });
|
|
93
97
|
this.recorder = new LambderMockCallRecorder({ callLogSize: options.callLogSize ?? DEFAULT_CALL_LOG_SIZE });
|
|
94
98
|
// The loopback address when nothing names a client, as for a request
|
|
@@ -196,13 +200,49 @@ export class LambderMockApp {
|
|
|
196
200
|
throw new Error(`LambderMockApp: session endpoint "${definition.name}" needs the sessions option at creation.`);
|
|
197
201
|
}
|
|
198
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* The table's entry for an endpoint, when create() was given one: null
|
|
205
|
+
* without a table, and a throw for a name the table does not hold or an
|
|
206
|
+
* entry registered under the other mode. The compiler already refuses
|
|
207
|
+
* both against the contract; this is where a stale table meets a caller
|
|
208
|
+
* the compiler did not see.
|
|
209
|
+
*/
|
|
210
|
+
declaredOptionsOf(name, mode) {
|
|
211
|
+
if (!this.apiOptions)
|
|
212
|
+
return null;
|
|
213
|
+
const declared = Object.prototype.hasOwnProperty.call(this.apiOptions, name) ? this.apiOptions[name] : undefined;
|
|
214
|
+
if (!declared) {
|
|
215
|
+
throw new Error(`LambderMockApp: "${name}" has no entry in the apiOptions table given to create(). The table predates this endpoint: regenerate it with writeApiOptions.`);
|
|
216
|
+
}
|
|
217
|
+
if (declared.mode !== mode) {
|
|
218
|
+
throw new Error(`LambderMockApp: "${name}" is a ${declared.mode} endpoint in the apiOptions table, registered here as a ${mode} one.`);
|
|
219
|
+
}
|
|
220
|
+
return declared;
|
|
221
|
+
}
|
|
222
|
+
/** The mode the apiOptions table gives a name, or null without a table or for a name it does not hold. */
|
|
223
|
+
declaredModeOf(name) {
|
|
224
|
+
if (!this.apiOptions || !Object.prototype.hasOwnProperty.call(this.apiOptions, name))
|
|
225
|
+
return null;
|
|
226
|
+
return this.apiOptions[name].mode;
|
|
227
|
+
}
|
|
199
228
|
buildEntry(name, mode, input) {
|
|
200
229
|
const options = (typeof input === "function" ? { handler: input } : input);
|
|
230
|
+
const declared = this.declaredOptionsOf(name, mode);
|
|
231
|
+
if (declared) {
|
|
232
|
+
// A caller the compiler did not see (a JavaScript slice, a cast)
|
|
233
|
+
// would otherwise have its restatement silently lose to the table.
|
|
234
|
+
for (const field of ["guards", "rateLimit", "idempotency"]) {
|
|
235
|
+
if (options[field] !== undefined) {
|
|
236
|
+
throw new Error(`LambderMockApp: "${name}" restates its ${field} option, which the apiOptions table given to create() already declares. Leave it out of the entry.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const declarations = declared ?? options;
|
|
201
241
|
const definition = {
|
|
202
242
|
name, mode,
|
|
203
|
-
guards:
|
|
204
|
-
rateLimit:
|
|
205
|
-
idempotency:
|
|
243
|
+
guards: declarations.guards,
|
|
244
|
+
rateLimit: declarations.rateLimit,
|
|
245
|
+
idempotency: declarations.idempotency,
|
|
206
246
|
// No cast: the entry's schema is a z.ZodType, the same type the
|
|
207
247
|
// definition holds. A structural { safeParse } here would let a
|
|
208
248
|
// validator that is not a zod schema reach the 422 body as
|
|
@@ -257,18 +297,21 @@ export class LambderMockApp {
|
|
|
257
297
|
* still refused, and an entry registered later (registerPartial, or a
|
|
258
298
|
* second register) takes its endpoint back from the rest.
|
|
259
299
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
300
|
+
* The mode of an unregistered name comes from the apiOptions table, when
|
|
301
|
+
* create() was given one, so an unmocked session endpoint still reads the
|
|
302
|
+
* session and a signed-out call answers sessionExpired as on the server.
|
|
303
|
+
* Without the table the mode is not knowable at runtime (the contract is
|
|
304
|
+
* a type), and a call it answers is processed as public: the protocol's
|
|
305
|
+
* pre-pass still runs, so a stale client still hears versionExpired, but
|
|
306
|
+
* a signed-out call to an unmocked session endpoint answers "not mocked"
|
|
307
|
+
* where the server answers sessionExpired. Declare an endpoint whose
|
|
308
|
+
* signed-out path a test cares about with sessionNotMocked there.
|
|
267
309
|
*/
|
|
268
310
|
restNotMocked(reason) {
|
|
269
311
|
return { restNotMockedReason: reason };
|
|
270
312
|
}
|
|
271
313
|
buildNotMockedEntry(name, mode, reason) {
|
|
314
|
+
this.declaredOptionsOf(name, mode);
|
|
272
315
|
const definition = { name, mode };
|
|
273
316
|
this.assertEntryRegistration(definition);
|
|
274
317
|
return { name, mode, definition, handler: null, notMockedReason: reason };
|
|
@@ -352,16 +395,18 @@ export class LambderMockApp {
|
|
|
352
395
|
/**
|
|
353
396
|
* The entry that answers a name nothing registered, when register() was
|
|
354
397
|
* given a rest entry: the notMocked refusal carrying its reason, run
|
|
355
|
-
* through the pipeline
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
398
|
+
* through the pipeline under the mode the apiOptions table gives the
|
|
399
|
+
* name, and as a public endpoint where there is no table to say (the
|
|
400
|
+
* mode of an unregistered name cannot otherwise be recovered at runtime).
|
|
401
|
+
* Everything before dispatch still runs (the signature gate, the payload
|
|
402
|
+
* restore, and for a session endpoint the session read).
|
|
359
403
|
*/
|
|
360
404
|
restNotMockedEntry(apiName) {
|
|
361
405
|
const reason = this.registry.restNotMockedReason;
|
|
362
406
|
if (reason === null)
|
|
363
407
|
return null;
|
|
364
|
-
|
|
408
|
+
const mode = this.declaredModeOf(apiName) ?? "public";
|
|
409
|
+
return { name: apiName, mode, definition: { name: apiName, mode }, handler: null, notMockedReason: reason };
|
|
365
410
|
}
|
|
366
411
|
// -----------------------------------------------------------------------
|
|
367
412
|
// Control surface
|
|
@@ -517,7 +562,6 @@ export class LambderMockApp {
|
|
|
517
562
|
apiName: request.apiName,
|
|
518
563
|
request,
|
|
519
564
|
signal: request.signal ?? new AbortController().signal,
|
|
520
|
-
envelope: {},
|
|
521
565
|
payload: request.payload,
|
|
522
566
|
guardInputs: request.guardInputs,
|
|
523
567
|
// The key as a handler can use it. A non-string is not a key: the
|
|
@@ -546,6 +590,7 @@ export class LambderMockApp {
|
|
|
546
590
|
methods: {
|
|
547
591
|
rateLimit: async (policy, key) => { await chargeRateLimit(policy, key, true); },
|
|
548
592
|
isRateLimited: (policy, key) => chargeRateLimit(policy, key, false),
|
|
593
|
+
...responseToolsOf(ctx, request.host),
|
|
549
594
|
},
|
|
550
595
|
});
|
|
551
596
|
return ctx;
|
|
@@ -581,12 +626,13 @@ export class LambderMockApp {
|
|
|
581
626
|
const id = this.recorder.nextCallId();
|
|
582
627
|
const registered = this.entryFor(request.apiName);
|
|
583
628
|
// The rest entry answers whatever nothing registered, when register()
|
|
584
|
-
// was given one. The mode reported
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
629
|
+
// was given one. The mode reported is the registered entry's, or the
|
|
630
|
+
// apiOptions table's for a name it holds, and null otherwise, exactly
|
|
631
|
+
// as for a name nothing knows: without the table a rest answer is
|
|
632
|
+
// processed as public, which is a property of the answer rather than
|
|
633
|
+
// a claim about the endpoint.
|
|
588
634
|
const entry = registered ?? this.restNotMockedEntry(request.apiName);
|
|
589
|
-
const mode = registered?.mode ??
|
|
635
|
+
const mode = registered?.mode ?? this.declaredModeOf(request.apiName);
|
|
590
636
|
const facts = this.callFacts(id, request, mode);
|
|
591
637
|
const startedAt = facts.startedAt;
|
|
592
638
|
const ctx = this.createContext(request);
|
|
@@ -637,7 +683,6 @@ export class LambderMockApp {
|
|
|
637
683
|
callCtx.payload = request.payload;
|
|
638
684
|
const payload = await handler(callCtx);
|
|
639
685
|
return envelopeAnswer(buildApiEnvelope(this.apiVersion, payload === undefined ? null : payload, {
|
|
640
|
-
message: callCtx.envelope.message,
|
|
641
686
|
logList: callCtx.logList,
|
|
642
687
|
}));
|
|
643
688
|
}
|
|
@@ -3,8 +3,9 @@ import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.
|
|
|
3
3
|
import type { LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
|
|
4
4
|
import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
5
5
|
import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
6
|
-
import type { LambderContractGuardNames, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractRateLimitNames, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
|
|
6
|
+
import type { LambderContractGuardNames, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitNames, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
|
|
7
7
|
import type { LambderApiGuard } from "../api/LambderApiGuards.js";
|
|
8
|
+
import type { LambderApiOptionEntry, LambderGuardDeclarationEntry } from "../shared/wire/LambderApiOptionEntries.js";
|
|
8
9
|
import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
|
|
9
10
|
import type { LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
10
11
|
import type { LambderApiTransport } from "../shared/transport/LambderApiTransport.js";
|
|
@@ -114,6 +115,37 @@ type LambderMockRateLimitsOptions<C, S, P extends LambderMockRateLimitPolicies<S
|
|
|
114
115
|
/** Let a call through when the limiter throws, instead of refusing it. Default: true. */
|
|
115
116
|
failOpen?: boolean;
|
|
116
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* The shape a mock guard has to have to stand in for a server guard the
|
|
120
|
+
* generated `guardDeclarations` table describes: the same input mode and the
|
|
121
|
+
* same session requirement. The contract cannot say these for every guard
|
|
122
|
+
* (it names a guardInput's shape, and nothing about a guard fed from the
|
|
123
|
+
* payload or from nothing), so a mock guard that reads a payload slice the
|
|
124
|
+
* server's guard never sees, or that requires a session where the server's
|
|
125
|
+
* does not, would run and decide differently without this.
|
|
126
|
+
*/
|
|
127
|
+
export type LambderMockGuardShapeOf<D> = (D extends {
|
|
128
|
+
input: "apiInput";
|
|
129
|
+
} ? {
|
|
130
|
+
apiInput: z.ZodType;
|
|
131
|
+
} : D extends {
|
|
132
|
+
input: "guardInput";
|
|
133
|
+
} ? {
|
|
134
|
+
guardInput: z.ZodType;
|
|
135
|
+
} : {
|
|
136
|
+
apiInput?: undefined;
|
|
137
|
+
guardInput?: undefined;
|
|
138
|
+
}) & (D extends {
|
|
139
|
+
session: true;
|
|
140
|
+
} ? {
|
|
141
|
+
session: true;
|
|
142
|
+
} : {
|
|
143
|
+
session?: false | undefined;
|
|
144
|
+
});
|
|
145
|
+
/** Each mock guard the declarations know held to its declared shape; a guard the table does not have is free. */
|
|
146
|
+
type LambderMockGuardsAgree<G, D> = {
|
|
147
|
+
[N in keyof G & keyof D]: G[N] extends LambderMockGuardShapeOf<D[N]> ? unknown : LambderMockGuardShapeOf<D[N]>;
|
|
148
|
+
};
|
|
117
149
|
/**
|
|
118
150
|
* The guards option: required whenever the contract declares any guard name,
|
|
119
151
|
* omittable only for a contract that declares none.
|
|
@@ -123,12 +155,12 @@ type LambderMockRateLimitsOptions<C, S, P extends LambderMockRateLimitPolicies<S
|
|
|
123
155
|
* would answer 200 here. Optional, it would be the droppable half of exactly
|
|
124
156
|
* the check it exists for.
|
|
125
157
|
*/
|
|
126
|
-
type LambderMockGuardsOption<C, S, G> = [
|
|
158
|
+
type LambderMockGuardsOption<C, S, G, D> = [
|
|
127
159
|
LambderContractGuardNames<C>
|
|
128
160
|
] extends [never] ? {
|
|
129
|
-
guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G
|
|
161
|
+
guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
|
|
130
162
|
} : {
|
|
131
|
-
guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G
|
|
163
|
+
guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
|
|
132
164
|
};
|
|
133
165
|
/**
|
|
134
166
|
* The sessions, idempotency and rateLimits options: each required whenever
|
|
@@ -168,7 +200,38 @@ type LambderMockRateLimitsOption<C, S, P extends LambderMockRateLimitPolicies<S>
|
|
|
168
200
|
type LambderMockGuardShapes<S, G> = {
|
|
169
201
|
[N in keyof G]: LambderMockSurplusKeys<G[N], LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>>;
|
|
170
202
|
};
|
|
171
|
-
|
|
203
|
+
/**
|
|
204
|
+
* What the generated `apiOptions` table has to hold to stand in for the
|
|
205
|
+
* restated declarations of a contract's entries: an entry for every endpoint
|
|
206
|
+
* the contract declares, of the endpoint's mode. A table generated before an
|
|
207
|
+
* endpoint was added, or before one changed mode, is a compile error at the
|
|
208
|
+
* option rather than a throw when that endpoint's entry registers.
|
|
209
|
+
*/
|
|
210
|
+
export type LambderMockApiOptionsCover<C> = {
|
|
211
|
+
[K in keyof C & string]: {
|
|
212
|
+
mode: LambderContractMode<C, K>;
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicies<S> = LambderMockRateLimitPolicies<S>, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>, D extends Record<string, LambderGuardDeclarationEntry> = {}, A extends Record<string, LambderApiOptionEntry> | undefined = undefined> = LambderMockGuardsOption<C, S, G, D> & LambderMockSessionsOption<C, S> & LambderMockIdempotencyOption<C, S, I> & LambderMockRateLimitsOption<C, S, P> & {
|
|
216
|
+
/**
|
|
217
|
+
* The server's guard declarations, as the generated options module
|
|
218
|
+
* exports them (`guardDeclarations`). Given, every mock guard of a name
|
|
219
|
+
* the table has is held to its input mode and session requirement at
|
|
220
|
+
* the `guards` option (see LambderMockGuardShapeOf). Nothing runs on it.
|
|
221
|
+
*/
|
|
222
|
+
guardDeclarations?: D;
|
|
223
|
+
/**
|
|
224
|
+
* The server's declared options per API, as the generated options module
|
|
225
|
+
* exports them (`apiOptions`). Given, every entry's guards, rateLimit and
|
|
226
|
+
* idempotency are read off the table rather than restated: an entry is
|
|
227
|
+
* its handler (and an input schema, if it has one), a restated option is
|
|
228
|
+
* a compile error, and an endpoint whose mode the table and the builder
|
|
229
|
+
* disagree on is refused at registration. A restNotMocked answer reads
|
|
230
|
+
* the endpoint's mode off the table too, so a session endpoint nothing
|
|
231
|
+
* mocks still reads the session first. The table has to cover the
|
|
232
|
+
* contract (see LambderMockApiOptionsCover).
|
|
233
|
+
*/
|
|
234
|
+
apiOptions?: A & LambderMockApiOptionsCover<C>;
|
|
172
235
|
/** Stamped on every answer's envelope as apiVersion, as the server's option is. */
|
|
173
236
|
apiVersion?: string;
|
|
174
237
|
/** The version floor, as on the server: a call naming a lower `version` answers versionExpired whatever its signature says. */
|
|
@@ -219,8 +282,8 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
|
|
|
219
282
|
};
|
|
220
283
|
/**
|
|
221
284
|
* What the server's input validation handler answers, as a mock states it:
|
|
222
|
-
* `res.api(payload, config)` as data, with the status it went
|
|
223
|
-
* unless named).
|
|
285
|
+
* the handler's `res.api(payload, config)` as data, with the status it went
|
|
286
|
+
* out with (200 unless named).
|
|
224
287
|
*/
|
|
225
288
|
export type LambderMockInvalidInputAnswer = {
|
|
226
289
|
payload?: unknown;
|
|
@@ -9,7 +9,7 @@ import type { z } from "zod";
|
|
|
9
9
|
import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
|
|
10
10
|
import type { LambderApiGuard, LambderGuardDataOf, LambderGuardMetaMap } from "../api/LambderApiGuards.js";
|
|
11
11
|
import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck } from "../api/LambderApiRateLimits.js";
|
|
12
|
-
import type { LambderApiCallContext } from "../api/LambderApiCallContext.js";
|
|
12
|
+
import type { LambderApiCallContext, LambderResponseTools } from "../api/LambderApiCallContext.js";
|
|
13
13
|
import type { LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
14
14
|
import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
15
15
|
import type { LambderApiDefinition } from "../api/LambderApiDefinition.js";
|
|
@@ -47,7 +47,7 @@ export type LambderMockSurplusKeys<TOptions, TShape> = [
|
|
|
47
47
|
* fields: the API core's call context plus the request, a session
|
|
48
48
|
* controller for the call, and the caller's abort signal.
|
|
49
49
|
*/
|
|
50
|
-
export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & {
|
|
50
|
+
export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & LambderResponseTools & {
|
|
51
51
|
apiName: string;
|
|
52
52
|
request: LambderApiRequest;
|
|
53
53
|
/** Create, rotate, refresh and end sessions, exactly as a server handler does through its own ctx.sessionController. */
|
|
@@ -61,15 +61,6 @@ export type LambderMockCallContext<S = any> = LambderApiCallContext<S> & {
|
|
|
61
61
|
/** The same count, answered instead of thrown, as ctx.isRateLimited on the server. */
|
|
62
62
|
isRateLimited: LambderContextRateLimitCheck<Record<string, LambderApiRateLimitPolicyConfig>>;
|
|
63
63
|
signal: AbortSignal;
|
|
64
|
-
/**
|
|
65
|
-
* The envelope fields that travel beside the payload, the mock's stand-in
|
|
66
|
-
* for the server's `res.api(payload, config)`: a mock handler returns its
|
|
67
|
-
* payload, so this is where the rest of the envelope goes. `logList` is
|
|
68
|
-
* the usual channel and lives on the context itself.
|
|
69
|
-
*/
|
|
70
|
-
envelope: {
|
|
71
|
-
message?: string;
|
|
72
|
-
};
|
|
73
64
|
};
|
|
74
65
|
/** The same, with the session present: what a `session: true` mock guard and a session endpoint's handler see. */
|
|
75
66
|
export type LambderMockSessionCallContext<S = any> = Omit<LambderMockCallContext<S>, "session"> & {
|
|
@@ -186,8 +177,26 @@ type LambderMockInputPin<C, K extends keyof C, TSchema extends z.ZodType> = [
|
|
|
186
177
|
}) : {
|
|
187
178
|
"LambderMockApp: this input schema takes something else than the endpoint's contract input": LambderMockInputOf<C, K>;
|
|
188
179
|
};
|
|
189
|
-
/**
|
|
190
|
-
|
|
180
|
+
/**
|
|
181
|
+
* The three declaration fields of an entry on a mock created with the
|
|
182
|
+
* generated `apiOptions` table: absent, because the runtime reads them off
|
|
183
|
+
* the table. A restatement beside the table would be a second copy of the
|
|
184
|
+
* server's declaration, so writing one is an error rather than an override.
|
|
185
|
+
*/
|
|
186
|
+
type LambderMockDerivedFields = {
|
|
187
|
+
/** Read off the apiOptions table given to create(); not restated. */
|
|
188
|
+
guards?: never;
|
|
189
|
+
/** Read off the apiOptions table given to create(); not restated. */
|
|
190
|
+
rateLimit?: never;
|
|
191
|
+
/** Read off the apiOptions table given to create(); not restated. */
|
|
192
|
+
idempotency?: never;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* An entry written in full: the handler, and the declarations restated and
|
|
196
|
+
* pinned to the contract, or, with `TDerived` (a mock created with the
|
|
197
|
+
* generated `apiOptions` table), the handler alone.
|
|
198
|
+
*/
|
|
199
|
+
export type LambderMockEntryOptions<C, K extends keyof C, S, G, TInputSchema extends z.ZodType = z.ZodType, TDerived extends boolean = false> = (TDerived extends true ? LambderMockDerivedFields : LambderMockGuardsField<C, K> & LambderMockRateLimitField<C, K> & LambderMockIdempotencyField<C, K>) & {
|
|
191
200
|
/**
|
|
192
201
|
* A schema to validate the posted payload against, so the mock answers
|
|
193
202
|
* 422 exactly as the server would. Optional, and the mock's own: the
|
|
@@ -201,15 +210,16 @@ export type LambderMockEntryOptions<C, K extends keyof C, S, G, TInputSchema ext
|
|
|
201
210
|
handler: LambderMockHandler<C, K, S, G>;
|
|
202
211
|
};
|
|
203
212
|
/**
|
|
204
|
-
* What publicApi/sessionApi accept
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
213
|
+
* What publicApi/sessionApi accept. On a mock created with the generated
|
|
214
|
+
* `apiOptions` table (`TDerived`), a bare handler or the options without the
|
|
215
|
+
* declarations, for every endpoint. Otherwise a bare handler only for an
|
|
216
|
+
* endpoint the contract declares nothing for, the full options elsewhere, so
|
|
217
|
+
* the form that cannot carry a restatement is unavailable exactly where one
|
|
218
|
+
* is owed. Keyed on all three declarations: keyed on guards alone, a
|
|
219
|
+
* guardless endpoint could drop its rate limit and idempotency through the
|
|
220
|
+
* bare form.
|
|
209
221
|
*/
|
|
210
|
-
export type LambderMockEntryInput<C, K extends keyof C, S, G, TInputSchema extends z.ZodType = z.ZodType> = [
|
|
211
|
-
LambderContractGuardsOf<C, K> | LambderContractRateLimitOf<C, K> | LambderContractIdempotencyOf<C, K>
|
|
212
|
-
] extends [never] ? LambderMockHandler<C, K, S, G> | LambderMockEntryOptions<C, K, S, G, TInputSchema> : LambderMockEntryOptions<C, K, S, G, TInputSchema>;
|
|
222
|
+
export type LambderMockEntryInput<C, K extends keyof C, S, G, TInputSchema extends z.ZodType = z.ZodType, TDerived extends boolean = false> = TDerived extends true ? LambderMockHandler<C, K, S, G> | LambderMockEntryOptions<C, K, S, G, TInputSchema, true> : [LambderContractGuardsOf<C, K> | LambderContractRateLimitOf<C, K> | LambderContractIdempotencyOf<C, K>] extends [never] ? LambderMockHandler<C, K, S, G> | LambderMockEntryOptions<C, K, S, G, TInputSchema> : LambderMockEntryOptions<C, K, S, G, TInputSchema>;
|
|
213
223
|
/** One registry entry: the endpoint's definition as the pipeline runs it, and its handler (null when registered as not mocked). */
|
|
214
224
|
export type LambderMockEntry<C, K extends keyof C & string> = {
|
|
215
225
|
readonly name: K;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LambderRateLimitKeyFn } from "../api/LambderApiRateLimits.js";
|
|
2
|
+
import type { LambderRateLimitPolicyEntry } from "../shared/wire/LambderApiOptionEntries.js";
|
|
3
|
+
/** The names of the policies in a generated table whose key the app derives, and so the ones a mock has to supply a key handler for. */
|
|
4
|
+
export type LambderCustomKeyedPolicyNames<TPolicies> = {
|
|
5
|
+
[N in keyof TPolicies]: TPolicies[N] extends {
|
|
6
|
+
per: "custom";
|
|
7
|
+
} ? N : never;
|
|
8
|
+
}[keyof TPolicies] & string;
|
|
9
|
+
/** One policy as the mock runs it: the table's entry with its `per` put back, the key handler for a custom one and the literal for the rest. */
|
|
10
|
+
type LambderMockPolicyOf<TPolicy, TKey> = Omit<TPolicy, "per"> & (TPolicy extends {
|
|
11
|
+
per: "custom";
|
|
12
|
+
} ? {
|
|
13
|
+
per: TKey;
|
|
14
|
+
} : TPolicy extends {
|
|
15
|
+
per: infer P;
|
|
16
|
+
} ? {
|
|
17
|
+
per: P;
|
|
18
|
+
} : {});
|
|
19
|
+
/** The key handlers a table needs: one per custom-keyed policy, none where the table has none. */
|
|
20
|
+
export type LambderMockPolicyKeys<TPolicies> = {
|
|
21
|
+
[N in LambderCustomKeyedPolicyNames<TPolicies>]: LambderRateLimitKeyFn<any, any>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The rate-limit policies a mock's create() takes, built from a generated
|
|
25
|
+
* `rateLimitPolicies` table and the key handlers for its custom-keyed
|
|
26
|
+
* policies (built with the mock's own `rateLimitKey`, so they see the mock's
|
|
27
|
+
* context). Required for exactly those policies, refused for any other:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const mockApp = mock.create({
|
|
31
|
+
* rateLimits: {
|
|
32
|
+
* policies: lambderMockPoliciesFrom(rateLimitPolicies, {
|
|
33
|
+
* keys: {
|
|
34
|
+
* codePerEmail: mock.rateLimitKey({ apiInput: z.object({ email: z.string() }), handler: (_ctx, { email }) => email.toLowerCase() }),
|
|
35
|
+
* },
|
|
36
|
+
* }),
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Everything else about a policy (windows, budget, charge point, message) is
|
|
42
|
+
* the table's, so a mock never disagrees with the server about a limit it
|
|
43
|
+
* did not mean to change. The result keeps each policy's `per` and `budget`
|
|
44
|
+
* as literals, which is what lets create() hold it to the contract.
|
|
45
|
+
*/
|
|
46
|
+
export declare const lambderMockPoliciesFrom: <const TPolicies extends Record<string, LambderRateLimitPolicyEntry>, const TKeys extends LambderMockPolicyKeys<TPolicies> = LambderMockPolicyKeys<TPolicies>>(policies: TPolicies, options: [LambderCustomKeyedPolicyNames<TPolicies>] extends [never] ? {
|
|
47
|
+
keys?: TKeys & Record<Exclude<keyof TKeys, LambderCustomKeyedPolicyNames<TPolicies>>, never>;
|
|
48
|
+
} : {
|
|
49
|
+
keys: TKeys & Record<Exclude<keyof TKeys, LambderCustomKeyedPolicyNames<TPolicies>>, never>;
|
|
50
|
+
}) => { [N in keyof TPolicies]: LambderMockPolicyOf<TPolicies[N], N extends keyof TKeys ? TKeys[N] : never>; };
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rate-limit policies a mock's create() takes, built from a generated
|
|
3
|
+
* `rateLimitPolicies` table and the key handlers for its custom-keyed
|
|
4
|
+
* policies (built with the mock's own `rateLimitKey`, so they see the mock's
|
|
5
|
+
* context). Required for exactly those policies, refused for any other:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const mockApp = mock.create({
|
|
9
|
+
* rateLimits: {
|
|
10
|
+
* policies: lambderMockPoliciesFrom(rateLimitPolicies, {
|
|
11
|
+
* keys: {
|
|
12
|
+
* codePerEmail: mock.rateLimitKey({ apiInput: z.object({ email: z.string() }), handler: (_ctx, { email }) => email.toLowerCase() }),
|
|
13
|
+
* },
|
|
14
|
+
* }),
|
|
15
|
+
* },
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Everything else about a policy (windows, budget, charge point, message) is
|
|
20
|
+
* the table's, so a mock never disagrees with the server about a limit it
|
|
21
|
+
* did not mean to change. The result keeps each policy's `per` and `budget`
|
|
22
|
+
* as literals, which is what lets create() hold it to the contract.
|
|
23
|
+
*/
|
|
24
|
+
export const lambderMockPoliciesFrom = (policies, options) => {
|
|
25
|
+
const keys = (options.keys ?? {});
|
|
26
|
+
const built = {};
|
|
27
|
+
for (const [name, entry] of Object.entries(policies)) {
|
|
28
|
+
const { per, ...rest } = entry;
|
|
29
|
+
if (per === "custom") {
|
|
30
|
+
const key = keys[name];
|
|
31
|
+
if (typeof key?.handler !== "function") {
|
|
32
|
+
throw new Error(`LambderMockApp: rate-limit policy "${name}" is keyed by a handler of the server's, so the mock has to supply one: lambderMockPoliciesFrom(rateLimitPolicies, { keys: { ${name}: mock.rateLimitKey({ ... }) } }).`);
|
|
33
|
+
}
|
|
34
|
+
built[name] = { ...rest, per: key };
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
built[name] = per === undefined ? { ...rest } : { ...rest, per };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const name of Object.keys(keys)) {
|
|
41
|
+
if (policies[name]?.per !== "custom") {
|
|
42
|
+
throw new Error(`LambderMockApp: a key handler was given for rate-limit policy "${name}", which the server ${name in policies ? "does not key by a handler" : "does not declare"}.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return built;
|
|
46
|
+
};
|
package/dist/mock.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export { LambderMockApp, initLambderMock } from "./mock/LambderMockApp.js";
|
|
|
10
10
|
export { LambderMockTransportError } from "./mock/LambderMockFailureInjector.js";
|
|
11
11
|
export type { LambderMockAppOptions, LambderMockSessionsOptions, LambderMockIdempotencyOptions, LambderMockInvalidInputAnswer, LambderMockTransport, LambderMockTransportOptions } from "./mock/LambderMockCreateOptions.js";
|
|
12
12
|
export type { LambderMockCallContext, LambderMockSessionCallContext, LambderMockContext, LambderMockGuards, LambderMockHandler, LambderMockEntry, LambderMockEntryOptions, LambderMockEntryInput, LambderMockSlice, LambderMockRestEntry, LambderMockRegistryCheck, LambderMockMissingNames, LambderMockStrayNames, LambderMockDuplicateNames, LambderMockPublicNames, LambderMockSessionNames, LambderMockLatency, LambderMockFailure, LambderMockFailureReason, LambderMockOutcome, LambderMockCallEvent, LambderMockRequestEvent, LambderMockResponseEvent, LambderMockCallRecord, LambderMockListener, LambderMockRateLimitPolicies, LambderMockInputOf, LambderMockOutputOf, LambderMockOverride, } from "./mock/LambderMockTypes.js";
|
|
13
|
+
export { lambderMockPoliciesFrom } from "./mock/lambderMockPoliciesFrom.js";
|
|
14
|
+
export type { LambderCustomKeyedPolicyNames, LambderMockPolicyKeys } from "./mock/lambderMockPoliciesFrom.js";
|
|
15
|
+
export type { LambderMockGuardShapeOf } from "./mock/LambderMockCreateOptions.js";
|
|
13
16
|
export { lambderMockConsoleLogger } from "./mock/lambderMockConsoleLogger.js";
|
|
14
17
|
export type { LambderMockConsoleLoggerOptions } from "./mock/lambderMockConsoleLogger.js";
|
|
15
18
|
export { lambderMockMswHandler } from "./mock/lambderMockMswHandler.js";
|
package/dist/mock.js
CHANGED
|
@@ -12,6 +12,9 @@ export { LambderMockApp, initLambderMock } from "./mock/LambderMockApp.js";
|
|
|
12
12
|
// exported: the runtime is reached through the app. Only the error a
|
|
13
13
|
// transport rejects with is public.
|
|
14
14
|
export { LambderMockTransportError } from "./mock/LambderMockFailureInjector.js";
|
|
15
|
+
// The server's rate-limit policies as the mock restates them, from the
|
|
16
|
+
// generated options module plus the key handlers the module cannot hold.
|
|
17
|
+
export { lambderMockPoliciesFrom } from "./mock/lambderMockPoliciesFrom.js";
|
|
15
18
|
export { lambderMockConsoleLogger } from "./mock/lambderMockConsoleLogger.js";
|
|
16
19
|
export { lambderMockMswHandler } from "./mock/lambderMockMswHandler.js";
|
|
17
20
|
// The storage a mock app's uploads go to: a memory bucket the mock's ticket and
|