lambder 6.0.1 → 7.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.
Files changed (195) hide show
  1. package/CHANGELOG.md +2316 -0
  2. package/README.md +60 -33
  3. package/dist/api/LambderApiAnswer.d.ts +40 -0
  4. package/dist/api/LambderApiAnswer.js +19 -0
  5. package/dist/api/LambderApiCallContext.d.ts +38 -0
  6. package/dist/api/LambderApiCallContext.js +13 -0
  7. package/dist/api/LambderApiDefinition.d.ts +18 -0
  8. package/dist/api/LambderApiDefinition.js +1 -0
  9. package/dist/api/LambderApiEnvelope.d.ts +67 -0
  10. package/dist/api/LambderApiEnvelope.js +180 -0
  11. package/dist/api/LambderApiGuards.d.ts +302 -0
  12. package/dist/api/LambderApiGuards.js +134 -0
  13. package/dist/api/LambderApiIdempotency.d.ts +122 -0
  14. package/dist/api/LambderApiIdempotency.js +330 -0
  15. package/dist/api/LambderApiPipeline.d.ts +134 -0
  16. package/dist/api/LambderApiPipeline.js +221 -0
  17. package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
  18. package/dist/api/LambderApiPolicyEngine.js +77 -0
  19. package/dist/api/LambderApiRateLimits.d.ts +206 -0
  20. package/dist/api/LambderApiRateLimits.js +239 -0
  21. package/dist/api/LambderApiRequest.d.ts +101 -0
  22. package/dist/api/LambderApiRequest.js +129 -0
  23. package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
  24. package/dist/api/LambderApiValidationRefusal.js +40 -0
  25. package/dist/client/LambderCaller.d.ts +62 -55
  26. package/dist/client/LambderCaller.js +147 -90
  27. package/dist/client/lambderFetchTransport.d.ts +9 -0
  28. package/dist/client/lambderFetchTransport.js +71 -0
  29. package/dist/client.d.ts +20 -10
  30. package/dist/client.js +11 -5
  31. package/dist/core/Lambder.d.ts +117 -253
  32. package/dist/core/Lambder.js +374 -341
  33. package/dist/core/LambderContext.d.ts +54 -44
  34. package/dist/core/LambderContext.js +41 -110
  35. package/dist/core/LambderCreateOptions.d.ts +285 -0
  36. package/dist/core/LambderCreateOptions.js +44 -0
  37. package/dist/core/LambderFiles.d.ts +1 -45
  38. package/dist/core/LambderFiles.js +18 -38
  39. package/dist/core/LambderIndexHtml.d.ts +37 -0
  40. package/dist/core/LambderIndexHtml.js +87 -0
  41. package/dist/core/LambderPolicyBuilders.d.ts +17 -0
  42. package/dist/core/LambderPolicyBuilders.js +16 -0
  43. package/dist/core/LambderPublicFiles.d.ts +5 -2
  44. package/dist/core/LambderPublicFiles.js +7 -2
  45. package/dist/core/LambderResolver.d.ts +8 -6
  46. package/dist/core/LambderResponse.d.ts +29 -11
  47. package/dist/core/LambderResponse.js +96 -49
  48. package/dist/core/LambderResponseBuilder.d.ts +18 -14
  49. package/dist/core/LambderResponseBuilder.js +19 -25
  50. package/dist/core/LambderRouting.d.ts +18 -7
  51. package/dist/core/LambderRouting.js +17 -7
  52. package/dist/core/LambderTemplatingEngine.d.ts +0 -62
  53. package/dist/core/LambderTemplatingEngine.js +7 -3
  54. package/dist/index.d.ts +85 -32
  55. package/dist/index.js +44 -16
  56. package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
  57. package/dist/invoke/LambderInvokeCaller.js +140 -335
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
  59. package/dist/invoke/LambderInvokeOutcome.js +129 -0
  60. package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
  61. package/dist/invoke/LambderLambdaEvent.js +187 -0
  62. package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
  63. package/dist/invoke/lambderHandlerTransport.js +89 -0
  64. package/dist/mock/LambderMockApp.d.ts +352 -0
  65. package/dist/mock/LambderMockApp.js +815 -0
  66. package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
  67. package/dist/mock/LambderMockBrowserCookies.js +76 -0
  68. package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
  69. package/dist/mock/LambderMockCallRecorder.js +183 -0
  70. package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
  71. package/dist/mock/LambderMockCreateOptions.js +9 -0
  72. package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
  73. package/dist/mock/LambderMockEntryRegistry.js +126 -0
  74. package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
  75. package/dist/mock/LambderMockFailureInjector.js +138 -0
  76. package/dist/mock/LambderMockTypes.d.ts +421 -0
  77. package/dist/mock/LambderMockTypes.js +8 -0
  78. package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
  79. package/dist/mock/lambderMockConsoleLogger.js +35 -0
  80. package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
  81. package/dist/mock/lambderMockInvokeTransport.js +52 -0
  82. package/dist/mock/lambderMockMswHandler.d.ts +99 -0
  83. package/dist/mock/lambderMockMswHandler.js +126 -0
  84. package/dist/mock.d.ts +34 -0
  85. package/dist/mock.js +27 -0
  86. package/dist/session/LambderSessionController.d.ts +199 -30
  87. package/dist/session/LambderSessionController.js +396 -82
  88. package/dist/session/LambderSessionCrypto.d.ts +66 -0
  89. package/dist/session/LambderSessionCrypto.js +101 -0
  90. package/dist/session/LambderSessionManager.d.ts +118 -80
  91. package/dist/session/LambderSessionManager.js +212 -184
  92. package/dist/shared/LambderI18n.d.ts +6 -6
  93. package/dist/shared/LambderI18n.js +1 -1
  94. package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
  95. package/dist/shared/contracts/LambderFileSource.js +19 -0
  96. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
  97. package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
  98. package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
  99. package/dist/shared/contracts/LambderRateLimiter.js +24 -0
  100. package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
  101. package/dist/shared/contracts/LambderSessionStore.js +13 -0
  102. package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
  103. package/dist/shared/transport/LambderApiTransport.js +65 -0
  104. package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
  105. package/dist/shared/transport/LambderCookieJar.js +246 -0
  106. package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
  107. package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
  108. package/dist/shared/util/LambderBase64.d.ts +10 -0
  109. package/dist/shared/util/LambderBase64.js +27 -0
  110. package/dist/shared/util/LambderCallAbort.d.ts +62 -0
  111. package/dist/shared/util/LambderCallAbort.js +80 -0
  112. package/dist/shared/util/LambderClientIp.d.ts +32 -0
  113. package/dist/shared/util/LambderClientIp.js +56 -0
  114. package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
  115. package/dist/shared/util/LambderExpiringMap.js +217 -0
  116. package/dist/shared/util/LambderKeyFields.d.ts +32 -0
  117. package/dist/shared/util/LambderKeyFields.js +34 -0
  118. package/dist/shared/util/LambderNodeModules.d.ts +9 -0
  119. package/dist/shared/util/LambderNodeModules.js +39 -0
  120. package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
  121. package/dist/shared/util/LambderOptionChecks.js +33 -0
  122. package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
  123. package/dist/shared/util/LambderResponseBrand.js +18 -0
  124. package/dist/shared/util/LambderTextDigest.d.ts +17 -0
  125. package/dist/shared/util/LambderTextDigest.js +34 -0
  126. package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
  127. package/dist/shared/util/LambderTypeUtilities.js +8 -0
  128. package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
  129. package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
  130. package/dist/shared/wire/LambderApiContract.d.ts +129 -0
  131. package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
  132. package/dist/shared/wire/LambderApiOptionValues.js +11 -0
  133. package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
  134. package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
  135. package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
  136. package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
  137. package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
  138. package/dist/shared/wire/LambderCallOptions.js +17 -0
  139. package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
  140. package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
  141. package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
  142. package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
  143. package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
  144. package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
  145. package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
  146. package/dist/shared/wire/LambderHttpStatus.js +1 -0
  147. package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
  148. package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
  149. package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
  150. package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
  151. package/dist/stores/LambderDdbCache.d.ts +12 -9
  152. package/dist/stores/LambderDdbCache.js +56 -47
  153. package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
  154. package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
  155. package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
  156. package/dist/stores/LambderDdbRateLimiter.js +47 -45
  157. package/dist/stores/LambderDdbSdk.d.ts +83 -6
  158. package/dist/stores/LambderDdbSdk.js +83 -2
  159. package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
  160. package/dist/stores/LambderDdbSessionStore.js +161 -0
  161. package/dist/stores/LambderHttpFileSource.d.ts +1 -1
  162. package/dist/stores/LambderHttpFileSource.js +10 -1
  163. package/dist/stores/LambderLocalFileSource.d.ts +15 -0
  164. package/dist/stores/LambderLocalFileSource.js +28 -0
  165. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
  166. package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
  167. package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
  168. package/dist/stores/LambderMemoryRateLimiter.js +64 -0
  169. package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
  170. package/dist/stores/LambderMemorySessionStore.js +74 -0
  171. package/dist/stores/LambderS3FileSource.d.ts +1 -1
  172. package/dist/stores/LambderS3FileSource.js +1 -1
  173. package/package.json +26 -24
  174. package/dist/client/LambderMSW.d.ts +0 -69
  175. package/dist/client/LambderMSW.js +0 -121
  176. package/dist/policies/LambderApiGuards.d.ts +0 -256
  177. package/dist/policies/LambderApiGuards.js +0 -94
  178. package/dist/policies/LambderApiIdempotency.d.ts +0 -58
  179. package/dist/policies/LambderApiIdempotency.js +0 -219
  180. package/dist/policies/LambderApiPolicies.d.ts +0 -42
  181. package/dist/policies/LambderApiPolicies.js +0 -52
  182. package/dist/policies/LambderApiRateLimits.d.ts +0 -132
  183. package/dist/policies/LambderApiRateLimits.js +0 -119
  184. package/dist/shared/LambderApiContract.d.ts +0 -57
  185. package/dist/shared/LambderApiOutcome.d.ts +0 -69
  186. package/dist/shared/LambderCallOptions.d.ts +0 -71
  187. package/dist/shared/LambderCallOptions.js +0 -16
  188. package/dist/shared/node-polyfills.d.ts +0 -4
  189. package/dist/shared/node-polyfills.js +0 -58
  190. package/dist/stores/LambderDdbIdempotency.js +0 -229
  191. package/dist/testing.d.ts +0 -9
  192. package/dist/testing.js +0 -8
  193. /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
  194. /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
  195. /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
