Skip to content

feat(cpa-api-keys): support key-policy plugin keys with source isolation - #443

Open
crossly wants to merge 1 commit into
Willxup:mainfrom
crossly:feat/plugin-key-policy-support
Open

feat(cpa-api-keys): support key-policy plugin keys with source isolation#443
crossly wants to merge 1 commit into
Willxup:mainfrom
crossly:feat/plugin-key-policy-support

Conversation

@crossly

@crossly crossly commented Aug 22, 2026

Copy link
Copy Markdown

Problem

API keys issued by the cpa-plugin-key-policy CPA plugin never appear in per-key statistics / rankings:

  • Usage events from plugin keys are ingested correctly (usage_events.api_group_key carries the plugin key id, e.g. kath), and overview aggregations include them.
  • But cpa_api_keys is synced from CPA's /v0/management/api-keys with full-replace semantics (SyncCPAAPIKeys): any row not in that list is soft-deleted. Plugin keys are never in CPA's core api-keys, so a manually added row cannot survive even one sync cycle.
  • Ranking / usage-analysis queries join cpa_api_keys ... AND is_deleted = 0, so plugin keys are permanently invisible in the UI.

Solution

Opt-in support for key-policy plugin keys, behind KEY_POLICY_SYNC_ENABLED (default false — zero behavior change when off):

  • New column cpa_api_keys.source (cpa / plugin, migration 20260822_add_cpa_api_key_source, existing rows backfilled to cpa).
  • SyncCPAAPIKeys now only soft-deletes source='cpa' rows; plugin rows are untouched by the CPA full-replace sync.
  • New client call FetchKeyPolicyPluginKeysGET {CPA_BASE_URL}/v0/management/plugins/cpa-key-policy/keys (same management Bearer token; only enabled keys are kept).
  • New SyncPluginAPIKeys: upserts plugin keys by their id (which is exactly what usage events carry in api_group_key), display_key uses the plugin's key_preview (falls back to id), key_alias is filled from the plugin key's name only when locally empty (user-edited aliases are never overwritten). Plugin keys removed from the plugin side are soft-deleted — but only when the plugin fetch succeeded; on failure, local state is left untouched (same semantics as the CPA key sync).
  • Plugin sync runs only after a successful CPA key sync, so a CPA-side failure can't cascade.

Security

cpa_api_keys doubles as the credential store for api-key viewer login (apiKeyLoginFindActiveCPAAPIKeyByValue). Plugin key ids are short, human-meaningful strings (e.g. kath) — storing them as login-able key values would be a weak-password equivalent. FindActiveCPAAPIKeyByValue therefore excludes source='plugin' rows: plugin keys are visible in stats but cannot be used to log in.

No plaintext plugin secrets are handled: the plugin's management API exposes only id/name/key_preview (the raw key is shown once at creation), and none is needed for attribution.

Tests

  • CPA full-replace sync no longer soft-deletes plugin rows.
  • Plugin sync upsert / soft-delete / restore semantics; fetch failure preserves state.
  • Plugin key login rejected (401) at repository, service, and API handler level; CPA key login still works.
  • Flag disabled → zero behavior change (plugin endpoint never called, no plugin rows).
  • Migration test (column add, backfill, idempotency); client tests (Bearer header, enabled filter, non-2xx).

go build ./..., go vet ./..., go test ./... all green.

Notes

Verified in production against CLIProxyAPI Plus v7.2.127 + cpa-key-policy v0.4.4: plugin key appears in rankings, survives CPA restarts and metadata sync cycles, and new usage aggregates correctly.

- add cpa_api_keys.source column (cpa/plugin) with backfill migration
- syncCPA/api-keys replacement now only soft-deletes source=cpa rows
- add key-policy plugin keys fetch (GET /v0/management/plugins/cpa-key-policy/keys)
- add SyncPluginAPIKeys upsert/soft-delete scoped to source=plugin rows
- exclude plugin rows from api-key login (short ids are guessable)
- gate plugin sync behind KEY_POLICY_SYNC_ENABLED (default off)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant