Skip to content

[Docs] Add initial files and configuration#3

Merged
matt-aitken merged 1 commit intotriggerdotdev:mainfrom
handotdev:main
Jan 4, 2023
Merged

[Docs] Add initial files and configuration#3
matt-aitken merged 1 commit intotriggerdotdev:mainfrom
handotdev:main

Conversation

@handotdev
Copy link
Copy Markdown
Contributor

Summary

This PR enables the initial documentation setup for Trigger.dev, currently available at docs.trigger.dev

docs

🚀 Setup

Simply merge in this PR and your documentation will be connected!

👩‍💻 Development

Install the Mintlify CLI to preview the documentation changes locally. To install, use the following command

npm i mintlify -g

Run the following command at the root of your documentation (where mint.json is)

mintlify dev

😎 Publishing Changes

Changes will be deployed to production automatically after pushing to the default branch.

You can also preview changes using PRs, which generates a preview link of the docs.

Troubleshooting

  • Mintlify dev isn't running - Run mintlify install it'll re-install dependencies.
  • Mintlify dev is updating really slowly - Run mintlify clear to clear the cache.

@matt-aitken matt-aitken merged commit 4cb8c37 into triggerdotdev:main Jan 4, 2023
tejassudsfp pushed a commit to tejassudsfp/trigger.dev that referenced this pull request Apr 22, 2026
…riggerBridgeModule

With the 4-hop provider cycle now broken (hotfix triggerdotdev#2 made AgentService
lazy-resolve RunsBridge via ModuleRef), there's no actual circular
dependency between TriggerBridgeModule and ConnectionsModule.
The forwardRef wrapper was making ConnectionsGateway's export invisible
to RunsBridgeService's @Inject(ConnectionsGateway) at instantiation
time, causing "UndefinedDependencyException: argument at index [0]"
on every boot.

Replace forwardRef(() => ConnectionsModule) with a plain
ConnectionsModule import. AgentRuntimeModule's forwardRef stays
(topological safety given transitive imports through ConnectionsModule).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
matt-aitken added a commit that referenced this pull request May 1, 2026
…ker tightening

#1 Batch trigger AND semantics (security): `api.v[12].tasks.batch` now uses
`everyResource(...)` so a JWT scoped to taskA can no longer submit a batch
that also includes taskB / taskC. Added an `everyResource` helper to
`apiBuilder` (Symbol-marked wrapper that flips `ability.can` to `every`).
Multi-key OR semantics still apply for single-resource arrays (a run carries
multiple identifiers). Updated the e2e test to assert AND behaviour.

#3 Realtime stream resource (correctness): `findResource` for
`realtime.v1.streams.$runId.$streamId` now selects `taskIdentifier`,
`runTags`, and `realtimeStreamsVersion` — fields the auth resource
builder + handler read but findResource was returning undefined for.

#4 projectCreated optional chaining (crash bug): added the missing
`?.` between v3Subscription and plan so a missing subscription no longer
throws and aborts project creation.

#5 RBAC plugin loader logging: distinguish "plugin itself missing" from
"plugin found but a transitive dep failed to resolve" by inspecting the
ERR_MODULE_NOT_FOUND error message for the plugin's own module specifier.
The transitive-dep case now logs at error level (matches the comment's
stated behaviour). Removed the orphan log line that contradicted it.

#6 account.tokens picker source mismatch: the picker now sources roles
from the same plan-tier-filtered list (`systemRoles().filter(available)`)
as the default-role calculation. Added server-side roleId revalidation
in the create action so a hand-crafted POST can't bind a PAT to an
unavailable role.
matt-aitken added a commit that referenced this pull request May 3, 2026
…ker tightening

#1 Batch trigger AND semantics (security): `api.v[12].tasks.batch` now uses
`everyResource(...)` so a JWT scoped to taskA can no longer submit a batch
that also includes taskB / taskC. Added an `everyResource` helper to
`apiBuilder` (Symbol-marked wrapper that flips `ability.can` to `every`).
Multi-key OR semantics still apply for single-resource arrays (a run carries
multiple identifiers). Updated the e2e test to assert AND behaviour.

