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.
@@ -7,11 +7,13 @@ export default class LambderSessionManager {
7
7
  partitionKey;
8
8
  sortKey;
9
9
  ddbDocumentClient;
10
- constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, }) {
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(8).toString("hex");
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
- throw new Error("Session not found");
101
- if (!session.sessionToken || session.sessionToken !== sessionToken)
102
- throw new Error("Not found: session.sessionToken");
116
+ return null;
117
+ if (!session.sessionToken || !this.constantTimeCompare(session.sessionToken, sessionToken))
118
+ return null;
103
119
  if (!session.csrfToken)
104
- throw new Error("Not found: session.csrfToken");
120
+ return null;
105
121
  if (!session.sessionKey)
106
- throw new Error("Not found: session.sessionKey");
122
+ return null;
107
123
  if (!session.createdAt)
108
- throw new Error("Not found: session.createdAt");
124
+ return null;
109
125
  if (!session.expiresAt || session.expiresAt < Date.now() / 1000)
110
- throw new Error("Not found: session.expiresAt");
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 !== 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 !== 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
+ ```