Mirror of github.com/Rootzreggae/keystrok (canonical on GitHub)
Find a file
Rootzreggae cd10d1d753
feat(keys): manual key registration backend + scan-time exposure linking (#16)
* feat(keys): manual key registration backend + scan-time exposure linking

The ledger's second door: POST /api/keys/register accepts one pasted key,
classifies it in-memory with the scanner's own patterns, and stores only a
masked preview plus salted hash (source 'manual', rotation clock from
registration via foundAt — no rotation-policy change needed).

- lib/scanner/classify.ts: pure classification + hash matching, testable
  without prisma; static error messages the route returns verbatim
- lib/manual-keys.ts: register, duplicate refusal (manual-vs-manual only,
  the honest limit is documented in the FAQ), tracked-candidate loading,
  and exposure attachment when a scan finds a registered key
- scanner core verifies raw matches against manual-key hashes in the one
  scope that still holds the value; both persistence paths turn a linked
  finding into an exposure event (git-blame date when available,
  earlier-is-worse, human attestation never overwritten) instead of a
  triage row, with activity + one-shot alert
- new Resend pattern (the key that exposed this gap couldn't classify)
- rate-limited 10/min/user; tests: classification, extraction identity,
  hash matching, static messages, manual-key rotation anchoring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(keys): register-a-key drawer, two-door empty state, registered-vs-found language

The UI half of manual key registration, from Nilson's single-key-workflow
handoff (section C):

- RegisterKeyDrawer: two steps (register -> confirm), live client-side
  classification on every keystroke (classify.ts split browser-safe, hash
  matching moved to scanner/tracked.ts), masked-preview typo check,
  env-line extraction with name prefill, single POST on confirm. 409
  renders the existing key as a pointer with a View key action; 429 keeps
  the paste, counts down Retry-After, re-enables itself. Name state resets
  when the extracted secret changes.
- Keys page: empty state gains the second door + tracked-since-birth hint;
  toolbar Register button; inline landed bar showing the server's
  rotationDueAt verbatim; manual rows wear a dashed 'manual - you' chip
  and 'registered Nd ago' (scanned rows keep 'found Nd ago').
- Language pass (task 3.2): drawer says Registered/by you, 'window: N days
  from registration', exposure 'none recorded'; timeline emits a
  'Registered - tracked since birth' event, no exposure window until
  evidence exists; radius no longer invents an exposure site from the
  synthetic 'registered by paste' location; scan-source exposure labeled
  'found in a scan'.
- Key drawer gains Delete for manual records (the no-edit-value promise
  needs a working delete); admin-gated server-side, 403 surfaced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spec: check off UI + security-battery tasks for manual-key-registration

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 23:21:31 +01:00
.claude chore: OpenSpec scaffolding (spec-driven changes for arc-sized work) 2026-07-13 09:30:48 +01:00
.github/workflows chore: retire the Codeberg mirror (#15) 2026-07-23 11:43:44 +00:00
app feat(keys): manual key registration backend + scan-time exposure linking (#16) 2026-07-25 23:21:31 +01:00
components feat(keys): manual key registration backend + scan-time exposure linking (#16) 2026-07-25 23:21:31 +01:00
docs feat(keys): manual key registration backend + scan-time exposure linking (#16) 2026-07-25 23:21:31 +01:00
lib feat(keys): manual key registration backend + scan-time exposure linking (#16) 2026-07-25 23:21:31 +01:00
openspec feat(keys): manual key registration backend + scan-time exposure linking (#16) 2026-07-25 23:21:31 +01:00
prisma feat(keys): manual key registration backend + scan-time exposure linking (#16) 2026-07-25 23:21:31 +01:00
public feat(landing): OpenGraph social card for link unfurls (#13) 2026-07-16 11:03:52 +01:00
scripts security: nonce-based CSP, dep patches, first-party analytics proxy (#14) 2026-07-22 14:58:25 +01:00
types feat(teams): Phase B+C — invites, roles UI, soft-remove, admin-only Team tab 2026-07-02 09:22:54 +01:00
.dockerignore feat(github): in-app GitHub App setup via Manifest flow; finish em-dash removal 2026-06-30 10:29:38 +01:00
.env.example fix(analytics): restore the Umami script, env-gated for self-hosters 2026-07-09 09:48:57 +01:00
.gitguardian.yaml chore(gitguardian): ignore the bundled compose default Postgres password 2026-07-02 04:24:22 +01:00
.gitignore chore: ignore local-only working artifacts now that this repo is home 2026-07-02 04:27:26 +01:00
.npmrc feat(github): in-app GitHub App setup via Manifest flow; finish em-dash removal 2026-06-30 10:29:38 +01:00
ai-declaration.md docs: add ai-declaration.md for the c/selfhosted [AIP] disclosure 2026-07-16 08:17:53 +01:00
CLAUDE.md Initial public release 2026-06-29 11:39:28 +01:00
DEPLOYMENT.md feat(github): in-app GitHub App setup via Manifest flow; finish em-dash removal 2026-06-30 10:29:38 +01:00
docker-compose.yml fix(team): make silent invite non-delivery legible for self-hosters 2026-07-09 08:14:20 +01:00
docker-entrypoint.sh fix(docker): give the Prisma CLI its own dependency closure 2026-07-13 17:49:00 +01:00
Dockerfile fix(docker): give the Prisma CLI its own dependency closure 2026-07-13 17:49:00 +01:00
eslint.config.mjs Initial public release 2026-06-29 11:39:28 +01:00
LICENSE Initial public release 2026-06-29 11:39:28 +01:00
middleware.ts security: nonce-based CSP, dep patches, first-party analytics proxy (#14) 2026-07-22 14:58:25 +01:00
next.config.ts security: nonce-based CSP, dep patches, first-party analytics proxy (#14) 2026-07-22 14:58:25 +01:00
package-lock.json security: patch @auth/core to 0.41.3 (three advisories) 2026-07-25 22:43:27 +01:00
package.json security: nonce-based CSP, dep patches, first-party analytics proxy (#14) 2026-07-22 14:58:25 +01:00
postcss.config.mjs Initial public release 2026-06-29 11:39:28 +01:00
README.md chore: retire the Codeberg mirror (#15) 2026-07-23 11:43:44 +00:00
tailwind.config.ts Initial public release 2026-06-29 11:39:28 +01:00
tsconfig.json Initial public release 2026-06-29 11:39:28 +01:00
vercel.json chore(vercel): never deploy the pr-assets branch 2026-07-09 11:02:55 +01:00

Keystrok

Find exposed API keys in your code, track them, and rotate them safely. Keystrok is a self-hostable security tool for developers who want to get on top of leaked secrets without handing their keys to a third party.

Docs: keystrok.dev/docs (also readable in-repo at docs/guide/).

Advisory, never automated. Keystrok ranks what actually needs attention and walks you through each rotation step, but it never rotates or revokes a key on its own. The irreversible actions stay in your hands. It also never claims to know a key's real age: every deadline is anchored to when a key was discovered, not a guessed creation date.

Keystrok home: what needs attention, ranked by urgency

What it does

  • Discovery: scan a GitHub repo (via a GitHub App) or a local folder for exposed secrets (AWS, Stripe, GitHub, observability platforms, and more). Findings are stored hashed/masked, never as plaintext.
  • Ledger: promote real findings to a tracked inventory. Each key gets a rotation deadline from its severity, counted from discovery.
  • Guided rotation: step-by-step, operator-gated walkthroughs. The revoke step is irreversible by confirmation.
  • Platform validation: connect a platform (Datadog, Grafana, Stripe, GitHub, …) to check whether a leaked key is still live. A dead key is far less urgent than a working one.
  • Bring-your-own AI assistant (optional): a chat that reasons over your key metadata (never the secret values) to answer "what should I rotate first?". Works with Anthropic, any OpenAI-compatible endpoint, or a local model via Ollama.
  • Teams: self-hosted, so one instance is one team. Invite members, gate the irreversible actions to admins, and keep a shared, attributed workspace.

Screenshots

The key ledger: liveness, severity, rotation window The ledger: every tracked key with its liveness, severity, and rotation window, one urgency signal per row. Key detail: incident timeline, liveness, provenance Key detail: where it was found, a masked preview, honest provenance, and a per-key incident timeline.
Guided, step-by-step rotation with an irreversible revoke step Guided rotation: run each step yourself; the revoke step is admin-gated and irreversible. Discovery: scan a source and triage findings Discovery: scan a Git source or local folder, then triage findings into the ledger.
The admin-only Members tab: invite, roles, remove Team: invite members, set roles, and manage access from an admin-only tab.

Stack

  • Next.js 15 (App Router) + React 19, TypeScript
  • NextAuth v5: magic-link (email) authentication
  • Prisma ORM on PostgreSQL
  • Tailwind CSS + a square, terminal-inspired design system (docs/DESIGN_SYSTEM.md)
  • Secrets encrypted at rest (AES-256-GCM); SSRF-guarded outbound calls

Quick start (self-host)

The fastest path is the bundled Docker stack, with app + Postgres + a local mail catcher, no external services:

cp .env.example .env
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" >> .env
echo "ENCRYPTION_KEY=$(openssl rand -base64 32)"  >> .env
echo "ALLOWED_EMAILS=you@example.com"             >> .env   # invite-only
docker compose up --build
# App: http://localhost:3001   Magic-link inbox: http://localhost:8025

Prefer a prebuilt image? docker compose pull && docker compose up uses the published ghcr.io/rootzreggae/keystrok instead of building. See DEPLOYMENT.md for the bring-your-own-infra path (managed Postgres + SMTP).

Note on email: the bundled mail catcher traps everything locally (that's the inbox at :8025), so solo use works with zero email setup, but team invites will never reach a real inbox. To invite teammates, set EMAIL_SERVER_* in .env to a real SMTP server (or RESEND_API_KEY); the Team page shows where invites are currently being delivered.

Local development

npm install
cp .env.example .env.local
npx prisma generate
npx prisma db push
npm run dev -- -p 3001    # http://localhost:3001

Key environment variables

Variable Purpose
DATABASE_URL PostgreSQL connection string
NEXTAUTH_URL / NEXTAUTH_SECRET App URL + session signing secret
ENCRYPTION_KEY AES-256-GCM key for encrypting stored credentials (openssl rand -base64 32)
EMAIL_SERVER_* / EMAIL_FROM SMTP for magic-link and invite email (a real server, or the bundled catcher)
ALLOWED_EMAILS Invite-only sign-in allowlist
CRON_SECRET Secret for the scheduled-job endpoints (repo re-scans, liveness checks)

Project layout

  • app/: Next.js App Router ((authenticated)/ routes, api/ handlers, auth/ pages)
  • lib/: auth, crypto, the scanner, rotation policy, integrations
  • components/: the UI component system
  • prisma/schema.prisma: database schema (source of truth)
  • docs/: design & API references

On how this is made

I'm a product designer who has spent years on developer and observability tools (among them Grafana's Frontend Observability and APM). I build Keystrok with AI as my engineering partner. The product, what it does, what it deliberately won't do (it never rotates a key on its own), and how it should feel to use, is mine. The implementation is a collaboration, written and reviewed with AI in the loop.

I mention the AI part plainly, not defensively. Judge the work, not the method: the self-host path is verified end to end, secrets are encrypted at rest, rotation is advisory and operator-gated, and the AI assistant only ever sees key metadata, never the secret values. Find a bug or a bad call? Open an issue. I'd rather fix it than defend it.

License

MIT: see LICENSE.