feat: Artist TikTok Connections via Composio#170
Conversation
- Add artist_composio_connections type to database.types.ts - Create selectArtistComposioConnection.ts (single lookup by artist+toolkit) - Create selectArtistComposioConnections.ts (all connections for artist) - Create insertArtistComposioConnection.ts (upsert on unique constraint) - Create deleteArtistComposioConnection.ts (delete by id) Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add ALLOWED_ARTIST_CONNECTORS constant with 'tiktok' as first connector - Create checkAccountArtistAccess function in Recoup-API (migrated from Recoup-Chat) - Create getArtistConnectors function to return connector status for artists - Create GET /api/artist-connectors endpoint with: - Bearer token and API key auth via validateAuthContext - Artist access validation via checkAccountArtistAccess - Returns list of allowed connectors with connection status Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add validateAuthorizeArtistConnectorBody.ts with Zod schema for request validation
- Add authorizeArtistConnector.ts to generate OAuth URLs via Composio
- Add POST /api/artist-connectors/authorize route with auth and access control
- Callback URL redirects to /chat?artist_connected={artistId}&toolkit={slug}
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add DELETE handler to disconnect an artist's connector from Composio - Create validateDisconnectArtistConnectorBody.ts with Zod schema - Create verifyArtistConnectorOwnership.ts to check connection ownership - Create disconnectArtistConnector.ts to remove from Composio and DB Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add 'artist-connectors' to CallbackDestination type in getCallbackUrl.ts
- Add artistId and toolkit to CallbackOptions interface
- Handle artist-connectors destination returning /chat?artist_connected={artistId}&toolkit={toolkit}
- Update authorizeArtistConnector.ts to use getCallbackUrl instead of local function
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add 'tiktok' to the ENABLED_TOOLKITS array so TikTok tools are available in Tool Router sessions, enabling the LLM to access TikTok data. Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add artistConnections parameter to createToolRouterSession (Record<string, string> | undefined) - Pass connectedAccounts option to composio.create() call - Update getComposioTools to accept and pass artistConnections parameter - Add JSDoc documentation for the new parameter This enables artist-specific Composio connections to be used when creating Tool Router sessions, allowing the LLM to use the correct TikTok account for the selected artist. Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Modified getComposioTools to accept artistId parameter - If artistId provided, fetches artist_composio_connections from DB - Transforms connections to Record<string, string> format - Passes artistConnections to createToolRouterSession - Modified setupToolsForRequest to extract artistId from body - Passes artistId through to getComposioTools Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add POST /api/artist-connectors/complete endpoint to finalize OAuth flow: - Query Composio for the user's connected account after OAuth redirect - Store the connection mapping in artist_composio_connections table - Add Zod validation for request body (artist_id, toolkit_slug) Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughRoutes were refactored to delegate connector operations to centralized handlers; connector APIs were made entity-scoped (account/artist), with new validators, access checks, artist-connector restrictions, and tooling to surface and pass artist connections to the tool router. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Route as /api/connectors/authorize
participant Handler as authorizeConnectorHandler
participant Validator as validateAuthorizeConnectorRequest
participant Auth as validateAuthContext
participant Access as checkAccountArtistAccess
participant Core as authorizeConnector
participant Composio as Composio API
Client->>Route: POST
Route->>Handler: delegate request
Handler->>Validator: validate + auth
Validator->>Auth: validateAuthContext
Auth-->>Validator: accountId
alt account_id provided
Validator->>Access: checkAccountArtistAccess
alt denied
Access-->>Validator: false
Validator-->>Handler: NextResponse(403)
Handler-->>Client: 403
else granted
Validator-->>Handler: params{composioEntityId, connector, authConfigs?, callbackUrl?}
end
else no account_id
Validator-->>Handler: params{composioEntityId, connector, callbackUrl?}
end
Handler->>Core: authorizeConnector(composioEntityId, connector, options)
Core->>Composio: create session
Composio-->>Core: session+redirectUrl
Core-->>Handler: result
Handler-->>Client: 200 {success:true, data}
sequenceDiagram
participant Client
participant Route as /api/connectors
participant Handler as getConnectorsHandler
participant Validator as validateGetConnectorsRequest
participant Auth as validateAuthContext
participant Access as checkAccountArtistAccess
participant Core as getConnectors
participant Composio as Composio API
Client->>Route: GET?account_id=...
Route->>Handler: delegate request
Handler->>Validator: validate + auth + query
Validator->>Auth: validateAuthContext
Auth-->>Validator: accountId
alt account_id provided
Validator->>Access: checkAccountArtistAccess
alt denied
Access-->>Validator: false
Validator-->>Handler: NextResponse(403)
Handler-->>Client: 403
else granted
Validator-->>Handler: params{composioEntityId, allowedToolkits?}
end
else no account_id
Validator-->>Handler: params{composioEntityId: accountId}
end
Handler->>Core: getConnectors(composioEntityId, options)
Core->>Composio: fetch connectors
Composio-->>Core: connectors[]
Core-->>Handler: connectors[]
Handler-->>Client: 200 {success:true, connectors}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Braintrust eval reportCatalog Opportunity Analysis Evaluation (HEAD-1770003038)
Catalog Songs Count Evaluation (HEAD-1770003038)
First Week Album Sales Evaluation (HEAD-1770003038)
Memory & Storage Tools Evaluation (HEAD-1770003038)
Monthly Listeners Tracking Evaluation (HEAD-1770003038)
Search Web Tool Evaluation (HEAD-1770003038)
Social Scraping Evaluation (HEAD-1770003037)
Spotify Followers Evaluation (HEAD-1770003037)
Spotify Tools Evaluation (HEAD-1770003037)
TikTok Analytics Questions Evaluation (HEAD-1770003037)
|
- Remove artist_composio_connections table and related code - Remove /complete endpoint (no longer needed) - Use artistId directly as Composio entity when connecting - Query Composio at chat time for artist connections - Pass connections to user session via connectedAccounts Composio is now the source of truth for artist connections.
…tConnectors, disconnectConnector
…/artist-connectors
- Add TDD to code principles - Document thin route files pattern (follow /api/pulses) - Document handler functions pattern - Document combined request validators (validateXxxRequest) - Add DRY guidance for entity types (use options, not duplicate files) - Add file naming convention (name after function, not constant) - Add testing requirements for API changes
- Merge test branch to sync with base - Update test to expect (accountId, artistId, roomId) signature - Add test case for when artistId is provided
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@app/api/connectors/authorize/route.ts`:
- Around line 23-26: The request currently accepts account_id in the body
(documented in the route JSDoc and extracted in validateAuthorizeConnectorBody /
validateAuthorizeConnectorRequest), which violates the guideline; remove
account_id from the body schema and JSDoc, stop extracting it from
validateAuthorizeConnectorBody/validateAuthorizeConnectorRequest, and instead
obtain the target account from the authentication context in the route handler
(or accept it as a query/path param and validate it against the authenticated
principal before use); update the route.ts JSDoc,
validateAuthorizeConnectorBody, and validateAuthorizeConnectorRequest to reflect
the new source (auth context or query) and ensure authorization checks compare
the requested account id to the authenticated user's allowed accounts.
In `@lib/composio/connectors/validateAuthorizeConnectorBody.ts`:
- Around line 5-11: The schema authorizeConnectorBodySchema currently includes
account_id which violates the guideline to never accept account_id in request
bodies; remove account_id from authorizeConnectorBodySchema (or rename it to a
clearly distinct field such as target_entity_id if the caller must supply a
different entity ID) and update any callers/validators to derive the real
account ID from authentication instead of the body; additionally, if an
identifier must be passed via HTTP, move it to a query param handler rather than
the body and update any references to account_id in related code (e.g.,
validateDisconnectConnectorBody consumers) to use the new name or auth-derived
account ID.
In `@lib/composio/connectors/validateAuthorizeConnectorRequest.ts`:
- Around line 66-77: The validator currently builds connector-specific auth
configs (authConfigs) inside validateAuthorizeConnectorRequest — move that
responsibility to a new helper and call it from the handler: create a new
function buildConnectorAuthConfigs(connector: string):
Record<string,string>|undefined that encapsulates the TikTok env-var logic (and
future connector rules) and remove the authConfigs construction from
validateAuthorizeConnectorRequest so it only returns validated account_id and
connector; then update authorizeConnectorHandler (or the handler that calls
authorizeConnector) to call buildConnectorAuthConfigs(validated.connector) and
attach the returned authConfigs before invoking authorizeConnector.
In `@lib/composio/connectors/validateDisconnectConnectorBody.ts`:
- Around line 5-8: The schema disconnectConnectorBodySchema currently accepts
account_id which violates the guideline to never accept account_id in request
bodies; remove account_id from disconnectConnectorBodySchema (and the analogous
authorize body schema) and ensure the code paths that consumed it in
app/api/connectors/route.ts derive the account ID from authentication instead;
if you truly need to act on a different entity, replace account_id with a
clearly-named field like target_entity_id or move it to a query parameter and
update consumers to treat it as a deliberate target (not the caller's account).
🧹 Nitpick comments (2)
lib/composio/connectors/validateAuthorizeConnectorRequest.ts (2)
7-8: Minor: consolidate imports from the same module.Both
isAllowedArtistConnectorandALLOWED_ARTIST_CONNECTORSare imported from"./isAllowedArtistConnector". Combine them into a single import statement.♻️ Suggested fix
-import { isAllowedArtistConnector } from "./isAllowedArtistConnector"; -import { ALLOWED_ARTIST_CONNECTORS } from "./isAllowedArtistConnector"; +import { isAllowedArtistConnector, ALLOWED_ARTIST_CONNECTORS } from "./isAllowedArtistConnector";
31-86: Function exceeds the 50-line guideline.
validateAuthorizeConnectorRequestspans ~56 lines. Extracting theauthConfigsconstruction (as suggested above) and potentially the account-access + artist-restriction block into a helper would bring it under the limit and improve readability.As per coding guidelines: "Keep functions under 50 lines".
| * Request body: | ||
| * - connector: The connector slug, e.g., "googlesheets" (required) | ||
| * - connector: The connector slug, e.g., "googlesheets" or "tiktok" (required) | ||
| * - callback_url: Optional custom callback URL after OAuth | ||
| * - account_id: Optional entity ID (e.g., artist ID) for entity-specific connections |
There was a problem hiding this comment.
account_id in the request body violates coding guidelines.
The JSDoc documents account_id as a body parameter, and the underlying handler/validator (validateAuthorizeConnectorRequest) extracts account_id from the parsed body. The coding guidelines explicitly state: "Never use account_id in request bodies or tool schemas — always derive account ID from authentication."
The account (or artist) to act upon should be derived from authentication context or passed via a route/query parameter that is cross-checked against the authenticated principal — not accepted as a trusted field in the request body. Accepting account_id in the body opens the door to authorization bypass if access checks are ever loosened or missed.
Consider redesigning so that the target entity is either:
- Derived entirely from auth context, or
- Passed as a path/query param (e.g.,
/api/connectors/authorize?account_id=...) with the access check kept in place.
This applies to the full handler chain — validateAuthorizeConnectorBody, validateAuthorizeConnectorRequest, and this route's JSDoc should all be updated.
As per coding guidelines: "Never use account_id in request bodies or tool schemas - always derive account ID from authentication."
🤖 Prompt for AI Agents
In `@app/api/connectors/authorize/route.ts` around lines 23 - 26, The request
currently accepts account_id in the body (documented in the route JSDoc and
extracted in validateAuthorizeConnectorBody /
validateAuthorizeConnectorRequest), which violates the guideline; remove
account_id from the body schema and JSDoc, stop extracting it from
validateAuthorizeConnectorBody/validateAuthorizeConnectorRequest, and instead
obtain the target account from the authentication context in the route handler
(or accept it as a query/path param and validate it against the authenticated
principal before use); update the route.ts JSDoc,
validateAuthorizeConnectorBody, and validateAuthorizeConnectorRequest to reflect
the new source (auth context or query) and ensure authorization checks compare
the requested account id to the authenticated user's allowed accounts.
| export const authorizeConnectorBodySchema = z.object({ | ||
| connector: z | ||
| .string({ message: "connector is required" }) | ||
| .min(1, "connector cannot be empty (e.g., 'googlesheets', 'gmail')"), | ||
| .min(1, "connector cannot be empty (e.g., 'googlesheets', 'tiktok')"), | ||
| callback_url: z.string().url("callback_url must be a valid URL").optional(), | ||
| account_id: z.string().uuid("account_id must be a valid UUID").optional(), | ||
| }); |
There was a problem hiding this comment.
Same account_id in request body concern applies here.
As noted in validateDisconnectConnectorBody.ts, accepting account_id in the request body conflicts with the coding guideline. The same recommendation applies: consider a query param or a distinctly-named field like target_entity_id.
As per coding guidelines, {app/api/**/*.ts,lib/mcp/tools/**/*.ts}: "Never use account_id in request bodies or tool schemas - always derive account ID from authentication".
🤖 Prompt for AI Agents
In `@lib/composio/connectors/validateAuthorizeConnectorBody.ts` around lines 5 -
11, The schema authorizeConnectorBodySchema currently includes account_id which
violates the guideline to never accept account_id in request bodies; remove
account_id from authorizeConnectorBodySchema (or rename it to a clearly distinct
field such as target_entity_id if the caller must supply a different entity ID)
and update any callers/validators to derive the real account ID from
authentication instead of the body; additionally, if an identifier must be
passed via HTTP, move it to a query param handler rather than the body and
update any references to account_id in related code (e.g.,
validateDisconnectConnectorBody consumers) to use the new name or auth-derived
account ID.
| // Build auth configs for custom OAuth | ||
| const authConfigs: Record<string, string> = {}; | ||
| if (connector === "tiktok" && process.env.COMPOSIO_TIKTOK_AUTH_CONFIG_ID) { | ||
| authConfigs.tiktok = process.env.COMPOSIO_TIKTOK_AUTH_CONFIG_ID; | ||
| } | ||
|
|
||
| return { | ||
| composioEntityId: account_id, | ||
| connector, | ||
| callbackUrl: callback_url, | ||
| authConfigs: Object.keys(authConfigs).length > 0 ? authConfigs : undefined, | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Connector-specific auth config construction doesn't belong in a validation function — violates single responsibility.
Lines 66–70 build TikTok-specific authConfigs by reading an environment variable. This is business/integration logic, not request validation. It couples the validator to connector-specific implementation details, making it harder to extend when new connectors (with their own auth configs) are added.
Extract authConfigs construction into a dedicated helper (e.g., buildConnectorAuthConfigs(connector: string)) and call it from the handler, not the validator. The validator should return the validated account_id and connector, and the handler should enrich the params before calling authorizeConnector.
♻️ Sketch of the separation
In a new file like lib/composio/connectors/buildConnectorAuthConfigs.ts:
export function buildConnectorAuthConfigs(
connector: string,
): Record<string, string> | undefined {
const configs: Record<string, string> = {};
if (connector === "tiktok" && process.env.COMPOSIO_TIKTOK_AUTH_CONFIG_ID) {
configs.tiktok = process.env.COMPOSIO_TIKTOK_AUTH_CONFIG_ID;
}
return Object.keys(configs).length > 0 ? configs : undefined;
}Then in authorizeConnectorHandler.ts, after validation:
const authConfigs = buildConnectorAuthConfigs(validated.connector);This also helps address the function exceeding the 50-line guideline limit.
As per coding guidelines: "Single responsibility per function" and "Keep functions under 50 lines".
🤖 Prompt for AI Agents
In `@lib/composio/connectors/validateAuthorizeConnectorRequest.ts` around lines 66
- 77, The validator currently builds connector-specific auth configs
(authConfigs) inside validateAuthorizeConnectorRequest — move that
responsibility to a new helper and call it from the handler: create a new
function buildConnectorAuthConfigs(connector: string):
Record<string,string>|undefined that encapsulates the TikTok env-var logic (and
future connector rules) and remove the authConfigs construction from
validateAuthorizeConnectorRequest so it only returns validated account_id and
connector; then update authorizeConnectorHandler (or the handler that calls
authorizeConnector) to call buildConnectorAuthConfigs(validated.connector) and
attach the returned authConfigs before invoking authorizeConnector.
| export const disconnectConnectorBodySchema = z.object({ | ||
| connected_account_id: z.string().min(1, "connected_account_id is required"), | ||
| account_id: z.string().uuid("account_id must be a valid UUID").optional(), | ||
| }); |
There was a problem hiding this comment.
account_id in request body conflicts with coding guidelines.
The coding guidelines state: "Never use account_id in request bodies or tool schemas - always derive account ID from authentication." This schema accepts account_id in the DELETE body, which flows through app/api/connectors/route.ts. The same pattern appears in the authorize body schema.
If the intent is to allow acting on behalf of another entity (e.g., an artist), consider using a query parameter or a differently-named field (e.g., target_entity_id) to distinguish it from the authenticated account's ID and clarify that this isn't the caller's identity. Otherwise, an explicit exemption or guideline update would help keep things consistent.
As per coding guidelines, {app/api/**/*.ts,lib/mcp/tools/**/*.ts}: "Never use account_id in request bodies or tool schemas - always derive account ID from authentication".
🤖 Prompt for AI Agents
In `@lib/composio/connectors/validateDisconnectConnectorBody.ts` around lines 5 -
8, The schema disconnectConnectorBodySchema currently accepts account_id which
violates the guideline to never accept account_id in request bodies; remove
account_id from disconnectConnectorBodySchema (and the analogous authorize body
schema) and ensure the code paths that consumed it in
app/api/connectors/route.ts derive the account ID from authentication instead;
if you truly need to act on a different entity, replace account_id with a
clearly-named field like target_entity_id or move it to a query parameter and
update consumers to treat it as a deliberate target (not the caller's account).
- API is now unopinionated: any account can connect any service - Tool router handles collision prevention in createToolRouterSession - Account connections always win over artist connections (no duplicates) - Remove allowedToolkits from GET/authorize validation - Update tests for new architecture
Merge test to main
Consolidates the authorize endpoint into the main connectors route to match the updated API docs. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…Access SRP: each query is now its own file with dedicated tests: - selectAccountArtistId (account_artist_ids) - selectArtistOrganizationIds (artist_organization_ids) - selectAccountOrganizationIds (account_organization_ids) Co-Authored-By: Claude Opus 4.6 <[email protected]>
| const { artist_id, connector } = validated; | ||
|
|
||
| // Verify connector is allowed | ||
| if (!isAllowedArtistConnector(connector)) { |
There was a problem hiding this comment.
SRP
- actual: multiple validations happening in the API endpoint definition file.
- required: move any verification of input params into the
validateAuthorizeArtistConnectorBodyfunction.
|
|
||
| // Verify user has access to this artist | ||
| const hasAccess = await checkAccountArtistAccess(accountId, artist_id); | ||
| if (!hasAccess) { |
There was a problem hiding this comment.
SRP
- actual: multiple validations happening in the API endpoint definition file.
- required: move any verification of input params into the validateAuthorizeArtistConnectorBody function.
- Check out other example endpoints like
/api/pulsesto see a cleaner implementation.
app/api/artist-connectors/route.ts
Outdated
| * @param request - The incoming request | ||
| * @returns List of connectors with connection status | ||
| */ | ||
| export async function GET(request: NextRequest): Promise<NextResponse> { |
There was a problem hiding this comment.
SRP
- actual: endpoint file also defines the handler
- required: standalone handler function similar to
/api/pulses
app/api/artist-connectors/route.ts
Outdated
| const { searchParams } = new URL(request.url); | ||
| const artistId = searchParams.get("artist_id"); | ||
|
|
||
| if (!artistId) { |
There was a problem hiding this comment.
SRP
- actual: multiple validations happening in the API endpoint definition file.
- required: move any verification of input params into a verify function.
- Check out other example endpoints like /api/pulses to see a cleaner implementation.
app/api/artist-connectors/route.ts
Outdated
|
|
||
| // Verify user has access to this artist | ||
| const hasAccess = await checkAccountArtistAccess(accountId, artistId); | ||
| if (!hasAccess) { |
There was a problem hiding this comment.
SRP
- actual: multiple validations happening in the API endpoint definition file.
- required: move any verification of input params into a verify function.
- Check out other example endpoints like /api/pulses to see a cleaner implementation.
| }); | ||
| vi.mocked(checkAccountArtistAccess).mockResolvedValue(true); | ||
|
|
||
| const request = new NextRequest(`http://localhost/api/connectors?entity_id=${mockEntityId}`); |
There was a problem hiding this comment.
Why are we using entity_id instead of account_id?
| * Custom auth configs for toolkits that require custom OAuth credentials. | ||
| * e.g., { tiktok: "ac_xxxxx" } | ||
| */ | ||
| authConfigs?: Record<string, string>; |
There was a problem hiding this comment.
Why is authConfigs a generic Record<string, string> type instead of a more explicit typing?
| * | ||
| * Why: Used by the /api/connectors/authorize endpoint to let users | ||
| * connect from the settings page (not in-chat). | ||
| * The entityId is an account ID - either the caller's own account or |
There was a problem hiding this comment.
Why are you using entityId instead of accountId?
There was a problem hiding this comment.
Why are any changes needed to this file?
There was a problem hiding this comment.
lib/supabase/account_workspace_ids/checkAccountWorkspaceAccess.ts -
refactor to selectAccountWorkspaces
…Access SRP: standalone supabase query with dedicated tests. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Use "account" terminology consistently per codebase conventions. Co-Authored-By: Claude Opus 4.6 <[email protected]>
No caller uses this option — YAGNI. Co-Authored-By: Claude Opus 4.6 <[email protected]>
The comment claimed ownership verification that the function doesn't do. Updated to reflect it only validates request shape. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Not a direct Supabase query — it aggregates supabase calls, so it belongs in the domain layer. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ntWorkspaceId directly YAGNI — the wrapper was just !!data. The sole consumer now calls the supabase query directly. Co-Authored-By: Claude Opus 4.6 <[email protected]>
| */ | ||
| export async function authorizeConnector( | ||
| userId: string, | ||
| entityId: string, |
There was a problem hiding this comment.
lib/composio/connectors/authorizeConnector.ts is there any reason
this needs to be called entityId? We prefer accountId
|
|
||
| // If filtering, ensure we return all allowed toolkits (even if not in Composio response) | ||
| if (allowedToolkits) { | ||
| const existingSlugs = new Set(connectors.map(c => c.slug)); | ||
| for (const slug of allowedToolkits) { | ||
| if (!existingSlugs.has(slug)) { | ||
| connectors.push({ | ||
| slug, | ||
| name: displayNames[slug] || slug, | ||
| isConnected: false, | ||
| connectedAccountId: undefined, | ||
| }); | ||
| } | ||
| } | ||
| // Filter to only allowed and maintain order | ||
| return allowedToolkits.map(slug => connectors.find(c => c.slug === slug)!); | ||
| } | ||
|
|
||
| return connectors; |
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds custom Zod messages and missing_fields/status fields to error responses for both POST and DELETE /api/connectors validation, matching the standard pattern used across other API endpoints. Co-Authored-By: Claude Opus 4.6 <[email protected]>

Summary
Stories Implemented
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit