import { createHash, timingSafeEqual } from "node:crypto"; import type { CredentialValueBag, CredentialKey } from "./types.js"; /** * Deterministic, value-revealing-resistant fingerprint of a secret value. * * A salted SHA-256 truncated to 16 hex chars. The salt is a fixed domain * separator so the same value fingerprints identically across machines (so two * endpoints can be diffed) while a fingerprint alone does not trivially expose * short/low-entropy values to a casual reader. This is an integrity/equality * marker, not a password hash — never treat it as a way to store secrets. */ const FINGERPRINT_DOMAIN = "logicsrc.credential.fingerprint.v1"; export function fingerprintValue(value: string): string { return createHash("sha256").update(FINGERPRINT_DOMAIN).update("").update(value, "utf8").digest("hex").slice(0, 16); } export function fingerprintsEqual(a: string | undefined, b: string | undefined): boolean { if (!a || !b || a.length !== b.length) { return false; } return timingSafeEqual(Buffer.from(a), Buffer.from(b)); } /** Build redacted keys (names + fingerprints) from a raw value bag. */ export function keysFromValues(values: CredentialValueBag): CredentialKey[] { return Object.keys(values) .sort() .map((name) => ({ name, present: true, fingerprint: fingerprintValue(values[name]) })); } /** Build name-only keys for write-only providers (no values readable). */ export function keysFromNames(names: string[]): CredentialKey[] { return [...names] .sort() .map((name) => ({ name, present: true })); } const REDACTED = "[redacted]"; /** Replace every value in a bag with a redaction marker (for safe previews). */ export function redactBag(values: CredentialValueBag): Record { const output: Record = {}; for (const key of Object.keys(values)) { output[key] = REDACTED; } return output; }