diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 625fe69780..22cdd5900b 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -22,6 +22,7 @@ import { showConflict, showDiff, extractNativeTriggerInfo, + redactEncryptionKey, } from "../../types.ts"; import { downloadZip } from "./pull.ts"; import { runLint, printReport, checkMissingLocks } from "../lint/lint.ts"; @@ -3095,16 +3096,22 @@ function prettyChanges( ), ); } else if (change.name === "edited") { + const changeType = getTypeStrFromPath(change.path); log.info( colors.yellow( - `~ ${getTypeStrFromPath(change.path)} ` + + `~ ${changeType} ` + displayPath + colors.gray(wsNote) + (change.codebase ? ` (codebase changed)` : ""), ), ); if (change.before != change.after) { - if (change.path.endsWith(".yaml")) { + if (changeType === "encryption_key") { + showDiff( + redactEncryptionKey(change.before), + redactEncryptionKey(change.after), + ); + } else if (change.path.endsWith(".yaml")) { try { showDiff( yamlStringify( diff --git a/cli/src/types.ts b/cli/src/types.ts index e0d45adb66..75bad535c8 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -129,11 +129,46 @@ export function showDiff(local: string, remote: string) { export function showConflict(path: string, local: string, remote: string) { log.info(colors.yellow(`- ${path}`)); - showDiff(local, remote); + let isEncryptionKey = false; + try { + isEncryptionKey = getTypeStrFromPath(path) === "encryption_key"; + } catch { + // ignore + } + if (isEncryptionKey) { + showDiff(redactEncryptionKey(local), redactEncryptionKey(remote)); + } else { + showDiff(local, remote); + } log.info("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m"); log.info("\n"); } +// Reveal only the first 5 chars of the key so a rotation is still visible in +// the diff (different prefixes), without leaking the whole secret to stdout. +// The remaining chars are replaced with `*`, preserving length so the diff +// keeps showing whether the key length changed. +export function redactEncryptionKey(content: string): string { + if (!content) return content; + // The encryption_key payload is JSON-encoded (a quoted string). Parse it so + // we redact the key value itself, then re-serialize to JSON to preserve the + // file's shape; fall back to raw redaction if parsing fails. + try { + const parsed = JSON.parse(content); + if (typeof parsed === "string") { + return JSON.stringify(redactString(parsed)); + } + } catch { + // not JSON — treat content as the raw key + } + return redactString(content); +} + +function redactString(s: string): string { + if (s.length <= 5) return s; + return s.slice(0, 5) + "*".repeat(s.length - 5); +} + /** * Pushes an object to the workspace server based on its type * @param workspace - The workspace ID to push to