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.
Files changed (40) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/api/LambderApiDefinition.d.ts +6 -3
  3. package/dist/api/LambderApiEnvelope.d.ts +1 -1
  4. package/dist/api/LambderApiEnvelope.js +1 -1
  5. package/dist/api/LambderApiGuards.d.ts +5 -0
  6. package/dist/api/LambderApiGuards.js +2 -2
  7. package/dist/api/LambderApiPipeline.d.ts +41 -11
  8. package/dist/api/LambderApiPipeline.js +55 -13
  9. package/dist/api/LambderApiRequest.d.ts +3 -1
  10. package/dist/api/LambderApiRequest.js +1 -0
  11. package/dist/api/LambderApiSignature.d.ts +19 -0
  12. package/dist/api/LambderApiSignature.js +96 -0
  13. package/dist/client/LambderCaller.d.ts +13 -0
  14. package/dist/client/LambderCaller.js +21 -1
  15. package/dist/client/LambderReloadLoopBreaker.d.ts +38 -0
  16. package/dist/client/LambderReloadLoopBreaker.js +71 -0
  17. package/dist/client.d.ts +4 -0
  18. package/dist/client.js +4 -0
  19. package/dist/core/Lambder.d.ts +17 -1
  20. package/dist/core/Lambder.js +30 -5
  21. package/dist/core/LambderCreateOptions.d.ts +29 -0
  22. package/dist/core/LambderCreateOptions.js +0 -5
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +5 -0
  25. package/dist/invoke/LambderInvokeCaller.d.ts +12 -1
  26. package/dist/invoke/LambderInvokeCaller.js +10 -1
  27. package/dist/invoke/LambderLambdaEvent.d.ts +1 -0
  28. package/dist/invoke/LambderLambdaEvent.js +1 -0
  29. package/dist/mock/LambderMockApp.d.ts +2 -2
  30. package/dist/mock/LambderMockApp.js +6 -4
  31. package/dist/mock/LambderMockCreateOptions.d.ts +6 -1
  32. package/dist/mock/LambderMockCreateOptions.js +0 -8
  33. package/dist/mock/LambderMockTypes.d.ts +2 -0
  34. package/dist/shared/transport/LambderApiTransport.d.ts +3 -0
  35. package/dist/shared/transport/LambderApiTransport.js +2 -0
  36. package/dist/shared/wire/LambderApiSignature.d.ts +46 -0
  37. package/dist/shared/wire/LambderApiSignature.js +45 -0
  38. package/dist/shared/wire/LambderVersionOrder.d.ts +11 -0
  39. package/dist/shared/wire/LambderVersionOrder.js +28 -0
  40. 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.2",
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": [