@@ -1,13 +1,13 @@
1
- import type { HttpStatusCode } from "../core/LambderResponse.js";
2
- export type LambderApiErrorOptions = {
1
+ import type { LambderHttpStatusCode } from "./LambderHttpStatus.js";
2
+ export type LambderApiRefusalOptions = {
3
3
  /**
4
- * Structured, user-facing failure detail placed on the API envelope's
5
- * `errorMessage` field. Any shape the app's errorMessageHandler expects
6
- * (e.g. `{ type: "warning", content: "..." }`). Defaults to the error
7
- * message string, so a bare `throw new LambderApiError("...")` is still
4
+ * User-facing failure detail placed on the API envelope's `errorMessage`
5
+ * field: a refusal message (`{ type: "warning", content: "..." }`, with
6
+ * an app's own `code`) or a plain string. Defaults to the error message
7
+ * string, so a bare `throw new LambderApiRefusal("...")` is still
8
8
  * visible to the client.
9
9
  */
10
- errorMessage?: any;
10
+ errorMessage?: LambderAppRefusalMessage | string;
11
11
  /** Sets the envelope's `notAuthorized` flag (routed to the caller's notAuthorizedHandler). */
12
12
  notAuthorized?: boolean;
13
13
  /** Sets the envelope's `sessionExpired` flag (the caller clears session cookies and calls sessionExpiredHandler). */
@@ -17,7 +17,7 @@ export type LambderApiErrorOptions = {
17
17
  * semantic channel. Avoid 5xx (LambderCaller treats those as crashes) and
18
18
  * 422 (reserved for input validation).
19
19
  */
20
- statusCode?: HttpStatusCode;
20
+ statusCode?: LambderHttpStatusCode;
21
21
  /** Extra response headers on the refusal (e.g. Retry-After on a rate limit). */
22
22
  headers?: Record<string, string>;
23
23
  /** Underlying cause, preserved on the standard Error `cause` property. */
@@ -25,8 +25,8 @@ export type LambderApiErrorOptions = {
25
25
  };
26
26
  /**
27
27
  * A typed refusal: "this request is denied/invalid" as opposed to "the server
28
- * crashed". Throw it from anywhere in an API call's call stack — handlers,
29
- * hooks, or nested helpers that have no access to the per-request resolver —
28
+ * crashed". Throw it from anywhere in an API call's call stack (handlers,
29
+ * hooks, or nested helpers that have no access to the per-request resolver)
30
30
  * and the render pipeline maps it onto the structured API envelope
31
31
  * (`res.api(null, { errorMessage, notAuthorized, sessionExpired })`) instead
32
32
  * of routing it through setGlobalErrorHandler. Refusals therefore never reach
@@ -39,22 +39,22 @@ export type LambderApiErrorOptions = {
39
39
  * checks) may import and throw it from packages used by both server and
40
40
  * browser builds; in the browser it is just an Error.
41
41
  */
42
- export declare class LambderApiError extends Error {
42
+ export declare class LambderApiRefusal extends Error {
43
43
  /**
44
44
  * Brand for detection across duplicate lambder installs: when two copies
45
- * of the package coexist in one bundle, `instanceof LambderApiError` fails
45
+ * of the package coexist in one bundle, `instanceof LambderApiRefusal` fails
46
46
  * across them while this marker does not. The pipeline checks the brand.
47
47
  */
48
- readonly isLambderApiError = true;
49
- readonly errorMessage?: any;
48
+ readonly isLambderApiRefusal = true;
49
+ readonly errorMessage?: LambderAppRefusalMessage | string;
50
50
  readonly notAuthorized?: boolean;
51
51
  readonly sessionExpired?: boolean;
52
- readonly statusCode?: HttpStatusCode;
52
+ readonly statusCode?: LambderHttpStatusCode;
53
53
  readonly headers?: Record<string, string>;
54
- constructor(message: string, options?: LambderApiErrorOptions);
54
+ constructor(message: string, options?: LambderApiRefusalOptions);
55
55
  }
56
- /** Brand-based type guard (see LambderApiError.isLambderApiError). */
57
- export declare const isLambderApiError: (err: unknown) => err is LambderApiError;
56
+ /** Brand-based type guard (see LambderApiRefusal.isLambderApiRefusal). */
57
+ export declare const isLambderApiRefusal: (err: unknown) => err is LambderApiRefusal;
58
58
  /**
59
59
  * The standard shape refusals carry on the envelope's errorMessage field.
60
60
  * `code` is the refusal's machine-readable identity: clients branch and
@@ -62,16 +62,36 @@ export declare const isLambderApiError: (err: unknown) => err is LambderApiError
62
62
  * human-readable fallback for codes a client does not know yet. Apps keep
63
63
  * their own typed code vocabulary; the framework's own refusals carry a
64
64
  * LambderRefusalCode. The caller's errorMessageHandler receives the object
65
- * as-is; apps with their own errorMessage vocabulary can keep using
66
- * LambderApiError directly instead.
65
+ * as-is, typed as LambderAppRefusalMessage or a plain string; an app's own
66
+ * vocabulary goes in `code`.
67
67
  */
68
- export type LambderRefusalMessage = {
68
+ export type LambderRefusalMessage<TAppCode extends string = never> = {
69
69
  type: "warning" | "error" | "info";
70
- /** Machine-readable identity of the refusal (the app's own vocabulary, or a LambderRefusalCode). */
71
- code?: string;
70
+ /**
71
+ * Machine-readable identity of the refusal: a LambderRefusalCode, plus
72
+ * whatever vocabulary the reader names in TAppCode.
73
+ *
74
+ * Parameterized rather than widened with `string & {}`, because a union
75
+ * with `string` in it does not narrow: inside a `switch(message.code)`
76
+ * the case was not assignable and the `default: never` assertion failed,
77
+ * which is exactly the exhaustiveness the codes exist for. A client that
78
+ * reads its own vocabulary declares it
79
+ * (`LambderRefusalMessage<"app/not-verified" | ...>`) and gets a switch
80
+ * that is checked; a value an app WRITES takes LambderAppRefusalMessage,
81
+ * where any code is welcome.
82
+ */
83
+ code?: LambderRefusalCode | TAppCode;
72
84
  title?: string;
73
85
  content: string;
74
86
  };
87
+ /**
88
+ * The refusal shape an app authors: any code, with the framework's own still
89
+ * autocompleting. What every option that takes a message from an app is
90
+ * typed as (a rate-limit policy's errorMessage, the mock's failure
91
+ * injection); LambderRefusalMessage itself defaults to the framework's codes
92
+ * alone, so a reader's switch over it is exhaustive.
93
+ */
94
+ export type LambderAppRefusalMessage = LambderRefusalMessage<string & {}>;
75
95
  /**
76
96
  * Codes the framework stamps on the refusals it authors itself, under the
77
97
  * reserved `lambder/` prefix so app codes never collide. Compare against
@@ -89,6 +109,8 @@ export declare const LAMBDER_REFUSAL_CODES: {
89
109
  readonly apiNotFound: "lambder/api-not-found";
90
110
  /** The request's compressed payload is malformed or over the size limit (400). */
91
111
  readonly invalidRequestPayload: "lambder/invalid-request-payload";
112
+ /** Only the mock runtime emits it: the endpoint is registered as not mocked, with a reason. */
113
+ readonly notMocked: "lambder/not-mocked";
92
114
  };
93
115
  export type LambderRefusalCode = (typeof LAMBDER_REFUSAL_CODES)[keyof typeof LAMBDER_REFUSAL_CODES];
94
116
  export type LambderRefuseOptions = {
@@ -103,7 +125,7 @@ export type LambderRefuseOptions = {
103
125
  /** Sets the envelope's sessionExpired flag. */
104
126
  sessionExpired?: boolean;
105
127
  /** HTTP status of the refusal. Default 200; avoid 5xx (caller treats as crash) and 422 (reserved for validation). */
106
- statusCode?: HttpStatusCode;
128
+ statusCode?: LambderHttpStatusCode;
107
129
  /** Extra response headers on the refusal (e.g. Retry-After). */
108
130
  headers?: Record<string, string>;
109
131
  /** Underlying cause, preserved on the Error cause property. */
@@ -111,10 +133,10 @@ export type LambderRefuseOptions = {
111
133
  };
112
134
  /**
113
135
  * Refuse the current API call: a routine business "no" (not found, invalid
114
- * input, not allowed) with a user-facing message. Throws a LambderApiError
136
+ * input, not allowed) with a user-facing message. Throws a LambderApiRefusal
115
137
  * carrying the standard LambderRefusalMessage shape, so the pipeline maps it
116
138
  * onto the structured envelope instead of a 500, and crash logging never
117
- * sees it. Callable from anywhere in the call stack — handlers, hooks,
139
+ * sees it. Callable from anywhere in the call stack: handlers, hooks,
118
140
  * guards, shared helpers with no resolver access.
119
141
  *
120
142
  * The const carries the annotation so TypeScript applies never-return
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A typed refusal: "this request is denied/invalid" as opposed to "the server
3
- * crashed". Throw it from anywhere in an API call's call stack — handlers,
4
- * hooks, or nested helpers that have no access to the per-request resolver —
3
+ * crashed". Throw it from anywhere in an API call's call stack (handlers,
4
+ * hooks, or nested helpers that have no access to the per-request resolver)
5
5
  * and the render pipeline maps it onto the structured API envelope
6
6
  * (`res.api(null, { errorMessage, notAuthorized, sessionExpired })`) instead
7
7
  * of routing it through setGlobalErrorHandler. Refusals therefore never reach
@@ -14,13 +14,13 @@
14
14
  * checks) may import and throw it from packages used by both server and
15
15
  * browser builds; in the browser it is just an Error.
16
16
  */
17
- export class LambderApiError extends Error {
17
+ export class LambderApiRefusal extends Error {
18
18
  /**
19
19
  * Brand for detection across duplicate lambder installs: when two copies
20
- * of the package coexist in one bundle, `instanceof LambderApiError` fails
20
+ * of the package coexist in one bundle, `instanceof LambderApiRefusal` fails
21
21
  * across them while this marker does not. The pipeline checks the brand.
22
22
  */
23
- isLambderApiError = true;
23
+ isLambderApiRefusal = true;
24
24
  errorMessage;
25
25
  notAuthorized;
26
26
  sessionExpired;
@@ -28,7 +28,7 @@ export class LambderApiError extends Error {
28
28
  headers;
29
29
  constructor(message, options = {}) {
30
30
  super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
31
- this.name = "LambderApiError";
31
+ this.name = "LambderApiRefusal";
32
32
  this.errorMessage = options.errorMessage ?? message;
33
33
  this.notAuthorized = options.notAuthorized;
34
34
  this.sessionExpired = options.sessionExpired;
@@ -36,8 +36,8 @@ export class LambderApiError extends Error {
36
36
  this.headers = options.headers;
37
37
  }
38
38
  }
39
- /** Brand-based type guard (see LambderApiError.isLambderApiError). */
40
- export const isLambderApiError = (err) => err instanceof Error && err.isLambderApiError === true;
39
+ /** Brand-based type guard (see LambderApiRefusal.isLambderApiRefusal). */
40
+ export const isLambderApiRefusal = (err) => err instanceof Error && err.isLambderApiRefusal === true;
41
41
  /**
42
42
  * Codes the framework stamps on the refusals it authors itself, under the
43
43
  * reserved `lambder/` prefix so app codes never collide. Compare against
@@ -55,13 +55,15 @@ export const LAMBDER_REFUSAL_CODES = {
55
55
  apiNotFound: "lambder/api-not-found",
56
56
  /** The request's compressed payload is malformed or over the size limit (400). */
57
57
  invalidRequestPayload: "lambder/invalid-request-payload",
58
+ /** Only the mock runtime emits it: the endpoint is registered as not mocked, with a reason. */
59
+ notMocked: "lambder/not-mocked",
58
60
  };
59
61
  /**
60
62
  * Refuse the current API call: a routine business "no" (not found, invalid
61
- * input, not allowed) with a user-facing message. Throws a LambderApiError
63
+ * input, not allowed) with a user-facing message. Throws a LambderApiRefusal
62
64
  * carrying the standard LambderRefusalMessage shape, so the pipeline maps it
63
65
  * onto the structured envelope instead of a 500, and crash logging never
64
- * sees it. Callable from anywhere in the call stack — handlers, hooks,
66
+ * sees it. Callable from anywhere in the call stack: handlers, hooks,
65
67
  * guards, shared helpers with no resolver access.
66
68
  *
67
69
  * The const carries the annotation so TypeScript applies never-return
@@ -69,7 +71,7 @@ export const LAMBDER_REFUSAL_CODES = {
69
71
  * `row` is defined afterwards).
70
72
  */
71
73
  export const refuse = (content, options = {}) => {
72
- throw new LambderApiError(content, {
74
+ throw new LambderApiRefusal(content, {
73
75
  errorMessage: {
74
76
  type: options.type ?? "warning",
75
77
  ...(options.code !== undefined ? { code: options.code } : {}),
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The per-call options both callers take, the contract-driven typing of a
3
+ * call's arguments, and the runtime merge of guard inputs, shared by the
4
+ * browser caller (LambderCaller) and the server-side invoke caller
5
+ * (LambderInvokeCaller). Both speak the same envelope to the same kind of
6
+ * contract, so what an API demands of its caller (a guardInput-mode guard's
7
+ * value, say) is decided here once and the two callers cannot drift on it.
8
+ * Pure types and one dependency-free function, so the browser entry resolves
9
+ * it.
10
+ */
11
+ import type { LambderContractIdempotencyOf } from "./LambderApiContract.js";
12
+ type IsAny<T> = 0 extends (1 & T) ? true : false;
13
+ /**
14
+ * The options every call takes, whichever caller sends it. Each caller adds
15
+ * its own on top: the browser's per-call handler overrides, the invoke
16
+ * caller's `clientIp` and `session`.
17
+ */
18
+ export type LambderSharedCallOptions = {
19
+ /** Extra request headers the server sees. */
20
+ headers?: Record<string, string>;
21
+ /** Abort the call after this many ms; overrides the constructor default. */
22
+ timeoutMs?: number;
23
+ /** External abort signal, combined with the timeout when both are set. */
24
+ signal?: AbortSignal;
25
+ /**
26
+ * Overrides the constructor's requestCompression for this call: `false`
27
+ * sends the payload plainly (a hot path where the CPU matters more than
28
+ * the bytes), `true` compresses it regardless of the size threshold.
29
+ * Either way a payload is only sent compressed when that is smaller.
30
+ */
31
+ compressRequest?: boolean;
32
+ /**
33
+ * Values for the API's guardInput-mode guards, keyed by guard name; sent
34
+ * beside the payload and consumed by the guards before validation. The
35
+ * typed contract makes this REQUIRED for APIs that declare such guards,
36
+ * except the guards a guardInputsProvider covers (these merge on top of
37
+ * the provider's values).
38
+ */
39
+ guardInputs?: Record<string, unknown>;
40
+ /**
41
+ * Replay-protection key for APIs declared idempotent on the server.
42
+ * Generate once per logical operation with
43
+ * LambderCaller.createIdempotencyKey() and send the same key on retries:
44
+ * duplicates of an in-flight request refuse, and repeats of a completed
45
+ * one replay its stored response instead of re-executing. Must be
46
+ * UNGUESSABLE random (it scopes the replay record for logged-out clients)
47
+ * and at least 16 characters; the server refuses shorter keys with a 400.
48
+ *
49
+ * The typed contract makes this REQUIRED for an API whose entry declares
50
+ * `idempotency`, the way it does for guardInput values: a server
51
+ * declaration that reads as protection and silently provides none (the
52
+ * server runs a keyless call, which dedupes nothing) is exactly what the
53
+ * typed caller is for.
54
+ */
55
+ idempotencyKey?: string;
56
+ };
57
+ /** The payload type one API of a contract takes; `any` for an untyped caller or a name the contract does not know. */
58
+ type LambderContractInputOf<TContract, TApiName> = IsAny<TContract> extends true ? any : TApiName extends keyof TContract ? TContract[TApiName] extends {
59
+ input: infer TInput;
60
+ } ? TInput : any : any;
61
+ /** The output type one API of a contract declares; `any` for an untyped caller or a name the contract does not know. */
62
+ export type LambderContractOutputOf<TContract, TApiName> = IsAny<TContract> extends true ? any : TApiName extends keyof TContract ? TContract[TApiName] extends {
63
+ output: infer TOutput;
64
+ } ? TOutput : any : any;
65
+ type GuardInputsOf<TEntry> = TEntry extends {
66
+ guardInputs: infer G;
67
+ } ? G : never;
68
+ /** Input type of guard G on one contract entry; never when that API does not declare it. */
69
+ type GuardInputOf<TEntry, G extends string> = GuardInputsOf<TEntry> extends infer I ? (G extends keyof I ? I[G] : never) : never;
70
+ /**
71
+ * What guardInputsProvider returns: for every provided guard name, the value
72
+ * the contract's APIs expect for it (a union across APIs when they differ).
73
+ * Naming a guard no API declares in guardInput mode resolves to never, so a
74
+ * typo fails the provider's return type instead of going missing at runtime.
75
+ */
76
+ export type LambderProvidedGuardInputs<TContract, TProvided extends string> = IsAny<TContract> extends true ? Record<TProvided, unknown> : {
77
+ [G in TProvided]: {
78
+ [K in keyof TContract]: GuardInputOf<TContract[K], G>;
79
+ }[keyof TContract];
80
+ };
81
+ /**
82
+ * Supplies guardInputs for every call from one place (the organization the
83
+ * UI is on, a device token), keyed by guard name; per-call guardInputs
84
+ * merge on top. Name the guards it covers in the caller's second type
85
+ * parameter, `new LambderCaller<Contract, "orgPermission">`, and calls to
86
+ * APIs whose guardInput guards are all covered do not require the
87
+ * options argument. May be async; a throw fails the call as an unknown
88
+ * error before anything is sent.
89
+ */
90
+ export type LambderGuardInputsProvider<TContract, TProvided extends string> = (apiName: keyof TContract & string) => LambderProvidedGuardInputs<TContract, TProvided> | Promise<LambderProvidedGuardInputs<TContract, TProvided>>;
91
+ /** Optional until the caller names provided guards: naming them without a provider would send nothing. */
92
+ export type LambderGuardInputsProviderOption<TContract, TProvided extends string> = [
93
+ TProvided
94
+ ] extends [never] ? {
95
+ guardInputsProvider?: LambderGuardInputsProvider<TContract, TProvided>;
96
+ } : {
97
+ guardInputsProvider: LambderGuardInputsProvider<TContract, TProvided>;
98
+ };
99
+ /** An API's guardInput guards the provider does not cover: those the call must still pass. */
100
+ type RemainingGuardInputs<TEntry, TProvided extends string> = Omit<GuardInputsOf<TEntry>, TProvided>;
101
+ /**
102
+ * The guardInputs field of one API's options: absent when its guards take no
103
+ * client input, optional when the provider covers every one of them (they may
104
+ * still be overridden per call), and mandatory with the uncovered ones spelled
105
+ * out otherwise.
106
+ */
107
+ type ContractGuardInputsField<TEntry, TProvided extends string> = [
108
+ GuardInputsOf<TEntry>
109
+ ] extends [never] ? {} : [keyof RemainingGuardInputs<TEntry, TProvided>] extends [never] ? {
110
+ guardInputs?: Partial<GuardInputsOf<TEntry>>;
111
+ } : {
112
+ guardInputs: RemainingGuardInputs<TEntry, TProvided> & Partial<GuardInputsOf<TEntry>>;
113
+ };
114
+ /**
115
+ * The idempotencyKey field of one API's options: mandatory once the entry
116
+ * declares `idempotency`, absent otherwise.
117
+ *
118
+ * Read as "required unless it says false" rather than "required only when it
119
+ * says true", so an entry whose option widened to `boolean` (declared through
120
+ * a spread, or built in a helper) keeps the requirement instead of quietly
121
+ * losing its compile-time half.
122
+ */
123
+ type ContractIdempotencyKeyField<TContract, TApiName> = TApiName extends keyof TContract ? [LambderContractIdempotencyOf<TContract, TApiName>] extends [never] ? {} : [LambderContractIdempotencyOf<TContract, TApiName>] extends [false] ? {} : {
124
+ idempotencyKey: string;
125
+ } : {};
126
+ /**
127
+ * What the contract adds to one call's options: the guardInputs field and the
128
+ * idempotencyKey field, each decided by what the API's entry declares.
129
+ */
130
+ type ContractCallFields<TContract, TApiName, TProvided extends string> = TApiName extends keyof TContract ? ContractGuardInputsField<TContract[TApiName], TProvided> & ContractIdempotencyKeyField<TContract, TApiName> : {};
131
+ /**
132
+ * The options argument of one call: optional normally, REQUIRED when the
133
+ * contract demands something of it, so forgetting a guard's value or an
134
+ * idempotent API's key is a compile error at the call site rather than a 422
135
+ * from the server or a replay that never happens. TOptions is the caller's own
136
+ * per-call options type; the contract's fields are layered on top of it.
137
+ *
138
+ * `{} extends TFields` is the question "is every field the contract added
139
+ * optional": an empty object is assignable to a type whose properties are all
140
+ * optional and to nothing else.
141
+ */
142
+ type LambderCallOptionsArg<TContract, TApiName, TProvided extends string, TOptions extends {
143
+ guardInputs?: Record<string, unknown>;
144
+ }> = IsAny<TContract> extends true ? [options?: TOptions] : TApiName extends keyof TContract ? ContractCallFields<TContract, TApiName, TProvided> extends infer TFields ? {} extends TFields ? [options?: TOptions & TFields] : [options: TOptions & TFields] : never : [options?: TOptions];
145
+ /**
146
+ * Everything one call passes after the API name: the payload, then the
147
+ * options, both decided by the contract.
148
+ *
149
+ * The payload is optional only when the API's input accepts undefined, so
150
+ * `caller.api("getUser")` against `input: { id: string }` is a compile error
151
+ * at the call site rather than a 422 from the server. Building it as one rest
152
+ * tuple is what makes that possible: a plain optional parameter cannot be
153
+ * made mandatory by a later type, and TypeScript has no per-argument
154
+ * conditional otherwise.
155
+ *
156
+ * When the options argument is itself mandatory (an uncovered guardInput
157
+ * guard, an idempotent API's key), the payload cannot stay optional in front
158
+ * of it, since a tuple's required element may not follow an optional one.
159
+ * Such a call passes its payload explicitly, `undefined` included.
160
+ */
161
+ export type LambderCallArgs<TContract, TApiName, TProvided extends string, TOptions extends {
162
+ guardInputs?: Record<string, unknown>;
163
+ }> = LambderCallOptionsArg<TContract, TApiName, TProvided, TOptions> extends [options: infer TRequired] ? [payload: LambderContractInputOf<TContract, TApiName>, options: TRequired] : LambderCallOptionsArg<TContract, TApiName, TProvided, TOptions> extends [options?: infer TOptional] ? undefined extends LambderContractInputOf<TContract, TApiName> ? [payload?: LambderContractInputOf<TContract, TApiName>, options?: TOptional] : [payload: LambderContractInputOf<TContract, TApiName>, options?: TOptional] : never;
164
+ /**
165
+ * Provider values underneath, per-call values on top; undefined when neither
166
+ * side supplied any. Synchronous on purpose: a caller awaits its provider
167
+ * only when it has one, so a call without a provider still issues its
168
+ * request in the same tick it was made.
169
+ */
170
+ export declare const mergeGuardInputs: (provided: Record<string, unknown> | undefined, perCall: Record<string, unknown> | undefined) => Record<string, unknown> | undefined;
171
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The per-call options both callers take, the contract-driven typing of a
3
+ * call's arguments, and the runtime merge of guard inputs, shared by the
4
+ * browser caller (LambderCaller) and the server-side invoke caller
5
+ * (LambderInvokeCaller). Both speak the same envelope to the same kind of
6
+ * contract, so what an API demands of its caller (a guardInput-mode guard's
7
+ * value, say) is decided here once and the two callers cannot drift on it.
8
+ * Pure types and one dependency-free function, so the browser entry resolves
9
+ * it.
10
+ */
11
+ /**
12
+ * Provider values underneath, per-call values on top; undefined when neither
13
+ * side supplied any. Synchronous on purpose: a caller awaits its provider
14
+ * only when it has one, so a call without a provider still issues its
15
+ * request in the same tick it was made.
16
+ */
17
+ export const mergeGuardInputs = (provided, perCall) => provided !== undefined || perCall !== undefined ? { ...provided, ...perCall } : undefined;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The one place Lambder compresses and decompresses bytes.
3
3
  *
4
- * Five things compress: sessions, LambderDdbCache and LambderDdbIdempotency
4
+ * Five things compress: sessions, LambderDdbCache and LambderDdbIdempotencyStore
5
5
  * (Brotli at rest in DynamoDB), HTTP responses (Brotli or gzip, negotiated)
6
6
  * and request payloads (gzip from a browser, whose CompressionStream offers
7
7
  * nothing else; Brotli from a Node caller). They all compress text, so TEXT
@@ -21,11 +21,15 @@
21
21
  * implementation: a bug fixed here is fixed for records at rest and for
22
22
  * untrusted request bodies alike.
23
23
  *
24
- * zlib is loaded lazily through node-polyfills, so a module importing this
24
+ * zlib is loaded lazily through LambderNodeModules, so a module importing this
25
25
  * one can still sit in a frontend bundle's import graph via the package
26
- * root. The option that decides WHETHER to compress, and the encoding
27
- * vocabulary, live in LambderCompressionOption, which stays free of zlib
28
- * entirely so the browser entry can resolve it.
26
+ * root. Compressing needs zlib and is server-side; restoring runs anywhere,
27
+ * through zlib where it exists and through the web DecompressionStream
28
+ * otherwise, so the API core can restore a gzipped request payload in a
29
+ * browser (the mock runtime) under the same bound. The option that decides
30
+ * WHETHER to compress, and the encoding vocabulary, live in
31
+ * LambderCompressionOption, which stays free of zlib entirely so the browser
32
+ * entry can resolve it.
29
33
  */
30
34
  import type { LambderEncoding } from "./LambderCompressionOption.js";
31
35
  /** Why a bounded restore failed, for callers that answer rather than throw. */
@@ -81,7 +85,7 @@ export type LambderRestoreBound = {
81
85
  * UTF-8 decode; going through a string would replace every byte that is not
82
86
  * valid UTF-8 and hand back a body that is silently not what was sent.
83
87
  */
84
- export declare const restoreBytes: (compressed: Uint8Array, encoding: LambderEncoding, bound: LambderRestoreBound) => Promise<Buffer>;
88
+ export declare const restoreBytes: (compressed: Uint8Array, encoding: LambderEncoding, bound: LambderRestoreBound) => Promise<Uint8Array>;
85
89
  /**
86
90
  * Restores text: restoreBytes plus the UTF-8 decode. What every text caller
87
91
  * uses (sessions, the DynamoDB stores, request payloads). The declared byte
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The one place Lambder compresses and decompresses bytes.
3
3
  *
4
- * Five things compress: sessions, LambderDdbCache and LambderDdbIdempotency
4
+ * Five things compress: sessions, LambderDdbCache and LambderDdbIdempotencyStore
5
5
  * (Brotli at rest in DynamoDB), HTTP responses (Brotli or gzip, negotiated)
6
6
  * and request payloads (gzip from a browser, whose CompressionStream offers
7
7
  * nothing else; Brotli from a Node caller). They all compress text, so TEXT
@@ -21,13 +21,18 @@
21
21
  * implementation: a bug fixed here is fixed for records at rest and for
22
22
  * untrusted request bodies alike.
23
23
  *
24
- * zlib is loaded lazily through node-polyfills, so a module importing this
24
+ * zlib is loaded lazily through LambderNodeModules, so a module importing this
25
25
  * one can still sit in a frontend bundle's import graph via the package
26
- * root. The option that decides WHETHER to compress, and the encoding
27
- * vocabulary, live in LambderCompressionOption, which stays free of zlib
28
- * entirely so the browser entry can resolve it.
26
+ * root. Compressing needs zlib and is server-side; restoring runs anywhere,
27
+ * through zlib where it exists and through the web DecompressionStream
28
+ * otherwise, so the API core can restore a gzipped request payload in a
29
+ * browser (the mock runtime) under the same bound. The option that decides
30
+ * WHETHER to compress, and the encoding vocabulary, live in
31
+ * LambderCompressionOption, which stays free of zlib entirely so the browser
32
+ * entry can resolve it.
29
33
  */
30
- import { getZlib } from "./node-polyfills.js";
34
+ import { getZlib } from "../util/LambderNodeModules.js";
35
+ import { assertPositiveInteger } from "../util/LambderOptionChecks.js";
31
36
  /** Why a bounded restore failed, for callers that answer rather than throw. */
32
37
  export const LAMBDER_RESTORE_FAILURES = {
33
38
  /** The declared byte length is absent or not a positive integer. */
@@ -103,27 +108,22 @@ export const compressText = async (input, encoding, quality) => {
103
108
  export const restoreBytes = async (compressed, encoding, bound) => {
104
109
  const verified = "declaredBytes" in bound;
105
110
  const limit = verified ? bound.declaredBytes : bound.maxBytes;
106
- if (!Number.isSafeInteger(limit) || limit <= 0) {
107
- if (verified) {
111
+ if (verified) {
112
+ // A declared length comes off the wire, so a bad one is a bad request
113
+ // with a reason a caller can be told, not a configuration error.
114
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
108
115
  throw new LambderCompressionError(LAMBDER_RESTORE_FAILURES.missingLength, "compressed value is missing its byte length");
109
116
  }
110
- throw new Error("restoreBytes: maxBytes must be a positive integer");
111
117
  }
112
- const zlib = await requireZlib();
118
+ else {
119
+ assertPositiveInteger(limit, "restoreBytes maxBytes");
120
+ }
121
+ const zlib = await getZlib();
113
122
  let output;
114
123
  try {
115
- // zlib reads any Uint8Array in place; a request-sized body is not copied first.
116
- output = await new Promise((resolve, reject) => {
117
- const done = (error, result) => { if (error)
118
- reject(error);
119
- else
120
- resolve(result); };
121
- // maxOutputLength is the bound: zlib stops rather than allocating past it.
122
- if (encoding === "br")
123
- zlib.brotliDecompress(compressed, { maxOutputLength: limit }, done);
124
- else
125
- zlib.gunzip(compressed, { maxOutputLength: limit }, done);
126
- });
124
+ output = zlib
125
+ ? await restoreWithZlib(zlib, compressed, encoding, limit)
126
+ : await restoreWithWebStreams(compressed, encoding, limit);
127
127
  }
128
128
  catch (err) {
129
129
  throw new LambderCompressionError(LAMBDER_RESTORE_FAILURES.undecodable, "compressed value could not be decompressed", { cause: err });
@@ -140,4 +140,48 @@ export const restoreBytes = async (compressed, encoding, bound) => {
140
140
  * which is the restored buffer's length, so the verification is the same one
141
141
  * either way.
142
142
  */
143
- export const restoreText = async (compressed, encoding, bound) => (await restoreBytes(compressed, encoding, bound)).toString("utf8");
143
+ export const restoreText = async (compressed, encoding, bound) => new TextDecoder().decode(await restoreBytes(compressed, encoding, bound));
144
+ /** zlib reads any Uint8Array in place; maxOutputLength is the bound, so it stops rather than allocating past it. */
145
+ const restoreWithZlib = (zlib, compressed, encoding, limit) => new Promise((resolve, reject) => {
146
+ const done = (error, result) => { if (error)
147
+ reject(error);
148
+ else
149
+ resolve(result); };
150
+ if (encoding === "br")
151
+ zlib.brotliDecompress(compressed, { maxOutputLength: limit }, done);
152
+ else
153
+ zlib.gunzip(compressed, { maxOutputLength: limit }, done);
154
+ });
155
+ /**
156
+ * The browser's restore: DecompressionStream, read chunk by chunk and cut
157
+ * off past the bound, which is the one thing the stream API does not do on
158
+ * its own. Brotli arrives here only where the runtime offers it; a runtime
159
+ * without it rejects the format, which reads as undecodable.
160
+ */
161
+ const restoreWithWebStreams = async (compressed, encoding, limit) => {
162
+ if (typeof DecompressionStream === "undefined")
163
+ throw new Error("Lambder compression requires zlib or DecompressionStream.");
164
+ const format = (encoding === "br" ? "br" : "gzip");
165
+ const bytes = new Uint8Array(compressed);
166
+ const reader = new Blob([bytes]).stream().pipeThrough(new DecompressionStream(format)).getReader();
167
+ const chunks = [];
168
+ let total = 0;
169
+ for (;;) {
170
+ const { done, value } = await reader.read();
171
+ if (done)
172
+ break;
173
+ total += value.length;
174
+ if (total > limit) {
175
+ await reader.cancel();
176
+ throw new Error("decompressed output exceeds the bound");
177
+ }
178
+ chunks.push(value);
179
+ }
180
+ const output = new Uint8Array(total);
181
+ let offset = 0;
182
+ for (const chunk of chunks) {
183
+ output.set(chunk, offset);
184
+ offset += chunk.length;
185
+ }
186
+ return output;
187
+ };
@@ -3,7 +3,7 @@
3
3
  * that resolves it.
4
4
  *
5
5
  * Five places compress something: sessions, LambderDdbCache and
6
- * LambderDdbIdempotency (Brotli at rest in DynamoDB), HTTP responses
6
+ * LambderDdbIdempotencyStore (Brotli at rest in DynamoDB), HTTP responses
7
7
  * (Brotli/gzip on the wire) and request payloads (gzip on the wire). They
8
8
  * differ in what they can be tuned with, so each declares its own settings
9
9
  * type, but they share one vocabulary and one resolution: `true` is on with
@@ -3,7 +3,7 @@
3
3
  * that resolves it.
4
4
  *
5
5
  * Five places compress something: sessions, LambderDdbCache and
6
- * LambderDdbIdempotency (Brotli at rest in DynamoDB), HTTP responses
6
+ * LambderDdbIdempotencyStore (Brotli at rest in DynamoDB), HTTP responses
7
7
  * (Brotli/gzip on the wire) and request payloads (gzip on the wire). They
8
8
  * differ in what they can be tuned with, so each declares its own settings
9
9
  * type, but they share one vocabulary and one resolution: `true` is on with
@@ -16,6 +16,7 @@
16
16
  * option without pulling Node built-ins into the bundle; the compression
17
17
  * primitives themselves live in LambderCompressionCodec, which does load zlib.
18
18
  */
19
+ import { assertNonNegativeInteger } from "../util/LambderOptionChecks.js";
19
20
  /** Algorithms Lambder can produce. Brotli at rest and preferred on responses; gzip everywhere a browser has to do the compressing. */
20
21
  export const LAMBDER_ENCODINGS = ["br", "gzip"];
21
22
  /**
@@ -37,9 +38,7 @@ export const resolveCompressionOption = (option, defaults) => {
37
38
  const config = option === true || option === undefined ? {} : option;
38
39
  const overrides = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
39
40
  const settings = { ...defaults, ...overrides };
40
- if (!Number.isSafeInteger(settings.minBytes) || settings.minBytes < 0) {
41
- throw new Error("compression.minBytes must be a non-negative integer");
42
- }
41
+ assertNonNegativeInteger(settings.minBytes, "compression.minBytes");
43
42
  if (settings.quality !== undefined && (!Number.isInteger(settings.quality) || settings.quality < 0 || settings.quality > 11)) {
44
43
  throw new Error("compression.quality must be an integer from 0 to 11");
45
44
  }
@@ -46,3 +46,13 @@ export declare const describeCrash: (error: unknown, ctx?: {
46
46
  * causes see the callee's stack as it was.
47
47
  */
48
48
  export declare const errorFromCrashDetail: (crash: LambderCrashDetail) => Error;
49
+ /**
50
+ * A thrown value as an Error, so a reporter or a `cause` chain always holds
51
+ * one. An Error passes through; anything else becomes an Error whose message
52
+ * describes the value the way describeCrash does, JSON where String() cannot
53
+ * (a null-prototype object or a throwing toString must not make the
54
+ * coercion itself throw). One implementation, rather than
55
+ * `err instanceof Error ? err : new Error(...)` spelled at every site with a
56
+ * fallback of its own.
57
+ */
58
+ export declare const coerceToError: (value: unknown, fallbackMessage?: string) => Error;