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:
co-authored by
Claude Opus 4.6
parent
e55fd3baf0
commit
589adb8b18
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user