@geek-fun/serverlessinsight 0.3.3 → 0.4.0

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.
@@ -10,28 +10,46 @@ const providerEnum_1 = require("./providerEnum");
10
10
  const node_async_hooks_1 = require("node:async_hooks");
11
11
  const imsClient_1 = require("./imsClient");
12
12
  const asyncLocalStorage = new node_async_hooks_1.AsyncLocalStorage();
13
+ const DEFAULT_IAC_FILES = [
14
+ 'serverlessinsight.yml',
15
+ 'serverlessInsight.yml',
16
+ 'ServerlessInsight.yml',
17
+ 'serverless-insight.yml',
18
+ ];
13
19
  const getIacLocation = (location) => {
14
20
  const projectRoot = node_path_1.default.resolve(process.cwd());
15
- if (location) {
16
- const candidate = node_path_1.default.isAbsolute(location) ? location : node_path_1.default.resolve(projectRoot, location);
17
- if (node_fs_1.default.existsSync(candidate)) {
18
- return candidate;
21
+ const searchTargets = location ? [location] : DEFAULT_IAC_FILES;
22
+ const attempted = new Set();
23
+ const toAbsolutePath = (target) => node_path_1.default.isAbsolute(target) ? target : node_path_1.default.resolve(projectRoot, target);
24
+ const tryResolveCandidate = (target) => {
25
+ const resolved = toAbsolutePath(target);
26
+ attempted.add(resolved);
27
+ if (!node_fs_1.default.existsSync(resolved)) {
28
+ return undefined;
19
29
  }
20
- throw new Error(`IaC file not found at '${candidate}'`);
21
- }
22
- const candidates = [
23
- 'serverlessinsight.yml',
24
- 'serverlessInsight.yml',
25
- 'ServerlessInsight.yml',
26
- 'serverless-insight.yml',
27
- ];
28
- for (const name of candidates) {
29
- const candidate = node_path_1.default.resolve(projectRoot, name);
30
- if (node_fs_1.default.existsSync(candidate)) {
31
- return candidate;
30
+ const stats = node_fs_1.default.statSync(resolved);
31
+ if (stats.isDirectory()) {
32
+ for (const fileName of DEFAULT_IAC_FILES) {
33
+ const nested = node_path_1.default.join(resolved, fileName);
34
+ attempted.add(nested);
35
+ if (node_fs_1.default.existsSync(nested) && node_fs_1.default.statSync(nested).isFile()) {
36
+ return nested;
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+ return resolved;
42
+ };
43
+ for (const candidate of searchTargets) {
44
+ const match = tryResolveCandidate(candidate);
45
+ if (match) {
46
+ return match;
32
47
  }
33
48
  }
34
- throw new Error(`No IaC file found. Tried: ${candidates.map((n) => `'${node_path_1.default.resolve(projectRoot, n)}'`).join(', ')}`);
49
+ const attemptedList = Array.from(attempted)
50
+ .map((n) => `'${n}'`)
51
+ .join(', ');
52
+ throw new Error(`No IaC file found. Tried: ${attemptedList}`);
35
53
  };
36
54
  exports.getIacLocation = getIacLocation;
37
55
  const setContext = async (config, reaValToken = false) => {
@@ -36,13 +36,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.formatRosId = exports.calcValue = exports.calcRefs = exports.getFileSource = exports.readCodeSize = exports.resolveCode = void 0;
39
+ exports.splitDomain = exports.formatRosId = exports.getIacDefinition = exports.calcValue = exports.calcRefs = exports.getFileSource = exports.readCodeSize = exports.resolveCode = void 0;
40
40
  const node_path_1 = __importDefault(require("node:path"));
41
41
  const node_fs_1 = __importDefault(require("node:fs"));
42
42
  const ros = __importStar(require("@alicloud/ros-cdk-core"));
43
43
  const ossDeployment = __importStar(require("@alicloud/ros-cdk-ossdeployment"));
44
44
  const node_crypto_1 = __importDefault(require("node:crypto"));
45
45
  const lodash_1 = require("lodash");
46
+ const parser_1 = require("../parser");
47
+ const logger_1 = require("./logger");
46
48
  const resolveCode = (location) => {
47
49
  const filePath = node_path_1.default.resolve(process.cwd(), location);
48
50
  const fileContent = node_fs_1.default.readFileSync(filePath);
@@ -104,7 +106,7 @@ exports.calcRefs = calcRefs;
104
106
  const getParam = (key, records) => {
105
107
  return records?.find((param) => param.key === key)?.value;
106
108
  };
107
- const calcValue = (rawValue, ctx) => {
109
+ const calcValue = (rawValue, ctx, iacVars) => {
108
110
  const containsStage = rawValue.match(/\$\{ctx.stage}/);
109
111
  const containsVar = rawValue.match(/\$\{vars.\w+}/);
110
112
  const containsMap = rawValue.match(/\$\{stages\.(\w+)}/);
@@ -113,14 +115,40 @@ const calcValue = (rawValue, ctx) => {
113
115
  value = rawValue.replace(/\$\{ctx.stage}/g, ctx.stage);
114
116
  }
115
117
  if (containsVar?.length) {
116
- value = value.replace(/\$\{vars\.(\w+)}/g, (_, key) => getParam(key, ctx.parameters));
118
+ // Use provided iacVars or parse from file
119
+ const vars = iacVars ?? (0, parser_1.parseYaml)(ctx.iacLocation).vars;
120
+ const mergedParams = Array.from(new Map([
121
+ ...Object.entries(vars ?? {}).map(([key, value]) => [key, String(value)]),
122
+ ...(ctx.parameters ?? []).map(({ key, value }) => [key, value]),
123
+ ].filter(([, v]) => v !== undefined)).entries()).map(([key, value]) => ({ key, value }));
124
+ value = value.replace(/\$\{vars\.(\w+)}/g, (_, key) => {
125
+ const paramValue = getParam(key, mergedParams);
126
+ if (!paramValue) {
127
+ logger_1.logger.warn(`Variable '${key}' not found in vars or parameters, using empty string`);
128
+ }
129
+ return paramValue || '';
130
+ });
117
131
  }
118
132
  if (containsMap?.length) {
119
- value = value.replace(/\$\{stages\.(\w+)}/g, (_, key) => getParam(key, (0, lodash_1.get)(ctx.stages, `${ctx.stage}`)));
133
+ value = value.replace(/\$\{stages\.(\w+)}/g, (_, key) => {
134
+ const stageValue = getParam(key, (0, lodash_1.get)(ctx.stages, `${ctx.stage}`));
135
+ if (!stageValue) {
136
+ logger_1.logger.warn(`Stage variable '${key}' not found in stage '${ctx.stage}', using empty string`);
137
+ }
138
+ return stageValue || '';
139
+ });
120
140
  }
121
141
  return value;
122
142
  };
123
143
  exports.calcValue = calcValue;
144
+ const getIacDefinition = (iac, rawValue) => {
145
+ const matchFn = rawValue.match(/^\$\{functions\.(\w+(\.\w+)?)}$/);
146
+ if (matchFn?.length) {
147
+ return iac.functions?.find((fc) => fc.key === matchFn[1]);
148
+ }
149
+ return iac.functions?.find((fc) => fc.key === rawValue);
150
+ };
151
+ exports.getIacDefinition = getIacDefinition;
124
152
  const formatRosId = (id) => {
125
153
  // Insert underscore before uppercase letters, but only when they follow a lowercase letter
126
154
  let result = id.replace(/([a-z])([A-Z])/g, '$1_$2');
@@ -135,3 +163,10 @@ const formatRosId = (id) => {
135
163
  return result;
136
164
  };
137
165
  exports.formatRosId = formatRosId;
166
+ const splitDomain = (domain) => {
167
+ const parts = domain.split('.');
168
+ const rr = parts.length > 2 ? parts[0] : '@';
169
+ const domainName = parts.length > 2 ? parts.slice(1).join('.') : domain;
170
+ return { rr, domainName };
171
+ };
172
+ exports.splitDomain = splitDomain;
@@ -8,4 +8,4 @@ export * from './constants';
8
8
  export * from './imsClient';
9
9
  export * from './base64';
10
10
  export * from './rosAssets';
11
- export * from './domainHelper';
11
+ export * from './requestHelper';
@@ -24,4 +24,4 @@ __exportStar(require("./constants"), exports);
24
24
  __exportStar(require("./imsClient"), exports);
25
25
  __exportStar(require("./base64"), exports);
26
26
  __exportStar(require("./rosAssets"), exports);
27
- __exportStar(require("./domainHelper"), exports);
27
+ __exportStar(require("./requestHelper"), exports);
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readRequestBody = void 0;
4
+ const readRequestBody = (req) => {
5
+ return new Promise((resolve, reject) => {
6
+ let body = '';
7
+ req.on('data', (chunk) => {
8
+ body += chunk.toString();
9
+ });
10
+ req.on('end', () => {
11
+ resolve(body);
12
+ });
13
+ req.on('error', reject);
14
+ });
15
+ };
16
+ exports.readRequestBody = readRequestBody;
@@ -9,7 +9,7 @@ const parseEvent = (events) => {
9
9
  key,
10
10
  name: event.name,
11
11
  type: event.type,
12
- triggers: event.triggers,
12
+ triggers: event.triggers?.map((trigger) => ({ ...trigger, method: trigger.method ?? 'GET' })),
13
13
  domain: event.domain,
14
14
  }));
15
15
  };
@@ -1,2 +1,3 @@
1
- import { ServerlessIac } from '../types';
1
+ import { ServerlessIac, Context } from '../types';
2
2
  export declare const parseYaml: (iacLocation: string) => ServerlessIac;
3
+ export declare const revalYaml: (iacLocation: string, ctx: Context) => ServerlessIac;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseYaml = void 0;
3
+ exports.revalYaml = exports.parseYaml = void 0;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const functionParser_1 = require("./functionParser");
6
6
  const eventParser_1 = require("./eventParser");
@@ -10,6 +10,7 @@ const yaml_1 = require("yaml");
10
10
  const validator_1 = require("../validator");
11
11
  const bucketParser_1 = require("./bucketParser");
12
12
  const tableParser_1 = require("./tableParser");
13
+ const common_1 = require("../common");
13
14
  const validateExistence = (path) => {
14
15
  if (!(0, node_fs_1.existsSync)(path)) {
15
16
  throw new Error(`File does not exist at path: ${path}`);
@@ -38,3 +39,33 @@ const parseYaml = (iacLocation) => {
38
39
  return transformYaml(iacJson);
39
40
  };
40
41
  exports.parseYaml = parseYaml;
42
+ const evaluateObject = (obj, ctx, iacVars) => {
43
+ if (typeof obj === 'string') {
44
+ return (0, common_1.calcValue)(obj, ctx, iacVars);
45
+ }
46
+ if (Array.isArray(obj)) {
47
+ return obj.map((item) => evaluateObject(item, ctx, iacVars));
48
+ }
49
+ if (typeof obj === 'object' && obj !== null) {
50
+ return Object.fromEntries(Object.entries(obj).map(([key, val]) => [key, evaluateObject(val, ctx, iacVars)]));
51
+ }
52
+ return obj;
53
+ };
54
+ const revalYaml = (iacLocation, ctx) => {
55
+ validateExistence(iacLocation);
56
+ const yamlContent = (0, node_fs_1.readFileSync)(iacLocation, 'utf8');
57
+ const iacJson = (0, yaml_1.parse)(yamlContent);
58
+ (0, validator_1.validateYaml)(iacJson);
59
+ const evaluatedIacJson = evaluateObject(iacJson, ctx, iacJson.vars);
60
+ const iac = transformYaml(evaluatedIacJson);
61
+ // Set default values for optional fields in functions
62
+ if (iac.functions) {
63
+ iac.functions = iac.functions.map((fn) => ({
64
+ ...fn,
65
+ memory: fn.memory || 128,
66
+ timeout: fn.timeout || 3,
67
+ }));
68
+ }
69
+ return iac;
70
+ };
71
+ exports.revalYaml = revalYaml;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateRequestId = exports.logApiGatewayRequest = exports.transformFCResponse = exports.addFCHeaders = exports.createAliyunContext = exports.createAliyunContextSerializable = exports.transformToAliyunEvent = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const common_1 = require("../../common");
6
+ const createFCLogger = (requestId) => {
7
+ const formatLog = (level, message) => {
8
+ const timestamp = new Date().toISOString();
9
+ console.log(`${timestamp} ${requestId} [${level}] ${message}`);
10
+ };
11
+ return {
12
+ debug: (message) => formatLog('DEBUG', message),
13
+ info: (message) => formatLog('INFO', message),
14
+ warn: (message) => formatLog('WARNING', message),
15
+ error: (message) => formatLog('ERROR', message),
16
+ log: (message) => formatLog('INFO', message),
17
+ };
18
+ };
19
+ const transformToAliyunEvent = async (req, url, query) => {
20
+ const rawBody = await (0, common_1.readRequestBody)(req);
21
+ const pathParameters = {};
22
+ const event = {
23
+ path: url,
24
+ httpMethod: req.method || 'GET',
25
+ headers: req.headers,
26
+ queryParameters: query,
27
+ pathParameters,
28
+ body: rawBody || undefined,
29
+ isBase64Encoded: false,
30
+ };
31
+ const eventBuffer = Buffer.from(JSON.stringify(event));
32
+ return { event: eventBuffer, headers: req.headers };
33
+ };
34
+ exports.transformToAliyunEvent = transformToAliyunEvent;
35
+ const createAliyunContextSerializable = (iac, functionName, handler, memory, timeout, requestId) => {
36
+ return {
37
+ requestId,
38
+ region: iac.provider.region || 'cn-hangzhou',
39
+ accountId: process.env.ALIYUN_ACCOUNT_ID || '000000000000',
40
+ credentials: {
41
+ accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || 'mock-access-key-id',
42
+ accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || 'mock-access-key-secret',
43
+ securityToken: process.env.ALIYUN_SECURITY_TOKEN || '',
44
+ },
45
+ function: {
46
+ name: functionName,
47
+ handler,
48
+ memory,
49
+ timeout,
50
+ initializer: '',
51
+ },
52
+ service: {
53
+ name: iac.service || 'default-service',
54
+ logProject: `${iac.service}-log-project`,
55
+ logStore: `${iac.service}-log-store`,
56
+ qualifier: 'LATEST',
57
+ versionId: '1',
58
+ },
59
+ tracing: {
60
+ spanContext: '',
61
+ jaegerEndpoint: '',
62
+ spanBaggages: {},
63
+ },
64
+ };
65
+ };
66
+ exports.createAliyunContextSerializable = createAliyunContextSerializable;
67
+ const createAliyunContext = (iac, functionName, handler, memory, timeout, requestId) => {
68
+ const baseContext = (0, exports.createAliyunContextSerializable)(iac, functionName, handler, memory, timeout, requestId);
69
+ return {
70
+ ...baseContext,
71
+ tracing: {
72
+ ...baseContext.tracing,
73
+ parseOpenTracingBaggages: () => ({}),
74
+ },
75
+ logger: createFCLogger(requestId),
76
+ };
77
+ };
78
+ exports.createAliyunContext = createAliyunContext;
79
+ const addFCHeaders = (context, headers) => {
80
+ return {
81
+ ...headers,
82
+ 'x-fc-request-id': context.requestId,
83
+ 'x-fc-access-key-id': context.credentials.accessKeyId,
84
+ 'x-fc-access-key-secret': context.credentials.accessKeySecret,
85
+ 'x-fc-security-token': context.credentials.securityToken,
86
+ 'x-fc-function-handler': context.function.handler,
87
+ 'x-fc-function-memory': String(context.function.memory),
88
+ 'x-fc-region': context.region,
89
+ 'x-fc-account-id': context.accountId,
90
+ 'x-fc-qualifier': context.service.qualifier,
91
+ 'x-fc-version-id': context.service.versionId,
92
+ 'x-fc-function-name': context.function.name,
93
+ 'x-fc-service-logproject': context.service.logProject,
94
+ 'x-fc-service-logstore': context.service.logStore,
95
+ 'x-fc-control-path': '/http-invoke',
96
+ };
97
+ };
98
+ exports.addFCHeaders = addFCHeaders;
99
+ const transformFCResponse = (result) => {
100
+ if (result && typeof result === 'object' && 'statusCode' in result && 'body' in result) {
101
+ const { statusCode: rawStatus = 200, body: rawBody, isBase64Encoded, headers = {}, } = result;
102
+ const parsedStatus = typeof rawStatus === 'string' ? parseInt(rawStatus, 10) : rawStatus;
103
+ const statusCode = isNaN(parsedStatus) ? 200 : parsedStatus;
104
+ let body = rawBody;
105
+ if (isBase64Encoded && typeof body === 'string') {
106
+ body = Buffer.from(body, 'base64').toString('utf-8');
107
+ }
108
+ if (typeof body === 'string') {
109
+ try {
110
+ body = JSON.parse(body);
111
+ }
112
+ catch {
113
+ // If parsing fails, keep as string
114
+ }
115
+ }
116
+ return { statusCode, headers, body };
117
+ }
118
+ return {
119
+ statusCode: 200,
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: result,
122
+ };
123
+ };
124
+ exports.transformFCResponse = transformFCResponse;
125
+ const logApiGatewayRequest = (requestId, apiPath, statusCode, startTime, endTime, sourceIp) => {
126
+ const duration = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(1);
127
+ const startTimeStr = formatDateTime(startTime);
128
+ const endTimeStr = formatDateTime(endTime);
129
+ const timestamp = formatDateTime(new Date());
130
+ console.log(`${timestamp} | ${requestId} | ${apiPath} | Sync Call | local-app | Development | local-project | ${statusCode} | ${startTimeStr} | ${endTimeStr} | ${duration}s | - | ${sourceIp}`);
131
+ };
132
+ exports.logApiGatewayRequest = logApiGatewayRequest;
133
+ const formatDateTime = (date) => {
134
+ const year = date.getFullYear();
135
+ const month = String(date.getMonth() + 1).padStart(2, '0');
136
+ const day = String(date.getDate()).padStart(2, '0');
137
+ const hours = String(date.getHours()).padStart(2, '0');
138
+ const minutes = String(date.getMinutes()).padStart(2, '0');
139
+ const seconds = String(date.getSeconds()).padStart(2, '0');
140
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
141
+ };
142
+ const generateRequestId = () => {
143
+ return (0, crypto_1.randomUUID)().replace(/-/g, '');
144
+ };
145
+ exports.generateRequestId = generateRequestId;
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.bucketsHandler = void 0;
7
+ const common_1 = require("../../common");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const TEXT_MIME_TYPES = new Set([
11
+ 'text/plain',
12
+ 'text/html',
13
+ 'text/css',
14
+ 'text/javascript',
15
+ 'text/markdown',
16
+ 'text/xml',
17
+ 'application/json',
18
+ 'application/javascript',
19
+ 'application/xml',
20
+ ]);
21
+ const getMimeType = (filename) => {
22
+ const ext = node_path_1.default.extname(filename).toLowerCase();
23
+ const mimeTypes = {
24
+ '.html': 'text/html',
25
+ '.htm': 'text/html',
26
+ '.css': 'text/css',
27
+ '.js': 'application/javascript',
28
+ '.json': 'application/json',
29
+ '.xml': 'application/xml',
30
+ '.txt': 'text/plain',
31
+ '.md': 'text/markdown',
32
+ '.jpg': 'image/jpeg',
33
+ '.jpeg': 'image/jpeg',
34
+ '.png': 'image/png',
35
+ '.gif': 'image/gif',
36
+ '.svg': 'image/svg+xml',
37
+ '.ico': 'image/x-icon',
38
+ '.pdf': 'application/pdf',
39
+ '.zip': 'application/zip',
40
+ '.tar': 'application/x-tar',
41
+ '.gz': 'application/gzip',
42
+ };
43
+ return mimeTypes[ext] || 'application/octet-stream';
44
+ };
45
+ const listDirectory = (dirPath, bucketPath) => {
46
+ try {
47
+ const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
48
+ return entries.map((entry) => {
49
+ const fullPath = node_path_1.default.join(dirPath, entry.name);
50
+ const relativePath = node_path_1.default.relative(bucketPath, fullPath);
51
+ const stats = node_fs_1.default.statSync(fullPath);
52
+ return {
53
+ name: entry.name,
54
+ type: entry.isDirectory() ? 'directory' : 'file',
55
+ size: entry.isDirectory() ? 0 : stats.size,
56
+ path: relativePath.replace(/\\/g, '/'), // Normalize path separators
57
+ };
58
+ });
59
+ }
60
+ catch (error) {
61
+ common_1.logger.error(`Error listing directory: ${error}`);
62
+ return [];
63
+ }
64
+ };
65
+ const getAllFiles = (dirPath, bucketPath, fileList = []) => {
66
+ try {
67
+ const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
68
+ for (const entry of entries) {
69
+ const fullPath = node_path_1.default.join(dirPath, entry.name);
70
+ const relativePath = node_path_1.default.relative(bucketPath, fullPath);
71
+ if (entry.isDirectory()) {
72
+ // Recursively list files in subdirectories
73
+ getAllFiles(fullPath, bucketPath, fileList);
74
+ }
75
+ else {
76
+ const stats = node_fs_1.default.statSync(fullPath);
77
+ fileList.push({
78
+ name: entry.name,
79
+ type: 'file',
80
+ size: stats.size,
81
+ path: relativePath.replace(/\\/g, '/'),
82
+ });
83
+ }
84
+ }
85
+ return fileList;
86
+ }
87
+ catch (error) {
88
+ common_1.logger.error(`Error getting all files: ${error}`);
89
+ return fileList;
90
+ }
91
+ };
92
+ const bucketsHandler = async (req, parsed, iac) => {
93
+ common_1.logger.info(`Bucket request received by local server -> ${req.method} ${parsed.identifier ?? '/'} ${parsed.url}`);
94
+ // Find the bucket definition
95
+ const bucketDef = iac.buckets?.find((bucket) => bucket.key === parsed.identifier);
96
+ if (!bucketDef) {
97
+ return {
98
+ statusCode: 404,
99
+ body: { error: 'Bucket not found', bucketKey: parsed.identifier },
100
+ };
101
+ }
102
+ // Determine the bucket path - use website.code if available, otherwise use bucket name
103
+ let bucketBasePath;
104
+ if (bucketDef.website?.code) {
105
+ bucketBasePath = node_path_1.default.resolve(process.cwd(), bucketDef.website.code);
106
+ }
107
+ else {
108
+ // Fallback: create a directory based on bucket name (for non-website buckets)
109
+ bucketBasePath = node_path_1.default.resolve(process.cwd(), bucketDef.name);
110
+ }
111
+ // Check if bucket path exists
112
+ if (!node_fs_1.default.existsSync(bucketBasePath)) {
113
+ return {
114
+ statusCode: 404,
115
+ body: {
116
+ error: 'Bucket directory not found',
117
+ bucketKey: bucketDef.key,
118
+ bucketName: bucketDef.name,
119
+ expectedPath: bucketBasePath,
120
+ },
121
+ };
122
+ }
123
+ // Root path lists all files in bucket
124
+ if (parsed.url === '/') {
125
+ try {
126
+ const files = getAllFiles(bucketBasePath, bucketBasePath);
127
+ return {
128
+ statusCode: 200,
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: {
131
+ bucket: bucketDef.name,
132
+ bucketKey: bucketDef.key,
133
+ files,
134
+ count: files.length,
135
+ },
136
+ };
137
+ }
138
+ catch (error) {
139
+ common_1.logger.error(`Error listing bucket files: ${error}`);
140
+ return {
141
+ statusCode: 500,
142
+ body: {
143
+ error: 'Failed to list bucket files',
144
+ message: error instanceof Error ? error.message : String(error),
145
+ },
146
+ };
147
+ }
148
+ }
149
+ // Otherwise, serve the requested file
150
+ const requestedPath = parsed.url.startsWith('/') ? parsed.url.slice(1) : parsed.url;
151
+ const filePath = node_path_1.default.join(bucketBasePath, requestedPath);
152
+ // Security check: ensure the requested file is within the bucket directory
153
+ const normalizedFilePath = node_path_1.default.normalize(filePath);
154
+ const normalizedBucketPath = node_path_1.default.normalize(bucketBasePath);
155
+ if (!normalizedFilePath.startsWith(normalizedBucketPath)) {
156
+ return {
157
+ statusCode: 403,
158
+ body: { error: 'Access denied: Path traversal attempt detected' },
159
+ };
160
+ }
161
+ // Check if file exists
162
+ if (!node_fs_1.default.existsSync(filePath)) {
163
+ return {
164
+ statusCode: 404,
165
+ body: {
166
+ error: 'File not found',
167
+ path: requestedPath,
168
+ bucketKey: bucketDef.key,
169
+ },
170
+ };
171
+ }
172
+ // If it's a directory, list its contents
173
+ const stats = node_fs_1.default.statSync(filePath);
174
+ if (stats.isDirectory()) {
175
+ try {
176
+ const files = listDirectory(filePath, bucketBasePath);
177
+ return {
178
+ statusCode: 200,
179
+ headers: { 'Content-Type': 'application/json' },
180
+ body: {
181
+ bucket: bucketDef.name,
182
+ bucketKey: bucketDef.key,
183
+ directory: requestedPath,
184
+ files,
185
+ count: files.length,
186
+ },
187
+ };
188
+ }
189
+ catch (error) {
190
+ common_1.logger.error(`Error listing directory: ${error}`);
191
+ return {
192
+ statusCode: 500,
193
+ body: {
194
+ error: 'Failed to list directory',
195
+ message: error instanceof Error ? error.message : String(error),
196
+ },
197
+ };
198
+ }
199
+ }
200
+ // Serve the file
201
+ try {
202
+ const fileContent = node_fs_1.default.readFileSync(filePath);
203
+ const mimeType = getMimeType(filePath);
204
+ // For text files, return as string; for binary files, return as base64
205
+ const isTextFile = TEXT_MIME_TYPES.has(mimeType);
206
+ return {
207
+ statusCode: 200,
208
+ headers: {
209
+ 'Content-Type': mimeType,
210
+ 'Content-Length': String(fileContent.length),
211
+ },
212
+ body: isTextFile ? fileContent.toString('utf-8') : fileContent.toString('base64'),
213
+ };
214
+ }
215
+ catch (error) {
216
+ common_1.logger.error(`Error reading file: ${error}`);
217
+ return {
218
+ statusCode: 500,
219
+ body: {
220
+ error: 'Failed to read file',
221
+ message: error instanceof Error ? error.message : String(error),
222
+ },
223
+ };
224
+ }
225
+ };
226
+ exports.bucketsHandler = bucketsHandler;