lambder 7.3.1 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/CHANGELOG.md +933 -3
  2. package/README.md +41 -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/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +140 -75
  41. package/dist/core/Lambder.js +347 -227
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +21 -7
  51. package/dist/core/LambderFiles.js +62 -34
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
  73. package/dist/invoke/LambderLambdaEvent.js +40 -22
  74. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  75. package/dist/invoke/lambderHandlerTransport.js +15 -18
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +1 -1
  93. package/dist/mock.js +2 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +124 -46
  99. package/dist/session/LambderSessionManager.js +262 -137
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  134. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  135. package/dist/shared/util/boundKeyField.d.ts +20 -0
  136. package/dist/shared/util/boundKeyField.js +34 -0
  137. package/dist/shared/util/canonicalJson.d.ts +11 -0
  138. package/dist/shared/util/canonicalJson.js +28 -0
  139. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  140. package/dist/shared/util/joinKeyFields.js +22 -0
  141. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  142. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  143. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  144. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  145. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  146. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  147. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  148. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  149. package/dist/shared/wire/LambderApiSignature.js +16 -19
  150. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  151. package/dist/shared/wire/LambderCallOptions.js +9 -11
  152. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  153. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  154. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  155. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  156. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  157. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  158. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  159. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  160. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  161. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  162. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  163. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  164. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  165. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  166. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  167. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  168. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  169. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  170. package/dist/stores/LambderCacheFiller.js +119 -0
  171. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  172. package/dist/stores/LambderCacheKeys.js +54 -0
  173. package/dist/stores/LambderCacheValues.d.ts +45 -0
  174. package/dist/stores/LambderCacheValues.js +74 -0
  175. package/dist/stores/LambderDdbCache.d.ts +121 -56
  176. package/dist/stores/LambderDdbCache.js +528 -225
  177. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  178. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  179. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  180. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  181. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  182. package/dist/stores/LambderDdbSdk.js +79 -33
  183. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  184. package/dist/stores/LambderDdbSessionStore.js +119 -47
  185. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  186. package/dist/stores/LambderHttpFileSource.js +15 -13
  187. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  188. package/dist/stores/LambderMemoryCache.js +113 -0
  189. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  190. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  191. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  192. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  193. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  194. package/dist/stores/LambderMemorySessionStore.js +38 -19
  195. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  196. package/dist/stores/LambderS3FileSource.js +12 -7
  197. package/dist/testing/LambderTestApp.d.ts +21 -23
  198. package/dist/testing/LambderTestApp.js +22 -24
  199. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  200. package/dist/testing/LambderTestVisitor.js +15 -15
  201. package/dist/testing.d.ts +1 -0
  202. package/dist/testing.js +1 -0
  203. package/package.json +12 -3
  204. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  205. package/dist/api/LambderApiPolicyEngine.js +0 -85
  206. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  207. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -1,27 +1,169 @@
1
1
  import { getFS } from "../shared/util/LambderNodeModules.js";
2
2
  import { renderHtmlValue } from "../shared/LambderHtml.js";
3
+ import { HtmlPositionReader, URL_ATTRIBUTE_NAMES, safeUrlValue, valueLanguageOf } from "../shared/LambderHtmlPositions.js";
3
4
  const TOKEN_PATTERN = /<!--\s*(?:(slot:([\w-]+)\s*\/)|(slot:([\w-]+))|(\/slot:([\w-]+))|(if:(!?)([\w-]+))|(else)|(\/if:(!?)([\w-]+)))\s*-->/g;
4
5
  /**
5
6
  * Fail loudly on slot positions where HTML escaping cannot protect against
6
- * injection: unquoted attribute values (`class=<!--slot:x/-->`) and inside
7
- * <script>/<style> elements (HTML escaping is the wrong grammar there; embed
8
- * data with jsonScript() into a normal slot instead).
7
+ * injection:
8
+ * - an unquoted attribute value (`class=<!--slot:x/-->`, with or without a
9
+ * prefix before the slot), where a space in the value starts a new
10
+ * attribute;
11
+ * - inside a tag where an attribute name goes (`<input <!--slot:x/-->>`),
12
+ * where any value is a new attribute;
13
+ * - inside the quoted value of an event handler, `style` or `srcdoc`, which
14
+ * the browser reads as another language (see valueLanguageOf);
15
+ * - inside <script>/<style> elements (HTML escaping is the wrong grammar
16
+ * there; embed data with jsonScript() into a normal slot instead).
17
+ * Attribute values vary through if/else around whole-tag variants, or a
18
+ * slot inside a quoted value.
9
19
  */
