lambder 1.0.144 → 1.0.146

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
@@ -14,10 +14,10 @@ export type LambderRenderContext<TApiPayload = any> = {
14
14
  get: Record<string, any>;
15
15
  post: Record<string, any>;
16
16
  cookie: Record<string, any>;
17
+ session: null;
17
18
  apiName: string;
18
19
  apiPayload: TApiPayload;
19
20
  headers: APIGatewayProxyEventHeaders;
20
- session: LambderSessionContext | null;
21
21
  event: APIGatewayProxyEvent;
22
22
  lambdaContext: Context;
23
23
  _otherInternal: {
@@ -34,9 +34,13 @@ export type LambderRenderContext<TApiPayload = any> = {
34
34
  logToApiResponseAccumulator: any[];
35
35
  };
36
36
  };
37
+ export type LambderSessionRenderContext<TApiPayload = any, SessionData = any> = Omit<LambderRenderContext<TApiPayload>, 'session'> & {
38
+ session: LambderSessionContext<SessionData>;
39
+ };
37
40
  type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
38
41
  type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
39
42
  type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
43
+ type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
40
44
  type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
41
45
  type HookBeforeRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderRenderContext<any> | Error | Promise<LambderRenderContext<any> | Error>;
42
46
  type HookAfterRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, response: LambderResolverResponse) => LambderResolverResponse | Error | Promise<LambderResolverResponse | Error>;
@@ -45,7 +49,7 @@ type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any> |
45
49
  type RouteFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
46
50
  type ApiFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
47
51
  export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext<any>;
48
- export default class Lambder<TContract extends ApiContractShape = any> {
52
+ export default class Lambder<TContract extends ApiContractShape = any, TSessionData = any> {
49
53
  apiPath: string;
50
54
  apiVersion: null | string;
51
55
  isCorsEnabled: boolean;
@@ -88,18 +92,18 @@ export default class Lambder<TContract extends ApiContractShape = any> {
88
92
  default: LambderModuleFunction;
89
93
  }>): Promise<void>;
90
94
  addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
91
- addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
95
+ addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: SessionActionFunction<TSessionData>): void;
92
96
  addApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
93
97
  addApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver<TContract, TApiName>) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
94
98
  addApi(apiName: string, actionFn: ActionFunction): void;
95
- addSessionApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
96
- addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver<TContract, TApiName>) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
97
- addSessionApi(apiName: string, actionFn: ActionFunction): void;
99
+ addSessionApi(apiName: ConditionFunction | RegExp, actionFn: SessionActionFunction<TSessionData>): void;
100
+ addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderSessionRenderContext<TContract[TApiName]['input'], TSessionData>, resolver: LambderResolver<TContract, TApiName>) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
101
+ addSessionApi(apiName: string, actionFn: SessionActionFunction<TSessionData>): void;
98
102
  addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
99
103
  addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
100
104
  addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
101
105
  addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
102
- getSessionController(ctx: LambderRenderContext<any>): LambderSessionController;
106
+ getSessionController(ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>): LambderSessionController<TSessionData>;
103
107
  getResponseBuilder(): LambderResponseBuilder<any>;
104
108
  private getResolver;
105
109
  render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<LambderResolverResponse>;
package/dist/Lambder.js CHANGED
@@ -1,4 +1,3 @@
1
- import querystring from "querystring";
2
1
  import cookieParser from "cookie";
3
2
  import { match } from "path-to-regexp";
4
3
  import LambderResolver from "./LambderResolver.js";
