diff --git a/packages/bunnycdn/client.ts b/packages/bunnycdn/client.ts new file mode 100644 index 000000000..38fda4af0 --- /dev/null +++ b/packages/bunnycdn/client.ts @@ -0,0 +1,61 @@ +import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; +import { request } from 'corsair/http'; + +export class BunnycdnAPIError extends Error { + constructor( + message: string, + public readonly code?: string, + ) { + super(message); + this.name = 'BunnycdnAPIError'; + } +} + +const BUNNYCDN_API_BASE = 'https://api.bunny.net'; + +export async function makeBunnycdnRequest( + endpoint: string, + apiKey: string, + options: { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + body?: Record; + query?: Record; + } = {}, +): Promise { + const { method = 'GET', body, query } = options; + + const config: OpenAPIConfig = { + BASE: BUNNYCDN_API_BASE, + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: apiKey, + HEADERS: { + 'Content-Type': 'application/json', + 'AccessKey': 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(config, requestOptions); + } catch (error) { + if (error && typeof error === 'object' && 'status' in error) { + throw error; + } + if (error instanceof Error) { + throw new BunnycdnAPIError(`BunnyCDN API Error: ${error.message}`); + } + throw new BunnycdnAPIError('Unknown error'); + } +} \ No newline at end of file diff --git a/packages/bunnycdn/endpoints/index.ts b/packages/bunnycdn/endpoints/index.ts new file mode 100644 index 000000000..d48036a28 --- /dev/null +++ b/packages/bunnycdn/endpoints/index.ts @@ -0,0 +1,29 @@ +import { makeBunnycdnRequest } from '../client'; +import type { BunnycdnContext } from '../index'; +import type { + PullZone, + PullZoneGetInput, + PullZoneListInput, +} from './types'; + +export const PullZoneEndpoints = { + list: async (ctx: BunnycdnContext, input: PullZoneListInput = {}): Promise => { + const key = (await ctx.keys?.get_api_key()) ?? ctx.options.key ?? ''; + return makeBunnycdnRequest('/pullzone', key, { + method: 'GET', + query: { + page: input.page, + perPage: input.perPage, + }, + }); + }, + + get: async (ctx: BunnycdnContext, input: PullZoneGetInput): Promise => { + const key = (await ctx.keys?.get_api_key()) ?? ctx.options.key ?? ''; + return makeBunnycdnRequest(`/pullzone/${input.id}`, key, { + method: 'GET', + }); + }, +}; + +export * from './types'; \ No newline at end of file diff --git a/packages/bunnycdn/endpoints/types.ts b/packages/bunnycdn/endpoints/types.ts new file mode 100644 index 000000000..94e093303 --- /dev/null +++ b/packages/bunnycdn/endpoints/types.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +const PullZoneListInputSchema = z.object({ + page: z.number().optional(), + perPage: z.number().optional(), +}); + +const PullZoneGetInputSchema = z.object({ + id: z.number(), +}); + +export type PullZoneListInput = z.infer; +export type PullZoneGetInput = z.infer; + +const PullZoneSchema = z.object({ + Id: z.number(), + Name: z.string(), + OriginUrl: z.string().optional(), + Enabled: z.boolean().optional(), + Hostnames: z.array(z.object({ + Id: z.number().optional(), + Value: z.string().optional(), + })).optional(), +}); + +export type PullZone = z.infer; + +export type BunnycdnEndpointInputs = { + pullZoneList: PullZoneListInput; + pullZoneGet: PullZoneGetInput; +}; + +export type BunnycdnEndpointOutputs = { + pullZoneList: PullZone[]; + pullZoneGet: PullZone; +}; + +export const BunnycdnEndpointInputSchemas = { + pullZoneList: PullZoneListInputSchema, + pullZoneGet: PullZoneGetInputSchema, +} as const; + +export const BunnycdnEndpointOutputSchemas = { + pullZoneList: z.array(PullZoneSchema), + pullZoneGet: PullZoneGetInputSchema, +} as const; \ No newline at end of file diff --git a/packages/bunnycdn/error-handlers.ts b/packages/bunnycdn/error-handlers.ts new file mode 100644 index 000000000..c2af29acd --- /dev/null +++ b/packages/bunnycdn/error-handlers.ts @@ -0,0 +1,31 @@ +import { ApiError } from 'corsair/http'; +import type { CorsairErrorHandler } from 'corsair/core'; + +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; diff --git a/packages/bunnycdn/index.ts b/packages/bunnycdn/index.ts new file mode 100644 index 000000000..58660dc98 --- /dev/null +++ b/packages/bunnycdn/index.ts @@ -0,0 +1,202 @@ +import type { + BindEndpoints, + BindWebhooks, + CorsairEndpoint, + CorsairErrorHandler, + CorsairPlugin, + CorsairPluginContext, + CorsairWebhook, + KeyBuilderContext, + PickAuth, + PluginAuthConfig, + PluginPermissionsConfig, + RequiredPluginEndpointMeta, + RequiredPluginEndpointSchemas, + RequiredPluginWebhookSchemas, +} from 'corsair/core'; +import type { AuthTypes } from 'corsair/core'; +import type { BunnycdnEndpointInputs, BunnycdnEndpointOutputs } from './endpoints/types'; +import { BunnycdnEndpointInputSchemas, BunnycdnEndpointOutputSchemas } from './endpoints/types'; +import type { + BunnycdnWebhookOutputs, + ExampleEvent, +} from './webhooks/types'; +import { ExampleEventSchema } from './webhooks/types'; +import { PullZoneEndpoints } from './endpoints'; +import { BunnycdnSchema } from './schema'; +import { ExampleWebhooks } from './webhooks'; +import { errorHandlers } from './error-handlers'; +import { matchBunnycdnTenantWebhook } from './webhooks/tenant-matcher'; + +export type BunnycdnPluginOptions = { + authType?: PickAuth<'api_key'>; + key?: string; + webhookSecret?: string; + hooks?: InternalBunnycdnPlugin['hooks']; + webhookHooks?: InternalBunnycdnPlugin['webhookHooks']; + errorHandlers?: CorsairErrorHandler; + permissions?: PluginPermissionsConfig; +}; + +export type BunnycdnContext = CorsairPluginContext< + typeof BunnycdnSchema, + BunnycdnPluginOptions +>; + +export type BunnycdnKeyBuilderContext = KeyBuilderContext; + +export type BunnycdnBoundEndpoints = BindEndpoints; + +type BunnycdnEndpoint< + K extends keyof BunnycdnEndpointOutputs, +> = CorsairEndpoint< + BunnycdnContext, + BunnycdnEndpointInputs[K], + BunnycdnEndpointOutputs[K] +>; + +export type BunnycdnEndpoints = { + pullZoneList: BunnycdnEndpoint<'pullZoneList'>; + pullZoneGet: BunnycdnEndpoint<'pullZoneGet'>; +}; + +type BunnycdnWebhook< + K extends keyof BunnycdnWebhookOutputs, + TEvent, +> = CorsairWebhook; + +export type BunnycdnWebhooks = { + example: BunnycdnWebhook<'example', ExampleEvent>; +}; + +export type BunnycdnBoundWebhooks = BindWebhooks; + +const bunnycdnEndpointsNested = { + pullZone: { + list: PullZoneEndpoints.list, + get: PullZoneEndpoints.get, + }, +} as const; + +const bunnycdnWebhooksNested = { + example: { + example: ExampleWebhooks.example, + }, +} as const; + +export const bunnycdnEndpointSchemas = { + 'pullZone.list': { + input: BunnycdnEndpointInputSchemas.pullZoneList, + output: BunnycdnEndpointOutputSchemas.pullZoneList, + }, + 'pullZone.get': { + input: BunnycdnEndpointInputSchemas.pullZoneGet, + output: BunnycdnEndpointOutputSchemas.pullZoneGet, + }, +} as const satisfies RequiredPluginEndpointSchemas; + +const bunnycdnWebhookSchemas = { + 'example.example': { + description: 'An example webhook event', + payload: ExampleEventSchema, + response: ExampleEventSchema, + }, +} as const satisfies RequiredPluginWebhookSchemas; + +const defaultAuthType: AuthTypes = 'api_key' as const; + +const bunnycdnEndpointMeta = { + 'pullZone.list': { + riskLevel: 'read', + description: 'Get list of all pull zones', + }, + 'pullZone.get': { + riskLevel: 'read', + description: 'Get details of a specific pull zone by ID', + }, +} as const satisfies RequiredPluginEndpointMeta; + +export const bunnycdnAuthConfig = { + api_key: { + account: ['tenant_external_id'] as const, + }, +} as const satisfies PluginAuthConfig; + +export type BaseBunnycdnPlugin = CorsairPlugin< + 'bunnycdn', + typeof BunnycdnSchema, + typeof bunnycdnEndpointsNested, + typeof bunnycdnWebhooksNested, + T, + typeof defaultAuthType +>; + +export type InternalBunnycdnPlugin = BaseBunnycdnPlugin; + +export type ExternalBunnycdnPlugin = + BaseBunnycdnPlugin; + +export function bunnycdn( + incomingOptions: BunnycdnPluginOptions & T = {} as BunnycdnPluginOptions & T, +): ExternalBunnycdnPlugin { + const options = { + ...incomingOptions, + authType: incomingOptions.authType ?? defaultAuthType, + }; + return { + id: 'bunnycdn', + authConfig: bunnycdnAuthConfig, + schema: BunnycdnSchema, + options: options, + hooks: options.hooks, + webhookHooks: options.webhookHooks, + endpoints: bunnycdnEndpointsNested, + webhooks: bunnycdnWebhooksNested, + endpointMeta: bunnycdnEndpointMeta, + endpointSchemas: bunnycdnEndpointSchemas, + webhookSchemas: bunnycdnWebhookSchemas, + pluginWebhookMatcher: (request) => { + const headers = request.headers; + return 'x-bunnycdn-signature' in headers; + }, + pluginTenantWebhookMatcher: matchBunnycdnTenantWebhook, + errorHandlers: { + ...errorHandlers, + ...options.errorHandlers, + }, + keyBuilder: async (ctx: BunnycdnKeyBuilderContext, 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 ?? ''; + } + + return ''; + }, + } satisfies InternalBunnycdnPlugin; +} + +export type { + ExampleEvent, + BunnycdnWebhookOutputs, +} from './webhooks/types'; + +export type { + BunnycdnEndpointInputs, + BunnycdnEndpointOutputs, + PullZone, + PullZoneGetInput, + PullZoneListInput, +} from './endpoints/types'; \ No newline at end of file diff --git a/packages/bunnycdn/jest.config.cjs b/packages/bunnycdn/jest.config.cjs new file mode 100644 index 000000000..8c6218f64 --- /dev/null +++ b/packages/bunnycdn/jest.config.cjs @@ -0,0 +1,55 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [''], + testMatch: [ + '**/*.test.ts', + '**/tests/**/*.test.ts', + '**/plugins/**/*.test.ts', + '**/setup/**/*.test.ts', + ], + collectCoverageFrom: [ + '**/*.ts', + '!**/*.d.ts', + '!**/node_modules/**', + '!**/dist/**', + '!jest.config.ts', + '!tests/**', + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + transform: { + '^.+\\.yaml$': '/../corsair/jest-yaml-transform.cjs', + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + verbatimModuleSyntax: false, + module: 'ESNext', + moduleResolution: 'Bundler', + }, + }, + ], + '.*\\.js$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + }, + }, + ], + }, + moduleNameMapper: { + '^corsair/core$': '/../corsair/core.ts', + '^corsair/http$': '/../corsair/http.ts', + '^(\\.\\.?/.*)\\.js$': '$1', + }, + transformIgnorePatterns: ['node_modules/(?!.*uuid.*)'], + extensionsToTreatAsEsm: ['.ts'], + testTimeout: 30000, + verbose: true, +}; diff --git a/packages/bunnycdn/package.json b/packages/bunnycdn/package.json new file mode 100644 index 000000000..81cfde167 --- /dev/null +++ b/packages/bunnycdn/package.json @@ -0,0 +1,44 @@ +{ + "name": "@corsair-dev/bunnycdn", + "version": "0.1.0", + "description": "Bunnycdn plugin for Corsair", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "dev-source": "./index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsc --build --force && tsup", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "peerDependencies": { + "corsair": ">=0.1.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "corsair": "workspace:*", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "tsup": "^8.0.1", + "typescript": "catalog:", + "zod": "^4.1.13" + }, + "keywords": [ + "corsair", + "bunnycdn", + "plugin" + ], + "author": "", + "license": "Apache-2.0", + "files": [ + "dist" + ] +} diff --git a/packages/bunnycdn/schema.test.ts b/packages/bunnycdn/schema.test.ts new file mode 100644 index 000000000..e0534ed04 --- /dev/null +++ b/packages/bunnycdn/schema.test.ts @@ -0,0 +1,20 @@ +import { BunnycdnSchema } from './schema'; + +describe('Bunnycdn schema', () => { + it('declares a semver version', () => { + expect(BunnycdnSchema.version).toBeDefined(); + expect(BunnycdnSchema.version).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('declares an entities map', () => { + expect(typeof BunnycdnSchema.entities).toBe('object'); + expect(BunnycdnSchema.entities).not.toBeNull(); + expect(Array.isArray(Object.keys(BunnycdnSchema.entities))).toBe(true); + for (const entity of Object.values(BunnycdnSchema.entities)) { + expect(entity).toBeDefined(); + } + }); +}); + +// Per .github/PLUGIN_PR_RULES.md (R2), every implemented endpoint +// needs a corresponding test. diff --git a/packages/bunnycdn/schema/database.ts b/packages/bunnycdn/schema/database.ts new file mode 100644 index 000000000..4985f8c1c --- /dev/null +++ b/packages/bunnycdn/schema/database.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +// TODO: Define your database entities here +// export const BunnycdnExample = z.object({ +// id: z.string(), +// name: z.string(), +// created_at: z.coerce.date().nullable().optional(), +// }); +// export type BunnycdnExample = z.infer; diff --git a/packages/bunnycdn/schema/index.ts b/packages/bunnycdn/schema/index.ts new file mode 100644 index 000000000..9eb64e561 --- /dev/null +++ b/packages/bunnycdn/schema/index.ts @@ -0,0 +1,4 @@ +export const BunnycdnSchema = { + version: '1.0.0', + entities: {}, +} as const; diff --git a/packages/bunnycdn/tsconfig.json b/packages/bunnycdn/tsconfig.json new file mode 100644 index 000000000..92fa48e0b --- /dev/null +++ b/packages/bunnycdn/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "esnext" + ], + "types": [ + "node", + "jest" + ], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "incremental": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": [ + "./**/*" + ], + "exclude": [ + "dist", + "node_modules" + ], + "references": [] +} diff --git a/packages/bunnycdn/tsup.config.ts b/packages/bunnycdn/tsup.config.ts new file mode 100644 index 000000000..3ec221e23 --- /dev/null +++ b/packages/bunnycdn/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + clean: false, + dts: false, + format: ['esm'], + target: 'esnext', + platform: 'node', + bundle: true, + splitting: true, + minify: true, + outDir: 'dist', + external: ['corsair', 'zod'], + entry: ['index.ts'], +}); diff --git a/packages/bunnycdn/webhooks/example.ts b/packages/bunnycdn/webhooks/example.ts new file mode 100644 index 000000000..29a15b7a6 --- /dev/null +++ b/packages/bunnycdn/webhooks/example.ts @@ -0,0 +1,27 @@ +import { logEventFromContext } from 'corsair/core'; +import type { BunnycdnWebhooks } from '..'; +import { createBunnycdnMatch, verifyBunnycdnWebhookSignature } from './types'; + +export const example: BunnycdnWebhooks['example'] = { + match: createBunnycdnMatch('example'), + + handler: async (ctx, request) => { + const verification = verifyBunnycdnWebhookSignature(request, ctx.key); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + + const event = request.payload; + if (event.type !== 'example') { + return { success: true, data: undefined }; + } + + await logEventFromContext(ctx, 'bunnycdn.webhook.example', { ...event }, 'completed'); + + return { success: true, data: event }; + }, +}; diff --git a/packages/bunnycdn/webhooks/index.ts b/packages/bunnycdn/webhooks/index.ts new file mode 100644 index 000000000..c04b25e53 --- /dev/null +++ b/packages/bunnycdn/webhooks/index.ts @@ -0,0 +1,9 @@ +import { example } from './example'; + +export const ExampleWebhooks = { + example: example, +}; + +export * from './types'; +export * from './tenant-matcher'; +export * from './oauth-tenant-link'; diff --git a/packages/bunnycdn/webhooks/oauth-tenant-link.ts b/packages/bunnycdn/webhooks/oauth-tenant-link.ts new file mode 100644 index 000000000..2ceea35eb --- /dev/null +++ b/packages/bunnycdn/webhooks/oauth-tenant-link.ts @@ -0,0 +1,31 @@ +import type { TokenResponse, WebhookTenantMatch } from 'corsair/core'; +import { asRecord, toExternalId } from 'corsair/core'; + +// TODO: Rename linkType 'tenant_external_id' to match pluginTenantWebhookMatcher. +// Called after OAuth to store the routing id on corsair_accounts.config. +export async function resolveBunnycdnOAuthWebhookTenantLink( + tokens: TokenResponse, +): Promise { + // TODO: Read from token response when the provider includes a stable id. + // const externalId = toExternalId(asRecord(tokens.team)?.id); + const externalId = toExternalId(tokens.tenant_external_id); + if (externalId) { + return { linkType: 'tenant_external_id', externalId }; + } + + const accessToken = tokens.access_token; + if (!accessToken) return null; + + // TODO: Fetch from provider API when the token response omits the id. + // const response = await fetch('https://api.example.com/me', { + // headers: { Authorization: `Bearer ${accessToken}` }, + // }); + // if (!response.ok) return null; + // const payload = (await response.json()) as { id?: string }; + // const fetchedId = toExternalId(payload.id); + // return fetchedId + // ? { linkType: 'tenant_external_id', externalId: fetchedId } + // : null; + + return null; +} diff --git a/packages/bunnycdn/webhooks/tenant-matcher.ts b/packages/bunnycdn/webhooks/tenant-matcher.ts new file mode 100644 index 000000000..0ce42cc76 --- /dev/null +++ b/packages/bunnycdn/webhooks/tenant-matcher.ts @@ -0,0 +1,12 @@ +import type { WebhookTenantMatch } from 'corsair/core'; + +export function matchBunnycdnTenantWebhook(body: any): WebhookTenantMatch | null { + const tenantCode = body?.tenant?.code || body?.payload?.tenant?.code; + + if (!tenantCode) return null; + + return { + linkType: 'account' as const, + externalId: String(tenantCode) + }; +} \ No newline at end of file diff --git a/packages/bunnycdn/webhooks/types.ts b/packages/bunnycdn/webhooks/types.ts new file mode 100644 index 000000000..264c6ac95 --- /dev/null +++ b/packages/bunnycdn/webhooks/types.ts @@ -0,0 +1,60 @@ +import type { CorsairWebhookMatcher, RawWebhookRequest, WebhookRequest } from 'corsair/core'; +import { z } from 'zod'; + +export const BunnycdnWebhookPayloadSchema = z.object({ + type: z.string(), + created_at: z.string(), + data: z.record(z.string(), z.unknown()), +}); + +export type BunnycdnWebhookPayload = z.infer< + typeof BunnycdnWebhookPayloadSchema +>; + +export const ExampleEventSchema = BunnycdnWebhookPayloadSchema.extend({ + type: z.literal('example'), + data: z + .object({ + id: z.string(), + }) + .loose(), +}); + +export type ExampleEvent = z.infer; + +export type BunnycdnWebhookOutputs = { + example: ExampleEvent; +}; + +function parseBody(body: unknown): Record | null { + if (typeof body === 'string') { + try { + const parsed = JSON.parse(body); + return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) + ? (parsed as Record) + : null; + } catch { + return null; + } + } + return body !== null && typeof body === 'object' && !Array.isArray(body) + ? (body as Record) + : null; +} + +export function createBunnycdnMatch(eventType: string): CorsairWebhookMatcher { + return (request: RawWebhookRequest) => { + const parsedBody = parseBody(request.body); + return parsedBody !== null && parsedBody.type === eventType; + }; +} + +export function verifyBunnycdnWebhookSignature( + request: WebhookRequest, + secret: string, +): { valid: boolean; error?: string } { + return { + valid: false, + error: 'Webhook signature verification is currently not supported for BunnyCDN' + }; +} \ No newline at end of file diff --git a/packages/corsair/core/constants.ts b/packages/corsair/core/constants.ts index 93ce1126d..171c5c612 100644 --- a/packages/corsair/core/constants.ts +++ b/packages/corsair/core/constants.ts @@ -18,7 +18,6 @@ export const BaseProviders = [ 'abyssale', 'accrediblecertificates', 'activecampaign', - 'anchorbrowser', 'activetrail', 'addresszen', 'aeroleads', @@ -40,14 +39,15 @@ export const BaseProviders = [ 'ambientweather', 'amcards', 'amplitude', + 'anchorbrowser', 'anthropicadministrator', 'apaleo', 'api2pdf', 'apibible', - 'apipie', 'apify', 'apilabz', 'apininjas', + 'apipie', 'apisports', 'asana', 'asindataapi', @@ -66,6 +66,7 @@ export const BaseProviders = [ 'botpress', 'box', 'bugsnag', + 'bunnycdn', 'cal', 'calendly', 'canva', @@ -92,8 +93,8 @@ export const BaseProviders = [ 'facebook', 'figma', 'firecrawl', - 'formbricks', 'fireflies', + 'formbricks', 'gemini', 'github', 'gitlab', @@ -185,7 +186,6 @@ export const ProviderDisplayNames = { abyssale: 'Abyssale', accrediblecertificates: 'Accredible Certificates', activecampaign: 'ActiveCampaign', - anchorbrowser: 'Anchor Browser', activetrail: 'Active Trail', addresszen: 'Addresszen', aeroleads: 'Aeroleads', @@ -207,14 +207,15 @@ export const ProviderDisplayNames = { ambientweather: 'Ambient Weather', amcards: 'AMcards', amplitude: 'Amplitude', + anchorbrowser: 'Anchor Browser', anthropicadministrator: 'Anthropic Administrator', apaleo: 'Apaleo', api2pdf: 'API2PDF', apibible: 'API.Bible', - apipie: 'APIpie AI', apify: 'Apify', apilabz: 'API Labz', apininjas: 'API Ninjas', + apipie: 'APIpie AI', apisports: 'API-Sports', asana: 'Asana', asindataapi: 'ASIN Data API', @@ -233,6 +234,7 @@ export const ProviderDisplayNames = { botpress: 'Botpress', box: 'Box', bugsnag: 'BugSnag', + bunnycdn: 'Bunnycdn', cal: 'Cal', calendly: 'Calendly', canva: 'Canva', @@ -259,8 +261,8 @@ export const ProviderDisplayNames = { facebook: 'Facebook', figma: 'Figma', firecrawl: 'Firecrawl', - formbricks: 'Formbricks', fireflies: 'Fireflies', + formbricks: 'Formbricks', gemini: 'Gemini', github: 'GitHub', gitlab: 'GitLab', @@ -359,7 +361,6 @@ export type AllProviders = | 'abyssale' | 'accrediblecertificates' | 'activecampaign' - | 'anchorbrowser' | 'activetrail' | 'addresszen' | 'aeroleads' @@ -381,14 +382,15 @@ export type AllProviders = | 'ambientweather' | 'amcards' | 'amplitude' + | 'anchorbrowser' | 'anthropicadministrator' | 'apaleo' | 'api2pdf' | 'apibible' - | 'apipie' | 'apify' | 'apilabz' | 'apininjas' + | 'apipie' | 'apisports' | 'asana' | 'asindataapi' @@ -407,6 +409,7 @@ export type AllProviders = | 'botpress' | 'box' | 'bugsnag' + | 'bunnycdn' | 'cal' | 'calendly' | 'canva' @@ -433,8 +436,8 @@ export type AllProviders = | 'facebook' | 'figma' | 'firecrawl' - | 'formbricks' | 'fireflies' + | 'formbricks' | 'gemini' | 'github' | 'gitlab' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0779d3702..fbc967228 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1621,6 +1621,30 @@ importers: specifier: 4.4.3 version: 4.4.3 + packages/bunnycdn: + devDependencies: + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + corsair: + specifier: workspace:* + version: link:../corsair + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.4.9 + version: 29.4.9(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.0)(jest-util@30.4.1)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3) + tsup: + specifier: ^8.0.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: 'catalog:' + version: 5.9.3 + zod: + specifier: 4.4.3 + version: 4.4.3 + packages/cal: devDependencies: '@types/jest':