lambder 1.0.105 → 1.0.109

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/dist/Lambder.d.ts CHANGED
@@ -2,16 +2,8 @@ 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
6
  type Path = `/${string}`;
6
- type LambderSession = {
7
- userId: string;
8
- userIdHash: string;
9
- sessionHash: string;
10
- csrfToken: string;
11
- createdTimeStamp: number;
12
- expiresTimeStamp: number;
13
- data: Record<string, any>;
14
- };
15
7
  type LambderRenderContext = {
16
8
  host: string;
17
9
  path: string;
@@ -23,7 +15,7 @@ type LambderRenderContext = {
23
15
  apiName: string;
24
16
  apiPayload: any;
25
17
  headers: APIGatewayProxyEventHeaders;
26
- session: LambderSession | null;
18
+ session: LambderSessionContext | null;
27
19
  lambdaContext: Context;
28
20
  };
29
21
  type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
@@ -49,19 +41,34 @@ export default class Lambder {
49
41
  private routeFallbackHandler;
50
42
  private apiFallbackHandler;
51
43
  utils: LambderUtils;
52
- constructor({ publicPath, apiPath, ejsPath, apiVersion, isCorsEnabled }: {
44
+ private lambderSession?;
45
+ private sessionTokenCookieKey;
46
+ private sessionCsrfCookieKey;
47
+ constructor({ publicPath, apiPath, ejsPath, apiVersion }: {
53
48
  publicPath: string;
54
49
  apiPath?: string;
55
50
  ejsPath?: string;
56
51
  apiVersion?: string;
57
52
  isCorsEnabled?: boolean;
58
53
  });
54
+ enableCORS(isCorsEnabled: boolean): void;
55
+ enableDdbSession({ tableName, tableRegion, sessionSalt }: {
56
+ tableName: string;
57
+ tableRegion: string;
58
+ sessionSalt: string;
59
+ }, { partitionKey, sortKey }?: {
60
+ partitionKey: string;
61
+ sortKey: string;
62
+ }): void;
63
+ setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): void;
59
64
  setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): void;
60
65
  setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): void;
61
66
  setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): void;
62
67
  getPatternMatch(pattern: string, path: string): Record<string, any>;
63
68
  testPatternMatch(pattern: string, path: string): boolean;
64
- private validateSession;
69
+ private fetchSession;
70
+ private validateSessionForRoute;
71
+ private validateSessionForAPI;
65
72
  private handleNoMatchedAction;
66
73
  addModule(moduleFn: LambderModuleFunction): Promise<void>;