#3 Realtime stream resource (correctness): `findResource` for
`realtime.v1.streams.$runId.$streamId` now selects `taskIdentifier`,
`runTags`, and `realtimeStreamsVersion` — fields the auth resource
builder + handler read but findResource was returning undefined for.

#4 projectCreated optional chaining (crash bug): added the missing
`?.` between v3Subscription and plan so a missing subscription no longer
throws and aborts project creation.

#5 RBAC plugin loader logging: distinguish "plugin itself missing" from
"plugin found but a transitive dep failed to resolve" by inspecting the
ERR_MODULE_NOT_FOUND error message for the plugin's own module specifier.
The transitive-dep case now logs at error level (matches the comment's
stated behaviour). Removed the orphan log line that contradicted it.

#6 account.tokens picker source mismatch: the picker now sources roles
from the same plan-tier-filtered list (`systemRoles().filter(available)`)
as the default-role calculation. Added server-side roleId revalidation
in the create action so a hand-crafted POST can't bind a PAT to an
unavailable role.
matt-aitken added a commit that referenced this pull request May 4, 2026
…ker tightening

#1 Batch trigger AND semantics (security): `api.v[12].tasks.batch` now uses
`everyResource(...)` so a JWT scoped to taskA can no longer submit a batch
that also includes taskB / taskC. Added an `everyResource` helper to
`apiBuilder` (Symbol-marked wrapper that flips `ability.can` to `every`).
Multi-key OR semantics still apply for single-resource arrays (a run carries
multiple identifiers). Updated the e2e test to assert AND behaviour.

#3 Realtime stream resource (correctness): `findResource` for
`realtime.v1.streams.$runId.$streamId` now selects `taskIdentifier`,
`runTags`, and `realtimeStreamsVersion` — fields the auth resource
builder + handler read but findResource was returning undefined for.

#4 projectCreated optional chaining (crash bug): added the missing
`?.` between v3Subscription and plan so a missing subscription no longer
throws and aborts project creation.

#5 RBAC plugin loader logging: distinguish "plugin itself missing" from
"plugin found but a transitive dep failed to resolve" by inspecting the
ERR_MODULE_NOT_FOUND error message for the plugin's own module specifier.
The transitive-dep case now logs at error level (matches the comment's
stated behaviour). Removed the orphan log line that contradicted it.

#6 account.tokens picker source mismatch: the picker now sources roles
from the same plan-tier-filtered list (`systemRoles().filter(available)`)
as the default-role calculation. Added server-side roleId revalidation
in the create action so a hand-crafted POST can't bind a PAT to an
unavailable role.
matt-aitken added a commit that referenced this pull request May 4, 2026
Five real-bug fixes from CodeRabbit + Devin review of #3499:

#1 personalAccessToken.server.ts: FALLBACK_NOT_INSTALLED_ERROR string
   was 'RBAC fallback not installed' but the OSS fallback actually
   returns 'RBAC plugin not installed'. The mismatch made every PAT
   creation with a roleId hit the compensating-delete branch on
   self-hosters with no plugin installed — including the dashboard
   PAT-creation flow. Aligns the constant with the canonical string.

#2 internal-packages/rbac/src/fallback.ts: authenticateBearer skipped
   the revoked-API-key grace window (RevokedApiKey table), so a
   freshly-rotated env API key would 401 immediately on the new auth
   path. Mirrors findEnvironmentByApiKey's fallback-to-RevokedApiKey
   logic so the auth-cross-cutting e2e tests pass.

#3 api.v1.query.ts: multi-table queries built a plain RbacResource
   array, which checkAuth treats as 'any element passes'. A JWT
   scoped to one detected table could submit a query that also reads
   another table it isn't scoped to. Wrap with everyResource — same
   AND-semantics fix as the batch trigger routes.

#4 account.tokens/route.tsx: defaultRoleId could land on a custom or
   plan-blocked role when userRoleId wasn't in the picker's assignable
   set. The action's submit-revalidation would then 400 until the user
   manually changed the dropdown. Clamp the default to roles the picker
   actually renders.

