lambder 7.3.1 → 8.1.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 (237) hide show
  1. package/CHANGELOG.md +1047 -3
  2. package/README.md +46 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +68 -62
  13. package/dist/api/LambderApiIdempotency.js +214 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +47 -38
  17. package/dist/api/LambderApiPipeline.js +122 -63
  18. package/dist/api/LambderApiRateLimits.d.ts +201 -54
  19. package/dist/api/LambderApiRateLimits.js +185 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/ContractTypePrinter.d.ts +85 -0
  27. package/dist/build/ContractTypePrinter.js +402 -0
  28. package/dist/build/freshProcessVerifier.d.ts +13 -0
  29. package/dist/build/freshProcessVerifier.js +19 -0
  30. package/dist/build/moduleLocation.d.ts +11 -0
  31. package/dist/build/moduleLocation.js +6 -0
  32. package/dist/build/writeApiContract.d.ts +78 -0
  33. package/dist/build/writeApiContract.js +302 -0
  34. package/dist/build/writeApiSignatures.d.ts +114 -0
  35. package/dist/build/writeApiSignatures.js +217 -0
  36. package/dist/build/writeFileAtomically.d.ts +8 -0
  37. package/dist/build/writeFileAtomically.js +22 -0
  38. package/dist/build.d.ts +14 -0
  39. package/dist/build.js +11 -0
  40. package/dist/client/LambderCaller.d.ts +13 -44
  41. package/dist/client/LambderCaller.js +77 -84
  42. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  43. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  44. package/dist/client/LambderUploadRunner.d.ts +96 -0
  45. package/dist/client/LambderUploadRunner.js +234 -0
  46. package/dist/client/lambderFetchTransport.d.ts +4 -1
  47. package/dist/client/lambderFetchTransport.js +52 -28
  48. package/dist/client.d.ts +9 -3
  49. package/dist/client.js +6 -1
  50. package/dist/core/Lambder.d.ts +143 -79
  51. package/dist/core/Lambder.js +350 -231
  52. package/dist/core/LambderContext.d.ts +82 -15
  53. package/dist/core/LambderContext.js +107 -20
  54. package/dist/core/LambderCors.d.ts +21 -3
  55. package/dist/core/LambderCors.js +35 -16
  56. package/dist/core/LambderCrashHandling.d.ts +40 -0
  57. package/dist/core/LambderCrashHandling.js +97 -0
  58. package/dist/core/LambderCreateOptions.d.ts +151 -75
  59. package/dist/core/LambderCreateOptions.js +16 -23
  60. package/dist/core/LambderFiles.d.ts +21 -7
  61. package/dist/core/LambderFiles.js +62 -34
  62. package/dist/core/LambderIndexHtml.js +12 -11
  63. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  64. package/dist/core/LambderPolicyBuilders.js +17 -5
  65. package/dist/core/LambderPublicFiles.d.ts +11 -5
  66. package/dist/core/LambderPublicFiles.js +32 -4
  67. package/dist/core/LambderRequestPath.d.ts +43 -0
  68. package/dist/core/LambderRequestPath.js +63 -0
  69. package/dist/core/LambderResponse.d.ts +26 -5
  70. package/dist/core/LambderResponse.js +157 -70
  71. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  72. package/dist/core/LambderResponseBuilder.js +64 -3
  73. package/dist/core/LambderRouting.d.ts +2 -3
  74. package/dist/core/LambderRouting.js +22 -7
  75. package/dist/core/LambderTemplatingEngine.js +211 -32
  76. package/dist/index.d.ts +25 -8
  77. package/dist/index.js +13 -4
  78. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  79. package/dist/invoke/LambderInvokeCaller.js +76 -66
  80. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  81. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  82. package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
  83. package/dist/invoke/LambderLambdaEvent.js +40 -22
  84. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  85. package/dist/invoke/lambderHandlerTransport.js +15 -18
  86. package/dist/mock/LambderMockApp.d.ts +67 -83
  87. package/dist/mock/LambderMockApp.js +167 -153
  88. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  89. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  90. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  91. package/dist/mock/LambderMockCallRecorder.js +19 -28
  92. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  93. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  94. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  95. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  96. package/dist/mock/LambderMockFailureInjector.js +3 -6
  97. package/dist/mock/LambderMockTypes.d.ts +78 -108
  98. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  99. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  100. package/dist/mock/lambderMockMswHandler.d.ts +43 -33
  101. package/dist/mock/lambderMockMswHandler.js +50 -39
  102. package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
  103. package/dist/mock/lambderMockUploadMswHandler.js +28 -0
  104. package/dist/mock.d.ts +4 -1
  105. package/dist/mock.js +6 -3
  106. package/dist/session/LambderSessionController.d.ts +108 -89
  107. package/dist/session/LambderSessionController.js +187 -168
  108. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  109. package/dist/session/LambderSessionCrypto.js +26 -12
  110. package/dist/session/LambderSessionManager.d.ts +124 -46
  111. package/dist/session/LambderSessionManager.js +262 -137
  112. package/dist/shared/LambderHtml.d.ts +42 -3
  113. package/dist/shared/LambderHtml.js +127 -7
  114. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  115. package/dist/shared/LambderHtmlPositions.js +652 -0
  116. package/dist/shared/LambderI18n.d.ts +10 -11
  117. package/dist/shared/LambderI18n.js +33 -21
  118. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  119. package/dist/shared/contracts/LambderCache.js +11 -0
  120. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  121. package/dist/shared/contracts/LambderFileSource.js +5 -8
  122. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  123. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  124. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  125. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  126. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  127. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  128. package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
  129. package/dist/shared/contracts/LambderUploadBucket.js +74 -0
  130. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  131. package/dist/shared/transport/LambderApiTransport.js +7 -7
  132. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  133. package/dist/shared/transport/LambderCookieJar.js +54 -66
  134. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  135. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  136. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  137. package/dist/shared/util/LambderCallAbort.js +5 -5
  138. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  139. package/dist/shared/util/LambderClientIp.js +96 -13
  140. package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
  141. package/dist/shared/util/LambderContentDisposition.js +13 -0
  142. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  143. package/dist/shared/util/LambderExpiringMap.js +41 -57
  144. package/dist/shared/util/LambderNodeModules.js +6 -7
  145. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  146. package/dist/shared/util/LambderOptionChecks.js +4 -4
  147. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  148. package/dist/shared/util/LambderResponseBrand.js +5 -5
  149. package/dist/shared/util/LambderTextDigest.d.ts +7 -5
  150. package/dist/shared/util/LambderTextDigest.js +11 -5
  151. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  152. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  153. package/dist/shared/util/boundKeyField.d.ts +20 -0
  154. package/dist/shared/util/boundKeyField.js +34 -0
  155. package/dist/shared/util/canonicalJson.d.ts +11 -0
  156. package/dist/shared/util/canonicalJson.js +28 -0
  157. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  158. package/dist/shared/util/joinKeyFields.js +22 -0
  159. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  160. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  161. package/dist/shared/wire/LambderApiContract.d.ts +98 -53
  162. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  163. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  164. package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
  165. package/dist/shared/wire/LambderApiRefusal.js +42 -7
  166. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  167. package/dist/shared/wire/LambderApiSignature.js +16 -19
  168. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  169. package/dist/shared/wire/LambderCallOptions.js +9 -11
  170. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  171. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  172. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  173. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  174. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  175. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  176. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  177. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  178. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  179. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  180. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  181. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  182. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  183. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  184. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  185. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  186. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  187. package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
  188. package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
  189. package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
  190. package/dist/shared/wire/LambderUploadRefusal.js +18 -0
  191. package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
  192. package/dist/shared/wire/LambderUploadSchemas.js +30 -0
  193. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  194. package/dist/stores/LambderCacheFiller.js +119 -0
  195. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  196. package/dist/stores/LambderCacheKeys.js +54 -0
  197. package/dist/stores/LambderCacheValues.d.ts +45 -0
  198. package/dist/stores/LambderCacheValues.js +74 -0
  199. package/dist/stores/LambderDdbCache.d.ts +121 -56
  200. package/dist/stores/LambderDdbCache.js +528 -225
  201. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  202. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  203. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  204. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  205. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  206. package/dist/stores/LambderDdbSdk.js +80 -38
  207. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  208. package/dist/stores/LambderDdbSessionStore.js +119 -47
  209. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  210. package/dist/stores/LambderHttpFileSource.js +15 -13
  211. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  212. package/dist/stores/LambderMemoryCache.js +113 -0
  213. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  214. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  215. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  216. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  217. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  218. package/dist/stores/LambderMemorySessionStore.js +38 -19
  219. package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
  220. package/dist/stores/LambderMemoryUploadBucket.js +219 -0
  221. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  222. package/dist/stores/LambderS3FileSource.js +12 -7
  223. package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
  224. package/dist/stores/LambderS3UploadBucket.js +144 -0
  225. package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
  226. package/dist/stores/LambderSdkInstallHint.js +14 -0
  227. package/dist/testing/LambderTestApp.d.ts +23 -25
  228. package/dist/testing/LambderTestApp.js +22 -24
  229. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  230. package/dist/testing/LambderTestVisitor.js +15 -15
  231. package/dist/testing.d.ts +3 -0
  232. package/dist/testing.js +2 -0
  233. package/package.json +26 -3
  234. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  235. package/dist/api/LambderApiPolicyEngine.js +0 -85
  236. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  237. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -3,9 +3,9 @@ export type LambderApiRefusalOptions = {
3
3
  /**
4
4
  * User-facing failure detail placed on the API envelope's `errorMessage`
5
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
- * visible to the client.
6
+ * an app's own `code`) or a plain string, which becomes an "error"
7
+ * message with that content. Defaults to the error message, so a bare
8
+ * `throw new LambderApiRefusal("...")` is still visible to the client.
9
9
  */
10
10
  errorMessage?: LambderAppRefusalMessage | string;
11
11
  /** Sets the envelope's `notAuthorized` flag (routed to the caller's notAuthorizedHandler). */
@@ -26,18 +26,18 @@ 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 that have no access to the per-request resolver)
30
- * and the render pipeline maps it onto the structured API envelope
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
31
  * (`res.api(null, { errorMessage, notAuthorized, sessionExpired })`) instead
