From fbe140c231a1526bfc0b48d23331cb17da527d2e Mon Sep 17 00:00:00 2001 From: guanbear <123guan@gmail.com> Date: Sat, 30 May 2026 00:16:53 +0800 Subject: [PATCH] Add self-service API key usage status (#2908) Integrated into release/v3.8.6 --- docs/bdd/self-service-api-key-usage.feature | 129 +++++++ .../self-service-api-key-usage/proposal.md | 65 ++++ .../specs/api-key-self-service-usage/spec.md | 198 ++++++++++ .../self-service-api-key-usage/tasks.md | 71 ++++ ...05-29-self-service-api-key-usage-design.md | 348 ++++++++++++++++++ .../api-manager/ApiManagerPageClient.tsx | 131 ++++++- .../dashboard/api-manager/apiManagerScopes.ts | 54 +++ src/app/api/keys/route.ts | 4 +- src/app/api/usage/budget/route.ts | 7 + src/app/api/v1/me/status/route.ts | 46 +++ src/i18n/messages/ar.json | 9 + src/i18n/messages/az.json | 9 + src/i18n/messages/bg.json | 9 + src/i18n/messages/bn.json | 9 + src/i18n/messages/cs.json | 9 + src/i18n/messages/da.json | 9 + src/i18n/messages/de.json | 9 + src/i18n/messages/en.json | 6 + src/i18n/messages/es.json | 9 + src/i18n/messages/fa.json | 9 + src/i18n/messages/fi.json | 9 + src/i18n/messages/fr.json | 9 + src/i18n/messages/gu.json | 9 + src/i18n/messages/he.json | 9 + src/i18n/messages/hi.json | 9 + src/i18n/messages/hu.json | 9 + src/i18n/messages/id.json | 9 + src/i18n/messages/in.json | 9 + src/i18n/messages/it.json | 9 + src/i18n/messages/ja.json | 9 + src/i18n/messages/ko.json | 9 + src/i18n/messages/mr.json | 9 + src/i18n/messages/ms.json | 9 + src/i18n/messages/nl.json | 9 + src/i18n/messages/no.json | 9 + src/i18n/messages/phi.json | 9 + src/i18n/messages/pl.json | 9 + src/i18n/messages/pt-BR.json | 9 + src/i18n/messages/pt.json | 9 + src/i18n/messages/ro.json | 9 + src/i18n/messages/ru.json | 9 + src/i18n/messages/sk.json | 9 + src/i18n/messages/sv.json | 9 + src/i18n/messages/sw.json | 9 + src/i18n/messages/ta.json | 9 + src/i18n/messages/te.json | 9 + src/i18n/messages/th.json | 9 + src/i18n/messages/tr.json | 9 + src/i18n/messages/uk-UA.json | 9 + src/i18n/messages/ur.json | 9 + src/i18n/messages/vi.json | 9 + src/i18n/messages/zh-CN.json | 14 +- .../075_api_key_self_service_usage_scopes.sql | 51 +++ src/lib/usage/apiKeySelfService.ts | 289 +++++++++++++++ src/shared/constants/selfServiceScopes.ts | 20 + src/shared/validation/schemas.ts | 4 +- tests/unit/api-key-scope-validation.test.ts | 56 +++ tests/unit/api-key-self-service.test.ts | 220 +++++++++++ .../api-manager-scope-preservation.test.ts | 52 +++ tests/unit/api/v1-me-status-route.test.ts | 27 ++ tests/unit/budget-route-auth.test.ts | 36 ++ 61 files changed, 2179 insertions(+), 9 deletions(-) create mode 100644 docs/bdd/self-service-api-key-usage.feature create mode 100644 docs/openspec/changes/self-service-api-key-usage/proposal.md create mode 100644 docs/openspec/changes/self-service-api-key-usage/specs/api-key-self-service-usage/spec.md create mode 100644 docs/openspec/changes/self-service-api-key-usage/tasks.md create mode 100644 docs/specs/2026-05-29-self-service-api-key-usage-design.md create mode 100644 src/app/(dashboard)/dashboard/api-manager/apiManagerScopes.ts create mode 100644 src/app/api/v1/me/status/route.ts create mode 100644 src/lib/db/migrations/075_api_key_self_service_usage_scopes.sql create mode 100644 src/lib/usage/apiKeySelfService.ts create mode 100644 src/shared/constants/selfServiceScopes.ts create mode 100644 tests/unit/api-key-scope-validation.test.ts create mode 100644 tests/unit/api-key-self-service.test.ts create mode 100644 tests/unit/api-manager-scope-preservation.test.ts create mode 100644 tests/unit/api/v1-me-status-route.test.ts create mode 100644 tests/unit/budget-route-auth.test.ts diff --git a/docs/bdd/self-service-api-key-usage.feature b/docs/bdd/self-service-api-key-usage.feature new file mode 100644 index 0000000000..2b976c79fa --- /dev/null +++ b/docs/bdd/self-service-api-key-usage.feature @@ -0,0 +1,129 @@ +Feature: Self-service API key usage and account quota visibility + + Background: + Given OmniRoute has usage accounting enabled + And management APIs require a dashboard session or a key with "manage" or "admin" + + Scenario: A delegated key reads its own cost and token usage + Given an API key named "team-a" has the scope "self:usage" + And "team-a" already has a monthly USD budget of 50 configured in the existing budget UI + And "team-a" has current-period spend of 12.50 USD + And "team-a" has current-period token usage: + | input | output | cache_read | cache_creation | reasoning | + | 900000 | 32000 | 120000 | 10000 | 5000 | + When "team-a" calls GET "/api/v1/me/status" with its Bearer token + Then the response status should be 200 + And the response apiKey.name should be "team-a" + And the response usage.cost.limitUsd should be 50 + And the response usage.cost.usedUsd should be 12.50 + And the response usage.cost.usedPercent should be 25 + And the response usage.tokens.totalTokens should be 1067000 + + Scenario: A delegated key cannot query another key by id + Given an API key named "team-a" has the scope "self:usage" + And an API key named "team-b" has the scope "self:usage" + And "team-b" has current-period spend of 99.00 USD + When "team-a" calls GET "/api/v1/me/status?apiKeyId=" with its Bearer token + Then the response status should be 200 + And the response apiKey.name should be "team-a" + And the response should not contain "team-b" + And the response should not contain "99.00" as team-b usage + + Scenario: Anonymous client API mode does not expose self-service status + Given global client API auth allows anonymous local traffic + When an anonymous caller calls GET "/api/v1/me/status" + Then the response status should be 401 + + Scenario: Self-service usage scope does not grant management access + Given an API key named "team-a" has the scope "self:usage" + And "team-a" does not have the scope "manage" + And "team-a" does not have the scope "admin" + When "team-a" calls GET "/api/usage/history" with its Bearer token + Then the response status should be 403 + + Scenario: Own usage visibility can be disabled + Given an API key named "team-a" does not have the scope "self:usage" + When "team-a" calls GET "/api/v1/me/status" with its Bearer token + Then the response status should be 403 + + Scenario: Existing ordinary keys are backfilled for own usage visibility + Given an ordinary API key named "legacy-key" existed before self-service usage scopes + And "legacy-key" does not have the scope "self:usage" + When OmniRoute runs the compatibility migration + Then "legacy-key" should have the scope "self:usage" + And "legacy-key" should not have the scope "self:account-quota" + + Scenario: Shared account quota is hidden by default + Given an API key named "team-a" has the scope "self:usage" + And "team-a" does not have the scope "self:account-quota" + And "team-a" is restricted to a Codex connection with available quota + When "team-a" calls GET "/api/v1/me/status" with its Bearer token + Then the response status should be 200 + And the response should not include shared account quota details + + Scenario: Shared Codex account quota is visible with explicit permission + Given an API key named "team-a" has the scope "self:usage" + And "team-a" has the scope "self:account-quota" + And "team-a" is restricted to exactly one Codex connection + And Codex reports a session quota with 1 percent used + And Codex reports a weekly quota with 97 percent used + When "team-a" calls GET "/api/v1/me/status" with its Bearer token + Then the response status should be 200 + And the response accountQuota.provider should be "codex" + And the response accountQuota.shared should be true + And the response accountQuota.quotas.session.remainingPercentage should be 99 + And the response accountQuota.quotas.weekly.remainingPercentage should be 3 + + Scenario: Account quota is not guessed for multi-connection keys + Given an API key named "team-a" has the scope "self:usage" + And "team-a" has the scope "self:account-quota" + And "team-a" is allowed to use two provider connections + When "team-a" calls GET "/api/v1/me/status" with its Bearer token + Then the response status should be 200 + And the response accountQuota.available should be false + And the response accountQuota.reason should be "ambiguous_connection" + + Scenario: Account quota is not guessed for unrestricted connection keys + Given an API key named "team-a" has the scope "self:usage" + And "team-a" has the scope "self:account-quota" + And "team-a" has no explicit allowed connection restrictions + When "team-a" calls GET "/api/v1/me/status" with its Bearer token + Then the response status should be 200 + And the response accountQuota.available should be false + And the response accountQuota.reason should be "ambiguous_connection" + + Scenario: Existing budget endpoint stays management-only + Given an API key named "team-a" has the scope "self:usage" + And "team-a" does not have the scope "manage" + When "team-a" calls GET "/api/usage/budget?apiKeyId=" with its Bearer token + Then the response status should be 403 + + Scenario: API Manager defaults are privacy-preserving + Given an operator opens the create API key dialog + Then the own cost and token usage visibility control should be checked + And the shared account quota visibility control should be unchecked + And management access should be unchecked + And the dialog should not include a second budget editor + + Scenario: API Manager preserves unrelated scopes + Given an API key has scopes: + | scope | + | self:usage | + | custom:scope | + When an operator enables shared account quota in the permissions dialog + And saves the permissions + Then the API key scopes should include "self:usage" + And the API key scopes should include "self:account-quota" + And the API key scopes should include "custom:scope" + + Scenario: API Manager uses existing budget configuration + Given an operator wants to set a monthly USD budget for an API key + When the operator uses the dashboard + Then the operator should use the existing budget configuration surface + And the create key dialog should not save budget limits + + Scenario: New API Manager text is localized + Given the dashboard locale is not English + When the API Manager renders self-service visibility controls + Then the labels should come from the API Manager translation namespace + And the component should not render hard-coded English strings for the new controls diff --git a/docs/openspec/changes/self-service-api-key-usage/proposal.md b/docs/openspec/changes/self-service-api-key-usage/proposal.md new file mode 100644 index 0000000000..1d8708d0ad --- /dev/null +++ b/docs/openspec/changes/self-service-api-key-usage/proposal.md @@ -0,0 +1,65 @@ +# Change: Self-Service API Key Usage and Quota Visibility + +## Summary + +Add a client-facing self-service status endpoint and dashboard controls that let each OmniRoute API key inspect its own USD usage, token usage, and percent used against its existing USD budget configuration. Optionally expose shared upstream account quota when an operator grants a dedicated per-key scope. + +## Motivation + +OmniRoute can route multiple delegated API keys through one upstream coding account. Operators need per-key accountability without giving every delegated key management access. Existing management usage APIs are too broad for delegated clients because they can expose other keys and operational state. + +This change creates a narrow own-key API and UI controls: + +- Own cost and token usage are visible by default for ordinary new keys. +- Shared account quota remains opt-in because it is account-level and sensitive. +- USD budgets remain the enforcement mechanism; token totals are reporting only. + +## Scope + +In scope: + +- New `GET /api/v1/me/status` endpoint authenticated by normal Bearer API key. +- New self-service API key scopes: `self:usage` and `self:account-quota`. +- Per-key cost and token aggregation for the calling key. +- Optional normalized provider account quota for unambiguous single-connection keys. +- API Manager create/edit controls for visibility scopes. +- Reuse the existing budget configuration surface for USD limits. +- i18n message keys for all new dashboard text. +- Tests and docs for the new behavior. + +Out of scope: + +- Token quota enforcement. +- Cross-key reporting through the self-service endpoint. +- Changing management usage APIs. +- Changing provider routing or quota preflight behavior. +- Raw upstream quota payload exposure. +- A second budget editor inside key creation or permissions dialogs. + +## Compatibility + +Existing keys should continue to work. A migration or first-start normalization step should backfill `self:usage` onto existing ordinary keys so they receive the same default own-usage visibility as newly created keys. Existing keys must not receive shared account quota visibility unless `self:account-quota` is explicitly granted. + +The new scopes must not grant management access. Only `manage` and `admin` remain management-grade. + +## Risks + +- Scope editing in the current dashboard can collapse scopes to only management access; implementation must preserve unrelated scopes. +- Shared account quota can reveal account exhaustion; it must remain disabled by default. +- Multi-connection and unrestricted-connection keys are ambiguous; first implementation should decline account quota rather than guessing. +- Backfill must be idempotent so upgrades do not repeatedly rewrite API keys or re-enable a permission an operator later disabled. +- New UI text can regress non-English dashboards if translation keys are not added consistently. +- The current scope validation cap is 16 entries; adding self-service scopes may require raising that cap. +- The current `/api/usage/budget` route relies on route-level authz rather than handler-level `requireManagementAuth()`, so the PR should harden it or explicitly test the proxy guard. + +## Rollout + +1. Add constants, validation, and helper tests. +2. Raise or otherwise adapt scope validation limits. +3. Add idempotent existing-key backfill for `self:usage`. +4. Harden `/api/usage/budget` with handler-level management auth or add explicit proxy-guard tests. +5. Add self-service status endpoint. +6. Add create/edit dashboard controls. +7. Add i18n message keys for dashboard text. +8. Add API/reference docs. +9. Verify against release branch used for upstream PR. diff --git a/docs/openspec/changes/self-service-api-key-usage/specs/api-key-self-service-usage/spec.md b/docs/openspec/changes/self-service-api-key-usage/specs/api-key-self-service-usage/spec.md new file mode 100644 index 0000000000..2fe7620689 --- /dev/null +++ b/docs/openspec/changes/self-service-api-key-usage/specs/api-key-self-service-usage/spec.md @@ -0,0 +1,198 @@ +# Specification: API Key Self-Service Usage + +## ADDED Requirements + +### Requirement: Self-service status endpoint + +OmniRoute SHALL provide `GET /api/v1/me/status` for a valid Bearer API key to retrieve status for that same API key. + +#### Scenario: Valid key reads own status + +- GIVEN a valid API key with own-usage visibility +- WHEN it calls `GET /api/v1/me/status` +- THEN the response status SHALL be `200` +- AND the response SHALL include the API key id and name +- AND the response SHALL include cost usage for that key +- AND the response SHALL include token usage for that key + +#### Scenario: Invalid key is rejected + +- GIVEN a missing or invalid Bearer token +- WHEN the caller calls `GET /api/v1/me/status` +- THEN the response status SHALL be `401` + +#### Scenario: Anonymous client API mode does not bypass self-service auth + +- GIVEN global client API auth allows anonymous local traffic +- WHEN a caller without a Bearer API key calls `GET /api/v1/me/status` +- THEN the response status SHALL be `401` + +#### Scenario: Environment management key is not a self-service key + +- GIVEN the deployment has an environment management key +- WHEN that key calls `GET /api/v1/me/status` +- THEN the response SHALL NOT expose delegated API key usage + +### Requirement: Own-key isolation + +The self-service endpoint SHALL derive the API key id from the authenticated Bearer key and SHALL NOT accept caller-supplied key ids for lookup. + +#### Scenario: Caller tries to query another key + +- GIVEN API key A and API key B both have usage +- WHEN API key A calls `GET /api/v1/me/status?apiKeyId=` +- THEN the response SHALL contain only API key A identity and usage +- AND the response SHALL NOT contain API key B usage + +### Requirement: USD budget status + +The self-service endpoint SHALL report per-key USD budget usage using the existing budget system. + +#### Scenario: Key has an active monthly budget + +- GIVEN an API key has a monthly USD budget of `50` +- AND the key has current-period cost of `12.50` +- WHEN the key calls the self-service status endpoint +- THEN `usage.cost.limitUsd` SHALL be `50` +- AND `usage.cost.usedUsd` SHALL be `12.50` +- AND `usage.cost.usedPercent` SHALL be `25` +- AND `usage.cost.remainingUsd` SHALL be `37.50` + +#### Scenario: Key has no budget + +- GIVEN an API key has no configured budget +- WHEN the key calls the self-service status endpoint +- THEN `usage.cost.limitUsd` SHALL be `null` +- AND `usage.cost.usedPercent` SHALL be `null` +- AND cost and token totals SHALL still be returned for the default display period + +### Requirement: Token usage reporting + +The self-service endpoint SHALL report token totals from `usage_history` for the authenticated API key and selected reporting period. + +#### Scenario: Token totals include all tracked categories + +- GIVEN an API key has usage rows with input, output, cache read, cache creation, and reasoning tokens +- WHEN the key calls the self-service status endpoint +- THEN the response SHALL include each token category total +- AND `totalTokens` SHALL include all reported token categories + +### Requirement: Self-service scopes + +OmniRoute SHALL support `self:usage` and `self:account-quota` API key scopes. These scopes SHALL NOT grant management API access. + +#### Scenario: Self-service scope is not management + +- GIVEN an API key has `self:usage` +- AND it does not have `manage` or `admin` +- WHEN it calls a management usage endpoint +- THEN the response SHALL be forbidden + +#### Scenario: New key defaults + +- GIVEN an operator opens the create API key UI +- THEN own cost and token usage visibility SHALL be enabled by default +- AND shared account quota visibility SHALL be disabled by default + +#### Scenario: Existing keys receive own-usage visibility on upgrade + +- GIVEN an ordinary API key existed before this feature +- AND it does not have `self:usage` +- WHEN the compatibility migration or startup normalization runs +- THEN the API key SHALL have `self:usage` +- AND the API key SHALL NOT have `self:account-quota` + +#### Scenario: Key without own-usage scope is denied + +- GIVEN a valid API key does not have `self:usage` +- WHEN it calls `GET /api/v1/me/status` +- THEN the response status SHALL be `403` + +### Requirement: Shared account quota permission + +The self-service endpoint SHALL include shared account quota only when the authenticated key has `self:account-quota`. + +#### Scenario: Account quota hidden by default + +- GIVEN a valid API key has own-usage visibility +- AND it does not have `self:account-quota` +- WHEN it calls the self-service endpoint +- THEN the response SHALL NOT include shared account quota details + +#### Scenario: Codex quota shown with explicit permission + +- GIVEN a valid API key has `self:account-quota` +- AND it is restricted to exactly one Codex connection +- AND Codex quota data is available +- WHEN it calls the self-service endpoint +- THEN the response SHALL include normalized `session` and `weekly` quota windows +- AND each window SHALL include used percentage, remaining percentage, and reset timestamp when known + +#### Scenario: Multiple connections are ambiguous + +- GIVEN a valid API key has `self:account-quota` +- AND it is allowed to use more than one connection +- WHEN it calls the self-service endpoint +- THEN `accountQuota.available` SHALL be `false` +- AND `accountQuota.reason` SHALL be `ambiguous_connection` + +#### Scenario: Unrestricted connections are ambiguous + +- GIVEN a valid API key has `self:account-quota` +- AND its `allowedConnections` list is empty, meaning all connections are allowed +- WHEN it calls the self-service endpoint +- THEN `accountQuota.available` SHALL be `false` +- AND `accountQuota.reason` SHALL be `ambiguous_connection` + +### Requirement: Dashboard configuration + +The API Manager SHALL allow operators to configure self-service visibility and SHALL reuse the existing budget configuration surface for USD limits. + +#### Scenario: Edit preserves unrelated scopes + +- GIVEN an API key has scopes `["self:usage", "custom:scope"]` +- WHEN an operator enables shared account quota in the permissions UI +- THEN the saved scopes SHALL include `self:usage` +- AND the saved scopes SHALL include `self:account-quota` +- AND the saved scopes SHALL still include `custom:scope` + +#### Scenario: Budget editing remains in existing budget UI + +- GIVEN an operator wants to change a key's USD budget limit +- WHEN they use the dashboard +- THEN OmniRoute SHALL direct them to the existing budget configuration surface +- AND the create-key dialog SHALL NOT introduce a second budget editor + +#### Scenario: No budget is displayed as not configured + +- GIVEN an API key has no configured budget +- WHEN the API Manager shows self-service usage for that key +- THEN the UI SHALL show usage and token totals +- AND the budget limit, remaining amount, and percent SHALL be shown as not configured + +### Requirement: Dashboard internationalization + +All new API Manager text for self-service usage visibility, shared account quota visibility, and no-budget display SHALL use OmniRoute's existing i18n message system. + +#### Scenario: New UI strings use translation keys + +- GIVEN the API Manager renders the new self-service controls +- THEN labels, descriptions, tooltips, empty states, and errors SHALL come from translation keys +- AND no new user-visible dashboard text SHALL be hard-coded in the component + +#### Scenario: Locale files stay structurally compatible + +- GIVEN new API Manager translation keys are added +- WHEN the translation consistency check runs +- THEN supported locale message files SHALL have compatible key structure + +### Requirement: Existing budget management remains protected + +The existing `/api/usage/budget` management endpoint SHALL NOT become an own-key self-service data source. + +#### Scenario: Self-service key cannot read arbitrary budget endpoint + +- GIVEN an API key has `self:usage` +- AND it does not have `manage` or `admin` +- WHEN it calls `/api/usage/budget?apiKeyId=` +- THEN the response SHALL be rejected by management auth diff --git a/docs/openspec/changes/self-service-api-key-usage/tasks.md b/docs/openspec/changes/self-service-api-key-usage/tasks.md new file mode 100644 index 0000000000..b7cb88ed5e --- /dev/null +++ b/docs/openspec/changes/self-service-api-key-usage/tasks.md @@ -0,0 +1,71 @@ +# Tasks + +## 1. Scope and Validation + +- [ ] Add `self:usage` and `self:account-quota` constants outside management scopes. +- [ ] Extend key creation validation to accept self-service scopes. +- [ ] Raise or replace the current 16-scope validation cap so new scopes do not break existing custom/MCP-heavy keys. +- [ ] Add an idempotent compatibility migration or startup normalization for existing keys. +- [ ] Add tests proving self-service scopes do not satisfy management auth. + +## 2. Usage Aggregation + +- [ ] Add helper to derive self-service status from authenticated API key metadata. +- [ ] Aggregate cost through existing `getCostSummary()` and `checkBudget()`. +- [ ] Aggregate token totals from `usage_history` by `api_key_id` and period start. +- [ ] Add tests for missing budget, configured budget, and token totals. + +## 3. Account Quota + +- [ ] Resolve account quota only when the key has `self:account-quota`. +- [ ] Use exactly one explicit allowed connection; treat unrestricted or multiple connections as ambiguous. +- [ ] Normalize Codex quota windows to `session` and `weekly`. +- [ ] Add tests for no scope, one connection, multiple connections, unsupported provider, and fetch failure. + +## 4. API Endpoint + +- [ ] Add `GET /api/v1/me/status`. +- [ ] Authenticate in the handler using a normal Bearer API key and derive the API key id from DB metadata. +- [ ] Reject anonymous access even when global client API auth would allow anonymous local traffic. +- [ ] Reject env-only management keys for this own-key endpoint. +- [ ] Reject missing/invalid keys with `401`. +- [ ] Reject keys without `self:usage` with `403` after compatibility backfill has run. +- [ ] Ignore any caller-supplied `apiKeyId`. +- [ ] Add route tests for isolation and response shape. + +## 5. Dashboard + +- [ ] Add create-key controls for own usage visibility and shared account quota visibility. +- [ ] Add edit-permissions controls for self-service visibility. +- [ ] Reuse the existing budget configuration surface for USD limit editing. +- [ ] Preserve unrelated scopes when editing permissions. +- [ ] Show per-key budget percent and token totals in the key details experience. +- [ ] Show no-budget state as not configured while still showing usage. +- [ ] Add UI tests for defaults and scope preservation. + +## 6. Internationalization + +- [ ] Add translation keys under the existing API Manager namespace for all new UI text. +- [ ] Update default and generated locale message files according to the repo's i18n workflow. +- [ ] Add or run a translation key consistency check. +- [ ] Run `npm run i18n:sync-ui:dry`. +- [ ] Run `npm run i18n:check-ui-coverage`. + +## 7. Budget Endpoint Hardening + +- [ ] Add handler-level management auth to `/api/usage/budget` GET and POST, or document and test why proxy-only protection is intentional. +- [ ] Add a regression test proving ordinary self-service keys cannot use `/api/usage/budget?apiKeyId=...` to read arbitrary keys. + +## 8. Documentation + +- [ ] Add API reference entry for `/api/v1/me/status`. +- [ ] Update user guide/API manager docs. +- [ ] Document privacy behavior for shared account quota. +- [ ] Add migration/compatibility note for existing keys. + +## 9. Verification + +- [ ] Run lint. +- [ ] Run typecheck. +- [ ] Run focused unit/API/UI tests. +- [ ] Run coverage or the repo-required validation command before PR. diff --git a/docs/specs/2026-05-29-self-service-api-key-usage-design.md b/docs/specs/2026-05-29-self-service-api-key-usage-design.md new file mode 100644 index 0000000000..5a3966f271 --- /dev/null +++ b/docs/specs/2026-05-29-self-service-api-key-usage-design.md @@ -0,0 +1,348 @@ +# Self-Service API Key Usage and Quota Visibility + +## Problem + +Operators often share one upstream coding account, such as Codex, across multiple OmniRoute API keys. OmniRoute already records per-key usage and supports per-key USD budgets, but a normal client API key cannot query its own spend or token totals. The existing usage APIs are management endpoints, so exposing them to each API key would disclose other keys, account metadata, and operational settings. + +Operators also need a way to decide whether a key may see the shared upstream account quota. For Codex this includes the short session window and weekly window fetched from ChatGPT usage APIs. That quota is account-level state, not key-level state, so it should not be visible by default. + +The goal is to add a small self-service status API and matching dashboard controls so a delegated API key can see: + +- Its own USD usage against its configured budget. +- Its own token usage totals. +- The percent used toward its own USD budget limit. +- Optionally, shared upstream account quota remaining when explicitly permitted. + +## Baseline + +This design was written after comparing the official source and a live deployment: + +- Official checkout: `origin/main` at `dc3915a4`, package version `3.8.5`. +- Live deployment: package version `3.8.3`, installed under `/usr/lib/node_modules/omniroute/app`. +- Contributor guide: PRs currently target `release/v3.8.3`, so implementation should start from the release branch even though the source survey used current `main`. + +Relevant current implementation: + +- API key creation is in `src/app/api/keys/route.ts`; `createKeySchema` currently accepts `name`, `noLog`, and `scopes`. +- API key metadata is stored in `api_keys`, including `scopes`, `allowed_connections`, model restrictions, request rate limits, and lifecycle fields. +- Management auth treats `manage` and `admin` as management scopes in `src/shared/constants/managementScopes.ts`. +- `/api/v1/*` routes are public from the route classifier perspective, but individual handlers still validate Bearer API keys. +- Per-key USD budgets already exist through `domain_budgets`, `domain_cost_history`, `getCostSummary(apiKeyId)`, and `checkBudget(apiKeyId)`. +- Token usage is already recorded per key in `usage_history.api_key_id` with input, output, cache read, cache creation, and reasoning token columns. +- Provider quota data is fetched through `src/lib/usage/providerLimits.ts` and Codex quota support in `open-sse/services/codexQuotaFetcher.ts` / `open-sse/services/usage.ts`. +- The API Manager UI currently has a management-access toggle on create/edit and sends `scopes: ["manage"]` or `[]`; the edit modal must be changed before adding more scope types so it does not discard unrelated scopes. + +## Goals + +- Add an authenticated self-service endpoint for the calling API key's own usage. +- Keep management endpoints protected exactly as they are today. +- Use USD budgets for enforcement and percentage reporting. +- Include token totals as reporting data only, not as quota enforcement. +- Make account quota visibility opt-in per API key. +- Add create/edit UI controls for self-service visibility while reusing the existing budget configuration flow for USD limits. +- Add all new dashboard text through OmniRoute's i18n message system. +- Preserve arbitrary existing scopes when the dashboard edits permissions. +- Provide a design that can become an upstream-quality PR with tests and docs. + +## Non-Goals + +- Do not expose other API keys' usage through the self-service endpoint. +- Do not add token-based quota enforcement in this change. +- Do not change provider routing, fallback, or quota preflight behavior. +- Do not disclose upstream access tokens, workspace IDs, emails, or connection secrets. +- Do not make shared account quota visible by default. +- Do not replace the existing management usage dashboards. + +## Proposed API + +Add: + +```text +GET /api/v1/me/status +Authorization: Bearer +``` + +The route is under `/api/v1` so it follows the client API surface, but the handler must explicitly validate the Bearer API key and load its metadata. It must not use `requireManagementAuth()`. + +The handler must not rely only on the global `CLIENT_API` authz policy. In the current source, `clientApiPolicy` can allow anonymous traffic when `REQUIRE_API_KEY` is not `"true"`, and some `/api/v1` helper code assumes the middleware already made that decision. This endpoint is more sensitive, so it must perform handler-local validation: + +- Require an `Authorization: Bearer ` credential. +- Call `validateApiKey()` / `getApiKeyMetadata()` or an equivalent DB-backed helper. +- Reject anonymous, dashboard-session-only, invalid, expired, revoked, inactive, and env-only management keys for this self-service response. +- Derive the returned API key id from metadata, never from request parameters. + +The response contains only the caller's own API key identity, budget usage, token usage, and optional account quota: + +```json +{ + "apiKey": { + "id": "key_123", + "name": "team-a" + }, + "usage": { + "cost": { + "period": "monthly", + "currency": "USD", + "usedUsd": 12.34, + "limitUsd": 50, + "remainingUsd": 37.66, + "usedPercent": 24.68, + "warningThreshold": 0.8, + "resetAt": "2026-06-01T00:00:00.000Z", + "periodStartAt": "2026-05-01T00:00:00.000Z" + }, + "tokens": { + "periodStartAt": "2026-05-01T00:00:00.000Z", + "inputTokens": 900000, + "outputTokens": 32000, + "cacheReadTokens": 120000, + "cacheCreationTokens": 10000, + "reasoningTokens": 5000, + "totalTokens": 1067000 + } + }, + "accountQuota": { + "provider": "codex", + "connectionId": "conn_123", + "shared": true, + "quotas": { + "session": { + "remainingPercentage": 99, + "usedPercentage": 1, + "resetAt": "2026-05-29T18:11:44.000Z" + }, + "weekly": { + "remainingPercentage": 3, + "usedPercentage": 97, + "resetAt": "2026-05-31T01:23:38.000Z" + } + } + } +} +``` + +`accountQuota` is omitted unless the key has the account quota scope. If the scope is present but the connection cannot be resolved safely, return: + +```json +{ + "accountQuota": { + "available": false, + "reason": "ambiguous_connection" + } +} +``` + +Use stable reason strings: `not_supported`, `ambiguous_connection`, `no_allowed_connection`, `not_available`, and `fetch_failed`. + +## Scopes + +Add self-service scopes that do not grant management access: + +- `self:usage`: allows a key to query its own spend, budget percent, and token totals. +- `self:account-quota`: allows a key to see shared upstream account quota for its resolved connection. + +`self:usage` should be enabled by default for newly created ordinary API keys. The UI should show it checked by default and persist the scope when the control is enabled. For backwards compatibility, the implementation should backfill `self:usage` onto existing ordinary keys during migration or first startup after upgrade. After that compatibility step, absence of `self:usage` means own-usage visibility is disabled and the self-service endpoint returns `403`. + +`self:account-quota` must be disabled by default. The dashboard should require an explicit opt-in when creating or editing a key. + +These scopes must not be added to `MANAGEMENT_API_KEY_SCOPES`. `manage` and `admin` remain the only management-grade scopes. + +## Budget Semantics + +The existing USD budget system remains authoritative: + +- `getCostSummary(apiKeyId)` provides current period cost, active USD limit, reset interval, reset time, and period boundaries. +- `checkBudget(apiKeyId)` remains the enforcement check used by request handling. +- The self-service endpoint reports budget percentage as `usedUsd / limitUsd * 100`. +- When no budget is configured, return `limitUsd: null`, `remainingUsd: null`, and `usedPercent: null`. + +The endpoint should report the active period from the budget window when configured. If a key has no budget, use the current calendar month for display-only usage aggregation so the API still returns useful cost and token totals. + +## Token Usage Semantics + +Add a small aggregation helper over `usage_history` scoped by `api_key_id` and time window: + +```sql +SELECT + COALESCE(SUM(tokens_input), 0) AS inputTokens, + COALESCE(SUM(tokens_output), 0) AS outputTokens, + COALESCE(SUM(tokens_cache_read), 0) AS cacheReadTokens, + COALESCE(SUM(tokens_cache_creation), 0) AS cacheCreationTokens, + COALESCE(SUM(tokens_reasoning), 0) AS reasoningTokens +FROM usage_history +WHERE api_key_id = ? + AND timestamp >= ? +``` + +`totalTokens` should include all reported token categories. Token totals are informational and should not affect budget enforcement. + +## Account Quota Resolution + +Account quota is shared provider state. The self-service endpoint may include it only when: + +- The API key has `self:account-quota`. +- A single provider connection can be resolved without ambiguity. +- The provider supports quota fetching. + +Connection resolution must follow the source semantics for `allowedConnections`: an empty array means unrestricted access to all connections, not "no connections". + +- If exactly one explicit allowed connection exists and it resolves to a quota-supported provider, use that connection. +- If `allowedConnections` is empty, treat the connection scope as ambiguous and return `available: false` with `ambiguous_connection`. This avoids exposing shared quota for a broad/unrestricted key. +- If explicit allowed connection ids are present but none resolve, return `available: false` with `no_allowed_connection`. +- If multiple explicit allowed connections exist, return `available: false` with `ambiguous_connection`. + +This conservative rule avoids accidentally exposing quota for an account the key may not actually use. A later change can add an explicitly authorized `?connectionId=` flow if there is demand for multi-connection keys. + +For Codex, reuse the existing provider limits / Codex quota path. Normalize Codex windows to `session` and `weekly` and return used/remaining percentages plus reset timestamps. Do not return raw upstream payloads. + +## Dashboard UX + +API Manager should expose these controls during key creation and editing. + +Create key modal: + +- Management access remains a separate, off-by-default toggle. +- Add "Self-service visibility": + - "Own cost and token usage" checked by default. + - "Shared account quota" unchecked by default. +- Do not add budget limit fields here. Per-key USD budgets already have a dedicated configuration surface, and this feature should link to or surface the existing budget state instead of creating a second configuration path. + +Editing permissions: + +- Keep existing model, endpoint, connection, schedule, and rate-limit controls. +- Add the same self-service visibility toggles. +- Preserve all existing scopes when toggling one permission. The current edit flow must not rebuild scopes as only `["manage"]` or `[]`. +- Do not move budget editing into the permissions modal. The permissions modal may show a read-only hint or link to the existing budget configuration area. + +Usage display: + +- In the key list or details panel, show USD used, active USD limit, and used percent when a budget exists. +- Show token totals in a compact details view. +- Show shared account quota only for keys with `self:account-quota`, clearly labeled as shared account quota, not per-key quota. +- When no USD budget is configured, show usage normally and render the limit, remaining amount, and percent as unset/not configured rather than `0%`. + +## Internationalization + +OmniRoute's dashboard is localized through `src/i18n/messages/*.json` and components use `useTranslations()`. All new API Manager labels, descriptions, tooltips, empty states, and error messages must use translation keys instead of hard-coded UI strings. + +Implementation should: + +- Add new keys under the existing `apiManager` namespace for self-service visibility labels, shared account quota labels, and unset-budget display text. +- Update the default source locale and keep other locale files structurally compatible with the repo's i18n workflow. +- Avoid concatenating translated fragments for dynamic text; use complete translation strings with variables where needed. +- Run the repo's UI i18n checks, especially `npm run i18n:sync-ui:dry` and `npm run i18n:check-ui-coverage`, so missing translations are caught before PR. +- If the implementation touches the existing budget page for links or hints, localize any new budget-page strings as well. The existing `BudgetTab` still has some hard-coded labels, so do not add more hard-coded user-facing text there. + +## Validation and Storage Changes + +Extend `createKeySchema` to accept: + +- `scopes` containing the new self-service scope names. + +`createKeySchema` and `updateKeyPermissionsSchema` currently cap `scopes` at 16 entries. Adding two self-service scopes can make legitimate keys exceed that limit when they already carry management or MCP/custom scopes. The implementation should either raise the cap to a documented value such as 32 or validate against named scope families instead of keeping the current 16-entry limit. + +Do not extend key creation with a budget object in this change. Budget limits are already configured through the existing budget APIs and UI. The self-service endpoint should read those existing limits and display `null` limit/percent fields when none are configured. + +Add a compatibility migration or startup normalization step: + +- Existing ordinary keys receive `self:usage`. +- Existing keys do not receive `self:account-quota`. +- Existing management keys keep their current management scopes and may also receive `self:usage` if they are expected to use the self-service endpoint. +- The backfill is one-time and guarded by the repo's existing migration/version mechanism so it cannot re-enable `self:usage` after an operator later disables it. +- After that one-time backfill, missing `self:usage` is an explicit denial for the self-service endpoint. + +The key creation route should: + +1. Validate the request. +2. Normalize scopes by preserving known custom scopes and adding `self:usage` when omitted by the UI default. +3. Create the key. +4. Return the created key metadata. + +The update permissions route should support the same scope preservation behavior. Scope mutation should be set-based: + +- Start from existing scopes. +- Add or remove only the scopes represented by the UI controls. +- Leave unknown or unrelated scopes intact. + +The current `PermissionsModal` calls `onSave(..., manageEnabled ? ["manage"] : [], ...)`, which would discard any new self-service or custom scope. This must be changed before the self-service toggles are added. + +## Existing Budget Endpoint Guard + +The global authz proxy classifies `/api/usage/budget` as a management API, but unlike `/api/usage/budget/bulk`, the current route handler does not call `requireManagementAuth()` directly. The self-service design must not reuse `/api/usage/budget?apiKeyId=...` because that endpoint accepts arbitrary key ids. + +For defense in depth and easier direct route testing, the implementation PR should either: + +- Add handler-level `requireManagementAuth()` to `/api/usage/budget` GET and POST, matching the bulk route; or +- Include an explicit note and tests proving the proxy is the only intended guard. + +The preferred upstream-quality fix is to add handler-level management auth to `/api/usage/budget` while adding the separate own-key `/api/v1/me/status` endpoint. + +## Security and Privacy + +The self-service handler must be own-key only. It should derive `apiKeyId` from the presented Bearer key and never accept an `apiKeyId` query parameter. + +Never include: + +- Full API key value. +- Upstream access tokens or refresh tokens. +- Provider account email unless that email is already visible to this key through another client API. +- Other keys' spend, token totals, names, or budgets. +- Raw ChatGPT/Codex usage payloads. + +Account quota should be treated as sensitive because it lets delegated users infer shared account exhaustion. The default remains off. + +## Error Handling + +- Missing or invalid Bearer key: `401` with a generic auth error. +- Valid key without `self:usage`: `403`. +- Budget missing: `200` with null limit and percent fields. +- Usage aggregation failure: `500` with generic message; log server-side details. +- Quota fetch unsupported or unavailable: `200` with `accountQuota.available: false`. +- Quota fetch auth failure: do not leak provider auth details; return `not_available` or `fetch_failed` and log details server-side. + +## Testing + +Add focused tests: + +- Self-service endpoint rejects missing and invalid Bearer keys. +- Self-service endpoint rejects anonymous access even when `REQUIRE_API_KEY` is not `"true"`. +- Self-service endpoint rejects env-only management keys or any key without DB metadata suitable for own-key usage. +- A normal key with `self:usage` can query its own cost and token totals without `manage`. +- The endpoint never accepts an `apiKeyId` override. +- Key A cannot see Key B usage. +- A key without account quota scope does not receive `accountQuota`. +- A key with account quota scope and one allowed Codex connection receives normalized session and weekly quota. +- Unrestricted or multiple allowed connections return `ambiguous_connection`. +- Create UI defaults own usage on and shared quota off. +- Edit UI preserves unrelated scopes. +- UI renders the no-budget state as not configured, with usage and token totals still visible. +- New dashboard strings are covered by i18n keys. +- `/api/usage/budget` remains management-only and is not usable as an own-key data escape hatch. + +## Implementation Notes + +Recommended new files: + +- `src/shared/constants/selfServiceScopes.ts` +- `src/lib/usage/apiKeySelfService.ts` +- `src/app/api/v1/me/status/route.ts` + +Recommended modified files: + +- `src/shared/validation/schemas.ts` +- `src/app/api/keys/route.ts` +- `src/app/api/keys/[id]/route.ts` +- `src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx` +- `src/i18n/messages/*.json` +- API reference docs after implementation. + +## Acceptance Criteria + +- Delegated keys can see their own USD usage, budget percentage, and token usage. +- Shared account quota is hidden unless explicitly enabled per key. +- The dashboard can configure self-service visibility during create/edit. +- The dashboard continues to use the existing budget configuration surface for USD limits. +- New UI text is localized through existing i18n files. +- Existing management usage APIs remain management-only. +- Scope edits do not discard unrelated scopes. +- Tests cover API, helper logic, and UI scope defaults. diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index 81ffb76516..f9480e52c9 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -16,6 +16,14 @@ import { } from "./apiManagerPageUtils"; import type { KeyStatus, KeyType } from "./apiManagerPageUtils"; import { readActiveOnlyPreference, writeActiveOnlyPreference } from "./apiManagerPageStorage"; +import { + buildApiKeyCreateScopes, + mergeApiKeyPermissionScopes, +} from "./apiManagerScopes"; +import { + SELF_ACCOUNT_QUOTA_SCOPE, + SELF_USAGE_SCOPE, +} from "@/shared/constants/selfServiceScopes"; // Constants for validation const MAX_KEY_NAME_LENGTH = 200; @@ -130,6 +138,8 @@ export default function ApiManagerPageClient() { const [showAddModal, setShowAddModal] = useState(false); const [newKeyName, setNewKeyName] = useState(""); const [newKeyManageEnabled, setNewKeyManageEnabled] = useState(false); + const [newKeySelfUsageEnabled, setNewKeySelfUsageEnabled] = useState(true); + const [newKeyAccountQuotaEnabled, setNewKeyAccountQuotaEnabled] = useState(false); const [createdKey, setCreatedKey] = useState(null); const [editingKey, setEditingKey] = useState(null); const [showPermissionsModal, setShowPermissionsModal] = useState(false); @@ -351,7 +361,11 @@ export default function ApiManagerPageClient() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: sanitizedName, - scopes: newKeyManageEnabled ? ["manage"] : [], + scopes: buildApiKeyCreateScopes({ + manageEnabled: newKeyManageEnabled, + selfUsageEnabled: newKeySelfUsageEnabled, + selfAccountQuotaEnabled: newKeyAccountQuotaEnabled, + }), }), }); const data = await res.json(); @@ -361,6 +375,8 @@ export default function ApiManagerPageClient() { await fetchData(); setNewKeyName(""); setNewKeyManageEnabled(false); + setNewKeySelfUsageEnabled(true); + setNewKeyAccountQuotaEnabled(false); setShowAddModal(false); } else { setCreateError(data.error || t("failedCreateKey")); @@ -999,6 +1015,8 @@ export default function ApiManagerPageClient() { setShowAddModal(false); setNewKeyName(""); setNewKeyManageEnabled(false); + setNewKeySelfUsageEnabled(true); + setNewKeyAccountQuotaEnabled(false); setNameError(null); setCreateError(null); }} @@ -1041,6 +1059,58 @@ export default function ApiManagerPageClient() { {newKeyManageEnabled ? tc("enabled") : tc("disabled")} +
+
+

{t("selfServiceVisibility")}

+

{t("selfServiceVisibilityDesc")}

+
+
+
+

{t("ownUsageVisibility")}

+

{t("ownUsageVisibilityDesc")}

+
+ +
+
+
+

{t("sharedAccountQuotaVisibility")}

+

{t("sharedAccountQuotaVisibilityDesc")}

+
+ +
+
{createError && (
error @@ -1053,6 +1123,8 @@ export default function ApiManagerPageClient() { setShowAddModal(false); setNewKeyName(""); setNewKeyManageEnabled(false); + setNewKeySelfUsageEnabled(true); + setNewKeyAccountQuotaEnabled(false); setNameError(null); setCreateError(null); }} @@ -1196,6 +1268,12 @@ const PermissionsModal = memo(function PermissionsModal({ const [manageEnabled, setManageEnabled] = useState( Array.isArray(apiKey?.scopes) && apiKey.scopes.includes("manage") ); + const [selfUsageEnabled, setSelfUsageEnabled] = useState( + Array.isArray(apiKey?.scopes) && apiKey.scopes.includes(SELF_USAGE_SCOPE) + ); + const [selfAccountQuotaEnabled, setSelfAccountQuotaEnabled] = useState( + Array.isArray(apiKey?.scopes) && apiKey.scopes.includes(SELF_ACCOUNT_QUOTA_SCOPE) + ); const [maxSessions, setMaxSessions] = useState( typeof apiKey?.maxSessions === "number" && apiKey.maxSessions > 0 ? apiKey.maxSessions : 0 ); @@ -1370,7 +1448,11 @@ const PermissionsModal = memo(function PermissionsModal({ maxSessions, schedule, rateLimits.length > 0 ? rateLimits : null, - manageEnabled ? ["manage"] : [], + mergeApiKeyPermissionScopes(apiKey?.scopes, { + manageEnabled, + selfUsageEnabled, + selfAccountQuotaEnabled, + }), allowAllEndpoints ? [] : selectedEndpoints ); }, [ @@ -1390,6 +1472,8 @@ const PermissionsModal = memo(function PermissionsModal({ expiresAt, maxSessions, manageEnabled, + selfUsageEnabled, + selfAccountQuotaEnabled, scheduleEnabled, scheduleFrom, scheduleUntil, @@ -1398,6 +1482,7 @@ const PermissionsModal = memo(function PermissionsModal({ rateLimits, allowAllEndpoints, selectedEndpoints, + apiKey?.scopes, t, ]); @@ -1842,6 +1927,48 @@ const PermissionsModal = memo(function PermissionsModal({ {manageEnabled ? tc("enabled") : tc("disabled")}
+ {/* Self-service Visibility */} +
+
+

{t("selfServiceVisibility")}

+

{t("selfServiceVisibilityDesc")}

+
+ +

{t("ownUsageVisibilityDesc")}

+ +

{t("sharedAccountQuotaVisibilityDesc")}

+
{/* Selected Models Summary (only in restrict mode) */} {!allowAll && selectedCount > 0 && ( diff --git a/src/app/(dashboard)/dashboard/api-manager/apiManagerScopes.ts b/src/app/(dashboard)/dashboard/api-manager/apiManagerScopes.ts new file mode 100644 index 0000000000..e8adb3546a --- /dev/null +++ b/src/app/(dashboard)/dashboard/api-manager/apiManagerScopes.ts @@ -0,0 +1,54 @@ +import { + SELF_ACCOUNT_QUOTA_SCOPE, + SELF_USAGE_SCOPE, +} from "@/shared/constants/selfServiceScopes"; + +const MANAGEMENT_SCOPE = "manage"; + +export interface CreateScopeOptions { + manageEnabled: boolean; + selfUsageEnabled?: boolean; + selfAccountQuotaEnabled?: boolean; +} + +export interface PermissionScopeOptions { + manageEnabled: boolean; + selfUsageEnabled: boolean; + selfAccountQuotaEnabled: boolean; +} + +export function buildApiKeyCreateScopes(options: CreateScopeOptions): string[] { + const scopes: string[] = []; + const selfUsageEnabled = options.selfUsageEnabled ?? true; + if (options.manageEnabled) scopes.push(MANAGEMENT_SCOPE); + if (selfUsageEnabled) scopes.push(SELF_USAGE_SCOPE); + if (selfUsageEnabled && options.selfAccountQuotaEnabled === true) { + scopes.push(SELF_ACCOUNT_QUOTA_SCOPE); + } + return scopes; +} + +export function mergeApiKeyPermissionScopes( + currentScopes: readonly string[] | null | undefined, + options: PermissionScopeOptions +): string[] { + const scopes = new Set((currentScopes ?? []).filter((scope) => typeof scope === "string")); + + setScope(scopes, MANAGEMENT_SCOPE, options.manageEnabled); + setScope(scopes, SELF_USAGE_SCOPE, options.selfUsageEnabled); + setScope( + scopes, + SELF_ACCOUNT_QUOTA_SCOPE, + options.selfUsageEnabled && options.selfAccountQuotaEnabled + ); + + return [...scopes]; +} + +function setScope(scopes: Set, scope: string, enabled: boolean): void { + if (enabled) { + scopes.add(scope); + } else { + scopes.delete(scope); + } +} diff --git a/src/app/api/keys/route.ts b/src/app/api/keys/route.ts index ff68d6aa90..2a1747a4c1 100644 --- a/src/app/api/keys/route.ts +++ b/src/app/api/keys/route.ts @@ -6,6 +6,7 @@ import { createKeySchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { isApiKeyRevealEnabled, maskStoredApiKey } from "@/lib/apiKeyExposure"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { normalizeSelfServiceScopesForCreate } from "@/shared/constants/selfServiceScopes"; import * as log from "@/sse/utils/logger"; function parsePagination(request: Request) { @@ -66,7 +67,8 @@ export async function POST(request) { // Always get machineId from server const machineId = await getConsistentMachineId(); - const apiKey = await createApiKey(name, machineId, scopes ?? []); + const normalizedScopes = normalizeSelfServiceScopesForCreate(scopes); + const apiKey = await createApiKey(name, machineId, normalizedScopes); if (noLog === true) { await updateApiKeyPermissions(apiKey.id, { noLog: true }); } diff --git a/src/app/api/usage/budget/route.ts b/src/app/api/usage/budget/route.ts index ef817c50f9..61fc259414 100644 --- a/src/app/api/usage/budget/route.ts +++ b/src/app/api/usage/budget/route.ts @@ -2,8 +2,12 @@ import { NextResponse } from "next/server"; import { getCostSummary, setBudget, checkBudget } from "@/domain/costRules"; import { setBudgetSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; export async function GET(request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { const { searchParams } = new URL(request.url); const apiKeyId = searchParams.get("apiKeyId"); @@ -37,6 +41,9 @@ export async function GET(request) { } export async function POST(request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + let rawBody; try { rawBody = await request.json(); diff --git a/src/app/api/v1/me/status/route.ts b/src/app/api/v1/me/status/route.ts new file mode 100644 index 0000000000..2cae418d83 --- /dev/null +++ b/src/app/api/v1/me/status/route.ts @@ -0,0 +1,46 @@ +import { NextResponse } from "next/server"; + +import { buildApiKeySelfServiceStatus } from "@/lib/usage/apiKeySelfService"; +import { hasSelfUsageScope } from "@/shared/constants/selfServiceScopes"; + +function extractBearerToken(request: Request): string | null { + const authorization = request.headers.get("Authorization") ?? ""; + const match = authorization.match(/^Bearer\s+(.+)$/i); + const token = match?.[1]?.trim(); + return token ? token : null; +} + +function authError(status = 401) { + return NextResponse.json({ error: status === 401 ? "Unauthorized" : "Forbidden" }, { status }); +} + +export async function GET(request: Request) { + const apiKey = extractBearerToken(request); + if (!apiKey) return authError(401); + + const { validateApiKey, getApiKeyMetadata } = await import("@/lib/localDb"); + + const valid = await validateApiKey(apiKey); + if (!valid) return authError(401); + + const metadata = await getApiKeyMetadata(apiKey); + if (!metadata || metadata.id === "env-key") return authError(401); + + if (!hasSelfUsageScope(metadata.scopes)) return authError(403); + + try { + const status = await buildApiKeySelfServiceStatus({ + id: metadata.id, + name: metadata.name, + scopes: metadata.scopes, + allowedConnections: metadata.allowedConnections, + }); + + return NextResponse.json(status); + } catch (error) { + if (error instanceof Error && error.message === "missing_self_usage_scope") { + return authError(403); + } + return NextResponse.json({ error: "Failed to build API key status" }, { status: 500 }); + } +} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index d2278d8a43..d2add6bca1 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "على سبيل المثال، مفتاح الإنتاج، مفتاح التطوير", "keyNameDesc": "اختر اسمًا وصفيًا لتحديد غرض هذا المفتاح", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "تم إنشاء مفتاح API", "keyCreatedSuccess": "تم إنشاء المفتاح بنجاح!", "keyCreatedNote": "انسخ هذا المفتاح وقم بتخزينه الآن - لن يتم عرضه مرة أخرى.", "done": "تم", "savePermissions": "حفظ الأذونات", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index cdb2465859..e8060136dc 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g. Production Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index 8012347d5f..454e854311 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "напр. производствен ключ, ключ за разработка", "keyNameDesc": "Изберете описателно име, за да идентифицирате целта на този ключ", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Ключът за API е създаден", "keyCreatedSuccess": "Ключът е създаден успешно!", "keyCreatedNote": "Копирайте и запазете този ключ сега — няма да се показва отново.", "done": "Готово", "savePermissions": "Запазване на разрешенията", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 9f0ac3c816..e726ebb1dd 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 121f15ad08..64a937e43b 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "např. Produkční Klíč, Vývojový Klíč", "keyNameDesc": "Zvolte popisný název, který identifikuje účel tohoto klíče.", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Klíč vytvořen", "keyCreatedSuccess": "Klíč úspěšně vytvořen!", "keyCreatedNote": "Teď di zkopírujte a uložte tento klíč – už se vám nezobrazí.", "done": "Hotovo", "savePermissions": "Uložit oprávnění", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-překlad", "autoResolveDesc": "Automaticky přeložit nejednoznačné názvy modelů na nativní poskytovatele pro tento API klíč.", "keyActive": "Aktivní Klíč", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 5ad1d22821..a6e03fdc73 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "f.eks. Produktionsnøgle, Udviklingsnøgle", "keyNameDesc": "Vælg et beskrivende navn for at identificere denne nøgles formål", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API-nøgle oprettet", "keyCreatedSuccess": "Nøglen blev oprettet!", "keyCreatedNote": "Kopiér og gem denne nøgle nu – den vises ikke igen.", "done": "Færdig", "savePermissions": "Gem tilladelser", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 8b4efcd27f..fd30604832 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "z. B. Produktionsschlüssel, Entwicklungsschlüssel", "keyNameDesc": "Wählen Sie einen aussagekräftigen Namen, um den Zweck dieses Schlüssels zu identifizieren", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API-Schlüssel erstellt", "keyCreatedSuccess": "Schlüssel erfolgreich erstellt!", "keyCreatedNote": "Kopieren und speichern Sie diesen Schlüssel jetzt – er wird nicht mehr angezeigt.", "done": "Fertig", "savePermissions": "Berechtigungen speichern", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 31a736561a..6536c49d1e 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1465,6 +1465,12 @@ "keyNamePlaceholder": "e.g. Production Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "Self-Service Visibility", + "selfServiceVisibilityDesc": "Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "Own Cost and Token Usage", + "ownUsageVisibilityDesc": "Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 0fc81c3e6b..2e877fc011 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "por ejemplo, clave de producción, clave de desarrollo", "keyNameDesc": "Elija un nombre descriptivo para identificar el propósito de esta clave", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Clave API creada", "keyCreatedSuccess": "¡Clave creada exitosamente!", "keyCreatedNote": "Copie y almacene esta clave ahora; no se volverá a mostrar.", "done": "hecho", "savePermissions": "Guardar permisos", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index b993300d32..a69ded5c11 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index d4ed515301..927e7ee30d 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "esim. tuotantoavain, kehitysavain", "keyNameDesc": "Valitse kuvaava nimi tämän avaimen tarkoituksen tunnistamiseksi", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API-avain luotu", "keyCreatedSuccess": "Avain luotu onnistuneesti!", "keyCreatedNote": "Kopioi ja tallenna tämä avain nyt – sitä ei näytetä uudelleen.", "done": "Valmis", "savePermissions": "Tallenna käyttöoikeudet", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index f59cec210c..79ccc194eb 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "par exemple, clé de production, clé de développement", "keyNameDesc": "Choisissez un nom descriptif pour identifier l'objectif de cette clé", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Clé API créée", "keyCreatedSuccess": "Clé créée avec succès !", "keyCreatedNote": "Copiez et stockez cette clé maintenant – elle ne sera plus affichée.", "done": "Terminé", "savePermissions": "Enregistrer les autorisations", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 8ec64dc4d1..363e6bdda0 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index bdd5f7bc83..72993dbe95 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "למשל מפתח ייצור, מפתח פיתוח", "keyNameDesc": "בחר שם תיאורי כדי לזהות את מטרת מפתח זה", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "מפתח API נוצר", "keyCreatedSuccess": "מפתח נוצר בהצלחה!", "keyCreatedNote": "העתק ואחסן את המפתח הזה עכשיו - הוא לא יוצג שוב.", "done": "בוצע", "savePermissions": "שמור הרשאות", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 2fdc3cda5a..7de1e0112c 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "उदाहरण के लिए, उत्पादन कुंजी, विकास कुंजी", "keyNameDesc": "इस कुंजी के उद्देश्य को पहचानने के लिए एक वर्णनात्मक नाम चुनें", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "एपीआई कुंजी बनाई गई", "keyCreatedSuccess": "कुंजी सफलतापूर्वक बनाई गई!", "keyCreatedNote": "इस कुंजी को अभी कॉपी करें और संग्रहीत करें - इसे दोबारा नहीं दिखाया जाएगा।", "done": "हो गया", "savePermissions": "अनुमतियाँ सहेजें", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 6b5a6a7fc7..49f1ac5479 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "pl. Gyártási kulcs, Fejlesztési Kulcs", "keyNameDesc": "Válasszon egy leíró nevet a kulcs céljának azonosításához", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API-kulcs létrehozva", "keyCreatedSuccess": "A kulcs sikeresen létrehozva!", "keyCreatedNote": "Másolja ki és tárolja ezt a kulcsot most – többé nem jelenik meg.", "done": "Kész", "savePermissions": "Engedélyek mentése", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 085c6667e7..8603d2a8a2 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "misalnya, Kunci Produksi, Kunci Pengembangan", "keyNameDesc": "Pilih nama deskriptif untuk mengidentifikasi tujuan kunci ini", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Kunci API Dibuat", "keyCreatedSuccess": "Kunci berhasil dibuat!", "keyCreatedNote": "Salin dan simpan kunci ini sekarang — kunci ini tidak akan ditampilkan lagi.", "done": "Selesai", "savePermissions": "Simpan Izin", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index 4592921d83..bea8d8dc16 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 1c2c5dfa99..71d4226e07 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "ad esempio, Chiave di produzione, Chiave di sviluppo", "keyNameDesc": "Scegli un nome descrittivo per identificare lo scopo di questa chiave", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Chiave API creata", "keyCreatedSuccess": "Chiave creata con successo!", "keyCreatedNote": "Copia e memorizza questa chiave adesso: non verrà più mostrata.", "done": "Fatto", "savePermissions": "Salva autorizzazioni", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index ec98b6f505..c46830e8ca 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "例: プロダクションキー、開発キー", "keyNameDesc": "このキーの目的を識別するためのわかりやすい名前を選択してください", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "APIキーが作成されました", "keyCreatedSuccess": "キーが正常に作成されました。", "keyCreatedNote": "このキーをコピーして保存してください。再度表示されなくなります。", "done": "完了", "savePermissions": "権限の保存", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index cd58278885..ad7706730f 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "예: 생산 키, 개발 키", "keyNameDesc": "이 키의 목적을 식별하려면 설명이 포함된 이름을 선택하세요.", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API 키가 생성되었습니다.", "keyCreatedSuccess": "키가 생성되었습니다!", "keyCreatedNote": "지금 이 키를 복사하여 저장하세요. 다시 표시되지 않습니다.", "done": "완료", "savePermissions": "저장 권한", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 166c619760..95b12dfae5 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 6d2e1a8cdb..228396d6a6 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "cth., Kunci Pengeluaran, Kunci Pembangunan", "keyNameDesc": "Pilih nama deskriptif untuk mengenal pasti tujuan kunci ini", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Kunci API Dicipta", "keyCreatedSuccess": "Kunci berjaya dibuat!", "keyCreatedNote": "Salin dan simpan kunci ini sekarang — ia tidak akan ditunjukkan lagi.", "done": "Selesai", "savePermissions": "Simpan Kebenaran", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 951b019ee9..ceb077a56b 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "bijvoorbeeld productiesleutel, ontwikkelingssleutel", "keyNameDesc": "Kies een beschrijvende naam om het doel van deze sleutel te identificeren", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API-sleutel gemaakt", "keyCreatedSuccess": "Sleutel succesvol aangemaakt!", "keyCreatedNote": "Kopieer en bewaar deze sleutel nu. Deze wordt niet meer weergegeven.", "done": "Klaar", "savePermissions": "Bewaar machtigingen", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index f9a8f28756..e2f741185c 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "f.eks. produksjonsnøkkel, utviklingsnøkkel", "keyNameDesc": "Velg et beskrivende navn for å identifisere formålet med denne nøkkelen", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API-nøkkel opprettet", "keyCreatedSuccess": "Nøkkel ble opprettet!", "keyCreatedNote": "Kopier og lagre denne nøkkelen nå – den vises ikke igjen.", "done": "Ferdig", "savePermissions": "Lagre tillatelser", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index d246005589..5413e5010d 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "hal., Production Key, Development Key", "keyNameDesc": "Pumili ng mapaglarawang pangalan para matukoy ang layunin ng susi na ito", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Nagawa ang API Key", "keyCreatedSuccess": "Matagumpay na nagawa ang susi!", "keyCreatedNote": "Kopyahin at iimbak ang key na ito ngayon — hindi na ito muling ipapakita.", "done": "Tapos na", "savePermissions": "I-save ang Mga Pahintulot", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 244c0ffa50..28d4f60390 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "np. klucz produkcyjny, klucz rozwojowy", "keyNameDesc": "Wybierz opisową nazwę identyfikującą przeznaczenie tego klucza", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Utworzono klucz API", "keyCreatedSuccess": "Klucz został utworzony pomyślnie!", "keyCreatedNote": "Skopiuj i zapisz ten klucz teraz — nie będzie on więcej wyświetlany.", "done": "Gotowe", "savePermissions": "Zapisz uprawnienia", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 5989677d6b..d4282a4d0a 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -336,11 +336,20 @@ "keyNamePlaceholder": "ex: Chave de Produção", "keyNameDesc": "Escolha um nome descritivo para identificar o propósito desta chave", "managementAccessDesc": "Permitir que esta chave de API gerencie a configuração do OmniRoute.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Chave de API Criada", "keyCreatedSuccess": "Chave criada com sucesso!", "keyCreatedNote": "Copie e armazene esta chave agora — ela não será mostrada novamente.", "done": "Pronto", "savePermissions": "Salvar Permissões", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Resolve automaticamente nomes ambíguos de modelo para o provedor nativo desta API key.", "keyActive": "Chave Ativa", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 0c9fbc0334..980b42953a 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "por exemplo, chave de produção, chave de desenvolvimento", "keyNameDesc": "Escolha um nome descritivo para identificar a finalidade desta chave", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Chave de API criada", "keyCreatedSuccess": "Chave criada com sucesso!", "keyCreatedNote": "Copie e armazene esta chave agora — ela não será mostrada novamente.", "done": "Concluído", "savePermissions": "Salvar permissões", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 7e7dafed91..86c028f3ce 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "de exemplu, cheie de producție, cheie de dezvoltare", "keyNameDesc": "Alegeți un nume descriptiv pentru a identifica scopul acestei chei", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Cheia API creată", "keyCreatedSuccess": "Cheie creată cu succes!", "keyCreatedNote": "Copiați și stocați această cheie acum - nu va fi afișată din nou.", "done": "Gata", "savePermissions": "Salvare permisiuni", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index f34d842a28..0f444aa551 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "например, Ключ производства, Ключ разработки", "keyNameDesc": "Выберите описательное имя, чтобы определить назначение этого ключа.", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Ключ API создан", "keyCreatedSuccess": "Ключ успешно создан!", "keyCreatedNote": "Скопируйте и сохраните этот ключ сейчас — он больше не будет отображаться.", "done": "Готово", "savePermissions": "Сохранить разрешения", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Авторазрешение", "autoResolveDesc": "Автоматически сопоставлять неоднозначные имена моделей с нативным провайдером для этого API-ключа.", "keyActive": "Ключ активен", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 993d956efa..a4505e452a 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "napr. Výrobný kľúč, Vývojový kľúč", "keyNameDesc": "Vyberte popisný názov na identifikáciu účelu tohto kľúča", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Kľúč API bol vytvorený", "keyCreatedSuccess": "Kľúč bol úspešne vytvorený!", "keyCreatedNote": "Skopírujte a uložte tento kľúč teraz – už sa nebude zobrazovať.", "done": "Hotovo", "savePermissions": "Uložiť povolenia", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 63a35aa6aa..d9da4bd318 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "t.ex. produktionsnyckel, utvecklingsnyckel", "keyNameDesc": "Välj ett beskrivande namn för att identifiera denna nyckels syfte", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API-nyckel skapad", "keyCreatedSuccess": "Nyckel skapad framgångsrikt!", "keyCreatedNote": "Kopiera och lagra den här nyckeln nu – den kommer inte att visas igen.", "done": "Klart", "savePermissions": "Spara behörigheter", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 2871f2f7ce..6183d59ca7 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index db0f010f0c..06a56309be 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 0a16c374ec..6c9eb79777 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 835b7ba514..afccf15c34 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "เช่น คีย์การผลิต คีย์การพัฒนา", "keyNameDesc": "เลือกชื่อที่สื่อความหมายเพื่อระบุวัตถุประสงค์ของคีย์นี้", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "สร้างคีย์ API แล้ว", "keyCreatedSuccess": "สร้างคีย์สำเร็จแล้ว!", "keyCreatedNote": "คัดลอกและจัดเก็บคีย์นี้ทันที ซึ่งจะไม่แสดงอีก", "done": "เสร็จแล้ว", "savePermissions": "บันทึกสิทธิ์", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 065cef19af..e5be891031 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "Örn. Üretim Anahtarı, Geliştirme Anahtarı", "keyNameDesc": "Bu anahtarın amacını tanımlamak için açıklayıcı bir ad seçin", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Anahtarı Oluşturuldu", "keyCreatedSuccess": "Anahtar başarıyla oluşturuldu!", "keyCreatedNote": "Bu anahtarı şimdi kopyalayıp saklayın; bir daha gösterilmeyecek.", "done": "Bitti", "savePermissions": "İzinleri Kaydet", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Otomatik Çözümle", "autoResolveDesc": "Bu API anahtarı için belirsiz model adlarını kaynak sağlayıcıya otomatik olarak çözümleyin.", "keyActive": "Anahtar Aktif", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 819ee6107a..1fa9bcfff3 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "наприклад, ключ виробництва, ключ розробки", "keyNameDesc": "Виберіть описову назву, щоб визначити призначення цього ключа", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Ключ API створено", "keyCreatedSuccess": "Ключ успішно створено!", "keyCreatedNote": "Скопіюйте та збережіть цей ключ зараз — він більше не відображатиметься.", "done": "Готово", "savePermissions": "Зберегти дозволи", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index be4bdf3871..dbdece1d6d 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "e.g., Production Key, Development Key", "keyNameDesc": "Choose a descriptive name to identify this key's purpose", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API Key Created", "keyCreatedSuccess": "Key created successfully!", "keyCreatedNote": "Copy and store this key now — it won't be shown again.", "done": "Done", "savePermissions": "Save Permissions", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 3ed0a1e72f..865059d249 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "ví dụ: Khóa sản xuất, Khóa phát triển", "keyNameDesc": "Chọn tên mô tả để xác định mục đích của khóa này", "managementAccessDesc": "__MISSING__:Allow this API key to manage OmniRoute configuration.", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "Đã tạo khóa API", "keyCreatedSuccess": "Đã tạo khóa thành công!", "keyCreatedNote": "Sao chép và lưu trữ khóa này ngay bây giờ — nó sẽ không được hiển thị lại.", "done": "Xong", "savePermissions": "Lưu quyền", + "endpointRestrictions": "__MISSING__:Allowed Endpoints", + "allEndpointsAllowed": "__MISSING__:This key can access all API endpoints.", + "endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.", "autoResolve": "Auto-Resolve", "autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.", "keyActive": "Key Active", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 5d7cfe03ba..4824b775fa 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1465,11 +1465,20 @@ "keyNamePlaceholder": "例如:生产环境密钥、开发环境密钥", "keyNameDesc": "使用清晰的名称标识该密钥的用途", "managementAccessDesc": "允许此 API 密钥管理 OmniRoute 配置。", + "selfServiceVisibility": "__MISSING__:Self-Service Visibility", + "selfServiceVisibilityDesc": "__MISSING__:Control what this key can see about its own usage and shared upstream quota.", + "ownUsageVisibility": "__MISSING__:Own Cost and Token Usage", + "ownUsageVisibilityDesc": "__MISSING__:Allow this key to call its status endpoint for its own USD usage, budget percent, and token totals.", + "sharedAccountQuotaVisibility": "__MISSING__:Shared Account Quota", + "sharedAccountQuotaVisibilityDesc": "__MISSING__:Allow this key to see shared upstream account quota when one explicit connection is configured.", "keyCreated": "API 密钥已创建", "keyCreatedSuccess": "密钥创建成功!", "keyCreatedNote": "请立即复制并保存此密钥,它不会再次显示。", "done": "完成", "savePermissions": "保存权限", + "endpointRestrictions": "允许的端点", + "allEndpointsAllowed": "此密钥可以访问所有 API 端点。", + "endpointsRestricted": "仅限 {count} 个端点。", "autoResolve": "自动解析", "autoResolveDesc": "为这个 API 密钥自动将有歧义的模型名解析到原生提供商。", "keyActive": "密钥启用状态", @@ -1557,10 +1566,7 @@ "filterTypeRestricted": "受限", "shownOf": "已显示 {shown} / {total}", "emptyFilterTitle": "没有密钥匹配当前筛选条件", - "emptyFilterClear": "清除筛选", - "allEndpointsAllowed": "此密钥可以访问所有 API 端点。", - "endpointRestrictions": "允许的端点", - "endpointsRestricted": "仅限 {count} 个端点。" + "emptyFilterClear": "清除筛选" }, "auditLog": { "title": "审核日志", diff --git a/src/lib/db/migrations/075_api_key_self_service_usage_scopes.sql b/src/lib/db/migrations/075_api_key_self_service_usage_scopes.sql new file mode 100644 index 0000000000..586e5c5f2c --- /dev/null +++ b/src/lib/db/migrations/075_api_key_self_service_usage_scopes.sql @@ -0,0 +1,51 @@ +-- Migration 075: backfill self-service own-usage visibility for existing API keys. +-- +-- This is intentionally a one-time compatibility update. After it has run, +-- operators may remove "self:usage" from a key and the absence of the scope +-- means self-service usage visibility is disabled. + +CREATE TABLE IF NOT EXISTS key_value ( + namespace TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (namespace, key) +); + +UPDATE api_keys +SET scopes = json_array('self:usage') +WHERE NOT EXISTS ( + SELECT 1 + FROM key_value + WHERE namespace = 'apiKeySelfService' + AND key = 'usageScopesBackfilled' + ) + AND ( + scopes IS NULL + OR trim(scopes) = '' + OR json_valid(scopes) = 0 + OR CASE + WHEN json_valid(scopes) = 1 THEN json_type(scopes) != 'array' + ELSE 0 + END + ); + +UPDATE api_keys +SET scopes = json_insert(scopes, '$[#]', 'self:usage') +WHERE NOT EXISTS ( + SELECT 1 + FROM key_value + WHERE namespace = 'apiKeySelfService' + AND key = 'usageScopesBackfilled' + ) + AND scopes IS NOT NULL + AND trim(scopes) != '' + AND json_valid(scopes) = 1 + AND json_type(scopes) = 'array' + AND NOT EXISTS ( + SELECT 1 + FROM json_each(api_keys.scopes) + WHERE value = 'self:usage' + ); + +INSERT OR IGNORE INTO key_value (namespace, key, value) +VALUES ('apiKeySelfService', 'usageScopesBackfilled', datetime('now')); diff --git a/src/lib/usage/apiKeySelfService.ts b/src/lib/usage/apiKeySelfService.ts new file mode 100644 index 0000000000..f5c5ee422e --- /dev/null +++ b/src/lib/usage/apiKeySelfService.ts @@ -0,0 +1,289 @@ +import { + hasSelfAccountQuotaScope, + hasSelfUsageScope, +} from "@/shared/constants/selfServiceScopes"; + +type JsonRecord = Record; + +interface ApiKeySelfServiceMetadata { + id: string; + name: string; + scopes: string[]; + allowedConnections: string[]; +} + +interface StatementLike { + get: (...params: unknown[]) => unknown; +} + +interface DbLike { + prepare: (sql: string) => StatementLike; +} + +interface CostSummaryLike { + budget: unknown; + totalCostMonth: number; + totalCostPeriod: number; + activeLimitUsd: number; + resetInterval: string | null; + budgetResetAt: number | null; + periodStartAt: number | null; + nextResetAt: number | null; + warningThreshold: number | null; +} + +type GetCostSummaryFn = (apiKeyId: string) => CostSummaryLike; +type CheckBudgetFn = (apiKeyId: string) => unknown; +type GetDbInstanceFn = () => DbLike; +type GetProviderConnectionByIdFn = (connectionId: string) => Promise; +type FetchAndPersistProviderLimitsFn = ( + connectionId: string, + source: "manual" +) => Promise<{ usage: JsonRecord }>; + +interface ApiKeySelfServiceDeps { + now?: () => number; + getCostSummary?: GetCostSummaryFn; + checkBudget?: CheckBudgetFn; + getDbInstance?: GetDbInstanceFn; + getProviderConnectionById?: GetProviderConnectionByIdFn; + fetchAndPersistProviderLimits?: FetchAndPersistProviderLimitsFn; +} + +interface TokenTotals { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheCreationTokens: number; + reasoningTokens: number; + totalTokens: number; +} + +function toNumber(value: unknown, fallback = 0): number { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "bigint") return Number(value); + if (typeof value === "string" && value.trim()) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; + } + return fallback; +} + +function roundNumber(value: number, precision = 6): number { + if (!Number.isFinite(value)) return 0; + return Number(value.toFixed(precision)); +} + +function isoOrNull(value: number | string | null | undefined): string | null { + if (typeof value === "number" && Number.isFinite(value) && value > 0) { + return new Date(value).toISOString(); + } + if (typeof value === "string" && value.trim()) { + const parsed = Date.parse(value); + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null; + } + return null; +} + +function getCurrentMonthWindow(now: number) { + const date = new Date(now); + const start = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1, 0, 0, 0, 0); + const next = Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1, 0, 0, 0, 0); + return { periodStartAt: start, resetAt: next }; +} + +function buildCostStatus(summary: CostSummaryLike, now: number) { + const hasBudget = !!summary.budget && toNumber(summary.activeLimitUsd) > 0; + const fallbackWindow = getCurrentMonthWindow(now); + const periodStartAt = hasBudget + ? toNumber(summary.periodStartAt, fallbackWindow.periodStartAt) + : fallbackWindow.periodStartAt; + const resetAt = hasBudget + ? toNumber(summary.nextResetAt ?? summary.budgetResetAt, fallbackWindow.resetAt) + : fallbackWindow.resetAt; + const usedUsd = hasBudget + ? roundNumber(toNumber(summary.totalCostPeriod)) + : roundNumber(toNumber(summary.totalCostMonth)); + const limitUsd = hasBudget ? roundNumber(toNumber(summary.activeLimitUsd)) : null; + const remainingUsd = limitUsd === null ? null : roundNumber(Math.max(limitUsd - usedUsd, 0)); + const usedPercent = + limitUsd === null || limitUsd <= 0 ? null : roundNumber((usedUsd / limitUsd) * 100, 2); + + return { + period: (hasBudget ? summary.resetInterval : "monthly") ?? "monthly", + currency: "USD", + usedUsd, + limitUsd, + remainingUsd, + usedPercent, + warningThreshold: hasBudget ? (summary.warningThreshold ?? null) : null, + resetAt: isoOrNull(resetAt), + periodStartAt: isoOrNull(periodStartAt), + }; +} + +function aggregateTokens(db: DbLike, apiKeyId: string, periodStartAt: string): TokenTotals { + const row = db + .prepare( + ` + SELECT + COALESCE(SUM(tokens_input), 0) AS inputTokens, + COALESCE(SUM(tokens_output), 0) AS outputTokens, + COALESCE(SUM(tokens_cache_read), 0) AS cacheReadTokens, + COALESCE(SUM(tokens_cache_creation), 0) AS cacheCreationTokens, + COALESCE(SUM(tokens_reasoning), 0) AS reasoningTokens + FROM usage_history + WHERE api_key_id = ? + AND timestamp >= ? + ` + ) + .get(apiKeyId, periodStartAt) as JsonRecord | undefined; + + const inputTokens = toNumber(row?.inputTokens); + const outputTokens = toNumber(row?.outputTokens); + const cacheReadTokens = toNumber(row?.cacheReadTokens); + const cacheCreationTokens = toNumber(row?.cacheCreationTokens); + const reasoningTokens = toNumber(row?.reasoningTokens); + + return { + inputTokens, + outputTokens, + cacheReadTokens, + cacheCreationTokens, + reasoningTokens, + totalTokens: + inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens + reasoningTokens, + }; +} + +function unavailableAccountQuota(reason: string) { + return { available: false, reason }; +} + +function quotaWindow(value: unknown) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const record = value as JsonRecord; + const usedPercentage = toNumber(record.usedPercentage ?? record.used, Number.NaN); + const remainingPercentage = toNumber( + record.remainingPercentage ?? record.remaining, + Number.isFinite(usedPercentage) ? 100 - usedPercentage : Number.NaN + ); + if (!Number.isFinite(usedPercentage) && !Number.isFinite(remainingPercentage)) return null; + + return { + usedPercentage: Number.isFinite(usedPercentage) + ? roundNumber(usedPercentage, 2) + : roundNumber(100 - remainingPercentage, 2), + remainingPercentage: Number.isFinite(remainingPercentage) + ? roundNumber(remainingPercentage, 2) + : roundNumber(100 - usedPercentage, 2), + resetAt: isoOrNull(record.resetAt as string | number | null | undefined), + }; +} + +async function resolveAccountQuota(metadata: ApiKeySelfServiceMetadata, deps: RequiredDeps) { + if (!hasSelfAccountQuotaScope(metadata.scopes)) return undefined; + + const allowedConnections = Array.isArray(metadata.allowedConnections) + ? metadata.allowedConnections + : []; + if (allowedConnections.length !== 1) { + return unavailableAccountQuota("ambiguous_connection"); + } + + const connection = (await deps.getProviderConnectionById(allowedConnections[0])) as + | JsonRecord + | null; + if (!connection) { + return unavailableAccountQuota("no_allowed_connection"); + } + + const provider = typeof connection.provider === "string" ? connection.provider : ""; + if (provider !== "codex") { + return unavailableAccountQuota("not_supported"); + } + + try { + const result = await deps.fetchAndPersistProviderLimits(allowedConnections[0], "manual"); + const usage = result.usage as JsonRecord; + const quotas = + usage.quotas && typeof usage.quotas === "object" && !Array.isArray(usage.quotas) + ? (usage.quotas as JsonRecord) + : null; + if (!quotas) return unavailableAccountQuota("not_available"); + + const session = quotaWindow(quotas.session); + const weekly = quotaWindow(quotas.weekly); + if (!session && !weekly) return unavailableAccountQuota("not_available"); + + return { + provider, + connectionId: allowedConnections[0], + shared: true, + quotas: { + ...(session && { session }), + ...(weekly && { weekly }), + }, + }; + } catch { + return unavailableAccountQuota("fetch_failed"); + } +} + +type RequiredDeps = Required; + +async function normalizeDeps(deps: ApiKeySelfServiceDeps): Promise { + const costRules = + deps.getCostSummary && deps.checkBudget ? null : await import("@/domain/costRules"); + const dbCore = deps.getDbInstance ? null : await import("@/lib/db/core"); + const localDb = deps.getProviderConnectionById ? null : await import("@/lib/localDb"); + const providerLimits = deps.fetchAndPersistProviderLimits + ? null + : await import("@/lib/usage/providerLimits"); + + return { + now: deps.now ?? Date.now, + getCostSummary: deps.getCostSummary ?? costRules!.getCostSummary, + checkBudget: deps.checkBudget ?? costRules!.checkBudget, + getDbInstance: deps.getDbInstance ?? dbCore!.getDbInstance, + getProviderConnectionById: deps.getProviderConnectionById ?? localDb!.getProviderConnectionById, + fetchAndPersistProviderLimits: + deps.fetchAndPersistProviderLimits ?? providerLimits!.fetchAndPersistProviderLimits, + }; +} + +export async function buildApiKeySelfServiceStatus( + metadata: ApiKeySelfServiceMetadata, + deps: ApiKeySelfServiceDeps = {} +) { + if (!hasSelfUsageScope(metadata.scopes)) { + throw new Error("missing_self_usage_scope"); + } + + const resolvedDeps = await normalizeDeps(deps); + const summary = resolvedDeps.getCostSummary(metadata.id); + resolvedDeps.checkBudget(metadata.id); + + const cost = buildCostStatus(summary, resolvedDeps.now()); + const tokens = aggregateTokens( + resolvedDeps.getDbInstance() as DbLike, + metadata.id, + cost.periodStartAt ?? new Date(getCurrentMonthWindow(resolvedDeps.now()).periodStartAt).toISOString() + ); + const accountQuota = await resolveAccountQuota(metadata, resolvedDeps); + + return { + apiKey: { + id: metadata.id, + name: metadata.name, + }, + usage: { + cost, + tokens: { + periodStartAt: cost.periodStartAt, + ...tokens, + }, + }, + ...(accountQuota !== undefined && { accountQuota }), + }; +} diff --git a/src/shared/constants/selfServiceScopes.ts b/src/shared/constants/selfServiceScopes.ts new file mode 100644 index 0000000000..81a4987af4 --- /dev/null +++ b/src/shared/constants/selfServiceScopes.ts @@ -0,0 +1,20 @@ +export const SELF_USAGE_SCOPE = "self:usage"; +export const SELF_ACCOUNT_QUOTA_SCOPE = "self:account-quota"; + +export const DEFAULT_SELF_SERVICE_SCOPES = [SELF_USAGE_SCOPE] as const; + +export function hasSelfUsageScope(scopes: readonly string[] | null | undefined): boolean { + return Array.isArray(scopes) && scopes.includes(SELF_USAGE_SCOPE); +} + +export function hasSelfAccountQuotaScope(scopes: readonly string[] | null | undefined): boolean { + return Array.isArray(scopes) && scopes.includes(SELF_ACCOUNT_QUOTA_SCOPE); +} + +export function normalizeSelfServiceScopesForCreate( + scopes: readonly string[] | null | undefined +): string[] { + const normalized = new Set((scopes ?? []).filter((scope) => typeof scope === "string" && scope)); + normalized.add(SELF_USAGE_SCOPE); + return [...normalized]; +} diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index b2af08bfb4..b696173914 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -491,7 +491,7 @@ export const importAgyAuthBulkSchema = z.object({ export const createKeySchema = z.object({ name: z.string().min(1, "Name is required").max(200), noLog: z.boolean().optional(), - scopes: z.array(z.string().trim().min(1).max(64)).max(16).optional(), + scopes: z.array(z.string().trim().min(1).max(64)).max(32).optional(), }); export const createSyncTokenSchema = z.object({ @@ -1826,7 +1826,7 @@ export const updateKeyPermissionsSchema = z z.null(), ]) .optional(), - scopes: z.array(z.string().trim().min(1).max(64)).max(16).optional(), + scopes: z.array(z.string().trim().min(1).max(64)).max(32).optional(), allowedEndpoints: z.array(z.string().trim().min(1).max(64)).max(20).optional(), }) .superRefine((value, ctx) => { diff --git a/tests/unit/api-key-scope-validation.test.ts b/tests/unit/api-key-scope-validation.test.ts new file mode 100644 index 0000000000..7ce6e37db0 --- /dev/null +++ b/tests/unit/api-key-scope-validation.test.ts @@ -0,0 +1,56 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + DEFAULT_SELF_SERVICE_SCOPES, + SELF_ACCOUNT_QUOTA_SCOPE, + SELF_USAGE_SCOPE, + hasSelfAccountQuotaScope, + hasSelfUsageScope, + normalizeSelfServiceScopesForCreate, +} from "../../src/shared/constants/selfServiceScopes.ts"; +import { createKeySchema, updateKeyPermissionsSchema } from "../../src/shared/validation/schemas.ts"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +test("self-service scope constants are distinct and usage defaults on create", () => { + assert.equal(SELF_USAGE_SCOPE, "self:usage"); + assert.equal(SELF_ACCOUNT_QUOTA_SCOPE, "self:account-quota"); + assert.deepEqual(DEFAULT_SELF_SERVICE_SCOPES, [SELF_USAGE_SCOPE]); + + assert.deepEqual(normalizeSelfServiceScopesForCreate(undefined), [SELF_USAGE_SCOPE]); + assert.deepEqual(normalizeSelfServiceScopesForCreate([]), [SELF_USAGE_SCOPE]); + assert.deepEqual(normalizeSelfServiceScopesForCreate(["manage"]), ["manage", SELF_USAGE_SCOPE]); + assert.deepEqual(normalizeSelfServiceScopesForCreate([SELF_ACCOUNT_QUOTA_SCOPE]), [ + SELF_ACCOUNT_QUOTA_SCOPE, + SELF_USAGE_SCOPE, + ]); +}); + +test("self-service scope helpers do not treat account quota as own-usage visibility", () => { + assert.equal(hasSelfUsageScope([SELF_USAGE_SCOPE]), true); + assert.equal(hasSelfUsageScope([SELF_ACCOUNT_QUOTA_SCOPE]), false); + assert.equal(hasSelfAccountQuotaScope([SELF_ACCOUNT_QUOTA_SCOPE]), true); + assert.equal(hasSelfAccountQuotaScope([SELF_USAGE_SCOPE]), false); +}); + +test("api key validation accepts more than sixteen scopes", () => { + const scopes = Array.from({ length: 18 }, (_, index) => `custom:${index}`); + + assert.equal(createKeySchema.safeParse({ name: "heavy-scope-key", scopes }).success, true); + assert.equal(updateKeyPermissionsSchema.safeParse({ scopes }).success, true); +}); + +test("api key create route normalizes omitted scopes to self-service usage", () => { + const source = fs.readFileSync(path.join(repoRoot, "src/app/api/keys/route.ts"), "utf8"); + + assert.match(source, /normalizeSelfServiceScopesForCreate/); + assert.ok( + source.indexOf("normalizeSelfServiceScopesForCreate(scopes)") < + source.indexOf("createApiKey(name, machineId"), + "create route must add default self-service scope before persistence" + ); +}); diff --git a/tests/unit/api-key-self-service.test.ts b/tests/unit/api-key-self-service.test.ts new file mode 100644 index 0000000000..0220a5d902 --- /dev/null +++ b/tests/unit/api-key-self-service.test.ts @@ -0,0 +1,220 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import DatabaseSync from "better-sqlite3"; + +import { SELF_ACCOUNT_QUOTA_SCOPE, SELF_USAGE_SCOPE } from "../../src/shared/constants/selfServiceScopes.ts"; +import { buildApiKeySelfServiceStatus } from "../../src/lib/usage/apiKeySelfService.ts"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const migrationPath = path.join( + repoRoot, + "src/lib/db/migrations/075_api_key_self_service_usage_scopes.sql" +); + +test("self-service scope migration backfills own usage once and preserves explicit account quota opt-in", () => { + const sql = fs.readFileSync(migrationPath, "utf8"); + const db = new DatabaseSync(":memory:"); + db.exec(` + CREATE TABLE api_keys ( + id TEXT PRIMARY KEY, + scopes TEXT + ); + + INSERT INTO api_keys (id, scopes) VALUES + ('legacy-empty', '[]'), + ('legacy-null', NULL), + ('custom', '["custom:scope"]'), + ('quota-opt-in', '["${SELF_ACCOUNT_QUOTA_SCOPE}"]'), + ('already-disabled-after-migration', '["custom:scope"]'); + `); + + db.exec(sql); + db.prepare("UPDATE api_keys SET scopes = ? WHERE id = ?").run( + JSON.stringify(["custom:scope"]), + "already-disabled-after-migration" + ); + db.exec(sql); + + const rows = db.prepare("SELECT id, scopes FROM api_keys ORDER BY id").all() as Array<{ + id: string; + scopes: string; + }>; + const scopesById = new Map(rows.map((row) => [row.id, JSON.parse(row.scopes) as string[]])); + + assert.deepEqual(scopesById.get("legacy-empty"), [SELF_USAGE_SCOPE]); + assert.deepEqual(scopesById.get("legacy-null"), [SELF_USAGE_SCOPE]); + assert.deepEqual(scopesById.get("custom"), ["custom:scope", SELF_USAGE_SCOPE]); + assert.deepEqual(scopesById.get("quota-opt-in"), [ + SELF_ACCOUNT_QUOTA_SCOPE, + SELF_USAGE_SCOPE, + ]); + assert.deepEqual(scopesById.get("already-disabled-after-migration"), ["custom:scope"]); +}); + +function makeDeps(overrides: Record = {}) { + const tokenRows = overrides.tokenRows ?? { + inputTokens: 900, + outputTokens: 30, + cacheReadTokens: 120, + cacheCreationTokens: 10, + reasoningTokens: 5, + }; + const dbParams: unknown[][] = []; + + return { + dbParams, + deps: { + now: () => Date.UTC(2026, 4, 29, 12, 0, 0), + getCostSummary: () => ({ + budget: null, + totalCostMonth: 12.34, + totalCostPeriod: 0, + activeLimitUsd: 0, + resetInterval: null, + resetTime: null, + budgetResetAt: null, + lastBudgetResetAt: null, + periodStartAt: null, + nextResetAt: null, + warningThreshold: null, + }), + checkBudget: () => ({ allowed: true }), + getDbInstance: () => ({ + prepare: () => ({ + get: (...params: unknown[]) => { + dbParams.push(params); + return tokenRows; + }, + }), + }), + getProviderConnectionById: async () => null, + fetchAndPersistProviderLimits: async () => { + throw new Error("unexpected quota fetch"); + }, + ...overrides, + }, + }; +} + +test("self-service status reports own cost and token usage with null budget fields when no budget exists", async () => { + const metadata = { + id: "key-a", + name: "team-a", + scopes: [SELF_USAGE_SCOPE], + allowedConnections: [], + }; + const { deps, dbParams } = makeDeps(); + + const status = await buildApiKeySelfServiceStatus(metadata, deps); + + assert.deepEqual(status.apiKey, { id: "key-a", name: "team-a" }); + assert.equal(status.usage.cost.usedUsd, 12.34); + assert.equal(status.usage.cost.limitUsd, null); + assert.equal(status.usage.cost.remainingUsd, null); + assert.equal(status.usage.cost.usedPercent, null); + assert.equal(status.usage.cost.period, "monthly"); + assert.equal(status.usage.tokens.totalTokens, 1065); + assert.equal(dbParams[0][0], "key-a"); + assert.equal(dbParams[0][1], "2026-05-01T00:00:00.000Z"); + assert.equal("accountQuota" in status, false); +}); + +test("self-service status reports USD budget percentage using the budget period", async () => { + const metadata = { + id: "key-budget", + name: "budgeted", + scopes: [SELF_USAGE_SCOPE], + allowedConnections: [], + }; + const periodStart = Date.UTC(2026, 4, 1, 0, 0, 0); + const nextReset = Date.UTC(2026, 5, 1, 0, 0, 0); + const { deps } = makeDeps({ + getCostSummary: () => ({ + budget: { resetInterval: "monthly" }, + totalCostMonth: 99, + totalCostPeriod: 12.5, + activeLimitUsd: 50, + resetInterval: "monthly", + resetTime: "00:00", + budgetResetAt: nextReset, + lastBudgetResetAt: periodStart, + periodStartAt: periodStart, + nextResetAt: nextReset, + warningThreshold: 0.8, + }), + }); + + const status = await buildApiKeySelfServiceStatus(metadata, deps); + + assert.equal(status.usage.cost.usedUsd, 12.5); + assert.equal(status.usage.cost.limitUsd, 50); + assert.equal(status.usage.cost.remainingUsd, 37.5); + assert.equal(status.usage.cost.usedPercent, 25); + assert.equal(status.usage.cost.periodStartAt, "2026-05-01T00:00:00.000Z"); + assert.equal(status.usage.cost.resetAt, "2026-06-01T00:00:00.000Z"); +}); + +test("self-service status treats unrestricted account quota connection access as ambiguous", async () => { + const metadata = { + id: "key-unrestricted", + name: "unrestricted", + scopes: [SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE], + allowedConnections: [], + }; + const { deps } = makeDeps(); + + const status = await buildApiKeySelfServiceStatus(metadata, deps); + + assert.deepEqual(status.accountQuota, { + available: false, + reason: "ambiguous_connection", + }); +}); + +test("self-service status normalizes Codex account quota only for one explicit connection", async () => { + const metadata = { + id: "key-codex", + name: "codex", + scopes: [SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE], + allowedConnections: ["conn-codex"], + }; + const { deps } = makeDeps({ + getProviderConnectionById: async (connectionId: string) => ({ + id: connectionId, + provider: "codex", + }), + fetchAndPersistProviderLimits: async () => ({ + connection: { id: "conn-codex", provider: "codex" }, + usage: { + quotas: { + session: { used: 1, remaining: 99, resetAt: "2026-05-29T18:11:44.000Z" }, + weekly: { used: 97, remaining: 3, resetAt: "2026-05-31T01:23:38.000Z" }, + }, + }, + cache: { quotas: null, plan: null, message: null, fetchedAt: "" }, + }), + }); + + const status = await buildApiKeySelfServiceStatus(metadata, deps); + + assert.deepEqual(status.accountQuota, { + provider: "codex", + connectionId: "conn-codex", + shared: true, + quotas: { + session: { + usedPercentage: 1, + remainingPercentage: 99, + resetAt: "2026-05-29T18:11:44.000Z", + }, + weekly: { + usedPercentage: 97, + remainingPercentage: 3, + resetAt: "2026-05-31T01:23:38.000Z", + }, + }, + }); +}); diff --git a/tests/unit/api-manager-scope-preservation.test.ts b/tests/unit/api-manager-scope-preservation.test.ts new file mode 100644 index 0000000000..eb0363a1d2 --- /dev/null +++ b/tests/unit/api-manager-scope-preservation.test.ts @@ -0,0 +1,52 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + buildApiKeyCreateScopes, + mergeApiKeyPermissionScopes, +} from "../../src/app/(dashboard)/dashboard/api-manager/apiManagerScopes.ts"; +import { + SELF_ACCOUNT_QUOTA_SCOPE, + SELF_USAGE_SCOPE, +} from "../../src/shared/constants/selfServiceScopes.ts"; + +test("create scopes enable own usage by default without shared account quota", () => { + assert.deepEqual(buildApiKeyCreateScopes({ manageEnabled: false }), [SELF_USAGE_SCOPE]); + assert.deepEqual(buildApiKeyCreateScopes({ manageEnabled: true }), ["manage", SELF_USAGE_SCOPE]); + assert.deepEqual( + buildApiKeyCreateScopes({ + manageEnabled: false, + selfUsageEnabled: false, + selfAccountQuotaEnabled: true, + }), + [] + ); +}); + +test("permission scope merge preserves unrelated scopes while toggling managed scopes", () => { + const scopes = mergeApiKeyPermissionScopes(["custom:scope", SELF_USAGE_SCOPE], { + manageEnabled: true, + selfUsageEnabled: true, + selfAccountQuotaEnabled: true, + }); + + assert.deepEqual(scopes, [ + "custom:scope", + SELF_USAGE_SCOPE, + "manage", + SELF_ACCOUNT_QUOTA_SCOPE, + ]); +}); + +test("permission scope merge removes shared quota visibility when own usage is disabled", () => { + const scopes = mergeApiKeyPermissionScopes( + ["custom:scope", SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE], + { + manageEnabled: false, + selfUsageEnabled: false, + selfAccountQuotaEnabled: true, + } + ); + + assert.deepEqual(scopes, ["custom:scope"]); +}); diff --git a/tests/unit/api/v1-me-status-route.test.ts b/tests/unit/api/v1-me-status-route.test.ts new file mode 100644 index 0000000000..b7358f989e --- /dev/null +++ b/tests/unit/api/v1-me-status-route.test.ts @@ -0,0 +1,27 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +const routePath = path.join(repoRoot, "src/app/api/v1/me/status/route.ts"); + +test("GET /api/v1/me/status rejects missing Bearer token in the handler", async () => { + const route = await import("../../../src/app/api/v1/me/status/route.ts"); + + const response = await route.GET(new Request("http://localhost/api/v1/me/status")); + + assert.equal(response.status, 401); +}); + +test("GET /api/v1/me/status derives identity from Bearer metadata and ignores query apiKeyId", () => { + const source = fs.readFileSync(routePath, "utf8"); + + assert.match(source, /Authorization/); + assert.match(source, /Bearer/); + assert.match(source, /validateApiKey/); + assert.match(source, /getApiKeyMetadata/); + assert.match(source, /metadata\.id === "env-key"/); + assert.doesNotMatch(source, /searchParams\.get\(["']apiKeyId["']\)/); +}); diff --git a/tests/unit/budget-route-auth.test.ts b/tests/unit/budget-route-auth.test.ts new file mode 100644 index 0000000000..ad16abdfdd --- /dev/null +++ b/tests/unit/budget-route-auth.test.ts @@ -0,0 +1,36 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const routePath = path.join(repoRoot, "src/app/api/usage/budget/route.ts"); + +test("/api/usage/budget enforces management auth inside GET and POST handlers", () => { + const source = fs.readFileSync(routePath, "utf8"); + + assert.match(source, /from ["']@\/lib\/api\/requireManagementAuth["']/); + + const getIndex = source.indexOf("export async function GET"); + const postIndex = source.indexOf("export async function POST"); + assert.ok(getIndex >= 0, "GET handler must exist"); + assert.ok(postIndex >= 0, "POST handler must exist"); + + const getBody = source.slice(getIndex, postIndex); + const postBody = source.slice(postIndex); + + assert.match(getBody, /const authError = await requireManagementAuth\(request\);/); + assert.match(getBody, /if \(authError\) return authError;/); + assert.ok( + getBody.indexOf("requireManagementAuth(request)") < getBody.indexOf("new URL(request.url)"), + "GET must authorize before reading arbitrary apiKeyId" + ); + + assert.match(postBody, /const authError = await requireManagementAuth\(request\);/); + assert.match(postBody, /if \(authError\) return authError;/); + assert.ok( + postBody.indexOf("requireManagementAuth(request)") < postBody.indexOf("request.json()"), + "POST must authorize before parsing budget mutations" + ); +});