Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion demo/mcp/corsair.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dotenv/config';
import { github } from '@corsair-dev/github';
import { gmail } from '@corsair-dev/gmail';
import { linear } from '@corsair-dev/linear';
// import { googlecalendar } from '@corsair-dev/googlecalendar'
import { slack } from '@corsair-dev/slack';
Expand Down
60 changes: 60 additions & 0 deletions packages/adrapid/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http';
import { request } from 'corsair/http';

export class AdrapidAPIError extends Error {
constructor(
message: string,
public readonly code?: string,
) {
super(message);
this.name = 'AdrapidAPIError';
}
}

// TODO: Update with your API base URL
const ADRAPID_API_BASE = 'https://api.example.com';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Endpoint uses placeholder API host

When a consumer invokes adrapid.api.example.get, the client sends the request to https://api.example.com rather than an Adrapid API. The plugin's only endpoint therefore fails instead of returning the requested provider resource.

Rule Used: Flag boilerplate residue from the plugin generator... (source)

Knowledge Base Used: The provider-plugin package pattern

Comment on lines +14 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Replace the placeholder API base URL before release.

Every endpoint request uses https://api.example.com. The Adrapid endpoint cannot reach the provider with this value. Configure the documented Adrapid API base URL before registering this plugin.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adrapid/client.ts` around lines 14 - 15, Replace the placeholder
value of ADRAPID_API_BASE with the documented Adrapid API base URL, removing the
example endpoint before plugin registration.


