mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-05 06:05:28 +00:00
Add SSH keys and config to credential sharing (#139)
* 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> * Add worked examples to secrets and secrets ssh help Commander's usage line shows only the first alias, so `logicsrc secrets` — the spelling people actually type — was invisible in its own help. The examples carry it, alongside the flows worth copying: link/up/down, the ssh backup round trip, and a plan → dry-run → approve sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Advertise the ssh provider on the marketing page The marketing-drift contract failed the build because `ssh` shipped in the provider registry with no entry in MARKETING_PROOF -- which is the test working: it exists so a provider cannot ship while the pages people actually land on still describe the tool without it. The proof regex is `/~\/\.ssh|SSH key/` rather than a bare `/SSH/` on purpose. The provider grid renders every registry `name`, and this one is "Local SSH directory", so `/SSH/` would already be satisfied by the generated grid and the provider could ship with no copy written about it at all -- passing the test while failing its intent. Requiring the path or the phrase means a human wrote a sentence. That sentence is the new block in the credential-sharing band: ~/.ssh is a directory of files whose permission bits are load-bearing, not a set of KEY=VALUE lines, which is the part that makes this provider different from the other six. README already named ~/.ssh keys, so it needed no change. apps/logicsrc-web: 75/75 contract tests pass (was 74 passed, 1 failed). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1cfec322ac
commit
b1805d08e5
13 changed files with 939 additions and 10 deletions
|
|
@ -27,7 +27,7 @@ import { linkedDirectory, requireSecretsLink, writeSecretsLink } from "./secrets
|
|||
* only ever sees ciphertext and per-member wrapped vault keys.
|
||||
*/
|
||||
|
||||
function authedClient(): { client: TeamClient; identity: ReturnType<typeof requireAuth> } {
|
||||
export function authedClient(): { client: TeamClient; identity: ReturnType<typeof requireAuth> } {
|
||||
const identity = requireAuth();
|
||||
const client = new TeamClient({ apiUrl: resolveApiUrl(identity), token: identity.apiToken });
|
||||
return { client, identity };
|
||||
|
|
@ -386,7 +386,7 @@ export async function teamsGrantAction(slug: string, project: string, env: strin
|
|||
print({ granted: email, team: slug, project, env, vault }, format);
|
||||
}
|
||||
|
||||
function teamEndpoint(slug: string, vault: string): CredentialEndpoint {
|
||||
export function teamEndpoint(slug: string, vault: string): CredentialEndpoint {
|
||||
return { provider: "team", project: slug, config: vault };
|
||||
}
|
||||
|
||||
|
|
@ -428,7 +428,7 @@ export async function teamsPullAction(slug: string, project: string, envName: st
|
|||
print({ team: slug, project, env: envName, vault, applied, keys: run.results.map((r) => ({ key: r.key, op: r.op, applied: r.applied })) }, options.format);
|
||||
}
|
||||
|
||||
async function selectOne(label: string, values: string[]): Promise<string> {
|
||||
export async function selectOne(label: string, values: string[]): Promise<string> {
|
||||
const choices = [...new Set(values)].sort();
|
||||
if (choices.length === 0) throw new Error(`No ${label.toLowerCase()} options are available.`);
|
||||
if (choices.length === 1) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue