846 lines
23 KiB
JavaScript
846 lines
23 KiB
JavaScript
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 = /<html[\s>]/i.test(value) || /<!doctype/i.test(value);
|
|
|
|
if (hasDocument) {
|
|
return value;
|
|
}
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<base target="_blank">
|
|
<style>body{margin:0;padding:16px;font-family:Arial,sans-serif;background:#fff;}</style>
|
|
</head>
|
|
<body>${value}</body>
|
|
</html>`;
|
|
}
|
|
|
|
function stripTags(value) {
|
|
return String(value || "")
|
|
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
.replace(/<[^>]+>/g, " ");
|
|
}
|
|
|
|
function formatAddressList(addresses) {
|
|
if (!addresses?.value?.length) {
|
|
return "";
|
|
}
|
|
|
|
return addresses.value
|
|
.map(({ name, address }) => {
|
|
if (name && address) {
|
|
return `${name} <${address}>`;
|
|
}
|
|
|
|
return address || name || "";
|
|
})
|
|
.filter(Boolean)
|
|
.join(", ");
|
|
}
|
|
|
|
async function loadMessageAttachment(messageId, attachmentIndex) {
|
|
const parsed = await parseSesMessageById(messageId);
|
|
|
|
if (!parsed) {
|
|
return null;
|
|
}
|
|
|
|
const attachment = parsed.attachments?.[attachmentIndex];
|
|
|
|
if (!attachment) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
filename: resolveAttachmentFilename(attachment, attachmentIndex),
|
|
contentType: attachment.contentType || "application/octet-stream",
|
|
content: Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content || "")
|
|
};
|
|
}
|
|
|
|
async function loadS3ObjectDownload({ bucket, key }) {
|
|
const response = await s3Client.send(
|
|
new GetObjectCommand({
|
|
Bucket: bucket,
|
|
Key: key
|
|
})
|
|
);
|
|
|
|
return {
|
|
filename: basenameFromKey(key),
|
|
contentType: response.ContentType || guessObjectContentType(key),
|
|
content: Buffer.from(await response.Body.transformToByteArray())
|
|
};
|
|
}
|
|
|
|
export {
|
|
buildAttachmentDisposition,
|
|
buildInlineDisposition,
|
|
clampNumber,
|
|
findSesMessageById,
|
|
loadLogEvents,
|
|
loadLogGroups,
|
|
loadLogStreams,
|
|
loadMessageAttachment,
|
|
loadMessages,
|
|
loadS3Buckets,
|
|
loadS3ObjectDownload,
|
|
loadS3ObjectPreview,
|
|
loadS3Objects,
|
|
loadSecretValue,
|
|
loadSecrets,
|
|
loadServiceHealthSummary
|
|
};
|