67
74
  importModule(moduleImport: Promise<{
package/dist/Lambder.js CHANGED
@@ -4,6 +4,7 @@ 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
8
  export const createContext = (event, lambdaContext, apiPath) => {
8
9
  const host = event.headers.Host || event.headers.host || "";
9
10
  const path = event.path;
@@ -34,7 +35,7 @@ export const createContext = (event, lambdaContext, apiPath) => {
34
35
  export default class Lambder {
35
36
  apiPath;
36
37
  apiVersion;
37
- isCorsEnabled;
38
+ isCorsEnabled = false;
38
39
  publicPath;
39
40
  ejsPath;
40
41
  actionList;
@@ -43,12 +44,14 @@ export default class Lambder {
43
44
  routeFallbackHandler = null;
44
45
  apiFallbackHandler = null;
45
46
  utils;
46
- constructor({ publicPath, apiPath, ejsPath, apiVersion, isCorsEnabled }) {
47
+ lambderSession;
48
+ sessionTokenCookieKey = "LMDRSESSIONTKID";
49
+ sessionCsrfCookieKey = "LMDRSESSIONCSID";
50
+ constructor({ publicPath, apiPath, ejsPath, apiVersion }) {
47
51
  this.publicPath = publicPath || "/incorrect-path-not-found";
48
52
  this.ejsPath = ejsPath || "/incorrect-ejs-path-not-found";
49
53
  this.apiPath = apiPath ?? "/api";
50
54
  this.apiVersion = apiVersion ?? null;
51
- this.isCorsEnabled = isCorsEnabled ?? false;
52
55
  this.actionList = [];
53
56
  this.hookList = {
54
57
  "beforeRender": [],
@@ -57,6 +60,18 @@ export default class Lambder {
57
60
  };
58
61
  this.utils = new LambderUtils({ ejsPath });
59
62
  }
63
+ enableCORS(isCorsEnabled) {
64
+ this.isCorsEnabled = isCorsEnabled;
65
+ }
66
+ enableDdbSession({ tableName, tableRegion, sessionSalt }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
67
+ this.lambderSession = new LambderSession({
68
+ tableName, tableRegion, partitionKey, sortKey, sessionSalt
69
+ });
70
+ }
71
+ setSessionCookieKey(sessionTokenCookieKey, sessionCsrfCookieKey) {
72
+ this.sessionTokenCookieKey = sessionTokenCookieKey;
73
+ this.sessionCsrfCookieKey = sessionCsrfCookieKey;
74
+ }
60
75
  setRouteFallbackHandler(routeFallbackHandler) {
61
76
  this.routeFallbackHandler = routeFallbackHandler;
62
77
  }
@@ -75,26 +90,30 @@ export default class Lambder {
75
90
  testPatternMatch(pattern, path) {
76
91
  return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
77
92
  }
78
- async validateSession(ctx) {
79
- const { session, apiName, post, cookie } = ctx;
80
- if (!session)
81
- return false;
82
- if (!session.userId || !session.userIdHash || !session.sessionHash || !session.csrfToken)
83
- return false;
84
- if (!session.createdTimeStamp || !session.expiresTimeStamp)
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)
85
106
  return false;
86
- if (session.expiresTimeStamp > Date.now())
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)
87
113
  return false;
88
- if (apiName) {
89
- if (!post.token)
90
- return false;
91
- if (post.token !== cookie.LMBDRTOKEN)
92
- return false;
93
- if (post.token !== session.csrfToken)
94
- return false;
95
- }
96
- // Check DDB;
97
- return false;
114
+ const sessionToken = ctx.cookie?.[this.sessionTokenCookieKey];
115
+ const csrfToken = ctx.post?.token;
116
+ return this.lambderSession.isSessionValid(ctx.session, sessionToken, csrfToken);
98
117
  }
99
118
  ;
100
119
  async handleNoMatchedAction(ctx, resolver) {
@@ -146,7 +165,8 @@ export default class Lambder {
146
165
  (typeof condition === "function" && condition(ctx)) ||
147
166
  (condition?.constructor == RegExp && condition.test(ctx.path)))),
148
167
  actionFn: async (ctx, resolver) => {
149
- const isSessionValid = this.validateSession(ctx);
168
+ await this.fetchSession(ctx);
169
+ const isSessionValid = this.validateSessionForRoute(ctx);
150
170
  if (!isSessionValid)
151
171
  throw new Error("Session not found");
152
172
  if (typeof condition === "string") {
@@ -175,7 +195,8 @@ export default class Lambder {
175
195
  (typeof apiName === "function" && apiName(ctx)) ||
176
196
  (apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
177
197
  actionFn: async (ctx, resolver) => {
178
- const isSessionValid = this.validateSession(ctx);
198
+ await this.fetchSession(ctx);
199
+ const isSessionValid = this.validateSessionForAPI(ctx);
179
200
  if (!isSessionValid)
180
201
  throw new Error("Session not found");
181
202
  return await actionFn(ctx, resolver);
@@ -48,7 +48,7 @@ export default class LambderCaller {
48
48
  fetchStartedHandler?: FetchStartEventHandler;
49
49
  fetchEndedHandler?: FetchEndEventHandler;
50
50
  });
51
- apiRaw<T>(apiName: string, payload?: any, options?: {
51
+ apiRaw<T = any>(apiName: string, payload?: any, options?: {
52
52
  headers?: Record<string, any>;
53
53
  versionExpiredHandler?: VoidFunction;
54
54
  sessionExpiredHandler?: VoidFunction;
@@ -58,7 +58,7 @@ export default class LambderCaller {
58
58
  errorHandler?: ErrorHandler;
59
59
  fetchStartedHandler?: FetchStartEventHandler;
60
60
  fetchEndedHandler?: FetchEndEventHandler;
61
- }): Promise<LambderApiResponse<T> | null>;
62
- api<T>(...params: Parameters<typeof LambderCaller.prototype.apiRaw>): Promise<T | null>;
61
+ }): Promise<LambderApiResponse<T> | null | undefined>;
62
+ api<T = any>(...params: Parameters<typeof LambderCaller.prototype.apiRaw>): Promise<T | null | undefined>;
63
63
  }
64
64
  export {};
@@ -116,8 +116,6 @@ export default class LambderCaller {
116
116
  // Use the same type for api but adjust the return type
117
117
  async api(...params) {
118
118
  const result = await this.apiRaw(...params);
119
- if (!result)
120
- return null;
121
- return result.payload ?? null;
119
+ return result?.payload;
122
120
  }
123
121
  }
@@ -0,0 +1,35 @@
1
+ export type LambderSessionContext = {
2
+ [x: string]: any;
3
+ sessionToken: string;
4
+ csrfToken: string;
5
+ userKey: string;
6
+ data: any;
7
+ createdAt: number;
8
+ expiresAt: number;
9
+ ttlInSeconds: number;
10
+ };
11
+ export default class LambderSession {
12
+ private tableName;
13
+ private sessionSalt;
14
+ private partitionKey;
15
+ private sortKey;
16
+ private ddbDocumentClient;
17
+ constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, }: {
18
+ tableName: string;
19
+ tableRegion: string;
20
+ partitionKey: string;
21
+ sortKey: string;
22
+ sessionSalt: string;
23
+ });
24
+ private sessionUserKeyHasher;
25
+ private ddbGetItem;
26
+ private ddbPutItem;
27
+ private ddbDeleteItem;
28
+ private ddbQueryAllByPartitionKey;
29
+ private ddbDeleteAllByPartitionKey;
30
+ createSession(userKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
31
+ getSession(sessionToken: string): Promise<LambderSessionContext | null>;
32
+ isSessionValid(session: any, sessionToken: any, csrfToken: any, skipCsrfTokenCheck?: boolean): boolean;
33
+ deleteSession(session: Record<string, any>): Promise<boolean>;
34
+ deleteSessionAll(session: Record<string, any>): Promise<boolean>;
35
+ }
@@ -0,0 +1,148 @@
1
+ import crypto from "crypto";
2
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
+ import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
4
+ export default class LambderSession {
5
+ tableName;
6
+ sessionSalt;
7
+ partitionKey;
8
+ sortKey;
9
+ ddbDocumentClient;
10
+ constructor({ tableName, tableRegion, partitionKey, sortKey, sessionSalt, }) {
11
+ this.tableName = tableName;
12
+ this.sessionSalt = sessionSalt;
13
+ this.partitionKey = partitionKey;
14
+ this.sortKey = sortKey;
15
+ const ddbClient = new DynamoDBClient({ region: tableRegion });
16
+ this.ddbDocumentClient = DynamoDBDocumentClient.from(ddbClient);
17
+ }
18
+ sessionUserKeyHasher(password) {
19
+ return crypto.createHash("sha256")
20
+ .update(`${password}${this.sessionSalt}`)
21
+ .digest("hex");
22
+ }
23
+ async ddbGetItem(key) {
24
+ const response = await this.ddbDocumentClient.send(new GetCommand({ TableName: this.tableName, Key: key, ConsistentRead: true }));
25
+ if (response.Item)
26
+ return response.Item;
27
+ return null;
28
+ }
29
+ ;
30
+ async ddbPutItem(item) {
31
+ return await this.ddbDocumentClient.send(new PutCommand({ TableName: this.tableName, Item: item, }));
32
+ }
33
+ ;
34
+ async ddbDeleteItem(key) {
35
+ return await this.ddbDocumentClient.send(new DeleteCommand({ TableName: this.tableName, Key: key, }));
36
+ }
37
+ ;
38
+ async ddbQueryAllByPartitionKey(partitionValue) {
39
+ const params = {
40
+ TableName: this.tableName,
41
+ KeyConditionExpression: "#pk = :pv",
42
+ ExpressionAttributeNames: { "#pk": this.partitionKey },
43
+ ExpressionAttributeValues: { ":pv": partitionValue },
44
+ };
45
+ const queryResults = [];
46
+ do {
47
+ const { Items, LastEvaluatedKey } = await this.ddbDocumentClient.send(new QueryCommand(params));
48
+ if (Items)
49
+ queryResults.push(...Items);
50
+ params.ExclusiveStartKey = LastEvaluatedKey;
51
+ if (typeof LastEvaluatedKey == "undefined")
52
+ return queryResults;
53
+ // eslint-disable-next-line no-constant-condition
54
+ } while (true);
55
+ }
56
+ ;
57
+ async ddbDeleteAllByPartitionKey(partitionValue) {
58
+ const queryResults = await this.ddbQueryAllByPartitionKey(partitionValue);
59
+ for (const item of queryResults) {
60
+ await this.ddbDocumentClient.send(new DeleteCommand({
61
+ TableName: this.tableName,
62
+ Key: { [this.partitionKey]: partitionValue, [this.sortKey]: item[this.sortKey] }
63
+ }));
64
+ }
65
+ }
66
+ async createSession(userKey, data = {}, ttlInSeconds = 30 * 24 * 60 * 60) {
67
+ const userKeyHash = this.sessionUserKeyHasher(userKey);
68
+ const sessionSortKey = crypto.randomBytes(32).toString("hex");
69
+ const sessionToken = `${userKeyHash}:${sessionSortKey}`;
70
+ const csrfToken = crypto.randomBytes(8).toString("hex");
71
+ const createdAt = Math.floor(Date.now() / 1000);
72
+ const expiresAt = Number(createdAt) + Number(ttlInSeconds);
73
+ const session = {
74
+ [this.partitionKey]: userKeyHash,
75
+ [this.sortKey]: sessionSortKey,
76
+ sessionToken, csrfToken,
77
+ userKey, data,
78
+ createdAt, expiresAt, ttlInSeconds
79
+ };
80
+ await this.ddbPutItem(session);
81
+ return session;
82
+ }
83
+ async getSession(sessionToken) {
84
+ const [userKeyHash, sessionSortKey] = sessionToken.split(":");
85
+ if (!userKeyHash || !sessionSortKey)
86
+ return null;
87
+ try {
88
+ let session = await this.ddbGetItem({
89
+ [this.partitionKey]: userKeyHash,
90
+ [this.sortKey]: sessionSortKey
91
+ });
92
+ if (!session)
93
+ throw "Session not found";
94
+ if (!session.sessionToken || session.sessionToken !== sessionToken)
95
+ throw "Not found: session.sessionToken";
96
+ if (!session.csrfToken)
97
+ throw "Not found: session.csrfToken";
98
+ if (!session.userKey)
99
+ throw "Not found: session.userKey";
100
+ if (!session.createdAt)
101
+ throw "Not found: session.createdAt";
102
+ if (!session.expiresAt || session.expiresAt < Date.now() / 1000)
103
+ throw "Not found: session.expiresAt";
104
+ return session;
105
+ }
106
+ catch (err) {
107
+ return null;
108
+ }
109
+ }
110
+ ;
111
+ isSessionValid(session, sessionToken, csrfToken, skipCsrfTokenCheck = false) {
112
+ if (!session)
113
+ return false;
114
+ if (!sessionToken || typeof sessionToken !== "string")
115
+ return false;
116
+ if (session.sessionToken !== sessionToken)
117
+ return false;
118
+ if (!session.csrfToken)
119
+ return false;
120
+ if (!session.userKey)
121
+ return false;
122
+ if (!session.createdAt)
123
+ return false;
124
+ if (!session.expiresAt || session.expiresAt < Date.now() / 1000)
125
+ return false;
126
+ if (!skipCsrfTokenCheck) {
127
+ if (!csrfToken || typeof csrfToken !== "string")
128
+ return false;
129
+ if (session.csrfToken !== csrfToken)
130
+ return false;
131
+ }
132
+ return true;
133
+ }
134
+ async deleteSession(session) {
135
+ await this.ddbDeleteItem({
136
+ [this.partitionKey]: session[this.partitionKey],
137
+ [this.sortKey]: session[this.sortKey],
138
+ });
139
+ return true;
140
+ }
141
+ ;
142
+ async deleteSessionAll(session) {
143
+ await this.ddbDeleteAllByPartitionKey(session[this.partitionKey]);
144
+ return true;
145
+ }
146
+ ;
147
+ }
148
+ ;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "1.0.105",
3
+ "version": "1.0.109",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,6 +13,8 @@
13
13
  "author": "",
14
14
  "license": "ISC",
15
15
  "dependencies": {
16
+ "@aws-sdk/client-dynamodb": "^3.574.0",
17
+ "@aws-sdk/lib-dynamodb": "^3.574.0",
16
18
  "cookie": "^0.6.0",
17
19
  "ejs": "^3.1.9",
18
20
  "js-cookie": "^3.0.5",
package/src/Lambder.ts CHANGED
@@ -6,19 +6,10 @@ 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
10
 
10
11
  type Path = `/${string}`;
11
12
 
12
- type LambderSession = {
13
- userId: string,
14
- userIdHash: string;
15
- sessionHash: string;
16
- csrfToken: string;
17
- createdTimeStamp: number;
18
- expiresTimeStamp: number;
19
- data: Record<string, any>;
20
- };
21
-
22
13
  type LambderRenderContext = {
23
14
  host: string;
24
15
  path: string;
@@ -30,7 +21,7 @@ type LambderRenderContext = {
30
21
  apiName: string;
31
22
  apiPayload: any;
32
23
  headers: APIGatewayProxyEventHeaders;
33
- session: LambderSession|null;
24
+ session: LambderSessionContext|null;
34
25
  lambdaContext: Context;
35
26
  };
36
27
 
@@ -83,7 +74,7 @@ export const createContext = (
83
74
  export default class Lambder {
84
75
  public apiPath: string;
85
76
  public apiVersion: null|string;
86
- public isCorsEnabled: boolean;
77
+ public isCorsEnabled: boolean = false;
87
78
  public publicPath: string;
88
79
  public ejsPath: string;
89
80
 
@@ -99,16 +90,18 @@ export default class Lambder {
99
90
 
100
91
  public utils: LambderUtils;
101
92
 
93
+ private lambderSession?: LambderSession;
94
+ private sessionTokenCookieKey = "LMDRSESSIONTKID";
95
+ private sessionCsrfCookieKey = "LMDRSESSIONCSID";
102
96
 
103
97
  constructor(
104
- { publicPath, apiPath, ejsPath, apiVersion, isCorsEnabled }:
98
+ { publicPath, apiPath, ejsPath, apiVersion }:
105
99
  { publicPath: string, apiPath?: string, ejsPath?: string, apiVersion?: string, isCorsEnabled?: boolean, }
106
100
  ){
107
101
  this.publicPath = publicPath || "/incorrect-path-not-found";
108
102
  this.ejsPath = ejsPath || "/incorrect-ejs-path-not-found";
109
103
  this.apiPath = apiPath ?? "/api";
110
104
  this.apiVersion = apiVersion ?? null;
111
- this.isCorsEnabled = isCorsEnabled ?? false;
112
105
 
113
106
  this.actionList = [];
114
107
  this.hookList = {
@@ -120,6 +113,24 @@ export default class Lambder {
120
113
  this.utils = new LambderUtils({ ejsPath });
121
114
  }
122
115
 
116
+ enableCORS(isCorsEnabled: boolean){
117
+ this.isCorsEnabled = isCorsEnabled;
118
+ }
119
+
120
+ enableDdbSession(
121
+ { tableName, tableRegion, sessionSalt }: { tableName: string; tableRegion: string; sessionSalt: string; },
122
+ { partitionKey, sortKey }: { partitionKey: string, sortKey: string } = { partitionKey: "pk", sortKey: "sk" }
123
+ ){
124
+ this.lambderSession = new LambderSession({
125
+ tableName, tableRegion, partitionKey, sortKey, sessionSalt
126
+ });
127
+ }
128
+
129
+ setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string){
130
+ this.sessionTokenCookieKey = sessionTokenCookieKey;
131
+ this.sessionCsrfCookieKey = sessionCsrfCookieKey;
132
+ }
133
+
123
134
  setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction){
124
135
  this.routeFallbackHandler = routeFallbackHandler;
125
136
  }
@@ -138,20 +149,26 @@ export default class Lambder {
138
149
  return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
139
150
  }
140
151
 
141
- private async validateSession (ctx: LambderRenderContext): Promise<boolean>{
142
- const { session, apiName, post, cookie } = ctx;
143
- if(!session) return false;
144
- if(!session.userId || !session.userIdHash || !session.sessionHash || !session.csrfToken) return false;
145
- if(!session.createdTimeStamp || !session.expiresTimeStamp) return false;
146
- if(session.expiresTimeStamp > Date.now()) return false;
147
-
148
- if(apiName){
149
- if(!post.token) return false;
150
- if(post.token !== cookie.LMBDRTOKEN) return false;
151
- if(post.token !== session.csrfToken) return false;
152
- }
153
- // Check DDB;
154
- return false;
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);
155
172
  };
156
173
 
157
174
  private async handleNoMatchedAction(ctx: LambderRenderContext, resolver: LambderResolver){
@@ -210,7 +227,8 @@ export default class Lambder {
210
227
  )
211
228
  ),
212
229
  actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
213
- const isSessionValid = this.validateSession(ctx);
230
+ await this.fetchSession(ctx);
231
+ const isSessionValid = this.validateSessionForRoute(ctx);
214
232
  if(!isSessionValid) throw new Error("Session not found");
215
233
  if(typeof condition === "string"){
216
234
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
@@ -245,7 +263,8 @@ export default class Lambder {
245
263
  )
246
264
  ),
247
265
  actionFn: async (ctx:LambderRenderContext, resolver: LambderResolver) => {
248
- const isSessionValid = this.validateSession(ctx);
266
+ await this.fetchSession(ctx);
267
+ const isSessionValid = this.validateSessionForAPI(ctx);
249
268
  if(!isSessionValid) throw new Error("Session not found");
250
269
  return await actionFn(ctx, resolver);
251
270
  }
@@ -81,7 +81,7 @@ export default class LambderCaller {
81
81
 
82
82
  };
83
83
 
84
- async apiRaw<T>(apiName: string, payload?: any, options?: {
84
+ async apiRaw<T=any>(apiName: string, payload?: any, options?: {
85
85
  headers?: Record<string, any>
86
86
  versionExpiredHandler?: VoidFunction,
87
87
  sessionExpiredHandler?: VoidFunction,
@@ -91,7 +91,7 @@ export default class LambderCaller {
91
91
  errorHandler?: ErrorHandler,
92
92
  fetchStartedHandler?: FetchStartEventHandler,
93
93
  fetchEndedHandler?: FetchEndEventHandler,
94
- }): Promise<LambderApiResponse<T>|null>{
94
+ }): Promise<LambderApiResponse<T>|null|undefined>{
95
95
  const headers = options?.headers;
96
96
  const fetchTracker: FetchTracker = { apiName, done: false, fetchEndCalled: false };
97
97
  try {
@@ -170,10 +170,9 @@ export default class LambderCaller {
170
170
  };
171
171
 
172
172
  // Use the same type for api but adjust the return type
173
- async api<T>(...params: Parameters<typeof LambderCaller.prototype.apiRaw>): Promise<T|null> {
173
+ async api<T=any>(...params: Parameters<typeof LambderCaller.prototype.apiRaw>): Promise<T|null|undefined> {
174
174
  const result = await this.apiRaw<T>(...params);
175
- if (!result) return null;
176
- return result.payload ?? null;
175
+ return result?.payload;
177
176
  }
178
177
 
179
178
  }
@@ -0,0 +1,166 @@
1
+ import crypto from "crypto";
2
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
+ import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
4
+
5
+ export type LambderSessionContext = {
6
+ [x: string]: any;
7
+ sessionToken: string;
8
+ csrfToken: string;
9
+ userKey: string;
10
+ data: any;
11
+ createdAt: number;
12
+ expiresAt: number;
13
+ ttlInSeconds: number;
14
+ };
15
+
16
+
17
+ export default class LambderSession{
18
+ private tableName: string;
19
+ private sessionSalt: string;
20
+ private partitionKey: string;
21
+ private sortKey: string;
22
+ private ddbDocumentClient: DynamoDBDocumentClient;
23
+
24
+ constructor(
25
+ {
26
+ tableName, tableRegion,
27
+ partitionKey, sortKey,
28
+ sessionSalt,
29
+ }: {
30
+ tableName: string, tableRegion: string,
31
+ partitionKey: string, sortKey: string,
32
+ sessionSalt: string,
33
+ }
34
+ ){
35
+ this.tableName = tableName;
36
+ this.sessionSalt = sessionSalt;
37
+ this.partitionKey = partitionKey;
38
+ this.sortKey = sortKey;
39
+
40
+ const ddbClient = new DynamoDBClient({ region: tableRegion });
41
+ this.ddbDocumentClient = DynamoDBDocumentClient.from(ddbClient);
42
+ }
43
+
44
+ private sessionUserKeyHasher(password:string){
45
+ return crypto.createHash("sha256")
46
+ .update(`${password}${this.sessionSalt}`)
47
+ .digest("hex");
48
+ }
49
+
50
+ private async ddbGetItem<T=any>(key:Record<string,string|number>){
51
+ const response = await this.ddbDocumentClient.send(
52
+ new GetCommand({ TableName: this.tableName, Key: key, ConsistentRead: true })
53
+ );
54
+ if(response.Item) return response.Item as T;
55
+ return null;
56
+ };
57
+ private async ddbPutItem(item: Record<string,any>){
58
+ return await this.ddbDocumentClient.send(
59
+ new PutCommand({ TableName: this.tableName, Item: item, })
60
+ );
61
+ };
62
+
63
+ private async ddbDeleteItem(key:Record<string,string|number>){
64
+ return await this.ddbDocumentClient.send(
65
+ new DeleteCommand({ TableName: this.tableName, Key: key, })
66
+ );
67
+ };
68
+
69
+ private async ddbQueryAllByPartitionKey (partitionValue: string){
70
+ const params: any = {
71
+ TableName: this.tableName,
72
+ KeyConditionExpression: "#pk = :pv",
73
+ ExpressionAttributeNames:{ "#pk": this.partitionKey },
74
+ ExpressionAttributeValues: { ":pv": partitionValue },
75
+ }
76
+ const queryResults: any[] = [];
77
+ do{
78
+ const {Items, LastEvaluatedKey} = await this.ddbDocumentClient.send(new QueryCommand(params));
79
+ if(Items) queryResults.push(...Items);
80
+ params.ExclusiveStartKey = LastEvaluatedKey;
81
+ if(typeof LastEvaluatedKey == "undefined") return queryResults;
82
+ // eslint-disable-next-line no-constant-condition
83
+ } while (true);
84
+ };
85
+
86
+ private async ddbDeleteAllByPartitionKey(partitionValue: string){
87
+ const queryResults = await this.ddbQueryAllByPartitionKey(partitionValue);
88
+ for(const item of queryResults){
89
+ await this.ddbDocumentClient.send(new DeleteCommand({
90
+ TableName: this.tableName,
91
+ Key: { [this.partitionKey]: partitionValue, [this.sortKey]: item[this.sortKey] }
92
+ }));
93
+ }
94
+ }
95
+
96
+ public async createSession(
97
+ userKey: string,
98
+ data: any = {},
99
+ ttlInSeconds:number = 30*24*60*60
100
+ ): Promise<LambderSessionContext> {
101
+ const userKeyHash = this.sessionUserKeyHasher(userKey);
102
+ const sessionSortKey = crypto.randomBytes(32).toString("hex");
103
+ const sessionToken = `${userKeyHash}:${sessionSortKey}`;
104
+ const csrfToken = crypto.randomBytes(8).toString("hex");
105
+ const createdAt = Math.floor(Date.now()/1000);
106
+ const expiresAt = Number(createdAt) + Number(ttlInSeconds);
107
+
108
+ const session = {
109
+ [this.partitionKey]: userKeyHash,
110
+ [this.sortKey]: sessionSortKey,
111
+ sessionToken, csrfToken,
112
+ userKey, data,
113
+ createdAt, expiresAt, ttlInSeconds
114
+ };
115
+ await this.ddbPutItem(session);
116
+ return session;
117
+ }
118
+
119
+ public async getSession(sessionToken: string): Promise<LambderSessionContext|null>{
120
+ const [ userKeyHash, sessionSortKey ] = sessionToken.split(":");
121
+ if(!userKeyHash || !sessionSortKey) return null;
122
+ try{
123
+ let session = await this.ddbGetItem({
124
+ [this.partitionKey]: userKeyHash,
125
+ [this.sortKey]: sessionSortKey
126
+ });
127
+ if(!session) throw "Session not found";
128
+ if(!session.sessionToken || session.sessionToken !== sessionToken) throw "Not found: session.sessionToken";
129
+ if(!session.csrfToken) throw "Not found: session.csrfToken";
130
+ if(!session.userKey) throw "Not found: session.userKey";
131
+ if(!session.createdAt) throw "Not found: session.createdAt";
132
+ if(!session.expiresAt || session.expiresAt < Date.now()/1000) throw "Not found: session.expiresAt";
133
+ return session;
134
+ }catch(err){
135
+ return null;
136
+ }
137
+ };
138
+
139
+ public isSessionValid(session: any, sessionToken: any, csrfToken: any, skipCsrfTokenCheck = false){
140
+ if(!session) return false;
141
+ if(!sessionToken || typeof sessionToken !== "string") return false;
142
+ if(session.sessionToken !== sessionToken) return false;
143
+ if(!session.csrfToken) return false;
144
+ if(!session.userKey) return false;
145
+ if(!session.createdAt) return false;
146
+ if(!session.expiresAt || session.expiresAt < Date.now()/1000) return false;
147
+ if(!skipCsrfTokenCheck){
148
+ if(!csrfToken || typeof csrfToken !== "string") return false;
149
+ if(session.csrfToken !== csrfToken) return false;
150
+ }
151
+ return true;
152
+ }
153
+
154
+ public async deleteSession(session: Record<string, any>): Promise<boolean>{
155
+ await this.ddbDeleteItem({
156
+ [this.partitionKey]: session[this.partitionKey],
157
+ [this.sortKey]: session[this.sortKey],
158
+ });
159
+ return true;
160
+ };
161
+
162
+ public async deleteSessionAll (session: Record<string, any>): Promise<boolean>{
163
+ await this.ddbDeleteAllByPartitionKey(session[this.partitionKey])
164
+ return true;
165
+ };
166
+ };