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
|
@@ -12,17 +12,17 @@ import { LambderTestVisitor } from "./LambderTestVisitor.js";
|
|
|
12
12
|
* stores put under it in place, and as many simulated browsers in front of it
|
|
13
13
|
* as a test needs. No HTTP, no AWS, and nothing in the app restructured.
|
|
14
14
|
*
|
|
15
|
-
* The app's own declarations all run as written:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* The app's own declarations all run as written: guards, named rate-limit
|
|
16
|
+
* policies, idempotency settings, session salt, cookie options and
|
|
17
|
+
* dataRefresh, hooks and error handlers. Only where things rest is replaced:
|
|
18
|
+
* once this is created, the stores the app was configured with are out of the
|
|
19
|
+
* instance's reach, so a test cannot touch a production table by mistake.
|
|
20
|
+
* What the app reaches on its own (its database, a mailer) is the app's to
|
|
21
|
+
* replace.
|
|
22
22
|
*
|
|
23
23
|
* The sibling of LambderMockApp, which serves a contract from mock handlers:
|
|
24
24
|
* the same verbs (`signIn`, `signOut`, `expireSessionData`, `reset`) over the
|
|
25
|
-
* real handlers
|
|
25
|
+
* real handlers.
|
|
26
26
|
*
|
|
27
27
|
* Time is not this class's: fake `Date` with the test runner
|
|
28
28
|
* (`vi.useFakeTimers({ toFake: ["Date"] })`), which moves the framework, the
|
|
@@ -50,16 +50,15 @@ export class LambderTestApp {
|
|
|
50
50
|
resetCount = 0;
|
|
51
51
|
crashList = [];
|
|
52
52
|
/**
|
|
53
|
-
* The call a crash happened under. The app
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* async context says which call a crash belongs to.
|
|
53
|
+
* The call a crash happened under. The app's 500 says nothing about the
|
|
54
|
+
* crash, so the error travels beside the answer, and with concurrent
|
|
55
|
+
* calls (a duplicate sent while the original is in flight is an ordinary
|
|
56
|
+
* idempotency test) only the async context says which call it belongs to.
|
|
58
57
|
*/
|
|
59
58
|
crashScope = new AsyncLocalStorage();
|
|
60
59
|
constructor(lambder, options = {}) {
|
|
61
|
-
// Only a Lambder instance
|
|
62
|
-
// key
|
|
60
|
+
// Only a Lambder instance from this same package copy answers to the
|
|
61
|
+
// key; saying so here beats a bare "is not a function".
|
|
63
62
|
if (typeof lambder?.[LAMBDER_BACKEND_SWAP] !== "function") {
|
|
64
63
|
throw new Error("lambderTestApp: expected a Lambder instance (what initLambder().create() returns), from the same installed copy of lambder as lambder/testing.");
|
|
65
64
|
}
|
|
@@ -94,11 +93,11 @@ export class LambderTestApp {
|
|
|
94
93
|
return { result: await this.crashScope.run(scope, run), crash: scope.crash };
|
|
95
94
|
},
|
|
96
95
|
issueSession: async (host, sessionKey, data, ttlSeconds) => {
|
|
97
|
-
// Through a session controller on a request context,
|
|
98
|
-
// login handler does
|
|
99
|
-
//
|
|
96
|
+
// Through a session controller on a request context, as a
|
|
97
|
+
// login handler does, so the cookies are what the app's own
|
|
98
|
+
// cookie options produce for that host.
|
|
100
99
|
const event = synthesizeLambdaHttpEvent({ method: "GET", path: "/", host }, { invoke: false });
|
|
101
|
-
const ctx = createContext(event, localLambdaContext("lambder-test"), lambder.apiPath);
|
|
100
|
+
const ctx = createContext(event, localLambdaContext("lambder-test"), { apiPath: lambder.apiPath });
|
|
102
101
|
const created = await lambder.getSessionController(ctx).issueSession(sessionKey, data, ttlSeconds);
|
|
103
102
|
const headers = {};
|
|
104
103
|
ctx.responseHeaders.applyInto(headers);
|
|
@@ -109,11 +108,10 @@ export class LambderTestApp {
|
|
|
109
108
|
/**
|
|
110
109
|
* Every error the app threw while answering a request since the last
|
|
111
110
|
* reset, in order: what reached its global error handler, or the
|
|
112
|
-
* framework's last-resort 500. The answers
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* with, which the test already holds.
|
|
111
|
+
* framework's last-resort 500. The answers say nothing about what was
|
|
112
|
+
* thrown, so a test reads it here; `expect(app.crashes).toEqual([])`
|
|
113
|
+
* says nothing crashed. A refusal is not a crash, and neither is an
|
|
114
|
+
* error an `event()` rejects with, which the test already holds.
|
|
117
115
|
*/
|
|
118
116
|
get crashes() {
|
|
119
117
|
return this.crashList;
|
|
@@ -100,10 +100,9 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
100
100
|
* The full outcome, never throwing: LambderCaller.apiOutcome, through
|
|
101
101
|
* this visitor. Pair it with assertApiSuccess / assertApiFailure.
|
|
102
102
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* here that error's `cause` is what the app actually threw, stack
|
|
103
|
+
* When the app crashed answering the call, the outcome is the `server`
|
|
104
|
+
* failure any client would get, whose error says only "Request failed:
|
|
105
|
+
* 500"; here that error's `cause` is what the app actually threw, stack
|
|
107
106
|
* included, so a failing test points at the line in the handler.
|
|
108
107
|
*/
|
|
109
108
|
readonly apiOutcome: LambderCaller<TContract, TProvidedGuards>["apiOutcome"];
|
|
@@ -117,10 +116,9 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
117
116
|
});
|
|
118
117
|
/**
|
|
119
118
|
* This visitor's cookies, to inspect or clear; every call and request
|
|
120
|
-
* reads and fills them.
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* in until an answer said otherwise.
|
|
119
|
+
* reads and fills them. The test app's reset() empties them, noticed here
|
|
120
|
+
* the next time anything asks: a jar still holding the token of an
|
|
121
|
+
* emptied store would read as signed in until an answer said otherwise.
|
|
124
122
|
*/
|
|
125
123
|
get jar(): LambderCookieJar;
|
|
126
124
|
/**
|
|
@@ -144,10 +142,10 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
144
142
|
* tokens too, as LambderMockApp.signIn does.
|
|
145
143
|
*
|
|
146
144
|
* Throws when the cookies do not stick. An app that scopes its session
|
|
147
|
-
* cookie to a domain (`cookie: { domain: ".example.com" }`)
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* sessionExpired with nothing to say why.
|
|
145
|
+
* cookie to a domain (`cookie: { domain: ".example.com" }`) this visitor's
|
|
146
|
+
* host is not under writes a cookie a browser on that host would drop, and
|
|
147
|
+
* the jar drops it too; left silent, every later session call would
|
|
148
|
+
* answer sessionExpired with nothing to say why.
|
|
151
149
|
*/
|
|
152
150
|
signIn(sessionKey: string, data: TSessionData, options?: {
|
|
153
151
|
ttlSeconds?: number;
|
|
@@ -31,10 +31,9 @@ export class LambderTestVisitor {
|
|
|
31
31
|
* The full outcome, never throwing: LambderCaller.apiOutcome, through
|
|
32
32
|
* this visitor. Pair it with assertApiSuccess / assertApiFailure.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* here that error's `cause` is what the app actually threw, stack
|
|
34
|
+
* When the app crashed answering the call, the outcome is the `server`
|
|
35
|
+
* failure any client would get, whose error says only "Request failed:
|
|
36
|
+
* 500"; here that error's `cause` is what the app actually threw, stack
|
|
38
37
|
* included, so a failing test points at the line in the handler.
|
|
39
38
|
*/
|
|
40
39
|
apiOutcome;
|
|
@@ -53,17 +52,19 @@ export class LambderTestVisitor {
|
|
|
53
52
|
const withVisitorHeaders = (request) => handlerTransport({ ...request, headers: { ...this.headers, ...request.headers } });
|
|
54
53
|
this.caller = new LambderCaller({
|
|
55
54
|
apiPath: wiring.apiPath,
|
|
56
|
-
isCorsEnabled: false,
|
|
57
55
|
apiVersion: options.apiVersion,
|
|
58
56
|
apiSignatures: options.apiSignatures,
|
|
59
57
|
guardInputsProvider: options.guardInputsProvider,
|
|
60
58
|
logListHandler: () => { },
|
|
61
59
|
// The jar is read per call rather than captured, so a call made
|
|
62
60
|
// after the test app was reset carries none of the old cookies.
|
|
63
|
-
|
|
61
|
+
// The visitor's session lives in that jar, so a CSRF token the
|
|
62
|
+
// caller read from a page's document.cookie (a test with a DOM)
|
|
63
|
+
// is dropped and the jar's own is posted.
|
|
64
|
+
transport: (request) => lambderCookieJarTransport(withVisitorHeaders, { jar: this.jar, host: this.host })({ ...request, token: "" }),
|
|
64
65
|
});
|
|
65
66
|
// The caller names the CSRF cookie the jar transport fills its token
|
|
66
|
-
// from, so an app
|
|
67
|
+
// from, so an app with custom session cookie names has to be matched
|
|
67
68
|
// here or every session call would post an empty token.
|
|
68
69
|
if (wiring.sessionCookieNames) {
|
|
69
70
|
this.caller.setSessionCookieKey(wiring.sessionCookieNames.tokenCookieKey, wiring.sessionCookieNames.csrfCookieKey);
|
|
@@ -78,10 +79,9 @@ export class LambderTestVisitor {
|
|
|
78
79
|
}
|
|
79
80
|
/**
|
|
80
81
|
* This visitor's cookies, to inspect or clear; every call and request
|
|
81
|
-
* reads and fills them.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* in until an answer said otherwise.
|
|
82
|
+
* reads and fills them. The test app's reset() empties them, noticed here
|
|
83
|
+
* the next time anything asks: a jar still holding the token of an
|
|
84
|
+
* emptied store would read as signed in until an answer said otherwise.
|
|
85
85
|
*/
|
|
86
86
|
get jar() {
|
|
87
87
|
const resetCount = this.wiring.resetCount();
|
|
@@ -134,10 +134,10 @@ export class LambderTestVisitor {
|
|
|
134
134
|
* tokens too, as LambderMockApp.signIn does.
|
|
135
135
|
*
|
|
136
136
|
* Throws when the cookies do not stick. An app that scopes its session
|
|
137
|
-
* cookie to a domain (`cookie: { domain: ".example.com" }`)
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* sessionExpired with nothing to say why.
|
|
137
|
+
* cookie to a domain (`cookie: { domain: ".example.com" }`) this visitor's
|
|
138
|
+
* host is not under writes a cookie a browser on that host would drop, and
|
|
139
|
+
* the jar drops it too; left silent, every later session call would
|
|
140
|
+
* answer sessionExpired with nothing to say why.
|
|
141
141
|
*/
|
|
142
142
|
async signIn(sessionKey, data, options = {}) {
|
|
143
143
|
if (!this.wiring.sessionCookieNames)
|
package/dist/testing.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export type { LambderExpectedFailure } from "./shared/wire/LambderOutcomeAsserti
|
|
|
18
18
|
export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
|
|
19
19
|
export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
|
|
20
20
|
export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
|
|
21
|
+
export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
|
|
22
|
+
export { LambderMemoryUploadBucket } from "./stores/LambderMemoryUploadBucket.js";
|
|
23
|
+
export type { LambderMemoryUploadBucketOptions, LambderMemoryUploadObject } from "./stores/LambderMemoryUploadBucket.js";
|
|
21
24
|
export { LambderLocalFileSource } from "./stores/LambderLocalFileSource.js";
|
|
22
25
|
export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
|
|
23
26
|
export { LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
package/dist/testing.js
CHANGED
|
@@ -18,6 +18,8 @@ export { assertApiSuccess, assertApiFailure } from "./shared/wire/LambderOutcome
|
|
|
18
18
|
export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
|
|
19
19
|
export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
|
|
20
20
|
export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
|
|
21
|
+
export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
|
|
22
|
+
export { LambderMemoryUploadBucket } from "./stores/LambderMemoryUploadBucket.js";
|
|
21
23
|
export { LambderLocalFileSource } from "./stores/LambderLocalFileSource.js";
|
|
22
24
|
export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
|
|
23
25
|
export { LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.1.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
"types": "./dist/testing.d.ts",
|
|
54
54
|
"default": "./dist/testing.js"
|
|
55
55
|
},
|
|
56
|
+
"./build": {
|
|
57
|
+
"types": "./dist/build.d.ts",
|
|
58
|
+
"default": "./dist/build.js"
|
|
59
|
+
},
|
|
56
60
|
"./package.json": "./package.json"
|
|
57
61
|
},
|
|
58
62
|
"typesVersions": {
|
|
@@ -65,15 +69,20 @@
|
|
|
65
69
|
],
|
|
66
70
|
"testing": [
|
|
67
71
|
"./dist/testing.d.ts"
|
|
72
|
+
],
|
|
73
|
+
"build": [
|
|
74
|
+
"./dist/build.d.ts"
|
|
68
75
|
]
|
|
69
76
|
}
|
|
70
77
|
},
|
|
71
78
|
"browser": {
|
|
72
79
|
"async_hooks": false,
|
|
80
|
+
"child_process": false,
|
|
73
81
|
"fs": false,
|
|
74
82
|
"path": false,
|
|
75
83
|
"zlib": false,
|
|
76
|
-
"crypto": false
|
|
84
|
+
"crypto": false,
|
|
85
|
+
"url": false
|
|
77
86
|
},
|
|
78
87
|
"files": [
|
|
79
88
|
"dist",
|
|
@@ -101,11 +110,14 @@
|
|
|
101
110
|
"tough-cookie": "^6.0.2"
|
|
102
111
|
},
|
|
103
112
|
"peerDependencies": {
|
|
104
|
-
"@aws-sdk/client-dynamodb": "^3.
|
|
113
|
+
"@aws-sdk/client-dynamodb": "^3.868.0",
|
|
105
114
|
"@aws-sdk/client-lambda": "^3.574.0",
|
|
106
115
|
"@aws-sdk/client-s3": "^3.574.0",
|
|
107
116
|
"@aws-sdk/lib-dynamodb": "^3.574.0",
|
|
117
|
+
"@aws-sdk/s3-presigned-post": "^3.574.0",
|
|
118
|
+
"@aws-sdk/s3-request-presigner": "^3.574.0",
|
|
108
119
|
"msw": "^2.0.0",
|
|
120
|
+
"typescript": ">=5.4",
|
|
109
121
|
"zod": "^4.6.2"
|
|
110
122
|
},
|
|
111
123
|
"peerDependenciesMeta": {
|
|
@@ -121,8 +133,17 @@
|
|
|
121
133
|
"@aws-sdk/lib-dynamodb": {
|
|
122
134
|
"optional": true
|
|
123
135
|
},
|
|
136
|
+
"@aws-sdk/s3-presigned-post": {
|
|
137
|
+
"optional": true
|
|
138
|
+
},
|
|
139
|
+
"@aws-sdk/s3-request-presigner": {
|
|
140
|
+
"optional": true
|
|
141
|
+
},
|
|
124
142
|
"msw": {
|
|
125
143
|
"optional": true
|
|
144
|
+
},
|
|
145
|
+
"typescript": {
|
|
146
|
+
"optional": true
|
|
126
147
|
}
|
|
127
148
|
},
|
|
128
149
|
"devDependencies": {
|
|
@@ -130,6 +151,8 @@
|
|
|
130
151
|
"@aws-sdk/client-lambda": "^3.1131.0",
|
|
131
152
|
"@aws-sdk/client-s3": "^3.1127.0",
|
|
132
153
|
"@aws-sdk/lib-dynamodb": "^3.913.0",
|
|
154
|
+
"@aws-sdk/s3-presigned-post": "^3.1141.0",
|
|
155
|
+
"@aws-sdk/s3-request-presigner": "^3.1141.0",
|
|
133
156
|
"@types/js-cookie": "^3.0.6",
|
|
134
157
|
"@types/mime-types": "^2.1.4",
|
|
135
158
|
"@types/node": "^24.9.0",
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { LambderApiRequest } from "./LambderApiRequest.js";
|
|
2
|
-
import type { LambderApiCallContext, LambderApiCallTrace } from "./LambderApiCallContext.js";
|
|
3
|
-
import type { LambderApiAnswer } from "./LambderApiAnswer.js";
|
|
4
|
-
import type { LambderApiDefinition } from "./LambderApiDefinition.js";
|
|
5
|
-
import { type LambderApiGuard } from "./LambderApiGuards.js";
|
|
6
|
-
import { type LambderApiRateLimitPolicyConfig, type LambderApiRateLimitsConfig } from "./LambderApiRateLimits.js";
|
|
7
|
-
import { type LambderApiIdempotencyConfig } from "./LambderApiIdempotency.js";
|
|
8
|
-
import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.js";
|
|
9
|
-
import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
|
|
10
|
-
import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
|
|
11
|
-
/**
|
|
12
|
-
* Runtime side of the declarative API options: composes the three policy
|
|
13
|
-
* subsystems (rate limits in ./LambderApiRateLimits.ts, guards in
|
|
14
|
-
* ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
|
|
15
|
-
* registrations against them at startup, and executes them around handlers
|
|
16
|
-
* at request time. Owned by LambderApiPipeline; apps interact through the
|
|
17
|
-
* create() options (rateLimits, guards, idempotency) and the per-API
|
|
18
|
-
* options.
|
|
19
|
-
*/
|
|
20
|
-
export declare class LambderApiPolicyEngine {
|
|
21
|
-
private rateLimits;
|
|
22
|
-
private guards;
|
|
23
|
-
private idempotency;
|
|
24
|
-
/** True once any of the three subsystems was configured. */
|
|
25
|
-
get isConfigured(): boolean;
|
|
26
|
-
configureRateLimits(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
|
|
27
|
-
configureGuards(guards: Record<string, LambderApiGuard<any, any, any>>): void;
|
|
28
|
-
configureIdempotency(config: LambderApiIdempotencyConfig): void;
|
|
29
|
-
/** The backend swap, handed on to the two subsystems that hold a store. Each answers whether it had a place for one. */
|
|
30
|
-
[LAMBDER_BACKEND_SWAP](backends: {
|
|
31
|
-
rateLimiter?: LambderRateLimiter;
|
|
32
|
-
idempotencyStore?: LambderIdempotencyStore;
|
|
33
|
-
}): {
|
|
34
|
-
rateLimits: boolean;
|
|
35
|
-
idempotency: boolean;
|
|
36
|
-
};
|
|
37
|
-
/** Startup validation of one API's declarative options. */
|
|
38
|
-
assertRegistration(definition: LambderApiDefinition): void;
|
|
39
|
-
/** The rate-limit policies that can be checked before the session is read: see LambderApiRateLimitsEngine.run. */
|
|
40
|
-
runSessionlessRateLimits(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition): Promise<void>;
|
|
41
|
-
/** The remaining rate limits, then guards, in declared order. Refusals throw; the trace records each guard as it runs. */
|
|
42
|
-
runPreflight(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace): Promise<void>;
|
|
43
|
-
/** Idempotency replay fast path, run before the preflight: see LambderApiIdempotencyEngine.findReplay. */
|
|
44
|
-
findReplay(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace): Promise<LambderApiAnswer | null>;
|
|
45
|
-
/** Idempotency claim/replay wrapper around handler execution: see LambderApiIdempotencyEngine.withIdempotency. */
|
|
46
|
-
withIdempotency(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace, exec: () => Promise<LambderApiAnswer>): Promise<LambderApiAnswer>;
|
|
47
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { LambderApiGuardsEngine } from "./LambderApiGuards.js";
|
|
2
|
-
import { LambderApiRateLimitsEngine } from "./LambderApiRateLimits.js";
|
|
3
|
-
import { LambderApiIdempotencyEngine } from "./LambderApiIdempotency.js";
|
|
4
|
-
import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
|
|
5
|
-
/** An API that asks for idempotency: declared, and not the explicit `false` opt-out. */
|
|
6
|
-
const usesIdempotency = (definition) => definition.idempotency !== undefined && definition.idempotency !== false;
|
|
7
|
-
/**
|
|
8
|
-
* Runtime side of the declarative API options: composes the three policy
|
|
9
|
-
* subsystems (rate limits in ./LambderApiRateLimits.ts, guards in
|
|
10
|
-
* ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
|
|
11
|
-
* registrations against them at startup, and executes them around handlers
|
|
12
|
-
* at request time. Owned by LambderApiPipeline; apps interact through the
|
|
13
|
-
* create() options (rateLimits, guards, idempotency) and the per-API
|
|
14
|
-
* options.
|
|
15
|
-
*/
|
|
16
|
-
export class LambderApiPolicyEngine {
|
|
17
|
-
rateLimits = new LambderApiRateLimitsEngine();
|
|
18
|
-
guards = new LambderApiGuardsEngine();
|
|
19
|
-
idempotency = new LambderApiIdempotencyEngine();
|
|
20
|
-
/** True once any of the three subsystems was configured. */
|
|
21
|
-
get isConfigured() {
|
|
22
|
-
return this.rateLimits.isConfigured || this.guards.isConfigured || this.idempotency.isConfigured;
|
|
23
|
-
}
|
|
24
|
-
configureRateLimits(config) {
|
|
25
|
-
this.rateLimits.configure(config);
|
|
26
|
-
}
|
|
27
|
-
configureGuards(guards) {
|
|
28
|
-
this.guards.configure(guards);
|
|
29
|
-
}
|
|
30
|
-
configureIdempotency(config) {
|
|
31
|
-
this.idempotency.configure(config);
|
|
32
|
-
}
|
|
33
|
-
/** The backend swap, handed on to the two subsystems that hold a store. Each answers whether it had a place for one. */
|
|
34
|
-
[LAMBDER_BACKEND_SWAP](backends) {
|
|
35
|
-
return {
|
|
36
|
-
rateLimits: backends.rateLimiter ? this.rateLimits[LAMBDER_BACKEND_SWAP](backends.rateLimiter) : false,
|
|
37
|
-
idempotency: backends.idempotencyStore ? this.idempotency[LAMBDER_BACKEND_SWAP](backends.idempotencyStore) : false,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
/** Startup validation of one API's declarative options. */
|
|
41
|
-
assertRegistration(definition) {
|
|
42
|
-
const { name, mode } = definition;
|
|
43
|
-
// Each subsystem reports its own absence. One combined message would
|
|
44
|
-
// tell an API that declares guards on an instance with no guards map
|
|
45
|
-
// that none of the three was configured, which reads as a question
|
|
46
|
-
// about all three when only one of them is missing.
|
|
47
|
-
if (definition.rateLimit !== undefined && !this.rateLimits.isConfigured) {
|
|
48
|
-
throw new Error(`Lambder: API "${name}" declares rateLimit but no rateLimits option was configured at creation.`);
|
|
49
|
-
}
|
|
50
|
-
if (definition.guards !== undefined && !this.guards.isConfigured) {
|
|
51
|
-
throw new Error(`Lambder: API "${name}" declares guards but no guards option was configured at creation.`);
|
|
52
|
-
}
|
|
53
|
-
this.rateLimits.assertRegistration(name, mode, definition.rateLimit);
|
|
54
|
-
this.guards.assertRegistration(name, mode, definition.guards);
|
|
55
|
-
// `idempotency: false` is an explicit opt-out, not a use: it asks for
|
|
56
|
-
// nothing and so needs no store behind it.
|
|
57
|
-
if (usesIdempotency(definition)) {
|
|
58
|
-
if (!this.idempotency.isConfigured) {
|
|
59
|
-
throw new Error(`Lambder: API "${name}" declares idempotency but no idempotency store was configured at creation.`);
|
|
60
|
-
}
|
|
61
|
-
this.idempotency.assertRegistration(name, definition.idempotency);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/** The rate-limit policies that can be checked before the session is read: see LambderApiRateLimitsEngine.run. */
|
|
65
|
-
async runSessionlessRateLimits(request, ctx, definition) {
|
|
66
|
-
await this.rateLimits.run(definition.name, request, ctx, definition.rateLimit, "beforeSession");
|
|
67
|
-
}
|
|
68
|
-
/** The remaining rate limits, then guards, in declared order. Refusals throw; the trace records each guard as it runs. */
|
|
69
|
-
async runPreflight(request, ctx, definition, trace) {
|
|
70
|
-
await this.rateLimits.run(definition.name, request, ctx, definition.rateLimit, "afterSession");
|
|
71
|
-
await this.guards.run(request, ctx, definition.guards, trace);
|
|
72
|
-
}
|
|
73
|
-
/** Idempotency replay fast path, run before the preflight: see LambderApiIdempotencyEngine.findReplay. */
|
|
74
|
-
async findReplay(request, ctx, definition, trace) {
|
|
75
|
-
if (!definition.idempotency)
|
|
76
|
-
return null;
|
|
77
|
-
return await this.idempotency.findReplay(definition.name, request, ctx, trace);
|
|
78
|
-
}
|
|
79
|
-
/** Idempotency claim/replay wrapper around handler execution: see LambderApiIdempotencyEngine.withIdempotency. */
|
|
80
|
-
async withIdempotency(request, ctx, definition, trace, exec) {
|
|
81
|
-
if (!definition.idempotency)
|
|
82
|
-
return await exec();
|
|
83
|
-
return await this.idempotency.withIdempotency(definition.name, request, ctx, definition.idempotency, trace, exec);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Joining the fields of a tracker or scope key.
|
|
3
|
-
*
|
|
4
|
-
* Both the rate limiter and the idempotency engine build a key by joining a
|
|
5
|
-
* few fields with a separator, and at least one field in each is caller data:
|
|
6
|
-
* a rate-limit key returned by a policy handler, an idempotency key posted
|
|
7
|
-
* with the request. A plain join lets two different field lists produce one
|
|
8
|
-
* string, so two callers land on one counter or one caller reads another's
|
|
9
|
-
* stored answer.
|
|
10
|
-
*
|
|
11
|
-
* Escaping the caller's use of the separator rather than rejecting or
|
|
12
|
-
* reserving it: the caller chose the character for its own reasons, and a
|
|
13
|
-
* limit that refuses a legal key is a bug of its own.
|
|
14
|
-
*
|
|
15
|
-
* The join is one-way. Nothing here reads a key back apart, because nothing
|
|
16
|
-
* needs to: a key is looked up, counted against or compared, never taken to
|
|
17
|
-
* pieces. The escaping is there so that distinct field lists cannot collide,
|
|
18
|
-
* not so that the fields can be recovered.
|
|
19
|
-
*
|
|
20
|
-
* LambderDdbCache's sort-key encoding looks similar and is deliberately the
|
|
21
|
-
* other thing: a reversible escape, because a cached entry's sort key is
|
|
22
|
-
* handed back to the caller by listSortKeys and therefore has to decode to
|
|
23
|
-
* exactly what was written. Two schemes, two jobs; neither should be reached
|
|
24
|
-
* for in the other's place.
|
|
25
|
-
*
|
|
26
|
-
* Written once because the two engines had drifted, one escaping and one not.
|
|
27
|
-
*/
|
|
28
|
-
/**
|
|
29
|
-
* The fields joined into one key, each escaped, so no two distinct field
|
|
30
|
-
* lists can produce the same string.
|
|
31
|
-
*/
|
|
32
|
-
export declare const joinKeyFields: (...fields: string[]) => string;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Joining the fields of a tracker or scope key.
|
|
3
|
-
*
|
|
4
|
-
* Both the rate limiter and the idempotency engine build a key by joining a
|
|
5
|
-
* few fields with a separator, and at least one field in each is caller data:
|
|
6
|
-
* a rate-limit key returned by a policy handler, an idempotency key posted
|
|
7
|
-
* with the request. A plain join lets two different field lists produce one
|
|
8
|
-
* string, so two callers land on one counter or one caller reads another's
|
|
9
|
-
* stored answer.
|
|
10
|
-
*
|
|
11
|
-
* Escaping the caller's use of the separator rather than rejecting or
|
|
12
|
-
* reserving it: the caller chose the character for its own reasons, and a
|
|
13
|
-
* limit that refuses a legal key is a bug of its own.
|
|
14
|
-
*
|
|
15
|
-
* The join is one-way. Nothing here reads a key back apart, because nothing
|
|
16
|
-
* needs to: a key is looked up, counted against or compared, never taken to
|
|
17
|
-
* pieces. The escaping is there so that distinct field lists cannot collide,
|
|
18
|
-
* not so that the fields can be recovered.
|
|
19
|
-
*
|
|
20
|
-
* LambderDdbCache's sort-key encoding looks similar and is deliberately the
|
|
21
|
-
* other thing: a reversible escape, because a cached entry's sort key is
|
|
22
|
-
* handed back to the caller by listSortKeys and therefore has to decode to
|
|
23
|
-
* exactly what was written. Two schemes, two jobs; neither should be reached
|
|
24
|
-
* for in the other's place.
|
|
25
|
-
*
|
|
26
|
-
* Written once because the two engines had drifted, one escaping and one not.
|
|
27
|
-
*/
|
|
28
|
-
/** One field, with the separator and its own escape character escaped. */
|
|
29
|
-
const escapeKeyField = (value) => value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
30
|
-
/**
|
|
31
|
-
* The fields joined into one key, each escaped, so no two distinct field
|
|
32
|
-
* lists can produce the same string.
|
|
33
|
-
*/
|
|
34
|
-
export const joinKeyFields = (...fields) => fields.map(escapeKeyField).join("|");
|