lambder 8.1.2 → 8.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +8 -7
  3. package/dist/api/LambderApiGuards.d.ts +2 -17
  4. package/dist/api/LambderApiRateLimits.d.ts +2 -29
  5. package/dist/build/generatedTables.d.ts +72 -0
  6. package/dist/build/generatedTables.js +99 -0
  7. package/dist/build/writeApiGuardParams.d.ts +60 -0
  8. package/dist/build/writeApiGuardParams.js +85 -0
  9. package/dist/build/writeApiOptions.d.ts +68 -0
  10. package/dist/build/writeApiOptions.js +102 -0
  11. package/dist/build.d.ts +10 -4
  12. package/dist/build.js +7 -4
  13. package/dist/client/LambderUploadRunner.d.ts +7 -7
  14. package/dist/client/LambderUploadRunner.js +12 -21
  15. package/dist/client.d.ts +7 -0
  16. package/dist/client.js +11 -0
  17. package/dist/core/Lambder.d.ts +21 -0
  18. package/dist/core/Lambder.js +69 -0
  19. package/dist/index.d.ts +13 -0
  20. package/dist/index.js +13 -0
  21. package/dist/mock/LambderMockApp.d.ts +34 -17
  22. package/dist/mock/LambderMockApp.js +67 -21
  23. package/dist/mock/LambderMockCreateOptions.d.ts +68 -5
  24. package/dist/mock/LambderMockTypes.d.ts +29 -10
  25. package/dist/mock/lambderMockPoliciesFrom.d.ts +51 -0
  26. package/dist/mock/lambderMockPoliciesFrom.js +46 -0
  27. package/dist/mock.d.ts +3 -0
  28. package/dist/mock.js +3 -0
  29. package/dist/secrets/LambderOneShotSecrets.d.ts +166 -0
  30. package/dist/secrets/LambderOneShotSecrets.js +217 -0
  31. package/dist/session/LambderSessionCrypto.js +6 -16
  32. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +3 -2
  33. package/dist/shared/contracts/LambderOneShotSecretStore.d.ts +122 -0
  34. package/dist/shared/contracts/LambderOneShotSecretStore.js +38 -0
  35. package/dist/shared/util/LambderBackoffTimer.d.ts +82 -0
  36. package/dist/shared/util/LambderBackoffTimer.js +86 -0
  37. package/dist/shared/util/LambderBase64.d.ts +14 -0
  38. package/dist/shared/util/LambderBase64.js +17 -0
  39. package/dist/shared/util/LambderSignedClaims.d.ts +78 -0
  40. package/dist/shared/util/LambderSignedClaims.js +109 -0
  41. package/dist/shared/util/LambderTextDigest.d.ts +19 -5
  42. package/dist/shared/util/LambderTextDigest.js +30 -5
  43. package/dist/shared/util/assertPlainData.d.ts +9 -0
  44. package/dist/shared/util/assertPlainData.js +41 -0
  45. package/dist/shared/wire/LambderApiOptionEntries.d.ts +148 -0
  46. package/dist/shared/wire/LambderApiOptionEntries.js +35 -0
  47. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +64 -0
  48. package/dist/stores/LambderDdbOneShotSecretStore.js +266 -0
  49. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +3 -2
  50. package/dist/stores/LambderMemoryIdempotencyStore.js +3 -2
  51. package/dist/stores/LambderMemoryOneShotSecretStore.d.ts +36 -0
  52. package/dist/stores/LambderMemoryOneShotSecretStore.js +93 -0
  53. package/dist/testing/LambderConformanceRunner.d.ts +46 -0
  54. package/dist/testing/LambderConformanceRunner.js +21 -0
  55. package/dist/testing/lambderIdempotencyStoreConformance.d.ts +33 -0
  56. package/dist/testing/lambderIdempotencyStoreConformance.js +237 -0
  57. package/dist/testing/lambderOneShotSecretStoreConformance.d.ts +43 -0
  58. package/dist/testing/lambderOneShotSecretStoreConformance.js +224 -0
  59. package/dist/testing/lambderRateLimiterConformance.d.ts +20 -0
  60. package/dist/testing/lambderRateLimiterConformance.js +72 -0
  61. package/dist/testing/lambderSessionStoreConformance.d.ts +27 -0
  62. package/dist/testing/lambderSessionStoreConformance.js +165 -0
  63. package/dist/testing.d.ts +14 -0
  64. package/dist/testing.js +12 -0
  65. package/package.json +1 -1