32
- * of routing it through setGlobalErrorHandler. Refusals therefore never reach
33
- * crash logging, and clients receive a parseable response they can surface.
32
+ * of routing it through setGlobalErrorHandler, so refusals never reach crash
33
+ * logging and clients receive a parseable response.
34
34
  *
35
35
  * Thrown outside an API call (e.g. in a route handler) it behaves like any
36
36
  * other error: global error handler, then the default 500.
37
37
  *
38
38
  * Isomorphic and dependency-free, so shared code (validators, permission
39
- * checks) may import and throw it from packages used by both server and
40
- * browser builds; in the browser it is just an Error.
39
+ * checks) used by both server and browser builds may throw it; in the
40
+ * browser it is just an Error.
41
41
  */
42
42
  export declare class LambderApiRefusal extends Error {
43
43
  /**
@@ -46,7 +46,8 @@ export declare class LambderApiRefusal extends Error {
46
46
  * across them while this marker does not. The pipeline checks the brand.
47
47
  */
48
48
  readonly isLambderApiRefusal = true;
49
- readonly errorMessage?: LambderAppRefusalMessage | string;
49
+ /** What the envelope's errorMessage carries: always a message object, a plain string given to the options made into one. */
50
+ readonly errorMessage: LambderAppRefusalMessage;
50
51
  readonly notAuthorized?: boolean;
51
52
  readonly sessionExpired?: boolean;
52
53
  readonly statusCode?: LambderHttpStatusCode;
@@ -59,11 +60,11 @@ export declare const isLambderApiRefusal: (err: unknown) => err is LambderApiRef
59
60
  * The standard shape refusals carry on the envelope's errorMessage field.
60
61
  * `code` is the refusal's machine-readable identity: clients branch and
61
62
  * translate on it and never string-match `content`, which stays the
62
- * human-readable fallback for codes a client does not know yet. Apps keep
63
- * their own typed code vocabulary; the framework's own refusals carry a
64
- * LambderRefusalCode. The caller's errorMessageHandler receives the object
65
- * as-is, typed as LambderAppRefusalMessage or a plain string; an app's own
66
- * vocabulary goes in `code`.
63
+ * human-readable fallback for codes a client does not know yet. The
64
+ * framework's own refusals carry a LambderRefusalCode; apps put their own
65
+ * typed vocabulary in `code`. The caller's errorMessageHandler receives the
66
+ * object as-is, typed as LambderAppRefusalMessage; a server that wrote a
67
+ * plain string reaches it as `{ type: "error", content }` (refusalMessageOf).
67
68
  */
68
69
  export type LambderRefusalMessage<TAppCode extends string = never> = {
69
70
  type: "warning" | "error" | "info";
@@ -71,14 +72,13 @@ export type LambderRefusalMessage<TAppCode extends string = never> = {
71
72
  * Machine-readable identity of the refusal: a LambderRefusalCode, plus
72
73
  * whatever vocabulary the reader names in TAppCode.
73
74
  *
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.
75
+ * Parameterized rather than widened with `string & {}`: a union with
76
+ * `string` in it does not narrow, so a `switch(message.code)` could not
77
+ * end in a `default: never` exhaustiveness check, which is what the codes
78
+ * exist for. A client that reads its own vocabulary declares it
79
+ * (`LambderRefusalMessage<"app/not-verified" | ...>`) and gets a checked
80
+ * switch; a value an app WRITES takes LambderAppRefusalMessage, where any
81
+ * code is welcome.
82
82
  */
83
83
  code?: LambderRefusalCode | TAppCode;
84
84
  title?: string;
@@ -86,12 +86,22 @@ export type LambderRefusalMessage<TAppCode extends string = never> = {
86
86
  };
87
87
  /**
88
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.
89
+ * autocompleting. Every option that takes a message from an app is typed as
90
+ * this (a rate-limit policy's errorMessage, the mock's failure injection);
91
+ * LambderRefusalMessage itself defaults to the framework's codes alone, so a
92
+ * reader's switch over it is exhaustive.
93
93
  */
94
94
  export type LambderAppRefusalMessage = LambderRefusalMessage<string & {}>;
95
+ /**
96
+ * An errorMessage as the one shape a reader handles: a plain string becomes
97
+ * an "error" message with that content, and a value that is not a message
98
+ * at all becomes one describing it. The envelope writer applies it, so a
99
+ * Lambder server only ever sends the object. A reader applies it too,
100
+ * because what it reads is wire input no server vouches for: a hand-built
101
+ * mock answer (an MSW handler, a test double) or a proxy that wrote its own
102
+ * body can put anything there.
103
+ */
104
+ export declare const refusalMessageOf: (message: unknown) => LambderAppRefusalMessage;
95
105
  /**
96
106
  * Codes the framework stamps on the refusals it authors itself, under the
97
107
  * reserved `lambder/` prefix so app codes never collide. Compare against
@@ -103,6 +113,8 @@ export declare const LAMBDER_REFUSAL_CODES: {
103
113
  readonly rateLimited: "lambder/rate-limited";
104
114
  /** The original of an idempotent request is still processing (409). */
105
115
  readonly duplicateInFlight: "lambder/duplicate-in-flight";
116
+ /** The idempotencyKey was already used for a request with a different payload (409). */
117
+ readonly idempotencyKeyReused: "lambder/idempotency-key-reused";
106
118
  /** The idempotencyKey is malformed (400). */
107
119
  readonly invalidIdempotencyKey: "lambder/invalid-idempotency-key";
108
120
  /** No API is registered under the requested name. */
@@ -111,6 +123,12 @@ export declare const LAMBDER_REFUSAL_CODES: {
111
123
  readonly invalidRequestPayload: "lambder/invalid-request-payload";
112
124
  /** Only the mock runtime emits it: the endpoint is registered as not mocked, with a reason. */
113
125
  readonly notMocked: "lambder/not-mocked";
126
+ /** An upload bucket would not sign a ticket for a file with no bytes. */
127
+ readonly uploadEmpty: "lambder/upload-empty";
128
+ /** An upload bucket would not sign a ticket for a content type the rule does not accept. */
129
+ readonly uploadTypeRejected: "lambder/upload-type-rejected";
130
+ /** An upload bucket would not sign a ticket for a file larger than the rule accepts. */
131
+ readonly uploadTooLarge: "lambder/upload-too-large";
114
132
  };
115
133
  export type LambderRefusalCode = (typeof LAMBDER_REFUSAL_CODES)[keyof typeof LAMBDER_REFUSAL_CODES];
116
134
  export type LambderRefuseOptions = {
@@ -1,18 +1,18 @@
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 that have no access to the per-request resolver)
5
- * and the render pipeline maps it onto the structured API envelope
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
6
  * (`res.api(null, { errorMessage, notAuthorized, sessionExpired })`) instead
7
- * of routing it through setGlobalErrorHandler. Refusals therefore never reach
8
- * crash logging, and clients receive a parseable response they can surface.
7
+ * of routing it through setGlobalErrorHandler, so refusals never reach crash
8
+ * logging and clients receive a parseable response.
9
9
  *
10
10
  * Thrown outside an API call (e.g. in a route handler) it behaves like any
11
11
  * other error: global error handler, then the default 500.
12
12
  *
13
13
  * Isomorphic and dependency-free, so shared code (validators, permission
14
- * checks) may import and throw it from packages used by both server and
15
- * browser builds; in the browser it is just an Error.
14
+ * checks) used by both server and browser builds may throw it; in the
15
+ * browser it is just an Error.
16
16
  */
17
17
  export class LambderApiRefusal extends Error {
18
18
  /**
@@ -21,6 +21,7 @@ export class LambderApiRefusal extends Error {
21
21
  * across them while this marker does not. The pipeline checks the brand.
22
22
  */
23
23
  isLambderApiRefusal = true;
24
+ /** What the envelope's errorMessage carries: always a message object, a plain string given to the options made into one. */
24
25
  errorMessage;
25
26
  notAuthorized;
26
27
  sessionExpired;
@@ -29,7 +30,7 @@ export class LambderApiRefusal extends Error {
29
30
  constructor(message, options = {}) {
30
31
  super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
31
32
  this.name = "LambderApiRefusal";
32
- this.errorMessage = options.errorMessage ?? message;
33
+ this.errorMessage = refusalMessageOf(options.errorMessage ?? message);
33
34
  this.notAuthorized = options.notAuthorized;
34
35
  this.sessionExpired = options.sessionExpired;
35
36
  this.statusCode = options.statusCode;
@@ -38,6 +39,32 @@ export class LambderApiRefusal extends Error {
38
39
  }
39
40
  /** Brand-based type guard (see LambderApiRefusal.isLambderApiRefusal). */
40
41
  export const isLambderApiRefusal = (err) => err instanceof Error && err.isLambderApiRefusal === true;
42
+ const REFUSAL_MESSAGE_TYPES = ["warning", "error", "info"];
43
+ /**
44
+ * An errorMessage as the one shape a reader handles: a plain string becomes
45
+ * an "error" message with that content, and a value that is not a message
46
+ * at all becomes one describing it. The envelope writer applies it, so a
47
+ * Lambder server only ever sends the object. A reader applies it too,
48
+ * because what it reads is wire input no server vouches for: a hand-built
49
+ * mock answer (an MSW handler, a test double) or a proxy that wrote its own
50
+ * body can put anything there.
51
+ */
52
+ export const refusalMessageOf = (message) => {
53
+ if (message !== null && typeof message === "object" && typeof message.content === "string") {
54
+ const candidate = message;
55
+ return REFUSAL_MESSAGE_TYPES.includes(candidate.type) ? candidate : { ...candidate, type: "error" };
56
+ }
57
+ if (typeof message === "string")
58
+ return { type: "error", content: message };
59
+ let content;
60
+ try {
61
+ content = JSON.stringify(message) ?? String(message);
62
+ }
63
+ catch {
64
+ content = String(message);
65
+ }
66
+ return { type: "error", content };
67
+ };
41
68
  /**
42
69
  * Codes the framework stamps on the refusals it authors itself, under the
43
70
  * reserved `lambder/` prefix so app codes never collide. Compare against
@@ -49,6 +76,8 @@ export const LAMBDER_REFUSAL_CODES = {
49
76
  rateLimited: "lambder/rate-limited",
50
77
  /** The original of an idempotent request is still processing (409). */
51
78
  duplicateInFlight: "lambder/duplicate-in-flight",
79
+ /** The idempotencyKey was already used for a request with a different payload (409). */
80
+ idempotencyKeyReused: "lambder/idempotency-key-reused",
52
81
  /** The idempotencyKey is malformed (400). */
53
82
  invalidIdempotencyKey: "lambder/invalid-idempotency-key",
54
83
  /** No API is registered under the requested name. */
@@ -57,6 +86,12 @@ export const LAMBDER_REFUSAL_CODES = {
57
86
  invalidRequestPayload: "lambder/invalid-request-payload",
58
87
  /** Only the mock runtime emits it: the endpoint is registered as not mocked, with a reason. */
59
88
  notMocked: "lambder/not-mocked",
89
+ /** An upload bucket would not sign a ticket for a file with no bytes. */
90
+ uploadEmpty: "lambder/upload-empty",
91
+ /** An upload bucket would not sign a ticket for a content type the rule does not accept. */
92
+ uploadTypeRejected: "lambder/upload-type-rejected",
93
+ /** An upload bucket would not sign a ticket for a file larger than the rule accepts. */
94
+ uploadTooLarge: "lambder/upload-too-large",
60
95
  };
61
96
  /**
62
97
  * Refuse the current API call: a routine business "no" (not found, invalid
@@ -6,9 +6,8 @@ import type { z } from "zod";
6
6
  * the digest of its client-facing shape (apiSignatureOf, computed on the
7
7
  * server side). A caller given the map sends the value with every call, and
8
8
  * the server answers versionExpired when it differs from the digest of what
9
- * it serves now. So a client built against an endpoint that has since
10
- * changed reloads, while one whose endpoint is unchanged keeps working
11
- * across deploys.
9
+ * it serves, so a client reloads only when an endpoint it calls has changed
10
+ * and otherwise keeps working across deploys.
12
11
  *
13
12
  * Keys are hashed so the map lists no endpoint names: the names a client
14
13
  * calls are in its own code already, and the rest of the surface stays out
@@ -26,14 +25,12 @@ export declare const API_SIGNATURE_HEX_LENGTH = 16;
26
25
  * name, cut to API_SIGNATURE_HEX_LENGTH hex characters. Async because
27
26
  * WebCrypto's digest is, and it is the only SHA-256 a browser has.
28
27
  *
29
- * Computed on the spot, every time, and nothing is kept. The digest that
30
- * actually describes an endpoint is the generator's, computed once at build
31
- * time; what is left here is one hash of a short name against a map already
32
- * in memory, which is nothing beside the request it belongs to. A cache of
33
- * it would have to be keyed by name, and on the server the name comes off
34
- * the wire before anything has checked that it is an endpoint at all, so it
35
- * would grow by an entry for every name a request cared to invent and never
36
- * shrink.
28
+ * Computed on the spot every time, with nothing kept. The digest that
29
+ * describes an endpoint is the generator's, computed once at build time;
30
+ * this is one hash of a short name, nothing beside the request it belongs
31
+ * to. A cache would be keyed by name, and on the server the name comes off
32
+ * the wire before anything checks that it is an endpoint, so the cache would
33
+ * gain an entry for every name a request cared to invent and never shrink.
37
34
  */
38
35
  export declare const apiNameKeyOf: (apiName: string) => Promise<string>;
39
36
  /** The map's signature for one endpoint, or null when the map holds none for it. */
@@ -59,23 +56,22 @@ export declare const EXTENSIBLE_ENUM_META_KEY = "x-lambder-extensible-enum";
59
56
  * widely returned payload (a session, a profile) then reloads only the
60
57
  * clients that send the list back, not every client that reads it.
61
58
  *
62
- * Where the enum is input its values still count: a value dropped from the
63
- * list is a request an older client may still send and the server now
64
- * refuses, so that endpoint's clients must reload. Everything else about the
65
- * schema is untouched: its type, its validation on both sides, and what it
66
- * is everywhere outside the digest.
59
+ * Where the enum is input its values still count: an older client may still
60
+ * send a value dropped from the list, which the server would refuse, so that
61
+ * endpoint's clients must reload. Everything else about the schema is
62
+ * untouched: its type, its validation on both sides, and what it is outside
63
+ * the digest.
67
64
  *
68
65
  * The mark is a promise the schema makes for its readers, and nothing checks
69
66
  * it. A client that switches over every value with no fallback, or indexes a
70
- * map by one, renders a value it does not know as nothing, or throws. Mark
71
- * only a list every reader handles that way on purpose.
67
+ * map by one, renders an unknown value as nothing, or throws. Mark only a
68
+ * list whose every reader handles an unknown value on purpose.
72
69
  *
73
70
  * It is zod metadata (`.meta()`), which zod keeps in one registry on
74
71
  * globalThis, so an enum marked in a shared package is read by the digest
75
- * even when the server resolves another copy of zod. A schema derived from a
76
- * marked enum by rebuilding it (`z.enum(marked.options)`, `.exclude()`)
77
- * carries no mark and counts in full, which costs a reload, never a missed
78
- * one.
72
+ * even when the server resolves another copy of zod. A schema rebuilt from a
73
+ * marked enum (`z.enum(marked.options)`, `.exclude()`) carries no mark and
74
+ * counts in full, which costs a reload, never a missed one.
79
75
  *
80
76
  * @example
81
77
  * export const RoleSchema = extensibleEnum(z.enum(["admin", "member"]));
@@ -12,14 +12,12 @@ const API_NAME_KEY_PREFIX = "lambder-api-name:";
12
12
  * name, cut to API_SIGNATURE_HEX_LENGTH hex characters. Async because
13
13
  * WebCrypto's digest is, and it is the only SHA-256 a browser has.
14
14
  *
15
- * Computed on the spot, every time, and nothing is kept. The digest that
16
- * actually describes an endpoint is the generator's, computed once at build
17
- * time; what is left here is one hash of a short name against a map already
18
- * in memory, which is nothing beside the request it belongs to. A cache of
19
- * it would have to be keyed by name, and on the server the name comes off
20
- * the wire before anything has checked that it is an endpoint at all, so it
21
- * would grow by an entry for every name a request cared to invent and never
22
- * shrink.
15
+ * Computed on the spot every time, with nothing kept. The digest that
16
+ * describes an endpoint is the generator's, computed once at build time;
17
+ * this is one hash of a short name, nothing beside the request it belongs
18
+ * to. A cache would be keyed by name, and on the server the name comes off
19
+ * the wire before anything checks that it is an endpoint, so the cache would
20
+ * gain an entry for every name a request cared to invent and never shrink.
23
21
  */
24
22
  export const apiNameKeyOf = async (apiName) => (await sha256HexOf(API_NAME_KEY_PREFIX + apiName)).slice(0, API_SIGNATURE_HEX_LENGTH);
25
23
  /** The map's signature for one endpoint, or null when the map holds none for it. */
@@ -57,23 +55,22 @@ export const EXTENSIBLE_ENUM_META_KEY = "x-lambder-extensible-enum";
57
55
  * widely returned payload (a session, a profile) then reloads only the
58
56
  * clients that send the list back, not every client that reads it.
59
57
  *
60
- * Where the enum is input its values still count: a value dropped from the
61
- * list is a request an older client may still send and the server now
62
- * refuses, so that endpoint's clients must reload. Everything else about the
63
- * schema is untouched: its type, its validation on both sides, and what it
64
- * is everywhere outside the digest.
58
+ * Where the enum is input its values still count: an older client may still
59
+ * send a value dropped from the list, which the server would refuse, so that
60
+ * endpoint's clients must reload. Everything else about the schema is
61
+ * untouched: its type, its validation on both sides, and what it is outside
62
+ * the digest.
65
63
  *
66
64
  * The mark is a promise the schema makes for its readers, and nothing checks
67
65
  * it. A client that switches over every value with no fallback, or indexes a
68
- * map by one, renders a value it does not know as nothing, or throws. Mark
69
- * only a list every reader handles that way on purpose.
66
+ * map by one, renders an unknown value as nothing, or throws. Mark only a
67
+ * list whose every reader handles an unknown value on purpose.
70
68
  *
71
69
  * It is zod metadata (`.meta()`), which zod keeps in one registry on
72
70
  * globalThis, so an enum marked in a shared package is read by the digest
73
- * even when the server resolves another copy of zod. A schema derived from a
74
- * marked enum by rebuilding it (`z.enum(marked.options)`, `.exclude()`)
75
- * carries no mark and counts in full, which costs a reload, never a missed
76
- * one.
71
+ * even when the server resolves another copy of zod. A schema rebuilt from a
72
+ * marked enum (`z.enum(marked.options)`, `.exclude()`) carries no mark and
73
+ * counts in full, which costs a reload, never a missed one.
77
74
  *
78
75
  * @example
79
76
  * export const RoleSchema = extensibleEnum(z.enum(["admin", "member"]));
@@ -1,14 +1,14 @@
1
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.
2
+ * The per-call options, the contract-driven typing of a call's arguments, and
3
+ * the runtime merge of guard inputs, shared by the browser caller
4
+ * (LambderCaller) and the server-side invoke caller (LambderInvokeCaller).
5
+ * Both speak the same envelope to the same kind of contract, so what an API
6
+ * demands of its caller (a guardInput-mode guard's value, say) is decided
7
+ * here once and the two cannot drift. Pure types and one dependency-free
8
+ * function, so the browser entry resolves it.
10
9
  */
11
10
  import type { LambderContractIdempotencyOf } from "./LambderApiContract.js";
11
+ import type { LambderIdempotencyKeyScope } from "./LambderIdempotencyKeyScope.js";
12
12
  type IsAny<T> = 0 extends (1 & T) ? true : false;
13
13
  /**
14
14
  * The options every call takes, whichever caller sends it. Each caller adds
@@ -40,19 +40,22 @@ export type LambderSharedCallOptions = {
40
40
  /**
41
41
  * Replay-protection key for APIs declared idempotent on the server.
42
42
  * Generate once per logical operation with
43
- * LambderCaller.createIdempotencyKey() and send the same key on retries:
43
+ * createIdempotencyKey() and send the same key on retries:
44
44
  * duplicates of an in-flight request refuse, and repeats of a completed
45
45
  * one replay its stored response instead of re-executing. Must be
46
46
  * UNGUESSABLE random (it scopes the replay record for logged-out clients)
47
47
  * and at least 16 characters; the server refuses shorter keys with a 400.
48
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.
49
+ * REQUIRED by the typed contract for an API that declares `idempotency`,
50
+ * since the server runs a keyless call unprotected.
51
+ *
52
+ * A key scope (createIdempotencyKeyScope()) is the easier
53
+ * form: it rotates once an answer settles the operation, so a retry after
54
+ * a dropped connection reuses the key and the next attempt (a corrected
55
+ * form after a refusal included) gets a new one. See
56
+ * LambderIdempotencyKeyScope for which answers settle it.
54
57
  */
55
- idempotencyKey?: string;
58
+ idempotencyKey?: string | LambderIdempotencyKeyScope;
56
59
  };
57
60
  /** The payload type one API of a contract takes; `any` for an untyped caller or a name the contract does not know. */
58
61
  type LambderContractInputOf<TContract, TApiName> = IsAny<TContract> extends true ? any : TApiName extends keyof TContract ? TContract[TApiName] extends {
@@ -80,12 +83,12 @@ export type LambderProvidedGuardInputs<TContract, TProvided extends string> = Is
80
83
  };
81
84
  /**
82
85
  * 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.
86
+ * UI is on, a device token), keyed by guard name; per-call guardInputs merge
87
+ * on top. Name the guards it covers in the caller's second type parameter,
88
+ * `new LambderCaller<Contract, "orgPermission">`, and calls to APIs whose
89
+ * guardInput guards are all covered do not require the options argument.
90
+ * May be async; a throw fails the call as an unknown error before anything
91
+ * is sent.
89
92
  */
90
93
  export type LambderGuardInputsProvider<TContract, TProvided extends string> = (apiName: keyof TContract & string) => LambderProvidedGuardInputs<TContract, TProvided> | Promise<LambderProvidedGuardInputs<TContract, TProvided>>;
91
94
  /** Optional until the caller names provided guards: naming them without a provider would send nothing. */
@@ -117,11 +120,10 @@ type ContractGuardInputsField<TEntry, TProvided extends string> = [
117
120
  *
118
121
  * Read as "required unless it says false" rather than "required only when it
119
122
  * 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.
123
+ * a spread, or built in a helper) keeps the requirement at compile time.
122
124
  */
123
125
  type ContractIdempotencyKeyField<TContract, TApiName> = TApiName extends keyof TContract ? [LambderContractIdempotencyOf<TContract, TApiName>] extends [never] ? {} : [LambderContractIdempotencyOf<TContract, TApiName>] extends [false] ? {} : {
124
- idempotencyKey: string;
126
+ idempotencyKey: string | LambderIdempotencyKeyScope;
125
127
  } : {};
126
128
  /**
127
129
  * What the contract adds to one call's options: the guardInputs field and the
@@ -131,41 +133,30 @@ type ContractCallFields<TContract, TApiName, TProvided extends string> = TApiNam
131
133
  /**
132
134
  * The options argument of one call: optional normally, REQUIRED when the
133
135
  * 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.
136
+ * idempotent API's key is a compile error rather than a 422 or a replay that
137
+ * never happens. TOptions is the caller's own per-call options type.
138
+ * `{} extends TFields` asks "is every field the contract added optional".
141
139
  */
142
140
  type LambderCallOptionsArg<TContract, TApiName, TProvided extends string, TOptions extends {
143
141
  guardInputs?: Record<string, unknown>;
144
142
  }> = 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
143
  /**
146
144
  * 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.
145
+ * options, both decided by the contract. The payload is optional only when
146
+ * the API's input accepts undefined, so `caller.api("getUser")` against
147
+ * `input: { id: string }` fails to compile rather than drawing a 422. That
148
+ * needs one rest tuple, since TypeScript has no other per-argument
149
+ * conditional. When the options are mandatory, the payload cannot stay
150
+ * optional before them (a tuple's required element may not follow an
151
+ * optional one), so such a call passes it explicitly, `undefined` included.
160
152
  */
161
153
  export type LambderCallArgs<TContract, TApiName, TProvided extends string, TOptions extends {
162
154
  guardInputs?: Record<string, unknown>;
163
155
  }> = 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
156
  /**
165
157
  * 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.
158
+ * side supplied any. Synchronous so a call without a provider still issues
159
+ * its request in the same tick it was made.
169
160
  */
170
161
  export declare const mergeGuardInputs: (provided: Record<string, unknown> | undefined, perCall: Record<string, unknown> | undefined) => Record<string, unknown> | undefined;
171
162
  export {};
@@ -1,17 +1,15 @@
1
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.
2
+ * The per-call options, the contract-driven typing of a call's arguments, and
3
+ * the runtime merge of guard inputs, shared by the browser caller
4
+ * (LambderCaller) and the server-side invoke caller (LambderInvokeCaller).
5
+ * Both speak the same envelope to the same kind of contract, so what an API
6
+ * demands of its caller (a guardInput-mode guard's value, say) is decided
7
+ * here once and the two cannot drift. Pure types and one dependency-free
8
+ * function, so the browser entry resolves it.
10
9
  */
11
10
  /**
12
11
  * 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.
12
+ * side supplied any. Synchronous so a call without a provider still issues
13
+ * its request in the same tick it was made.
16
14
  */
17
15
  export const mergeGuardInputs = (provided, perCall) => provided !== undefined || perCall !== undefined ? { ...provided, ...perCall } : undefined;