Skip to content

feat: AnswerAI internal provisioning endpoint for Alpha Agent cross-provisioning#975

Open
bradtaylorsf wants to merge 2 commits into
stagingfrom
feature/cross-provision-answerai-endpoint
Open

feat: AnswerAI internal provisioning endpoint for Alpha Agent cross-provisioning#975
bradtaylorsf wants to merge 2 commits into
stagingfrom
feature/cross-provision-answerai-endpoint

Conversation

@bradtaylorsf
Copy link
Copy Markdown
Collaborator

Summary

  • Add POST /api/v1/internal/provision-apikey endpoint that allows Alpha Agent to provision AnswerAI API keys for users via Auth0 M2M tokens
  • Endpoint reuses existing verifyAAIToken middleware for JWT validation and the full user/org/workspace creation pipeline
  • Creates an "AlphaAgent" API key in the user's active workspace and returns the plaintext key
  • Whitelists the endpoint in constants.ts so it bypasses the main auth middleware (handles its own auth)

How it works

  1. Alpha Agent's management Lambda gets an Auth0 M2M token scoped to AnswerAI's API audience
  2. Calls POST /api/v1/internal/provision-apikey with { auth0Id, email, name, orgId } and x-request-from: aai header
  3. verifyAAIToken validates the JWT, creates/finds user + org + workspaces (all idempotent)
  4. Endpoint generates a fresh API key, deleting any existing "AlphaAgent" key first
  5. Returns { apiKey, userId, workspaceId } — the plaintext key (only time it's visible)

Files changed

File Change
packages/server/src/aai/routes/internal-provision.ts New — Provisioning endpoint
packages/server/src/routes/index.ts Import + export new router factory
packages/server/src/index.ts Mount route at /api/v1/internal
packages/server/src/utils/constants.ts Whitelist the endpoint

Related PRs

  • Alpha Agent: last-rev-llc/alphaagent#feature/cross-provision-answerai — M2M token helper + provisioning call
  • Alpha Claw: last-rev-llc/alphaclaw#feature/cross-provision-answerai — Skill + integration manifest

Manual setup required (Auth0)

  1. Create/configure M2M Application with provision:apikey scope for the AnswerAI API audience
  2. Ensure Alpha Agent's Auth0 org_id is in the AUTH0_ORGANIZATION_ID allowlist

Test plan

  • Build compiles: pnpm --filter flowise build
  • Test endpoint locally with a valid JWT token
  • Verify returned API key works for chatflow listing
  • Verify idempotent behavior (second call replaces key)
  • Verify 401 for invalid/missing token

🤖 Generated with Claude Code

…ioning

Add POST /api/v1/internal/provision-apikey endpoint that allows Alpha Agent
to provision AnswerAI API keys for users via Auth0 M2M tokens. The endpoint
reuses existing user/org/workspace creation pipeline via verifyAAIToken
middleware and returns a plaintext API key for storage in the user's container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 21, 2026

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

Project Deployment Actions Updated (UTC)
answerai-docs Ready Ready Preview Feb 21, 2026 6:34pm
the-answerai Ready Ready Preview Feb 21, 2026 6:34pm

Request Review

@claude
Copy link
Copy Markdown

claude Bot commented Feb 21, 2026

PR Review: feat: AnswerAI internal provisioning endpoint for Alpha Agent cross-provisioning

Summary: This PR adds a POST /api/v1/internal/provision-apikey endpoint that allows an external service (Alpha Agent) to provision API keys for users using an Auth0 M2M JWT. The implementation is generally sound - it correctly reuses the existing verifyAAIToken middleware and the idempotent user/org/workspace creation pipeline. However, there are several issues ranging from a schema-breaking bug to security and maintainability concerns that should be addressed before merge.


Critical Issues

1. UUID stored in a varchar(20) primary key column — data truncation / unique constraint violation

Location: packages/server/src/aai/routes/internal-provision.ts:80

The ApiKey entity declares its primary key as:

@PrimaryColumn({ type: 'varchar', length: 20 })
id: string

The new endpoint assigns a full UUID (36 characters) to that field:

newKey.id = uuidv4()  // e.g. "550e8400-e29b-41d4-a716-446655440000" — 36 chars

At the database level this will either silently truncate the id to 20 characters (SQLite) or raise a column-length error (PostgreSQL strict mode). Truncation would produce identical ids for different keys that share the same first 20 characters, causing unique-constraint failures on subsequent calls and silently corrupting data.

The existing createApiKey service in packages/server/src/services/apikey/index.ts (line 83) uses the same uuidv4() call, so either the column was recently widened (and the entity definition needs updating), or the service has the same latent bug. Regardless, this PR should not introduce additional callers until the id format is clarified.

Recommendation: Use the same createApiKey service function that already exists (packages/server/src/services/apikey/index.ts) rather than duplicating the key-creation logic inline. This both avoids the id-type question and keeps the logic DRY.


2. Endpoint is fully unauthenticated at the network/whitelist level — verifyAAIToken is the only gate

Location: packages/server/src/utils/constants.ts (new whitelist entry) and packages/server/src/aai/routes/internal-provision.ts:43

Adding /api/v1/internal/provision-apikey to WHITELIST_URLS bypasses every authentication mechanism in the main middleware stack. The only protection is the verifyAAIToken middleware applied on the route handler itself.

That is acceptable as long as verifyAAIToken is guaranteed to run before the business logic — and it does, since it is the first argument to router.post(...). However, there are two risks worth calling out:

  • If someone later refactors the route registration and accidentally drops the middleware, the endpoint becomes completely open. The PR description acknowledges this, but there is no network-layer protection (IP allowlist, VPC restriction, etc.) as a defence-in-depth measure.
  • The verifyAAIToken middleware has a fallback path to the enterprise HS256 passport (verifyToken). That means any valid enterprise JWT — not just an M2M token — can reach this endpoint. If the intent is to restrict access strictly to the Alpha Agent M2M application, the JWT azp (authorized party) claim or a custom scope (provision:apikey) should be validated explicitly.

Recommendation: Add an explicit check for the provision:apikey scope (which the PR description mentions must be configured in Auth0) inside the route handler:

const authPayload = (req as any).auth?.payload
if (!authPayload?.scope?.includes('provision:apikey')) {
    return res.status(403).json({ error: 'Forbidden: missing provision:apikey scope' })
}

Major Concerns

3. no-console ESLint rule suppressed file-wide

Location: packages/server/src/aai/routes/internal-provision.ts:1

/* eslint-disable no-console */

The file contains eight console.log / console.error calls. The project-wide ESLint rule exists for a reason — unstructured console output in production makes log aggregation and filtering harder. The existing AAI routes (e.g., auth-me.ts) also use console.log, so this is a pre-existing pattern, but adding a new file that disables the rule wholesale perpetuates the problem.

Recommendation: Use the project logger (if one exists) or at minimum scope the disable comment to individual lines rather than suppressing the rule for the entire file.


4. Error handling does not use InternalFlowiseError

Location: packages/server/src/aai/routes/internal-provision.ts:96-98

} catch (error: any) {
    console.error('[internal/provision-apikey] Error:', error.message)
    return res.status(500).json({ error: 'Internal Server Error' })
}

The project standard (documented in CLAUDE.md and packages/server/CLAUDE.md) requires using InternalFlowiseError and the central error handler middleware (errorHandlerMiddleware). The current approach returns a raw res.status(500) which:

  • Bypasses the central error handler (no structured logging, no Sentry/telemetry capture)
  • Returns a different error shape from the rest of the API
  • Leaks nothing useful for debugging (swallows the stack trace)

The auth-me.ts route in the same directory has the same pattern, so this is consistent with the nearby code — but it remains a divergence from the project standard and should not be expanded.

Recommendation: Use next(error) and let errorHandlerMiddleware handle it, or at minimum wrap errors using InternalFlowiseError.


5. activeOrganizationId fallback may mask a misconfigured user object

Location: packages/server/src/aai/routes/internal-provision.ts:58

const organizationId = user.activeOrganizationId || user.organizationId

If verifyAAIToken succeeded, both fields should be populated (the middleware sets both). The fallback to user.organizationId silently proceeds even if activeOrganizationId is empty, which could result in a key being created with an incorrect organizationId. A missing activeOrganizationId likely indicates a workspace-population failure that should be surfaced as an error, not silently fallen back from.


6. Deletion of existing keys is not atomic with creation

Location: packages/server/src/aai/routes/internal-provision.ts:62-76

