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 Permalink 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;
}