#5 settings.team/route.tsx: the role Select used defaultValue, so a
   failed set-role submit left the attempted role visible while the
   server kept the old one. Switch to a controlled value bound to
   currentRoleId.
matt-aitken added a commit that referenced this pull request May 6, 2026
#1 internal-packages/rbac/src/ability.ts (severity: 🔴 silent privilege
   escalation): buildJwtAbility was treating any scope starting with
   `admin:` as a universal wildcard. Pre-RBAC, the legacy
   checkAuthorization string-matched superScopes — `admin:sessions` only
   granted access to routes that explicitly listed it. After the JWT-
   ability split, the same scope was returning true for every action on
   every resource. Restrict the bypass to bare `admin` (no second
   segment); `admin:<type>` now flows through normal matching as
   action="admin" against resources of that type. Adds 2 regression
   tests in ability.test.ts.

#2 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts (status
   discard): authenticateRequestForApiBuilder hardcoded `status: 401`
   even though BearerAuthResult.status is `401 | 403`. A plugin
   returning 403 (e.g. suspended account, IP block) would silently get
   downgraded to 401 — semantically wrong (401 = "who are you?", 403 =
   "you're not allowed") and confusing for client retry logic. Plumb
   result.status through.

#3 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts
   (everyResource([]) vacuous truth): [].every() returns true, so
   everyResource([]) was passing auth for any token. Not exploitable
   today (Zod rejects empty bodies before auth), but the auth layer
   should never grant on empty input. Same defensive guard added to
   anyResource() for symmetry — only PERMISSIVE_ABILITY would have
   granted there, but the pattern shouldn't depend on the ability's
   choice.

#4 internal-packages/rbac/src/fallback.ts (PREVIEW env regression): the
   fallback's authenticateBearer looked up environments by apiKey only,
   skipping the branch-aware resolution that findEnvironmentByApiKey
   does for PREVIEW envs. Self-hosters using preview/branch envs would
   either fail or operate against the parent env. Mirror the legacy
   path: read x-trigger-branch, include matching child env, and pivot
   the resolved env to the child (apiKey/orgMember/organization/project
   inherited from parent). sanitizeBranchName inlined here because
   internal-packages can't import webapp code; comment notes the
   duplication.

All four flagged by Devin's PR review. Cloud plugin's buildJwtAbility
gets the same #1 fix in a sibling commit on this PR's cloud branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
matt-aitken added a commit that referenced this pull request May 6, 2026
…ker tightening

#1 Batch trigger AND semantics (security): `api.v[12].tasks.batch` now uses
`everyResource(...)` so a JWT scoped to taskA can no longer submit a batch
that also includes taskB / taskC. Added an `everyResource` helper to
`apiBuilder` (Symbol-marked wrapper that flips `ability.can` to `every`).
Multi-key OR semantics still apply for single-resource arrays (a run carries
multiple identifiers). Updated the e2e test to assert AND behaviour.

#3 Realtime stream resource (correctness): `findResource` for
`realtime.v1.streams.$runId.$streamId` now selects `taskIdentifier`,
`runTags`, and `realtimeStreamsVersion` — fields the auth resource
builder + handler read but findResource was returning undefined for.

#4 projectCreated optional chaining (crash bug): added the missing
`?.` between v3Subscription and plan so a missing subscription no longer
throws and aborts project creation.

#5 RBAC plugin loader logging: distinguish "plugin itself missing" from
"plugin found but a transitive dep failed to resolve" by inspecting the
ERR_MODULE_NOT_FOUND error message for the plugin's own module specifier.
The transitive-dep case now logs at error level (matches the comment's
stated behaviour). Removed the orphan log line that contradicted it.

#6 account.tokens picker source mismatch: the picker now sources roles
from the same plan-tier-filtered list (`systemRoles().filter(available)`)
as the default-role calculation. Added server-side roleId revalidation
in the create action so a hand-crafted POST can't bind a PAT to an
unavailable role.
matt-aitken added a commit that referenced this pull request May 6, 2026
Five real-bug fixes from CodeRabbit + Devin review of #3499:

