lambder 1.0.113 → 1.0.115

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
@@ -5,7 +5,7 @@ Lambder is a highly opinionated dynamic serverless framework designed to facilit
5
5
  ## Features
6
6
 
7
7
  - **Simple API & Route Declaration**: Define your APIs and routes using concise and expressive syntax.
8
- - **Session Management**: IN PROGRESS: Built-in session management to secure and personalize user experiences.
8
+ - **Session Management**: Built-in session management to secure and personalize user experiences.
9
9
  - **Flexible Hooks System**: Employ hooks to execute code at different stages of the request lifecycle, enabling fine-grained control over the application flow.
10
10
  - **Error Handling**: Comprehensive error handling capabilities, including global error handlers and route-specific fallbacks.
11
11
  - **Seamless Integration**: Designed to work effortlessly with AWS Lambda and API Gateway, providing a straightforward path to deploy serverless applications.
@@ -30,11 +30,21 @@ import * as path from 'path';
30
30
 
31
31
  const lambder = new Lambder({
32
32
  apiPath: "/secure",
33
- isCorsEnabled: true,
34
33
  publicPath: path.resolve(`./public`),
35
34
  // ejsPath: path.resolve(`./ejs-templates`),
36
35
  });
37
36
 
37
+
38
+ // Enable session
39
+ lambder.enableDdbSession({
40
+ tableName: "website-session",
41
+ tableRegion: "us-east-1",
42
+ sessionSalt: "8p6Vt+4b1w3N8d/dcJ47QF3DRkp9koFg0G" // Change salt
43
+ });
44
+
45
+ // Enable Cors
46
+ lambder.setIsCorsEnabled(true);
47
+
38
48
  // Define a simple api
