fix(keys): match reconcile to current key name via shared prefix constant

Reconcile filtered app keys by the old "llmapikey:" prefix while mint now names
them "llmapikey/gh-<id>", so orphan/cost-leak detection silently missed every
new key. Centralize the prefix in lib/keys/key-name.js (KEY_NAME_PREFIX +
keyName) and use it in both mint and reconcile so they can't drift.
This commit is contained in:
2026-06-14 14:18:13 +07:00
parent b710537f63
commit e191a138e9
3 changed files with 27 additions and 4 deletions
+21
View File
@@ -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}`;
}
+2 -1
View File
@@ -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,
+4 -3
View File
@@ -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));