Skip to content

feat(api-keys): read-only API keys and expiry at creation - #9855

Merged
waleedlatif1 merged 10 commits into
stagingfrom
feat/api-key-read-only-expiry
Nov 10, 2026
Merged

waleedlatif1 merged 10 commits into
stagingfrom
feat/api-key-read-only-expiry

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Adds read-only API keys: api_key.access (read | write, default write, migration 0455, metadata-only) carried on personal and workspace key principals
  • A read key is granted api:read in the operation funnel, the same classification OAuth tokens already use, and is refused with 403 READ_ONLY_API_KEY on any write or unscoped operation. Surfaces that authorize outside the funnel refuse explicitly: v1 non-GET (except knowledge search), legacy workflow execute, and workflow MCP tools/call
  • Lets callers choose an expiry at creation (1–366 days, or never); expired keys now get 401 API key has expired instead of a generic invalid-key error, checked after validity so foreign or banned keys still read as invalid
  • Settings create modal gains Access (Full access / Read only) and Expires (30 days, 90 days, 1 year, Never; default Never so existing flows are unchanged); deploy and workflow MCP flows always create full-access keys. Key rows show a Read only tag and expiry
  • /api/v2/meta reports the credential's scopes; the mothership-delegation: key-name prefix is reserved
  • Creating read keys is gated by the api-key-read-only feature flag (off by default); enforcement of read keys is never gated
  • Docs: authentication pages describe access levels, credit-consuming reads (search, search chat, selectors), and each surface's refusal shape

Rollout: turn api-key-read-only on only after every pod runs this release, since older pods ignore access and would serve a read key with full access. Rolling back below this release after read keys exist widens them the same way.

Type of Change

  • New feature

Testing

  • apps/sim/lib/api-key/__integration__/read-only-access.integration.ts (8 passing) against PostgreSQL: read keys refused on every write surface (v2, Sim MCP, v1, legacy execute, workflow MCP serve, resume/cancel) with no rows written, reads still succeed, pre-migration keys behave as write, expiry message on v2 and v1, flag-off creation and reserved names refused; reverting each guard turns its test red
  • Unit: principal serialization round-trip and old-format parse, scope enforcement, key auth, route policies, key creation, MCP handlers, execute and resume routes
  • tsc on apps/sim, check:test-types; check:migrations, schema-mock, openapi, cli-api, mcp-operations, cli-docs, api-validation:strict, principal-kind-parity, client-boundary, boundaries; root scripts and sim-cli suites; biome

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing (new tests pass the test-audit authoring gate)
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Personal and workspace API keys gain an access level (full access or read
only) and an expiry picked when the key is created (30 days, 90 days,
1 year, or never; bounded server-side to one day through one year).

A read-only key is granted the api:read scope: the operation funnel
refuses it any operation api:read does not cover, including operations
that declare no scope, with a 403 READ_ONLY_API_KEY refusal rather than
an OAuth insufficient_scope challenge. Surfaces that authorize outside the
funnel refuse it too: v1 refuses every non-GET except knowledge search,
the legacy execute route refuses it, and a workflow MCP server refuses
tools/call. An expired key now answers 401 "API key has expired" on v2,
v1, and hybrid auth. /api/v2/meta reports the credential's scopes.

Migration 0455 adds api_key.access (enum api_key_access, NOT NULL DEFAULT
'write'), so existing keys and the deployed app keep full access. Write
keys serialize without the field and older serialized principals parse
as write, so queued runs survive a mixed-version deploy.
…, share key principal builder

- Create modal defaults Expires to Never; docs say so and note read-only keys keep search chat
- Client key list defaults a missing access to write during deploy rollout
- apiKeyPrincipal builds the key principal once for v1, hybrid, and workflow route auth
- Auth result carries expired: true instead of callers matching the message text
- Integration fixture uses valid table names; v2 create answers 201
- Serialized-principal test uses the wire shape; migration snapshot formatted
…docs

- api-key-read-only flag (global, fallback API_KEY_READ_ONLY) gates creating read keys:
  the server answers 400 and the create modal hides Access while it is off. Enforcement
  of existing read keys is never gated, since older pods would serve them as full access
- Reserve the mothership-delegation: key-name prefix so delegation minting can never
  adopt a user's key
- Expiry is checked after every other key gate, so a foreign or banned key reports invalid
- Workflow MCP serve and MCP discover answer an expired key with API key has expired
- Share DAY_MS from the api-keys contract; the year bound's day of slack is for clock skew
- Docs: per-surface refusal shapes, what read keys can still spend, and where the
  expiry message appears
- Integration: flag-off and reserved-name refusals, v1 search asserts 200
@vercel

vercel Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs Ready Ready Preview Nov 10, 2026 10:04pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge with the documented rollout restriction.

Summary

Adds read-only API keys and optional expiration at creation.

  • Read-only API keys can read but cannot change resources or run workflows.
  • Key settings choose read or full access and when each key expires.
  • Existing API key test fixtures now include an access level.

No new actionable issues were found. The three previous unnumbered findings are addressed in the current code.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[API key request] --> B[Authenticate key and check expiry]
  B -->|Invalid or expired| C[401 response]
  B -->|Valid| D[Principal carries access]
  D --> E{Read-only key?}
  E -->|No| F[Existing permission checks]
  E -->|Yes| G{Operation permits reads?}
  G -->|No| H[403 READ_ONLY_API_KEY]
  G -->|Yes| F
Loading

Reviews (4) · Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." · Reviewed by Greptile

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed

Turn on auto-fix | Re-trigger cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 195 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/docs/openapi-v2-resources.json Resolved
…meta scopes

- The account settings layout resolves api-key-read-only and wraps its sections in
  FeatureFlagsProvider, as the workspace and organization layouts do, so personal API
  key settings no longer throw from the create modal's flag read
- The /api/v2/meta scopes description says read-only keys are also refused every
  operation that declares no API scope; OpenAPI regenerated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

No issues found across 202 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

No issues found across 202 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1
waleedlatif1 merged commit 02293a9 into staging Nov 10, 2026
91 checks passed
@waleedlatif1
waleedlatif1 deleted the feat/api-key-read-only-expiry branch November 10, 2026 23:04
waleedlatif1 added a commit that referenced this pull request Nov 11, 2026
Renumber the personal-organization migrations 0455-0459 to 0456-0460 after
staging's 0455_api_key_access, rechaining their snapshots and journal.

Port read-only API keys and creation-time expiry (#9855) into the Platform:
the create-key modal offers access (behind api-key-read-only) and expiry,
the deploy dialog and MCP server detail issue full-access keys only, and the
keys list shows a Read only tag and expiry. The flag is seeded through the
organization surface. Legacy layouts and settings modified on staging stay
deleted.

This branch was successfully deployed

1 active deployment
Preview — c5cec7db Deployed Nov 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant