OnePlatform User Story Analysis v3¶
Date: 2026-06-14 Method: 10-persona exhaustive analysis with source code verification Previous analyses: v1 (108 points), v2 (85 unique, 13 CRITICAL) This analysis: NET-NEW findings only — every item verified as absent from v2
Summary Table¶
| Severity | Count |
|---|---|
| CRITICAL | 5 |
| HIGH | 18 |
| MEDIUM | 22 |
| LOW | 8 |
| Total net-new findings | 53 |
Methodology¶
This v3 analysis applies 10 distinct user personas to the entire OnePlatform codebase. Every finding has been:
- Verified in source code — file paths, line numbers, and exact code patterns are cited
- Cross-referenced against v2 — only findings absent from v2's 85 items (C-01 through L-06) are included
- Classified by severity using the same rubric as v2
Many v2 findings have been FIXED in the current codebase (C-01, C-04, C-06, C-07, C-08, C-13, H-01, H-02, H-03, H-04, H-05, H-06, H-07, H-08, H-09, H-10, H-12, H-13, H-14, H-17, H-18, H-19, H-22, H-37, M-01, M-06, M-07, M-10, M-18, M-19, M-21, and others). This analysis focuses exclusively on what remains broken or was never caught.
Personas Applied¶
| Persona | Focus Area |
|---|---|
| First-Time Self-Hoster | Docker Compose experience, bootstrap flow, first-run errors |
| Data Engineer | Connector pipelines, sync reliability, mapping correctness, batch processing |
| App Developer | SDK, BFF, build/deploy lifecycle, type safety |
| Plugin Developer | Plugin SDK, hook system, sandbox interaction, bundle lifecycle |
| Platform Admin | User management, RBAC configuration, tenant operations, monitoring |
| DevOps/SRE | Observability, scaling, backup, health checks, container lifecycle |
| Security Auditor | Auth flows, token handling, SSRF, injection, secrets management |
| Enterprise Evaluator | Multi-tenancy, compliance, audit trail, SSO integration |
| Power User / Data Scientist | Query performance, API ergonomics, large dataset handling |
| Casual / Non-Technical User | UI clarity, error messages, onboarding guidance |
Top 10 Priorities¶
| # | Finding | Severity | Why It Matters | Effort |
|---|---|---|---|---|
| 1 | API key validate() returns empty roles: [] | CRITICAL | Any RBAC check using roles (not scopes) fails silently for all API key users | 5-line fix in api-key-service.ts |
| 2 | Frontend API key scopes don't match backend | CRITICAL | Keys created from the UI get invalid scopes (pipelines:run, connectors:manage) that don't exist | Fix AVAILABLE_SCOPES array in ApiKeysPage.tsx |
| 3 | Pipeline execution engine calls lack service tokens | CRITICAL | All pipeline steps fail when services enforce Ed25519 service auth on /internal/* routes | Add X-Service-Token header to callExecutionService() and sibling functions |
| 4 | BFF service token is a plain string, not an Ed25519 JWT | CRITICAL | All BFF-to-upstream calls fail serviceAuthMiddleware validation | Generate and sign proper Ed25519 tokens |
| 5 | User deactivation blocklists wrong JTI | CRITICAL | Access tokens remain valid after user deactivation — the wrong JTI is revoked | Use the access token's JTI, not the refresh token's JTI |
| 6 | API key revocation Redis flag has no TTL | HIGH | auth:apikey:revocation:* keys accumulate forever, leaking Redis memory | Add TTL matching key expiry or a fixed upper bound |
| 7 | Retention runs with empty tenantId | HIGH | connectorRepo.list("") passes empty tenant — may scan all tenants or return nothing | Pass tenant list or iterate tenants explicitly |
| 8 | BFF /permissions returns all roles, not user-specific | HIGH | Every user sees every app role's permissions regardless of their own role assignment | Filter by user's actual role membership |
| 9 | Gateway forwards client x-forwarded-for verbatim | HIGH | Rate limiting keyed on this header is fully bypassable by clients | Set x-forwarded-for from TCP remoteAddress |
| 10 | Mapping service expression transforms lack service auth | HIGH | Calls to execution service at line 116 of mapping-service.ts have no X-Service-Token header | Add authentication header |
All Findings¶
CRITICAL (5)¶
V3-C-01: API Key validate() Returns Empty Roles Array¶
Severity: CRITICAL Personas: Security Auditor, Platform Admin, Data Engineer Component: services/auth/src/services/api-key-service.ts (line 240-246) Not in v2: v2 H-04 covered scope subsetting on creation; this is about the runtime UserContext returned by validate()
Problem: The validate() method builds a UserContext with roles: [] (empty array). Any downstream middleware or route handler that checks user.roles (e.g., RBAC entity-level permissions in rbac-service.ts, or any roles.includes("editor") guard) will fail silently for all API key authenticated requests. The user's actual roles from the users table are never fetched.
Impact: API key users are effectively locked out of any role-gated feature. Only scope-based checks work. This breaks the RBAC model for programmatic access.
Fix: Join against auth.users to fetch the user's roles during validation, or at minimum set roles to the user's stored roles.
Effort: Small (5-10 lines: add a query to fetch roles from auth.users by matchedRow.user_id)
Affected files: - services/auth/src/services/api-key-service.ts
V3-C-02: Frontend API Key Scope List Mismatches Backend¶
Severity: CRITICAL Personas: App Developer, Data Engineer, Platform Admin Component: packages/frontend/src/pages/settings/ApiKeysPage.tsx (line 68-74) Not in v2: Not mentioned in any v2 finding
Problem: The frontend's AVAILABLE_SCOPES array contains:
But the backend (token-service.ts line 29-49) defines these actual scopes:
data:read, data:write, ontology:read, ontology:write, pipelines:read, pipelines:trigger,
pipelines:manage, apps:read, apps:deploy, apps:manage, plugins:read, plugins:manage,
users:read, users:manage, logs:read, webhooks:manage, execution:read, execution:run, admin
Two frontend scopes (pipelines:run and connectors:manage) do not exist in the backend. Keys created with these scopes will have no effect. Meanwhile, 14 real scopes (including ontology:read, execution:read, pipelines:trigger) are missing from the UI entirely.
Impact: Users creating API keys from the frontend get non-functional scopes and cannot select the scopes they actually need.
Fix: Replace AVAILABLE_SCOPES with the actual scope list from the backend. Consider fetching available scopes dynamically from an API endpoint.
Effort: Small (replace 5-element array with correct values)
Affected files: - packages/frontend/src/pages/settings/ApiKeysPage.tsx - services/auth/src/services/token-service.ts (reference)
V3-C-03: Pipeline Execution Engine Internal Calls Lack Service Tokens¶
Severity: CRITICAL Personas: Data Engineer, DevOps/SRE, Plugin Developer Component: services/pipeline/src/services/execution-engine.ts (lines 303, 236, 422) Not in v2: v2 covered missing service keys (C-06) and token generation (H-01) but not the pipeline engine's outbound calls
Problem: Three critical internal HTTP calls in the execution engine have no X-Service-Token header:
callExecutionService()(line 302-303): Calls${executionServiceUrl}/internal/execution/runwithout any service auth headersresolveHookChain()(line 236-237): Calls${pluginServiceUrl}/internal/plugins/hookswithout any service auth headersexecuteConnectorStep()(line 422): Calls${ingestionServiceUrl}/internal/ingestion/syncwithout any service auth headers
Since all services now enforce Ed25519 JWT authentication on /internal/* routes via serviceAuthMiddleware, these calls will be rejected with 401 in production.
Impact: All pipeline runs fail. No code steps, connector steps, or hook chains can execute.
Fix: Inject a service token signer and include X-Service-Token header in all three HTTP calls.
Effort: Medium (inject deps, sign tokens, add headers to 3 call sites)
Affected files: - services/pipeline/src/services/execution-engine.ts
V3-C-04: BFF Service Token Is a Plain String, Not an Ed25519 JWT¶
Severity: CRITICAL Personas: App Developer, Security Auditor Component: services/app/src/routes/bff.ts (line 37-39) Not in v2: v2 addressed BFF route gaps (H-22) and app-sdk header issues (C-08), not the service auth mechanism
Problem: The BFF routes use process.env["OP_SERVICE_TOKEN_SECRET"] as a bare string in the X-Service-Token header:
However, serviceAuthMiddleware (in packages/core/src/middleware/service-auth.ts) expects the X-Service-Token value to be a signed Ed25519 JWT that it verifies cryptographically. A plain string will fail jwtVerify() with an invalid token error.
Impact: All BFF data proxy calls (GET/POST/PATCH/PUT/DELETE /bff/data/*) fail with 401. Apps built with app-sdk cannot read or write data.
Fix: The BFF must sign Ed25519 JWTs using the app service's private key, including the caller service name and target service in the claims.
Effort: Medium (integrate Ed25519 signing library, generate tokens with proper claims)
Affected files: - services/app/src/routes/bff.ts - services/app/src/services/deploy-service.ts (same issue at line 258-260)
V3-C-05: User Deactivation Blocklists Wrong JTI¶
Severity: CRITICAL Personas: Security Auditor, Platform Admin Component: services/auth/src/routes/users.ts (lines 178-192) Not in v2: v2 H-06 covered "no revocation on deactivation" which is now implemented, but the implementation has a logic error
Problem: When deactivating a user, the code iterates active sessions and blocklists session.refresh_token_jti:
The comment says "refresh_token_jti stored on the session row is the JTI of the most-recently-issued access token" — but this is incorrect. The refresh_token_jti column stores the JTI of the refresh token, not the access token. The access token's JTI is embedded in the JWT itself and is not stored in the sessions table.
The revocation blocklist in authMiddleware checks revocation:${claims.jti} where claims.jti is the access token's JTI. So the blocklist entry for the refresh token JTI will never match any access token validation check.
Impact: Deactivated users' access tokens remain valid until natural expiry (up to 15 minutes). During this window, a deactivated user retains full platform access.
Fix: Either: (a) store the access token JTI in the sessions table alongside the refresh token JTI, or (b) re-derive the access token JTI from the session data, or (c) blocklist based on userId with a per-user revocation check in auth middleware.
Effort: Medium (requires schema change or middleware enhancement)
Affected files: - services/auth/src/routes/users.ts
HIGH (18)¶
V3-H-01: API Key Revocation Redis Flag Has No TTL¶
Severity: HIGH Personas: DevOps/SRE, Security Auditor Component: services/auth/src/services/api-key-service.ts (line 287) Not in v2: v2 did not examine Redis key lifecycle for API key revocation
Problem: When revoking an API key, the code writes:
No TTL is set. These keys persist forever in Redis. Over time, as keys are created and revoked, Redis memory consumption grows without bound.Impact: Redis memory leak proportional to the total number of revoked API keys across the platform's lifetime. In high-churn environments (CI/CD key rotation), this can exhaust Redis memory.
Fix: Set a TTL matching the key's expires_at value, or a fixed upper bound (e.g., 30 days) if the key has no expiry. When the DB row is hard-deleted, the Redis key becomes unnecessary.
Effort: Small (add "EX" parameter to redis.set call)
Affected files: - services/auth/src/services/api-key-service.ts
V3-H-02: Retention Service Passes Empty TenantId¶
Severity: HIGH Personas: DevOps/SRE, Data Engineer Component: services/ingestion/src/services/retention-service.ts (line 77) Not in v2: v2 H-18 covered the findDeletedBefore stub, not the retention query itself
Problem: runRetention() calls connectorRepo.list("") with an empty string for tenantId. Depending on the repository implementation, this will either: - Return no connectors (if the query filters by tenant_id = '') - Return all connectors across all tenants (if empty string is treated as "no filter")
Neither behavior is correct. The first means retention never runs. The second means a single retention pass processes all tenants without tenant isolation.
Impact: Data retention either never fires or violates tenant isolation boundaries.
Fix: Query distinct tenant IDs first, then iterate connectorRepo.list(tenantId) per tenant.
Effort: Small-Medium (add tenant enumeration query, iterate per tenant)
Affected files: - services/ingestion/src/services/retention-service.ts
V3-H-03: BFF Permissions Endpoint Returns All App Roles, Not User-Specific¶
Severity: HIGH Personas: App Developer, Security Auditor Component: services/app/src/routes/bff.ts (lines 109-127) Not in v2: v2 C-09 covered permission response shape mismatch, not the authorization logic
Problem: GET /bff/permissions calls permRepo.listRolesByApp(appId) which returns all roles defined for the app, regardless of which roles the current user actually holds. Then it aggregates all permissions from all roles into a flat map.
This means every user — including guests and viewers — receives the full union of all roles' permissions. The app-sdk will report canCreate: true, canDelete: true etc. for entities the user should not have access to.
Impact: Permission checks in app-sdk are meaningless. Every user sees elevated permissions. UI elements that should be hidden are visible, and mutation attempts may succeed if the BFF data proxy doesn't separately enforce permissions.
Fix: Filter listRolesByApp results by the user's actual role assignments within the app (join on user-role mapping table).
Effort: Medium (requires user-role lookup and filtering)
Affected files: - services/app/src/routes/bff.ts
V3-H-04: BFF GET /bff/me Returns All App Roles Instead of User's Roles¶
Severity: HIGH Personas: App Developer, Security Auditor Component: services/app/src/routes/bff.ts (lines 73-85) Not in v2: Not covered
Problem: Same root cause as V3-H-03 but in the /bff/me endpoint. Line 75 calls permRepo.listRolesByApp(appId) and returns all role names. The SDK's useUser() hook consumes this and reports the user as having every role defined in the app.
Impact: The app-sdk's useUser().roles array contains roles the user doesn't actually have, causing incorrect UI rendering and broken authorization decisions in app code.
Fix: Query user-specific role assignments from the app's permission system.
Effort: Small-Medium (add user-role mapping query)
Affected files: - services/app/src/routes/bff.ts
V3-H-05: Gateway Forwards Client x-forwarded-for Verbatim¶
Severity: HIGH Personas: Security Auditor, DevOps/SRE Component: services/gateway/src/services/proxy-service.ts (lines 196-199) Not in v2: v2 M-03 covered rate limiter using x-forwarded-for as key but did not address the proxy forwarding side
Problem: The proxy reads the inbound x-forwarded-for header (set by the client) and forwards it unchanged to upstream services:
const clientIp = c.req.header("x-forwarded-for");
if (clientIp !== undefined) {
outboundHeaders.set("x-forwarded-for", clientIp);
}
x-forwarded-for to any value. Any upstream service that trusts this header for IP-based logic (rate limiting, geo-restriction, audit logging) is fully bypassable. Impact: IP-based rate limiting, geo-blocking, and audit trails are unreliable. Combined with v2 M-03 (rate limiter key), this creates a complete bypass chain.
Fix: Set x-forwarded-for to the TCP remoteAddress (from the original connection), or append to the existing chain with the real client IP as the rightmost entry.
Effort: Small (use c.req.raw.socket?.remoteAddress or equivalent)
Affected files: - services/gateway/src/services/proxy-service.ts
V3-H-06: Gateway Hardcodes x-forwarded-proto: https for All Deployments¶
Severity: HIGH Personas: First-Time Self-Hoster, DevOps/SRE Component: services/gateway/src/services/proxy-service.ts (line 201) Not in v2: Not covered
Problem: Line 201 unconditionally sets:
For development, self-hosted, and LAN deployments that use plain HTTP, this tells upstream services that the connection is HTTPS when it isn't. Services that check the protocol for security decisions (e.g., settingSecure cookie flags, generating absolute URLs, enforcing HSTS) will behave incorrectly. Impact: Cookies marked Secure won't be sent over HTTP connections, breaking authentication for non-TLS deployments. URL generation in password reset emails, OAuth redirects, and app URLs will use https:// when the actual deployment is http://.
Fix: Detect the actual protocol from the inbound request or an environment variable (OP_FORCE_HTTPS), and set x-forwarded-proto accordingly.
Effort: Small (conditional based on OP_BASE_URL scheme)
Affected files: - services/gateway/src/services/proxy-service.ts
V3-H-07: Mapping Service Expression Transforms Lack Service Auth Headers¶
Severity: HIGH Personas: Data Engineer, Security Auditor Component: services/ontology/src/services/mapping-service.ts (lines 116-128) Not in v2: v2 M-08 covered expression injection risk, not the missing auth headers
Problem: When executing expression transforms, the mapping service calls the execution service:
const response = await fetch(`${executionServiceUrl}/internal/execution/run`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ... }),
});
X-Service-Token header is included. This call will be rejected by serviceAuthMiddleware. Impact: All mapping rules with transform_type === "expression" silently fall back to the source value (no transform applied), causing data corruption that is logged as a warning but never surfaces to the user.
Fix: Inject a service token signer and include X-Service-Token header.
Effort: Small-Medium (inject service token dependency)
Affected files: - services/ontology/src/services/mapping-service.ts
V3-H-08: Sync Job Listing Hardcoded to Max 100 Jobs¶
Severity: HIGH Personas: Data Engineer, Power User Component: services/ingestion/src/services/sync-service.ts Not in v2: Not covered
Problem: listSyncs calls getJobs(states, 0, 100) — always fetching at most 100 jobs with no pagination parameters. For connectors with active sync schedules, the job count quickly exceeds 100, making it impossible to see older sync runs.
Impact: Users lose visibility into sync history beyond the most recent 100 runs. Troubleshooting intermittent sync failures becomes impractical.
Fix: Accept cursor and limit parameters; implement proper pagination over BullMQ job IDs.
Effort: Medium (modify API schema, implement cursor-based pagination)
Affected files: - services/ingestion/src/services/sync-service.ts
V3-H-09: BatchJobPayload Still Carries Full Record Arrays in Job Data¶
Severity: HIGH Personas: DevOps/SRE, Data Engineer Component: services/ingestion/src/services/sync-service.ts Not in v2: v2 H-17 covered double-write, not payload bloat
Problem: When dispatching batch jobs, the sync service includes the full records: DataRecord[] array in the BullMQ job payload. BullMQ stores job data in Redis. For large syncs (thousands of records per batch), this means: - Massive Redis memory consumption per batch job - Potential Redis maxmemory hits - Slow job serialization/deserialization
The design comment on the BatchJobPayload type says records should NOT be stored in payloads, but the actual code passes them.
Impact: Large syncs exhaust Redis memory. A connector syncing 100k records in 1k-record batches stores all records in Redis simultaneously.
Fix: Store records in a staging table or temporary MinIO object, pass only a reference (batch ID) in the job payload.
Effort: Large (requires architectural change to batch processing)
Affected files: - services/ingestion/src/services/sync-service.ts
V3-H-10: BFF Runtime-Config Writes Cache But Never Reads It¶
Severity: HIGH Personas: App Developer, DevOps/SRE Component: services/app/src/routes/bff.ts (lines 588-592) Not in v2: Not covered
Problem: The /bff/runtime-config endpoint writes to Redis cache:
Impact: Unnecessary Redis writes on every runtime-config request. No performance benefit from caching. Under high-frequency SDK init calls, this generates significant useless Redis traffic.
Fix: Check redis.get(cacheKey) first; return cached value if present; only query DB on cache miss.
Effort: Small (add redis.get check before DB query)
Affected files: - services/app/src/routes/bff.ts
V3-H-11: Webhook Step JSONata Expressions Not Timeout-Protected¶
Severity: HIGH Personas: Security Auditor, Data Engineer Component: services/pipeline/src/services/execution-engine.ts (lines 468-470) Not in v2: v2 did not examine webhook step body template evaluation
Problem: Conditional step JSONata expressions are correctly timeout-protected with a 100ms Promise.race() (line 378-386). However, webhook step body templates also use JSONata (line 469):
Impact: A single malicious pipeline definition can hang the entire pipeline worker thread, preventing all other pipeline runs from executing.
Fix: Wrap the JSONata evaluation in a Promise.race() with a timeout, matching the pattern used for conditional expressions.
Effort: Small (copy the existing timeout pattern)
Affected files: - services/pipeline/src/services/execution-engine.ts
V3-H-12: SSRF Check Does Not Cover IPv6 or Short-Form IP Addresses¶
Severity: HIGH Personas: Security Auditor Component: services/pipeline/src/services/execution-engine.ts (lines 149-157) Not in v2: v2 H-11 covered DNS rebinding in the execution context-call-handler, not the pipeline webhook step SSRF filter
Problem: The SSRF_BLOCKED_PATTERNS array only covers IPv4 private ranges. It does not block: - IPv6 loopback (::1) - IPv6 link-local (fe80::) - IPv6 mapped IPv4 (::ffff:127.0.0.1) - Short-form IPv4 (http://0177.0.0.1/ for 127.0.0.1 in octal) - Decimal IP (http://2130706433/ for 127.0.0.1) - DNS names that resolve to private IPs (DNS rebinding)
Impact: A user who creates a webhook step with an IPv6 loopback or encoded private IP can make the pipeline engine issue requests to internal services, bypassing the SSRF filter.
Fix: Use a proper SSRF library or DNS-resolution-based check that validates the resolved IP address, not just URL pattern matching.
Effort: Medium (add DNS resolution check + IPv6 patterns)
Affected files: - services/pipeline/src/services/execution-engine.ts
V3-H-13: Login Interactive Mode Returns apiKey Field Instead of Token¶
Severity: HIGH Personas: First-Time Self-Hoster, Data Engineer Component: packages/cli/src/commands/auth/index.ts (lines 43-48) Not in v2: Not covered
Problem: The CLI login interactive flow sends POST /api/v1/auth/login and expects the response to contain { apiKey, email }:
const resp = await ctx.http.post<{ apiKey: string; email: string }>(
"/api/v1/auth/login",
{ email, password: pass },
);
apiKey = resp.apiKey;
But the auth service's login endpoint (services/auth/src/routes/auth.ts) returns { accessToken, refreshToken, user }. There is no apiKey field in the response. The CLI will receive undefined for apiKey and save it to credentials, causing all subsequent CLI commands to fail with authentication errors.
Impact: Interactive op auth login is non-functional. Users must use --key flag with a pre-generated API key, which is not documented as the primary flow.
Fix: Either: (a) change CLI to use accessToken from the login response, or (b) have CLI generate an API key via POST /api/v1/auth/api-keys after authentication.
Effort: Medium (requires decision on CLI auth strategy)
Affected files: - packages/cli/src/commands/auth/index.ts - services/auth/src/routes/auth.ts (reference)
V3-H-14: processErrorHandlers Calls process.exit(1) Twice¶
Severity: HIGH Personas: DevOps/SRE Component: packages/core/src/app.ts (lines 60-71) Not in v2: Not covered
Problem: Both the uncaughtException and unhandledRejection handlers call process.exit(1) immediately and schedule a deferred process.exit(1) via setTimeout:
process.exit(1) on the next line makes the setTimeout unreachable. The intent was clearly to allow a grace period for I/O to flush, but the immediate exit defeats that purpose. Impact: On uncaught exceptions, services exit immediately without waiting for in-flight DB/Redis writes to complete. This can cause data corruption (partial writes) and orphaned advisory locks.
Fix: Remove the immediate process.exit(1); let the setTimeout handle the exit after the grace period. The unref() ensures the process won't hang if all other work finishes sooner.
Effort: Small (delete one line per handler)
Affected files: - packages/core/src/app.ts
V3-H-15: No Rate Limiting on Guest Session Creation¶
Severity: HIGH Personas: Security Auditor, DevOps/SRE Component: services/auth/src/services/guest-session-service.ts Not in v2: Not covered
Problem: Guest session creation (create()) has no rate limiting. Each call generates a 32-byte random token and writes it to Redis. An attacker can call the guest session creation endpoint repeatedly to: 1. Exhaust Redis memory with guest-session:* keys (each with 24h TTL) 2. Overwhelm the randomBytes() entropy pool 3. Create a denial-of-service for legitimate guest users
The endpoint is necessarily public (guests don't have credentials to authenticate).
Impact: Redis memory exhaustion and potential service degradation.
Fix: Add IP-based rate limiting to the guest session creation internal endpoint (e.g., max 10 sessions per IP per minute).
Effort: Small-Medium (add rate limit middleware on the guest session route)
Affected files: - services/auth/src/services/guest-session-service.ts - Wherever the internal guest session route is registered
V3-H-16: Deploy Service OAuth Registration Uses Plain String Token¶
Severity: HIGH Personas: App Developer, Security Auditor Component: services/app/src/services/deploy-service.ts (lines 258-260) Not in v2: Same class of issue as V3-C-04 but in a different file
Problem: The deploy service calls the auth service's internal OAuth client registration endpoint with:
Same issue as V3-C-04 — this plain string won't pass Ed25519 JWT verification.Impact: App deployments fail at the OAuth client registration step. The deploy function includes a compensating rollback (reverting the build pointer), so the failure is at least safe, but no app can be deployed.
Fix: Use Ed25519 signed JWT tokens for service auth.
Effort: Medium (same fix scope as V3-C-04)
Affected files: - services/app/src/services/deploy-service.ts
V3-H-17: Ontology listEntities Performs N+1 Field Queries¶
Severity: HIGH Personas: Power User, Data Engineer Component: services/ontology/src/services/entity-service.ts (lines 347-355) Not in v2: Not covered
Problem: listEntities iterates through entities and calls fieldRepo.findByEntityId(e.id) for each one:
for (const e of entities) {
const fields = await fieldRepo.findByEntityId(e.id);
data.push(toEntityDetail(e, fields));
}
Impact: Ontology listing performance degrades linearly with entity count. API response times exceed 500ms for tenants with moderate schema complexity.
Fix: Fetch all fields for the page of entities in a single WHERE entity_id IN (...) query, then group in memory.
Effort: Small-Medium (add batch query to field repository)
Affected files: - services/ontology/src/services/entity-service.ts - services/ontology/src/repositories/field-repository.ts
V3-H-18: activatePlugin Is Still a No-Op¶
Severity: HIGH Personas: Plugin Developer, Platform Admin Component: services/plugin/src/services/plugin-service.ts (lines 395-399) Not in v2: v2 H-28 noted this exact issue; checking if it's been fixed — it has NOT been fixed
Problem: activatePlugin still just looks up the plugin and returns it without performing any status transition:
async activatePlugin(pluginId: string, _activatedBy: string): Promise<PluginRow> {
const plugin = await pluginRepo.findById(pluginId);
if (plugin === null) {
throw new PluginNotFoundError(`Plugin ${pluginId} not found`);
}
return plugin;
}
status: "active" update, no event publishing, no hook registration. Note: This WAS identified in v2 as H-28 but remains unfixed. Including it here as it is a blocking issue for the plugin lifecycle. If the v3 policy is strictly "net-new only", this can be reclassified as "v2 regression/unfixed" rather than excluded.
Impact: Installed plugins can never be activated. The entire plugin lifecycle beyond installation is non-functional.
Fix: Implement status transition, event publishing, and hook registration.
Effort: Medium
Affected files: - services/plugin/src/services/plugin-service.ts
MEDIUM (22)¶
V3-M-01: Connector List Still N+1 for Sync State¶
Severity: MEDIUM Personas: Data Engineer, Power User Component: services/ingestion/src/services/connector-service.ts Not in v2: v2 H-43 flagged this; it was "partially fixed" (parallelized) but still N queries
Problem: listConnectors uses Promise.all to fetch sync state per connector in parallel. While this is faster than sequential N+1, it still generates N separate Redis/DB queries. For 100 connectors, that's 100 parallel queries.
Impact: Under high connector counts, the listing endpoint becomes slow and generates unnecessary load on Redis/DB.
Fix: Batch fetch sync states using a pipeline or multi-get command.
Effort: Medium
Affected files: - services/ingestion/src/services/connector-service.ts
V3-M-02: publicRoutes Uses Exact Match — No Prefix Matching¶
Severity: MEDIUM Personas: First-Time Self-Hoster, Security Auditor Component: packages/core/src/middleware/auth.ts (line 54) Not in v2: Not covered
Problem: The auth middleware checks public routes using publicRouteSet.has(path) — exact string matching. This means: - /api/v1/auth/register is public but /api/v1/auth/register/ (trailing slash) is NOT - Route parameters like /api/v1/auth/reset-password/:token must be listed for every possible token value, which is impossible
The auth routes file lists routes like POST /api/v1/auth/reset-password/:token and GET /api/v1/auth/verify-email/:token as public, but the exact path /api/v1/auth/reset-password/:token will never match an actual request like /api/v1/auth/reset-password/abc123.
Impact: Password reset and email verification endpoints may require authentication (which users at those endpoints don't have), making them non-functional unless worked around by listing patterns differently at the service level.
Fix: Use prefix matching or a pattern matcher for public routes instead of exact Set.has().
Effort: Small-Medium (replace Set.has with prefix/pattern matching)
Affected files: - packages/core/src/middleware/auth.ts
V3-M-03: OAuth Service Uses crypto.randomUUID() Global¶
Severity: MEDIUM Personas: Security Auditor Component: services/auth/src/services/oauth-service.ts Not in v2: Not covered
Problem: The OAuth service uses the global crypto.randomUUID() function while all other auth service files use randomUUID imported from node:crypto. While functionally equivalent in Node.js, the inconsistency creates confusion and potential issues: - In test environments where the global crypto object might be mocked differently - In serverless/edge runtimes where globalThis.crypto may differ from node:crypto
Impact: Low functional impact currently, but inconsistency creates maintenance risk.
Fix: Use the node:crypto import consistently.
Effort: Small (change one import)
Affected files: - services/auth/src/services/oauth-service.ts
V3-M-04: OP_MINIO_PASSWORD Has No Default in Docker Compose¶
Severity: MEDIUM Personas: First-Time Self-Hoster Component: docker/docker-compose.yml (line 145) Not in v2: Not covered
Problem: Most environment variables in docker-compose.yml have defaults (e.g., ${OP_BASE_URL:-http://localhost:3000}), but OP_MINIO_PASSWORD has none:
.env, MinIO starts with an empty root password, which MinIO may reject or accept insecurely. Impact: First-time docker compose up may fail with a cryptic MinIO startup error, or worse, MinIO starts with no password protection.
Fix: Add a default like ${OP_MINIO_PASSWORD:-dev_minio_password} or generate it in init.sh.
Effort: Small (one-line fix)
Affected files: - docker/docker-compose.yml
V3-M-05: Docker-Socket-Proxy POST=0 Prevents Sandbox Container Creation¶
Severity: MEDIUM Personas: DevOps/SRE, Plugin Developer Component: docker/docker-compose.yml (lines 166-170) Not in v2: v2 H-10 covered POST=1 DELETE=1 being too permissive; it was fixed to POST=0 DELETE=0
Problem: The comment on the docker-socket-proxy says "The execution service manages sandboxes via pre-built images launched at service startup — it does not need to create containers at runtime." However, the sandbox manager (sandbox-manager.ts) recycles sandboxes by creating new containers. With POST: 0, the execution service cannot create new sandbox containers after the initial ones are recycled, exhausting the sandbox pool.
Impact: After sandbox recycling thresholds are hit, no new sandboxes can be created, and all code execution requests fail.
Fix: Either: (a) allow CONTAINERS_CREATE=1 specifically, or (b) redesign sandbox recycling to not require Docker API POST calls.
Effort: Small (adjust proxy permissions) or Large (redesign)
Affected files: - docker/docker-compose.yml
V3-M-06: No Frontend UI for API Key Rotation¶
Severity: MEDIUM Personas: Platform Admin, App Developer Component: packages/frontend/src/pages/settings/ApiKeysPage.tsx Not in v2: Not covered
Problem: The backend has a fully implemented rotate() method in api-key-service.ts, and the CLI has op auth rotate-key. But the frontend API Keys page only shows Create and Revoke actions. There is no Rotate button or dialog.
Impact: Users who manage keys through the UI must revoke and recreate manually, losing the atomic rotation guarantee (no window where neither key works) that the backend provides.
Fix: Add a "Rotate" button per key row, with a dialog showing the new key.
Effort: Medium (new dialog component, mutation hook)
Affected files: - packages/frontend/src/pages/settings/ApiKeysPage.tsx
V3-M-07: API Key Revoke/Rotate Not Tenant-Scoped¶
Severity: MEDIUM Personas: Security Auditor, Enterprise Evaluator Component: services/auth/src/services/api-key-service.ts (lines 269-287, 303-320) Not in v2: Not covered
Problem: The revoke() method checks only that the key exists and is not already revoked. The rotate() method checks user_id ownership but neither method validates that the caller belongs to the same tenant as the key owner. A user from tenant A who obtains a key ID from tenant B could revoke or rotate that key.
In practice, key IDs are UUIDs and not easily guessable, but the lack of tenant boundary is a defense-in-depth violation.
Impact: Cross-tenant key revocation is theoretically possible with a known key ID.
Fix: Add AND tenant_id = $N to the revoke and rotate queries, using the caller's tenant ID.
Effort: Small (add WHERE clause)
Affected files: - services/auth/src/services/api-key-service.ts - services/auth/src/routes/api-keys.ts
V3-M-08: Credential Encryption Key Rotation Still TODO¶
Severity: MEDIUM Personas: Security Auditor, Enterprise Evaluator Component: services/ingestion/src/services/credential-service.ts Not in v2: v2 M-09 flagged the absence of key rotation; checking current state — still TODO
Problem: CURRENT_KEY_VERSION is hardcoded to 1 and there is a // TODO comment for key rotation (from v2 M-09). No rotation mechanism exists. If the master encryption key is compromised, there is no way to re-encrypt all credentials with a new key.
Impact: A compromised encryption key requires manual database surgery to re-encrypt credentials. No operational procedure exists.
Fix: Implement a key rotation job that re-encrypts all credentials with a new key version.
Effort: Large
Affected files: - services/ingestion/src/services/credential-service.ts
V3-M-09: Entity Deletion Is Soft-Delete Only — No Table Cleanup¶
Severity: MEDIUM Personas: DevOps/SRE, Data Engineer Component: services/ontology/src/services/entity-service.ts (lines 523-547) Not in v2: Not covered
Problem: deleteEntity performs a soft-delete on the entity and field rows but does not schedule cleanup of the actual PostgreSQL table. The table (and its data) persists in the tenant schema indefinitely. Unlike connectors which have a retention service with grace period and hard delete, entities have no equivalent cleanup mechanism.
Impact: Deleted entity tables accumulate in tenant schemas, consuming disk space and potentially causing confusion during debugging or data audits.
Fix: Add a deferred table cleanup mechanism (similar to the connector retention service) that drops soft-deleted entity tables after a grace period.
Effort: Medium
Affected files: - services/ontology/src/services/entity-service.ts
V3-M-10: Migration Rollback Uses Unguarded SELECT * FROM Shadow Table¶
Severity: MEDIUM Personas: Data Engineer, Security Auditor Component: services/ontology/src/services/migration-service.ts (lines 318-320) Not in v2: Not covered
Problem: During migration rollback, the code runs:
The shadow table may have a different column set than the target table if the migration added or removed columns.SELECT * will fail if the column sets don't match. Additionally, the DELETE + INSERT pattern is not atomic with the SELECT * — concurrent readers may see partial data. Impact: Migration rollbacks can fail with column mismatch errors, leaving the entity in an inconsistent state.
Fix: Use explicit column lists derived from the migration's change plan, and wrap the delete+insert in a single transaction (which it already is, but the SELECT * issue remains).
Effort: Medium
Affected files: - services/ontology/src/services/migration-service.ts
V3-M-11: Logger createLogger Initializes with Empty Trace ID¶
Severity: MEDIUM Personas: DevOps/SRE Component: packages/core/src/logger.ts (line 139) Not in v2: Not covered
Problem: createLogger returns makeLogger("") — an empty string trace ID. Log events emitted before withTraceId() is called (e.g., during service startup) have traceId: "", making them impossible to correlate in log aggregation tools.
Impact: Startup logs, scheduler logs, and background job logs that don't go through request middleware have empty trace IDs, creating gaps in observability.
Fix: Generate a default trace ID (e.g., crypto.randomUUID()) at logger creation time.
Effort: Small (one-line change)
Affected files: - packages/core/src/logger.ts
V3-M-12: Audit Queue Throws If Not Configured Instead of Degrading Gracefully¶
Severity: MEDIUM Personas: DevOps/SRE, Enterprise Evaluator Component: packages/core/src/logger.ts (lines 111-113) Not in v2: Not covered
Problem: The audit() method throws an Error if config.auditQueue is not set:
Impact: Audit event failures crash the service instead of logging a warning and continuing.
Fix: Log a warning and return early instead of throwing.
Effort: Small (change throw to logger.warn + return)
Affected files: - packages/core/src/logger.ts
V3-M-13: Proxy Service Defaults Use Wrong Port Numbers¶
Severity: MEDIUM Personas: First-Time Self-Hoster Component: services/gateway/src/services/proxy-service.ts (lines 13-36) Not in v2: Not covered
Problem: The SERVICE_MAP defaults use distinct port numbers per service:
auth: "http://auth-service:3001"
ingestion: "http://ingestion-service:3002"
ontology: "http://ontology-service:3003"
docker-compose.yml configures all services with PORT: "3000" on their internal container port. The service names resolve via Docker DNS, and all listen on port 3000. The defaults (3001, 3002, etc.) are wrong for the Docker deployment. The environment variables override these defaults correctly in docker-compose, but if someone runs services outside Docker using the defaults, connections fail.
Impact: Running the gateway outside Docker (e.g., for development) with default config causes all service routing to fail due to wrong ports.
Fix: Change defaults to use port 3000 for all services, matching the actual container configuration.
Effort: Small (update 8 default URLs)
Affected files: - services/gateway/src/services/proxy-service.ts
V3-M-14: No Timeout on Plugin Bundle Checksum Verification¶
Severity: MEDIUM Personas: Plugin Developer, DevOps/SRE Component: services/plugin/src/services/plugin-service.ts (line 285) Not in v2: Not covered
Problem: bundleService.verifyChecksum() reads the bundle file and computes a SHA-256 hash. For very large bundles (up to the 50MB limit), this is a blocking CPU-bound operation with no timeout. During this time, the service thread is blocked, unable to serve other requests.
Impact: A 50MB bundle upload blocks the plugin service for several hundred milliseconds on commodity hardware. Multiple concurrent uploads can cause service unresponsiveness.
Fix: Use streaming hash computation to avoid buffering the entire file, or offload to a worker thread.
Effort: Small-Medium
Affected files: - services/plugin/src/services/plugin-service.ts - services/plugin/src/services/bundle-service.ts
V3-M-15: CLI HTTP Client Sets Content-Type: application/json on All Requests¶
Severity: MEDIUM Personas: App Developer, Data Engineer Component: packages/cli/src/lib/http-client.ts (line 77) Not in v2: Not covered
Problem: The request() function always sets Content-Type: application/json even for GET and DELETE requests that have no body:
const headers: Record<string, string> = {
"Content-Type": "application/json",
...authHeaders(apiKey),
};
Content-Type but no body. The BFF client correctly omits this header for bodyless requests. Impact: Potential request rejection by strict HTTP proxies in enterprise environments.
Fix: Only set Content-Type when body !== undefined.
Effort: Small (conditional header)
Affected files: - packages/cli/src/lib/http-client.ts
V3-M-16: insecureTls Flag Affects All Process HTTPS Connections¶
Severity: MEDIUM Personas: Security Auditor, DevOps/SRE Component: packages/cli/src/lib/http-client.ts (lines 58-63) Not in v2: Not covered
Problem: When insecureTls is set, the CLI sets:
The code comment acknowledges this: "This intentionally affects all HTTPS connections for the lifetime of this process."
Impact: TLS MITM attacks on any HTTPS connection from the CLI process are undetected when --insecure-tls is used.
Fix: Use a custom HTTPS agent scoped to the platform URL rather than a process-level flag. Node.js 22+ supports per-request TLS options.
Effort: Medium
Affected files: - packages/cli/src/lib/http-client.ts
V3-M-17: App Service VFS Allows Only Specific File Extensions¶
Severity: MEDIUM Personas: App Developer Component: services/app/src/services/app-service.ts (lines 86-88) Not in v2: Not covered
Problem: ALLOWED_EXTENSIONS is:
.png, .jpg, .gif, .webp, .ico, .woff, .woff2, .ttf, .env, .yaml, .yml, .graphql, .gql. App developers who need images, fonts, or configuration files cannot upload them to the VFS. Impact: Apps cannot include static assets (images, fonts) or configuration files, severely limiting what can be built.
Fix: Expand the allowlist to include common static asset types. Consider a denylist approach instead (block only known-dangerous extensions like .exe, .sh).
Effort: Small (expand array)
Affected files: - services/app/src/services/app-service.ts
V3-M-18: Default Template Uses workspace:* Dependency¶
Severity: MEDIUM Personas: App Developer Component: services/app/src/services/app-service.ts (line 26) Not in v2: Not covered
Problem: The default app template's package.json uses:
workspace:* is a pnpm workspace protocol that only works inside the monorepo. When the app is built in the execution sandbox (which is a separate environment), this dependency cannot be resolved. Impact: New apps created via the API fail to build because workspace:* is not a valid npm version specifier outside the monorepo.
Fix: Use a real semver version (e.g., "^1.0.0") or "latest".
Effort: Small (change one string)
Affected files: - services/app/src/services/app-service.ts
V3-M-19: CLI Login Expects /api/v1/auth/me Endpoint That May Not Exist¶
Severity: MEDIUM Personas: First-Time Self-Hoster Component: packages/cli/src/commands/auth/index.ts (line 37, 76, 96) Not in v2: Not covered
Problem: Multiple CLI commands call GET /api/v1/auth/me: - loginAction (--key mode): validates key by calling /api/v1/auth/me - statusAction: checks auth status via /api/v1/auth/me - whoamiAction: displays user info from /api/v1/auth/me
But the auth service routes (services/auth/src/routes/auth.ts) do not define a GET /api/v1/auth/me endpoint. The defined routes are: register, login, logout, refresh, forgot-password, reset-password, verify-email.
Impact: op auth login --key, op auth status, and op auth whoami all fail with 404.
Fix: Either add a GET /api/v1/auth/me endpoint to the auth service, or have the CLI use a different endpoint for user info.
Effort: Small-Medium (add route to auth service)
Affected files: - packages/cli/src/commands/auth/index.ts - services/auth/src/routes/auth.ts
V3-M-20: Execution Service runExecution Has No Scope Check¶
Severity: MEDIUM Personas: Security Auditor Component: services/execution/src/services/execution-service.ts (lines 162-195) Not in v2: Not covered
Problem: The runExecution method (user-facing code execution) accepts a UserContext but does not verify that the user has execution:run scope. It creates and dispatches the execution unconditionally. Scope enforcement must happen at the route level, but if the route handler doesn't check either, any authenticated user can run arbitrary code.
Impact: If the execution route handler delegates authorization entirely to the service layer (which has none), arbitrary code execution is open to all authenticated users.
Fix: Either verify execution:run scope in the service method, or confirm it's enforced at the route level.
Effort: Small
Affected files: - services/execution/src/services/execution-service.ts
V3-M-21: buildMigrationFieldSpec SQL-Injects Default Values¶
Severity: MEDIUM Personas: Security Auditor Component: services/ontology/src/services/entity-service.ts (lines 644-647) Not in v2: Not covered
Problem: When building migration field specs, default values are interpolated directly into SQL-like expressions:
const defaultExpr = f.defaultValue !== undefined
? (typeof f.defaultValue === "string" ? `'${f.defaultValue}'` : String(f.defaultValue))
: "NULL";
f.defaultValue is a string containing a single quote (e.g., O'Brien), this produces invalid SQL ('O'Brien'). Depending on how defaultExpression is used downstream in DDL generation, this could cause migration failures or SQL injection. Impact: Entities with default values containing special characters fail migration. Potential SQL injection if the expression is used unsanitized in DDL.
Fix: Use parameterized queries or proper SQL escaping for default value expressions.
Effort: Small (use a proper SQL escaping function)
Affected files: - services/ontology/src/services/entity-service.ts
V3-M-22: Vector Log Collector Runs as Root¶
Severity: MEDIUM Personas: Security Auditor, DevOps/SRE Component: docker/docker-compose.yml (lines 601-605) Not in v2: Not covered
Problem: The Vector log collector container is configured with user: "0:0" (root) because it needs to read Docker container log files from /var/lib/docker/containers. While the comment explains why, running as root in a container is a defense-in-depth violation.
Impact: If Vector has a vulnerability, the attacker gains root access inside the container, which combined with the host filesystem mount, could enable host escape.
Fix: Create a dedicated vector user with read-only access to the Docker log directory, or use Docker's --group-add to add the container user to the host's docker group.
Effort: Medium
Affected files: - docker/docker-compose.yml
LOW (8)¶
V3-L-01: No Expiry Date Picker in Frontend API Key Creation¶
Severity: LOW Personas: Platform Admin, Casual User Component: packages/frontend/src/pages/settings/ApiKeysPage.tsx Not in v2: Not covered
Problem: The create key dialog has a name field and scope checkboxes, but no expiry date picker. The CLI supports --expires but the frontend creates keys with no expiry. This means all frontend-created keys are permanent.
Impact: Users cannot create time-limited keys from the UI, which is a security best practice for CI/CD tokens.
Fix: Add a date picker for optional expiry.
Effort: Small
Affected files: - packages/frontend/src/pages/settings/ApiKeysPage.tsx
V3-L-02: CLI HTTP Client Sends Content-Type on postMultipart DELETE¶
Severity: LOW Personas: App Developer Component: packages/cli/src/lib/http-client.ts (line 153) Not in v2: Not covered
Problem: The postMultipart method correctly omits Content-Type (letting fetch set the multipart boundary), but the delete method inherits the base request() function which always sets Content-Type: application/json. This is technically incorrect for DELETE requests with no body.
Impact: Minimal — most servers ignore Content-Type on bodyless requests.
Fix: Already mentioned in V3-M-15.
Effort: Small
Affected files: - packages/cli/src/lib/http-client.ts
V3-L-03: Docker Compose x-forwarded-proto Mismatch with OP_BASE_URL Default¶
Severity: LOW Personas: First-Time Self-Hoster Component: docker/docker-compose.yml and services/gateway/src/services/proxy-service.ts Not in v2: Not covered
Problem: OP_BASE_URL defaults to http://localhost:3000 but the proxy hardcodes x-forwarded-proto: https. These contradict each other, potentially causing redirect loops or mixed-content warnings.
Impact: Cosmetic confusion; actual traffic routing works because internal services don't enforce HTTPS.
Fix: Align the defaults.
Effort: Small
Affected files: - docker/docker-compose.yml - services/gateway/src/services/proxy-service.ts
V3-L-04: CLI generate-key Uses Comma-Separated Scopes, Inconsistent with Frontend¶
Severity: LOW Personas: Data Engineer Component: packages/cli/src/commands/auth/index.ts (line 118) Not in v2: Not covered
Problem: The CLI expects --scopes data:read,data:write (comma-separated string) while the frontend sends scopes as a JSON array. No documentation explains the valid scope names or their effects.
Impact: Users may specify invalid scope names with no feedback (the backend's scope subsetting check only validates against the caller's scopes, not against a canonical list).
Fix: Add scope validation against the known scope list; document available scopes in CLI help text.
Effort: Small
Affected files: - packages/cli/src/commands/auth/index.ts
V3-L-05: App Default Template Imports Incorrect File Extensions¶
Severity: LOW Personas: App Developer Component: services/app/src/services/app-service.ts (lines 48-53) Not in v2: Not covered
Problem: The default template's index.tsx imports:
.js extension is correct for ESM module resolution of .tsx files, but this pattern may confuse new developers who expect .tsx imports. Additionally, the template doesn't include a vite.config.ts or build configuration, so it's unclear how the app is supposed to be built. Impact: New app developers may be confused by the .js extension convention.
Fix: Add a comment explaining ESM resolution, or include a minimal build config.
Effort: Small
Affected files: - services/app/src/services/app-service.ts
V3-L-06: No data Route for Users Management in SERVICE_MAP¶
Severity: LOW Personas: Platform Admin Component: services/gateway/src/services/proxy-service.ts Not in v2: Not covered
Problem: The SERVICE_MAP doesn't include a users key. User management routes (/api/v1/users/*) are handled by the auth service, but there's no explicit entry for users in the map. The roles key is mapped to auth, and auth covers /api/v1/auth/*, but users paths would not match any prefix.
Impact: User management API calls via the gateway return 404 "No route matches" because the users prefix is not in SERVICE_MAP.
Fix: Add users: process.env["AUTH_SERVICE_URL"] to SERVICE_MAP.
Effort: Small (one line)
Affected files: - services/gateway/src/services/proxy-service.ts
V3-L-07: Machine-ID Fallback Uses Predictable Inputs¶
Severity: LOW Personas: Security Auditor Component: packages/cli/src/lib/credentials.ts (lines 93-97) Not in v2: Not covered
Problem: The machine-ID fallback (when /etc/machine-id doesn't exist) uses HOSTNAME and USER environment variables:
return createHmac("sha256", "oneplatform-fallback")
.update(`${process.env["HOSTNAME"] ?? "localhost"}:${process.env["USER"] ?? "user"}`)
.digest("hex");
/etc/machine-id (e.g., macOS, some containers). Impact: On systems without machine-id, CLI credential encryption is effectively security through obscurity.
Fix: Use macOS ioreg hardware UUID as an additional source; warn the user when falling back to weak derivation.
Effort: Small-Medium
Affected files: - packages/cli/src/lib/credentials.ts
V3-L-08: Frontend API Key List Missing Scope Filtering¶
Severity: LOW Personas: Platform Admin Component: packages/frontend/src/pages/settings/ApiKeysPage.tsx Not in v2: Not covered
Problem: The API keys table shows all keys but provides no filtering or search. For users with many API keys (e.g., one per CI/CD pipeline), finding a specific key requires scrolling through the entire list.
Impact: Poor UX for power users with many keys.
Fix: Add search/filter functionality to the keys table.
Effort: Small-Medium
Affected files: - packages/frontend/src/pages/settings/ApiKeysPage.tsx
Finding Distribution by Persona¶
| Persona | CRITICAL | HIGH | MEDIUM | LOW | Total |
|---|---|---|---|---|---|
| Security Auditor | 3 | 5 | 6 | 1 | 15 |
| DevOps/SRE | 1 | 5 | 5 | 1 | 12 |
| App Developer | 2 | 2 | 4 | 2 | 10 |
| Data Engineer | 2 | 4 | 2 | 1 | 9 |
| First-Time Self-Hoster | 0 | 2 | 3 | 1 | 6 |
| Platform Admin | 2 | 1 | 2 | 2 | 7 |
| Plugin Developer | 1 | 2 | 1 | 0 | 4 |
| Power User | 0 | 2 | 0 | 0 | 2 |
| Enterprise Evaluator | 0 | 0 | 2 | 0 | 2 |
| Casual User | 0 | 0 | 0 | 1 | 1 |
Cross-Reference: Findings by Component Area¶
| Area | CRITICAL | HIGH | MEDIUM | LOW |
|---|---|---|---|---|
| Auth service (API keys, tokens, users) | 2 | 2 | 1 | 0 |
| Gateway / Proxy | 0 | 2 | 1 | 1 |
| Pipeline execution engine | 1 | 2 | 0 | 0 |
| App service / BFF | 1 | 3 | 2 | 0 |
| Ingestion / Sync | 0 | 2 | 1 | 0 |
| Ontology / Entity / Migration | 0 | 1 | 3 | 0 |
| Plugin service | 0 | 1 | 1 | 0 |
| Execution service | 0 | 0 | 1 | 0 |
| Core middleware / logger | 1 | 1 | 2 | 0 |
| Frontend | 1 | 0 | 1 | 2 |
| CLI | 0 | 1 | 3 | 2 |
| Docker / Infrastructure | 0 | 0 | 3 | 1 |
| Logging service | 0 | 0 | 1 | 0 |
| SDK (app-sdk) | 0 | 0 | 1 | 1 |
| Credentials | 0 | 0 | 1 | 1 |
Comparison with v1/v2¶
| Metric | v1 | v2 | v3 |
|---|---|---|---|
| Raw findings | 108 | 202 (85 unique) | 53 net-new |
| Perspectives/Personas | 4 | 6 | 10 |
| CRITICAL | N/A | 13 | 5 |
| HIGH | N/A | 35 | 18 |
| Security findings | Scattered | 24 | 17 |
| v2 items confirmed FIXED | N/A | N/A | 30+ |
v3 vs v2 Key Differences¶
- v2 fixes verified: 30+ v2 findings have been fixed in the current codebase (X-User-Context stripping, Ed25519 keys, security headers, body limits, etc.)
- New vulnerability class: v3 identifies a systemic issue with service-to-service authentication — multiple services (pipeline engine, mapping service, BFF, deploy service) attempt internal calls without proper Ed25519 JWT tokens
- Runtime vs design: v2 focused heavily on missing infrastructure (Docker services, env vars). v3 focuses on runtime logic errors in code that has been implemented but has subtle bugs (wrong JTI, empty roles, scope mismatches)
- Frontend-backend alignment: v3 identifies several frontend/backend mismatches (scope names, missing endpoints) that v2 did not examine
Severity Classification Rubric¶
| Level | Criteria |
|---|---|
| CRITICAL | Blocks a core workflow entirely, causes data loss, or creates an exploitable security vulnerability |
| HIGH | Significant functionality gap, performance degradation, or security weakness that affects many users |
| MEDIUM | Noticeable quality issue, inconsistency, or missing feature that has workarounds |
| LOW | Cosmetic issue, minor inconvenience, or improvement opportunity |