lambder 1.0.128 → 1.0.129
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/dist/Lambder.d.ts +5 -4
- package/dist/Lambder.js +2 -2
- 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/docs/DYNAMODB_SETUP.md +96 -0
- package/docs/TYPE_SAFE_QUICK_START.md +48 -4
- 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 +1 -1
- package/src/Lambder.ts +4 -4
- 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/tests/output-type-runtime.test.ts +365 -0
- package/tests/type-safety.test.ts +312 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test file to verify output type enforcement
|
|
3
|
+
* This file should have TypeScript errors if output types are not correct
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Lambder from '../src/Lambder.js';
|
|
7
|
+
import type { ApiContract } from '../src/index.js';
|
|
8
|
+
|
|
9
|
+
// Define a simple API contract
|
|
10
|
+
type TestContract = ApiContract<{
|
|
11
|
+
getNumber: { input: void, output: number },
|
|
12
|
+
getString: { input: { id: string }, output: string },
|
|
13
|
+
getUser: { input: { userId: string }, output: { id: string; name: string; age: number } },
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
const lambder = new Lambder<TestContract>({
|
|
17
|
+
publicPath: './public',
|
|
18
|
+
apiPath: '/api',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ✅ CORRECT: Returns the right type
|
|
22
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
23
|
+
// This should work - returning number
|
|
24
|
+
return resolver.api(42);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ❌ INCORRECT: Should cause TypeScript error - returning wrong type
|
|
28
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
29
|
+
// @ts-expect-error - Testing type enforcement: should not accept string when output is number
|
|
30
|
+
return resolver.api("wrong type");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ✅ CORRECT: Returns string
|
|
34
|
+
lambder.addApi('getString', async (ctx, resolver) => {
|
|
35
|
+
const id = ctx.apiPayload.id;
|
|
36
|
+
return resolver.api(`String for ${id}`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ❌ INCORRECT: Should cause TypeScript error - returning wrong type
|
|
40
|
+
lambder.addApi('getString', async (ctx, resolver) => {
|
|
41
|
+
// @ts-expect-error - Testing type enforcement: should not accept number when output is string
|
|
42
|
+
return resolver.api(123);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ✅ CORRECT: Returns correct object shape
|
|
46
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
47
|
+
return resolver.api({
|
|
48
|
+
id: ctx.apiPayload.userId,
|
|
49
|
+
name: "John Doe",
|
|
50
|
+
age: 30,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ❌ INCORRECT: Should cause TypeScript error - missing required field
|
|
55
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
56
|
+
// @ts-expect-error - Testing type enforcement: missing 'age' field
|
|
57
|
+
return resolver.api({
|
|
58
|
+
id: ctx.apiPayload.userId,
|
|
59
|
+
name: "John Doe",
|
|
60
|
+
// age is missing
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ❌ INCORRECT: Should cause TypeScript error - wrong field type
|
|
65
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
66
|
+
return resolver.api({
|
|
67
|
+
id: ctx.apiPayload.userId,
|
|
68
|
+
name: "John Doe",
|
|
69
|
+
// @ts-expect-error - Testing type enforcement: age should be number, not string
|
|
70
|
+
age: "30", // wrong type
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ✅ CORRECT: Using null is allowed
|
|
75
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
76
|
+
return resolver.api(null);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ✅ CORRECT: Using resolver.die.api also enforces types
|
|
80
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
81
|
+
return resolver.die.api(42);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ❌ INCORRECT: resolver.die.api should also enforce types
|
|
85
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
86
|
+
// @ts-expect-error - Testing type enforcement: die.api should also check types
|
|
87
|
+
return resolver.die.api("wrong type");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Test with addSessionApi
|
|
91
|
+
lambder.addSessionApi('getString', async (ctx, resolver) => {
|
|
92
|
+
// Should also enforce output type for session APIs
|
|
93
|
+
return resolver.api("correct string");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
lambder.addSessionApi('getString', async (ctx, resolver) => {
|
|
97
|
+
// @ts-expect-error - Testing type enforcement: session API should also enforce types
|
|
98
|
+
return resolver.api(123);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log("If this file compiles with @ts-expect-error comments, output type enforcement is working!");
|
package/package.json
CHANGED
package/src/Lambder.ts
CHANGED
|
@@ -140,11 +140,11 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
enableDdbSession(
|
|
143
|
-
{ tableName, tableRegion, sessionSalt }: { tableName: string; tableRegion: string; sessionSalt: string; },
|
|
143
|
+
{ tableName, tableRegion, sessionSalt, enableSlidingExpiration }: { tableName: string; tableRegion: string; sessionSalt: string; enableSlidingExpiration?: boolean; },
|
|
144
144
|
{ partitionKey, sortKey }: { partitionKey: string, sortKey: string } = { partitionKey: "pk", sortKey: "sk" }
|
|
145
145
|
){
|
|
146
146
|
this.lambderSessionManager = new LambderSessionManager({
|
|
147
|
-
tableName, tableRegion, partitionKey, sortKey, sessionSalt
|
|
147
|
+
tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -249,7 +249,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
249
249
|
apiName: TApiName,
|
|
250
250
|
actionFn: (
|
|
251
251
|
ctx: LambderRenderContext<TContract[TApiName]['input']>,
|
|
252
|
-
resolver: LambderResolver
|
|
252
|
+
resolver: LambderResolver<TContract, TApiName>
|
|
253
253
|
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
254
254
|
):void;
|
|
255
255
|
// Overload for untyped API with string (backward compatibility, must be last)
|
|
@@ -281,7 +281,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
281
281
|
apiName: TApiName,
|
|
282
282
|
actionFn: (
|
|
283
283
|
ctx: LambderRenderContext<TContract[TApiName]['input']>,
|
|
284
|
-
resolver: LambderResolver
|
|
284
|
+
resolver: LambderResolver<TContract, TApiName>
|
|
285
285
|
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
286
286
|
):void;
|
|
287
287
|
// Overload for untyped session API with string (backward compatibility, must be last)
|
package/src/LambderResolver.ts
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./Lambder.js";
|
|
2
2
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
3
3
|
import LambderUtils from "./LambderUtils.js";
|
|
4
|
+
import type { ApiContractShape, ApiOutput } from "./LambderApiContract.js";
|
|
4
5
|
|
|
5
6
|
type MethodType<T, M extends keyof T> = T[M] extends (...args: any[]) => any ? T[M] : never;
|
|
6
7
|
|
|
7
|
-
interface DieResolverMethods {
|
|
8
|
-
raw: MethodType<LambderResponseBuilder
|
|
9
|
-
json: MethodType<LambderResponseBuilder
|
|
10
|
-
xml: MethodType<LambderResponseBuilder
|
|
11
|
-
html: MethodType<LambderResponseBuilder
|
|
12
|
-
status301: MethodType<LambderResponseBuilder
|
|
13
|
-
status404: MethodType<LambderResponseBuilder
|
|
14
|
-
cors: MethodType<LambderResponseBuilder
|
|
15
|
-
fileBase64: MethodType<LambderResponseBuilder
|
|
16
|
-
file: MethodType<LambderResponseBuilder
|
|
17
|
-
ejsFile: MethodType<LambderResponseBuilder
|
|
18
|
-
ejsTemplate: MethodType<LambderResponseBuilder
|
|
19
|
-
api:
|
|
8
|
+
interface DieResolverMethods<TContract extends ApiContractShape, TApiName extends keyof TContract & string> {
|
|
9
|
+
raw: MethodType<LambderResponseBuilder<TContract>, 'raw'>;
|
|
10
|
+
json: MethodType<LambderResponseBuilder<TContract>, 'json'>;
|
|
11
|
+
xml: MethodType<LambderResponseBuilder<TContract>, 'xml'>;
|
|
12
|
+
html: MethodType<LambderResponseBuilder<TContract>, 'html'>;
|
|
13
|
+
status301: MethodType<LambderResponseBuilder<TContract>, 'status301'>;
|
|
14
|
+
status404: MethodType<LambderResponseBuilder<TContract>, 'status404'>;
|
|
15
|
+
cors: MethodType<LambderResponseBuilder<TContract>, 'cors'>;
|
|
16
|
+
fileBase64: MethodType<LambderResponseBuilder<TContract>, 'fileBase64'>;
|
|
17
|
+
file: MethodType<LambderResponseBuilder<TContract>, 'file'>;
|
|
18
|
+
ejsFile: MethodType<LambderResponseBuilder<TContract>, 'ejsFile'>;
|
|
19
|
+
ejsTemplate: MethodType<LambderResponseBuilder<TContract>, 'ejsTemplate'>;
|
|
20
|
+
api: (
|
|
21
|
+
payload: ApiOutput<TContract, TApiName> | null,
|
|
22
|
+
config?: Parameters<LambderResponseBuilder<TContract>['api']>[1],
|
|
23
|
+
headers?: Parameters<LambderResponseBuilder<TContract>['api']>[2]
|
|
24
|
+
) => LambderResolverResponse;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
export default class LambderResolver
|
|
27
|
+
export default class LambderResolver<
|
|
28
|
+
TContract extends ApiContractShape = any,
|
|
29
|
+
TApiName extends keyof TContract & string = any
|
|
30
|
+
> extends LambderResponseBuilder<TContract> {
|
|
23
31
|
public resolve: (response: LambderResolverResponse) => void;
|
|
24
32
|
public reject: (err: Error) => void;
|
|
25
|
-
public die: DieResolverMethods
|
|
33
|
+
public die: DieResolverMethods<TContract, TApiName>;
|
|
26
34
|
|
|
27
35
|
constructor(
|
|
28
36
|
{ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx, resolve, reject }:
|
|
@@ -56,6 +64,15 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
56
64
|
};
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
// Override api method with proper typing
|
|
68
|
+
api(
|
|
69
|
+
payload: ApiOutput<TContract, TApiName> | null,
|
|
70
|
+
config?: Parameters<LambderResponseBuilder<TContract>['api']>[1],
|
|
71
|
+
headers?: Parameters<LambderResponseBuilder<TContract>['api']>[2]
|
|
72
|
+
): LambderResolverResponse {
|
|
73
|
+
return super.api(payload, config, headers);
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
private autoResolve<
|
|
60
77
|
T extends (...args: any[]) => LambderResolverResponse
|
|
61
78
|
>(method: T): (...funcArgs: Parameters<T>) => LambderResolverResponse {
|
|
@@ -4,6 +4,7 @@ import ejs from "ejs";
|
|
|
4
4
|
import mimeTypeResolver from "mime-types";
|
|
5
5
|
import LambderUtils from "./LambderUtils.js";
|
|
6
6
|
import { LambderRenderContext } from "./Lambder.js";
|
|
7
|
+
import type { ApiContractShape } from "./LambderApiContract.js";
|
|
7
8
|
|
|
8
9
|
const convertToMultiHeader = (
|
|
9
10
|
headers: Record<string, string|string[]> | undefined
|
|
@@ -38,7 +39,7 @@ export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
|
38
39
|
payload?: T | null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export default class LambderResponseBuilder {
|
|
42
|
+
export default class LambderResponseBuilder<TContract extends ApiContractShape = any> {
|
|
42
43
|
private isCorsEnabled: boolean;
|
|
43
44
|
private publicPath: string;
|
|
44
45
|
private apiVersion: string|null;
|
|
@@ -48,6 +48,15 @@ export default class LambderSessionController {
|
|
|
48
48
|
return this.ctx.session;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
async regenerateSession (): Promise<LambderSessionContext> {
|
|
52
|
+
if(!this.ctx.session) throw new Error("Session not found.");
|
|
53
|
+
const newSession = await this.lambderSessionManager.regenerateSession(this.ctx.session);
|
|
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` });
|
|
55
|
+
this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionCsrfCookieKey}=${newSession.csrfToken}; Expires=${new Date(newSession.expiresAt * 1000).toUTCString()}; Path=/; SameSite=Lax; Secure` });
|
|
56
|
+
this.ctx.session = newSession;
|
|
57
|
+
return this.ctx.session;
|
|
58
|
+
};
|
|
59
|
+
|
|
51
60
|
async fetchSession (): Promise<LambderSessionContext>{
|
|
52
61
|
if(!this.areRequestSessionTokensValid()){ throw new Error("Session tokens are invalid"); }
|
|
53
62
|
|