{"_id":"@cachestudios/keys","_rev":"3-cd6d86d819b2d667c50672de3aa087fa","name":"@cachestudios/keys","dist-tags":{"latest":"0.2.1"},"versions":{"0.1.0":{"name":"@cachestudios/keys","version":"0.1.0","_id":"@cachestudios/keys@0.1.0","maintainers":[{"name":"jlviper44","email":"justin.m.lee.dev@gmail.com"}],"bin":{"keys":"dist/index.js"},"dist":{"shasum":"f944ba630d4ab8ae1499fa28cca915a25e8f8d86","tarball":"https://registry.npmjs.org/@cachestudios/keys/-/keys-0.1.0.tgz","fileCount":3,"integrity":"sha512-voTOHcYFN/A+ItwJGLnEF2RyKXSCGTXClDNLOq0AIMMXgxT31Aq1ss0I8E4NpNQz/Vy9wQ4eiy5pEMc+UnyHFA==","signatures":[{"sig":"MEQCID8Qpps4RFV8cAGnJSqDpD2nxRQ1Gu9u7TJmkxnxTWZ3AiAPWJ8feW/im8DgXdmN89werSzAwDol1R4qhS22q+8kkg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37214},"type":"module","gitHead":"34be976d9955e7507e4de7878f984b1e891ce719","scripts":{"build":"rm -rf skill && cp -R ../../skill skill && esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\"","typecheck":"tsc --noEmit"},"_npmUser":{"name":"jlviper44","email":"justin.m.lee.dev@gmail.com"},"_npmVersion":"11.6.0","directories":{},"_nodeVersion":"24.8.0","_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.25.0","typescript":"^7.0.2","@types/node":"^26.4.0","@cachestudios/keys-core":"*"},"_npmOperationalInternal":{"tmp":"tmp/keys_0.1.0_1788320749441_0.4184546167243586","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@cachestudios/keys","version":"0.2.0","_id":"@cachestudios/keys@0.2.0","maintainers":[{"name":"jlviper44","email":"justin.m.lee.dev@gmail.com"}],"bin":{"keys":"dist/index.js"},"dist":{"shasum":"470ba1ce7ad25d55dd3d3668ad491ab8ef758065","tarball":"https://registry.npmjs.org/@cachestudios/keys/-/keys-0.2.0.tgz","fileCount":3,"integrity":"sha512-A7zEFwkENVqG1BMCrNm7t5lY3eC8uZ80Hulf8PAkTLGhN8gXMfV1i85KPi3dhIP8FQN0zmVeD3QNdUh2TGv0xw==","signatures":[{"sig":"MEUCIQDWd1lKvPt6tksUtRp45xgnw15PrIHrQhGqAdn90vHuyAIgLkzyrdZJwvfLXHRhkUvP6aIMjcbkbpVgxcORnPq/iNY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":57243},"type":"module","gitHead":"f98660e4227ca1a47697ef19a2073b6dea11fa42","scripts":{"build":"rm -rf skill && cp -R ../../skill skill && esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\"","typecheck":"tsc --noEmit"},"_npmUser":{"name":"jlviper44","email":"justin.m.lee.dev@gmail.com"},"_npmVersion":"11.6.0","directories":{},"_nodeVersion":"24.8.0","_hasShrinkwrap":false,"devDependencies":{"esbuild":"^0.25.0","typescript":"^7.0.2","@types/node":"^26.4.0","@cachestudios/keys-core":"*"},"_npmOperationalInternal":{"tmp":"tmp/keys_0.2.0_1788366257611_0.01488617207103915","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@cachestudios/keys","version":"0.2.1","type":"module","bin":{"keys":"dist/index.js"},"scripts":{"build":"rm -rf skill && cp -R ../../skill skill && cp ../../README.md README.md && esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\"","typecheck":"tsc --noEmit"},"devDependencies":{"esbuild":"^0.25.0","typescript":"^7.0.2","@types/node":"^26.4.0","@cachestudios/keys-core":"*"},"_id":"@cachestudios/keys@0.2.1","gitHead":"6ca1356dd1c5210eae64b24718fa41cb073c41e4","description":"A keystore that holds exactly one file per project: that project's `.env.keys`.","_nodeVersion":"24.8.0","_npmVersion":"11.6.0","dist":{"integrity":"sha512-QzwGpKtsfidM4LJ/YdDpU0ugNthVUeJDSxV5iYntwT0VSTVVAOgE5XmysKHn6ceKVHPEmowSakxjLwC4NNwrmw==","shasum":"403baf244e7773f05af38450ba731cc37cb13987","tarball":"https://registry.npmjs.org/@cachestudios/keys/-/keys-0.2.1.tgz","fileCount":4,"unpackedSize":65429,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDhqNuq6lwJonnFc3aQtZ8QWAs+kjkUqtqaHqC583smvgIgS+fyURc4w+pFe0OYOYdAZ0ErIpWYvfTIiqd5jcwyLbQ="}]},"_npmUser":{"name":"jlviper44","email":"justin.m.lee.dev@gmail.com"},"directories":{},"maintainers":[{"name":"jlviper44","email":"justin.m.lee.dev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/keys_0.2.1_1788366726298_0.33219119546671716"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-02T03:45:49.286Z","modified":"2026-09-02T16:32:06.868Z","0.1.0":"2026-09-02T03:45:49.566Z","0.2.0":"2026-09-02T16:24:17.735Z","0.2.1":"2026-09-02T16:32:06.463Z"},"maintainers":[{"name":"jlviper44","email":"justin.m.lee.dev@gmail.com"}],"readme":"# keys\n\nA keystore that holds exactly one file per project: that project's `.env.keys`.\n\nEverything else about your secrets lives in git, encrypted. [dotenvx](https://dotenvx.com)\ndoes the cryptography and turns `.env` into committable ciphertext; `keys`\nsolves the one problem dotenvx leaves you with — **how the private key reaches\nthe next laptop.**\n\n```\ngit clone … && cd …\nnpx @cachestudios/keys pull      # writes .env.keys, resolved from the git remote\nnpm install && npm run dev       # working app\n```\n\nNo shared password manager entry, no \"can you DM me the env file\", no\n`.env.example` that drifts.\n\n---\n\n## Install\n\n```bash\nnpm i -g @cachestudios/keys     # or use npx, as below\nkeys login                       # opens a browser; sign in with Google\n```\n\nSign-in is an allowlist — an admin invites you once, and you then reach every\nproject. If you see *\"not on the allowlist\"*, that is the wall working; ask an\nadmin to add you at `/settings/users`.\n\n## The three things you will actually do\n\n**Set up a project that has secrets today**\n\n```bash\ncd my-app\nnpx @cachestudios/keys init\n```\n\nEncrypts your existing `.env` in place, fixes `.gitignore`, installs a\npre-commit hook, wraps your dev scripts, creates the project, and pushes the\nkey. Commit `.env` afterwards — it is ciphertext now.\n\n**Join a project someone else set up**\n\n```bash\nnpx @cachestudios/keys pull\n```\n\nNo arguments. It resolves the project from `git remote get-url origin`.\n\n**Change a secret**\n\n```bash\nnpx @cachestudios/keys set STRIPE_SECRET_KEY \"sk_live_…\"\nnpx @cachestudios/keys push -m \"rotate stripe key\"\n```\n\n---\n\n## The two files\n\n| File | Holds | Git |\n|---|---|---|\n| `.env` | `KEY=encrypted:BFY…` — ciphertext, plus plaintext key names | **committed** |\n| `.env.keys` | `DOTENV_PRIVATE_KEY=…` — decrypts the above | **never committed** |\n\n`keys` is the delivery mechanism for the second file. That is its whole job.\n\nA third file, `.env.decrypted`, appears only when you run `keys decrypt`. It is\ngitignored, blocked by the pre-commit hook, and meant to be deleted when you are\ndone reading it.\n\n## One environment, and the `LOCAL_` prefix\n\nThere is no `.env.production`, no `.env.local`, and no `--env` flag. `.env` holds\nthe values your deployed app runs on. A value that must differ on dev machines\ngoes in the key's **name**:\n\n```\nDATABASE_URL=postgres://prod-host/app         # deployed, and used locally…\nLOCAL_DATABASE_URL=postgres://localhost/dev   # …unless this shadows it\n```\n\n- `keys run -- <cmd>` maps `LOCAL_X` over `X`, then execs. Local dev uses this.\n- `keys env` prints deploy-safe JSON with every `LOCAL_*` stripped.\n\nOverrides are per-key, so unrelated values fall through. They are encrypted and\ncommitted like anything else, which means a fresh clone gets working local\ndefaults for free — so keep them machine-independent, never a path under your\nhome directory.\n\n## Commands\n\n| | |\n|---|---|\n| `keys login [--token ck_…]` | sign in; `--token` for SSH and containers |\n| `keys init` | set up this repo end to end |\n| `keys pull [--version N]` | write `.env.keys` here; `--version` to roll back |\n| `keys push [-m note]` | upload `.env.keys` as a new version |\n| `keys set KEY \"value\"` | encrypt one value into `.env` |\n| `keys get [KEY]` | one value, or all of them as JSON |\n| `keys decrypt [--stdout]` | write `.env.decrypted` — every value in the clear |\n| `keys import <file>` | bulk-encrypt a plaintext `KEY=value` file, then shred it |\n| `keys install-key` | put `DOTENV_PRIVATE_KEY` on GitHub Actions or Vercel |\n| `keys env` | deploy-safe JSON, `LOCAL_*` stripped |\n| `keys run -- <cmd>` | run with `LOCAL_*` applied |\n| `keys ls` | projects you can reach |\n| `keys rotate` | new keypair, re-encrypt, push |\n| `keys whoami` | who and which instance |\n\n`--project <slug>` overrides project resolution anywhere. `KEYS_URL` points at a\ndifferent instance.\n\n## Deploy targets\n\nTwo shapes, and picking the wrong one fails quietly.\n\n**Cloudflare Workers get the values, not the key.** A Worker has no `.env` at\nruntime, so `dotenvx run -- wrangler deploy` ships a Worker that 500s. `init`\nwrites the sync into your deploy script:\n\n```json\n\"deploy\": \"keys env | wrangler secret bulk && wrangler deploy\"\n```\n\n**GitHub Actions and Vercel get the key, not the values.**\n\n```bash\nnpx @cachestudios/keys install-key                    # detects the platform\nnpx @cachestudios/keys install-key --target vercel\n```\n\nOn GitHub, setting the secret is only half of it — a secret is not an\nenvironment variable, so the workflow needs an explicit mapping:\n\n```yaml\njobs:\n  build:\n    env:\n      DOTENV_PRIVATE_KEY: ${{ secrets.DOTENV_PRIVATE_KEY }}\n    steps:\n      - uses: actions/checkout@v4\n      - run: npm ci\n      - run: npx dotenvx run -- npm run build\n```\n\n`install-key` prints that block and names any workflow file that is missing it.\n\n**Builds never need `keys`.** They build from committed ciphertext plus one\nplatform secret. If `keys` is down, every deploy still ships — only a human\nonboarding a laptop is blocked.\n\n## Traps worth knowing before they bite\n\n- **dotenvx does not encrypt comments.** A commented-out old password ships to\n  git in plain text. `init` audits for this and refuses to continue if it finds\n  one; it is the most common real leak in a commit-your-env workflow.\n- **`dotenvx run` does not fail when it cannot decrypt.** It warns, **exits 0**,\n  and passes the raw `encrypted:…` string through as the value. A green CI build\n  is not evidence the key is wired — check a value's shape.\n- **`wrangler secret bulk` never deletes.** A var removed from `.env` stays live\n  on the Worker until `wrangler secret delete NAME`.\n- **`git add -f` bypasses `.gitignore`.** This is why the pre-commit hook greps\n  the staged files rather than trusting ignore rules.\n- **After `keys rotate`, nothing errors immediately.** The old private key still\n  decrypts the old ciphertext, so CI stays green until the next deploy reads a\n  re-encrypted value. Re-install the new key everywhere the old one lived,\n  straight away.\n\n## Using it with Claude\n\n`keys init` installs a skill into `.claude/skills/keys/`, so Claude Code in that\nrepo knows the workflows, the routing for what you actually say (\"I just cloned\nthis and nothing works\"), and the rules — including never printing a secret into\nthe chat transcript.\n\nAsk in plain language. \"Set up secrets for this project\", \"let me see the keys\",\n\"here is a file of keys, import them\", \"GitHub Actions can't see the env\" all\nroute to the right command.\n\nIf you paste a secret into a chat, it is burned — it lives in the transcript.\nRotate it.\n\n---\n\n## Working on this repo\n\n```\nKEYS-SPEC.md                the design and its rationale\nHANDOVER.md                 build state, settled decisions, verified findings\nskill/keys/SKILL.md         the Claude skill; init copies it into each repo\npackages/core/src/          crypto.ts, remote.ts — shared by server and CLI\npackages/server/            Next on Cloudflare (OpenNext) + D1\npackages/cli/src/           the CLI\n```\n\n**Read `HANDOVER.md` first.** It records decisions that are settled and findings\nthat were expensive to learn, so they are not rediscovered.\n\n```bash\nnpm install\nnpm test                                    # core: 16 tests\n\ncd packages/server\nnpx wrangler d1 migrations apply keys --local\nnpx next dev --port 3111\n\ncd packages/cli && npm run build            # esbuild, one file\n```\n\nTwo things that will trip you up, both recorded in `HANDOVER.md` with the\nevidence:\n\n- `packages/server`'s `build` script must be `next build`, never\n  `opennextjs-cloudflare build` — the latter invokes the package's own build\n  script and so re-enters itself forever, taking the machine down with it.\n- `@cachestudios/keys-core` must stay a **devDependency** of the CLI. esbuild\n  inlines it; as a real dependency it would 404 on every install.\n\nThis repo must never be `keys init`'d. Its own secrets cannot live in `keys`\nwithout a bootstrap trap you could not recover from during an outage.\n","readmeFilename":"README.md","description":"A keystore that holds exactly one file per project: that project's `.env.keys`."}