import fetch from "node-fetch"; import { DescribeLogGroupsCommand, DescribeLogStreamsCommand, FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs"; import { GetSecretValueCommand, ListSecretsCommand } from "@aws-sdk/client-secrets-manager"; import { GetObjectCommand, HeadObjectCommand, ListBucketsCommand, ListObjectsV2Command } from "@aws-sdk/client-s3"; import { simpleParser } from "mailparser"; import { CLOUDWATCH_ENDPOINT, CLOUDWATCH_REGION, FETCH_TIMEOUT_MS, S3_ENDPOINT, S3_IMAGE_PREVIEW_MAX_BYTES, S3_PREVIEW_MAX_BYTES, S3_REGION, SES_ENDPOINT, SECRETS_ENDPOINT, SECRETS_REGION, cloudWatchLogsClient, s3Client, secretsManagerClient } from "./config.js"; async function loadMessages() { const startedAt = Date.now(); const sesMessages = await fetchSesMessages(); const messages = await Promise.all(sesMessages.map((message, index) => toMessageViewModel(message, index))); messages.sort((left, right) => { if ((right.timestampMs || 0) !== (left.timestampMs || 0)) { return (right.timestampMs || 0) - (left.timestampMs || 0); } return right.index - left.index; }); return { endpoint: SES_ENDPOINT, fetchedAt: new Date().toISOString(), fetchDurationMs: Date.now() - startedAt, totalMessages: messages.length, parseErrors: messages.filter((message) => Boolean(message.parseError)).length, latestMessageTimestamp: messages[0]?.timestamp || "", messages }; } async function fetchSesMessages() { const response = await fetch(SES_ENDPOINT, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); if (!response.ok) { throw new Error(`SES endpoint responded with ${response.status}`); } const data = await response.json(); return Array.isArray(data.messages) ? data.messages : []; } async function loadLogGroups() { const groups = []; let nextToken; let pageCount = 0; do { const response = await cloudWatchLogsClient.send( new DescribeLogGroupsCommand({ nextToken, limit: 50 }) ); groups.push( ...(response.logGroups || []).map((group) => ({ name: group.logGroupName || "", arn: group.arn || "", storedBytes: group.storedBytes || 0, retentionInDays: group.retentionInDays || 0, creationTime: group.creationTime || 0 })) ); nextToken = response.nextToken; pageCount += 1; } while (nextToken && pageCount < 10); return groups.sort((left, right) => left.name.localeCompare(right.name)); } async function loadLogStreams(logGroupName) { const streams = []; let nextToken; let pageCount = 0; do { const response = await cloudWatchLogsClient.send( new DescribeLogStreamsCommand({ logGroupName, descending: true, orderBy: "LastEventTime", nextToken, limit: 50 }) ); streams.push( ...(response.logStreams || []).map((stream) => ({ name: stream.logStreamName || "", arn: stream.arn || "", lastEventTimestamp: stream.lastEventTimestamp || 0, lastIngestionTime: stream.lastIngestionTime || 0, storedBytes: stream.storedBytes || 0 })) ); nextToken = response.nextToken; pageCount += 1; } while (nextToken && pageCount < 6 && streams.length < 250); return streams; } async function loadLogEvents({ logGroupName, logStreamName, windowMs, limit }) { const startedAt = Date.now(); const eventMap = new Map(); const startTime = Date.now() - windowMs; let nextToken; let previousToken = ""; let pageCount = 0; let searchedLogStreams = 0; do { const response = await cloudWatchLogsClient.send( new FilterLogEventsCommand({ logGroupName, logStreamNames: logStreamName ? [logStreamName] : undefined, startTime, endTime: Date.now(), limit, nextToken }) ); for (const event of response.events || []) { const id = event.eventId || `${event.logStreamName || "stream"}-${event.timestamp || 0}-${event.ingestionTime || 0}`; if (!eventMap.has(id)) { const message = String(event.message || "").trim(); eventMap.set(id, { id, timestamp: event.timestamp || 0, ingestionTime: event.ingestionTime || 0, logStreamName: event.logStreamName || "", message, preview: buildLogPreview(message) }); } } searchedLogStreams = Math.max(searchedLogStreams, (response.searchedLogStreams || []).length); previousToken = nextToken || ""; nextToken = response.nextToken; pageCount += 1; } while (nextToken && nextToken !== previousToken && pageCount < 10 && eventMap.size < limit); const events = [...eventMap.values()] .sort((left, right) => { if ((right.timestamp || 0) !== (left.timestamp || 0)) { return (right.timestamp || 0) - (left.timestamp || 0); } return left.logStreamName.localeCompare(right.logStreamName); }) .slice(0, limit); return { endpoint: CLOUDWATCH_ENDPOINT, region: CLOUDWATCH_REGION, logGroupName, logStreamName, fetchDurationMs: Date.now() - startedAt, latestTimestamp: events[0]?.timestamp || 0, searchedLogStreams, totalEvents: events.length, events }; } async function loadSecrets() { const startedAt = Date.now(); const secrets = []; let nextToken; let pageCount = 0; do { const response = await secretsManagerClient.send( new ListSecretsCommand({ NextToken: nextToken, MaxResults: 50 }) ); secrets.push( ...(response.SecretList || []).map((secret, index) => ({ id: secret.ARN || secret.Name || `secret-${index}`, name: secret.Name || "Unnamed secret", arn: secret.ARN || "", description: secret.Description || "", createdDate: normalizeTimestamp(secret.CreatedDate), lastChangedDate: normalizeTimestamp(secret.LastChangedDate), lastAccessedDate: normalizeTimestamp(secret.LastAccessedDate), deletedDate: normalizeTimestamp(secret.DeletedDate), primaryRegion: secret.PrimaryRegion || "", owningService: secret.OwningService || "", rotationEnabled: Boolean(secret.RotationEnabled), versionCount: Object.keys(secret.SecretVersionsToStages || {}).length, tagCount: Array.isArray(secret.Tags) ? secret.Tags.length : 0, tags: (secret.Tags || []) .map((tag) => ({ key: tag.Key || "", value: tag.Value || "" })) .filter((tag) => tag.key || tag.value) })) ); nextToken = response.NextToken; pageCount += 1; } while (nextToken && pageCount < 10 && secrets.length < 500); secrets.sort((left, right) => { const leftTime = Date.parse(left.lastChangedDate || left.createdDate || 0) || 0; const rightTime = Date.parse(right.lastChangedDate || right.createdDate || 0) || 0; if (rightTime !== leftTime) { return rightTime - leftTime; } return left.name.localeCompare(right.name); }); return { endpoint: SECRETS_ENDPOINT, region: SECRETS_REGION, fetchedAt: new Date().toISOString(), fetchDurationMs: Date.now() - startedAt, totalSecrets: secrets.length, latestTimestamp: secrets[0]?.lastChangedDate || secrets[0]?.createdDate || "", secrets }; } async function loadSecretValue(secretId) { const startedAt = Date.now(); const response = await secretsManagerClient.send( new GetSecretValueCommand({ SecretId: secretId }) ); const secretBinary = response.SecretBinary ? typeof response.SecretBinary === "string" ? response.SecretBinary : Buffer.from(response.SecretBinary).toString("base64") : ""; return { endpoint: SECRETS_ENDPOINT, region: SECRETS_REGION, fetchDurationMs: Date.now() - startedAt, id: secretId, name: response.Name || "", arn: response.ARN || "", versionId: response.VersionId || "", versionStages: Array.isArray(response.VersionStages) ? response.VersionStages : [], createdDate: normalizeTimestamp(response.CreatedDate), secretString: typeof response.SecretString === "string" ? response.SecretString : "", secretBinary }; } async function loadS3Buckets() { const startedAt = Date.now(); const response = await s3Client.send(new ListBucketsCommand({})); const buckets = (response.Buckets || []) .map((bucket) => ({ name: bucket.Name || "", creationDate: normalizeTimestamp(bucket.CreationDate) })) .filter((bucket) => bucket.name) .sort((left, right) => left.name.localeCompare(right.name)); return { endpoint: S3_ENDPOINT, region: S3_REGION, fetchedAt: new Date().toISOString(), fetchDurationMs: Date.now() - startedAt, totalBuckets: buckets.length, buckets }; } async function loadS3Objects({ bucket, prefix }) { const startedAt = Date.now(); const objects = []; let continuationToken; let pageCount = 0; do { const response = await s3Client.send( new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix || undefined, ContinuationToken: continuationToken, MaxKeys: 200 }) ); objects.push( ...(response.Contents || []).map((object, index) => ({ id: `${bucket}::${object.Key || index}`, bucket, key: object.Key || "", size: object.Size || 0, lastModified: normalizeTimestamp(object.LastModified), etag: String(object.ETag || "").replace(/^"|"$/g, ""), storageClass: object.StorageClass || "STANDARD" })) ); continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; pageCount += 1; } while (continuationToken && pageCount < 10 && objects.length < 1000); objects.sort((left, right) => { const leftTime = Date.parse(left.lastModified || 0) || 0; const rightTime = Date.parse(right.lastModified || 0) || 0; if (rightTime !== leftTime) { return rightTime - leftTime; } return left.key.localeCompare(right.key); }); return { endpoint: S3_ENDPOINT, region: S3_REGION, bucket, prefix, fetchedAt: new Date().toISOString(), fetchDurationMs: Date.now() - startedAt, totalObjects: objects.length, latestTimestamp: objects[0]?.lastModified || "", objects }; } async function loadS3ObjectPreview({ bucket, key }) { const startedAt = Date.now(); const head = await s3Client.send( new HeadObjectCommand({ Bucket: bucket, Key: key }) ); const contentType = head.ContentType || guessObjectContentType(key); const contentLength = Number(head.ContentLength || 0); const previewType = resolveS3PreviewType(contentType, key); const result = { endpoint: S3_ENDPOINT, region: S3_REGION, bucket, key, fetchDurationMs: 0, contentType, contentLength, etag: String(head.ETag || "").replace(/^"|"$/g, ""), lastModified: normalizeTimestamp(head.LastModified), metadata: head.Metadata || {}, previewType, previewText: "", imageDataUrl: "", truncated: false }; const shouldLoadTextPreview = previewType === "json" || previewType === "text" || previewType === "html"; const shouldLoadImagePreview = previewType === "image" && contentLength > 0 && contentLength <= S3_IMAGE_PREVIEW_MAX_BYTES; if ((shouldLoadTextPreview || shouldLoadImagePreview) && contentLength > 0) { const previewBytes = Math.max(1, Math.min(contentLength || S3_PREVIEW_MAX_BYTES, S3_PREVIEW_MAX_BYTES)); const response = await s3Client.send( new GetObjectCommand({ Bucket: bucket, Key: key, Range: `bytes=0-${previewBytes - 1}` }) ); const content = Buffer.from(await response.Body.transformToByteArray()); result.truncated = contentLength > content.length; if (shouldLoadImagePreview) { result.imageDataUrl = `data:${contentType};base64,${content.toString("base64")}`; } else { result.previewText = content.toString("utf8"); } } result.fetchDurationMs = Date.now() - startedAt; return result; } async function loadServiceHealthSummary() { const startedAt = Date.now(); const [sesResult, logsResult, secretsResult, s3Result] = await Promise.allSettled([ fetchSesMessages(), loadLogGroups(), loadSecrets(), loadS3Buckets() ]); return { fetchedAt: new Date().toISOString(), fetchDurationMs: Date.now() - startedAt, services: { emails: summarizeHealthResult({ icon: "✉️", panel: "emails", label: "SES Emails", result: sesResult, count: sesResult.status === "fulfilled" ? sesResult.value.length : 0, detail: SES_ENDPOINT, noun: "email" }), logs: summarizeHealthResult({ icon: "📜", panel: "logs", label: "CloudWatch Logs", result: logsResult, count: logsResult.status === "fulfilled" ? logsResult.value.length : 0, detail: `${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`, noun: "group" }), secrets: summarizeHealthResult({ icon: "🔐", panel: "secrets", label: "Secrets Manager", result: secretsResult, count: secretsResult.status === "fulfilled" ? secretsResult.value.totalSecrets : 0, detail: `${SECRETS_ENDPOINT} (${SECRETS_REGION})`, noun: "secret" }), s3: summarizeHealthResult({ icon: "🪣", panel: "s3", label: "S3 Explorer", result: s3Result, count: s3Result.status === "fulfilled" ? s3Result.value.totalBuckets : 0, detail: `${S3_ENDPOINT} (${S3_REGION})`, noun: "bucket" }) } }; } async function findSesMessageById(id) { const messages = await fetchSesMessages(); return messages.find((message, index) => resolveMessageId(message, index) === id) || null; } async function parseSesMessageById(id) { const message = await findSesMessageById(id); if (!message) { return null; } return simpleParser(message.RawData || ""); } async function toMessageViewModel(message, index) { const id = resolveMessageId(message, index); try { const parsed = await simpleParser(message.RawData || ""); const textContent = normalizeText(parsed.text || ""); const renderedHtml = buildRenderedHtml(parsed.html || parsed.textAsHtml || ""); const timestamp = normalizeTimestamp(message.Timestamp || parsed.date); return { id, index, from: formatAddressList(parsed.from) || message.Source || "Unknown sender", to: formatAddressList(parsed.to) || "No To Address", replyTo: formatAddressList(parsed.replyTo), subject: parsed.subject || "No Subject", region: message.Region || "", timestamp, timestampMs: timestamp ? Date.parse(timestamp) : 0, messageId: parsed.messageId || "", rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), attachmentCount: parsed.attachments.length, attachments: parsed.attachments.map((attachment, attachmentIndex) => ({ index: attachmentIndex, filename: resolveAttachmentFilename(attachment, attachmentIndex), contentType: attachment.contentType || "application/octet-stream", size: attachment.size || 0 })), preview: buildPreview(textContent, renderedHtml), textContent, renderedHtml, hasHtml: Boolean(renderedHtml), parseError: "" }; } catch (error) { return { id, index, from: message.Source || "Unknown sender", to: "Unknown recipient", replyTo: "", subject: "Unable to parse message", region: message.Region || "", timestamp: normalizeTimestamp(message.Timestamp), timestampMs: message.Timestamp ? Date.parse(message.Timestamp) : 0, messageId: "", rawSizeBytes: Buffer.byteLength(message.RawData || "", "utf8"), attachmentCount: 0, attachments: [], preview: "This message could not be parsed. Open the raw view to inspect the MIME source.", textContent: "", renderedHtml: "", hasHtml: false, parseError: error.message }; } } function resolveMessageId(message, index = 0) { return message.Id || `${message.Timestamp || "unknown"}-${message.Source || "unknown"}-${index}`; } function resolveAttachmentFilename(attachment, index = 0) { if (attachment?.filename) { return attachment.filename; } return `attachment-${index + 1}${attachmentExtension(attachment?.contentType)}`; } function attachmentExtension(contentType) { const normalized = String(contentType || "") .split(";")[0] .trim() .toLowerCase(); return ( { "application/json": ".json", "application/pdf": ".pdf", "application/zip": ".zip", "image/gif": ".gif", "image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp", "text/calendar": ".ics", "text/csv": ".csv", "text/html": ".html", "text/plain": ".txt" }[normalized] || "" ); } function buildAttachmentDisposition(filename) { const fallback = String(filename || "attachment") .replace(/[^\x20-\x7e]/g, "_") .replace(/["\\]/g, "_"); return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "attachment")}`; } function buildInlineDisposition(filename) { const fallback = String(filename || "file") .replace(/[^\x20-\x7e]/g, "_") .replace(/["\\]/g, "_"); return `inline; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename || "file")}`; } function basenameFromKey(key) { const value = String(key || ""); const parts = value.split("/").filter(Boolean); return parts[parts.length - 1] || "file"; } function guessObjectContentType(key) { const normalizedKey = String(key || "").toLowerCase(); if (normalizedKey.endsWith(".json")) { return "application/json"; } if (normalizedKey.endsWith(".csv")) { return "text/csv"; } if (normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) { return "text/html"; } if (normalizedKey.endsWith(".txt") || normalizedKey.endsWith(".log") || normalizedKey.endsWith(".md")) { return "text/plain"; } if (normalizedKey.endsWith(".png")) { return "image/png"; } if (normalizedKey.endsWith(".jpg") || normalizedKey.endsWith(".jpeg")) { return "image/jpeg"; } if (normalizedKey.endsWith(".gif")) { return "image/gif"; } if (normalizedKey.endsWith(".webp")) { return "image/webp"; } if (normalizedKey.endsWith(".svg")) { return "image/svg+xml"; } if (normalizedKey.endsWith(".pdf")) { return "application/pdf"; } return "application/octet-stream"; } function resolveS3PreviewType(contentType, key) { const normalizedType = String(contentType || "").toLowerCase(); const normalizedKey = String(key || "").toLowerCase(); if (normalizedType.includes("json") || normalizedKey.endsWith(".json")) { return "json"; } if (normalizedType.startsWith("image/")) { return "image"; } if (normalizedType.includes("html") || normalizedKey.endsWith(".html") || normalizedKey.endsWith(".htm")) { return "html"; } if ( normalizedType.startsWith("text/") || [".txt", ".log", ".csv", ".xml", ".yml", ".yaml", ".md"].some((extension) => normalizedKey.endsWith(extension)) ) { return "text"; } return "binary"; } function summarizeHealthResult({ icon, panel, label, result, count, detail, noun }) { if (result.status === "fulfilled") { return { ok: true, icon, panel, label, count, summary: `${count} ${noun}${count === 1 ? "" : "s"}`, detail }; } return { ok: false, icon, panel, label, count: 0, summary: "Needs attention", detail: result.reason?.message || detail }; } function normalizeTimestamp(value) { if (!value) { return ""; } const date = value instanceof Date ? value : new Date(value); return Number.isNaN(date.getTime()) ? "" : date.toISOString(); } function normalizeText(value) { return String(value || "") .replace(/\r\n/g, "\n") .trim(); } function buildPreview(textContent, renderedHtml) { const source = (textContent || stripTags(renderedHtml)).replace(/\s+/g, " ").trim(); if (!source) { return "No message preview available."; } return source.length > 220 ? `${source.slice(0, 217)}...` : source; } function buildLogPreview(message) { const source = String(message || "") .replace(/\s+/g, " ") .trim(); if (!source) { return "No log preview available."; } return source.length > 220 ? `${source.slice(0, 217)}...` : source; } function clampNumber(value, fallback, min, max) { const parsed = Number(value); if (!Number.isFinite(parsed)) { return fallback; } return Math.min(Math.max(parsed, min), max); } function buildRenderedHtml(html) { if (!html) { return ""; } const value = String(html); const hasDocument = /]/i.test(value) || /