diff --git a/_reference/localEmailViewer/README.md b/_reference/localEmailViewer/README.md index 6492c1176..019d6f946 100644 --- a/_reference/localEmailViewer/README.md +++ b/_reference/localEmailViewer/README.md @@ -3,6 +3,7 @@ This app connects to your Docker LocalStack endpoints and gives you a compact in - SES generated emails - CloudWatch log groups, streams, and recent events - Secrets Manager secrets and values +- S3 buckets and object previews ```shell npm start @@ -21,9 +22,14 @@ Features: - SES email workspace with manual refresh, live refresh, search, HTML/text/raw views, attachment downloads, and new-message highlighting - CloudWatch Logs workspace with log group selection, stream filtering, adjustable time window, - adjustable event limit, live refresh, and in-browser log search + adjustable event limit, live refresh, in-browser log search, log-level highlighting, wrap toggle, + and optional tail-to-newest mode - Secrets Manager workspace with live refresh, search, expandable secret metadata, lazy-loaded - secret values, and copyable JSON/text values + secret values, masked-by-default secret viewing, and quick copy actions +- S3 Explorer workspace with bucket selection, prefix filtering, object search, lazy object + previews, + object key/URI copy actions, and downloads +- 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 Optional environment variables: @@ -40,4 +46,9 @@ CLOUDWATCH_VIEWER_WINDOW_MS=900000 CLOUDWATCH_VIEWER_LIMIT=200 SECRETS_VIEWER_ENDPOINT=http://localhost:4566 SECRETS_VIEWER_REGION=ca-central-1 +S3_VIEWER_ENDPOINT=http://localhost:4566 +S3_VIEWER_REGION=ca-central-1 +S3_VIEWER_BUCKET= +S3_VIEWER_PREVIEW_BYTES=262144 +S3_VIEWER_IMAGE_PREVIEW_BYTES=1048576 ``` diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index f45eab624..64b72fbd7 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -7,6 +7,13 @@ import { FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs"; import { GetSecretValueCommand, ListSecretsCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { + GetObjectCommand, + HeadObjectCommand, + ListBucketsCommand, + ListObjectsV2Command, + S3Client +} from "@aws-sdk/client-s3"; import { simpleParser } from "mailparser"; const app = express(); @@ -22,6 +29,11 @@ const CLOUDWATCH_DEFAULT_WINDOW_MS = Number(process.env.CLOUDWATCH_VIEWER_WINDOW 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" @@ -36,6 +48,12 @@ const secretsManagerClient = new SecretsManagerClient({ endpoint: SECRETS_ENDPOINT, credentials: LOCALSTACK_CREDENTIALS }); +const s3Client = new S3Client({ + region: S3_REGION, + endpoint: S3_ENDPOINT, + credentials: LOCALSTACK_CREDENTIALS, + forcePathStyle: true +}); app.use((req, res, next) => { res.set("Cache-Control", "no-store"); @@ -57,13 +75,26 @@ app.get("/health", (req, res) => { endpoints: { ses: SES_ENDPOINT, cloudWatchLogs: CLOUDWATCH_ENDPOINT, - secretsManager: SECRETS_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()); @@ -236,6 +267,108 @@ app.get("/api/secrets/value", async (req, res) => { } }); +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 response = await s3Client.send( + new GetObjectCommand({ + Bucket: bucket, + Key: key + }) + ); + const content = Buffer.from(await response.Body.transformToByteArray()); + const filename = basenameFromKey(key); + + res.setHeader("Content-Type", response.ContentType || guessObjectContentType(key)); + res.setHeader( + "Content-Disposition", + inline ? buildInlineDisposition(filename) : buildAttachmentDisposition(filename) + ); + res.setHeader("Content-Length", String(content.length)); + res.send(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}`); + } +}); + async function loadMessages() { const startedAt = Date.now(); const sesMessages = await fetchSesMessages(); @@ -496,6 +629,192 @@ async function loadSecretValue(secretId) { }; } +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; @@ -612,6 +931,116 @@ function buildAttachmentDisposition(filename) { 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 ""; @@ -715,6 +1144,9 @@ function getClientConfig() { cloudWatchRegion: CLOUDWATCH_REGION, secretsEndpoint: SECRETS_ENDPOINT, secretsRegion: SECRETS_REGION, + s3Endpoint: S3_ENDPOINT, + s3Region: S3_REGION, + defaultS3Bucket: S3_DEFAULT_BUCKET, defaultLogGroup: CLOUDWATCH_DEFAULT_GROUP, defaultLogWindowMs: CLOUDWATCH_DEFAULT_WINDOW_MS, defaultLogLimit: CLOUDWATCH_DEFAULT_LIMIT @@ -739,12 +1171,15 @@ function renderHtml() {
${renderLogPreviewContent(event)}
${renderLogBodyContent(event.message)}
${escapeHtml(buildS3Preview(object))}
+${valueState.displayHtml}
+ ${revealed ? valueState.displayHtml : maskedHtml}
+ `;
+ }
+
+ function renderS3PreviewPanel(object, previewState) {
+ if (!previewState) {
+ return `No inline preview available for this object type.`; + + if (previewState.previewType === "image" && previewState.imageDataUrl) { + previewContent = `
${highlightJsonText(prettyJsonOrText(previewState.previewText))}`;
+ } else if (previewState.previewType === "text" || previewState.previewType === "html") {
+ previewContent = `${escapeHtml(previewState.previewText || "No preview text available.")}`;
+ }
+
+ return `
+
+ ${previewContent}
`;
}
@@ -2782,6 +4031,31 @@ function clientApp(config) {
window.requestAnimationFrame(applySecretState);
}
+ function syncS3Expansion() {
+ const applyS3State = () => {
+ el.s3List.querySelectorAll(".s3Card").forEach((details) => {
+ const id = details.dataset.id;
+ const shouldOpen = Boolean(id && s3State.openIds.has(id));
+
+ if (shouldOpen && !details.open) {
+ details.open = true;
+ }
+
+ if (!shouldOpen && details.open) {
+ details.open = false;
+ return;
+ }
+
+ if (shouldOpen) {
+ ensureS3Preview(id, { force: false });
+ }
+ });
+ };
+
+ applyS3State();
+ window.requestAnimationFrame(applyS3State);
+ }
+
function resolveAttachmentIcon(attachment) {
const filename = String(attachment?.filename || "").toLowerCase();
const contentType = String(attachment?.contentType || "").toLowerCase();
@@ -2870,6 +4144,10 @@ function clientApp(config) {
return secretsState.items.find((secret) => secret.id === id);
}
+ function getS3Object(id) {
+ return s3State.objects.find((object) => object.id === id);
+ }
+
async function loadRaw(id) {
if (state.raw[id]?.status === "loaded") {
return state.raw[id].value;
@@ -2967,6 +4245,67 @@ function clientApp(config) {
}
}
+ async function ensureS3Preview(id, options = {}) {
+ const { force = false } = options;
+
+ if (!id) {
+ return null;
+ }
+
+ if (!force && s3State.previews[id]?.status === "loaded") {
+ return s3State.previews[id];
+ }
+
+ if (s3State.previews[id]?.status === "loading") {
+ return null;
+ }
+
+ const object = getS3Object(id);
+
+ if (!object) {
+ return null;
+ }
+
+ s3State.previews[id] = { status: "loading" };
+ renderS3All();
+
+ try {
+ const response = await fetch(
+ `/api/s3/object?bucket=${encodeURIComponent(object.bucket)}&key=${encodeURIComponent(object.key)}`,
+ { cache: "no-store" }
+ );
+
+ if (!response.ok) {
+ const payload = await safeJson(response);
+ throw new Error(payload?.details || payload?.error || `Request failed with ${response.status}`);
+ }
+
+ const payload = await response.json();
+ const entry = {
+ status: "loaded",
+ previewType: payload.previewType || "binary",
+ previewText: payload.previewText || "",
+ imageDataUrl: payload.imageDataUrl || "",
+ contentType: payload.contentType || "",
+ contentLength: payload.contentLength || 0,
+ truncated: Boolean(payload.truncated),
+ metadata: payload.metadata || {}
+ };
+
+ s3State.previews[id] = entry;
+ return entry;
+ } catch (error) {
+ s3State.previews[id] = {
+ status: "error",
+ error: error.message || "Unknown S3 preview error"
+ };
+ setS3Status("Could not load the S3 object preview.", "bad");
+ return null;
+ } finally {
+ renderS3All();
+ }
+ }
+
async function copyText(value) {
try {
await navigator.clipboard.writeText(value);
@@ -3013,9 +4352,19 @@ function clientApp(config) {
el.secretsStatusChip.textContent = message;
}
+ function setS3Status(message, tone) {
+ el.s3StatusChip.className = "status";
+
+ if (tone) {
+ el.s3StatusChip.classList.add(tone);
+ }
+
+ el.s3StatusChip.textContent = message;
+ }
+
function getInitialPanel() {
const storedPanel = readStoredValue(PANEL_STORAGE_KEY);
- return ["emails", "logs", "secrets"].includes(storedPanel) ? storedPanel : "emails";
+ return ["emails", "logs", "secrets", "s3"].includes(storedPanel) ? storedPanel : "emails";
}
function getInitialTheme() {
@@ -3072,7 +4421,9 @@ function clientApp(config) {
auto: logsState.auto,
interval: logsState.interval,
windowMs: logsState.windowMs,
- limit: logsState.limit
+ limit: logsState.limit,
+ wrapLines: logsState.wrapLines,
+ tailNewest: logsState.tailNewest
});
}
@@ -3084,6 +4435,33 @@ function clientApp(config) {
});
}
+ function persistS3Preferences() {
+ writeStoredJson(S3_PREFERENCES_STORAGE_KEY, {
+ bucket: s3State.bucket,
+ prefix: s3State.prefix,
+ search: s3State.search,
+ auto: s3State.auto,
+ interval: s3State.interval
+ });
+ }
+
+ function resetSavedState() {
+ [
+ THEME_STORAGE_KEY,
+ PANEL_STORAGE_KEY,
+ EMAIL_PREFERENCES_STORAGE_KEY,
+ LOG_PREFERENCES_STORAGE_KEY,
+ SECRET_PREFERENCES_STORAGE_KEY,
+ S3_PREFERENCES_STORAGE_KEY
+ ].forEach((key) => {
+ try {
+ window.localStorage.removeItem(key);
+ } catch {}
+ });
+
+ window.location.reload();
+ }
+
function getStoredPreferences(key) {
try {
const rawValue = window.localStorage.getItem(key);
@@ -3238,6 +4616,74 @@ function clientApp(config) {
return fragments.join(" โข ");
}
+ function toggleSecretReveal(id) {
+ if (secretsState.revealedIds.has(id)) {
+ secretsState.revealedIds.delete(id);
+ } else {
+ secretsState.revealedIds.add(id);
+ }
+
+ renderSecretsAll();
+ }
+
+ function maskSecretValue(value) {
+ const source = String(value || "");
+
+ if (!source) {
+ return "Secret is loaded but empty.";
+ }
+
+ const lines = source.split("\n");
+ return lines.map((line) => "โข".repeat(Math.max(12, Math.min(line.length || 0, 48)))).join("\n");
+ }
+
+ function buildS3Preview(object) {
+ const fragments = [];
+
+ if (object.storageClass) {
+ fragments.push(object.storageClass);
+ }
+
+ fragments.push(formatBytes(object.size));
+
+ if (object.etag) {
+ fragments.push(`ETag ${object.etag.slice(0, 12)}`);
+ }
+
+ return fragments.join(" โข ");
+ }
+
+ function prettyJsonOrText(value) {
+ const parsed = tryParseJsonText(value);
+ return parsed.ok ? JSON.stringify(parsed.value, null, 2) : String(value || "");
+ }
+
+ function detectLogLevel(event) {
+ const parsed = tryParseJsonText(event?.message);
+ const candidates = parsed.ok
+ ? [parsed.value?.level, parsed.value?.severity, parsed.value?.logLevel, parsed.value?.status, parsed.value?.lvl]
+ : [String(event?.message || "").match(/\b(error|warn|warning|info|debug|trace|fatal)\b/i)?.[0] || ""];
+ const normalized = String(candidates.find(Boolean) || "").toLowerCase();
+
+ if (["fatal", "error", "critical"].includes(normalized)) {
+ return { label: normalized.toUpperCase(), className: "levelError" };
+ }
+
+ if (["warn", "warning"].includes(normalized)) {
+ return { label: "WARN", className: "levelWarn" };
+ }
+
+ if (["info", "notice"].includes(normalized)) {
+ return { label: normalized.toUpperCase(), className: "levelInfo" };
+ }
+
+ if (["debug", "trace"].includes(normalized)) {
+ return { label: normalized.toUpperCase(), className: "levelDebug" };
+ }
+
+ return null;
+ }
+
function renderLogPreviewContent(event) {
const parsedLog = tryParseJsonText(event?.message);
@@ -3329,4 +4775,5 @@ app.listen(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})`);
});
diff --git a/_reference/localEmailViewer/package-lock.json b/_reference/localEmailViewer/package-lock.json
index 2f00ef450..b01591261 100644
--- a/_reference/localEmailViewer/package-lock.json
+++ b/_reference/localEmailViewer/package-lock.json
@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.1012.0",
+ "@aws-sdk/client-s3": "^3.1013.0",
"@aws-sdk/client-secrets-manager": "^3.1013.0",
"express": "^5.1.0",
"mailparser": "^3.7.4",
@@ -30,6 +31,69 @@
"node": ">=16.0.0"
}
},
+ "node_modules/@aws-crypto/crc32c": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz",
+ "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz",
+ "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@aws-crypto/sha256-browser": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
@@ -208,6 +272,72 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/client-s3": {
+ "version": "3.1013.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1013.0.tgz",
+ "integrity": "sha512-vFdyRyRatF+xP9Fi+4alZkmzZadqOAM34Pm6SUZsYtumNrWkgMc/pFWITnsq6eltM8qcV/vcinQ1ZBXWm/PlKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha1-browser": "5.2.0",
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/credential-provider-node": "^3.972.23",
+ "@aws-sdk/middleware-bucket-endpoint": "^3.972.8",
+ "@aws-sdk/middleware-expect-continue": "^3.972.8",
+ "@aws-sdk/middleware-flexible-checksums": "^3.974.2",
+ "@aws-sdk/middleware-host-header": "^3.972.8",
+ "@aws-sdk/middleware-location-constraint": "^3.972.8",
+ "@aws-sdk/middleware-logger": "^3.972.8",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.8",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.22",
+ "@aws-sdk/middleware-ssec": "^3.972.8",
+ "@aws-sdk/middleware-user-agent": "^3.972.23",
+ "@aws-sdk/region-config-resolver": "^3.972.8",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.10",
+ "@aws-sdk/types": "^3.973.6",
+ "@aws-sdk/util-endpoints": "^3.996.5",
+ "@aws-sdk/util-user-agent-browser": "^3.972.8",
+ "@aws-sdk/util-user-agent-node": "^3.973.9",
+ "@smithy/config-resolver": "^4.4.11",
+ "@smithy/core": "^3.23.12",
+ "@smithy/eventstream-serde-browser": "^4.2.12",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.12",
+ "@smithy/eventstream-serde-node": "^4.2.12",
+ "@smithy/fetch-http-handler": "^5.3.15",
+ "@smithy/hash-blob-browser": "^4.2.13",
+ "@smithy/hash-node": "^4.2.12",
+ "@smithy/hash-stream-node": "^4.2.12",
+ "@smithy/invalid-dependency": "^4.2.12",
+ "@smithy/md5-js": "^4.2.12",
+ "@smithy/middleware-content-length": "^4.2.12",
+ "@smithy/middleware-endpoint": "^4.4.26",
+ "@smithy/middleware-retry": "^4.4.43",
+ "@smithy/middleware-serde": "^4.2.15",
+ "@smithy/middleware-stack": "^4.2.12",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/node-http-handler": "^4.5.0",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/smithy-client": "^4.12.6",
+ "@smithy/types": "^4.13.1",
+ "@smithy/url-parser": "^4.2.12",
+ "@smithy/util-base64": "^4.3.2",
+ "@smithy/util-body-length-browser": "^4.2.2",
+ "@smithy/util-body-length-node": "^4.2.3",
+ "@smithy/util-defaults-mode-browser": "^4.3.42",
+ "@smithy/util-defaults-mode-node": "^4.2.45",
+ "@smithy/util-endpoints": "^3.3.3",
+ "@smithy/util-middleware": "^4.2.12",
+ "@smithy/util-retry": "^4.2.12",
+ "@smithy/util-stream": "^4.5.20",
+ "@smithy/util-utf8": "^4.2.2",
+ "@smithy/util-waiter": "^4.2.13",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/client-secrets-manager": {
"version": "3.1013.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1013.0.tgz",
@@ -282,6 +412,19 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/crc64-nvme": {
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz",
+ "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/credential-provider-env": {
"version": "3.972.20",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.20.tgz",
@@ -440,6 +583,64 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/middleware-bucket-endpoint": {
+ "version": "3.972.8",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz",
+ "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@aws-sdk/util-arn-parser": "^3.972.3",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-config-provider": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-expect-continue": {
+ "version": "3.972.8",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz",
+ "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-flexible-checksums": {
+ "version": "3.974.2",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.2.tgz",
+ "integrity": "sha512-4soN/N4R6ptdnHw7hXPVDZMIIL+vhN8rwtLdDyS0uD7ExhadtJzolTBIM5eKSkbw5uBEbIwtJc8HCG2NM6tN/g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@aws-crypto/crc32c": "5.2.0",
+ "@aws-crypto/util": "5.2.0",
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/crc64-nvme": "^3.972.5",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/is-array-buffer": "^4.2.2",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-middleware": "^4.2.12",
+ "@smithy/util-stream": "^4.5.20",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/middleware-host-header": {
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz",
@@ -455,6 +656,20 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/middleware-location-constraint": {
+ "version": "3.972.8",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz",
+ "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/middleware-logger": {
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz",
@@ -485,6 +700,45 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.972.22",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.22.tgz",
+ "integrity": "sha512-dkUcRxF4rVpPbyHpxjCApGK6b7JpnSeo7tDoNakpRKmiLMCqgy4tlGBgeEYJnZgLrA4xc5jVKuXgvgqKqU18Kw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/types": "^3.973.6",
+ "@aws-sdk/util-arn-parser": "^3.972.3",
+ "@smithy/core": "^3.23.12",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/signature-v4": "^5.3.12",
+ "@smithy/smithy-client": "^4.12.6",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-config-provider": "^4.2.2",
+ "@smithy/util-middleware": "^4.2.12",
+ "@smithy/util-stream": "^4.5.20",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-ssec": {
+ "version": "3.972.8",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz",
+ "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/middleware-user-agent": {
"version": "3.972.23",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.23.tgz",
@@ -569,6 +823,23 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.10",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.10.tgz",
+ "integrity": "sha512-yJSbFTedh1McfqXa9wZzjchqQ2puq5PI/qRz5kUjg2UXS5mO4MBYBbeXaZ2rp/h+ZbkcYEdo4Qsiah9psyoxrA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "^3.972.22",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/signature-v4": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/token-providers": {
"version": "3.1013.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1013.0.tgz",
@@ -600,6 +871,18 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/util-arn-parser": {
+ "version": "3.972.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz",
+ "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/util-endpoints": {
"version": "3.996.5",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz",
@@ -714,6 +997,31 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/chunked-blob-reader": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz",
+ "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/chunked-blob-reader-native": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz",
+ "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-base64": "^4.3.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@smithy/config-resolver": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz",
@@ -854,6 +1162,21 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/hash-blob-browser": {
+ "version": "4.2.13",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz",
+ "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/chunked-blob-reader": "^5.2.2",
+ "@smithy/chunked-blob-reader-native": "^4.2.3",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@smithy/hash-node": {
"version": "4.2.12",
"resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz",
@@ -869,6 +1192,20 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/hash-stream-node": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz",
+ "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@smithy/invalid-dependency": {
"version": "4.2.12",
"resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz",
@@ -894,6 +1231,20 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/md5-js": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz",
+ "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@smithy/middleware-content-length": {
"version": "4.2.12",
"resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz",
@@ -1340,6 +1691,20 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/util-waiter": {
+ "version": "4.2.13",
+ "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.13.tgz",
+ "integrity": "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@smithy/uuid": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz",
diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json
index 67ea3fc6b..956794f4c 100644
--- a/_reference/localEmailViewer/package.json
+++ b/_reference/localEmailViewer/package.json
@@ -10,9 +10,10 @@
"keywords": [],
"author": "",
"license": "ISC",
- "description": "LocalStack inspector for SES emails, CloudWatch logs, and Secrets Manager",
+ "description": "LocalStack inspector for SES emails, CloudWatch logs, Secrets Manager, and S3",
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.1012.0",
+ "@aws-sdk/client-s3": "^3.1013.0",
"@aws-sdk/client-secrets-manager": "^3.1013.0",
"express": "^5.1.0",
"mailparser": "^3.7.4",