fix: alpha release hardening — onboarding, security, and ISO build

- Convert "Choose Your Path" screen to informative (read-only cards)
- Harden "Choose Your Setup" (gray out Coming Soon options, auto-select Fresh Start)
- Auto-fetch DID on mount with retry and auto-advance after success
- Improve backup download for mobile compatibility
- Add retry logic to verify step with graceful skip option
- Route verify → done → login for complete onboarding flow
- Add AIUI install confirmation via custom event (SEC-001)
- Add file path whitelist for AIUI file access (SEC-002)
- Add log redaction for container logs sent to AIUI (SEC-003)
- Add Secure flag to session cookie in production (SEC-004)
- Fix ISO build script to handle zstd compression errors gracefully
- Sync archipelago.service from live server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-06 13:00:28 +00:00
co-authored by Claude Opus 4.6
parent e55fd3baf0
commit 589adb8b18
10 changed files with 252 additions and 198 deletions
+82 -15
View File
@@ -150,20 +150,49 @@ export class ContextBroker {
} satisfies ArchyActionResponse)
return
}
appStore.installPackage(params.appId, params.marketplaceUrl, params.version).then(() => {
this.postToIframe({
type: 'action:response',
id,
success: true,
} satisfies ArchyActionResponse)
}).catch((err: Error) => {
this.postToIframe({
type: 'action:response',
id,
success: false,
error: err.message,
} satisfies ArchyActionResponse)
})
// Capture values for use in closure
const appId = params.appId
const marketplaceUrl = params.marketplaceUrl
const version = params.version
// Emit event for UI confirmation instead of installing directly
window.dispatchEvent(new CustomEvent('aiui:install-request', {
detail: { requestId: id, appId, marketplaceUrl, version },
}))
{
const broker = this
const responseHandler = (e: Event) => {
const detail = (e as CustomEvent).detail as { requestId: string; confirmed: boolean }
if (detail.requestId !== id) return
window.removeEventListener('aiui:install-response', responseHandler)
if (detail.confirmed) {
appStore.installPackage(appId, marketplaceUrl, version).then(() => {
broker.postToIframe({
type: 'action:response',
id,
success: true,
} satisfies ArchyActionResponse)
}).catch((err: Error) => {
broker.postToIframe({
type: 'action:response',
id,
success: false,
error: err.message,
} satisfies ArchyActionResponse)
})
} else {
broker.postToIframe({
type: 'action:response',
id,
success: false,
error: 'User declined the installation',
} satisfies ArchyActionResponse)
}
}
window.addEventListener('aiui:install-response', responseHandler)
setTimeout(() => {
window.removeEventListener('aiui:install-response', responseHandler)
}, 60000)
}
return
}
error = 'Missing required parameters (appId, marketplaceUrl, version)'
@@ -497,6 +526,27 @@ export class ContextBroker {
}
}
private static readonly ALLOWED_FILE_DIRS = [
'/var/lib/archipelago/',
'/var/log/',
'/opt/archipelago/',
'/home/archipelago/',
]
private static readonly SENSITIVE_PATH_PATTERNS = [
'id_rsa', 'id_ed25519', 'private', 'secret', 'password',
'seed', '.env', 'wallet', 'macaroon', 'tls.key', 'tls.cert',
'credentials', 'keystore', 'mnemonic',
]
private isPathAllowed(path: string): boolean {
const normalized = path.replace(/\/+/g, '/').replace(/\.\.\//g, '')
const inAllowedDir = ContextBroker.ALLOWED_FILE_DIRS.some(dir => normalized.startsWith(dir))
if (!inAllowedDir) return false
const lower = normalized.toLowerCase()
return !ContextBroker.SENSITIVE_PATH_PATTERNS.some(pattern => lower.includes(pattern))
}
private async handleReadFileAction(id: string, path?: string) {
const perms = useAIPermissionsStore()
if (!perms.isEnabled('files')) {
@@ -507,6 +557,10 @@ export class ContextBroker {
this.postToIframe({ type: 'action:response', id, success: false, error: 'Missing path parameter' } satisfies ArchyActionResponse)
return
}
if (!this.isPathAllowed(path)) {
this.postToIframe({ type: 'action:response', id, success: false, error: 'Access denied: path is outside allowed directories or contains sensitive patterns' } satisfies ArchyActionResponse)
return
}
try {
if (!fileBrowserClient.isAuthenticated) {
const ok = await fileBrowserClient.login()
@@ -538,9 +592,10 @@ export class ContextBroker {
const lines = Math.min(parseInt(linesStr || '50', 10) || 50, 200)
try {
const logs = await rpcClient.call<string[]>({ method: 'container-logs', params: { app_id: appId, lines } })
const redactedLogs = logs.map(line => ContextBroker.redactLogLine(line))
this.postToIframe({
type: 'action:response', id, success: true,
data: { appId, lines: logs, count: logs.length },
data: { appId, lines: redactedLogs, count: redactedLogs.length },
} as ArchyActionResponse)
} catch (err) {
this.postToIframe({
@@ -550,6 +605,18 @@ export class ContextBroker {
}
}
private static redactLogLine(line: string): string {
// Redact RPC passwords (e.g., rpcpassword=xxx)
let redacted = line.replace(/(?:rpcpassword|rpcauth|password|passwd|secret|token|apikey|api_key|macaroon)[\s]*[=:]\s*\S+/gi, '$&'.replace(/[=:]\s*\S+/, '=[REDACTED]'))
// More targeted: key=value patterns
redacted = redacted.replace(/((?:password|secret|token|apikey|api_key|macaroon|rpcpassword|rpcauth)\s*[=:]\s*)\S+/gi, '$1[REDACTED]')
// Redact long hex strings (>32 chars, likely private keys)
redacted = redacted.replace(/\b[0-9a-fA-F]{64,}\b/g, '[REDACTED_KEY]')
// Redact base64 macaroon values (long base64 strings)
redacted = redacted.replace(/\b[A-Za-z0-9+/]{64,}={0,2}\b/g, '[REDACTED_TOKEN]')
return redacted
}
private postToIframe(msg: ArchyResponse) {
if (!this.iframe.value?.contentWindow) return
this.iframe.value.contentWindow.postMessage(msg, this.allowedOrigin)