lambder 6.0.2 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +2316 -0
  2. package/README.md +60 -33
  3. package/dist/api/LambderApiAnswer.d.ts +40 -0
  4. package/dist/api/LambderApiAnswer.js +19 -0
  5. package/dist/api/LambderApiCallContext.d.ts +38 -0
  6. package/dist/api/LambderApiCallContext.js +13 -0
  7. package/dist/api/LambderApiDefinition.d.ts +18 -0
  8. package/dist/api/LambderApiDefinition.js +1 -0
  9. package/dist/api/LambderApiEnvelope.d.ts +67 -0
  10. package/dist/api/LambderApiEnvelope.js +180 -0
  11. package/dist/api/LambderApiGuards.d.ts +302 -0
  12. package/dist/api/LambderApiGuards.js +134 -0
  13. package/dist/api/LambderApiIdempotency.d.ts +122 -0
  14. package/dist/api/LambderApiIdempotency.js +330 -0
  15. package/dist/api/LambderApiPipeline.d.ts +134 -0
  16. package/dist/api/LambderApiPipeline.js +221 -0
  17. package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
  18. package/dist/api/LambderApiPolicyEngine.js +77 -0
  19. package/dist/api/LambderApiRateLimits.d.ts +206 -0
  20. package/dist/api/LambderApiRateLimits.js +239 -0
  21. package/dist/api/LambderApiRequest.d.ts +101 -0
  22. package/dist/api/LambderApiRequest.js +129 -0
  23. package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
  24. package/dist/api/LambderApiValidationRefusal.js +40 -0
  25. package/dist/client/LambderCaller.d.ts +62 -55
  26. package/dist/client/LambderCaller.js +147 -90
  27. package/dist/client/lambderFetchTransport.d.ts +9 -0
  28. package/dist/client/lambderFetchTransport.js +71 -0
  29. package/dist/client.d.ts +20 -10
  30. package/dist/client.js +11 -5
  31. package/dist/core/Lambder.d.ts +117 -253
  32. package/dist/core/Lambder.js +374 -341
  33. package/dist/core/LambderContext.d.ts +54 -44
  34. package/dist/core/LambderContext.js +41 -110
  35. package/dist/core/LambderCreateOptions.d.ts +285 -0
  36. package/dist/core/LambderCreateOptions.js +44 -0
  37. package/dist/core/LambderFiles.d.ts +1 -45
  38. package/dist/core/LambderFiles.js +18 -38
  39. package/dist/core/LambderIndexHtml.d.ts +37 -0
  40. package/dist/core/LambderIndexHtml.js +87 -0
  41. package/dist/core/LambderPolicyBuilders.d.ts +17 -0
  42. package/dist/core/LambderPolicyBuilders.js +16 -0
  43. package/dist/core/LambderPublicFiles.d.ts +5 -2
  44. package/dist/core/LambderPublicFiles.js +7 -2
  45. package/dist/core/LambderResolver.d.ts +8 -6
  46. package/dist/core/LambderResponse.d.ts +29 -11
  47. package/dist/core/LambderResponse.js +96 -49
  48. package/dist/core/LambderResponseBuilder.d.ts +18 -14
  49. package/dist/core/LambderResponseBuilder.js +19 -25
  50. package/dist/core/LambderRouting.d.ts +18 -7
  51. package/dist/core/LambderRouting.js +17 -7
  52. package/dist/core/LambderTemplatingEngine.d.ts +0 -62
  53. package/dist/core/LambderTemplatingEngine.js +7 -3
  54. package/dist/index.d.ts +85 -32
  55. package/dist/index.js +44 -16
  56. package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
  57. package/dist/invoke/LambderInvokeCaller.js +140 -335
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
  59. package/dist/invoke/LambderInvokeOutcome.js +129 -0
  60. package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
  61. package/dist/invoke/LambderLambdaEvent.js +187 -0
  62. package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
  63. package/dist/invoke/lambderHandlerTransport.js +89 -0
  64. package/dist/mock/LambderMockApp.d.ts +352 -0
  65. package/dist/mock/LambderMockApp.js +815 -0
  66. package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
  67. package/dist/mock/LambderMockBrowserCookies.js +76 -0
  68. package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
  69. package/dist/mock/LambderMockCallRecorder.js +183 -0
  70. package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
  71. package/dist/mock/LambderMockCreateOptions.js +9 -0
  72. package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
  73. package/dist/mock/LambderMockEntryRegistry.js +126 -0
  74. package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
  75. package/dist/mock/LambderMockFailureInjector.js +138 -0
  76. package/dist/mock/LambderMockTypes.d.ts +421 -0
  77. package/dist/mock/LambderMockTypes.js +8 -0
  78. package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
  79. package/dist/mock/lambderMockConsoleLogger.js +35 -0
  80. package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
  81. package/dist/mock/lambderMockInvokeTransport.js +52 -0
  82. package/dist/mock/lambderMockMswHandler.d.ts +99 -0
  83. package/dist/mock/lambderMockMswHandler.js +126 -0
  84. package/dist/mock.d.ts +34 -0
  85. package/dist/mock.js +27 -0
  86. package/dist/session/LambderSessionController.d.ts +199 -30
  87. package/dist/session/LambderSessionController.js +396 -82
  88. package/dist/session/LambderSessionCrypto.d.ts +66 -0
  89. package/dist/session/LambderSessionCrypto.js +101 -0
  90. package/dist/session/LambderSessionManager.d.ts +118 -80
  91. package/dist/session/LambderSessionManager.js +212 -184
  92. package/dist/shared/LambderI18n.d.ts +6 -6
  93. package/dist/shared/LambderI18n.js +1 -1
  94. package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
  95. package/dist/shared/contracts/LambderFileSource.js +19 -0
  96. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
  97. package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
  98. package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
  99. package/dist/shared/contracts/LambderRateLimiter.js +24 -0
  100. package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
  101. package/dist/shared/contracts/LambderSessionStore.js +13 -0
  102. package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
  103. package/dist/shared/transport/LambderApiTransport.js +65 -0
  104. package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
  105. package/dist/shared/transport/LambderCookieJar.js +246 -0
  106. package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
  107. package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
  108. package/dist/shared/util/LambderBase64.d.ts +10 -0
  109. package/dist/shared/util/LambderBase64.js +27 -0
  110. package/dist/shared/util/LambderCallAbort.d.ts +62 -0
  111. package/dist/shared/util/LambderCallAbort.js +80 -0
  112. package/dist/shared/util/LambderClientIp.d.ts +32 -0
  113. package/dist/shared/util/LambderClientIp.js +56 -0
  114. package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
  115. package/dist/shared/util/LambderExpiringMap.js +217 -0
  116. package/dist/shared/util/LambderKeyFields.d.ts +32 -0
  117. package/dist/shared/util/LambderKeyFields.js +34 -0
  118. package/dist/shared/util/LambderNodeModules.d.ts +9 -0
  119. package/dist/shared/util/LambderNodeModules.js +39 -0
  120. package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
  121. package/dist/shared/util/LambderOptionChecks.js +33 -0
  122. package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
  123. package/dist/shared/util/LambderResponseBrand.js +18 -0
  124. package/dist/shared/util/LambderTextDigest.d.ts +17 -0
  125. package/dist/shared/util/LambderTextDigest.js +34 -0
  126. package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
  127. package/dist/shared/util/LambderTypeUtilities.js +8 -0
  128. package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
  129. package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
  130. package/dist/shared/wire/LambderApiContract.d.ts +129 -0
  131. package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
  132. package/dist/shared/wire/LambderApiOptionValues.js +11 -0
  133. package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
  134. package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
  135. package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
  136. package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
  137. package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
  138. package/dist/shared/wire/LambderCallOptions.js +17 -0
  139. package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
  140. package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
  141. package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
  142. package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
  143. package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
  144. package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
  145. package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
  146. package/dist/shared/wire/LambderHttpStatus.js +1 -0
  147. package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
  148. package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
  149. package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
  150. package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
  151. package/dist/stores/LambderDdbCache.d.ts +12 -9
  152. package/dist/stores/LambderDdbCache.js +56 -47
  153. package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
  154. package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
  155. package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
  156. package/dist/stores/LambderDdbRateLimiter.js +47 -45
  157. package/dist/stores/LambderDdbSdk.d.ts +83 -6
  158. package/dist/stores/LambderDdbSdk.js +83 -2
  159. package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
  160. package/dist/stores/LambderDdbSessionStore.js +161 -0
  161. package/dist/stores/LambderHttpFileSource.d.ts +1 -1
  162. package/dist/stores/LambderHttpFileSource.js +10 -1
  163. package/dist/stores/LambderLocalFileSource.d.ts +15 -0
  164. package/dist/stores/LambderLocalFileSource.js +28 -0
  165. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
  166. package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
  167. package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
  168. package/dist/stores/LambderMemoryRateLimiter.js +64 -0
  169. package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
  170. package/dist/stores/LambderMemorySessionStore.js +74 -0
  171. package/dist/stores/LambderS3FileSource.d.ts +1 -1
  172. package/dist/stores/LambderS3FileSource.js +1 -1
  173. package/package.json +21 -19
  174. package/dist/client/LambderMSW.d.ts +0 -69
  175. package/dist/client/LambderMSW.js +0 -121
  176. package/dist/policies/LambderApiGuards.d.ts +0 -256
  177. package/dist/policies/LambderApiGuards.js +0 -94
  178. package/dist/policies/LambderApiIdempotency.d.ts +0 -58
  179. package/dist/policies/LambderApiIdempotency.js +0 -219
  180. package/dist/policies/LambderApiPolicies.d.ts +0 -42
  181. package/dist/policies/LambderApiPolicies.js +0 -52
  182. package/dist/policies/LambderApiRateLimits.d.ts +0 -132
  183. package/dist/policies/LambderApiRateLimits.js +0 -119
  184. package/dist/shared/LambderApiContract.d.ts +0 -57
  185. package/dist/shared/LambderApiOutcome.d.ts +0 -69
  186. package/dist/shared/LambderCallOptions.d.ts +0 -71
  187. package/dist/shared/LambderCallOptions.js +0 -16
  188. package/dist/shared/node-polyfills.d.ts +0 -4
  189. package/dist/shared/node-polyfills.js +0 -58
  190. package/dist/stores/LambderDdbIdempotency.js +0 -229
  191. package/dist/testing.d.ts +0 -9
  192. package/dist/testing.js +0 -8
  193. /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
  194. /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
  195. /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
