Add WorkOS dashboard API foundation#24
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR implements WorkOS-backed web authentication (JWT verification with cached JWKS and WorkOS user fetch), adds a workspace_members table and Drizzle schema/types, extends Local and Postgres repositories to provision/resolve workspace members and expose web-scoped read APIs (workspace, artifacts, keys, audit, settings), registers new web routes and OpenAPI contracts, updates web frontend typing and authed-session wiring, and adds unit tests plus an end-to-end smoke-test script. Sequence Diagram(s)sequenceDiagram
participant Browser
participant WebUI as Web Frontend
participant API as API Worker
participant Auth as WorkOS/AuthService
participant Repo as Repository/DB
Browser->>WebUI: User action with WorkOS token
WebUI->>API: POST /v1/auth/web/callback Authorization: Bearer <token>
API->>Auth: authenticateWebIdentity(bearer)
Auth-->>API: WorkOsIdentity or null
alt identity
API->>Repo: resolveWebMember(workos_user_id,email)
Repo-->>API: WebAuthCallbackResponse (workspace, member, default_api_key?)
API-->>WebUI: 200 web auth callback response
WebUI->>API: GET /v1/web/artifacts Authorization: Bearer <token>
API->>Repo: getWebMemberByWorkOsUserId -> verify scopes
Repo-->>API: artifacts list
API-->>WebUI: 200 artifacts response
else no identity
API-->>WebUI: 401 not_authenticated
end
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-24.isaac-a46.workers.dev |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 61-63: The CI guardrail block currently checks pnpm config keys
but does not assert Node version parity; add a new test in the same block (after
the existing pnpm config tests such as the
engine-strict/minimumReleaseAge/nodeLinker checks) that compares the declared
Node version files and fails the job if they differ by asserting the contents of
.nvmrc and .node-version are equal (normalizing/trimming newlines as needed) so
CI will break on drift between .nvmrc and .node-version.
In `@apps/api/src/workos.test.ts`:
- Around line 26-54: Add regression tests to explicitly cover the
requireClientIdClaim behavior when a token contains only aud (no client_id or
azp): create fixtures via tokenFixture with aud set to the expected clientId but
omit client_id and azp, stub the JWKS fetch with
stubWorkOsFetch(fixture.publicJwk), call resolveWorkOsIdentity(`Bearer
${fixture.token}`, options({ requireClientIdClaim: true })) and assert it
resolves to null; add equivalent assertions for both the tests around the "wrong
client_id" and the other related block (the tests around lines 66-80) so tokens
that only match aud are rejected when requireClientIdClaim is true.
In `@apps/api/src/workos.ts`:
- Around line 128-140: The clientIdMatches function currently allows tokens
without client_id/azp to pass when aud matches even if requireClaim is true;
change it so that when requireClaim is true and neither
stringClaim(payload.client_id) nor stringClaim(payload.azp) exist the function
immediately returns false, otherwise continue to compare the found clientClaim
to clientId; if no clientClaim and requireClaim is false then fall back to
checking payload.aud (string or array) as before. Ensure you update the logic in
clientIdMatches to perform the early requireClaim + clientClaim existence check
and keep the existing audience handling (payload.aud) for the non-strict case.
- Around line 76-88: fetchWorkOsUser can throw on network or JSON parse errors;
wrap the fetch + response.json() and normalization steps in a try/catch so any
thrown error returns null instead of bubbling up. Specifically, surrounding the
call to
fetch(`${workOsBaseUrl(options.apiBaseUrl)}/user_management/users/${encodeURIComponent(workosUserId)}`,
{ headers: { authorization: `Bearer ${options.apiKey}` } }) and the subsequent
await response.json() and normalizeWorkOsUser(value) with try/catch will ensure
you catch network/parse/normalize failures and return null; keep the existing
response.ok check and return null on non-ok as well.
In `@packages/db/migrations/0004_workspace_members.sql`:
- Line 6: The migration currently enforces global uniqueness on workos_user_id;
change it to a workspace-scoped uniqueness by replacing the single-column unique
constraint on workos_user_id with a composite unique constraint on
(workspace_id, workos_user_id) in the workspace_members migration so a single
WorkOS user can belong to multiple workspaces; update the CREATE TABLE / ALTER
TABLE in packages/db/migrations/0004_workspace_members.sql to drop the existing
unique constraint on workos_user_id and add a UNIQUE(workspace_id,
workos_user_id) constraint (refer to the workspace_members table, the
workos_user_id and workspace_id columns) and ensure any rollback/forward
migration steps reflect this change.
In `@packages/db/src/local-repository.ts`:
- Around line 245-253: The getWebWorkspace function accepts ApiActor but
immediately calls mustWorkspaceMember(actor.id), which only makes sense for
WorkspaceMemberActor; narrow the type of getWebWorkspace (and its interface in
repository) to WorkspaceMemberActor or add an explicit branch to handle
ApiKeyActor: update the method signature for getWebWorkspace to accept
WorkspaceMemberActor (and adjust callers such as the webWorkspace
endpoint/withWebMember usage if necessary) OR add a runtime guard that throws or
handles api_key cases before calling mustWorkspaceMember; reference
getWebWorkspace, ApiActor, WorkspaceMemberActor, mustWorkspaceMember,
webWorkspace endpoint, withWebMember, and db.getWebMemberByWorkOsUserId when
implementing the change.
In `@packages/db/src/postgres/repository.ts`:
- Around line 820-864: The same row-formatting helpers (toWebArtifactRow,
webArtifactStatus, toWebAuditRow, summarizeEventDetails) are duplicated in
local-repository.ts and postgres/repository.ts; extract them into a shared
module (e.g., transforms.ts) that exports those functions, re-use the Artifact
and OperationEvent types (or re-export them if needed), replace the local
implementations in both repository files with imports from transforms.ts, and
update any references to ensure behavior and types remain identical (including
exported names: toWebArtifactRow, webArtifactStatus, toWebAuditRow,
summarizeEventDetails).
In `@packages/db/src/queries/operation-events.ts`:
- Around line 64-84: The listForWorkspace function currently returns an
unbounded result set; update the operationEvents query in listForWorkspace to
accept and apply pagination parameters (limit and a cursor composed of
occurredAt and id) so results are bounded and stable. Modify listForWorkspace to
take optional params (e.g., limit: number, cursor?: { occurredAt: string, id:
string }), add a .limit(limit) and a WHERE clause that implements cursor
comparison (occurredAt < cursor.occurredAt OR (occurredAt = cursor.occurredAt
AND id < cursor.id)) while preserving the
orderBy(desc(operationEvents.occurredAt), desc(operationEvents.id)); also update
the returned payload to include a nextCursor (based on the last row's occurredAt
and id) and wire these new params/response fields through the route contract
that calls listForWorkspace.
In `@packages/db/src/schema.ts`:
- Line 32: The `scopes` column declaration
(jsonb("scopes").$type<Array<"publish" | "read" | "admin">>().notNull()) is
missing the default that the SQL migration sets; update the `scopes` column to
include a default value of ["publish","read","admin"] (i.e. add the default via
the column builder on the `scopes` jsonb column) so the Drizzle schema matches
the migration and prevents schema drift.
- Line 30: workos_user_id is currently marked globally unique (workosUserId:
text("workos_user_id").notNull().unique()), which prevents the same WorkOS user
from existing in multiple workspaces; remove the .unique() on workosUserId and
instead add a composite unique constraint/index for (workspace_id,
workos_user_id) at the table level so the pair is unique per workspace (search
for the table definition containing workosUserId and add the composite unique
key for workspace_id + workos_user_id).
In `@scripts/smoke-web-api.mjs`:
- Around line 19-20: Replace use of URL.pathname for spawned entry paths by
converting the file:// URLs to platform file paths using Node's fileURLToPath;
update the cliEntry and serverEntry initializations to call fileURLToPath(new
URL("... ", import.meta.url)) so the paths are percent-decoded and Windows-safe
(use the fileURLToPath function imported from 'url' and apply it to new
URL("../apps/cli/dist/src/index.js", import.meta.url) and new
URL("./local-mvp-server.mjs", import.meta.url)).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e9c43fee-9004-469b-8742-0a37e9a70638
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (42)
.github/workflows/ci.yml.node-version.npmrcapps/api/package.jsonapps/api/src/index.test.tsapps/api/src/index.tsapps/api/src/workos.test.tsapps/api/src/workos.tsapps/api/wrangler.jsoncapps/upload/src/index.tsapps/web/src/routes/_authed.artifacts.$artifactId.tsxapps/web/src/routes/_authed.artifacts.index.tsxapps/web/src/routes/_authed.audit.tsxapps/web/src/routes/_authed.keys.tsxapps/web/src/routes/_authed.settings.tsxapps/web/src/routes/_authed.tsxpackage.jsonpackages/auth/package.jsonpackages/commands/package.jsonpackages/contracts/openapi/api.jsonpackages/contracts/src/index.tspackages/contracts/src/mvp-contracts.test.tspackages/contracts/src/openapi/api.tspackages/contracts/src/openapi/shared.tspackages/contracts/src/routes.tspackages/contracts/src/web.tspackages/db/migrations/0004_workspace_members.sqlpackages/db/snapshot/schema.sqlpackages/db/src/index.test.tspackages/db/src/index.tspackages/db/src/local-repository.tspackages/db/src/postgres/repository.tspackages/db/src/queries/api-keys.tspackages/db/src/queries/index.tspackages/db/src/queries/operation-events.tspackages/db/src/queries/workspace-members.tspackages/db/src/schema.tspackages/db/src/types.tsscripts/bootstrap-secrets.mjsscripts/local-mvp-server.mjsscripts/smoke-web-api.mjsturbo.json
| function toWebArtifactRow(artifact: Artifact) { | ||
| return { | ||
| id: artifact.id, | ||
| title: artifact.title, | ||
| status: webArtifactStatus(artifact), | ||
| latest_revision_id: artifact.revision_id, | ||
| pinned: false, | ||
| lockdown: false, | ||
| last_published_at: artifact.created_at, | ||
| auto_delete_at: artifact.status === "deleted" ? null : artifact.expires_at, | ||
| }; | ||
| } | ||
|
|
||
| function webArtifactStatus(artifact: Artifact): "Published" | "Deleted" | "Expired" { | ||
| if (artifact.status === "deleted") { | ||
| return "Deleted"; | ||
| } | ||
| if (artifact.status === "expired") { | ||
| return "Expired"; | ||
| } | ||
| return "Published"; | ||
| } | ||
|
|
||
| function toWebAuditRow(event: OperationEvent) { | ||
| return { | ||
| id: event.id, | ||
| time: event.occurred_at, | ||
| actor: `${event.actor_type}:${event.actor_id ?? "unknown"}`, | ||
| action: event.action, | ||
| target: `${event.target_type}:${event.target_id}`, | ||
| change_summary: summarizeEventDetails(event.details), | ||
| request_id: event.request_id ?? "", | ||
| }; | ||
| } | ||
|
|
||
| function summarizeEventDetails(details: Record<string, unknown>): string { | ||
| const keys = Object.keys(details); | ||
| if (keys.length === 0) { | ||
| return ""; | ||
| } | ||
| return keys | ||
| .sort() | ||
| .map((key) => `${key}=${String(details[key])}`) | ||
| .join(", "); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Consider extracting shared web row formatters.
The toWebArtifactRow, webArtifactStatus, toWebAuditRow, and summarizeEventDetails functions are duplicated between local-repository.ts and postgres/repository.ts. Extracting them to a shared module (e.g., transforms.ts) would reduce duplication and ensure they stay in sync.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/db/src/postgres/repository.ts` around lines 820 - 864, The same
row-formatting helpers (toWebArtifactRow, webArtifactStatus, toWebAuditRow,
summarizeEventDetails) are duplicated in local-repository.ts and
postgres/repository.ts; extract them into a shared module (e.g., transforms.ts)
that exports those functions, re-use the Artifact and OperationEvent types (or
re-export them if needed), replace the local implementations in both repository
files with imports from transforms.ts, and update any references to ensure
behavior and types remain identical (including exported names: toWebArtifactRow,
webArtifactStatus, toWebAuditRow, summarizeEventDetails).
| async listForWorkspace(db: DrizzleDb, workspaceId: string): Promise<OperationEvent[]> { | ||
| const rows = await db | ||
| .select() | ||
| .from(operationEvents) | ||
| .where(eq(operationEvents.workspaceId, workspaceId)) | ||
| .orderBy(desc(operationEvents.occurredAt)); | ||
| return rows.map( | ||
| (row): OperationEvent => ({ | ||
| id: row.id, | ||
| workspace_id: row.workspaceId, | ||
| actor_type: row.actorType as OperationEvent["actor_type"], | ||
| actor_id: row.actorId, | ||
| action: row.action, | ||
| target_type: row.targetType, | ||
| target_id: row.targetId, | ||
| details: row.details, | ||
| request_id: row.requestId, | ||
| occurred_at: row.occurredAt.toISOString(), | ||
| }), | ||
| ); | ||
| }, |
There was a problem hiding this comment.
Add pagination/cursor bounds for workspace audit queries.
listForWorkspace is unbounded. On larger workspaces this can create large DB and response payloads, degrading latency and reliability. Add a limit + cursor (e.g., occurredAt/id) and wire it through the route contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/db/src/queries/operation-events.ts` around lines 64 - 84, The
listForWorkspace function currently returns an unbounded result set; update the
operationEvents query in listForWorkspace to accept and apply pagination
parameters (limit and a cursor composed of occurredAt and id) so results are
bounded and stable. Modify listForWorkspace to take optional params (e.g.,
limit: number, cursor?: { occurredAt: string, id: string }), add a .limit(limit)
and a WHERE clause that implements cursor comparison (occurredAt <
cursor.occurredAt OR (occurredAt = cursor.occurredAt AND id < cursor.id)) while
preserving the orderBy(desc(operationEvents.occurredAt),
desc(operationEvents.id)); also update the returned payload to include a
nextCursor (based on the last row's occurredAt and id) and wire these new
params/response fields through the route contract that calls listForWorkspace.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/workos.ts`:
- Around line 77-82: The fetch calls to WorkOS must be bounded with an
AbortController timeout: wrap the user lookup fetch (the call using
workOsBaseUrl(options.apiBaseUrl)/user_management/users/${encodeURIComponent(workosUserId)}
and headers with options.apiKey) and the JWKS fetch (the other fetch in this
file) with an AbortController, start a setTimeout to call controller.abort()
after a sensible default (e.g., 5s or a configurable timeout), pass
controller.signal to fetch, and clearTimeout on success; ensure you handle
AbortError/timeout by throwing or returning a descriptive error so the auth path
fails fast instead of hanging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 4f8f810f-8a66-45da-abec-74b18775082a
📒 Files selected for processing (12)
.github/workflows/ci.ymlapps/api/src/index.test.tsapps/api/src/index.tsapps/api/src/workos.test.tsapps/api/src/workos.tspackages/db/migrations/0004_workspace_members.sqlpackages/db/snapshot/schema.sqlpackages/db/src/index.test.tspackages/db/src/local-repository.tspackages/db/src/postgres/repository.tspackages/db/src/schema.tsscripts/smoke-web-api.mjs
| const response = await fetch( | ||
| `${workOsBaseUrl(options.apiBaseUrl)}/user_management/users/${encodeURIComponent(workosUserId)}`, | ||
| { | ||
| headers: { authorization: `Bearer ${options.apiKey}` }, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify outbound WorkOS fetch call sites and whether they include a timeout signal.
rg -n 'fetch\(' apps/api/src/workos.ts -C3
rg -n 'signal\s*:' apps/api/src/workos.ts -C3Repository: zaks-io/agent-paste
Length of output: 633
Add bounded timeouts to WorkOS fetch calls in the auth path.
apps/api/src/workos.ts performs WorkOS outbound fetch calls for both the user lookup (around lines 77-82) and the JWKS fetch (around line 108) without any AbortController/signal, so a hung dependency can stall request handling and cause cascading latency. Add an explicit timeout (e.g., AbortController + setTimeout + clearTimeout) to both calls.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/workos.ts` around lines 77 - 82, The fetch calls to WorkOS must
be bounded with an AbortController timeout: wrap the user lookup fetch (the call
using
workOsBaseUrl(options.apiBaseUrl)/user_management/users/${encodeURIComponent(workosUserId)}
and headers with options.apiKey) and the JWKS fetch (the other fetch in this
file) with an AbortController, start a setTimeout to call controller.abort()
after a sensible default (e.g., 5s or a configurable timeout), pass
controller.signal to fetch, and clearTimeout on success; ensure you handle
AbortError/timeout by throwing or returning a descriptive error so the auth path
fails fast instead of hanging.
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-24.isaac-a46.workers.dev |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/pr-preview.yml:
- Line 97: Replace the fallback that assigns PR_PREVIEW_SECRET_SEED from
CLOUDFLARE_API_TOKEN so the workflow only uses the dedicated
PR_PREVIEW_SECRET_SEED; remove the "|| secrets.CLOUDFLARE_API_TOKEN" fallback
and add a fail-fast check step that validates secrets.PR_PREVIEW_SECRET_SEED is
set (exit non‑zero or use a conditional that stops the job) so preview admin
token/signing secrets/API key pepper never silently inherit
CLOUDFLARE_API_TOKEN.
In `@scripts/smoke-hosted.mjs`:
- Around line 157-175: In waitForAdminAuth, make transport errors (fetch/network
exceptions) retry until the deadline while treating permanent auth failures
(HTTP 401/403) as immediate failures: wrap the fetch call in a try/catch, on
catch record lastBody/lastStatus appropriately and continue the loop (sleep and
retry) until deadline; after a successful fetch, if response.status is 401 or
403 throw immediately with a clear error mentioning c.adminToken and url, but if
it’s another non-200 status keep retrying until deadline; preserve the existing
deadline, sleep(2000) backoff and include the last response body snippet in the
eventual thrown error.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3d2ad787-6a97-4537-8b07-aa2284f2e633
📒 Files selected for processing (3)
.github/workflows/pr-preview.ymlscripts/deploy-pr-preview.mjsscripts/smoke-hosted.mjs
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/smoke-hosted.mjs`:
- Line 170: The code assigns the string "transport_error" to lastStatus (which
otherwise holds numeric HTTP statuses), causing type inconsistency and mixed
output; change that assignment to a numeric sentinel such as -1 (replace the
"transport_error" assignment to lastStatus with -1) and update any downstream
reporting/formatting that prints lastStatus (the timeout/deadline message that
currently interpolates lastStatus) to render -1 as a human-friendly label (e.g.,
map -1 => "transport_error") so comparisons and logs remain consistent;
alternatively, if you prefer, add a separate boolean/enum (e.g., transportError
flag) instead of mutating lastStatus.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 96fba547-3c9f-40da-b11a-98c2569e4f2a
📒 Files selected for processing (2)
.github/workflows/pr-preview.ymlscripts/smoke-hosted.mjs
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-24.isaac-a46.workers.dev |
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-24.isaac-a46.workers.dev |
…ope boundary (#43) * docs: reconcile project-status with main after #42 Snapshot pointed at 22c4b36 (#24), ~18 commits stale. Update to 76c2b34 (#42), refresh latest-commit list, mark Phase 1 ~99% (only parked Logpush remains), drop completed backlog item 1, and add a Recently Completed entry for the hosted Artifact Rate Limit smoke assertion. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: reconcile Phase 2/3 scope and retire Auth0 framing phases.md, project-status.md, and web-app-todo.md disagreed on what Phase 3 is. Align all three with project-status as the source of truth and fold in the agreed decisions: - Phase 3 = WorkOS dashboard + CLI login, no Access Links. Dashboard committed to Phase 3 (was hedged to Phase 6). - CLI login uses WorkOS loopback PKCE; ADR 0060 rewritten off Auth0. - Operator gate is Cloudflare Access; rotation agent authenticates via an Access service token (ADR 0046/0045), replacing the gone Auth0 M2M path. API keys stay barred from admin routes. - Per-workspace retention in scope, bounded by ADR 0048 caps. - Access Links confirmed Phase 4 (ADR 0047/0052); viewer dropped from the web todo. Also fixes stale Auth0 references in ADR 0045/0046, the ADR README, and CONTEXT.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: resolve CLI-vs-dashboard scope boundary under WorkOS Closes the open ADR 0060 implementation detail. WorkOS RBAC claims are org-membership-scoped (identical across CLI and dashboard) and the JWT template is per-environment, so the boundary cannot be a token claim. Resolve it as a dedicated CLI WorkOS OAuth client (aud = cli_client_id) plus a server-side per-client scope ceiling that strips the Member-Only Scopes for the CLI surface; base scopes still come from workspace_members.scopes. ADR 0034's Auth0 audience exception re-noted for the WorkOS mechanism. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: address CodeRabbit review on ADR README - Reword "decided off Auth0" to plainer phrasing (ADR 0060 line). - Fix MCP tense to "will be formally re-decided" and note its on-paper decision is still Auth0 DCR (ADR 0061 line); MCP was decided, so the word stays "re-decided", only the tense changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Adds the API-side WorkOS foundation needed for the web dashboard to provision members and read canonical workspace data after PR #23. This keeps dashboard reads scoped by WorkOS membership while leaving key mutations, Access Link resolve, operator lockdown, and denylist reconciliation as later slices.
Changes
Risk: HIGH
Test plan
CodeRabbit notes: local CodeRabbit pre-flight was skipped because the CLI auth flow failed with authentication_failed: Failed to start server. Is port 0 in use?.
Summary by CodeRabbit
New Features
Chores
Tests