10
- const assertSafeSlotPosition = (source, slotIndex, slotName) => {
11
- const before = source.slice(0, slotIndex);
12
- if (/=\s*$/.test(before)) {
20
+ const assertSafeSlotPosition = (point, slotName) => {
21
+ if (point.position === "afterEquals" || point.position === "unquoted") {
13
22
  throw new Error(`LambderTemplatingEngine: slot "${slotName}" is in an unquoted attribute position. ` +
14
23
  `Escaping cannot prevent injection there; quote the attribute: attr="<!--slot:${slotName}/-->".`);
15
24
  }
16
- for (const tag of ["script", "style"]) {
17
- const lastOpen = before.toLowerCase().lastIndexOf(`<${tag}`);
18
- if (lastOpen !== -1 && before.toLowerCase().indexOf(`</${tag}`, lastOpen) === -1) {
19
- throw new Error(`LambderTemplatingEngine: slot "${slotName}" is inside a <${tag}> element where HTML ` +
20
- `escaping does not apply. Pass data with the jsonScript() helper in a regular slot instead.`);
21
- }
25
+ if (point.position === "tag") {
26
+ throw new Error(`LambderTemplatingEngine: slot "${slotName}" is inside a tag where an attribute name goes. ` +
27
+ `Escaping cannot prevent injection there; put the slot inside a quoted value, or vary the whole tag with <!--if:...-->.`);
28
+ }
29
+ const valueLanguage = point.position === "quoted" ? valueLanguageOf(point.attributeName) : null;
30
+ if (valueLanguage) {
31
+ throw new Error(`LambderTemplatingEngine: slot "${slotName}" is inside the ${point.attributeName} attribute, whose value the browser ` +
32
+ `reads as ${valueLanguage} once it has decoded the HTML escapes, so escaping cannot prevent injection there. ` +
33
+ `Pass the value in a data- attribute a script reads, or vary the whole tag with <!--if:...-->.`);
34
+ }
35
+ if (point.position === "rawText") {
36
+ throw new Error(`LambderTemplatingEngine: slot "${slotName}" is inside a <${point.tagName}> element where HTML ` +
37
+ `escaping does not apply. Pass data with the jsonScript() helper in a regular slot instead.`);
38
+ }
39
+ };
40
+ /** Where a point sits, in the words of a refusal. */
41
+ const describePoint = (point) => {
42
+ switch (point.position) {
43
+ case "text": return "in text";
44
+ case "comment": return "in a comment";
45
+ case "tag": return "inside a tag where an attribute name goes";
46
+ case "afterEquals":
47
+ case "unquoted": return `in the unquoted ${point.attributeName} value`;
48
+ case "quoted": return `in the quoted ${point.attributeName} value`;
49
+ case "rawText": return `inside a <${point.tagName}> element`;
22
50
  }
23
51
  };
52
+ /** What a refusal of branches that leave the template in different positions tells the author to do. */
53
+ const BRANCH_ADVICE = "Vary the whole tag or element inside the branches instead, so every branch ends where the others do.";
24
54
  const parseTemplate = (source) => {
55
+ const tokens = [...source.matchAll(TOKEN_PATTERN)];
56
+ // Every slot's position is checked before the tree is built, which also
57
+ // finds the URL values holding one: the tree wraps each such value in a
58
+ // node of its own, so a render can check the value whole.
59
+ //
60
+ // The HTML is read in one pass along the template's own structure, with
61
+ // the tokens left out: they are not HTML, and inside a tag the ">" that
62
+ // ends <!--if:x--> would otherwise read as the tag's end. Each branch of
63
+ // an if is read from where the block starts, a missing else being an
64
+ // empty branch, and a slot with default content is a block whose
65
+ // branches are the default and the value. After a block, the text that
66
+ // follows is read in both resulting readings until they sit at the same
67
+ // position and join, and they have to before the next block, or which
68
+ // position the next block's branches start from would depend on the
69
+ // data. A slot met while they are still apart passes only when it lands
70
+ // alike in both. Each character is read at most twice, whatever the
71
+ // nesting. Malformed nesting stops the walk, and the build below refuses
72
+ // it by name.
73
+ const urlValues = new Map();
74
+ let readings = [new HtmlPositionReader()];
75
+ let apartSince = "";
76
+ const blocks = [];
77
+ const readOn = (text) => {
78
+ for (const reading of readings) {
79
+ reading.read(text);
80
+ const undecidable = reading.undecidable;
81
+ if (undecidable?.reason === "commentValue") {
82
+ throw new Error(`LambderTemplatingEngine: slot "${undecidable.label}" is inside a comment, right before text that ends the comment ` +
83
+ `or not depending on how the value ends ("-", "--" or "--!" before a ">"). Put a space after the slot.`);
84
+ }
85
+ if (undecidable) {
86
+ throw new Error(`LambderTemplatingEngine: the branches of ${undecidable.label} end after different attribute names, and the "=" ` +
87
+ `after them would give its value to a different attribute in each. ${BRANCH_ADVICE}`);
88
+ }
89
+ }
90
+ if (readings.length === 2 && readings[0].joinWith(readings[1], apartSince))
91
+ readings = [readings[0]];
92
+ };
93
+ const requireOneReading = (token) => {
94
+ if (readings.length === 1)
95
+ return readings[0];
96
+ const [first, second] = readings.map((reading) => describePoint(reading.point()));
97
+ throw new Error(`LambderTemplatingEngine: the branches of ${apartSince} leave the template in different positions` +
98
+ `${first === second ? "" : ` (${first} after one, ${second} after the other)`}, and ${token} comes before they meet again. ` +
99
+ BRANCH_ADVICE);
100
+ };
101
+ let readCursor = 0;
102
+ let walkedToEnd = true;
103
+ for (const token of tokens) {
104
+ readOn(source.slice(readCursor, token.index));
105
+ readCursor = token.index + token[0].length;
106
+ const [, , selfClosingName, openSlot, openSlotName, closeSlot, closeSlotName, openIf, openIfNegation, openIfName, elseTag, , closeIfNegation, closeIfName] = token;
107
+ const slotName = selfClosingName ?? openSlotName;
108
+ if (slotName !== undefined) {
109
+ const [point, otherPoint] = readings.map((reading) => reading.point());
110
+ if (otherPoint && (otherPoint.position !== point.position || otherPoint.attributeName !== point.attributeName
111
+ || otherPoint.quote !== point.quote || otherPoint.tagName !== point.tagName)) {
112
+ throw new Error(`LambderTemplatingEngine: slot "${slotName}" comes after ${apartSince}, whose branches leave the template in different ` +
113
+ `positions: the slot is ${describePoint(point)} after one, ${describePoint(otherPoint)} after the other. ${BRANCH_ADVICE}`);
114
+ }
115
+ assertSafeSlotPosition(point, slotName);
116
+ if (point.position === "quoted" && URL_ATTRIBUTE_NAMES.has(point.attributeName)) {
117
+ // No token holds a quote character, so the nearest quotes
118
+ // around the slot are its value's own. A quote only one
119
+ // branch holds leaves that block crossing the value's edge,
120
+ // which the build below refuses.
121
+ const start = source.lastIndexOf(point.quote, token.index) + 1;
122
+ const end = source.indexOf(point.quote, token.index);
123
+ if (end === -1) {
124
+ throw new Error(`LambderTemplatingEngine: slot "${slotName}" is inside a quoted ${point.attributeName} value that never closes.`);
125
+ }
126
+ urlValues.set(start, { start, end, attributeName: point.attributeName });
127
+ }
128
+ if (openSlot) {
129
+ const valueReading = requireOneReading(token[0]).copy();
130
+ valueReading.skipValue(slotName);
131
+ blocks.push({ opening: token[0], key: `slot:${slotName}`, secondStart: valueReading, firstEnd: null });
132
+ }
133
+ else {
134
+ for (const reading of readings)
135
+ reading.skipValue(slotName);
136
+ }
137
+ continue;
138
+ }
139
+ if (openIf) {
140
+ blocks.push({ opening: token[0], key: `if:${openIfNegation}${openIfName}`, secondStart: requireOneReading(token[0]).copy(), firstEnd: null });
141
+ continue;
142
+ }
143
+ const block = blocks.pop();
144
+ const closingKey = closeSlot ? `slot:${closeSlotName}` : `if:${closeIfNegation}${closeIfName}`;
145
+ if (!block || (elseTag ? !block.key.startsWith("if:") || block.firstEnd !== null : block.key !== closingKey)) {
146
+ walkedToEnd = false;
147
+ break;
148
+ }
149
+ const reading = requireOneReading(token[0]);
150
+ if (elseTag) {
151
+ block.firstEnd = reading;
152
+ readings = [block.secondStart];
153
+ blocks.push(block);
154
+ continue;
155
+ }
156
+ readings = block.firstEnd ? [block.firstEnd, reading] : [reading, block.secondStart];
157
+ apartSince = block.opening;
158
+ }
159
+ if (walkedToEnd)
160
+ readOn(source.slice(readCursor));
161
+ // In source order already: the values were met in token order, and two
162
+ // never overlap.
163
+ const boundaries = [...urlValues.values()].flatMap((range) => [
164
+ { index: range.start, range, opens: true },
165
+ { index: range.end, range, opens: false },
166
+ ]);
25
167
  const root = { kind: "root", name: "", negated: false, nodes: [], elseNodes: [], inElse: false };
26
168
  const stack = [root];
27
169
  const top = () => stack[stack.length - 1];
@@ -29,21 +171,42 @@ const parseTemplate = (source) => {
29
171
  const frame = top();
30
172
  (frame.inElse ? frame.elseNodes : frame.nodes).push(node);
31
173
  };
32
- TOKEN_PATTERN.lastIndex = 0;
33
174
  let cursor = 0;
34
- let match;
35
- while ((match = TOKEN_PATTERN.exec(source)) !== null) {
36
- if (match.index > cursor) {
37
- emit({ type: "text", value: source.slice(cursor, match.index) });
175
+ const emitTextUpTo = (index) => {
176
+ if (index > cursor)
177
+ emit({ type: "text", value: source.slice(cursor, index) });
178
+ cursor = index;
179
+ };
180
+ let nextBoundary = 0;
181
+ const passBoundariesUpTo = (index) => {
182
+ for (; nextBoundary < boundaries.length && boundaries[nextBoundary].index <= index; nextBoundary += 1) {
183
+ const { index: boundaryIndex, range, opens } = boundaries[nextBoundary];
184
+ emitTextUpTo(boundaryIndex);
185
+ if (opens) {
186
+ stack.push({ kind: "urlValue", name: range.attributeName, negated: false, nodes: [], elseNodes: [], inElse: false });
187
+ continue;
188
+ }
189
+ const frame = stack.pop();
190
+ if (frame.kind !== "urlValue") {
191
+ throw new Error(`LambderTemplatingEngine: <!--${frame.kind}:${frame.name}--> opens inside a quoted ${range.attributeName} value holding a slot ` +
192
+ `and closes outside it. Keep the block wholly inside the value, or the value wholly inside the block.`);
193
+ }
194
+ emit({ type: "urlValue", nodes: frame.nodes });
38
195
  }
39
- cursor = TOKEN_PATTERN.lastIndex;
196
+ };
197
+ for (const match of tokens) {
198
+ passBoundariesUpTo(match.index);
199
+ emitTextUpTo(match.index);
200
+ cursor = match.index + match[0].length;
40
201
  const [, selfClosingSlot, selfClosingName, openSlot, openSlotName, closeSlot, closeSlotName, openIf, openIfNegation, openIfName, elseTag, closeIf, closeIfNegation, closeIfName] = match;
202
+ if ((closeSlot || elseTag || closeIf) && top().kind === "urlValue") {
203
+ throw new Error(`LambderTemplatingEngine: ${match[0]} belongs to a block opened outside the quoted ${top().name} value holding a slot it sits in. ` +
204
+ `Keep the block wholly inside the value, or the value wholly inside the block.`);
205
+ }
41
206
  if (selfClosingSlot) {
42
- assertSafeSlotPosition(source, match.index, selfClosingName);
43
207
  emit({ type: "slot", name: selfClosingName, defaultNodes: [] });
44
208
  }
45
209
  else if (openSlot) {
46
- assertSafeSlotPosition(source, match.index, openSlotName);
47
210
  stack.push({ kind: "slot", name: openSlotName, negated: false, nodes: [], elseNodes: [], inElse: false });
48
211
  }
49
212
  else if (closeSlot) {
@@ -72,35 +235,46 @@ const parseTemplate = (source) => {
72
235
  emit({ type: "if", name: frame.name, negated: frame.negated, thenNodes: frame.nodes, elseNodes: frame.elseNodes });
73
236
  }
74
237
  }
75
- if (cursor < source.length) {
76
- emit({ type: "text", value: source.slice(cursor) });
77
- }
238
+ passBoundariesUpTo(source.length);
239
+ emitTextUpTo(source.length);
78
240
  if (stack.length !== 1) {
79
241
  const frame = top();
80
242
  throw new Error(`LambderTemplatingEngine: unclosed <!--${frame.kind}:${frame.name}--> block.`);
81
243
  }
82
244
  return root.nodes;
83
245
  };
84
- const renderNodes = (nodes, data) => {
85
- let out = "";
246
+ const renderNodes = (nodes, data, output) => {
86
247
  for (const node of nodes) {
87
248
  if (node.type === "text") {
88
- out += node.value;
249
+ output.html += node.value;
89
250
  }
90
251
  else if (node.type === "slot") {
91
252
  const value = Object.prototype.hasOwnProperty.call(data, node.name) ? data[node.name] : undefined;
92
- out += value === undefined ? renderNodes(node.defaultNodes, data) : renderHtmlValue(value);
253
+ if (value === undefined) {
254
+ renderNodes(node.defaultNodes, data, output);
255
+ }
256
+ else {
257
+ if (output.firstDataIndex === -1)
258
+ output.firstDataIndex = output.html.length;
259
+ output.html += renderHtmlValue(value);
260
+ }
261
+ }
262
+ else if (node.type === "urlValue") {
263
+ // Rendered on its own, so the check reads the value whole and
264
+ // knows which part of it the template wrote.
265
+ const value = { html: "", firstDataIndex: -1 };
266
+ renderNodes(node.nodes, data, value);
267
+ output.html += safeUrlValue(value.html, value.firstDataIndex);
93
268
  }
94
269
  else {
95
270
  // Own properties only, the same lookup the slot branch uses:
96
- // through the prototype, <!--if:toString--> was unconditionally
97
- // true on every render.
271
+ // through the prototype, <!--if:toString--> would be true on
272
+ // every render.
98
273
  const value = Object.prototype.hasOwnProperty.call(data, node.name) ? data[node.name] : undefined;
99
274
  const condition = !!value !== node.negated;
100
- out += renderNodes(condition ? node.thenNodes : node.elseNodes, data);
275
+ renderNodes(condition ? node.thenNodes : node.elseNodes, data, output);
101
276
  }
102
277
  }
103
- return out;
104
278
  };
105
279
  const collectNames = (nodes, slots, conditions) => {
106
280
  for (const node of nodes) {
@@ -113,6 +287,9 @@ const collectNames = (nodes, slots, conditions) => {
113
287
  collectNames(node.thenNodes, slots, conditions);
114
288
  collectNames(node.elseNodes, slots, conditions);
115
289
  }
290
+ else if (node.type === "urlValue") {
291
+ collectNames(node.nodes, slots, conditions);
292
+ }
116
293
  }
117
294
  };
118
295
  export class LambderTemplatingEngine {
@@ -144,7 +321,9 @@ export class LambderTemplatingEngine {
144
321
  }
145
322
  /** Render with escaped-by-default data; unknown keys ignored, omitted slots keep defaults. */
146
323
  render(data = {}) {
147
- return renderNodes(this.nodes, data);
324
+ const output = { html: "", firstDataIndex: -1 };
325
+ renderNodes(this.nodes, data, output);
326
+ return output.html;
148
327
  }
149
328
  }
150
329
  /** Wrap the <title> content and the pre-</head> position in virtual slot markers. */
package/dist/index.d.ts CHANGED
@@ -3,7 +3,9 @@ export default Lambder;
3
3
  export { initLambder } from './core/Lambder.js';
4
4
  export type { LambderCreatedHook } from './core/Lambder.js';
5
5
  export { default as LambderCaller } from "./client/LambderCaller.js";
6
- export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderIdempotencyKeyScope, LambderLogListHandler } from "./client/LambderCaller.js";
6
+ export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderLogListHandler } from "./client/LambderCaller.js";
7
+ export { createIdempotencyKey, createIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
8
+ export type { LambderIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
7
9
  export { lambderFetchTransport } from "./client/lambderFetchTransport.js";
8
10
  export { buildTransportEnvelope, LambderTransportFailure, isLambderTransportFailure } from "./shared/transport/LambderApiTransport.js";
9
11
  export { lambderCookieJarTransport } from "./shared/transport/lambderCookieJarTransport.js";
@@ -34,13 +36,14 @@ export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVer
34
36
  export { buildApiEnvelope, envelopeAnswer, refusalAnswer, validationAnswer, apiNotFoundAnswer, sessionExpiredAnswer, versionExpiredAnswer, invalidPayloadAnswer, crashAnswer, API_ANSWER_CONTENT_TYPE, } from "./api/LambderApiEnvelope.js";
35
37
  export type { LambderApiEnvelopeConfig, LambderValidationAnswerBody } from "./api/LambderApiEnvelope.js";
36
38
  export { LambderApiValidationRefusal, isLambderApiValidationRefusal } from "./api/LambderApiValidationRefusal.js";
39
+ export { LambderApiOutputValidationError } from "./api/LambderApiOutputValidationError.js";
37
40
  export { LambderInvokeError, isLambderInvokeError } from "./invoke/LambderInvokeOutcome.js";
38
41
  export type { LambderInvokeOutcome, LambderInvokeFailure, LambderInvokeValidationFailure, LambderInvokeCrashFailure, LambderInvokePayloadTooLargeFailure, LambderInvokeEnvelopeFailure, LambderInvokeDeliveryFailure, LambderInvokeFailureReason, LambderInvokeFunctionError, } from "./invoke/LambderInvokeOutcome.js";
39
42
  export { default as LambderInvokeCaller, LAMBDER_INVOKE_MAX_EVENT_BYTES, } from "./invoke/LambderInvokeCaller.js";
40
43
  export type { LambderInvokeCallerOptions, LambderInvokeCallOptions, LambderInvokeFailureHandler, LambderInvokeLogListHandler, LambderInvokeTransport, LambderInvokeTransportResult, LambderInvokeRequestInit, LambderInvokeEventInit, } from "./invoke/LambderInvokeCaller.js";
41
44
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
42
45
  export type { LambderCrashDetail, LambderCrashCause } from "./shared/wire/LambderCrashDetail.js";
43
- export { LambderApiRefusal, isLambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
46
+ export { LambderApiRefusal, isLambderApiRefusal, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
44
47
  export type { LambderApiRefusalOptions, LambderRefusalMessage, LambderAppRefusalMessage, LambderRefusalCode, LambderRefuseOptions } from "./shared/wire/LambderApiRefusal.js";
45
48
  export { default as LambderResponseBuilder } from "./core/LambderResponseBuilder.js";
46
49
  export { default as LambderResolver } from "./core/LambderResolver.js";
@@ -49,7 +52,7 @@ export type { LambderSessionManagerOptions } from "./session/LambderSessionManag
49
52
  export { default as LambderSessionController } from "./session/LambderSessionController.js";
50
53
  export { DEFAULT_SESSION_TOKEN_COOKIE_KEY, DEFAULT_SESSION_CSRF_COOKIE_KEY } from "./shared/wire/LambderSessionCookieNames.js";
51
54
  export type { LambderSessionControllerOptions, LambderSessionRequestInfo } from "./session/LambderSessionController.js";
52
- export type { LambderSessionStore, LambderSessionRecord } from "./shared/contracts/LambderSessionStore.js";
55
+ export type { LambderSessionStore, LambderSessionRecord, LambderSessionChanges, LambderSessionUpdateResult } from "./shared/contracts/LambderSessionStore.js";
53
56
  export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
54
57
  export { LambderWebCrypto, LambderPlainSessionCrypto, isWebCryptoAvailable } from "./session/LambderSessionCrypto.js";
55
58
  export type { LambderSessionCrypto } from "./session/LambderSessionCrypto.js";
@@ -65,7 +68,7 @@ export type { LambderRouteMatcher, LambderRouteConditionFn, LambderRouteConditio
65
68
  export type { LambderCorsConfig } from "./core/LambderCors.js";
66
69
  export type { LambderCreateOptions, LambderSessionOptions, LambderActionTools, LambderHandler, } from "./core/LambderCreateOptions.js";
67
70
  export type { LambderIndexHtmlOptions } from "./core/LambderIndexHtml.js";
68
- export type { LambderRouteHandler, LambderSessionRouteHandler, LambderActionHandler, LambderActionFilter, LambderHookEvent, LambderBeforeRenderHook, LambderAfterRenderHook, LambderFallbackHook, LambderGlobalErrorHandler, LambderFallbackHandler, LambderInputValidationHandler, } from "./core/LambderCreateOptions.js";
71
+ export type { LambderRouteHandler, LambderSessionRouteHandler, LambderActionHandler, LambderActionFilter, LambderHookEvent, LambderBeforeRenderHook, LambderAfterRenderHook, LambderFallbackHook, LambderGlobalErrorHandler, LambderFallbackHandler, LambderInputValidationHandler, LambderCrashOptions, LambderCrashReporter, LambderCrashSite, } from "./core/LambderCreateOptions.js";
69
72
  export type { LambderPublicFilesOptions } from "./core/LambderPublicFiles.js";
70
73
  export { LambderFiles } from "./core/LambderFiles.js";
71
74
  export type { LambderFilesOption, LambderFileMemoryCacheOption, LambderReadFile } from "./core/LambderFiles.js";
@@ -83,8 +86,11 @@ export { compressText, restoreBytes, restoreText, LambderCompressionError, LAMBD
83
86
  export type { LambderRestoreFailure, LambderRestoreBound } from "./shared/wire/LambderCompressionCodec.js";
84
87
  export { LambderSessionDataRefreshError, LambderSessionReadError } from "./session/LambderSessionManager.js";
85
88
  export { LambderSessionNotFoundError, LambderSessionAmbiguousError } from "./session/LambderSessionController.js";
89
+ export type { LambderCache, LambderCacheKey, LambderCacheSetOptions, LambderCacheListOptions } from "./shared/contracts/LambderCache.js";
86
90
  export { LambderDdbCache } from "./stores/LambderDdbCache.js";
87
- export type { LambderCacheKey, LambderDdbCacheOptions, LambderDdbCacheSetOptions, LambderDdbCacheGetOrSetOptions, LambderDdbCacheListOptions, } from "./stores/LambderDdbCache.js";
91
+ export type { LambderDdbCacheOptions, LambderDdbCacheGetOrSetOptions, } from "./stores/LambderDdbCache.js";
92
+ export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
93
+ export type { LambderMemoryCacheOptions } from "./stores/LambderMemoryCache.js";
88
94
  export { RATE_LIMIT_WINDOWS } from "./shared/contracts/LambderRateLimiter.js";
89
95
  export type { LambderRateLimiter, LambderRateLimitWindow, LambderRateLimitPolicy, LambderRateLimitExceeded, LambderRateLimitResult, } from "./shared/contracts/LambderRateLimiter.js";
90
96
  export { LambderDdbRateLimiter } from "./stores/LambderDdbRateLimiter.js";
@@ -96,19 +102,20 @@ export type { LambderDdbIdempotencyStoreOptions } from "./stores/LambderDdbIdemp
96
102
  export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
97
103
  export { lambderGuard, lambderRateLimitKey } from "./core/LambderPolicyBuilders.js";
98
104
  export { lambderGuardBuilder } from "./api/LambderApiGuards.js";
99
- export type { LambderGuardBuilder, LambderApiGuard, LambderGuardMeta, LambderGuardMetaMap, LambderAllowedGuardNames, LambderParamlessGuardNames, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf, } from "./api/LambderApiGuards.js";
105
+ export type { LambderGuardBuilder, LambderApiGuard, LambderGuardMeta, LambderGuardMetaMap, LambderGuardRunAt, LambderAllowedGuardNames, LambderParamlessGuardNames, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf, } from "./api/LambderApiGuards.js";
100
106
  export { lambderRateLimitKeyBuilder, rateLimitRefusal, DEFAULT_RATE_LIMIT_REFUSAL } from "./api/LambderApiRateLimits.js";
101
107
  export type { LambderRateLimitKeyBuilder } from "./api/LambderApiRateLimits.js";
102
- export type { LambderRateLimitKeyFn, LambderRateLimitPer, LambderRateLimitBudget, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderAllowedPolicyNames, LambderRateLimitOption, } from "./api/LambderApiRateLimits.js";
108
+ export type { LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult, LambderChargeablePolicyNames, LambderChargeKeyArgs, LambderRateLimitKeyFn, LambderRateLimitPer, LambderRateLimitBudget, LambderRateLimitChargeAt, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderAllowedPolicyNames, LambderRateLimitOption, } from "./api/LambderApiRateLimits.js";
103
109
  export type { LambderApiIdempotencyConfig } from "./api/LambderApiIdempotency.js";
104
110
  export type { LambderGuardsOptionValue, LambderRateLimitOverride, LambderRateLimitOptionValue, LambderApiIdempotencyOption, } from "./shared/wire/LambderApiOptionValues.js";
105
111
  export { createLambderI18n } from "./shared/LambderI18n.js";
106
112
  export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
107
- export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderApiNullAnswerConfig, LambderContractEntry, LambderMergeContract, LambderFlattenContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
113
+ export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderApiNullAnswerConfig, LambderContractEntry, LambderMergeContract, LambderFlattenContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderJsonOutputOf, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
108
114
  export type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent, LambderHttpEventFormat } from "./core/LambderContext.js";
109
115
  export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
110
116
  export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
111
117
  export { createContext, isV2HttpEvent } from "./core/LambderContext.js";
118
+ export type { LambderContextOptions } from "./core/LambderContext.js";
112
119
  export { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS, compressPayloadBrotli, } from "./shared/wire/LambderRequestPayload.js";
113
120
  export type { LambderCompressedGzipPayload, LambderCompressedBrotliPayload, LambderRequestCompressionOption, LambderRequestCompressionSettings, } from "./shared/wire/LambderRequestPayload.js";
114
121
  export { serializeCookie, serializeClearCookie, resolveCookieDomain } from "./shared/wire/LambderCookie.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import Lambder from './core/Lambder.js';
2
2
  export default Lambder;
3
3
  export { initLambder } from './core/Lambder.js';
4
4
  export { default as LambderCaller } from "./client/LambderCaller.js";
5
+ export { createIdempotencyKey, createIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
5
6
  // Transports: how a caller reaches a server (fetch in production, a handler in-process for tests, a cookie jar over either)
6
7
  export { lambderFetchTransport } from "./client/lambderFetchTransport.js";
7
8
  export { buildTransportEnvelope, LambderTransportFailure, isLambderTransportFailure } from "./shared/transport/LambderApiTransport.js";
@@ -23,13 +24,14 @@ export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
23
24
  export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
24
25
  export { buildApiEnvelope, envelopeAnswer, refusalAnswer, validationAnswer, apiNotFoundAnswer, sessionExpiredAnswer, versionExpiredAnswer, invalidPayloadAnswer, crashAnswer, API_ANSWER_CONTENT_TYPE, } from "./api/LambderApiEnvelope.js";
25
26
  export { LambderApiValidationRefusal, isLambderApiValidationRefusal } from "./api/LambderApiValidationRefusal.js";
27
+ export { LambderApiOutputValidationError } from "./api/LambderApiOutputValidationError.js";
26
28
  // Calling a Lambder app from another lambda (server-only: the Lambda SDK, zlib)
27
29
  export { LambderInvokeError, isLambderInvokeError } from "./invoke/LambderInvokeOutcome.js";
28
30
  export { default as LambderInvokeCaller, LAMBDER_INVOKE_MAX_EVENT_BYTES, } from "./invoke/LambderInvokeCaller.js";
29
31
  // A crash described for a caller allowed to see it (the envelope's `crash` field)
30
32
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
31
33
  // Typed API refusals (isomorphic: shared code may throw them from anywhere)
32
- export { LambderApiRefusal, isLambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
34
+ export { LambderApiRefusal, isLambderApiRefusal, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
33
35
  export { default as LambderResponseBuilder } from "./core/LambderResponseBuilder.js";
34
36
  export { default as LambderResolver } from "./core/LambderResolver.js";
35
37
  export { default as LambderSessionManager } from "./session/LambderSessionManager.js";
@@ -55,8 +57,8 @@ export { resolveCompressionOption, LAMBDER_ENCODINGS } from "./shared/wire/Lambd
55
57
  export { compressText, restoreBytes, restoreText, LambderCompressionError, LAMBDER_RESTORE_FAILURES, } from "./shared/wire/LambderCompressionCodec.js";
56
58
  export { LambderSessionDataRefreshError, LambderSessionReadError } from "./session/LambderSessionManager.js";
57
59
  export { LambderSessionNotFoundError, LambderSessionAmbiguousError } from "./session/LambderSessionController.js";
58
- // DynamoDB-backed compressed cache (standalone, server-only)
59
60
  export { LambderDdbCache } from "./stores/LambderDdbCache.js";
61
+ export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
60
62
  // Fixed-window rate limiting: the shared vocabulary, the DynamoDB limiter and the in-memory one
61
63
  export { RATE_LIMIT_WINDOWS } from "./shared/contracts/LambderRateLimiter.js";
62
64
  export { LambderDdbRateLimiter } from "./stores/LambderDdbRateLimiter.js";
@@ -74,8 +76,7 @@ export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
74
76
  export { createContext, isV2HttpEvent } from "./core/LambderContext.js";
75
77
  // Request payload compression: the wire format LambderCaller and the server share.
76
78
  export { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, DEFAULT_MAX_RESTORED_PAYLOAD_BYTES,
77
- // The Brotli twin of the browser's compressPayloadGzip, beside it now
78
- // rather than inside LambderInvokeCaller; the root entry's name is unchanged.
79
+ // The Brotli twin of the browser's compressPayloadGzip, and its defaults.
79
80
  DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS, compressPayloadBrotli, } from "./shared/wire/LambderRequestPayload.js";
80
81
  // Cookies (res.setCookie / res.clearCookie build on these; exported for code holding a LambderResponse)
81
82
  export { serializeCookie, serializeClearCookie, resolveCookieDomain } from "./shared/wire/LambderCookie.js";
@@ -2,17 +2,17 @@
2
2
  * Calling a Lambder app from another lambda, or from any server code that
3
3
  * holds AWS credentials and lambda:InvokeFunction on it.
4
4
  *
5
- * API Gateway delivers an HTTP request to a Lambder app as a JSON event and
6
- * takes a JSON response object back; a direct InvokeCommand carries JSON in
7
- * both directions too. So this caller builds the payload-format-2.0 event
8
- * API Gateway would have built, invokes the function with it, and reads the
9
- * response object Lambder returns. The callee is an unmodified Lambder app,
10
- * and everything it offers over HTTP (zod validation, the inferred contract,
11
- * refusals, guards, idempotency keys, Brotli answers, logList, the crash
12
- * detail its global error handler chooses to send) applies unchanged. The
13
- * callee tells an invoke from a browser only by the x-lambder-invoke header,
14
- * which is a marker for guards and hooks, never an authorization: the IAM
15
- * grant is that.
5
+ * Builds the payload-format-2.0 event API Gateway would have built, invokes
6
+ * the function with it directly, and reads the response object Lambder
7
+ * returns. The callee is an unmodified Lambder app, so everything it offers
8
+ * over HTTP (zod validation, the inferred contract, refusals, guards,
9
+ * idempotency keys, Brotli answers, logList, the crash detail its error
10
+ * handler chooses to send) applies unchanged. The callee tells an invoke
11
+ * apart by the event's requestContext.apiId, which no gateway lets a client
12
+ * write, and reads no forwarding header on one, so `clientIp` and `host` are
13
+ * the only address and host it sees. The x-lambder-invoke header is a marker
14
+ * for guards and hooks, never an authorization: the IAM grant is the
15
+ * authorization.
16
16
  *
17
17
  * Server-only (zlib, the Lambda SDK), so it is exported from the root entry
18
18
  * and never from lambder/client. The SDK is an optional peer dependency
@@ -72,23 +72,18 @@ type LambderInvokeCallerBaseOptions = {
72
72
  /** Function name or ARN. */
73
73
  functionName: string;
74
74
  /**
75
- * A ready client, e.g. one shared with the rest of the app. It keeps
76
- * whatever `maxAttempts` it was built with, which is the SDK's own 3
77
- * unless the app said otherwise: `clientConfig` below is not consulted
78
- * for a client this caller did not create, and a retry at that layer
79
- * re-executes a callee whose response was merely lost. Build it with
80
- * `{ maxAttempts: 1 }`, or send an `idempotencyKey` and let the callee
81
- * settle the repeat.
75
+ * A ready client, e.g. one shared with the rest of the app. `clientConfig`
76
+ * does not apply to it, so it keeps its own `maxAttempts` (the SDK's 3 by
77
+ * default), and a retry re-executes a callee whose response was merely
78
+ * lost: build it with `{ maxAttempts: 1 }`, or send an `idempotencyKey`.
82
79
  */
83
80
  client?: LambdaClient;
84
81
  /**
85
82
  * Otherwise the client is created from this on the first call (region,
86
- * credentials, maxAttempts). `maxAttempts` defaults to 1 here rather than
87
- * to the SDK's 3: a RequestResponse invoke whose response is lost has
88
- * already run the callee, so a retry at this layer executes the operation
89
- * a second time, and the transport contract says one call is one delivery
90
- * attempt. Raise it deliberately if the callee is idempotent, or send an
91
- * `idempotencyKey` and let the callee settle it.
83
+ * credentials, maxAttempts). `maxAttempts` defaults to 1 rather than the
84
+ * SDK's 3, since a lost response means the callee already ran and a retry
85
+ * would run it twice. Raise it only for an idempotent callee, or send an
86
+ * `idempotencyKey`.
92
87
  */
93
88
  clientConfig?: LambdaClientConfig;
94
89
  /** Must match the callee's apiPath. Default: "/api". */
@@ -118,10 +113,10 @@ type LambderInvokeCallerBaseOptions = {
118
113
  /** Receives each answer's logList. Default: console.log with the function and api name. A throw is logged and otherwise ignored. */
119
114
  onLogList?: LambderInvokeLogListHandler;
120
115
  /**
121
- * Called, and awaited, for every failed call before api() throws or
122
- * apiOutcome() returns, so failures are reported in one place whichever
123
- * method the site used, and before the lambda answers. A throw inside it
124
- * is logged and otherwise ignored: apiOutcome() never throws.
116
+ * Called and awaited for every failed call before api() throws or
117
+ * apiOutcome() returns, so failures are reported in one place, before the
118
+ * lambda answers. A throw inside it is logged and otherwise ignored, so
119
+ * apiOutcome() never throws.
125
120
  */
126
121
  onFailure?: LambderInvokeFailureHandler;
127
122
  /** The session token cookie's name, when a session is carried and the callee uses a non-default `tokenCookieKey`. The CSRF value rides in the envelope's `token` field, which has no name to configure. */
@@ -195,10 +190,9 @@ export default class LambderInvokeCaller<TContract extends LambderApiContractSha
195
190
  * Lambda would: a thrown error becomes a FunctionError payload. For
196
191
  * tests that want the real handlers behind the real envelope.
197
192
  *
198
- * It honours the signal the way lambderHandlerTransport does, by ending
199
- * the wait: a function call in this process cannot be cancelled, so the
200
- * handler runs to completion regardless and what a timeout buys is the
201
- * caller's answer. Ignoring it made timeoutMs a no-op here.
193
+ * It honours the signal by ending the wait, as lambderHandlerTransport
194
+ * does: an in-process call cannot be cancelled, so the handler runs to
195
+ * completion regardless, but timeoutMs still frees the caller.
202
196
  */
203
197
  static localTransport(handler: (event: APIGatewayProxyEventV2, context: Context) => Promise<unknown>, context?: Partial<Context>): LambderInvokeTransport;
204
198
  private loadSdk;
@@ -208,25 +202,26 @@ export default class LambderInvokeCaller<TContract extends LambderApiContractSha
208
202
  /** Builds the failure and its error, reports it once, and hands it back. */
209
203
  private failureOutcome;
210
204
  private surfaceLogs;
211
- /** One call, one outcome. Never throws; api() is what throws. */
205
+ /**
206
+ * One call, one outcome. Never throws; api() is what throws. A key scope
207
+ * is told how the attempt ended, as it is on LambderCaller.
208
+ */
212
209
  private dispatch;
210
+ private dispatchAttempt;
213
211
  /**
214
212
  * Full-fidelity call: resolves to a discriminated LambderInvokeOutcome
215
- * instead of throwing. Never throws; for sites that degrade gracefully.
216
- *
217
- * The output is computed from the contract in the return type rather than
218
- * taken as a type parameter, so a call site cannot replace it by
219
- * annotating what it assigns to.
213
+ * instead of throwing, for sites that degrade gracefully. The output type
214
+ * comes from the contract rather than a type parameter, so a call site
215
+ * cannot replace it by annotating what it assigns to.
220
216
  */
221
217
  apiOutcome<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderInvokeCallOptions>): Promise<LambderInvokeOutcome<LambderContractOutputOf<TContract, TApiName>>>;
222
218
  /**
223
219
  * The declared output, or a thrown LambderInvokeError carrying the
224
220
  * outcome. A failed dependency is a failed request: the throw reaches the
225
221
  * app's global error handler with the callee's error as its cause. The
226
- * result is the callee's output type as it declared it: the resolver
227
- * only lets a handler answer null when the output allows it or beside a
228
- * reason (LambderApiAnswer), so a nullable output is the one place null
229
- * arrives.
222
+ * resolver lets a handler answer null only when the output allows it or
223
+ * beside a reason (LambderApiAnswer), so null arrives only for a
224
+ * nullable output.
230
225
  */
231
226
  api<TApiName extends keyof TContract & string = string>(apiName: TApiName, ...rest: LambderCallArgs<TContract, TApiName, TProvidedGuards, LambderInvokeCallOptions>): Promise<LambderContractOutputOf<TContract, TApiName>>;
232
227
  /**