The endpoint deletes existing AlphaAgent keys and then creates a new one in two separate database operations with no transaction. If the process crashes or is interrupted between delete and insert, the user ends up with no AlphaAgent key at all. The Alpha Agent Lambda would then receive an error on its next provisioning call and be unable to authenticate.

Recommendation: Wrap the delete and insert in a TypeORM QueryRunner transaction, or insert first and then delete (so there is always at least one valid key).


Minor Issues and Suggestions

7. Request body fields (auth0Id, email, name, orgId) are accepted but never used

Location: packages/server/src/aai/routes/internal-provision.ts — the entire handler

The PR description states the body contains { auth0Id, email, name, orgId }, and verifyAAIToken already extracts all of this from the JWT claims. The body fields are silently ignored. This creates confusion: callers think they must send a body, but the endpoint doesn't validate or use it. Either document that the body is ignored and remove it from the API contract, or add explicit body validation if those fields are intended for future use.


8. The ApiKey entity id field type inconsistency warrants a follow-up task

As noted in issue 1, the @PrimaryColumn({ type: 'varchar', length: 20 }) declaration is inconsistent with the uuidv4() calls throughout the codebase (both old and new). This should be tracked as a separate issue regardless of whether this PR is merged.


9. No tests included

The test plan in the PR description is entirely manual. For a security-sensitive endpoint that provisions API keys using M2M tokens, automated tests would significantly reduce regression risk. Consider adding:

  • A unit test that mocks verifyAAIToken and the ApiKey repository to verify the happy path returns { apiKey, userId, workspaceId }
  • A test that verifies a 401 is returned when the middleware rejects the token
  • A test that verifies idempotent behavior (delete + recreate)

Positive Observations

  • The decision to reuse verifyAAIToken rather than implementing a separate JWT verification path is the right call — it keeps the Auth0 RS256 validation, org allowlist check, and user/org/workspace sync in a single well-tested location.
  • The idempotent design (delete existing key, create fresh one) correctly handles the constraint that hashed secrets cannot be recovered, and is clearly documented in the code comments.
  • Whitelisting at the constants level and self-handling auth in the route handler is consistent with how auth-me.ts and credentials-refresh.ts work, so the overall approach fits the existing pattern.
  • The PR description is thorough and clearly explains the flow, related PRs, and manual Auth0 setup steps required.

Next Steps

  1. Must fix before merge: Resolve the varchar(20) vs UUID id mismatch (issue 1). The safest fix is to call the existing createApiKey service rather than duplicating creation logic.
  2. Should fix before merge: Add the provision:apikey scope check (issue 2) to harden the auth boundary.
  3. Should fix before merge: Wrap delete + insert in a transaction (issue 6).
  4. Follow-up tasks: Add automated tests, address the console.log pattern, align error handling with InternalFlowiseError.

🤖 Reviewed with Claude Code

M2M tokens from client_credentials grant don't carry email/org_id claims,
so verifyAAIToken would 401. Switch to direct jwtCheck for signature-only
validation, then read user details from the request body instead of JWT
claims. Run the user/org/workspace creation pipeline inline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented Feb 21, 2026

PR Review: feat: AnswerAI internal provisioning endpoint for Alpha Agent cross-provisioning

Summary: This PR adds a new POST /api/v1/internal/provision-apikey endpoint that allows Alpha Agent to provision AnswerAI API keys for users via Auth0 M2M tokens. The overall approach is sound — using JWT validation and an org allowlist is a reasonable design. That said, there are several security and architectural concerns that should be addressed before merging, particularly around missing scope validation, the absence of rate limiting on a privileged endpoint, and deviation from the project's established error-handling patterns.


Critical Issues

1. No OAuth Scope Validation on a Privileged Endpoint

Location: packages/server/src/aai/routes/internal-provision.ts:32-42

