keymap-cli 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +29 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A values-free map of every secret you run, where it lives, and one command to rotate it everywhere.
4
4
 
5
+ ![keymap incident railway, then a dry-run rotate](docs/keymap-demo.svg)
6
+
5
7
  **It is a map, not a vault.** keymap never stores a secret value. The manifest and its state hold names, locations, timestamps and short salted hashes. Values move only between your existing CLIs (`gh`, `railway`, …) and the host, over stdin or a mode-600 temp file, never through a shell string or on a command line.
6
8
 
7
9
  Built after the OpenCode (Oct 16, 2026) and Railway (Oct 21, 2026) key thefts, when "rotate every variable" met "I don't remember where half of these are used".
@@ -15,6 +17,7 @@ npm i -g keymap-cli # or: npx keymap-cli
15
17
  cd your-project
16
18
  keymap init -s local:.env github:owner/repo railway:api/production
17
19
  # edit keymap.yaml: issuers, rotate_every, ignore list
20
+ # (init skips config-looking names such as APP_URL, UPLOAD_DIR, MERCHANT_ID; pass --all to include them)
18
21
  keymap audit # exit 1 on missing / drift / overdue / unmapped
19
22
  keymap mark OPENAI_API_KEY # you rotated it by hand today
20
23
  pbpaste | keymap rotate OPENAI_API_KEY # push a new key everywhere, hosts first, laptop last
@@ -47,7 +50,7 @@ INCIDENT railway: 4 secrets to rotate, most dangerous first
47
50
  [2] Database and storage credentials
48
51
  DATABASE_URL custom exposed at railway:api; also lives at local:.env
49
52
  [4] Package registry credentials
50
- NPM_TOKEN custom exposed at railway:worker; also lives at nowhere else
53
+ NPM_TOKEN custom exposed at railway:worker; lives nowhere else
51
54
  ```
52
55
 
53
56
  The order follows Railway's own disclosure guidance: AI keys, cloud and payments, databases, source control, package registries. `--walk` asks for each new value, pushes it everywhere, and ends with the list of anything still exposed.
package/dist/cli.js CHANGED
@@ -11150,7 +11150,7 @@ var supportedSinks = Object.keys(registry);
11150
11150
  var import_yaml = __toESM(require_dist(), 1);
11151
11151
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
11152
11152
  var MANIFEST_FILE = "keymap.yaml";
11153
- var ISSUERS = /* @__PURE__ */ new Set(["openai", "anthropic", "openrouter", "google", "stripe", "supabase", "cloudflare", "custom"]);
11153
+ var ISSUERS = /* @__PURE__ */ new Set(["openai", "anthropic", "openrouter", "google", "stripe", "supabase", "cloudflare", "resend", "telegram", "github", "custom"]);
11154
11154
  var SINKS = /* @__PURE__ */ new Set(["local", "github", "railway", "vercel", "fly", "cloudflare", "supabase"]);
11155
11155
  var ManifestError = class extends Error {
11156
11156
  };
@@ -11175,8 +11175,14 @@ function checkFields(kind, fields, path) {
11175
11175
  return out;
11176
11176
  }
11177
11177
  var NOISE = /^(NODE_ENV|PORT|HOST|HOSTNAME|PATH|HOME|TZ|LANG|LC_|CI|DEBUG|LOG_LEVEL|NEXT_PUBLIC_|VITE_|PUBLIC_|RAILWAY_|VERCEL_)/;
11178
+ var CREDENTIAL = /(SECRET|KEY|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|SIGNING|WEBHOOK|DATABASE|DIRECT_URL|SHADOW_URL|DSN|CONNECTION|REDIS|MONGO|POSTGRES|MYSQL)/;
11179
+ var CONFIG_SUFFIX = /(_URL|_URI|_HOST|_PORT|_DIR|_PATH|_FILE|_ID|_NAME|_FROM|_TO|_EMAIL|_PRICE|_AMOUNT|_LIMIT|_COUNT|_SIZE|_TIMEOUT|_INTERVAL|_REGION|_ZONE|_BUCKET|_VERSION|_MODE|_ENV|_FLAG|_ENABLED|_URL_PROD)$/;
11180
+ function looksLikeConfig(name) {
11181
+ if (CREDENTIAL.test(name)) return false;
11182
+ return CONFIG_SUFFIX.test(name);
11183
+ }
11178
11184
  function isIgnored(name, ignore = []) {
11179
- if (NOISE.test(name)) return true;
11185
+ if (NOISE.test(name) || looksLikeConfig(name)) return true;
11180
11186
  return ignore.some((g) => g.endsWith("*") ? name.startsWith(g.slice(0, -1)) : name === g);
11181
11187
  }
11182
11188
  function normaliseSink(raw, path) {
@@ -11376,6 +11382,9 @@ function guessIssuer(name) {
11376
11382
  if (n.includes("STRIPE")) return "stripe";
11377
11383
  if (n.includes("SUPABASE")) return "supabase";
11378
11384
  if (n.includes("CLOUDFLARE") || n.startsWith("CF_")) return "cloudflare";
11385
+ if (n.includes("RESEND")) return "resend";
11386
+ if (n.includes("TELEGRAM")) return "telegram";
11387
+ if (n.includes("GITHUB") || n.startsWith("GH_")) return "github";
11379
11388
  return "custom";
11380
11389
  }
11381
11390
  async function buildManifest(opts, ctx2) {
@@ -11463,6 +11472,19 @@ function manualChecklist(issuer, name) {
11463
11472
  return [
11464
11473
  "Delete the old key at https://openrouter.ai/settings/keys once traffic has moved (or set KEYMAP_OPENROUTER_MGMT_KEY to let keymap do this)."
11465
11474
  ];
11475
+ case "resend":
11476
+ return [
11477
+ `Open https://resend.com/api-keys and confirm the new ${name} is listed.`,
11478
+ "Delete the old key once every sink has the new one."
11479
+ ];
11480
+ case "telegram":
11481
+ return [
11482
+ `In Telegram, open @BotFather -> /revoke and pick the bot behind ${name}; the old token stops working at once, so push the new one everywhere first.`
11483
+ ];
11484
+ case "github":
11485
+ return [
11486
+ "GitHub -> Settings -> Developer settings -> tokens: delete the old token after every sink has the new one."
11487
+ ];
11466
11488
  case "custom":
