lambder 1.0.114 → 1.0.116

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/Readme.md CHANGED
@@ -55,7 +55,7 @@ lambder.addApi("getCompanyPage", async ({ apiPayload }, res) => {
55
55
  // Start a session from an API
56
56
  lambder.addApi("loginUser", async (ctx, res) => {
57
57
  const user = await fetchUserData();
58
- await lambder.getSessionController(ctx).startSession(user.id);
58
+ await lambder.getSessionController(ctx).createSession(user.id);
59
59
  return res.api({ success: true });
60
60
  });
61
61
 
@@ -218,25 +218,81 @@ lambder.addModule(async (lambder: Lambder): Promise<void> => {
218
218
 
219
219
  ### Session Management
220
220
 
221
+ You can enable session tracking by:
222
+
221
223
  ```typescript
222
- lambder.addApi("getCompanyPage", async (ctx, res) => {
224
+ // Enable session
225
+ lambder.enableDdbSession({
226
+ tableName: "website-session",
227
+ tableRegion: "us-east-1",
228
+ sessionSalt: "8p6Vt+4b1w3N8d/dcJ47QF3DRkp9koFg0G" // Change salt
229
+ });
230
+ ```
231
+
232
+ After you enable the session, you can access to the session controller:
233
+
234
+ ```typescript
235
+ // Create session controller:
223
236
  const sessionController = lambder.getSessionController(ctx);
237
+
238
+ /*
239
+ sessionController: {
240
+ async createSession(sessionKey, data, ttlInSeconds):
241
+ // Starts a new session and persists the session data to DDB.
242
+
243
+ async fetchSession():
244
+ // Fetch and validate if there is an existing session
245
+ // This is automatically done for addSessionRoute and addSessionApi
246
+ // Throws if session not found
247
+
248
+ async fetchSessionIfExists():
249
+ // Runs fetchSession and returns the session if found. Otherwise return null
250
+
251
+ async updateSessionData(updatedData):
252
+ // Updates the active sessions data and persist it to ddb.
253
+
254
+ async endSession(): Start a new session
255
+ // End session and delete from DDB.
256
+
257
+ async endSessionAll(): Start a new session
258
+ // Ends and deletes all registered sessions for this sessionKey across all devices
259
+ }
260
+ */
261
+ ```
262
+
263
+
264
+
265
+ #### Session Examples
266
+ ```typescript
267
+ lambder.addApi("getCompanyPage", async (ctx, res) => {
268
+
269
+ // createSession: Start a new session
224
270
  const userId = "37234";
225
- await sessionController.startSession(userId, { "business": "Session data goes here" });
226
- console.log(ctx.session?.userKey); // "37234"
271
+ await lambder.getSessionController(ctx)
272
+ .createSession(userId, { "business": "Session data goes here" });
273
+ console.log(ctx.session?.sessionKey); // "37234"
227
274
  console.log(ctx.session?.data?.business); // "Session data goes here"
228
275
 
229
- await sessionController.updateSessionData({ "business2": "Session data updated" });
230
- console.log(ctx.session?.userKey); // "37234"
276
+ // fetchSession: Fetch and validate if there is an existing session
277
+ // This is automatically done for addSessionRoute and addSessionApi
278
+ await lambder.getSessionController(ctx).fetchSession();
279
+ console.log(ctx.session?.sessionKey); // "37234"
280
+
281
+ // updateSessionData: Updates the active sessions data and persist it to ddb.
282
+ await lambder.getSessionController(ctx)
283
+ .updateSessionData({ "business2": "Session data updated" });
284
+ console.log(ctx.session?.sessionKey); // "37234"
231
285
  console.log(ctx.session?.data?.business); // undefined
232
286
  console.log(ctx.session?.data?.business2); // "Session data updated"
233
287
 
234
- await sessionController.deleteSession(); // End session
235
- console.log(ctx.session?.userKey); // undefined
288
+ // endSession: Ends the session and removes it from ddb
289
+ await lambder.getSessionController(ctx).endSession(); // End session
290
+ console.log(ctx.session?.sessionKey); // undefined
236
291
  console.log(ctx.session?.data?.business); // undefined
237
292
 
238
- await sessionController.deleteSessionAll(); // End session for this user in all devices
239
- console.log(ctx.session?.userKey); // undefined
293
+ // endSessionAll: Ends all registered sessions for this user in all devices.
294
+ await lambder.getSessionController(ctx).endSessionAll();
295
+ console.log(ctx.session?.sessionKey); // undefined
240
296
  console.log(ctx.session?.data?.business); // undefined
241
297
 
242
298
  });
@@ -296,6 +352,7 @@ lambder.addApi("getCompanyName", async (ctx, res) => {
296
352
  headers, // Request Headers in an object. Exp: { "User-Agent": "....", ... }
297
353
  apiName, // In this function it would return "getCompanyName"
298
354
  apiPayload, // Same as post.payload
355
+ session, // Stores session. Only available in addSessionRoute and addSessionApi, otherwise null.
299
356
  } = ctx;
300
357
  return res.json({});
301
358
  });
package/dist/Lambder.d.ts CHANGED
@@ -2,9 +2,10 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from
2
2
  import LambderResolver from "./LambderResolver.js";
3
3
  import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
4
4
  import LambderUtils from "./LambderUtils.js";
5
- import { type LambderSessionContext } from "./LambderSession.js";
5
+ import { type LambderSessionContext } from "./LambderSessionManager.js";
6
+ import LambderSessionController from "./LambderSessionController.js";
6
7
  type Path = `/${string}`;
7
- type LambderRenderContext = {
8
+ export type LambderRenderContext = {
8
9
  host: string;
9
10
  path: string;
10
11
  pathParams: Record<string, any> | null;
@@ -17,6 +18,9 @@ type LambderRenderContext = {
17
18
  headers: APIGatewayProxyEventHeaders;
18
19
  session: LambderSessionContext | null;
19
20
  lambdaContext: Context;
21
+ _otherInternal: {
22
+ isApiCall: boolean;
23
+ };
20
24
  };
21
25
  type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
22
26
  type ConditionFunction = (ctx: LambderRenderContext) => boolean;
@@ -41,7 +45,7 @@ export default class Lambder {
41
45
  private routeFallbackHandler;
42
46
  private apiFallbackHandler;
43
47
  utils: LambderUtils;
44
- private lambderSession?;
48
+ private lambderSessionManager?;
45
49
  private sessionTokenCookieKey;
46
50
  private sessionCsrfCookieKey;
47
51
  constructor({ publicPath, apiPath, ejsPath, apiVersion }: {
@@ -63,11 +67,8 @@ export default class Lambder {
63
67
  setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): void;
64
68
  setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): void;
65
69
  setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): void;
66
- getPatternMatch(pattern: string, path: string): Record<string, any>;
67
- testPatternMatch(pattern: string, path: string): boolean;
68
- private fetchSession;
69
- private validateSessionForRoute;
70
- private validateSessionForAPI;
70
+ private getPatternMatch;
71
+ private testPatternMatch;
71
72
  private handleNoMatchedAction;
72
73
  addModule(moduleFn: LambderModuleFunction): Promise<void>;
73
74
  importModule(moduleImport: Promise<{
@@ -81,12 +82,7 @@ export default class Lambder {
81
82
  addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
82
83
  addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
83
84
  addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
84
- getSessionController(ctx: LambderRenderContext): {
85
- startSession: (userKey: string, data?: any, ttlInSeconds?: number) => Promise<LambderSessionContext>;
86
- updateSessionData: (newData: any) => Promise<LambderSessionContext>;
87
- deleteSession: () => Promise<void>;
88
- deleteSessionAll: () => Promise<void>;
89
- };
85
+ getSessionController(ctx: LambderRenderContext): LambderSessionController;
90
86
  getResponseBuilder(): LambderResponseBuilder;
91
87
  private getResolver;
92
88
  render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<LambderResolverResponse>;
package/dist/Lambder.js CHANGED
@@ -4,7 +4,8 @@ import { match } from "path-to-regexp";
4
4
  import LambderResolver from "./LambderResolver.js";
5
5
  import LambderResponseBuilder from "./LambderResponseBuilder.js";
6
6
  import LambderUtils from "./LambderUtils.js";
7
- import LambderSession from "./LambderSession.js";
7
+ import LambderSessionManager from "./LambderSessionManager.js";
8
+ import LambderSessionController from "./LambderSessionController.js";
8
9
  export const createContext = (event, lambdaContext, apiPath) => {
9
10
  const host = event.headers.Host || event.headers.host || "";
10
11
  const path = event.path;
@@ -27,10 +28,16 @@ export const createContext = (event, lambdaContext, apiPath) => {
27
28
  }
28
29
  catch (e) { }
29
30
  // Parse api variables
30
- const isAPICall = method === "POST" && apiPath && path === apiPath && post.apiName;
31
- const apiName = isAPICall ? post.apiName : null;
32
- const apiPayload = isAPICall ? post.payload : null;
33
- return { host, path, pathParams, method, get, post, cookie, apiName, apiPayload, headers, session, lambdaContext };
31
+ const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
32
+ const apiName = isApiCall ? post.apiName : null;
33
+ const apiPayload = isApiCall ? post.payload : null;
34
+ return {
35
+ host, path, pathParams, method,
36
+ get, post, cookie,
37
+ apiName, apiPayload,
38
+ headers, session, lambdaContext,
39
+ _otherInternal: { isApiCall }
40
+ };
34
41
  };
35
42
  export default class Lambder {
36
43
  apiPath;
@@ -44,7 +51,7 @@ export default class Lambder {
44
51
  routeFallbackHandler = null;
45
52
  apiFallbackHandler = null;
46
53
  utils;
47
- lambderSession;
54
+ lambderSessionManager;
48
55
  sessionTokenCookieKey = "LMDRSESSIONTKID";
49
56
  sessionCsrfCookieKey = "LMDRSESSIONCSTK";
50
57
  constructor({ publicPath, apiPath, ejsPath, apiVersion }) {
@@ -64,7 +71,7 @@ export default class Lambder {
64
71
  this.isCorsEnabled = isCorsEnabled;
65
72
  }
66
73
  enableDdbSession({ tableName, tableRegion, sessionSalt }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
67
- this.lambderSession = new LambderSession({
74
+ this.lambderSessionManager = new LambderSessionManager({
68
75
  tableName, tableRegion, partitionKey, sortKey, sessionSalt
69
76
  });
70
77
  }
@@ -90,32 +97,6 @@ export default class Lambder {
90
97
  testPatternMatch(pattern, path) {
91
98
  return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
92
99
  }
93
- async fetchSession(ctx) {
94
- if (!this.lambderSession)
95
- throw "Session not found";
96
- const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
97
- if (!sessionToken)
98
- throw "Session not found";
99
- const session = await this.lambderSession.getSession(sessionToken);
100
- ctx.session = session;
101
- return true;
102
- }
103
- ;
104
- validateSessionForRoute(ctx) {
105
- if (!this.lambderSession)
106
- return false;
107
- const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
108
- return this.lambderSession.isSessionValid(ctx.session, sessionToken, null, true);
109
- }
110
- ;
111
- validateSessionForAPI(ctx) {
112
- if (!this.lambderSession)
113
- return false;
114
- const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
115
- const csrfToken = ctx.post?.token;
116
- return this.lambderSession.isSessionValid(ctx.session, sessionToken, csrfToken);
117
- }
118
- ;
119
100
  async handleNoMatchedAction(ctx, resolver) {
120
101
  for (const hook of this.hookList["fallback"]) {
121
102
  await hook.hookFn(ctx, resolver);
@@ -165,10 +146,7 @@ export default class Lambder {
165
146
  (typeof condition === "function" && condition(ctx)) ||
166
147
  (condition?.constructor == RegExp && condition.test(ctx.path)))),
167
148
  actionFn: async (ctx, resolver) => {
168
- await this.fetchSession(ctx);
169
- const isSessionValid = this.validateSessionForRoute(ctx);
170
- if (!isSessionValid)
171
- throw new Error("Session not found");
149
+ await this.getSessionController(ctx).fetchSession();
172
150
  if (typeof condition === "string") {
173
151
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
174
152
  }
@@ -195,10 +173,7 @@ export default class Lambder {
195
173
  (typeof apiName === "function" && apiName(ctx)) ||
196
174
  (apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
197
175
  actionFn: async (ctx, resolver) => {
198
- await this.fetchSession(ctx);
199
- const isSessionValid = this.validateSessionForAPI(ctx);
200
- if (!isSessionValid)
201
- throw new Error("Session not found");
176
+ await this.getSessionController(ctx).fetchSession();
202
177
  return await actionFn(ctx, resolver);
203
178
  }
204
179
  });
@@ -214,38 +189,13 @@ export default class Lambder {
214
189
  }
215
190
  }
216
191
  getSessionController(ctx) {
217
- return {
218
- startSession: async (userKey, data, ttlInSeconds) => {
219
- if (!this.lambderSession)
220
- throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
221
- ctx.session = await this.lambderSession.createSession(userKey, data, ttlInSeconds);
222
- return ctx.session;
223
- },
224
- updateSessionData: async (newData) => {
225
- if (!this.lambderSession)
226
- throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
227
- if (!ctx.session)
228
- throw "Session not found.";
229
- ctx.session = await this.lambderSession.updateSessionData(ctx.session, newData);
230
- return ctx.session;
231
- },
232
- deleteSession: async () => {
233
- if (!this.lambderSession)
234
- throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
235
- if (!ctx.session)
236
- throw "Session not found.";
237
- await this.lambderSession.deleteSession(ctx.session);
238
- ctx.session = null;
239
- },
240
- deleteSessionAll: async () => {
241
- if (!this.lambderSession)
242
- throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
243
- if (!ctx.session)
244
- throw "Session not found.";
245
- await this.lambderSession.deleteSessionAll(ctx.session);
246
- ctx.session = null;
247
- },
248
- };
192
+ if (!this.lambderSessionManager)
193
+ throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
194
+ return new LambderSessionController({
195
+ lambderSessionManager: this.lambderSessionManager,
196
+ sessionTokenCookieKey: this.sessionTokenCookieKey,
197
+ ctx,
198
+ });
249
199
  }
250
200
  getResponseBuilder() {
251
201
  return new LambderResponseBuilder({
@@ -0,0 +1,21 @@
1
+ import { LambderRenderContext } from "./Lambder.js";
2
+ import type LambderSessionManager from "./LambderSessionManager.js";
3
+ import type { LambderSessionContext } from "./LambderSessionManager.js";
4
+ export default class LambderSessionController {
5
+ lambderSessionManager: LambderSessionManager;
6
+ sessionTokenCookieKey: string;
7
+ ctx: LambderRenderContext;
8
+ constructor({ lambderSessionManager, sessionTokenCookieKey, ctx, }: {
9
+ lambderSessionManager: LambderSessionManager;
10
+ sessionTokenCookieKey: string;
11
+ ctx: LambderRenderContext;
12
+ });
13
+ private areRequestSessionTokensValid;
14
+ createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
15
+ fetchSession(): Promise<LambderSessionContext>;
16
+ fetchSessionIfExists(): Promise<LambderSessionContext | null>;
17
+ isSessionValid(session: any): boolean;
18
+ updateSessionData(newData: any): Promise<LambderSessionContext>;
19
+ endSession(): Promise<void>;
20
+ endSessionAll(): Promise<void>;
21
+ }
@@ -0,0 +1,88 @@
1
+ export default class LambderSessionController {
2
+ lambderSessionManager;
3
+ sessionTokenCookieKey;
4
+ ctx;
5
+ constructor({ lambderSessionManager, sessionTokenCookieKey, ctx, }) {
6
+ this.ctx = ctx;
7
+ this.sessionTokenCookieKey = sessionTokenCookieKey;
8
+ this.lambderSessionManager = lambderSessionManager;
9
+ }
10
+ ;
11
+ areRequestSessionTokensValid() {
12
+ const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
13
+ const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
14
+ if (this.ctx._otherInternal.isApiCall) {
15
+ const csrfToken = this.ctx.post?.token;
16
+ const isCsrfTokenValid = typeof csrfToken === "string" && csrfToken.length > 0;
17
+ return isSessionTokenValid && isCsrfTokenValid;
18
+ }
19
+ else {
20
+ return isSessionTokenValid;
21
+ }
22
+ }
23
+ ;
24
+ async createSession(sessionKey, data, ttlInSeconds) {
25
+ this.ctx.session = await this.lambderSessionManager.createSession(sessionKey, data, ttlInSeconds);
26
+ return this.ctx.session;
27
+ }
28
+ ;
29
+ async fetchSession() {
30
+ if (!this.areRequestSessionTokensValid()) {
31
+ throw new Error("Session tokens are invalid");
32
+ }
33
+ const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
34
+ if (!sessionToken)
35
+ throw new Error("Session token not found");
36
+ const session = await this.lambderSessionManager.getSession(sessionToken);
37
+ if (!session)
38
+ throw new Error("Session not found");
39
+ if (!this.isSessionValid(session))
40
+ throw new Error("Invalid session");
41
+ this.ctx.session = session;
42
+ return session;
43
+ }
44
+ ;
45
+ async fetchSessionIfExists() {
46
+ try {
47
+ return await this.fetchSession();
48
+ }
49
+ catch (err) {
50
+ return null;
51
+ }
52
+ }
53
+ ;
54
+ isSessionValid(session) {
55
+ if (this.ctx._otherInternal.isApiCall) {
56
+ const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
57
+ const csrfToken = this.ctx.post?.token;
58
+ return this.lambderSessionManager.isSessionValid(session, sessionToken, csrfToken);
59
+ }
60
+ else {
61
+ const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
62
+ return this.lambderSessionManager.isSessionValid(session, sessionToken, null, true);
63
+ }
64
+ }
65
+ ;
66
+ async updateSessionData(newData) {
67
+ if (!this.ctx.session)
68
+ throw new Error("Session not found.");
69
+ this.ctx.session = await this.lambderSessionManager.updateSessionData(this.ctx.session, newData);
70
+ return this.ctx.session;
71
+ }
72
+ ;
73
+ async endSession() {
74
+ if (!this.ctx.session)
75
+ throw new Error("Session not found.");
76
+ await this.lambderSessionManager.deleteSession(this.ctx.session);
77
+ this.ctx.session = null;
78
+ }
79
+ ;
80
+ async endSessionAll() {
81
+ if (!this.ctx.session)
82
+ throw new Error("Session not found.");
83
+ await this.lambderSessionManager.deleteSessionAll(this.ctx.session);
84
+ this.ctx.session = null;
85
+ }
86
+ ;
87
+ }
88
+ ;
@@ -2,13 +2,13 @@ export type LambderSessionContext = {
2
2
  [x: string]: any;
3
3
  sessionToken: string;
4
4
  csrfToken: string;
5
- userKey: string;
5
+ sessionKey: string;
6
6
  data: any;
7
7
  createdAt: number;
8
8
  expiresAt: number;
9
9
  ttlInSeconds: number;
10
10
  };
11
- export default class LambderSession {
11
+ export default class LambderSessionManager {
12
12
  private tableName;
13
13
  private sessionSalt;
14
14
  private partitionKey;
@@ -27,7 +27,7 @@ export default class LambderSession {
27
27
  private ddbDeleteItem;
28
28
  private ddbQueryAllByPartitionKey;
29
29
  private ddbDeleteAllByPartitionKey;
30
- createSession(userKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
30
+ createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
31
31
  updateSessionData(session: LambderSessionContext, newData?: any): Promise<LambderSessionContext>;
32
32
  getSession(sessionToken: string): Promise<LambderSessionContext | null>;
33
33
  isSessionValid(session: any, sessionToken: any, csrfToken: any, skipCsrfTokenCheck?: boolean): boolean;
@@ -1,7 +1,7 @@
1
1
  import crypto from "crypto";
2
2
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
3
  import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
4
- export default class LambderSession {
4
+ export default class LambderSessionManager {
5
5
  tableName;
6
6
  sessionSalt;
7
7
  partitionKey;
@@ -63,18 +63,18 @@ export default class LambderSession {
63
63
  }));
64
64
  }
65
65
  }
66
- async createSession(userKey, data = {}, ttlInSeconds = 30 * 24 * 60 * 60) {
67
- const userKeyHash = this.sessionUserKeyHasher(userKey);
66
+ async createSession(sessionKey, data = {}, ttlInSeconds = 30 * 24 * 60 * 60) {
67
+ const sessionKeyHash = this.sessionUserKeyHasher(sessionKey);
68
68
  const sessionSortKey = crypto.randomBytes(32).toString("hex");
69
- const sessionToken = `${userKeyHash}:${sessionSortKey}`;
69
+ const sessionToken = `${sessionKeyHash}:${sessionSortKey}`;
70
70
  const csrfToken = crypto.randomBytes(8).toString("hex");
71
71
  const createdAt = Math.floor(Date.now() / 1000);
72
72
  const expiresAt = Number(createdAt) + Number(ttlInSeconds);
73
73
  const session = {
74
- [this.partitionKey]: userKeyHash,
74
+ [this.partitionKey]: sessionKeyHash,
75
75
  [this.sortKey]: sessionSortKey,
76
76
  sessionToken, csrfToken,
77
- userKey, data,
77
+ sessionKey, data,
78
78
  createdAt, expiresAt, ttlInSeconds
79
79
  };
80
80
  await this.ddbPutItem(session);
@@ -82,32 +82,32 @@ export default class LambderSession {
82
82
  }
83
83
  async updateSessionData(session, newData) {
84
84
  if (!session)
85
- throw "Invalid session";
85
+ throw new Error("Invalid session");
86
86
  session.data = newData;
87
87
  await this.ddbPutItem(session);
88
88
  return session;
89
89
  }
90
90
  async getSession(sessionToken) {
91
- const [userKeyHash, sessionSortKey] = sessionToken.split(":");
92
- if (!userKeyHash || !sessionSortKey)
91
+ const [sessionKeyHash, sessionSortKey] = sessionToken.split(":");
92
+ if (!sessionKeyHash || !sessionSortKey)
93
93
  return null;
94
94
  try {
95
95
  let session = await this.ddbGetItem({
96
- [this.partitionKey]: userKeyHash,
96
+ [this.partitionKey]: sessionKeyHash,
97
97
  [this.sortKey]: sessionSortKey
98
98
  });
99
99
  if (!session)
100
- throw "Session not found";
100
+ throw new Error("Session not found");
101
101
  if (!session.sessionToken || session.sessionToken !== sessionToken)
102
- throw "Not found: session.sessionToken";
102
+ throw new Error("Not found: session.sessionToken");
103
103
  if (!session.csrfToken)
104
- throw "Not found: session.csrfToken";
105
- if (!session.userKey)
106
- throw "Not found: session.userKey";
104
+ throw new Error("Not found: session.csrfToken");
105
+ if (!session.sessionKey)
106
+ throw new Error("Not found: session.sessionKey");
107
107
  if (!session.createdAt)
108
- throw "Not found: session.createdAt";
108
+ throw new Error("Not found: session.createdAt");
109
109
  if (!session.expiresAt || session.expiresAt < Date.now() / 1000)
110
- throw "Not found: session.expiresAt";
110
+ throw new Error("Not found: session.expiresAt");
111
111
  return session;
112
112
  }
113
113
  catch (err) {
@@ -124,7 +124,7 @@ export default class LambderSession {
124
124
  return false;
125
125
  if (!session.csrfToken)
126
126
  return false;
127
- if (!session.userKey)
127
+ if (!session.sessionKey)
128
128
  return false;
129
129
  if (!session.createdAt)
130
130
  return false;
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export default Lambder;
3
3
  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
- export { default as LambderSession } from "./LambderSession.js";
6
+ export { default as LambderSessionManager } from "./LambderSessionManager.js";
package/dist/index.js CHANGED
@@ -3,4 +3,4 @@ export default Lambder;
3
3
  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
- export { default as LambderSession } from "./LambderSession.js";
6
+ export { default as LambderSessionManager } from "./LambderSessionManager.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "1.0.114",
3
+ "version": "1.0.116",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/Lambder.ts CHANGED
@@ -6,11 +6,12 @@ import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from
6
6
  import LambderResolver from "./LambderResolver.js";
7
7
  import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
8
8
  import LambderUtils from "./LambderUtils.js";
9
- import LambderSession, { type LambderSessionContext } from "./LambderSession.js";
9
+ import LambderSessionManager, { type LambderSessionContext } from "./LambderSessionManager.js";
10
+ import LambderSessionController from "./LambderSessionController.js";
10
11
 
11
12
  type Path = `/${string}`;
12
13
 
13
- type LambderRenderContext = {
14
+ export type LambderRenderContext = {
14
15
  host: string;
15
16
  path: string;
16
17
  pathParams: Record<string, any> | null;
@@ -23,6 +24,7 @@ type LambderRenderContext = {
23
24
  headers: APIGatewayProxyEventHeaders;
24
25
  session: LambderSessionContext|null;
25
26
  lambdaContext: Context;
27
+ _otherInternal: { isApiCall: boolean };
26
28
  };
27
29
 
28
30
  type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
@@ -55,6 +57,7 @@ export const createContext = (
55
57
  const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
56
58
  const headers = event.headers;
57
59
  const session = null;
60
+
58
61
  // Decode body for the post
59
62
  let post: Record<string, any> = {};
60
63
  try {
@@ -63,11 +66,18 @@ export const createContext = (
63
66
  catch(e){ post = querystring.parse(decodedBody) || {}; }
64
67
  }catch(e){}
65
68
  // Parse api variables
66
- const isAPICall = method === "POST" && apiPath && path === apiPath && post.apiName;
67
- const apiName:string = isAPICall ? post.apiName : null;
68
- const apiPayload:string = isAPICall ? post.payload : null;
69
69
 
70
- return { host, path, pathParams, method, get, post, cookie, apiName, apiPayload, headers, session, lambdaContext };
70
+ const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
71
+ const apiName:string = isApiCall ? post.apiName : null;
72
+ const apiPayload:string = isApiCall ? post.payload : null;
73
+
74
+ return {
75
+ host, path, pathParams, method,
76
+ get, post, cookie,
77
+ apiName, apiPayload,
78
+ headers, session, lambdaContext,
79
+ _otherInternal: { isApiCall }
80
+ };
71
81
  }
72
82
 
73
83
 
@@ -90,7 +100,7 @@ export default class Lambder {
90
100
 
91
101
  public utils: LambderUtils;
92
102
 
93
- private lambderSession?: LambderSession;
103
+ private lambderSessionManager?: LambderSessionManager;
94
104
  private sessionTokenCookieKey = "LMDRSESSIONTKID";
95
105
  private sessionCsrfCookieKey = "LMDRSESSIONCSTK";
96
106
 
@@ -121,7 +131,7 @@ export default class Lambder {
121
131
  { tableName, tableRegion, sessionSalt }: { tableName: string; tableRegion: string; sessionSalt: string; },
122
132
  { partitionKey, sortKey }: { partitionKey: string, sortKey: string } = { partitionKey: "pk", sortKey: "sk" }
123
133
  ){
124
- this.lambderSession = new LambderSession({
134
+ this.lambderSessionManager = new LambderSessionManager({
125
135
  tableName, tableRegion, partitionKey, sortKey, sessionSalt
126
136
  });
127
137
  }
@@ -140,37 +150,15 @@ export default class Lambder {
140
150
  setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction){
141
151
  this.globalErrorHandler = globalErrorHandler;
142
152
  }
143
- getPatternMatch(pattern: string, path: string): Record<string, any> {
153
+ private getPatternMatch(pattern: string, path: string): Record<string, any> {
144
154
  const result = (match(pattern, { decode: decodeURIComponent }))(path);
145
155
  if(!result) return {};
146
156
  return result?.params || {};
147
157
  }
148
- testPatternMatch(pattern: string, path: string): boolean{
158
+ private testPatternMatch(pattern: string, path: string): boolean{
149
159
  return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
150
160
  }
151
-
152
- private async fetchSession (ctx: LambderRenderContext): Promise<boolean>{
153
- if(!this.lambderSession) throw "Session not found";
154
- const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
155
- if(!sessionToken) throw "Session not found";
156
- const session = await this.lambderSession.getSession(sessionToken);
157
- ctx.session = session;
158
- return true;
159
- };
160
-
161
- private validateSessionForRoute (ctx: LambderRenderContext): boolean{
162
- if(!this.lambderSession) return false;
163
- const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
164
- return this.lambderSession.isSessionValid(ctx.session, sessionToken, null, true);
165
- };
166
-
167
- private validateSessionForAPI (ctx: LambderRenderContext): boolean{
168
- if(!this.lambderSession) return false;
169
- const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
170
- const csrfToken = ctx.post?.token;
171
- return this.lambderSession.isSessionValid(ctx.session, sessionToken, csrfToken);
172
- };
173
-
161
+
174
162
  private async handleNoMatchedAction(ctx: LambderRenderContext, resolver: LambderResolver){
175
163
  for(const hook of this.hookList["fallback"]){ await hook.hookFn(ctx, resolver); }
176
164
 
@@ -227,9 +215,8 @@ export default class Lambder {
227
215
  )
228
216
  ),
229
217
  actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
230
- await this.fetchSession(ctx);
231
- const isSessionValid = this.validateSessionForRoute(ctx);
232
- if(!isSessionValid) throw new Error("Session not found");
218
+ await this.getSessionController(ctx).fetchSession();
219
+
233
220
  if(typeof condition === "string"){
234
221
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
235
222
  }else if(condition?.constructor == RegExp){
@@ -263,9 +250,7 @@ export default class Lambder {
263
250
  )
264
251
  ),
265
252
  actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
266
- await this.fetchSession(ctx);
267
- const isSessionValid = this.validateSessionForAPI(ctx);
268
- if(!isSessionValid) throw new Error("Session not found");
253
+ await this.getSessionController(ctx).fetchSession();
269
254
  return await actionFn(ctx, resolver);
270
255
  }
271
256
  });
@@ -288,35 +273,17 @@ export default class Lambder {
288
273
  }
289
274
  }
290
275
 
291
- public getSessionController(ctx: LambderRenderContext){
292
- return {
293
- startSession: async (userKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext> => {
294
- if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
295
- ctx.session = await this.lambderSession.createSession(userKey, data, ttlInSeconds);
296
- return ctx.session;
297
- },
298
- updateSessionData: async (newData: any): Promise<LambderSessionContext> => {
299
- if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
300
- if(!ctx.session) throw "Session not found.";
301
- ctx.session = await this.lambderSession.updateSessionData(ctx.session, newData);
302
- return ctx.session;
303
- },
304
- deleteSession: async () => {
305
- if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
306
- if(!ctx.session) throw "Session not found.";
307
- await this.lambderSession.deleteSession(ctx.session);
308
- ctx.session = null
309
- },
310
- deleteSessionAll: async () => {
311
- if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
312
- if(!ctx.session) throw "Session not found.";
313
- await this.lambderSession.deleteSessionAll(ctx.session);
314
- ctx.session = null
315
- },
316
- }
276
+ getSessionController(ctx: LambderRenderContext): LambderSessionController{
277
+ if(!this.lambderSessionManager) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
278
+
279
+ return new LambderSessionController({
280
+ lambderSessionManager: this.lambderSessionManager,
281
+ sessionTokenCookieKey: this.sessionTokenCookieKey,
282
+ ctx,
283
+ });
317
284
  }
318
285
 
319
- public getResponseBuilder(){
286
+ getResponseBuilder(){
320
287
  return new LambderResponseBuilder({
321
288
  isCorsEnabled: this.isCorsEnabled,
322
289
  publicPath: this.publicPath,
@@ -0,0 +1,94 @@
1
+ import { LambderRenderContext } from "./Lambder.js";
2
+ import type LambderSessionManager from "./LambderSessionManager.js";
3
+ import type { LambderSessionContext } from "./LambderSessionManager.js";
4
+
5
+ export default class LambderSessionController {
6
+ lambderSessionManager: LambderSessionManager;
7
+ sessionTokenCookieKey: string;
8
+ ctx: LambderRenderContext;
9
+
10
+ constructor(
11
+ {
12
+ lambderSessionManager,
13
+ sessionTokenCookieKey,
14
+ ctx,
15
+ }: {
16
+ lambderSessionManager: LambderSessionManager,
17
+ sessionTokenCookieKey: string,
18
+ ctx: LambderRenderContext,
19
+ }
20
+ ){
21
+ this.ctx = ctx;
22
+ this.sessionTokenCookieKey = sessionTokenCookieKey;
23
+ this.lambderSessionManager = lambderSessionManager;
24
+ };
25
+
26
+ private areRequestSessionTokensValid(): boolean {
27
+ const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
28
+ const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
29
+
30
+ if(this.ctx._otherInternal.isApiCall){
31
+ const csrfToken = this.ctx.post?.token;
32
+ const isCsrfTokenValid = typeof csrfToken === "string" && csrfToken.length > 0
33
+ return isSessionTokenValid && isCsrfTokenValid;
34
+ }else{
35
+ return isSessionTokenValid;
36
+ }
37
+ };
38
+
39
+ async createSession (sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext> {
40
+ this.ctx.session = await this.lambderSessionManager.createSession(sessionKey, data, ttlInSeconds);
41
+ return this.ctx.session;
42
+ };
43
+
44
+ async fetchSession (): Promise<LambderSessionContext>{
45
+ if(!this.areRequestSessionTokensValid()){ throw new Error("Session tokens are invalid"); }
46
+
47
+ const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
48
+ if(!sessionToken) throw new Error("Session token not found");
49
+
50
+ const session = await this.lambderSessionManager.getSession(sessionToken);
51
+ if(!session) throw new Error("Session not found");
52
+
53
+ if(!this.isSessionValid(session)) throw new Error("Invalid session");
54
+ this.ctx.session = session;
55
+ return session;
56
+ };
57
+
58
+ async fetchSessionIfExists (): Promise<LambderSessionContext|null> {
59
+ try {
60
+ return await this.fetchSession();
61
+ }catch(err){
62
+ return null;
63
+ }
64
+ };
65
+
66
+ isSessionValid(session: any): boolean {
67
+ if(this.ctx._otherInternal.isApiCall){
68
+ const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
69
+ const csrfToken = this.ctx.post?.token;
70
+ return this.lambderSessionManager.isSessionValid(session, sessionToken, csrfToken);
71
+ }else{
72
+ const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
73
+ return this.lambderSessionManager.isSessionValid(session, sessionToken, null, true);
74
+ }
75
+ };
76
+
77
+ async updateSessionData (newData: any): Promise<LambderSessionContext> {
78
+ if(!this.ctx.session) throw new Error("Session not found.");
79
+ this.ctx.session = await this.lambderSessionManager.updateSessionData(this.ctx.session, newData);
80
+ return this.ctx.session;
81
+ };
82
+
83
+ async endSession (){
84
+ if(!this.ctx.session) throw new Error("Session not found.");
85
+ await this.lambderSessionManager.deleteSession(this.ctx.session);
86
+ this.ctx.session = null
87
+ };
88
+
89
+ async endSessionAll (){
90
+ if(!this.ctx.session) throw new Error("Session not found.");
91
+ await this.lambderSessionManager.deleteSessionAll(this.ctx.session);
92
+ this.ctx.session = null
93
+ };
94
+ };
@@ -6,7 +6,7 @@ export type LambderSessionContext = {
6
6
  [x: string]: any;
7
7
  sessionToken: string;
8
8
  csrfToken: string;
9
- userKey: string;
9
+ sessionKey: string;
10
10
  data: any;
11
11
  createdAt: number;
12
12
  expiresAt: number;
@@ -14,7 +14,7 @@ export type LambderSessionContext = {
14
14
  };
15
15
 
16
16
 
17
- export default class LambderSession{
17
+ export default class LambderSessionManager{
18
18
  private tableName: string;
19
19
  private sessionSalt: string;
20
20
  private partitionKey: string;
@@ -94,22 +94,22 @@ export default class LambderSession{
94
94
  }
95
95
 
96
96
  public async createSession(
97
- userKey: string,
97
+ sessionKey: string,
98
98
  data: any = {},
99
99
  ttlInSeconds:number = 30*24*60*60
100
100
  ): Promise<LambderSessionContext> {
101
- const userKeyHash = this.sessionUserKeyHasher(userKey);
101
+ const sessionKeyHash = this.sessionUserKeyHasher(sessionKey);
102
102
  const sessionSortKey = crypto.randomBytes(32).toString("hex");
103
- const sessionToken = `${userKeyHash}:${sessionSortKey}`;
103
+ const sessionToken = `${sessionKeyHash}:${sessionSortKey}`;
104
104
  const csrfToken = crypto.randomBytes(8).toString("hex");
105
105
  const createdAt = Math.floor(Date.now()/1000);
106
106
  const expiresAt = Number(createdAt) + Number(ttlInSeconds);
107
107
 
108
108
  const session = {
109
- [this.partitionKey]: userKeyHash,
109
+ [this.partitionKey]: sessionKeyHash,
110
110
  [this.sortKey]: sessionSortKey,
111
111
  sessionToken, csrfToken,
112
- userKey, data,
112
+ sessionKey, data,
113
113
  createdAt, expiresAt, ttlInSeconds
114
114
  };
115
115
  await this.ddbPutItem(session);
@@ -120,26 +120,26 @@ export default class LambderSession{
120
120
  session: LambderSessionContext,
121
121
  newData?: any
122
122
  ): Promise<LambderSessionContext> {
123
- if(!session) throw "Invalid session";
123
+ if(!session) throw new Error("Invalid session");
124
124
  session.data = newData;
125
125
  await this.ddbPutItem(session);
126
126
  return session;
127
127
  }
128
128
 
129
129
  public async getSession(sessionToken: string): Promise<LambderSessionContext|null>{
130
- const [ userKeyHash, sessionSortKey ] = sessionToken.split(":");
131
- if(!userKeyHash || !sessionSortKey) return null;
130
+ const [ sessionKeyHash, sessionSortKey ] = sessionToken.split(":");
131
+ if(!sessionKeyHash || !sessionSortKey) return null;
132
132
  try{
133
133
  let session = await this.ddbGetItem({
134
- [this.partitionKey]: userKeyHash,
134
+ [this.partitionKey]: sessionKeyHash,
135
135
  [this.sortKey]: sessionSortKey
136
136
  });
137
- if(!session) throw "Session not found";
138
- if(!session.sessionToken || session.sessionToken !== sessionToken) throw "Not found: session.sessionToken";
139
- if(!session.csrfToken) throw "Not found: session.csrfToken";
140
- if(!session.userKey) throw "Not found: session.userKey";
141
- if(!session.createdAt) throw "Not found: session.createdAt";
142
- if(!session.expiresAt || session.expiresAt < Date.now()/1000) throw "Not found: session.expiresAt";
137
+ if(!session) throw new Error("Session not found");
138
+ if(!session.sessionToken || session.sessionToken !== sessionToken) throw new Error("Not found: session.sessionToken");
139
+ if(!session.csrfToken) throw new Error("Not found: session.csrfToken");
140
+ if(!session.sessionKey) throw new Error("Not found: session.sessionKey");
141
+ if(!session.createdAt) throw new Error("Not found: session.createdAt");
142
+ if(!session.expiresAt || session.expiresAt < Date.now()/1000) throw new Error("Not found: session.expiresAt");
143
143
  return session;
144
144
  }catch(err){
145
145
  return null;
@@ -151,7 +151,7 @@ export default class LambderSession{
151
151
  if(!sessionToken || typeof sessionToken !== "string") return false;
152
152
  if(session.sessionToken !== sessionToken) return false;
153
153
  if(!session.csrfToken) return false;
154
- if(!session.userKey) return false;
154
+ if(!session.sessionKey) return false;
155
155
  if(!session.createdAt) return false;
156
156
  if(!session.expiresAt || session.expiresAt < Date.now()/1000) return false;
157
157
  if(!skipCsrfTokenCheck){
package/src/index.ts CHANGED
@@ -4,4 +4,4 @@ export default Lambder;
4
4
  export { default as LambderCaller } from "./LambderCaller.js";
5
5
  export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
6
6
  export { default as LambderResolver } from "./LambderResolver.js";
7
- export { default as LambderSession } from "./LambderSession.js";
7
+ export { default as LambderSessionManager } from "./LambderSessionManager.js";