logicsrc/plugins/credential-sharing/src/fingerprint.ts
Anthony Ettinger cf73fe5af2 feat(credential-sharing): implement the Credential Sharing OpenSpec (M1-M3)
New @logicsrc/plugin-credential-sharing: a provider-neutral secret-sync engine
with env/.env, Doppler, Railway, and GitHub Secrets adapters behind one
CredentialProvider contract.

- engine: inspect -> diff -> plan -> approve -> sync -> rollback -> audit/export
- dry-run is the default for sync; --approve writes; destructive changes gated
- fingerprint-based diffs (salted SHA-256); raw values never printed or stored in
  plans/runs/audit; rollback pre-image kept in a 0600 .logicsrc vault (gitignored)
- github-secrets is write-only for values (sealed-box via libsodium), so it cannot
  be a sync source or value-restoring rollback target
- CLI: real `logicsrc credentials <providers|inspect|diff|plan|approve|sync|
  rollback|audit|export>` (replaces the prior stub)
- 4 JSON schemas registered in @logicsrc/validators
- flip logicsrc.com/credential-sharing band from coming-soon to available
- 37 tests pass; full env->env lifecycle verified; artifacts schema-validate

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:24:30 +00:00

49 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, string> {
const output: Record<string, string> = {};
for (const key of Object.keys(values)) {
output[key] = REDACTED;
}
return output;
}