The jwtCheck configured with express-oauth2-jwt-bearer only validates the JWT signature, audience, and issuer. It does not validate that the M2M token carries a specific scope (e.g., provision:apikey). The PR description mentions creating an M2M application with a provision:apikey scope in Auth0, but the code never enforces this scope. Any valid M2M token issued against the same Auth0 audience can call this endpoint and provision API keys.

Why this matters: An M2M token issued to a completely unrelated service (e.g., a monitoring Lambda, a data pipeline) that happens to use the same Auth0 audience would have the full ability to provision API keys.

Suggestion: Add scope enforcement using express-oauth2-jwt-bearer's requiredScopes middleware or the claimCheck option:

import { auth, requiredScopes } from 'express-oauth2-jwt-bearer'

// Chain the scope check after jwtCheck in the route definition:
router.post(
  '/provision-apikey',
  (req, res, next) => { jwtCheck(req, res, (err?: any) => { if (err) return res.status(401).json({ error: 'Unauthorized' }); next() }) },
  requiredScopes('provision:apikey'),
  async (req: Request, res: Response) => { ... }
)

2. No Rate Limiting on a Privileged User-Creation Endpoint

Location: packages/server/src/aai/routes/internal-provision.ts (entire file)

This endpoint creates users, organizations, workspaces, and API keys in a single call. There is no rate limiting applied. If an attacker obtains a valid M2M token (or if the scope gap above is exploited), they could hammer this endpoint to create arbitrary users and organizations at high volume. The project already has a RateLimiterManager in utils/rateLimit.ts.

Why this matters: Without rate limiting, a single compromised token enables account flooding, database exhaustion, and potential DoS of the provisioning pipeline.

Suggestion: Apply the project's existing rate limiting infrastructure to this endpoint before mounting it. Even a conservative limit (e.g., 10 requests/minute per IP or per token sub) would significantly reduce blast radius.


3. Plaintext API Key in Response Body — Risk of Log Capture

Location: packages/server/src/aai/routes/internal-provision.ts:139-143

The response returns { apiKey, userId, workspaceId } where apiKey is the raw plaintext key. If any request/response logging middleware (e.g., Morgan with body logging, or a future addition) captures the response body, the plaintext key would end up in logs. The PR description correctly notes this is "the only time it's visible" — that awareness should be enforced in the code itself.

Why this matters: Plaintext API keys in logs are a common source of credential exposure.

Suggestion: Add an explicit comment next to the response block warning that the response body must never be logged, and confirm no existing middleware captures response bodies for this route.


Major Concerns

4. Deviation from Project Error-Handling Standards

Location: packages/server/src/aai/routes/internal-provision.ts:143-146

The project standard (documented in CLAUDE.md, packages/server/CLAUDE.md, and .claude/rules/api-routes.md) requires using InternalFlowiseError with next(error) for all error cases. The new endpoint uses raw res.status(500).json(...) calls instead, bypassing the centralized errorHandlerMiddleware.

Why this matters: Bypassing InternalFlowiseError means this endpoint does not benefit from the centralized error-handling middleware, which normalizes error responses and provides structured logging. This also creates inconsistent error shapes for callers.

Suggestion: Import and throw InternalFlowiseError from ../../errors/internalFlowiseError for error cases, and pass errors to next(error) — consistent with the 4-layer pattern used elsewhere in the server.


5. Business Logic Collapsed into Route Handler — Bypasses 4-Layer Architecture

Location: packages/server/src/aai/routes/internal-provision.ts (entire file)

The server's documented architecture separates concerns into Routes, Controllers, Services, and Entities. All business logic (finding/creating users, orgs, workspaces, and API keys) is collapsed into a single route handler. The existing apikeyService.createApiKey in packages/server/src/services/apikey/index.ts already encapsulates key creation with proper organizationId, workspaceId, and userId assignment.

Why this matters: Bypassing the service layer duplicates logic that already exists, creates a maintenance burden, and makes it harder to apply cross-cutting concerns (audit logging, quota enforcement) consistently.

Suggestion: Extract provisioning logic into a dedicated service (e.g., provisioningService.provisionApiKey) and reuse the existing apikeyService where possible. The route handler should be thin — validate inputs, call the service, return the response.


