import express from "express"; import { readFileSync } from "node:fs"; import { CLOUDWATCH_DEFAULT_LIMIT, CLOUDWATCH_DEFAULT_WINDOW_MS, CLOUDWATCH_ENDPOINT, CLOUDWATCH_REGION, DEFAULT_REFRESH_MS, PORT, S3_ENDPOINT, S3_REGION, SES_ENDPOINT, SECRETS_ENDPOINT, SECRETS_REGION } from "./server/config.js"; import { getClientConfig, renderHtml } from "./server/page.js"; import { buildAttachmentDisposition, buildInlineDisposition, clampNumber, findSesMessageById, loadLogEvents, loadLogGroups, loadLogStreams, loadMessageAttachment, loadMessages, loadS3Buckets, loadS3ObjectDownload, loadS3ObjectPreview, loadS3Objects, loadSecretValue, loadSecrets, loadServiceHealthSummary } from "./server/localstack-service.js"; const app = express(); const CLIENT_APP_PATH = new URL("./public/client-app.js", import.meta.url); const CLIENT_APP_SOURCE = readFileSync(CLIENT_APP_PATH, "utf8"); app.use((req, res, next) => { res.set("Cache-Control", "no-store"); next(); }); app.get("/", (req, res) => { res.type("html").send(renderHtml()); }); app.get("/app.js", (req, res) => { res.type("application/javascript").send(`${CLIENT_APP_SOURCE}\n\nclientApp(${JSON.stringify(getClientConfig())});\n`); }); app.get("/health", (req, res) => { res.json({ ok: true, endpoint: SES_ENDPOINT, endpoints: { ses: SES_ENDPOINT, cloudWatchLogs: CLOUDWATCH_ENDPOINT, secretsManager: SECRETS_ENDPOINT, s3: S3_ENDPOINT }, port: PORT, defaultRefreshMs: DEFAULT_REFRESH_MS }); }); app.get("/api/service-health", async (req, res) => { try { res.json(await loadServiceHealthSummary()); } catch (error) { console.error("Error fetching service health:", error); res.status(502).json({ error: "Unable to fetch LocalStack service health", details: error.message }); } }); app.get("/api/messages", async (req, res) => { try { res.json(await loadMessages()); } catch (error) { console.error("Error fetching messages:", error); res.status(502).json({ error: "Unable to fetch messages from LocalStack SES", details: error.message, endpoint: SES_ENDPOINT }); } }); app.get("/api/messages/:id/raw", async (req, res) => { try { const message = await findSesMessageById(req.params.id); if (!message) { res.status(404).type("text/plain").send("Message not found"); return; } res.type("text/plain").send(message.RawData || ""); } catch (error) { console.error("Error fetching raw message:", error); res.status(502).type("text/plain").send(`Unable to fetch raw message: ${error.message}`); } }); app.get("/api/messages/:id/attachments/:index", async (req, res) => { try { const attachmentIndex = Number.parseInt(req.params.index, 10); if (!Number.isInteger(attachmentIndex) || attachmentIndex < 0) { res.status(400).type("text/plain").send("Attachment index must be a non-negative integer"); return; } const attachment = await loadMessageAttachment(req.params.id, attachmentIndex); if (!attachment) { res.status(404).type("text/plain").send("Attachment not found"); return; } res.setHeader("Content-Type", attachment.contentType); res.setHeader("Content-Disposition", buildAttachmentDisposition(attachment.filename)); res.setHeader("Content-Length", String(attachment.content.length)); res.send(attachment.content); } catch (error) { console.error("Error downloading attachment:", error); res.status(502).type("text/plain").send(`Unable to download attachment: ${error.message}`); } }); app.get("/api/logs/groups", async (req, res) => { try { const groups = await loadLogGroups(); res.json({ endpoint: CLOUDWATCH_ENDPOINT, region: CLOUDWATCH_REGION, groups }); } catch (error) { console.error("Error fetching log groups:", error); res.status(502).json({ error: "Unable to fetch CloudWatch log groups from LocalStack", details: error.message, endpoint: CLOUDWATCH_ENDPOINT }); } }); app.get("/api/logs/streams", async (req, res) => { try { const logGroupName = String(req.query.group || ""); if (!logGroupName) { res.status(400).json({ error: "Query parameter 'group' is required" }); return; } res.json({ logGroupName, streams: await loadLogStreams(logGroupName) }); } catch (error) { console.error("Error fetching log streams:", error); res.status(502).json({ error: "Unable to fetch CloudWatch log streams from LocalStack", details: error.message, endpoint: CLOUDWATCH_ENDPOINT }); } }); app.get("/api/logs/events", async (req, res) => { try { const logGroupName = String(req.query.group || ""); const logStreamName = String(req.query.stream || ""); const windowMs = clampNumber(req.query.windowMs, CLOUDWATCH_DEFAULT_WINDOW_MS, 60 * 1000, 24 * 60 * 60 * 1000); const limit = clampNumber(req.query.limit, CLOUDWATCH_DEFAULT_LIMIT, 25, 500); if (!logGroupName) { res.status(400).json({ error: "Query parameter 'group' is required" }); return; } res.json(await loadLogEvents({ logGroupName, logStreamName, windowMs, limit })); } catch (error) { console.error("Error fetching log events:", error); res.status(502).json({ error: "Unable to fetch CloudWatch log events from LocalStack", details: error.message, endpoint: CLOUDWATCH_ENDPOINT }); } }); app.get("/api/secrets", async (req, res) => { try { res.json(await loadSecrets()); } catch (error) { console.error("Error fetching secrets:", error); res.status(502).json({ error: "Unable to fetch Secrets Manager secrets from LocalStack", details: error.message, endpoint: SECRETS_ENDPOINT }); } }); app.get("/api/secrets/value", async (req, res) => { try { const secretId = String(req.query.id || ""); if (!secretId) { res.status(400).json({ error: "Query parameter 'id' is required" }); return; } res.json(await loadSecretValue(secretId)); } catch (error) { if (error?.name === "ResourceNotFoundException") { res.status(404).json({ error: "Secret not found", details: error.message, endpoint: SECRETS_ENDPOINT }); return; } console.error("Error fetching secret value:", error); res.status(502).json({ error: "Unable to fetch Secrets Manager value from LocalStack", details: error.message, endpoint: SECRETS_ENDPOINT }); } }); app.get("/api/s3/buckets", async (req, res) => { try { res.json(await loadS3Buckets()); } catch (error) { console.error("Error fetching S3 buckets:", error); res.status(502).json({ error: "Unable to fetch S3 buckets from LocalStack", details: error.message, endpoint: S3_ENDPOINT }); } }); app.get("/api/s3/objects", async (req, res) => { try { const bucket = String(req.query.bucket || ""); const prefix = String(req.query.prefix || ""); if (!bucket) { res.status(400).json({ error: "Query parameter 'bucket' is required" }); return; } res.json(await loadS3Objects({ bucket, prefix })); } catch (error) { console.error("Error fetching S3 objects:", error); res.status(502).json({ error: "Unable to fetch S3 objects from LocalStack", details: error.message, endpoint: S3_ENDPOINT }); } }); app.get("/api/s3/object", async (req, res) => { try { const bucket = String(req.query.bucket || ""); const key = String(req.query.key || ""); if (!bucket || !key) { res.status(400).json({ error: "Query parameters 'bucket' and 'key' are required" }); return; } res.json(await loadS3ObjectPreview({ bucket, key })); } catch (error) { if (error?.name === "NoSuchKey" || error?.name === "NotFound") { res.status(404).json({ error: "Object not found", details: error.message, endpoint: S3_ENDPOINT }); return; } console.error("Error fetching S3 object preview:", error); res.status(502).json({ error: "Unable to fetch S3 object preview from LocalStack", details: error.message, endpoint: S3_ENDPOINT }); } }); app.get("/api/s3/download", async (req, res) => { try { const bucket = String(req.query.bucket || ""); const key = String(req.query.key || ""); const inline = String(req.query.inline || "") === "1"; if (!bucket || !key) { res.status(400).type("text/plain").send("Query parameters 'bucket' and 'key' are required"); return; } const object = await loadS3ObjectDownload({ bucket, key }); res.setHeader("Content-Type", object.contentType); res.setHeader( "Content-Disposition", inline ? buildInlineDisposition(object.filename) : buildAttachmentDisposition(object.filename) ); res.setHeader("Content-Length", String(object.content.length)); res.send(object.content); } catch (error) { if (error?.name === "NoSuchKey" || error?.name === "NotFound") { res.status(404).type("text/plain").send("Object not found"); return; } console.error("Error downloading S3 object:", error); res.status(502).type("text/plain").send(`Unable to download S3 object: ${error.message}`); } }); app.listen(PORT, () => { console.log(`LocalStack inspector is running on http://localhost:${PORT}`); console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`); console.log(`Watching LocalStack CloudWatch Logs endpoint at ${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`); console.log(`Watching LocalStack Secrets Manager endpoint at ${SECRETS_ENDPOINT} (${SECRETS_REGION})`); console.log(`Watching LocalStack S3 endpoint at ${S3_ENDPOINT} (${S3_REGION})`); });