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>
Two additions to Credential Sharing.
`logicsrc credentials rotate` (alias `logicsrc secrets rotate`) re-keys a
team vault: fresh DEK, re-sealed to the members who keep access, every
secret re-encrypted under it. Values do not change, so nothing that
consumes them breaks; what changes is that every wrapped key issued
before the rotation is dead. --active (the default) keeps only active
members and revokes the rest -- the "someone left" rotation. --all keeps
everyone who holds access, for plain hygiene. Dry run by default, like
`sync`.
The DEK is recoverable ONLY through the grants, so a half-applied
rotation makes a vault permanently unreadable by everyone. The whole next
state therefore goes to the server in one request and commits in one
transaction (new db.batch helper). The server also requires every
submitted fingerprint to equal the stored one: it cannot see values, but
it can prove a re-key did not swap any. Rotations that would leave the
caller ungranted, grant nobody, or cover the wrong secret count are
rejected before anything is written. GET /vaults/:id/grants now returns
publicKey and status so a client can re-seal in one pass instead of N+1
user lookups, and revocation finally deletes the grant row rather than
leaving one that reports access it no longer confers.
The sh1pt adapter is the fifth provider. It is the only one driven
through a CLI rather than HTTP, because sh1pt publishes
`sh1pt secret set|get|list|rm` as the interface to its vault and
documents no REST endpoint. Values go over the child's stdin, never argv
-- a secret in argv is readable by any user on the host via ps. Since
`sh1pt secret get` needs interactive confirmation it cannot be scripted,
so the adapter is write-only for values like github-secrets: a sync
target, never a source, no value-restoring rollback.
Tests drive a real fake sh1pt binary rather than a mocked execFile, which
is how the hang surfaced: with nothing to pipe, stdin was left open and
any subcommand that reads it would wait forever. It is now always closed.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`logicsrc login` defaulted to http://localhost:4010 — a dev origin that
doesn't exist on an installed machine, so the printed authorize URL went
nowhere. It now defaults to the hosted credentials app (apps/pwa), reads
the documented $LOGICSRC_API, and only reuses a stored apiUrl once that
identity has actually completed a login (which is how machines got stuck
pointing at localhost). Note logicsrc.com is the marketing site and has
no /cli routes.
The loopback flow is also unusable over SSH: redirect_uri is
http://127.0.0.1:<port>/callback, which resolves to the *browser's*
machine, not the CLI's. Added a device-authorization flow — the CLI
prints a short user_code, the human approves it from any browser:
POST /cli/device/code mint device_code + user_code (10 min TTL)
GET /cli/device approve page (login required; typo-tolerant)
POST /cli/device approve/deny (CSRF-guarded browser form)
POST /cli/device/token CLI polls -> lsk_ API key
device_code is stored sha256-hashed, single-use, with authorization_pending
/ slow_down / access_denied / expired_token poll semantics. The CLI picks
the flow automatically (SSH/CI/no-DISPLAY -> device), with --device/--web
to force it and a fallback to loopback against servers without /cli/device.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a `team` credential provider + team/member management so teammates can
share secrets by email instead of passing .env files over chat. Fully E2E:
the server only ever stores ciphertext, per-member sealed vault keys, and
public keys — it never sees a plaintext value or the vault DEK.
Plugin (@logicsrc/plugin-credential-sharing)
- crypto.ts: X25519 identity keys, per-vault DEK (secretbox), DEK sealed to
each member's pubkey (crypto_box_seal), value encrypt/decrypt (libsodium)
- identity.ts: local ~/.logicsrc/identity.json (0600) holding the device key
+ API token; never uploads the secret key
- client.ts: typed /api/credshare client
- providers/team.ts: `team:<slug>/<vault>` CredentialProvider (inspect,
readValues=decrypt, write=encrypt, rollback); fingerprints match env so
env<->team diffs line up
- fixes latent libsodium-wrappers ESM load bug (createRequire) here + in
github-secrets
Server (commandboard-api /api/credshare)
- zero-knowledge router: email-code auth, keys, teams, members, invites,
vaults, sealed grants, ciphertext secrets, audit; membership authz in app
- CredShareStore abstraction: in-memory (dev/tests) + Supabase (prod)
- Resend email transport for login codes + invites (no-op -> echoes locally)
- supabase migration: credshare_* tables, deny-by-default RLS
CLI
- real `logicsrc login` (email code -> token + key upload)
- `logicsrc teams create/list/invite/accept/members/vaults/grant/push/pull`
Web (logicsrc.com/teams + /teams/accept)
- management surface only (browser holds no private key, never decrypts):
login, view teams/members/vaults, invite, accept
Tests: crypto round-trip, server contract (invite->accept->push->grant->pull
+ authz boundaries), and a real HTTP+client+crypto E2E asserting the server
never holds plaintext. Full workspace build + tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>