diff --git a/.gitignore b/.gitignore
index 4b0d51183..59c48f34d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,8 +142,6 @@ docker_data
/CLAUDE.md
/COPILOT.md
/GEMINI.md
-/_reference/select-component-test-plan.md
-
/.cursorrules
/AGENTS.md
/AI_CONTEXT.md
@@ -151,4 +149,3 @@ docker_data
/COPILOT.md
/.github/copilot-instructions.md
/GEMINI.md
-/_reference/select-component-test-plan.md
diff --git a/_reference/localEmailViewer/README.md b/_reference/localEmailViewer/README.md
index 3f8a6fc71..cbfe9e555 100644
--- a/_reference/localEmailViewer/README.md
+++ b/_reference/localEmailViewer/README.md
@@ -1,7 +1,62 @@
-This will connect to your dockers local stack session and render the email in HTML.
+This app connects to your Docker LocalStack endpoints and gives you a compact inspector for:
+
+- SES generated emails
+- CloudWatch log groups, streams, and recent events
+- Secrets Manager secrets and values
+- S3 buckets and object previews
+
+```shell
+npm start
+```
+
+Or:
```shell
node index.js
```
-http://localhost:3334
+Open: http://localhost:3334
+
+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, 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, 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
+
+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
+PORT=3334
+SES_VIEWER_ENDPOINT=http://localhost:4566/_aws/ses
+SES_VIEWER_REFRESH_MS=10000
+SES_VIEWER_FETCH_TIMEOUT_MS=5000
+CLOUDWATCH_VIEWER_ENDPOINT=http://localhost:4566
+CLOUDWATCH_VIEWER_REGION=ca-central-1
+CLOUDWATCH_VIEWER_LOG_GROUP=development
+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 1dd17f779..67df19055 100644
--- a/_reference/localEmailViewer/index.js
+++ b/_reference/localEmailViewer/index.js
@@ -1,96 +1,342 @@
-// index.js
-
import express from "express";
-import fetch from "node-fetch";
-import { simpleParser } from "mailparser";
+import { readFileSync } from "node:fs";
+import {
+ CLOUDWATCH_DEFAULT_LIMIT,
+ CLOUDWATCH_DEFAULT_WINDOW_MS,
+ CLOUDWATCH_ENDPOINT,
+ CLOUDWATCH_REGION,
+ DEFAULT_REFRESH_MS,
+ PORT,
+ S3_ENDPOINT,
+ S3_REGION,
+ SES_ENDPOINT,
+ SECRETS_ENDPOINT,
+ SECRETS_REGION
+} from "./server/config.js";
+import { getClientConfig, renderHtml } from "./server/page.js";
+import {
+ buildAttachmentDisposition,
+ buildInlineDisposition,
+ clampNumber,
+ findSesMessageById,
+ loadLogEvents,
+ loadLogGroups,
+ loadLogStreams,
+ loadMessageAttachment,
+ loadMessages,
+ loadS3Buckets,
+ loadS3ObjectDownload,
+ loadS3ObjectPreview,
+ loadS3Objects,
+ loadSecretValue,
+ loadSecrets,
+ loadServiceHealthSummary
+} from "./server/localstack-service.js";
const app = express();
-const PORT = 3334;
+const CLIENT_APP_PATH = new URL("./public/client-app.js", import.meta.url);
+const CLIENT_APP_SOURCE = readFileSync(CLIENT_APP_PATH, "utf8");
-app.get("/", async (req, res) => {
+app.use((req, res, next) => {
+ res.set("Cache-Control", "no-store");
+ next();
+});
+
+app.get("/", (req, res) => {
+ res.type("html").send(renderHtml());
+});
+
+app.get("/app.js", (req, res) => {
+ res.type("application/javascript").send(`${CLIENT_APP_SOURCE}\n\nclientApp(${JSON.stringify(getClientConfig())});\n`);
+});
+
+app.get("/health", (req, res) => {
+ res.json({
+ ok: true,
+ endpoint: SES_ENDPOINT,
+ endpoints: {
+ ses: SES_ENDPOINT,
+ cloudWatchLogs: CLOUDWATCH_ENDPOINT,
+ secretsManager: SECRETS_ENDPOINT,
+ s3: S3_ENDPOINT
+ },
+ port: PORT,
+ defaultRefreshMs: DEFAULT_REFRESH_MS
+ });
+});
+
+app.get("/api/service-health", async (req, res) => {
try {
- const response = await fetch("http://localhost:4566/_aws/ses");
- if (!response.ok) {
- throw new Error("Network response was not ok");
- }
- const data = await response.json();
- const messagesHtml = await parseMessages(data.messages);
- res.send(renderHtml(messagesHtml));
+ res.json(await loadServiceHealthSummary());
} catch (error) {
- console.error("Error fetching messages:", error);
- res.status(500).send("Error fetching messages");
+ console.error("Error fetching service health:", error);
+ res.status(502).json({
+ error: "Unable to fetch LocalStack service health",
+ details: error.message
+ });
}
});
-async function parseMessages(messages) {
- const parsedMessages = await Promise.all(
- messages.map(async (message, index) => {
- try {
- const parsed = await simpleParser(message.RawData);
- return `
-
-
-
Message ${index + 1}
-
From: ${message.Source}
-
To: ${parsed.to.text || "No To Address"}
-
Subject: ${parsed.subject || "No Subject"}
-
Region: ${message.Region}
-
Timestamp: ${message.Timestamp}
-
-
${parsed.html || parsed.textAsHtml || "No HTML content available"}
-
- `;
- } catch (error) {
- console.error("Error parsing email:", error);
- return `
-
-
Message ${index + 1}
-
From: ${message.Source}
-
Region: ${message.Region}
-
Timestamp: ${message.Timestamp}
-
Error parsing email content
-
- `;
- }
- })
- );
- return parsedMessages.join("");
-}
+app.get("/api/messages", async (req, res) => {
+ try {
+ res.json(await loadMessages());
+ } catch (error) {
+ console.error("Error fetching messages:", error);
+ res.status(502).json({
+ error: "Unable to fetch messages from LocalStack SES",
+ details: error.message,
+ endpoint: SES_ENDPOINT
+ });
+ }
+});
-function renderHtml(messagesHtml) {
- return `
-
-
-
-
-
- Email Messages Viewer
-
-
-
-
-
-
Email Messages Viewer
-
${messagesHtml}
-
-
-
- `;
-}
+app.get("/api/messages/:id/raw", async (req, res) => {
+ try {
+ const message = await findSesMessageById(req.params.id);
+
+ if (!message) {
+ res.status(404).type("text/plain").send("Message not found");
+ return;
+ }
+
+ res.type("text/plain").send(message.RawData || "");
+ } catch (error) {
+ console.error("Error fetching raw message:", error);
+ res.status(502).type("text/plain").send(`Unable to fetch raw message: ${error.message}`);
+ }
+});
+
+app.get("/api/messages/:id/attachments/:index", async (req, res) => {
+ try {
+ const attachmentIndex = Number.parseInt(req.params.index, 10);
+
+ if (!Number.isInteger(attachmentIndex) || attachmentIndex < 0) {
+ res.status(400).type("text/plain").send("Attachment index must be a non-negative integer");
+ return;
+ }
+
+ const attachment = await loadMessageAttachment(req.params.id, attachmentIndex);
+
+ if (!attachment) {
+ res.status(404).type("text/plain").send("Attachment not found");
+ return;
+ }
+
+ res.setHeader("Content-Type", attachment.contentType);
+ res.setHeader("Content-Disposition", buildAttachmentDisposition(attachment.filename));
+ res.setHeader("Content-Length", String(attachment.content.length));
+ res.send(attachment.content);
+ } catch (error) {
+ console.error("Error downloading attachment:", error);
+ res.status(502).type("text/plain").send(`Unable to download attachment: ${error.message}`);
+ }
+});
+
+app.get("/api/logs/groups", async (req, res) => {
+ try {
+ const groups = await loadLogGroups();
+ res.json({
+ endpoint: CLOUDWATCH_ENDPOINT,
+ region: CLOUDWATCH_REGION,
+ groups
+ });
+ } catch (error) {
+ console.error("Error fetching log groups:", error);
+ res.status(502).json({
+ error: "Unable to fetch CloudWatch log groups from LocalStack",
+ details: error.message,
+ endpoint: CLOUDWATCH_ENDPOINT
+ });
+ }
+});
+
+app.get("/api/logs/streams", async (req, res) => {
+ try {
+ const logGroupName = String(req.query.group || "");
+
+ if (!logGroupName) {
+ res.status(400).json({ error: "Query parameter 'group' is required" });
+ return;
+ }
+
+ res.json({
+ logGroupName,
+ streams: await loadLogStreams(logGroupName)
+ });
+ } catch (error) {
+ console.error("Error fetching log streams:", error);
+ res.status(502).json({
+ error: "Unable to fetch CloudWatch log streams from LocalStack",
+ details: error.message,
+ endpoint: CLOUDWATCH_ENDPOINT
+ });
+ }
+});
+
+app.get("/api/logs/events", async (req, res) => {
+ try {
+ const logGroupName = String(req.query.group || "");
+ const logStreamName = String(req.query.stream || "");
+ const windowMs = clampNumber(req.query.windowMs, CLOUDWATCH_DEFAULT_WINDOW_MS, 60 * 1000, 24 * 60 * 60 * 1000);
+ const limit = clampNumber(req.query.limit, CLOUDWATCH_DEFAULT_LIMIT, 25, 500);
+
+ if (!logGroupName) {
+ res.status(400).json({ error: "Query parameter 'group' is required" });
+ return;
+ }
+
+ res.json(await loadLogEvents({ logGroupName, logStreamName, windowMs, limit }));
+ } catch (error) {
+ console.error("Error fetching log events:", error);
+ res.status(502).json({
+ error: "Unable to fetch CloudWatch log events from LocalStack",
+ details: error.message,
+ endpoint: CLOUDWATCH_ENDPOINT
+ });
+ }
+});
+
+app.get("/api/secrets", async (req, res) => {
+ try {
+ res.json(await loadSecrets());
+ } catch (error) {
+ console.error("Error fetching secrets:", error);
+ res.status(502).json({
+ error: "Unable to fetch Secrets Manager secrets from LocalStack",
+ details: error.message,
+ endpoint: SECRETS_ENDPOINT
+ });
+ }
+});
+
+app.get("/api/secrets/value", async (req, res) => {
+ try {
+ const secretId = String(req.query.id || "");
+
+ if (!secretId) {
+ res.status(400).json({ error: "Query parameter 'id' is required" });
+ return;
+ }
+
+ res.json(await loadSecretValue(secretId));
+ } catch (error) {
+ if (error?.name === "ResourceNotFoundException") {
+ res.status(404).json({
+ error: "Secret not found",
+ details: error.message,
+ endpoint: SECRETS_ENDPOINT
+ });
+ return;
+ }
+
+ console.error("Error fetching secret value:", error);
+ res.status(502).json({
+ error: "Unable to fetch Secrets Manager value from LocalStack",
+ details: error.message,
+ endpoint: SECRETS_ENDPOINT
+ });
+ }
+});
+
+app.get("/api/s3/buckets", async (req, res) => {
+ try {
+ res.json(await loadS3Buckets());
+ } catch (error) {
+ console.error("Error fetching S3 buckets:", error);
+ res.status(502).json({
+ error: "Unable to fetch S3 buckets from LocalStack",
+ details: error.message,
+ endpoint: S3_ENDPOINT
+ });
+ }
+});
+
+app.get("/api/s3/objects", async (req, res) => {
+ try {
+ const bucket = String(req.query.bucket || "");
+ const prefix = String(req.query.prefix || "");
+
+ if (!bucket) {
+ res.status(400).json({ error: "Query parameter 'bucket' is required" });
+ return;
+ }
+
+ res.json(await loadS3Objects({ bucket, prefix }));
+ } catch (error) {
+ console.error("Error fetching S3 objects:", error);
+ res.status(502).json({
+ error: "Unable to fetch S3 objects from LocalStack",
+ details: error.message,
+ endpoint: S3_ENDPOINT
+ });
+ }
+});
+
+app.get("/api/s3/object", async (req, res) => {
+ try {
+ const bucket = String(req.query.bucket || "");
+ const key = String(req.query.key || "");
+
+ if (!bucket || !key) {
+ res.status(400).json({ error: "Query parameters 'bucket' and 'key' are required" });
+ return;
+ }
+
+ res.json(await loadS3ObjectPreview({ bucket, key }));
+ } catch (error) {
+ if (error?.name === "NoSuchKey" || error?.name === "NotFound") {
+ res.status(404).json({
+ error: "Object not found",
+ details: error.message,
+ endpoint: S3_ENDPOINT
+ });
+ return;
+ }
+
+ console.error("Error fetching S3 object preview:", error);
+ res.status(502).json({
+ error: "Unable to fetch S3 object preview from LocalStack",
+ details: error.message,
+ endpoint: S3_ENDPOINT
+ });
+ }
+});
+
+app.get("/api/s3/download", async (req, res) => {
+ try {
+ const bucket = String(req.query.bucket || "");
+ const key = String(req.query.key || "");
+ const inline = String(req.query.inline || "") === "1";
+
+ if (!bucket || !key) {
+ res.status(400).type("text/plain").send("Query parameters 'bucket' and 'key' are required");
+ return;
+ }
+
+ const object = await loadS3ObjectDownload({ bucket, key });
+
+ res.setHeader("Content-Type", object.contentType);
+ res.setHeader(
+ "Content-Disposition",
+ inline ? buildInlineDisposition(object.filename) : buildAttachmentDisposition(object.filename)
+ );
+ res.setHeader("Content-Length", String(object.content.length));
+ res.send(object.content);
+ } catch (error) {
+ if (error?.name === "NoSuchKey" || error?.name === "NotFound") {
+ res.status(404).type("text/plain").send("Object not found");
+ return;
+ }
+
+ console.error("Error downloading S3 object:", error);
+ res.status(502).type("text/plain").send(`Unable to download S3 object: ${error.message}`);
+ }
+});
app.listen(PORT, () => {
- console.log(`Server is running on http://localhost:${PORT}`);
+ console.log(`LocalStack inspector is running on http://localhost:${PORT}`);
+ console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`);
+ console.log(`Watching LocalStack CloudWatch Logs endpoint at ${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`);
+ console.log(`Watching LocalStack Secrets Manager endpoint at ${SECRETS_ENDPOINT} (${SECRETS_REGION})`);
+ console.log(`Watching LocalStack S3 endpoint at ${S3_ENDPOINT} (${S3_REGION})`);
});
diff --git a/_reference/localEmailViewer/package-lock.json b/_reference/localEmailViewer/package-lock.json
index 2c7ecad93..b01591261 100644
--- a/_reference/localEmailViewer/package-lock.json
+++ b/_reference/localEmailViewer/package-lock.json
@@ -9,11 +9,968 @@
"version": "1.0.0",
"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",
"node-fetch": "^3.3.2"
}
},
+ "node_modules/@aws-crypto/crc32": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
+ "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@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/sha256-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/sha256-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/sha256-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-js": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
+ "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/supports-web-crypto": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
+ "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
+ "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.222.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/util/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/util/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/util/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-sdk/client-cloudwatch-logs": {
+ "version": "3.1012.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1012.0.tgz",
+ "integrity": "sha512-nkMczmeCG9a1YD48zDKZLX7wLGb5ycXMJXVFnXF8xVxQ2v2uo2xFz1ZZganWHSQxR6ZfBPG7R9OTkDrVx79ZPA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.21",
+ "@aws-sdk/credential-provider-node": "^3.972.22",
+ "@aws-sdk/middleware-host-header": "^3.972.8",
+ "@aws-sdk/middleware-logger": "^3.972.8",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.8",
+ "@aws-sdk/middleware-user-agent": "^3.972.22",
+ "@aws-sdk/region-config-resolver": "^3.972.8",
+ "@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.8",
+ "@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-node": "^4.2.12",
+ "@smithy/invalid-dependency": "^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-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-jVleaMhnwz7JsYZ8vo3otRdhNl1vmJPfwYQtAfb0T29TwfI9pvfRuWD4CXbm0DPmCJzhN7YT+tqn1dLhk83Y3g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@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-host-header": "^3.972.8",
+ "@aws-sdk/middleware-logger": "^3.972.8",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.8",
+ "@aws-sdk/middleware-user-agent": "^3.972.23",
+ "@aws-sdk/region-config-resolver": "^3.972.8",
+ "@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/fetch-http-handler": "^5.3.15",
+ "@smithy/hash-node": "^4.2.12",
+ "@smithy/invalid-dependency": "^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-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/core": {
+ "version": "3.973.22",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.22.tgz",
+ "integrity": "sha512-lY6g5L95jBNgOUitUhfV2N/W+i08jHEl3xuLODYSQH5Sf50V+LkVYBSyZRLtv2RyuXZXiV7yQ+acpswK1tlrOA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@aws-sdk/xml-builder": "^3.972.14",
+ "@smithy/core": "^3.23.12",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/property-provider": "^4.2.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-base64": "^4.3.2",
+ "@smithy/util-middleware": "^4.2.12",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-vI0QN96DFx3g9AunfOWF3CS4cMkqFiR/WM/FyP9QHr5rZ2dKPkYwP3tCgAOvGuu9CXI7dC1vU2FVUuZ+tfpNvQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.972.22",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.22.tgz",
+ "integrity": "sha512-aS/81smalpe7XDnuQfOq4LIPuaV2PRKU2aMTrHcqO5BD4HwO5kESOHNcec2AYfBtLtIDqgF6RXisgBnfK/jt0w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/fetch-http-handler": "^5.3.15",
+ "@smithy/node-http-handler": "^4.5.0",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/smithy-client": "^4.12.6",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-stream": "^4.5.20",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.972.22",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.22.tgz",
+ "integrity": "sha512-rpF8fBT0LllMDp78s62aL2A/8MaccjyJ0ORzqu+ZADeECLSrrCWIeeXsuRam+pxiAMkI1uIyDZJmgLGdadkPXw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/credential-provider-env": "^3.972.20",
+ "@aws-sdk/credential-provider-http": "^3.972.22",
+ "@aws-sdk/credential-provider-login": "^3.972.22",
+ "@aws-sdk/credential-provider-process": "^3.972.20",
+ "@aws-sdk/credential-provider-sso": "^3.972.22",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.22",
+ "@aws-sdk/nested-clients": "^3.996.12",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/credential-provider-imds": "^4.2.12",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-login": {
+ "version": "3.972.22",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.22.tgz",
+ "integrity": "sha512-u33CO9zeNznlVSg9tWTCRYxaGkqr1ufU6qeClpmzAabXZa8RZxQoVXxL5T53oZJFzQYj+FImORCSsi7H7B77gQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/nested-clients": "^3.996.12",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.972.23",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.23.tgz",
+ "integrity": "sha512-U8tyLbLOZItuVWTH0ay9gWo4xMqZwqQbg1oMzdU4FQSkTpqXemm4X0uoKBR6llqAStgBp30ziKFJHTA43l4qMw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "^3.972.20",
+ "@aws-sdk/credential-provider-http": "^3.972.22",
+ "@aws-sdk/credential-provider-ini": "^3.972.22",
+ "@aws-sdk/credential-provider-process": "^3.972.20",
+ "@aws-sdk/credential-provider-sso": "^3.972.22",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.22",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/credential-provider-imds": "^4.2.12",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.972.20",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.20.tgz",
+ "integrity": "sha512-QRfk7GbA4/HDRjhP3QYR6QBr/QKreVoOzvvlRHnOuGgYJkeoPgPY3LAI1kK1ZMgZ4hH9KiGp757/ntol+INAig==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.972.22",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.22.tgz",
+ "integrity": "sha512-4vqlSaUbBj4aNPVKfB6yXuIQ2Z2mvLfIGba2OzzF6zUkN437/PGWsxBU2F8QPSFHti6seckvyCXidU3H+R8NvQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/nested-clients": "^3.996.12",
+ "@aws-sdk/token-providers": "3.1013.0",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.972.22",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.22.tgz",
+ "integrity": "sha512-/wN1CYg2rVLhW8/jLxMWacQrkpaynnL+4j/Z+e6X1PfoE6NiC0BeOw3i0JmtZrKun85wNV5GmspvuWJihfeeUw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/nested-clients": "^3.996.12",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==",
+ "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-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",
+ "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==",
+ "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-recursion-detection": {
+ "version": "3.972.8",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz",
+ "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@aws/lambda-invoke-store": "^0.2.2",
+ "@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-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",
+ "integrity": "sha512-HQu8QoqGZZTvg0Spl9H39QTsSMFwgu+8yz/QGKndXFLk9FZMiCiIgBCVlTVKMDvVbgqIzD9ig+/HmXsIL2Rb+g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/types": "^3.973.6",
+ "@aws-sdk/util-endpoints": "^3.996.5",
+ "@smithy/core": "^3.23.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-retry": "^4.2.12",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients": {
+ "version": "3.996.12",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.12.tgz",
+ "integrity": "sha512-KLdQGJPSm98uLINolQ0Tol8OAbk7g0Y7zplHJ1K83vbMIH13aoCvR6Tho66xueW4l4aZlEgVGLWBnD8ifUMsGQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/middleware-host-header": "^3.972.8",
+ "@aws-sdk/middleware-logger": "^3.972.8",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.8",
+ "@aws-sdk/middleware-user-agent": "^3.972.23",
+ "@aws-sdk/region-config-resolver": "^3.972.8",
+ "@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/fetch-http-handler": "^5.3.15",
+ "@smithy/hash-node": "^4.2.12",
+ "@smithy/invalid-dependency": "^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-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.972.8",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz",
+ "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/config-resolver": "^4.4.11",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-IL1c54UvbuERrs9oLm5rvkzMciwhhpn1FL0SlC3XUMoLlFhdBsWJgQKK8O5fsQLxbFVqjbjFx9OBkrn44X9PHw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.22",
+ "@aws-sdk/nested-clients": "^3.996.12",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.973.6",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz",
+ "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/types": "^4.13.1",
+ "@smithy/url-parser": "^4.2.12",
+ "@smithy/util-endpoints": "^3.3.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-locate-window": {
+ "version": "3.965.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz",
+ "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.972.8",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz",
+ "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/types": "^4.13.1",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.973.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.9.tgz",
+ "integrity": "sha512-jeFqqp8KD/P5O+qeKxyGeu7WEVIZFNprnkaDjGmBOjwxYwafCBhpxTgV1TlW6L8e76Vh/siNylNmN/OmSIFBUQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "^3.972.23",
+ "@aws-sdk/types": "^3.973.6",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-config-provider": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder": {
+ "version": "3.972.14",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.14.tgz",
+ "integrity": "sha512-G/Yd8Bnnyh8QrqLf8jWJbixEnScUFW24e/wOBGYdw1Cl4r80KX/DvHyM2GVZ2vTp7J4gTEr8IXJlTadA8+UfuQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "fast-xml-parser": "5.5.6",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws/lambda-invoke-store": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
+ "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
@@ -27,6 +984,739 @@
"url": "https://ko-fi.com/killymxi"
}
},
+ "node_modules/@smithy/abort-controller": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz",
+ "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-config-provider": "^4.2.2",
+ "@smithy/util-endpoints": "^3.3.3",
+ "@smithy/util-middleware": "^4.2.12",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/core": {
+ "version": "3.23.12",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz",
+ "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.3.12",
+ "@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-middleware": "^4.2.12",
+ "@smithy/util-stream": "^4.5.20",
+ "@smithy/util-utf8": "^4.2.2",
+ "@smithy/uuid": "^1.1.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/credential-provider-imds": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz",
+ "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/url-parser": "^4.2.12",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-codec": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz",
+ "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-hex-encoding": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-browser": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz",
+ "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/eventstream-serde-universal": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-config-resolver": {
+ "version": "4.3.12",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz",
+ "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-node": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz",
+ "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/eventstream-serde-universal": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-universal": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz",
+ "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/eventstream-codec": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/fetch-http-handler": {
+ "version": "5.3.15",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz",
+ "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/querystring-builder": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-base64": "^4.3.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-buffer-from": "^4.2.2",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/is-array-buffer": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz",
+ "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-endpoint": {
+ "version": "4.4.27",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz",
+ "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.23.12",
+ "@smithy/middleware-serde": "^4.2.15",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "@smithy/url-parser": "^4.2.12",
+ "@smithy/util-middleware": "^4.2.12",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-retry": {
+ "version": "4.4.44",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz",
+ "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/service-error-classification": "^4.2.12",
+ "@smithy/smithy-client": "^4.12.7",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-middleware": "^4.2.12",
+ "@smithy/util-retry": "^4.2.12",
+ "@smithy/uuid": "^1.1.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-serde": {
+ "version": "4.2.15",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz",
+ "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.23.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-stack": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz",
+ "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/node-config-provider": {
+ "version": "4.3.12",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz",
+ "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/shared-ini-file-loader": "^4.4.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/node-http-handler": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz",
+ "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.2.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/querystring-builder": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/property-provider": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz",
+ "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/protocol-http": {
+ "version": "5.3.12",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz",
+ "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/querystring-builder": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz",
+ "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-uri-escape": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/querystring-parser": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz",
+ "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/service-error-classification": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz",
+ "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/shared-ini-file-loader": {
+ "version": "4.4.7",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz",
+ "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/signature-v4": {
+ "version": "5.3.12",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz",
+ "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.2.2",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-hex-encoding": "^4.2.2",
+ "@smithy/util-middleware": "^4.2.12",
+ "@smithy/util-uri-escape": "^4.2.2",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/smithy-client": {
+ "version": "4.12.7",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz",
+ "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.23.12",
+ "@smithy/middleware-endpoint": "^4.4.27",
+ "@smithy/middleware-stack": "^4.2.12",
+ "@smithy/protocol-http": "^5.3.12",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-stream": "^4.5.20",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/types": {
+ "version": "4.13.1",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz",
+ "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/url-parser": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz",
+ "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/querystring-parser": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-base64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz",
+ "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.2.2",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-body-length-browser": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz",
+ "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-body-length-node": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz",
+ "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-buffer-from": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz",
+ "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-config-provider": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz",
+ "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-defaults-mode-browser": {
+ "version": "4.3.43",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz",
+ "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/smithy-client": "^4.12.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-defaults-mode-node": {
+ "version": "4.2.47",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz",
+ "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/config-resolver": "^4.4.13",
+ "@smithy/credential-provider-imds": "^4.2.12",
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/property-provider": "^4.2.12",
+ "@smithy/smithy-client": "^4.12.7",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-endpoints": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz",
+ "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-hex-encoding": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz",
+ "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-middleware": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz",
+ "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-retry": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz",
+ "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/service-error-classification": "^4.2.12",
+ "@smithy/types": "^4.13.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-stream": {
+ "version": "4.5.20",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz",
+ "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/fetch-http-handler": "^5.3.15",
+ "@smithy/node-http-handler": "^4.5.0",
+ "@smithy/types": "^4.13.1",
+ "@smithy/util-base64": "^4.3.2",
+ "@smithy/util-buffer-from": "^4.2.2",
+ "@smithy/util-hex-encoding": "^4.2.2",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-uri-escape": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz",
+ "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-utf8": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz",
+ "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -60,6 +1750,12 @@
"node": ">=18"
}
},
+ "node_modules/bowser": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
+ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
+ "license": "MIT"
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -373,6 +2069,41 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/fast-xml-builder": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
+ "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.1.3"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.5.6",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz",
+ "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "fast-xml-builder": "^1.1.4",
+ "path-expression-matcher": "^1.1.3",
+ "strnum": "^2.1.2"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -856,6 +2587,21 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-expression-matcher": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
+ "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
@@ -1113,6 +2859,18 @@
"node": ">= 0.8"
}
},
+ "node_modules/strnum": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz",
+ "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/tlds": {
"version": "1.259.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.259.0.tgz",
@@ -1131,6 +2889,12 @@
"node": ">=0.6"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json
index 5f553cf17..6972c7f95 100644
--- a/_reference/localEmailViewer/package.json
+++ b/_reference/localEmailViewer/package.json
@@ -4,13 +4,17 @@
"main": "index.js",
"type": "module",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "start": "node index.js",
+ "check": "node --check index.js && node --check public/client-app.js && node --check server/config.js && node --check server/localstack-service.js && node --check server/page.js"
},
"keywords": [],
"author": "",
"license": "ISC",
- "description": "",
+ "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",
"node-fetch": "^3.3.2"
diff --git a/_reference/localEmailViewer/public/client-app.js b/_reference/localEmailViewer/public/client-app.js
new file mode 100644
index 000000000..2e446c8fe
--- /dev/null
+++ b/_reference/localEmailViewer/public/client-app.js
@@ -0,0 +1,3154 @@
+function clientApp(config) {
+ const THEME_STORAGE_KEY = "localstack-inspector-theme";
+ const PANEL_STORAGE_KEY = "localstack-inspector-panel";
+ const EMAIL_PREFERENCES_STORAGE_KEY = "localstack-inspector-email-preferences";
+ const LOG_PREFERENCES_STORAGE_KEY = "localstack-inspector-log-preferences";
+ const SECRET_PREFERENCES_STORAGE_KEY = "localstack-inspector-secret-preferences";
+ const S3_PREFERENCES_STORAGE_KEY = "localstack-inspector-s3-preferences";
+ const HEALTH_REFRESH_MS = 30000;
+ const REFRESH_INTERVALS = [5000, 10000, 15000, 30000, 60000];
+ const LOG_WINDOWS = [300000, 900000, 3600000, 21600000, 86400000];
+ const LOG_LIMITS = [100, 200, 300, 500];
+ const storedEmailPreferences = getStoredPreferences(EMAIL_PREFERENCES_STORAGE_KEY);
+ const storedLogPreferences = getStoredPreferences(LOG_PREFERENCES_STORAGE_KEY);
+ const storedSecretPreferences = getStoredPreferences(SECRET_PREFERENCES_STORAGE_KEY);
+ const storedS3Preferences = getStoredPreferences(S3_PREFERENCES_STORAGE_KEY);
+
+ const appState = {
+ panel: getInitialPanel(),
+ logsReady: false,
+ secretsReady: false,
+ s3Ready: false,
+ theme: getInitialTheme()
+ };
+
+ const state = {
+ messages: [],
+ filtered: [],
+ search: getStoredText(storedEmailPreferences?.search),
+ auto: getStoredBoolean(storedEmailPreferences?.auto, true),
+ interval: getStoredNumber(storedEmailPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs),
+ loading: false,
+ error: "",
+ updatedAt: 0,
+ source: "initial",
+ duration: 0,
+ parseErrors: 0,
+ newest: "",
+ newIds: new Set(),
+ knownIds: new Set(),
+ openIds: new Set(),
+ views: {},
+ raw: {},
+ listSignature: ""
+ };
+
+ const logsState = {
+ groups: [],
+ streams: [],
+ events: [],
+ filtered: [],
+ group: getStoredText(storedLogPreferences?.group, config.defaultLogGroup || ""),
+ stream: getStoredText(storedLogPreferences?.stream),
+ search: getStoredText(storedLogPreferences?.search),
+ auto: getStoredBoolean(storedLogPreferences?.auto, true),
+ interval: getStoredNumber(storedLogPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs),
+ windowMs: getStoredNumber(storedLogPreferences?.windowMs, LOG_WINDOWS, config.defaultLogWindowMs),
+ limit: getStoredNumber(storedLogPreferences?.limit, LOG_LIMITS, config.defaultLogLimit),
+ wrapLines: getStoredBoolean(storedLogPreferences?.wrapLines, true),
+ tailNewest: getStoredBoolean(storedLogPreferences?.tailNewest, false),
+ loading: false,
+ error: "",
+ updatedAt: 0,
+ source: "initial",
+ duration: 0,
+ newest: 0,
+ searchedLogStreams: 0,
+ openIds: new Set(),
+ listSignature: ""
+ };
+
+ const secretsState = {
+ items: [],
+ filtered: [],
+ search: getStoredText(storedSecretPreferences?.search),
+ auto: getStoredBoolean(storedSecretPreferences?.auto, true),
+ interval: getStoredNumber(storedSecretPreferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs),
+ loading: false,
+ error: "",
+ updatedAt: 0,
+ source: "initial",
+ duration: 0,
+ newest: "",
+ openIds: new Set(),
+ values: {},
+ revealedIds: new Set(),
+ listSignature: ""
+ };
+
+ const s3State = {
+ buckets: [],
+ objects: [],
+ filtered: [],
+ bucket: getStoredText(storedS3Preferences?.bucket, config.defaultS3Bucket || ""),
+ prefix: getStoredText(storedS3Preferences?.prefix),
+ search: getStoredText(storedS3Preferences?.search),
+ auto: getStoredBoolean(storedS3Preferences?.auto, true),
+ interval: getStoredNumber(storedS3Preferences?.interval, REFRESH_INTERVALS, config.defaultRefreshMs),
+ loading: false,
+ error: "",
+ updatedAt: 0,
+ source: "initial",
+ duration: 0,
+ newest: "",
+ openIds: new Set(),
+ previews: {},
+ listSignature: ""
+ };
+
+ const healthState = {
+ services: {},
+ loading: false,
+ error: "",
+ updatedAt: 0,
+ source: "initial"
+ };
+
+ const el = {
+ themeToggle: document.getElementById("themeToggle"),
+ resetStateButton: document.getElementById("resetStateButton"),
+ healthRefreshButton: document.getElementById("healthRefreshButton"),
+ healthStrip: document.getElementById("healthStrip"),
+ emailsPanel: document.getElementById("emailsPanel"),
+ logsPanel: document.getElementById("logsPanel"),
+ secretsPanel: document.getElementById("secretsPanel"),
+ s3Panel: document.getElementById("s3Panel"),
+ refreshButton: document.getElementById("refreshButton"),
+ autoToggle: document.getElementById("autoToggle"),
+ intervalSelect: document.getElementById("intervalSelect"),
+ searchInput: document.getElementById("searchInput"),
+ clearSearchButton: document.getElementById("clearSearchButton"),
+ expandAllButton: document.getElementById("expandAllButton"),
+ collapseAllButton: document.getElementById("collapseAllButton"),
+ scrollToTopButton: document.getElementById("scrollToTopButton"),
+ statusChip: document.getElementById("statusChip"),
+ totalStat: document.getElementById("totalStat"),
+ visibleStat: document.getElementById("visibleStat"),
+ newStat: document.getElementById("newStat"),
+ newestStat: document.getElementById("newestStat"),
+ updatedStat: document.getElementById("updatedStat"),
+ fetchStat: document.getElementById("fetchStat"),
+ fetchDetail: document.getElementById("fetchDetail"),
+ banner: document.getElementById("banner"),
+ empty: document.getElementById("empty"),
+ list: document.getElementById("list"),
+ emailsContentPane: document.getElementById("emailsContentPane"),
+ logsRefreshButton: document.getElementById("logsRefreshButton"),
+ logsAutoToggle: document.getElementById("logsAutoToggle"),
+ logsIntervalSelect: document.getElementById("logsIntervalSelect"),
+ logsGroupSelect: document.getElementById("logsGroupSelect"),
+ logsStreamSelect: document.getElementById("logsStreamSelect"),
+ logsWindowSelect: document.getElementById("logsWindowSelect"),
+ logsLimitSelect: document.getElementById("logsLimitSelect"),
+ logsSearchInput: document.getElementById("logsSearchInput"),
+ logsClearSearchButton: document.getElementById("logsClearSearchButton"),
+ logsWrapToggle: document.getElementById("logsWrapToggle"),
+ logsTailToggle: document.getElementById("logsTailToggle"),
+ logsExpandAllButton: document.getElementById("logsExpandAllButton"),
+ logsCollapseAllButton: document.getElementById("logsCollapseAllButton"),
+ logsScrollToTopButton: document.getElementById("logsScrollToTopButton"),
+ logsStatusChip: document.getElementById("logsStatusChip"),
+ logsTotalStat: document.getElementById("logsTotalStat"),
+ logsVisibleStat: document.getElementById("logsVisibleStat"),
+ logsStreamsStat: document.getElementById("logsStreamsStat"),
+ logsNewestStat: document.getElementById("logsNewestStat"),
+ logsUpdatedStat: document.getElementById("logsUpdatedStat"),
+ logsFetchStat: document.getElementById("logsFetchStat"),
+ logsFetchDetail: document.getElementById("logsFetchDetail"),
+ logsBanner: document.getElementById("logsBanner"),
+ logsEmpty: document.getElementById("logsEmpty"),
+ logsList: document.getElementById("logsList"),
+ logsContentPane: document.getElementById("logsContentPane"),
+ secretsRefreshButton: document.getElementById("secretsRefreshButton"),
+ secretsAutoToggle: document.getElementById("secretsAutoToggle"),
+ secretsIntervalSelect: document.getElementById("secretsIntervalSelect"),
+ secretsSearchInput: document.getElementById("secretsSearchInput"),
+ secretsClearSearchButton: document.getElementById("secretsClearSearchButton"),
+ secretsExpandAllButton: document.getElementById("secretsExpandAllButton"),
+ secretsCollapseAllButton: document.getElementById("secretsCollapseAllButton"),
+ secretsScrollToTopButton: document.getElementById("secretsScrollToTopButton"),
+ secretsStatusChip: document.getElementById("secretsStatusChip"),
+ secretsTotalStat: document.getElementById("secretsTotalStat"),
+ secretsVisibleStat: document.getElementById("secretsVisibleStat"),
+ secretsLoadedStat: document.getElementById("secretsLoadedStat"),
+ secretsNewestStat: document.getElementById("secretsNewestStat"),
+ secretsUpdatedStat: document.getElementById("secretsUpdatedStat"),
+ secretsFetchStat: document.getElementById("secretsFetchStat"),
+ secretsFetchDetail: document.getElementById("secretsFetchDetail"),
+ secretsBanner: document.getElementById("secretsBanner"),
+ secretsEmpty: document.getElementById("secretsEmpty"),
+ secretsList: document.getElementById("secretsList"),
+ secretsContentPane: document.getElementById("secretsContentPane"),
+ s3RefreshButton: document.getElementById("s3RefreshButton"),
+ s3AutoToggle: document.getElementById("s3AutoToggle"),
+ s3IntervalSelect: document.getElementById("s3IntervalSelect"),
+ s3BucketSelect: document.getElementById("s3BucketSelect"),
+ s3PrefixInput: document.getElementById("s3PrefixInput"),
+ s3ApplyPrefixButton: document.getElementById("s3ApplyPrefixButton"),
+ s3SearchInput: document.getElementById("s3SearchInput"),
+ s3ClearSearchButton: document.getElementById("s3ClearSearchButton"),
+ s3ExpandAllButton: document.getElementById("s3ExpandAllButton"),
+ s3CollapseAllButton: document.getElementById("s3CollapseAllButton"),
+ s3ScrollToTopButton: document.getElementById("s3ScrollToTopButton"),
+ s3StatusChip: document.getElementById("s3StatusChip"),
+ s3TotalStat: document.getElementById("s3TotalStat"),
+ s3VisibleStat: document.getElementById("s3VisibleStat"),
+ s3BucketsStat: document.getElementById("s3BucketsStat"),
+ s3NewestStat: document.getElementById("s3NewestStat"),
+ s3UpdatedStat: document.getElementById("s3UpdatedStat"),
+ s3FetchStat: document.getElementById("s3FetchStat"),
+ s3FetchDetail: document.getElementById("s3FetchDetail"),
+ s3Banner: document.getElementById("s3Banner"),
+ s3Empty: document.getElementById("s3Empty"),
+ s3List: document.getElementById("s3List"),
+ s3ContentPane: document.getElementById("s3ContentPane")
+ };
+
+ el.autoToggle.checked = state.auto;
+ el.intervalSelect.value = String(state.interval);
+ el.searchInput.value = state.search;
+ el.logsAutoToggle.checked = logsState.auto;
+ el.logsIntervalSelect.value = String(logsState.interval);
+ el.logsWindowSelect.value = String(logsState.windowMs);
+ el.logsLimitSelect.value = String(logsState.limit);
+ el.logsSearchInput.value = logsState.search;
+ el.logsWrapToggle.checked = logsState.wrapLines;
+ el.logsTailToggle.checked = logsState.tailNewest;
+ el.secretsAutoToggle.checked = secretsState.auto;
+ el.secretsIntervalSelect.value = String(secretsState.interval);
+ el.secretsSearchInput.value = secretsState.search;
+ el.s3AutoToggle.checked = s3State.auto;
+ el.s3IntervalSelect.value = String(s3State.interval);
+ el.s3PrefixInput.value = s3State.prefix;
+ el.s3SearchInput.value = s3State.search;
+ applyTheme(appState.theme);
+ persistPanel();
+ persistEmailPreferences();
+ persistLogPreferences();
+ persistSecretPreferences();
+ persistS3Preferences();
+ wire();
+ renderWorkspace();
+ renderAll();
+ renderLogsAll();
+ renderSecretsAll();
+ renderS3All();
+ renderHealthStrip();
+ if (appState.panel === "logs") {
+ ensureLogsReady();
+ } else if (appState.panel === "secrets") {
+ ensureSecretsReady();
+ } else if (appState.panel === "s3") {
+ ensureS3Ready();
+ } else {
+ refreshMessages("initial");
+ }
+ refreshHealthSummary("initial");
+ window.setInterval(() => {
+ if (!document.hidden) {
+ refreshHealthSummary("auto");
+ }
+ }, HEALTH_REFRESH_MS);
+ window.setInterval(() => {
+ renderLiveClock();
+ renderLogsLiveClock();
+ renderSecretsLiveClock();
+ renderS3LiveClock();
+ renderHealthStrip();
+ }, 1000);
+
+ function wire() {
+ el.themeToggle.addEventListener("click", () => {
+ applyTheme(appState.theme === "dark" ? "light" : "dark");
+ });
+
+ el.resetStateButton.addEventListener("click", () => {
+ resetSavedState();
+ });
+
+ el.healthRefreshButton.addEventListener("click", () => {
+ refreshHealthSummary("manual");
+ });
+
+ el.healthStrip.addEventListener("click", async (event) => {
+ const button = event.target.closest("button[data-health-panel]");
+
+ if (!button) {
+ return;
+ }
+
+ await setPanel(button.dataset.healthPanel);
+ });
+
+ el.refreshButton.addEventListener("click", () => refreshMessages("manual"));
+
+ el.autoToggle.addEventListener("change", () => {
+ state.auto = el.autoToggle.checked;
+ persistEmailPreferences();
+ scheduleRefresh();
+ renderStatus();
+ });
+
+ el.intervalSelect.addEventListener("change", () => {
+ state.interval = Number(el.intervalSelect.value) || config.defaultRefreshMs;
+ persistEmailPreferences();
+ scheduleRefresh();
+ renderStatus();
+ });
+
+ el.searchInput.addEventListener("input", (event) => {
+ state.search = event.target.value;
+ applyFilter();
+ });
+
+ el.clearSearchButton.addEventListener("click", () => {
+ state.search = "";
+ el.searchInput.value = "";
+ applyFilter();
+ });
+
+ el.expandAllButton.addEventListener("click", () => {
+ state.filtered.forEach((message) => state.openIds.add(message.id));
+ syncCardExpansion();
+ });
+
+ el.collapseAllButton.addEventListener("click", () => {
+ state.openIds.clear();
+ syncCardExpansion();
+ });
+
+ el.scrollToTopButton.addEventListener("click", () => {
+ scrollPaneToTop(el.emailsContentPane);
+ });
+
+ el.emailsContentPane.addEventListener("scroll", () => {
+ updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton);
+ });
+
+ el.list.addEventListener("click", async (event) => {
+ const button = event.target.closest("button[data-action]");
+
+ if (!button) {
+ return;
+ }
+
+ const id = button.dataset.id;
+ const message = getMessage(id);
+
+ if (!message) {
+ return;
+ }
+
+ if (button.dataset.action === "view") {
+ state.views[id] = button.dataset.view;
+ renderList();
+ return;
+ }
+
+ if (button.dataset.action === "load-raw") {
+ await loadRaw(id);
+ renderList();
+ return;
+ }
+
+ if (button.dataset.action === "copy-raw") {
+ const raw = await loadRaw(id);
+
+ if (raw) {
+ await copyText(raw);
+ setStatus("Raw message copied to the clipboard.", "ok");
+ }
+ }
+ });
+
+ el.logsRefreshButton.addEventListener("click", () => refreshLogs("manual"));
+
+ el.logsAutoToggle.addEventListener("change", () => {
+ logsState.auto = el.logsAutoToggle.checked;
+ persistLogPreferences();
+ scheduleLogsRefresh();
+ renderLogsStatus();
+ });
+
+ el.logsIntervalSelect.addEventListener("change", () => {
+ logsState.interval = Number(el.logsIntervalSelect.value) || config.defaultRefreshMs;
+ persistLogPreferences();
+ scheduleLogsRefresh();
+ renderLogsStatus();
+ });
+
+ el.logsWindowSelect.addEventListener("change", () => {
+ logsState.windowMs = Number(el.logsWindowSelect.value) || config.defaultLogWindowMs;
+ persistLogPreferences();
+ refreshLogs("window");
+ });
+
+ el.logsLimitSelect.addEventListener("change", () => {
+ logsState.limit = Number(el.logsLimitSelect.value) || config.defaultLogLimit;
+ persistLogPreferences();
+ refreshLogs("limit");
+ });
+
+ el.logsSearchInput.addEventListener("input", (event) => {
+ logsState.search = event.target.value;
+ applyLogsFilter();
+ });
+
+ el.logsClearSearchButton.addEventListener("click", () => {
+ logsState.search = "";
+ el.logsSearchInput.value = "";
+ applyLogsFilter();
+ });
+
+ el.logsWrapToggle.addEventListener("change", () => {
+ logsState.wrapLines = el.logsWrapToggle.checked;
+ persistLogPreferences();
+ renderLogsList();
+ });
+
+ el.logsTailToggle.addEventListener("change", () => {
+ logsState.tailNewest = el.logsTailToggle.checked;
+ persistLogPreferences();
+ });
+
+ el.logsExpandAllButton.addEventListener("click", () => {
+ logsState.filtered.forEach((event) => logsState.openIds.add(event.id));
+ renderLogsList();
+ });
+
+ el.logsCollapseAllButton.addEventListener("click", () => {
+ logsState.openIds.clear();
+ renderLogsList();
+ });
+
+ el.logsScrollToTopButton.addEventListener("click", () => {
+ scrollPaneToTop(el.logsContentPane);
+ });
+
+ el.logsContentPane.addEventListener("scroll", () => {
+ updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton);
+ });
+
+ el.logsGroupSelect.addEventListener("change", async () => {
+ logsState.group = el.logsGroupSelect.value;
+ logsState.stream = "";
+ persistLogPreferences();
+ await refreshLogStreams();
+ await refreshLogs("group");
+ });
+
+ el.logsStreamSelect.addEventListener("change", () => {
+ logsState.stream = el.logsStreamSelect.value;
+ persistLogPreferences();
+ refreshLogs("stream");
+ });
+
+ el.logsList.addEventListener("click", async (event) => {
+ const button = event.target.closest("button[data-log-action]");
+
+ if (!button) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ const logEvent = getLogEvent(button.dataset.id);
+
+ if (!logEvent) {
+ return;
+ }
+
+ if (button.dataset.logAction === "copy") {
+ await copyText(formatLogMessage(logEvent.message));
+ setLogsStatus("Log payload copied to the clipboard.", "ok");
+ }
+ });
+
+ el.secretsRefreshButton.addEventListener("click", () => refreshSecrets("manual"));
+
+ el.secretsAutoToggle.addEventListener("change", () => {
+ secretsState.auto = el.secretsAutoToggle.checked;
+ persistSecretPreferences();
+ scheduleSecretsRefresh();
+ renderSecretsStatus();
+ });
+
+ el.secretsIntervalSelect.addEventListener("change", () => {
+ secretsState.interval = Number(el.secretsIntervalSelect.value) || config.defaultRefreshMs;
+ persistSecretPreferences();
+ scheduleSecretsRefresh();
+ renderSecretsStatus();
+ });
+
+ el.secretsSearchInput.addEventListener("input", (event) => {
+ secretsState.search = event.target.value;
+ applySecretsFilter();
+ });
+
+ el.secretsClearSearchButton.addEventListener("click", () => {
+ secretsState.search = "";
+ el.secretsSearchInput.value = "";
+ applySecretsFilter();
+ });
+
+ el.secretsExpandAllButton.addEventListener("click", () => {
+ secretsState.filtered.forEach((secret) => secretsState.openIds.add(secret.id));
+ syncSecretExpansion();
+ });
+
+ el.secretsCollapseAllButton.addEventListener("click", () => {
+ secretsState.openIds.clear();
+ syncSecretExpansion();
+ });
+
+ el.secretsScrollToTopButton.addEventListener("click", () => {
+ scrollPaneToTop(el.secretsContentPane);
+ });
+
+ el.secretsContentPane.addEventListener("scroll", () => {
+ updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton);
+ });
+
+ el.secretsList.addEventListener("click", async (event) => {
+ const button = event.target.closest("button[data-secret-action]");
+
+ if (!button) {
+ return;
+ }
+
+ const id = button.dataset.id;
+ const secret = getSecret(id);
+
+ if (!secret) {
+ return;
+ }
+
+ if (button.dataset.secretAction === "load-value") {
+ await ensureSecretValue(id, { force: false });
+ return;
+ }
+
+ if (button.dataset.secretAction === "reload-value") {
+ await ensureSecretValue(id, { force: true });
+ return;
+ }
+
+ if (button.dataset.secretAction === "copy-value") {
+ const entry = await ensureSecretValue(id, { force: false });
+
+ if (entry?.status === "loaded") {
+ await copyText(entry.copyValue);
+ setSecretsStatus("Secret value copied to the clipboard.", "ok");
+ }
+
+ return;
+ }
+
+ if (button.dataset.secretAction === "toggle-reveal") {
+ toggleSecretReveal(id);
+ return;
+ }
+
+ if (button.dataset.secretAction === "copy-name") {
+ await copyText(secret.name || "");
+ setSecretsStatus("Secret name copied to the clipboard.", "ok");
+ return;
+ }
+
+ if (button.dataset.secretAction === "copy-arn") {
+ await copyText(secret.arn || "");
+ setSecretsStatus("Secret ARN copied to the clipboard.", "ok");
+ return;
+ }
+
+ if (button.dataset.secretAction === "copy-env") {
+ const entry = await ensureSecretValue(id, { force: false });
+
+ if (entry?.status === "loaded") {
+ await copyText(`${secret.name}=${entry.copyValue}`);
+ setSecretsStatus("Secret env line copied to the clipboard.", "ok");
+ }
+ }
+ });
+
+ el.s3RefreshButton.addEventListener("click", async () => {
+ await refreshS3Buckets();
+ await refreshS3("manual");
+ });
+
+ el.s3AutoToggle.addEventListener("change", () => {
+ s3State.auto = el.s3AutoToggle.checked;
+ persistS3Preferences();
+ scheduleS3Refresh();
+ renderS3Status();
+ });
+
+ el.s3IntervalSelect.addEventListener("change", () => {
+ s3State.interval = Number(el.s3IntervalSelect.value) || config.defaultRefreshMs;
+ persistS3Preferences();
+ scheduleS3Refresh();
+ renderS3Status();
+ });
+
+ el.s3BucketSelect.addEventListener("change", () => {
+ s3State.bucket = el.s3BucketSelect.value;
+ persistS3Preferences();
+ refreshS3("bucket");
+ });
+
+ el.s3PrefixInput.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ s3State.prefix = el.s3PrefixInput.value.trim();
+ persistS3Preferences();
+ refreshS3("prefix");
+ }
+ });
+
+ el.s3ApplyPrefixButton.addEventListener("click", () => {
+ s3State.prefix = el.s3PrefixInput.value.trim();
+ persistS3Preferences();
+ refreshS3("prefix");
+ });
+
+ el.s3SearchInput.addEventListener("input", (event) => {
+ s3State.search = event.target.value;
+ applyS3Filter();
+ });
+
+ el.s3ClearSearchButton.addEventListener("click", () => {
+ s3State.search = "";
+ el.s3SearchInput.value = "";
+ applyS3Filter();
+ });
+
+ el.s3ExpandAllButton.addEventListener("click", () => {
+ s3State.filtered.forEach((object) => s3State.openIds.add(object.id));
+ syncS3Expansion();
+ });
+
+ el.s3CollapseAllButton.addEventListener("click", () => {
+ s3State.openIds.clear();
+ syncS3Expansion();
+ });
+
+ el.s3ScrollToTopButton.addEventListener("click", () => {
+ scrollPaneToTop(el.s3ContentPane);
+ });
+
+ el.s3ContentPane.addEventListener("scroll", () => {
+ updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton);
+ });
+
+ el.s3List.addEventListener("click", async (event) => {
+ const button = event.target.closest("button[data-s3-action]");
+
+ if (!button) {
+ return;
+ }
+
+ const id = button.dataset.id;
+ const object = getS3Object(id);
+
+ if (!object) {
+ return;
+ }
+
+ if (button.dataset.s3Action === "load-preview") {
+ await ensureS3Preview(id, { force: false });
+ return;
+ }
+
+ if (button.dataset.s3Action === "reload-preview") {
+ await ensureS3Preview(id, { force: true });
+ return;
+ }
+
+ if (button.dataset.s3Action === "copy-key") {
+ await copyText(object.key);
+ setS3Status("Object key copied to the clipboard.", "ok");
+ return;
+ }
+
+ if (button.dataset.s3Action === "copy-uri") {
+ await copyText(`s3://${object.bucket}/${object.key}`);
+ setS3Status("S3 URI copied to the clipboard.", "ok");
+ }
+ });
+
+ document.addEventListener("visibilitychange", () => {
+ window.clearTimeout(state.timer);
+ window.clearTimeout(logsState.timer);
+ window.clearTimeout(secretsState.timer);
+ window.clearTimeout(s3State.timer);
+
+ if (document.hidden) {
+ renderStatus();
+ renderLogsStatus();
+ renderSecretsStatus();
+ renderS3Status();
+ return;
+ }
+
+ refreshHealthSummary("visibility");
+
+ if (appState.panel === "s3") {
+ if (s3State.auto) {
+ refreshS3("visibility");
+ } else {
+ renderS3Status();
+ }
+ return;
+ }
+
+ if (appState.panel === "secrets") {
+ if (secretsState.auto) {
+ refreshSecrets("visibility");
+ } else {
+ renderSecretsStatus();
+ }
+ return;
+ }
+
+ if (appState.panel === "logs") {
+ if (logsState.auto) {
+ refreshLogs("visibility");
+ } else {
+ renderLogsStatus();
+ }
+ return;
+ }
+
+ if (state.auto) {
+ refreshMessages("visibility");
+ } else {
+ renderStatus();
+ }
+ });
+
+ window.addEventListener("keydown", (event) => {
+ const isField =
+ event.target instanceof HTMLElement &&
+ (event.target.matches("input,textarea,select") || event.target.isContentEditable);
+
+ if (!isField && event.key.toLowerCase() === "r") {
+ event.preventDefault();
+
+ if (appState.panel === "logs") {
+ refreshLogs("keyboard");
+ return;
+ }
+
+ if (appState.panel === "secrets") {
+ refreshSecrets("keyboard");
+ return;
+ }
+
+ if (appState.panel === "s3") {
+ refreshS3("keyboard");
+ return;
+ }
+
+ refreshMessages("keyboard");
+ }
+ });
+
+ updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton);
+ updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton);
+ updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton);
+ updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton);
+ }
+
+ async function setPanel(panel) {
+ if (!panel || panel === appState.panel) {
+ if (panel === "logs") {
+ await ensureLogsReady();
+ } else if (panel === "secrets") {
+ await ensureSecretsReady();
+ } else if (panel === "s3") {
+ await ensureS3Ready();
+ }
+
+ renderWorkspace();
+ return;
+ }
+
+ appState.panel = panel;
+ persistPanel();
+ renderWorkspace();
+
+ if (panel === "logs") {
+ await ensureLogsReady();
+ } else if (panel === "secrets") {
+ await ensureSecretsReady();
+ } else if (panel === "s3") {
+ await ensureS3Ready();
+ } else if (!state.updatedAt && !state.loading) {
+ await refreshMessages("panel");
+ }
+
+ scheduleRefresh();
+ scheduleLogsRefresh();
+ scheduleSecretsRefresh();
+ scheduleS3Refresh();
+ renderStatus();
+ renderLogsStatus();
+ renderSecretsStatus();
+ renderS3Status();
+ }
+
+ function renderWorkspace() {
+ el.emailsPanel.hidden = appState.panel !== "emails";
+ el.logsPanel.hidden = appState.panel !== "logs";
+ el.secretsPanel.hidden = appState.panel !== "secrets";
+ el.s3Panel.hidden = appState.panel !== "s3";
+ renderHealthStrip();
+ }
+
+ async function ensureLogsReady() {
+ if (appState.logsReady) {
+ return;
+ }
+
+ await refreshLogGroups();
+ appState.logsReady = !logsState.error;
+
+ if (logsState.group) {
+ await refreshLogs("initial");
+ }
+ }
+
+ async function ensureSecretsReady() {
+ if (appState.secretsReady) {
+ return;
+ }
+
+ await refreshSecrets("initial");
+ appState.secretsReady = !secretsState.error;
+ }
+
+ async function ensureS3Ready() {
+ if (appState.s3Ready) {
+ return;
+ }
+
+ await refreshS3Buckets();
+ appState.s3Ready = !s3State.error;
+
+ if (s3State.bucket) {
+ await refreshS3("initial");
+ }
+ }
+
+ async function refreshMessages(source) {
+ if (state.loading) {
+ return;
+ }
+
+ let shouldRenderList = false;
+
+ state.loading = true;
+ state.source = source;
+ state.error = "";
+ renderStatus();
+ renderFetch();
+
+ try {
+ const response = await fetch("/api/messages", { 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 messages = Array.isArray(payload.messages) ? payload.messages : [];
+ const nextIds = new Set(messages.map((message) => message.id));
+ const nextSignature = computeListSignature(messages);
+
+ shouldRenderList = nextSignature !== state.listSignature;
+ state.newIds =
+ state.updatedAt && shouldRenderList
+ ? new Set(messages.filter((message) => !state.knownIds.has(message.id)).map((message) => message.id))
+ : state.newIds;
+ state.knownIds = nextIds;
+ state.messages = messages;
+ state.duration = payload.fetchDurationMs || 0;
+ state.parseErrors = payload.parseErrors || 0;
+ state.newest = payload.latestMessageTimestamp || "";
+ state.updatedAt = Date.now();
+ state.listSignature = nextSignature;
+
+ pruneState();
+ applyFilter(shouldRenderList);
+ setStatus(`Updated ${messages.length} message${messages.length === 1 ? "" : "s"}.`, "ok");
+ } catch (error) {
+ state.error = error.message || "Unknown refresh error";
+ setStatus(`Refresh failed: ${state.error}`, "bad");
+ } finally {
+ state.loading = false;
+ scheduleRefresh();
+ renderAll({ renderList: shouldRenderList });
+ }
+ }
+
+ async function refreshLogGroups() {
+ try {
+ const response = await fetch("/api/logs/groups", { 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();
+ logsState.groups = Array.isArray(payload.groups) ? payload.groups : [];
+
+ const availableGroups = logsState.groups.map((group) => group.name).filter(Boolean);
+
+ if (!availableGroups.includes(logsState.group)) {
+ logsState.group = availableGroups.includes(config.defaultLogGroup)
+ ? config.defaultLogGroup
+ : availableGroups[0] || "";
+ }
+
+ logsState.error = "";
+ await refreshLogStreams();
+ } catch (error) {
+ logsState.error = error.message || "Unknown log group refresh error";
+ } finally {
+ persistLogPreferences();
+ renderLogsAll();
+ }
+ }
+
+ async function refreshLogStreams() {
+ if (!logsState.group) {
+ logsState.streams = [];
+ logsState.stream = "";
+ renderLogsAll();
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/logs/streams?group=${encodeURIComponent(logsState.group)}`, {
+ 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();
+ logsState.streams = Array.isArray(payload.streams) ? payload.streams : [];
+
+ if (!logsState.streams.some((stream) => stream.name === logsState.stream)) {
+ logsState.stream = "";
+ }
+
+ logsState.error = "";
+ } catch (error) {
+ logsState.streams = [];
+ logsState.stream = "";
+ logsState.error = error.message || "Unknown log stream refresh error";
+ } finally {
+ persistLogPreferences();
+ renderLogsAll();
+ }
+ }
+
+ async function refreshLogs(source) {
+ if (logsState.loading) {
+ return;
+ }
+
+ if (!appState.logsReady) {
+ await ensureLogsReady();
+ return;
+ }
+
+ if (!logsState.group) {
+ logsState.error = logsState.groups.length ? "Choose a log group to load events." : "No log groups found.";
+ renderLogsAll();
+ return;
+ }
+
+ let shouldRenderList = false;
+
+ logsState.loading = true;
+ logsState.source = source;
+ logsState.error = "";
+ renderLogsStatus();
+ renderLogsFetch();
+
+ try {
+ const params = new URLSearchParams({
+ group: logsState.group,
+ windowMs: String(logsState.windowMs),
+ limit: String(logsState.limit)
+ });
+
+ if (logsState.stream) {
+ params.set("stream", logsState.stream);
+ }
+
+ const response = await fetch(`/api/logs/events?${params.toString()}`, { 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 events = Array.isArray(payload.events) ? payload.events : [];
+ const nextSignature = computeLogListSignature(events);
+ shouldRenderList = nextSignature !== logsState.listSignature;
+ logsState.events = events;
+ logsState.duration = payload.fetchDurationMs || 0;
+ logsState.newest = payload.latestTimestamp || 0;
+ logsState.updatedAt = Date.now();
+ logsState.searchedLogStreams = payload.searchedLogStreams || (logsState.stream ? 1 : logsState.streams.length);
+ logsState.listSignature = nextSignature;
+
+ pruneLogsState();
+ applyLogsFilter(shouldRenderList);
+ setLogsStatus(`Updated ${logsState.events.length} log event${logsState.events.length === 1 ? "" : "s"}.`, "ok");
+ } catch (error) {
+ logsState.error = error.message || "Unknown log refresh error";
+ setLogsStatus(`Refresh failed: ${logsState.error}`, "bad");
+ } finally {
+ logsState.loading = false;
+ scheduleLogsRefresh();
+ renderLogsAll({ renderList: shouldRenderList });
+
+ if (shouldRenderList && logsState.tailNewest) {
+ scrollPaneToTop(el.logsContentPane);
+ }
+ }
+ }
+
+ async function refreshSecrets(source) {
+ if (secretsState.loading) {
+ return;
+ }
+
+ let shouldRenderList = false;
+
+ secretsState.loading = true;
+ secretsState.source = source;
+ secretsState.error = "";
+ renderSecretsStatus();
+ renderSecretsFetch();
+
+ try {
+ const response = await fetch("/api/secrets", { 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 items = Array.isArray(payload.secrets) ? payload.secrets : [];
+ const nextSignature = computeSecretsListSignature(items);
+ shouldRenderList = nextSignature !== secretsState.listSignature;
+ secretsState.items = items;
+ secretsState.duration = payload.fetchDurationMs || 0;
+ secretsState.newest = payload.latestTimestamp || "";
+ secretsState.updatedAt = Date.now();
+ secretsState.listSignature = nextSignature;
+
+ pruneSecretsState();
+ applySecretsFilter(shouldRenderList);
+ appState.secretsReady = true;
+ setSecretsStatus(`Updated ${items.length} secret${items.length === 1 ? "" : "s"}.`, "ok");
+ } catch (error) {
+ secretsState.error = error.message || "Unknown secrets refresh error";
+ appState.secretsReady = false;
+ setSecretsStatus(`Refresh failed: ${secretsState.error}`, "bad");
+ } finally {
+ secretsState.loading = false;
+ scheduleSecretsRefresh();
+ renderSecretsAll({ renderList: shouldRenderList });
+ }
+ }
+
+ async function refreshHealthSummary(source) {
+ if (healthState.loading && source === "auto") {
+ return;
+ }
+
+ healthState.loading = true;
+ healthState.source = source;
+ renderHealthStrip();
+
+ try {
+ const response = await fetch("/api/service-health", { 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();
+ healthState.services = payload.services || {};
+ healthState.updatedAt = Date.now();
+ healthState.error = "";
+ } catch (error) {
+ healthState.error = error.message || "Unknown health refresh error";
+ } finally {
+ healthState.loading = false;
+ renderHealthStrip();
+ }
+ }
+
+ async function refreshS3Buckets() {
+ try {
+ const response = await fetch("/api/s3/buckets", { 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();
+ s3State.buckets = Array.isArray(payload.buckets) ? payload.buckets : [];
+
+ const availableBuckets = s3State.buckets.map((bucket) => bucket.name).filter(Boolean);
+
+ if (!availableBuckets.includes(s3State.bucket)) {
+ s3State.bucket = availableBuckets.includes(config.defaultS3Bucket)
+ ? config.defaultS3Bucket
+ : availableBuckets[0] || "";
+ }
+
+ s3State.error = "";
+ } catch (error) {
+ s3State.buckets = [];
+ s3State.bucket = "";
+ s3State.error = error.message || "Unknown S3 bucket refresh error";
+ } finally {
+ appState.s3Ready = !s3State.error;
+ persistS3Preferences();
+ renderS3All();
+ }
+ }
+
+ async function refreshS3(source) {
+ if (s3State.loading) {
+ return;
+ }
+
+ if (!appState.s3Ready) {
+ await ensureS3Ready();
+ return;
+ }
+
+ if (!s3State.bucket) {
+ s3State.error = s3State.buckets.length ? "Choose a bucket to load objects." : "No S3 buckets found.";
+ renderS3All();
+ return;
+ }
+
+ let shouldRenderList = false;
+
+ s3State.loading = true;
+ s3State.source = source;
+ s3State.error = "";
+ renderS3Status();
+ renderS3Fetch();
+
+ try {
+ const params = new URLSearchParams({
+ bucket: s3State.bucket
+ });
+
+ if (s3State.prefix) {
+ params.set("prefix", s3State.prefix);
+ }
+
+ const response = await fetch(`/api/s3/objects?${params.toString()}`, { 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 objects = Array.isArray(payload.objects) ? payload.objects : [];
+ const nextSignature = computeS3ListSignature(objects);
+ shouldRenderList = nextSignature !== s3State.listSignature;
+ s3State.objects = objects;
+ s3State.duration = payload.fetchDurationMs || 0;
+ s3State.newest = payload.latestTimestamp || "";
+ s3State.updatedAt = Date.now();
+ s3State.listSignature = nextSignature;
+
+ pruneS3State();
+ applyS3Filter(shouldRenderList);
+ appState.s3Ready = true;
+ setS3Status(`Updated ${objects.length} object${objects.length === 1 ? "" : "s"}.`, "ok");
+ } catch (error) {
+ s3State.error = error.message || "Unknown S3 refresh error";
+ appState.s3Ready = false;
+ setS3Status(`Refresh failed: ${s3State.error}`, "bad");
+ } finally {
+ s3State.loading = false;
+ scheduleS3Refresh();
+ renderS3All({ renderList: shouldRenderList });
+ }
+ }
+
+ function applyLogsFilter(shouldRenderList = true) {
+ const search = logsState.search.trim().toLowerCase();
+ logsState.filtered = !search
+ ? [...logsState.events]
+ : logsState.events.filter((event) => logHaystack(event).includes(search));
+ persistLogPreferences();
+ renderLogsAll({ renderList: shouldRenderList });
+ }
+
+ function applySecretsFilter(shouldRenderList = true) {
+ const search = secretsState.search.trim().toLowerCase();
+ secretsState.filtered = !search
+ ? [...secretsState.items]
+ : secretsState.items.filter((secret) => secretHaystack(secret).includes(search));
+ persistSecretPreferences();
+ renderSecretsAll({ renderList: shouldRenderList });
+ }
+
+ function applyS3Filter(shouldRenderList = true) {
+ const search = s3State.search.trim().toLowerCase();
+ s3State.filtered = !search
+ ? [...s3State.objects]
+ : s3State.objects.filter((object) => s3Haystack(object).includes(search));
+ persistS3Preferences();
+ renderS3All({ renderList: shouldRenderList });
+ }
+
+ function pruneState() {
+ const ids = new Set(state.messages.map((message) => message.id));
+ state.openIds = new Set([...state.openIds].filter((id) => ids.has(id)));
+ state.newIds = new Set([...state.newIds].filter((id) => ids.has(id)));
+
+ Object.keys(state.views).forEach((id) => {
+ if (!ids.has(id)) {
+ delete state.views[id];
+ }
+ });
+
+ Object.keys(state.raw).forEach((id) => {
+ if (!ids.has(id)) {
+ delete state.raw[id];
+ }
+ });
+ }
+
+ function pruneLogsState() {
+ const ids = new Set(logsState.events.map((event) => event.id));
+ logsState.openIds = new Set([...logsState.openIds].filter((id) => ids.has(id)));
+ }
+
+ function pruneSecretsState() {
+ const ids = new Set(secretsState.items.map((secret) => secret.id));
+ secretsState.openIds = new Set([...secretsState.openIds].filter((id) => ids.has(id)));
+
+ Object.keys(secretsState.values).forEach((id) => {
+ if (!ids.has(id)) {
+ delete secretsState.values[id];
+ }
+ });
+
+ secretsState.revealedIds = new Set([...secretsState.revealedIds].filter((id) => ids.has(id)));
+ }
+
+ function pruneS3State() {
+ const ids = new Set(s3State.objects.map((object) => object.id));
+ s3State.openIds = new Set([...s3State.openIds].filter((id) => ids.has(id)));
+
+ Object.keys(s3State.previews).forEach((id) => {
+ if (!ids.has(id)) {
+ delete s3State.previews[id];
+ }
+ });
+ }
+
+ function applyFilter(shouldRenderList = true) {
+ const search = state.search.trim().toLowerCase();
+ state.filtered = !search
+ ? [...state.messages]
+ : state.messages.filter((message) => haystack(message).includes(search));
+ persistEmailPreferences();
+ renderAll({ renderList: shouldRenderList });
+ }
+
+ function computeListSignature(messages) {
+ return messages
+ .map((message) =>
+ [
+ message.id,
+ message.timestampMs || 0,
+ message.rawSizeBytes || 0,
+ message.attachmentCount || 0,
+ message.hasHtml ? 1 : 0,
+ message.preview || "",
+ message.parseError || ""
+ ].join("::")
+ )
+ .join("|");
+ }
+
+ function computeLogListSignature(events) {
+ return events
+ .map((event) =>
+ [event.id, event.timestamp || 0, event.ingestionTime || 0, event.logStreamName || "", event.message || ""].join(
+ "::"
+ )
+ )
+ .join("|");
+ }
+
+ function computeSecretsListSignature(items) {
+ return items
+ .map((secret) =>
+ [
+ secret.id,
+ secret.name || "",
+ secret.arn || "",
+ secret.description || "",
+ secret.lastChangedDate || "",
+ secret.createdDate || "",
+ secret.rotationEnabled ? 1 : 0,
+ secret.owningService || "",
+ secret.primaryRegion || "",
+ secret.versionCount || 0,
+ secret.tagCount || 0
+ ].join("::")
+ )
+ .join("|");
+ }
+
+ function computeS3ListSignature(objects) {
+ return objects
+ .map((object) =>
+ [object.id, object.key || "", object.size || 0, object.lastModified || "", object.etag || ""].join("::")
+ )
+ .join("|");
+ }
+
+ function haystack(message) {
+ return [
+ message.subject,
+ message.from,
+ message.to,
+ message.replyTo,
+ message.preview,
+ message.textContent,
+ message.region,
+ ...(message.attachments || []).flatMap((attachment) => [attachment.filename, attachment.contentType])
+ ]
+ .filter(Boolean)
+ .join(" ")
+ .toLowerCase();
+ }
+
+ function logHaystack(event) {
+ return [event.logStreamName, event.message, event.preview].filter(Boolean).join(" ").toLowerCase();
+ }
+
+ function secretHaystack(secret) {
+ return [
+ secret.name,
+ secret.arn,
+ secret.description,
+ secret.primaryRegion,
+ secret.owningService,
+ ...(secret.tags || []).flatMap((tag) => [tag.key, tag.value])
+ ]
+ .filter(Boolean)
+ .join(" ")
+ .toLowerCase();
+ }
+
+ function s3Haystack(object) {
+ return [object.bucket, object.key, object.storageClass, object.etag].filter(Boolean).join(" ").toLowerCase();
+ }
+
+ function scheduleRefresh() {
+ window.clearTimeout(state.timer);
+
+ if (appState.panel !== "emails" || !state.auto || document.hidden || state.loading) {
+ return;
+ }
+
+ state.timer = window.setTimeout(() => refreshMessages("auto"), state.interval);
+ }
+
+ function scheduleLogsRefresh() {
+ window.clearTimeout(logsState.timer);
+
+ if (appState.panel !== "logs" || !logsState.auto || document.hidden || logsState.loading) {
+ return;
+ }
+
+ logsState.timer = window.setTimeout(() => refreshLogs("auto"), logsState.interval);
+ }
+
+ function scheduleSecretsRefresh() {
+ window.clearTimeout(secretsState.timer);
+
+ if (appState.panel !== "secrets" || !secretsState.auto || document.hidden || secretsState.loading) {
+ return;
+ }
+
+ secretsState.timer = window.setTimeout(() => refreshSecrets("auto"), secretsState.interval);
+ }
+
+ function scheduleS3Refresh() {
+ window.clearTimeout(s3State.timer);
+
+ if (appState.panel !== "s3" || !s3State.auto || document.hidden || s3State.loading) {
+ return;
+ }
+
+ s3State.timer = window.setTimeout(() => refreshS3("auto"), s3State.interval);
+ }
+
+ function renderAll(options = {}) {
+ const { renderList: shouldRenderList = true } = options;
+ renderStats();
+ renderFetch();
+ renderStatus();
+ if (shouldRenderList) {
+ renderList();
+ }
+ renderLiveClock();
+ }
+
+ function renderLogsAll(options = {}) {
+ const { renderList: shouldRenderList = true } = options;
+ renderLogsFilters();
+ renderLogsStats();
+ renderLogsFetch();
+ renderLogsStatus();
+ if (shouldRenderList) {
+ renderLogsList();
+ }
+ renderLogsLiveClock();
+ }
+
+ function renderSecretsAll(options = {}) {
+ const { renderList: shouldRenderList = true } = options;
+ renderSecretsStats();
+ renderSecretsFetch();
+ renderSecretsStatus();
+ if (shouldRenderList) {
+ renderSecretsList();
+ }
+ renderSecretsLiveClock();
+ }
+
+ function renderS3All(options = {}) {
+ const { renderList: shouldRenderList = true } = options;
+ renderS3Filters();
+ renderS3Stats();
+ renderS3Fetch();
+ renderS3Status();
+ if (shouldRenderList) {
+ renderS3List();
+ }
+ renderS3LiveClock();
+ }
+
+ function renderHealthStrip() {
+ const serviceOrder = [
+ { key: "emails", panel: "emails", icon: "✉️", label: "SES Emails", shortLabel: "SES" },
+ { key: "logs", panel: "logs", icon: "📜", label: "CloudWatch Logs", shortLabel: "Logs" },
+ { key: "secrets", panel: "secrets", icon: "🔐", label: "Secrets Manager", shortLabel: "Secrets" },
+ { key: "s3", panel: "s3", icon: "🪣", label: "S3 Explorer", shortLabel: "S3" }
+ ];
+
+ el.healthStrip.innerHTML = serviceOrder
+ .map((service) => {
+ const entry = healthState.services?.[service.key];
+ const toneClass = healthState.loading && !entry ? "warn" : entry?.ok ? "ok" : entry ? "bad" : "";
+ const activeClass = service.panel === appState.panel ? "active" : "";
+ const className = ["healthBadge", toneClass, activeClass].filter(Boolean).join(" ");
+ const summary = entry?.summary || (healthState.loading ? "Checking..." : "Waiting");
+ const detail = entry?.detail || (healthState.error ? healthState.error : "Not checked yet");
+ const updatedMeta = healthState.updatedAt
+ ? `Updated ${formatRelative(healthState.updatedAt)} via ${healthState.source}`
+ : "";
+ const titleParts = [`${service.label}: ${detail}`];
+
+ if (updatedMeta) {
+ titleParts.push(updatedMeta);
+ }
+
+ return ``;
+ })
+ .join("");
+ }
+
+ function renderStats() {
+ el.totalStat.textContent = String(state.messages.length);
+ el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`;
+ el.newStat.textContent = String(state.newIds.size);
+ el.newestStat.textContent = state.newest ? formatDateTime(state.newest) : "No messages";
+ }
+
+ function renderLogsFilters() {
+ const groups = logsState.groups.length
+ ? logsState.groups.map((group) => ``)
+ : [''];
+ const streams = [
+ '',
+ ...logsState.streams.map(
+ (stream) => ``
+ )
+ ];
+
+ el.logsGroupSelect.innerHTML = groups.join("");
+ el.logsStreamSelect.innerHTML = streams.join("");
+
+ if (logsState.group) {
+ el.logsGroupSelect.value = logsState.group;
+ }
+
+ el.logsStreamSelect.value = logsState.stream;
+ el.logsWrapToggle.checked = logsState.wrapLines;
+ el.logsTailToggle.checked = logsState.tailNewest;
+ }
+
+ function renderS3Filters() {
+ const bucketOptions = s3State.buckets.length
+ ? s3State.buckets.map(
+ (bucket) => ``
+ )
+ : [''];
+
+ el.s3BucketSelect.innerHTML = bucketOptions.join("");
+
+ if (s3State.bucket) {
+ el.s3BucketSelect.value = s3State.bucket;
+ }
+
+ el.s3PrefixInput.value = s3State.prefix;
+ el.s3SearchInput.value = s3State.search;
+ }
+
+ function renderLogsStats() {
+ el.logsTotalStat.textContent = String(logsState.events.length);
+ el.logsVisibleStat.textContent = `${logsState.filtered.length} visible${logsState.search ? " after search" : ""}`;
+ el.logsStreamsStat.textContent = String(logsState.streams.length || logsState.searchedLogStreams || 0);
+ el.logsNewestStat.textContent = logsState.newest ? formatDateTime(logsState.newest) : "No events";
+ }
+
+ function renderSecretsStats() {
+ el.secretsTotalStat.textContent = String(secretsState.items.length);
+ el.secretsVisibleStat.textContent = `${secretsState.filtered.length} visible${secretsState.search ? " after search" : ""}`;
+ el.secretsLoadedStat.textContent = String(
+ Object.values(secretsState.values).filter((entry) => entry?.status === "loaded").length
+ );
+ el.secretsNewestStat.textContent = secretsState.newest ? formatDateTime(secretsState.newest) : "No secrets";
+ }
+
+ function renderS3Stats() {
+ el.s3TotalStat.textContent = String(s3State.objects.length);
+ el.s3VisibleStat.textContent = `${s3State.filtered.length} visible${s3State.search ? " after search" : ""}`;
+ el.s3BucketsStat.textContent = String(s3State.buckets.length);
+ el.s3NewestStat.textContent = s3State.newest ? formatDateTime(s3State.newest) : "No objects";
+ }
+
+ function renderFetch() {
+ if (state.loading) {
+ el.fetchStat.textContent = "Refreshing...";
+ el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`;
+ return;
+ }
+
+ if (state.error) {
+ el.fetchStat.textContent = "Needs attention";
+ el.fetchDetail.textContent = state.error;
+ return;
+ }
+
+ if (!state.updatedAt) {
+ el.fetchStat.textContent = "Idle";
+ el.fetchDetail.textContent = `Endpoint: ${config.endpoint}`;
+ return;
+ }
+
+ el.fetchStat.textContent = `${state.duration}ms`;
+ el.fetchDetail.textContent = `${state.parseErrors} parse error${state.parseErrors === 1 ? "" : "s"}. Endpoint: ${config.endpoint}`;
+ }
+
+ function renderLogsFetch() {
+ if (logsState.loading) {
+ el.logsFetchStat.textContent = "Refreshing...";
+ el.logsFetchDetail.textContent = `Endpoint: ${config.cloudWatchEndpoint} (${config.cloudWatchRegion})`;
+ return;
+ }
+
+ if (logsState.error) {
+ el.logsFetchStat.textContent = "Needs attention";
+ el.logsFetchDetail.textContent = logsState.error;
+ return;
+ }
+
+ if (!logsState.updatedAt) {
+ el.logsFetchStat.textContent = "Idle";
+ el.logsFetchDetail.textContent = `Endpoint: ${config.cloudWatchEndpoint} (${config.cloudWatchRegion})`;
+ return;
+ }
+
+ el.logsFetchStat.textContent = `${logsState.duration}ms`;
+ el.logsFetchDetail.textContent = `${logsState.group || "No group selected"}. Endpoint: ${config.cloudWatchEndpoint}`;
+ }
+
+ function renderSecretsFetch() {
+ if (secretsState.loading) {
+ el.secretsFetchStat.textContent = "Refreshing...";
+ el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`;
+ return;
+ }
+
+ if (secretsState.error) {
+ el.secretsFetchStat.textContent = "Needs attention";
+ el.secretsFetchDetail.textContent = secretsState.error;
+ return;
+ }
+
+ if (!secretsState.updatedAt) {
+ el.secretsFetchStat.textContent = "Idle";
+ el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`;
+ return;
+ }
+
+ el.secretsFetchStat.textContent = `${secretsState.duration}ms`;
+ el.secretsFetchDetail.textContent = `Endpoint: ${config.secretsEndpoint} (${config.secretsRegion})`;
+ }
+
+ function renderS3Fetch() {
+ if (s3State.loading) {
+ el.s3FetchStat.textContent = "Refreshing...";
+ el.s3FetchDetail.textContent = `Endpoint: ${config.s3Endpoint} (${config.s3Region})`;
+ return;
+ }
+
+ if (s3State.error) {
+ el.s3FetchStat.textContent = "Needs attention";
+ el.s3FetchDetail.textContent = s3State.error;
+ return;
+ }
+
+ if (!s3State.updatedAt) {
+ el.s3FetchStat.textContent = "Idle";
+ el.s3FetchDetail.textContent = `Endpoint: ${config.s3Endpoint} (${config.s3Region})`;
+ return;
+ }
+
+ el.s3FetchStat.textContent = `${s3State.duration}ms`;
+ el.s3FetchDetail.textContent = `${s3State.bucket || "No bucket selected"}. Endpoint: ${config.s3Endpoint}`;
+ }
+
+ function renderStatus() {
+ el.statusChip.className = "status";
+
+ if (state.loading) {
+ el.statusChip.classList.add("warn");
+ el.statusChip.textContent = "Refreshing messages...";
+ return;
+ }
+
+ if (state.error) {
+ el.statusChip.classList.add("bad");
+ el.statusChip.textContent = `Refresh failed: ${state.error}`;
+ return;
+ }
+
+ if (!state.auto) {
+ el.statusChip.textContent = "Live refresh paused";
+ return;
+ }
+
+ if (document.hidden) {
+ el.statusChip.classList.add("warn");
+ el.statusChip.textContent = "Tab hidden, live refresh paused";
+ return;
+ }
+
+ if (!state.updatedAt) {
+ el.statusChip.textContent = "Waiting for first refresh...";
+ return;
+ }
+
+ const seconds = Math.max(0, Math.ceil((state.updatedAt + state.interval - Date.now()) / 1000));
+ el.statusChip.classList.add("ok");
+ el.statusChip.textContent = `Live refresh on, next check in ${seconds}s`;
+ }
+
+ function renderLogsStatus() {
+ el.logsStatusChip.className = "status";
+
+ if (logsState.loading) {
+ el.logsStatusChip.classList.add("warn");
+ el.logsStatusChip.textContent = "Refreshing logs...";
+ return;
+ }
+
+ if (logsState.error) {
+ el.logsStatusChip.classList.add("bad");
+ el.logsStatusChip.textContent = `Refresh failed: ${logsState.error}`;
+ return;
+ }
+
+ if (!logsState.auto) {
+ el.logsStatusChip.textContent = "Live refresh paused";
+ return;
+ }
+
+ if (document.hidden) {
+ el.logsStatusChip.classList.add("warn");
+ el.logsStatusChip.textContent = "Tab hidden, live refresh paused";
+ return;
+ }
+
+ if (!logsState.updatedAt) {
+ el.logsStatusChip.textContent = "Waiting for first refresh...";
+ return;
+ }
+
+ const seconds = Math.max(0, Math.ceil((logsState.updatedAt + logsState.interval - Date.now()) / 1000));
+ el.logsStatusChip.classList.add("ok");
+ el.logsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`;
+ }
+
+ function renderSecretsStatus() {
+ el.secretsStatusChip.className = "status";
+
+ if (secretsState.loading) {
+ el.secretsStatusChip.classList.add("warn");
+ el.secretsStatusChip.textContent = "Refreshing secrets...";
+ return;
+ }
+
+ if (secretsState.error) {
+ el.secretsStatusChip.classList.add("bad");
+ el.secretsStatusChip.textContent = `Refresh failed: ${secretsState.error}`;
+ return;
+ }
+
+ if (!secretsState.auto) {
+ el.secretsStatusChip.textContent = "Live refresh paused";
+ return;
+ }
+
+ if (document.hidden) {
+ el.secretsStatusChip.classList.add("warn");
+ el.secretsStatusChip.textContent = "Tab hidden, live refresh paused";
+ return;
+ }
+
+ if (!secretsState.updatedAt) {
+ el.secretsStatusChip.textContent = "Waiting for first refresh...";
+ return;
+ }
+
+ const seconds = Math.max(0, Math.ceil((secretsState.updatedAt + secretsState.interval - Date.now()) / 1000));
+ el.secretsStatusChip.classList.add("ok");
+ el.secretsStatusChip.textContent = `Live refresh on, next check in ${seconds}s`;
+ }
+
+ function renderS3Status() {
+ el.s3StatusChip.className = "status";
+
+ if (s3State.loading) {
+ el.s3StatusChip.classList.add("warn");
+ el.s3StatusChip.textContent = "Refreshing objects...";
+ return;
+ }
+
+ if (s3State.error) {
+ el.s3StatusChip.classList.add("bad");
+ el.s3StatusChip.textContent = `Refresh failed: ${s3State.error}`;
+ return;
+ }
+
+ if (!s3State.auto) {
+ el.s3StatusChip.textContent = "Live refresh paused";
+ return;
+ }
+
+ if (document.hidden) {
+ el.s3StatusChip.classList.add("warn");
+ el.s3StatusChip.textContent = "Tab hidden, live refresh paused";
+ return;
+ }
+
+ if (!s3State.updatedAt) {
+ el.s3StatusChip.textContent = "Waiting for first refresh...";
+ return;
+ }
+
+ const seconds = Math.max(0, Math.ceil((s3State.updatedAt + s3State.interval - Date.now()) / 1000));
+ el.s3StatusChip.classList.add("ok");
+ el.s3StatusChip.textContent = `Live refresh on, next check in ${seconds}s`;
+ }
+
+ function renderLiveClock() {
+ if (!state.updatedAt) {
+ el.updatedStat.textContent = "Not refreshed yet";
+ return;
+ }
+
+ el.updatedStat.textContent = `Updated ${formatRelative(state.updatedAt)} via ${state.source}`;
+ renderStatus();
+ }
+
+ function renderLogsLiveClock() {
+ if (!logsState.updatedAt) {
+ el.logsUpdatedStat.textContent = "Not refreshed yet";
+ return;
+ }
+
+ el.logsUpdatedStat.textContent = `Updated ${formatRelative(logsState.updatedAt)} via ${logsState.source}`;
+ renderLogsStatus();
+ }
+
+ function renderSecretsLiveClock() {
+ if (!secretsState.updatedAt) {
+ el.secretsUpdatedStat.textContent = "Not refreshed yet";
+ return;
+ }
+
+ el.secretsUpdatedStat.textContent = `Updated ${formatRelative(secretsState.updatedAt)} via ${secretsState.source}`;
+ renderSecretsStatus();
+ }
+
+ function renderS3LiveClock() {
+ if (!s3State.updatedAt) {
+ el.s3UpdatedStat.textContent = "Not refreshed yet";
+ return;
+ }
+
+ el.s3UpdatedStat.textContent = `Updated ${formatRelative(s3State.updatedAt)} via ${s3State.source}`;
+ renderS3Status();
+ }
+
+ function renderList() {
+ el.banner.hidden = !state.error;
+ el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : "";
+
+ if (!state.filtered.length) {
+ el.list.innerHTML = "";
+ el.empty.hidden = false;
+ el.empty.textContent = state.messages.length
+ ? "No messages match the current search."
+ : "No emails yet. Send one through LocalStack SES and refresh.";
+ updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton);
+ return;
+ }
+
+ el.empty.hidden = true;
+ el.list.innerHTML = state.filtered.map(renderCard).join("");
+ bindCardToggles();
+ syncCardExpansion();
+ updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton);
+ }
+
+ function renderLogsList() {
+ el.logsBanner.hidden = !logsState.error;
+ el.logsBanner.textContent = logsState.error ? `Refresh failed: ${logsState.error}` : "";
+
+ if (!logsState.group && !logsState.groups.length) {
+ el.logsList.innerHTML = "";
+ el.logsEmpty.hidden = false;
+ el.logsEmpty.textContent = "No CloudWatch log groups found in LocalStack yet.";
+ updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton);
+ return;
+ }
+
+ if (!logsState.filtered.length) {
+ el.logsList.innerHTML = "";
+ el.logsEmpty.hidden = false;
+ el.logsEmpty.textContent = logsState.events.length
+ ? "No log events match the current search."
+ : "No log events found for the selected group, stream, and time window.";
+ updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton);
+ return;
+ }
+
+ el.logsEmpty.hidden = true;
+ el.logsList.innerHTML = logsState.filtered.map((event) => renderLogEvent(event)).join("");
+ bindLogToggles();
+ updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton);
+ }
+
+ function renderSecretsList() {
+ el.secretsBanner.hidden = !secretsState.error;
+ el.secretsBanner.textContent = secretsState.error ? `Refresh failed: ${secretsState.error}` : "";
+
+ if (!secretsState.filtered.length) {
+ el.secretsList.innerHTML = "";
+ el.secretsEmpty.hidden = false;
+ el.secretsEmpty.textContent = secretsState.items.length
+ ? "No secrets match the current search."
+ : "No Secrets Manager entries found in LocalStack yet.";
+ updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton);
+ return;
+ }
+
+ el.secretsEmpty.hidden = true;
+ el.secretsList.innerHTML = secretsState.filtered.map((secret) => renderSecretCard(secret)).join("");
+ bindSecretToggles();
+ syncSecretExpansion();
+ updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton);
+ }
+
+ function renderS3List() {
+ el.s3Banner.hidden = !s3State.error;
+ el.s3Banner.textContent = s3State.error ? `Refresh failed: ${s3State.error}` : "";
+
+ if (!s3State.bucket && !s3State.buckets.length) {
+ el.s3List.innerHTML = "";
+ el.s3Empty.hidden = false;
+ el.s3Empty.textContent = "No S3 buckets found in LocalStack yet.";
+ updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton);
+ return;
+ }
+
+ if (!s3State.filtered.length) {
+ el.s3List.innerHTML = "";
+ el.s3Empty.hidden = false;
+ el.s3Empty.textContent = s3State.objects.length
+ ? "No S3 objects match the current search."
+ : "No S3 objects found for the selected bucket and prefix.";
+ updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton);
+ return;
+ }
+
+ el.s3Empty.hidden = true;
+ el.s3List.innerHTML = s3State.filtered.map((object) => renderS3ObjectCard(object)).join("");
+ bindS3Toggles();
+ syncS3Expansion();
+ updatePaneTopButtonVisibility(el.s3ContentPane, el.s3ScrollToTopButton);
+ }
+
+ function renderLogEvent(event) {
+ const level = detectLogLevel(event);
+ const levelTag = level ? `${escapeHtml(level.label)}` : "";
+
+ return `
+
+
+
+
+ ${escapeHtml(formatDateTime(event.timestamp))}
+ ${escapeHtml(event.logStreamName || "Unknown stream")}
+ ${levelTag}
+
+
+
+ ${escapeHtml(formatRelative(event.timestamp || Date.now()))}
+
+
+ ${renderLogPreviewContent(event)}
+
+
+
${renderLogBodyContent(event.message)}
+
+
+ `;
+ }
+
+ function renderSecretCard(secret) {
+ const valueState = secretsState.values[secret.id];
+ const tags = [];
+
+ if (secret.owningService) {
+ tags.push(`${escapeHtml(secret.owningService)}`);
+ }
+
+ if (secret.primaryRegion) {
+ tags.push(`${escapeHtml(secret.primaryRegion)}`);
+ }
+
+ if (secret.rotationEnabled) {
+ tags.push('Rotation on');
+ } else {
+ tags.push('Rotation off');
+ }
+
+ if (secret.tagCount) {
+ tags.push(`${secret.tagCount} tag${secret.tagCount === 1 ? "" : "s"}`);
+ }
+
+ if (valueState?.status === "loaded") {
+ tags.push(`${escapeHtml(valueState.label)}`);
+ }
+
+ if (valueState?.status === "error") {
+ tags.push('Value load failed');
+ }
+
+ return `
+
+
+
+
+
🔐 ${escapeHtml(secret.name)}
+
${escapeHtml(secret.description || secret.arn || "No description")}
+
+
${escapeHtml(formatDateTime(secret.lastChangedDate || secret.createdDate))}
+
+ ${tags.join("")}
+ ${escapeHtml(buildSecretPreview(secret))}
+
+
+
+ ${metaCard("Name", secret.name)}
+ ${metaCard("ARN", secret.arn || "Not available")}
+ ${metaCard("Description", secret.description || "None")}
+ ${metaCard("Last changed", formatDateTime(secret.lastChangedDate || secret.createdDate))}
+ ${metaCard("Created", formatDateTime(secret.createdDate))}
+ ${metaCard("Last accessed", formatDateTime(secret.lastAccessedDate))}
+ ${metaCard("Primary region", secret.primaryRegion || "Not set")}
+ ${metaCard("Owning service", secret.owningService || "Not set")}
+
+
+
+ ${renderSecretValuePanel(secret, valueState)}
+
+
+
+ `;
+ }
+
+ function renderS3ObjectCard(object) {
+ const previewState = s3State.previews[object.id];
+ const tags = [
+ `${escapeHtml(object.bucket)}`,
+ `${escapeHtml(formatBytes(object.size))}`
+ ];
+
+ if (object.storageClass) {
+ tags.push(`${escapeHtml(object.storageClass)}`);
+ }
+
+ if (previewState?.status === "loaded") {
+ tags.push(`${escapeHtml(previewState.previewType)}`);
+ }
+
+ if (previewState?.status === "error") {
+ tags.push('Preview failed');
+ }
+
+ return `
+
+
+
+
+
🪣 ${escapeHtml(object.key)}
+
${escapeHtml(object.bucket)} • ${escapeHtml(formatBytes(object.size))}
+
+
${escapeHtml(formatDateTime(object.lastModified))}
+
+ ${tags.join("")}
+ ${escapeHtml(buildS3Preview(object))}
+
+
+
+
+
+ ${metaCard("Bucket", object.bucket)}
+ ${metaCard("Key", object.key)}
+ ${metaCard("Size", formatBytes(object.size))}
+ ${metaCard("Modified", formatDateTime(object.lastModified))}
+ ${metaCard("Storage class", object.storageClass || "STANDARD")}
+ ${metaCard("ETag", object.etag || "Not available")}
+
+
+
${renderS3PreviewPanel(object, previewState)}
+
+
+ `;
+ }
+
+ function bindLogToggles() {
+ el.logsList.querySelectorAll(".logEvent").forEach((details) => {
+ details.addEventListener("toggle", () => {
+ const id = details.dataset.id;
+
+ if (!id) {
+ return;
+ }
+
+ if (details.open) {
+ logsState.openIds.add(id);
+ } else {
+ logsState.openIds.delete(id);
+ }
+ });
+ });
+ }
+
+ function bindSecretToggles() {
+ el.secretsList.querySelectorAll(".secretCard").forEach((details) => {
+ details.addEventListener("toggle", () => {
+ const id = details.dataset.id;
+
+ if (!id) {
+ return;
+ }
+
+ if (details.open) {
+ secretsState.openIds.add(id);
+ ensureSecretValue(id, { force: false });
+ } else {
+ secretsState.openIds.delete(id);
+ }
+ });
+ });
+ }
+
+ function bindS3Toggles() {
+ el.s3List.querySelectorAll(".s3Card").forEach((details) => {
+ details.addEventListener("toggle", () => {
+ const id = details.dataset.id;
+
+ if (!id) {
+ return;
+ }
+
+ if (details.open) {
+ s3State.openIds.add(id);
+ ensureS3Preview(id, { force: false });
+ } else {
+ s3State.openIds.delete(id);
+ }
+ });
+ });
+ }
+
+ function bindCardToggles() {
+ el.list.querySelectorAll(".card").forEach((details) => {
+ details.addEventListener("toggle", () => {
+ const id = details.dataset.id;
+
+ if (!id) {
+ return;
+ }
+
+ if (details.open) {
+ state.openIds.add(id);
+ } else {
+ state.openIds.delete(id);
+ }
+
+ hydrate(details, getMessage(id));
+ });
+ });
+ }
+
+ function renderCard(message) {
+ const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text");
+ const tags = [];
+
+ if (state.newIds.has(message.id)) {
+ tags.push('New');
+ }
+
+ if (message.attachmentCount) {
+ tags.push(
+ `${message.attachmentCount} attachment${message.attachmentCount === 1 ? "" : "s"}`
+ );
+ }
+
+ tags.push(`${message.hasHtml ? "HTML" : "Text only"}`);
+
+ if (message.parseError) {
+ tags.push('Parse issue');
+ }
+
+ return `
+
+
+
+
+
${escapeHtml(message.subject)}
+
${escapeHtml(message.from)} to ${escapeHtml(message.to)}
+
+
${escapeHtml(formatDateTime(message.timestamp))}
+
+ ${tags.join("")}
+ ${escapeHtml(message.preview)}
+
+
+
+
+
+ ${metaCard("From", message.from)}
+ ${metaCard("To", message.to)}
+ ${metaCard("Reply-To", message.replyTo || "None")}
+ ${metaCard("Sent", formatDateTime(message.timestamp))}
+ ${metaCard("Region", message.region || "Unknown region")}
+ ${metaCard("LocalStack Id", message.id)}
+ ${metaCard("Message-Id", message.messageId || "Not available")}
+ ${metaCard("Raw size", formatBytes(message.rawSizeBytes))}
+ ${message.parseError ? metaCard("Parse error", message.parseError) : ""}
+
+
+ ${
+ message.attachments?.length
+ ? `
`
+ : ""
+ }
+
+
${renderPanel(message, view)}
+
+
+ `;
+ }
+
+ function renderPanel(message, view) {
+ if (view === "rendered" && message.hasHtml) {
+ return ``;
+ }
+
+ if (view === "raw") {
+ const raw = state.raw[message.id];
+
+ if (!raw) {
+ return `Raw MIME source is loaded on demand.
`;
+ }
+
+ if (raw.status === "loading") {
+ return 'Loading raw source...
';
+ }
+
+ if (raw.status === "error") {
+ return `Unable to load raw source: ${escapeHtml(raw.error)}
`;
+ }
+
+ return `${escapeHtml(raw.value)}`;
+ }
+
+ return `${escapeHtml(message.textContent || "No plain-text content available for this message.")}`;
+ }
+
+ function renderSecretValuePanel(secret, valueState) {
+ if (!valueState) {
+ return `Secret values are loaded on demand.
`;
+ }
+
+ if (valueState.status === "loading") {
+ return 'Loading secret value...
';
+ }
+
+ if (valueState.status === "error") {
+ return `Unable to load secret value: ${escapeHtml(valueState.error)}
`;
+ }
+
+ const revealed = secretsState.revealedIds.has(secret.id);
+ const maskedHtml = escapeHtml(maskSecretValue(valueState.copyValue));
+
+ return `
+
+ ${revealed ? valueState.displayHtml : maskedHtml}
+ `;
+ }
+
+ function renderS3PreviewPanel(object, previewState) {
+ if (!previewState) {
+ return `Object previews load on demand.
`;
+ }
+
+ if (previewState.status === "loading") {
+ return 'Loading object preview...
';
+ }
+
+ if (previewState.status === "error") {
+ return `Unable to load object preview: ${escapeHtml(previewState.error)}
`;
+ }
+
+ const truncatedTag = previewState.truncated ? 'Preview truncated' : "";
+ let previewContent = `No inline preview available for this object type.
`;
+
+ if (previewState.previewType === "image" && previewState.imageDataUrl) {
+ previewContent = `
`;
+ } else if (previewState.previewType === "json") {
+ previewContent = `${highlightJsonText(prettyJsonOrText(previewState.previewText))}`;
+ } else if (previewState.previewType === "text" || previewState.previewType === "html") {
+ previewContent = `${escapeHtml(previewState.previewText || "No preview text available.")}`;
+ }
+
+ return `
+
+ ${previewContent}
+ `;
+ }
+
+ function metaCard(label, value) {
+ return `${escapeHtml(label)}${escapeHtml(value)}`;
+ }
+
+ function syncCardExpansion() {
+ const applyCardState = () => {
+ el.list.querySelectorAll(".card").forEach((details) => {
+ const id = details.dataset.id;
+ const shouldOpen = Boolean(id && state.openIds.has(id));
+
+ if (shouldOpen && !details.open) {
+ details.open = true;
+ }
+
+ if (!shouldOpen && details.open) {
+ details.open = false;
+ return;
+ }
+
+ if (shouldOpen) {
+ hydrate(details, getMessage(id));
+ }
+ });
+ };
+
+ applyCardState();
+ window.requestAnimationFrame(applyCardState);
+ }
+
+ function syncSecretExpansion() {
+ const applySecretState = () => {
+ el.secretsList.querySelectorAll(".secretCard").forEach((details) => {
+ const id = details.dataset.id;
+ const shouldOpen = Boolean(id && secretsState.openIds.has(id));
+
+ if (shouldOpen && !details.open) {
+ details.open = true;
+ }
+
+ if (!shouldOpen && details.open) {
+ details.open = false;
+ return;
+ }
+
+ if (shouldOpen) {
+ ensureSecretValue(id, { force: false });
+ }
+ });
+ };
+
+ applySecretState();
+ 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();
+
+ if (filename.endsWith(".pdf") || contentType.includes("pdf")) {
+ return "📄";
+ }
+
+ if (
+ [".doc", ".docx", ".txt", ".rtf", ".md"].some((extension) => filename.endsWith(extension)) ||
+ contentType.includes("word") ||
+ contentType.startsWith("text/")
+ ) {
+ return "📝";
+ }
+
+ if (
+ [".xls", ".xlsx", ".csv"].some((extension) => filename.endsWith(extension)) ||
+ contentType.includes("sheet") ||
+ contentType.includes("csv")
+ ) {
+ return "📊";
+ }
+
+ if (
+ filename.endsWith(".json") ||
+ filename.endsWith(".xml") ||
+ filename.endsWith(".yaml") ||
+ filename.endsWith(".yml")
+ ) {
+ return "🧾";
+ }
+
+ if (contentType.startsWith("image/")) {
+ return "🖼️";
+ }
+
+ if (contentType.startsWith("audio/")) {
+ return "🎵";
+ }
+
+ if (contentType.startsWith("video/")) {
+ return "🎬";
+ }
+
+ if (
+ [".zip", ".rar", ".7z", ".tar", ".gz"].some((extension) => filename.endsWith(extension)) ||
+ contentType.includes("zip") ||
+ contentType.includes("compressed")
+ ) {
+ return "🗜️";
+ }
+
+ return "📎";
+ }
+
+ function hydrate(details, message) {
+ if (!details || !details.open || !message) {
+ return;
+ }
+
+ const view = state.views[message.id] || (message.hasHtml ? "rendered" : "text");
+
+ if (view !== "rendered" || !message.hasHtml) {
+ return;
+ }
+
+ const iframe = details.querySelector("[data-frame]");
+
+ if (iframe) {
+ iframe.referrerPolicy = "no-referrer";
+ iframe.sandbox = "";
+ iframe.srcdoc = message.renderedHtml || "";
+ }
+ }
+
+ function getMessage(id) {
+ return state.messages.find((message) => message.id === id);
+ }
+
+ function getLogEvent(id) {
+ return logsState.events.find((event) => event.id === id);
+ }
+
+ function getSecret(id) {
+ 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;
+ }
+
+ if (state.raw[id]?.status === "loading") {
+ return null;
+ }
+
+ state.raw[id] = { status: "loading" };
+ renderList();
+
+ try {
+ const response = await fetch(`/api/messages/${encodeURIComponent(id)}/raw`, { cache: "no-store" });
+
+ if (!response.ok) {
+ throw new Error((await response.text()) || `Request failed with ${response.status}`);
+ }
+
+ const value = await response.text();
+ state.raw[id] = { status: "loaded", value };
+ return value;
+ } catch (error) {
+ state.raw[id] = { status: "error", error: error.message || "Unknown raw load error" };
+ setStatus("Could not load the raw message source.", "bad");
+ return null;
+ } finally {
+ renderList();
+ }
+ }
+
+ async function ensureSecretValue(id, options = {}) {
+ const { force = false } = options;
+
+ if (!id) {
+ return null;
+ }
+
+ if (!force && secretsState.values[id]?.status === "loaded") {
+ return secretsState.values[id];
+ }
+
+ if (secretsState.values[id]?.status === "loading") {
+ return null;
+ }
+
+ secretsState.values[id] = { status: "loading" };
+ renderSecretsAll();
+
+ try {
+ const response = await fetch(`/api/secrets/value?id=${encodeURIComponent(id)}`, { 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 secretString = typeof payload.secretString === "string" ? payload.secretString : "";
+ const secretBinary = typeof payload.secretBinary === "string" ? payload.secretBinary : "";
+ const parsedString = secretString ? tryParseJsonText(secretString) : { ok: false, value: null };
+ const entry = {
+ status: "loaded",
+ label: secretBinary ? "Binary" : parsedString.ok ? "JSON" : secretString ? "Text" : "Empty",
+ copyValue: secretBinary
+ ? secretBinary
+ : parsedString.ok
+ ? JSON.stringify(parsedString.value, null, 2)
+ : secretString || "No secret value.",
+ displayHtml: secretBinary
+ ? escapeHtml(secretBinary)
+ : parsedString.ok
+ ? highlightJsonText(JSON.stringify(parsedString.value, null, 2))
+ : escapeHtml(secretString || "No secret value."),
+ isJson: parsedString.ok,
+ isBinary: Boolean(secretBinary),
+ versionId: payload.versionId || "",
+ versionStages: Array.isArray(payload.versionStages) ? payload.versionStages : [],
+ createdDate: payload.createdDate || "",
+ arn: payload.arn || "",
+ name: payload.name || ""
+ };
+
+ secretsState.values[id] = entry;
+ return entry;
+ } catch (error) {
+ secretsState.values[id] = {
+ status: "error",
+ error: error.message || "Unknown secret value error"
+ };
+ setSecretsStatus("Could not load the secret value.", "bad");
+ return null;
+ } finally {
+ renderSecretsAll();
+ }
+ }
+
+ 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);
+ } catch {
+ const input = document.createElement("textarea");
+ input.value = value;
+ input.setAttribute("readonly", "");
+ input.style.position = "fixed";
+ input.style.opacity = "0";
+ document.body.appendChild(input);
+ input.select();
+ document.execCommand("copy");
+ document.body.removeChild(input);
+ }
+ }
+
+ function setStatus(message, tone) {
+ el.statusChip.className = "status";
+
+ if (tone) {
+ el.statusChip.classList.add(tone);
+ }
+
+ el.statusChip.textContent = message;
+ }
+
+ function setLogsStatus(message, tone) {
+ el.logsStatusChip.className = "status";
+
+ if (tone) {
+ el.logsStatusChip.classList.add(tone);
+ }
+
+ el.logsStatusChip.textContent = message;
+ }
+
+ function setSecretsStatus(message, tone) {
+ el.secretsStatusChip.className = "status";
+
+ if (tone) {
+ el.secretsStatusChip.classList.add(tone);
+ }
+
+ 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", "s3"].includes(storedPanel) ? storedPanel : "emails";
+ }
+
+ function getInitialTheme() {
+ const storedTheme = readStoredValue(THEME_STORAGE_KEY);
+
+ if (storedTheme === "dark" || storedTheme === "light") {
+ return storedTheme;
+ }
+
+ return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light";
+ }
+
+ function applyTheme(theme) {
+ const nextTheme = theme === "dark" ? "dark" : "light";
+ appState.theme = nextTheme;
+ document.body.dataset.theme = nextTheme;
+
+ try {
+ window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
+ } catch {}
+
+ renderThemeToggle();
+ }
+
+ function renderThemeToggle() {
+ if (!el.themeToggle) {
+ return;
+ }
+
+ const isDark = appState.theme === "dark";
+ el.themeToggle.textContent = isDark ? "🌙 Dark theme" : "☀️ Light theme";
+ el.themeToggle.setAttribute("aria-pressed", isDark ? "true" : "false");
+ el.themeToggle.setAttribute("aria-label", isDark ? "Switch to light theme" : "Switch to dark theme");
+ el.themeToggle.title = isDark ? "Switch to light theme" : "Switch to dark theme";
+ }
+
+ function persistPanel() {
+ writeStoredValue(PANEL_STORAGE_KEY, appState.panel);
+ }
+
+ function persistEmailPreferences() {
+ writeStoredJson(EMAIL_PREFERENCES_STORAGE_KEY, {
+ search: state.search,
+ auto: state.auto,
+ interval: state.interval
+ });
+ }
+
+ function persistLogPreferences() {
+ writeStoredJson(LOG_PREFERENCES_STORAGE_KEY, {
+ group: logsState.group,
+ stream: logsState.stream,
+ search: logsState.search,
+ auto: logsState.auto,
+ interval: logsState.interval,
+ windowMs: logsState.windowMs,
+ limit: logsState.limit,
+ wrapLines: logsState.wrapLines,
+ tailNewest: logsState.tailNewest
+ });
+ }
+
+ function persistSecretPreferences() {
+ writeStoredJson(SECRET_PREFERENCES_STORAGE_KEY, {
+ search: secretsState.search,
+ auto: secretsState.auto,
+ interval: secretsState.interval
+ });
+ }
+
+ 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);
+
+ if (!rawValue) {
+ return null;
+ }
+
+ const parsedValue = JSON.parse(rawValue);
+ return parsedValue && typeof parsedValue === "object" && !Array.isArray(parsedValue) ? parsedValue : null;
+ } catch {
+ return null;
+ }
+ }
+
+ function readStoredValue(key) {
+ try {
+ return window.localStorage.getItem(key);
+ } catch {
+ return null;
+ }
+ }
+
+ function writeStoredValue(key, value) {
+ try {
+ window.localStorage.setItem(key, String(value));
+ } catch {}
+ }
+
+ function writeStoredJson(key, value) {
+ try {
+ window.localStorage.setItem(key, JSON.stringify(value));
+ } catch {}
+ }
+
+ function getStoredText(value, fallback = "") {
+ return typeof value === "string" ? value : fallback;
+ }
+
+ function getStoredBoolean(value, fallback) {
+ return typeof value === "boolean" ? value : fallback;
+ }
+
+ function getStoredNumber(value, allowedValues, fallback) {
+ const normalizedValue = Number(value);
+ return Number.isFinite(normalizedValue) && allowedValues.includes(normalizedValue) ? normalizedValue : fallback;
+ }
+
+ function scrollPaneToTop(element) {
+ if (!element) {
+ return;
+ }
+
+ element.scrollTo({ top: 0, behavior: "smooth" });
+ }
+
+ function updatePaneTopButtonVisibility(pane, button) {
+ if (!pane || !button) {
+ return;
+ }
+
+ button.classList.toggle("visible", pane.scrollTop > 140);
+ }
+
+ function formatDateTime(value) {
+ if (!value) {
+ return "Unknown time";
+ }
+
+ const date = new Date(value);
+ return Number.isNaN(date.getTime())
+ ? "Unknown time"
+ : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
+ }
+
+ function formatRelative(timestampMs) {
+ const seconds = Math.max(1, Math.round((Date.now() - timestampMs) / 1000));
+
+ if (seconds < 60) {
+ return `${seconds}s ago`;
+ }
+
+ const minutes = Math.round(seconds / 60);
+
+ if (minutes < 60) {
+ return `${minutes}m ago`;
+ }
+
+ const hours = Math.round(minutes / 60);
+
+ if (hours < 24) {
+ return `${hours}h ago`;
+ }
+
+ return `${Math.round(hours / 24)}d ago`;
+ }
+
+ function formatBytes(value) {
+ if (!value) {
+ return "0 B";
+ }
+
+ const units = ["B", "KB", "MB", "GB"];
+ let size = value;
+ let index = 0;
+
+ while (size >= 1024 && index < units.length - 1) {
+ size /= 1024;
+ index += 1;
+ }
+
+ return `${index === 0 ? size : size.toFixed(1)} ${units[index]}`;
+ }
+
+ function formatLogMessage(message) {
+ const value = String(message || "").trim();
+
+ if (!value) {
+ return "No log payload.";
+ }
+
+ try {
+ return JSON.stringify(JSON.parse(value), null, 2);
+ } catch {
+ return value;
+ }
+ }
+
+ function buildSecretPreview(secret) {
+ const fragments = [];
+
+ if (secret.description) {
+ fragments.push(secret.description);
+ }
+
+ if (secret.owningService) {
+ fragments.push(`Service: ${secret.owningService}`);
+ }
+
+ if (secret.primaryRegion) {
+ fragments.push(`Region: ${secret.primaryRegion}`);
+ }
+
+ if (secret.tagCount) {
+ fragments.push(`${secret.tagCount} tag${secret.tagCount === 1 ? "" : "s"}`);
+ }
+
+ if (!fragments.length) {
+ fragments.push("No description or tags yet.");
+ }
+
+ 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);
+
+ if (!parsedLog.ok) {
+ return escapeHtml(event?.preview || "No preview available.");
+ }
+
+ const compactJson = JSON.stringify(parsedLog.value);
+ const previewText = compactJson.length > 220 ? `${compactJson.slice(0, 217)}...` : compactJson;
+ return highlightJsonText(previewText);
+ }
+
+ function renderLogBodyContent(message) {
+ const parsedLog = tryParseJsonText(message);
+
+ if (!parsedLog.ok) {
+ return escapeHtml(formatLogMessage(message));
+ }
+
+ return highlightJsonText(JSON.stringify(parsedLog.value, null, 2));
+ }
+
+ function tryParseJsonText(message) {
+ const value = String(message || "").trim();
+
+ if (!value) {
+ return { ok: false, value: null };
+ }
+
+ try {
+ return { ok: true, value: JSON.parse(value) };
+ } catch {
+ return { ok: false, value: null };
+ }
+ }
+
+ function highlightJsonText(value) {
+ const source = String(value ?? "");
+ const tokenRegex =
+ /("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?)/g;
+ let html = "";
+ let lastIndex = 0;
+
+ for (const match of source.matchAll(tokenRegex)) {
+ const [token] = match;
+ const index = match.index ?? 0;
+ let className = "jsonNumber";
+
+ html += escapeHtml(source.slice(lastIndex, index));
+
+ if (token.endsWith(":")) {
+ className = "jsonKey";
+ } else if (token === "true" || token === "false") {
+ className = "jsonBoolean";
+ } else if (token === "null") {
+ className = "jsonNull";
+ } else if (token.startsWith('"')) {
+ className = "jsonString";
+ }
+
+ html += `${escapeHtml(token)}`;
+ lastIndex = index + token.length;
+ }
+
+ html += escapeHtml(source.slice(lastIndex));
+ return html;
+ }
+
+ function escapeHtml(value) {
+ return String(value ?? "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ async function safeJson(response) {
+ try {
+ return await response.json();
+ } catch {
+ return null;
+ }
+ }
+}
diff --git a/_reference/localEmailViewer/server/config.js b/_reference/localEmailViewer/server/config.js
new file mode 100644
index 000000000..c5d4ed154
--- /dev/null
+++ b/_reference/localEmailViewer/server/config.js
@@ -0,0 +1,45 @@
+import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
+import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
+import { S3Client } from "@aws-sdk/client-s3";
+
+export const PORT = Number(process.env.PORT || 3334);
+export const SES_ENDPOINT = process.env.SES_VIEWER_ENDPOINT || "http://localhost:4566/_aws/ses";
+export const FETCH_TIMEOUT_MS = Number(process.env.SES_VIEWER_FETCH_TIMEOUT_MS || 5000);
+export const DEFAULT_REFRESH_MS = Number(process.env.SES_VIEWER_REFRESH_MS || 10000);
+export const CLOUDWATCH_ENDPOINT = process.env.CLOUDWATCH_VIEWER_ENDPOINT || "http://localhost:4566";
+export const CLOUDWATCH_REGION =
+ process.env.CLOUDWATCH_VIEWER_REGION || process.env.AWS_DEFAULT_REGION || "ca-central-1";
+export const CLOUDWATCH_DEFAULT_GROUP = process.env.CLOUDWATCH_VIEWER_LOG_GROUP || "development";
+export const CLOUDWATCH_DEFAULT_WINDOW_MS = Number(process.env.CLOUDWATCH_VIEWER_WINDOW_MS || 15 * 60 * 1000);
+export const CLOUDWATCH_DEFAULT_LIMIT = Number(process.env.CLOUDWATCH_VIEWER_LIMIT || 200);
+export const SECRETS_ENDPOINT = process.env.SECRETS_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT;
+export const SECRETS_REGION = process.env.SECRETS_VIEWER_REGION || CLOUDWATCH_REGION;
+export const S3_ENDPOINT = process.env.S3_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT;
+export const S3_REGION = process.env.S3_VIEWER_REGION || CLOUDWATCH_REGION;
+export const S3_DEFAULT_BUCKET = process.env.S3_VIEWER_BUCKET || "";
+export const S3_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_PREVIEW_BYTES || 256 * 1024);
+export const S3_IMAGE_PREVIEW_MAX_BYTES = Number(process.env.S3_VIEWER_IMAGE_PREVIEW_BYTES || 1024 * 1024);
+
+export const LOCALSTACK_CREDENTIALS = {
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID || "test",
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "test"
+};
+
+export const cloudWatchLogsClient = new CloudWatchLogsClient({
+ region: CLOUDWATCH_REGION,
+ endpoint: CLOUDWATCH_ENDPOINT,
+ credentials: LOCALSTACK_CREDENTIALS
+});
+
+export const secretsManagerClient = new SecretsManagerClient({
+ region: SECRETS_REGION,
+ endpoint: SECRETS_ENDPOINT,
+ credentials: LOCALSTACK_CREDENTIALS
+});
+
+export const s3Client = new S3Client({
+ region: S3_REGION,
+ endpoint: S3_ENDPOINT,
+ credentials: LOCALSTACK_CREDENTIALS,
+ forcePathStyle: true
+});
diff --git a/_reference/localEmailViewer/server/localstack-service.js b/_reference/localEmailViewer/server/localstack-service.js
new file mode 100644
index 000000000..ad2b7473f
--- /dev/null
+++ b/_reference/localEmailViewer/server/localstack-service.js
@@ -0,0 +1,845 @@
+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) || /
+
+
+
+
+
+
+
+ ${value}
+`;
+}
+
+function stripTags(value) {
+ return String(value || "")
+ .replace(/
+
+
+
+
+
+
+
LocalStack Toolbox
+
Inspector
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total00 visible
+ New0New since last refresh
+ NewestNo messagesNot refreshed yet
+ FetchIdleEndpoint: ${escapeHtml(SES_ENDPOINT)}
+
+
+
+
+
+
+
+
+
+ Events00 visible
+ Streams0Streams in selected group
+ LatestNo eventsNot refreshed yet
+ FetchIdleEndpoint: ${escapeHtml(CLOUDWATCH_ENDPOINT)} (${escapeHtml(CLOUDWATCH_REGION)})
+
+
+
+
+
+
+
+
+
+ Secrets00 visible
+ Loaded0Values loaded this session
+ LatestNo secretsNot refreshed yet
+ FetchIdleEndpoint: ${escapeHtml(SECRETS_ENDPOINT)} (${escapeHtml(SECRETS_REGION)})
+
+
+
+
+
+
+
+
+
+ Objects00 visible
+ Buckets0Available in LocalStack
+ LatestNo objectsNot refreshed yet
+ FetchIdleEndpoint: ${escapeHtml(S3_ENDPOINT)} (${escapeHtml(S3_REGION)})
+
+
+
+
+
+
+
+
+