diff --git a/demo/testing/src/scripts/test-script.ts b/demo/testing/src/scripts/test-script.ts index 497559193..d3100bf97 100644 --- a/demo/testing/src/scripts/test-script.ts +++ b/demo/testing/src/scripts/test-script.ts @@ -19,10 +19,14 @@ async function setInstagramCredentials() { } const main = async () => { - const res = await corsair.slack.api.messages.post({ - channel: 'general', - text: 'hello', - }); + const res = await corsair.slack.api.messages.post({ + channel: 'general', + text: 'hello', + }); + + const projects = await corsair.webvizio.api.projects.list({}); + + console.log('Webvizio projects:', projects); }; main().catch((err) => { diff --git a/demo/testing/src/server/corsair.ts b/demo/testing/src/server/corsair.ts index 3755c8b3e..f46573e55 100644 --- a/demo/testing/src/server/corsair.ts +++ b/demo/testing/src/server/corsair.ts @@ -13,6 +13,7 @@ import { sharepoint } from '@corsair-dev/sharepoint'; import { slack } from '@corsair-dev/slack'; import { twilio } from '@corsair-dev/twilio'; import { vapi } from '@corsair-dev/vapi'; +import { webvizio } from '@corsair-dev/webvizio'; import { createCorsair } from 'corsair'; import { sqlite } from '../db'; @@ -63,6 +64,9 @@ export const corsair = createCorsair({ key: process.env.VAPI_API_KEY, webhookSecret: process.env.VAPI_WEBHOOK_SECRET, }), +webvizio({ +key: process.env.WEBVIZIO_API_KEY, +}), instagram(), ], }); diff --git a/packages/corsair/core/constants.ts b/packages/corsair/core/constants.ts index b5dca6fd6..a7fc703ee 100644 --- a/packages/corsair/core/constants.ts +++ b/packages/corsair/core/constants.ts @@ -187,6 +187,7 @@ export const BaseProviders = [ 'vapi', 'vercel', 'webflow', + 'webvizio', 'whatsapp', 'witai', 'wiza', @@ -375,6 +376,7 @@ export const ProviderDisplayNames = { vapi: 'Vapi', vercel: 'Vercel', webflow: 'Webflow', + webvizio: 'Webvizio', whatsapp: 'WhatsApp', witai: 'WitAi', wiza: 'Wiza', @@ -570,6 +572,7 @@ export type AllProviders = | 'vapi' | 'vercel' | 'webflow' + | 'webvizio' | 'whatsapp' | 'witai' | 'wiza' diff --git a/packages/webvizio/client.ts b/packages/webvizio/client.ts new file mode 100644 index 000000000..97afeec2f --- /dev/null +++ b/packages/webvizio/client.ts @@ -0,0 +1,81 @@ +import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; +import { ApiError, request } from 'corsair/http'; + +export class WebvizioAPIError extends Error { + constructor( + message: string, + public readonly code?: string | number, + ) { + super(message); + this.name = 'WebvizioAPIError'; + } +} + +const WEBVIZIO_MCP_API_BASE = 'https://app.webvizio.com/api/mcp/v1'; +const WEBVIZIO_WEBHOOK_API_BASE = 'https://app.webvizio.com/api/v1'; + +export async function makeWebvizioRequest( + endpoint: string, + apiKey: string, + options: { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + body?: Record; + query?: Record; + baseUrl?: string; + } = {}, +): Promise { + const { + method = 'GET', + body, + query, + baseUrl = WEBVIZIO_MCP_API_BASE, + } = options; + + const config: OpenAPIConfig = { + BASE: baseUrl, + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: apiKey, + HEADERS: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + }; + + const requestOptions: ApiRequestOptions = { + method, + url: endpoint, + body: + method === 'POST' || method === 'PUT' || method === 'PATCH' + ? body + : undefined, + mediaType: 'application/json', + query, + }; + + try { + return await request(config, requestOptions); + } catch (error) { + if (error instanceof ApiError) { + const detail = + typeof error.body === 'object' + ? JSON.stringify(error.body) + : String(error.body ?? ''); + + throw new WebvizioAPIError( + `${error.message} (status=${error.status}, body=${detail})`, + error.status, + ); + } + + if (error instanceof WebvizioAPIError) { + throw error; + } + + throw new WebvizioAPIError( + error instanceof Error ? error.message : 'Unknown error', + ); + } +} diff --git a/packages/webvizio/endpoints/index.ts b/packages/webvizio/endpoints/index.ts new file mode 100644 index 000000000..c59d6991b --- /dev/null +++ b/packages/webvizio/endpoints/index.ts @@ -0,0 +1,12 @@ +import { list as projectsList } from './projects'; +import { list as webhooksList } from './webhooks'; + +export const Projects = { + list: projectsList, +}; + +export const Webhooks = { + list: webhooksList, +}; + +export * from './types'; diff --git a/packages/webvizio/endpoints/projects.ts b/packages/webvizio/endpoints/projects.ts new file mode 100644 index 000000000..966851e0e --- /dev/null +++ b/packages/webvizio/endpoints/projects.ts @@ -0,0 +1,24 @@ +import { logEventFromContext } from 'corsair/core'; +import { makeWebvizioRequest } from '../client'; +import type { WebvizioEndpoints } from '../index'; + +export const list: WebvizioEndpoints['projectsList'] = async (ctx, input) => { + const result = await makeWebvizioRequest( + '/projects', + ctx.key, + ); + + await logEventFromContext( + ctx, + 'webvizio.projects.list', + { ...input }, + 'completed', + ); + + return result as WebvizioEndpoints['projectsList'] extends ( + ctx: infer _, + input: infer _, + ) => Promise + ? R + : never; +}; diff --git a/packages/webvizio/endpoints/types.ts b/packages/webvizio/endpoints/types.ts new file mode 100644 index 000000000..548872b82 --- /dev/null +++ b/packages/webvizio/endpoints/types.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; + +const WebvizioProjectSchema = z + .object({ + id: z.string(), + name: z.string().optional(), + description: z.string().optional(), + }) + .passthrough(); + +export type WebvizioProject = z.infer; + +const WebvizioWebhookSubscriptionSchema = z + .object({ + id: z.string().optional(), + url: z.string().optional(), + event: z.string().optional(), + }) + .passthrough(); + +export type WebvizioWebhookSubscription = z.infer< + typeof WebvizioWebhookSubscriptionSchema +>; + +export type WebvizioEndpointInputs = { + projectsList: Record; + webhooksList: Record; +}; + +export type WebvizioEndpointOutputs = { + projectsList: WebvizioProject[]; + webhooksList: WebvizioWebhookSubscription[]; +}; + +export const WebvizioEndpointInputSchemas = { + projectsList: z.object({}), + webhooksList: z.object({}), +} as const; + +export const WebvizioEndpointOutputSchemas = { + projectsList: z.array(WebvizioProjectSchema), + webhooksList: z.array(WebvizioWebhookSubscriptionSchema), +} as const; diff --git a/packages/webvizio/endpoints/webhooks.ts b/packages/webvizio/endpoints/webhooks.ts new file mode 100644 index 000000000..10f0fe5c1 --- /dev/null +++ b/packages/webvizio/endpoints/webhooks.ts @@ -0,0 +1,28 @@ +import { logEventFromContext } from 'corsair/core'; +import { makeWebvizioRequest } from '../client'; +import type { WebvizioEndpoints } from '../index'; + +export const list: WebvizioEndpoints['webhooksList'] = async (ctx, input) => { + const result = await makeWebvizioRequest( + '/webhook', + ctx.key, + { + baseUrl: 'https://app.webvizio.com/api/v1', + }, + ); + + await logEventFromContext( + ctx, + 'webvizio.webhooks.list', + { ...input }, + 'completed', + ); + + return result as WebvizioEndpoints['webhooksList'] extends ( + ctx: infer _, + input: infer _, + ) => Promise + ? R + +: never; +}; diff --git a/packages/webvizio/error-handlers.ts b/packages/webvizio/error-handlers.ts new file mode 100644 index 000000000..c2af29acd --- /dev/null +++ b/packages/webvizio/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/webvizio/index.ts b/packages/webvizio/index.ts new file mode 100644 index 000000000..7f7e71936 --- /dev/null +++ b/packages/webvizio/index.ts @@ -0,0 +1,172 @@ +import type { + BindEndpoints, + CorsairErrorHandler, + CorsairEndpoint, + CorsairPlugin, + CorsairPluginContext, + KeyBuilderContext, + PickAuth, + PluginAuthConfig, + PluginPermissionsConfig, + RequiredPluginEndpointMeta, + RequiredPluginEndpointSchemas, +} from 'corsair/core'; +import type { AuthTypes } from 'corsair/core'; +import { + WebvizioEndpointInputSchemas, + WebvizioEndpointOutputSchemas, +} from './endpoints/types'; +import { Projects, Webhooks } from './endpoints'; +import { WebvizioSchema } from './schema'; +import { errorHandlers } from './error-handlers'; + +export type WebvizioPluginOptions = { + authType?: PickAuth<'api_key'>; + key?: string; + hooks?: InternalWebvizioPlugin['hooks']; + errorHandlers?: CorsairErrorHandler; + permissions?: PluginPermissionsConfig; +}; + +export type WebvizioContext = CorsairPluginContext< + typeof WebvizioSchema, + WebvizioPluginOptions +>; + +export type WebvizioKeyBuilderContext = + KeyBuilderContext; + +export type WebvizioBoundEndpoints = + BindEndpoints; + +type WebvizioEndpoint< + K extends keyof WebvizioEndpointOutputs, +> = CorsairEndpoint< + WebvizioContext, + WebvizioEndpointInputs[K], + WebvizioEndpointOutputs[K] +>; + +import type { + WebvizioEndpointInputs, + WebvizioEndpointOutputs, +} from './endpoints/types'; + +export type WebvizioEndpoints = { + projectsList: WebvizioEndpoint<'projectsList'>; + webhooksList: WebvizioEndpoint<'webhooksList'>; +}; + +const webvizioEndpointsNested = { + projects: { + list: Projects.list, + }, + webhooks: { + list: Webhooks.list, + }, +} as const; + +const webvizioWebhooksNested = {} as const; + +export const webvizioEndpointSchemas = { + 'projects.list': { + input: WebvizioEndpointInputSchemas.projectsList, + output: WebvizioEndpointOutputSchemas.projectsList, + }, + 'webhooks.list': { + input: WebvizioEndpointInputSchemas.webhooksList, + output: WebvizioEndpointOutputSchemas.webhooksList, + }, +} as const satisfies RequiredPluginEndpointSchemas< + typeof webvizioEndpointsNested +>; + +const defaultAuthType: AuthTypes = 'api_key'; + +const webvizioEndpointMeta = { + 'projects.list': { + riskLevel: 'read', + description: 'List all available Webvizio projects', + }, + 'webhooks.list': { + riskLevel: 'read', + description: 'List Webvizio webhook subscriptions', + }, +} as const satisfies RequiredPluginEndpointMeta< + typeof webvizioEndpointsNested +>; + +export const webvizioAuthConfig = { + api_key: { + account: ['tenant_external_id'] as const, + }, +} as const satisfies PluginAuthConfig; + +export type BaseWebvizioPlugin< + T extends WebvizioPluginOptions, +> = CorsairPlugin< + 'webvizio', + typeof WebvizioSchema, + typeof webvizioEndpointsNested, + typeof webvizioWebhooksNested, + T, + typeof defaultAuthType +>; + +export type InternalWebvizioPlugin = + BaseWebvizioPlugin; + +export type ExternalWebvizioPlugin< + T extends WebvizioPluginOptions, +> = BaseWebvizioPlugin; + +export function webvizio( + incomingOptions: WebvizioPluginOptions & T = + {} as WebvizioPluginOptions & T, +): ExternalWebvizioPlugin { + const options = { + ...incomingOptions, + authType: incomingOptions.authType ?? defaultAuthType, + }; + + return { + id: 'webvizio', + authConfig: webvizioAuthConfig, + schema: WebvizioSchema, + options, + hooks: options.hooks, + endpoints: webvizioEndpointsNested, + webhooks: webvizioWebhooksNested, + endpointMeta: webvizioEndpointMeta, + endpointSchemas: webvizioEndpointSchemas, + errorHandlers: { + ...errorHandlers, + ...options.errorHandlers, + }, + keyBuilder: async ( + ctx: WebvizioKeyBuilderContext, + source, + ) => { + 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 InternalWebvizioPlugin; +} + +export type { + WebvizioEndpointInputs, + WebvizioEndpointOutputs, + WebvizioProject, + WebvizioWebhookSubscription, +} from './endpoints/types'; diff --git a/packages/webvizio/jest.config.cjs b/packages/webvizio/jest.config.cjs new file mode 100644 index 000000000..8c6218f64 --- /dev/null +++ b/packages/webvizio/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/webvizio/package.json b/packages/webvizio/package.json new file mode 100644 index 000000000..04876d263 --- /dev/null +++ b/packages/webvizio/package.json @@ -0,0 +1,44 @@ +{ + "name": "@corsair-dev/webvizio", + "version": "0.1.0", + "description": "Webvizio 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", + "webvizio", + "plugin" + ], + "author": "", + "license": "Apache-2.0", + "files": [ + "dist" + ] +} diff --git a/packages/webvizio/schema.test.ts b/packages/webvizio/schema.test.ts new file mode 100644 index 000000000..e82c64ba5 --- /dev/null +++ b/packages/webvizio/schema.test.ts @@ -0,0 +1,20 @@ +import { WebvizioSchema } from './schema'; + +describe('Webvizio schema', () => { + it('declares a semver version', () => { + expect(WebvizioSchema.version).toBeDefined(); + expect(WebvizioSchema.version).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('declares an entities map', () => { + expect(typeof WebvizioSchema.entities).toBe('object'); + expect(WebvizioSchema.entities).not.toBeNull(); + expect(Array.isArray(Object.keys(WebvizioSchema.entities))).toBe(true); + for (const entity of Object.values(WebvizioSchema.entities)) { + expect(entity).toBeDefined(); + } + }); +}); + +// Per .github/PLUGIN_PR_RULES.md (R2), every implemented endpoint +// needs a corresponding test. diff --git a/packages/webvizio/schema/database.ts b/packages/webvizio/schema/database.ts new file mode 100644 index 000000000..7e0703d17 --- /dev/null +++ b/packages/webvizio/schema/database.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +// TODO: Define your database entities here +// export const WebvizioExample = z.object({ +// id: z.string(), +// name: z.string(), +// created_at: z.coerce.date().nullable().optional(), +// }); +// export type WebvizioExample = z.infer; diff --git a/packages/webvizio/schema/index.ts b/packages/webvizio/schema/index.ts new file mode 100644 index 000000000..3c60b2928 --- /dev/null +++ b/packages/webvizio/schema/index.ts @@ -0,0 +1,4 @@ +export const WebvizioSchema = { + version: '1.0.0', + entities: {}, +} as const; diff --git a/packages/webvizio/tsconfig.json b/packages/webvizio/tsconfig.json new file mode 100644 index 000000000..92fa48e0b --- /dev/null +++ b/packages/webvizio/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/webvizio/tsup.config.ts b/packages/webvizio/tsup.config.ts new file mode 100644 index 000000000..3ec221e23 --- /dev/null +++ b/packages/webvizio/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/webvizio/webhooks/oauth-tenant-link.ts b/packages/webvizio/webhooks/oauth-tenant-link.ts new file mode 100644 index 000000000..c2c1384ea --- /dev/null +++ b/packages/webvizio/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 resolveWebvizioOAuthWebhookTenantLink( + 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/webvizio/webhooks/tenant-matcher.ts b/packages/webvizio/webhooks/tenant-matcher.ts new file mode 100644 index 000000000..00e9128e3 --- /dev/null +++ b/packages/webvizio/webhooks/tenant-matcher.ts @@ -0,0 +1,25 @@ +import type { RawWebhookRequest, WebhookTenantMatch } from 'corsair/core'; +import { asRecord, firstString, readBodyRecord } from 'corsair/core'; + +// TODO: Rename linkType 'tenant_external_id' to match the provider field +// (e.g. team_id, installation_id, organization_id). Must match authConfig.account +// and oauthWebhookTenantLinkResolver. +// Return null for URL verification / handshake payloads that have no tenant id. +export function matchWebvizioTenantWebhook( + request: RawWebhookRequest, +): WebhookTenantMatch | null { + const body = readBodyRecord(request); + if (!body) return null; + + // TODO: Extract the stable external id from the webhook payload. + // Example: + // const externalId = firstString([body.tenant_external_id, asRecord(body.data)?.id]); + const externalId = firstString([ + body.tenant_external_id, + asRecord(body.data)?.tenant_external_id, + ]); + + if (!externalId) return null; + + return { linkType: 'tenant_external_id', externalId }; +} diff --git a/packages/webvizio/webhooks/types.ts b/packages/webvizio/webhooks/types.ts new file mode 100644 index 000000000..9ca3d2807 --- /dev/null +++ b/packages/webvizio/webhooks/types.ts @@ -0,0 +1,58 @@ +import type { CorsairWebhookMatcher, RawWebhookRequest, WebhookRequest } from 'corsair/core'; +import { z } from 'zod'; + +export const WebvizioWebhookPayloadSchema = z.object({ + type: z.string(), + created_at: z.string(), + data: z.record(z.string(), z.unknown()), +}); + +export type WebvizioWebhookPayload = z.infer< + typeof WebvizioWebhookPayloadSchema +>; + +export const ExampleEventSchema = WebvizioWebhookPayloadSchema.extend({ + type: z.literal('example'), + data: z + .object({ + id: z.string(), + }) + .loose(), +}); + +export type ExampleEvent = z.infer; + +export type WebvizioWebhookOutputs = { + 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 createWebvizioMatch(eventType: string): CorsairWebhookMatcher { + return (request: RawWebhookRequest) => { + const parsedBody = parseBody(request.body); + return parsedBody !== null && parsedBody.type === eventType; + }; +} + +export function verifyWebvizioWebhookSignature( + request: WebhookRequest, + secret: string, +): { valid: boolean; error?: string } { + // TODO: Implement webhook signature verification + return { valid: true }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd3d10ddc..15976333a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4919,6 +4919,30 @@ importers: specifier: 4.4.3 version: 4.4.3 + packages/webvizio: + 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/whatsapp: devDependencies: '@types/jest':