6. No Automated Tests for the New Endpoint

Location: No test files included in this PR.

The test plan is manual-only. No automated tests exist for: valid M2M token provisions key successfully, invalid/missing token returns 401, orgId not in allowlist returns 401, missing required fields returns 400, idempotent behavior (second call replaces the key), or missing AUTH0_ORGANIZATION_ID env var edge case.

The project has test infrastructure at packages/server/test/ with relevant examples in test/auth/ and test/routes/. Given that this endpoint performs privileged user and key creation, automated test coverage is especially important before shipping.

Suggestion: Add integration tests in packages/server/test/routes/v1/internal-provision.test.ts covering the scenarios above.


Minor Issues and Suggestions

7. Potential ApiKey.id Column Length Mismatch

Location: packages/server/src/aai/routes/internal-provision.ts:120

newKey.id = uuidv4()

The ApiKey entity (packages/server/src/database/entities/ApiKey.ts) defines id as @PrimaryColumn({ type: 'varchar', length: 20 }). A UUID v4 string is 36 characters (with hyphens), which exceeds this column constraint. The existing apikeyService.createApiKey also uses uuidv4(), so this may be a pre-existing issue or the column definition may differ in the actual migration — but it is worth explicitly verifying before this endpoint is used in production.

Suggestion: Confirm the actual column definition in the database migration and reconcile with the entity definition.


8. Excessive Raw console Logging (eslint-disable no-console)

Location: packages/server/src/aai/routes/internal-provision.ts:1 and throughout (8 console.* calls)

The file disables the no-console lint rule and uses raw console.log/warn/error throughout. Other internal routes do the same, so this is not unique to this PR — but it does mean log output from this privileged endpoint is unstructured and harder to query in production monitoring.

Suggestion: Not a blocker. Consider using a structured logger in the aai/routes/ layer to make traces filterable in production.


9. Missing Startup Guard for AUTH0_ORGANIZATION_ID

Location: packages/server/src/aai/routes/internal-provision.ts:73

const allowedOrgs = process.env.AUTH0_ORGANIZATION_ID?.split(',') || []

If AUTH0_ORGANIZATION_ID is not set, allowedOrgs is an empty array and all provisioning requests silently return 401 with no clear signal that configuration is missing.

Suggestion: Add a startup-time warning or assertion when AUTH0_ORGANIZATION_ID is not configured, so operators can detect this misconfiguration quickly.


Positive Observations

  • Using auth from express-oauth2-jwt-bearer for RS256 JWT validation is consistent with the existing verifyAAIToken middleware and is the correct approach for M2M tokens that lack user claims like email and org_id.
  • The org allowlist check (validating orgId from the request body against AUTH0_ORGANIZATION_ID) adds a meaningful second layer of authorization beyond JWT signature validation alone.
  • The idempotent design — deleting existing "AlphaAgent" keys before creating a new one — is well-considered and correctly handles the constraint that hashed secrets cannot be recovered.
  • The endpoint correctly sets organizationId, userId, and workspaceId on the created ApiKey entity, satisfying the project's multi-tenancy requirements.
  • The PR description is thorough and clearly documents the flow, related PRs, and manual Auth0 setup steps. This is excellent documentation practice.
  • Mounting the route after the main auth middleware and self-handling JWT validation (whitelisted in constants.ts) is consistent with how auth-me.ts and auth0.ts are structured.

Next Steps

  1. Critical: Add requiredScopes('provision:apikey') (or equivalent claimCheck) to enforce the M2M token scope — without this, any token for this audience can provision keys.
  2. Critical: Add rate limiting using the project's existing RateLimiterManager before this endpoint reaches production.
  3. Major: Migrate business logic to a service layer and adopt InternalFlowiseError / next(error) for error handling consistent with project standards.
  4. Major: Add automated integration tests covering the success path and all documented failure modes.
  5. Minor: Verify the ApiKey.id column length constraint against the actual database migration.
  6. Minor: Add a startup warning when AUTH0_ORGANIZATION_ID is not configured.

Review conducted by Claude Code PR Review Agent

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