@@ -0,0 +1,165 @@
1
+ import { CONFORMANCE_START_MILLIS as START, conformanceClock, } from "./LambderConformanceRunner.js";
2
+ /**
3
+ * Registers the session store rules as cases of the runner, one `it` each,
4
+ * against the store `create` builds:
5
+ *
6
+ * ```ts
7
+ * import { describe, it, expect } from "vitest";
8
+ * import { lambderSessionStoreConformance } from "lambder/testing";
9
+ *
10
+ * describe("PostgresSessionStore", () => {
11
+ * lambderSessionStoreConformance({ it, expect, create: async () => { await emptySessions(); return new PostgresSessionStore(pool); } });
12
+ * });
13
+ * ```
14
+ *
15
+ * The records a case writes are dated from the case's clock, which starts at
16
+ * a fixed moment and stands still unless a case moves it: a store that
17
+ * expires records by the system clock, rather than by `now`, finds them all
18
+ * long expired.
19
+ */
20
+ export const lambderSessionStoreConformance = (options) => {
21
+ const { it, expect } = options;
22
+ const isMemoryOnly = options.isMemoryOnly ?? false;
23
+ const startSeconds = Math.floor(START / 1000);
24
+ const begin = async () => await options.create(conformanceClock());
25
+ const sessionRecord = (over = {}) => ({
26
+ sessionKeyHash: "partition-a",
27
+ secretHash: "secret-1",
28
+ csrfTokenHash: "csrf-1",
29
+ sessionKey: "user-1",
30
+ data: { name: "Ada" },
31
+ createdAt: startSeconds,
32
+ expiresAt: startSeconds + 3600,
33
+ lastAccessedAt: startSeconds,
34
+ ttlInSeconds: 3600,
35
+ dataVersion: 0,
36
+ ...over,
37
+ });
38
+ it("says whether it outlives the process, which is what gates non-cryptographic hashing", async () => {
39
+ expect((await begin()).isMemoryOnly).toBe(isMemoryOnly);
40
+ });
41
+ it("round-trips a record under its two hashes", async () => {
42
+ const store = await begin();
43
+ await store.create(sessionRecord());
44
+ const read = await store.get("partition-a", "secret-1");
45
+ expect(read).toMatchObject({ sessionKey: "user-1", csrfTokenHash: "csrf-1", data: { name: "Ada" }, dataVersion: 0 });
46
+ });
47
+ it("answers null for a record that is absent, never throws", async () => {
48
+ const store = await begin();
49
+ expect(await store.get("partition-a", "nope")).toBeNull();
50
+ expect(await store.get("no-such-partition", "secret-1")).toBeNull();
51
+ });
52
+ it("refuses to create a record over an existing one: a session is minted once", async () => {
53
+ const store = await begin();
54
+ await store.create(sessionRecord());
55
+ await expect(store.create(sessionRecord({ data: { name: "Grace" } }))).rejects.toThrow();
56
+ expect(await store.get("partition-a", "secret-1")).toMatchObject({ data: { name: "Ada" } });
57
+ expect(await store.listSecretHashes("partition-a")).toEqual(["secret-1"]);
58
+ });
59
+ it("lists the secret hashes of one partition and nothing from another", async () => {
60
+ const store = await begin();
61
+ await store.create(sessionRecord({ secretHash: "secret-1" }));
62
+ await store.create(sessionRecord({ secretHash: "secret-2" }));
63
+ await store.create(sessionRecord({ sessionKeyHash: "partition-b", secretHash: "secret-3" }));
64
+ expect((await store.listSecretHashes("partition-a")).sort()).toEqual(["secret-1", "secret-2"]);
65
+ expect(await store.listSecretHashes("partition-b")).toEqual(["secret-3"]);
66
+ expect(await store.listSecretHashes("partition-empty")).toEqual([]);
67
+ });
68
+ it("deletes one session without touching its siblings", async () => {
69
+ const store = await begin();
70
+ await store.create(sessionRecord({ secretHash: "secret-1" }));
71
+ await store.create(sessionRecord({ secretHash: "secret-2" }));
72
+ expect(await store.delete("partition-a", "secret-1")).toMatchObject({ secretHash: "secret-1", sessionKey: "user-1" });
73
+ expect(await store.get("partition-a", "secret-1")).toBeNull();
74
+ expect(await store.get("partition-a", "secret-2")).not.toBeNull();
75
+ });
76
+ it("hands back the record as it was stored when deleted, a later write included", async () => {
77
+ const store = await begin();
78
+ await store.create(sessionRecord());
79
+ await store.update("partition-a", "secret-1", { data: { name: "Grace" } });
80
+ expect((await store.delete("partition-a", "secret-1"))?.data).toEqual({ name: "Grace" });
81
+ });
82
+ it("deleting a record that is already gone is a no-op that answers null, not an error", async () => {
83
+ const store = await begin();
84
+ await expect(store.delete("partition-a", "never-existed")).resolves.toBeNull();
85
+ });
86
+ it("updates the named fields of a live record and leaves the rest", async () => {
87
+ const store = await begin();
88
+ await store.create(sessionRecord());
89
+ const at = startSeconds + 30;
90
+ expect(await store.update("partition-a", "secret-1", { dataExpiresAt: at })).toBe("updated");
91
+ expect(await store.update("partition-a", "secret-1", { data: { name: "Grace" }, expiresAt: at + 7200, lastAccessedAt: at })).toBe("updated");
92
+ expect(await store.get("partition-a", "secret-1")).toMatchObject({
93
+ data: { name: "Grace" }, dataExpiresAt: at, expiresAt: at + 7200, lastAccessedAt: at,
94
+ sessionKey: "user-1", csrfTokenHash: "csrf-1", createdAt: startSeconds,
95
+ });
96
+ });
97
+ it("never brings back a record that vanished: an update of one answers missing", async () => {
98
+ // A logout that lands while a renewal is in flight must stay a
99
+ // logout, so an update of a deleted record writes nothing at all.
100
+ const store = await begin();
101
+ await store.create(sessionRecord());
102
+ await store.delete("partition-a", "secret-1");
103
+ expect(await store.update("partition-a", "secret-1", { expiresAt: startSeconds + 7200 })).toBe("missing");
104
+ expect(await store.update("partition-a", "secret-1", { data: { name: "Grace" } }, { dataVersion: 0 })).toBe("missing");
105
+ expect(await store.get("partition-a", "secret-1")).toBeNull();
106
+ });
107
+ it("moves dataVersion by one on each write of data or dataExpiresAt, and on no other write", async () => {
108
+ const store = await begin();
109
+ await store.create(sessionRecord());
110
+ const at = startSeconds + 30;
111
+ const versionNow = async () => (await store.get("partition-a", "secret-1"))?.dataVersion;
112
+ await store.update("partition-a", "secret-1", { lastAccessedAt: at, expiresAt: at + 3600 });
113
+ expect(await versionNow()).toBe(0);
114
+ await store.update("partition-a", "secret-1", { data: { name: "Grace" } });
115
+ expect(await versionNow()).toBe(1);
116
+ await store.update("partition-a", "secret-1", { dataExpiresAt: at });
117
+ expect(await versionNow()).toBe(2);
118
+ // One write carrying both, and the slide beside them, is one step.
119
+ await store.update("partition-a", "secret-1", { data: { name: "Lin" }, dataExpiresAt: at + 600, lastAccessedAt: at });
120
+ expect(await versionNow()).toBe(3);
121
+ });
122
+ it("applies a conditioned update only while dataVersion is the one it was read with, and says stale otherwise", async () => {
123
+ const store = await begin();
124
+ await store.create(sessionRecord());
125
+ const at = startSeconds + 30;
126
+ expect(await store.update("partition-a", "secret-1", { data: { name: "Grace" }, dataExpiresAt: at }, { dataVersion: 1 })).toBe("stale");
127
+ expect(await store.update("partition-a", "secret-1", { data: { name: "Grace" }, dataExpiresAt: at }, { dataVersion: 0 })).toBe("updated");
128
+ // That write moved the version, so the one it named no longer holds.
129
+ expect(await store.update("partition-a", "secret-1", { data: { name: "Lin" } }, { dataVersion: 0 })).toBe("stale");
130
+ expect(await store.update("partition-a", "secret-1", { data: { name: "Lin" } }, { dataVersion: 1 })).toBe("updated");
131
+ expect(await store.get("partition-a", "secret-1")).toMatchObject({ data: { name: "Lin" }, dataExpiresAt: at, dataVersion: 2 });
132
+ });
133
+ it("answers stale after a mark that wrote the deadline already stored, in the second a refresh read it", async () => {
134
+ // The record falls due this second, a read starts its refresh, and a
135
+ // revocation marks it due in that same second. The deadline reads
136
+ // exactly as the refresh read it, so a condition on the deadline
137
+ // would let the refresh land data derived before the revocation for
138
+ // a whole data TTL. Only the version shows the mark.
139
+ const store = await begin();
140
+ await store.create(sessionRecord({ dataExpiresAt: startSeconds }));
141
+ const read = (await store.get("partition-a", "secret-1"));
142
+ expect(await store.update("partition-a", "secret-1", { dataExpiresAt: startSeconds })).toBe("updated");
143
+ expect(await store.update("partition-a", "secret-1", { data: { name: "Grace" }, dataExpiresAt: startSeconds + 600 }, { dataVersion: read.dataVersion })).toBe("stale");
144
+ expect(await store.get("partition-a", "secret-1")).toMatchObject({ data: { name: "Ada" }, dataExpiresAt: startSeconds });
145
+ });
146
+ it("answers stale after a second mark in the second of the first, which a refresh of the first did not see", async () => {
147
+ // The first revocation marks the record due, a read starts a refresh
148
+ // that sees only that one, and a second revocation marks it again in
149
+ // the same second. Both marks write the same deadline, so only the
150
+ // version tells the refresh that it is one revocation behind.
151
+ const store = await begin();
152
+ await store.create(sessionRecord({ dataExpiresAt: startSeconds + 600 }));
153
+ await store.update("partition-a", "secret-1", { dataExpiresAt: startSeconds });
154
+ const read = (await store.get("partition-a", "secret-1"));
155
+ await store.update("partition-a", "secret-1", { dataExpiresAt: startSeconds });
156
+ expect(await store.update("partition-a", "secret-1", { data: { name: "Grace" }, dataExpiresAt: startSeconds + 600 }, { dataVersion: read.dataVersion })).toBe("stale");
157
+ expect(await store.get("partition-a", "secret-1")).toMatchObject({ data: { name: "Ada" }, dataExpiresAt: startSeconds, dataVersion: 2 });
158
+ });
159
+ it("holds data with an undefined inside it the way JSON does, rather than refusing it", async () => {
160
+ const store = await begin();
161
+ await store.create(sessionRecord({ data: { name: "Ada", nickname: undefined } }));
162
+ await store.update("partition-a", "secret-1", { data: { name: "Grace", nickname: undefined } });
163
+ expect((await store.get("partition-a", "secret-1"))?.data).toEqual({ name: "Grace" });
164
+ });
165
+ };
package/dist/testing.d.ts CHANGED
@@ -9,15 +9,29 @@
9
9
  *
