Skip to content
Draft
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
3 changes: 3 additions & 0 deletions packages/corsair/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export const BaseProviders = [
'tavilymcp',
'teams',
'telegram',
'textrazor',
'todoist',
'toggl',
'trello',
Expand Down Expand Up @@ -362,6 +363,7 @@ export const ProviderDisplayNames = {
tavilymcp: 'Tavily MCP',
teams: 'Teams',
telegram: 'Telegram',
textrazor: 'Textrazor',
todoist: 'Todoist',
toggl: 'Toggl',
trello: 'Trello',
Expand Down Expand Up @@ -557,6 +559,7 @@ export type AllProviders =
| 'tavilymcp'
| 'teams'
| 'telegram'
| 'textrazor'
| 'todoist'
| 'toggl'
| 'trello'
Expand Down
60 changes: 60 additions & 0 deletions packages/textrazor/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 TextrazorAPIError extends Error {
constructor(
message: string,
public readonly code?: string,
) {
super(message);
this.name = 'TextrazorAPIError';
}
}

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

export async function makeTextrazorRequest<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: TEXTRAZOR_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 TextrazorAPIError(error.message);
}
throw new TextrazorAPIError('Unknown error');
}
}
18 changes: 18 additions & 0 deletions packages/textrazor/endpoints/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { logEventFromContext } from 'corsair/core';
import type { TextrazorEndpoints } from '..';
import { makeTextrazorRequest } from '../client';
import type { TextrazorEndpointOutputs } from './types';

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

await logEventFromContext(
ctx,
'textrazor.example.get',
{ ...input },
'completed',
);
return response;
};
7 changes: 7 additions & 0 deletions packages/textrazor/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/textrazor/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 TextrazorEndpointInputs = {
exampleGet: ExampleGetInput;
};

export type TextrazorEndpointOutputs = {
exampleGet: ExampleGetResponse;
};

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

export const TextrazorEndpointOutputSchemas = {
exampleGet: ExampleGetResponseSchema,
} as const;
31 changes: 31 additions & 0 deletions packages/textrazor/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;
215 changes: 215 additions & 0 deletions packages/textrazor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
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 {
TextrazorEndpointInputs,
TextrazorEndpointOutputs,
} from './endpoints/types';
import {
TextrazorEndpointInputSchemas,
TextrazorEndpointOutputSchemas,
} from './endpoints/types';
import { errorHandlers } from './error-handlers';
import { TextrazorSchema } from './schema';
import { ExampleWebhooks } from './webhooks';
import { resolveTextrazorOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link';
import { matchTextrazorTenantWebhook } from './webhooks/tenant-matcher';
import type { ExampleEvent, TextrazorWebhookOutputs } from './webhooks/types';
import { ExampleEventSchema } from './webhooks/types';

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

export type TextrazorContext = CorsairPluginContext<
typeof TextrazorSchema,
TextrazorPluginOptions
>;

export type TextrazorKeyBuilderContext =
KeyBuilderContext<TextrazorPluginOptions>;

export type TextrazorBoundEndpoints = BindEndpoints<
typeof textrazorEndpointsNested
>;

type TextrazorEndpoint<K extends keyof TextrazorEndpointOutputs> =
CorsairEndpoint<
TextrazorContext,
TextrazorEndpointInputs[K],
TextrazorEndpointOutputs[K]
>;

export type TextrazorEndpoints = {
exampleGet: TextrazorEndpoint<'exampleGet'>;
};

type TextrazorWebhook<
K extends keyof TextrazorWebhookOutputs,
TEvent,
> = CorsairWebhook<TextrazorContext, TEvent, TextrazorWebhookOutputs[K]>;

export type TextrazorWebhooks = {
example: TextrazorWebhook<'example', ExampleEvent>;
};

export type TextrazorBoundWebhooks = BindWebhooks<TextrazorWebhooks>;

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

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

export const textrazorEndpointSchemas = {
'example.get': {
input: TextrazorEndpointInputSchemas.exampleGet,
output: TextrazorEndpointOutputSchemas.exampleGet,
},
} as const satisfies RequiredPluginEndpointSchemas<
typeof textrazorEndpointsNested
>;

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

const defaultAuthType: AuthTypes = 'api_key' as const;

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

export const textrazorAuthConfig = {
api_key: {
account: ['tenant_external_id'] as const,
},
oauth_2: {
account: ['tenant_external_id'] as const,
},
} as const satisfies PluginAuthConfig;

export type BaseTextrazorPlugin<T extends TextrazorPluginOptions> =
CorsairPlugin<
'textrazor',
typeof TextrazorSchema,
typeof textrazorEndpointsNested,
typeof textrazorWebhooksNested,
T,
typeof defaultAuthType
>;

export type InternalTextrazorPlugin =
BaseTextrazorPlugin<TextrazorPluginOptions>;

export type ExternalTextrazorPlugin<T extends TextrazorPluginOptions> =
BaseTextrazorPlugin<T>;

export function textrazor<const T extends TextrazorPluginOptions>(
incomingOptions: TextrazorPluginOptions & T = {} as TextrazorPluginOptions &
T,
): ExternalTextrazorPlugin<T> {
const options = {
...incomingOptions,
authType: incomingOptions.authType ?? defaultAuthType,
};
return {
id: 'textrazor',
authConfig: textrazorAuthConfig,
schema: TextrazorSchema,
options: options,
hooks: options.hooks,
webhookHooks: options.webhookHooks,
endpoints: textrazorEndpointsNested,
webhooks: textrazorWebhooksNested,
endpointMeta: textrazorEndpointMeta,
endpointSchemas: textrazorEndpointSchemas,
webhookSchemas: textrazorWebhookSchemas,
pluginWebhookMatcher: (request) => {
const headers = request.headers;
// TODO: Update to match your webhook signature headers
return 'x-textrazor-signature' in headers;
},
pluginTenantWebhookMatcher: matchTextrazorTenantWebhook,
oauthWebhookTenantLinkResolver: resolveTextrazorOAuthWebhookTenantLink,
errorHandlers: {
...errorHandlers,
...options.errorHandlers,
},
keyBuilder: async (ctx: TextrazorKeyBuilderContext, 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 InternalTextrazorPlugin;
}

export type {
ExampleGetInput,
ExampleGetResponse,
TextrazorEndpointInputs,
TextrazorEndpointOutputs,
} from './endpoints/types';
export type {
ExampleEvent,
TextrazorWebhookOutputs,
} from './webhooks/types';
Loading
Loading