Skip to content

feat(webapp): add multiple environment API key management - #4390

Open
carderne wants to merge 13 commits into
mainfrom
feat/multi-keys-surface
Open

feat(webapp): add multiple environment API key management#4390
carderne wants to merge 13 commits into
mainfrom
feat/multi-keys-surface

Conversation

@carderne

@cardernecarderne commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Projects can create, inspect, expire, and revoke multiple API keys for each environment. Plaintext values are shown only at creation; stored credentials are hashed and the API keys page displays only an obfuscated suffix afterward.

Self-hosted installations support full-access additional keys by default. Authorization extensions can provide additional access presets and optional task selection. Additional keys can also mint scoped public access tokens through the Trigger.dev API without receiving the environment signing key.

Feature notes

  • Only admin+ can create API keys (Developer can make in Development branch).
  • JWT self-signing will be a server call when used with new _ak_ keys.
  • JWTs with long expiry can keep working even with api key deleted (gets priveleges from api key, signed with root key)
  • Unfiltered session listings intentionally preserve the existing broad task-read behavior. Filtered listings enforce task-level scopes for every requested task.
  • Buffered runs without a task identifier are not safely authorizable, so cancel/replay requests fail closed rather than resolving an unscoped run.
  • Batch and waitpoint endpoints intentionally return server-minted, narrowly scoped public tokens to all callers. These tokens have bounded lifetimes and may remain valid until expiry after API-key revocation.

Deployment notes

Deploy the management UI and public-token endpoint with new key creation disabled. Enable creation for selected organizations after the authentication path and released SDK have been verified, then expand availability gradually.

Revoking an API key prevents new bearer requests and new token minting. Public tokens already minted by that key remain valid until their own expiration because they are signed by the environment signing key.

TODO

  • Add "Created by" to the key table
  • Document that streamed batch ingestion is non-atomic and may
    partially accept items before a validation or authorization error.

Follow-ups

  • Add an organization-level feature flag for the API key management UI and creation action.
  • Document rollout ordering: enable additional-key lookup before enabling issuance.
  • Add a system-wide gate that can stop new key issuance without disabling authentication for existing keys.
  • Replace the generic SDK compatibility warning with the first published compatible version. Old SDK will mint an unusable token if given an _ak_ key.
  • Add public documentation covering creation, storage, expiration, revocation, SDK compatibility, and public-token lifetime behavior.
  • Add observability for key creation, revocation, policy preparation failures, and public-token mint failures.
  • Exercise create, copy-once display, authenticate, mint, expire, and revoke flows end to end before broad enablement.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e643624e-bd8d-4021-a04f-5e17bd6c2666

📥 Commits

Reviewing files that changed from the base of the PR and between b537812 and 3952cbe.

📒 Files selected for processing (1)
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx
📜 Recent review details
⏰ Context from checks skipped due to timeout. (20)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 12)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 12)
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: obsmap / 🧪 Unit Tests: Observability Map
  • GitHub Check: typecheck / typecheck
  • GitHub Check: code-quality / code-quality
  • GitHub Check: report
  • GitHub Check: 🛡️ E2E Auth Tests (full)
  • GitHub Check: Analyze (javascript-typescript)

Walkthrough

This PR adds feature-flagged environment API key creation and revocation with RBAC-based presets, task scoping, expiration validation, persistence, telemetry, and audit crumbs. It updates API key listing and management UI with organization scoping, revoked-key filtering, status metadata, and creation or revocation dialogs. It adds public-token minting with scope and expiration validation and JWT signing. Authentication services now accept injectable dependencies. Tests and change notes cover the changes.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 6.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely describes the primary change: multiple environment API key management.
Description check✅ PassedThe description clearly covers scope, behavior, deployment, security, observability, and follow-ups, despite omitting several template sections.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/multi-keys-surface

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@changeset-bot

changeset-botBot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3952cbe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 27 packages
NameType
@trigger.dev/sdkPatch
@trigger.dev/pythonPatch
@internal/dashboard-agentPatch
@internal/sdk-compat-testsPatch
@trigger.dev/buildPatch
@trigger.dev/corePatch
@trigger.dev/react-hooksPatch
@trigger.dev/redis-workerPatch
@trigger.dev/rscPatch
@trigger.dev/schema-to-jsonPatch
@trigger.dev/databasePatch
@trigger.dev/otlp-importerPatch
@trigger.dev/rbacPatch
@trigger.dev/ssoPatch
trigger.devPatch
@internal/cachePatch
@internal/clickhousePatch
@internal/llm-model-catalogPatch
@internal/metrics-pipelinePatch
@internal/redisPatch
@internal/replicationPatch
@internal/run-enginePatch
@internal/run-storePatch
@internal/schedule-enginePatch
@internal/testcontainersPatch
@internal/tracingPatch
@internal/tsqlPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@carderne
carderneforce-pushed the feat/multi-keys-surface branch from 6aba247 to 2579113CompareJuly 27, 2026 11:51
@pkg-pr-new

pkg-pr-newBot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@b537812

trigger.dev

npm i https://pkg.pr.new/trigger.dev@b537812

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@b537812

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@b537812

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@b537812

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@b537812

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@b537812

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@b537812

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@b537812

commit: b537812

@carderne
carderneforce-pushed the feat/multi-keys-surface branch from 2579113 to dbca394CompareJuly 27, 2026 12:35
@carderne
carderneforce-pushed the feat/multi-keys-surface branch 3 times, most recently from d60d5a5 to ba81e5eCompareJuly 28, 2026 11:21
@carderne
carderne marked this pull request as ready for review July 29, 2026 17:07
devin-ai-integration[bot]

This comment was marked as resolved.

