lambder 7.0.2 → 7.1.4
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 +60 -0
- package/dist/api/LambderApiDefinition.d.ts +6 -3
- package/dist/api/LambderApiEnvelope.d.ts +1 -1
- package/dist/api/LambderApiEnvelope.js +1 -1
- package/dist/api/LambderApiGuards.d.ts +5 -0
- package/dist/api/LambderApiGuards.js +2 -2
- package/dist/api/LambderApiPipeline.d.ts +30 -14
- package/dist/api/LambderApiPipeline.js +28 -18
- package/dist/api/LambderApiRequest.d.ts +3 -1
- package/dist/api/LambderApiRequest.js +1 -0
- package/dist/api/LambderApiSignature.d.ts +41 -0
- package/dist/api/LambderApiSignature.js +91 -0
- package/dist/client/LambderCaller.d.ts +13 -0
- package/dist/client/LambderCaller.js +21 -1
- package/dist/client/LambderReloadLoopBreaker.d.ts +38 -0
- package/dist/client/LambderReloadLoopBreaker.js +71 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +3 -0
- package/dist/core/Lambder.d.ts +15 -1
- package/dist/core/Lambder.js +30 -7
- package/dist/core/LambderCreateOptions.d.ts +6 -0
- package/dist/core/LambderCreateOptions.js +0 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/invoke/LambderInvokeCaller.d.ts +12 -1
- package/dist/invoke/LambderInvokeCaller.js +10 -1
- package/dist/invoke/LambderLambdaEvent.d.ts +1 -0
- package/dist/invoke/LambderLambdaEvent.js +1 -0
- package/dist/mock/LambderMockApp.d.ts +2 -2
- package/dist/mock/LambderMockApp.js +21 -13
- package/dist/mock/LambderMockCreateOptions.d.ts +10 -1
- package/dist/mock/LambderMockCreateOptions.js +0 -8
- package/dist/mock/LambderMockTypes.d.ts +2 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +3 -0
- package/dist/shared/transport/LambderApiTransport.js +2 -0
- package/dist/shared/wire/LambderApiSignature.d.ts +33 -0
- package/dist/shared/wire/LambderApiSignature.js +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stops a stale client from reloading forever.
|
|
3
|
+
*
|
|
4
|
+
* A versionExpired answer means "this client's signature for the endpoint is
|
|
5
|
+
* not the one the server holds", and the ordinary response is to reload and
|
|
6
|
+
* get the current bundle. When the bundle being served is itself the stale
|
|
7
|
+
* one (a frontend deployed with a signature map the server does not match, a
|
|
8
|
+
* cached bundle, a server deploy that failed behind a fresh frontend), the
|
|
9
|
+
* reload brings back the same signature, the same call fails the same way,
|
|
10
|
+
* and the page reloads again, indefinitely.
|
|
11
|
+
*
|
|
12
|
+
* The evidence of that loop is a versionExpired for the same endpoint with
|
|
13
|
+
* the same signature shortly after the last one: a bundle that had actually
|
|
14
|
+
* changed the endpoint would carry a different signature. The record lives
|
|
15
|
+
* in sessionStorage, which is per tab and survives a reload, so the new page
|
|
16
|
+
* instance sees what the previous one saw; without sessionStorage (a test, a
|
|
17
|
+
* non-browser runtime) an in-memory record does the same within one page.
|
|
18
|
+
*
|
|
19
|
+
* Once a repeat is confirmed, every versionExpired within the window from
|
|
20
|
+
* the first one counts as a repeat too, whichever endpoint it names: a stale
|
|
21
|
+
* bundle is usually stale for several endpoints, and one reload per endpoint
|
|
22
|
+
* is still a loop, only a slower one. After the window a reload is allowed
|
|
23
|
+
* again, so a client stuck on a stale bundle retries a few times an hour and
|
|
24
|
+
* recovers by itself once the deploy is fixed.
|
|
25
|
+
*/
|
|
26
|
+
/** How long after the first versionExpired a repeat counts as the same loop. */
|
|
27
|
+
export const RELOAD_LOOP_WINDOW_MS = 5 * 60 * 1000;
|
|
28
|
+
const STORAGE_KEY = "lambder:version-expired";
|
|
29
|
+
const isExpiredRecord = (value) => typeof value === "object" && value !== null
|
|
30
|
+
&& typeof value.apiName === "string"
|
|
31
|
+
&& typeof value.signature === "string"
|
|
32
|
+
&& typeof value.at === "number"
|
|
33
|
+
&& typeof value.confirmed === "boolean";
|
|
34
|
+
export class LambderReloadLoopBreaker {
|
|
35
|
+
/** The record when sessionStorage is unavailable; sessionStorage is read first wherever it exists. */
|
|
36
|
+
memory = null;
|
|
37
|
+
/**
|
|
38
|
+
* Records this versionExpired and says whether it repeats a recent one,
|
|
39
|
+
* in which case the caller must not invoke versionExpiredHandler again.
|
|
40
|
+
*/
|
|
41
|
+
isRepeat(apiName, signature, now = Date.now()) {
|
|
42
|
+
const last = this.read();
|
|
43
|
+
if (last && now - last.at < RELOAD_LOOP_WINDOW_MS && (last.confirmed || (last.apiName === apiName && last.signature === signature))) {
|
|
44
|
+
// `at` stays the first event's, so the window runs from the start
|
|
45
|
+
// of the loop rather than being pushed forward by every repeat.
|
|
46
|
+
this.write({ apiName, signature, at: last.at, confirmed: true });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
this.write({ apiName, signature, at: now, confirmed: false });
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
read() {
|
|
53
|
+
try {
|
|
54
|
+
const raw = globalThis.sessionStorage?.getItem(STORAGE_KEY);
|
|
55
|
+
if (raw) {
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
if (isExpiredRecord(parsed))
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { /* a private window or blocked storage: the in-memory record stands in */ }
|
|
62
|
+
return this.memory;
|
|
63
|
+
}
|
|
64
|
+
write(record) {
|
|
65
|
+
this.memory = record;
|
|
66
|
+
try {
|
|
67
|
+
globalThis.sessionStorage?.setItem(STORAGE_KEY, JSON.stringify(record));
|
|
68
|
+
}
|
|
69
|
+
catch { /* same: the in-memory record stands in */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ export type { LambderApiTransport, LambderApiTransportRequest, LambderTransportF
|
|
|
15
15
|
export { LambderCookieJar, parseSetCookie } from "./shared/transport/LambderCookieJar.js";
|
|
16
16
|
export type { LambderStoredCookie } from "./shared/transport/LambderCookieJar.js";
|
|
17
17
|
export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
|
|
18
|
+
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH } from "./shared/wire/LambderApiSignature.js";
|
|
19
|
+
export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignature.js";
|
|
20
|
+
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
18
21
|
export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
|
|
19
22
|
export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderIdempotencyKeyScope, LambderLogListHandler, } from "./client/LambderCaller.js";
|
|
20
23
|
export { LambderApiRefusal, isLambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
package/dist/client.js
CHANGED
|
@@ -14,6 +14,9 @@ export { buildTransportEnvelope, LambderTransportFailure, isLambderTransportFail
|
|
|
14
14
|
export { lambderCookieJarTransport } from "./shared/transport/lambderCookieJarTransport.js";
|
|
15
15
|
export { LambderCookieJar, parseSetCookie } from "./shared/transport/LambderCookieJar.js";
|
|
16
16
|
export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
|
|
17
|
+
// The per-endpoint signature map a build ships with, how a caller reads it, and the reload-loop window.
|
|
18
|
+
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH } from "./shared/wire/LambderApiSignature.js";
|
|
19
|
+
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
17
20
|
// Typed API refusals (isomorphic: shared code may throw them from anywhere;
|
|
18
21
|
// in the browser they are plain Errors).
|
|
19
22
|
export { LambderApiRefusal, isLambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
package/dist/core/Lambder.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type LambderSessionController from "../session/LambderSessionController.j
|
|
|
9
9
|
import { type LambderPublicFilesOptions } from "./LambderPublicFiles.js";
|
|
10
10
|
import { type LambderIndexHtmlOptions } from "./LambderIndexHtml.js";
|
|
11
11
|
import { LambderFiles } from "./LambderFiles.js";
|
|
12
|
+
import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
|
|
12
13
|
import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
|
|
13
14
|
import type { LambderApiGuard, LambderGuardMetaMap, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf } from "../api/LambderApiGuards.js";
|
|
14
15
|
import type { LambderApiRateLimitPolicyConfig, LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
|
|
@@ -50,6 +51,7 @@ export type LambderCreatedHook = (lambderInstance: Lambder<any, any, any, any, a
|
|
|
50
51
|
*/
|
|
51
52
|
export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}, _TRateLimitPolicies extends Record<string, LambderApiRateLimitPolicyConfig> = {}, _TGuards extends Record<string, any> = {}, _TIdempotencyEnabled extends boolean = false, _TSessionGuardsRequired extends boolean = false, _TPublicGuardsRequired extends boolean = false, _TSessionsEnabled extends boolean = true> {
|
|
52
53
|
apiPath: string;
|
|
54
|
+
/** Stamped on every API answer's envelope as apiVersion. Informational: a client's staleness is judged per endpoint by its signature, see apiSignatures(). */
|
|
53
55
|
apiVersion: null | string;
|
|
54
56
|
/** The instance's file reader (source + caches), or null without the files option. */
|
|
55
57
|
files: LambderFiles | null;
|
|
@@ -67,7 +69,10 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
67
69
|
private actionList;
|
|
68
70
|
/** The API core: the pipeline every API call runs through, shared in shape with the mock runtime. */
|
|
69
71
|
private readonly pipeline;
|
|
70
|
-
|
|
72
|
+
/** Every registered API by name: what resolves a request's name to its definition ahead of the pipeline, and what apiSignatures() digests. */
|
|
73
|
+
private readonly apiDefinitions;
|
|
74
|
+
/** The signature of each endpoint as this server serves it, digested once per endpoint on first use. */
|
|
75
|
+
private readonly signatureDigests;
|
|
71
76
|
private hookList;
|
|
72
77
|
private createdHooks;
|
|
73
78
|
private initPromise;
|
|
@@ -165,6 +170,15 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
165
170
|
getSessionController(ctx: LambderRenderContext | LambderSessionRenderContext<any, TSessionData>): LambderSessionController<TSessionData>;
|
|
166
171
|
/** The session manager, for code that works on sessions outside a request (maintenance, tests). */
|
|
167
172
|
getSessionManager(): LambderSessionManager<TSessionData>;
|
|
173
|
+
/**
|
|
174
|
+
* Every registered endpoint's signature, keyed by its hashed name: the
|
|
175
|
+
* LambderApiSignatureMap a client build ships with. A generator imports
|
|
176
|
+
* the finished instance, awaits this, and writes the result to a file the
|
|
177
|
+
* frontend passes to LambderCaller as apiSignatures; at request time the
|
|
178
|
+
* server compares each call's signature against these same digests. Keys
|
|
179
|
+
* are sorted, so the generated file diffs by endpoint.
|
|
180
|
+
*/
|
|
181
|
+
apiSignatures(): Promise<LambderApiSignatureMap>;
|
|
168
182
|
getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder<any>;
|
|
169
183
|
private getResolver;
|
|
170
184
|
getHandler(): LambderHandler;
|
package/dist/core/Lambder.js
CHANGED
|
@@ -10,6 +10,8 @@ import { LambderIndexHtmlHandler } from "./LambderIndexHtml.js";
|
|
|
10
10
|
import { LambderFiles } from "./LambderFiles.js";
|
|
11
11
|
import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
12
12
|
import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
|
|
13
|
+
import { LambderApiSignatureDigests } from "../api/LambderApiSignature.js";
|
|
14
|
+
import { apiNameKeyOf } from "../shared/wire/LambderApiSignature.js";
|
|
13
15
|
import { apiNotFoundAnswer, crashAnswer, refusalAnswer, } from "../api/LambderApiEnvelope.js";
|
|
14
16
|
import { createContext, isV2HttpEvent } from "./LambderContext.js";
|
|
15
17
|
import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD } from "../shared/wire/LambderRequestPayload.js";
|
|
@@ -45,6 +47,7 @@ export default class Lambder {
|
|
|
45
47
|
// Everything an instance is, fixed before the first registration.
|
|
46
48
|
// =====================================================================
|
|
47
49
|
apiPath;
|
|
50
|
+
/** Stamped on every API answer's envelope as apiVersion. Informational: a client's staleness is judged per endpoint by its signature, see apiSignatures(). */
|
|
48
51
|
apiVersion;
|
|
49
52
|
/** The instance's file reader (source + caches), or null without the files option. */
|
|
50
53
|
files;
|
|
@@ -62,7 +65,10 @@ export default class Lambder {
|
|
|
62
65
|
actionList = [];
|
|
63
66
|
/** The API core: the pipeline every API call runs through, shared in shape with the mock runtime. */
|
|
64
67
|
pipeline;
|
|
65
|
-
|
|
68
|
+
/** Every registered API by name: what resolves a request's name to its definition ahead of the pipeline, and what apiSignatures() digests. */
|
|
69
|
+
apiDefinitions = new Map();
|
|
70
|
+
/** The signature of each endpoint as this server serves it, digested once per endpoint on first use. */
|
|
71
|
+
signatureDigests;
|
|
66
72
|
hookList = { "beforeRender": [], "afterRender": [], "fallback": [] };
|
|
67
73
|
createdHooks = [];
|
|
68
74
|
initPromise = null;
|
|
@@ -95,8 +101,10 @@ export default class Lambder {
|
|
|
95
101
|
this.corsConfig = options.cors === true ? {} : options.cors;
|
|
96
102
|
}
|
|
97
103
|
const session = options.session;
|
|
104
|
+
this.signatureDigests = new LambderApiSignatureDigests(options.guards);
|
|
98
105
|
this.pipeline = new LambderApiPipeline({
|
|
99
106
|
apiVersion: this.apiVersion,
|
|
107
|
+
signatures: this.signatureDigests,
|
|
100
108
|
maxRequestPayloadBytes: options.maxRequestPayloadBytes,
|
|
101
109
|
// The app's own validation handler is read at call time, since
|
|
102
110
|
// setApiInputValidationErrorHandler runs after creation.
|
|
@@ -200,7 +208,8 @@ export default class Lambder {
|
|
|
200
208
|
// Typed API with Zod
|
|
201
209
|
addApi(name, schema, handler) {
|
|
202
210
|
this.assertApiRegistration(name, "public", schema);
|
|
203
|
-
const definition = { name, mode: "public", guards: schema.guards, rateLimit: schema.rateLimit, idempotency: schema.idempotency, input: schema.input };
|
|
211
|
+
const definition = { name, mode: "public", guards: schema.guards, rateLimit: schema.rateLimit, idempotency: schema.idempotency, input: schema.input, output: schema.output };
|
|
212
|
+
this.apiDefinitions.set(name, definition);
|
|
204
213
|
this.actionList.push({
|
|
205
214
|
match: (ctx) => ctx.apiName === name ? {} : false,
|
|
206
215
|
actionFn: (ctx, resolver) => this.runApi(ctx, resolver, definition, handler),
|
|
@@ -210,7 +219,8 @@ export default class Lambder {
|
|
|
210
219
|
// Typed Session API with Zod
|
|
211
220
|
addSessionApi(name, schema, handler) {
|
|
212
221
|
this.assertApiRegistration(name, "session", schema);
|
|
213
|
-
const definition = { name, mode: "session", guards: schema.guards, rateLimit: schema.rateLimit, idempotency: schema.idempotency, input: schema.input };
|
|
222
|
+
const definition = { name, mode: "session", guards: schema.guards, rateLimit: schema.rateLimit, idempotency: schema.idempotency, input: schema.input, output: schema.output };
|
|
223
|
+
this.apiDefinitions.set(name, definition);
|
|
214
224
|
this.actionList.push({
|
|
215
225
|
match: (ctx) => ctx.apiName === name ? {} : false,
|
|
216
226
|
actionFn: (ctx, resolver) => this.runApi(ctx, resolver, definition, handler),
|
|
@@ -276,6 +286,19 @@ export default class Lambder {
|
|
|
276
286
|
getSessionManager() {
|
|
277
287
|
return this.pipeline.sessionManager;
|
|
278
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Every registered endpoint's signature, keyed by its hashed name: the
|
|
291
|
+
* LambderApiSignatureMap a client build ships with. A generator imports
|
|
292
|
+
* the finished instance, awaits this, and writes the result to a file the
|
|
293
|
+
* frontend passes to LambderCaller as apiSignatures; at request time the
|
|
294
|
+
* server compares each call's signature against these same digests. Keys
|
|
295
|
+
* are sorted, so the generated file diffs by endpoint.
|
|
296
|
+
*/
|
|
297
|
+
async apiSignatures() {
|
|
298
|
+
const entries = await Promise.all([...this.apiDefinitions.values()].map(async (definition) => [await apiNameKeyOf(definition.name), await this.signatureDigests.signatureOf(definition)]));
|
|
299
|
+
entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
300
|
+
return Object.fromEntries(entries);
|
|
301
|
+
}
|
|
279
302
|
getResponseBuilder(ctx) {
|
|
280
303
|
return new LambderResponseBuilder({
|
|
281
304
|
files: this.files,
|
|
@@ -401,8 +424,9 @@ export default class Lambder {
|
|
|
401
424
|
// The protocol's own pre-pass, run here rather than left to the
|
|
402
425
|
// pipeline so that hooks and route matching see a plain payload,
|
|
403
426
|
// and so a stale client is answered before any of them, whether or
|
|
404
|
-
// not the name it asked for exists
|
|
405
|
-
|
|
427
|
+
// not the name it asked for exists: the gate is handed the
|
|
428
|
+
// definition the name resolves to, or null.
|
|
429
|
+
const prepared = await this.pipeline.prepare(ctx.api, this.apiDefinitions.get(ctx.api.apiName) ?? null);
|
|
406
430
|
if (prepared)
|
|
407
431
|
return responseFromAnswer(prepared);
|
|
408
432
|
// ctx.post is the raw body view; it shows the restored payload and
|
|
@@ -583,7 +607,7 @@ export default class Lambder {
|
|
|
583
607
|
// =====================================================================
|
|
584
608
|
/** Registration-time checks shared by addApi/addSessionApi. */
|
|
585
609
|
assertApiRegistration(name, mode, options) {
|
|
586
|
-
if (this.
|
|
610
|
+
if (this.apiDefinitions.has(name)) {
|
|
587
611
|
throw new Error(`Lambder: duplicate API name "${name}". Dispatch is first-match, so the second registration would be silently dead code.`);
|
|
588
612
|
}
|
|
589
613
|
// Everything that can refuse this registration runs before the name is
|
|
@@ -603,7 +627,6 @@ export default class Lambder {
|
|
|
603
627
|
`Declare the guard that authorizes it, or ${optOut}.`);
|
|
604
628
|
}
|
|
605
629
|
this.pipeline.assertRegistration({ name, mode, guards: options.guards, rateLimit: options.rateLimit, idempotency: options.idempotency });
|
|
606
|
-
this.registeredApiNames.add(name);
|
|
607
630
|
}
|
|
608
631
|
/**
|
|
609
632
|
* The answer for a rejected input: the app's
|
|
@@ -99,6 +99,12 @@ export type LambderCreateOptions<TSessionData = any> = {
|
|
|
99
99
|
*/
|
|
100
100
|
files?: LambderFilesOption;
|
|
101
101
|
apiPath?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Stamped on every API answer's envelope as `apiVersion`, so a client can
|
|
104
|
+
* tell which build answered. Informational: whether a client is stale is
|
|
105
|
+
* decided per endpoint by the signature it sends (see
|
|
106
|
+
* Lambder.apiSignatures()), not by this string.
|
|
107
|
+
*/
|
|
102
108
|
apiVersion?: string;
|
|
103
109
|
/**
|
|
104
110
|
* Automatic compression for compressible responses. `true` (the default)
|
|
@@ -23,11 +23,6 @@ export const assertCreateOptions = (options) => {
|
|
|
23
23
|
if (options.apiPath !== undefined && (options.apiPath === "" || !options.apiPath.startsWith("/"))) {
|
|
24
24
|
throw new Error(`Lambder: apiPath must be a path starting with "/", got ${JSON.stringify(options.apiPath)}.`);
|
|
25
25
|
}
|
|
26
|
-
// "" is the one string that turns the version gate off while looking like
|
|
27
|
-
// it was set; say so rather than accepting every version a client names.
|
|
28
|
-
if (options.apiVersion === "") {
|
|
29
|
-
throw new Error("Lambder: apiVersion must not be empty. Leave it out to run without the version gate.");
|
|
30
|
-
}
|
|
31
26
|
// 0 or a negative ceiling turned every response into the size guard's own 500.
|
|
32
27
|
if (options.maxResponseBytes !== undefined)
|
|
33
28
|
assertPositiveInteger(options.maxResponseBytes, "maxResponseBytes");
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export { LambderAnswerHeaders, getAnswerHeader, setAnswerHeader, addAnswerHeader
|
|
|
25
25
|
export { createApiCallContext } from "./api/LambderApiCallContext.js";
|
|
26
26
|
export type { LambderApiCallContext, LambderApiCallTrace } from "./api/LambderApiCallContext.js";
|
|
27
27
|
export type { LambderApiDefinition } from "./api/LambderApiDefinition.js";
|
|
28
|
+
export { apiSignatureOf, LambderApiSignatureDigests } from "./api/LambderApiSignature.js";
|
|
29
|
+
export type { LambderApiSignatureSource } from "./api/LambderApiSignature.js";
|
|
30
|
+
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH } from "./shared/wire/LambderApiSignature.js";
|
|
31
|
+
export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignature.js";
|
|
32
|
+
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
28
33
|
export { buildApiEnvelope, envelopeAnswer, refusalAnswer, validationAnswer, apiNotFoundAnswer, sessionExpiredAnswer, versionExpiredAnswer, invalidPayloadAnswer, crashAnswer, API_ANSWER_CONTENT_TYPE, } from "./api/LambderApiEnvelope.js";
|
|
29
34
|
export type { LambderApiEnvelopeConfig, LambderValidationAnswerBody } from "./api/LambderApiEnvelope.js";
|
|
30
35
|
export { LambderApiValidationRefusal, isLambderApiValidationRefusal } from "./api/LambderApiValidationRefusal.js";
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,10 @@ export { readApiEnvelope, restoreCompressedPayload } from "./api/LambderApiReque
|
|
|
16
16
|
export { toHttpAnswer } from "./api/LambderApiAnswer.js";
|
|
17
17
|
export { LambderAnswerHeaders, getAnswerHeader, setAnswerHeader, addAnswerHeader } from "./shared/wire/LambderAnswerHeaders.js";
|
|
18
18
|
export { createApiCallContext } from "./api/LambderApiCallContext.js";
|
|
19
|
+
// Per-endpoint signatures: what a client build ships with, digested from the server's own registrations.
|
|
20
|
+
export { apiSignatureOf, LambderApiSignatureDigests } from "./api/LambderApiSignature.js";
|
|
21
|
+
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH } from "./shared/wire/LambderApiSignature.js";
|
|
22
|
+
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
19
23
|
export { buildApiEnvelope, envelopeAnswer, refusalAnswer, validationAnswer, apiNotFoundAnswer, sessionExpiredAnswer, versionExpiredAnswer, invalidPayloadAnswer, crashAnswer, API_ANSWER_CONTENT_TYPE, } from "./api/LambderApiEnvelope.js";
|
|
20
24
|
export { LambderApiValidationRefusal, isLambderApiValidationRefusal } from "./api/LambderApiValidationRefusal.js";
|
|
21
25
|
// Calling a Lambder app from another lambda (server-only: the Lambda SDK, zlib)
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { APIGatewayProxyEventV2, Context } from "aws-lambda";
|
|
24
24
|
import { type LambderInvokeFailure, type LambderInvokeOutcome } from "./LambderInvokeOutcome.js";
|
|
25
|
+
import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
|
|
25
26
|
import type { LambdaClient, LambdaClientConfig } from "@aws-sdk/client-lambda";
|
|
26
27
|
import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
|
|
27
28
|
import { type LambderCallArgs, type LambderContractOutputOf, type LambderGuardInputsProviderOption, type LambderSharedCallOptions } from "../shared/wire/LambderCallOptions.js";
|
|
@@ -92,8 +93,15 @@ type LambderInvokeCallerBaseOptions = {
|
|
|
92
93
|
clientConfig?: LambdaClientConfig;
|
|
93
94
|
/** Must match the callee's apiPath. Default: "/api". */
|
|
94
95
|
apiPath?: string;
|
|
95
|
-
/** Sent as `version
|
|
96
|
+
/** Sent as `version`, informational: the callee stamps its own on every answer. Default: none. */
|
|
96
97
|
apiVersion?: string;
|
|
98
|
+
/**
|
|
99
|
+
* The callee's signature map, generated from its instance
|
|
100
|
+
* (Lambder.apiSignatures()) when this caller was built. Sent per call as
|
|
101
|
+
* `signature`, so the callee answers versionExpired to a call built
|
|
102
|
+
* against another shape of the endpoint. Default: none, and no gate.
|
|
103
|
+
*/
|
|
104
|
+
apiSignatures?: LambderApiSignatureMap;
|
|
97
105
|
/** The Host the callee sees (ctx.host). Default: functionName. */
|
|
98
106
|
host?: string;
|
|
99
107
|
/**
|
|
@@ -146,6 +154,8 @@ export type LambderInvokeEventInit = {
|
|
|
146
154
|
/** Default: "lambder-invoke". */
|
|
147
155
|
host?: string;
|
|
148
156
|
apiVersion?: string;
|
|
157
|
+
/** The caller's signature for the endpoint, out of the callee's map. */
|
|
158
|
+
signature?: string;
|
|
149
159
|
guardInputs?: Record<string, unknown>;
|
|
150
160
|
idempotencyKey?: string;
|
|
151
161
|
clientIp?: string;
|
|
@@ -161,6 +171,7 @@ export default class LambderInvokeCaller<TContract extends LambderApiContractSha
|
|
|
161
171
|
private readonly functionName;
|
|
162
172
|
private readonly apiPath;
|
|
163
173
|
private readonly apiVersion?;
|
|
174
|
+
private readonly apiSignatures?;
|
|
164
175
|
private readonly host;
|
|
165
176
|
private readonly requestCompression;
|
|
166
177
|
private readonly maxResponsePayloadBytes;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { classifyDeliveryFailure, describeFailure, errorFromFunctionError, LambderInvokeError, parseFunctionError, } from "./LambderInvokeOutcome.js";
|
|
24
24
|
import { DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionCookieNames.js";
|
|
25
|
+
import { readApiSignature } from "../shared/wire/LambderApiSignature.js";
|
|
25
26
|
import { resolveApiOutcome } from "../shared/wire/LambderApiOutcome.js";
|
|
26
27
|
import { mergeGuardInputs, } from "../shared/wire/LambderCallOptions.js";
|
|
27
28
|
import { createCallAbort, stopWaitingWhenAborted } from "../shared/util/LambderCallAbort.js";
|
|
@@ -44,6 +45,7 @@ export default class LambderInvokeCaller {
|
|
|
44
45
|
functionName;
|
|
45
46
|
apiPath;
|
|
46
47
|
apiVersion;
|
|
48
|
+
apiSignatures;
|
|
47
49
|
host;
|
|
48
50
|
requestCompression;
|
|
49
51
|
maxResponsePayloadBytes;
|
|
@@ -57,7 +59,7 @@ export default class LambderInvokeCaller {
|
|
|
57
59
|
client;
|
|
58
60
|
sdk;
|
|
59
61
|
constructor(options) {
|
|
60
|
-
const { functionName, client, clientConfig, apiPath, apiVersion, host, requestCompression, maxResponsePayloadBytes, timeoutMs, onLogList, onFailure, sessionTokenCookieKey, transport, guardInputsProvider, } = options;
|
|
62
|
+
const { functionName, client, clientConfig, apiPath, apiVersion, apiSignatures, host, requestCompression, maxResponsePayloadBytes, timeoutMs, onLogList, onFailure, sessionTokenCookieKey, transport, guardInputsProvider, } = options;
|
|
61
63
|
if (!functionName?.trim())
|
|
62
64
|
throw new Error("LambderInvokeCaller: functionName is required");
|
|
63
65
|
this.functionName = functionName;
|
|
@@ -65,6 +67,7 @@ export default class LambderInvokeCaller {
|
|
|
65
67
|
this.clientConfig = clientConfig;
|
|
66
68
|
this.apiPath = apiPath ?? "/api";
|
|
67
69
|
this.apiVersion = apiVersion;
|
|
70
|
+
this.apiSignatures = apiSignatures;
|
|
68
71
|
this.host = host ?? functionName;
|
|
69
72
|
// `?? false`: like the browser caller, off unless asked for.
|
|
70
73
|
this.requestCompression = resolveCompressionOption(requestCompression ?? false, DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS);
|
|
@@ -93,6 +96,7 @@ export default class LambderInvokeCaller {
|
|
|
93
96
|
body: buildEnvelopeJson({
|
|
94
97
|
apiName: init.apiName,
|
|
95
98
|
version: init.apiVersion,
|
|
99
|
+
signature: init.signature,
|
|
96
100
|
csrf: init.session?.csrf,
|
|
97
101
|
siteHost: host,
|
|
98
102
|
payloadJson: init.payload !== undefined ? JSON.stringify(init.payload) : undefined,
|
|
@@ -290,6 +294,10 @@ export default class LambderInvokeCaller {
|
|
|
290
294
|
let event;
|
|
291
295
|
let eventJson;
|
|
292
296
|
try {
|
|
297
|
+
// The callee's signature for this endpoint, when this caller was
|
|
298
|
+
// built with the callee's map. A name the map lacks fails here,
|
|
299
|
+
// as a provider that threw would: the map predates the endpoint.
|
|
300
|
+
const signature = this.apiSignatures ? await readApiSignature(this.apiSignatures, apiName) : undefined;
|
|
293
301
|
// Provider values underneath, per-call values on top.
|
|
294
302
|
const provided = this.guardInputsProvider
|
|
295
303
|
? await this.guardInputsProvider(apiName)
|
|
@@ -314,6 +322,7 @@ export default class LambderInvokeCaller {
|
|
|
314
322
|
body: buildEnvelopeJson({
|
|
315
323
|
apiName,
|
|
316
324
|
version: this.apiVersion,
|
|
325
|
+
signature,
|
|
317
326
|
csrf: options.session?.csrf,
|
|
318
327
|
siteHost: this.host,
|
|
319
328
|
payloadJson: compressed ? undefined : payloadJson,
|
|
@@ -57,6 +57,7 @@ export declare const synthesizeLambdaHttpEvent: (request: LambderSynthesizedRequ
|
|
|
57
57
|
export declare const buildEnvelopeJson: (fields: {
|
|
58
58
|
apiName: string;
|
|
59
59
|
version?: string;
|
|
60
|
+
signature?: string;
|
|
60
61
|
csrf?: string;
|
|
61
62
|
siteHost: string;
|
|
62
63
|
/** The payload's own JSON, when it goes plainly. */
|
|
@@ -117,6 +117,7 @@ export const buildEnvelopeJson = (fields) => {
|
|
|
117
117
|
const withoutPayload = JSON.stringify(buildEnvelopeFields({
|
|
118
118
|
apiName: fields.apiName,
|
|
119
119
|
version: fields.version,
|
|
120
|
+
signature: fields.signature,
|
|
120
121
|
token: fields.csrf ?? "",
|
|
121
122
|
siteHost: fields.siteHost,
|
|
122
123
|
compressed: fields.compressed,
|
|
@@ -16,7 +16,7 @@ import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, L
|
|
|
16
16
|
* Lambda server runs) over memory stores, with a registry of typed mock
|
|
17
17
|
* handlers where the server has app handlers, and mock guards where it has
|
|
18
18
|
* app guards. Everything the protocol does (envelope, refusals, sessions
|
|
19
|
-
* and their cookies, guards, rate limits, idempotency, the
|
|
19
|
+
* and their cookies, guards, rate limits, idempotency, the signature gate)
|
|
20
20
|
* happens in the core; this class only resolves a name to an entry, wraps
|
|
21
21
|
* the handler's return into the envelope, and adds what a mock needs on
|
|
22
22
|
* top: failure injection, latency, a subscription, a call log, reset.
|
|
@@ -176,7 +176,7 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
176
176
|
*
|
|
177
177
|
* Public because the mode of an unregistered name cannot be recovered at
|
|
178
178
|
* runtime, the contract being a type. Everything that precedes dispatch
|
|
179
|
-
* still runs (the
|
|
179
|
+
* still runs (the signature gate, the payload restore); the session read is
|
|
180
180
|
* the one step this answer cannot have, which is the fidelity limit
|
|
181
181
|
* restNotMocked documents.
|
|
182
182
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lookupApiSignature } from "../shared/wire/LambderApiSignature.js";
|
|
1
2
|
import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
|
|
2
3
|
import { readApiEnvelope, cookieValuesByName, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
3
4
|
import { createApiCallContext } from "../api/LambderApiCallContext.js";
|
|
@@ -40,7 +41,7 @@ const defaultCookieHost = () => globalThis.location?.host || "localhost";
|
|
|
40
41
|
* Lambda server runs) over memory stores, with a registry of typed mock
|
|
41
42
|
* handlers where the server has app handlers, and mock guards where it has
|
|
42
43
|
* app guards. Everything the protocol does (envelope, refusals, sessions
|
|
43
|
-
* and their cookies, guards, rate limits, idempotency, the
|
|
44
|
+
* and their cookies, guards, rate limits, idempotency, the signature gate)
|
|
44
45
|
* happens in the core; this class only resolves a name to an entry, wraps
|
|
45
46
|
* the handler's return into the envelope, and adds what a mock needs on
|
|
46
47
|
* top: failure injection, latency, a subscription, a call log, reset.
|
|
@@ -116,8 +117,13 @@ export class LambderMockApp {
|
|
|
116
117
|
const idempotencyOptions = options.idempotency === true ? {} : options.idempotency || null;
|
|
117
118
|
const memoryIdempotency = idempotencyOptions && !idempotencyOptions.store ? new LambderMemoryIdempotencyStore() : null;
|
|
118
119
|
this.idempotencyStore = memoryIdempotency;
|
|
120
|
+
// The generated map stands in for the server's schemas: the runtime
|
|
121
|
+
// cannot digest what it does not hold, so it answers with the map's
|
|
122
|
+
// entry for the name and the pipeline compares, as on the server.
|
|
123
|
+
const apiSignatures = options.apiSignatures;
|
|
119
124
|
this.pipeline = new LambderApiPipeline({
|
|
120
125
|
apiVersion: this.apiVersion,
|
|
126
|
+
signatures: apiSignatures ? { expectedSignatureOf: (apiName) => lookupApiSignature(apiSignatures, apiName) } : undefined,
|
|
121
127
|
maxRequestPayloadBytes: options.maxRequestPayloadBytes,
|
|
122
128
|
sessions: sessionOptions
|
|
123
129
|
? {
|
|
@@ -351,7 +357,7 @@ export class LambderMockApp {
|
|
|
351
357
|
*
|
|
352
358
|
* Public because the mode of an unregistered name cannot be recovered at
|
|
353
359
|
* runtime, the contract being a type. Everything that precedes dispatch
|
|
354
|
-
* still runs (the
|
|
360
|
+
* still runs (the signature gate, the payload restore); the session read is
|
|
355
361
|
* the one step this answer cannot have, which is the fidelity limit
|
|
356
362
|
* restNotMocked documents.
|
|
357
363
|
*/
|
|
@@ -549,7 +555,7 @@ export class LambderMockApp {
|
|
|
549
555
|
return {
|
|
550
556
|
phase: "request", id, apiName: request.apiName, mode,
|
|
551
557
|
payload: request.payload, guardInputs: request.guardInputs, idempotencyKey: request.idempotencyKey,
|
|
552
|
-
version: request.version, headers: request.headers,
|
|
558
|
+
version: request.version, signature: request.signature, headers: request.headers,
|
|
553
559
|
hasSessionCookie: (request.cookies[this.tokenCookieKey]?.length ?? 0) > 0,
|
|
554
560
|
at,
|
|
555
561
|
};
|
|
@@ -593,16 +599,18 @@ export class LambderMockApp {
|
|
|
593
599
|
let outcome;
|
|
594
600
|
let error;
|
|
595
601
|
try {
|
|
596
|
-
// The protocol's pre-pass, run
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
|
|
602
|
+
// The protocol's pre-pass, run ahead of dispatch with the
|
|
603
|
+
// definition the name resolved to (null for a name nothing
|
|
604
|
+
// registered), which is where the server runs it. Two things
|
|
605
|
+
// depended on it: an unknown name reached the notFound refusal
|
|
606
|
+
// without the signature gate or the payload restore, so a stale
|
|
607
|
+
// client or a malformed compressed payload was answered
|
|
608
|
+
// differently here than on the server; and the request event
|
|
609
|
+
// carried the wire fields instead of the payload, so a dev panel
|
|
610
|
+
// watching calls in flight showed nothing for exactly the
|
|
611
|
+
// compressed calls someone opens a panel for. run() calls prepare
|
|
612
|
+
// again, which is safe by construction.
|
|
613
|
+
const prepared = await this.pipeline.prepare(request, registered?.definition ?? null);
|
|
606
614
|
this.emit(this.requestEvent(id, request, mode, startedAt));
|
|
607
615
|
await this.failures.wait(this.failures.latencyFor(request.apiName), request.signal);
|
|
608
616
|
if (this.failures.offline)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
|
|
1
2
|
import type { LambderContractGuardNames } from "../shared/wire/LambderApiContract.js";
|
|
2
3
|
import type { LambderApiGuard } from "../api/LambderApiGuards.js";
|
|
3
4
|
import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
|
|
@@ -99,8 +100,16 @@ type LambderMockGuardShapes<S, G> = {
|
|
|
99
100
|
[N in keyof G]: LambderMockSurplusKeys<G[N], LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>>;
|
|
100
101
|
};
|
|
101
102
|
export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicies<S> = LambderMockRateLimitPolicies<S>, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>> = LambderMockGuardsOption<C, S, G> & {
|
|
102
|
-
/**
|
|
103
|
+
/** Stamped on every answer's envelope as apiVersion, as the server's option is. */
|
|
103
104
|
apiVersion?: string;
|
|
105
|
+
/**
|
|
106
|
+
* The generated signature map the caller carries, so the runtime refuses a
|
|
107
|
+
* stale signature exactly as the server would: a call whose signature is
|
|
108
|
+
* not the map's entry for its endpoint answers versionExpired. Without it
|
|
109
|
+
* every signature passes, since the runtime holds no server schema to
|
|
110
|
+
* digest.
|
|
111
|
+
*/
|
|
112
|
+
apiSignatures?: LambderApiSignatureMap;
|
|
104
113
|
/** Artificial latency per call; off by default. */
|
|
105
114
|
latency?: LambderMockLatency;
|
|
106
115
|
/** Sessions over the memory store: `true` for the defaults, or the options. Off by default: session endpoints then fail at registration. */
|
|
@@ -1,9 +1 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* What a mock runtime is configured with, and the shapes of what a mock
|
|
3
|
-
* transport takes.
|
|
4
|
-
*
|
|
5
|
-
* Everything create() takes lives here, beside the rules that decide which
|
|
6
|
-
* keys it accepts (the guards option and the surplus-key checks under it),
|
|
7
|
-
* exactly as core/LambderCreateOptions.ts holds the server's.
|
|
8
|
-
*/
|
|
9
1
|
export {};
|
|
@@ -374,6 +374,8 @@ export type LambderMockRequestEvent = {
|
|
|
374
374
|
/** Exactly as posted, so `unknown`: the key is client data and only the idempotency engine judges it. */
|
|
375
375
|
idempotencyKey: unknown;
|
|
376
376
|
version: string | null;
|
|
377
|
+
/** The signature the caller sent for the endpoint; null when it carries no map. */
|
|
378
|
+
signature: string | null;
|
|
377
379
|
headers: Record<string, string>;
|
|
378
380
|
/** True when the request carried a session cookie. */
|
|
379
381
|
hasSessionCookie: boolean;
|
|
@@ -10,6 +10,8 @@ export type LambderApiTransportRequest = {
|
|
|
10
10
|
apiPath: string;
|
|
11
11
|
apiName: string;
|
|
12
12
|
version?: string;
|
|
13
|
+
/** The caller's signature for this endpoint, out of its LambderApiSignatureMap; absent when it carries no map. */
|
|
14
|
+
signature?: string;
|
|
13
15
|
/** The CSRF token the caller read from its cookie; "" when it holds none. */
|
|
14
16
|
token: string;
|
|
15
17
|
/**
|
|
@@ -101,6 +103,7 @@ export type LambderApiTransport = (request: LambderApiTransportRequest) => Promi
|
|
|
101
103
|
export declare const buildEnvelopeFields: (fields: {
|
|
102
104
|
apiName: string;
|
|
103
105
|
version?: string;
|
|
106
|
+
signature?: string;
|
|
104
107
|
/** The CSRF token, as the envelope names it. */
|
|
105
108
|
token: string;
|
|
106
109
|
siteHost: string;
|
|
@@ -29,6 +29,7 @@ export const isLambderTransportFailure = (err) => err instanceof Error && err.is
|
|
|
29
29
|
export const buildEnvelopeFields = (fields) => ({
|
|
30
30
|
apiName: fields.apiName,
|
|
31
31
|
version: fields.version,
|
|
32
|
+
...(fields.signature !== undefined ? { signature: fields.signature } : {}),
|
|
32
33
|
token: fields.token,
|
|
33
34
|
siteHost: fields.siteHost,
|
|
34
35
|
...(fields.compressed ?? fields.payloadSlot ?? {}),
|
|
@@ -43,6 +44,7 @@ export const buildEnvelopeFields = (fields) => ({
|
|
|
43
44
|
export const buildTransportEnvelope = (request) => buildEnvelopeFields({
|
|
44
45
|
apiName: request.apiName,
|
|
45
46
|
version: request.version,
|
|
47
|
+
signature: request.signature,
|
|
46
48
|
token: request.token,
|
|
47
49
|
siteHost: request.siteHost,
|
|
48
50
|
payloadSlot: { payload: request.payload },
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-endpoint signatures a client carries, generated from the server's
|
|
3
|
+
* own registrations (Lambder.apiSignatures()) and shipped with the client
|
|
4
|
+
* build. The key is the endpoint's name hashed (apiNameKeyOf); the value is
|
|
5
|
+
* the digest of its client-facing shape (apiSignatureOf, computed on the
|
|
6
|
+
* server side). A caller given the map sends the value with every call, and
|
|
7
|
+
* the server answers versionExpired when it differs from the digest of what
|
|
8
|
+
* it serves now. So a client built against an endpoint that has since
|
|
9
|
+
* changed reloads, while one whose endpoint is unchanged keeps working
|
|
10
|
+
* across deploys.
|
|
11
|
+
*
|
|
12
|
+
* Keys are hashed so the map lists no endpoint names: the names a client
|
|
13
|
+
* calls are in its own code already, and the rest of the surface stays out
|
|
14
|
+
* of the bundle.
|
|
15
|
+
*/
|
|
16
|
+
export type LambderApiSignatureMap = Record<string, string>;
|
|
17
|
+
/**
|
|
18
|
+
* How many hex characters a key and a signature keep. This is change
|
|
19
|
+
* detection, not authentication: 64 bits cannot collide by accident across
|
|
20
|
+
* the shapes one endpoint takes over its life, and the map stays small.
|
|
21
|
+
*/
|
|
22
|
+
export declare const API_SIGNATURE_HEX_LENGTH = 16;
|
|
23
|
+
/** The key an endpoint's signature is stored under: SHA-256 over the prefixed name, cut to API_SIGNATURE_HEX_LENGTH hex characters. */
|
|
24
|
+
export declare const apiNameKeyOf: (apiName: string) => Promise<string>;
|
|
25
|
+
/** The map's signature for one endpoint, or null when the map holds none for it. */
|
|
26
|
+
export declare const lookupApiSignature: (signatures: LambderApiSignatureMap, apiName: string) => Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* The signature a caller sends for one endpoint. A name the map does not
|
|
29
|
+
* hold throws: the map was generated from a server that did not have this
|
|
30
|
+
* endpoint, so the file is stale, and a call sent without a signature would
|
|
31
|
+
* run instead of saying so.
|
|
32
|
+
*/
|
|
33
|
+
export declare const readApiSignature: (signatures: LambderApiSignatureMap, apiName: string) => Promise<string>;
|