lambder 7.3.1 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +933 -3
- package/README.md +41 -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/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -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/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +140 -75
- package/dist/core/Lambder.js +347 -227
- 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 +15 -8
- package/dist/index.js +5 -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 +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +1 -1
- package/dist/mock.js +2 -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/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/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/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 +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -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/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 +79 -33
- 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/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +21 -23
- 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 +1 -0
- package/dist/testing.js +1 -0
- package/package.json +12 -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
|
@@ -2,56 +2,35 @@ import { z } from "zod";
|
|
|
2
2
|
import { toGuardEntries } from "./LambderApiGuards.js";
|
|
3
3
|
import { API_SIGNATURE_HEX_LENGTH, EXTENSIBLE_ENUM_META_KEY } from "../shared/wire/LambderApiSignature.js";
|
|
4
4
|
import { sha256HexOf } from "../shared/util/LambderTextDigest.js";
|
|
5
|
+
import { canonicalJson } from "../shared/util/canonicalJson.js";
|
|
5
6
|
/*
|
|
6
7
|
* The digest of an endpoint's client-facing shape, computed once, by the
|
|
7
8
|
* generator, through Lambder.apiSignatures(). Nothing digests at request
|
|
8
|
-
* time:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* time: server and client both carry the generated map and the pipeline
|
|
10
|
+
* compares entries, so there is no second computation to disagree with.
|
|
11
|
+
* See LambderApiSignatureMap.
|
|
11
12
|
*/
|
|
12
|
-
/**
|
|
13
|
-
* JSON with object keys sorted at every level, so two descriptions of the
|
|
14
|
-
* same shape hash the same whatever order they were built in. Arrays keep
|
|
15
|
-
* their order: a tuple's positions and an enum's values are part of the
|
|
16
|
-
* shape. Undefined entries are dropped, as JSON.stringify would drop them.
|
|
17
|
-
*/
|
|
18
|
-
const canonicalJson = (value) => JSON.stringify(sortKeys(value));
|
|
19
|
-
const sortKeys = (value) => {
|
|
20
|
-
if (Array.isArray(value))
|
|
21
|
-
return value.map(sortKeys);
|
|
22
|
-
if (value === null || typeof value !== "object")
|
|
23
|
-
return value;
|
|
24
|
-
const source = value;
|
|
25
|
-
const sorted = {};
|
|
26
|
-
for (const key of Object.keys(source).sort()) {
|
|
27
|
-
if (source[key] !== undefined)
|
|
28
|
-
sorted[key] = sortKeys(source[key]);
|
|
29
|
-
}
|
|
30
|
-
return sorted;
|
|
31
|
-
};
|
|
32
13
|
/**
|
|
33
14
|
* Three edits to every node zod emits, before it is hashed.
|
|
34
15
|
*
|
|
35
16
|
* The `default` keyword goes. Its value is server behaviour, not shape: a
|
|
36
|
-
* client never sends it
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* a client can see, that the field may be omitted, stays through `required`.
|
|
17
|
+
* client never sends it. For a function default (`.default(() => new
|
|
18
|
+
* Date())`, `.prefault`, `.catch`) zod writes whatever the function returned
|
|
19
|
+
* at conversion time, a clock reading or a random value, so the digest would
|
|
20
|
+
* differ on every computation and the generated map would never match the
|
|
21
|
+
* server. Once evaluated, such a default looks like a constant one, so every
|
|
22
|
+
* default goes; what a client can see of it, that the field may be omitted,
|
|
23
|
+
* stays through `required`.
|
|
44
24
|
*
|
|
45
|
-
* `required` is sorted
|
|
46
|
-
*
|
|
25
|
+
* `required` is sorted: it is a set, and declaration order is not shape.
|
|
26
|
+
* Left as emitted, reordering two fields would force a reload.
|
|
47
27
|
*
|
|
48
|
-
* An enum marked with extensibleEnum() loses its values in an output
|
|
49
|
-
* clients tolerate
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* marking an enum changes no input's digest.
|
|
28
|
+
* An enum marked with extensibleEnum() loses its values in an output: its
|
|
29
|
+
* clients tolerate unknown values, so adding or removing one changes nothing
|
|
30
|
+
* they can see, and the node still says it holds a string. In an input the
|
|
31
|
+
* values stay, since a value dropped from the list is one an older client
|
|
32
|
+
* may still send and the server would refuse. The mark itself goes in both,
|
|
33
|
+
* so marking an enum changes no input's digest.
|
|
55
34
|
*/
|
|
56
35
|
const keepShapeOnly = (node, io) => {
|
|
57
36
|
delete node.default;
|
|
@@ -76,18 +55,16 @@ const ownGuard = (guards, name) => guards !== undefined && Object.prototype.hasO
|
|
|
76
55
|
* input and output schemas as JSON Schema, every guard it declares with the
|
|
77
56
|
* schema that guard validates (the guardInput the client sends separately,
|
|
78
57
|
* or the apiInput slice of the payload), and whether it demands an
|
|
79
|
-
* idempotency key. Anything else
|
|
80
|
-
*
|
|
81
|
-
*
|
|
58
|
+
* idempotency key. Anything else (rate limits, a guard's parameter, the
|
|
59
|
+
* handler) changes nothing for a client and is left out, so changing it never
|
|
60
|
+
* forces a reload.
|
|
82
61
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* at construction digests differently in the generator's process and on the
|
|
90
|
-
* server.
|
|
62
|
+
* Schemas are hashed as built, descriptions and titles included, so a client
|
|
63
|
+
* built against a different one reloads once. The exception is an output
|
|
64
|
+
* extensibleEnum()'s values (see keepShapeOnly). Schemas must be built from
|
|
65
|
+
* static values: one that reads the clock, a random source or the
|
|
66
|
+
* environment at construction digests differently in the generator's process
|
|
67
|
+
* and on the server.
|
|
91
68
|
*/
|
|
92
69
|
export const apiSignatureOf = async (definition, guards) => {
|
|
93
70
|
const guardShapes = toGuardEntries(definition.guards).map(({ name }) => {
|
|
@@ -5,13 +5,12 @@ import { LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
|
5
5
|
* response. The API's own schema and every preflight slice (a guard's input,
|
|
6
6
|
* a rate-limit key's fields) throw this when a value fails to parse, and the
|
|
7
7
|
* pipeline renders it in one place: through the app's input validation
|
|
8
|
-
* handler when
|
|
9
|
-
*
|
|
10
|
-
* lets them run outside a Lambda.
|
|
8
|
+
* handler when set, otherwise as the standard 422 body. The engines never
|
|
9
|
+
* build a response or see a resolver, which lets them run outside a Lambda.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
* the brand
|
|
14
|
-
*
|
|
11
|
+
* It is a LambderApiRefusal, so every catch that maps refusals handles it;
|
|
12
|
+
* the brand routes it through the validation handler instead of the refusal
|
|
13
|
+
* envelope.
|
|
15
14
|
*/
|
|
16
15
|
export declare class LambderApiValidationRefusal extends LambderApiRefusal {
|
|
17
16
|
/** Brand for detection across duplicate lambder installs, like LambderApiRefusal's. */
|
|
@@ -26,7 +25,8 @@ export declare const isLambderApiValidationRefusal: (err: unknown) => err is Lam
|
|
|
26
25
|
* a guardInput value from the raw guardInputs map). Runs before the API's
|
|
27
26
|
* own validation; a failure throws the same LambderApiValidationRefusal the
|
|
28
27
|
* API's schema throws, so the pipeline answers every rejected input alike.
|
|
29
|
-
* Shared by the guards
|
|
30
|
-
*
|
|
28
|
+
* Shared by the guards and rate-limit engines, so it lives beside the error
|
|
29
|
+
* it throws. Parsed asynchronously, so a slice with an async refinement (a
|
|
30
|
+
* lookup, say) validates instead of making zod throw on every call.
|
|
31
31
|
*/
|
|
32
|
-
export declare const parsePreflightSlice: (input: z.ZodType, value: unknown) => unknown
|
|
32
|
+
export declare const parsePreflightSlice: (input: z.ZodType, value: unknown) => Promise<unknown>;
|
|
@@ -4,13 +4,12 @@ import { LambderApiRefusal, isLambderApiRefusal } from "../shared/wire/LambderAp
|
|
|
4
4
|
* response. The API's own schema and every preflight slice (a guard's input,
|
|
5
5
|
* a rate-limit key's fields) throw this when a value fails to parse, and the
|
|
6
6
|
* pipeline renders it in one place: through the app's input validation
|
|
7
|
-
* handler when
|
|
8
|
-
*
|
|
9
|
-
* lets them run outside a Lambda.
|
|
7
|
+
* handler when set, otherwise as the standard 422 body. The engines never
|
|
8
|
+
* build a response or see a resolver, which lets them run outside a Lambda.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
* the brand
|
|
13
|
-
*
|
|
10
|
+
* It is a LambderApiRefusal, so every catch that maps refusals handles it;
|
|
11
|
+
* the brand routes it through the validation handler instead of the refusal
|
|
12
|
+
* envelope.
|
|
14
13
|
*/
|
|
15
14
|
export class LambderApiValidationRefusal extends LambderApiRefusal {
|
|
16
15
|
/** Brand for detection across duplicate lambder installs, like LambderApiRefusal's. */
|
|
@@ -29,11 +28,12 @@ export const isLambderApiValidationRefusal = (err) => isLambderApiRefusal(err) &
|
|
|
29
28
|
* a guardInput value from the raw guardInputs map). Runs before the API's
|
|
30
29
|
* own validation; a failure throws the same LambderApiValidationRefusal the
|
|
31
30
|
* API's schema throws, so the pipeline answers every rejected input alike.
|
|
32
|
-
* Shared by the guards
|
|
33
|
-
*
|
|
31
|
+
* Shared by the guards and rate-limit engines, so it lives beside the error
|
|
32
|
+
* it throws. Parsed asynchronously, so a slice with an async refinement (a
|
|
33
|
+
* lookup, say) validates instead of making zod throw on every call.
|
|
34
34
|
*/
|
|
35
|
-
export const parsePreflightSlice = (input, value) => {
|
|
36
|
-
const parsed = input.
|
|
35
|
+
export const parsePreflightSlice = async (input, value) => {
|
|
36
|
+
const parsed = await input.safeParseAsync(value);
|
|
37
37
|
if (!parsed.success)
|
|
38
38
|
throw new LambderApiValidationRefusal(parsed.error);
|
|
39
39
|
return parsed.data;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** What the parent asks for: which export of which module to digest, and the file to compare it with. */
|
|
2
|
+
export type FreshProcessRequest = {
|
|
3
|
+
moduleUrl: string;
|
|
4
|
+
exportName: string;
|
|
5
|
+
file: string;
|
|
6
|
+
};
|
|
7
|
+
/** The comparison, or why none could be made. */
|
|
8
|
+
export type FreshProcessVerdict = {
|
|
9
|
+
same: boolean;
|
|
10
|
+
lines: string[];
|
|
11
|
+
} | {
|
|
12
|
+
failure: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { describeSignatureChanges, readSignatureMap, VERIFY_REPORT_PREFIX } from "./writeApiSignatures.js";
|
|
3
|
+
const request = JSON.parse(process.argv[2] ?? "null");
|
|
4
|
+
const namespace = await import(request.moduleUrl);
|
|
5
|
+
const source = namespace[request.exportName];
|
|
6
|
+
let verdict;
|
|
7
|
+
if (typeof source?.apiSignatureEntries !== "function") {
|
|
8
|
+
verdict = { failure: `${request.moduleUrl} has no export "${request.exportName}" that lists API signatures: name the export holding the instance in exportName` };
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
const { movedLines, summary } = describeSignatureChanges(await source.apiSignatureEntries(), readSignatureMap(readFileSync(request.file, "utf8")));
|
|
12
|
+
verdict = { same: movedLines.length === 0, lines: [summary, ...movedLines] };
|
|
13
|
+
}
|
|
14
|
+
// Waited for: a write to a pipe can still be pending when the process exits,
|
|
15
|
+
// and the parent reads the pipe.
|
|
16
|
+
await new Promise((resolveWrite) => process.stdout.write(`\n${VERIFY_REPORT_PREFIX}${JSON.stringify(verdict)}\n`, () => resolveWrite()));
|
|
17
|
+
// The app's module may hold the process open (a client's sockets, a timer),
|
|
18
|
+
// and this process exists for the one comparison.
|
|
19
|
+
process.exit(0);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { LambderApiSignatureEntry } from "../api/LambderApiSignature.js";
|
|
2
|
+
/**
|
|
3
|
+
* How the fresh process reports its comparison: one line on stdout starting
|
|
4
|
+
* with this, then the verdict as JSON. The line, not the exit status, is the
|
|
5
|
+
* answer: the app's module prints what it likes while it loads, and a
|
|
6
|
+
* process that fails before the line never compared anything.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VERIFY_REPORT_PREFIX = "lambder-api-signatures-verified:";
|
|
9
|
+
/** This process's execArgv, less the flags only it may run with (see PARENT_ONLY_FLAG). */
|
|
10
|
+
export declare const freshProcessNodeFlags: (execArgv: readonly string[]) => string[];
|
|
11
|
+
/** What writeApiSignatures reads the signatures from: a Lambder instance, or anything else that lists them the same way. */
|
|
12
|
+
export type LambderApiSignatureSource = {
|
|
13
|
+
apiSignatureEntries(): Promise<LambderApiSignatureEntry[]>;
|
|
14
|
+
};
|
|
15
|
+
export type LambderApiSignatureFileOptions = {
|
|
16
|
+
/** The TypeScript module to write, exporting `apiSignatures`. Relative to the working directory. */
|
|
17
|
+
file: string;
|
|
18
|
+
/** Write nothing, and answer whether the file on disk is what the registrations produce now. Default: false. */
|
|
19
|
+
check?: boolean;
|
|
20
|
+
/** The comment the file opens with, one `//` line per line. It should name what generates the file. Default: a note naming writeApiSignatures(). */
|
|
21
|
+
header?: string;
|
|
22
|
+
/** Quotes in the generated module. Default: "double". */
|
|
23
|
+
quotes?: "single" | "double";
|
|
24
|
+
/** End the generated statements with semicolons. Default: true. */
|
|
25
|
+
semicolons?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* After writing, or after a check that found the file current, load the
|
|
28
|
+
* module that holds the instance in a fresh Node process and check the
|
|
29
|
+
* file against what it digests there. A schema built from the clock or a
|
|
30
|
+
* random source digests differently in every process; this catches it by
|
|
31
|
+
* endpoint name instead of letting signatures change on every build. Only
|
|
32
|
+
* that module is loaded, never the calling script, so nothing the script
|
|
33
|
+
* does runs twice; the module's own top-level code does run again. The
|
|
34
|
+
* fresh process gets this process's Node flags (`--import`, `--require`,
|
|
35
|
+
* `--loader`, `--conditions`) less the inspector, watch mode, the test
|
|
36
|
+
* runner and the eval flags (`-e`, `-p`, `--input-type`), so a TypeScript
|
|
37
|
+
* module loads there as it did here when its loader is on the command
|
|
38
|
+
* line or in NODE_OPTIONS. Default: not verified.
|
|
39
|
+
*/
|
|
40
|
+
verifyInFreshProcess?: {
|
|
41
|
+
/**
|
|
42
|
+
* The module that exports the instance: a path relative to the
|
|
43
|
+
* working directory, or a file URL, as a URL such as
|
|
44
|
+
* `new URL("../backend/index.js", import.meta.url)` beside the
|
|
45
|
+
* generator's own import of it, or as the string
|
|
46
|
+
* `import.meta.resolve()` answers.
|
|
47
|
+
*/
|
|
48
|
+
module: string | URL;
|
|
49
|
+
/** The export that holds the instance. Default: "default", the module's default export. */
|
|
50
|
+
exportName?: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export type LambderApiSignatureFileResult = {
|
|
54
|
+
/** False when a check found the file stale, or a fresh process digested different signatures or never compared them. */
|
|
55
|
+
ok: boolean;
|
|
56
|
+
/** The absolute path. */
|
|
57
|
+
file: string;
|
|
58
|
+
/** How many APIs the file holds. */
|
|
59
|
+
count: number;
|
|
60
|
+
/** True when the file was (re)written; a file that already holds these signatures is left untouched, however it is formatted. */
|
|
61
|
+
written: boolean;
|
|
62
|
+
/** Endpoints whose signature differs from the file that was on disk, by name. */
|
|
63
|
+
changed: string[];
|
|
64
|
+
added: string[];
|
|
65
|
+
/** Endpoints the file held and the registrations no longer have. Named by key only: a key is a one-way hash of a name that is gone. */
|
|
66
|
+
removedKeys: string[];
|
|
67
|
+
/** What happened, as lines to print: a summary, then one line per endpoint that moved. */
|
|
68
|
+
lines: string[];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The map a generated file holds, read back by its hex pairs. Either quote
|
|
72
|
+
* style parses, and so does a key without quotes: a formatter that quotes
|
|
73
|
+
* properties only as needed (Prettier's default, Biome's, ESLint's
|
|
74
|
+
* quote-props) unquotes every key that starts with a letter.
|
|
75
|
+
*/
|
|
76
|
+
export declare const readSignatureMap: (contents: string) => Record<string, string>;
|
|
77
|
+
/** How the registrations differ from a map read off a file, by endpoint, as the lines both this process and a fresh one print. */
|
|
78
|
+
export declare const describeSignatureChanges: (entries: LambderApiSignatureEntry[], previousMap: Record<string, string>) => {
|
|
79
|
+
changed: string[];
|
|
80
|
+
added: string[];
|
|
81
|
+
removedKeys: string[];
|
|
82
|
+
movedLines: string[];
|
|
83
|
+
summary: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Writes the signature map both sides ship (see Lambder.apiSignatures()) to
|
|
87
|
+
* a TypeScript module, or checks the one on disk, and says which endpoints
|
|
88
|
+
* moved: every changed signature is a forced reload for the tabs calling
|
|
89
|
+
* that endpoint, so this is the line that says how wide a deploy's reload
|
|
90
|
+
* will be.
|
|
91
|
+
*
|
|
92
|
+
* Call it from a generator script that imports the app's instance:
|
|
93
|
+
*
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { writeApiSignatures } from "lambder/build";
|
|
96
|
+
* import { lambder } from "../server/src/index.js";
|
|
97
|
+
*
|
|
98
|
+
* const result = await writeApiSignatures(lambder, { file: "shared/generated/apiSignatures.generated.ts", check: process.argv.includes("--check") });
|
|
99
|
+
* console.log(result.lines.join("\n"));
|
|
100
|
+
* process.exit(result.ok ? 0 : 1);
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* Signatures are compared as the map the file holds, so a checkout that
|
|
104
|
+
* rewrote its line endings or a formatter that re-indented it or unquoted
|
|
105
|
+
* its keys is neither stale nor rewritten. `verifyInFreshProcess` also
|
|
106
|
+
* checks the file, written or found current, against the instance's module
|
|
107
|
+
* loaded in a fresh process.
|
|
108
|
+
*/
|
|
109
|
+
export declare const writeApiSignatures: (source: LambderApiSignatureSource, options: LambderApiSignatureFileOptions) => Promise<LambderApiSignatureFileResult>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
5
|
+
/*
|
|
6
|
+
* The signature file every app with apiSignatures needs, written and checked
|
|
7
|
+
* by the framework that defines it.
|
|
8
|
+
*
|
|
9
|
+
* The digests are computed once, by the generator, into this file, and both
|
|
10
|
+
* sides read the file: the frontend hands it to LambderCaller and the server
|
|
11
|
+
* hands it to create(), so the two can never disagree. What can still go wrong is the file itself (stale against the
|
|
12
|
+
* registrations, or different in every process because a schema reads the
|
|
13
|
+
* clock), and both are checked here.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* How the fresh process reports its comparison: one line on stdout starting
|
|
17
|
+
* with this, then the verdict as JSON. The line, not the exit status, is the
|
|
18
|
+
* answer: the app's module prints what it likes while it loads, and a
|
|
19
|
+
* process that fails before the line never compared anything.
|
|
20
|
+
*/
|
|
21
|
+
export const VERIFY_REPORT_PREFIX = "lambder-api-signatures-verified:";
|
|
22
|
+
/** How long a fresh-process verification may run before it counts as failed. */
|
|
23
|
+
const VERIFY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
24
|
+
/** What a fresh process may print before its verdict line: a chatty module must not fill the pipe and fail the check. */
|
|
25
|
+
const VERIFY_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
26
|
+
/** The fresh process's entry, beside this module in the build. */
|
|
27
|
+
const FRESH_PROCESS_ENTRY = new URL("./freshProcessVerifier.js", import.meta.url);
|
|
28
|
+
/**
|
|
29
|
+
* This process's own Node flags that the fresh process must not inherit,
|
|
30
|
+
* because they make it something other than a plain run of the verifier: the
|
|
31
|
+
* inspector (`--inspect-brk` waits for a debugger forever, and a fixed port
|
|
32
|
+
* collides with this process's), watch mode (a watched process never exits),
|
|
33
|
+
* the test runner (`--test` runs the entry as a test file), and the eval
|
|
34
|
+
* flags: `-e`, `-p` and a cluster of them such as `-pe` (which run their code
|
|
35
|
+
* in place of the entry), and `--input-type` (which Node refuses beside an
|
|
36
|
+
* entry file). Everything else, `--import`, `--require`, `--loader`,
|
|
37
|
+
* `--conditions` and the rest, is how this process loads the app's modules,
|
|
38
|
+
* and the fresh one needs it to load the same way.
|
|
39
|
+
*/
|
|
40
|
+
const PARENT_ONLY_FLAG = /^(--(inspect|debug-port|watch|test)([-=].*)?|--(eval|print|input-type)(=.*)?|-[a-z]*[ep][a-z]*)$/;
|
|
41
|
+
/** This process's execArgv, less the flags only it may run with (see PARENT_ONLY_FLAG). */
|
|
42
|
+
export const freshProcessNodeFlags = (execArgv) => {
|
|
43
|
+
const kept = [];
|
|
44
|
+
for (let index = 0; index < execArgv.length; index++) {
|
|
45
|
+
const flag = execArgv[index];
|
|
46
|
+
if (!PARENT_ONLY_FLAG.test(flag)) {
|
|
47
|
+
kept.push(flag);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// execArgv holds only Node's own options (the script and its
|
|
51
|
+
// arguments are argv), so an element that is not a flag is the value
|
|
52
|
+
// of the flag before it, and goes with it.
|
|
53
|
+
const next = execArgv[index + 1];
|
|
54
|
+
if (!flag.includes("=") && next !== undefined && !next.startsWith("-"))
|
|
55
|
+
index++;
|
|
56
|
+
}
|
|
57
|
+
return kept;
|
|
58
|
+
};
|
|
59
|
+
const DEFAULT_HEADER = [
|
|
60
|
+
"Generated by writeApiSignatures() from lambder/build. Do not edit.",
|
|
61
|
+
"",
|
|
62
|
+
"Every API the server registers, keyed by the hash of its name, with the",
|
|
63
|
+
"digest of its client-facing shape. The client sends the value with every",
|
|
64
|
+
"call and the server compares it with its own copy of this file, so a",
|
|
65
|
+
"deploy reloads only the tabs that call an endpoint whose shape changed.",
|
|
66
|
+
].join("\n");
|
|
67
|
+
/**
|
|
68
|
+
* The map a generated file holds, read back by its hex pairs. Either quote
|
|
69
|
+
* style parses, and so does a key without quotes: a formatter that quotes
|
|
70
|
+
* properties only as needed (Prettier's default, Biome's, ESLint's
|
|
71
|
+
* quote-props) unquotes every key that starts with a letter.
|
|
72
|
+
*/
|
|
73
|
+
export const readSignatureMap = (contents) => {
|
|
74
|
+
const map = {};
|
|
75
|
+
for (const match of contents.matchAll(/(?<![\w$])(['"]?)([0-9a-f]+)\1\s*:\s*['"]([0-9a-f]+)['"]/g))
|
|
76
|
+
map[match[2]] = match[3];
|
|
77
|
+
return map;
|
|
78
|
+
};
|
|
79
|
+
/** How the registrations differ from a map read off a file, by endpoint, as the lines both this process and a fresh one print. */
|
|
80
|
+
export const describeSignatureChanges = (entries, previousMap) => {
|
|
81
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
82
|
+
const changed = entries.filter(({ key, signature }) => key in previousMap && previousMap[key] !== signature).sort(byName).map(({ name }) => name);
|
|
83
|
+
const added = entries.filter(({ key }) => !(key in previousMap)).sort(byName).map(({ name }) => name);
|
|
84
|
+
const keys = new Set(entries.map(({ key }) => key));
|
|
85
|
+
const removedKeys = Object.keys(previousMap).filter((key) => !keys.has(key));
|
|
86
|
+
const movedLines = [
|
|
87
|
+
...changed.map((name) => ` ~ ${name}`),
|
|
88
|
+
...added.map((name) => ` + ${name}`),
|
|
89
|
+
...removedKeys.map((key) => ` - ${key} (removed: a key cannot be resolved back to its name)`),
|
|
90
|
+
];
|
|
91
|
+
const summary = `${changed.length} changed, ${added.length} added, ${removedKeys.length} removed (${entries.length - changed.length - added.length} unchanged)`;
|
|
92
|
+
return { changed, added, removedKeys, movedLines, summary };
|
|
93
|
+
};
|
|
94
|
+
const renderSignatureFile = (entries, options) => {
|
|
95
|
+
const quote = options.quotes === "single" ? "'" : "\"";
|
|
96
|
+
const semicolon = options.semicolons === false ? "" : ";";
|
|
97
|
+
const header = (options.header ?? DEFAULT_HEADER).split("\n").map((line) => line ? `// ${line}` : "//");
|
|
98
|
+
// Keys and signatures are hex, so swapping the quote character touches nothing inside them.
|
|
99
|
+
const map = JSON.stringify(Object.fromEntries(entries.map(({ key, signature }) => [key, signature])), null, 4).replace(/"/g, quote);
|
|
100
|
+
return [
|
|
101
|
+
...header,
|
|
102
|
+
`import type { LambderApiSignatureMap } from ${quote}lambder/client${quote}${semicolon}`,
|
|
103
|
+
"",
|
|
104
|
+
// Kept as written by a formatter that honours it, so a format pass
|
|
105
|
+
// after every generation has nothing to change. One that does not
|
|
106
|
+
// honour it changes the text only: the map reads back the same.
|
|
107
|
+
"// prettier-ignore",
|
|
108
|
+
`export const apiSignatures: LambderApiSignatureMap = ${map}${semicolon}`,
|
|
109
|
+
"",
|
|
110
|
+
].join("\n");
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Writes the signature map both sides ship (see Lambder.apiSignatures()) to
|
|
114
|
+
* a TypeScript module, or checks the one on disk, and says which endpoints
|
|
115
|
+
* moved: every changed signature is a forced reload for the tabs calling
|
|
116
|
+
* that endpoint, so this is the line that says how wide a deploy's reload
|
|
117
|
+
* will be.
|
|
118
|
+
*
|
|
119
|
+
* Call it from a generator script that imports the app's instance:
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { writeApiSignatures } from "lambder/build";
|
|
123
|
+
* import { lambder } from "../server/src/index.js";
|
|
124
|
+
*
|
|
125
|
+
* const result = await writeApiSignatures(lambder, { file: "shared/generated/apiSignatures.generated.ts", check: process.argv.includes("--check") });
|
|
126
|
+
* console.log(result.lines.join("\n"));
|
|
127
|
+
* process.exit(result.ok ? 0 : 1);
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* Signatures are compared as the map the file holds, so a checkout that
|
|
131
|
+
* rewrote its line endings or a formatter that re-indented it or unquoted
|
|
132
|
+
* its keys is neither stale nor rewritten. `verifyInFreshProcess` also
|
|
133
|
+
* checks the file, written or found current, against the instance's module
|
|
134
|
+
* loaded in a fresh process.
|
|
135
|
+
*/
|
|
136
|
+
export const writeApiSignatures = async (source, options) => {
|
|
137
|
+
const file = resolve(options.file);
|
|
138
|
+
const entries = await source.apiSignatureEntries();
|
|
139
|
+
const previous = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
140
|
+
const { changed, added, removedKeys, movedLines, summary } = describeSignatureChanges(entries, previous === null ? {} : readSignatureMap(previous));
|
|
141
|
+
const unchanged = previous !== null && movedLines.length === 0;
|
|
142
|
+
const result = (ok, written, lines) => ({ ok, file, count: entries.length, written, changed, added, removedKeys, lines });
|
|
143
|
+
// A stale file is the answer by itself: a fresh process could only find
|
|
144
|
+
// it stale again. A current one goes on to the verification, as a write
|
|
145
|
+
// does, so a check that names verifyInFreshProcess verifies.
|
|
146
|
+
let written = false;
|
|
147
|
+
let lines;
|
|
148
|
+
if (options.check) {
|
|
149
|
+
if (!unchanged)
|
|
150
|
+
return result(false, false, [`✗ ${options.file} is stale: regenerate it`, ` ${summary}`, ...movedLines]);
|
|
151
|
+
lines = [`✓ ${options.file} matches the ${entries.length} registered APIs`];
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// A file that already holds this map is left as it is, however it is
|
|
155
|
+
// formatted, so a watcher or an incremental build sees no change
|
|
156
|
+
// where there is none, and a formatter's or a checkout's version of
|
|
157
|
+
// the file is not rewritten back on every run. A change of header,
|
|
158
|
+
// quotes or semicolons shows the next time the map changes. The write
|
|
159
|
+
// goes to a file beside the target and is renamed over it, so a build
|
|
160
|
+
// reading the file meanwhile sees the old map or the new one, never
|
|
161
|
+
// half of one. A symlink is followed to the file it names: renamed
|
|
162
|
+
// over, the link itself would become the new file and its target
|
|
163
|
+
// would keep the old map.
|
|
164
|
+
written = !unchanged;
|
|
165
|
+
if (written) {
|
|
166
|
+
const target = previous === null ? file : realpathSync(file);
|
|
167
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
168
|
+
const partial = `${target}.${process.pid}.tmp`;
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(partial, renderSignatureFile(entries, options));
|
|
171
|
+
renameSync(partial, target);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
rmSync(partial, { force: true });
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
lines = [
|
|
179
|
+
written ? `✓ Wrote ${options.file} (${entries.length} APIs)` : `✓ ${options.file} is up to date (${entries.length} APIs)`,
|
|
180
|
+
...(movedLines.length ? [` ${summary}`, ...movedLines] : [" no signatures changed: this build forces no reloads"]),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
if (!options.verifyInFreshProcess)
|
|
184
|
+
return result(true, written, lines);
|
|
185
|
+
const { module: instanceModule, exportName = "default" } = options.verifyInFreshProcess;
|
|
186
|
+
const request = {
|
|
187
|
+
// A string that is already a file URL (what import.meta.resolve()
|
|
188
|
+
// answers) is taken as one: resolved as a path, it would name a
|
|
189
|
+
// directory called "file:" under the working directory.
|
|
190
|
+
moduleUrl: typeof instanceModule === "string" && !/^file:/i.test(instanceModule)
|
|
191
|
+
? pathToFileURL(resolve(instanceModule)).href
|
|
192
|
+
: new URL(instanceModule).href,
|
|
193
|
+
exportName,
|
|
194
|
+
file,
|
|
195
|
+
};
|
|
196
|
+
const child = spawnSync(process.execPath, [...freshProcessNodeFlags(process.execArgv), fileURLToPath(FRESH_PROCESS_ENTRY), JSON.stringify(request)], {
|
|
197
|
+
encoding: "utf8",
|
|
198
|
+
timeout: VERIFY_TIMEOUT_MS,
|
|
199
|
+
maxBuffer: VERIFY_OUTPUT_BYTES,
|
|
200
|
+
});
|
|
201
|
+
const reportLine = (child.stdout ?? "").split("\n").findLast((line) => line.startsWith(VERIFY_REPORT_PREFIX));
|
|
202
|
+
if (reportLine === undefined) {
|
|
203
|
+
const stderrTail = (child.stderr ?? "").trim().split("\n").slice(-20).filter(Boolean);
|
|
204
|
+
return result(false, written, [
|
|
205
|
+
...lines,
|
|
206
|
+
child.error
|
|
207
|
+
? `✗ the fresh process that verifies ${options.file} could not finish: ${child.error.message}`
|
|
208
|
+
: `✗ the fresh process that verifies ${options.file} never compared it: loading ${request.moduleUrl} failed (exit status ${child.status ?? "none"})`,
|
|
209
|
+
...stderrTail.map((line) => ` ${line}`),
|
|
210
|
+
]);
|
|
211
|
+
}
|
|
212
|
+
const verdict = JSON.parse(reportLine.slice(VERIFY_REPORT_PREFIX.length));
|
|
213
|
+
if ("failure" in verdict)
|
|
214
|
+
return result(false, written, [...lines, `✗ the fresh process that verifies ${options.file} never compared it: ${verdict.failure}`]);
|
|
215
|
+
if (verdict.same)
|
|
216
|
+
return result(true, written, [...lines, "✓ a fresh process digests the same signatures"]);
|
|
217
|
+
return result(false, written, [
|
|
218
|
+
...lines,
|
|
219
|
+
`✗ a fresh process digests different signatures for ${options.file}: a schema reads the clock or a random source`,
|
|
220
|
+
...verdict.lines.map((line) => ` ${line}`),
|
|
221
|
+
]);
|
|
222
|
+
};
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build entry point (`import ... from "lambder/build"`).
|
|
3
|
+
*
|
|
4
|
+
* What a generator script runs at build time over the app's own instance to
|
|
5
|
+
* write the signature file both sides ship. Node-only and imported by nothing
|
|
6
|
+
* else in the package, so no deployment or bundle carries it.
|
|
7
|
+
*/
|
|
8
|
+
export { writeApiSignatures } from "./build/writeApiSignatures.js";
|
|
9
|
+
export type { LambderApiSignatureSource, LambderApiSignatureFileOptions, LambderApiSignatureFileResult, } from "./build/writeApiSignatures.js";
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build entry point (`import ... from "lambder/build"`).
|
|
3
|
+
*
|
|
4
|
+
* What a generator script runs at build time over the app's own instance to
|
|
5
|
+
* write the signature file both sides ship. Node-only and imported by nothing
|
|
6
|
+
* else in the package, so no deployment or bundle carries it.
|
|
7
|
+
*/
|
|
8
|
+
export { writeApiSignatures } from "./build/writeApiSignatures.js";
|