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 +136 -2
- package/dist/Lambder.d.ts +12 -11
- package/dist/Lambder.js +65 -36
- package/dist/LambderSession.d.ts +3 -3
- package/dist/LambderSession.js +18 -18
- package/package.json +1 -1
- package/src/Lambder.ts +71 -40
- package/src/LambderSession.ts +18 -18
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**:
|
|
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
|
-
|
|
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
|
|
68
|
-
testPatternMatch
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
private
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
31
|
-
const apiName =
|
|
32
|
-
const apiPayload =
|
|
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
|
-
|
|
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
|
|
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
|
|
119
|
+
return session;
|
|
102
120
|
}
|
|
103
121
|
;
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
242
|
+
createSession: async (sessionKey, data, ttlInSeconds) => {
|
|
219
243
|
if (!this.lambderSession)
|
|
220
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
221
|
-
ctx.session = await this.lambderSession.createSession(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
261
|
+
endSession: async () => {
|
|
233
262
|
if (!this.lambderSession)
|
|
234
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
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
|
-
|
|
269
|
+
endSessionAll: async () => {
|
|
241
270
|
if (!this.lambderSession)
|
|
242
|
-
throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
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
|
},
|
package/dist/LambderSession.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type LambderSessionContext = {
|
|
|
2
2
|
[x: string]: any;
|
|
3
3
|
sessionToken: string;
|
|
4
4
|
csrfToken: string;
|
|
5
|
-
|
|
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(
|
|
31
|
-
|
|
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>;
|
package/dist/LambderSession.js
CHANGED
|
@@ -63,51 +63,51 @@ export default class LambderSession {
|
|
|
63
63
|
}));
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
async createSession(
|
|
67
|
-
const
|
|
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 = `${
|
|
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]:
|
|
74
|
+
[this.partitionKey]: sessionKeyHash,
|
|
75
75
|
[this.sortKey]: sessionSortKey,
|
|
76
76
|
sessionToken, csrfToken,
|
|
77
|
-
|
|
77
|
+
sessionKey, data,
|
|
78
78
|
createdAt, expiresAt, ttlInSeconds
|
|
79
79
|
};
|
|
80
80
|
await this.ddbPutItem(session);
|
|
81
81
|
return session;
|
|
82
82
|
}
|
|
83
|
-
async
|
|
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 [
|
|
92
|
-
if (!
|
|
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]:
|
|
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.
|
|
106
|
-
throw "Not found: session.
|
|
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.
|
|
127
|
+
if (!session.sessionKey)
|
|
128
128
|
return false;
|
|
129
129
|
if (!session.createdAt)
|
|
130
130
|
return false;
|
package/package.json
CHANGED
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
|
|
67
|
-
const apiName:string =
|
|
68
|
-
const apiPayload:string =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
|
175
|
+
return session;
|
|
159
176
|
};
|
|
160
177
|
|
|
161
|
-
private
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
};
|
|
181
|
+
const isSessionTokenValid = sessionToken && sessionToken?.split(":")?.length === 2;
|
|
166
182
|
|
|
167
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
318
|
+
getSessionController(ctx: LambderRenderContext){
|
|
292
319
|
return {
|
|
293
|
-
|
|
294
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
295
|
-
ctx.session = await this.lambderSession.createSession(
|
|
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
|
-
|
|
299
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
305
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
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
|
-
|
|
311
|
-
if(!this.lambderSession) throw "Session is not enabled. Use lambder.enableDdbSession(...) to
|
|
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
|
-
|
|
350
|
+
getResponseBuilder(){
|
|
320
351
|
return new LambderResponseBuilder({
|
|
321
352
|
isCorsEnabled: this.isCorsEnabled,
|
|
322
353
|
publicPath: this.publicPath,
|
package/src/LambderSession.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type LambderSessionContext = {
|
|
|
6
6
|
[x: string]: any;
|
|
7
7
|
sessionToken: string;
|
|
8
8
|
csrfToken: string;
|
|
9
|
-
|
|
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
|
-
|
|
97
|
+
sessionKey: string,
|
|
98
98
|
data: any = {},
|
|
99
99
|
ttlInSeconds:number = 30*24*60*60
|
|
100
100
|
): Promise<LambderSessionContext> {
|
|
101
|
-
const
|
|
101
|
+
const sessionKeyHash = this.sessionUserKeyHasher(sessionKey);
|
|
102
102
|
const sessionSortKey = crypto.randomBytes(32).toString("hex");
|
|
103
|
-
const sessionToken = `${
|
|
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]:
|
|
109
|
+
[this.partitionKey]: sessionKeyHash,
|
|
110
110
|
[this.sortKey]: sessionSortKey,
|
|
111
111
|
sessionToken, csrfToken,
|
|
112
|
-
|
|
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
|
|
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 [
|
|
131
|
-
if(!
|
|
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]:
|
|
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.
|
|
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.
|
|
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){
|