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.
@@ -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 absolutePath = path.resolve(publicPath, filePath);
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 absolutePath = path.resolve(publicPath, filePath);
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 || "text/html"], ...convertToMultiHeader(headers) },
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) {
@@ -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 absolutePath = path.resolve(ejsPath, filePath);
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 absolutePath = path.resolve(ejsPath, filePath);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -69,7 +69,8 @@ export default class LambderResponseBuilder<TResponse = any> {
69
69
  }
70
70
 
71
71
  const publicPath = path.resolve(this.publicPath);
72
- const absolutePath = path.resolve(publicPath, filePath);
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 absolutePath = path.resolve(publicPath, filePath);
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 || "text/html"], ...convertToMultiHeader(headers) },
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(
@@ -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 absolutePath = path.resolve(ejsPath, filePath);
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 absolutePath = path.resolve(ejsPath, filePath);
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; }