lambder 1.0.128 → 1.0.130
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/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +90 -0
- package/Readme.md +49 -0
- package/dist/Lambder.d.ts +5 -4
- package/dist/Lambder.js +2 -2
- package/dist/LambderMSW.d.ts +104 -0
- package/dist/LambderMSW.js +203 -0
- package/dist/LambderResolver.d.ts +17 -15
- package/dist/LambderResolver.js +4 -0
- package/dist/LambderResponseBuilder.d.ts +2 -1
- package/dist/LambderSessionController.d.ts +1 -0
- package/dist/LambderSessionController.js +10 -0
- package/dist/LambderSessionManager.d.ts +6 -1
- package/dist/LambderSessionManager.js +43 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/docs/DYNAMODB_SETUP.md +96 -0
- package/docs/MSW_MOCKING.md +590 -0
- package/docs/TYPE_SAFE_QUICK_START.md +48 -4
- package/examples/msw-mocking-example.ts +241 -0
- package/examples/output-type-enforcement-example.ts +218 -0
- package/examples/secure-session-example.ts +191 -0
- package/examples/test-output-type-enforcement.ts +101 -0
- package/package.json +9 -1
- package/src/Lambder.ts +4 -4
- package/src/LambderMSW.ts +330 -0
- package/src/LambderResolver.ts +32 -15
- package/src/LambderResponseBuilder.ts +2 -1
- package/src/LambderSessionController.ts +9 -0
- package/src/LambderSessionManager.ts +51 -10
- package/src/index.ts +11 -0
- package/tests/msw-integration.test.ts +183 -0
- package/tests/output-type-runtime.test.ts +365 -0
- package/tests/type-safety.test.ts +312 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Using MSW to mock Lambder API endpoints
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to use Mock Service Worker (MSW) with Lambder
|
|
5
|
+
* to create type-safe mocks for your API endpoints.
|
|
6
|
+
*
|
|
7
|
+
* Prerequisites:
|
|
8
|
+
* - Install MSW: npm install --save-dev msw
|
|
9
|
+
* - Define your API contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
mockLambderApi,
|
|
14
|
+
mockLambderApiError,
|
|
15
|
+
mockLambderSessionExpired,
|
|
16
|
+
mockLambderNotAuthorized,
|
|
17
|
+
mockLambderVersionExpired,
|
|
18
|
+
type ApiContractShape
|
|
19
|
+
} from '../src/index.js';
|
|
20
|
+
|
|
21
|
+
// 1. Define your API contract
|
|
22
|
+
const myApiContract = {
|
|
23
|
+
'user.getProfile': {
|
|
24
|
+
input: { userId: '' as string },
|
|
25
|
+
output: { name: '' as string, email: '' as string, role: '' as string }
|
|
26
|
+
},
|
|
27
|
+
'user.updateProfile': {
|
|
28
|
+
input: { userId: '' as string, name: '' as string, email: '' as string },
|
|
29
|
+
output: { success: true as boolean, message: '' as string }
|
|
30
|
+
},
|
|
31
|
+
'user.deleteAccount': {
|
|
32
|
+
input: { userId: '' as string },
|
|
33
|
+
output: { success: true as boolean }
|
|
34
|
+
},
|
|
35
|
+
'admin.listUsers': {
|
|
36
|
+
input: { page: 0 as number, limit: 0 as number },
|
|
37
|
+
output: { users: [] as Array<{ id: string; name: string }>, total: 0 as number }
|
|
38
|
+
}
|
|
39
|
+
} satisfies ApiContractShape;
|
|
40
|
+
|
|
41
|
+
type MyApiContract = typeof myApiContract;
|
|
42
|
+
|
|
43
|
+
// 2. Create mock handlers
|
|
44
|
+
|
|
45
|
+
// Basic success mock
|
|
46
|
+
export const getUserProfileHandler = mockLambderApi<MyApiContract, 'user.getProfile'>(
|
|
47
|
+
'user.getProfile',
|
|
48
|
+
(input) => {
|
|
49
|
+
console.log('Mocking user.getProfile for userId:', input.userId);
|
|
50
|
+
|
|
51
|
+
// Return mock data with full type safety
|
|
52
|
+
return {
|
|
53
|
+
name: 'John Doe',
|
|
54
|
+
email: 'john@example.com',
|
|
55
|
+
role: 'user'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Mock with dynamic response based on input
|
|
61
|
+
export const updateProfileHandler = mockLambderApi<MyApiContract, 'user.updateProfile'>(
|
|
62
|
+
'user.updateProfile',
|
|
63
|
+
(input) => {
|
|
64
|
+
console.log('Mocking user.updateProfile:', input);
|
|
65
|
+
|
|
66
|
+
// Validate input
|
|
67
|
+
if (!input.name || input.name.length < 2) {
|
|
68
|
+
throw new Error('Name must be at least 2 characters');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
message: `Profile updated for ${input.name}`
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
delay: 500 // Simulate network delay
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Mock with error response
|
|
82
|
+
export const updateProfileErrorHandler = mockLambderApiError<MyApiContract, 'user.updateProfile'>(
|
|
83
|
+
'user.updateProfile',
|
|
84
|
+
'Failed to update profile due to server error',
|
|
85
|
+
{
|
|
86
|
+
delay: 300
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Mock session expired
|
|
91
|
+
export const deleteAccountSessionExpiredHandler = mockLambderSessionExpired<MyApiContract, 'user.deleteAccount'>(
|
|
92
|
+
'user.deleteAccount'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Mock not authorized
|
|
96
|
+
export const listUsersNotAuthorizedHandler = mockLambderNotAuthorized<MyApiContract, 'admin.listUsers'>(
|
|
97
|
+
'admin.listUsers'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Mock version expired
|
|
101
|
+
export const getProfileVersionExpiredHandler = mockLambderVersionExpired<MyApiContract, 'user.getProfile'>(
|
|
102
|
+
'user.getProfile'
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Mock with pagination
|
|
106
|
+
export const listUsersHandler = mockLambderApi<MyApiContract, 'admin.listUsers'>(
|
|
107
|
+
'admin.listUsers',
|
|
108
|
+
(input) => {
|
|
109
|
+
// Generate mock paginated data
|
|
110
|
+
const allUsers = Array.from({ length: 50 }, (_, i) => ({
|
|
111
|
+
id: `user-${i + 1}`,
|
|
112
|
+
name: `User ${i + 1}`
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const start = input.page * input.limit;
|
|
116
|
+
const end = start + input.limit;
|
|
117
|
+
const users = allUsers.slice(start, end);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
users,
|
|
121
|
+
total: allUsers.length
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// 3. Set up MSW handlers
|
|
127
|
+
|
|
128
|
+
// For Node.js testing (Vitest, Jest, etc.)
|
|
129
|
+
/**
|
|
130
|
+
* Setup example for Node.js tests:
|
|
131
|
+
*
|
|
132
|
+
* ```typescript
|
|
133
|
+
* // test-setup.ts
|
|
134
|
+
* import { setupServer } from 'msw/node';
|
|
135
|
+
* import { getUserProfileHandler, updateProfileHandler } from './msw-example';
|
|
136
|
+
*
|
|
137
|
+
* export const server = setupServer(
|
|
138
|
+
* getUserProfileHandler,
|
|
139
|
+
* updateProfileHandler,
|
|
140
|
+
* listUsersHandler
|
|
141
|
+
* );
|
|
142
|
+
*
|
|
143
|
+
* // vitest.setup.ts
|
|
144
|
+
* beforeAll(() => server.listen());
|
|
145
|
+
* afterEach(() => server.resetHandlers());
|
|
146
|
+
* afterAll(() => server.close());
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
// For browser development/Storybook
|
|
151
|
+
/**
|
|
152
|
+
* Setup example for browser:
|
|
153
|
+
*
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // mocks/browser.ts
|
|
156
|
+
* import { setupWorker } from 'msw/browser';
|
|
157
|
+
* import { getUserProfileHandler, updateProfileHandler } from './msw-example';
|
|
158
|
+
*
|
|
159
|
+
* export const worker = setupWorker(
|
|
160
|
+
* getUserProfileHandler,
|
|
161
|
+
* updateProfileHandler,
|
|
162
|
+
* listUsersHandler
|
|
163
|
+
* );
|
|
164
|
+
*
|
|
165
|
+
* // main.tsx
|
|
166
|
+
* if (import.meta.env.DEV) {
|
|
167
|
+
* const { worker } = await import('./mocks/browser');
|
|
168
|
+
* worker.start();
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
// 4. Using in tests
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Test example:
|
|
177
|
+
*
|
|
178
|
+
* ```typescript
|
|
179
|
+
* import { describe, it, expect } from 'vitest';
|
|
180
|
+
* import { server } from './test-setup';
|
|
181
|
+
* import { caller } from './api-client';
|
|
182
|
+
*
|
|
183
|
+
* describe('User Profile', () => {
|
|
184
|
+
* it('should get user profile', async () => {
|
|
185
|
+
* const profile = await caller.api('user.getProfile', { userId: '123' });
|
|
186
|
+
*
|
|
187
|
+
* expect(profile).toEqual({
|
|
188
|
+
* name: 'John Doe',
|
|
189
|
+
* email: 'john@example.com',
|
|
190
|
+
* role: 'user'
|
|
191
|
+
* });
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* it('should handle update errors', async () => {
|
|
195
|
+
* // Override handler for this test
|
|
196
|
+
* server.use(updateProfileErrorHandler);
|
|
197
|
+
*
|
|
198
|
+
* const result = await caller.api('user.updateProfile', {
|
|
199
|
+
* userId: '123',
|
|
200
|
+
* name: 'New Name',
|
|
201
|
+
* email: 'new@example.com'
|
|
202
|
+
* });
|
|
203
|
+
*
|
|
204
|
+
* expect(result).toBeNull(); // Error handler returns null
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* it('should handle session expiry', async () => {
|
|
208
|
+
* server.use(deleteAccountSessionExpiredHandler);
|
|
209
|
+
*
|
|
210
|
+
* const result = await caller.api('user.deleteAccount', { userId: '123' });
|
|
211
|
+
*
|
|
212
|
+
* expect(result).toBeNull();
|
|
213
|
+
* // Verify sessionExpiredHandler was called if configured
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* it('should handle not authorized', async () => {
|
|
217
|
+
* server.use(listUsersNotAuthorizedHandler);
|
|
218
|
+
*
|
|
219
|
+
* const result = await caller.api('admin.listUsers', { page: 0, limit: 10 });
|
|
220
|
+
*
|
|
221
|
+
* expect(result).toBeNull();
|
|
222
|
+
* // Verify notAuthorizedHandler was called if configured
|
|
223
|
+
* });
|
|
224
|
+
* });
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
// Export all handlers for easy setup
|
|
229
|
+
export const handlers = [
|
|
230
|
+
getUserProfileHandler,
|
|
231
|
+
updateProfileHandler,
|
|
232
|
+
listUsersHandler,
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// Export error handlers separately
|
|
236
|
+
export const errorHandlers = [
|
|
237
|
+
updateProfileErrorHandler,
|
|
238
|
+
deleteAccountSessionExpiredHandler,
|
|
239
|
+
listUsersNotAuthorizedHandler,
|
|
240
|
+
getProfileVersionExpiredHandler,
|
|
241
|
+
];
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Type Enforcement Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how Lambder now enforces output types
|
|
5
|
+
* in addition to input types for type-safe APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Lambder from '../src/Lambder.js';
|
|
9
|
+
import type { ApiContract } from '../src/index.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Define Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
type User = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
email: string;
|
|
19
|
+
age: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Product = {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
price: number;
|
|
26
|
+
inStock: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Stats = {
|
|
30
|
+
totalUsers: number;
|
|
31
|
+
totalProducts: number;
|
|
32
|
+
revenue: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Define API Contract
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
type MyApiContract = ApiContract<{
|
|
40
|
+
// Simple primitive outputs
|
|
41
|
+
getCount: { input: void, output: number },
|
|
42
|
+
getMessage: { input: { id: string }, output: string },
|
|
43
|
+
isActive: { input: void, output: boolean },
|
|
44
|
+
|
|
45
|
+
// Complex object outputs
|
|
46
|
+
getUser: { input: { userId: string }, output: User },
|
|
47
|
+
getProduct: { input: { productId: string }, output: Product },
|
|
48
|
+
|
|
49
|
+
// Array outputs
|
|
50
|
+
listUsers: { input: void, output: User[] },
|
|
51
|
+
listProducts: { input: { category: string }, output: Product[] },
|
|
52
|
+
|
|
53
|
+
// Complex nested outputs
|
|
54
|
+
getStats: { input: void, output: Stats },
|
|
55
|
+
|
|
56
|
+
// Nullable outputs
|
|
57
|
+
findUser: { input: { email: string }, output: User | null },
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Setup Backend with Type Enforcement
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
const lambder = new Lambder<MyApiContract>({
|
|
65
|
+
publicPath: './public',
|
|
66
|
+
apiPath: '/api',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ✅ EXAMPLE 1: Simple primitive outputs are enforced
|
|
70
|
+
lambder.addApi('getCount', async (ctx, resolver) => {
|
|
71
|
+
const count = 42;
|
|
72
|
+
return resolver.api(count); // ✅ TypeScript validates this is a number
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
lambder.addApi('getMessage', async (ctx, resolver) => {
|
|
76
|
+
const message = `Message for ${ctx.apiPayload.id}`;
|
|
77
|
+
return resolver.api(message); // ✅ TypeScript validates this is a string
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
lambder.addApi('isActive', async (ctx, resolver) => {
|
|
81
|
+
const active = true;
|
|
82
|
+
return resolver.api(active); // ✅ TypeScript validates this is a boolean
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ✅ EXAMPLE 2: Complex objects must match the shape
|
|
86
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
87
|
+
const user: User = {
|
|
88
|
+
id: ctx.apiPayload.userId,
|
|
89
|
+
name: "John Doe",
|
|
90
|
+
email: "john@example.com",
|
|
91
|
+
age: 30,
|
|
92
|
+
};
|
|
93
|
+
return resolver.api(user); // ✅ All required fields are present
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
lambder.addApi('getProduct', async (ctx, resolver) => {
|
|
97
|
+
const product: Product = {
|
|
98
|
+
id: ctx.apiPayload.productId,
|
|
99
|
+
name: "Widget",
|
|
100
|
+
price: 99.99,
|
|
101
|
+
inStock: true,
|
|
102
|
+
};
|
|
103
|
+
return resolver.api(product); // ✅ Correct shape
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ✅ EXAMPLE 3: Arrays are type-checked
|
|
107
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
108
|
+
const users: User[] = [
|
|
109
|
+
{ id: "1", name: "Alice", email: "alice@example.com", age: 25 },
|
|
110
|
+
{ id: "2", name: "Bob", email: "bob@example.com", age: 30 },
|
|
111
|
+
];
|
|
112
|
+
return resolver.api(users); // ✅ Array of User objects
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
lambder.addApi('listProducts', async (ctx, resolver) => {
|
|
116
|
+
const category = ctx.apiPayload.category;
|
|
117
|
+
const products: Product[] = [
|
|
118
|
+
{ id: "1", name: "Item 1", price: 10, inStock: true },
|
|
119
|
+
{ id: "2", name: "Item 2", price: 20, inStock: false },
|
|
120
|
+
];
|
|
121
|
+
return resolver.api(products); // ✅ Array of Product objects
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ✅ EXAMPLE 4: Nested objects are validated
|
|
125
|
+
lambder.addApi('getStats', async (ctx, resolver) => {
|
|
126
|
+
const stats: Stats = {
|
|
127
|
+
totalUsers: 100,
|
|
128
|
+
totalProducts: 50,
|
|
129
|
+
revenue: 10000,
|
|
130
|
+
};
|
|
131
|
+
return resolver.api(stats); // ✅ Matches Stats shape
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ✅ EXAMPLE 5: Nullable types work correctly
|
|
135
|
+
lambder.addApi('findUser', async (ctx, resolver) => {
|
|
136
|
+
const email = ctx.apiPayload.email;
|
|
137
|
+
|
|
138
|
+
if (email === "john@example.com") {
|
|
139
|
+
// Found user
|
|
140
|
+
const user: User = {
|
|
141
|
+
id: "123",
|
|
142
|
+
name: "John",
|
|
143
|
+
email: email,
|
|
144
|
+
age: 30,
|
|
145
|
+
};
|
|
146
|
+
return resolver.api(user); // ✅ Can return User
|
|
147
|
+
} else {
|
|
148
|
+
// Not found
|
|
149
|
+
return resolver.api(null); // ✅ Can return null
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ✅ EXAMPLE 6: Using die.api() also enforces types
|
|
154
|
+
lambder.addApi('getCount', async (ctx, resolver) => {
|
|
155
|
+
return resolver.die.api(42); // ✅ die.api() also type-checks
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ✅ EXAMPLE 7: Session APIs also enforce output types
|
|
159
|
+
lambder.addSessionApi('getUser', async (ctx, resolver) => {
|
|
160
|
+
const user: User = {
|
|
161
|
+
id: ctx.apiPayload.userId,
|
|
162
|
+
name: "Jane Doe",
|
|
163
|
+
email: "jane@example.com",
|
|
164
|
+
age: 28,
|
|
165
|
+
};
|
|
166
|
+
return resolver.api(user); // ✅ Type-safe for session APIs too
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// What TypeScript Prevents (these would cause compile errors)
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
// ❌ ERROR: Wrong primitive type
|
|
175
|
+
lambder.addApi('getCount', async (ctx, resolver) => {
|
|
176
|
+
return resolver.api("not a number"); // Error: string not assignable to number
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ❌ ERROR: Missing required field
|
|
180
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
181
|
+
return resolver.api({
|
|
182
|
+
id: "123",
|
|
183
|
+
name: "John",
|
|
184
|
+
email: "john@example.com",
|
|
185
|
+
// age is missing! - TypeScript error
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ❌ ERROR: Wrong field type
|
|
190
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
191
|
+
return resolver.api({
|
|
192
|
+
id: "123",
|
|
193
|
+
name: "John",
|
|
194
|
+
email: "john@example.com",
|
|
195
|
+
age: "30", // Error: string not assignable to number
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ❌ ERROR: Wrong array element type
|
|
200
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
201
|
+
return resolver.api([
|
|
202
|
+
{ id: "1", name: "Alice", email: "alice@example.com", age: 25 },
|
|
203
|
+
"not a user object", // Error: string not assignable to User
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ❌ ERROR: Returning wrong type when null is not allowed
|
|
208
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
209
|
+
return resolver.api(null); // Error: null not assignable to User (findUser allows null, but getUser doesn't)
|
|
210
|
+
});
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
console.log("✅ All examples demonstrate proper output type enforcement!");
|
|
214
|
+
console.log("✅ TypeScript will catch type mismatches at compile time!");
|
|
215
|
+
console.log("✅ This works with both resolver.api() and resolver.die.api()!");
|
|
216
|
+
console.log("✅ Both addApi() and addSessionApi() enforce output types!");
|
|
217
|
+
|
|
218
|
+
export default lambder;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import Lambder from "../src/Lambder.js";
|
|
2
|
+
|
|
3
|
+
// Example: Secure session handling with all security fixes applied
|
|
4
|
+
|
|
5
|
+
const lambder = new Lambder({
|
|
6
|
+
publicPath: "/public",
|
|
7
|
+
apiPath: "/api",
|
|
8
|
+
ejsPath: "/views",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Enable session management with sliding expiration
|
|
12
|
+
lambder.enableDdbSession(
|
|
13
|
+
{
|
|
14
|
+
tableName: process.env.SESSION_TABLE || "sessions",
|
|
15
|
+
tableRegion: process.env.AWS_REGION || "us-east-1",
|
|
16
|
+
sessionSalt: process.env.SESSION_SALT || "change-this-to-a-secure-random-string",
|
|
17
|
+
enableSlidingExpiration: true, // Sessions extend on each access
|
|
18
|
+
},
|
|
19
|
+
{ partitionKey: "pk", sortKey: "sk" }
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Example: Login API with session regeneration
|
|
23
|
+
lambder.addApi("user.login", async (ctx, resolver) => {
|
|
24
|
+
const { username, password } = ctx.apiPayload;
|
|
25
|
+
|
|
26
|
+
// Validate credentials (implement your own logic)
|
|
27
|
+
const user = await authenticateUser(username, password);
|
|
28
|
+
if (!user) {
|
|
29
|
+
return resolver.api({ success: false, error: "Invalid credentials" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create new session
|
|
33
|
+
const sessionController = lambder.getSessionController(ctx);
|
|
34
|
+
await sessionController.createSession(user.id, {
|
|
35
|
+
userId: user.id,
|
|
36
|
+
username: user.username,
|
|
37
|
+
role: user.role,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return resolver.api({
|
|
41
|
+
success: true,
|
|
42
|
+
csrfToken: ctx.session?.csrfToken,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Example: Protected API that requires session
|
|
47
|
+
lambder.addSessionApi("user.profile", async (ctx, resolver) => {
|
|
48
|
+
// Session is automatically fetched and validated
|
|
49
|
+
const sessionData = ctx.session?.data;
|
|
50
|
+
|
|
51
|
+
return resolver.api({
|
|
52
|
+
userId: sessionData.userId,
|
|
53
|
+
username: sessionData.username,
|
|
54
|
+
role: sessionData.role,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Example: Sensitive operation that regenerates session
|
|
59
|
+
lambder.addSessionApi("user.changePassword", async (ctx, resolver) => {
|
|
60
|
+
const { oldPassword, newPassword } = ctx.apiPayload;
|
|
61
|
+
const sessionController = lambder.getSessionController(ctx);
|
|
62
|
+
|
|
63
|
+
// Validate old password
|
|
64
|
+
const isValid = await validatePassword(
|
|
65
|
+
ctx.session?.data.userId,
|
|
66
|
+
oldPassword
|
|
67
|
+
);
|
|
68
|
+
if (!isValid) {
|
|
69
|
+
return resolver.api({ success: false, error: "Invalid password" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Update password
|
|
73
|
+
await updatePassword(ctx.session?.data.userId, newPassword);
|
|
74
|
+
|
|
75
|
+
// IMPORTANT: Regenerate session after password change to prevent session fixation
|
|
76
|
+
await sessionController.regenerateSession();
|
|
77
|
+
|
|
78
|
+
// OPTIONAL: End all other sessions for this user
|
|
79
|
+
await sessionController.endSessionAll();
|
|
80
|
+
|
|
81
|
+
return resolver.api({
|
|
82
|
+
success: true,
|
|
83
|
+
message: "Password changed successfully",
|
|
84
|
+
csrfToken: ctx.session?.csrfToken, // Send new CSRF token
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Example: Update session data
|
|
89
|
+
lambder.addSessionApi("user.updatePreferences", async (ctx, resolver) => {
|
|
90
|
+
const { theme, language } = ctx.apiPayload;
|
|
91
|
+
const sessionController = lambder.getSessionController(ctx);
|
|
92
|
+
|
|
93
|
+
// Update session data (also extends expiration if sliding is enabled)
|
|
94
|
+
await sessionController.updateSessionData({
|
|
95
|
+
...ctx.session?.data,
|
|
96
|
+
preferences: { theme, language },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return resolver.api({
|
|
100
|
+
success: true,
|
|
101
|
+
message: "Preferences updated",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Example: Logout
|
|
106
|
+
lambder.addSessionApi("user.logout", async (ctx, resolver) => {
|
|
107
|
+
const sessionController = lambder.getSessionController(ctx);
|
|
108
|
+
|
|
109
|
+
// End current session
|
|
110
|
+
await sessionController.endSession();
|
|
111
|
+
|
|
112
|
+
return resolver.api({
|
|
113
|
+
success: true,
|
|
114
|
+
message: "Logged out successfully",
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Example: Logout from all devices
|
|
119
|
+
lambder.addSessionApi("user.logoutAll", async (ctx, resolver) => {
|
|
120
|
+
const sessionController = lambder.getSessionController(ctx);
|
|
121
|
+
|
|
122
|
+
// End all sessions for this user (same sessionKey)
|
|
123
|
+
await sessionController.endSessionAll();
|
|
124
|
+
|
|
125
|
+
return resolver.api({
|
|
126
|
+
success: true,
|
|
127
|
+
message: "Logged out from all devices",
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Example: Optional session (check if logged in)
|
|
132
|
+
lambder.addApi("user.checkAuth", async (ctx, resolver) => {
|
|
133
|
+
const sessionController = lambder.getSessionController(ctx);
|
|
134
|
+
|
|
135
|
+
// Try to fetch session without throwing error
|
|
136
|
+
const session = await sessionController.fetchSessionIfExists();
|
|
137
|
+
|
|
138
|
+
if (session) {
|
|
139
|
+
return resolver.api({
|
|
140
|
+
authenticated: true,
|
|
141
|
+
userId: session.data.userId,
|
|
142
|
+
username: session.data.username,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return resolver.api({
|
|
147
|
+
authenticated: false,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Example: Route with session
|
|
152
|
+
lambder.addSessionRoute("/dashboard", async (ctx, resolver) => {
|
|
153
|
+
// Session is automatically fetched and validated
|
|
154
|
+
const userData = ctx.session?.data;
|
|
155
|
+
|
|
156
|
+
return resolver.ejsFile("dashboard.ejs", {
|
|
157
|
+
user: userData,
|
|
158
|
+
csrfToken: ctx.session?.csrfToken,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Example: Route with optional session
|
|
163
|
+
lambder.addRoute("/", async (ctx, resolver) => {
|
|
164
|
+
const sessionController = lambder.getSessionController(ctx);
|
|
165
|
+
const session = await sessionController.fetchSessionIfExists();
|
|
166
|
+
|
|
167
|
+
return resolver.ejsFile("home.ejs", {
|
|
168
|
+
isLoggedIn: !!session,
|
|
169
|
+
user: session?.data,
|
|
170
|
+
csrfToken: session?.csrfToken,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Dummy functions (implement these)
|
|
175
|
+
async function authenticateUser(username: string, password: string) {
|
|
176
|
+
// Implement your authentication logic
|
|
177
|
+
return { id: "user123", username, role: "user" };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function validatePassword(userId: string, password: string) {
|
|
181
|
+
// Implement password validation
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function updatePassword(userId: string, newPassword: string) {
|
|
186
|
+
// Implement password update
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const handler = async (event: any, context: any) => {
|
|
190
|
+
return await lambder.render(event, context);
|
|
191
|
+
};
|