diff --git a/_reference/localEmailViewer/README.md b/_reference/localEmailViewer/README.md index 019d6f946..cbfe9e555 100644 --- a/_reference/localEmailViewer/README.md +++ b/_reference/localEmailViewer/README.md @@ -32,6 +32,14 @@ Features: - Shared LocalStack service health strip plus a reset action for clearing saved viewer state - Compact single-page UI for switching between the local stack tools you use most +Code layout: + +- `index.js`: small Express bootstrap and route registration +- `server/config.js`: LocalStack endpoints, defaults, and AWS client setup +- `server/localstack-service.js`: SES, Logs, Secrets, and S3 data loading helpers +- `server/page.js`: server-rendered HTML shell, CSS, and client config payload +- `public/client-app.js`: browser-side UI state, rendering, refresh logic, and interactions + Optional environment variables: ```shell diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 64b72fbd7..67df19055 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -1,59 +1,41 @@ import express from "express"; -import fetch from "node-fetch"; +import { readFileSync } from "node:fs"; import { - CloudWatchLogsClient, - DescribeLogGroupsCommand, - DescribeLogStreamsCommand, - FilterLogEventsCommand -} from "@aws-sdk/client-cloudwatch-logs"; -import { GetSecretValueCommand, ListSecretsCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; + 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 { - GetObjectCommand, - HeadObjectCommand, - ListBucketsCommand, - ListObjectsV2Command, - S3Client -} from "@aws-sdk/client-s3"; -import { simpleParser } from "mailparser"; + 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 PORT = Number(process.env.PORT || 3334); -const SES_ENDPOINT = process.env.SES_VIEWER_ENDPOINT || "http://localhost:4566/_aws/ses"; -const FETCH_TIMEOUT_MS = Number(process.env.SES_VIEWER_FETCH_TIMEOUT_MS || 5000); -const DEFAULT_REFRESH_MS = Number(process.env.SES_VIEWER_REFRESH_MS || 10000); -const CLOUDWATCH_ENDPOINT = process.env.CLOUDWATCH_VIEWER_ENDPOINT || "http://localhost:4566"; -const CLOUDWATCH_REGION = process.env.CLOUDWATCH_VIEWER_REGION || process.env.AWS_DEFAULT_REGION || "ca-central-1"; -const CLOUDWATCH_DEFAULT_GROUP = process.env.CLOUDWATCH_VIEWER_LOG_GROUP || "development"; -const CLOUDWATCH_DEFAULT_WINDOW_MS = Number(process.env.CLOUDWATCH_VIEWER_WINDOW_MS || 15 * 60 * 1000); -const CLOUDWATCH_DEFAULT_LIMIT = Number(process.env.CLOUDWATCH_VIEWER_LIMIT || 200); -const SECRETS_ENDPOINT = process.env.SECRETS_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; -const SECRETS_REGION = process.env.SECRETS_VIEWER_REGION || CLOUDWATCH_REGION; -const S3_ENDPOINT = process.env.S3_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; -const S3_REGION = process.env.S3_VIEWER_REGION || CLOUDWATCH_REGION; -const S3_DEFAULT_BUCKET = process.env.S3_VIEWER_BUCKET || ""; -const S3_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_PREVIEW_BYTES || 256 * 1024); -const S3_IMAGE_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_IMAGE_PREVIEW_BYTES || 1024 * 1024); -const LOCALSTACK_CREDENTIALS = { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || "test", - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "test" -}; -const cloudWatchLogsClient = new CloudWatchLogsClient({ - region: CLOUDWATCH_REGION, - endpoint: CLOUDWATCH_ENDPOINT, - credentials: LOCALSTACK_CREDENTIALS -}); -const secretsManagerClient = new SecretsManagerClient({ - region: SECRETS_REGION, - endpoint: SECRETS_ENDPOINT, - credentials: LOCALSTACK_CREDENTIALS -}); -const s3Client = new S3Client({ - region: S3_REGION, - endpoint: S3_ENDPOINT, - credentials: LOCALSTACK_CREDENTIALS, - forcePathStyle: true -}); +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"); @@ -65,7 +47,7 @@ app.get("/", (req, res) => { }); app.get("/app.js", (req, res) => { - res.type("application/javascript").send(`(${clientApp.toString()})(${JSON.stringify(getClientConfig())});`); + res.type("application/javascript").send(`${CLIENT_APP_SOURCE}\n\nclientApp(${JSON.stringify(getClientConfig())});\n`); }); app.get("/health", (req, res) => { @@ -133,27 +115,17 @@ app.get("/api/messages/:id/attachments/:index", async (req, res) => { return; } - const parsed = await parseSesMessageById(req.params.id); - - if (!parsed) { - res.status(404).type("text/plain").send("Message not found"); - return; - } - - const attachment = parsed.attachments?.[attachmentIndex]; + const attachment = await loadMessageAttachment(req.params.id, attachmentIndex); if (!attachment) { res.status(404).type("text/plain").send("Attachment not found"); return; } - const filename = resolveAttachmentFilename(attachment, attachmentIndex); - const content = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content || ""); - - res.setHeader("Content-Type", attachment.contentType || "application/octet-stream"); - res.setHeader("Content-Disposition", buildAttachmentDisposition(filename)); - res.setHeader("Content-Length", String(content.length)); - res.send(content); + 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}`); @@ -166,7 +138,6 @@ app.get("/api/logs/groups", async (req, res) => { res.json({ endpoint: CLOUDWATCH_ENDPOINT, region: CLOUDWATCH_REGION, - defaultGroup: CLOUDWATCH_DEFAULT_GROUP, groups }); } catch (error) { @@ -342,22 +313,15 @@ app.get("/api/s3/download", async (req, res) => { return; } - const response = await s3Client.send( - new GetObjectCommand({ - Bucket: bucket, - Key: key - }) - ); - const content = Buffer.from(await response.Body.transformToByteArray()); - const filename = basenameFromKey(key); + const object = await loadS3ObjectDownload({ bucket, key }); - res.setHeader("Content-Type", response.ContentType || guessObjectContentType(key)); + res.setHeader("Content-Type", object.contentType); res.setHeader( "Content-Disposition", - inline ? buildInlineDisposition(filename) : buildAttachmentDisposition(filename) + inline ? buildInlineDisposition(object.filename) : buildAttachmentDisposition(object.filename) ); - res.setHeader("Content-Length", String(content.length)); - res.send(content); + 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"); @@ -369,4407 +333,6 @@ app.get("/api/s3/download", async (req, res) => { } }); -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) || / - -
- - -LocalStack Toolbox
-