10
10
  * The sibling of `lambder/mock`, which serves a contract from mock handlers
11
11
  * for frontend work. This one runs the real handlers.
12
+ *
13
+ * Beside the test app, the store conformance suites: the rules each store
14
+ * interface promises the engines, for an app to run against a store it
15
+ * writes itself.
12
16
  */
13
17
  export { lambderTestApp } from "./testing/LambderTestApp.js";
14
18
  export type { LambderTestApp, LambderTestAppOptions, LambderTestedInstance } from "./testing/LambderTestApp.js";
15
19
  export type { LambderTestVisitor, LambderTestVisitorOptions, LambderTestRequestInit } from "./testing/LambderTestVisitor.js";
16
20
  export { assertApiSuccess, assertApiFailure } from "./shared/wire/LambderOutcomeAssertions.js";
17
21
  export type { LambderExpectedFailure } from "./shared/wire/LambderOutcomeAssertions.js";
22
+ export { lambderSessionStoreConformance } from "./testing/lambderSessionStoreConformance.js";
23
+ export type { LambderSessionStoreConformanceOptions } from "./testing/lambderSessionStoreConformance.js";
24
+ export { lambderIdempotencyStoreConformance } from "./testing/lambderIdempotencyStoreConformance.js";
25
+ export type { LambderIdempotencyStoreConformanceOptions } from "./testing/lambderIdempotencyStoreConformance.js";
26
+ export { lambderRateLimiterConformance } from "./testing/lambderRateLimiterConformance.js";
27
+ export type { LambderRateLimiterConformanceOptions } from "./testing/lambderRateLimiterConformance.js";
28
+ export { lambderOneShotSecretStoreConformance } from "./testing/lambderOneShotSecretStoreConformance.js";
29
+ export type { LambderOneShotSecretStoreConformanceOptions } from "./testing/lambderOneShotSecretStoreConformance.js";
30
+ export type { LambderConformanceRunner, LambderConformanceIt, LambderConformanceExpect, LambderConformanceAssertion, LambderConformanceSetup, } from "./testing/LambderConformanceRunner.js";
18
31
  export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
