diff --git a/lib/keys/key-name.js b/lib/keys/key-name.js new file mode 100644 index 0000000..83ff7af --- /dev/null +++ b/lib/keys/key-name.js @@ -0,0 +1,21 @@ +/** + * Single source of truth for the OpenRouter key `name` this app mints. Used by + * the mint path (to set the name) and the reconcile script (to recognize app + * keys). Keep both sides on this constant so they can never drift apart. + * + * The name is opaque and non-PII: a fixed prefix + the numeric, immutable GitHub + * id (never the mutable login). + */ + +/** Prefix on every key this app mints. Reconcile matches app keys by this. */ +export const KEY_NAME_PREFIX = "llmapikey/gh-"; + +/** + * Build the OpenRouter key name for a GitHub user. + * + * @param {string} githubUserId numeric GitHub provider_id + * @returns {string} + */ +export function keyName(githubUserId) { + return `${KEY_NAME_PREFIX}${githubUserId}`; +} diff --git a/lib/keys/mint-key.js b/lib/keys/mint-key.js index 328a45e..146086e 100644 --- a/lib/keys/mint-key.js +++ b/lib/keys/mint-key.js @@ -1,6 +1,7 @@ import "server-only"; import { last4 } from "@/lib/keys/key-format"; +import { keyName } from "@/lib/keys/key-name"; import * as repo from "@/lib/keys/api-keys-repository"; import { createKey, deleteKey } from "@/lib/openrouter/provisioning-client"; @@ -29,7 +30,7 @@ export async function mintAndPersist(reservedId, githubUserId) { let mint; try { mint = await createKey({ - name: `llmapikey/gh-${githubUserId}`, // opaque numeric id — no PII into OpenRouter logs + name: keyName(githubUserId), // opaque numeric id — no PII into OpenRouter logs limitUsd: numEnv("KEY_DAILY_LIMIT_USD", 10), resetPeriod: "daily", includeByok: true, diff --git a/scripts/reconcile-keys.js b/scripts/reconcile-keys.js index 36a89bc..68fac8a 100644 --- a/scripts/reconcile-keys.js +++ b/scripts/reconcile-keys.js @@ -2,8 +2,8 @@ * Reconcile OpenRouter provisioned keys against the DB registry. * * Reports: - * - Orphaned OpenRouter keys: minted by this app (name `llmapikey:*`) but with - * no matching DB row → a billable cost leak to revoke. + * - Orphaned OpenRouter keys: minted by this app (name starts with + * KEY_NAME_PREFIX) but with no matching DB row → a billable cost leak to revoke. * - Dangling DB rows: an active row whose key was deleted out-of-band. * - Stale pending rows: a reservation from an interrupted mint (>10 min old) * that still has no key — would block that user until reclaimed. @@ -13,6 +13,7 @@ */ import { getSql } from "../lib/db/postgres-client.js"; import { listKeys } from "../lib/openrouter/provisioning-client.js"; +import { KEY_NAME_PREFIX } from "../lib/keys/key-name.js"; async function main() { const sql = getSql(); @@ -24,7 +25,7 @@ async function main() { const dbHashes = new Set(dbRows.map((r) => r.openrouter_delete_hash).filter(Boolean)); const orHashes = new Set(orKeys.map((k) => k.hash)); - const appKeys = orKeys.filter((k) => typeof k.name === "string" && k.name.startsWith("llmapikey:")); + const appKeys = orKeys.filter((k) => typeof k.name === "string" && k.name.startsWith(KEY_NAME_PREFIX)); const orphans = appKeys.filter((k) => !dbHashes.has(k.hash)); const dangling = dbRows.filter((r) => r.openrouter_delete_hash && !orHashes.has(r.openrouter_delete_hash));