#1 personalAccessToken.server.ts: FALLBACK_NOT_INSTALLED_ERROR string
   was 'RBAC fallback not installed' but the OSS fallback actually
   returns 'RBAC plugin not installed'. The mismatch made every PAT
   creation with a roleId hit the compensating-delete branch on
   self-hosters with no plugin installed — including the dashboard
   PAT-creation flow. Aligns the constant with the canonical string.

#2 internal-packages/rbac/src/fallback.ts: authenticateBearer skipped
   the revoked-API-key grace window (RevokedApiKey table), so a
   freshly-rotated env API key would 401 immediately on the new auth
   path. Mirrors findEnvironmentByApiKey's fallback-to-RevokedApiKey
   logic so the auth-cross-cutting e2e tests pass.

#3 api.v1.query.ts: multi-table queries built a plain RbacResource
   array, which checkAuth treats as 'any element passes'. A JWT
   scoped to one detected table could submit a query that also reads
   another table it isn't scoped to. Wrap with everyResource — same
   AND-semantics fix as the batch trigger routes.

#4 account.tokens/route.tsx: defaultRoleId could land on a custom or
   plan-blocked role when userRoleId wasn't in the picker's assignable
   set. The action's submit-revalidation would then 400 until the user
   manually changed the dropdown. Clamp the default to roles the picker
   actually renders.

#5 settings.team/route.tsx: the role Select used defaultValue, so a
   failed set-role submit left the attempted role visible while the
   server kept the old one. Switch to a controlled value bound to
   currentRoleId.
matt-aitken added a commit that referenced this pull request May 6, 2026
#1 internal-packages/rbac/src/ability.ts (severity: 🔴 silent privilege
   escalation): buildJwtAbility was treating any scope starting with
   `admin:` as a universal wildcard. Pre-RBAC, the legacy
   checkAuthorization string-matched superScopes — `admin:sessions` only
   granted access to routes that explicitly listed it. After the JWT-
   ability split, the same scope was returning true for every action on
   every resource. Restrict the bypass to bare `admin` (no second
   segment); `admin:<type>` now flows through normal matching as
   action="admin" against resources of that type. Adds 2 regression
   tests in ability.test.ts.

#2 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts (status
   discard): authenticateRequestForApiBuilder hardcoded `status: 401`
   even though BearerAuthResult.status is `401 | 403`. A plugin
   returning 403 (e.g. suspended account, IP block) would silently get
   downgraded to 401 — semantically wrong (401 = "who are you?", 403 =
   "you're not allowed") and confusing for client retry logic. Plumb
   result.status through.

#3 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts
   (everyResource([]) vacuous truth): [].every() returns true, so
   everyResource([]) was passing auth for any token. Not exploitable
   today (Zod rejects empty bodies before auth), but the auth layer
   should never grant on empty input. Same defensive guard added to
   anyResource() for symmetry — only PERMISSIVE_ABILITY would have
   granted there, but the pattern shouldn't depend on the ability's
   choice.

#4 internal-packages/rbac/src/fallback.ts (PREVIEW env regression): the
   fallback's authenticateBearer looked up environments by apiKey only,
   skipping the branch-aware resolution that findEnvironmentByApiKey
   does for PREVIEW envs. Self-hosters using preview/branch envs would
   either fail or operate against the parent env. Mirror the legacy
   path: read x-trigger-branch, include matching child env, and pivot
   the resolved env to the child (apiKey/orgMember/organization/project
   inherited from parent). sanitizeBranchName inlined here because
   internal-packages can't import webapp code; comment notes the
   duplication.

All four flagged by Devin's PR review. Cloud plugin's buildJwtAbility
gets the same #1 fix in a sibling commit on this PR's cloud branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
matt-aitken added a commit that referenced this pull request May 6, 2026
…ker tightening

