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.
- package/CHANGELOG.md +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +21 -19
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /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
|
|
File without changes
|