diff --git a/CLAUDE.md b/CLAUDE.md index 17b11438c..1213d74a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,4 +21,8 @@ Lint: `golangci-lint run` ## Security TODOs -- **Log Fields masking**: `LogEntry.Fields` (`map[string]any`) is exposed via WebSocket (`/miniapp/api/logs/ws`) and snapshots (`/miniapp/api/logs/snapshot`). If any code logs sensitive values (tokens, API keys, passwords) in Fields, they will be visible to Mini App users. Add a sanitizer in `RecentLogs()` and `wsLogs()` that masks values for keys matching patterns like `token`, `key`, `secret`, `password`, `authorization`. Track in: `pkg/logger/logger.go` (RecentLogs), `pkg/miniapp/miniapp.go` (wsLogs stream). +- ~~**Log Fields masking**~~: Done. `SanitizeFields()` in `pkg/logger/logger.go` masks keys matching `token`, `key`, `secret`, `password`, `authorization`, `credential`. Applied in `RecentLogs()` and `wsLogs()` stream. + +## Known Gaps + +- **Mini App log viewer has no frontend tests**: `renderLogs()` in `pkg/miniapp/static/index.html` is inline vanilla JS with no unit/E2E test coverage. Backend (Go) tests cover `RecentLogs`, `SanitizeFields`, and JSON serialization, but nothing verifies the JS rendering. This allowed the Fields display bug (fields sent but not rendered) to ship undetected. diff --git a/pkg/miniapp/static/index.html b/pkg/miniapp/static/index.html index 806de0fdb..2353c278f 100644 --- a/pkg/miniapp/static/index.html +++ b/pkg/miniapp/static/index.html @@ -755,6 +755,7 @@ .log-badge.debug { background: rgba(142,142,147,0.12); color: #8e8e93; } .log-comp { color: var(--link); flex-shrink: 0; font-size: 10px; } .log-msg { flex: 1; word-break: break-all; color: var(--text); } + .log-fields { color: var(--hint); font-size: 10px; } .log-actions { display: flex; gap: 8px; align-items: center; margin-top: 8px; } .log-snap-btn { padding: 6px 12px; @@ -1607,11 +1608,19 @@ function renderLogs() { var lvl = (e.level || 'info').toLowerCase(); var ts = e.timestamp ? e.timestamp.substring(11, 19) : ''; var comp = e.component ? '' + escapeHtml(e.component) + '' : ''; + var fields = ''; + if (e.fields && typeof e.fields === 'object') { + var parts = []; + for (var k in e.fields) { + if (e.fields.hasOwnProperty(k)) parts.push(k + '=' + e.fields[k]); + } + if (parts.length) fields = ' {' + escapeHtml(parts.join(', ')) + '}'; + } html += '
' + '' + ts + '' + '' + lvl + '' + comp + - '' + escapeHtml(e.message || '') + '' + + '' + escapeHtml(e.message || '') + fields + '' + '
'; } container.innerHTML = html;