#1 Batch trigger AND semantics (security): `api.v[12].tasks.batch` now uses
`everyResource(...)` so a JWT scoped to taskA can no longer submit a batch
that also includes taskB / taskC. Added an `everyResource` helper to
`apiBuilder` (Symbol-marked wrapper that flips `ability.can` to `every`).
Multi-key OR semantics still apply for single-resource arrays (a run carries
multiple identifiers). Updated the e2e test to assert AND behaviour.

#3 Realtime stream resource (correctness): `findResource` for
`realtime.v1.streams.$runId.$streamId` now selects `taskIdentifier`,
`runTags`, and `realtimeStreamsVersion` — fields the auth resource
builder + handler read but findResource was returning undefined for.

#4 projectCreated optional chaining (crash bug): added the missing
`?.` between v3Subscription and plan so a missing subscription no longer
throws and aborts project creation.

#5 RBAC plugin loader logging: distinguish "plugin itself missing" from
"plugin found but a transitive dep failed to resolve" by inspecting the
ERR_MODULE_NOT_FOUND error message for the plugin's own module specifier.
The transitive-dep case now logs at error level (matches the comment's
stated behaviour). Removed the orphan log line that contradicted it.

#6 account.tokens picker source mismatch: the picker now sources roles
from the same plan-tier-filtered list (`systemRoles().filter(available)`)
as the default-role calculation. Added server-side roleId revalidation
in the create action so a hand-crafted POST can't bind a PAT to an
unavailable role.
matt-aitken added a commit that referenced this pull request May 6, 2026
Five real-bug fixes from CodeRabbit + Devin review of #3499:

#1 personalAccessToken.server.ts: FALLBACK_NOT_INSTALLED_ERROR string
   was 'RBAC fallback not installed' but the OSS fallback actually
   returns 'RBAC plugin not installed'. The mismatch made every PAT
   creation with a roleId hit the compensating-delete branch on
   self-hosters with no plugin installed — including the dashboard
   PAT-creation flow. Aligns the constant with the canonical string.

#2 internal-packages/rbac/src/fallback.ts: authenticateBearer skipped
   the revoked-API-key grace window (RevokedApiKey table), so a
   freshly-rotated env API key would 401 immediately on the new auth
   path. Mirrors findEnvironmentByApiKey's fallback-to-RevokedApiKey
   logic so the auth-cross-cutting e2e tests pass.

#3 api.v1.query.ts: multi-table queries built a plain RbacResource
   array, which checkAuth treats as 'any element passes'. A JWT
   scoped to one detected table could submit a query that also reads
   another table it isn't scoped to. Wrap with everyResource — same
   AND-semantics fix as the batch trigger routes.

#4 account.tokens/route.tsx: defaultRoleId could land on a custom or
   plan-blocked role when userRoleId wasn't in the picker's assignable
   set. The action's submit-revalidation would then 400 until the user
   manually changed the dropdown. Clamp the default to roles the picker
   actually renders.

#5 settings.team/route.tsx: the role Select used defaultValue, so a
   failed set-role submit left the attempted role visible while the
   server kept the old one. Switch to a controlled value bound to
   currentRoleId.
matt-aitken added a commit that referenced this pull request May 6, 2026
#1 internal-packages/rbac/src/ability.ts (severity: 🔴 silent privilege
   escalation): buildJwtAbility was treating any scope starting with
   `admin:` as a universal wildcard. Pre-RBAC, the legacy
   checkAuthorization string-matched superScopes — `admin:sessions` only
   granted access to routes that explicitly listed it. After the JWT-
   ability split, the same scope was returning true for every action on
   every resource. Restrict the bypass to bare `admin` (no second
   segment); `admin:<type>` now flows through normal matching as
   action="admin" against resources of that type. Adds 2 regression
   tests in ability.test.ts.

#2 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts (status
   discard): authenticateRequestForApiBuilder hardcoded `status: 401`
   even though BearerAuthResult.status is `401 | 403`. A plugin
   returning 403 (e.g. suspended account, IP block) would silently get
   downgraded to 401 — semantically wrong (401 = "who are you?", 403 =
   "you're not allowed") and confusing for client retry logic. Plumb
   result.status through.

