fix(apikey): revoke must verify a row was actually updated
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

claudemesh apikey revoke <id> reported success even when the input
didn't match any row in mesh.api_key. The CLI's `apikey list` shows
truncated 8-char prefixes; users naturally paste those; broker did
exact-id match against meshApiKey.id; UPDATE affected 0 rows; old
revokeApiKey returned void so the CLI couldn't tell. Discovered via
end-to-end CLI smoke test against prod (roadmap validation pass).

Three-part fix:

- broker.revokeApiKey now returns
  { status: "revoked"|"not_found"|"not_unique"; id?, matches? } and
  accepts either the full id or a unique prefix (>=6 chars). Prefix
  matching is bounded to the caller's mesh and only succeeds if
  exactly one row matches; ambiguous prefixes return not_unique so
  we never silently revoke the wrong key.

- New WSApiKeyRevokeResponseMessage carries the structured status
  back to the CLI. Old apikey_revoke_ok type removed before being
  released — never shipped to users. The error path is no longer
  used for not_found/not_unique cases; the unified response carries
  both outcomes.

- CLI's apiKeyRevoke now resolves with { ok, id } | { ok: false,
  code, message }. runApiKeyRevoke surfaces the code/message and
  exits non-zero on failure (NOT_FOUND for missing, INVALID_ARGS
  for ambiguous prefix).

Net effect: pasting `claudemesh apikey revoke vq0fwjdX` now actually
revokes the key whose id starts with vq0fwjdX (or fails loud if 0
or >1 keys match). Verified against prod via the new branch's CLI
binary before commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-02 18:39:25 +01:00
parent 7d1538d743
commit 0f32529370
5 changed files with 121 additions and 15 deletions

View File

@@ -112,9 +112,21 @@ export async function runApiKeyRevoke(id: string, flags: ApiKeyFlags): Promise<n
return EXIT.INVALID_ARGS;
}
return await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
await client.apiKeyRevoke(id);
if (flags.json) console.log(JSON.stringify({ revoked: id }));
else render.ok("revoked", clay(id.slice(0, 8)));
const result = await client.apiKeyRevoke(id);
if (!result.ok) {
if (flags.json) {
console.log(JSON.stringify({ ok: false, code: result.code, message: result.message }));
} else {
render.err(`${result.code}: ${result.message}`);
}
return result.code === "not_found"
? EXIT.NOT_FOUND
: result.code === "not_unique"
? EXIT.INVALID_ARGS
: EXIT.INTERNAL_ERROR;
}
if (flags.json) console.log(JSON.stringify({ revoked: result.id }));
else render.ok("revoked", clay(result.id.slice(0, 8)));
return EXIT.SUCCESS;
});
}