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.
@@ -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
+ };