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.
- package/README.md +4 -1
- package/dist/cli.js +29 -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
|
+

|
|
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;
|
|
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(", ")};
|
|
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");
|