lambder 2.0.8 → 2.0.10
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/LambderResponseBuilder.js +6 -4
- package/dist/LambderUtils.js +4 -2
- package/package.json +1 -1
- package/src/LambderResponseBuilder.ts +6 -4
- package/src/LambderUtils.ts +4 -2
- package/tests/file-serving.test.ts +194 -0
- package/tests/fixtures/public/index.html +1 -0
- package/tests/fixtures/public/main.css +1 -0
|
@@ -28,7 +28,8 @@ export default class LambderResponseBuilder {
|
|
|
28
28
|
throw new Error("File system operations are not available in browser environment");
|
|
29
29
|
}
|
|
30
30
|
const publicPath = path.resolve(this.publicPath);
|
|
31
|
-
const
|
|
31
|
+
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
32
|
+
const absolutePath = path.resolve(publicPath, normalizedFilePath);
|
|
32
33
|
if (!absolutePath.startsWith(publicPath)) {
|
|
33
34
|
return "forbidden-public-path";
|
|
34
35
|
}
|
|
@@ -42,7 +43,8 @@ export default class LambderResponseBuilder {
|
|
|
42
43
|
return false;
|
|
43
44
|
}
|
|
44
45
|
const publicPath = path.resolve(this.publicPath);
|
|
45
|
-
const
|
|
46
|
+
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
47
|
+
const absolutePath = path.resolve(publicPath, normalizedFilePath);
|
|
46
48
|
if (!absolutePath.startsWith(publicPath)) {
|
|
47
49
|
return false;
|
|
48
50
|
}
|
|
@@ -147,7 +149,7 @@ export default class LambderResponseBuilder {
|
|
|
147
149
|
return this.raw({
|
|
148
150
|
statusCode: 200,
|
|
149
151
|
isBase64Encoded: true,
|
|
150
|
-
multiValueHeaders: { "Content-Type": [mimeType || "
|
|
152
|
+
multiValueHeaders: { "Content-Type": [mimeType || "application/octet-stream"], ...convertToMultiHeader(headers) },
|
|
151
153
|
body: fileBase64,
|
|
152
154
|
});
|
|
153
155
|
}
|
|
@@ -171,7 +173,7 @@ export default class LambderResponseBuilder {
|
|
|
171
173
|
const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
172
174
|
const bodyBase64 = bodyBuffer.toString("base64");
|
|
173
175
|
console.log("bodyBase64.length", bodyBase64.length);
|
|
174
|
-
return this.fileBase64(bodyBase64, mimeType || "", headers);
|
|
176
|
+
return this.fileBase64(bodyBase64, mimeType || "application/octet-stream", headers);
|
|
175
177
|
}
|
|
176
178
|
;
|
|
177
179
|
async ejsTemplate(template, pageData, headers) {
|
package/dist/LambderUtils.js
CHANGED
|
@@ -16,7 +16,8 @@ export default class LambderUtils {
|
|
|
16
16
|
return "EJS PATH NOT SET!";
|
|
17
17
|
}
|
|
18
18
|
const ejsPath = path.resolve(this.ejsPath);
|
|
19
|
-
const
|
|
19
|
+
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
20
|
+
const absolutePath = path.resolve(ejsPath, normalizedFilePath);
|
|
20
21
|
if (!absolutePath.startsWith(ejsPath)) {
|
|
21
22
|
return "forbidden-ejs-path";
|
|
22
23
|
}
|
|
@@ -33,7 +34,8 @@ export default class LambderUtils {
|
|
|
33
34
|
return "EJS PATH NOT SET!";
|
|
34
35
|
}
|
|
35
36
|
const ejsPath = path.resolve(this.ejsPath);
|
|
36
|
-
const
|
|
37
|
+
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
38
|
+
const absolutePath = path.resolve(ejsPath, normalizedFilePath);
|
|
37
39
|
if (!absolutePath.startsWith(ejsPath)) {
|
|
38
40
|
return false;
|
|
39
41
|
}
|
package/package.json
CHANGED
|
@@ -69,7 +69,8 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const publicPath = path.resolve(this.publicPath);
|
|
72
|
-
const
|
|
72
|
+
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
73
|
+
const absolutePath = path.resolve(publicPath, normalizedFilePath);
|
|
73
74
|
if(!absolutePath.startsWith(publicPath)){ return "forbidden-public-path"; }
|
|
74
75
|
return await fs.promises.readFile(absolutePath);
|
|
75
76
|
};
|
|
@@ -81,7 +82,8 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
81
82
|
if (!fs || !path) { return false; }
|
|
82
83
|
|
|
83
84
|
const publicPath = path.resolve(this.publicPath);
|
|
84
|
-
const
|
|
85
|
+
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
86
|
+
const absolutePath = path.resolve(publicPath, normalizedFilePath);
|
|
85
87
|
if(!absolutePath.startsWith(publicPath)){ return false; }
|
|
86
88
|
try {
|
|
87
89
|
const stat = await fs.promises.stat(absolutePath);
|
|
@@ -200,7 +202,7 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
200
202
|
return this.raw({
|
|
201
203
|
statusCode: 200,
|
|
202
204
|
isBase64Encoded: true,
|
|
203
|
-
multiValueHeaders: { "Content-Type": [mimeType || "
|
|
205
|
+
multiValueHeaders: { "Content-Type": [mimeType || "application/octet-stream"], ...convertToMultiHeader(headers) },
|
|
204
206
|
body: fileBase64,
|
|
205
207
|
});
|
|
206
208
|
};
|
|
@@ -228,7 +230,7 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
228
230
|
const bodyBuffer: Buffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
229
231
|
const bodyBase64 = bodyBuffer.toString("base64");
|
|
230
232
|
console.log("bodyBase64.length",bodyBase64.length);
|
|
231
|
-
return this.fileBase64(bodyBase64, mimeType || "", headers);
|
|
233
|
+
return this.fileBase64(bodyBase64, mimeType || "application/octet-stream", headers);
|
|
232
234
|
};
|
|
233
235
|
|
|
234
236
|
async ejsTemplate(
|
package/src/LambderUtils.ts
CHANGED
|
@@ -21,7 +21,8 @@ export default class LambderUtils {
|
|
|
21
21
|
}
|
|
22
22
|
if(!this.ejsPath){ return "EJS PATH NOT SET!"; }
|
|
23
23
|
const ejsPath = path.resolve(this.ejsPath);
|
|
24
|
-
const
|
|
24
|
+
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
25
|
+
const absolutePath = path.resolve(ejsPath, normalizedFilePath);
|
|
25
26
|
if(!absolutePath.startsWith(ejsPath)){ return "forbidden-ejs-path"; }
|
|
26
27
|
return await fs.promises.readFile(absolutePath, 'utf-8');
|
|
27
28
|
};
|
|
@@ -35,7 +36,8 @@ export default class LambderUtils {
|
|
|
35
36
|
}
|
|
36
37
|
if(!this.ejsPath){ return "EJS PATH NOT SET!"; }
|
|
37
38
|
const ejsPath = path.resolve(this.ejsPath);
|
|
38
|
-
const
|
|
39
|
+
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
40
|
+
const absolutePath = path.resolve(ejsPath, normalizedFilePath);
|
|
39
41
|
if(!absolutePath.startsWith(ejsPath)){ return false; }
|
|
40
42
|
try {
|
|
41
43
|
const stat = await fs.promises.stat(absolutePath);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Serving Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for file serving functionality including:
|
|
5
|
+
* - Serving existing files correctly
|
|
6
|
+
* - Fallback to index.html for non-existent files
|
|
7
|
+
* - Not serving index.html when the requested file exists
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import Lambder from '../src/Lambder.js';
|
|
12
|
+
import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
|
|
15
|
+
const createMockEvent = (path: string, method: string = 'GET'): APIGatewayProxyEvent => ({
|
|
16
|
+
body: null,
|
|
17
|
+
headers: {
|
|
18
|
+
Host: 'localhost',
|
|
19
|
+
},
|
|
20
|
+
multiValueHeaders: {},
|
|
21
|
+
httpMethod: method,
|
|
22
|
+
isBase64Encoded: false,
|
|
23
|
+
path,
|
|
24
|
+
pathParameters: null,
|
|
25
|
+
queryStringParameters: null,
|
|
26
|
+
multiValueQueryStringParameters: null,
|
|
27
|
+
stageVariables: null,
|
|
28
|
+
requestContext: {} as any,
|
|
29
|
+
resource: '',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const createMockContext = (): Context => ({
|
|
33
|
+
callbackWaitsForEmptyEventLoop: false,
|
|
34
|
+
functionName: 'test',
|
|
35
|
+
functionVersion: '1',
|
|
36
|
+
invokedFunctionArn: 'arn',
|
|
37
|
+
memoryLimitInMB: '128',
|
|
38
|
+
awsRequestId: '123',
|
|
39
|
+
logGroupName: 'group',
|
|
40
|
+
logStreamName: 'stream',
|
|
41
|
+
getRemainingTimeInMillis: () => 1000,
|
|
42
|
+
done: () => {},
|
|
43
|
+
fail: () => {},
|
|
44
|
+
succeed: () => {},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('File Serving with Fallback', () => {
|
|
48
|
+
it('should serve main.css when it exists, NOT index.html', async () => {
|
|
49
|
+
const lambder = new Lambder({
|
|
50
|
+
publicPath: path.resolve('./tests/fixtures/public'),
|
|
51
|
+
apiPath: '/api'
|
|
52
|
+
})
|
|
53
|
+
.addRoute('/(.*)', (ctx, res) => {
|
|
54
|
+
return res.file(ctx.path, {}, 'index.html');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const handler = lambder.getHandler();
|
|
58
|
+
const event = createMockEvent('/main.css');
|
|
59
|
+
const result = await handler(event, createMockContext());
|
|
60
|
+
|
|
61
|
+
expect(result.statusCode).toBe(200);
|
|
62
|
+
|
|
63
|
+
// Check content type is CSS
|
|
64
|
+
expect(result.multiValueHeaders?.['Content-Type']).toContain('text/css');
|
|
65
|
+
|
|
66
|
+
// Check body contains CSS content
|
|
67
|
+
const body = Buffer.from(result.body || '', 'base64').toString();
|
|
68
|
+
expect(body).toContain('body { margin: 0; }');
|
|
69
|
+
expect(body).not.toContain('<h1>Test HTML</h1>');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should serve index.html when requested file does not exist', async () => {
|
|
73
|
+
const lambder = new Lambder({
|
|
74
|
+
publicPath: path.resolve('./tests/fixtures/public'),
|
|
75
|
+
apiPath: '/api'
|
|
76
|
+
})
|
|
77
|
+
.addRoute('/(.*)', (ctx, res) => {
|
|
78
|
+
return res.file(ctx.path, {}, 'index.html');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const handler = lambder.getHandler();
|
|
82
|
+
const event = createMockEvent('/non-existent-file.js');
|
|
83
|
+
const result = await handler(event, createMockContext());
|
|
84
|
+
|
|
85
|
+
expect(result.statusCode).toBe(200);
|
|
86
|
+
|
|
87
|
+
// Check content type is HTML
|
|
88
|
+
expect(result.multiValueHeaders?.['Content-Type']).toContain('text/html');
|
|
89
|
+
|
|
90
|
+
// Check body contains HTML content from index.html
|
|
91
|
+
const body = Buffer.from(result.body || '', 'base64').toString();
|
|
92
|
+
expect(body).toContain('<h1>Test HTML</h1>');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should serve index.html when requested directly', async () => {
|
|
96
|
+
const lambder = new Lambder({
|
|
97
|
+
publicPath: path.resolve('./tests/fixtures/public'),
|
|
98
|
+
apiPath: '/api'
|
|
99
|
+
})
|
|
100
|
+
.addRoute('/(.*)', (ctx, res) => {
|
|
101
|
+
return res.file(ctx.path, {}, 'index.html');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const handler = lambder.getHandler();
|
|
105
|
+
const event = createMockEvent('/index.html');
|
|
106
|
+
const result = await handler(event, createMockContext());
|
|
107
|
+
|
|
108
|
+
expect(result.statusCode).toBe(200);
|
|
109
|
+
|
|
110
|
+
// Check content type is HTML
|
|
111
|
+
expect(result.multiValueHeaders?.['Content-Type']).toContain('text/html');
|
|
112
|
+
|
|
113
|
+
// Check body contains HTML content
|
|
114
|
+
const body = Buffer.from(result.body || '', 'base64').toString();
|
|
115
|
+
expect(body).toContain('<h1>Test HTML</h1>');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should return error when both requested file and fallback do not exist', async () => {
|
|
119
|
+
const lambder = new Lambder({
|
|
120
|
+
publicPath: path.resolve('./tests/fixtures/public'),
|
|
121
|
+
apiPath: '/api'
|
|
122
|
+
})
|
|
123
|
+
.addRoute('/(.*)', (ctx, res) => {
|
|
124
|
+
return res.file(ctx.path, {}, 'non-existent-fallback.html');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const handler = lambder.getHandler();
|
|
128
|
+
const event = createMockEvent('/non-existent-file.js');
|
|
129
|
+
const result = await handler(event, createMockContext());
|
|
130
|
+
|
|
131
|
+
expect(result.statusCode).toBe(200);
|
|
132
|
+
|
|
133
|
+
// Should return JSON error
|
|
134
|
+
const body = JSON.parse(result.body || '{}');
|
|
135
|
+
expect(body.error).toContain('File not found');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should serve correct file even when catch-all route is last', async () => {
|
|
139
|
+
const lambder = new Lambder({
|
|
140
|
+
publicPath: path.resolve('./tests/fixtures/public'),
|
|
141
|
+
apiPath: '/api'
|
|
142
|
+
})
|
|
143
|
+
.addRoute('/specific', (ctx, res) => {
|
|
144
|
+
return res.html('Specific Route');
|
|
145
|
+
})
|
|
146
|
+
.addRoute('/(.*)', (ctx, res) => {
|
|
147
|
+
return res.file(ctx.path, {}, 'index.html');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const handler = lambder.getHandler();
|
|
151
|
+
|
|
152
|
+
// Test specific route still works
|
|
153
|
+
const specificEvent = createMockEvent('/specific');
|
|
154
|
+
const specificResult = await handler(specificEvent, createMockContext());
|
|
155
|
+
const specificBody = Buffer.from(specificResult.body || '', 'base64').toString();
|
|
156
|
+
expect(specificBody).toBe('Specific Route');
|
|
157
|
+
|
|
158
|
+
// Test main.css is served correctly
|
|
159
|
+
const cssEvent = createMockEvent('/main.css');
|
|
160
|
+
const cssResult = await handler(cssEvent, createMockContext());
|
|
161
|
+
const cssBody = Buffer.from(cssResult.body || '', 'base64').toString();
|
|
162
|
+
expect(cssBody).toContain('body { margin: 0; }');
|
|
163
|
+
|
|
164
|
+
// Test fallback to index.html for non-existent files
|
|
165
|
+
const fallbackEvent = createMockEvent('/some-route');
|
|
166
|
+
const fallbackResult = await handler(fallbackEvent, createMockContext());
|
|
167
|
+
const fallbackBody = Buffer.from(fallbackResult.body || '', 'base64').toString();
|
|
168
|
+
expect(fallbackBody).toContain('<h1>Test HTML</h1>');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should serve CSS file with correct text/css MIME type', async () => {
|
|
172
|
+
const lambder = new Lambder({
|
|
173
|
+
publicPath: path.resolve('./tests/fixtures/public'),
|
|
174
|
+
apiPath: '/api'
|
|
175
|
+
})
|
|
176
|
+
.addRoute('/(.*)', (ctx, res) => {
|
|
177
|
+
return res.file(ctx.path);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const handler = lambder.getHandler();
|
|
181
|
+
const event = createMockEvent('/main.css');
|
|
182
|
+
const result = await handler(event, createMockContext());
|
|
183
|
+
|
|
184
|
+
expect(result.statusCode).toBe(200);
|
|
185
|
+
|
|
186
|
+
// Verify Content-Type header is exactly text/css
|
|
187
|
+
expect(result.multiValueHeaders?.['Content-Type']).toBeDefined();
|
|
188
|
+
expect(result.multiValueHeaders?.['Content-Type']?.[0]).toBe('text/css');
|
|
189
|
+
|
|
190
|
+
// Verify body contains CSS content
|
|
191
|
+
const body = Buffer.from(result.body || '', 'base64').toString();
|
|
192
|
+
expect(body).toContain('body { margin: 0; }');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>Test HTML</h1>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
body { margin: 0; }
|