#3 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts
   (everyResource([]) vacuous truth): [].every() returns true, so
   everyResource([]) was passing auth for any token. Not exploitable
   today (Zod rejects empty bodies before auth), but the auth layer
   should never grant on empty input. Same defensive guard added to
   anyResource() for symmetry — only PERMISSIVE_ABILITY would have
   granted there, but the pattern shouldn't depend on the ability's
   choice.

#4 internal-packages/rbac/src/fallback.ts (PREVIEW env regression): the
   fallback's authenticateBearer looked up environments by apiKey only,
   skipping the branch-aware resolution that findEnvironmentByApiKey
   does for PREVIEW envs. Self-hosters using preview/branch envs would
   either fail or operate against the parent env. Mirror the legacy
   path: read x-trigger-branch, include matching child env, and pivot
   the resolved env to the child (apiKey/orgMember/organization/project
   inherited from parent). sanitizeBranchName inlined here because
   internal-packages can't import webapp code; comment notes the
   duplication.

All four flagged by Devin's PR review. Cloud plugin's buildJwtAbility
gets the same #1 fix in a sibling commit on this PR's cloud branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
matt-aitken added a commit that referenced this pull request May 8, 2026
…ker tightening

#1 Batch trigger AND semantics (security): `api.v[12].tasks.batch` now uses
`everyResource(...)` so a JWT scoped to taskA can no longer submit a batch
that also includes taskB / taskC. Added an `everyResource` helper to
`apiBuilder` (Symbol-marked wrapper that flips `ability.can` to `every`).
Multi-key OR semantics still apply for single-resource arrays (a run carries
multiple identifiers). Updated the e2e test to assert AND behaviour.

#3 Realtime stream resource (correctness): `findResource` for
`realtime.v1.streams.$runId.$streamId` now selects `taskIdentifier`,
`runTags`, and `realtimeStreamsVersion` — fields the auth resource
builder + handler read but findResource was returning undefined for.

#4 projectCreated optional chaining (crash bug): added the missing
`?.` between v3Subscription and plan so a missing subscription no longer
throws and aborts project creation.

#5 RBAC plugin loader logging: distinguish "plugin itself missing" from
"plugin found but a transitive dep failed to resolve" by inspecting the
ERR_MODULE_NOT_FOUND error message for the plugin's own module specifier.
The transitive-dep case now logs at error level (matches the comment's
stated behaviour). Removed the orphan log line that contradicted it.

#6 account.tokens picker source mismatch: the picker now sources roles
from the same plan-tier-filtered list (`systemRoles().filter(available)`)
as the default-role calculation. Added server-side roleId revalidation
in the create action so a hand-crafted POST can't bind a PAT to an
unavailable role.
matt-aitken added a commit that referenced this pull request May 8, 2026
Five real-bug fixes from CodeRabbit + Devin review of #3499:

#1 personalAccessToken.server.ts: FALLBACK_NOT_INSTALLED_ERROR string
   was 'RBAC fallback not installed' but the OSS fallback actually
   returns 'RBAC plugin not installed'. The mismatch made every PAT
   creation with a roleId hit the compensating-delete branch on
   self-hosters with no plugin installed — including the dashboard
   PAT-creation flow. Aligns the constant with the canonical string.

#2 internal-packages/rbac/src/fallback.ts: authenticateBearer skipped
   the revoked-API-key grace window (RevokedApiKey table), so a
   freshly-rotated env API key would 401 immediately on the new auth
   path. Mirrors findEnvironmentByApiKey's fallback-to-RevokedApiKey
   logic so the auth-cross-cutting e2e tests pass.

#3 api.v1.query.ts: multi-table queries built a plain RbacResource
   array, which checkAuth treats as 'any element passes'. A JWT
   scoped to one detected table could submit a query that also reads
   another table it isn't scoped to. Wrap with everyResource — same
   AND-semantics fix as the batch trigger routes.

