security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul

Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation

Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)

UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet

Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-19 12:44:31 +00:00
co-authored by Claude Opus 4.6
parent d1b48388fb
commit 1a74a930f7
77 changed files with 2485 additions and 966 deletions
+3 -2
View File
@@ -92,8 +92,9 @@ export const useAIPermissionsStore = defineStore('aiPermissions', () => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored) as AIContextCategory[]
return new Set(parsed.filter(c => AI_PERMISSION_CATEGORIES.some(cat => cat.id === c)))
const parsed: unknown = JSON.parse(stored)
if (!Array.isArray(parsed)) return new Set()
return new Set(parsed.filter((c: unknown) => typeof c === 'string' && AI_PERMISSION_CATEGORIES.some(cat => cat.id === c)) as AIContextCategory[])
}
} catch (e) {
if (import.meta.env.DEV) console.warn('Failed to load AI permissions from storage', e)
+9 -3
View File
@@ -63,7 +63,10 @@ const APPROVED_ORIGINS_KEY = 'neode_nostr_approved_origins'
function getApprovedOrigins(): Set<string> {
try {
const stored = localStorage.getItem(APPROVED_ORIGINS_KEY)
return stored ? new Set(JSON.parse(stored) as string[]) : new Set()
if (!stored) return new Set()
const parsed: unknown = JSON.parse(stored)
if (!Array.isArray(parsed)) return new Set()
return new Set(parsed.filter((s: unknown) => typeof s === 'string'))
} catch {
return new Set()
}
@@ -205,8 +208,11 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
try {
const stored = localStorage.getItem(appKey)
if (stored) {
const parsed = JSON.parse(stored) as { id?: string }
appIdentityId = parsed.id || null
const parsed: unknown = JSON.parse(stored)
if (typeof parsed === 'object' && parsed !== null && 'id' in parsed) {
const idVal = (parsed as Record<string, unknown>).id
appIdentityId = typeof idVal === 'string' ? idVal : null
}
}
} catch { /* ignore */ }
+11 -2
View File
@@ -23,8 +23,17 @@ export const useSpotlightStore = defineStore('spotlight', () => {
try {
const raw = localStorage.getItem(RECENT_ITEMS_KEY)
if (raw) {
const parsed = JSON.parse(raw) as RecentItem[]
recentItems.value = parsed.slice(0, MAX_RECENT_ITEMS)
const parsed: unknown = JSON.parse(raw)
if (!Array.isArray(parsed)) {
recentItems.value = []
return
}
recentItems.value = parsed
.filter((item: unknown) =>
typeof item === 'object' && item !== null &&
'id' in item && 'label' in item && 'type' in item && 'timestamp' in item
)
.slice(0, MAX_RECENT_ITEMS) as RecentItem[]
} else {
recentItems.value = []
}