lambder 7.0.2 → 7.2.0
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 +112 -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 +41 -11
- package/dist/api/LambderApiPipeline.js +55 -13
- package/dist/api/LambderApiRequest.d.ts +3 -1
- package/dist/api/LambderApiRequest.js +1 -0
- package/dist/api/LambderApiSignature.d.ts +19 -0
- package/dist/api/LambderApiSignature.js +96 -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 +4 -0
- package/dist/client.js +4 -0
- package/dist/core/Lambder.d.ts +17 -1
- package/dist/core/Lambder.js +30 -5
- package/dist/core/LambderCreateOptions.d.ts +29 -0
- package/dist/core/LambderCreateOptions.js +0 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -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 +6 -4
- package/dist/mock/LambderMockCreateOptions.d.ts +6 -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 +46 -0
- package/dist/shared/wire/LambderApiSignature.js +45 -0
- package/dist/shared/wire/LambderVersionOrder.d.ts +11 -0
- package/dist/shared/wire/LambderVersionOrder.js +28 -0
- package/package.json +1 -1
|
@@ -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,46 @@
|
|
|
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
|
+
/**
|
|
24
|
+
* The key an endpoint's signature is stored under: SHA-256 over the prefixed
|
|
25
|
+
* name, cut to API_SIGNATURE_HEX_LENGTH hex characters. Async because
|
|
26
|
+
* WebCrypto's digest is, and it is the only SHA-256 a browser has.
|
|
27
|
+
*
|
|
28
|
+
* Computed on the spot, every time, and nothing is kept. The digest that
|
|
29
|
+
* actually describes an endpoint is the generator's, computed once at build
|
|
30
|
+
* time; what is left here is one hash of a short name against a map already
|
|
31
|
+
* in memory, which is nothing beside the request it belongs to. A cache of
|
|
32
|
+
* it would have to be keyed by name, and on the server the name comes off
|
|
33
|
+
* the wire before anything has checked that it is an endpoint at all, so it
|
|
34
|
+
* would grow by an entry for every name a request cared to invent and never
|
|
35
|
+
* shrink.
|
|
36
|
+
*/
|
|
37
|
+
export declare const apiNameKeyOf: (apiName: string) => Promise<string>;
|
|
38
|
+
/** The map's signature for one endpoint, or null when the map holds none for it. */
|
|
39
|
+
export declare const lookupApiSignature: (signatures: LambderApiSignatureMap, apiName: string) => Promise<string | null>;
|
|
40
|
+
/**
|
|
41
|
+
* The signature a caller sends for one endpoint. A name the map does not
|
|
42
|
+
* hold throws: the map was generated from a server that did not have this
|
|
43
|
+
* endpoint, so the file is stale, and a call sent without a signature would
|
|
44
|
+
* run instead of saying so.
|
|
45
|
+
*/
|
|
46
|
+
export declare const readApiSignature: (signatures: LambderApiSignatureMap, apiName: string) => Promise<string>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { sha256HexOf } from "../util/LambderTextDigest.js";
|
|
2
|
+
/**
|
|
3
|
+
* How many hex characters a key and a signature keep. This is change
|
|
4
|
+
* detection, not authentication: 64 bits cannot collide by accident across
|
|
5
|
+
* the shapes one endpoint takes over its life, and the map stays small.
|
|
6
|
+
*/
|
|
7
|
+
export const API_SIGNATURE_HEX_LENGTH = 16;
|
|
8
|
+
/** Domain-separated, so a name's key can never equal a signature computed over a description that happens to read the same. */
|
|
9
|
+
const API_NAME_KEY_PREFIX = "lambder-api-name:";
|
|
10
|
+
/**
|
|
11
|
+
* The key an endpoint's signature is stored under: SHA-256 over the prefixed
|
|
12
|
+
* name, cut to API_SIGNATURE_HEX_LENGTH hex characters. Async because
|
|
13
|
+
* WebCrypto's digest is, and it is the only SHA-256 a browser has.
|
|
14
|
+
*
|
|
15
|
+
* Computed on the spot, every time, and nothing is kept. The digest that
|
|
16
|
+
* actually describes an endpoint is the generator's, computed once at build
|
|
17
|
+
* time; what is left here is one hash of a short name against a map already
|
|
18
|
+
* in memory, which is nothing beside the request it belongs to. A cache of
|
|
19
|
+
* it would have to be keyed by name, and on the server the name comes off
|
|
20
|
+
* the wire before anything has checked that it is an endpoint at all, so it
|
|
21
|
+
* would grow by an entry for every name a request cared to invent and never
|
|
22
|
+
* shrink.
|
|
23
|
+
*/
|
|
24
|
+
export const apiNameKeyOf = async (apiName) => (await sha256HexOf(API_NAME_KEY_PREFIX + apiName)).slice(0, API_SIGNATURE_HEX_LENGTH);
|
|
25
|
+
/** The map's signature for one endpoint, or null when the map holds none for it. */
|
|
26
|
+
export const lookupApiSignature = async (signatures, apiName) => {
|
|
27
|
+
const key = await apiNameKeyOf(apiName);
|
|
28
|
+
// Own properties only: the map is a plain object, and a key that happened
|
|
29
|
+
// to spell a prototype member would otherwise read a function.
|
|
30
|
+
const signature = Object.prototype.hasOwnProperty.call(signatures, key) ? signatures[key] : undefined;
|
|
31
|
+
return typeof signature === "string" ? signature : null;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The signature a caller sends for one endpoint. A name the map does not
|
|
35
|
+
* hold throws: the map was generated from a server that did not have this
|
|
36
|
+
* endpoint, so the file is stale, and a call sent without a signature would
|
|
37
|
+
* run instead of saying so.
|
|
38
|
+
*/
|
|
39
|
+
export const readApiSignature = async (signatures, apiName) => {
|
|
40
|
+
const signature = await lookupApiSignature(signatures, apiName);
|
|
41
|
+
if (signature === null) {
|
|
42
|
+
throw new Error(`Lambder: apiSignatures holds no signature for API "${apiName}". The map predates this endpoint; regenerate it from the server's apiSignatures().`);
|
|
43
|
+
}
|
|
44
|
+
return signature;
|
|
45
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dotted version strings ("1.2.10"), compared segment by segment as numbers,
|
|
3
|
+
* so "1.2.10" sorts after "1.2.9" where a string comparison would put it
|
|
4
|
+
* first. The server's version floor (minApiVersion) reads a caller's version
|
|
5
|
+
* this way, and an app deciding whether a client is behind can read the
|
|
6
|
+
* envelope's apiVersion the same way.
|
|
7
|
+
*/
|
|
8
|
+
/** True for one or more decimal segments joined by dots: "7", "1.2", "1.2.10". */
|
|
9
|
+
export declare const isDottedVersion: (value: string) => boolean;
|
|
10
|
+
/** -1 when `a` is older than `b`, 1 when newer, 0 when equal. A missing segment counts as 0, so "1.2" equals "1.2.0". */
|
|
11
|
+
export declare const compareDottedVersions: (a: string, b: string) => -1 | 0 | 1;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dotted version strings ("1.2.10"), compared segment by segment as numbers,
|
|
3
|
+
* so "1.2.10" sorts after "1.2.9" where a string comparison would put it
|
|
4
|
+
* first. The server's version floor (minApiVersion) reads a caller's version
|
|
5
|
+
* this way, and an app deciding whether a client is behind can read the
|
|
6
|
+
* envelope's apiVersion the same way.
|
|
7
|
+
*/
|
|
8
|
+
/** True for one or more decimal segments joined by dots: "7", "1.2", "1.2.10". */
|
|
9
|
+
export const isDottedVersion = (value) => /^\d+(\.\d+)*$/.test(value);
|
|
10
|
+
/** A segment as a number; anything that is not one counts as 0, so a version nothing can read sorts below every real one. */
|
|
11
|
+
const segmentOf = (text) => {
|
|
12
|
+
const parsed = parseInt(text, 10);
|
|
13
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
14
|
+
};
|
|
15
|
+
/** -1 when `a` is older than `b`, 1 when newer, 0 when equal. A missing segment counts as 0, so "1.2" equals "1.2.0". */
|
|
16
|
+
export const compareDottedVersions = (a, b) => {
|
|
17
|
+
const left = a.split(".").map(segmentOf);
|
|
18
|
+
const right = b.split(".").map(segmentOf);
|
|
19
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
20
|
+
const x = left[i] ?? 0;
|
|
21
|
+
const y = right[i] ?? 0;
|
|
22
|
+
if (x < y)
|
|
23
|
+
return -1;
|
|
24
|
+
if (x > y)
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.2.0",
|
|
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": [
|