lambder 15.0.2 → 16.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 +118 -0
- package/README.md +4 -4
- package/dist/api/LambderApiDeclarations.d.ts +21 -5
- package/dist/api/LambderApiDeclarations.js +21 -3
- package/dist/api/LambderApiGuards.d.ts +23 -3
- package/dist/api/LambderApiGuards.js +50 -25
- package/dist/api/LambderApiModules.d.ts +50 -0
- package/dist/api/LambderApiModules.js +36 -0
- package/dist/api/LambderApiPipeline.d.ts +27 -4
- package/dist/api/LambderApiPipeline.js +71 -5
- package/dist/api/LambderApiRateLimits.d.ts +35 -4
- package/dist/api/LambderApiRateLimits.js +74 -35
- package/dist/core/Lambder.d.ts +68 -93
- package/dist/core/Lambder.js +147 -71
- package/dist/core/LambderApiBuilders.d.ts +105 -0
- package/dist/core/LambderApiBuilders.js +12 -0
- package/dist/core/LambderApiModule.d.ts +106 -0
- package/dist/core/LambderApiModule.js +45 -0
- package/dist/core/LambderCallSummary.d.ts +11 -9
- package/dist/core/LambderCreateOptions.d.ts +29 -24
- package/dist/core/LambderInvocationScope.d.ts +9 -3
- package/dist/core/LambderRouting.d.ts +9 -0
- package/dist/core/LambderRouting.js +20 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +2 -0
- package/dist/mock/LambderMockApiModule.d.ts +70 -0
- package/dist/mock/LambderMockApiModule.js +34 -0
- package/dist/mock/LambderMockApp.d.ts +53 -8
- package/dist/mock/LambderMockApp.js +104 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +18 -18
- package/dist/mock/LambderMockTypes.d.ts +46 -26
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +2 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
- package/dist/testing/LambderTestApp.d.ts +4 -3
- package/dist/testing/LambderTestApp.js +4 -3
- package/package.json +1 -1
package/dist/core/Lambder.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import LambderResolver from "./LambderResolver.js";
|
|
2
2
|
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
3
3
|
import { LambderResponse, finalizeResponse, answerFromResponse, responseFromAnswer, emitResponse, DEFAULT_FINALIZE_OPTIONS, DEFAULT_RESPONSE_COMPRESSION_SETTINGS, } from "./LambderResponse.js";
|
|
4
|
-
import { compileRouteMatcher } from "./LambderRouting.js";
|
|
4
|
+
import { compileRouteMatcher, routeNameOf } from "./LambderRouting.js";
|
|
5
5
|
import { allowedCorsOriginOf, applyCorsHeaders } from "./LambderCors.js";
|
|
6
6
|
import LambderSessionManager from "../session/LambderSessionManager.js";
|
|
7
7
|
import { resolveCompressionOption } from "../shared/wire/LambderCompressionOption.js";
|
|
@@ -13,7 +13,7 @@ import { LambderFiles } from "./LambderFiles.js";
|
|
|
13
13
|
import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
14
14
|
import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
|
|
15
15
|
import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
|
|
16
|
-
import { declaredRefusalVocabulary, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
|
|
16
|
+
import { assertRefusalCodesDeclared, declaredRefusalVocabulary, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
|
|
17
17
|
import { refuse } from "../shared/wire/LambderApiRefusal.js";
|
|
18
18
|
import { apiSignatureOf } from "../api/LambderApiSignature.js";
|
|
19
19
|
import { apiNameKeyOf } from "../shared/wire/LambderApiSignatureMap.js";
|
|
@@ -22,7 +22,10 @@ import { apiNotFoundAnswer, envelopeAnswer, sessionExpiredAnswer, successEnvelop
|
|
|
22
22
|
import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
|
|
23
23
|
import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
|
|
24
24
|
import { toGuardEntries } from "../api/LambderApiGuards.js";
|
|
25
|
-
import {
|
|
25
|
+
import { apiModuleDeclarationsOf } from "../api/LambderApiModules.js";
|
|
26
|
+
import { apiModuleUsesOf, isRegistrableApiGroup, } from "../api/LambderApiDeclarations.js";
|
|
27
|
+
import { apiBuildersFor, } from "./LambderApiBuilders.js";
|
|
28
|
+
import { LambderApiModule, } from "./LambderApiModule.js";
|
|
26
29
|
import { splitApiName } from "../shared/wire/LambderApiNames.js";
|
|
27
30
|
import { bindContextTools, createContext, isV2HttpEvent, } from "./LambderContext.js";
|
|
28
31
|
import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD } from "../shared/wire/LambderRequestPayload.js";
|
|
@@ -36,8 +39,10 @@ import { policyBuildersFor } from "./LambderPolicyBuilders.js";
|
|
|
36
39
|
import { assertCreateOptions, mergeNamedMaps, } from "./LambderCreateOptions.js";
|
|
37
40
|
/** The refusals of a name no API is registered under: no code, and none required. */
|
|
38
41
|
const NO_DECLARED_REFUSALS = { codes: new Map(), codeRequired: false };
|
|
42
|
+
/** The instance's own declaration builders: no module, and nothing of any instance, so every instance hands out the same three functions, each typed on its own app types. */
|
|
43
|
+
const INSTANCE_BUILDERS = apiBuildersFor(null);
|
|
39
44
|
/** Longest endpoint name a call summary carries: a page built before paths posts the name in its body, and a line is no place for a long one. */
|
|
40
|
-
const
|
|
45
|
+
const MAX_SUMMARY_NAME_CHARS = 200;
|
|
41
46
|
/** A request id as the parent header may carry one: what Lambda's own ids look like, and no longer than any of them. */
|
|
42
47
|
const REQUEST_ID_PATTERN = /^[A-Za-z0-9-]{1,128}$/;
|
|
43
48
|
/**
|
|
@@ -52,7 +57,11 @@ const parentRequestIdOf = (event) => {
|
|
|
52
57
|
return typeof value === "string" && REQUEST_ID_PATTERN.test(value) ? value : null;
|
|
53
58
|
};
|
|
54
59
|
/** The API call a request is, as its invocation records it; null for a request that is no API call. */
|
|
55
|
-
const apiCallOf = (ctx, event) => ctx.api ? { api: ctx.api.apiName, parentRequestId: parentRequestIdOf(event) } : null;
|
|
60
|
+
const apiCallOf = (ctx, event) => ctx.api ? { api: ctx.api.apiName, route: null, parentRequestId: parentRequestIdOf(event) } : null;
|
|
61
|
+
/** The call a request a route answered is: the route by its registered name, never by the path asked for. */
|
|
62
|
+
const routeCallOf = (ctx, route) => ({ api: null, route, parentRequestId: parentRequestIdOf(ctx.event) });
|
|
63
|
+
/** A route's outcome, which no envelope names: read from the status it answered with, as an API call's 5xx reads. */
|
|
64
|
+
const outcomeOfStatus = (status) => ({ outcome: status >= 500 ? "crash" : status >= 400 ? "other" : "success", code: null });
|
|
56
65
|
/**
|
|
57
66
|
* Main Lambder class for building type-safe serverless APIs. Create
|
|
58
67
|
* instances with initLambder<SessionData>().create({...}) (see below): the
|
|
@@ -124,9 +133,14 @@ export default class Lambder {
|
|
|
124
133
|
apiGroupNames = new Set();
|
|
125
134
|
/** The lazy groups not loaded yet, each with its load in progress, if one is. */
|
|
126
135
|
lazyApiGroups = new Map();
|
|
127
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* The guards given at creation merged into one map, and every registered
|
|
138
|
+
* module's beside them, kept for registration (a guard's session and
|
|
139
|
+
* refusal codes), apiSignatures() (a guard's schema is part of the
|
|
140
|
+
* signature of every endpoint declaring it) and apiOptionEntries().
|
|
141
|
+
*/
|
|
128
142
|
guards;
|
|
129
|
-
/** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
|
|
143
|
+
/** The rate-limit policies given at creation, and every registered module's beside them, kept for apiOptionEntries(), which records each one less its key handler. */
|
|
130
144
|
rateLimitPolicies;
|
|
131
145
|
/** The refusal vocabulary given at creation: what each API's and each guard's refusal codes resolve against. Null without the option. */
|
|
132
146
|
refusalVocabulary;
|
|
@@ -157,7 +171,7 @@ export default class Lambder {
|
|
|
157
171
|
trustedClientIpHeaders;
|
|
158
172
|
trustedHostHeaders;
|
|
159
173
|
originProof;
|
|
160
|
-
/** Where each
|
|
174
|
+
/** Where each call's summary goes, an API call's or a route's (the callSummary option); null writes none. Replaceable through the backend swap alone. */
|
|
161
175
|
callSummaryWriter;
|
|
162
176
|
constructor(given = {}) {
|
|
163
177
|
// The guards and the rate-limit policies as one map each, whether
|
|
@@ -166,7 +180,10 @@ export default class Lambder {
|
|
|
166
180
|
const options = {
|
|
167
181
|
...given,
|
|
168
182
|
guards: given.guards && mergeNamedMaps(given.guards, "guard"),
|
|
169
|
-
rateLimits: given.rateLimits && {
|
|
183
|
+
rateLimits: given.rateLimits && {
|
|
184
|
+
...given.rateLimits,
|
|
185
|
+
policies: given.rateLimits.policies === undefined ? undefined : mergeNamedMaps(given.rateLimits.policies, "rate-limit policy"),
|
|
186
|
+
},
|
|
170
187
|
};
|
|
171
188
|
assertCreateOptions(options);
|
|
172
189
|
this.files = options.files ? new LambderFiles(options.files) : null;
|
|
@@ -185,8 +202,8 @@ export default class Lambder {
|
|
|
185
202
|
this.corsConfig = options.cors === true ? {} : options.cors;
|
|
186
203
|
}
|
|
187
204
|
const session = options.session;
|
|
188
|
-
this.guards = options.guards;
|
|
189
|
-
this.rateLimitPolicies = options.rateLimits?.policies;
|
|
205
|
+
this.guards = { ...options.guards };
|
|
206
|
+
this.rateLimitPolicies = { ...options.rateLimits?.policies };
|
|
190
207
|
this.refusalVocabulary = readRefusalVocabulary(options.refusals);
|
|
191
208
|
this.pipeline = new LambderApiPipeline({
|
|
192
209
|
apiVersion: this.apiVersion,
|
|
@@ -314,6 +331,7 @@ export default class Lambder {
|
|
|
314
331
|
addRoute(condition, actionFn) {
|
|
315
332
|
this.actionList.push({
|
|
316
333
|
match: compileRouteMatcher(condition),
|
|
334
|
+
name: routeNameOf(condition),
|
|
317
335
|
actionFn: (ctx, resolver) => actionFn(ctx, resolver),
|
|
318
336
|
});
|
|
319
337
|
return this;
|
|
@@ -321,6 +339,7 @@ export default class Lambder {
|
|
|
321
339
|
addSessionRoute(condition, actionFn) {
|
|
322
340
|
this.actionList.push({
|
|
323
341
|
match: compileRouteMatcher(condition),
|
|
342
|
+
name: routeNameOf(condition),
|
|
324
343
|
actionFn: async (ctx, resolver) => {
|
|
325
344
|
await this.requireSession(ctx, resolver);
|
|
326
345
|
// requireSession answered already if there was no session, so
|
|
@@ -335,46 +354,56 @@ export default class Lambder {
|
|
|
335
354
|
// Declared as values, gathered into named groups, registered in one call.
|
|
336
355
|
// =====================================================================
|
|
337
356
|
/**
|
|
338
|
-
* Declares one endpoint, typed on this instance's types
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
357
|
+
* Declares one endpoint, typed on this instance's types (see
|
|
358
|
+
* LambderDefineApi). Nothing is registered until the declaration is put
|
|
359
|
+
* in a group (defineApiGroup) and the group is registered
|
|
360
|
+
* (registerApiGroups).
|
|
342
361
|
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* An arrow property rather than a method, so an app can hand it out
|
|
350
|
-
* detached: `export const { defineApi, defineApiGroup } = lambderApp;`.
|
|
362
|
+
* An arrow property rather than a method, as are the two below, so an
|
|
363
|
+
* app can hand them out detached; handed out under the instance's name
|
|
364
|
+
* (`export const defineApi: AppLambder["defineApi"] = lambderApp.defineApi`),
|
|
365
|
+
* a declaration file names each by the instance's type rather than
|
|
366
|
+
* writing the app's types out again in each.
|
|
351
367
|
*/
|
|
352
|
-
defineApi =
|
|
353
|
-
/**
|
|
354
|
-
|
|
368
|
+
defineApi = INSTANCE_BUILDERS.defineApi;
|
|
369
|
+
/** Gathers endpoints into a named group (see LambderDefineApiGroup). */
|
|
370
|
+
defineApiGroup = INSTANCE_BUILDERS.defineApiGroup;
|
|
371
|
+
/** A group loaded on the first call to one of its endpoints (see LambderDefineLazyApiGroup). */
|
|
372
|
+
lazyApiGroup = INSTANCE_BUILDERS.lazyApiGroup;
|
|
355
373
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
374
|
+
* Defines a module: guards and rate-limit policies a part of the app
|
|
375
|
+
* declares for its own endpoints, beside the ones this instance was
|
|
376
|
+
* created with. Returns the module's builders, whose endpoints may name
|
|
377
|
+
* the instance's guards and policies and the module's own, and nothing
|
|
378
|
+
* else; their groups are registered with registerApiGroups() as any
|
|
379
|
+
* are, and their registration adds the module's guards and policies to
|
|
380
|
+
* the instance that registers them.
|
|
361
381
|
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
* A
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* checks and loading checks again. `loadApiGroups()` loads every group,
|
|
375
|
-
* for a build step or a boot check that has to see them all.
|
|
382
|
+
* ```typescript
|
|
383
|
+
* export const giftCardModule = storeApp.defineApiModule({
|
|
384
|
+
* guards: { cardHolder },
|
|
385
|
+
* rateLimitPolicies: { redeemPerCard: { perHour: 5, per: "session" } },
|
|
386
|
+
* });
|
|
387
|
+
* ```
|
|
388
|
+
*
|
|
389
|
+
* A name the instance declares is refused here, at compile time and at
|
|
390
|
+
* the call; a name two modules declare is refused when the second is
|
|
391
|
+
* registered, as create() refuses a name two maps declare. A module's
|
|
392
|
+
* rate-limit policies count on the instance's limiter, so they need the
|
|
393
|
+
* rateLimits option at creation, whose own policies may then be left out.
|
|
376
394
|
*/
|
|
377
|
-
|
|
395
|
+
defineApiModule(options) {
|
|
396
|
+
const given = options;
|
|
397
|
+
const declarations = apiModuleDeclarationsOf(given, "Lambder");
|
|
398
|
+
for (const [name, guard] of Object.entries(declarations.guards)) {
|
|
399
|
+
assertRefusalCodesDeclared(`guard "${name}"`, (guard?.refusals ?? []), this.refusalVocabulary);
|
|
400
|
+
}
|
|
401
|
+
// Checked against this instance now, so a name it declares fails
|
|
402
|
+
// where the module is defined; registration checks again against
|
|
403
|
+
// whichever instance takes the module, and against the other modules.
|
|
404
|
+
this.pipeline.modulesToAdd([{ module: declarations, usedBy: "being defined" }]);
|
|
405
|
+
return new LambderApiModule(declarations);
|
|
406
|
+
}
|
|
378
407
|
/**
|
|
379
408
|
* Registers groups of endpoints, as one contract: `typeof
|
|
380
409
|
* lambder.ApiContract` then maps every `group.action` to its entry, one
|
|
@@ -405,7 +434,6 @@ export default class Lambder {
|
|
|
405
434
|
// caller that catches the error and fixes the declaration is told the
|
|
406
435
|
// real problem on its next try.
|
|
407
436
|
const names = new Set();
|
|
408
|
-
const prepared = [];
|
|
409
437
|
for (const group of groups) {
|
|
410
438
|
if (!isRegistrableApiGroup(group)) {
|
|
411
439
|
throw new Error("Lambder: registerApiGroups takes groups built by defineApiGroup() or lazyApiGroup().");
|
|
@@ -414,10 +442,21 @@ export default class Lambder {
|
|
|
414
442
|
throw new Error(`Lambder: group "${group.name}" is registered twice. A group is one namespace, so its endpoints belong in one defineApiGroup().`);
|
|
415
443
|
}
|
|
416
444
|
names.add(group.name);
|
|
445
|
+
}
|
|
446
|
+
// The modules the groups bring, a lazy group's at once: checked
|
|
447
|
+
// against the instance and each other here, taken only after every
|
|
448
|
+
// endpoint passed against them.
|
|
449
|
+
const registrable = groups;
|
|
450
|
+
const added = instance.pipeline.modulesToAdd(registrable.flatMap(apiModuleUsesOf));
|
|
451
|
+
const prepared = [];
|
|
452
|
+
for (const group of registrable) {
|
|
417
453
|
if (group.kind === "lambderApiGroup")
|
|
418
|
-
prepared.push(...instance.prepareApiGroup(group));
|
|
454
|
+
prepared.push(...instance.prepareApiGroup(group, added));
|
|
419
455
|
}
|
|
420
|
-
|
|
456
|
+
instance.pipeline.addModules(added);
|
|
457
|
+
instance.guards = { ...instance.guards, ...added.guards };
|
|
458
|
+
instance.rateLimitPolicies = { ...instance.rateLimitPolicies, ...added.rateLimitPolicies };
|
|
459
|
+
for (const group of registrable) {
|
|
421
460
|
instance.apiGroupNames.add(group.name);
|
|
422
461
|
if (group.kind === "lambderLazyApiGroup")
|
|
423
462
|
instance.lazyApiGroups.set(group.name, { group, loading: null });
|
|
@@ -429,6 +468,7 @@ export default class Lambder {
|
|
|
429
468
|
const group = ctx.api ? splitApiName(ctx.api.apiName)?.group : undefined;
|
|
430
469
|
return group !== undefined && names.has(group) ? {} : false;
|
|
431
470
|
},
|
|
471
|
+
name: null,
|
|
432
472
|
actionFn: (ctx, resolver) => instance.dispatchApiCall(ctx, resolver),
|
|
433
473
|
});
|
|
434
474
|
return instance;
|
|
@@ -471,9 +511,14 @@ export default class Lambder {
|
|
|
471
511
|
await this.loadLazyApiGroup(group);
|
|
472
512
|
return this.apiDefinitions.get(apiName);
|
|
473
513
|
}
|
|
474
|
-
/**
|
|
475
|
-
|
|
476
|
-
|
|
514
|
+
/**
|
|
515
|
+
* The endpoints of one group, each checked as `group.action` against the
|
|
516
|
+
* instance's guards and policies and those `adding` holds, of the modules
|
|
517
|
+
* the same registration adds; nothing is registered until they are
|
|
518
|
+
* committed.
|
|
519
|
+
*/
|
|
520
|
+
prepareApiGroup(group, adding) {
|
|
521
|
+
return Object.entries(group.apis).map(([action, declaration]) => this.prepareApi(`${group.name}.${action}`, declaration.options, declaration.handler, adding));
|
|
477
522
|
}
|
|
478
523
|
/**
|
|
479
524
|
* A lazy group, loaded once: its import, checked to be the group it was
|
|
@@ -494,6 +539,16 @@ export default class Lambder {
|
|
|
494
539
|
if (loaded.name !== name) {
|
|
495
540
|
throw new Error(`Lambder: lazy group "${name}" loaded the group "${loaded.name}". The loader must resolve to the group of the name it was registered under.`);
|
|
496
541
|
}
|
|
542
|
+
// A module's guards and policies are added when its group is
|
|
543
|
+
// registered, at startup, so a collision fails the boot and a
|
|
544
|
+
// route charging one of its policies finds it from the first
|
|
545
|
+
// request; one first met here would do neither.
|
|
546
|
+
for (const { module } of apiModuleUsesOf(loaded)) {
|
|
547
|
+
if (!this.pipeline.hasModule(module)) {
|
|
548
|
+
throw new Error(`Lambder: lazy group "${name}" loaded endpoints a module's builders declared, and was registered with a lazyApiGroup that does not carry that module. ` +
|
|
549
|
+
`Register it with the module's own lazyApiGroup, so its guards and rate-limit policies are added at startup.`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
497
552
|
for (const api of this.prepareApiGroup(loaded))
|
|
498
553
|
this.commitApi(api);
|
|
499
554
|
this.lazyApiGroups.delete(name);
|
|
@@ -510,9 +565,12 @@ export default class Lambder {
|
|
|
510
565
|
* digests, beside its handler. Nothing is recorded here (commitApi does
|
|
511
566
|
* that), so a refusal anywhere in a registration leaves nothing behind.
|
|
512
567
|
*/
|
|
513
|
-
prepareApi(name, schema, handler) {
|
|
568
|
+
prepareApi(name, schema, handler, adding) {
|
|
514
569
|
const guardEntries = toGuardEntries(schema.guards);
|
|
515
|
-
const
|
|
570
|
+
const guardOf = (guard) => Object.prototype.hasOwnProperty.call(this.guards, guard) ? this.guards[guard]
|
|
571
|
+
: adding && Object.prototype.hasOwnProperty.call(adding.guards, guard) ? adding.guards[guard]
|
|
572
|
+
: undefined;
|
|
573
|
+
const guardNeedsSession = (guard) => guardOf(guard)?.session === true;
|
|
516
574
|
const mode = guardEntries.some(({ name: guard }) => guardNeedsSession(guard)) ? "session" : "public";
|
|
517
575
|
if (mode === "session" && !this.pipeline.hasSessions) {
|
|
518
576
|
throw new Error(`Lambder: a guard of API "${name}" needs a session, and the instance was created without the session option.`);
|
|
@@ -527,13 +585,10 @@ export default class Lambder {
|
|
|
527
585
|
if (schema.refusals !== undefined && ownRefusals.length === 0) {
|
|
528
586
|
throw new Error(`Lambder: API "${name}" declares an empty refusals option, which declares no code. Name the codes it refuses with, or omit the option entirely.`);
|
|
529
587
|
}
|
|
530
|
-
const allowedCodes = resolveAllowedRefusals(name, this.refusalVocabulary, ownRefusals, guardEntries.map(({ name: guard }) => ({
|
|
531
|
-
guard,
|
|
532
|
-
codes: (this.guards && Object.prototype.hasOwnProperty.call(this.guards, guard) ? this.guards[guard]?.refusals : undefined) ?? [],
|
|
533
|
-
})));
|
|
588
|
+
const allowedCodes = resolveAllowedRefusals(name, this.refusalVocabulary, ownRefusals, guardEntries.map(({ name: guard }) => ({ guard, codes: guardOf(guard)?.refusals ?? [] })));
|
|
534
589
|
const refusals = { codes: allowedCodes, codeRequired: this.requireRefusalCodes };
|
|
535
590
|
const definition = { name, mode, guards: schema.guards, rateLimit: schema.rateLimit, idempotency: schema.idempotency, input: schema.input, output: schema.output, refusals };
|
|
536
|
-
this.pipeline.assertRegistration(definition);
|
|
591
|
+
this.pipeline.assertRegistration(definition, adding);
|
|
537
592
|
return { definition, refusalsOption: schema.refusals, output: schema.output, compress: schema.compress ?? "auto", handler };
|
|
538
593
|
}
|
|
539
594
|
/** Records a checked endpoint: its definition, its refusals option as written, and what answers it. */
|
|
@@ -556,6 +611,7 @@ export default class Lambder {
|
|
|
556
611
|
// HTTP side: joins the route/API chain in registration order.
|
|
557
612
|
this.actionList.push({
|
|
558
613
|
match: (ctx) => filter(ctx.event, ctx) ? {} : false,
|
|
614
|
+
name: null,
|
|
559
615
|
actionFn: async (ctx, resolver) => {
|
|
560
616
|
const result = await actionFn(ctx.event, { ctx, res: resolver, lambdaContext: ctx.lambdaContext });
|
|
561
617
|
if (!(result instanceof LambderResponse)) {
|
|
@@ -695,7 +751,7 @@ export default class Lambder {
|
|
|
695
751
|
apis[name] = entry;
|
|
696
752
|
}
|
|
697
753
|
const rateLimitPolicies = {};
|
|
698
|
-
for (const name of Object.keys(this.rateLimitPolicies
|
|
754
|
+
for (const name of Object.keys(this.rateLimitPolicies).sort()) {
|
|
699
755
|
const { per, budget, chargeAt, refusal, ...windows } = this.rateLimitPolicies[name];
|
|
700
756
|
const entry = {};
|
|
701
757
|
for (const [window, limit] of Object.entries(windows)) {
|
|
@@ -715,7 +771,7 @@ export default class Lambder {
|
|
|
715
771
|
rateLimitPolicies[name] = entry;
|
|
716
772
|
}
|
|
717
773
|
const guards = {};
|
|
718
|
-
for (const name of Object.keys(this.guards
|
|
774
|
+
for (const name of Object.keys(this.guards).sort()) {
|
|
719
775
|
const guard = this.guards[name];
|
|
720
776
|
guards[name] = {
|
|
721
777
|
input: guard.apiInput ? "apiInput" : guard.guardInput ? "guardInput" : "none",
|
|
@@ -919,15 +975,32 @@ export default class Lambder {
|
|
|
919
975
|
// Set before the hooks run, so a beforeRender hook on a matched route
|
|
920
976
|
// sees the route's own path params.
|
|
921
977
|
ctx.pathParams = matched.params;
|
|
978
|
+
// A route's call is named as soon as it has matched, before the hooks
|
|
979
|
+
// run, so the line a hook's answer ends in is the route's too. An API
|
|
980
|
+
// call keeps the name its path gave it, whatever action answers it.
|
|
981
|
+
const invocation = currentInvocation();
|
|
982
|
+
if (invocation && !invocation.call)
|
|
983
|
+
invocation.call = routeCallOf(ctx, matched.action.name);
|
|
922
984
|
const beforeRenderResult = await this.runBeforeRenderHooks(ctx, resolver, onContextReplaced);
|
|
923
985
|
if (beforeRenderResult instanceof LambderResponse)
|
|
924
986
|
return beforeRenderResult;
|
|
925
|
-
|
|
987
|
+
// An API call's handler time is the pipeline's to measure, inside its
|
|
988
|
+
// own work; a route's is the whole action.
|
|
989
|
+
if (ctx.api)
|
|
990
|
+
return await matched.action.actionFn(beforeRenderResult, resolver);
|
|
991
|
+
const handlerStarted = performance.now();
|
|
992
|
+
try {
|
|
993
|
+
return await matched.action.actionFn(beforeRenderResult, resolver);
|
|
994
|
+
}
|
|
995
|
+
finally {
|
|
996
|
+
if (invocation)
|
|
997
|
+
invocation.handlerMs = performance.now() - handlerStarted;
|
|
998
|
+
}
|
|
926
999
|
}
|
|
927
1000
|
/**
|
|
928
1001
|
* One HTTP invocation, from the event to the finalized response, under
|
|
929
|
-
* an invocation record of its own (see LambderInvocationScope): what
|
|
930
|
-
*
|
|
1002
|
+
* an invocation record of its own (see LambderInvocationScope): what its
|
|
1003
|
+
* call summary line is written from once the response is final.
|
|
931
1004
|
*/
|
|
932
1005
|
async render(event, lambdaContext) {
|
|
933
1006
|
return await runInvocation(lambdaContext, async (invocation) => {
|
|
@@ -995,7 +1068,8 @@ export default class Lambder {
|
|
|
995
1068
|
// reach it.
|
|
996
1069
|
ctx.responseHeaders.applyTo(response, response === responseIntoHooks ? headersAppliedIntoHooks : 0);
|
|
997
1070
|
this.applyCors(allowedOrigin, response, this.isCorsPreflight(ctx));
|
|
998
|
-
|
|
1071
|
+
// An API answer names its outcome; a route's answer names none, and reads from its status.
|
|
1072
|
+
invocation.outcome = response.callOutcome ?? (ctx.api ? { outcome: "other", code: null } : outcomeOfStatus(response.statusCode));
|
|
999
1073
|
return await finalizeResponse(ctx, response, this.finalizeOptions, ctx.eventFormat);
|
|
1000
1074
|
}
|
|
1001
1075
|
catch (err) {
|
|
@@ -1101,9 +1175,9 @@ export default class Lambder {
|
|
|
1101
1175
|
return emitResponse(eventFormat, crashResponse.statusCode, crashResponse.headers, typeof crashResponse.body === "string" ? crashResponse.body : "", false);
|
|
1102
1176
|
}
|
|
1103
1177
|
/**
|
|
1104
|
-
* The summary line of an API call
|
|
1105
|
-
* for an invocation that served
|
|
1106
|
-
* the call its line and nothing else.
|
|
1178
|
+
* The summary line of an API call or of a request a route answered, once
|
|
1179
|
+
* its response is final: nothing for an invocation that served neither. A
|
|
1180
|
+
* writer that throws costs the call its line and nothing else.
|
|
1107
1181
|
*/
|
|
1108
1182
|
writeCallSummary(invocation, status) {
|
|
1109
1183
|
const { call } = invocation;
|
|
@@ -1112,8 +1186,10 @@ export default class Lambder {
|
|
|
1112
1186
|
const summary = {
|
|
1113
1187
|
kind: "lambder.call",
|
|
1114
1188
|
// The path form is two identifiers; a page built before paths
|
|
1115
|
-
// posts the name in its body, which is whatever it sent.
|
|
1116
|
-
|
|
1189
|
+
// posts the name in its body, which is whatever it sent. A route's
|
|
1190
|
+
// name is the app's own, cut the same way.
|
|
1191
|
+
api: call.api === null ? null : call.api.slice(0, MAX_SUMMARY_NAME_CHARS),
|
|
1192
|
+
route: call.route === null ? null : call.route.slice(0, MAX_SUMMARY_NAME_CHARS),
|
|
1117
1193
|
outcome: invocation.outcome?.outcome ?? "other",
|
|
1118
1194
|
code: invocation.outcome?.code ?? null,
|
|
1119
1195
|
status,
|
|
@@ -1128,7 +1204,7 @@ export default class Lambder {
|
|
|
1128
1204
|
this.callSummaryWriter(summary);
|
|
1129
1205
|
}
|
|
1130
1206
|
catch (err) {
|
|
1131
|
-
console.error(`Lambder: the callSummary writer threw, so the summary of "${summary.api}" was not written. ${coerceToError(err).message}`);
|
|
1207
|
+
console.error(`Lambder: the callSummary writer threw, so the summary of "${summary.api ?? summary.route ?? "a route"}" was not written. ${coerceToError(err).message}`);
|
|
1132
1208
|
}
|
|
1133
1209
|
}
|
|
1134
1210
|
/**
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
|
|
3
|
+
import type { LambderObjectOutputCheck, LambderPayloadSliceCheck, LambderRequirableGuardsField } from "./LambderCreateOptions.js";
|
|
4
|
+
import type { LambderReadonlyDeep, LambderMaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
5
|
+
import type { LambderDeclaredRefuse } from "../shared/wire/LambderApiRefusal.js";
|
|
6
|
+
import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
|
|
7
|
+
import type { LambderContractEntry, LambderJsonOf } from "../shared/wire/LambderApiContract.js";
|
|
8
|
+
import type { LambderGuardDataOf, LambderGuardInputsOf, LambderGuardRefusalNamesOf, LambderGuardsOption } from "../api/LambderApiGuards.js";
|
|
9
|
+
import type { LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
|
|
10
|
+
import type { LambderHandlerRefusalsOf, LambderRefusalNamesIn, LambderRefusalsOption, LambderWireRefusalsOf } from "../api/LambderApiRefusals.js";
|
|
11
|
+
import type { LambderApiModuleDeclarations } from "../api/LambderApiModules.js";
|
|
12
|
+
import { type LambderActionNamesCheck, type LambderApiDeclaration, type LambderApiDeclarations, type LambderApiGroup, type LambderApiModeOf, type LambderAppTypes, type LambderGroupNameCheck, type LambderGroupPartsCheck, type LambderLazyApiGroup, type LambderMergedParts, type LambderPublicSessionPolicyCheck, type LambderSessionModeCheck } from "../api/LambderApiDeclarations.js";
|
|
13
|
+
/** The codes one API may refuse with, typed: its own refusals option's and its declared guards', as the vocabulary holds them. */
|
|
14
|
+
export type LambderApiRefusalCodes<TVocabulary, TGuards, TRefusalsOpt, TGuardsOpt> = (LambderRefusalNamesIn<TRefusalsOpt> | LambderGuardRefusalNamesOf<TGuards, TGuardsOpt>) & keyof TVocabulary & string;
|
|
15
|
+
/**
|
|
16
|
+
* What an endpoint's handler is handed: the session context when one of its
|
|
17
|
+
* guards needs a session (so ctx.session is typed present), the plain one
|
|
18
|
+
* otherwise, with the payload, the guards' data and a refuse() typed to the
|
|
19
|
+
* codes the endpoint may refuse with.
|
|
20
|
+
*/
|
|
21
|
+
export type LambderApiHandlerContext<TApp extends LambderAppTypes, TPayload, TGuardsOpt, TRefusalsOpt> = (LambderApiModeOf<TApp["guards"], TGuardsOpt> extends "session" ? LambderSessionRenderContext<TPayload, TApp["session"], Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["policies"]> : LambderRenderContext<TPayload, Record<string, string>, LambderGuardDataOf<TApp["guards"], TGuardsOpt>, TApp["session"], TApp["policies"]>) & {
|
|
22
|
+
refuse: LambderDeclaredRefuse<LambderHandlerRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>, TApp["refusalCodesRequired"]>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Declares one endpoint, typed on one set of app types: its input and output
|
|
26
|
+
* schemas, its guards, rate limits, idempotency and refusals, and its
|
|
27
|
+
* handler. Nothing is registered until the declaration is put in a group
|
|
28
|
+
* (defineApiGroup) and the group is registered (registerApiGroups).
|
|
29
|
+
*
|
|
30
|
+
* The guards say who may call it, and so its mode: an endpoint whose guards
|
|
31
|
+
* include one that needs a session (`session: true`) is a session endpoint,
|
|
32
|
+
* its session read before the guards run and a call without one answered
|
|
33
|
+
* sessionExpired, and its handler's ctx.session is typed present. Any other
|
|
34
|
+
* endpoint is public.
|
|
35
|
+
*/
|
|
36
|
+
export type LambderDefineApi<TApp extends LambderAppTypes> = <TInput extends z.ZodType, TOutput extends z.ZodType, const TAnswer extends LambderReadonlyDeep<z.input<TOutput>>, const TRateOpt extends LambderRateLimitOption<TApp["policies"]> = never, const TGuardsOpt extends LambderGuardsOption<TApp["guards"]> = never, const TIdempotencyOpt extends LambderApiIdempotencyOption = never, const TRefusalsOpt extends LambderRefusalsOption<TApp["refusals"]> = never>(options: {
|
|
37
|
+
input: TInput;
|
|
38
|
+
output: TOutput;
|
|
39
|
+
} & {
|
|
40
|
+
/** Named rate limits, checked in declared order within their phase (per ip before the session read, per session before the guards, a custom key after the guards and input validation): a name, a list of names, or a { name: true | override } map (windows overridable on perApi budgets, refusal on any). The first exceeded one refuses (429 envelope + Retry-After); attempts count on every counter checked before it. A per-session policy needs one of the endpoint's guards to need a session. */
|
|
41
|
+
rateLimit?: TRateOpt;
|
|
42
|
+
/** Replay-protect this API per client idempotencyKey. Requires the idempotency option at creation. */
|
|
43
|
+
idempotency?: TApp["idempotency"] extends true ? TIdempotencyOpt : never;
|
|
44
|
+
/**
|
|
45
|
+
* The refusal codes this API may refuse with, from the vocabulary given at creation: one code or a
|
|
46
|
+
* non-empty list. Its guards' codes join them. The handler raises one with `ctx.refuse(content, { code, data })`,
|
|
47
|
+
* its callers narrow on them, and a refusal with any other code is a crash rather than an answer.
|
|
48
|
+
*/
|
|
49
|
+
refusals?: TRefusalsOpt;
|
|
50
|
+
/**
|
|
51
|
+
* Whether this API's answers are compressed for a caller that accepts it: "auto" (the default) when the
|
|
52
|
+
* body is large enough to gain, false never, true always. false suits an answer of base64 bytes: once
|
|
53
|
+
* compressed it leaves the function base64-encoded again, so it is no smaller under Lambda's response
|
|
54
|
+
* cap or to a lambda caller, and a browser gets it only about a quarter smaller for the time spent at
|
|
55
|
+
* both ends. A transport setting of this server's, not part of the API's contract.
|
|
56
|
+
*/
|
|
57
|
+
compress?: boolean | "auto";
|
|
58
|
+
} & LambderRequirableGuardsField<TApp["guardsRequired"], TGuardsOpt> & LambderObjectOutputCheck<TOutput> & LambderPayloadSliceCheck<TApp["guards"], TApp["policies"], TGuardsOpt, TRateOpt, z.input<TInput>> & LambderPublicSessionPolicyCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["policies"], TRateOpt> & LambderSessionModeCheck<LambderApiModeOf<TApp["guards"], TGuardsOpt>, TApp["sessions"]>,
|
|
59
|
+
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
|
|
60
|
+
handler: (ctx: LambderApiHandlerContext<TApp, z.infer<TInput>, TGuardsOpt, TRefusalsOpt>) => LambderMaybePromise<TAnswer>) => LambderApiDeclaration<LambderContractEntry<z.input<TInput>, LambderJsonOf<z.output<TOutput>>, LambderApiModeOf<TApp["guards"], TGuardsOpt>, LambderGuardInputsOf<TApp["guards"], TGuardsOpt>, TGuardsOpt, TRateOpt, TIdempotencyOpt, LambderWireRefusalsOf<TApp["refusals"], LambderApiRefusalCodes<TApp["refusals"], TApp["guards"], TRefusalsOpt, TGuardsOpt>>>>;
|
|
61
|
+
/**
|
|
62
|
+
* Gathers endpoints into a named group: each is registered as `name.action`
|
|
63
|
+
* and called at `{apiPath}/{name}/{action}`, and every Lambder caller
|
|
64
|
+
* reaches it as `caller.name.action(input)`. A group's name and its actions
|
|
65
|
+
* are identifiers, and a group may not take a name a caller already answers
|
|
66
|
+
* for (see LAMBDER_RESERVED_GROUP_NAMES).
|
|
67
|
+
*
|
|
68
|
+
* A group declared across files takes each file's part:
|
|
69
|
+
* `defineApiGroup("orders", orderReadApis, orderWriteApis)`. An action two
|
|
70
|
+
* parts declare is refused, at compile time and at startup, rather than one
|
|
71
|
+
* silently replacing the other as an object spread would. Endpoints a
|
|
72
|
+
* module's builders declared may sit in any group: registering it adds
|
|
73
|
+
* their module.
|
|
74
|
+
*/
|
|
75
|
+
export type LambderDefineApiGroup = <const TName extends string, const TParts extends readonly [LambderApiDeclarations, ...LambderApiDeclarations[]]>(name: TName & LambderGroupNameCheck<TName>, ...parts: TParts & LambderGroupPartsCheck<TParts> & {
|
|
76
|
+
[TIndex in keyof TParts]: LambderActionNamesCheck<TParts[TIndex]>;
|
|
77
|
+
}) => LambderApiGroup<TName, LambderMergedParts<TParts>>;
|
|
78
|
+
/**
|
|
79
|
+
* A group loaded on the first call to one of its endpoints:
|
|
80
|
+
* `lazyApiGroup("orders", () => import("./orders.js").then((m) => m.orderApis))`.
|
|
81
|
+
* A cold start then parses none of it, nor anything only it imports, until
|
|
82
|
+
* a request calls it; the contract is the loaded group's, read off its type.
|
|
83
|
+
* The group loaded must carry the same name, which the type checks and
|
|
84
|
+
* loading checks again. `loadApiGroups()` loads every group, for a build
|
|
85
|
+
* step or a boot check that has to see them all.
|
|
86
|
+
*
|
|
87
|
+
* A module's lazyApiGroup carries the module, so its guards and policies
|
|
88
|
+
* are added when the group is registered, at startup, rather than when it
|
|
89
|
+
* loads; a group whose endpoints a module's builders declared is registered
|
|
90
|
+
* lazily with that module's.
|
|
91
|
+
*/
|
|
92
|
+
export type LambderDefineLazyApiGroup = <const TName extends string, TGroup extends LambderApiGroup<TName, any>>(name: TName & LambderGroupNameCheck<TName>, load: () => Promise<TGroup>) => LambderLazyApiGroup<TName, NoInfer<TGroup>>;
|
|
93
|
+
/** The three builders, typed on one set of app types. */
|
|
94
|
+
export type LambderApiBuilders<TApp extends LambderAppTypes> = {
|
|
95
|
+
readonly defineApi: LambderDefineApi<TApp>;
|
|
96
|
+
readonly defineApiGroup: LambderDefineApiGroup;
|
|
97
|
+
readonly lazyApiGroup: LambderDefineLazyApiGroup;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The builders as values: the instance's own (module null), or a module's,
|
|
101
|
+
* whose endpoints and lazy groups carry the module for registration to add.
|
|
102
|
+
* Plain functions closing over nothing else, so an app may hand them out
|
|
103
|
+
* detached.
|
|
104
|
+
*/
|
|
105
|
+
export declare const apiBuildersFor: <TApp extends LambderAppTypes>(module: LambderApiModuleDeclarations | null) => LambderApiBuilders<TApp>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildApiGroup, buildLazyApiGroup, } from "../api/LambderApiDeclarations.js";
|
|
2
|
+
/**
|
|
3
|
+
* The builders as values: the instance's own (module null), or a module's,
|
|
4
|
+
* whose endpoints and lazy groups carry the module for registration to add.
|
|
5
|
+
* Plain functions closing over nothing else, so an app may hand them out
|
|
6
|
+
* detached.
|
|
7
|
+
*/
|
|
8
|
+
export const apiBuildersFor = (module) => ({
|
|
9
|
+
defineApi: ((options, handler) => module ? { kind: "lambderApi", options, handler, module } : { kind: "lambderApi", options, handler }),
|
|
10
|
+
defineApiGroup: ((name, ...parts) => buildApiGroup(name, parts)),
|
|
11
|
+
lazyApiGroup: ((name, load) => buildLazyApiGroup(name, load, module)),
|
|
12
|
+
});
|