The credential store resolved its base directory against process.cwd().
Running the CLI from inside a git checkout wrote `.logicsrc/credentials`
into that repo's working tree — a directory containing `vault/`, the one
place raw credential values touch disk — untracked, unignored, and one
`git add -A` from being committed. Two such directories were sitting in
unrelated repos on the machine this was found on.
A per-directory store is also the wrong shape for what the store is for.
It is the record of what was rotated and what the prior values were, and
a record that forks per project folder is several records that disagree.
There is one user, one identity, one vault.
Everything now hangs off a single logicsrcHome(): $LOGICSRC_HOME, else
$XDG_CONFIG_HOME/logicsrc, else ~/.config/logicsrc. The credential store,
the identity and the CLI config all read it rather than each deriving
their own answer — three separate derivations is how the vault ended up
somewhere the config never was.
~/.logicsrc is migrated rather than abandoned. It holds the X25519 secret
key, and losing that loses access to every team vault the member was ever
given, so it is moved on first use; a move that fails says so on stderr
instead of leaving someone silently logged out with a key still on disk
somewhere they were not told about. If the new directory already exists
it wins and the old one is left untouched, because two directories both
claiming to be the identity is how a login writes one and a read finds
the other.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The default was the apex, which runs the marketing app -- so every path the CLI
needs (/cli/device/code, /cli/device/token, /cli/authorize, /cli/token,
/api/me) returns 404 there. Point it at the host that actually serves them.
app.logicsrc.com is now a custom domain on the credentials service with a valid
certificate, verified live: /cli/device/code returns 200 and issues a real user
code, /api/me returns 401 rather than 404, which is routing working correctly
for an unauthenticated request.
The apex can forward these paths instead -- that is what the rewrites in
apps/logicsrc-web/next.config.ts do -- but that needs a second service deployed
to be true, while this needs nothing beyond the domain that already exists. The
rewrites stay useful as a convenience; they are no longer load-bearing.
$LOGICSRC_API still overrides, unchanged.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The default API origin was the generated Railway hostname
(logicsrc-credentials-production.up.railway.app), which leaked deployment
infrastructure into every login prompt and stored identity. Point it at the
production domain instead.
NOTE: logicsrc.com does not currently serve the credentials app's CLI routes —
/cli/device/code, /cli/device/token, /cli/authorize, /cli/token, and /api/me
live in apps/pwa (src/routes/cli.mjs), while the apex serves apps/logicsrc-web.
At time of writing all of those return 404 on logicsrc.com and 200 on the
Railway origin, so login will fail until the apex (or a subdomain) is pointed
at the pwa service. $LOGICSRC_API overrides the default in the meantime.
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>