Skip to content

feat: Plan 9 PR C — control-plane writes + agent-builder view - #28

Merged
messagesgoel-blip merged 1 commit into
mainfrom
feat/dashboard-pr-c
Aug 7, 2026
Merged

feat: Plan 9 PR C — control-plane writes + agent-builder view#28
messagesgoel-blip merged 1 commit into
mainfrom
feat/dashboard-pr-c

Conversation

@messagesgoel-blip

Copy link
Copy Markdown
Collaborator

Plan 9 PR C — control-plane writes + agent-builder view

@coderabbitai review

Implements the PR C slice of docs/superpowers/plans/2026-08-01-plan-9-dashboard.md (PR A #26, PR B #27 merged).

Control plane — writes

  • PUT /v1/policies/active (policy:admin / tenant admin role): create-or-replaces the active policy and emits a policy.replace sync event in one transaction (mirrors scoreWriter's appendEventWithClient pattern) so edges pull the new policy over SSE instead of serving a stale snapshot. Pure parsePolicyUpdate validator (+unit tests) enforces the migration CHECKs (threshold 0-100 int, action enums, allow_sample_rate 0-1, fingerprint arrays).
  • API-key CRUDGET/POST /v1/api-keys, DELETE /v1/api-keys/:id. Secret (vrl_ + 64 hex) returned exactly once; HMAC-SHA256 hash + prefix persisted; soft-revoke (revoked_at). Scope allowlist (attest:read|write, admin:read, policy:read|admin, *). Cross-tenant delete is a 404 (no info leak).
  • principals list returns derived assurance_level (verified_key iff a non-revoked key has control_verified_at); principal detail includes read-only issuer attrs (trust_weight/verified_at/is_bootstrap) via LEFT JOIN. Made entity_kind/limit/offset query params optional so the agent-builder can list owned principals (latent strict-validation fix).

Dashboard — agent-builder view (/agent-builder)

Master-detail: owned-principals list with assurance badges → selected principal shows keys + assurance, in/out attestation feed, network score-history line chart (recharts), and a read-only issuer relationship card. Composed from existing /v1/principals*, /v1/attestations, /v1/scores/:id/history endpoints. Active tenant + selected principal id embedded in query keys.

Verification

  • CP: tsc --noEmit clean; 143 unit + 40 integration tests pass (new integration suite covers policy PUT create/update/sync-event/authz, api-key CRUD/isolation, agent-builder reads).
  • Dashboard: typecheck clean; 45 vitest pass; vite build succeeds.

Out of scope (PR D): billing (Stripe checkout/portal/webhook) + admin (tenants/bootstrap/graph health/issuer queue). No new migrations.

Note on attestation feed authz

GET /v1/attestations requires attest:read (API key) or tenant admin/staff role (OIDC), unchanged. For owned principals the caller's tenant owns the issuer/subject, so participants-visibility attestations are visible to that tenant. The view degrades gracefully to an error panel when the caller lacks the scope; refining read authz for plain tenant members is out of scope here.

CP:
- PUT /v1/policies/active (policy:admin): create-or-replace the active
  policy and emit a policy.replace sync_event in one transaction so edges
  pull the new policy over SSE instead of serving a stale snapshot.
  Pure parsePolicyUpdate validator (+unit tests) for threshold/action/
  sample-rate/fingerprint bounds matching the migration CHECKs.
- API-key CRUD: GET/POST /v1/api-keys, DELETE /v1/api-keys/:id. Secret
  (vrl_ + 64 hex) returned once; HMAC hash + prefix persisted; soft-revoke.
  Scope allowlist validation. Cross-tenant delete is a 404 (no leak).
- principals list returns derived assurance_level (verified_key iff a
  non-revoked key has control_verified_at); principal detail includes
  read-only issuer attrs (trust_weight/verified_at/is_bootstrap) via
  LEFT JOIN. principals query params (entity_kind/limit/offset) made
  optional so agent-builder can list owned principals.
- integration coverage: policy PUT (create/update/sync event/authz),
  api-key CRUD (mint/list/revoke/isolation), agent-builder reads
  (assurance_level, issuer attrs).

Dashboard:
- agent-builder view (/agent-builder): owned-principals master list with
  assurance badges, selected-principal detail with keys + assurance,
  in/out attestation feed, network score-history line chart, read-only
  issuer relationship card. Tenant + principal id embedded in query keys.
- vitest: scoreSeries transform, PrincipalList/KeyList/AttestationFeed,
  agentBuilderQueryKeys.

Verification: CP tsc clean, 143 unit + 40 integration pass; dashboard
typecheck clean, 45 vitest pass, vite build succeeds.

Out of scope (PR D): billing (Stripe checkout/portal/webhook) + admin
(tenants/bootstrap/graph health/issuer queue). No new migrations.
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd2328f7-d01f-4aee-879e-7278bd32d8d2

📥 Commits

Reviewing files that changed from the base of the PR and between 842bf86 and 9f71bd2.

📒 Files selected for processing (24)
  • control-plane/src/__tests__/integration/dashboard-writes.test.ts
  • control-plane/src/app.ts
  • control-plane/src/domains/apikey/apiKeyRepository.ts
  • control-plane/src/domains/policy/policyRepository.ts
  • control-plane/src/domains/policy/policyValidation.test.ts
  • control-plane/src/domains/policy/policyValidation.ts
  • control-plane/src/domains/principal/principalRepository.ts
  • control-plane/src/routes/apiKeys.ts
  • control-plane/src/routes/policies.ts
  • control-plane/src/routes/principals.ts
  • dashboard/src/api/agentBuilder.ts
  • dashboard/src/lib/queryKeys.test.ts
  • dashboard/src/lib/queryKeys.ts
  • dashboard/src/lib/scoreSeries.test.ts
  • dashboard/src/lib/scoreSeries.ts
  • dashboard/src/pages/AgentBuilderHomePage.tsx
  • dashboard/src/pages/agent-builder/AttestationFeed.tsx
  • dashboard/src/pages/agent-builder/IssuerCard.tsx
  • dashboard/src/pages/agent-builder/KeyList.tsx
  • dashboard/src/pages/agent-builder/PrincipalList.tsx
  • dashboard/src/pages/agent-builder/ScoreHistoryChart.tsx
  • dashboard/src/pages/agent-builder/components.test.tsx
  • dashboard/src/styles/index.css
  • docs/superpowers/plans/HANDOVER.md

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Plan 9 PR C: policy writes + API-key CRUD + agent-builder view

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add tenant-scoped policy writes (PUT /v1/policies/active) emitting policy.replace sync events.
• Implement tenant API-key CRUD with one-time secrets, scope allowlist, and soft-revoke.
• Build /agent-builder master-detail view backed by principals/attestations/scores endpoints.
Diagram

graph TD
  D["Dashboard /agent-builder"] --> CP["Control-plane API"]
  CP --> PR["Principals/Attestations/Scores"] --> DB[("Postgres")]
  CP --> AK["API-keys"] --> DB
  CP --> PO["Policies (PUT active)"] --> DB --> SE[("sync_events")]
  SE --> EN["Edge nodes (SSE pull)"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use schema validation library (e.g., Zod) for request bodies
  • ➕ More declarative validation with better error shaping and type inference
  • ➕ Reusable validators for other endpoints (API keys, policies, etc.)
  • ➖ Introduces a new dependency and conventions to the codebase
  • ➖ May be heavier than needed for a small number of payloads right now
2. Store API key hashes with a slow password KDF (Argon2/bcrypt) instead of HMAC
  • ➕ Reduces risk if the hashing secret is compromised (no single secret to rotate)
  • ➕ Standard pattern for secrets-at-rest
  • ➖ More CPU per auth check and more operational tuning
  • ➖ Current design likely assumes a server-held secret for other HMAC use; changes may ripple

Recommendation: The PR’s approach is solid for Plan 9’s scope: keep policy replacement + sync event emission transactional to avoid stale edge snapshots, and use HMAC-hashed API keys with one-time secrets and tenant-scoped revocation to prevent leaks. If validation needs expand across many endpoints, consider standardizing on a schema library (Zod) later; if API keys become long-lived/high-value, revisit hash strategy and rotation story (HMAC secret rotation or a KDF).

Files changed (24) +1433 / -33

Enhancement (17) +908 / -24
app.tsMount new /v1/api-keys router +2/-1

Mount new /v1/api-keys router

• Wires the new API key CRUD router into the Express app at /v1/api-keys.

control-plane/src/app.ts

apiKeyRepository.tsAdd API key repository (mint/list/revoke) with HMAC hashing +69/-0

Add API key repository (mint/list/revoke) with HMAC hashing

• Implements generation of one-time API key secrets, HMAC-SHA256 hashing using configured secret, listing non-revoked keys for a tenant, and soft-revocation with 404-on-mismatch to avoid cross-tenant leaks.

control-plane/src/domains/apikey/apiKeyRepository.ts

policyRepository.tsAdd transactional active policy replacement with sync event emission +90/-7

Add transactional active policy replacement with sync event emission

• Refactors policy selects to reuse a shared column list and adds replaceActivePolicy() that upserts the active policy and appends a policy.replace sync event within the same transaction, returning the allocated sync_version.

control-plane/src/domains/policy/policyRepository.ts

policyValidation.tsIntroduce parsePolicyUpdate request validator for policy PUT +76/-0

Introduce parsePolicyUpdate request validator for policy PUT

• Adds a pure validator that applies defaults and enforces invariants matching DB CHECK constraints (threshold integer range, action enums, sample-rate range, boolean flags, non-negative max age, and bounded fingerprint string arrays).

control-plane/src/domains/policy/policyValidation.ts

principalRepository.tsEnrich principal reads with issuer attrs and derived assurance_level +41/-3

Enrich principal reads with issuer attrs and derived assurance_level

• Updates principal detail to LEFT JOIN issuer attributes and return them as a nested issuer object (or null). Updates principal list to compute assurance_level based on existence of a non-revoked, control-verified principal key.

control-plane/src/domains/principal/principalRepository.ts

apiKeys.tsAdd /v1/api-keys GET/POST/DELETE endpoints with scope allowlist +91/-0

Add /v1/api-keys GET/POST/DELETE endpoints with scope allowlist

• Adds authenticated tenant-scoped API key listing, minting (returns secret once), and revocation. Validates scopes against an allowlist, caps scope count, and enforces tenant presence.

control-plane/src/routes/apiKeys.ts

policies.tsAdd PUT /v1/policies/active with policy:admin scope and validation +28/-8

Add PUT /v1/policies/active with policy:admin scope and validation

• Refactors tenant extraction into a helper, keeps GET /active behavior, and adds PUT /active guarded by requireScope('policy:admin') to replace the active policy and return sync_version after emitting policy.replace.

control-plane/src/routes/policies.ts

agentBuilder.tsAdd agent-builder API client for principals, keys, attestations, and score history +108/-0

Add agent-builder API client for principals, keys, attestations, and score history

• Introduces typed client helpers for agent-builder data sources, including safeScoreHistory() which returns null on 404 to support principals without score history yet.

dashboard/src/api/agentBuilder.ts

queryKeys.tsAdd agent-builder query key namespace +17/-3

Add agent-builder query key namespace

• Adds agentBuilderQueryKeys() that scopes React Query caches by tenant id and selected principal id for correct cross-tenant/selection invalidation behavior.

dashboard/src/lib/queryKeys.ts

scoreSeries.tsAdd scoreHistory -> chart series adapter +20/-0

Add scoreHistory -> chart series adapter

• Implements scoreSeries() to sort score history points oldest-first and map them into chart-friendly points with a formatted timestamp label.

dashboard/src/lib/scoreSeries.ts

AgentBuilderHomePage.tsxImplement agent-builder master/detail page with queries and panels +128/-2

Implement agent-builder master/detail page with queries and panels

• Builds the /agent-builder UI: owned principals list with selection, plus detail panels for keys/assurance, score history chart, incoming/outgoing attestation feeds, and issuer relationship. Uses tenant+principal-scoped query keys and defaults to selecting the first principal.

dashboard/src/pages/AgentBuilderHomePage.tsx

AttestationFeed.tsxRender in/out attestation feed table with empty state +54/-0

Render in/out attestation feed table with empty state

• Adds a compact table for attestations with direction-aware counterparty labeling, visibility badges, and a friendly empty state when none are visible.

dashboard/src/pages/agent-builder/AttestationFeed.tsx

IssuerCard.tsxAdd read-only issuer relationship card +24/-0

Add read-only issuer relationship card

• Displays issuer attributes (trust weight, verification timestamp, bootstrap flag) when the selected principal is also an issuer, otherwise shows a neutral empty state.

dashboard/src/pages/agent-builder/IssuerCard.tsx

KeyList.tsxRender principal key list with assurance badges +43/-0

Render principal key list with assurance badges

• Displays keys with hash previews, validity and status, and badges for revoked/verified/control-unverified keys, including an empty state for principals with no keys.

dashboard/src/pages/agent-builder/KeyList.tsx

PrincipalList.tsxAdd selectable owned-principals list with assurance badges +37/-0

Add selectable owned-principals list with assurance badges

• Introduces a selectable list UI for owned principals, highlighting the selected principal and showing verified vs unverified assurance badges.

dashboard/src/pages/agent-builder/PrincipalList.tsx

ScoreHistoryChart.tsxAdd Recharts line chart for network score history +35/-0

Add Recharts line chart for network score history

• Renders a responsive line chart for score over time with axes/tooltip, and an empty state when no score history exists.

dashboard/src/pages/agent-builder/ScoreHistoryChart.tsx

index.cssAdd CSS for agent-builder select list and subtitles +45/-0

Add CSS for agent-builder select list and subtitles

• Introduces styling for panel subtitles and a selectable list pattern (selected state, layout, typography) used by the agent-builder principals list.

dashboard/src/styles/index.css

Bug fix (1) +3 / -3
principals.tsMake principals list query params optional +3/-3

Make principals list query params optional

• Relaxes strict query validation by making entity_kind/limit/offset optional to support agent-builder’s owned-principals list without requiring parameters.

control-plane/src/routes/principals.ts

Tests (5) +515 / -0
dashboard-writes.test.tsAdd integration coverage for policy PUT, API-key CRUD, and agent-builder reads +271/-0

Add integration coverage for policy PUT, API-key CRUD, and agent-builder reads

• Introduces an end-to-end integration suite validating: active policy replacement emits policy.replace events, policy authz/validation behavior, API-key mint/list/revoke with tenant isolation, and principal read-shapes required by agent-builder (assurance_level and issuer attrs).

control-plane/src/tests/integration/dashboard-writes.test.ts

policyValidation.test.tsUnit tests for parsePolicyUpdate validator +87/-0

Unit tests for parsePolicyUpdate validator

• Adds tests ensuring policy update defaults, bounds checks (threshold, sample rate), enum validation for actions, fingerprint array validation/limits, and correct 400 behavior for missing/non-object bodies.

control-plane/src/domains/policy/policyValidation.test.ts

queryKeys.test.tsAdd tests for agentBuilderQueryKeys cache partitioning +18/-0

Add tests for agentBuilderQueryKeys cache partitioning

• Extends query-key tests to ensure agent-builder keys vary by tenant and selected principal, and use a sentinel when inputs are null.

dashboard/src/lib/queryKeys.test.ts

scoreSeries.test.tsAdd tests for scoreSeries chart transformation +27/-0

Add tests for scoreSeries chart transformation

• Verifies chronological sorting, preservation of blacklisted flags, and empty handling for the score-history chart series conversion.

dashboard/src/lib/scoreSeries.test.ts

components.test.tsxAdd component tests for agent-builder list/feed widgets +112/-0

Add component tests for agent-builder list/feed widgets

• Adds vitest + testing-library coverage for PrincipalList selection/empty state/badges, KeyList badge logic and empty state, and AttestationFeed direction rendering and empty state.

dashboard/src/pages/agent-builder/components.test.tsx

Documentation (1) +7 / -6
HANDOVER.mdUpdate Plan 9 handover status for PR B merged and PR C in flight +7/-6

Update Plan 9 handover status for PR B merged and PR C in flight

• Updates the handover header and Plan 9 section to reflect PR B merged and the new PR C scope (policy writes, API-key CRUD, agent-builder view).

docs/superpowers/plans/HANDOVER.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Unscoped API-key CRUD 🐞 Bug ⛨ Security
Description
The new /v1/api-keys GET/POST/DELETE routes only require authentication and a tenantId, so any
authenticated API key (even with empty scopes) can mint new keys with elevated scopes (including
'*') and revoke other active keys in the same tenant.
Code

control-plane/src/routes/apiKeys.ts[R9-11]

+const router = Router();
+router.use(authMiddleware);
+
Relevance

●●● Strong

Privilege-escalation risk; team has accepted tightening tenant/authz boundaries and role/scope
semantics in similar security reviews.

PR-#6
PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The API-key CRUD router only uses authMiddleware and never calls requireScope, while also allowing
the wildcard '*' scope; since API key scopes are populated by authMiddleware, a low-scope key can
call these endpoints to mint a stronger key or revoke other keys in its tenant.

control-plane/src/routes/apiKeys.ts[9-19]
control-plane/src/routes/apiKeys.ts[51-89]
control-plane/src/middleware/auth.ts[62-97]
control-plane/src/middleware/requireScope.ts[10-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`/v1/api-keys` endpoints are authenticated but not authorized. Because `parseScopes()` allows powerful scopes like `policy:admin` and `*`, any authenticated API key can mint a stronger key and/or revoke other keys within the tenant.

### Issue Context
- The router applies `authMiddleware` but never applies `requireScope()` (or any role/scope check).
- `requireScope()` exists and already enforces API-key scopes (including `*`).

### Fix Focus Areas
- control-plane/src/routes/apiKeys.ts[9-89]
- control-plane/src/middleware/requireScope.ts[4-36]

### What to change
1. Add an explicit authorization gate for API-key management (e.g. `requireScope('policy:admin')`) to **GET**, **POST**, and **DELETE** routes (or `router.use(requireScope('policy:admin'))` after `authMiddleware`).
2. Prevent privilege escalation by ensuring minted scopes are not more permissive than the caller’s own scopes unless the caller is platform staff/admin (OIDC) or already has `*`.
3. Add an integration test proving a low-scope/empty-scope key gets `403` for POST/DELETE (and for GET if intended).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Cross-tenant role bypass 🐞 Bug ⛨ Security
Description
PUT /v1/policies/active uses requireScope('policy:admin'), but requireScope() authorizes OIDC users
if ANY membership role is admin/staff, not the active tenant role; a user who is admin in tenant A
and only a member in tenant B can still update tenant B’s policy when selecting tenant B via
X-Tenant-Id.
Code

control-plane/src/routes/policies.ts[R40-43]

+router.put(
+  '/active',
+  requireScope('policy:admin'),
+  defineHandler({
Relevance

●●● Strong

Matches prior accepted fix: avoid using roles across all memberships for authorization; must bind to
active tenant membership.

PR-#6

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The policies PUT route newly gates writes using requireScope('policy:admin'). authMiddleware’s OIDC
path sets tenantId to the active membership (X-Tenant-Id) but retains roles for all memberships, and
requireScope grants scope if any role is admin/staff, enabling escalation when active tenant role is
only member.

control-plane/src/routes/policies.ts[40-49]
control-plane/src/middleware/auth.ts[126-145]
control-plane/src/middleware/requireScope.ts[23-33]
PR-#6

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`requireScope()` currently checks `req.user.roles` across *all* memberships and treats any `admin`/`staff` membership as sufficient. For tenant-scoped writes like `PUT /v1/policies/active`, this allows privilege escalation when a user belongs to multiple tenants with different roles.

### Issue Context
- `authenticateOidc()` sets `req.user.tenantId` from the *active* membership selected via `X-Tenant-Id`, but also sets `req.user.roles` to all membership roles.
- `requireScope()` checks `req.user.roles.some(role in {staff,admin})` rather than checking the role for `req.user.tenantId`.

### Fix Focus Areas
- control-plane/src/middleware/requireScope.ts[18-35]
- control-plane/src/middleware/auth.ts[126-145]
- control-plane/src/routes/policies.ts[40-49]

### What to change
1. Update `requireScope()` to authorize OIDC users based on the **active membership role** (`req.user.role`) or by looking up the role for `req.user.tenantId`, rather than any role across `req.user.roles`.
2. Add an integration test with one OIDC user who is `admin` in tenant A and `member` in tenant B proving `PUT /v1/policies/active` for tenant B returns `403`.
3. Ensure other tenant-scoped endpoints using `requireScope()` follow the same active-tenant semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Principal detail fields leak 🐞 Bug ≡ Correctness
Description
getPrincipal() SELECTs issuer columns into the row and then spreads the row into the returned
object, so GET /v1/principals/:id includes unexpected top-level
trust_weight/verified_at/is_bootstrap fields in addition to the nested issuer object.
Code

control-plane/src/domains/principal/principalRepository.ts[R60-63]

+    `SELECT p.*, i.trust_weight::float AS trust_weight, i.verified_at, i.is_bootstrap
+     FROM principals p
+     LEFT JOIN issuers i ON i.principal_id = p.id
+     WHERE p.id = $1`,
Relevance

●●● Strong

Leaking joined issuer columns at top-level is a clear response-shape bug; team usually accepts
correctness fixes in principal/auth paths.

PR-#6
PR-#4

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The repository query aliases issuer columns into the result set and then returns {...row} in both
branches, which will carry the aliased columns into the returned object. The principals route
returns this object directly via ok(res, principal), so the extra fields reach API consumers.

control-plane/src/domains/principal/principalRepository.ts[58-78]
control-plane/src/routes/principals.ts[45-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`principalRepository.getPrincipal()` adds issuer columns via a LEFT JOIN and then spreads the entire SQL row into the returned principal object. This causes the HTTP response to include unintended top-level fields (`trust_weight`, `verified_at`, `is_bootstrap`) in addition to `issuer`.

### Issue Context
`GET /v1/principals/:id` returns the repository object directly, so any extra properties on the returned object become part of the public API payload.

### Fix Focus Areas
- control-plane/src/domains/principal/principalRepository.ts[58-78]
- control-plane/src/routes/principals.ts[45-50]

### What to change
1. Destructure the joined columns out of the row before spreading, e.g.:
  - `const { trust_weight, verified_at, is_bootstrap, ...p } = row;`
  - return `{ ...p, issuer: trust_weight == null ? null : { trust_weight, verified_at, is_bootstrap } }`
2. Add/extend a test asserting those three fields are absent from the top-level response for both issuer and non-issuer principals.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Hardcoded DB URL and HMAC 📘 Rule violation ⛨ Security
Description
The new integration test hard-codes a Postgres connection string with embedded credentials and a
fixed API_KEY_HMAC_SECRET. This violates the requirement that source code contain no
secrets/credentials detectable by gitleaks.
Code

control-plane/src/tests/integration/dashboard-writes.test.ts[R1-3]

+process.env.DATABASE_URL ||=
+  'postgresql://verilink:********@127.0.0.1:15432/verilink_test';
+process.env.API_KEY_HMAC_SECRET ||= 'test-hmac-secret-for-integration';
Relevance

● Weak

Team previously rejected removing test DSN/HMAC literals despite gitleaks concerns; likely keep
consistent integration-test defaults.

PR-#16
PR-#21
PR-#7

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2373972 forbids committing secrets/credentials in source code. The added test sets
default DATABASE_URL containing verilink:verilink@... and a literal API_KEY_HMAC_SECRET, which
are hard-coded credential/secret material.

Rule 2373972: Source code must contain no secrets or credentials as detected by gitleaks
control-plane/src/tests/integration/dashboard-writes.test.ts[1-3]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`control-plane/src/__tests__/integration/dashboard-writes.test.ts` introduces hard-coded credentials/secrets (`DATABASE_URL` with embedded user/password and `API_KEY_HMAC_SECRET`). This can be flagged by gitleaks and violates the repo policy of not committing secrets/credentials.

## Issue Context
Even if these are intended for local/dev integration tests, the compliance rule requires the repository to be free of secrets/credentials as detected by gitleaks. Prefer environment-provided values, or a narrowly scoped, explicitly documented allowlist entry if these values are truly non-sensitive test fixtures.

## Fix Focus Areas
- control-plane/src/__tests__/integration/dashboard-writes.test.ts[1-3]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 18 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +9 to +11
const router = Router();
router.use(authMiddleware);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Unscoped api-key crud 🐞 Bug ⛨ Security

The new /v1/api-keys GET/POST/DELETE routes only require authentication and a tenantId, so any
authenticated API key (even with empty scopes) can mint new keys with elevated scopes (including
'*') and revoke other active keys in the same tenant.
Agent Prompt
### Issue description
`/v1/api-keys` endpoints are authenticated but not authorized. Because `parseScopes()` allows powerful scopes like `policy:admin` and `*`, any authenticated API key can mint a stronger key and/or revoke other keys within the tenant.

### Issue Context
- The router applies `authMiddleware` but never applies `requireScope()` (or any role/scope check).
- `requireScope()` exists and already enforces API-key scopes (including `*`).

### Fix Focus Areas
- control-plane/src/routes/apiKeys.ts[9-89]
- control-plane/src/middleware/requireScope.ts[4-36]

### What to change
1. Add an explicit authorization gate for API-key management (e.g. `requireScope('policy:admin')`) to **GET**, **POST**, and **DELETE** routes (or `router.use(requireScope('policy:admin'))` after `authMiddleware`).
2. Prevent privilege escalation by ensuring minted scopes are not more permissive than the caller’s own scopes unless the caller is platform staff/admin (OIDC) or already has `*`.
3. Add an integration test proving a low-scope/empty-scope key gets `403` for POST/DELETE (and for GET if intended).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +40 to +43
router.put(
'/active',
requireScope('policy:admin'),
defineHandler({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Cross-tenant role bypass 🐞 Bug ⛨ Security

PUT /v1/policies/active uses requireScope('policy:admin'), but requireScope() authorizes OIDC users
if ANY membership role is admin/staff, not the active tenant role; a user who is admin in tenant A
and only a member in tenant B can still update tenant B’s policy when selecting tenant B via
X-Tenant-Id.
Agent Prompt
### Issue description
`requireScope()` currently checks `req.user.roles` across *all* memberships and treats any `admin`/`staff` membership as sufficient. For tenant-scoped writes like `PUT /v1/policies/active`, this allows privilege escalation when a user belongs to multiple tenants with different roles.

### Issue Context
- `authenticateOidc()` sets `req.user.tenantId` from the *active* membership selected via `X-Tenant-Id`, but also sets `req.user.roles` to all membership roles.
- `requireScope()` checks `req.user.roles.some(role in {staff,admin})` rather than checking the role for `req.user.tenantId`.

### Fix Focus Areas
- control-plane/src/middleware/requireScope.ts[18-35]
- control-plane/src/middleware/auth.ts[126-145]
- control-plane/src/routes/policies.ts[40-49]

### What to change
1. Update `requireScope()` to authorize OIDC users based on the **active membership role** (`req.user.role`) or by looking up the role for `req.user.tenantId`, rather than any role across `req.user.roles`.
2. Add an integration test with one OIDC user who is `admin` in tenant A and `member` in tenant B proving `PUT /v1/policies/active` for tenant B returns `403`.
3. Ensure other tenant-scoped endpoints using `requireScope()` follow the same active-tenant semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +60 to +63
`SELECT p.*, i.trust_weight::float AS trust_weight, i.verified_at, i.is_bootstrap
FROM principals p
LEFT JOIN issuers i ON i.principal_id = p.id
WHERE p.id = $1`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Principal detail fields leak 🐞 Bug ≡ Correctness

getPrincipal() SELECTs issuer columns into the row and then spreads the row into the returned
object, so GET /v1/principals/:id includes unexpected top-level
trust_weight/verified_at/is_bootstrap fields in addition to the nested issuer object.
Agent Prompt
### Issue description
`principalRepository.getPrincipal()` adds issuer columns via a LEFT JOIN and then spreads the entire SQL row into the returned principal object. This causes the HTTP response to include unintended top-level fields (`trust_weight`, `verified_at`, `is_bootstrap`) in addition to `issuer`.

### Issue Context
`GET /v1/principals/:id` returns the repository object directly, so any extra properties on the returned object become part of the public API payload.

### Fix Focus Areas
- control-plane/src/domains/principal/principalRepository.ts[58-78]
- control-plane/src/routes/principals.ts[45-50]

### What to change
1. Destructure the joined columns out of the row before spreading, e.g.:
   - `const { trust_weight, verified_at, is_bootstrap, ...p } = row;`
   - return `{ ...p, issuer: trust_weight == null ? null : { trust_weight, verified_at, is_bootstrap } }`
2. Add/extend a test asserting those three fields are absent from the top-level response for both issuer and non-issuer principals.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@messagesgoel-blip
messagesgoel-blip merged commit 948705a into main Aug 7, 2026
6 checks passed
@messagesgoel-blip
messagesgoel-blip deleted the feat/dashboard-pr-c branch August 7, 2026 05:17
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