#4 account.tokens/route.tsx: defaultRoleId could land on a custom or
   plan-blocked role when userRoleId wasn't in the picker's assignable
   set. The action's submit-revalidation would then 400 until the user
   manually changed the dropdown. Clamp the default to roles the picker
   actually renders.

#5 settings.team/route.tsx: the role Select used defaultValue, so a
   failed set-role submit left the attempted role visible while the
   server kept the old one. Switch to a controlled value bound to
   currentRoleId.
matt-aitken added a commit that referenced this pull request May 8, 2026
#1 internal-packages/rbac/src/ability.ts (severity: 🔴 silent privilege
   escalation): buildJwtAbility was treating any scope starting with
   `admin:` as a universal wildcard. Pre-RBAC, the legacy
   checkAuthorization string-matched superScopes — `admin:sessions` only
   granted access to routes that explicitly listed it. After the JWT-
   ability split, the same scope was returning true for every action on
   every resource. Restrict the bypass to bare `admin` (no second
   segment); `admin:<type>` now flows through normal matching as
   action="admin" against resources of that type. Adds 2 regression
   tests in ability.test.ts.

#2 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts (status
   discard): authenticateRequestForApiBuilder hardcoded `status: 401`
   even though BearerAuthResult.status is `401 | 403`. A plugin
   returning 403 (e.g. suspended account, IP block) would silently get
   downgraded to 401 — semantically wrong (401 = "who are you?", 403 =
   "you're not allowed") and confusing for client retry logic. Plumb
   result.status through.

#3 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts
   (everyResource([]) vacuous truth): [].every() returns true, so
   everyResource([]) was passing auth for any token. Not exploitable
   today (Zod rejects empty bodies before auth), but the auth layer
   should never grant on empty input. Same defensive guard added to
   anyResource() for symmetry — only PERMISSIVE_ABILITY would have
   granted there, but the pattern shouldn't depend on the ability's
   choice.

#4 internal-packages/rbac/src/fallback.ts (PREVIEW env regression): the
   fallback's authenticateBearer looked up environments by apiKey only,
   skipping the branch-aware resolution that findEnvironmentByApiKey
   does for PREVIEW envs. Self-hosters using preview/branch envs would
   either fail or operate against the parent env. Mirror the legacy
   path: read x-trigger-branch, include matching child env, and pivot
   the resolved env to the child (apiKey/orgMember/organization/project
   inherited from parent). sanitizeBranchName inlined here because
   internal-packages can't import webapp code; comment notes the
   duplication.

All four flagged by Devin's PR review. Cloud plugin's buildJwtAbility
gets the same #1 fix in a sibling commit on this PR's cloud branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
matt-aitken added a commit that referenced this pull request May 8, 2026
…ker tightening

#1 Batch trigger AND semantics (security): `api.v[12].tasks.batch` now uses
`everyResource(...)` so a JWT scoped to taskA can no longer submit a batch
that also includes taskB / taskC. Added an `everyResource` helper to
`apiBuilder` (Symbol-marked wrapper that flips `ability.can` to `every`).
Multi-key OR semantics still apply for single-resource arrays (a run carries
multiple identifiers). Updated the e2e test to assert AND behaviour.

#3 Realtime stream resource (correctness): `findResource` for
`realtime.v1.streams.$runId.$streamId` now selects `taskIdentifier`,
`runTags`, and `realtimeStreamsVersion` — fields the auth resource
builder + handler read but findResource was returning undefined for.

#4 projectCreated optional chaining (crash bug): added the missing
`?.` between v3Subscription and plan so a missing subscription no longer
throws and aborts project creation.

#5 RBAC plugin loader logging: distinguish "plugin itself missing" from
"plugin found but a transitive dep failed to resolve" by inspecting the
ERR_MODULE_NOT_FOUND error message for the plugin's own module specifier.
The transitive-dep case now logs at error level (matches the comment's
stated behaviour). Removed the orphan log line that contradicted it.