@@ -1,69 +0,0 @@
1
- /**
2
- * The one mapping from an HTTP answer to an API outcome.
3
- *
4
- * LambderCaller (a browser, over fetch) and LambderInvokeCaller (a server,
5
- * over a direct Lambda invoke) receive the same envelope and must read it
6
- * the same way: which status is a crash, which is a rejected input, in what
7
- * order the envelope flags are honoured, what a non-envelope body means.
8
- * Both hand their answer to resolveApiOutcome and act on the result; the
9
- * side effects each has (handlers, cookie clearing, error reporting) stay
10
- * with the caller that owns them. Pure and dependency-free, so the browser
11
- * entry resolves it.
12
- */
13
- import type { z } from "zod";
14
- import type { LambderApiResponse } from "./LambderApiContract.js";
15
- /**
16
- * The 422 body's `zodError` as it survives JSON: a ZodError's name and
17
- * message, and its issues spelled out. Not a ZodError instance (it has no
18
- * methods on this side of the wire), which is why it is not typed as one.
19
- */
20
- export type LambderValidationError = {
21
- name: string;
22
- message: string;
23
- issues: z.core.$ZodIssue[];
24
- };
25
- export type LambderApiFailureReason = 'network' | 'timeout' | 'server' | 'validation' | 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage' | 'unknown';
26
- /**
27
- * Discriminated result of an API call: `ok: true` carries the payload, every
28
- * failure carries a machine-readable reason, so "the server returned null"
29
- * and "the request failed" are never conflated.
30
- */
31
- export type LambderApiOutcome<T> = {
32
- ok: true;
33
- payload: T | null | undefined;
34
- response: LambderApiResponse<T>;
35
- } | {
36
- ok: false;
37
- reason: LambderApiFailureReason;
38
- /** HTTP status, when a response was received. */
39
- status?: number;
40
- /** Envelope errorMessage, when the server provided one. */
41
- errorMessage?: any;
42
- /** Seconds to wait before retrying, from the response's Retry-After header (rate-limit refusals send it). */
43
- retryAfterSeconds?: number;
44
- /** Underlying Error for network/timeout/server/unknown failures. */
45
- error?: Error;
46
- /** The issues for 'validation'. */
47
- zodError?: LambderValidationError;
48
- /** The parsed envelope, when one was received (protocol-level failures, and a 5xx that answered with Lambder's own envelope). */
49
- response?: LambderApiResponse<T>;
50
- };
51
- /** What the mapping needs from an HTTP answer, whichever transport produced it. */
52
- export type LambderApiHttpAnswer = {
53
- status: number;
54
- statusText?: string;
55
- /** Case-insensitive header lookup; null or undefined when absent. */
56
- header: (name: string) => string | null | undefined;
57
- /** The body parsed as JSON; rejects when it is not JSON. */
58
- json: () => Promise<unknown>;
59
- /** The body as text. */
60
- text: () => Promise<string>;
61
- };
62
- /**
63
- * Reads one HTTP answer into an outcome. A 5xx is a server failure that keeps
64
- * the envelope when the server sent one (Lambder's own 500 body carries
65
- * errorMessage, and a global error handler may add crash and logList); a
66
- * 422 is a validation failure only with Lambder's validation body; anything
67
- * else must be a JSON envelope, whose flags are honoured in a fixed order.
68
- */
69
- export declare const resolveApiOutcome: <T>(answer: LambderApiHttpAnswer) => Promise<LambderApiOutcome<T>>;
@@ -1,71 +0,0 @@
1
- /**
2
- * The contract-driven typing of a call's options argument, and the runtime
3
- * merge of guard inputs, shared by the browser caller (LambderCaller) and the
4
- * server-side invoke caller (LambderInvokeCaller). Both speak the same
5
- * envelope to the same kind of contract, so what an API demands of its caller
6
- * (a guardInput-mode guard's value, say) is decided here once and the two
7
- * callers cannot drift on it. Pure types and one dependency-free function,
8
- * so the browser entry resolves it.
9
- */
10
- export type IsAny<T> = 0 extends (1 & T) ? true : false;
11
- export type GuardInputsOf<TEntry> = TEntry extends {
12
- guardInputs: infer G;
13
- } ? G : never;
14
- /** Input type of guard G on one contract entry; never when that API does not declare it. */
15
- type GuardInputOf<TEntry, G extends string> = GuardInputsOf<TEntry> extends infer I ? (G extends keyof I ? I[G] : never) : never;
16
- /**
17
- * What guardInputsProvider returns: for every provided guard name, the value
18
- * the contract's APIs expect for it (a union across APIs when they differ).
19
- * Naming a guard no API declares in guardInput mode resolves to never, so a
20
- * typo fails the provider's return type instead of going missing at runtime.
21
- */
22
- export type LambderProvidedGuardInputs<TContract, TProvided extends string> = IsAny<TContract> extends true ? Record<TProvided, unknown> : {
23
- [G in TProvided]: {
24
- [K in keyof TContract]: GuardInputOf<TContract[K], G>;
25
- }[keyof TContract];
26
- };
27
- /**
28
- * Supplies guardInputs for every call from one place (the organization the
29
- * UI is on, a device token), keyed by guard name; per-call guardInputs
30
- * merge on top. Name the guards it covers in the caller's second type
31
- * parameter, `new LambderCaller<Contract, "orgPermission">`, and calls to
32
- * APIs whose guardInput guards are all covered no longer require the
33
- * options argument. May be async; a throw fails the call as an unknown
34
- * error before anything is sent.
35
- */
36
- export type LambderGuardInputsProvider<TContract, TProvided extends string> = (apiName: keyof TContract & string) => LambderProvidedGuardInputs<TContract, TProvided> | Promise<LambderProvidedGuardInputs<TContract, TProvided>>;
37
- /** Optional until the caller names provided guards: naming them without a provider would send nothing. */
38
- export type LambderGuardInputsProviderOption<TContract, TProvided extends string> = [
39
- TProvided
40
- ] extends [never] ? {
41
- guardInputsProvider?: LambderGuardInputsProvider<TContract, TProvided>;
42
- } : {
43
- guardInputsProvider: LambderGuardInputsProvider<TContract, TProvided>;
44
- };
45
- /** An API's guardInput guards the provider does not cover: those the call must still pass. */
46
- type RemainingGuardInputs<TEntry, TProvided extends string> = Omit<GuardInputsOf<TEntry>, TProvided>;
47
- /**
48
- * The options argument of one call: optional normally, REQUIRED (with
49
- * guardInputs) when the API's contract declares guardInput-mode guards the
50
- * provider does not cover, so forgetting to send a guard's value is a
51
- * compile error at the call site. Provided guards may still be overridden
52
- * per call. TOptions is the caller's own per-call options type; the
53
- * guardInputs requirement is layered on top of it.
54
- */
55
- export type LambderCallOptionsArg<TContract, TApiName, TProvided extends string, TOptions extends {
56
- guardInputs?: Record<string, unknown>;
57
- }> = IsAny<TContract> extends true ? [options?: TOptions] : TApiName extends keyof TContract ? [GuardInputsOf<TContract[TApiName]>] extends [never] ? [options?: TOptions] : [keyof RemainingGuardInputs<TContract[TApiName], TProvided>] extends [never] ? [options?: TOptions & {
58
- guardInputs?: Partial<GuardInputsOf<TContract[TApiName]>>;
59
- }] : [
60
- options: TOptions & {
61
- guardInputs: RemainingGuardInputs<TContract[TApiName], TProvided> & Partial<GuardInputsOf<TContract[TApiName]>>;
62
- }
63
- ] : [options?: TOptions];
64
- /**
65
- * Provider values underneath, per-call values on top; undefined when neither
66
- * side supplied any. Synchronous on purpose: a caller awaits its provider
67
- * only when it has one, so a call without a provider still issues its
68
- * request in the same tick it was made.
69
- */
70
- export declare const mergeGuardInputs: (provided: Record<string, unknown> | undefined, perCall: Record<string, unknown> | undefined) => Record<string, unknown> | undefined;
71
- export {};
@@ -1,16 +0,0 @@
1
- /**
2
- * The contract-driven typing of a call's options argument, and the runtime
3
- * merge of guard inputs, shared by the browser caller (LambderCaller) and the
4
- * server-side invoke caller (LambderInvokeCaller). Both speak the same
5
- * envelope to the same kind of contract, so what an API demands of its caller
6
- * (a guardInput-mode guard's value, say) is decided here once and the two
7
- * callers cannot drift on it. Pure types and one dependency-free function,
8
- * so the browser entry resolves it.
9
- */
10
- /**
11
- * Provider values underneath, per-call values on top; undefined when neither
12
- * side supplied any. Synchronous on purpose: a caller awaits its provider
13
- * only when it has one, so a call without a provider still issues its
14
- * request in the same tick it was made.
15
- */
16
- export const mergeGuardInputs = (provided, perCall) => provided !== undefined || perCall !== undefined ? { ...provided, ...perCall } : undefined;
@@ -1,4 +0,0 @@
1
- export declare function getFS(): Promise<typeof import('fs') | null>;
2
- export declare function getPath(): Promise<typeof import('path') | null>;
3
- export declare function getZlib(): Promise<typeof import('zlib') | null>;
4
- export declare function getCrypto(): Promise<typeof import('crypto') | null>;
@@ -1,58 +0,0 @@
1
- // Node.js polyfills for browser compatibility
2
- // This file provides optional Node.js modules that fail gracefully in browser environments
3
- let fs = null;
4
- let path = null;
5
- let zlib = null;
6
- let crypto = null;
7
- export async function getFS() {
8
- try {
9
- if (fs) {
10
- return fs;
11
- }
12
- fs = await import('fs');
13
- return fs;
14
- }
15
- catch (e) {
16
- // Silently fail - we're in a browser environment
17
- return null;
18
- }
19
- }
20
- export async function getPath() {
21
- try {
22
- if (path) {
23
- return path;
24
- }
25
- path = await import('path');
26
- return path;
27
- }
28
- catch (e) {
29
- // Silently fail - we're in a browser environment
30
- return null;
31
- }
32
- }
33
- export async function getZlib() {
34
- try {
35
- if (zlib) {
36
- return zlib;
37
- }
38
- zlib = await import('zlib');
39
- return zlib;
40
- }
41
- catch (e) {
42
- // Silently fail - we're in a browser environment
43
- return null;
44
- }
45
- }
46
- export async function getCrypto() {
47
- try {
48
- if (crypto) {
49
- return crypto;
50
- }
51
- crypto = await import('crypto');
52
- return crypto;
53
- }
54
- catch (e) {
55
- // Silently fail - we're in a browser environment
56
- return null;
57
- }
58
- }
@@ -1,229 +0,0 @@
1
- import crypto from "crypto";
2
- import { loadDynamoClientSdk } from "./LambderDdbSdk.js";
3
- import { compressText, restoreText } from "../shared/LambderCompressionCodec.js";
4
- import { resolveCompressionOption, } from "../shared/LambderCompressionOption.js";
5
- /** Bodies of 1KB or more are stored Brotli-compressed by default; smaller ones stay plain. */
6
- const COMPRESSION_DEFAULTS = { minBytes: 1024, quality: 5 };
7
- /**
8
- * Stored-body budget inside DynamoDB's 400KB item limit (headers, keys and
9
- * attributes need headroom). Applies to the bytes actually stored, so a
10
- * large compressible response (JSON usually shrinks 5-10x) still replays.
11
- */
12
- const MAX_STORED_BODY_BYTES = 350_000;
13
- /**
14
- * DynamoDB-backed idempotency records: one item per (identity, api, key)
15
- * scope, claimed atomically with a conditional put. The first request claims
16
- * the scope as "pending"; concurrent duplicates see "pending"; once the
17
- * response is stored via complete(), replays get it back verbatim until the
18
- * TTL. Records whose expiresAt has passed count as absent (DynamoDB TTL
19
- * deletion is lazy, so expiry is enforced in the condition, not left to TTL).
20
- *
21
- * Every claim carries a random ownerToken, and complete()/abandon() are
22
- * conditional on still holding it: an original that outlives its pending TTL
23
- * and loses the scope to a retry can no longer overwrite or delete the
24
- * retry's claim (both settle calls become silent no-ops instead).
25
- *
26
- * Stored bodies are Brotli-compressed from 1KB by default (same scheme as
27
- * LambderDdbCache, see the `compression` option): the bodies are JSON
28
- * envelopes that typically shrink 5-10x, which cuts DynamoDB write units
29
- * and lets large responses fit the item budget instead of skipping replay
30
- * storage.
31
- *
32
- * Table shape: string hash key `pk`, string range key `sk`, TTL on
33
- * `expiresAt`. Items are prefixed `IDEM#` by default, so the table can be
34
- * shared with LambderDdbRateLimiter (`RL#`) and LambderDdbCache (`CACHE#`)
35
- * without key collisions.
36
- */
37
- export class LambderDdbIdempotency {
38
- tableName;
39
- keyPrefix;
40
- compression;
41
- /** The client given at creation, or one created from `region` on first use; the SDK arrives with it. */
42
- providedClient;
43
- region;
44
- readyPromise;
45
- constructor(options) {
46
- if (!options.tableName.trim())
47
- throw new Error("tableName is required");
48
- this.tableName = options.tableName;
49
- this.keyPrefix = options.keyPrefix ?? "IDEM";
50
- this.compression = resolveCompressionOption(options.compression, COMPRESSION_DEFAULTS);
51
- this.providedClient = options.client;
52
- this.region = options.region;
53
- }
54
- /** The SDK and the client, loaded and created the first time the table is touched (see LambderDdbSdk). */
55
- ready() {
56
- this.readyPromise ??= loadDynamoClientSdk("LambderDdbIdempotency")
57
- .then((sdk) => ({ sdk, client: this.providedClient ?? new sdk.DynamoDBClient(this.region ? { region: this.region } : {}) }))
58
- .catch((error) => { this.readyPromise = undefined; throw error; });
59
- return this.readyPromise;
60
- }
61
- itemKey(scopeKey) {
62
- return { pk: { S: `${this.keyPrefix}#${scopeKey}` }, sk: { S: "idem" } };
63
- }
64
- /** Parse a stored item's response headers. */
65
- static readItemHeaders(item) {
66
- const raw = item.headersJson?.S;
67
- if (!raw)
68
- return {};
69
- try {
70
- const parsed = JSON.parse(raw);
71
- if (parsed && typeof parsed === "object")
72
- return parsed;
73
- }
74
- catch { /* corrupt record: replay with no headers rather than fail */ }
75
- return {};
76
- }
77
- /** A stored item's response body: plain (`body`) or Brotli (`bodyBr` + `bodyBytes`). */
78
- static async readItemBody(item) {
79
- const compressed = item.bodyBr?.B;
80
- if (compressed)
81
- return await restoreText(compressed, "br", { declaredBytes: Number(item.bodyBytes?.N ?? 0) });
82
- return item.body?.S ?? "";
83
- }
84
- /**
85
- * Read the scope without claiming it: the stored response when a
86
- * completed, unexpired record exists, null otherwise (absent, pending, or
87
- * expired). Eventually-consistent read: a miss here only means the caller
88
- * proceeds to begin(), whose read is authoritative.
89
- */
90
- async peek(scopeKey) {
91
- const { client, sdk } = await this.ready();
92
- const existing = await client.send(new sdk.GetItemCommand({
93
- TableName: this.tableName,
94
- Key: this.itemKey(scopeKey),
95
- }));
96
- const item = existing.Item;
97
- if (!item || item.state?.S !== "done")
98
- return null;
99
- const nowSeconds = Math.floor(Date.now() / 1000);
100
- if (Number(item.expiresAt?.N ?? 0) <= nowSeconds)
101
- return null;
102
- return {
103
- statusCode: Number(item.statusCode?.N ?? 200),
104
- headers: LambderDdbIdempotency.readItemHeaders(item),
105
- body: await LambderDdbIdempotency.readItemBody(item),
106
- };
107
- }
108
- /**
109
- * Claim the scope. "new" means this request now owns it (proven by the
110
- * returned ownerToken) and must call complete() or abandon(); "pending"
111
- * means another request owns it right now; "done" carries the stored
112
- * response to replay.
113
- */
114
- async begin(scopeKey, { pendingTtlSeconds }) {
115
- const nowSeconds = Math.floor(Date.now() / 1000);
116
- const ownerToken = crypto.randomBytes(16).toString("hex");
117
- try {
118
- const { client, sdk } = await this.ready();
119
- await client.send(new sdk.PutItemCommand({
120
- TableName: this.tableName,
121
- Item: {
122
- ...this.itemKey(scopeKey),
123
- state: { S: "pending" },
124
- ownerToken: { S: ownerToken },
125
- expiresAt: { N: String(nowSeconds + pendingTtlSeconds) },
126
- },
127
- ConditionExpression: "attribute_not_exists(pk) OR expiresAt <= :now",
128
- ExpressionAttributeValues: { ":now": { N: String(nowSeconds) } },
129
- }));
130
- return { state: "new", ownerToken };
131
- }
132
- catch (error) {
133
- if (error.name !== "ConditionalCheckFailedException")
134
- throw error;
135
- }
136
- const { client, sdk } = await this.ready();
137
- const existing = await client.send(new sdk.GetItemCommand({
138
- TableName: this.tableName,
139
- Key: this.itemKey(scopeKey),
140
- ConsistentRead: true,
141
- }));
142
- const item = existing.Item;
143
- // Deleted between the put and the read: treat as in-flight, the retry resolves it.
144
- if (!item)
145
- return { state: "pending" };
146
- if (item.state?.S === "done") {
147
- return {
148
- state: "done",
149
- statusCode: Number(item.statusCode?.N ?? 200),
150
- headers: LambderDdbIdempotency.readItemHeaders(item),
151
- body: await LambderDdbIdempotency.readItemBody(item),
152
- };
153
- }
154
- return { state: "pending" };
155
- }
156
- /**
157
- * Store the response for replays, overwriting the pending claim. Bodies
158
- * from the compression option's minBytes are stored Brotli-compressed
159
- * (they are JSON envelopes, which typically shrink 5-10x), cutting
160
- * DynamoDB write units and letting large responses fit the item budget;
161
- * smaller bodies, or all of them with compression off, stay plain.
162
- * Returns:
163
- *
164
- * - "stored": the record is in place and will replay.
165
- * - "too-large": even compressed, the body exceeds the item budget;
166
- * nothing was written and the caller should release the claim.
167
- * - "lost": the ownerToken no longer matches, i.e. the claim expired and
168
- * a retry took the scope over; nothing was written.
169
- */
170
- async complete(scopeKey, ownerToken, { statusCode, headers, body, ttlSeconds }) {
171
- const nowSeconds = Math.floor(Date.now() / 1000);
172
- const rawBody = Buffer.from(body, "utf8");
173
- let bodyAttributes;
174
- if (this.compression && rawBody.byteLength >= this.compression.minBytes) {
175
- const compressed = await compressText(rawBody, "br", this.compression.quality);
176
- if (compressed.byteLength > MAX_STORED_BODY_BYTES)
177
- return "too-large";
178
- // bodyBytes bounds and verifies decompression on read.
179
- bodyAttributes = { bodyBr: { B: compressed }, bodyBytes: { N: String(rawBody.byteLength) } };
180
- }
181
- else {
182
- bodyAttributes = { body: { S: body } };
183
- }
184
- try {
185
- const { client, sdk } = await this.ready();
186
- await client.send(new sdk.PutItemCommand({
187
- TableName: this.tableName,
188
- Item: {
189
- ...this.itemKey(scopeKey),
190
- state: { S: "done" },
191
- ownerToken: { S: ownerToken },
192
- statusCode: { N: String(statusCode) },
193
- headersJson: { S: JSON.stringify(headers) },
194
- ...bodyAttributes,
195
- expiresAt: { N: String(nowSeconds + ttlSeconds) },
196
- },
197
- ConditionExpression: "ownerToken = :owner",
198
- ExpressionAttributeValues: { ":owner": { S: ownerToken } },
199
- }));
200
- return "stored";
201
- }
202
- catch (error) {
203
- if (error.name !== "ConditionalCheckFailedException")
204
- throw error;
205
- return "lost";
206
- }
207
- }
208
- /**
209
- * Release the claim without storing a response (crash, uncacheable
210
- * response), so a retry can execute. Conditional on still holding the
211
- * claim; a lost claim makes this a silent no-op.
212
- */
213
- async abandon(scopeKey, ownerToken) {
214
- try {
215
- const { client, sdk } = await this.ready();
216
- await client.send(new sdk.DeleteItemCommand({
217
- TableName: this.tableName,
218
- Key: this.itemKey(scopeKey),
219
- ConditionExpression: "ownerToken = :owner",
220
- ExpressionAttributeValues: { ":owner": { S: ownerToken } },
221
- }));
222
- }
223
- catch (error) {
224
- if (error.name !== "ConditionalCheckFailedException")
225
- throw error;
226
- }
227
- }
228
- }
229
- export default LambderDdbIdempotency;
package/dist/testing.d.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Testing entry point (`import ... from "lambder/testing"`).
3
- *
4
- * Mock tooling that neither the server nor the production client bundle
5
- * should carry: the MSW adapter that serves an app's typed API contract
6
- * from in-browser mock handlers during development and tests.
7
- */
8
- export { default as LambderMSW } from "./client/LambderMSW.js";
9
- export type { LambderMswModule } from "./client/LambderMSW.js";
package/dist/testing.js DELETED
@@ -1,8 +0,0 @@
1
- /**
2
- * Testing entry point (`import ... from "lambder/testing"`).
3
- *
4
- * Mock tooling that neither the server nor the production client bundle
5
- * should carry: the MSW adapter that serves an app's typed API contract
6
- * from in-browser mock handlers during development and tests.
7
- */
8
- export { default as LambderMSW } from "./client/LambderMSW.js";
File without changes
File without changes