11467
11489
  return [`Tell the service that consumes ${name} about the new value (its own config, or a peer app) and retire the old one.`];
11468
11490
  }
@@ -11714,6 +11736,10 @@ function tierOf(name, issuer) {
11714
11736
  if (/(NPM_|PYPI|CARGO_|NUGET|DOCKER|GHCR|REGISTRY)/.test(n)) return 4;
11715
11737
  return 5;
11716
11738
  }
11739
+ function elsewhere(it) {
11740
+ const rest = it.allSinks.filter((s) => !it.exposedAt.includes(s));
11741
+ return rest.length ? `also lives at ${rest.join(", ")}` : "lives nowhere else";
11742
+ }
11717
11743
  function planIncident(manifest, host, filter) {
11718
11744
  const items = [];
11719
11745
  for (const [name, spec] of Object.entries(manifest.secrets)) {
@@ -11738,7 +11764,7 @@ function formatPlan(host, items) {
11738
11764
  lines.push(`[${it.tier}] ${TIER_LABEL[it.tier]}`);
11739
11765
  last = it.tier;
11740
11766
  }
11741
- lines.push(` ${it.secret.padEnd(28)} ${it.issuer.padEnd(10)} exposed at ${it.exposedAt.join(", ")}; also lives at ${it.allSinks.filter((s) => !it.exposedAt.includes(s)).join(", ") || "nowhere else"}`);
11767
+ lines.push(` ${it.secret.padEnd(28)} ${it.issuer.padEnd(10)} exposed at ${it.exposedAt.join(", ")}; ${elsewhere(it)}`);
11742
11768
  }
11743
11769
  lines.push("", "Run `keymap incident <host> --walk` to rotate each one in turn, or `keymap rotate <SECRET>` individually.");
11744
11770
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keymap-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Values-free map of every secret you run, where it lives, and one command to rotate it everywhere.",
5
5
  "license": "MIT",
6
6
  "author": "Nekruz Kasimov",