#6 account.tokens picker source mismatch: the picker now sources roles
from the same plan-tier-filtered list (`systemRoles().filter(available)`)
as the default-role calculation. Added server-side roleId revalidation
in the create action so a hand-crafted POST can't bind a PAT to an
unavailable role.
matt-aitken added a commit that referenced this pull request May 8, 2026
Five real-bug fixes from CodeRabbit + Devin review of #3499:

#1 personalAccessToken.server.ts: FALLBACK_NOT_INSTALLED_ERROR string
   was 'RBAC fallback not installed' but the OSS fallback actually
   returns 'RBAC plugin not installed'. The mismatch made every PAT
   creation with a roleId hit the compensating-delete branch on
   self-hosters with no plugin installed — including the dashboard
   PAT-creation flow. Aligns the constant with the canonical string.

#2 internal-packages/rbac/src/fallback.ts: authenticateBearer skipped
   the revoked-API-key grace window (RevokedApiKey table), so a
   freshly-rotated env API key would 401 immediately on the new auth
   path. Mirrors findEnvironmentByApiKey's fallback-to-RevokedApiKey
   logic so the auth-cross-cutting e2e tests pass.

#3 api.v1.query.ts: multi-table queries built a plain RbacResource
   array, which checkAuth treats as 'any element passes'. A JWT
   scoped to one detected table could submit a query that also reads
   another table it isn't scoped to. Wrap with everyResource — same
   AND-semantics fix as the batch trigger routes.

#4 account.tokens/route.tsx: defaultRoleId could land on a custom or
   plan-blocked role when userRoleId wasn't in the picker's assignable
   set. The action's submit-revalidation would then 400 until the user
   manually changed the dropdown. Clamp the default to roles the picker
   actually renders.

#5 settings.team/route.tsx: the role Select used defaultValue, so a
   failed set-role submit left the attempted role visible while the
   server kept the old one. Switch to a controlled value bound to
   currentRoleId.
matt-aitken added a commit that referenced this pull request May 8, 2026
#1 internal-packages/rbac/src/ability.ts (severity: 🔴 silent privilege
   escalation): buildJwtAbility was treating any scope starting with
   `admin:` as a universal wildcard. Pre-RBAC, the legacy
   checkAuthorization string-matched superScopes — `admin:sessions` only
   granted access to routes that explicitly listed it. After the JWT-
   ability split, the same scope was returning true for every action on
   every resource. Restrict the bypass to bare `admin` (no second
   segment); `admin:<type>` now flows through normal matching as
   action="admin" against resources of that type. Adds 2 regression
   tests in ability.test.ts.

#2 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts (status
   discard): authenticateRequestForApiBuilder hardcoded `status: 401`
   even though BearerAuthResult.status is `401 | 403`. A plugin
   returning 403 (e.g. suspended account, IP block) would silently get
   downgraded to 401 — semantically wrong (401 = "who are you?", 403 =
   "you're not allowed") and confusing for client retry logic. Plumb
   result.status through.

#3 apps/webapp/app/services/routeBuilders/apiBuilder.server.ts
   (everyResource([]) vacuous truth): [].every() returns true, so
   everyResource([]) was passing auth for any token. Not exploitable
   today (Zod rejects empty bodies before auth), but the auth layer
   should never grant on empty input. Same defensive guard added to
   anyResource() for symmetry — only PERMISSIVE_ABILITY would have
   granted there, but the pattern shouldn't depend on the ability's
   choice.

#4 internal-packages/rbac/src/fallback.ts (PREVIEW env regression): the
   fallback's authenticateBearer looked up environments by apiKey only,
   skipping the branch-aware resolution that findEnvironmentByApiKey
   does for PREVIEW envs. Self-hosters using preview/branch envs would
   either fail or operate against the parent env. Mirror the legacy
   path: read x-trigger-branch, include matching child env, and pivot
   the resolved env to the child (apiKey/orgMember/organization/project
   inherited from parent). sanitizeBranchName inlined here because
   internal-packages can't import webapp code; comment notes the
   duplication.

All four flagged by Devin's PR review. Cloud plugin's buildJwtAbility
gets the same #1 fix in a sibling commit on this PR's cloud branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants