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 +11 -7
- package/dist/Lambder.js +18 -7
- package/dist/LambderMSW.js +2 -0
- package/dist/LambderResponseBuilder.js +12 -6
- package/dist/LambderSessionController.d.ts +7 -7
- package/dist/LambderSessionController.js +1 -1
- package/dist/LambderSessionManager.d.ts +2 -2
- package/dist/LambderSessionManager.js +2 -2
- package/dist/LambderUtils.js +12 -6
- package/examples/secure-session-example.ts +10 -10
- package/package.json +2 -1
- package/src/Lambder.ts +35 -19
- package/src/LambderMSW.ts +3 -0
- package/src/LambderResponseBuilder.ts +11 -6
- package/src/LambderSessionController.ts +9 -9
- package/src/LambderSessionManager.ts +4 -4
- package/src/LambderUtils.ts +11 -6
- package/tests/session.test.ts +1069 -0
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:
|
|
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:
|
|
96
|
-
addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx:
|
|
97
|
-
addSessionApi(apiName: string, actionFn:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
if (!sessionCtx.session) {
|
|
194
|
+
throw new Error("Session not found.");
|
|
195
|
+
}
|
|
196
|
+
return await actionFn(sessionCtx, resolver);
|
|
186
197
|
}
|
|
187
198
|
});
|
|
188
199
|
}
|
package/dist/LambderMSW.js
CHANGED
|
@@ -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.
|
|
31
|
+
const absolutePath = path.resolve(publicPath, filePath);
|
|
32
32
|
console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
|
|
33
|
-
if (!absolutePath.
|
|
33
|
+
if (!absolutePath.startsWith(publicPath)) {
|
|
34
34
|
return "forbidden-public-path";
|
|
35
35
|
}
|
|
36
|
-
return fs.
|
|
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.
|
|
46
|
+
const absolutePath = path.resolve(publicPath, filePath);
|
|
47
47
|
console.log("checkPublicFileExist", { filePath, publicPath, absolutePath });
|
|
48
|
-
if (!absolutePath.
|
|
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?:
|
|
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:
|
|
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
|
-
//
|
|
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
|
}
|
package/dist/LambderUtils.js
CHANGED
|
@@ -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.
|
|
19
|
+
const absolutePath = path.resolve(ejsPath, filePath);
|
|
20
20
|
console.log("readEjsFileSync", { filePath, ejsPath, absolutePath });
|
|
21
|
-
if (!absolutePath.
|
|
21
|
+
if (!absolutePath.startsWith(ejsPath)) {
|
|
22
22
|
return "forbidden-ejs-path";
|
|
23
23
|
}
|
|
24
|
-
return String(fs.
|
|
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.
|
|
37
|
+
const absolutePath = path.resolve(ejsPath, filePath);
|
|
38
38
|
console.log("checkEjsFileExist", { filePath, ejsPath, absolutePath });
|
|
39
|
-
if (!absolutePath.
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
154
|
+
const userData = ctx.session.data;
|
|
155
155
|
|
|
156
156
|
return resolver.ejsFile("dashboard.ejs", {
|
|
157
157
|
user: userData,
|
|
158
|
-
csrfToken: ctx.session
|
|
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.
|
|
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
|
|
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){
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
306
|
+
actionFn: SessionActionFunction<TSessionData>
|
|
293
307
|
):void;
|
|
294
308
|
// Implementation
|
|
295
|
-
addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn:
|
|
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
|
-
|
|
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.
|
|
73
|
+
const absolutePath = path.resolve(publicPath, filePath);
|
|
74
74
|
console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
|
|
75
|
-
if(!absolutePath.
|
|
76
|
-
return fs.
|
|
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.
|
|
86
|
+
const absolutePath = path.resolve(publicPath, filePath);
|
|
87
87
|
console.log("checkPublicFileExist", { filePath, publicPath, absolutePath });
|
|
88
|
-
if(!absolutePath.
|
|
89
|
-
|
|
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?:
|
|
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
|
};
|