export async function makeAdrapidRequest<T>(
endpoint: string,
apiKey: string,
options: {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
body?: Record<string, unknown>;
query?: Record<string, string | number | boolean | undefined>;
} = {},
): Promise<T> {
const { method = 'GET', body, query } = options;

const config: OpenAPIConfig = {
BASE: ADRAPID_API_BASE,
VERSION: '1.0.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'omit',
TOKEN: apiKey,
HEADERS: {
'Content-Type': 'application/json',
// TODO: Add authentication headers
// 'Authorization': \`Bearer \${apiKey}\`
},
};

const requestOptions: ApiRequestOptions = {
method,
url: endpoint,
body:
method === 'POST' || method === 'PUT' || method === 'PATCH'
? body
: undefined,
mediaType: 'application/json; charset=utf-8',
query: method === 'GET' ? query : undefined,
};

try {
return await request<T>(config, requestOptions);
} catch (error) {
if (error instanceof Error) {
throw new AdrapidAPIError(error.message);
}
throw new AdrapidAPIError('Unknown error');
Comment on lines +54 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline packages/adrapid --items all --type class,function
rg -n -C 5 'makeAdrapidRequest|instanceof ApiError|retryAfter|errorHandlers' packages/adrapid

Repository: corsairdev/corsair

Length of output: 6371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- packages/adrapid/client.ts ---'
cat -n packages/adrapid/client.ts

printf '%s\n' '--- packages/adrapid/error-handlers.ts ---'
cat -n packages/adrapid/error-handlers.ts

printf '%s\n' '--- ApiError definition and request implementation ---'
rg -n -C 8 'class ApiError|export class ApiError|function request|const request|retryAfter' packages --glob '*.{ts,tsx,js,jsx}'

Repository: corsairdev/corsair

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Adrapid client ---'
sed -n '1,110p' packages/adrapid/client.ts

printf '%s\n' '--- Adrapid handlers ---'
sed -n '1,80p' packages/adrapid/error-handlers.ts

printf '%s\n' '--- Core ApiError/request locations ---'
rg -l 'export class ApiError|class ApiError|export async function request|async function request' packages/corsair --glob '*.{ts,tsx}' \
  | head -n 30

printf '%s\n' '--- Core ApiError/request definitions ---'
rg -n -C 12 'export class ApiError|class ApiError|export async function request|async function request' packages/corsair --glob '*.{ts,tsx}' \
  | head -n 240

Repository: corsairdev/corsair

Length of output: 4514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ApiError implementation ---'
sed -n '100,180p' packages/corsair/async-core/ApiError.ts

printf '%s\n' '--- corsair/http exports ---'
rg -n -C 5 'ApiError|request' packages/corsair --glob 'index.ts' --glob '*http*' --glob '*request*' \
  | head -n 240

printf '%s\n' '--- request call and error construction ---'
rg -n -C 8 'new ApiError|request<|request\(' packages/corsair/async-core packages/corsair/http --glob '*.{ts,tsx}' \
  | head -n 300

Repository: corsairdev/corsair

Length of output: 19300


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- corsair/http export path ---'
fd -i 'http|index.ts|package.json' packages/corsair | head -n 120
rg -n -C 4 'async-core/(ApiError|request)|from .*ApiError|export .*ApiError|corsair/http' packages/corsair packages/adrapid \
  --glob '*.{ts,tsx,json}' | head -n 220

printf '%s\n' '--- Static behavior check ---'
python3 - <<'PY'
from pathlib import Path

client = Path("packages/adrapid/client.ts").read_text()
handlers = Path("packages/adrapid/error-handlers.ts").read_text()
request = Path("packages/corsair/async-core/request.ts").read_text()
api_error = Path("packages/corsair/async-core/ApiError.ts").read_text()

checks = {
    "client wraps Error": "throw new AdrapidAPIError(error.message)" in client,
    "client does not import ApiError": "ApiError" not in client.split("export async function", 1)[0],
    "ApiError exposes status": "public readonly status: number;" in api_error,
    "ApiError exposes retryAfter": "public readonly retryAfter?: number;" in api_error,
    "request throws ApiError": "throw new ApiError(options, result" in request,
    "rate handler checks ApiError": "error instanceof ApiError && error.status === 429" in handlers,
    "rate handler reads retryAfter": "error instanceof ApiError && error.retryAfter !== undefined" in handlers,
    "auth handler checks ApiError": "error instanceof ApiError && error.status === 401" in handlers,
    "placeholder base URL remains": "https://api.example.com" in client,
}

for name, passed in checks.items():
    print(f"{name}: {'yes' if passed else 'no'}")
PY

Repository: corsairdev/corsair

Length of output: 11618


Use the Adrapid API URL and preserve ApiError metadata.

  • Replace ADRAPID_API_BASE = 'https://api.example.com' with the Adrapid API base URL. Current requests target a placeholder.
  • Re-throw ApiError before wrapping other errors. The wrapper removes status and retryAfter, so the error handlers cannot use status checks or the server-provided retry delay.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adrapid/client.ts` around lines 54 - 58, Update the ADRAPID_API_BASE
constant to the actual Adrapid API base URL instead of the placeholder. In the
error handling around the client request, re-throw existing ApiError instances
before wrapping other Error values, preserving their status and retryAfter
metadata; retain the unknown-error fallback for non-Error values.

}
}
18 changes: 18 additions & 0 deletions packages/adrapid/endpoints/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { logEventFromContext } from 'corsair/core';
import type { AdrapidEndpoints } from '..';
import { makeAdrapidRequest } from '../client';
import type { AdrapidEndpointOutputs } from './types';

export const get: AdrapidEndpoints['exampleGet'] = async (ctx, input) => {
const response = await makeAdrapidRequest<
AdrapidEndpointOutputs['exampleGet']
>(`example/${input.id}`, ctx.key, { method: 'GET' });

Comment on lines +7 to +10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Endpoint has no behavioral test

The package's only test checks schema metadata and never invokes this implemented endpoint. As a result, the placeholder host and request behavior pass the package test suite without any endpoint assertion, contrary to the required plugin test coverage.

Rule Used: Plugin packages must include at least one *.test.t... (source)

Knowledge Base Used: The provider-plugin package pattern

await logEventFromContext(
ctx,
'adrapid.example.get',
{ ...input },
'completed',
);
return response;
Comment on lines +4 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline packages/adrapid --items all --type function
rg -n -C 4 'makeAdrapidRequest|AdrapidEndpointOutputSchemas|\.parse\(' packages/adrapid

Repository: corsairdev/corsair

Length of output: 4095


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- packages/adrapid/client.ts ---'
cat -n packages/adrapid/client.ts
printf '%s\n' '--- packages/adrapid/endpoints/types.ts ---'
cat -n packages/adrapid/endpoints/types.ts
printf '%s\n' '--- packages/adrapid/endpoints/example.ts ---'
cat -n packages/adrapid/endpoints/example.ts
printf '%s\n' '--- endpoint registration and invocation ---'
rg -n -C 6 'adrapidEndpointSchemas|adrapidEndpointsNested|exampleGet|RequiredPluginEndpointSchemas|output.*parse|parse.*output' packages/adrapid packages/corsair

Repository: corsairdev/corsair

Length of output: 15720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- endpoint schema consumers ---'
rg -n -C 8 'endpointSchemas|\.output\b|outputSchema|safeParse|parseAsync|safeParseAsync' packages/corsair packages --glob '*.{ts,tsx,js,jsx}' | head -n 500
printf '%s\n' '--- endpoint execution definitions ---'
rg -n -C 10 'execute.*endpoint|invoke.*endpoint|endpoints.*schemas|EndpointTree|CorsairEndpoint' packages/corsair/core packages/corsair --glob '*.{ts,tsx}'

Repository: corsairdev/corsair

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- all endpointSchemas references ---'
rg -l 'endpointSchemas' packages --glob '*.{ts,tsx,js,jsx}' | sort
printf '%s\n' '--- schema parsing references in Corsair core ---'
rg -n 'safeParse|parseAsync|safeParseAsync|\.parse\(' packages/corsair/core packages/corsair/http packages/corsair/client packages/corsair --glob '*.{ts,tsx}' --glob '!tests/**' | head -n 300
printf '%s\n' '--- binding and plugin construction ---'
rg -n -C 12 'bindEndpoints|function bind|const bind|endpoints:' packages/corsair/core packages/corsair --glob '*.{ts,tsx}' --glob '!tests/**' | head -n 500

Repository: corsairdev/corsair

Length of output: 7702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Corsair core files ---'
git ls-files packages/corsair/core | sed -n '1,240p'
printf '%s\n' '--- endpoint schema references in core ---'
rg -n -C 12 'endpointSchemas' packages/corsair/core
printf '%s\n' '--- endpoint binding references ---'
rg -n -C 10 'bindEndpoints|BoundEndpointFn|enforcePermission|endpointPath' packages/corsair --glob '*.{ts,tsx}' --glob '!tests/**' | head -n 500

Repository: corsairdev/corsair

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

example = Path("packages/adrapid/endpoints/example.ts").read_text()
client = Path("packages/adrapid/client.ts").read_text()
plugins = Path("packages/corsair/core/plugins/index.ts").read_text()
inspect = Path("packages/corsair/core/inspect/index.ts").read_text()
bind = Path("packages/corsair/core/endpoints/bind.ts").read_text()

checks = {
    "endpoint calls typed request helper": "makeAdrapidRequest<" in example,
    "endpoint imports output schema": "AdrapidEndpointOutputSchemas" in example,
    "endpoint parses response": ".parse(" in example or ".safeParse(" in example,
    "helper returns request result directly": "return await request<T>" in client,
    "plugin docs describe schemas as inspection metadata": "Used by get_schema()" in plugins,
    "inspection converts output schema to docs": "zodToFormSchema(schemas.output)" in inspect,
    "binding layer references endpoint schemas": "endpointSchemas" in bind,
}

for name, result in checks.items():
    print(f"{name}: {result}")

assert checks["endpoint calls typed request helper"]
assert not checks["endpoint imports output schema"]
assert not checks["endpoint parses response"]
assert checks["helper returns request result directly"]
assert checks["plugin docs describe schemas as inspection metadata"]
assert checks["inspection converts output schema to docs"]
assert not checks["binding layer references endpoint schemas"]
PY

Repository: corsairdev/corsair

Length of output: 468


Validate the provider response before logging completion.

makeAdrapidRequest<AdrapidEndpointOutputs['exampleGet']> provides only a static type. Parse the response with AdrapidEndpointOutputSchemas.exampleGet so malformed provider data cannot reach consumers or produce a completed event.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adrapid/endpoints/example.ts` around lines 4 - 17, Validate the
response in the get endpoint using AdrapidEndpointOutputSchemas.exampleGet
immediately after makeAdrapidRequest and before logEventFromContext. Use the
parsed result as the returned response, so malformed provider data is rejected
before consumers receive it or the completed event is logged.

};
7 changes: 7 additions & 0 deletions packages/adrapid/endpoints/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { get as exampleGet } from './example';

export const Example = {
get: exampleGet,
};

export * from './types';
29 changes: 29 additions & 0 deletions packages/adrapid/endpoints/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from 'zod';

const ExampleGetInputSchema = z.object({
id: z.string(),
});

export type ExampleGetInput = z.infer<typeof ExampleGetInputSchema>;

const ExampleGetResponseSchema = z.object({
id: z.string(),
});

export type ExampleGetResponse = z.infer<typeof ExampleGetResponseSchema>;

export type AdrapidEndpointInputs = {
exampleGet: ExampleGetInput;
};

export type AdrapidEndpointOutputs = {
exampleGet: ExampleGetResponse;
};

export const AdrapidEndpointInputSchemas = {
exampleGet: ExampleGetInputSchema,
} as const;

export const AdrapidEndpointOutputSchemas = {
exampleGet: ExampleGetResponseSchema,
} as const;
31 changes: 31 additions & 0 deletions packages/adrapid/error-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { CorsairErrorHandler } from 'corsair/core';
import { ApiError } from 'corsair/http';

export const errorHandlers = {
RATE_LIMIT_ERROR: {
match: (error: Error) => {
if (error instanceof ApiError && error.status === 429) return true;
const msg = error.message.toLowerCase();
return msg.includes('rate_limited') || msg.includes('429');
},
handler: async (error: Error) => {
let retryAfterMs: number | undefined;
if (error instanceof ApiError && error.retryAfter !== undefined) {
retryAfterMs = error.retryAfter;
}
return { maxRetries: 5, headersRetryAfterMs: retryAfterMs };
},
},
AUTH_ERROR: {
match: (error: Error) => {
if (error instanceof ApiError && error.status === 401) return true;
const msg = error.message.toLowerCase();
return msg.includes('unauthorized') || msg.includes('invalid_auth');
},
handler: async () => ({ maxRetries: 0 }),
},
DEFAULT: {
match: () => true,
handler: async () => ({ maxRetries: 0 }),
},
} satisfies CorsairErrorHandler;
206 changes: 206 additions & 0 deletions packages/adrapid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import type {
AuthTypes,
BindEndpoints,
BindWebhooks,
CorsairEndpoint,
CorsairErrorHandler,
CorsairPlugin,
CorsairPluginContext,
CorsairWebhook,
KeyBuilderContext,
PickAuth,
PluginAuthConfig,
PluginPermissionsConfig,
RequiredPluginEndpointMeta,
RequiredPluginEndpointSchemas,
RequiredPluginWebhookSchemas,
} from 'corsair/core';
import { Example } from './endpoints';
import type {
AdrapidEndpointInputs,
AdrapidEndpointOutputs,
} from './endpoints/types';
import {
AdrapidEndpointInputSchemas,
AdrapidEndpointOutputSchemas,
} from './endpoints/types';
import { errorHandlers } from './error-handlers';
import { AdrapidSchema } from './schema';
import { ExampleWebhooks } from './webhooks';
import { resolveAdrapidOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link';
import { matchAdrapidTenantWebhook } from './webhooks/tenant-matcher';
import type { AdrapidWebhookOutputs, ExampleEvent } from './webhooks/types';
import { ExampleEventSchema } from './webhooks/types';

export type AdrapidPluginOptions = {
authType?: PickAuth<'api_key' | 'oauth_2'>;
key?: string;
webhookSecret?: string;
hooks?: InternalAdrapidPlugin['hooks'];
webhookHooks?: InternalAdrapidPlugin['webhookHooks'];
errorHandlers?: CorsairErrorHandler;
permissions?: PluginPermissionsConfig<typeof adrapidEndpointsNested>;
};

export type AdrapidContext = CorsairPluginContext<
typeof AdrapidSchema,
AdrapidPluginOptions
>;

export type AdrapidKeyBuilderContext = KeyBuilderContext<AdrapidPluginOptions>;

export type AdrapidBoundEndpoints = BindEndpoints<
typeof adrapidEndpointsNested
>;

type AdrapidEndpoint<K extends keyof AdrapidEndpointOutputs> = CorsairEndpoint<
AdrapidContext,
AdrapidEndpointInputs[K],
AdrapidEndpointOutputs[K]
>;

export type AdrapidEndpoints = {
exampleGet: AdrapidEndpoint<'exampleGet'>;
};

type AdrapidWebhook<
K extends keyof AdrapidWebhookOutputs,
TEvent,
> = CorsairWebhook<AdrapidContext, TEvent, AdrapidWebhookOutputs[K]>;

export type AdrapidWebhooks = {
example: AdrapidWebhook<'example', ExampleEvent>;
};

export type AdrapidBoundWebhooks = BindWebhooks<AdrapidWebhooks>;

const adrapidEndpointsNested = {
example: {
get: Example.get,
},
} as const;

const adrapidWebhooksNested = {
example: {
example: ExampleWebhooks.example,
},
} as const;

export const adrapidEndpointSchemas = {
'example.get': {
input: AdrapidEndpointInputSchemas.exampleGet,
output: AdrapidEndpointOutputSchemas.exampleGet,
},
} as const satisfies RequiredPluginEndpointSchemas<
typeof adrapidEndpointsNested
>;

const adrapidWebhookSchemas = {
'example.example': {
description: 'An example webhook event',
payload: ExampleEventSchema,
response: ExampleEventSchema,
},
} as const satisfies RequiredPluginWebhookSchemas<typeof adrapidWebhooksNested>;

const defaultAuthType: AuthTypes = 'api_key' as const;

const adrapidEndpointMeta = {
'example.get': {
riskLevel: 'read',
description: 'Get an example resource by ID',
},
} as const satisfies RequiredPluginEndpointMeta<typeof adrapidEndpointsNested>;

export const adrapidAuthConfig = {
api_key: {
account: ['tenant_external_id'] as const,
},
oauth_2: {
account: ['tenant_external_id'] as const,
},
} as const satisfies PluginAuthConfig;
Comment on lines +115 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- plugin files ---'
git ls-files 'packages/adrapid/**' '.github/PLUGIN_PR_RULES.md' 'packages/corsair/core/constants.ts'

printf '%s\n' '--- relevant source ---'
for f in packages/adrapid/index.ts \
         packages/adrapid/webhooks/tenant-matcher.ts \
         packages/adrapid/webhooks/oauth-tenant-link.ts; do
  if [ -f "$f" ]; then
    printf '\n### %s\n' "$f"
    cat -n "$f"
  fi
done

printf '%s\n' '--- adrapid references ---'
rg -n -C 3 'tenant_external_id|tenant-matcher|oauth-tenant-link|adrapid' packages .github \
  -g '!node_modules' -g '!dist' -g '!build' || true

printf '%s\n' '--- plugin rules ---'
if [ -f .github/PLUGIN_PR_RULES.md ]; then
  cat -n .github/PLUGIN_PR_RULES.md
fi

Repository: corsairdev/corsair

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- plugin rules ---'
cat -n .github/PLUGIN_PR_RULES.md

printf '%s\n' '--- adrapid package metadata and docs ---'
for f in packages/adrapid/package.json packages/adrapid/schema/database.ts packages/adrapid/schema/index.ts packages/adrapid/webhooks/types.ts packages/adrapid/schema.test.ts; do
  printf '\n### %s\n' "$f"
  cat -n "$f"
done
find packages/adrapid -maxdepth 2 -type f \( -iname 'README*' -o -iname '*.md' \) -print

printf '%s\n' '--- comparable tenant implementations ---'
for f in packages/{slack,gitlab,github,hubspot,linear}/webhooks/tenant-matcher.ts \
         packages/{slack,gitlab,github,hubspot,linear}/webhooks/oauth-tenant-link.ts; do
  if [ -f "$f" ]; then
    printf '\n### %s\n' "$f"
    cat -n "$f"
  fi
done

printf '%s\n' '--- core tenant-link call sites and types ---'
rg -n -C 5 'oauthWebhookTenantLinkResolver|pluginTenantWebhookMatcher|WebhookTenantMatch|authConfig\.account|tenant_external_id' packages/corsair packages -g '*.ts' -g '*.tsx' \
  | head -n 1200

Repository: corsairdev/corsair

Length of output: 50375


🌐 Web query:

Adrapid API OAuth webhook tenant identifier tenant_external_id webhook payload

💡 Result:

The Adrapid platform provides a REST API primarily authenticated via bearer tokens [1][2] and supports OAuth-based integration through third-party platforms [3]. While Adrapid documentation outlines tools for managing banners, templates, and user access [2][4], there is no public technical documentation confirming a standardized internal API schema that utilizes a specific "tenant_external_id" field within a native webhook payload. For integrations involving Adrapid, tenant management and webhook handling are typically managed through the following mechanisms: 1. Integration Layer: Platforms that integrate with Adrapid (such as automation tools or middleware) often abstract the webhook handling [3][5]. In these environments, identifiers like tenant IDs are managed by the integration provider's own infrastructure rather than natively as a required field in the Adrapid API request payload [5]. 2. Administrative API: Access and user-specific configurations are handled via the Adrapid administrative API, which includes endpoints for retrieving user-specific access URLs and widget configurations [4]. 3. Webhook Handling: If you are implementing custom webhooks to receive notifications from Adrapid, you are responsible for defining the endpoint. Since Adrapid documentation does not explicitly document a "tenant_external_id" as a mandatory part of its outgoing webhook payload, you should inspect the raw JSON payload sent to your defined endpoint to determine which identifiers (such as user ID or account ID) are available for your mapping needs. If you are developing a multi-tenant application, it is standard practice to map the Adrapid User ID or an internal account identifier to your own "tenant_external_id" within your database upon receiving a webhook, rather than expecting that specific field to be natively present in the payload. [1][4][5]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- OAuth link persistence path ---'
sed -n '1,180p' packages/corsair/webhooks/resolve-oauth-tenant-link.ts
sed -n '1,280p' packages/corsair/webhooks/tenant-links.ts

printf '%s\n' '--- exact tenant-link type and auth configuration definitions ---'
rg -n -A 18 -B 8 'export type WebhookTenantMatch|type WebhookTenantMatch|export type PluginAuthConfig|interface PluginAuthConfig|account\??:' packages/corsair/core packages/corsair -g '*.ts' | head -n 500

printf '%s\n' '--- adrapid TODO and placeholder inventory ---'
rg -n 'TODO|api\.example\.com|tenant_external_id|example' packages/adrapid

printf '%s\n' '--- tracked test inventory for adrapid ---'
git ls-files 'packages/adrapid/**/*test.ts' 'packages/adrapid/**/*.test.ts' | sort

printf '%s\n' '--- bounded behavioral probe of the shown extractors ---'
python3 - <<'PY'
from typing import Any

def first_string(values):
    return next((v for v in values if isinstance(v, str) and v), None)

def as_record(value):
    return value if isinstance(value, dict) else None

def matcher(body: Any):
    if not isinstance(body, dict):
        return None
    external_id = first_string([
        body.get("tenant_external_id"),
        (as_record(body.get("data")) or {}).get("tenant_external_id"),
    ])
    return None if not external_id else {
        "linkType": "tenant_external_id",
        "externalId": external_id,
    }

def resolver(tokens: dict):
    value = tokens.get("tenant_external_id")
    external_id = value if isinstance(value, (str, int, float)) and not isinstance(value, bool) and str(value) else None
    if external_id is not None:
        return {"linkType": "tenant_external_id", "externalId": str(external_id)}
    if not tokens.get("access_token"):
        return None
    return None

cases = [
    ("documented example payload", {"type": "example", "created_at": "x", "data": {"id": "acct-1"}}),
    ("placeholder top-level field", {"tenant_external_id": "acct-1"}),
    ("placeholder nested field", {"data": {"tenant_external_id": "acct-1"}}),
]
for name, body in cases:
    print(name, "=>", matcher(body))

for name, tokens in [
    ("token has no placeholder id", {"access_token": "token"}),
    ("token has placeholder id", {"access_token": "token", "tenant_external_id": "acct-1"}),
    ("token has no access token", {}),
]:
    print(name, "=>", resolver(tokens))
PY

Repository: corsairdev/corsair

Length of output: 49034


Implement provider-specific tenant linkage before enabling tenant routing.

adrapidAuthConfig registers the placeholder tenant_external_id. The matcher only reads that placeholder, while the webhook schema defines data.id. When OAuth tokens omit tenant_external_id, the resolver returns null, so OAuth does not store a webhook link. Determine the provider’s stable account identifier, then use the same link type in adrapidAuthConfig, the webhook matcher, and the OAuth resolver. Remove the placeholder URL and TODO stubs.

📍 Affects 3 files
  • packages/adrapid/index.ts#L115-L122 (this comment)
  • packages/adrapid/webhooks/tenant-matcher.ts#L17-L24
  • packages/adrapid/webhooks/oauth-tenant-link.ts#L9-L30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adrapid/index.ts` around lines 115 - 122, Determine AdRapid’s stable
account identifier and use that single link type consistently: update
adrapidAuthConfig in packages/adrapid/index.ts (115-122), the webhook matcher in
packages/adrapid/webhooks/tenant-matcher.ts (17-24), and the OAuth resolver in
packages/adrapid/webhooks/oauth-tenant-link.ts (9-30). Ensure OAuth tokens
resolve and store the webhook link using the provider’s webhook
data.id-compatible identifier, and remove the placeholder URL and TODO stubs.


export type BaseAdrapidPlugin<T extends AdrapidPluginOptions> = CorsairPlugin<
'adrapid',
typeof AdrapidSchema,
typeof adrapidEndpointsNested,
typeof adrapidWebhooksNested,
T,
typeof defaultAuthType
>;

export type InternalAdrapidPlugin = BaseAdrapidPlugin<AdrapidPluginOptions>;

export type ExternalAdrapidPlugin<T extends AdrapidPluginOptions> =
BaseAdrapidPlugin<T>;

export function adrapid<const T extends AdrapidPluginOptions>(
incomingOptions: AdrapidPluginOptions & T = {} as AdrapidPluginOptions & T,
): ExternalAdrapidPlugin<T> {
const options = {
...incomingOptions,
authType: incomingOptions.authType ?? defaultAuthType,
};
return {
id: 'adrapid',
authConfig: adrapidAuthConfig,
schema: AdrapidSchema,
options: options,
hooks: options.hooks,
webhookHooks: options.webhookHooks,
endpoints: adrapidEndpointsNested,
webhooks: adrapidWebhooksNested,
endpointMeta: adrapidEndpointMeta,
endpointSchemas: adrapidEndpointSchemas,
webhookSchemas: adrapidWebhookSchemas,
pluginWebhookMatcher: (request) => {
const headers = request.headers;
// TODO: Update to match your webhook signature headers
return 'x-adrapid-signature' in headers;
},
pluginTenantWebhookMatcher: matchAdrapidTenantWebhook,
oauthWebhookTenantLinkResolver: resolveAdrapidOAuthWebhookTenantLink,
errorHandlers: {
...errorHandlers,
...options.errorHandlers,
},
keyBuilder: async (ctx: AdrapidKeyBuilderContext, source) => {
if (source === 'webhook' && options.webhookSecret) {
return options.webhookSecret;
}

if (source === 'webhook') {
const res = await ctx.keys.get_webhook_signature();
return res ?? '';
}

if (source === 'endpoint' && options.key) {
return options.key;
}

if (source === 'endpoint' && ctx.authType === 'api_key') {
const res = await ctx.keys.get_api_key();
return res ?? '';
}

if (source === 'endpoint' && ctx.authType === 'oauth_2') {
const res = await ctx.keys.get_access_token();
return res ?? '';
}

return '';
},
} satisfies InternalAdrapidPlugin;
}

export type {
AdrapidEndpointInputs,
AdrapidEndpointOutputs,
ExampleGetInput,
ExampleGetResponse,
} from './endpoints/types';
export type {
AdrapidWebhookOutputs,
ExampleEvent,
} from './webhooks/types';
Loading
Loading