logicsrc/plugins/credential-sharing/src/index.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

123 lines
3.6 KiB
TypeScript

import type { PluginDefinition } from "@logicsrc/plugin-core";
import { credentialSharingManifest } from "./manifest.js";
import { CredentialEngine, type CredentialEngineOptions } from "./engine.js";
import { listCredentialProviderManifests } from "./providers/index.js";
export const credentialSharingPlugin: PluginDefinition = {
manifest: credentialSharingManifest,
configDefaults: {
enabled: true,
default_policy: "approval_required_for_destructive",
credential_home: "${LOGICSRC_CREDENTIAL_HOME}"
},
routes: [
{ method: "GET", path: "/api/credentials/providers", capability: "credentials.providers.list" },
{ method: "GET", path: "/api/credentials/inspect", capability: "credentials.inspect" },
{ method: "POST", path: "/api/credentials/diff", capability: "credentials.diff" },
{ method: "POST", path: "/api/credentials/plans", capability: "credentials.plan" },
{ method: "POST", path: "/api/credentials/plans/:id/approve", capability: "credentials.approve" },
{ method: "POST", path: "/api/credentials/plans/:id/sync", capability: "credentials.sync" },
{ method: "POST", path: "/api/credentials/runs/:id/rollback", capability: "credentials.rollback" },
{ method: "GET", path: "/api/credentials/runs/:id/audit", capability: "credentials.audit.read" }
],
permissions: [
"credentials:inspect",
"credentials:diff",
"credentials:plan",
"credentials:approve",
"credentials:sync",
"credentials:rollback",
"credentials:audit:read"
],
tuiPanels: [{ id: "credential-sharing", title: "Credential Sharing" }]
};
/** Factory mirroring the LogicSRC Credential Sharing SDK spec. */
export function createCredentialEngine(options: CredentialEngineOptions = {}): CredentialEngine {
return new CredentialEngine(options);
}
/** Provider listing without constructing an engine (used by the CLI `providers` command). */
export function listCredentialProviders() {
return listCredentialProviderManifests();
}
export { credentialSharingManifest };
export { CredentialEngine, DEFAULT_CREDENTIAL_POLICY, endpointLabel } from "./engine.js";
export type { CredentialEngineOptions } from "./engine.js";
export {
credentialProviders,
credentialProviderRegistry,
listCredentialProviderManifests,
envProvider,
dopplerProvider,
railwayProvider,
githubSecretsProvider,
sshProvider,
teamProvider,
parseEnv,
applyEnv,
classifySshFile,
decodeSshFile,
defaultSshDirectory,
encodeSshFile,
isPassphraseless,
readSshDirectory,
secretNameForPath,
sshDirectory,
SSH_ENVELOPE_VERSION,
type SshFile,
type SshFileKind
} from "./providers/index.js";
export {
TeamClient,
TeamApiError,
type TeamClientOptions,
type RemoteUser,
type RemoteTeam,
type RemoteMember,
type RemoteVault,
type RemoteSecret,
type RemoteGrantRow
} from "./client.js";
export {
generateIdentityKeyPair,
generateVaultKey,
wrapVaultKey,
unwrapVaultKey,
encryptValue,
decryptValue,
publicKeyForSecret,
type IdentityKeyPair,
type SealedValue
} from "./crypto.js";
export {
loadOrCreateIdentity,
readIdentity,
saveIdentity,
updateIdentity,
requireAuth,
verifyIdentityIntegrity,
identityPath,
logicsrcHome,
defaultApiUrl,
envApiUrl,
resolveApiUrl,
DEFAULT_API_URL,
type LocalIdentity
} from "./identity.js";
export {
createFileCredentialStore,
createMemoryCredentialStore,
defaultCredentialHome,
type CredentialStore
} from "./store.js";
export { fingerprintValue, fingerprintsEqual } from "./fingerprint.js";
export {
planVaultRekey,
type RekeyPlan,
type RekeyPlanInput,
type RekeyMember,
type SealedSecret
} from "./rekey.js";
export * from "./types.js";