19
32
  export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
20
33
  export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
34
+ export { LambderMemoryOneShotSecretStore } from "./stores/LambderMemoryOneShotSecretStore.js";
21
35
  export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
22
36
  export { LambderMemoryUploadBucket } from "./stores/LambderMemoryUploadBucket.js";
23
37
  export type { LambderMemoryUploadBucketOptions, LambderMemoryUploadObject } from "./stores/LambderMemoryUploadBucket.js";
package/dist/testing.js CHANGED
@@ -9,15 +9,27 @@
9
9
  *
10
10
  * The sibling of `lambder/mock`, which serves a contract from mock handlers
11
11
  * for frontend work. This one runs the real handlers.
12
+ *
13
+ * Beside the test app, the store conformance suites: the rules each store
14
+ * interface promises the engines, for an app to run against a store it
15
+ * writes itself.
12
16
  */
13
17
  export { lambderTestApp } from "./testing/LambderTestApp.js";
14
18
  export { assertApiSuccess, assertApiFailure } from "./shared/wire/LambderOutcomeAssertions.js";
19
+ // The rules each store interface promises, as cases an app registers with its
20
+ // own runner's `it` and `expect` to hold a store it writes over its own
21
+ // database to the same rules Lambder's memory and DynamoDB stores meet.
22
+ export { lambderSessionStoreConformance } from "./testing/lambderSessionStoreConformance.js";
23
+ export { lambderIdempotencyStoreConformance } from "./testing/lambderIdempotencyStoreConformance.js";
24
+ export { lambderRateLimiterConformance } from "./testing/lambderRateLimiterConformance.js";
25
+ export { lambderOneShotSecretStoreConformance } from "./testing/lambderOneShotSecretStoreConformance.js";
15
26
  // What a test reaches for beside the test app: the stores to inspect or to
16
27
  // hand it, a file source over fixtures, the refusal codes to assert on, and
17
28
  // the types of what a visitor and its session hand back.
18
29
  export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
19
30
  export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
20
31
  export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
32
+ export { LambderMemoryOneShotSecretStore } from "./stores/LambderMemoryOneShotSecretStore.js";
21
33
  export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
22
34
  export { LambderMemoryUploadBucket } from "./stores/LambderMemoryUploadBucket.js";
23
35
  export { LambderLocalFileSource } from "./stores/LambderLocalFileSource.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "8.1.2",
3
+ "version": "8.3.1",
4
4
  "sideEffects": false,
5
5
  "description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
6
6
  "keywords": [