logicsrc/packages/cli/src/ssh.test.ts
Anthony Ettinger 64ff854bd8 Add SSH keys and config to credential sharing
Private keys have lived as plaintext-on-disk files guarded only by a
passphrase. This puts them in the same end-to-end-encrypted vaults as
.env secrets, and adds an agent path so a machine can use a key without
ever writing one to its disk.

- `ssh` provider: ~/.ssh as a value bag. Files are picked by sniffing
  contents (PRIVATE KEY blocks, ssh-*/ecdsa-*/sk-* public keys) plus
  config, config.d/* and allowed_signers. known_hosts and
  authorized_keys are host-specific and access-granting, so they need
  an explicit --include.
- Each file is one secret carrying a JSON envelope of path, mode and
  body. The engine only hands write() the secrets that CHANGED, so a
  separate manifest secret would be absent whenever a key's contents
  change but the file list doesn't — self-describing values keep every
  restore total.
- `logicsrc secrets ssh push|pull|list|agent`, addressed by PERSON not
  project: the vault is ssh--<username>, which teams vaults reads as
  project ssh, env <username>. One teammate's keys never land in
  another's restore; sharing stays a deliberate teams grant.
- Both directions hold back anything that would overwrite a file that
  already differs, and say what they skipped. --force opts in. A
  restore onto a machine with its own keys is otherwise a way to lose
  them.
- Restores chmod each file back to its recorded mode; writeFileSync's
  mode applies only on create, so an existing world-readable key would
  otherwise stay world-readable. The adapter declares delete:false.
- push warns about passphrase-less private keys before they go up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:29:59 +00:00

42 lines
1.5 KiB
TypeScript

import { userInfo } from "node:os";
import { describe, expect, it } from "vitest";
import { keysToHoldBack, sshVaultUser, SSH_PROJECT } from "./ssh.js";
import { vaultName } from "./teams.js";
describe("ssh vault addressing", () => {
it("defaults to this machine's username", () => {
expect(sshVaultUser()).toBe(userInfo().username.toLowerCase());
});
it("slugifies a username into the vault charset", () => {
expect(sshVaultUser("Anthony_Young")).toBe("anthony-young");
expect(sshVaultUser("anthony@profullstack.com")).toBe("anthony-profullstack-com");
});
it("rejects a username with nothing usable in it", () => {
expect(() => sshVaultUser("!!!")).toThrow(/Could not work out a username/);
});
it("produces a vault name teams vaults can split back into project and env", () => {
expect(vaultName(SSH_PROJECT, sshVaultUser("anthony"))).toBe("ssh--anthony");
});
});
describe("overwrite hold-back", () => {
const entries = [
{ key: "SSH_CONFIG", op: "add" as const, destructive: false },
{ key: "SSH_ID_ED25519", op: "update" as const, destructive: true }
];
it("holds back files that already differ on the far side", () => {
expect(keysToHoldBack(entries, false)).toEqual(["SSH_ID_ED25519"]);
});
it("overwrites everything once --force is given", () => {
expect(keysToHoldBack(entries, true)).toEqual([]);
});
it("never holds back a file that is only being added", () => {
expect(keysToHoldBack([entries[0]], false)).toEqual([]);
});
});