diff --git a/plugins/composio-direct/GUIDE.md b/plugins/composio-direct/GUIDE.md index 1a1c2a6..d3244a9 100644 --- a/plugins/composio-direct/GUIDE.md +++ b/plugins/composio-direct/GUIDE.md @@ -4,14 +4,14 @@ Use `composio-direct` when a user asks Teleton to work with an external app supp ## Required Setup -The plugin requires the Teleton secret `composio_api_key`, using either a Composio project API key or user API key. It can also be supplied through `COMPOSIO_DIRECT_COMPOSIO_API_KEY`, with `COMPOSIO_API_KEY` kept as a legacy fallback. If the key is missing, stop and ask the operator to configure it before attempting Composio calls. +The plugin requires the Teleton secret `composio_api_key`, using a Composio project API key, user API key, or organization API key. It can also be supplied through `COMPOSIO_DIRECT_COMPOSIO_API_KEY`, with `COMPOSIO_API_KEY` kept as a legacy fallback. If the key is missing, stop and ask the operator to configure it before attempting Composio calls. Default runtime settings: | Setting | Default | Use | |---|---:|---| | `base_url` | `https://backend.composio.dev/api/v3.1` | Composio API endpoint | -| `api_key_auth_scheme` | `auto` | API key header mode: `auto`, `project`, or `user` | +| `api_key_auth_scheme` | `auto` | API key header mode: `auto`, `project`, `user`, or `org` | | `timeout_ms` | `30000` | Default request timeout | | `max_parallel_executions` | `10` | Batch execution concurrency | | `tool_version` | `latest` | Tool execution/schema version | @@ -29,6 +29,18 @@ Default runtime settings: This order matters because Composio tool inputs vary by toolkit and version. Do not guess parameter names when `composio_get_tool_schemas` can confirm them. +## Custom Provider And Sessions Alignment + +Composio's TypeScript custom provider docs define a provider as the adapter layer that transforms tool definitions, executes tool calls, and exposes platform helpers. Teleton already provides the agent tool registry, so `composio-direct` acts as the Teleton-specific direct provider over Composio REST instead of importing `@composio/core` provider classes at runtime. + +The mapping is: + +- Transform step: `composio_search_tools` and `composio_get_tool_schemas` convert Composio tool objects into Teleton-visible results with `tool_slug`, schemas, and `execute_with` guidance. +- Execution step: `composio_execute_tool` and `composio_multi_execute` call the current Composio execute API with the sender-scoped `user_id`, tool `arguments`, optional `connected_account_id`, and selected tool version. +- Provider helpers: auth, connection, toolkit, file, trigger, webhook, and remote meta-tool wrappers expose the supporting Composio flows an agent needs around execution. + +Composio sessions are the recommended SDK path for new agent integrations. `composio-direct` stays on the direct REST path because Teleton registers static plugin tools, this package must remain dependency-free, and trigger/webhook/file workflows still rely on direct API surfaces. If a future Teleton integration uses Tool Router sessions or MCP, keep it separate from the direct plugin so the current execution contract remains stable. + ## Tool Discovery Call `composio_search_tools` when the user describes an action but not a specific Composio slug. @@ -186,7 +198,7 @@ The plugin returns structured results: For `auth_required`, do not retry blindly. Generate or surface a connection link, wait for user confirmation, then retry. For validation errors, fetch the schema again and correct the parameters. For transient network or 5xx failures, the plugin already retries three times with exponential backoff. -HTTP 401/403 from Composio indicates API key authentication or permission failure, not a missing external app connection. Do not call `composio_auth_link` for those errors; verify the `composio_api_key` key type, `api_key_auth_scheme`, endpoint permissions, and any Composio IP allowlist. +HTTP 401/403 from Composio indicates API key authentication or permission failure, not a missing external app connection. Do not call `composio_auth_link` for those errors; verify the `composio_api_key` key type, `api_key_auth_scheme`, endpoint permissions, and any Composio IP allowlist. In `auto` mode, the plugin tries `x-api-key`, then `x-user-api-key`, then `x-org-api-key` on endpoints that are not project-only. ## Security Rules diff --git a/plugins/composio-direct/README.md b/plugins/composio-direct/README.md index 11ff7bb..f22e7de 100644 --- a/plugins/composio-direct/README.md +++ b/plugins/composio-direct/README.md @@ -16,9 +16,19 @@ Direct integration with **1000+ Composio automation tools** — no MCP transport - **Parallel batch execution** — configurable concurrency limit - **Zero sensitive data in logs** — API keys and OAuth tokens are never logged +## Composio Provider Model + +Composio's TypeScript custom provider guide describes a provider as the layer that transforms tools, executes tool calls, and exposes helper methods for a target agent framework. Teleton plugins are registered as static Teleton tools, so `composio-direct` implements that provider role directly over Composio REST instead of installing a Composio SDK provider class at runtime. + +- Discovery and schema tools are the transform step: they return `tool_slug`, JSON schemas, and `execute_with` instructions. +- `composio_execute_tool` and `composio_multi_execute` are the execution step: they send sender-scoped `user_id`, tool `arguments`, optional top-level `connected_account_id`, and version data to Composio. +- Auth, connection, toolkit, file, trigger, webhook, and remote meta-tool wrappers are the provider helpers Teleton needs around execution. + +Composio sessions are still the preferred SDK path for new agent integrations. This plugin intentionally remains the direct REST integration because it has no npm runtime dependency, Teleton registers fixed plugin tools, and several supported flows still use direct API surfaces. + ## Setup -1. Get your Composio project or user API key at +1. Get your Composio project, user, or organization API key at 2. Set the `composio_api_key` secret in Teleton: ```text @@ -27,14 +37,14 @@ Direct integration with **1000+ Composio automation tools** — no MCP transport For container and CI deployments, Teleton also resolves the secret from `COMPOSIO_DIRECT_COMPOSIO_API_KEY`. The plugin keeps `COMPOSIO_API_KEY` as a direct fallback for older deployments. -By default `api_key_auth_scheme` is `auto`: the plugin sends the key as a project key (`x-api-key`) first and, for endpoints that accept user API keys, retries as `x-user-api-key` on Composio 401/403 responses. Set it to `project` or `user` only when you want to force a specific header. +By default `api_key_auth_scheme` is `auto`: the plugin sends the key as a project key (`x-api-key`) first and, for endpoints that accept non-project API keys, retries as `x-user-api-key` and then `x-org-api-key` on Composio 401/403 responses. Set it to `project`, `user`, or `org` only when you want to force a specific header. Project-only endpoints, such as Files and Webhooks, still use `x-api-key`. ```yaml # config.yaml example plugins: composio_direct: base_url: "https://backend.composio.dev/api/v3.1" # optional - api_key_auth_scheme: "auto" # optional (auto/project/user) + api_key_auth_scheme: "auto" # optional (auto/project/user/org) timeout_ms: 30000 # optional (default: 30s) max_parallel_executions: 10 # optional (default: 10) tool_version: "latest" # optional @@ -473,4 +483,6 @@ node --test plugins/composio-direct/test/unit/composio-direct.test.js \ - Added Triggers API coverage through trigger type discovery, active trigger listing, trigger upsert, enable/disable, and delete endpoints. - Added Webhooks API coverage through event type discovery and webhook subscription CRUD/secret rotation endpoints. - Meta-tool alignment: `composio_search_tools`, `composio_get_tool_schemas`, `composio_multi_execute`, connection/auth tools, `composio_manage_connections`, `composio_remote_bash`, and `composio_remote_workbench` cover the practical `search_tools`, `get_tool_schemas`, `multi_execute_tool`, `manage_connections`, `remote_bash_tool`, and `remote_workbench` flows for Teleton. -- HTTP 401/403 responses are reported as Composio API key access failures, not as `auth_required` service authorization. Check the project/user key type, `api_key_auth_scheme`, endpoint permissions, and any Composio IP allowlist before retrying. +- Custom provider alignment: Teleton consumes the static plugin tools exported here, so the plugin implements the Composio provider transform/execute/helper contract over REST instead of importing `@composio/core` provider classes. +- Sessions note: Composio recommends sessions for SDK agents, while this direct plugin keeps the explicit `/tools`, `/tools/execute`, auth, connection, trigger, webhook, and file routes available to Teleton. +- HTTP 401/403 responses are reported as Composio API key access failures, not as `auth_required` service authorization. Check the project/user/org key type, `api_key_auth_scheme`, endpoint permissions, and any Composio IP allowlist before retrying. diff --git a/plugins/composio-direct/index.js b/plugins/composio-direct/index.js index 860dd7e..d5d05cf 100644 --- a/plugins/composio-direct/index.js +++ b/plugins/composio-direct/index.js @@ -20,7 +20,8 @@ * * Authentication: * - Requires a Composio API key stored in sdk.secrets as "composio_api_key" - * - Supports project API keys (x-api-key) and user API keys (x-user-api-key) + * - Supports project API keys (x-api-key), user API keys (x-user-api-key), + * and organization API keys (x-org-api-key) * - Set COMPOSIO_DIRECT_COMPOSIO_API_KEY, COMPOSIO_API_KEY, or use the secrets store * * Transport: @@ -66,7 +67,7 @@ const COMPOSIO_EXECUTION_GUIDANCE = { export const manifest = { name: "composio-direct", - version: "1.9.3", + version: "1.9.4", sdkVersion: ">=1.0.0", description: "Direct access to 1000+ Composio automation tools plus v3.1 toolkits, files, triggers, webhooks, connection reuse, and meta-tools without MCP transport", @@ -75,7 +76,7 @@ export const manifest = { required: true, env: "COMPOSIO_DIRECT_COMPOSIO_API_KEY", description: - "Composio project or user API key (create at https://app.composio.dev/settings)", + "Composio project, user, or organization API key (create at https://app.composio.dev/settings)", }, }, defaultConfig: { @@ -105,12 +106,11 @@ function sleep(ms) { /** * Build common headers for Composio API requests. * @param {string} apiKey - * @param {"project" | "user"} [apiKeyAuthScheme] + * @param {"project" | "user" | "org"} [apiKeyAuthScheme] * @returns {Record} */ function buildHeaders(apiKey, apiKeyAuthScheme = "project") { - const apiKeyHeader = - apiKeyAuthScheme === "user" ? "x-user-api-key" : "x-api-key"; + const apiKeyHeader = getApiKeyHeaderName(apiKeyAuthScheme); return { "Content-Type": "application/json", [apiKeyHeader]: apiKey, @@ -120,7 +120,7 @@ function buildHeaders(apiKey, apiKeyAuthScheme = "project") { /** * Normalize API-key auth scheme config. * @param {unknown} value - * @returns {"auto" | "project" | "user"} + * @returns {"auto" | "project" | "user" | "org"} */ function normalizeApiKeyAuthScheme(value) { const scheme = String(value ?? DEFAULT_API_KEY_AUTH_SCHEME) @@ -128,35 +128,42 @@ function normalizeApiKeyAuthScheme(value) { .toLowerCase() .replace(/_/g, "-"); - if (scheme === "auto" || scheme === "project" || scheme === "user") { + if (scheme === "auto" || scheme === "project" || scheme === "user" || scheme === "org") { return scheme; } + if (scheme === "organization") return "org"; if (scheme === "x-api-key") return "project"; if (scheme === "x-user-api-key") return "user"; + if (scheme === "x-org-api-key") return "org"; return DEFAULT_API_KEY_AUTH_SCHEME; } /** - * @param {"project" | "user"} apiKeyAuthScheme - * @returns {"x-api-key" | "x-user-api-key"} + * @param {"project" | "user" | "org"} apiKeyAuthScheme + * @returns {"x-api-key" | "x-user-api-key" | "x-org-api-key"} */ function getApiKeyHeaderName(apiKeyAuthScheme) { - return apiKeyAuthScheme === "user" ? "x-user-api-key" : "x-api-key"; + if (apiKeyAuthScheme === "user") return "x-user-api-key"; + if (apiKeyAuthScheme === "org") return "x-org-api-key"; + return "x-api-key"; } /** * Composio v3.1 accepts project keys on x-api-key and user keys on - * x-user-api-key for most user-facing endpoints. In auto mode, try the - * project header first to preserve existing behavior, then retry with the - * user header only when the endpoint supports it and Composio rejected access. - * @param {"auto" | "project" | "user"} apiKeyAuthScheme - * @param {boolean} supportsUserApiKey - * @returns {Array<"project" | "user">} + * x-user-api-key for most user-facing endpoints. Composio also exposes + * organization keys on x-org-api-key for org-scoped access. In auto mode, try + * the project header first to preserve existing behavior, then retry with + * user and organization headers only when the endpoint is not project-only and + * Composio rejected access. + * @param {"auto" | "project" | "user" | "org"} apiKeyAuthScheme + * @param {boolean} supportsNonProjectApiKeys + * @returns {Array<"project" | "user" | "org">} */ -function getApiKeyAuthAttempts(apiKeyAuthScheme, supportsUserApiKey) { +function getApiKeyAuthAttempts(apiKeyAuthScheme, supportsNonProjectApiKeys) { if (apiKeyAuthScheme === "project") return ["project"]; if (apiKeyAuthScheme === "user") return ["user"]; - return supportsUserApiKey ? ["project", "user"] : ["project"]; + if (apiKeyAuthScheme === "org") return ["org"]; + return supportsNonProjectApiKeys ? ["project", "user", "org"] : ["project"]; } /** @@ -291,7 +298,7 @@ function formatComposioApiAccessError(response) { if (response.status === 403) { return ( "Composio API key permission denied. Check that composio_api_key is a " + - "project or user API key with permissions for this endpoint, that " + + "project, user, or organization API key with permissions for this endpoint, that " + "api_key_auth_scheme matches the key type when set explicitly, and that " + `any Composio IP allowlist includes this runtime. Composio response: ${message}` ); @@ -299,7 +306,7 @@ function formatComposioApiAccessError(response) { return ( "Composio API key was rejected. Check that composio_api_key is a valid " + - `project or user API key. Composio response: ${message}` + `project, user, or organization API key. Composio response: ${message}` ); } @@ -905,7 +912,7 @@ export const tools = (sdk) => { return { success: false, error: - "Composio API key is not configured. Please set the composio_api_key secret or COMPOSIO_DIRECT_COMPOSIO_API_KEY with your project or user key from https://app.composio.dev/settings", + "Composio API key is not configured. Please set the composio_api_key secret or COMPOSIO_DIRECT_COMPOSIO_API_KEY with your project, user, or organization key from https://app.composio.dev/settings", }; } @@ -917,7 +924,7 @@ export const tools = (sdk) => { * @param {string} opts.method * @param {unknown} [opts.body] * @param {number} opts.timeoutMs - * @param {boolean} [opts.supportsUserApiKey] + * @param {boolean} [opts.supportsNonProjectApiKeys] * @returns {Promise<{ status: number; data: unknown }>} */ async function requestComposio({ @@ -926,10 +933,13 @@ export const tools = (sdk) => { method, body, timeoutMs, - supportsUserApiKey = true, + supportsNonProjectApiKeys = true, }) { const { apiKeyAuthScheme } = getConfig(); - const authAttempts = getApiKeyAuthAttempts(apiKeyAuthScheme, supportsUserApiKey); + const authAttempts = getApiKeyAuthAttempts( + apiKeyAuthScheme, + supportsNonProjectApiKeys + ); let response = null; let apiAccessErrorResponse = null; @@ -975,7 +985,7 @@ export const tools = (sdk) => { * @param {URLSearchParams} [opts.query] * @param {unknown} [opts.body] * @param {number} [opts.timeoutMs] - * @param {boolean} [opts.supportsUserApiKey] + * @param {boolean} [opts.supportsNonProjectApiKeys] * @returns {Promise<{ status: number; data: unknown }>} */ function callComposio({ @@ -985,7 +995,7 @@ export const tools = (sdk) => { query, body, timeoutMs, - supportsUserApiKey = true, + supportsNonProjectApiKeys = true, }) { const cfg = getConfig(); return requestComposio({ @@ -994,7 +1004,7 @@ export const tools = (sdk) => { method, body, timeoutMs: timeoutMs ?? cfg.timeoutMs, - supportsUserApiKey, + supportsNonProjectApiKeys, }); } @@ -2301,7 +2311,7 @@ export const tools = (sdk) => { apiKey, path: "/files/list", query: qs, - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (response.status !== 200) { return failedResponse("Could not list files", response); @@ -2383,7 +2393,7 @@ export const tools = (sdk) => { path: "/files/upload/request", method: "POST", body, - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (!isOkStatus(response.status, [200, 201]) || !isRecord(response.data)) { return failedResponse("Could not request file upload", response); @@ -2897,7 +2907,7 @@ export const tools = (sdk) => { const response = await callComposio({ apiKey, path: "/webhook_subscriptions/event_types", - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (response.status !== 200) { return failedResponse("Could not list webhook event types", response); @@ -2960,7 +2970,7 @@ export const tools = (sdk) => { apiKey, path: "/webhook_subscriptions", query: qs, - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (response.status !== 200) { return failedResponse("Could not list webhook subscriptions", response); @@ -3019,7 +3029,7 @@ export const tools = (sdk) => { const response = await callComposio({ apiKey, path: `/webhook_subscriptions/${encodeURIComponent(params.webhook_id)}`, - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (response.status !== 200 || !isRecord(response.data)) { return failedResponse(`Could not get webhook ${params.webhook_id}`, response); @@ -3096,7 +3106,7 @@ export const tools = (sdk) => { path: "/webhook_subscriptions", method: "POST", body, - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (!isOkStatus(response.status, [200, 201]) || !isRecord(response.data)) { return failedResponse("Could not create webhook subscription", response); @@ -3179,7 +3189,7 @@ export const tools = (sdk) => { path: `/webhook_subscriptions/${encodeURIComponent(params.webhook_id)}`, method: "PATCH", body, - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (!isOkStatus(response.status, [200, 204]) || (response.status !== 204 && !isRecord(response.data))) { return failedResponse(`Could not update webhook ${params.webhook_id}`, response); @@ -3238,7 +3248,7 @@ export const tools = (sdk) => { apiKey, path: `/webhook_subscriptions/${encodeURIComponent(params.webhook_id)}/rotate_secret`, method: "POST", - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (!isOkStatus(response.status, [200, 201]) || !isRecord(response.data)) { return failedResponse(`Could not rotate webhook ${params.webhook_id} secret`, response); @@ -3289,7 +3299,7 @@ export const tools = (sdk) => { apiKey, path: `/webhook_subscriptions/${encodeURIComponent(params.webhook_id)}`, method: "DELETE", - supportsUserApiKey: false, + supportsNonProjectApiKeys: false, }); if (!isOkStatus(response.status, [200, 204])) { return failedResponse(`Could not delete webhook ${params.webhook_id}`, response); diff --git a/plugins/composio-direct/manifest.json b/plugins/composio-direct/manifest.json index e141ba7..456d671 100644 --- a/plugins/composio-direct/manifest.json +++ b/plugins/composio-direct/manifest.json @@ -1,7 +1,7 @@ { "id": "composio-direct", "name": "Composio Direct", - "version": "1.9.3", + "version": "1.9.4", "description": "Direct access to 1000+ Composio automation tools plus v3.1 toolkits, files, triggers, webhooks, connection reuse, and meta-tools without MCP transport", "author": { "name": "xlabtg", @@ -15,7 +15,7 @@ "composio_api_key": { "required": true, "env": "COMPOSIO_DIRECT_COMPOSIO_API_KEY", - "description": "Composio project or user API key (create at https://app.composio.dev/settings)" + "description": "Composio project, user, or organization API key (create at https://app.composio.dev/settings)" } }, "defaultConfig": { diff --git a/plugins/composio-direct/package.json b/plugins/composio-direct/package.json index b8b501a..59c0793 100644 --- a/plugins/composio-direct/package.json +++ b/plugins/composio-direct/package.json @@ -1,7 +1,7 @@ { "name": "teleton-plugin-composio-direct", "type": "module", - "version": "1.9.3", + "version": "1.9.4", "private": true, "description": "Teleton plugin for direct Composio API access" } diff --git a/plugins/composio-direct/test/unit/composio-direct.test.js b/plugins/composio-direct/test/unit/composio-direct.test.js index e73f57d..c1d171a 100644 --- a/plugins/composio-direct/test/unit/composio-direct.test.js +++ b/plugins/composio-direct/test/unit/composio-direct.test.js @@ -149,7 +149,7 @@ describe("manifest", () => { assert.ok(manifest.name, "manifest.name is set"); assert.ok(manifest.version, "manifest.version is set"); assert.ok(manifest.secrets?.composio_api_key, "secret composio_api_key declared"); - assert.equal(manifest.version, "1.9.3"); + assert.equal(manifest.version, "1.9.4"); assert.equal(manifest.defaultConfig?.base_url, "https://backend.composio.dev/api/v3.1"); assert.equal(manifest.defaultConfig?.api_key_auth_scheme, "auto"); }); diff --git a/plugins/composio-direct/tests/guide.test.js b/plugins/composio-direct/tests/guide.test.js index 83f4048..e3e649a 100644 --- a/plugins/composio-direct/tests/guide.test.js +++ b/plugins/composio-direct/tests/guide.test.js @@ -17,6 +17,12 @@ describe("composio-direct agent guide", () => { "composio_get_tool_schemas", "composio_execute_tool", "composio_multi_execute", + "Custom Provider And Sessions Alignment", + "Teleton-specific direct provider", + "Transform step", + "Execution step", + "Provider helpers", + "Composio sessions are the recommended SDK path", "composio_auth_link", "composio_list_connections", "composio_manage_connections", diff --git a/plugins/composio-direct/tests/index.test.js b/plugins/composio-direct/tests/index.test.js index 4a57cdd..4a9f8e5 100644 --- a/plugins/composio-direct/tests/index.test.js +++ b/plugins/composio-direct/tests/index.test.js @@ -109,7 +109,7 @@ describe("composio-direct Teleton integration", () => { const sdk = makeSdk(); const toolList = toolsFactory(sdk); - assert.equal(manifest.version, "1.9.3"); + assert.equal(manifest.version, "1.9.4"); assert.equal(manifest.defaultConfig.base_url, "https://backend.composio.dev/api/v3.1"); assert.equal(manifest.defaultConfig.api_key_auth_scheme, "auto"); assert.deepEqual( @@ -186,6 +186,73 @@ describe("composio-direct Teleton integration", () => { } }); + it("wraps SDK-style Composio tools as Teleton custom provider results", async () => { + const inputSchema = { + type: "object", + properties: { + owner: { type: "string" }, + repo: { type: "string" }, + }, + required: ["owner", "repo"], + additionalProperties: false, + }; + const outputSchema = { + type: "object", + properties: { + starred: { type: "boolean" }, + }, + }; + const { restore } = mockFetch(() => ({ + status: 200, + data: { + items: [ + { + slug: "GITHUB_STAR_REPOSITORY", + name: "Star repository", + description: "Star a GitHub repository", + toolkit: { slug: "github", name: "GitHub" }, + no_auth: false, + inputParameters: inputSchema, + outputParameters: outputSchema, + }, + ], + }, + })); + + try { + const searchTool = toolsFactory(makeSdk()).find((tool) => tool.name === "composio_search_tools"); + const result = await searchTool.execute( + { toolkit: "github", limit: 1, include_params: true }, + makeContext() + ); + + assert.equal(result.success, true); + assert.equal(result.data.execution.tool, "composio_execute_tool"); + assert.deepEqual(result.data.tools, [ + { + tool_slug: "GITHUB_STAR_REPOSITORY", + display_name: "Star repository", + description: "Star a GitHub repository", + toolkit: "github", + auth_required: true, + version: null, + tags: [], + execute_with: { + tool: "composio_execute_tool", + tool_slug: "GITHUB_STAR_REPOSITORY", + parameters_param: "parameters", + }, + parameters_schema: inputSchema, + output_schema: outputSchema, + }, + ]); + assert.equal(Object.hasOwn(result.data.tools[0], "name"), false); + assert.equal(Object.hasOwn(result.data.tools[0], "execute"), false); + } finally { + restore(); + } + }); + it("executes tools through current /tools/execute API with sender-scoped user_id", async () => { const { calls, restore } = mockFetch(() => ({ status: 200, @@ -505,6 +572,61 @@ describe("composio-direct Teleton integration", () => { } }); + it("retries execute with x-org-api-key when project and user headers are rejected", async () => { + const { calls, restore } = mockFetch((call, idx) => { + if (idx === 1) { + assert.equal(call.headers["x-api-key"], "test-api-key"); + assert.equal(call.headers["x-user-api-key"], undefined); + assert.equal(call.headers["x-org-api-key"], undefined); + return { + status: 403, + data: { + error: { + message: "The API key doesn't have permissions to perform the request.", + slug: "HTTP_Forbidden", + status: 403, + }, + }, + }; + } + + if (idx === 2) { + assert.equal(call.headers["x-api-key"], undefined); + assert.equal(call.headers["x-user-api-key"], "test-api-key"); + assert.equal(call.headers["x-org-api-key"], undefined); + return { + status: 401, + data: { message: "Unauthorized" }, + }; + } + + assert.equal(call.headers["x-api-key"], undefined); + assert.equal(call.headers["x-user-api-key"], undefined); + assert.equal(call.headers["x-org-api-key"], "test-api-key"); + return { + status: 200, + data: { + successful: true, + data: { ok: true }, + }, + }; + }); + + try { + const executeTool = toolsFactory(makeSdk()).find((tool) => tool.name === "composio_execute_tool"); + const result = await executeTool.execute( + { tool_slug: "github_list_repos", parameters: {} }, + makeContext() + ); + + assert.equal(result.success, true); + assert.equal(result.data.ok, true); + assert.equal(calls.length, 3); + } finally { + restore(); + } + }); + it("uses x-user-api-key first when api_key_auth_scheme is user", async () => { const { calls, restore } = mockFetch((call) => { assert.equal(call.headers["x-api-key"], undefined); @@ -535,11 +657,43 @@ describe("composio-direct Teleton integration", () => { } }); - it("keeps the actionable x-api-key 403 when x-user-api-key fallback is also rejected", async () => { + it("uses x-org-api-key first when api_key_auth_scheme is org", async () => { + const { calls, restore } = mockFetch((call) => { + assert.equal(call.headers["x-api-key"], undefined); + assert.equal(call.headers["x-user-api-key"], undefined); + assert.equal(call.headers["x-org-api-key"], "test-api-key"); + return { + status: 200, + data: { + successful: true, + data: { ok: true }, + }, + }; + }); + + try { + const executeTool = toolsFactory( + makeSdk({ pluginConfig: { api_key_auth_scheme: "organization" } }) + ).find((tool) => tool.name === "composio_execute_tool"); + const result = await executeTool.execute( + { tool_slug: "github_list_repos", parameters: {} }, + makeContext() + ); + + assert.equal(result.success, true); + assert.equal(result.data.ok, true); + assert.equal(calls.length, 1); + } finally { + restore(); + } + }); + + it("keeps the actionable x-api-key 403 when fallback headers are also rejected", async () => { const { calls, restore } = mockFetch((call, idx) => { if (idx === 1) { assert.equal(call.headers["x-api-key"], "test-api-key"); assert.equal(call.headers["x-user-api-key"], undefined); + assert.equal(call.headers["x-org-api-key"], undefined); return { status: 403, data: { @@ -552,8 +706,19 @@ describe("composio-direct Teleton integration", () => { }; } + if (idx === 2) { + assert.equal(call.headers["x-api-key"], undefined); + assert.equal(call.headers["x-user-api-key"], "test-api-key"); + assert.equal(call.headers["x-org-api-key"], undefined); + return { + status: 401, + data: { message: "Unauthorized" }, + }; + } + assert.equal(call.headers["x-api-key"], undefined); - assert.equal(call.headers["x-user-api-key"], "test-api-key"); + assert.equal(call.headers["x-user-api-key"], undefined); + assert.equal(call.headers["x-org-api-key"], "test-api-key"); return { status: 401, data: { message: "Unauthorized" }, @@ -571,7 +736,36 @@ describe("composio-direct Teleton integration", () => { assert.equal(result.auth, undefined); assert.match(result.error, /permission denied/i); assert.match(result.error, /permissions/i); - assert.equal(calls.length, 2); + assert.equal(calls.length, 3); + } finally { + restore(); + } + }); + + it("does not retry files API with user or organization headers", async () => { + const { calls, restore } = mockFetch((call) => { + assert.equal(call.headers["x-api-key"], "test-api-key"); + assert.equal(call.headers["x-user-api-key"], undefined); + assert.equal(call.headers["x-org-api-key"], undefined); + return { + status: 403, + data: { + error: { + message: "Project API key required.", + slug: "HTTP_Forbidden", + status: 403, + }, + }, + }; + }); + + try { + const listFiles = toolsFactory(makeSdk()).find((tool) => tool.name === "composio_list_files"); + const result = await listFiles.execute({ toolkit: "gmail" }, makeContext()); + + assert.equal(result.success, false); + assert.match(result.error, /Project API key required|permission denied/i); + assert.equal(calls.length, 1); } finally { restore(); }