diff --git a/_reference/localEmailViewer/README.md b/_reference/localEmailViewer/README.md index 238c2bf22..6492c1176 100644 --- a/_reference/localEmailViewer/README.md +++ b/_reference/localEmailViewer/README.md @@ -2,6 +2,7 @@ This app connects to your Docker LocalStack endpoints and gives you a compact in - SES generated emails - CloudWatch log groups, streams, and recent events +- Secrets Manager secrets and values ```shell npm start @@ -21,6 +22,8 @@ Features: attachment downloads, and new-message highlighting - CloudWatch Logs workspace with log group selection, stream filtering, adjustable time window, adjustable event limit, live refresh, and in-browser log search +- Secrets Manager workspace with live refresh, search, expandable secret metadata, lazy-loaded + secret values, and copyable JSON/text values - Compact single-page UI for switching between the local stack tools you use most Optional environment variables: @@ -35,4 +38,6 @@ 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 ``` diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 162f9504b..f45eab624 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -6,6 +6,7 @@ import { DescribeLogStreamsCommand, FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs"; +import { GetSecretValueCommand, ListSecretsCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { simpleParser } from "mailparser"; const app = express(); @@ -19,6 +20,8 @@ const CLOUDWATCH_REGION = process.env.CLOUDWATCH_VIEWER_REGION || process.env.AW const CLOUDWATCH_DEFAULT_GROUP = process.env.CLOUDWATCH_VIEWER_LOG_GROUP || "development"; const CLOUDWATCH_DEFAULT_WINDOW_MS = Number(process.env.CLOUDWATCH_VIEWER_WINDOW_MS || 15 * 60 * 1000); const CLOUDWATCH_DEFAULT_LIMIT = Number(process.env.CLOUDWATCH_VIEWER_LIMIT || 200); +const SECRETS_ENDPOINT = process.env.SECRETS_VIEWER_ENDPOINT || CLOUDWATCH_ENDPOINT; +const SECRETS_REGION = process.env.SECRETS_VIEWER_REGION || CLOUDWATCH_REGION; const LOCALSTACK_CREDENTIALS = { accessKeyId: process.env.AWS_ACCESS_KEY_ID || "test", secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "test" @@ -28,6 +31,11 @@ const cloudWatchLogsClient = new CloudWatchLogsClient({ endpoint: CLOUDWATCH_ENDPOINT, credentials: LOCALSTACK_CREDENTIALS }); +const secretsManagerClient = new SecretsManagerClient({ + region: SECRETS_REGION, + endpoint: SECRETS_ENDPOINT, + credentials: LOCALSTACK_CREDENTIALS +}); app.use((req, res, next) => { res.set("Cache-Control", "no-store"); @@ -48,7 +56,8 @@ app.get("/health", (req, res) => { endpoint: SES_ENDPOINT, endpoints: { ses: SES_ENDPOINT, - cloudWatchLogs: CLOUDWATCH_ENDPOINT + cloudWatchLogs: CLOUDWATCH_ENDPOINT, + secretsManager: SECRETS_ENDPOINT }, port: PORT, defaultRefreshMs: DEFAULT_REFRESH_MS @@ -185,6 +194,48 @@ app.get("/api/logs/events", async (req, res) => { } }); +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 + }); + } +}); + async function loadMessages() { const startedAt = Date.now(); const sesMessages = await fetchSesMessages(); @@ -352,6 +403,99 @@ async function loadLogEvents({ logGroupName, logStreamName, windowMs, limit }) { }; } +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 findSesMessageById(id) { const messages = await fetchSesMessages(); return messages.find((message, index) => resolveMessageId(message, index) === id) || null; @@ -569,6 +713,8 @@ function getClientConfig() { endpoint: SES_ENDPOINT, cloudWatchEndpoint: CLOUDWATCH_ENDPOINT, cloudWatchRegion: CLOUDWATCH_REGION, + secretsEndpoint: SECRETS_ENDPOINT, + secretsRegion: SECRETS_REGION, defaultLogGroup: CLOUDWATCH_DEFAULT_GROUP, defaultLogWindowMs: CLOUDWATCH_DEFAULT_WINDOW_MS, defaultLogLimit: CLOUDWATCH_DEFAULT_LIMIT @@ -596,6 +742,7 @@ function renderHtml() {
+
@@ -712,6 +859,49 @@ function renderHtml() { + + @@ -721,7 +911,7 @@ function renderHtml() { function renderStyles() { return ` - :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);} + :root{--panel:rgba(255,255,255,.82);--panel-strong:#fff;--card-shell:linear-gradient(180deg,rgba(255,246,236,.98),rgba(255,252,247,.99));--card-body:#fffdf9;--log-shell:linear-gradient(180deg,rgba(239,246,255,.98),rgba(248,251,255,.99));--log-body:#f8fbff;--secret-shell:linear-gradient(180deg,rgba(239,251,246,.98),rgba(247,253,249,.99));--secret-body:#f6fcf8;--ink:#1f2933;--muted:#607080;--line:rgba(31,41,51,.12);--card-line:rgba(207,109,60,.24);--log-line:rgba(48,113,169,.22);--secret-line:rgba(31,143,101,.2);--accent:#cf6d3c;--accent-soft:rgba(207,109,60,.1);--info:#3071a9;--info-soft:rgba(48,113,169,.1);--secret:#1f8f65;--secret-soft:rgba(31,143,101,.1);--ok:#1f8f65;--warn:#9d5f00;--bad:#b33a3a;--shadow:0 12px 28px rgba(35,43,53,.08);--card-shadow:0 18px 34px rgba(122,78,34,.12);--log-shadow:0 16px 32px rgba(48,113,169,.12);--secret-shadow:0 16px 32px rgba(31,143,101,.12);} *{box-sizing:border-box} html,body{margin:0;height:100%;overflow:hidden} body{color-scheme:light;background:radial-gradient(circle at top left,rgba(207,109,60,.18),transparent 28%),radial-gradient(circle at top right,rgba(31,143,101,.12),transparent 24%),linear-gradient(180deg,#f8f5ef,#efe7da);color:var(--ink);font:15px/1.45 "Aptos","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;transition:background-color .18s ease,color .18s ease} @@ -808,16 +998,22 @@ function renderStyles() { .attachmentLink:hover{transform:translateY(-1px);background:#fff;border-color:rgba(207,109,60,.28)} .panel{overflow:hidden;border-radius:12px;border:1px solid rgba(207,109,60,.14);background:#fff} .logEvent{width:100%;overflow:hidden;border-radius:16px;border:1px solid var(--log-line);background:var(--log-shell);box-shadow:var(--log-shadow)} + .secretCard{background:var(--secret-shell);border:1px solid var(--secret-line);box-shadow:var(--secret-shadow)} .logSummary{list-style:none;display:grid;gap:7px;padding:10px 12px;cursor:pointer} .logSummary::-webkit-details-marker{display:none} + .secretSummary{background:linear-gradient(180deg,rgba(244,253,248,.9),rgba(236,249,242,.96))} .logSummaryTop{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center} .logMeta{display:flex;flex-wrap:wrap;gap:8px;align-items:center} .logSummaryActions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:flex-end} .logTag{background:var(--info-soft);color:var(--info);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .secretTag{background:var(--secret-soft);color:var(--secret);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .logPreview{margin:0;color:#324150;font:600 .88rem/1.45 "Cascadia Code","Consolas",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .logBody{padding:8px 12px 12px;border-top:1px solid rgba(48,113,169,.14);background:var(--log-body)} + .secretBody{border-top-color:rgba(31,143,101,.14);background:var(--secret-body)} .logCopyButton{box-shadow:none} .logBody pre{border-radius:12px;border:1px solid rgba(48,113,169,.14);padding:12px;background:linear-gradient(180deg,rgba(48,113,169,.04),transparent 140px),#fff} + .secretValuePanel{display:grid;gap:10px} + .secretValuePanel pre{border-radius:12px;border:1px solid rgba(31,143,101,.14);padding:12px;background:linear-gradient(180deg,rgba(31,143,101,.04),transparent 140px),#fff} .jsonSyntax .jsonKey{color:#b55f2d} .jsonSyntax .jsonString{color:#1f8f65} .jsonSyntax .jsonNumber{color:#2f6ea9} @@ -833,6 +1029,7 @@ function renderStyles() { body[data-theme="dark"] .stat{background:rgba(15,21,30,.84);border-color:rgba(148,163,184,.16);box-shadow:0 14px 30px rgba(0,0,0,.32)} body[data-theme="dark"] .card{background:linear-gradient(180deg,rgba(50,35,28,.96),rgba(31,25,22,.98));border-color:rgba(207,109,60,.24);box-shadow:0 18px 34px rgba(0,0,0,.34)} body[data-theme="dark"] .logEvent{background:linear-gradient(180deg,rgba(18,31,45,.96),rgba(14,24,36,.98));border-color:rgba(73,144,204,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} + body[data-theme="dark"] .secretCard{background:linear-gradient(180deg,rgba(19,39,31,.96),rgba(14,30,24,.98));border-color:rgba(64,170,126,.22);box-shadow:0 16px 32px rgba(0,0,0,.34)} body[data-theme="dark"] .workspaceTabs{background:rgba(148,163,184,.12)} body[data-theme="dark"] .workspaceTab, body[data-theme="dark"] .tab{color:#aab8c8} @@ -853,6 +1050,8 @@ function renderStyles() { body[data-theme="dark"] .body{background:#211a17;border-top-color:rgba(207,109,60,.18)} body[data-theme="dark"] .logSummary{background:linear-gradient(180deg,rgba(21,34,47,.94),rgba(16,27,39,.98))} body[data-theme="dark"] .logBody{background:#13212d;border-top-color:rgba(73,144,204,.18)} + body[data-theme="dark"] .secretSummary{background:linear-gradient(180deg,rgba(21,43,34,.94),rgba(16,34,27,.98))} + body[data-theme="dark"] .secretBody{background:#12241c;border-top-color:rgba(64,170,126,.18)} body[data-theme="dark"] .metaCard{background:rgba(17,25,35,.64);border-color:rgba(148,163,184,.14)} body[data-theme="dark"] .attachment{background:rgba(50,35,28,.9);border-color:rgba(207,109,60,.18)} body[data-theme="dark"] .attachmentLink{color:#f6c4a9} @@ -860,12 +1059,14 @@ function renderStyles() { body[data-theme="dark"] .panel, body[data-theme="dark"] pre, body[data-theme="dark"] .logBody pre{background:linear-gradient(180deg,rgba(73,144,204,.06),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} + body[data-theme="dark"] .secretValuePanel pre{background:linear-gradient(180deg,rgba(64,170,126,.08),transparent 140px),#0f1722;color:#e8edf3;border-color:rgba(148,163,184,.16)} body[data-theme="dark"] .panel{border-color:rgba(148,163,184,.14)} body[data-theme="dark"] .banner, body[data-theme="dark"] .empty{background:rgba(15,21,30,.82);border-color:rgba(148,163,184,.16)} body[data-theme="dark"] .time{background:rgba(148,163,184,.12);color:#e8edf3} body[data-theme="dark"] .tag{background:rgba(207,109,60,.14);color:#f0c2aa} body[data-theme="dark"] .logTag{background:rgba(73,144,204,.16);color:#93cfff} + body[data-theme="dark"] .secretTag{background:rgba(64,170,126,.16);color:#9fe0be} body[data-theme="dark"] .preview, body[data-theme="dark"] .logPreview, body[data-theme="dark"] .metaCard dd, @@ -906,15 +1107,18 @@ function clientApp(config) { 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 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 appState = { panel: getInitialPanel(), logsReady: false, + secretsReady: false, theme: getInitialTheme() }; @@ -962,11 +1166,29 @@ function clientApp(config) { 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: {}, + listSignature: "" + }; + const el = { workspaceTabs: document.getElementById("workspaceTabs"), themeToggle: document.getElementById("themeToggle"), emailsPanel: document.getElementById("emailsPanel"), logsPanel: document.getElementById("logsPanel"), + secretsPanel: document.getElementById("secretsPanel"), refreshButton: document.getElementById("refreshButton"), autoToggle: document.getElementById("autoToggle"), intervalSelect: document.getElementById("intervalSelect"), @@ -1010,7 +1232,27 @@ function clientApp(config) { logsBanner: document.getElementById("logsBanner"), logsEmpty: document.getElementById("logsEmpty"), logsList: document.getElementById("logsList"), - logsContentPane: document.getElementById("logsContentPane") + 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") }; el.autoToggle.checked = state.auto; @@ -1021,22 +1263,30 @@ function clientApp(config) { el.logsWindowSelect.value = String(logsState.windowMs); el.logsLimitSelect.value = String(logsState.limit); el.logsSearchInput.value = logsState.search; + el.secretsAutoToggle.checked = secretsState.auto; + el.secretsIntervalSelect.value = String(secretsState.interval); + el.secretsSearchInput.value = secretsState.search; applyTheme(appState.theme); persistPanel(); persistEmailPreferences(); persistLogPreferences(); + persistSecretPreferences(); wire(); renderWorkspace(); renderAll(); renderLogsAll(); + renderSecretsAll(); if (appState.panel === "logs") { ensureLogsReady(); + } else if (appState.panel === "secrets") { + ensureSecretsReady(); } else { refreshMessages("initial"); } window.setInterval(() => { renderLiveClock(); renderLogsLiveClock(); + renderSecretsLiveClock(); }, 1000); function wire() { @@ -1228,18 +1478,112 @@ function clientApp(config) { } }); + 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"); + } + } + }); + document.addEventListener("visibilitychange", () => { window.clearTimeout(state.timer); window.clearTimeout(logsState.timer); + window.clearTimeout(secretsState.timer); if (document.hidden) { renderStatus(); renderLogsStatus(); + renderSecretsStatus(); return; } - if (appState.panel === "logs" && logsState.auto) { - refreshLogs("visibility"); + if (appState.panel === "secrets") { + if (secretsState.auto) { + refreshSecrets("visibility"); + } else { + renderSecretsStatus(); + } + return; + } + + if (appState.panel === "logs") { + if (logsState.auto) { + refreshLogs("visibility"); + } else { + renderLogsStatus(); + } return; } @@ -1263,18 +1607,26 @@ function clientApp(config) { return; } + if (appState.panel === "secrets") { + refreshSecrets("keyboard"); + return; + } + refreshMessages("keyboard"); } }); updatePaneTopButtonVisibility(el.emailsContentPane, el.scrollToTopButton); updatePaneTopButtonVisibility(el.logsContentPane, el.logsScrollToTopButton); + updatePaneTopButtonVisibility(el.secretsContentPane, el.secretsScrollToTopButton); } async function setPanel(panel) { if (!panel || panel === appState.panel) { if (panel === "logs") { await ensureLogsReady(); + } else if (panel === "secrets") { + await ensureSecretsReady(); } renderWorkspace(); @@ -1287,19 +1639,24 @@ function clientApp(config) { if (panel === "logs") { await ensureLogsReady(); + } else if (panel === "secrets") { + await ensureSecretsReady(); } else if (!state.updatedAt && !state.loading) { await refreshMessages("panel"); } scheduleRefresh(); scheduleLogsRefresh(); + scheduleSecretsRefresh(); renderStatus(); renderLogsStatus(); + renderSecretsStatus(); } function renderWorkspace() { el.emailsPanel.hidden = appState.panel !== "emails"; el.logsPanel.hidden = appState.panel !== "logs"; + el.secretsPanel.hidden = appState.panel !== "secrets"; el.workspaceTabs.querySelectorAll("button[data-panel]").forEach((button) => { const active = button.dataset.panel === appState.panel; @@ -1321,6 +1678,15 @@ function clientApp(config) { } } + async function ensureSecretsReady() { + if (appState.secretsReady) { + return; + } + + await refreshSecrets("initial"); + appState.secretsReady = !secretsState.error; + } + async function refreshMessages(source) { if (state.loading) { return; @@ -1505,6 +1871,52 @@ function clientApp(config) { } } + 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 }); + } + } + function applyLogsFilter(shouldRenderList = true) { const search = logsState.search.trim().toLowerCase(); logsState.filtered = !search @@ -1514,6 +1926,15 @@ function clientApp(config) { 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 pruneState() { const ids = new Set(state.messages.map((message) => message.id)); state.openIds = new Set([...state.openIds].filter((id) => ids.has(id))); @@ -1537,6 +1958,17 @@ function clientApp(config) { 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]; + } + }); + } + function applyFilter(shouldRenderList = true) { const search = state.search.trim().toLowerCase(); state.filtered = !search @@ -1572,6 +2004,26 @@ function clientApp(config) { .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 haystack(message) { return [ message.subject, @@ -1592,6 +2044,20 @@ function clientApp(config) { 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 scheduleRefresh() { window.clearTimeout(state.timer); @@ -1612,6 +2078,16 @@ function clientApp(config) { 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 renderAll(options = {}) { const { renderList: shouldRenderList = true } = options; renderStats(); @@ -1635,6 +2111,17 @@ function clientApp(config) { renderLogsLiveClock(); } + function renderSecretsAll(options = {}) { + const { renderList: shouldRenderList = true } = options; + renderSecretsStats(); + renderSecretsFetch(); + renderSecretsStatus(); + if (shouldRenderList) { + renderSecretsList(); + } + renderSecretsLiveClock(); + } + function renderStats() { el.totalStat.textContent = String(state.messages.length); el.visibleStat.textContent = `${state.filtered.length} visible${state.search ? " after search" : ""}`; @@ -1670,6 +2157,15 @@ function clientApp(config) { 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 renderFetch() { if (state.loading) { el.fetchStat.textContent = "Refreshing..."; @@ -1716,6 +2212,29 @@ function clientApp(config) { 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 renderStatus() { el.statusChip.className = "status"; @@ -1788,6 +2307,42 @@ function clientApp(config) { 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 renderLiveClock() { if (!state.updatedAt) { el.updatedStat.textContent = "Not refreshed yet"; @@ -1808,6 +2363,16 @@ function clientApp(config) { 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 renderList() { el.banner.hidden = !state.error; el.banner.textContent = state.error ? `Refresh failed: ${state.error}` : ""; @@ -1857,6 +2422,27 @@ function clientApp(config) { 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 renderLogEvent(event) { return `
@@ -1880,6 +2466,69 @@ function clientApp(config) { `; } + 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 bindLogToggles() { el.logsList.querySelectorAll(".logEvent").forEach((details) => { details.addEventListener("toggle", () => { @@ -1898,6 +2547,25 @@ function clientApp(config) { }); } + 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 bindCardToggles() { el.list.querySelectorAll(".card").forEach((details) => { details.addEventListener("toggle", () => { @@ -2023,6 +2691,43 @@ function clientApp(config) { 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)}
`; + } + + return ` +
+
+ ${escapeHtml(valueState.label)} + ${ + valueState.versionId + ? `Version ${escapeHtml(valueState.versionId.slice(0, 8))}` + : "" + } + ${ + valueState.versionStages.length + ? `${escapeHtml(valueState.versionStages.join(", "))}` + : "" + } +
+
+ + +
+
+
${valueState.displayHtml}
+ `; + } + function metaCard(label, value) { return `
${escapeHtml(label)}
${escapeHtml(value)}
`; } @@ -2035,7 +2740,6 @@ function clientApp(config) { if (shouldOpen && !details.open) { details.open = true; - return; } if (!shouldOpen && details.open) { @@ -2053,6 +2757,31 @@ function clientApp(config) { 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 resolveAttachmentIcon(attachment) { const filename = String(attachment?.filename || "").toLowerCase(); const contentType = String(attachment?.contentType || "").toLowerCase(); @@ -2137,6 +2866,10 @@ function clientApp(config) { return logsState.events.find((event) => event.id === id); } + function getSecret(id) { + return secretsState.items.find((secret) => secret.id === id); + } + async function loadRaw(id) { if (state.raw[id]?.status === "loaded") { return state.raw[id].value; @@ -2168,6 +2901,72 @@ function clientApp(config) { } } + 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 copyText(value) { try { await navigator.clipboard.writeText(value); @@ -2204,9 +3003,19 @@ function clientApp(config) { el.logsStatusChip.textContent = message; } + function setSecretsStatus(message, tone) { + el.secretsStatusChip.className = "status"; + + if (tone) { + el.secretsStatusChip.classList.add(tone); + } + + el.secretsStatusChip.textContent = message; + } + function getInitialPanel() { const storedPanel = readStoredValue(PANEL_STORAGE_KEY); - return storedPanel === "logs" ? "logs" : "emails"; + return ["emails", "logs", "secrets"].includes(storedPanel) ? storedPanel : "emails"; } function getInitialTheme() { @@ -2267,6 +3076,14 @@ function clientApp(config) { }); } + function persistSecretPreferences() { + writeStoredJson(SECRET_PREFERENCES_STORAGE_KEY, { + search: secretsState.search, + auto: secretsState.auto, + interval: secretsState.interval + }); + } + function getStoredPreferences(key) { try { const rawValue = window.localStorage.getItem(key); @@ -2395,8 +3212,34 @@ function clientApp(config) { } } + 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 renderLogPreviewContent(event) { - const parsedLog = tryParseLogJson(event?.message); + const parsedLog = tryParseJsonText(event?.message); if (!parsedLog.ok) { return escapeHtml(event?.preview || "No preview available."); @@ -2408,7 +3251,7 @@ function clientApp(config) { } function renderLogBodyContent(message) { - const parsedLog = tryParseLogJson(message); + const parsedLog = tryParseJsonText(message); if (!parsedLog.ok) { return escapeHtml(formatLogMessage(message)); @@ -2417,7 +3260,7 @@ function clientApp(config) { return highlightJsonText(JSON.stringify(parsedLog.value, null, 2)); } - function tryParseLogJson(message) { + function tryParseJsonText(message) { const value = String(message || "").trim(); if (!value) { @@ -2485,4 +3328,5 @@ app.listen(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})`); }); diff --git a/_reference/localEmailViewer/package-lock.json b/_reference/localEmailViewer/package-lock.json index 2dec2be60..2f00ef450 100644 --- a/_reference/localEmailViewer/package-lock.json +++ b/_reference/localEmailViewer/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.1012.0", + "@aws-sdk/client-secrets-manager": "^3.1013.0", "express": "^5.1.0", "mailparser": "^3.7.4", "node-fetch": "^3.3.2" @@ -207,14 +208,64 @@ "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.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.21.tgz", - "integrity": "sha512-OTUcDX9Yfz/FLKbHjiMaP9D4Hs44lYJzN7zBcrK2nDmBt0Wr8D6nYt12QoBkZsW0nVMFsTIGaZCrsU9zCcIMXQ==", + "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.12", + "@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", @@ -232,12 +283,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.19.tgz", - "integrity": "sha512-33NpkQtmnsjLr9QdZvL3w8bjy+WoBJ+jY8JwuzxIq38rDNi1kwpBWW7Yjh+8bMlksd+ZAWW0fH4S/6OeoAdU5A==", + "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.21", + "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", @@ -248,12 +299,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.21.tgz", - "integrity": "sha512-xFke7yjbON4unNOG0TApQwz+o1LH5VhVLgWlUuiLRWNDyBfeHIFje2ck8qHybvJ8Fkm5m3SsN+pvHtVo6PGWlQ==", + "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.21", + "@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", @@ -269,19 +320,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.21.tgz", - "integrity": "sha512-fmJN7KhB7CoG65w9fC2LVOd2wZbR2d1yJIpZNe2J5CeDPu7nUHSmavuJAeGEoE3OL5UIBVPNhmK/fV/NQrs3Hw==", + "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.21", - "@aws-sdk/credential-provider-env": "^3.972.19", - "@aws-sdk/credential-provider-http": "^3.972.21", - "@aws-sdk/credential-provider-login": "^3.972.21", - "@aws-sdk/credential-provider-process": "^3.972.19", - "@aws-sdk/credential-provider-sso": "^3.972.21", - "@aws-sdk/credential-provider-web-identity": "^3.972.21", - "@aws-sdk/nested-clients": "^3.996.11", + "@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", @@ -294,13 +345,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.21.tgz", - "integrity": "sha512-ENU+YCiuQocQjfIf9bPxZ+ZY0wIBkl3SMH22optBQwy8UFpSfonHynXzGT27xQxer4cYTNOpwDqbfo57BusbpQ==", + "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.21", - "@aws-sdk/nested-clients": "^3.996.11", + "@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", @@ -313,17 +364,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.22.tgz", - "integrity": "sha512-VE6i8nkmrRyhKut7nnfCWRbdDf+CfyRr8ixSwdaPDguYlgvkAO2pHu9oK11XzbSuatB0io1ozI/vpYhelXn8Pg==", + "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.19", - "@aws-sdk/credential-provider-http": "^3.972.21", - "@aws-sdk/credential-provider-ini": "^3.972.21", - "@aws-sdk/credential-provider-process": "^3.972.19", - "@aws-sdk/credential-provider-sso": "^3.972.21", - "@aws-sdk/credential-provider-web-identity": "^3.972.21", + "@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", @@ -336,12 +387,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.19.tgz", - "integrity": "sha512-hjj5bFo4kf5/WzAMjDEFByVOMbq5gZiagIpJexf7Kp9nIDaGzhCphMsx03NCA8s9zUJzHlD1lXazd7MS+e03Lg==", + "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.21", + "@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", @@ -353,14 +404,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.21.tgz", - "integrity": "sha512-9jWRCuMZpZKlqCZ46bvievqdfswsyB2yPAr9rOiN+FxaGgf8jrR5iYDqJgscvk1jrbAxiK4cIjHv3XjIAWAhzQ==", + "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.21", - "@aws-sdk/nested-clients": "^3.996.11", - "@aws-sdk/token-providers": "3.1012.0", + "@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", @@ -372,13 +423,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.21.tgz", - "integrity": "sha512-ShWQO/cQVZ+j3zUDK7Kj+m7grPzQCVA2iaZdJ+hJTGvVH5lR32Ip/rgZZ+zBdH6D6wczP9Upa4NMXoqJdGpK1g==", + "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.21", - "@aws-sdk/nested-clients": "^3.996.11", + "@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", @@ -435,12 +486,12 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.22.tgz", - "integrity": "sha512-pZPNGWZVQvgUIO/P9PXZNz7ciq9mLYb/wQEurg3phKTa3DiBIunIRcgA0eBNwmog6S3oy0KR1bv4EJ4ld9A5sQ==", + "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.21", + "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", @@ -454,23 +505,23 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.11.tgz", - "integrity": "sha512-i7SwoSR4JB/79JoGDUACnFUQOZwXGLWNX35lIb1Pq72nUGlVV+RFZp+BLa8S+mog2pbXU9+6Kc5YwGiMi5bKhQ==", + "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.21", + "@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.22", + "@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.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", @@ -519,13 +570,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1012.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1012.0.tgz", - "integrity": "sha512-vzKwy020zjuiF4WTJzejx5nYcXJnRhHpb6i3lyZHIwfFwXG1yX4bzBVNMWYWF+bz1i2Pp2VhJbPyzpqj4VuJXQ==", + "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.21", - "@aws-sdk/nested-clients": "^3.996.11", + "@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", @@ -590,12 +641,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.8.tgz", - "integrity": "sha512-Kvb96TafGPLYo4Z2GRCzQTne77epXgiZEo0DDXwavzkWmgDV/1XD1tMA766gzRcHHFUraWsE+4T8DKtPTZUxgQ==", + "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.22", + "@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", @@ -615,9 +666,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.13.tgz", - "integrity": "sha512-I/+BMxM4WE/6xL0tyV7tAUDOAXmyw/va1oGr/eSly43HmLUcD1G+v96vEKAA8VoLcZ03ZQo/PWzjmN9zQErqPQ==", + "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", diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json index 377708878..67ea3fc6b 100644 --- a/_reference/localEmailViewer/package.json +++ b/_reference/localEmailViewer/package.json @@ -10,9 +10,10 @@ "keywords": [], "author": "", "license": "ISC", - "description": "LocalStack inspector for SES emails and CloudWatch logs", + "description": "LocalStack inspector for SES emails, CloudWatch logs, and Secrets Manager", "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.1012.0", + "@aws-sdk/client-secrets-manager": "^3.1013.0", "express": "^5.1.0", "mailparser": "^3.7.4", "node-fetch": "^3.3.2"