@@ -14,7 +13,6 @@ export const createContext = (event, lambdaContext, apiPath) => {
14
13
  const method = event.httpMethod;
15
14
  const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
16
15
  const headers = event.headers;
17
- const session = null;
18
16
  // Decode body for the post
19
17
  let post = {};
20
18
  try {
@@ -23,7 +21,11 @@ export const createContext = (event, lambdaContext, apiPath) => {
23
21
  post = JSON.parse(decodedBody) || {};
24
22
  }
25
23
  catch (e) {
26
- post = querystring.parse(decodedBody) || {};
24
+ const params = new URLSearchParams(decodedBody);
25
+ post = {};
26
+ for (const [key, value] of params.entries()) {
27
+ post[key] = value;
28
+ }
27
29
  }
28
30
  }
29
31
  catch (e) { }
@@ -35,8 +37,9 @@ export const createContext = (event, lambdaContext, apiPath) => {
35
37
  return {
36
38
  host, path, pathParams, method,
37
39
  get, post, cookie, event,
40
+ session: null,
38
41
  apiName, apiPayload,
39
- headers, session, lambdaContext,
42
+ headers, lambdaContext,
40
43
  _otherInternal: {
41
44
  isApiCall, requestVersion,
42
45
  setHeaderFnAccumulator: [],
@@ -152,14 +155,18 @@ export default class Lambder {
152
155
  (typeof condition === "function" && condition(ctx)) ||
153
156
  (condition?.constructor == RegExp && condition.test(ctx.path)))),
154
157
  actionFn: async (ctx, resolver) => {
155
- await this.getSessionController(ctx).fetchSession();
156
158
  if (typeof condition === "string") {
157
159
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
158
160
  }
159
161
  else if (condition?.constructor == RegExp) {
160
162
  ctx.pathParams = ctx.path.match(condition);
161
163
  }
162
- return await actionFn(ctx, resolver);
164
+ const sessionCtx = ctx;
165
+ await this.getSessionController(ctx).fetchSession();
166
+ if (!sessionCtx.session) {
167
+ throw new Error("Session not found.");
168
+ }
169
+ return await actionFn(sessionCtx, resolver);
163
170
  }
164
171
  });
165
172
  }
@@ -181,8 +188,12 @@ export default class Lambder {
181
188
  (typeof apiName === "function" && apiName(ctx)) ||
182
189
  (apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
183
190
  actionFn: async (ctx, resolver) => {
191
+ const sessionCtx = ctx;
184
192
  await this.getSessionController(ctx).fetchSession();
185
- return await actionFn(ctx, resolver);
193
+ if (!sessionCtx.session) {
194
+ throw new Error("Session not found.");
195
+ }
196
+ return await actionFn(sessionCtx, resolver);
186
197
  }
187
198
  });
188
199
  }
