lambder 1.0.128 → 1.0.130
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/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +90 -0
- package/Readme.md +49 -0
- package/dist/Lambder.d.ts +5 -4
- package/dist/Lambder.js +2 -2
- package/dist/LambderMSW.d.ts +104 -0
- package/dist/LambderMSW.js +203 -0
- package/dist/LambderResolver.d.ts +17 -15
- package/dist/LambderResolver.js +4 -0
- package/dist/LambderResponseBuilder.d.ts +2 -1
- package/dist/LambderSessionController.d.ts +1 -0
- package/dist/LambderSessionController.js +10 -0
- package/dist/LambderSessionManager.d.ts +6 -1
- package/dist/LambderSessionManager.js +43 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/docs/DYNAMODB_SETUP.md +96 -0
- package/docs/MSW_MOCKING.md +590 -0
- package/docs/TYPE_SAFE_QUICK_START.md +48 -4
- package/examples/msw-mocking-example.ts +241 -0
- package/examples/output-type-enforcement-example.ts +218 -0
- package/examples/secure-session-example.ts +191 -0
- package/examples/test-output-type-enforcement.ts +101 -0
- package/package.json +9 -1
- package/src/Lambder.ts +4 -4
- package/src/LambderMSW.ts +330 -0
- package/src/LambderResolver.ts +32 -15
- package/src/LambderResponseBuilder.ts +2 -1
- package/src/LambderSessionController.ts +9 -0
- package/src/LambderSessionManager.ts +51 -10
- package/src/index.ts +11 -0
- package/tests/msw-integration.test.ts +183 -0
- package/tests/output-type-runtime.test.ts +365 -0
- package/tests/type-safety.test.ts +312 -0
|
@@ -7,11 +7,13 @@ export default class LambderSessionManager {
|
|
|
7
7
|
partitionKey;
|
|
8
8
|
sortKey;
|
|
9
9
|
ddbDocumentClient;
|
|
10
|
-
|
|
10
|
+
enableSlidingExpiration;
|
|
11
|
+
constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration = true, }) {
|
|
11
12
|
this.tableName = tableName;
|
|
12
13
|
this.sessionSalt = sessionSalt;
|
|
13
14
|
this.partitionKey = partitionKey;
|
|
14
15
|
this.sortKey = sortKey;
|
|
16
|
+
this.enableSlidingExpiration = enableSlidingExpiration;
|
|
15
17
|
const ddbClient = new DynamoDBClient({ region: tableRegion });
|
|
16
18
|
this.ddbDocumentClient = DynamoDBDocumentClient.from(ddbClient);
|
|
17
19
|
}
|
|
@@ -20,6 +22,13 @@ export default class LambderSessionManager {
|
|
|
20
22
|
.update(`${password}${this.sessionSalt}`)
|
|
21
23
|
.digest("hex");
|
|
22
24
|
}
|
|
25
|
+
constantTimeCompare(a, b) {
|
|
26
|
+
if (a.length !== b.length)
|
|
27
|
+
return false;
|
|
28
|
+
const bufferA = Buffer.from(a, 'utf8');
|
|
29
|
+
const bufferB = Buffer.from(b, 'utf8');
|
|
30
|
+
return crypto.timingSafeEqual(new Uint8Array(bufferA), new Uint8Array(bufferB));
|
|
31
|
+
}
|
|
23
32
|
async ddbGetItem(key) {
|
|
24
33
|
const response = await this.ddbDocumentClient.send(new GetCommand({ TableName: this.tableName, Key: key, ConsistentRead: true }));
|
|
25
34
|
if (response.Item)
|
|
@@ -67,15 +76,16 @@ export default class LambderSessionManager {
|
|
|
67
76
|
const sessionKeyHash = this.sessionUserKeyHasher(sessionKey);
|
|
68
77
|
const sessionSortKey = crypto.randomBytes(32).toString("hex");
|
|
69
78
|
const sessionToken = `${sessionKeyHash}:${sessionSortKey}`;
|
|
70
|
-
const csrfToken = crypto.randomBytes(
|
|
79
|
+
const csrfToken = crypto.randomBytes(32).toString("hex");
|
|
71
80
|
const createdAt = Math.floor(Date.now() / 1000);
|
|
81
|
+
const lastAccessedAt = createdAt;
|
|
72
82
|
const expiresAt = Number(createdAt) + Number(ttlInSeconds);
|
|
73
83
|
const session = {
|
|
74
84
|
[this.partitionKey]: sessionKeyHash,
|
|
75
85
|
[this.sortKey]: sessionSortKey,
|
|
76
86
|
sessionToken, csrfToken,
|
|
77
87
|
sessionKey, data,
|
|
78
|
-
createdAt, expiresAt, ttlInSeconds
|
|
88
|
+
createdAt, lastAccessedAt, expiresAt, ttlInSeconds
|
|
79
89
|
};
|
|
80
90
|
await this.ddbPutItem(session);
|
|
81
91
|
return session;
|
|
@@ -84,6 +94,11 @@ export default class LambderSessionManager {
|
|
|
84
94
|
if (!session)
|
|
85
95
|
throw new Error("Invalid session");
|
|
86
96
|
session.data = newData;
|
|
97
|
+
session.lastAccessedAt = Math.floor(Date.now() / 1000);
|
|
98
|
+
// Update expiration if sliding expiration is enabled
|
|
99
|
+
if (this.enableSlidingExpiration) {
|
|
100
|
+
session.expiresAt = session.lastAccessedAt + session.ttlInSeconds;
|
|
101
|
+
}
|
|
87
102
|
await this.ddbPutItem(session);
|
|
88
103
|
return session;
|
|
89
104
|
}
|
|
@@ -96,18 +111,26 @@ export default class LambderSessionManager {
|
|
|
96
111
|
[this.partitionKey]: sessionKeyHash,
|
|
97
112
|
[this.sortKey]: sessionSortKey
|
|
98
113
|
});
|
|
114
|
+
// Use constant error response to prevent timing attacks
|
|
99
115
|
if (!session)
|
|
100
|
-
|
|
101
|
-
if (!session.sessionToken || session.sessionToken
|
|
102
|
-
|
|
116
|
+
return null;
|
|
117
|
+
if (!session.sessionToken || !this.constantTimeCompare(session.sessionToken, sessionToken))
|
|
118
|
+
return null;
|
|
103
119
|
if (!session.csrfToken)
|
|
104
|
-
|
|
120
|
+
return null;
|
|
105
121
|
if (!session.sessionKey)
|
|
106
|
-
|
|
122
|
+
return null;
|
|
107
123
|
if (!session.createdAt)
|
|
108
|
-
|
|
124
|
+
return null;
|
|
109
125
|
if (!session.expiresAt || session.expiresAt < Date.now() / 1000)
|
|
110
|
-
|
|
126
|
+
return null;
|
|
127
|
+
// Update last accessed time if sliding expiration is enabled
|
|
128
|
+
if (this.enableSlidingExpiration) {
|
|
129
|
+
session.lastAccessedAt = Math.floor(Date.now() / 1000);
|
|
130
|
+
session.expiresAt = session.lastAccessedAt + session.ttlInSeconds;
|
|
131
|
+
// Fire and forget - don't wait for the update
|
|
132
|
+
this.ddbPutItem(session).catch(() => { });
|
|
133
|
+
}
|
|
111
134
|
return session;
|
|
112
135
|
}
|
|
113
136
|
catch (err) {
|
|
@@ -120,7 +143,7 @@ export default class LambderSessionManager {
|
|
|
120
143
|
return false;
|
|
121
144
|
if (!sessionToken || typeof sessionToken !== "string")
|
|
122
145
|
return false;
|
|
123
|
-
if (session.sessionToken
|
|
146
|
+
if (!this.constantTimeCompare(session.sessionToken, sessionToken))
|
|
124
147
|
return false;
|
|
125
148
|
if (!session.csrfToken)
|
|
126
149
|
return false;
|
|
@@ -133,7 +156,7 @@ export default class LambderSessionManager {
|
|
|
133
156
|
if (!skipCsrfTokenCheck) {
|
|
134
157
|
if (!csrfToken || typeof csrfToken !== "string")
|
|
135
158
|
return false;
|
|
136
|
-
if (session.csrfToken
|
|
159
|
+
if (!this.constantTimeCompare(session.csrfToken, csrfToken))
|
|
137
160
|
return false;
|
|
138
161
|
}
|
|
139
162
|
return true;
|
|
@@ -151,5 +174,13 @@ export default class LambderSessionManager {
|
|
|
151
174
|
return true;
|
|
152
175
|
}
|
|
153
176
|
;
|
|
177
|
+
async regenerateSession(session) {
|
|
178
|
+
if (!session)
|
|
179
|
+
throw new Error("Invalid session");
|
|
180
|
+
// Delete old session
|
|
181
|
+
await this.deleteSession(session);
|
|
182
|
+
// Create new session with same sessionKey and data but new tokens
|
|
183
|
+
return await this.createSession(session.sessionKey, session.data, session.ttlInSeconds);
|
|
184
|
+
}
|
|
154
185
|
}
|
|
155
186
|
;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
|
5
5
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
6
6
|
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
7
7
|
export { type ApiContractShape, type ApiContract, type ApiInput, type ApiOutput, } from "./LambderApiContract.js";
|
|
8
|
+
export { mockLambderApi, mockLambderApiError, mockLambderSessionExpired, mockLambderNotAuthorized, mockLambderVersionExpired, createLambderApiHandler, type HttpHandler, } from "./LambderMSW.js";
|
package/dist/index.js
CHANGED
|
@@ -4,3 +4,5 @@ export { default as LambderCaller } from "./LambderCaller.js";
|
|
|
4
4
|
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
5
5
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
6
6
|
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
7
|
+
// MSW mocking utilities (optional - requires msw to be installed)
|
|
8
|
+
export { mockLambderApi, mockLambderApiError, mockLambderSessionExpired, mockLambderNotAuthorized, mockLambderVersionExpired, createLambderApiHandler, } from "./LambderMSW.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# DynamoDB Session Table Setup Guide
|
|
2
|
+
|
|
3
|
+
This guide helps you set up a DynamoDB table for Lambder session management with all security features enabled.
|
|
4
|
+
|
|
5
|
+
## Table Creation
|
|
6
|
+
|
|
7
|
+
### Using Terraform
|
|
8
|
+
|
|
9
|
+
```hcl
|
|
10
|
+
resource "aws_dynamodb_table" "lambder_sessions" {
|
|
11
|
+
name = "lambder-sessions"
|
|
12
|
+
billing_mode = "PAY_PER_REQUEST"
|
|
13
|
+
hash_key = "pk"
|
|
14
|
+
range_key = "sk"
|
|
15
|
+
|
|
16
|
+
attribute {
|
|
17
|
+
name = "pk"
|
|
18
|
+
type = "S"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attribute {
|
|
22
|
+
name = "sk"
|
|
23
|
+
type = "S"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ttl {
|
|
27
|
+
enabled = true
|
|
28
|
+
attribute_name = "expiresAt"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
tags = {
|
|
32
|
+
Purpose = "Session Management"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Enable Time to Live (TTL)
|
|
38
|
+
|
|
39
|
+
TTL automatically removes expired sessions from DynamoDB, saving storage costs.
|
|
40
|
+
|
|
41
|
+
### Using AWS Console
|
|
42
|
+
|
|
43
|
+
1. Go to DynamoDB Console
|
|
44
|
+
2. Select your table (`lambder-sessions`)
|
|
45
|
+
3. Navigate to **Additional settings** tab
|
|
46
|
+
4. Click **Edit** under **Time to Live (TTL)**
|
|
47
|
+
5. Enable TTL
|
|
48
|
+
6. Set **TTL attribute** to: `expiresAt`
|
|
49
|
+
7. Save changes
|
|
50
|
+
|
|
51
|
+
## IAM Permissions
|
|
52
|
+
|
|
53
|
+
Your Lambda function needs these permissions:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"Version": "2012-10-17",
|
|
58
|
+
"Statement": [
|
|
59
|
+
{
|
|
60
|
+
"Effect": "Allow",
|
|
61
|
+
"Action": [
|
|
62
|
+
"dynamodb:GetItem",
|
|
63
|
+
"dynamodb:PutItem",
|
|
64
|
+
"dynamodb:DeleteItem",
|
|
65
|
+
"dynamodb:Query"
|
|
66
|
+
],
|
|
67
|
+
"Resource": [
|
|
68
|
+
"arn:aws:dynamodb:us-east-1:123456789012:table/lambder-sessions"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Session Data Structure
|
|
76
|
+
|
|
77
|
+
Each session is stored as:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"pk": "hash_of_user_id",
|
|
82
|
+
"sk": "random_64_char_hex",
|
|
83
|
+
"sessionToken": "hash_of_user_id:random_64_char_hex",
|
|
84
|
+
"csrfToken": "random_64_char_hex",
|
|
85
|
+
"sessionKey": "user_123",
|
|
86
|
+
"data": {
|
|
87
|
+
"userId": "user_123",
|
|
88
|
+
"username": "john_doe",
|
|
89
|
+
"role": "admin"
|
|
90
|
+
},
|
|
91
|
+
"createdAt": 1697712000,
|
|
92
|
+
"lastAccessedAt": 1697712300,
|
|
93
|
+
"expiresAt": 1700304000,
|
|
94
|
+
"ttlInSeconds": 2592000
|
|
95
|
+
}
|
|
96
|
+
```
|