lambder 1.0.143 → 1.0.145
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 +14 -6
- package/dist/LambderSessionController.d.ts +7 -7
- package/dist/LambderSessionController.js +1 -1
- package/dist/LambderSessionManager.d.ts +2 -2
- package/examples/secure-session-example.ts +10 -10
- package/package.json +2 -1
- package/src/Lambder.ts +29 -18
- package/src/LambderSessionController.ts +9 -9
- package/src/LambderSessionManager.ts +2 -2
- 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
|
@@ -14,7 +14,6 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
14
14
|
const method = event.httpMethod;
|
|
15
15
|
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
16
16
|
const headers = event.headers;
|
|
17
|
-
const session = null;
|
|
18
17
|
// Decode body for the post
|
|
19
18
|
let post = {};
|
|
20
19
|
try {
|
|
@@ -35,8 +34,9 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
35
34
|
return {
|
|
36
35
|
host, path, pathParams, method,
|
|
37
36
|
get, post, cookie, event,
|
|
37
|
+
session: null,
|
|
38
38
|
apiName, apiPayload,
|
|
39
|
-
headers,
|
|
39
|
+
headers, lambdaContext,
|
|
40
40
|
_otherInternal: {
|
|
41
41
|
isApiCall, requestVersion,
|
|
42
42
|
setHeaderFnAccumulator: [],
|
|
@@ -152,14 +152,18 @@ export default class Lambder {
|
|
|
152
152
|
(typeof condition === "function" && condition(ctx)) ||
|
|
153
153
|
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
154
154
|
actionFn: async (ctx, resolver) => {
|
|
155
|
-
await this.getSessionController(ctx).fetchSession();
|
|
156
155
|
if (typeof condition === "string") {
|
|
157
156
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
158
157
|
}
|
|
159
158
|
else if (condition?.constructor == RegExp) {
|
|
160
159
|
ctx.pathParams = ctx.path.match(condition);
|
|
161
160
|
}
|
|
162
|
-
|
|
161
|
+
const sessionCtx = ctx;
|
|
162
|
+
await this.getSessionController(ctx).fetchSession();
|
|
163
|
+
if (!sessionCtx.session) {
|
|
164
|
+
throw new Error("Session not found.");
|
|
165
|
+
}
|
|
166
|
+
return await actionFn(sessionCtx, resolver);
|
|
163
167
|
}
|
|
164
168
|
});
|
|
165
169
|
}
|
|
@@ -181,8 +185,12 @@ export default class Lambder {
|
|
|
181
185
|
(typeof apiName === "function" && apiName(ctx)) ||
|
|
182
186
|
(apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
|
|
183
187
|
actionFn: async (ctx, resolver) => {
|
|
188
|
+
const sessionCtx = ctx;
|
|
184
189
|
await this.getSessionController(ctx).fetchSession();
|
|
185
|
-
|
|
190
|
+
if (!sessionCtx.session) {
|
|
191
|
+
throw new Error("Session not found.");
|
|
192
|
+
}
|
|
193
|
+
return await actionFn(sessionCtx, resolver);
|
|
186
194
|
}
|
|
187
195
|
});
|
|
188
196
|
}
|
|
@@ -239,7 +247,7 @@ export default class Lambder {
|
|
|
239
247
|
if (firstMatchedAction) {
|
|
240
248
|
// Check version if provided by the client and the server
|
|
241
249
|
if (this.apiVersion && ctx._otherInternal.requestVersion) {
|
|
242
|
-
if (ctx.
|
|
250
|
+
if (ctx._otherInternal.requestVersion !== this.apiVersion) {
|
|
243
251
|
const responseBuilder = this.getResponseBuilder();
|
|
244
252
|
return resolve(responseBuilder.versionExpired());
|
|
245
253
|
}
|
|
@@ -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;
|
|
@@ -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.145",
|
|
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
|
@@ -20,10 +20,10 @@ export type LambderRenderContext<TApiPayload = any> = {
|
|
|
20
20
|
get: Record<string, any>;
|
|
21
21
|
post: Record<string, any>;
|
|
22
22
|
cookie: Record<string, any>;
|
|
23
|
+
session: null;
|
|
23
24
|
apiName: string;
|
|
24
25
|
apiPayload: TApiPayload;
|
|
25
26
|
headers: APIGatewayProxyEventHeaders;
|
|
26
|
-
session: LambderSessionContext|null;
|
|
27
27
|
event: APIGatewayProxyEvent;
|
|
28
28
|
lambdaContext: Context;
|
|
29
29
|
_otherInternal: {
|
|
@@ -35,11 +35,17 @@ export type LambderRenderContext<TApiPayload = any> = {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
export type LambderSessionRenderContext<
|
|
39
|
+
TApiPayload = any, SessionData = any
|
|
40
|
+
> = Omit<LambderRenderContext<TApiPayload>, 'session'> & { session: LambderSessionContext<SessionData> };
|
|
41
|
+
|
|
38
42
|
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
39
43
|
|
|
40
44
|
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
41
45
|
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
42
|
-
type
|
|
46
|
+
type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
47
|
+
|
|
48
|
+
type ActionObject = { conditionFn: ConditionFunction, actionFn: ActionFunction | SessionActionFunction };
|
|
43
49
|
|
|
44
50
|
type HookEventType = "created"|"beforeRender"|"afterRender"|"fallback";
|
|
45
51
|
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
@@ -64,7 +70,6 @@ export const createContext = (
|
|
|
64
70
|
const method = event.httpMethod;
|
|
65
71
|
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
66
72
|
const headers = event.headers;
|
|
67
|
-
const session = null;
|
|
68
73
|
|
|
69
74
|
// Decode body for the post
|
|
70
75
|
let post: Record<string, any> = {};
|
|
@@ -83,8 +88,9 @@ export const createContext = (
|
|
|
83
88
|
return {
|
|
84
89
|
host, path, pathParams, method,
|
|
85
90
|
get, post, cookie, event,
|
|
91
|
+
session: null,
|
|
86
92
|
apiName, apiPayload,
|
|
87
|
-
headers,
|
|
93
|
+
headers, lambdaContext,
|
|
88
94
|
_otherInternal: {
|
|
89
95
|
isApiCall, requestVersion,
|
|
90
96
|
setHeaderFnAccumulator: [],
|
|
@@ -95,7 +101,7 @@ export const createContext = (
|
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
|
|
98
|
-
export default class Lambder<TContract extends ApiContractShape = any> {
|
|
104
|
+
export default class Lambder<TContract extends ApiContractShape = any, TSessionData = any> {
|
|
99
105
|
public apiPath: string;
|
|
100
106
|
public apiVersion: null|string;
|
|
101
107
|
public isCorsEnabled: boolean = false;
|
|
@@ -218,7 +224,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
218
224
|
});
|
|
219
225
|
};
|
|
220
226
|
|
|
221
|
-
addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn:
|
|
227
|
+
addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>):void{
|
|
222
228
|
this.actionList.push({
|
|
223
229
|
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
224
230
|
ctx.method === "GET" &&
|
|
@@ -229,14 +235,17 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
229
235
|
)
|
|
230
236
|
),
|
|
231
237
|
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
232
|
-
await this.getSessionController(ctx).fetchSession();
|
|
233
|
-
|
|
234
238
|
if(typeof condition === "string"){
|
|
235
239
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
236
240
|
}else if(condition?.constructor == RegExp){
|
|
237
241
|
ctx.pathParams = ctx.path.match(condition);
|
|
238
242
|
}
|
|
239
|
-
|
|
243
|
+
|
|
244
|
+
const sessionCtx = ctx as unknown as LambderSessionRenderContext<any, TSessionData>;
|
|
245
|
+
await this.getSessionController(ctx).fetchSession();
|
|
246
|
+
if(!sessionCtx.session){ throw new Error("Session not found."); }
|
|
247
|
+
|
|
248
|
+
return await actionFn(sessionCtx, resolver);
|
|
240
249
|
}
|
|
241
250
|
});
|
|
242
251
|
};
|
|
@@ -276,23 +285,23 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
276
285
|
// Overload for untyped session API with RegExp or function
|
|
277
286
|
addSessionApi(
|
|
278
287
|
apiName: ConditionFunction|RegExp,
|
|
279
|
-
actionFn:
|
|
288
|
+
actionFn: SessionActionFunction<TSessionData>
|
|
280
289
|
):void;
|
|
281
290
|
// Overload for typed session API with string name (must come before untyped string overload)
|
|
282
291
|
addSessionApi<TApiName extends keyof TContract & string>(
|
|
283
292
|
apiName: TApiName,
|
|
284
293
|
actionFn: (
|
|
285
|
-
ctx:
|
|
294
|
+
ctx: LambderSessionRenderContext<TContract[TApiName]['input'], TSessionData>,
|
|
286
295
|
resolver: LambderResolver<TContract, TApiName>
|
|
287
296
|
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
288
297
|
):void;
|
|
289
298
|
// Overload for untyped session API with string (backward compatibility, must be last)
|
|
290
299
|
addSessionApi(
|
|
291
300
|
apiName: string,
|
|
292
|
-
actionFn:
|
|
301
|
+
actionFn: SessionActionFunction<TSessionData>
|
|
293
302
|
):void;
|
|
294
303
|
// Implementation
|
|
295
|
-
addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn:
|
|
304
|
+
addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>):void{
|
|
296
305
|
this.actionList.push({
|
|
297
306
|
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
298
307
|
!!ctx.apiName && (
|
|
@@ -302,8 +311,10 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
302
311
|
)
|
|
303
312
|
),
|
|
304
313
|
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
314
|
+
const sessionCtx = ctx as unknown as LambderSessionRenderContext<any, TSessionData>;
|
|
305
315
|
await this.getSessionController(ctx).fetchSession();
|
|
306
|
-
|
|
316
|
+
if(!sessionCtx.session){ throw new Error("Session not found."); }
|
|
317
|
+
return await actionFn(sessionCtx, resolver);
|
|
307
318
|
}
|
|
308
319
|
});
|
|
309
320
|
};
|
|
@@ -325,10 +336,10 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
325
336
|
}
|
|
326
337
|
}
|
|
327
338
|
|
|
328
|
-
getSessionController(ctx: LambderRenderContext<any>): LambderSessionController{
|
|
339
|
+
getSessionController(ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>): LambderSessionController<TSessionData>{
|
|
329
340
|
if(!this.lambderSessionManager) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
330
341
|
|
|
331
|
-
return new LambderSessionController({
|
|
342
|
+
return new LambderSessionController<TSessionData>({
|
|
332
343
|
lambderSessionManager: this.lambderSessionManager,
|
|
333
344
|
sessionTokenCookieKey: this.sessionTokenCookieKey,
|
|
334
345
|
sessionCsrfCookieKey: this.sessionCsrfCookieKey,
|
|
@@ -381,7 +392,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
381
392
|
if(firstMatchedAction){
|
|
382
393
|
// Check version if provided by the client and the server
|
|
383
394
|
if(this.apiVersion && ctx._otherInternal.requestVersion){
|
|
384
|
-
if(ctx.
|
|
395
|
+
if(ctx._otherInternal.requestVersion !== this.apiVersion){
|
|
385
396
|
const responseBuilder = this.getResponseBuilder();
|
|
386
397
|
return resolve(responseBuilder.versionExpired());
|
|
387
398
|
}
|
|
@@ -393,7 +404,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
393
404
|
ctx = hookCtx;
|
|
394
405
|
}
|
|
395
406
|
// Run matched action
|
|
396
|
-
let response = await firstMatchedAction.actionFn(ctx, resolver);
|
|
407
|
+
let response = await firstMatchedAction.actionFn(ctx as any, resolver);
|
|
397
408
|
// Run afterRender hooks
|
|
398
409
|
for(const hook of this.hookList["afterRender"]){
|
|
399
410
|
const hookResponse = await hook.hookFn(ctx, resolver, response);
|
|
@@ -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
|
};
|
|
@@ -2,12 +2,12 @@ import crypto from "crypto";
|
|
|
2
2
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
3
|
import { DynamoDBDocumentClient, QueryCommand, DeleteCommand, PutCommand, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
4
4
|
|
|
5
|
-
export type LambderSessionContext = {
|
|
5
|
+
export type LambderSessionContext<SessionData = any> = {
|
|
6
6
|
[x: string]: any;
|
|
7
7
|
sessionToken: string;
|
|
8
8
|
csrfToken: string;
|
|
9
9
|
sessionKey: string;
|
|
10
|
-
data:
|
|
10
|
+
data: SessionData;
|
|
11
11
|
createdAt: number;
|
|
12
12
|
expiresAt: number;
|
|
13
13
|
lastAccessedAt: number;
|