lambder 9.0.2 → 10.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 +195 -0
- package/README.md +29 -14
- package/dist/api/LambderApiDefinition.d.ts +9 -0
- package/dist/api/LambderApiEnvelope.d.ts +22 -10
- package/dist/api/LambderApiEnvelope.js +57 -22
- package/dist/api/LambderApiGuards.d.ts +77 -22
- package/dist/api/LambderApiGuards.js +13 -1
- package/dist/api/LambderApiIdempotency.js +4 -4
- package/dist/api/LambderApiOutputValidationError.d.ts +10 -4
- package/dist/api/LambderApiOutputValidationError.js +14 -6
- package/dist/api/LambderApiPipeline.d.ts +2 -1
- package/dist/api/LambderApiPipeline.js +10 -2
- package/dist/api/LambderApiRateLimits.d.ts +15 -11
- package/dist/api/LambderApiRateLimits.js +31 -7
- package/dist/api/LambderApiRefusals.d.ts +204 -0
- package/dist/api/LambderApiRefusals.js +223 -0
- package/dist/api/LambderApiSignature.d.ts +13 -4
- package/dist/api/LambderApiSignature.js +21 -4
- package/dist/build/writeApiOptions.js +7 -5
- package/dist/client/LambderCaller.d.ts +28 -17
- package/dist/client/LambderCaller.js +19 -15
- package/dist/client/LambderUploadRunner.d.ts +5 -5
- package/dist/client/LambderUploadRunner.js +25 -16
- package/dist/client.d.ts +6 -5
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +121 -23
- package/dist/core/Lambder.js +105 -51
- package/dist/core/LambderContext.d.ts +1 -1
- package/dist/core/LambderContext.js +5 -0
- package/dist/core/LambderCreateOptions.d.ts +58 -15
- package/dist/core/LambderCreateOptions.js +10 -0
- package/dist/core/LambderPolicyBuilders.d.ts +11 -8
- package/dist/core/LambderPolicyBuilders.js +10 -8
- package/dist/core/LambderResolver.d.ts +1 -1
- package/dist/core/LambderResolver.js +1 -1
- package/dist/core/LambderResponseBuilder.d.ts +11 -9
- package/dist/core/LambderResponseBuilder.js +12 -11
- package/dist/index.d.ts +13 -9
- package/dist/index.js +6 -2
- package/dist/invoke/LambderInvokeCaller.d.ts +5 -6
- package/dist/invoke/LambderInvokeCaller.js +13 -12
- package/dist/invoke/LambderInvokeOutcome.d.ts +32 -23
- package/dist/invoke/LambderInvokeOutcome.js +4 -4
- package/dist/mock/LambderMockApp.d.ts +86 -24
- package/dist/mock/LambderMockApp.js +108 -16
- package/dist/mock/LambderMockCallRecorder.js +6 -3
- package/dist/mock/LambderMockCreateOptions.d.ts +4 -5
- package/dist/mock/LambderMockFailureInjector.d.ts +9 -2
- package/dist/mock/LambderMockFailureInjector.js +18 -4
- package/dist/mock/LambderMockTypes.d.ts +34 -11
- package/dist/mock.d.ts +4 -3
- package/dist/mock.js +4 -3
- package/dist/shared/contracts/LambderUploadBucket.d.ts +18 -3
- package/dist/shared/contracts/LambderUploadBucket.js +2 -2
- package/dist/shared/util/LambderTypeUtilities.d.ts +11 -0
- package/dist/shared/wire/LambderApiContract.d.ts +96 -31
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +19 -12
- package/dist/shared/wire/LambderApiOptionValues.d.ts +15 -6
- package/dist/shared/wire/LambderApiOptionValues.js +3 -3
- package/dist/shared/wire/LambderApiOutcome.d.ts +49 -27
- package/dist/shared/wire/LambderApiOutcome.js +28 -11
- package/dist/shared/wire/LambderApiRefusal.d.ts +127 -42
- package/dist/shared/wire/LambderApiRefusal.js +15 -7
- package/dist/shared/wire/LambderCrashDetail.d.ts +1 -1
- package/dist/shared/wire/LambderCrashDetail.js +1 -1
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +3 -2
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -5
- package/dist/shared/wire/LambderObjectPayload.d.ts +15 -0
- package/dist/shared/wire/LambderObjectPayload.js +24 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +52 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +34 -8
- package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -1
- package/dist/shared/wire/LambderUploadObjectFields.js +22 -1
- package/dist/shared/wire/LambderUploadSchemas.d.ts +8 -2
- package/dist/shared/wire/LambderUploadSchemas.js +18 -7
- package/dist/stores/LambderMemoryUploadBucket.d.ts +21 -7
- package/dist/stores/LambderMemoryUploadBucket.js +60 -12
- package/dist/stores/LambderS3UploadBucket.d.ts +22 -3
- package/dist/stores/LambderS3UploadBucket.js +51 -4
- package/dist/testing/LambderTestApp.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +1 -1
package/dist/core/Lambder.js
CHANGED
|
@@ -13,17 +13,23 @@ 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 { checkedRefusal, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
|
|
17
|
+
import { refuse } from "../shared/wire/LambderApiRefusal.js";
|
|
16
18
|
import { apiSignatureOf } from "../api/LambderApiSignature.js";
|
|
17
19
|
import { apiNameKeyOf } from "../shared/wire/LambderApiSignatureMap.js";
|
|
18
20
|
import { assertPlainData } from "../shared/util/assertPlainData.js";
|
|
19
|
-
import { apiNotFoundAnswer,
|
|
21
|
+
import { apiNotFoundAnswer, envelopeAnswer, refusalAnswer, sessionExpiredAnswer, successEnvelope, } from "../api/LambderApiEnvelope.js";
|
|
22
|
+
import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
|
|
20
23
|
import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
|
|
24
|
+
import { toGuardEntries } from "../api/LambderApiGuards.js";
|
|
21
25
|
import { bindContextTools, createContext, isV2HttpEvent, } from "./LambderContext.js";
|
|
22
26
|
import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD } from "../shared/wire/LambderRequestPayload.js";
|
|
23
27
|
import { coerceToError } from "../shared/wire/LambderCrashDetail.js";
|
|
24
28
|
import { LambderCrashHandling } from "./LambderCrashHandling.js";
|
|
25
29
|
import { policyBuildersFor } from "./LambderPolicyBuilders.js";
|
|
26
30
|
import { assertCreateOptions, } from "./LambderCreateOptions.js";
|
|
31
|
+
/** The refusals of a name no API is registered under: no code, and none required. */
|
|
32
|
+
const NO_DECLARED_REFUSALS = { codes: new Map(), codeRequired: false };
|
|
27
33
|
/**
|
|
28
34
|
* Main Lambder class for building type-safe serverless APIs. Create
|
|
29
35
|
* instances with initLambder<SessionData>().create({...}) (see below): the
|
|
@@ -38,6 +44,8 @@ import { assertCreateOptions, } from "./LambderCreateOptions.js";
|
|
|
38
44
|
* @typeParam _TSessionGuardsRequired - @internal True when create() received requireSessionApiGuards (do not pass manually)
|
|
39
45
|
* @typeParam _TPublicGuardsRequired - @internal True when create() received requirePublicApiGuards (do not pass manually)
|
|
40
46
|
* @typeParam _TSessionsEnabled - @internal True when create() received the session option (do not pass manually). Defaults to true, unlike its siblings, so a plugin annotating its parameter as the bare Lambder<SessionData> can still register session APIs. create() knows the option and supplies the false; `new Lambder(...)` relies on the registration-time throw alone.
|
|
47
|
+
* @typeParam _TRefusals - @internal The refusal vocabulary declareRefusals() gave the init (do not pass manually)
|
|
48
|
+
* @typeParam _TRefusalCodesRequired - @internal True when declareRefusals() received requireCodes (do not pass manually)
|
|
41
49
|
*
|
|
42
50
|
* @example
|
|
43
51
|
* ```typescript
|
|
@@ -84,6 +92,10 @@ export default class Lambder {
|
|
|
84
92
|
guards;
|
|
85
93
|
/** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
|
|
86
94
|
rateLimitPolicies;
|
|
95
|
+
/** The refusal vocabulary given at creation: what each API's and each guard's refusal codes resolve against. Null without the option. */
|
|
96
|
+
refusalVocabulary;
|
|
97
|
+
/** Every API's refusals option as written, for apiOptionEntries(); its definition holds the resolved set. */
|
|
98
|
+
refusalOptions = new Map();
|
|
87
99
|
hookList = { "beforeRender": [], "afterRender": [], "fallback": [] };
|
|
88
100
|
createdHooks = [];
|
|
89
101
|
initPromise = null;
|
|
@@ -98,6 +110,8 @@ export default class Lambder {
|
|
|
98
110
|
corsConfig = null;
|
|
99
111
|
finalizeOptions;
|
|
100
112
|
requireSessionApiGuards;
|
|
113
|
+
/** Whether every refusal an API answers with has to name a code (declareRefusals's requireCodes). */
|
|
114
|
+
requireRefusalCodes;
|
|
101
115
|
/** Told what a request threw, beside whatever answers it; null outside a test. See LAMBDER_CRASH_WATCH. */
|
|
102
116
|
crashWatcher = null;
|
|
103
117
|
/** The crashes option applied: reporting, and the framework's own 500. */
|
|
@@ -127,6 +141,7 @@ export default class Lambder {
|
|
|
127
141
|
const session = options.session;
|
|
128
142
|
this.guards = options.guards;
|
|
129
143
|
this.rateLimitPolicies = options.rateLimits?.policies;
|
|
144
|
+
this.refusalVocabulary = readRefusalVocabulary(options.refusals);
|
|
130
145
|
this.pipeline = new LambderApiPipeline({
|
|
131
146
|
apiVersion: this.apiVersion,
|
|
132
147
|
minApiVersion: options.minApiVersion,
|
|
@@ -157,12 +172,13 @@ export default class Lambder {
|
|
|
157
172
|
this.trustedClientIpHeaders = options.trustedClientIpHeaders ?? [];
|
|
158
173
|
this.trustedHostHeaders = options.trustedHostHeaders ?? [];
|
|
159
174
|
this.requireSessionApiGuards = options.requireSessionApiGuards ?? false;
|
|
175
|
+
this.requireRefusalCodes = options.requireRefusalCodes ?? false;
|
|
160
176
|
this.requirePublicApiGuards = options.requirePublicApiGuards ?? false;
|
|
161
177
|
this.crashHandling = new LambderCrashHandling(options.crashes ?? {}, this.apiVersion);
|
|
162
178
|
this.contextTools = {
|
|
163
179
|
sessionControllerFor: (ctx) => this.getSessionController(ctx),
|
|
164
180
|
chargeRateLimit: async (ctx, policy, key, refuse) => {
|
|
165
|
-
const { checkResult, refusal } = await this.pipeline.chargeRateLimit(policy, {
|
|
181
|
+
const { checkResult, refusal: refusalToThrow } = await this.pipeline.chargeRateLimit(policy, {
|
|
166
182
|
// A per-API budget counts per registered API. The posted
|
|
167
183
|
// name of a call no API matched (a hook or the fallback
|
|
168
184
|
// charging it) is the caller's choice, and a fresh name
|
|
@@ -172,13 +188,13 @@ export default class Lambder {
|
|
|
172
188
|
session: ctx.session,
|
|
173
189
|
key,
|
|
174
190
|
});
|
|
175
|
-
if (refuse &&
|
|
191
|
+
if (refuse && refusalToThrow) {
|
|
176
192
|
if (ctx.api)
|
|
177
|
-
throw
|
|
193
|
+
throw refusalToThrow;
|
|
178
194
|
// A route has no envelope to carry a refusal, so it
|
|
179
195
|
// answers the same 429 as text, with the same Retry-After
|
|
180
196
|
// and the policy's own words.
|
|
181
|
-
throw this.getResolver(ctx).text(refusal.
|
|
197
|
+
throw this.getResolver(ctx).text(refusalToThrow.refusal.content, { statusCode: 429, headers: refusalToThrow.headers });
|
|
182
198
|
}
|
|
183
199
|
return checkResult;
|
|
184
200
|
},
|
|
@@ -265,14 +281,14 @@ export default class Lambder {
|
|
|
265
281
|
}
|
|
266
282
|
// Typed API with Zod
|
|
267
283
|
addApi(name, schema,
|
|
268
|
-
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with refuse(). */
|
|
284
|
+
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
|
|
269
285
|
handler) {
|
|
270
286
|
this.registerApi(name, "public", schema, handler);
|
|
271
287
|
return this;
|
|
272
288
|
}
|
|
273
289
|
// Typed Session API with Zod
|
|
274
290
|
addSessionApi(name, schema,
|
|
275
|
-
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with refuse(). */
|
|
291
|
+
/** Answers the call by returning its output (parsed through `output` before it is sent), or refuses it with ctx.refuse() or refuse(). */
|
|
276
292
|
handler) {
|
|
277
293
|
this.registerApi(name, "session", schema, handler);
|
|
278
294
|
return this;
|
|
@@ -303,9 +319,22 @@ export default class Lambder {
|
|
|
303
319
|
throw new Error(`Lambder: ${mode} API "${name}" declares no guards, and require${mode === "session" ? "Session" : "Public"}ApiGuards is on. ` +
|
|
304
320
|
`Declare the guard that authorizes it, or ${optOut}.`);
|
|
305
321
|
}
|
|
306
|
-
|
|
322
|
+
// The option, like guards and rateLimit, is a declaration or absent:
|
|
323
|
+
// an empty list would read as declaring codes while declaring none.
|
|
324
|
+
const ownRefusals = toRefusalCodes(schema.refusals);
|
|
325
|
+
if (schema.refusals !== undefined && ownRefusals.length === 0) {
|
|
326
|
+
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.`);
|
|
327
|
+
}
|
|
328
|
+
const allowedCodes = resolveAllowedRefusals(name, this.refusalVocabulary, ownRefusals, toGuardEntries(schema.guards).map(({ name: guard }) => ({
|
|
329
|
+
guard,
|
|
330
|
+
codes: (this.guards && Object.prototype.hasOwnProperty.call(this.guards, guard) ? this.guards[guard]?.refusals : undefined) ?? [],
|
|
331
|
+
})));
|
|
332
|
+
const refusals = { codes: allowedCodes, codeRequired: this.requireRefusalCodes };
|
|
333
|
+
const definition = { name, mode, guards: schema.guards, rateLimit: schema.rateLimit, idempotency: schema.idempotency, input: schema.input, output: schema.output, refusals };
|
|
307
334
|
this.pipeline.assertRegistration(definition);
|
|
308
335
|
this.apiDefinitions.set(name, definition);
|
|
336
|
+
if (schema.refusals !== undefined)
|
|
337
|
+
this.refusalOptions.set(name, schema.refusals);
|
|
309
338
|
this.actionList.push({
|
|
310
339
|
match: (ctx) => ctx.apiName === name ? {} : false,
|
|
311
340
|
actionFn: (ctx) => this.runApi(ctx, definition, schema.output, schema.compress ?? "auto", handler),
|
|
@@ -436,13 +465,16 @@ export default class Lambder {
|
|
|
436
465
|
* and guard name. A policy's key handler is never written: its `per`
|
|
437
466
|
* says "custom" and no more. A guard's input schema is never written
|
|
438
467
|
* either; its declaration says only which of the three input modes it
|
|
439
|
-
* has.
|
|
440
|
-
* and
|
|
468
|
+
* has. The refusal vocabulary is not written at all: it is shared code
|
|
469
|
+
* (codes, zod schemas, statuses and flags), and the mock declares the
|
|
470
|
+
* same object. Every table is sorted by name, so the module diffs by
|
|
471
|
+
* endpoint and never moves when registrations are reordered.
|
|
441
472
|
*/
|
|
442
473
|
apiOptionEntries() {
|
|
443
474
|
const apis = {};
|
|
444
475
|
for (const name of [...this.apiDefinitions.keys()].sort()) {
|
|
445
476
|
const { mode, guards, rateLimit, idempotency } = this.apiDefinitions.get(name);
|
|
477
|
+
const refusals = this.refusalOptions.get(name);
|
|
446
478
|
const entry = { mode };
|
|
447
479
|
if (guards !== undefined) {
|
|
448
480
|
assertPlainData(guards, `the guards option of API "${name}"`);
|
|
@@ -454,11 +486,13 @@ export default class Lambder {
|
|
|
454
486
|
}
|
|
455
487
|
if (idempotency !== undefined)
|
|
456
488
|
entry.idempotency = idempotency;
|
|
489
|
+
if (refusals !== undefined)
|
|
490
|
+
entry.refusals = refusals;
|
|
457
491
|
apis[name] = entry;
|
|
458
492
|
}
|
|
459
493
|
const rateLimitPolicies = {};
|
|
460
494
|
for (const name of Object.keys(this.rateLimitPolicies ?? {}).sort()) {
|
|
461
|
-
const { per, budget, chargeAt,
|
|
495
|
+
const { per, budget, chargeAt, refusal, ...windows } = this.rateLimitPolicies[name];
|
|
462
496
|
const entry = {};
|
|
463
497
|
for (const [window, limit] of Object.entries(windows)) {
|
|
464
498
|
if (limit !== undefined)
|
|
@@ -470,9 +504,9 @@ export default class Lambder {
|
|
|
470
504
|
entry.budget = budget;
|
|
471
505
|
if (chargeAt !== undefined)
|
|
472
506
|
entry.chargeAt = chargeAt;
|
|
473
|
-
if (
|
|
474
|
-
assertPlainData(
|
|
475
|
-
entry.
|
|
507
|
+
if (refusal !== undefined) {
|
|
508
|
+
assertPlainData(refusal, `the refusal of rate-limit policy "${name}"`);
|
|
509
|
+
entry.refusal = refusal;
|
|
476
510
|
}
|
|
477
511
|
rateLimitPolicies[name] = entry;
|
|
478
512
|
}
|
|
@@ -483,6 +517,7 @@ export default class Lambder {
|
|
|
483
517
|
input: guard.apiInput ? "apiInput" : guard.guardInput ? "guardInput" : "none",
|
|
484
518
|
session: guard.session === true,
|
|
485
519
|
runAt: guard.runAt ?? "beforeInputValidation",
|
|
520
|
+
...(guard.refusals?.length ? { refusals: guard.refusals } : {}),
|
|
486
521
|
};
|
|
487
522
|
}
|
|
488
523
|
return { apis, rateLimitPolicies, guards };
|
|
@@ -902,7 +937,9 @@ export default class Lambder {
|
|
|
902
937
|
* see the declared shape. The handler returns the schema's input form, so
|
|
903
938
|
* a transform runs exactly once.
|
|
904
939
|
*
|
|
905
|
-
* An output the schema rejects is
|
|
940
|
+
* An output the schema rejects, or one that is not an object or an array
|
|
941
|
+
* (isObjectPayload: what makes a caller's success never falsy), is the
|
|
942
|
+
* handler breaking its contract,
|
|
906
943
|
* answered as a crash rather than sent (LambderApiOutputValidationError,
|
|
907
944
|
* which an idempotency key records as its answer, since the handler has
|
|
908
945
|
* already run). The parse is synchronous, so an output schema cannot be
|
|
@@ -929,52 +966,69 @@ export default class Lambder {
|
|
|
929
966
|
}
|
|
930
967
|
if (!parsed.success)
|
|
931
968
|
throw new LambderApiOutputValidationError(definition.name, { zodError: parsed.error });
|
|
932
|
-
|
|
969
|
+
if (!isObjectPayload(parsed.data))
|
|
970
|
+
throw new LambderApiOutputValidationError(definition.name, { notObject: describePayloadKind(parsed.data) });
|
|
971
|
+
return envelopeAnswer(successEnvelope(this.apiVersion, parsed.data, ctx.logList));
|
|
933
972
|
});
|
|
934
973
|
// The API's compress option, on whatever answer the call ended with:
|
|
935
974
|
// a replayed one comes back from its store without the hint.
|
|
936
975
|
return responseFromAnswer({ ...answer, compress });
|
|
937
976
|
}
|
|
938
|
-
/**
|
|
977
|
+
/**
|
|
978
|
+
* A thrown LambderApiRefusal (from a hook, say) as the structured API
|
|
979
|
+
* envelope: the core's one mapping, after the same check the pipeline
|
|
980
|
+
* applies, against the endpoint the call names. A name no API is
|
|
981
|
+
* registered under declares no code, so only an uncoded or a framework
|
|
982
|
+
* refusal goes out for it.
|
|
983
|
+
*/
|
|
939
984
|
apiErrorResponse(err, ctx) {
|
|
940
|
-
|
|
985
|
+
const apiName = ctx.apiName ?? "";
|
|
986
|
+
const refusal = checkedRefusal(apiName, this.apiDefinitions.get(apiName)?.refusals ?? NO_DECLARED_REFUSALS, err);
|
|
987
|
+
return responseFromAnswer(refusalAnswer(refusal, this.apiVersion, ctx.logList));
|
|
941
988
|
}
|
|
942
989
|
}
|
|
990
|
+
/** The init bound to one vocabulary (or none): the policy builders, refuse and create() that share it. */
|
|
991
|
+
const lambderInitOf = (declared) => ({
|
|
992
|
+
...policyBuildersFor(declared?.vocabulary ?? null),
|
|
993
|
+
/**
|
|
994
|
+
* refuse() typed to the app's whole vocabulary, for a shared helper or a
|
|
995
|
+
* hook that raises a declared code with no endpoint in hand: `code` is
|
|
996
|
+
* one of the vocabulary's and `data` follows it. Which endpoint may send
|
|
997
|
+
* the code is checked where the refusal is rendered, as for the free
|
|
998
|
+
* refuse(). Inside an API handler ctx.refuse is narrower, that
|
|
999
|
+
* endpoint's codes alone.
|
|
1000
|
+
*/
|
|
1001
|
+
refuse: refuse,
|
|
1002
|
+
create(options) {
|
|
1003
|
+
const withRefusals = { ...options, refusals: declared?.refusals, requireRefusalCodes: declared?.requireCodes ?? false };
|
|
1004
|
+
return new Lambder(withRefusals);
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
943
1007
|
/**
|
|
944
|
-
* The
|
|
945
|
-
*
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
* ```typescript
|
|
951
|
-
* // app.ts (imports no api modules, so modules can import the type back)
|
|
952
|
-
* export const lambderApp = initLambder<SessionData>().create({
|
|
953
|
-
* apiPath: "/api",
|
|
954
|
-
* session: { store: new LambderDdbSessionStore({ tableName: "app-session", region: "us-east-1" }), sessionSalt: "..." },
|
|
955
|
-
* rateLimits: { limiter, policies },
|
|
956
|
-
* guards,
|
|
957
|
-
* idempotency: { store },
|
|
958
|
-
* });
|
|
959
|
-
* export type AppLambder = typeof lambderApp;
|
|
960
|
-
*
|
|
961
|
-
* // orders.ts
|
|
962
|
-
* export const orderApi = (lambder: AppLambder) => lambder.addSessionApi(...);
|
|
963
|
-
*
|
|
964
|
-
* // index.ts: registration only
|
|
965
|
-
* const lambder = lambderApp.addHook(...).use(orderApi)...;
|
|
966
|
-
* export const handler = lambder.getHandler();
|
|
967
|
-
* ```
|
|
968
|
-
*
|
|
969
|
-
* Curried because TypeScript type arguments are all-or-nothing per call:
|
|
970
|
-
* passing the session data type to `new Lambder<S>(...)` would silently
|
|
971
|
-
* widen the inferred policy and guard types to their {} defaults. Fixing the
|
|
972
|
-
* session type in the first call lets the second infer everything else.
|
|
973
|
-
* `new Lambder(options)` serves untyped or session-data-free instances.
|
|
1008
|
+
* The entry point of an app: binds the session data type, and hands out the
|
|
1009
|
+
* builders and create() that share it. `declareRefusals()` binds the app's
|
|
1010
|
+
* refusal vocabulary too, so a guard's ctx.refuse and the init's own refuse
|
|
1011
|
+
* are typed to it before any instance exists, and create() gives it to the
|
|
1012
|
+
* instance for every API's refusals option to name codes from.
|
|
974
1013
|
*/
|
|
975
1014
|
export const initLambder = () => ({
|
|
976
|
-
...
|
|
977
|
-
|
|
978
|
-
|
|
1015
|
+
...lambderInitOf(null),
|
|
1016
|
+
/**
|
|
1017
|
+
* Declares the app's refusal vocabulary: every code once, with the schema
|
|
1018
|
+
* of its data (`{ data: schema }`) or none (`{}`), the status every
|
|
1019
|
+
* refusal with it leaves with (`status`, 200 by default) and whether it
|
|
1020
|
+
* sets the notAuthorized flag (`notAuthorized: true`). Returns the init
|
|
1021
|
+
* bound to it: its guard() types ctx.refuse to a guard's refusals and
|
|
1022
|
+
* refuses a guard naming a code outside the vocabulary, its refuse is
|
|
1023
|
+
* typed to the whole vocabulary, and its create() hands the vocabulary to
|
|
1024
|
+
* the instance. With `requireCodes`, an uncoded refusal from an API
|
|
1025
|
+
* handler, a guard or a helper is a crash rather than an answer, so every
|
|
1026
|
+
* "no" a client reads names a code.
|
|
1027
|
+
*/
|
|
1028
|
+
declareRefusals(refusals, options = {}) {
|
|
1029
|
+
const vocabulary = readRefusalVocabulary(refusals);
|
|
1030
|
+
if (!vocabulary)
|
|
1031
|
+
throw new Error("Lambder: declareRefusals() takes the vocabulary, an object of codes.");
|
|
1032
|
+
return lambderInitOf({ refusals, vocabulary, requireCodes: (options.requireCodes ?? false) });
|
|
979
1033
|
},
|
|
980
1034
|
});
|
|
@@ -120,7 +120,7 @@ export type LambderRenderContext<TApiPayload = any, TPathParams extends Record<s
|
|
|
120
120
|
/**
|
|
121
121
|
* Counts one attempt against a named rate-limit policy and refuses the
|
|
122
122
|
* request when it is over: a 429 envelope on an API call, a plain 429 on
|
|
123
|
-
* a route, with Retry-After and the policy's
|
|
123
|
+
* a route, with Retry-After and the policy's refusal either way. A
|
|
124
124
|
* policy without `per` takes the key as the second argument; a `per:
|
|
125
125
|
* "ip"` or `per: "session"` one reads it off the request. The instance's
|
|
126
126
|
* limiter, failOpen and key bounding apply, as for a declared limit.
|
|
@@ -6,6 +6,7 @@ import { DEFAULT_API_PATH } from "../shared/wire/LambderDefaultApiPath.js";
|
|
|
6
6
|
import { LAMBDER_INVOKE_API_ID, LAMBDER_LOCAL_API_ID } from "../shared/wire/LambderInvokeApiId.js";
|
|
7
7
|
import { bindCallTools, responseToolsOf } from "../api/LambderApiCallContext.js";
|
|
8
8
|
import { decodeRequestPath } from "./LambderRequestPath.js";
|
|
9
|
+
import { refuse } from "../shared/wire/LambderApiRefusal.js";
|
|
9
10
|
/** True for API Gateway HTTP API / Lambda Function URL (payload v2) events. */
|
|
10
11
|
export const isV2HttpEvent = (event) => !!event && typeof event === "object"
|
|
11
12
|
&& event.version === "2.0"
|
|
@@ -23,6 +24,10 @@ export const bindContextTools = (ctx, tools) => {
|
|
|
23
24
|
methods: {
|
|
24
25
|
rateLimit: async (policy, key) => { await tools.chargeRateLimit(bound, policy, key, true); },
|
|
25
26
|
isRateLimited: (policy, key) => tools.chargeRateLimit(bound, policy, key, false),
|
|
27
|
+
// refuse() itself: what an API handler's context types as the
|
|
28
|
+
// endpoint's declared codes (LambderDeclaredRefuse). The check
|
|
29
|
+
// against the declaration happens where the refusal is rendered.
|
|
30
|
+
refuse,
|
|
26
31
|
...responseToolsOf(bound, bound.host),
|
|
27
32
|
},
|
|
28
33
|
});
|
|
@@ -14,6 +14,10 @@ import type { LambderSessionCrypto } from "../session/LambderSessionCrypto.js";
|
|
|
14
14
|
import type { LambderApiGuard } from "../api/LambderApiGuards.js";
|
|
15
15
|
import type { LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig } from "../api/LambderApiRateLimits.js";
|
|
16
16
|
import type { LambderApiIdempotencyConfig } from "../api/LambderApiIdempotency.js";
|
|
17
|
+
import { type LambderRefusalVocabulary } from "../api/LambderApiRefusals.js";
|
|
18
|
+
import type { LambderNoExtraKeys } from "../shared/util/LambderTypeUtilities.js";
|
|
19
|
+
import type { LambderJsonOf } from "../shared/wire/LambderApiContract.js";
|
|
20
|
+
import type { LambderRateLimitMessage } from "../shared/wire/LambderApiOptionValues.js";
|
|
17
21
|
import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
|
|
18
22
|
export type LambderRouteHandler = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
19
23
|
export type LambderSessionRouteHandler<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
@@ -250,6 +254,29 @@ export type LambderCreateOptions<TSessionData = any> = {
|
|
|
250
254
|
* rejected here rather than reading fields that are not on its context.
|
|
251
255
|
*/
|
|
252
256
|
guards?: Record<string, LambderApiGuard<any, any, any, LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>>>;
|
|
257
|
+
/**
|
|
258
|
+
* The app's refusal vocabulary: every code an API or a guard may refuse
|
|
259
|
+
* with, each declared once with the schema of its data (`{ data: schema }`
|
|
260
|
+
* or none), the status every refusal with it leaves with and whether it
|
|
261
|
+
* sets the notAuthorized flag. An API names the codes it may refuse with
|
|
262
|
+
* in its own `refusals` option, a guard in its own, and a refusal
|
|
263
|
+
* carrying any other code is a crash rather than an answer
|
|
264
|
+
* (LambderApiRefusalValidationError). The codes are what goes on the
|
|
265
|
+
* wire, so they may not start with the framework's `lambder/`; a code's
|
|
266
|
+
* data is an object or an array, as an output is.
|
|
267
|
+
*
|
|
268
|
+
* An app declares it with `initLambder().declareRefusals()`, which hands
|
|
269
|
+
* it here and types the init's guard() and refuse to it; it is written
|
|
270
|
+
* here only when the class is constructed directly.
|
|
271
|
+
*/
|
|
272
|
+
refusals?: LambderRefusalVocabulary;
|
|
273
|
+
/**
|
|
274
|
+
* Whether every refusal an API answers with has to name a declared code
|
|
275
|
+
* (or a framework code): an uncoded refuse() is then a crash rather than
|
|
276
|
+
* an answer, and ctx.refuse requires a code. Set through
|
|
277
|
+
* declareRefusals()'s `requireCodes`, as `refusals` is. Default false.
|
|
278
|
+
*/
|
|
279
|
+
requireRefusalCodes?: boolean;
|
|
253
280
|
/**
|
|
254
281
|
* Make an authorization declaration part of registering a session API:
|
|
255
282
|
* every addSessionApi must declare `guards`, at the type level (a missing
|
|
@@ -305,6 +332,19 @@ type LambderSessionOptionRequired = {
|
|
|
305
332
|
* away to nothing.
|
|
306
333
|
*/
|
|
307
334
|
export type LambderSessionEnabledInstance<TSessionsEnabled extends boolean> = TSessionsEnabled extends true ? unknown : LambderSessionOptionRequired;
|
|
335
|
+
/**
|
|
336
|
+
* An API answers with an object or an array (see isObjectPayload), so a
|
|
337
|
+
* caller's success is never null, false, 0 or "". Intersected onto an API's
|
|
338
|
+
* options: an output schema whose wire form is anything else (z.void(),
|
|
339
|
+
* z.boolean(), a nullable or an optional object, a Date) is refused where it
|
|
340
|
+
* is written, and the property name is the message.
|
|
341
|
+
*/
|
|
342
|
+
export type LambderObjectOutputCheck<TOutput extends z.ZodType> = 0 extends 1 & z.output<TOutput> ? unknown : undefined extends z.output<TOutput> ? LambderObjectOutputRefusal : [LambderJsonOf<z.output<TOutput>>] extends [never] ? LambderObjectOutputRefusal : [LambderJsonOf<z.output<TOutput>>] extends [object] ? unknown : LambderObjectOutputRefusal;
|
|
343
|
+
type LambderObjectOutputRefusal = {
|
|
344
|
+
output: {
|
|
345
|
+
readonly "lambder: an API answers with an object or an array. One with nothing to answer declares output: z.object({}) and returns {}.": never;
|
|
346
|
+
};
|
|
347
|
+
};
|
|
308
348
|
/**
|
|
309
349
|
* The `guards` field of an API's options: optional by default, required once
|
|
310
350
|
* create() received the require*ApiGuards flag for that kind of API, so that
|
|
@@ -320,17 +360,7 @@ export type LambderRequirableGuardsField<TRequired extends boolean, TGuardsOpt>
|
|
|
320
360
|
/** Named guards, run in declared order before input validation (after it for a guard declared runAt: "afterInputValidation"): a name, a non-empty list of names, or a non-empty { name: param } map for parameterized guards. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
|
|
321
361
|
guards?: TGuardsOpt;
|
|
322
362
|
};
|
|
323
|
-
|
|
324
|
-
* Rejects a key the options type does not have, which `const TOptions` would
|
|
325
|
-
* otherwise wave through: inferring a generic from an object literal switches
|
|
326
|
-
* excess-property checking off for the whole literal, so
|
|
327
|
-
* `requireSessionApiGuard` (no trailing "s") or `maxResponseByte` would
|
|
328
|
-
* compile, be dropped in silence, and leave the app on the default. That is
|
|
329
|
-
* worst for the two require*ApiGuards flags, which exist to make a missing
|
|
330
|
-
* authorization declaration a compile error. Mapping every surplus key to
|
|
331
|
-
* `never` puts the error back on the key itself.
|
|
332
|
-
*/
|
|
333
|
-
export type LambderNoExtraKeys<TOptions, TShape> = TOptions & Record<Exclude<keyof TOptions, keyof TShape>, never>;
|
|
363
|
+
export type { LambderNoExtraKeys };
|
|
334
364
|
/**
|
|
335
365
|
* What create() was actually given for one option, or undefined when the
|
|
336
366
|
* literal does not carry the key at all.
|
|
@@ -346,6 +376,16 @@ export type LambderGivenOption<TOptions, TKey extends PropertyKey> = TKey extend
|
|
|
346
376
|
* option they check.
|
|
347
377
|
*/
|
|
348
378
|
type LambderOptionShape<TSessionData, TKey extends keyof LambderCreateOptions<TSessionData>> = NonNullable<LambderCreateOptions<TSessionData>[TKey]>;
|
|
379
|
+
/**
|
|
380
|
+
* A guard's `refusals` against the vocabulary given beside it: the guard is
|
|
381
|
+
* built before the instance, so a code it names that the vocabulary does not
|
|
382
|
+
* hold is caught here, on the guard's entry in the guards option.
|
|
383
|
+
*/
|
|
384
|
+
type LambderGuardRefusalsKnown<TGuard, TRefusals> = TGuard extends {
|
|
385
|
+
refusals?: infer TNamed;
|
|
386
|
+
} ? NonNullable<TNamed> extends readonly (infer TCode extends string)[] ? string extends TCode ? unknown : [Exclude<TCode, keyof NonNullable<TRefusals> & string>] extends [never] ? unknown : {
|
|
387
|
+
refusals: readonly (keyof NonNullable<TRefusals> & string)[];
|
|
388
|
+
} : unknown : unknown;
|
|
349
389
|
/**
|
|
350
390
|
* The surplus-key rule one level down, over the option objects a typo is
|
|
351
391
|
* worst on. Excess-property checking is off for the WHOLE literal under
|
|
@@ -358,7 +398,7 @@ type LambderOptionShape<TSessionData, TKey extends keyof LambderCreateOptions<TS
|
|
|
358
398
|
* checked against its own option's shape, so the error lands on the
|
|
359
399
|
* misspelled key.
|
|
360
400
|
*/
|
|
361
|
-
export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCreateOptions<TSessionData
|
|
401
|
+
export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCreateOptions<TSessionData>, TRefusals = TOptions["refusals"]> = {
|
|
362
402
|
session?: LambderNoExtraKeys<NonNullable<TOptions["session"]>, LambderOptionShape<TSessionData, "session">> & {
|
|
363
403
|
cookie?: LambderNoExtraKeys<NonNullable<NonNullable<TOptions["session"]>["cookie"]>, LambderSessionCookieOptions>;
|
|
364
404
|
};
|
|
@@ -366,11 +406,15 @@ export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCrea
|
|
|
366
406
|
crashes?: LambderNoExtraKeys<NonNullable<TOptions["crashes"]>, LambderOptionShape<TSessionData, "crashes">>;
|
|
367
407
|
rateLimits?: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>, LambderOptionShape<TSessionData, "rateLimits">> & {
|
|
368
408
|
policies?: {
|
|
369
|
-
[TPolicy in keyof NonNullable<TOptions["rateLimits"]>["policies"]]: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>["policies"][TPolicy], LambderApiRateLimitPolicyConfig<LambderRenderContext
|
|
409
|
+
[TPolicy in keyof NonNullable<TOptions["rateLimits"]>["policies"]]: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>["policies"][TPolicy], LambderApiRateLimitPolicyConfig<LambderRenderContext>> & (NonNullable<TOptions["rateLimits"]>["policies"][TPolicy] extends {
|
|
410
|
+
refusal: infer TMessage;
|
|
411
|
+
} ? {
|
|
412
|
+
refusal?: LambderNoExtraKeys<TMessage, LambderRateLimitMessage>;
|
|
413
|
+
} : unknown);
|
|
370
414
|
};
|
|
371
415
|
};
|
|
372
416
|
guards?: {
|
|
373
|
-
[TGuard in keyof NonNullable<TOptions["guards"]>]: LambderNoExtraKeys<NonNullable<TOptions["guards"]>[TGuard], LambderOptionShape<TSessionData, "guards">[string]>;
|
|
417
|
+
[TGuard in keyof NonNullable<TOptions["guards"]>]: LambderNoExtraKeys<NonNullable<TOptions["guards"]>[TGuard], LambderOptionShape<TSessionData, "guards">[string]> & LambderGuardRefusalsKnown<NonNullable<TOptions["guards"]>[TGuard], TRefusals>;
|
|
374
418
|
};
|
|
375
419
|
files?: TOptions["files"] extends {
|
|
376
420
|
source: unknown;
|
|
@@ -387,4 +431,3 @@ export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCrea
|
|
|
387
431
|
* response (maxResponseBytes: 0) that an app discovers in production.
|
|
388
432
|
*/
|
|
389
433
|
export declare const assertCreateOptions: (options: LambderCreateOptions<any>) => void;
|
|
390
|
-
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertRefusalCodesDeclared, readRefusalVocabulary } from "../api/LambderApiRefusals.js";
|
|
1
2
|
import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
|
|
2
3
|
/**
|
|
3
4
|
* Everything create() refuses before an instance exists, in one place: a
|
|
@@ -25,6 +26,15 @@ export const assertCreateOptions = (options) => {
|
|
|
25
26
|
if (typeof cors === "object" && cors.credentials && (cors.origins === undefined || cors.origins === "*")) {
|
|
26
27
|
throw new Error('Lambder: cors.credentials needs cors.origins to be an allowlist or a predicate. With every origin allowed, any website could make credentialed calls and read the answers.');
|
|
27
28
|
}
|
|
29
|
+
// The vocabulary, and every code a guard names against it: a guard built
|
|
30
|
+
// by the standalone lambderGuard() meets the vocabulary here first.
|
|
31
|
+
const vocabulary = readRefusalVocabulary(options.refusals);
|
|
32
|
+
for (const [name, guard] of Object.entries(options.guards ?? {})) {
|
|
33
|
+
assertRefusalCodesDeclared(`guard "${name}"`, (guard?.refusals ?? []), vocabulary);
|
|
34
|
+
}
|
|
35
|
+
if (options.requireRefusalCodes && !vocabulary) {
|
|
36
|
+
throw new Error("Lambder: requireRefusalCodes needs a refusals vocabulary to name codes from; declare one with initLambder().declareRefusals().");
|
|
37
|
+
}
|
|
28
38
|
if ((options.requireSessionApiGuards || options.requirePublicApiGuards) && !options.guards) {
|
|
29
39
|
const requireFlag = options.requireSessionApiGuards ? "requireSessionApiGuards" : "requirePublicApiGuards";
|
|
30
40
|
throw new Error(`Lambder: ${requireFlag} needs a guards map at creation for APIs to declare from.`);
|
|
@@ -9,21 +9,24 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
|
|
11
11
|
import { type LambderGuardBuilder } from "../api/LambderApiGuards.js";
|
|
12
|
+
import type { LambderRefusalDeclaration, LambderRefusalVocabulary } from "../api/LambderApiRefusals.js";
|
|
12
13
|
import { type LambderRateLimitKeyBuilder } from "../api/LambderApiRateLimits.js";
|
|
13
14
|
/** Builder for the server's guards: the handler sees the render context (session-typed when `session: true`). */
|
|
14
15
|
export declare const lambderGuard: LambderGuardBuilder<LambderRenderContext, LambderSessionRenderContext<any, any>>;
|
|
15
16
|
/** Builder for the server's rate-limit keys: the handler sees the render context. */
|
|
16
17
|
export declare const lambderRateLimitKey: LambderRateLimitKeyBuilder<LambderRenderContext>;
|
|
17
18
|
/**
|
|
18
|
-
* The same two builders bound to one app's session data
|
|
19
|
-
* initLambder<SessionData>() hands out beside
|
|
20
|
-
* `ctx.session.data` and `ctx.sessionController` are
|
|
21
|
-
* lambderGuard() leaves them `any
|
|
22
|
-
* `
|
|
19
|
+
* The same two builders bound to one app's session data and refusal
|
|
20
|
+
* vocabulary, which is what initLambder<SessionData>() hands out beside
|
|
21
|
+
* create(): a guard's `ctx.session.data` and `ctx.sessionController` are
|
|
22
|
+
* SessionData where the standalone lambderGuard() leaves them `any`, and its
|
|
23
|
+
* `ctx.refuse` is typed to the guard's refusals from the vocabulary
|
|
24
|
+
* declareRefusals() gave (null: none declared, so a guard names no code).
|
|
25
|
+
* The server's counterpart of the mock's `guard` and `rateLimitKey`.
|
|
23
26
|
*/
|
|
24
|
-
export declare const policyBuildersFor: <TSessionData>() => {
|
|
25
|
-
/** Builds a guard whose handler sees this app's session type. */
|
|
26
|
-
guard: LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData
|
|
27
|
+
export declare const policyBuildersFor: <TSessionData, TRefusals extends LambderRefusalVocabulary = {}, TCodesRequired extends boolean = false>(vocabulary: ReadonlyMap<string, LambderRefusalDeclaration> | null) => {
|
|
28
|
+
/** Builds a guard whose handler sees this app's session type and, on ctx.refuse, the guard's own refusal codes. */
|
|
29
|
+
guard: LambderGuardBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>, TRefusals, TCodesRequired>;
|
|
27
30
|
/** Builds a rate-limit key whose handler sees this app's session type; the counterpart of `guard`. */
|
|
28
31
|
rateLimitKey: LambderRateLimitKeyBuilder<LambderRenderContext<any, Record<string, string>, {}, TSessionData>>;
|
|
29
32
|
};
|
|
@@ -14,15 +14,17 @@ export const lambderGuard = lambderGuardBuilder();
|
|
|
14
14
|
/** Builder for the server's rate-limit keys: the handler sees the render context. */
|
|
15
15
|
export const lambderRateLimitKey = lambderRateLimitKeyBuilder();
|
|
16
16
|
/**
|
|
17
|
-
* The same two builders bound to one app's session data
|
|
18
|
-
* initLambder<SessionData>() hands out beside
|
|
19
|
-
* `ctx.session.data` and `ctx.sessionController` are
|
|
20
|
-
* lambderGuard() leaves them `any
|
|
21
|
-
* `
|
|
17
|
+
* The same two builders bound to one app's session data and refusal
|
|
18
|
+
* vocabulary, which is what initLambder<SessionData>() hands out beside
|
|
19
|
+
* create(): a guard's `ctx.session.data` and `ctx.sessionController` are
|
|
20
|
+
* SessionData where the standalone lambderGuard() leaves them `any`, and its
|
|
21
|
+
* `ctx.refuse` is typed to the guard's refusals from the vocabulary
|
|
22
|
+
* declareRefusals() gave (null: none declared, so a guard names no code).
|
|
23
|
+
* The server's counterpart of the mock's `guard` and `rateLimitKey`.
|
|
22
24
|
*/
|
|
23
|
-
export const policyBuildersFor = () => ({
|
|
24
|
-
/** Builds a guard whose handler sees this app's session type. */
|
|
25
|
-
guard: lambderGuardBuilder(),
|
|
25
|
+
export const policyBuildersFor = (vocabulary) => ({
|
|
26
|
+
/** Builds a guard whose handler sees this app's session type and, on ctx.refuse, the guard's own refusal codes. */
|
|
27
|
+
guard: lambderGuardBuilder(vocabulary),
|
|
26
28
|
/** Builds a rate-limit key whose handler sees this app's session type; the counterpart of `guard`. */
|
|
27
29
|
rateLimitKey: lambderRateLimitKeyBuilder(),
|
|
28
30
|
});
|
|
@@ -14,7 +14,7 @@ interface DieResolverMethods {
|
|
|
14
14
|
redirect: SyncDie<LambderResponseBuilder["redirect"]>;
|
|
15
15
|
versionExpired: SyncDie<LambderResponseBuilder["versionExpired"]>;
|
|
16
16
|
fileBase64: SyncDie<LambderResponseBuilder["fileBase64"]>;
|
|
17
|
-
|
|
17
|
+
apiRefusal: SyncDie<LambderResponseBuilder["apiRefusal"]>;
|
|
18
18
|
file: AsyncDie<LambderResponseBuilder["file"]>;
|
|
19
19
|
templateFile: AsyncDie<LambderResponseBuilder["templateFile"]>;
|
|
20
20
|
}
|
|
@@ -22,7 +22,7 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
22
22
|
redirect: (...a) => { throw this.redirect(...a); },
|
|
23
23
|
versionExpired: (...a) => { throw this.versionExpired(...a); },
|
|
24
24
|
fileBase64: (...a) => { throw this.fileBase64(...a); },
|
|
25
|
-
|
|
25
|
+
apiRefusal: (...a) => { throw this.apiRefusal(...a); },
|
|
26
26
|
file: async (...a) => { throw await this.file(...a); },
|
|
27
27
|
templateFile: async (...a) => { throw await this.templateFile(...a); },
|
|
28
28
|
};
|
|
@@ -4,8 +4,8 @@ import { LambderResponse, type LambderHeadersInput } from "./LambderResponse.js"
|
|
|
4
4
|
import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
|
|
5
5
|
import { LambderSafeHtml } from "../shared/LambderHtml.js";
|
|
6
6
|
import type { LambderTemplateData } from "./LambderTemplatingEngine.js";
|
|
7
|
-
import type {
|
|
8
|
-
export type { LambderApiEnvelopeBody,
|
|
7
|
+
import type { LambderApiRefusalConfig } from "../shared/wire/LambderApiContract.js";
|
|
8
|
+
export type { LambderApiEnvelopeBody, LambderApiRefusalConfig } from "../shared/wire/LambderApiContract.js";
|
|
9
9
|
export type LambderResponseOptions = {
|
|
10
10
|
statusCode?: LambderHttpStatusCode;
|
|
11
11
|
headers?: LambderHeadersInput;
|
|
@@ -79,12 +79,14 @@ export default class LambderResponseBuilder {
|
|
|
79
79
|
htmlVirtualSlots?: boolean;
|
|
80
80
|
}): Promise<LambderResponse>;
|
|
81
81
|
/**
|
|
82
|
-
* An API
|
|
83
|
-
* handler or
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
82
|
+
* An API call answered from outside its handler: what a hook, a fallback,
|
|
83
|
+
* the input validation handler or the global error handler answers with.
|
|
84
|
+
* It is always a refusal (a refusal message, or one of the versionExpired,
|
|
85
|
+
* sessionExpired and notAuthorized flags) with a null payload, so a
|
|
86
|
+
* caller's success is only ever the handler's parsed output. Its message
|
|
87
|
+
* carries a framework code or none, and no data, since it answers outside
|
|
88
|
+
* any one endpoint's declared refusals. The logList channel is what the
|
|
89
|
+
* request accumulated unless the config names its own.
|
|
88
90
|
*/
|
|
89
|
-
|
|
91
|
+
apiRefusal(config: LambderApiRefusalConfig, options?: LambderResponseOptions): LambderResponse;
|
|
90
92
|
}
|