-
Notifications
You must be signed in to change notification settings - Fork 291
feat: add Adrapid plugin #653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||
|
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🤖 Prompt for AI Agents |
||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/adrapidRepository: 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 240Repository: 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 300Repository: 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'}")
PYRepository: corsairdev/corsair Length of output: 11618 Use the Adrapid API URL and preserve
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/adrapidRepository: 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/corsairRepository: 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 500Repository: 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 500Repository: 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"]
PYRepository: corsairdev/corsair Length of output: 468 Validate the provider response before logging completion.
🤖 Prompt for AI Agents |
||
| }; | ||
| 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'; |
| 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; |
| 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; |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
fiRepository: 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 1200Repository: corsairdev/corsair Length of output: 50375 🌐 Web query:
💡 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))
PYRepository: corsairdev/corsair Length of output: 49034 Implement provider-specific tenant linkage before enabling tenant routing.
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
|
|
||
| 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'; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a consumer invokes
adrapid.api.example.get, the client sends the request tohttps://api.example.comrather 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