39
49
  lambder.addApi("getCompanyPage", async ({ apiPayload }, res) => {
40
50
  const companyName = apiPayload.companyName;
@@ -42,6 +52,13 @@ lambder.addApi("getCompanyPage", async ({ apiPayload }, res) => {
42
52
  return res.api(data);
43
53
  });
44
54
 
55
+ // Start a session from an API
56
+ lambder.addApi("loginUser", async (ctx, res) => {
57
+ const user = await fetchUserData();
58
+ await lambder.getSessionController(ctx).createSession(user.id);
59
+ return res.api({ success: true });
60
+ });
61
+
45
62
  // Define a simple route
46
63
  lambder.addRoute("/hello-world", (ctx, res) => {
47
64
  return res.html("Hello World");
@@ -114,6 +131,29 @@ export const handler = (event, context) => {
114
131
  ### Adding APIs
115
132
 
116
133
  For more details on route matching, please check [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) package.
134
+ ```typescript
135
+ // Add routes
136
+ lambder.addRoute(pathAsString, async (ctx, res) => {});
137
+ lambder.addRoute(pathAsRegex, async (ctx, res) => {});
138
+ lambder.addRoute(matchFunction, async (ctx, res) => {});
139
+
140
+ // Add apis
141
+ lambder.addApi(apiNameAsString, async (ctx, res) => {});
142
+ lambder.addApi(apiNameAsRegex, async (ctx, res) => {});
143
+ lambder.addApi(matchFunction, async (ctx, res) => {});
144
+
145
+ // Add only session accessible routes
146
+ lambder.addSessionRoute(pathAsString, async (ctx, res) => {});
147
+ lambder.addSessionRoute(pathAsRegex, async (ctx, res) => {});
148
+ lambder.addSessionRoute(matchFunction, async (ctx, res) => {});
149
+
150
+ // Add only session accessible apis
151
+ lambder.addSessionApi(apiNameAsString, async (ctx, res) => {});
152
+ lambder.addSessionApi(apiNameAsRegex, async (ctx, res) => {});
153
+ lambder.addSessionApi(matchFunction, async (ctx, res) => {});
154
+
155
+ ```
156
+
117
157
 
118
158
  ```typescript
119
159
  // Define a simple api
@@ -128,6 +168,21 @@ lambder.addApi("getCompanyPage", async (ctx, res) => {
128
168
  });
129
169
  ```
130
170
 
171
+ ```typescript
172
+ // Define an only session accessible api
173
+ lambder.addSessionApi("getCompanyPage", async (ctx, res) => {
174
+ const {
175
+ host, path, get, post, cookie, headers,
176
+ session, apiName, apiPayload
177
+ } = ctx;
178
+
179
+ const companyName = session.data.companyName;
180
+ const data = await fetchDataSomehow(companyName);
181
+ return res.api(data);
182
+ });
183
+ ```
184
+
185
+
131
186
 
132
187
  ### Adding Routes
133
188
  ```typescript
@@ -161,6 +216,84 @@ lambder.addModule(async (lambder: Lambder): Promise<void> => {
161
216
  });
162
217
  ```
163
218
 
219
+ ### Session Management
220
+
221
+ You can enable session tracking by:
222
+
223
+ ```typescript
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:
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
+
247
+ async updateSessionData(updatedData):
248
+ // Updates the active sessions data and persist it to ddb.
249
+
250
+ async endSession(): Start a new session
251
+ // End session and delete from DDB.
252
+
253
+ async endSessionAll(): Start a new session
254
+ // Ends and deletes all registered sessions for this sessionKey across all devices
255
+ }
256
+ */
257
+ ```
258
+
259
+
260
+
261
+ #### Session Examples
262
+ ```typescript
263
+ lambder.addApi("getCompanyPage", async (ctx, res) => {
264
+
265
+ // createSession: Start a new session
266
+ const userId = "37234";
267
+ await lambder.getSessionController(ctx)
268
+ .createSession(userId, { "business": "Session data goes here" });
269
+ console.log(ctx.session?.sessionKey); // "37234"
270
+ console.log(ctx.session?.data?.business); // "Session data goes here"
271
+
272
+ // fetchSession: Fetch and validate if there is an existing session
273
+ // This is automatically done for addSessionRoute and addSessionApi
274
+ await lambder.getSessionController(ctx).fetchSession();
275
+ console.log(ctx.session?.sessionKey); // "37234"
276
+
277
+ // updateSessionData: Updates the active sessions data and persist it to ddb.
278
+ await lambder.getSessionController(ctx)
279
+ .updateSessionData({ "business2": "Session data updated" });
280
+ console.log(ctx.session?.sessionKey); // "37234"
281
+ console.log(ctx.session?.data?.business); // undefined
282
+ console.log(ctx.session?.data?.business2); // "Session data updated"
283
+
284
+ // endSession: Ends the session and removes it from ddb
285
+ await lambder.getSessionController(ctx).endSession(); // End session
286
+ console.log(ctx.session?.sessionKey); // undefined
287
+ console.log(ctx.session?.data?.business); // undefined
288
+
289
+ // endSessionAll: Ends all registered sessions for this user in all devices.
290
+ await lambder.getSessionController(ctx).endSessionAll();
291
+ console.log(ctx.session?.sessionKey); // undefined
292
+ console.log(ctx.session?.data?.business); // undefined
293
+
294
+ });
295
+ ```
296
+
164
297
  ### EJS Templates:
165
298
 
166
299
  EJS templates have the variables `page` and `partial` available:
@@ -215,6 +348,7 @@ lambder.addApi("getCompanyName", async (ctx, res) => {
215
348
  headers, // Request Headers in an object. Exp: { "User-Agent": "....", ... }
216
349
  apiName, // In this function it would return "getCompanyName"
217
350
  apiPayload, // Same as post.payload
351
+ session, // Stores session. Only available in addSessionRoute and addSessionApi, otherwise null.
218
352
  } = ctx;
219
353
  return res.json({});
220
354
  });
package/dist/Lambder.d.ts CHANGED
@@ -49,9 +49,8 @@ export default class Lambder {
49
49
  apiPath?: string;
50
50
  ejsPath?: string;
51
51
  apiVersion?: string;
52
- isCorsEnabled?: boolean;
53
52
  });
54
- enableCORS(isCorsEnabled: boolean): void;
53
+ setIsCorsEnabled(isCorsEnabled: boolean): void;
55
54
  enableDdbSession({ tableName, tableRegion, sessionSalt }: {
56
55
  tableName: string;
57
56
  tableRegion: string;
@@ -64,11 +63,12 @@ export default class Lambder {
64
63
  setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): void;
65
64
  setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): void;
66
65
  setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): void;
67
- getPatternMatch(pattern: string, path: string): Record<string, any>;
68
- testPatternMatch(pattern: string, path: string): boolean;
69
- private fetchSession;
70
- private validateSessionForRoute;
71
- private validateSessionForAPI;
66
+ private getPatternMatch;
67
+ private testPatternMatch;
68
+ fetchSessionIfExist(ctx: LambderRenderContext): Promise<LambderSessionContext | null>;
69
+ fetchSession(ctx: LambderRenderContext): Promise<LambderSessionContext>;
70
+ private areRequestSessionTokensValid;
71
+ private isSessionValid;
72
72
  private handleNoMatchedAction;
73
73
  addModule(moduleFn: LambderModuleFunction): Promise<void>;
74
74
  importModule(moduleImport: Promise<{
@@ -83,10 +83,11 @@ export default class Lambder {
83
83
  addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
84
84
  addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
85
85
  getSessionController(ctx: LambderRenderContext): {
86
- startSession: (userKey: string, data?: any, ttlInSeconds?: number) => Promise<LambderSessionContext>;
87
- editSession: (newData: any) => Promise<LambderSessionContext>;
88
- deleteSession: () => Promise<void>;
89
- deleteSessionAll: () => Promise<void>;
86
+ createSession: (sessionKey: string, data?: any, ttlInSeconds?: number) => Promise<LambderSessionContext>;
87
+ fetchSession: () => Promise<LambderSessionContext>;
88
+ updateSessionData: (newData: any) => Promise<LambderSessionContext>;
89
+ endSession: () => Promise<void>;
90
+ endSessionAll: () => Promise<void>;
90
91
  };
91
92
  getResponseBuilder(): LambderResponseBuilder;
92
93
  private getResolver;
package/dist/Lambder.js CHANGED
@@ -5,6 +5,9 @@ import LambderResolver from "./LambderResolver.js";
5
5
  import LambderResponseBuilder from "./LambderResponseBuilder.js";
6
6
  import LambderUtils from "./LambderUtils.js";
7
7
  import LambderSession from "./LambderSession.js";
8
+ const isApiCallChecker = (method, path, post, apiPath) => {
9
+ return method === "POST" && apiPath && path === apiPath && post.apiName;
10
+ };
8
11
  export const createContext = (event, lambdaContext, apiPath) => {
9
12
  const host = event.headers.Host || event.headers.host || "";
10
13
  const path = event.path;
@@ -27,9 +30,9 @@ export const createContext = (event, lambdaContext, apiPath) => {
27
30
  }
28
31
  catch (e) { }
29
32
  // 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
+ const isApiCall = isApiCallChecker(method, path, post, apiPath);
34
+ const apiName = isApiCall ? post.apiName : null;
35
+ const apiPayload = isApiCall ? post.payload : null;
33
36
  return { host, path, pathParams, method, get, post, cookie, apiName, apiPayload, headers, session, lambdaContext };
34
37
  };
35
38
  export default class Lambder {
@@ -60,7 +63,7 @@ export default class Lambder {
60
63
  };
61
64
  this.utils = new LambderUtils({ ejsPath });
62
65
  }
63
- enableCORS(isCorsEnabled) {
66
+ setIsCorsEnabled(isCorsEnabled) {
64
67
  this.isCorsEnabled = isCorsEnabled;
65
68
  }
66
69
  enableDdbSession({ tableName, tableRegion, sessionSalt }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
@@ -90,32 +93,59 @@ export default class Lambder {
90
93
  testPatternMatch(pattern, path) {
91
94
  return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
92
95
  }
96
+ async fetchSessionIfExist(ctx) {
97
+ try {
98
+ return await this.fetchSession(ctx);
99
+ }
100
+ catch (err) {
101
+ return null;
102
+ }
103
+ }
93
104
  async fetchSession(ctx) {
94
105
  if (!this.lambderSession)
95
- throw "Session not found";
106
+ throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
107
+ if (!this.areRequestSessionTokensValid(ctx)) {
108
+ throw new Error("Session tokens are invalid");
109
+ }
96
110
  const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
97
111
  if (!sessionToken)
98
- throw "Session not found";
112
+ throw new Error("Session token not found");
99
113
  const session = await this.lambderSession.getSession(sessionToken);
114
+ if (!session)
115
+ throw new Error("Session not found");
116
+ if (!this.isSessionValid(ctx, session))
117
+ throw new Error("Invalid session");
100
118
  ctx.session = session;
101
- return true;
119
+ return session;
102
120
  }
103
121
  ;
104
- validateSessionForRoute(ctx) {
105
- if (!this.lambderSession)
106
- return false;
122
+ areRequestSessionTokensValid(ctx) {
123
+ const isApiCall = isApiCallChecker(ctx.method, ctx.path, ctx.post, this.apiPath);
107
124
  const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
108
- return this.lambderSession.isSessionValid(ctx.session, sessionToken, null, true);
125
+ const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
126
+ if (isApiCall) {
127
+ const csrfToken = ctx.post?.token;
128
+ const isCsrfTokenValid = typeof csrfToken === "string" && csrfToken.length > 0;
129
+ return isSessionTokenValid && isCsrfTokenValid;
130
+ }
131
+ else {
132
+ return isSessionTokenValid;
133
+ }
109
134
  }
110
- ;
111
- validateSessionForAPI(ctx) {
135
+ isSessionValid(ctx, session) {
112
136
  if (!this.lambderSession)
113
137
  return false;
114
- const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
115
- const csrfToken = ctx.post?.token;
116
- return this.lambderSession.isSessionValid(ctx.session, sessionToken, csrfToken);
138
+ const isApiCall = isApiCallChecker(ctx.method, ctx.path, ctx.post, this.apiPath);
139
+ if (isApiCall) {
140
+ const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
141
+ const csrfToken = ctx.post?.token;
142
+ return this.lambderSession.isSessionValid(session, sessionToken, csrfToken);
143
+ }
144
+ else {
145
+ const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
146
+ return this.lambderSession.isSessionValid(session, sessionToken, null, true);
147
+ }
117
148
  }
118
- ;
119
149
  async handleNoMatchedAction(ctx, resolver) {
120
150
  for (const hook of this.hookList["fallback"]) {
121
151
  await hook.hookFn(ctx, resolver);
@@ -166,9 +196,6 @@ export default class Lambder {
166
196
  (condition?.constructor == RegExp && condition.test(ctx.path)))),
167
197
  actionFn: async (ctx, resolver) => {
168
198
  await this.fetchSession(ctx);
169
- const isSessionValid = this.validateSessionForRoute(ctx);
170
- if (!isSessionValid)
171
- throw new Error("Session not found");
172
199
  if (typeof condition === "string") {
173
200
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
174
201
  }
@@ -196,9 +223,6 @@ export default class Lambder {
196
223
  (apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
197
224
  actionFn: async (ctx, resolver) => {
198
225
  await this.fetchSession(ctx);
199
- const isSessionValid = this.validateSessionForAPI(ctx);
200
- if (!isSessionValid)
201
- throw new Error("Session not found");
202
226
  return await actionFn(ctx, resolver);
203
227
  }
204
228
  });
@@ -215,33 +239,38 @@ export default class Lambder {
215
239
  }
216
240
  getSessionController(ctx) {
217
241
  return {
218
- startSession: async (userKey, data, ttlInSeconds) => {
242
+ createSession: async (sessionKey, data, ttlInSeconds) => {
219
243
  if (!this.lambderSession)
220
- throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
221
- ctx.session = await this.lambderSession.createSession(userKey, data, ttlInSeconds);
244
+ throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
245
+ ctx.session = await this.lambderSession.createSession(sessionKey, data, ttlInSeconds);
222
246
  return ctx.session;
223
247
  },
224
- editSession: async (newData) => {
248
+ fetchSession: async () => {
249
+ if (!this.lambderSession)
250
+ throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
251
+ return this.fetchSession(ctx);
252
+ },
253
+ updateSessionData: async (newData) => {
225
254
  if (!this.lambderSession)
226
- throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
255
+ throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
227
256
  if (!ctx.session)
228
- throw "Session not found.";
229
- ctx.session = await this.lambderSession.editSession(ctx.session, newData);
257
+ throw new Error("Session not found.");
258
+ ctx.session = await this.lambderSession.updateSessionData(ctx.session, newData);
230
259
  return ctx.session;
231
260
  },
232
- deleteSession: async () => {
261
+ endSession: async () => {
233
262
  if (!this.lambderSession)
234
- throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
263
+ throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
235
264
  if (!ctx.session)
236
- throw "Session not found.";
265
+ throw new Error("Session not found.");
237
266
  await this.lambderSession.deleteSession(ctx.session);
238
267
  ctx.session = null;
239
268
  },
240
- deleteSessionAll: async () => {
269
+ endSessionAll: async () => {
241
270
  if (!this.lambderSession)
242
- throw "Session is not enabled. Use lambder.enableDdbSession(...) to configure.";
271
+ throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
243
272
  if (!ctx.session)
244
- throw "Session not found.";
273
+ throw new Error("Session not found.");
245
274
  await this.lambderSession.deleteSessionAll(ctx.session);
246
275
  ctx.session = null;
247
276
  },
@@ -2,7 +2,7 @@ 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;
@@ -27,8 +27,8 @@ 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>;
31
- editSession(session: LambderSessionContext, newData?: any): Promise<LambderSessionContext>;
30
+ createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
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;
34
34
  deleteSession(session: Record<string, any>): Promise<boolean>;
@@ -63,51 +63,51 @@ 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);
81
81
  return session;
82
82
  }
83
- async editSession(session, newData) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "1.0.113",
3
+ "version": "1.0.115",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/Lambder.ts CHANGED
@@ -41,6 +41,9 @@ type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext|null, r
41
41
  type RouteFallbackHandlerFunction = (ctx:LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
42
42
  type ApiFallbackHandlerFunction = (ctx:LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
43
43
 
44
+ const isApiCallChecker = (method: string, path: string, post: Record<any,any>, apiPath: string): boolean => {
45
+ return method === "POST" && apiPath && path === apiPath && post.apiName;
46
+ }
44
47
 
45
48
  export const createContext = (
46
49
  event: APIGatewayProxyEvent,
@@ -63,9 +66,9 @@ 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
+ const isApiCall = isApiCallChecker(method, path, post, apiPath);
70
+ const apiName:string = isApiCall ? post.apiName : null;
71
+ const apiPayload:string = isApiCall ? post.payload : null;
69
72
 
70
73
  return { host, path, pathParams, method, get, post, cookie, apiName, apiPayload, headers, session, lambdaContext };
71
74
  }
@@ -96,7 +99,7 @@ export default class Lambder {
96
99
 
97
100
  constructor(
98
101
  { publicPath, apiPath, ejsPath, apiVersion }:
99
- { publicPath: string, apiPath?: string, ejsPath?: string, apiVersion?: string, isCorsEnabled?: boolean, }
102
+ { publicPath: string, apiPath?: string, ejsPath?: string, apiVersion?: string }
100
103
  ){
101
104
  this.publicPath = publicPath || "/incorrect-path-not-found";
102
105
  this.ejsPath = ejsPath || "/incorrect-ejs-path-not-found";
@@ -113,7 +116,7 @@ export default class Lambder {
113
116
  this.utils = new LambderUtils({ ejsPath });
114
117
  }
115
118
 
116
- enableCORS(isCorsEnabled: boolean){
119
+ setIsCorsEnabled(isCorsEnabled: boolean){
117
120
  this.isCorsEnabled = isCorsEnabled;
118
121
  }
119
122
 
@@ -140,37 +143,65 @@ export default class Lambder {
140
143
  setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction){
141
144
  this.globalErrorHandler = globalErrorHandler;
142
145
  }
143
- getPatternMatch(pattern: string, path: string): Record<string, any> {
146
+ private getPatternMatch(pattern: string, path: string): Record<string, any> {
144
147
  const result = (match(pattern, { decode: decodeURIComponent }))(path);
145
148
  if(!result) return {};
146
149
  return result?.params || {};
147
150
  }
148
- testPatternMatch(pattern: string, path: string): boolean{
151
+ private testPatternMatch(pattern: string, path: string): boolean{
149
152
  return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
150
153
  }
151
154
 
152
- private async fetchSession (ctx: LambderRenderContext): Promise<boolean>{
153
- if(!this.lambderSession) throw "Session not found";
155
+ async fetchSessionIfExist(ctx: LambderRenderContext): Promise<LambderSessionContext|null>{
156
+ try {
157
+ return await this.fetchSession(ctx);
158
+ }catch(err){
159
+ return null;
160
+ }
161
+ }
162
+
163
+ async fetchSession (ctx: LambderRenderContext): Promise<LambderSessionContext>{
164
+ if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
165
+ if(!this.areRequestSessionTokensValid(ctx)){ throw new Error("Session tokens are invalid"); }
166
+
154
167
  const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
155
- if(!sessionToken) throw "Session not found";
168
+ if(!sessionToken) throw new Error("Session token not found");
169
+
156
170
  const session = await this.lambderSession.getSession(sessionToken);
171
+ if(!session) throw new Error("Session not found");
172
+
173
+ if(!this.isSessionValid(ctx, session)) throw new Error("Invalid session");
157
174
  ctx.session = session;
158
- return true;
175
+ return session;
159
176
  };
160
177
 
161
- private validateSessionForRoute (ctx: LambderRenderContext): boolean{
162
- if(!this.lambderSession) return false;
178
+ private areRequestSessionTokensValid(ctx: LambderRenderContext): boolean {
179
+ const isApiCall = isApiCallChecker(ctx.method, ctx.path, ctx.post, this.apiPath);
163
180
  const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
164
- return this.lambderSession.isSessionValid(ctx.session, sessionToken, null, true);
165
- };
181
+ const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
166
182
 
167
- private validateSessionForAPI (ctx: LambderRenderContext): boolean{
183
+ if(isApiCall){
184
+ const csrfToken = ctx.post?.token;
185
+ const isCsrfTokenValid = typeof csrfToken === "string" && csrfToken.length > 0
186
+ return isSessionTokenValid && isCsrfTokenValid;
187
+ }else{
188
+ return isSessionTokenValid;
189
+ }
190
+ }
191
+
192
+ private isSessionValid(ctx: LambderRenderContext, session: any): boolean {
168
193
  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
-
194
+ const isApiCall = isApiCallChecker(ctx.method, ctx.path, ctx.post, this.apiPath);
195
+ if(isApiCall){
196
+ const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
197
+ const csrfToken = ctx.post?.token;
198
+ return this.lambderSession.isSessionValid(session, sessionToken, csrfToken);
199
+ }else{
200
+ const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
201
+ return this.lambderSession.isSessionValid(session, sessionToken, null, true);
202
+ }
203
+ }
204
+
174
205
  private async handleNoMatchedAction(ctx: LambderRenderContext, resolver: LambderResolver){
175
206
  for(const hook of this.hookList["fallback"]){ await hook.hookFn(ctx, resolver); }
176
207
 
@@ -228,8 +259,6 @@ export default class Lambder {
228
259
  ),
229
260
  actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
230
261
  await this.fetchSession(ctx);
231
- const isSessionValid = this.validateSessionForRoute(ctx);
232
- if(!isSessionValid) throw new Error("Session not found");
233
262
  if(typeof condition === "string"){
234
263
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
235
264
  }else if(condition?.constructor == RegExp){
@@ -264,8 +293,6 @@ export default class Lambder {
264
293
  ),
265
294
  actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
266
295
  await this.fetchSession(ctx);
267
- const isSessionValid = this.validateSessionForAPI(ctx);
268
- if(!isSessionValid) throw new Error("Session not found");
269
296
  return await actionFn(ctx, resolver);
270
297
  }
271
298
  });
@@ -288,35 +315,39 @@ export default class Lambder {
288
315
  }
289
316
  }
290
317
 
291
- public getSessionController(ctx: LambderRenderContext){
318
+ getSessionController(ctx: LambderRenderContext){
292
319
  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);
320
+ createSession: async (sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext> => {
321
+ if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
322
+ ctx.session = await this.lambderSession.createSession(sessionKey, data, ttlInSeconds);
296
323
  return ctx.session;
297
324
  },
298
- editSession: 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.editSession(ctx.session, newData);
325
+ fetchSession: async (): Promise<LambderSessionContext> => {
326
+ if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
327
+ return this.fetchSession(ctx);
328
+ },
329
+ updateSessionData: async (newData: any): Promise<LambderSessionContext> => {
330
+ if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
331
+ if(!ctx.session) throw new Error("Session not found.");
332
+ ctx.session = await this.lambderSession.updateSessionData(ctx.session, newData);
302
333
  return ctx.session;
303
334
  },
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.";
335
+ endSession: async () => {
336
+ if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
337
+ if(!ctx.session) throw new Error("Session not found.");
307
338
  await this.lambderSession.deleteSession(ctx.session);
308
339
  ctx.session = null
309
340
  },
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.";
341
+ endSessionAll: async () => {
342
+ if(!this.lambderSession) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
343
+ if(!ctx.session) throw new Error("Session not found.");
313
344
  await this.lambderSession.deleteSessionAll(ctx.session);
314
345
  ctx.session = null
315
346
  },
316
347
  }
317
348
  }
318
349
 
319
- public getResponseBuilder(){
350
+ getResponseBuilder(){
320
351
  return new LambderResponseBuilder({
321
352
  isCorsEnabled: this.isCorsEnabled,
322
353
  publicPath: this.publicPath,
@@ -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;
@@ -94,52 +94,52 @@ 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);
116
116
  return session;
117
117
  }
118
118
 
119
- public async editSession(
119
+ public async updateSessionData(
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){