lambder 8.3.1 → 9.0.2

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +18 -25
  3. package/dist/api/LambderApiCallContext.d.ts +31 -1
  4. package/dist/api/LambderApiCallContext.js +8 -0
  5. package/dist/api/LambderApiDefinition.d.ts +2 -2
  6. package/dist/api/LambderApiEnvelope.d.ts +1 -1
  7. package/dist/api/LambderApiEnvelope.js +3 -4
  8. package/dist/api/LambderApiIdempotency.js +5 -7
  9. package/dist/api/LambderApiPipeline.d.ts +1 -1
  10. package/dist/api/LambderApiPipeline.js +1 -1
  11. package/dist/api/LambderApiSignature.js +1 -1
  12. package/dist/client/LambderCaller.d.ts +1 -5
  13. package/dist/client/LambderCaller.js +2 -10
  14. package/dist/client.d.ts +2 -2
  15. package/dist/client.js +1 -1
  16. package/dist/core/Lambder.d.ts +51 -13
  17. package/dist/core/Lambder.js +48 -39
  18. package/dist/core/LambderContext.d.ts +9 -6
  19. package/dist/core/LambderContext.js +2 -1
  20. package/dist/core/LambderCreateOptions.d.ts +1 -1
  21. package/dist/core/LambderResolver.d.ts +6 -12
  22. package/dist/core/LambderResolver.js +2 -14
  23. package/dist/core/LambderResponseBuilder.d.ts +15 -70
  24. package/dist/core/LambderResponseBuilder.js +15 -99
  25. package/dist/index.d.ts +5 -5
  26. package/dist/index.js +2 -2
  27. package/dist/invoke/LambderInvokeCaller.d.ts +1 -1
  28. package/dist/invoke/LambderInvokeCaller.js +4 -5
  29. package/dist/mock/LambderMockApp.js +2 -3
  30. package/dist/mock/LambderMockCreateOptions.d.ts +3 -3
  31. package/dist/mock/LambderMockTypes.d.ts +2 -11
  32. package/dist/shared/util/LambderTypeUtilities.d.ts +18 -0
  33. package/dist/shared/wire/LambderAnswerHeaders.d.ts +3 -2
  34. package/dist/shared/wire/LambderAnswerHeaders.js +3 -2
  35. package/dist/shared/wire/LambderApiContract.d.ts +9 -14
  36. package/dist/shared/wire/LambderApiRefusal.d.ts +3 -4
  37. package/dist/shared/wire/LambderApiRefusal.js +3 -4
  38. package/dist/testing/LambderTestVisitor.d.ts +1 -1
  39. package/package.json +1 -1
  40. /package/dist/shared/wire/{LambderApiSignature.d.ts → LambderApiSignatureMap.d.ts} +0 -0
  41. /package/dist/shared/wire/{LambderApiSignature.js → LambderApiSignatureMap.js} +0 -0
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import { classifyDeliveryFailure, describeFailure, errorFromFunctionError, LambderInvokeError, parseFunctionError, } from "./LambderInvokeOutcome.js";
24
24
  import { DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionCookieNames.js";
25
- import { readApiSignature } from "../shared/wire/LambderApiSignature.js";
25
+ import { readApiSignature } from "../shared/wire/LambderApiSignatureMap.js";
26
26
  import { resolveApiOutcome } from "../shared/wire/LambderApiOutcome.js";
27
27
  import { mergeGuardInputs, } from "../shared/wire/LambderCallOptions.js";
28
28
  import { beginIdempotentAttempt, IDEMPOTENT_ATTEMPT_NOT_SENT } from "../shared/wire/LambderIdempotencyKeyScope.js";
@@ -378,10 +378,9 @@ export default class LambderInvokeCaller {
378
378
  // The answer's Set-Cookie values, so a session the callee rotated or
379
379
  // cleared is visible to whoever is carrying it.
380
380
  const cookies = http.cookies;
381
- // The declared output, by the callee's own typing: res.api(null)
382
- // compiles only for an output that allows null or beside a reason (an
383
- // errorMessage is a failure below; a message-only null is the
384
- // callee's contract to keep).
381
+ // The declared output, by the callee's own typing: a handler returns
382
+ // its output, so a success payload is null only where the output
383
+ // allows null.
385
384
  if (outcome.ok)
386
385
  return { ok: true, payload: (outcome.payload ?? null), response: outcome.response, logList, cookies };
387
386
  const shared = { status: outcome.status, retryAfterSeconds: outcome.retryAfterSeconds, logList, cookies };
@@ -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";
@@ -562,7 +562,6 @@ export class LambderMockApp {
562
562
  apiName: request.apiName,
563
563
  request,
564
564
  signal: request.signal ?? new AbortController().signal,
565
- envelope: {},
566
565
  payload: request.payload,
567
566
  guardInputs: request.guardInputs,
568
567
  // The key as a handler can use it. A non-string is not a key: the
@@ -591,6 +590,7 @@ export class LambderMockApp {
591
590
  methods: {
592
591
  rateLimit: async (policy, key) => { await chargeRateLimit(policy, key, true); },
593
592
  isRateLimited: (policy, key) => chargeRateLimit(policy, key, false),
593
+ ...responseToolsOf(ctx, request.host),
594
594
  },
595
595
  });
596
596
  return ctx;
@@ -683,7 +683,6 @@ export class LambderMockApp {
683
683
  callCtx.payload = request.payload;
684
684
  const payload = await handler(callCtx);
685
685
  return envelopeAnswer(buildApiEnvelope(this.apiVersion, payload === undefined ? null : payload, {
686
- message: callCtx.envelope.message,
687
686
  logList: callCtx.logList,
688
687
  }));
689
688
  }
@@ -1,5 +1,5 @@
1
1
  import type { z } from "zod";
2
- import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
2
+ import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
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";
@@ -282,8 +282,8 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
282
282
  };
283
283
  /**
284
284
  * What the server's input validation handler answers, as a mock states it:
285
- * `res.api(payload, config)` as data, with the status it went out with (200
286
- * unless named).
285
+ * the handler's `res.api(payload, config)` as data, with the status it went
286
+ * out with (200 unless named).
287
287
  */
288
288
  export type LambderMockInvalidInputAnswer = {
289
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"> & {
@@ -12,6 +12,24 @@
12
12
  * awaits either.
13
13
  */
14
14
  export type MaybePromise<T> = T | Promise<T>;
15
+ /**
16
+ * T with every object and array readonly, all the way down: what an API
17
+ * handler's returned answer is checked against.
18
+ *
19
+ * An answer is a return value, and TypeScript widens the literals of a
20
+ * return value whose expected type is still generic (`{ kind: "a" }` reads
21
+ * as `{ kind: string }`), where a call argument keeps them. The handler's
22
+ * return is therefore its own `const` type parameter, which keeps literals;
23
+ * `const` also makes array literals readonly, which a schema's mutable arrays
24
+ * would refuse, so the bound is this readonly view of the output. Nothing
25
+ * writes to an answer (it is parsed and sent), so readonly costs nothing.
26
+ * Functions and Dates pass through whole. Eight levels deep and no further,
27
+ * because an output may be recursive (`z.json()`), and past that depth the
28
+ * type is left as it is.
29
+ */
30
+ export type LambderReadonlyDeep<T, TDepth extends unknown[] = []> = TDepth["length"] extends 8 ? T : T extends (...args: never[]) => unknown ? T : T extends Date ? T : T extends object ? {
31
+ readonly [K in keyof T]: LambderReadonlyDeep<T[K], [...TDepth, unknown]>;
32
+ } : T;
15
33
  /**
16
34
  * A declaration map with AT LEAST ONE entry: the union, over every declarable
17
35
  * name, of "this one required and the rest optional".
@@ -21,8 +21,9 @@ export type LambderHeaderTarget = {
21
21
  addHeader(key: string, value: string): unknown;
22
22
  };
23
23
  /**
24
- * Response headers written while a call runs (`res.setHeader`, `res.addHeader`,
25
- * the session controller's Set-Cookie), applied onto the answer once the
24
+ * Response headers written while a call runs (`ctx.setResponseHeader`,
25
+ * `ctx.addResponseHeader`, `ctx.setCookie`, the session controller's
26
+ * Set-Cookie), applied onto the answer once the
26
27
  * call has one. Recorded as operations in call order rather than as a map,
27
28
  * so `set` replaces what the answer itself carries (a Content-Type, say) and
28
29
  * `add` appends to it, exactly as if called on the answer directly.
@@ -36,8 +36,9 @@ export const addAnswerHeader = (headers, name, value) => {
36
36
  headers[name] = [value];
37
37
  };
38
38
  /**
39
- * Response headers written while a call runs (`res.setHeader`, `res.addHeader`,
40
- * the session controller's Set-Cookie), applied onto the answer once the
39
+ * Response headers written while a call runs (`ctx.setResponseHeader`,
40
+ * `ctx.addResponseHeader`, `ctx.setCookie`, the session controller's
41
+ * Set-Cookie), applied onto the answer once the
41
42
  * call has one. Recorded as operations in call order rather than as a map,
42
43
  * so `set` replaces what the answer itself carries (a Content-Type, say) and
43
44
  * `add` appends to it, exactly as if called on the answer directly.
@@ -4,7 +4,6 @@
4
4
  * Contracts are built via method chaining and inferred using typeof lambder.ApiContract
5
5
  */
6
6
  import type { LambderCrashDetail } from "./LambderCrashDetail.js";
7
- import type { LambderNonEmptyOptionMap } from "../util/LambderTypeUtilities.js";
8
7
  import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
9
8
  import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue } from "./LambderApiOptionValues.js";
10
9
  /** Whether an endpoint runs without a session or requires one (addApi versus addSessionApi). */
@@ -32,15 +31,18 @@ export type LambderApiContractShape = Record<string, {
32
31
  /** Present when the API declares idempotency: the `idempotency` option exactly as written. */
33
32
  idempotency?: LambderApiIdempotencyOption;
34
33
  }>;
35
- /** Envelope flags/channels the server may set beside (or instead of) the payload. */
34
+ /**
35
+ * Envelope flags and channels beside (or instead of) the payload: what a
36
+ * refusal, the pipeline's own answers, a hook or an error handler set. An API
37
+ * handler sets none of them itself: it returns its output or refuses.
38
+ */
36
39
  export type LambderApiResponseConfig = {
37
40
  versionExpired?: boolean;
38
41
  sessionExpired?: boolean;
39
42
  notAuthorized?: boolean;
40
- message?: any;
41
43
  /**
42
- * A refusal message, or a plain string when writing
43
- * (`res.api(null, { errorMessage: "..." })`): the envelope goes out with
44
+ * A refusal message, or a plain string when writing (a refusal thrown
45
+ * with refuse(), or an error handler's `res.api(null, { errorMessage })`): the envelope goes out with
44
46
  * the message object either way. Read off the wire it can still be a
45
47
  * string, or no message at all, wherever a Lambder server did not write
46
48
  * the body (a hand-built mock answer, a proxy); refusalMessageOf reads
@@ -58,15 +60,8 @@ export type LambderApiResponseConfig = {
58
60
  crash?: LambderCrashDetail;
59
61
  };
60
62
  /**
61
- * The config a null answer carries: at least one of the reason fields, so
62
- * `res.api(null, {})` is a compile error. A bare null with no flag and no
63
- * message reaches the caller as a success whose payload is null, which is
64
- * indistinguishable from an endpoint that answered nothing on purpose.
65
- */
66
- export type LambderApiNullAnswerConfig = LambderNonEmptyOptionMap<Pick<LambderApiResponseConfig, "versionExpired" | "sessionExpired" | "notAuthorized" | "errorMessage" | "message">> & LambderApiResponseConfig;
67
- /**
68
- * The API wire envelope both sides speak: res.api() emits it, LambderCaller
69
- * parses it. `apiVersion` is always there (null when the server set none):
63
+ * The API wire envelope both sides speak: the server writes it around a
64
+ * handler's output or a refusal, LambderCaller parses it. `apiVersion` is always there (null when the server set none):
70
65
  * it is how a reader tells a Lambder envelope from another JSON answer, such
71
66
  * as API Gateway's own `{ "message": ... }` errors, so an answer without it
72
67
  * reads as a server failure.
@@ -26,10 +26,9 @@ export type LambderApiRefusalOptions = {
26
26
  /**
27
27
  * A typed refusal: "this request is denied/invalid" as opposed to "the server
28
28
  * crashed". Throw it from anywhere in an API call's call stack (handlers,
29
- * hooks, or nested helpers with no access to the per-request resolver) and
30
- * the render pipeline maps it onto the structured API envelope
31
- * (`res.api(null, { errorMessage, notAuthorized, sessionExpired })`) instead
32
- * of routing it through setGlobalErrorHandler, so refusals never reach crash
29
+ * hooks, guards or nested helpers) and the render pipeline maps it onto the
30
+ * structured API envelope (its errorMessage, notAuthorized and
31
+ * sessionExpired) instead of routing it through setGlobalErrorHandler, so refusals never reach crash
33
32
  * logging and clients receive a parseable response.
34
33
  *
35
34
  * Thrown outside an API call (e.g. in a route handler) it behaves like any
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * A typed refusal: "this request is denied/invalid" as opposed to "the server
3
3
  * crashed". Throw it from anywhere in an API call's call stack (handlers,
4
- * hooks, or nested helpers with no access to the per-request resolver) and
5
- * the render pipeline maps it onto the structured API envelope
6
- * (`res.api(null, { errorMessage, notAuthorized, sessionExpired })`) instead
7
- * of routing it through setGlobalErrorHandler, so refusals never reach crash
4
+ * hooks, guards or nested helpers) and the render pipeline maps it onto the
5
+ * structured API envelope (its errorMessage, notAuthorized and
6
+ * sessionExpired) instead of routing it through setGlobalErrorHandler, so refusals never reach crash
8
7
  * logging and clients receive a parseable response.
9
8
  *
10
9
  * Thrown outside an API call (e.g. in a route handler) it behaves like any
@@ -5,7 +5,7 @@ import { type LambderLambdaHttpResult } from "../invoke/LambderLambdaEvent.js";
5
5
  import type { LambderCreatedSession } from "../session/LambderSessionManager.js";
6
6
  import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
7
7
  import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
8
- import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
8
+ import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignatureMap.js";
9
9
  import type { LambderGuardInputsProviderOption } from "../shared/wire/LambderCallOptions.js";
10
10
  /**
11
11
  * How one visitor differs from the next. Everything is optional: a visitor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "8.3.1",
3
+ "version": "9.0.2",
4
4
  "sideEffects": false,
5
5
  "description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
6
6
  "keywords": [