@carderne
carderne changed the base branch from feat/multi-keys-auth to mainJuly 30, 2026 20:19
@carderne
carderne changed the base branch from main to feat/multi-keys-authJuly 30, 2026 20:19
devin-ai-integration[bot]

This comment was marked as resolved.

Base automatically changed from feat/multi-keys-auth to mainAugust 3, 2026 13:00
devin-ai-integration[bot]

This comment was marked as resolved.

@carderne
carderneforce-pushed the feat/multi-keys-surface branch from ca744a5 to 5f47b92CompareAugust 4, 2026 13:48
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

A number passed to `expirationTime` is a Unix timestamp in seconds, not
milliseconds as the JSDoc claimed. Following the old docs produced a token
that effectively never expired.
Also fail loudly when an additional API key reaches a local self-signing
fallback. Those keys are not the environment's JWT signing material, so the
token would never verify. Every endpoint that returns a public access token
sets `x-trigger-jwt`, so this is unreachable today.
The API key policy methods are optional on the plugin-facing controller
contract, so `Pick` over it yields optional members that these call sites
would have to guard. Both already receive the LazyController singleton,
which has substituted its fail-closed defaults, so point them at
HostRbacController and keep the call sites guard-free.
Require both the global issuance switch and organization rollout flag before creating additional keys, while leaving existing credentials available for use and revocation. Show nullable creators and identify SDK v4.5.8 as the first compatible public-token version.
Record bounded outcomes for additional key creation, policy preparation, revocation, and public-token minting.
@carderne
carderneforce-pushed the feat/multi-keys-surface branch from c91b01d to b537812CompareAugust 5, 2026 17:07
@coderabbitai

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@github-actions

github-actionsBot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Observability map

As of 3952cbe.

18/100 over 412 measured of 428 entry points (base 18, no change)

What this PR changed

routebaseheadnow failing
/_app/orgs/:organizationSlug/projects/:projectParam/env/:envParam/apikeys6750error-classification
/api/v1/auth/public-tokensnewnot measured

FIX FIRST

  • /api/v1/projects/:projectRef/envvars (sensitive) - auth-boundary, request-context
  • /auth/sso (sensitive) - auth-boundary, request-context
  • /_app/orgs/:organizationSlug/settings/team (sensitive) - error-classification, auth-scope, request-context

AUDIT 3 of 50 sensitive mutations record an actor. 47 without one.
CONTEXT 11 of 412 entry points name a tenant on a failure path. 325 appear only here, 39 of them sensitive, in the JSON rather than the fix list.

What the score is made of
CHECKS
error-classification 167 applicable, 94 pass, 0 sole, global without it 10
auth-boundary 62 applicable, 57 pass, 0 sole, global without it 15
auth-scope 19 applicable, 17 pass, 0 sole, global without it 18
request-context 412 applicable, 11 pass, 223 sole, global without it 64
audit-trail 50 applicable, 3 pass, 0 sole, not in the score

Report only, nothing here gates the merge. The rules and their reasons: internal-packages/observability-map/README.md.

@devin-ai-integrationdevin-ai-integrationBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +1011 to +1012
<div className="mt-1 text-sm font-semibold text-text-bright">
{preset?.label ?? "No restrictions"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Key creation screen can claim a limited key grants unrestricted access

The permissions summary shown while creating a key falls back to the unrestricted description (PRESET_SCOPE_DETAILS.FULL_ACCESS at apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx:1011) whenever the chosen access level isn't in the hard-coded list, so a limited key is advertised as granting everything.
Impact: Someone creating a key can be told it has unrestricted access when it does not, leading to keys being created and trusted based on wrong information.

How an unlisted access preset ends up rendering the full-access summary

PRESET_SCOPE_DETAILS is a hard-coded map keyed by preset id (FULL_ACCESS, TRIGGER_ONLY, TASK_OPERATOR, ENVIRONMENT_OBSERVER, ENVIRONMENT_OPERATOR, DEPLOY_ONLY, ENV_VARS_ONLY), while the actual preset catalogue comes from the RBAC controller at runtime (rbac.apiKeyPresets, surfaced through the loader). When the controller returns a preset whose id is not in that map, ApiKeyScopePanel silently substitutes the FULL_ACCESS detail, which sets detail.admin = true; the panel then renders the amber admin box ("A single scope that grants everything below") and marks every capability row as "Full access", even though the header still shows the restricted preset's label.

This is reachable without a plugin adding new ids: defaultPresetId (route.tsx:584-587) picks the first available preset when FULL_ACCESS is unavailable, and that id may not appear in the radio groups rendered by PresetOptions, so the form can be submitted with a preset the user never explicitly picked while the panel described it as full access. Consider deriving the scope summary from data returned by the controller (e.g. describeApiKeyPolicy) instead of a client-side hard-coded map, or rendering an explicit "scopes unavailable" state for unknown preset ids rather than defaulting to full access.

Prompt for agents
In the API keys route (apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx), ApiKeyScopePanel resolves the capability summary from the hard-coded PRESET_SCOPE_DETAILS map and falls back to the FULL_ACCESS entry when the selected preset id is missing from that map. The preset catalogue is supplied at runtime by the RBAC controller (rbac.apiKeyPresets), so a preset id outside the hard-coded list — including the auto-selected defaultPresetId when FULL_ACCESS is unavailable — renders the 'admin' box and 'Full access' on every capability row for a key that is actually restricted. Fix by not defaulting unknown preset ids to the full-access detail: either drive the panel from data the controller can describe for the preset, or render a neutral 'scope details unavailable' state, and make sure the auto-selected default preset is always one that is actually rendered as a selectable option.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@carderne