lambder 7.3.1 → 8.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1047 -3
- package/README.md +46 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +68 -62
- package/dist/api/LambderApiIdempotency.js +214 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +47 -38
- package/dist/api/LambderApiPipeline.js +122 -63
- package/dist/api/LambderApiRateLimits.d.ts +201 -54
- package/dist/api/LambderApiRateLimits.js +185 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/ContractTypePrinter.d.ts +85 -0
- package/dist/build/ContractTypePrinter.js +402 -0
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/moduleLocation.d.ts +11 -0
- package/dist/build/moduleLocation.js +6 -0
- package/dist/build/writeApiContract.d.ts +78 -0
- package/dist/build/writeApiContract.js +302 -0
- package/dist/build/writeApiSignatures.d.ts +114 -0
- package/dist/build/writeApiSignatures.js +217 -0
- package/dist/build/writeFileAtomically.d.ts +8 -0
- package/dist/build/writeFileAtomically.js +22 -0
- package/dist/build.d.ts +14 -0
- package/dist/build.js +11 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/LambderUploadRunner.d.ts +96 -0
- package/dist/client/LambderUploadRunner.js +234 -0
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +9 -3
- package/dist/client.js +6 -1
- package/dist/core/Lambder.d.ts +143 -79
- package/dist/core/Lambder.js +350 -231
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +21 -7
- package/dist/core/LambderFiles.js +62 -34
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +25 -8
- package/dist/index.js +13 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +29 -9
- package/dist/invoke/LambderLambdaEvent.js +40 -22
- package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
- package/dist/invoke/lambderHandlerTransport.js +15 -18
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +43 -33
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
- package/dist/mock/lambderMockUploadMswHandler.js +28 -0
- package/dist/mock.d.ts +4 -1
- package/dist/mock.js +6 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +124 -46
- package/dist/session/LambderSessionManager.js +262 -137
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
- package/dist/shared/contracts/LambderUploadBucket.js +74 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
- package/dist/shared/util/LambderContentDisposition.js +13 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTextDigest.d.ts +7 -5
- package/dist/shared/util/LambderTextDigest.js +11 -5
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +98 -53
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +45 -27
- package/dist/shared/wire/LambderApiRefusal.js +42 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
- package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
- package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
- package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
- package/dist/shared/wire/LambderUploadRefusal.js +18 -0
- package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
- package/dist/shared/wire/LambderUploadSchemas.js +30 -0
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +80 -38
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
- package/dist/stores/LambderMemoryUploadBucket.js +219 -0
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
- package/dist/stores/LambderS3UploadBucket.js +144 -0
- package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
- package/dist/stores/LambderSdkInstallHint.js +14 -0
- package/dist/testing/LambderTestApp.d.ts +23 -25
- package/dist/testing/LambderTestApp.js +22 -24
- package/dist/testing/LambderTestVisitor.d.ts +10 -12
- package/dist/testing/LambderTestVisitor.js +15 -15
- package/dist/testing.d.ts +3 -0
- package/dist/testing.js +2 -0
- package/package.json +26 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
- package/dist/api/LambderApiPolicyEngine.js +0 -85
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* call wrote so it can be applied onto whichever answer the call ends up
|
|
5
5
|
* with.
|
|
6
6
|
*
|
|
7
|
-
* One implementation
|
|
8
|
-
*
|
|
9
|
-
* functions,
|
|
7
|
+
* One implementation at the bottom of the stack, because every layer above
|
|
8
|
+
* needs the same behavior: LambderResponse's own header methods are these
|
|
9
|
+
* three functions, so a second copy cannot drift from them.
|
|
10
10
|
*/
|
|
11
11
|
/** The header's values under a case-insensitive lookup, or undefined. */
|
|
12
12
|
export declare const getAnswerHeader: (headers: Record<string, string[]>, name: string) => string[] | undefined;
|
|
@@ -25,18 +25,15 @@ export type LambderHeaderTarget = {
|
|
|
25
25
|
* the session controller's Set-Cookie), applied onto the answer once the
|
|
26
26
|
* call has one. Recorded as operations in call order rather than as a map,
|
|
27
27
|
* so `set` replaces what the answer itself carries (a Content-Type, say) and
|
|
28
|
-
* `add` appends to it, exactly as
|
|
29
|
-
* directly.
|
|
28
|
+
* `add` appends to it, exactly as if called on the answer directly.
|
|
30
29
|
*
|
|
31
30
|
* These headers belong to the CALL, not to the response that first carried
|
|
32
|
-
* them:
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* collapse to one; duplicate identical header values carry no meaning in
|
|
39
|
-
* HTTP, so nothing observable is lost.
|
|
31
|
+
* them: an afterRender hook may answer with a different response than the
|
|
32
|
+
* handler produced, and a session cookie written during the call must travel
|
|
33
|
+
* to it. So applying never forgets the operations, and applying them twice
|
|
34
|
+
* is a no-op (`add` skips a value the header already carries). The cost is
|
|
35
|
+
* that two identical `add` values under one name collapse to one, which HTTP
|
|
36
|
+
* gives no meaning to anyway.
|
|
40
37
|
*/
|
|
41
38
|
export declare class LambderAnswerHeaders {
|
|
42
39
|
private operations;
|
|
@@ -50,9 +47,8 @@ export declare class LambderAnswerHeaders {
|
|
|
50
47
|
get size(): number;
|
|
51
48
|
/**
|
|
52
49
|
* Applies the recorded operations, in order, onto anything that reads and
|
|
53
|
-
* writes headers: a LambderResponse, or a header map through applyInto
|
|
54
|
-
*
|
|
55
|
-
* drift apart.
|
|
50
|
+
* writes headers: a LambderResponse, or a header map through applyInto,
|
|
51
|
+
* so both targets share one definition of what an operation does.
|
|
56
52
|
*/
|
|
57
53
|
applyTo(target: LambderHeaderTarget, fromIndex?: number): void;
|
|
58
54
|
/** The same, onto an answer's plain header map. */
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* call wrote so it can be applied onto whichever answer the call ends up
|
|
5
5
|
* with.
|
|
6
6
|
*
|
|
7
|
-
* One implementation
|
|
8
|
-
*
|
|
9
|
-
* functions,
|
|
7
|
+
* One implementation at the bottom of the stack, because every layer above
|
|
8
|
+
* needs the same behavior: LambderResponse's own header methods are these
|
|
9
|
+
* three functions, so a second copy cannot drift from them.
|
|
10
10
|
*/
|
|
11
11
|
/** The header's values under a case-insensitive lookup, or undefined. */
|
|
12
12
|
export const getAnswerHeader = (headers, name) => {
|
|
@@ -40,18 +40,15 @@ export const addAnswerHeader = (headers, name, value) => {
|
|
|
40
40
|
* the session controller's Set-Cookie), applied onto the answer once the
|
|
41
41
|
* call has one. Recorded as operations in call order rather than as a map,
|
|
42
42
|
* so `set` replaces what the answer itself carries (a Content-Type, say) and
|
|
43
|
-
* `add` appends to it, exactly as
|
|
44
|
-
* directly.
|
|
43
|
+
* `add` appends to it, exactly as if called on the answer directly.
|
|
45
44
|
*
|
|
46
45
|
* These headers belong to the CALL, not to the response that first carried
|
|
47
|
-
* them:
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* collapse to one; duplicate identical header values carry no meaning in
|
|
54
|
-
* HTTP, so nothing observable is lost.
|
|
46
|
+
* them: an afterRender hook may answer with a different response than the
|
|
47
|
+
* handler produced, and a session cookie written during the call must travel
|
|
48
|
+
* to it. So applying never forgets the operations, and applying them twice
|
|
49
|
+
* is a no-op (`add` skips a value the header already carries). The cost is
|
|
50
|
+
* that two identical `add` values under one name collapse to one, which HTTP
|
|
51
|
+
* gives no meaning to anyway.
|
|
55
52
|
*/
|
|
56
53
|
export class LambderAnswerHeaders {
|
|
57
54
|
operations = [];
|
|
@@ -69,9 +66,8 @@ export class LambderAnswerHeaders {
|
|
|
69
66
|
get size() { return this.operations.length; }
|
|
70
67
|
/**
|
|
71
68
|
* Applies the recorded operations, in order, onto anything that reads and
|
|
72
|
-
* writes headers: a LambderResponse, or a header map through applyInto
|
|
73
|
-
*
|
|
74
|
-
* drift apart.
|
|
69
|
+
* writes headers: a LambderResponse, or a header map through applyInto,
|
|
70
|
+
* so both targets share one definition of what an operation does.
|
|
75
71
|
*/
|
|
76
72
|
applyTo(target, fromIndex = 0) {
|
|
77
73
|
for (const operation of this.operations.slice(fromIndex)) {
|
|
@@ -38,7 +38,14 @@ export type LambderApiResponseConfig = {
|
|
|
38
38
|
sessionExpired?: boolean;
|
|
39
39
|
notAuthorized?: boolean;
|
|
40
40
|
message?: any;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A refusal message, or a plain string when writing
|
|
43
|
+
* (`res.api(null, { errorMessage: "..." })`): the envelope goes out with
|
|
44
|
+
* the message object either way. Read off the wire it can still be a
|
|
45
|
+
* string, or no message at all, wherever a Lambder server did not write
|
|
46
|
+
* the body (a hand-built mock answer, a proxy); refusalMessageOf reads
|
|
47
|
+
* whatever arrives as a message.
|
|
48
|
+
*/
|
|
42
49
|
errorMessage?: LambderAppRefusalMessage | string;
|
|
43
50
|
logList?: any[];
|
|
44
51
|
/**
|
|
@@ -57,16 +64,76 @@ export type LambderApiResponseConfig = {
|
|
|
57
64
|
* indistinguishable from an endpoint that answered nothing on purpose.
|
|
58
65
|
*/
|
|
59
66
|
export type LambderApiNullAnswerConfig = LambderNonEmptyOptionMap<Pick<LambderApiResponseConfig, "versionExpired" | "sessionExpired" | "notAuthorized" | "errorMessage" | "message">> & LambderApiResponseConfig;
|
|
60
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* The API wire envelope both sides speak: res.api() emits it, LambderCaller
|
|
69
|
+
* parses it. `apiVersion` is always there (null when the server set none):
|
|
70
|
+
* it is how a reader tells a Lambder envelope from another JSON answer, such
|
|
71
|
+
* as API Gateway's own `{ "message": ... }` errors, so an answer without it
|
|
72
|
+
* reads as a server failure.
|
|
73
|
+
*/
|
|
61
74
|
export type LambderApiEnvelopeBody<T> = LambderApiResponseConfig & {
|
|
62
|
-
apiVersion
|
|
75
|
+
apiVersion: string | null;
|
|
63
76
|
payload?: T | null;
|
|
64
77
|
};
|
|
78
|
+
/** A value that is already JSON, recursive structures such as z.json() included. */
|
|
79
|
+
type LambderJsonValue = string | number | boolean | null | LambderJsonValue[] | {
|
|
80
|
+
[key: string]: LambderJsonValue;
|
|
81
|
+
};
|
|
82
|
+
/** An array item once it has been through JSON: what an object would drop, an array writes as null. */
|
|
83
|
+
type LambderJsonArrayItemOf<T> = T extends undefined | symbol | ((...args: any[]) => unknown) ? null : LambderJsonOf<T>;
|
|
84
|
+
/**
|
|
85
|
+
* The keys of object T that JSON may leave out: those whose value may be
|
|
86
|
+
* undefined, since JSON.stringify omits such a key. Distributed over K, the
|
|
87
|
+
* keys of T, one at a time. An index signature is never one of them: a
|
|
88
|
+
* record's undefined entries are left out, which its value type already
|
|
89
|
+
* says once undefined is dropped from it.
|
|
90
|
+
*/
|
|
91
|
+
type LambderJsonOmissibleKeys<T, K extends keyof T = keyof T> = K extends keyof T ? (string extends K ? never : number extends K ? never : undefined extends T[K] ? K : never) : never;
|
|
92
|
+
/**
|
|
93
|
+
* The type a value has once it has been through JSON: what an API's output
|
|
94
|
+
* reaches a client as. A Date becomes its string (through toJSON), a
|
|
95
|
+
* function, a symbol or an undefined member is dropped (written as null in
|
|
96
|
+
* an array), and a bigint, which JSON.stringify refuses, is never. A key
|
|
97
|
+
* whose value may be undefined is optional, since JSON leaves it out then,
|
|
98
|
+
* and a Map or a Set, whose entries are not properties, is written as an
|
|
99
|
+
* empty object. `unknown` stays unknown, and a type that is already JSON
|
|
100
|
+
* maps to itself, which is also what lets a recursive one such as z.json()
|
|
101
|
+
* resolve.
|
|
102
|
+
*
|
|
103
|
+
* An object is mapped in two steps. The omissible keys are made optional
|
|
104
|
+
* first, through the key types alone, and the mapping over the result then
|
|
105
|
+
* keeps each key's modifiers. Deciding optionality inside the mapping, per
|
|
106
|
+
* key, would need the mapped value of every key before the object's own
|
|
107
|
+
* keys were known, which a recursive type (a tree of its own nodes) cannot
|
|
108
|
+
* give without recursing for ever.
|
|
109
|
+
*/
|
|
110
|
+
export type LambderJsonOf<T> = unknown extends T ? T : T extends LambderJsonValue ? T : T extends {
|
|
111
|
+
toJSON(): infer TJson;
|
|
112
|
+
} ? LambderJsonOf<TJson> : T extends undefined | bigint | symbol | ((...args: any[]) => unknown) ? never : T extends readonly unknown[] ? {
|
|
113
|
+
[K in keyof T]: LambderJsonArrayItemOf<T[K]>;
|
|
114
|
+
} : T extends ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> ? {} : T extends object ? (Partial<Pick<T, LambderJsonOmissibleKeys<T>>> & Omit<T, LambderJsonOmissibleKeys<T>>) extends infer TKeyed ? {
|
|
115
|
+
[K in keyof TKeyed as K extends string | number ? (string extends K ? K : number extends K ? K : [LambderJsonOf<TKeyed[K]>] extends [never] ? never : K) : never]: LambderJsonOf<TKeyed[K]>;
|
|
116
|
+
} : never : never;
|
|
117
|
+
/**
|
|
118
|
+
* What an API's output reaches the client as: LambderJsonOf of the schema's
|
|
119
|
+
* output, except at the top, where an envelope carries no payload at all
|
|
120
|
+
* rather than a JSON `undefined`. A void or undefined output keeps its type,
|
|
121
|
+
* so a handler and a mock handler answer nothing, and an output that may be
|
|
122
|
+
* undefined keeps that member, which LambderJsonOf drops as it would a
|
|
123
|
+
* member of an object.
|
|
124
|
+
*/
|
|
125
|
+
export type LambderJsonOutputOf<T> = [T] extends [void] ? T : (undefined extends T ? undefined : never) | LambderJsonOf<T>;
|
|
65
126
|
/**
|
|
66
127
|
* One contract entry as addApi/addSessionApi record it: the payload types,
|
|
67
128
|
* the mode, and every declarative option exactly as written. Options that
|
|
68
129
|
* were not written are absent rather than undefined, so `keyof` an entry
|
|
69
130
|
* lists only what the endpoint declared.
|
|
131
|
+
*
|
|
132
|
+
* `In` is what a client sends (the input schema's z.input: a field with a
|
|
133
|
+
* default is optional, a transform's source type is what is posted) and `Out`
|
|
134
|
+
* what it receives (the output schema's z.output as JSON, see
|
|
135
|
+
* LambderJsonOf). The handler's own types are the other side of each, and
|
|
136
|
+
* are not recorded here.
|
|
70
137
|
*/
|
|
71
138
|
export type LambderContractEntry<In, Out, Mode extends LambderApiMode, GuardInputs = never, Guards = never, RateLimit = never, Idempotency = never> = {
|
|
72
139
|
input: In;
|
|
@@ -81,59 +148,19 @@ export type LambderContractEntry<In, Out, Mode extends LambderApiMode, GuardInpu
|
|
|
81
148
|
}) & ([Idempotency] extends [never] ? {} : {
|
|
82
149
|
idempotency: Idempotency;
|
|
83
150
|
});
|
|
84
|
-
/** Helper type for merging a new entry into the contract during chaining. */
|
|
85
|
-
export type LambderMergeContract<Old, Name extends string, Entry> = Old & {
|
|
86
|
-
[K in Name]: Entry;
|
|
87
|
-
};
|
|
88
151
|
/**
|
|
89
|
-
*
|
|
90
|
-
* app declares its contract through:
|
|
152
|
+
* Merges a new entry into the contract during chaining.
|
|
91
153
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* the atom the reading helpers below are built from, so its cost is paid
|
|
100
|
-
* again by each of them, per endpoint, in every app that registers a mock,
|
|
101
|
-
* declares a needs map, or otherwise reads the contract generically: in a
|
|
102
|
-
* 182-endpoint app one indexed access measured ~3,000 type instantiations
|
|
103
|
-
* and one mock registration ~18,000.
|
|
104
|
-
*
|
|
105
|
-
* Extending an interface is what collapses it. An interface's members are
|
|
106
|
-
* declared, so they are resolved once for the whole declaration rather than
|
|
107
|
-
* per lookup, and the same access measured ~6 instantiations after the
|
|
108
|
-
* change: a 182-endpoint app's frontend type check went from 27.8M
|
|
109
|
-
* instantiations to 7.0M and from 20.2s to 10.6s of check time. The alias
|
|
110
|
-
* form (`type C = LambderFlattenContract<...>`) does NOT do this: a mapped
|
|
111
|
-
* type stays deferred and each lookup pays the full cost again, so the
|
|
112
|
-
* `interface ... extends` spelling is the point.
|
|
113
|
-
*
|
|
114
|
-
* Diagnostics are the same ones, and they read better: a message naming the
|
|
115
|
-
* contract prints the interface by name, where the intersection is printed
|
|
116
|
-
* as a truncated spill of entries.
|
|
117
|
-
*
|
|
118
|
-
* Every endpoint name must be a string literal for an interface to extend
|
|
119
|
-
* the result, which registration through addApi/addSessionApi guarantees.
|
|
120
|
-
*
|
|
121
|
-
* Two things quietly undo it, both of which look like tidying:
|
|
122
|
-
*
|
|
123
|
-
* - `@typescript-eslint/no-empty-object-type` reports the empty body as
|
|
124
|
-
* "equivalent to its supertype" and its fix is a type alias, which is the
|
|
125
|
-
* one spelling that does not collapse anything. Disable the rule on the
|
|
126
|
-
* line rather than taking the fix.
|
|
127
|
-
* - Extending anything but a mapped type loses the inferable index signature.
|
|
128
|
-
* An interface has none of its own, so a hand-written `interface C { ... }`
|
|
129
|
-
* is not assignable to LambderApiContractShape and is rejected by
|
|
130
|
-
* initLambderMock<C>, LambderCaller<C> and LambderInvokeCaller<C>;
|
|
131
|
-
* extending this mapped type is what keeps it. api-contract.test.ts pins
|
|
132
|
-
* that, along with the flattened contract being the same type member for
|
|
133
|
-
* member.
|
|
154
|
+
* The contract is therefore an intersection one member deep per endpoint, and
|
|
155
|
+
* every `C[K]` read generically (a typed caller, a mock registry, a test
|
|
156
|
+
* visitor) resolves the property across all of them. That costs nothing
|
|
157
|
+
* worth measuring in a small app and most of a large client's type check,
|
|
158
|
+
* which is what writeApiContract (lambder/build) is for: it writes the
|
|
159
|
+
* contract out as one object type with plain members, for clients to import
|
|
160
|
+
* instead of the server.
|
|
134
161
|
*/
|
|
135
|
-
export type
|
|
136
|
-
[K in
|
|
162
|
+
export type LambderMergeContract<Old, Name extends string, Entry> = Old & {
|
|
163
|
+
[K in Name]: Entry;
|
|
137
164
|
};
|
|
138
165
|
/** Guard names referenced by a guards option, whichever of its three forms is used. */
|
|
139
166
|
export type LambderGuardNamesIn<TOpt> = TOpt extends string ? TOpt : TOpt extends readonly (infer N extends string)[] ? N : TOpt extends object ? keyof TOpt & string : never;
|
|
@@ -149,6 +176,23 @@ export type LambderContractKeysWithMode<C, M extends LambderApiMode> = {
|
|
|
149
176
|
export type LambderContractGuardsOf<C, K extends keyof C> = C[K] extends {
|
|
150
177
|
guards: infer G;
|
|
151
178
|
} ? G : never;
|
|
179
|
+
/**
|
|
180
|
+
* The endpoint names whose guards option names guard N, in any of its three
|
|
181
|
+
* forms and whatever else it declares beside it. What a test that calls
|
|
182
|
+
* every endpoint behind one guard loops over, and what a list meant to hold
|
|
183
|
+
* exactly those endpoints is checked against. `satisfies` refuses a name the
|
|
184
|
+
* guard does not cover; a missing name needs a check of its own:
|
|
185
|
+
*
|
|
186
|
+
* ```ts
|
|
187
|
+
* type AdminApi = LambderContractKeysWithGuard<Contract, "platformAdmin">;
|
|
188
|
+
* const ADMIN_APIS = ["admin.listUsers", "admin.deleteUser"] as const satisfies readonly AdminApi[];
|
|
189
|
+
* // Fails to compile while an endpoint behind the guard is left off the list.
|
|
190
|
+
* const adminApisComplete: [Exclude<AdminApi, (typeof ADMIN_APIS)[number]>] extends [never] ? true : false = true;
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export type LambderContractKeysWithGuard<C, N extends string> = {
|
|
194
|
+
[K in keyof C]: N extends LambderGuardNamesIn<LambderContractGuardsOf<C, K>> ? K : never;
|
|
195
|
+
}[keyof C] & string;
|
|
152
196
|
/** Every guard name any endpoint of the contract declares, or only the endpoints of mode M. */
|
|
153
197
|
export type LambderContractGuardNames<C, M extends LambderApiMode = LambderApiMode> = {
|
|
154
198
|
[K in LambderContractKeysWithMode<C, M>]: LambderGuardNamesIn<LambderContractGuardsOf<C, K>>;
|
|
@@ -181,3 +225,4 @@ export type LambderContractRateLimitNames<C, M extends LambderApiMode = LambderA
|
|
|
181
225
|
export type LambderContractIdempotencyOf<C, K extends keyof C> = C[K] extends {
|
|
182
226
|
idempotency: infer I;
|
|
183
227
|
} ? I : never;
|
|
228
|
+
export {};
|
|
@@ -5,14 +5,13 @@
|
|
|
5
5
|
* over a direct Lambda invoke) receive the same envelope and must read it
|
|
6
6
|
* the same way: which status is a crash, which is a rejected input, in what
|
|
7
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;
|
|
9
|
-
* side effects
|
|
10
|
-
*
|
|
11
|
-
* entry resolves it.
|
|
8
|
+
* Both hand their answer to resolveApiOutcome and act on the result; their
|
|
9
|
+
* side effects (handlers, cookie clearing, error reporting) stay with them.
|
|
10
|
+
* Pure and dependency-free, so the browser entry can include it.
|
|
12
11
|
*/
|
|
13
12
|
import type { z } from "zod";
|
|
14
13
|
import type { LambderApiEnvelopeBody } from "./LambderApiContract.js";
|
|
15
|
-
import type
|
|
14
|
+
import { type LambderAppRefusalMessage } from "./LambderApiRefusal.js";
|
|
16
15
|
/**
|
|
17
16
|
* The 422 body's `zodError` as it survives JSON: a ZodError's name and
|
|
18
17
|
* message, and its issues spelled out. Not a ZodError instance (it has no
|
|
@@ -37,18 +36,17 @@ type LambderApiFailureFields = {
|
|
|
37
36
|
ok: false;
|
|
38
37
|
/** HTTP status, when a response was received. */
|
|
39
38
|
status?: number;
|
|
40
|
-
/** Envelope errorMessage, when the server provided one. */
|
|
41
|
-
errorMessage?: LambderAppRefusalMessage
|
|
42
|
-
/** Seconds to wait before retrying, from the response's Retry-After header (rate-limit refusals send it). */
|
|
39
|
+
/** Envelope errorMessage, when the server provided one: always the message object, a plain string having been read as one (refusalMessageOf). */
|
|
40
|
+
errorMessage?: LambderAppRefusalMessage;
|
|
41
|
+
/** Seconds to wait before retrying, from the response's Retry-After header (rate-limit refusals send it, and so may a 503). */
|
|
43
42
|
retryAfterSeconds?: number;
|
|
44
43
|
/**
|
|
45
44
|
* The answer's logList, when it carried one: the envelope's on a success
|
|
46
45
|
* or an envelope refusal, the parsed 500 body's on a server failure, and
|
|
47
|
-
* the validation body's on a 422
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* printed the logs of the answer whose logs matter most, a 500.
|
|
46
|
+
* the validation body's on a 422. It is on every arm so a caller surfaces
|
|
47
|
+
* logs in ONE place, right after reading the answer, rather than per
|
|
48
|
+
* outcome, where early returns would skip the logs that matter most: a
|
|
49
|
+
* 500's.
|
|
52
50
|
*/
|
|
53
51
|
logList?: unknown[];
|
|
54
52
|
};
|
|
@@ -69,30 +67,32 @@ export type LambderApiValidationFailure = LambderApiFailureFields & {
|
|
|
69
67
|
reason: 'validation';
|
|
70
68
|
zodError: LambderValidationError;
|
|
71
69
|
};
|
|
72
|
-
/** The server answered, and the envelope itself says the call is refused. Always carries that envelope. */
|
|
70
|
+
/** The server answered, and the envelope itself says the call is refused. Always carries that envelope, and an `errorMessage` refusal always carries its message. */
|
|
73
71
|
export type LambderApiEnvelopeFailure<T> = LambderApiFailureFields & {
|
|
74
|
-
reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage';
|
|
75
72
|
response: LambderApiEnvelopeBody<T>;
|
|
76
|
-
}
|
|
73
|
+
} & ({
|
|
74
|
+
reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized';
|
|
75
|
+
} | {
|
|
76
|
+
reason: 'errorMessage';
|
|
77
|
+
errorMessage: LambderAppRefusalMessage;
|
|
78
|
+
});
|
|
77
79
|
/**
|
|
78
80
|
* Discriminated result of an API call: `ok: true` carries the payload, every
|
|
79
81
|
* failure carries a machine-readable reason, so "the server returned null"
|
|
80
82
|
* and "the request failed" are never conflated.
|
|
81
83
|
*
|
|
82
|
-
* The failure side is discriminated by `reason
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* framework's own reader needed three non-null assertions to say what the
|
|
87
|
-
* union already knew.
|
|
84
|
+
* The failure side is discriminated by `reason`, so narrowing to a reason
|
|
85
|
+
* narrows to what it carries: `zodError` after `reason === 'validation'`,
|
|
86
|
+
* `response` after an envelope reason, `error` after the rest, with no
|
|
87
|
+
* non-null assertion needed.
|
|
88
88
|
*/
|
|
89
89
|
export type LambderApiOutcome<T> = LambderApiSuccessOutcome<T> | LambderApiCallFailure<T> | LambderApiValidationFailure | LambderApiEnvelopeFailure<T>;
|
|
90
90
|
/**
|
|
91
|
-
* What reading one HTTP answer can produce
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
91
|
+
* What reading one HTTP answer can produce: LambderApiOutcome minus the
|
|
92
|
+
* three reasons no answer carries (`network` and `timeout` are the caller's
|
|
93
|
+
* own abort, `unknown` is something throwing around the call). A caller that
|
|
94
|
+
* has handled `server` and `validation` holds a success or an envelope
|
|
95
|
+
* refusal, both of which carry the envelope.
|
|
96
96
|
*/
|
|
97
97
|
export type LambderApiAnswerOutcome<T> = LambderApiSuccessOutcome<T> | (LambderApiCallFailure<T> & {
|
|
98
98
|
reason: 'server';
|
|
@@ -100,10 +100,9 @@ export type LambderApiAnswerOutcome<T> = LambderApiSuccessOutcome<T> | (LambderA
|
|
|
100
100
|
/**
|
|
101
101
|
* What the mapping needs from an HTTP answer, whichever transport produced it.
|
|
102
102
|
*
|
|
103
|
-
* Exactly one of `json()` and `text()` is read per answer,
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* that a non-envelope body is still reportable).
|
|
103
|
+
* Exactly one of `json()` and `text()` is read per answer, since a real
|
|
104
|
+
* Response body may only be read once (a 5xx reads text and parses it
|
|
105
|
+
* itself, so a non-envelope body is still reportable).
|
|
107
106
|
*/
|
|
108
107
|
export type LambderApiHttpAnswer = {
|
|
109
108
|
status: number;
|
|
@@ -116,6 +115,17 @@ export type LambderApiHttpAnswer = {
|
|
|
116
115
|
text: () => Promise<string>;
|
|
117
116
|
/** The answer's Set-Cookie header values, for a transport that can see them (a cookie jar consumes them); absent in a browser. */
|
|
118
117
|
setCookies?: string[];
|
|
118
|
+
/**
|
|
119
|
+
* The CSRF tokens of a transport that keeps the session's cookies itself
|
|
120
|
+
* (a cookie jar), where document.cookie is not where they live: the one
|
|
121
|
+
* it posted, and a read of the one it holds now. LambderCaller judges
|
|
122
|
+
* whether a sessionExpired is about the session the page still holds by
|
|
123
|
+
* these; absent, it compares document.cookie before and after the call.
|
|
124
|
+
*/
|
|
125
|
+
csrfTokens?: {
|
|
126
|
+
posted: string;
|
|
127
|
+
held: () => string;
|
|
128
|
+
};
|
|
119
129
|
};
|
|
120
130
|
/**
|
|
121
131
|
* Reads one HTTP answer into an outcome. A 5xx is a server failure that keeps
|
|
@@ -123,6 +133,8 @@ export type LambderApiHttpAnswer = {
|
|
|
123
133
|
* errorMessage, and a global error handler may add crash and logList); a
|
|
124
134
|
* 422 is a validation failure only with Lambder's validation body; anything
|
|
125
135
|
* else must be a JSON envelope, whose flags are honoured in a fixed order.
|
|
136
|
+
* A failure read off any answer but a 422 carries the answer's Retry-After
|
|
137
|
+
* as retryAfterSeconds.
|
|
126
138
|
*/
|
|
127
139
|
export declare const resolveApiOutcome: <T>(answer: LambderApiHttpAnswer) => Promise<LambderApiAnswerOutcome<T>>;
|
|
128
140
|
export {};
|
|
@@ -5,47 +5,65 @@
|
|
|
5
5
|
* over a direct Lambda invoke) receive the same envelope and must read it
|
|
6
6
|
* the same way: which status is a crash, which is a rejected input, in what
|
|
7
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;
|
|
9
|
-
* side effects
|
|
10
|
-
*
|
|
11
|
-
* entry resolves it.
|
|
8
|
+
* Both hand their answer to resolveApiOutcome and act on the result; their
|
|
9
|
+
* side effects (handlers, cookie clearing, error reporting) stay with them.
|
|
10
|
+
* Pure and dependency-free, so the browser entry can include it.
|
|
12
11
|
*/
|
|
12
|
+
import { refusalMessageOf } from "./LambderApiRefusal.js";
|
|
13
|
+
/**
|
|
14
|
+
* Whether a parsed body is Lambder's envelope, which always carries
|
|
15
|
+
* apiVersion (null when the server set none). An object without it is
|
|
16
|
+
* somebody else's answer: API Gateway's own errors ({"message": ...} on a
|
|
17
|
+
* 413, a throttle, a WAF or missing-route 403, an authorizer 401, a 502 from
|
|
18
|
+
* a crashed function), a proxy's, a load balancer's.
|
|
19
|
+
*/
|
|
20
|
+
const isApiEnvelope = (value) => value !== null && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "apiVersion");
|
|
13
21
|
/**
|
|
14
22
|
* Reads one HTTP answer into an outcome. A 5xx is a server failure that keeps
|
|
15
23
|
* the envelope when the server sent one (Lambder's own 500 body carries
|
|
16
24
|
* errorMessage, and a global error handler may add crash and logList); a
|
|
17
25
|
* 422 is a validation failure only with Lambder's validation body; anything
|
|
18
26
|
* else must be a JSON envelope, whose flags are honoured in a fixed order.
|
|
27
|
+
* A failure read off any answer but a 422 carries the answer's Retry-After
|
|
28
|
+
* as retryAfterSeconds.
|
|
19
29
|
*/
|
|
20
30
|
export const resolveApiOutcome = async (answer) => {
|
|
21
31
|
const status = answer.status;
|
|
32
|
+
const errorMessageOf = (envelope) => envelope?.errorMessage !== undefined ? { errorMessage: refusalMessageOf(envelope.errorMessage) } : {};
|
|
33
|
+
// Retry-After (delta-seconds) rides every refusal that knows its reset
|
|
34
|
+
// time, e.g. a rate limit, and a 503 that says when to come back; absent
|
|
35
|
+
// or unreadable is undefined.
|
|
36
|
+
const retryAfterValue = Number(answer.header("retry-after") ?? NaN);
|
|
37
|
+
const retryAfter = Number.isFinite(retryAfterValue) && retryAfterValue >= 0 ? { retryAfterSeconds: retryAfterValue } : {};
|
|
22
38
|
if (status >= 500) {
|
|
23
39
|
// Lambder's own 500 fallback is a JSON envelope, but custom error
|
|
24
|
-
// handlers may answer text
|
|
40
|
+
// handlers may answer text or HTML, and a gateway in front answers
|
|
41
|
+
// JSON of its own: parse defensively, and keep only the envelope, so
|
|
42
|
+
// a foreign body's fields never read as the app's message or crash.
|
|
25
43
|
let envelope;
|
|
26
44
|
try {
|
|
27
45
|
const bodyText = await answer.text();
|
|
28
46
|
try {
|
|
29
47
|
const parsed = JSON.parse(bodyText);
|
|
30
|
-
if (parsed
|
|
48
|
+
if (isApiEnvelope(parsed))
|
|
31
49
|
envelope = parsed;
|
|
32
50
|
}
|
|
33
|
-
catch { /* not
|
|
51
|
+
catch { /* not JSON */ }
|
|
34
52
|
}
|
|
35
53
|
catch { /* body unavailable */ }
|
|
36
54
|
return {
|
|
37
55
|
ok: false, reason: 'server', status,
|
|
38
|
-
|
|
56
|
+
...errorMessageOf(envelope),
|
|
39
57
|
logList: envelope?.logList,
|
|
40
58
|
...(envelope ? { response: envelope } : {}),
|
|
41
59
|
error: new Error("Request failed: " + status + " - " + (answer.statusText ?? "")),
|
|
60
|
+
...retryAfter,
|
|
42
61
|
};
|
|
43
62
|
}
|
|
44
63
|
if (status === 422) {
|
|
45
64
|
// A 422 without Lambder's validation body (e.g. a proxy's error page)
|
|
46
|
-
// is a server failure, not a validation result.
|
|
47
|
-
//
|
|
48
|
-
// answer does, so it is read here rather than left on the wire.
|
|
65
|
+
// is a server failure, not a validation result. The validation body
|
|
66
|
+
// carries the call's logList like every other answer, so it is read.
|
|
49
67
|
let body;
|
|
50
68
|
try {
|
|
51
69
|
body = await answer.json();
|
|
@@ -57,10 +75,6 @@ export const resolveApiOutcome = async (answer) => {
|
|
|
57
75
|
}
|
|
58
76
|
return { ok: false, reason: 'validation', status, zodError, logList: body?.logList };
|
|
59
77
|
}
|
|
60
|
-
// Retry-After (delta-seconds) rides every refusal that knows its reset
|
|
61
|
-
// time, e.g. a rate limit; absent or unreadable is undefined.
|
|
62
|
-
const retryAfterValue = Number(answer.header("retry-after") ?? NaN);
|
|
63
|
-
const retryAfter = Number.isFinite(retryAfterValue) && retryAfterValue >= 0 ? { retryAfterSeconds: retryAfterValue } : {};
|
|
64
78
|
let data;
|
|
65
79
|
try {
|
|
66
80
|
data = await answer.json();
|
|
@@ -69,18 +83,29 @@ export const resolveApiOutcome = async (answer) => {
|
|
|
69
83
|
}
|
|
70
84
|
catch (err) {
|
|
71
85
|
// A non-envelope body (e.g. an HTML error page) is a server failure.
|
|
72
|
-
return { ok: false, reason: 'server', status, error: new Error("Request failed: response is not a valid API envelope (status " + status + ")", { cause: err }) };
|
|
86
|
+
return { ok: false, reason: 'server', status, error: new Error("Request failed: response is not a valid API envelope (status " + status + ")", { cause: err }), ...retryAfter };
|
|
87
|
+
}
|
|
88
|
+
// Read as envelopes, a gateway's own JSON errors would resolve as
|
|
89
|
+
// successes with no payload, so a refused save would look saved.
|
|
90
|
+
if (!isApiEnvelope(data)) {
|
|
91
|
+
const gatewayMessage = typeof data.message === "string" ? `: ${data.message}` : "";
|
|
92
|
+
return { ok: false, reason: 'server', status, error: new Error(`Request failed: ${status} - the answer is not a Lambder envelope${gatewayMessage}`), ...retryAfter };
|
|
73
93
|
}
|
|
74
94
|
if (data.versionExpired)
|
|
75
|
-
return { ok: false, reason: 'versionExpired', status,
|
|
95
|
+
return { ok: false, reason: 'versionExpired', status, ...errorMessageOf(data), response: data, logList: data.logList, ...retryAfter };
|
|
76
96
|
if (data.sessionExpired)
|
|
77
|
-
return { ok: false, reason: 'sessionExpired', status,
|
|
97
|
+
return { ok: false, reason: 'sessionExpired', status, ...errorMessageOf(data), response: data, logList: data.logList, ...retryAfter };
|
|
78
98
|
if (data.notAuthorized)
|
|
79
|
-
return { ok: false, reason: 'notAuthorized', status,
|
|
80
|
-
// Presence, not truthiness: the writer keeps an errorMessage an app
|
|
81
|
-
//
|
|
82
|
-
// refusal
|
|
99
|
+
return { ok: false, reason: 'notAuthorized', status, ...errorMessageOf(data), response: data, logList: data.logList, ...retryAfter };
|
|
100
|
+
// Presence, not truthiness: the writer keeps an errorMessage an app set
|
|
101
|
+
// to the empty string, and a refusal that says nothing is still a
|
|
102
|
+
// refusal, not a success.
|
|
83
103
|
if (data.errorMessage !== undefined)
|
|
84
|
-
return { ok: false, reason: 'errorMessage', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
|
|
104
|
+
return { ok: false, reason: 'errorMessage', status, errorMessage: refusalMessageOf(data.errorMessage), response: data, logList: data.logList, ...retryAfter };
|
|
105
|
+
// An envelope that says nothing is wrong is still not a success when the
|
|
106
|
+
// status says otherwise.
|
|
107
|
+
if (status < 200 || status >= 300) {
|
|
108
|
+
return { ok: false, reason: 'server', status, response: data, logList: data.logList, error: new Error(`Request failed: ${status} - ${answer.statusText ?? ""}`), ...retryAfter };
|
|
109
|
+
}
|
|
85
110
|
return { ok: true, payload: data.payload, response: data, logList: data.logList };
|
|
86
111
|
};
|