@@ -1,3 +1,5 @@
1
+ import { createRequire } from 'module';
2
+ const require = createRequire(import.meta.url);
1
3
  export default class LambderMSW {
2
4
  apiPath;
3
5
  apiVersion;
@@ -28,12 +28,12 @@ export default class LambderResponseBuilder {
28
28
  throw new Error("File system operations are not available in browser environment");
29
29
  }
30
30
  const publicPath = path.resolve(this.publicPath);
31
- const absolutePath = path.join(publicPath, filePath);
31
+ const absolutePath = path.resolve(publicPath, filePath);
32
32
  console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
33
- if (!absolutePath.includes(publicPath)) {
33
+ if (!absolutePath.startsWith(publicPath)) {
34
34
  return "forbidden-public-path";
35
35
  }
36
- return fs.readFileSync(absolutePath);
36
+ return await fs.promises.readFile(absolutePath);
37
37
  }
38
38
  ;
39
39
  async checkPublicFileExist(filePath) {
@@ -43,12 +43,18 @@ export default class LambderResponseBuilder {
43
43
  return false;
44
44
  }
45
45
  const publicPath = path.resolve(this.publicPath);
46
- const absolutePath = path.join(this.publicPath, filePath);
46
+ const absolutePath = path.resolve(publicPath, filePath);
47
47
  console.log("checkPublicFileExist", { filePath, publicPath, absolutePath });
48
- if (!absolutePath.includes(publicPath)) {
48
+ if (!absolutePath.startsWith(publicPath)) {
49
+ return false;
50
+ }
51
+ try {
52
+ const stat = await fs.promises.stat(absolutePath);
53
+ return stat.isFile();
54
+ }
55
+ catch {
49
56
  return false;
50
57
  }
51
- return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
52
58
  }
53
59
  ;
54
60
  addHeader(key, value) {
@@ -1,21 +1,21 @@
1
- import { LambderRenderContext } from "./Lambder.js";
1
+ import { LambderRenderContext, LambderSessionRenderContext } from "./Lambder.js";
2
2
  import type LambderSessionManager from "./LambderSessionManager.js";
3
3
  import type { LambderSessionContext } from "./LambderSessionManager.js";
4
- export default class LambderSessionController {
4
+ export default class LambderSessionController<TSessionData = any> {
5
5
  lambderSessionManager: LambderSessionManager;
6
6
  sessionTokenCookieKey: string;
7
7
  sessionCsrfCookieKey: string;
8
- ctx: LambderRenderContext<any>;
8
+ ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>;
9
9
  constructor({ lambderSessionManager, sessionTokenCookieKey, sessionCsrfCookieKey, ctx, }: {
10
10
  lambderSessionManager: LambderSessionManager;
11
11
  sessionTokenCookieKey: string;
12
12
  sessionCsrfCookieKey: string;
13
- ctx: LambderRenderContext<any>;
13
+ ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>;
14
14
  });
15
15
  private areRequestSessionTokensValid;
16
- createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
17
- regenerateSession(): Promise<LambderSessionContext>;
18
- fetchSession(): Promise<LambderSessionContext>;
16
+ createSession(sessionKey: string, data?: TSessionData, ttlInSeconds?: number): Promise<LambderSessionContext<TSessionData>>;
17
+ regenerateSession(): Promise<LambderSessionContext<TSessionData>>;
18
+ fetchSession(): Promise<LambderSessionContext<TSessionData>>;
19
19
  fetchSessionIfExists(): Promise<LambderSessionContext | null>;
20
20
  isSessionValid(session: any): boolean;
21
21
  updateSessionData(newData: any): Promise<LambderSessionContext>;
@@ -2,7 +2,7 @@ export default class LambderSessionController {
2
2
  lambderSessionManager;
3
3
  sessionTokenCookieKey;
4
4
  sessionCsrfCookieKey;
5
- ctx;
5
+ ctx; // Internal context with mutable session property
6
6
  constructor({ lambderSessionManager, sessionTokenCookieKey, sessionCsrfCookieKey, ctx, }) {
7
7
  this.lambderSessionManager = lambderSessionManager;
8
8
  this.sessionTokenCookieKey = sessionTokenCookieKey;
@@ -1,9 +1,9 @@
1
- export type LambderSessionContext = {
1
+ export type LambderSessionContext<SessionData = any> = {
2
2
  [x: string]: any;
3
3
  sessionToken: string;
4
4
  csrfToken: string;
5
5
  sessionKey: string;
6
- data: any;
6
+ data: SessionData;
7
7
  createdAt: number;
8
8
  expiresAt: number;
9
9
  lastAccessedAt: number;
@@ -128,8 +128,8 @@ export default class LambderSessionManager {
128
128
  if (this.enableSlidingExpiration) {
129
129
  session.lastAccessedAt = Math.floor(Date.now() / 1000);
130
130
  session.expiresAt = session.lastAccessedAt + session.ttlInSeconds;
131
- // Fire and forget - don't wait for the update
132
- this.ddbPutItem(session).catch(() => { });
131
+ // Wait for the update to ensure it persists before Lambda freezes
132
+ await this.ddbPutItem(session).catch(() => { });
133
133
  }
134
134
  return session;
135
135
  }
@@ -16,12 +16,12 @@ export default class LambderUtils {
16
16
  return "EJS PATH NOT SET!";
17
17
  }
18
18
  const ejsPath = path.resolve(this.ejsPath);
19
- const absolutePath = path.join(ejsPath, filePath);
19
+ const absolutePath = path.resolve(ejsPath, filePath);
20
20
  console.log("readEjsFileSync", { filePath, ejsPath, absolutePath });
21
- if (!absolutePath.includes(ejsPath)) {
21
+ if (!absolutePath.startsWith(ejsPath)) {
22
22
  return "forbidden-ejs-path";
23
23
  }
24
- return String(fs.readFileSync(absolutePath));
24
+ return String(await fs.promises.readFile(absolutePath));
25
25
  }
26
26
  ;
27
27
  async checkEjsFileExist(filePath) {
@@ -34,12 +34,18 @@ export default class LambderUtils {
34
34
  return "EJS PATH NOT SET!";
35
35
  }
36
36
  const ejsPath = path.resolve(this.ejsPath);
37
- const absolutePath = path.join(this.ejsPath, filePath);
37
+ const absolutePath = path.resolve(ejsPath, filePath);
38
38
  console.log("checkEjsFileExist", { filePath, ejsPath, absolutePath });
39
- if (!absolutePath.includes(ejsPath)) {
39
+ if (!absolutePath.startsWith(ejsPath)) {
40
+ return false;
41
+ }
42
+ try {
43
+ const stat = await fs.promises.stat(absolutePath);
44
+ return stat.isFile();
45
+ }
46
+ catch {
40
47
  return false;
41
48
  }
42
- return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
43
49
  }
44
50
  ;
45
51
  async renderEjs(template, pageData) {
@@ -31,7 +31,7 @@ lambder.addApi("user.login", async (ctx, resolver) => {
31
31
 
32
32
  // Create new session
33
33
  const sessionController = lambder.getSessionController(ctx);
34
- await sessionController.createSession(user.id, {
34
+ const session = await sessionController.createSession(user.id, {
35
35
  userId: user.id,
36
36
  username: user.username,
37
37
  role: user.role,
@@ -39,14 +39,14 @@ lambder.addApi("user.login", async (ctx, resolver) => {
39
39
 
40
40
  return resolver.api({
41
41
  success: true,
42
- csrfToken: ctx.session?.csrfToken,
42
+ csrfToken: session.csrfToken,
43
43
  });
44
44
  });
45
45
 
46
46
  // Example: Protected API that requires session
47
47
  lambder.addSessionApi("user.profile", async (ctx, resolver) => {
48
48
  // Session is automatically fetched and validated
49
- const sessionData = ctx.session?.data;
49
+ const sessionData = ctx.session.data;
50
50
 
51
51
  return resolver.api({
52
52
  userId: sessionData.userId,
@@ -62,7 +62,7 @@ lambder.addSessionApi("user.changePassword", async (ctx, resolver) => {
62
62
 
63
63
  // Validate old password
64
64
  const isValid = await validatePassword(
65
- ctx.session?.data.userId,
65
+ ctx.session.data.userId,
66
66
  oldPassword
67
67
  );
68
68
  if (!isValid) {
@@ -70,10 +70,10 @@ lambder.addSessionApi("user.changePassword", async (ctx, resolver) => {
70
70
  }
71
71
 
72
72
  // Update password
73
- await updatePassword(ctx.session?.data.userId, newPassword);
73
+ await updatePassword(ctx.session.data.userId, newPassword);
74
74
 
75
75
  // IMPORTANT: Regenerate session after password change to prevent session fixation
76
- await sessionController.regenerateSession();
76
+ const newSession = await sessionController.regenerateSession();
77
77
 
78
78
  // OPTIONAL: End all other sessions for this user
79
79
  await sessionController.endSessionAll();
@@ -81,7 +81,7 @@ lambder.addSessionApi("user.changePassword", async (ctx, resolver) => {
81
81
  return resolver.api({
82
82
  success: true,
83
83
  message: "Password changed successfully",
84
- csrfToken: ctx.session?.csrfToken, // Send new CSRF token
84
+ csrfToken: newSession.csrfToken, // Send new CSRF token
85
85
  });
86
86
  });
87
87
 
@@ -92,7 +92,7 @@ lambder.addSessionApi("user.updatePreferences", async (ctx, resolver) => {
92
92
 
93
93
  // Update session data (also extends expiration if sliding is enabled)
94
94
  await sessionController.updateSessionData({
95
- ...ctx.session?.data,
95
+ ...ctx.session.data,
96
96
  preferences: { theme, language },
97
97
  });
98
98
 
@@ -151,11 +151,11 @@ lambder.addApi("user.checkAuth", async (ctx, resolver) => {
151
151
  // Example: Route with session
152
152
  lambder.addSessionRoute("/dashboard", async (ctx, resolver) => {
153
153
  // Session is automatically fetched and validated
154
- const userData = ctx.session?.data;
154
+ const userData = ctx.session.data;
155
155
 
156
156
  return resolver.ejsFile("dashboard.ejs", {
157
157
  user: userData,
158
- csrfToken: ctx.session?.csrfToken,
158
+ csrfToken: ctx.session.csrfToken,
159
159
  });
160
160
  });
161
161
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "1.0.144",
3
+ "version": "1.0.146",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -55,6 +55,7 @@
55
55
  "@types/path-to-regexp": "^1.7.0",
56
56
  "@typescript-eslint/eslint-plugin": "^7.2.0",
57
57
  "@typescript-eslint/parser": "^7.2.0",
58
+ "aws-sdk-client-mock": "^4.1.0",
58
59
  "eslint": "^8.57.0",
59
60
  "typescript": "^5.9.3",
60
61
  "vitest": "^3.2.4"
package/src/Lambder.ts CHANGED
@@ -1,4 +1,3 @@
1
- import querystring from "querystring";
2
1
  import cookieParser from "cookie";
3
2
  import { match } from "path-to-regexp";
4
3
 
@@ -20,10 +19,10 @@ export type LambderRenderContext<TApiPayload = any> = {
20
19
  get: Record<string, any>;
21
20
  post: Record<string, any>;
22
21
  cookie: Record<string, any>;
22
+ session: null;
23
23
  apiName: string;
24
24
  apiPayload: TApiPayload;
25
25
  headers: APIGatewayProxyEventHeaders;
26
- session: LambderSessionContext|null;
27
26
  event: APIGatewayProxyEvent;
28
27
  lambdaContext: Context;
29
28
  _otherInternal: {
@@ -35,11 +34,17 @@ export type LambderRenderContext<TApiPayload = any> = {
35
34
  };
36
35
  };
37
36
 
37
+ export type LambderSessionRenderContext<
38
+ TApiPayload = any, SessionData = any
39
+ > = Omit<LambderRenderContext<TApiPayload>, 'session'> & { session: LambderSessionContext<SessionData> };
40
+
38
41
  type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
39
42
 
40
43
  type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
41
44
  type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
42
- type ActionObject = { conditionFn: ConditionFunction, actionFn: ActionFunction };
45
+ type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
46
+
47
+ type ActionObject = { conditionFn: ConditionFunction, actionFn: ActionFunction | SessionActionFunction };
43
48
 
44
49
  type HookEventType = "created"|"beforeRender"|"afterRender"|"fallback";
45
50
  type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
@@ -64,14 +69,19 @@ export const createContext = (
64
69
  const method = event.httpMethod;
65
70
  const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
66
71
  const headers = event.headers;
67
- const session = null;
68
72
 
69
73
  // Decode body for the post
70
74
  let post: Record<string, any> = {};
71
75
  try {
72
76
  const decodedBody = event.isBase64Encoded ? ( event.body ? Buffer.from(event.body,"base64").toString() : "{}" ) : ( event.body || "{}" );
73
77
  try { post = JSON.parse(decodedBody) || {}; }
74
- catch(e){ post = querystring.parse(decodedBody) || {}; }
78
+ catch(e){
79
+ const params = new URLSearchParams(decodedBody);
80
+ post = {};
81
+ for(const [key, value] of params.entries()){
82
+ post[key] = value;
83
+ }
84
+ }
75
85
  }catch(e){}
76
86
  // Parse api variables
77
87
 
@@ -83,8 +93,9 @@ export const createContext = (
83
93
  return {
84
94
  host, path, pathParams, method,
85
95
  get, post, cookie, event,
96
+ session: null,
86
97
  apiName, apiPayload,
87
- headers, session, lambdaContext,
98
+ headers, lambdaContext,
88
99
  _otherInternal: {
89
100
  isApiCall, requestVersion,
90
101
  setHeaderFnAccumulator: [],
@@ -95,7 +106,7 @@ export const createContext = (
95
106
  }
96
107
 
97
108
 
98
- export default class Lambder<TContract extends ApiContractShape = any> {
109
+ export default class Lambder<TContract extends ApiContractShape = any, TSessionData = any> {
99
110
  public apiPath: string;
100
111
  public apiVersion: null|string;
101
112
  public isCorsEnabled: boolean = false;
@@ -218,7 +229,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
218
229
  });
219
230
  };
220
231
 
221
- addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: ActionFunction):void{
232
+ addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>):void{
222
233
  this.actionList.push({
223
234
  conditionFn: (ctx:LambderRenderContext<any>) => (
224
235
  ctx.method === "GET" &&
@@ -229,14 +240,17 @@ export default class Lambder<TContract extends ApiContractShape = any> {
229
240
  )
230
241
  ),
231
242
  actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
232
- await this.getSessionController(ctx).fetchSession();
233
-
234
243
  if(typeof condition === "string"){
235
244
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
236
245
  }else if(condition?.constructor == RegExp){
237
246
  ctx.pathParams = ctx.path.match(condition);
238
247
  }
239
- return await actionFn(ctx, resolver);
248
+
249
+ const sessionCtx = ctx as unknown as LambderSessionRenderContext<any, TSessionData>;
250
+ await this.getSessionController(ctx).fetchSession();
251
+ if(!sessionCtx.session){ throw new Error("Session not found."); }
252
+
253
+ return await actionFn(sessionCtx, resolver);
240
254
  }
241
255
  });
242
256
  };
@@ -276,23 +290,23 @@ export default class Lambder<TContract extends ApiContractShape = any> {
276
290
  // Overload for untyped session API with RegExp or function
277
291
  addSessionApi(
278
292
  apiName: ConditionFunction|RegExp,
279
- actionFn: ActionFunction
293
+ actionFn: SessionActionFunction<TSessionData>
280
294
  ):void;
281
295
  // Overload for typed session API with string name (must come before untyped string overload)
282
296
  addSessionApi<TApiName extends keyof TContract & string>(
283
297
  apiName: TApiName,
284
298
  actionFn: (
285
- ctx: LambderRenderContext<TContract[TApiName]['input']>,
299
+ ctx: LambderSessionRenderContext<TContract[TApiName]['input'], TSessionData>,
286
300
  resolver: LambderResolver<TContract, TApiName>
287
301
  ) => LambderResolverResponse|Promise<LambderResolverResponse>
288
302
  ):void;
289
303
  // Overload for untyped session API with string (backward compatibility, must be last)
290
304
  addSessionApi(
291
305
  apiName: string,
292
- actionFn: ActionFunction
306
+ actionFn: SessionActionFunction<TSessionData>
293
307
  ):void;
294
308
  // Implementation
295
- addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
309
+ addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>):void{
296
310
  this.actionList.push({
297
311
  conditionFn: (ctx:LambderRenderContext<any>) => (
298
312
  !!ctx.apiName && (
@@ -302,8 +316,10 @@ export default class Lambder<TContract extends ApiContractShape = any> {
302
316
  )
303
317
  ),
304
318
  actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
319
+ const sessionCtx = ctx as unknown as LambderSessionRenderContext<any, TSessionData>;
305
320
  await this.getSessionController(ctx).fetchSession();
306
- return await actionFn(ctx, resolver);
321
+ if(!sessionCtx.session){ throw new Error("Session not found."); }
322
+ return await actionFn(sessionCtx, resolver);
307
323
  }
308
324
  });
309
325
  };
@@ -325,10 +341,10 @@ export default class Lambder<TContract extends ApiContractShape = any> {
325
341
  }
326
342
  }
327
343
 
328
- getSessionController(ctx: LambderRenderContext<any>): LambderSessionController{
344
+ getSessionController(ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>): LambderSessionController<TSessionData>{
329
345
  if(!this.lambderSessionManager) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
330
346
 
331
- return new LambderSessionController({
347
+ return new LambderSessionController<TSessionData>({
332
348
  lambderSessionManager: this.lambderSessionManager,
333
349
  sessionTokenCookieKey: this.sessionTokenCookieKey,
334
350
  sessionCsrfCookieKey: this.sessionCsrfCookieKey,
@@ -393,7 +409,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
393
409
  ctx = hookCtx;
394
410
  }
395
411
  // Run matched action
396
- let response = await firstMatchedAction.actionFn(ctx, resolver);
412
+ let response = await firstMatchedAction.actionFn(ctx as any, resolver);
397
413
  // Run afterRender hooks
398
414
  for(const hook of this.hookList["afterRender"]){
399
415
  const hookResponse = await hook.hookFn(ctx, resolver, response);
package/src/LambderMSW.ts CHANGED
@@ -1,6 +1,9 @@
1
+ import { createRequire } from 'module';
1
2
  import type { ApiContractShape } from './LambderApiContract';
2
3
  import type { LambderApiResponse } from './LambderResponseBuilder';
3
4
 
5
+ const require = createRequire(import.meta.url);
6
+
4
7
  // MSW types - these will be resolved at runtime when msw is installed
5
8
  type RequestHandler = any;
6
9
 
@@ -70,10 +70,10 @@ export default class LambderResponseBuilder<TContract extends ApiContractShape =
70
70
  }
71
71
 
72
72
  const publicPath = path.resolve(this.publicPath);
73
- const absolutePath = path.join(publicPath, filePath);
73
+ const absolutePath = path.resolve(publicPath, filePath);
74
74
  console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
75
- if(!absolutePath.includes(publicPath)){ return "forbidden-public-path"; }
76
- return fs.readFileSync(absolutePath);
75
+ if(!absolutePath.startsWith(publicPath)){ return "forbidden-public-path"; }
76
+ return await fs.promises.readFile(absolutePath);
77
77
  };
78
78
 
79
79
  private async checkPublicFileExist(filePath: string){
@@ -83,10 +83,15 @@ export default class LambderResponseBuilder<TContract extends ApiContractShape =
83
83
  if (!fs || !path) { return false; }
84
84
 
85
85
  const publicPath = path.resolve(this.publicPath);
86
- const absolutePath = path.join(this.publicPath, filePath);
86
+ const absolutePath = path.resolve(publicPath, filePath);
87
87
  console.log("checkPublicFileExist", { filePath, publicPath, absolutePath });
88
- if(!absolutePath.includes(publicPath)){ return false; }
89
- return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
88
+ if(!absolutePath.startsWith(publicPath)){ return false; }
89
+ try {
90
+ const stat = await fs.promises.stat(absolutePath);
91
+ return stat.isFile();
92
+ } catch {
93
+ return false;
94
+ }
90
95
  };
91
96
 
92
97
  addHeader(key: string, value: string){
@@ -1,12 +1,12 @@
1
- import { LambderRenderContext } from "./Lambder.js";
1
+ import { LambderRenderContext, LambderSessionRenderContext } from "./Lambder.js";
2
2
  import type LambderSessionManager from "./LambderSessionManager.js";
3
3
  import type { LambderSessionContext } from "./LambderSessionManager.js";
4
4
 
5
- export default class LambderSessionController {
5
+ export default class LambderSessionController<TSessionData = any> {
6
6
  lambderSessionManager: LambderSessionManager;
7
7
  sessionTokenCookieKey: string;
8
8
  sessionCsrfCookieKey: string;
9
- ctx: LambderRenderContext<any>;
9
+ ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>; // Internal context with mutable session property
10
10
 
11
11
  constructor(
12
12
  {
@@ -18,7 +18,7 @@ export default class LambderSessionController {
18
18
  lambderSessionManager: LambderSessionManager,
19
19
  sessionTokenCookieKey: string,
20
20
  sessionCsrfCookieKey: string,
21
- ctx: LambderRenderContext<any>,
21
+ ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>,
22
22
  }
23
23
  ){
24
24
  this.lambderSessionManager = lambderSessionManager;
@@ -40,7 +40,7 @@ export default class LambderSessionController {
40
40
  }
41
41
  };
42
42
 
43
- async createSession (sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext> {
43
+ async createSession (sessionKey: string, data?: TSessionData, ttlInSeconds?: number): Promise<LambderSessionContext<TSessionData>> {
44
44
  const session = await this.lambderSessionManager.createSession(sessionKey, data, ttlInSeconds);
45
45
  this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionTokenCookieKey}=${session.sessionToken}; Expires=${new Date(session.expiresAt * 1000).toUTCString()}; Path=/; HttpOnly; SameSite=Lax; Secure` });
46
46
  this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionCsrfCookieKey}=${session.csrfToken}; Expires=${new Date(session.expiresAt * 1000).toUTCString()}; Path=/; SameSite=Lax; Secure` });
@@ -48,7 +48,7 @@ export default class LambderSessionController {
48
48
  return this.ctx.session;
49
49
  };
50
50
 
51
- async regenerateSession (): Promise<LambderSessionContext> {
51
+ async regenerateSession (): Promise<LambderSessionContext<TSessionData>> {
52
52
  if(!this.ctx.session) throw new Error("Session not found.");
53
53
  const newSession = await this.lambderSessionManager.regenerateSession(this.ctx.session);
54
54
  this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionTokenCookieKey}=${newSession.sessionToken}; Expires=${new Date(newSession.expiresAt * 1000).toUTCString()}; Path=/; HttpOnly; SameSite=Lax; Secure` });
@@ -57,7 +57,7 @@ export default class LambderSessionController {
57
57
  return this.ctx.session;
58
58
  };
59
59
 
60
- async fetchSession (): Promise<LambderSessionContext>{
60
+ async fetchSession (): Promise<LambderSessionContext<TSessionData>>{
61
61
  if(!this.areRequestSessionTokensValid()){ throw new Error("Session tokens are invalid"); }
62
62
 
63
63
  const sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey];
@@ -101,7 +101,7 @@ export default class LambderSessionController {
101
101
  await this.lambderSessionManager.deleteSession(this.ctx.session);
102
102
  this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionTokenCookieKey}=0; Expires=${new Date(Date.now() - 100000).toUTCString()}; Path=/; HttpOnly; SameSite=Lax; Secure` });
103
103
  this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionCsrfCookieKey}=0; Expires=${new Date(Date.now() - 100000).toUTCString()}; Path=/; SameSite=Lax; Secure` });
104
- this.ctx.session = null
104
+ (this.ctx as any).session = null;
105
105
  };
106
106
 
107
107
  async endSessionAll (){
@@ -109,6 +109,6 @@ export default class LambderSessionController {
109
109
  await this.lambderSessionManager.deleteSessionAll(this.ctx.session);
110
110
  this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionTokenCookieKey}=0; Expires=${new Date(Date.now() - 100000).toUTCString()}; Path=/; HttpOnly; SameSite=Lax; Secure` });
111
111
  this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionCsrfCookieKey}=0; Expires=${new Date(Date.now() - 100000).toUTCString()}; Path=/; SameSite=Lax; Secure` });
112
- this.ctx.session = null
112
+ (this.ctx as any).session = null;
113
113
  };
114
114
  };