From 33e727b52de210d7c04ea3a440ab332b0757905f Mon Sep 17 00:00:00 2001 From: Mehul Gupta Date: Sat, 22 Aug 2026 18:23:09 +0530 Subject: [PATCH 1/6] feat: add Griptape plugin --- demo/testing/package.json | 1 + packages/corsair/core/constants.ts | 25 +- packages/griptape/README.md | 16 ++ packages/griptape/client.ts | 58 +++++ packages/griptape/endpoints.test.ts | 153 ++++++++++++ packages/griptape/endpoints/assistant-get.ts | 20 ++ packages/griptape/endpoints/assistant-list.ts | 24 ++ packages/griptape/endpoints/endpoint.test.ts | 18 ++ packages/griptape/endpoints/index.ts | 9 + packages/griptape/endpoints/types.ts | 74 ++++++ packages/griptape/error-handlers.ts | 31 +++ packages/griptape/index.ts | 220 ++++++++++++++++++ packages/griptape/jest.config.cjs | 55 +++++ packages/griptape/package.json | 45 ++++ packages/griptape/schema.test.ts | 20 ++ packages/griptape/schema/database.ts | 7 + packages/griptape/schema/index.ts | 4 + packages/griptape/tsconfig.json | 20 ++ packages/griptape/tsup.config.ts | 15 ++ packages/griptape/webhooks/example.ts | 32 +++ packages/griptape/webhooks/index.ts | 9 + .../griptape/webhooks/oauth-tenant-link.ts | 31 +++ packages/griptape/webhooks/tenant-matcher.ts | 25 ++ packages/griptape/webhooks/types.ts | 64 +++++ pnpm-lock.yaml | 111 +++------ 25 files changed, 996 insertions(+), 91 deletions(-) create mode 100644 packages/griptape/README.md create mode 100644 packages/griptape/client.ts create mode 100644 packages/griptape/endpoints.test.ts create mode 100644 packages/griptape/endpoints/assistant-get.ts create mode 100644 packages/griptape/endpoints/assistant-list.ts create mode 100644 packages/griptape/endpoints/endpoint.test.ts create mode 100644 packages/griptape/endpoints/index.ts create mode 100644 packages/griptape/endpoints/types.ts create mode 100644 packages/griptape/error-handlers.ts create mode 100644 packages/griptape/index.ts create mode 100644 packages/griptape/jest.config.cjs create mode 100644 packages/griptape/package.json create mode 100644 packages/griptape/schema.test.ts create mode 100644 packages/griptape/schema/database.ts create mode 100644 packages/griptape/schema/index.ts create mode 100644 packages/griptape/tsconfig.json create mode 100644 packages/griptape/tsup.config.ts create mode 100644 packages/griptape/webhooks/example.ts create mode 100644 packages/griptape/webhooks/index.ts create mode 100644 packages/griptape/webhooks/oauth-tenant-link.ts create mode 100644 packages/griptape/webhooks/tenant-matcher.ts create mode 100644 packages/griptape/webhooks/types.ts diff --git a/demo/testing/package.json b/demo/testing/package.json index 3dcf0da17..0dc54ab16 100644 --- a/demo/testing/package.json +++ b/demo/testing/package.json @@ -33,6 +33,7 @@ "@corsair-dev/gmail": "workspace:*", "@corsair-dev/googlecalendar": "workspace:*", "@corsair-dev/googlesheets": "workspace:*", + "@corsair-dev/griptape": "workspace:*", "@corsair-dev/hubspot": "workspace:^", "@corsair-dev/linear": "workspace:*", "@corsair-dev/mcp": "workspace:*", diff --git a/packages/corsair/core/constants.ts b/packages/corsair/core/constants.ts index 0b8ea8cd5..151f26bc5 100644 --- a/packages/corsair/core/constants.ts +++ b/packages/corsair/core/constants.ts @@ -17,7 +17,6 @@ export const BaseProviders = [ 'abstract', 'abyssale', 'activecampaign', - 'anchorbrowser', 'activetrail', 'addresszen', 'aeroleads', @@ -39,14 +38,15 @@ export const BaseProviders = [ 'ambientweather', 'amcards', 'amplitude', + 'anchorbrowser', 'anthropicadministrator', 'apaleo', 'api2pdf', 'apibible', - 'apipie', 'apify', 'apilabz', 'apininjas', + 'apipie', 'apisports', 'asana', 'asindataapi', @@ -67,8 +67,8 @@ export const BaseProviders = [ 'bugsnag', 'cal', 'calendly', - 'canvas', 'canva', + 'canvas', 'circleci', 'clientary', 'cloudflare', @@ -91,8 +91,8 @@ export const BaseProviders = [ 'facebook', 'figma', 'firecrawl', - 'formbricks', 'fireflies', + 'formbricks', 'gemini', 'github', 'gitlab', @@ -106,6 +106,7 @@ export const BaseProviders = [ 'googlemeet', 'googlesheets', 'grafana', + 'griptape', 'groqcloud', 'habitica', 'hackernews', @@ -183,7 +184,6 @@ export const ProviderDisplayNames = { abstract: 'Abstract', abyssale: 'Abyssale', activecampaign: 'ActiveCampaign', - anchorbrowser: 'Anchor Browser', activetrail: 'Active Trail', addresszen: 'Addresszen', aeroleads: 'Aeroleads', @@ -205,14 +205,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,8 +234,8 @@ export const ProviderDisplayNames = { bugsnag: 'BugSnag', cal: 'Cal', calendly: 'Calendly', - canvas: 'Canvas LMS', canva: 'Canva', + canvas: 'Canvas LMS', circleci: 'CircleCI', clientary: 'Clientary', cloudflare: 'Cloudflare', @@ -257,8 +258,8 @@ export const ProviderDisplayNames = { facebook: 'Facebook', figma: 'Figma', firecrawl: 'Firecrawl', - formbricks: 'Formbricks', fireflies: 'Fireflies', + formbricks: 'Formbricks', gemini: 'Gemini', github: 'GitHub', gitlab: 'GitLab', @@ -272,6 +273,7 @@ export const ProviderDisplayNames = { googlemeet: 'Google Meet', googlesheets: 'Google Sheets', grafana: 'Grafana', + griptape: 'Griptape', groqcloud: 'GroqCloud', habitica: 'Habitica', hackernews: 'Hacker News', @@ -356,7 +358,6 @@ export type AllProviders = | 'abstract' | 'abyssale' | 'activecampaign' - | 'anchorbrowser' | 'activetrail' | 'addresszen' | 'aeroleads' @@ -378,14 +379,15 @@ export type AllProviders = | 'ambientweather' | 'amcards' | 'amplitude' + | 'anchorbrowser' | 'anthropicadministrator' | 'apaleo' | 'api2pdf' | 'apibible' - | 'apipie' | 'apify' | 'apilabz' | 'apininjas' + | 'apipie' | 'apisports' | 'asana' | 'asindataapi' @@ -430,8 +432,8 @@ export type AllProviders = | 'facebook' | 'figma' | 'firecrawl' - | 'formbricks' | 'fireflies' + | 'formbricks' | 'gemini' | 'github' | 'gitlab' @@ -445,6 +447,7 @@ export type AllProviders = | 'googlemeet' | 'googlesheets' | 'grafana' + | 'griptape' | 'groqcloud' | 'habitica' | 'hackernews' diff --git a/packages/griptape/README.md b/packages/griptape/README.md new file mode 100644 index 000000000..85905c224 --- /dev/null +++ b/packages/griptape/README.md @@ -0,0 +1,16 @@ +# @corsair-dev/griptape + +Corsair integration for the Griptape Cloud API. + +## Authentication + +Griptape Cloud uses HTTP Bearer authentication. + +Provide a Griptape Cloud API key when creating the plugin: + +```ts +import { griptape } from '@corsair-dev/griptape'; + +const plugin = griptape({ + key: process.env.GT_CLOUD_API_KEY, +}); \ No newline at end of file diff --git a/packages/griptape/client.ts b/packages/griptape/client.ts new file mode 100644 index 000000000..1f137f438 --- /dev/null +++ b/packages/griptape/client.ts @@ -0,0 +1,58 @@ +import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; +import { request } from 'corsair/http'; + +export class GriptapeAPIError extends Error { + constructor( + message: string, + public readonly code?: string, + ) { + super(message); + this.name = 'GriptapeAPIError'; + } +} + +const GRIPTAPE_API_BASE = 'https://cloud.griptape.ai/api'; + +export async function makeGriptapeRequest( + 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: GRIPTAPE_API_BASE, + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: apiKey, + HEADERS: { + 'Content-Type': 'application/json', + 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(config, requestOptions); + } catch (error) { + if (error instanceof Error) { + throw new GriptapeAPIError(error.message); + } + throw new GriptapeAPIError('Unknown error'); + } +} diff --git a/packages/griptape/endpoints.test.ts b/packages/griptape/endpoints.test.ts new file mode 100644 index 000000000..a332e052a --- /dev/null +++ b/packages/griptape/endpoints.test.ts @@ -0,0 +1,153 @@ +import { request } from 'corsair/http'; +import { griptape } from './index'; + +jest.mock('corsair/http', () => ({ + ...jest.requireActual('corsair/http'), + request: jest.fn(), +})); + +jest.mock('corsair/core', () => ({ + ...jest.requireActual('corsair/core'), + logEventFromContext: jest.fn(async () => undefined), +})); + +const mockRequest = request as jest.MockedFunction; + +describe('Griptape Plugin API', () => { + const apiKey = 'test-api-key'; + const plugin = griptape({ key: apiKey }) as any; + const ctx = { key: apiKey } as any; + + beforeEach(() => { + mockRequest.mockReset(); + }); + + describe('listAssistants', () => { + it('sends GET /assistants with pagination parameters', async () => { + const mockResponse = { + assistants: [ + { + assistant_id: '550e8400-e29b-41d4-a716-446655440000', + created_at: '2026-01-01T00:00:00Z', + created_by: 'user@example.com', + description: 'Test assistant', + input: 'text', + knowledge_base_ids: [], + model: 'gpt-5', + name: 'Test Assistant', + organization_id: '550e8400-e29b-41d4-a716-446655440001', + retriever_ids: [], + ruleset_ids: [], + structure_ids: [], + tool_ids: [], + updated_at: '2026-01-01T00:00:00Z', + }, + ], + pagination: { + page_number: 1, + page_size: 10, + total_count: 1, + total_pages: 1, + next_page: 2, + previous_page: 0, + }, + }; + + mockRequest.mockResolvedValueOnce(mockResponse); + + const result = await plugin.endpoints.assistant.list(ctx, { + page: 1, + page_size: 10, + }); + + expect(mockRequest).toHaveBeenCalledWith( + expect.objectContaining({ + BASE: 'https://cloud.griptape.ai/api', + HEADERS: expect.objectContaining({ + Authorization: `Bearer ${apiKey}`, + }), + }), + expect.objectContaining({ + method: 'GET', + url: 'assistants', + query: { + page: 1, + page_size: 10, + }, + }), + ); + + expect(result).toEqual(mockResponse); + }); + + it('lists assistants without optional pagination parameters', async () => { + const mockResponse = { + assistants: [], + pagination: { + page_number: 1, + page_size: 20, + total_count: 0, + total_pages: 0, + }, + }; + + mockRequest.mockResolvedValueOnce(mockResponse); + + const result = await plugin.endpoints.assistant.list(ctx, {}); + + expect(mockRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: 'GET', + url: 'assistants', + query: { + page: undefined, + page_size: undefined, + }, + }), + ); + + expect(result).toEqual(mockResponse); + }); + }); + + describe('getAssistant', () => { + it('sends GET /assistants/{assistant_id}', async () => { + const mockResponse = { + assistant_id: '550e8400-e29b-41d4-a716-446655440000', + created_at: '2026-01-01T00:00:00Z', + created_by: 'user@example.com', + description: 'Test assistant', + knowledge_base_ids: [], + name: 'Test Assistant', + organization_id: '550e8400-e29b-41d4-a716-446655440001', + retriever_ids: [], + ruleset_ids: [], + structure_ids: [], + tool_ids: [], + updated_at: '2026-01-01T00:00:00Z', + }; + + mockRequest.mockResolvedValueOnce(mockResponse); + + const result = await plugin.endpoints.assistant.get(ctx, { + assistant_id: '550e8400-e29b-41d4-a716-446655440000', + }); + + expect(mockRequest).toHaveBeenCalledWith( + expect.objectContaining({ + BASE: 'https://cloud.griptape.ai/api', + HEADERS: expect.objectContaining({ + Authorization: `Bearer ${apiKey}`, + }), + }), + expect.objectContaining({ + method: 'GET', + url: 'assistants/550e8400-e29b-41d4-a716-446655440000', + }), + ); + + expect(result).toEqual(mockResponse); + }); + }); +}); diff --git a/packages/griptape/endpoints/assistant-get.ts b/packages/griptape/endpoints/assistant-get.ts new file mode 100644 index 000000000..349e854da --- /dev/null +++ b/packages/griptape/endpoints/assistant-get.ts @@ -0,0 +1,20 @@ +import { logEventFromContext } from 'corsair/core'; +import type { GriptapeEndpointOutputs, GriptapeEndpoints } from '..'; +import { makeGriptapeRequest } from '../client'; + +export const get: GriptapeEndpoints['assistantGet'] = async (ctx, input) => { + const response = await makeGriptapeRequest< + GriptapeEndpointOutputs['assistantGet'] + >(`assistants/${input.assistant_id}`, ctx.key, { + method: 'GET', + }); + + await logEventFromContext( + ctx, + 'griptape.assistant.get', + { ...input }, + 'completed', + ); + + return response; +}; diff --git a/packages/griptape/endpoints/assistant-list.ts b/packages/griptape/endpoints/assistant-list.ts new file mode 100644 index 000000000..58818f89e --- /dev/null +++ b/packages/griptape/endpoints/assistant-list.ts @@ -0,0 +1,24 @@ +import { logEventFromContext } from 'corsair/core'; +import type { GriptapeEndpointOutputs, GriptapeEndpoints } from '..'; +import { makeGriptapeRequest } from '../client'; + +export const list: GriptapeEndpoints['assistantList'] = async (ctx, input) => { + const response = await makeGriptapeRequest< + GriptapeEndpointOutputs['assistantList'] + >('assistants', ctx.key, { + method: 'GET', + query: { + page: input.page, + page_size: input.page_size, + }, + }); + + await logEventFromContext( + ctx, + 'griptape.assistant.list', + { ...input }, + 'completed', + ); + + return response; +}; diff --git a/packages/griptape/endpoints/endpoint.test.ts b/packages/griptape/endpoints/endpoint.test.ts new file mode 100644 index 000000000..86dfeac0c --- /dev/null +++ b/packages/griptape/endpoints/endpoint.test.ts @@ -0,0 +1,18 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +describe('griptape endpoints scaffold', () => { + it('does not keep the generator example file', () => { + expect(existsSync(join(__dirname, 'example.ts'))).toBe(false); + }); + + it('wires assistant list and get endpoints', () => { + const src = readFileSync(join(__dirname, 'index.ts'), 'utf8'); + + expect(src).toContain("from './assistant-list'"); + expect(src).toContain("from './assistant-get'"); + expect(src).toContain('list: assistantList'); + expect(src).toContain('get: assistantGet'); + expect(src).not.toContain("from './example'"); + }); +}); diff --git a/packages/griptape/endpoints/index.ts b/packages/griptape/endpoints/index.ts new file mode 100644 index 000000000..5165fa49e --- /dev/null +++ b/packages/griptape/endpoints/index.ts @@ -0,0 +1,9 @@ +import { get as assistantGet } from './assistant-get'; +import { list as assistantList } from './assistant-list'; + +export const Assistant = { + list: assistantList, + get: assistantGet, +}; + +export * from './types'; diff --git a/packages/griptape/endpoints/types.ts b/packages/griptape/endpoints/types.ts new file mode 100644 index 000000000..ef42bcd90 --- /dev/null +++ b/packages/griptape/endpoints/types.ts @@ -0,0 +1,74 @@ +import { z } from 'zod'; + +const UUID_REGEX = + /^[0-9(a-f|A-F)]{8}-[0-9(a-f|A-F)]{4}-4[0-9(a-f|A-F)]{3}-[89ab][0-9(a-f|A-F)]{3}-[0-9(a-f|A-F)]{12}$/; + +export const AssistantDetailSchema = z.object({ + assistant_id: z.string().regex(UUID_REGEX), + created_at: z.string().datetime(), + created_by: z.string(), + description: z.string(), + input: z.string().optional(), + knowledge_base_ids: z.array(z.string().regex(UUID_REGEX)), + model: z.string().optional(), + name: z.string(), + organization_id: z.string().regex(UUID_REGEX), + retriever_ids: z.array(z.string().regex(UUID_REGEX)), + ruleset_ids: z.array(z.string().regex(UUID_REGEX)), + structure_ids: z.array(z.string().regex(UUID_REGEX)), + tool_ids: z.array(z.string().regex(UUID_REGEX)), + updated_at: z.string().datetime(), +}); + +const AssistantGetInputSchema = z.object({ + assistant_id: z.string().regex(UUID_REGEX), +}); + +const AssistantGetResponseSchema = AssistantDetailSchema; + +const PaginationSchema = z.object({ + next_page: z.number().optional(), + page_number: z.number(), + page_size: z.number(), + previous_page: z.number().optional(), + total_count: z.number(), + total_pages: z.number(), +}); + +const AssistantListInputSchema = z.object({ + page: z.number().optional(), + page_size: z.number().optional(), +}); + +const AssistantListResponseSchema = z.object({ + assistants: z.array(AssistantDetailSchema), + pagination: PaginationSchema, +}); + +export type AssistantListInput = z.infer; + +export type AssistantListResponse = z.infer; + +export type AssistantGetInput = z.infer; + +export type AssistantGetResponse = z.infer; + +export type GriptapeEndpointInputs = { + assistantList: AssistantListInput; + assistantGet: AssistantGetInput; +}; + +export type GriptapeEndpointOutputs = { + assistantList: AssistantListResponse; + assistantGet: AssistantGetResponse; +}; + +export const GriptapeEndpointInputSchemas = { + assistantList: AssistantListInputSchema, + assistantGet: AssistantGetInputSchema, +} as const; + +export const GriptapeEndpointOutputSchemas = { + assistantList: AssistantListResponseSchema, + assistantGet: AssistantGetResponseSchema, +} as const; diff --git a/packages/griptape/error-handlers.ts b/packages/griptape/error-handlers.ts new file mode 100644 index 000000000..5a4f4c19f --- /dev/null +++ b/packages/griptape/error-handlers.ts @@ -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; diff --git a/packages/griptape/index.ts b/packages/griptape/index.ts new file mode 100644 index 000000000..ec7afbe31 --- /dev/null +++ b/packages/griptape/index.ts @@ -0,0 +1,220 @@ +import type { + AuthTypes, + BindEndpoints, + BindWebhooks, + CorsairEndpoint, + CorsairErrorHandler, + CorsairPlugin, + CorsairPluginContext, + CorsairWebhook, + KeyBuilderContext, + PickAuth, + PluginAuthConfig, + PluginPermissionsConfig, + RequiredPluginEndpointMeta, + RequiredPluginEndpointSchemas, + RequiredPluginWebhookSchemas, +} from 'corsair/core'; +import { Assistant } from './endpoints'; +import type { + GriptapeEndpointInputs, + GriptapeEndpointOutputs, +} from './endpoints/types'; +import { + GriptapeEndpointInputSchemas, + GriptapeEndpointOutputSchemas, +} from './endpoints/types'; +import { errorHandlers } from './error-handlers'; +import { GriptapeSchema } from './schema'; +import { ExampleWebhooks } from './webhooks'; +import { resolveGriptapeOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link'; +import { matchGriptapeTenantWebhook } from './webhooks/tenant-matcher'; +import type { ExampleEvent, GriptapeWebhookOutputs } from './webhooks/types'; +import { ExampleEventSchema } from './webhooks/types'; + +export type GriptapePluginOptions = { + authType?: PickAuth<'api_key' | 'oauth_2'>; + key?: string; + webhookSecret?: string; + hooks?: InternalGriptapePlugin['hooks']; + webhookHooks?: InternalGriptapePlugin['webhookHooks']; + errorHandlers?: CorsairErrorHandler; + permissions?: PluginPermissionsConfig; +}; + +export type GriptapeContext = CorsairPluginContext< + typeof GriptapeSchema, + GriptapePluginOptions +>; + +export type GriptapeKeyBuilderContext = + KeyBuilderContext; + +export type GriptapeBoundEndpoints = BindEndpoints< + typeof griptapeEndpointsNested +>; + +type GriptapeEndpoint = + CorsairEndpoint< + GriptapeContext, + GriptapeEndpointInputs[K], + GriptapeEndpointOutputs[K] + >; + +export type GriptapeEndpoints = { + assistantList: GriptapeEndpoint<'assistantList'>; + assistantGet: GriptapeEndpoint<'assistantGet'>; +}; + +type GriptapeWebhook< + K extends keyof GriptapeWebhookOutputs, + TEvent, +> = CorsairWebhook; + +export type GriptapeWebhooks = { + example: GriptapeWebhook<'example', ExampleEvent>; +}; + +export type GriptapeBoundWebhooks = BindWebhooks; + +const griptapeEndpointsNested = { + assistant: { + list: Assistant.list, + get: Assistant.get, + }, +} as const; + +const griptapeWebhooksNested = { + example: { + example: ExampleWebhooks.example, + }, +} as const; + +export const griptapeEndpointSchemas = { + 'assistant.list': { + input: GriptapeEndpointInputSchemas.assistantList, + output: GriptapeEndpointOutputSchemas.assistantList, + }, + 'assistant.get': { + input: GriptapeEndpointInputSchemas.assistantGet, + output: GriptapeEndpointOutputSchemas.assistantGet, + }, +} as const satisfies RequiredPluginEndpointSchemas< + typeof griptapeEndpointsNested +>; + +const griptapeWebhookSchemas = { + 'example.example': { + description: 'An example webhook event', + payload: ExampleEventSchema, + response: ExampleEventSchema, + }, +} as const satisfies RequiredPluginWebhookSchemas< + typeof griptapeWebhooksNested +>; + +const defaultAuthType: AuthTypes = 'api_key' as const; + +const griptapeEndpointMeta = { + 'assistant.list': { + riskLevel: 'read', + description: 'List assistants', + }, + 'assistant.get': { + riskLevel: 'read', + description: 'Get an assistant', + }, +} as const satisfies RequiredPluginEndpointMeta; + +export const griptapeAuthConfig = { + api_key: { + account: ['tenant_external_id'] as const, + }, + oauth_2: { + account: ['tenant_external_id'] as const, + }, +} as const satisfies PluginAuthConfig; + +export type BaseGriptapePlugin = CorsairPlugin< + 'griptape', + typeof GriptapeSchema, + typeof griptapeEndpointsNested, + typeof griptapeWebhooksNested, + T, + typeof defaultAuthType +>; + +export type InternalGriptapePlugin = BaseGriptapePlugin; + +export type ExternalGriptapePlugin = + BaseGriptapePlugin; + +export function griptape( + incomingOptions: GriptapePluginOptions & T = {} as GriptapePluginOptions & T, +): ExternalGriptapePlugin { + const options = { + ...incomingOptions, + authType: incomingOptions.authType ?? defaultAuthType, + }; + return { + id: 'griptape', + authConfig: griptapeAuthConfig, + schema: GriptapeSchema, + options: options, + hooks: options.hooks, + webhookHooks: options.webhookHooks, + endpoints: griptapeEndpointsNested, + webhooks: griptapeWebhooksNested, + endpointMeta: griptapeEndpointMeta, + endpointSchemas: griptapeEndpointSchemas, + webhookSchemas: griptapeWebhookSchemas, + pluginWebhookMatcher: (request) => { + const headers = request.headers; + // TODO: Update to match your webhook signature headers + return 'x-griptape-signature' in headers; + }, + pluginTenantWebhookMatcher: matchGriptapeTenantWebhook, + oauthWebhookTenantLinkResolver: resolveGriptapeOAuthWebhookTenantLink, + errorHandlers: { + ...errorHandlers, + ...options.errorHandlers, + }, + keyBuilder: async (ctx: GriptapeKeyBuilderContext, 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 InternalGriptapePlugin; +} + +export type { + AssistantListInput, + AssistantListResponse, + GriptapeEndpointInputs, + GriptapeEndpointOutputs, +} from './endpoints/types'; +export type { + ExampleEvent, + GriptapeWebhookOutputs, +} from './webhooks/types'; diff --git a/packages/griptape/jest.config.cjs b/packages/griptape/jest.config.cjs new file mode 100644 index 000000000..8c6218f64 --- /dev/null +++ b/packages/griptape/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/griptape/package.json b/packages/griptape/package.json new file mode 100644 index 000000000..a04057254 --- /dev/null +++ b/packages/griptape/package.json @@ -0,0 +1,45 @@ +{ + "name": "@corsair-dev/griptape", + "version": "0.1.0", + "description": "Griptape 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": "rimraf 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", + "rimraf": "^6.1.3", + "ts-jest": "^29.4.9", + "tsup": "^8.0.1", + "typescript": "catalog:", + "zod": "^4.1.13" + }, + "keywords": [ + "corsair", + "griptape", + "plugin" + ], + "author": "", + "license": "Apache-2.0", + "files": [ + "dist" + ] +} diff --git a/packages/griptape/schema.test.ts b/packages/griptape/schema.test.ts new file mode 100644 index 000000000..8539f7ef1 --- /dev/null +++ b/packages/griptape/schema.test.ts @@ -0,0 +1,20 @@ +import { GriptapeSchema } from './schema'; + +describe('Griptape schema', () => { + it('declares a semver version', () => { + expect(GriptapeSchema.version).toBeDefined(); + expect(GriptapeSchema.version).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('declares an entities map', () => { + expect(typeof GriptapeSchema.entities).toBe('object'); + expect(GriptapeSchema.entities).not.toBeNull(); + expect(Array.isArray(Object.keys(GriptapeSchema.entities))).toBe(true); + for (const entity of Object.values(GriptapeSchema.entities)) { + expect(entity).toBeDefined(); + } + }); +}); + +// Per .github/PLUGIN_PR_RULES.md (R2), every implemented endpoint +// needs a corresponding test. diff --git a/packages/griptape/schema/database.ts b/packages/griptape/schema/database.ts new file mode 100644 index 000000000..294335055 --- /dev/null +++ b/packages/griptape/schema/database.ts @@ -0,0 +1,7 @@ +// TODO: Define your database entities here +// export const GriptapeExample = z.object({ +// id: z.string(), +// name: z.string(), +// created_at: z.coerce.date().nullable().optional(), +// }); +// export type GriptapeExample = z.infer; diff --git a/packages/griptape/schema/index.ts b/packages/griptape/schema/index.ts new file mode 100644 index 000000000..9bb06c397 --- /dev/null +++ b/packages/griptape/schema/index.ts @@ -0,0 +1,4 @@ +export const GriptapeSchema = { + version: '1.0.0', + entities: {}, +} as const; diff --git a/packages/griptape/tsconfig.json b/packages/griptape/tsconfig.json new file mode 100644 index 000000000..15e507a13 --- /dev/null +++ b/packages/griptape/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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/griptape/tsup.config.ts b/packages/griptape/tsup.config.ts new file mode 100644 index 000000000..3ec221e23 --- /dev/null +++ b/packages/griptape/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/griptape/webhooks/example.ts b/packages/griptape/webhooks/example.ts new file mode 100644 index 000000000..efb81f371 --- /dev/null +++ b/packages/griptape/webhooks/example.ts @@ -0,0 +1,32 @@ +import { logEventFromContext } from 'corsair/core'; +import type { GriptapeWebhooks } from '..'; +import { createGriptapeMatch, verifyGriptapeWebhookSignature } from './types'; + +export const example: GriptapeWebhooks['example'] = { + match: createGriptapeMatch('example'), + + handler: async (ctx, request) => { + const verification = verifyGriptapeWebhookSignature(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, + 'griptape.webhook.example', + { ...event }, + 'completed', + ); + + return { success: true, data: event }; + }, +}; diff --git a/packages/griptape/webhooks/index.ts b/packages/griptape/webhooks/index.ts new file mode 100644 index 000000000..a12134e8a --- /dev/null +++ b/packages/griptape/webhooks/index.ts @@ -0,0 +1,9 @@ +import { example } from './example'; + +export const ExampleWebhooks = { + example: example, +}; + +export * from './oauth-tenant-link'; +export * from './tenant-matcher'; +export * from './types'; diff --git a/packages/griptape/webhooks/oauth-tenant-link.ts b/packages/griptape/webhooks/oauth-tenant-link.ts new file mode 100644 index 000000000..e98aca1f5 --- /dev/null +++ b/packages/griptape/webhooks/oauth-tenant-link.ts @@ -0,0 +1,31 @@ +import type { TokenResponse, WebhookTenantMatch } from 'corsair/core'; +import { 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 resolveGriptapeOAuthWebhookTenantLink( + 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/griptape/webhooks/tenant-matcher.ts b/packages/griptape/webhooks/tenant-matcher.ts new file mode 100644 index 000000000..e830b8b7a --- /dev/null +++ b/packages/griptape/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 matchGriptapeTenantWebhook( + 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/griptape/webhooks/types.ts b/packages/griptape/webhooks/types.ts new file mode 100644 index 000000000..921adc343 --- /dev/null +++ b/packages/griptape/webhooks/types.ts @@ -0,0 +1,64 @@ +import type { + CorsairWebhookMatcher, + RawWebhookRequest, + WebhookRequest, +} from 'corsair/core'; +import { z } from 'zod'; + +export const GriptapeWebhookPayloadSchema = z.object({ + type: z.string(), + created_at: z.string(), + data: z.record(z.string(), z.unknown()), +}); + +export type GriptapeWebhookPayload = z.infer< + typeof GriptapeWebhookPayloadSchema +>; + +export const ExampleEventSchema = GriptapeWebhookPayloadSchema.extend({ + type: z.literal('example'), + data: z + .object({ + id: z.string(), + }) + .loose(), +}); + +export type ExampleEvent = z.infer; + +export type GriptapeWebhookOutputs = { + 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 createGriptapeMatch(eventType: string): CorsairWebhookMatcher { + return (request: RawWebhookRequest) => { + const parsedBody = parseBody(request.body); + return parsedBody !== null && parsedBody.type === eventType; + }; +} + +export function verifyGriptapeWebhookSignature( + 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 3f81f23e0..097a780b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,7 +130,7 @@ importers: version: link:../../packages/slack corsair: specifier: ^0.1.4 - version: 0.1.119(postgres@3.4.7)(react@19.2.7) + version: link:../../packages/corsair dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -186,6 +186,9 @@ importers: '@corsair-dev/googlesheets': specifier: workspace:* version: link:../../packages/googlesheets + '@corsair-dev/griptape': + specifier: workspace:* + version: link:../../packages/griptape '@corsair-dev/hubspot': specifier: workspace:^ version: link:../../packages/hubspot @@ -2779,6 +2782,33 @@ importers: specifier: 4.4.3 version: 4.4.3 + packages/griptape: + 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)) + rimraf: + specifier: ^6.1.3 + version: 6.1.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/groqcloud: devDependencies: '@types/jest': @@ -5954,36 +5984,6 @@ packages: '@codemirror/view@6.43.3': resolution: {integrity: sha512-MwEwCAr/o0agJefhC2+reBv5kfOQpMcDRUNQrRYZgWlhH8IwQcerMZrpqWyUFSyO0ebgN2cnh/w87F7G4BGSng==} - '@corsair-dev/frpc-darwin-arm64@0.1.117': - resolution: {integrity: sha512-PrxfqHlRKzm5xW5J2wnZ1knVF4xV6svgmeo11YvHMAAUqsNBY6mczMQFlu1HJcMvZ9yysqMiTZBKdjAC+DnhlQ==} - cpu: [arm64] - os: [darwin] - - '@corsair-dev/frpc-darwin-x64@0.1.117': - resolution: {integrity: sha512-8GZP5G5kPhu/TPGLnSgecvyyxXB6+ZdwDmXAhz3PvqiE8mmQ2PRa7FaoVYdOa4nY8XWa5YmnXC/DcjsTLIfPmQ==} - cpu: [x64] - os: [darwin] - - '@corsair-dev/frpc-linux-arm64@0.1.117': - resolution: {integrity: sha512-TwVeRBYd17hy6YJByCUouMVBo2itK4fPuvm3TPwdq5ZVxN64PEKE2KHQIo+uW7opxcMXre6DVSzLQyFmrqI/Xg==} - cpu: [arm64] - os: [linux] - - '@corsair-dev/frpc-linux-x64@0.1.117': - resolution: {integrity: sha512-Ch98qSFQXYSA6sg2ysTKrRCZ08tNWfXjSEHKT3nMM7vYhuDXkQcBmXM4tbPEO8SI6BzLa9/g1lsXT5T0hx48dQ==} - cpu: [x64] - os: [linux] - - '@corsair-dev/frpc-win32-arm64@0.1.117': - resolution: {integrity: sha512-QyLWoaVNrq1C5V81d7DIOLm63n4+0YSKcKJK1MUTdaQAEroaejybeAuskiYhq11LCJ2wb+WTkv3qaWF0yLgg6g==} - cpu: [arm64] - os: [win32] - - '@corsair-dev/frpc-win32-x64@0.1.117': - resolution: {integrity: sha512-V4sI1JUwlNBi9nAJHedXPXzmivfi5DHaU2yJMwnf++5PQvVcxRMJQcec1p9+Q8bPyAZ7gtq+4XdRuBJVZXdFkg==} - cpu: [x64] - os: [win32] - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -11038,14 +11038,6 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} - corsair@0.1.119: - resolution: {integrity: sha512-Cl86/VhMNOdzTbUzmeb9RwlhuucojM1gEmgTQJsQYdcxP2qFiPPly2VGK9M2h5GkHLZ3kZ4HRALTQP3wIhepBQ==} - peerDependencies: - react: '>=18.0.0' - peerDependenciesMeta: - react: - optional: true - cosmiconfig@9.0.1: resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} @@ -17433,24 +17425,6 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 - '@corsair-dev/frpc-darwin-arm64@0.1.117': - optional: true - - '@corsair-dev/frpc-darwin-x64@0.1.117': - optional: true - - '@corsair-dev/frpc-linux-arm64@0.1.117': - optional: true - - '@corsair-dev/frpc-linux-x64@0.1.117': - optional: true - - '@corsair-dev/frpc-win32-arm64@0.1.117': - optional: true - - '@corsair-dev/frpc-win32-x64@0.1.117': - optional: true - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -22949,23 +22923,6 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - corsair@0.1.119(postgres@3.4.7)(react@19.2.7): - dependencies: - kysely: 0.28.17 - kysely-postgres-js: 3.0.0(kysely@0.28.17)(postgres@3.4.7) - uuid: 13.0.0 - zod: 4.4.3 - optionalDependencies: - '@corsair-dev/frpc-darwin-arm64': 0.1.117 - '@corsair-dev/frpc-darwin-x64': 0.1.117 - '@corsair-dev/frpc-linux-arm64': 0.1.117 - '@corsair-dev/frpc-linux-x64': 0.1.117 - '@corsair-dev/frpc-win32-arm64': 0.1.117 - '@corsair-dev/frpc-win32-x64': 0.1.117 - react: 19.2.7 - transitivePeerDependencies: - - postgres - cosmiconfig@9.0.1(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -25085,12 +25042,6 @@ snapshots: kleur@4.1.5: {} - kysely-postgres-js@3.0.0(kysely@0.28.17)(postgres@3.4.7): - dependencies: - kysely: 0.28.17 - optionalDependencies: - postgres: 3.4.7 - kysely-postgres-js@3.0.0(kysely@0.28.9)(postgres@3.4.7): dependencies: kysely: 0.28.9 From 91f61b7ff9642943b7e3c8a4aaf46208a53c9444 Mon Sep 17 00:00:00 2001 From: Mehul Gupta Date: Sat, 22 Aug 2026 19:16:50 +0530 Subject: [PATCH 2/6] Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- demo/testing/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/demo/testing/package.json b/demo/testing/package.json index 0dc54ab16..3dcf0da17 100644 --- a/demo/testing/package.json +++ b/demo/testing/package.json @@ -33,7 +33,6 @@ "@corsair-dev/gmail": "workspace:*", "@corsair-dev/googlecalendar": "workspace:*", "@corsair-dev/googlesheets": "workspace:*", - "@corsair-dev/griptape": "workspace:*", "@corsair-dev/hubspot": "workspace:^", "@corsair-dev/linear": "workspace:*", "@corsair-dev/mcp": "workspace:*", From 34f2c6c55530b94ba677e99dc2387a3870ae3067 Mon Sep 17 00:00:00 2001 From: Mehul Gupta Date: Sat, 22 Aug 2026 19:21:37 +0530 Subject: [PATCH 3/6] chore: update lockfile --- pnpm-lock.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 097a780b3..4f905fd79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,9 +186,6 @@ importers: '@corsair-dev/googlesheets': specifier: workspace:* version: link:../../packages/googlesheets - '@corsair-dev/griptape': - specifier: workspace:* - version: link:../../packages/griptape '@corsair-dev/hubspot': specifier: workspace:^ version: link:../../packages/hubspot @@ -211,7 +208,7 @@ importers: specifier: workspace:* version: link:../../packages/openweathermap '@corsair-dev/outlook': - specifier: workspace:* + specifier: workspace:s* version: link:../../packages/outlook '@corsair-dev/reddit': specifier: workspace:* From 8ead34b61179db015b4a216e4feb7e559eccce2f Mon Sep 17 00:00:00 2001 From: Mayank Saini Date: Sun, 23 Aug 2026 18:39:00 +0530 Subject: [PATCH 4/6] fix(griptape): rethrow ApiError unchanged so rate-limit and auth handlers work Wrapping every failure as GriptapeAPIError stripped status/retryAfter, so RATE_LIMIT_ERROR never matched a real 429 ('Too Many Requests' contains neither '429' nor 'rate_limited') and Retry-After was lost. ApiError now propagates as-is; only non-API failures are wrapped. Adds edge-case tests for 429/401 propagation, network errors, and non-Error rejections. --- packages/griptape/client.test.ts | 94 ++++++++++++++++++++++++++++++++ packages/griptape/client.ts | 8 ++- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 packages/griptape/client.test.ts diff --git a/packages/griptape/client.test.ts b/packages/griptape/client.test.ts new file mode 100644 index 000000000..43ad3852d --- /dev/null +++ b/packages/griptape/client.test.ts @@ -0,0 +1,94 @@ +import type { ApiRequestOptions, ApiResult } from 'corsair/http'; +import { ApiError, request } from 'corsair/http'; +import { GriptapeAPIError, makeGriptapeRequest } from './client'; + +jest.mock('corsair/http', () => ({ + ...jest.requireActual('corsair/http'), + request: jest.fn(), +})); + +const mockRequest = request as jest.MockedFunction; + +const sampleRequest: ApiRequestOptions = { + method: 'GET', + url: 'assistants', +}; + +function apiErrorOf( + status: number, + statusText: string, + retryAfterMs?: number, +): ApiError { + const result: ApiResult = { + url: 'https://cloud.griptape.ai/api/assistants', + ok: false, + status, + statusText, + body: { message: statusText }, + }; + return new ApiError( + sampleRequest, + result, + statusText, + retryAfterMs === undefined ? undefined : { retryAfter: retryAfterMs }, + ); +} + +describe('makeGriptapeRequest error handling', () => { + beforeEach(() => { + mockRequest.mockReset(); + }); + + it('rethrows ApiError unchanged so status-based handlers keep working', async () => { + const rateLimitError = apiErrorOf(429, 'Too Many Requests', 30000); + mockRequest.mockRejectedValueOnce(rateLimitError); + + await expect( + makeGriptapeRequest('assistants', 'test-api-key'), + ).rejects.toBe(rateLimitError); + }); + + it('keeps status and Retry-After readable on the rethrown rate-limit error', async () => { + mockRequest.mockRejectedValueOnce( + apiErrorOf(429, 'Too Many Requests', 45000), + ); + + const error = await makeGriptapeRequest('assistants', 'test-api-key').then( + () => null, + (error: unknown) => error, + ); + + expect(error).toBeInstanceOf(ApiError); + expect(error).toMatchObject({ + name: 'ApiError', + status: 429, + retryAfter: 45000, + }); + }); + + it('propagates authentication errors with their status code', async () => { + const authError = apiErrorOf(401, 'Unauthorized'); + mockRequest.mockRejectedValueOnce(authError); + + await expect(makeGriptapeRequest('assistants', 'invalid-key')).rejects.toBe( + authError, + ); + }); + + it('wraps non-API network failures as GriptapeAPIError', async () => { + mockRequest.mockRejectedValueOnce(new Error('socket hang up')); + + const rejection = makeGriptapeRequest('assistants', 'test-api-key'); + + await expect(rejection).rejects.toBeInstanceOf(GriptapeAPIError); + await expect(rejection).rejects.toThrow('socket hang up'); + }); + + it('maps non-Error rejections to GriptapeAPIError with a generic message', async () => { + mockRequest.mockRejectedValueOnce('boom'); + + await expect( + makeGriptapeRequest('assistants', 'test-api-key'), + ).rejects.toEqual(new GriptapeAPIError('Unknown error')); + }); +}); diff --git a/packages/griptape/client.ts b/packages/griptape/client.ts index 1f137f438..13bf3a342 100644 --- a/packages/griptape/client.ts +++ b/packages/griptape/client.ts @@ -1,5 +1,5 @@ import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; -import { request } from 'corsair/http'; +import { ApiError, request } from 'corsair/http'; export class GriptapeAPIError extends Error { constructor( @@ -50,6 +50,12 @@ export async function makeGriptapeRequest( try { return await request(config, requestOptions); } catch (error) { + // Re-thrown as-is: ApiError already carries the HTTP status code and + // Retry-After info that error-handlers.ts inspects. Wrapping it here + // would hide those fields behind a message string. + if (error instanceof ApiError) { + throw error; + } if (error instanceof Error) { throw new GriptapeAPIError(error.message); } From 33ecd1c77a0dd1f9a73a1261ab9f94e686696742 Mon Sep 17 00:00:00 2001 From: Mayank Saini Date: Sun, 23 Aug 2026 19:40:06 +0530 Subject: [PATCH 5/6] fix(griptape): remove webhook boilerplate, fix UUID validation, harden types/tests - Delete generator-residue webhooks/ (Griptape Cloud has no webhook support); removes the always-pass signature verifier flagged by Greptile - Replace buggy UUID regex (accepted garbage strings) with zod v4 z.uuid() - Drop unsupported oauth_2 auth option and webhook key branches; api_key only - Replace TODO stub in schema/database.ts with a real explanation - Remove 'as any' from endpoint tests; add error-handler and schema edge-case tests (28 tests, 5 suites, all passing) --- packages/griptape/endpoints.test.ts | 122 +++++++++++++-- packages/griptape/endpoints/types.ts | 19 +-- packages/griptape/error-handlers.test.ts | 141 ++++++++++++++++++ packages/griptape/error-handlers.ts | 6 +- packages/griptape/index.ts | 72 ++------- packages/griptape/schema.test.ts | 3 - packages/griptape/schema/database.ts | 10 +- packages/griptape/webhooks/example.ts | 32 ---- packages/griptape/webhooks/index.ts | 9 -- .../griptape/webhooks/oauth-tenant-link.ts | 31 ---- packages/griptape/webhooks/tenant-matcher.ts | 25 ---- packages/griptape/webhooks/types.ts | 64 -------- pnpm-lock.yaml | 2 +- 13 files changed, 280 insertions(+), 256 deletions(-) create mode 100644 packages/griptape/error-handlers.test.ts delete mode 100644 packages/griptape/webhooks/example.ts delete mode 100644 packages/griptape/webhooks/index.ts delete mode 100644 packages/griptape/webhooks/oauth-tenant-link.ts delete mode 100644 packages/griptape/webhooks/tenant-matcher.ts delete mode 100644 packages/griptape/webhooks/types.ts diff --git a/packages/griptape/endpoints.test.ts b/packages/griptape/endpoints.test.ts index a332e052a..270e0a8e4 100644 --- a/packages/griptape/endpoints.test.ts +++ b/packages/griptape/endpoints.test.ts @@ -1,4 +1,14 @@ -import { request } from 'corsair/http'; +import { logEventFromContext } from 'corsair/core'; +import { ApiError, request } from 'corsair/http'; +import type { + AssistantGetResponse, + AssistantListResponse, +} from './endpoints/types'; +import { + GriptapeEndpointInputSchemas, + GriptapeEndpointOutputSchemas, +} from './endpoints/types'; +import type { GriptapeContext } from './index'; import { griptape } from './index'; jest.mock('corsair/http', () => ({ @@ -12,19 +22,28 @@ jest.mock('corsair/core', () => ({ })); const mockRequest = request as jest.MockedFunction; +const mockLog = jest.mocked(logEventFromContext); describe('Griptape Plugin API', () => { const apiKey = 'test-api-key'; - const plugin = griptape({ key: apiKey }) as any; - const ctx = { key: apiKey } as any; + + const griptapePlugin = griptape({ key: apiKey }); + const endpoints = griptapePlugin.endpoints; + if (!endpoints) throw new Error('griptape plugin must expose endpoints'); + + // Narrow assertion: the handlers under test only read ctx.key, and + // logEventFromContext is mocked above, so a partial context is safe here. + // A full context would require constructing the encrypted key manager. + const ctx = { key: apiKey } as unknown as GriptapeContext; beforeEach(() => { mockRequest.mockReset(); + mockLog.mockClear(); }); - describe('listAssistants', () => { + describe('assistant.list', () => { it('sends GET /assistants with pagination parameters', async () => { - const mockResponse = { + const mockResponse: AssistantListResponse = { assistants: [ { assistant_id: '550e8400-e29b-41d4-a716-446655440000', @@ -55,7 +74,7 @@ describe('Griptape Plugin API', () => { mockRequest.mockResolvedValueOnce(mockResponse); - const result = await plugin.endpoints.assistant.list(ctx, { + const result = await endpoints.assistant.list(ctx, { page: 1, page_size: 10, }); @@ -81,7 +100,7 @@ describe('Griptape Plugin API', () => { }); it('lists assistants without optional pagination parameters', async () => { - const mockResponse = { + const mockResponse: AssistantListResponse = { assistants: [], pagination: { page_number: 1, @@ -93,7 +112,7 @@ describe('Griptape Plugin API', () => { mockRequest.mockResolvedValueOnce(mockResponse); - const result = await plugin.endpoints.assistant.list(ctx, {}); + const result = await endpoints.assistant.list(ctx, {}); expect(mockRequest).toHaveBeenCalledWith( expect.anything(), @@ -111,9 +130,9 @@ describe('Griptape Plugin API', () => { }); }); - describe('getAssistant', () => { + describe('assistant.get', () => { it('sends GET /assistants/{assistant_id}', async () => { - const mockResponse = { + const mockResponse: AssistantGetResponse = { assistant_id: '550e8400-e29b-41d4-a716-446655440000', created_at: '2026-01-01T00:00:00Z', created_by: 'user@example.com', @@ -130,7 +149,7 @@ describe('Griptape Plugin API', () => { mockRequest.mockResolvedValueOnce(mockResponse); - const result = await plugin.endpoints.assistant.get(ctx, { + const result = await endpoints.assistant.get(ctx, { assistant_id: '550e8400-e29b-41d4-a716-446655440000', }); @@ -149,5 +168,86 @@ describe('Griptape Plugin API', () => { expect(result).toEqual(mockResponse); }); + + it('propagates ApiError unchanged so status-based handlers keep working', async () => { + const requestOptions = { + method: 'GET' as const, + url: 'assistants/550e8400-e29b-41d4-a716-446655440000', + }; + const rateLimitError = new ApiError( + requestOptions, + { + url: 'https://cloud.griptape.ai/api/assistants/550e8400-e29b-41d4-a716-446655440000', + ok: false, + status: 429, + statusText: 'Too Many Requests', + body: { message: 'Too Many Requests' }, + }, + 'Too Many Requests', + { retryAfter: 30000 }, + ); + + mockRequest.mockRejectedValueOnce(rateLimitError); + + await expect( + endpoints.assistant.get(ctx, { + assistant_id: '550e8400-e29b-41d4-a716-446655440000', + }), + ).rejects.toBe(rateLimitError); + expect(mockLog).not.toHaveBeenCalled(); + }); + + it('rejects non-UUID assistant ids at the input schema boundary', () => { + const result = GriptapeEndpointInputSchemas.assistantGet.safeParse({ + assistant_id: 'not-a-uuid', + }); + + expect(result.success).toBe(false); + }); + }); + + describe('endpoint schemas', () => { + it('validates well-formed list responses through the output schema', () => { + const parsed = GriptapeEndpointOutputSchemas.assistantList.safeParse({ + assistants: [], + pagination: { + page_number: 2, + page_size: 20, + total_count: 41, + total_pages: 3, + next_page: 3, + previous_page: 1, + }, + }); + + expect(parsed.success).toBe(true); + }); + + it('rejects responses with missing pagination fields', () => { + const parsed = GriptapeEndpointOutputSchemas.assistantList.safeParse({ + assistants: [], + }); + + expect(parsed.success).toBe(false); + }); + + it('rejects detail payloads where ids are not UUIDs', () => { + const parsed = GriptapeEndpointOutputSchemas.assistantGet.safeParse({ + assistant_id: 'garbage-id', + created_at: '2026-01-01T00:00:00Z', + created_by: 'user@example.com', + description: 'Test assistant', + knowledge_base_ids: [], + name: 'Test Assistant', + organization_id: '550e8400-e29b-41d4-a716-446655440001', + retriever_ids: [], + ruleset_ids: [], + structure_ids: [], + tool_ids: [], + updated_at: '2026-01-01T00:00:00Z', + }); + + expect(parsed.success).toBe(false); + }); }); }); diff --git a/packages/griptape/endpoints/types.ts b/packages/griptape/endpoints/types.ts index ef42bcd90..b94ffd20f 100644 --- a/packages/griptape/endpoints/types.ts +++ b/packages/griptape/endpoints/types.ts @@ -1,27 +1,24 @@ import { z } from 'zod'; -const UUID_REGEX = - /^[0-9(a-f|A-F)]{8}-[0-9(a-f|A-F)]{4}-4[0-9(a-f|A-F)]{3}-[89ab][0-9(a-f|A-F)]{3}-[0-9(a-f|A-F)]{12}$/; - export const AssistantDetailSchema = z.object({ - assistant_id: z.string().regex(UUID_REGEX), + assistant_id: z.uuid(), created_at: z.string().datetime(), created_by: z.string(), description: z.string(), input: z.string().optional(), - knowledge_base_ids: z.array(z.string().regex(UUID_REGEX)), + knowledge_base_ids: z.array(z.uuid()), model: z.string().optional(), name: z.string(), - organization_id: z.string().regex(UUID_REGEX), - retriever_ids: z.array(z.string().regex(UUID_REGEX)), - ruleset_ids: z.array(z.string().regex(UUID_REGEX)), - structure_ids: z.array(z.string().regex(UUID_REGEX)), - tool_ids: z.array(z.string().regex(UUID_REGEX)), + organization_id: z.uuid(), + retriever_ids: z.array(z.uuid()), + ruleset_ids: z.array(z.uuid()), + structure_ids: z.array(z.uuid()), + tool_ids: z.array(z.uuid()), updated_at: z.string().datetime(), }); const AssistantGetInputSchema = z.object({ - assistant_id: z.string().regex(UUID_REGEX), + assistant_id: z.uuid(), }); const AssistantGetResponseSchema = AssistantDetailSchema; diff --git a/packages/griptape/error-handlers.test.ts b/packages/griptape/error-handlers.test.ts new file mode 100644 index 000000000..a9263983c --- /dev/null +++ b/packages/griptape/error-handlers.test.ts @@ -0,0 +1,141 @@ +import type { ErrorContext } from 'corsair/core'; +import type { ApiRequestOptions } from 'corsair/http'; +import { ApiError } from 'corsair/http'; +import { errorHandlers } from './error-handlers'; + +const sampleRequest: ApiRequestOptions = { + method: 'GET', + url: 'assistants', +}; + +function apiErrorOf( + status: number, + statusText: string, + retryAfterMs?: number, +): ApiError { + return new ApiError( + sampleRequest, + { + url: 'https://cloud.griptape.ai/api/assistants', + ok: false, + status, + statusText, + body: { message: statusText }, + }, + statusText, + retryAfterMs === undefined ? undefined : { retryAfter: retryAfterMs }, + ); +} + +const errorContext: ErrorContext = { + pluginId: 'griptape', + operation: 'assistant.list', + input: {}, + originalError: new Error('original'), +}; + +type ErrorHandlerKey = 'RATE_LIMIT_ERROR' | 'AUTH_ERROR' | 'DEFAULT'; + +function handlerFor(key: ErrorHandlerKey) { + const entry = errorHandlers[key]; + if (!entry) throw new Error(`missing ${key} error handler`); + return entry; +} + +describe('griptape error handlers', () => { + describe('RATE_LIMIT_ERROR', () => { + it('matches ApiError with status 429', () => { + expect( + handlerFor('RATE_LIMIT_ERROR').match( + apiErrorOf(429, 'x'), + errorContext, + ), + ).toBe(true); + }); + + it('matches rate-limit text on plain errors', () => { + expect( + handlerFor('RATE_LIMIT_ERROR').match( + new Error('rate_limited: slow down'), + errorContext, + ), + ).toBe(true); + }); + + it('does not match unrelated errors', () => { + expect( + handlerFor('RATE_LIMIT_ERROR').match( + new Error('not found'), + errorContext, + ), + ).toBe(false); + }); + + it('passes the provider Retry-After through to the retry policy', async () => { + const policy = await handlerFor('RATE_LIMIT_ERROR').handler( + apiErrorOf(429, 'Too Many Requests', 45000), + errorContext, + ); + + expect(policy).toEqual({ maxRetries: 5, headersRetryAfterMs: 45000 }); + }); + + it('omits Retry-After when the provider does not send one', async () => { + const policy = await handlerFor('RATE_LIMIT_ERROR').handler( + apiErrorOf(429, 'Too Many Requests'), + errorContext, + ); + + expect(policy).toEqual({ maxRetries: 5, headersRetryAfterMs: undefined }); + }); + }); + + describe('AUTH_ERROR', () => { + it('matches ApiError with status 401', () => { + expect( + handlerFor('AUTH_ERROR').match(apiErrorOf(401, 'x'), errorContext), + ).toBe(true); + }); + + it('matches unauthorized text on plain errors', () => { + expect( + handlerFor('AUTH_ERROR').match( + new Error('unauthorized key'), + errorContext, + ), + ).toBe(true); + }); + + it('does not match 403 errors', () => { + expect( + handlerFor('AUTH_ERROR').match(apiErrorOf(403, 'x'), errorContext), + ).toBe(false); + }); + + it('does not retry auth failures', async () => { + const policy = await handlerFor('AUTH_ERROR').handler( + apiErrorOf(401, 'Unauthorized'), + errorContext, + ); + + expect(policy).toEqual({ maxRetries: 0 }); + }); + }); + + describe('DEFAULT', () => { + it('matches every error', () => { + expect( + handlerFor('DEFAULT').match(new Error('anything'), errorContext), + ).toBe(true); + }); + + it('does not retry by default', async () => { + const policy = await handlerFor('DEFAULT').handler( + new Error('boom'), + errorContext, + ); + + expect(policy).toEqual({ maxRetries: 0 }); + }); + }); +}); diff --git a/packages/griptape/error-handlers.ts b/packages/griptape/error-handlers.ts index 5a4f4c19f..ac3cc9f12 100644 --- a/packages/griptape/error-handlers.ts +++ b/packages/griptape/error-handlers.ts @@ -1,7 +1,9 @@ import type { CorsairErrorHandler } from 'corsair/core'; import { ApiError } from 'corsair/http'; -export const errorHandlers = { +// Annotated (not `satisfies`) so match/handler keep the full canonical +// signature — callers and tests pass the error through uniformly. +export const errorHandlers: CorsairErrorHandler = { RATE_LIMIT_ERROR: { match: (error: Error) => { if (error instanceof ApiError && error.status === 429) return true; @@ -28,4 +30,4 @@ export const errorHandlers = { match: () => true, handler: async () => ({ maxRetries: 0 }), }, -} satisfies CorsairErrorHandler; +}; diff --git a/packages/griptape/index.ts b/packages/griptape/index.ts index ec7afbe31..9a7272e04 100644 --- a/packages/griptape/index.ts +++ b/packages/griptape/index.ts @@ -1,12 +1,10 @@ import type { AuthTypes, BindEndpoints, - BindWebhooks, CorsairEndpoint, CorsairErrorHandler, CorsairPlugin, CorsairPluginContext, - CorsairWebhook, KeyBuilderContext, PickAuth, PluginAuthConfig, @@ -15,6 +13,7 @@ import type { RequiredPluginEndpointSchemas, RequiredPluginWebhookSchemas, } from 'corsair/core'; + import { Assistant } from './endpoints'; import type { GriptapeEndpointInputs, @@ -26,16 +25,10 @@ import { } from './endpoints/types'; import { errorHandlers } from './error-handlers'; import { GriptapeSchema } from './schema'; -import { ExampleWebhooks } from './webhooks'; -import { resolveGriptapeOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link'; -import { matchGriptapeTenantWebhook } from './webhooks/tenant-matcher'; -import type { ExampleEvent, GriptapeWebhookOutputs } from './webhooks/types'; -import { ExampleEventSchema } from './webhooks/types'; export type GriptapePluginOptions = { - authType?: PickAuth<'api_key' | 'oauth_2'>; + authType?: PickAuth<'api_key'>; key?: string; - webhookSecret?: string; hooks?: InternalGriptapePlugin['hooks']; webhookHooks?: InternalGriptapePlugin['webhookHooks']; errorHandlers?: CorsairErrorHandler; @@ -66,16 +59,9 @@ export type GriptapeEndpoints = { assistantGet: GriptapeEndpoint<'assistantGet'>; }; -type GriptapeWebhook< - K extends keyof GriptapeWebhookOutputs, - TEvent, -> = CorsairWebhook; - -export type GriptapeWebhooks = { - example: GriptapeWebhook<'example', ExampleEvent>; -}; +export type GriptapeWebhooks = Record; -export type GriptapeBoundWebhooks = BindWebhooks; +export type GriptapeBoundWebhooks = Record; const griptapeEndpointsNested = { assistant: { @@ -84,11 +70,7 @@ const griptapeEndpointsNested = { }, } as const; -const griptapeWebhooksNested = { - example: { - example: ExampleWebhooks.example, - }, -} as const; +const griptapeWebhooksNested = {} as const; export const griptapeEndpointSchemas = { 'assistant.list': { @@ -103,15 +85,10 @@ export const griptapeEndpointSchemas = { typeof griptapeEndpointsNested >; -const griptapeWebhookSchemas = { - 'example.example': { - description: 'An example webhook event', - payload: ExampleEventSchema, - response: ExampleEventSchema, - }, -} as const satisfies RequiredPluginWebhookSchemas< - typeof griptapeWebhooksNested ->; +const griptapeWebhookSchemas = + {} as const satisfies RequiredPluginWebhookSchemas< + typeof griptapeWebhooksNested + >; const defaultAuthType: AuthTypes = 'api_key' as const; @@ -130,9 +107,6 @@ export const griptapeAuthConfig = { api_key: { account: ['tenant_external_id'] as const, }, - oauth_2: { - account: ['tenant_external_id'] as const, - }, } as const satisfies PluginAuthConfig; export type BaseGriptapePlugin = CorsairPlugin< @@ -168,27 +142,14 @@ export function griptape( endpointMeta: griptapeEndpointMeta, endpointSchemas: griptapeEndpointSchemas, webhookSchemas: griptapeWebhookSchemas, - pluginWebhookMatcher: (request) => { - const headers = request.headers; - // TODO: Update to match your webhook signature headers - return 'x-griptape-signature' in headers; - }, - pluginTenantWebhookMatcher: matchGriptapeTenantWebhook, - oauthWebhookTenantLinkResolver: resolveGriptapeOAuthWebhookTenantLink, + pluginWebhookMatcher: () => false, + errorHandlers: { ...errorHandlers, ...options.errorHandlers, }, - keyBuilder: async (ctx: GriptapeKeyBuilderContext, source) => { - if (source === 'webhook' && options.webhookSecret) { - return options.webhookSecret; - } - - if (source === 'webhook') { - const res = await ctx.keys.get_webhook_signature(); - return res ?? ''; - } + keyBuilder: async (ctx: GriptapeKeyBuilderContext, source) => { if (source === 'endpoint' && options.key) { return options.key; } @@ -198,11 +159,6 @@ export function griptape( return res ?? ''; } - if (source === 'endpoint' && ctx.authType === 'oauth_2') { - const res = await ctx.keys.get_access_token(); - return res ?? ''; - } - return ''; }, } satisfies InternalGriptapePlugin; @@ -214,7 +170,3 @@ export type { GriptapeEndpointInputs, GriptapeEndpointOutputs, } from './endpoints/types'; -export type { - ExampleEvent, - GriptapeWebhookOutputs, -} from './webhooks/types'; diff --git a/packages/griptape/schema.test.ts b/packages/griptape/schema.test.ts index 8539f7ef1..d69565682 100644 --- a/packages/griptape/schema.test.ts +++ b/packages/griptape/schema.test.ts @@ -15,6 +15,3 @@ describe('Griptape schema', () => { } }); }); - -// Per .github/PLUGIN_PR_RULES.md (R2), every implemented endpoint -// needs a corresponding test. diff --git a/packages/griptape/schema/database.ts b/packages/griptape/schema/database.ts index 294335055..def23ef05 100644 --- a/packages/griptape/schema/database.ts +++ b/packages/griptape/schema/database.ts @@ -1,7 +1,3 @@ -// TODO: Define your database entities here -// export const GriptapeExample = z.object({ -// id: z.string(), -// name: z.string(), -// created_at: z.coerce.date().nullable().optional(), -// }); -// export type GriptapeExample = z.infer; +// Griptape has no persisted database entities yet. Assistant metadata is +// fetched live from the Griptape Cloud API on each call, so this plugin's +// schema declares an empty entity set. diff --git a/packages/griptape/webhooks/example.ts b/packages/griptape/webhooks/example.ts deleted file mode 100644 index efb81f371..000000000 --- a/packages/griptape/webhooks/example.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { logEventFromContext } from 'corsair/core'; -import type { GriptapeWebhooks } from '..'; -import { createGriptapeMatch, verifyGriptapeWebhookSignature } from './types'; - -export const example: GriptapeWebhooks['example'] = { - match: createGriptapeMatch('example'), - - handler: async (ctx, request) => { - const verification = verifyGriptapeWebhookSignature(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, - 'griptape.webhook.example', - { ...event }, - 'completed', - ); - - return { success: true, data: event }; - }, -}; diff --git a/packages/griptape/webhooks/index.ts b/packages/griptape/webhooks/index.ts deleted file mode 100644 index a12134e8a..000000000 --- a/packages/griptape/webhooks/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { example } from './example'; - -export const ExampleWebhooks = { - example: example, -}; - -export * from './oauth-tenant-link'; -export * from './tenant-matcher'; -export * from './types'; diff --git a/packages/griptape/webhooks/oauth-tenant-link.ts b/packages/griptape/webhooks/oauth-tenant-link.ts deleted file mode 100644 index e98aca1f5..000000000 --- a/packages/griptape/webhooks/oauth-tenant-link.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { TokenResponse, WebhookTenantMatch } from 'corsair/core'; -import { 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 resolveGriptapeOAuthWebhookTenantLink( - 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/griptape/webhooks/tenant-matcher.ts b/packages/griptape/webhooks/tenant-matcher.ts deleted file mode 100644 index e830b8b7a..000000000 --- a/packages/griptape/webhooks/tenant-matcher.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 matchGriptapeTenantWebhook( - 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/griptape/webhooks/types.ts b/packages/griptape/webhooks/types.ts deleted file mode 100644 index 921adc343..000000000 --- a/packages/griptape/webhooks/types.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { - CorsairWebhookMatcher, - RawWebhookRequest, - WebhookRequest, -} from 'corsair/core'; -import { z } from 'zod'; - -export const GriptapeWebhookPayloadSchema = z.object({ - type: z.string(), - created_at: z.string(), - data: z.record(z.string(), z.unknown()), -}); - -export type GriptapeWebhookPayload = z.infer< - typeof GriptapeWebhookPayloadSchema ->; - -export const ExampleEventSchema = GriptapeWebhookPayloadSchema.extend({ - type: z.literal('example'), - data: z - .object({ - id: z.string(), - }) - .loose(), -}); - -export type ExampleEvent = z.infer; - -export type GriptapeWebhookOutputs = { - 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 createGriptapeMatch(eventType: string): CorsairWebhookMatcher { - return (request: RawWebhookRequest) => { - const parsedBody = parseBody(request.body); - return parsedBody !== null && parsedBody.type === eventType; - }; -} - -export function verifyGriptapeWebhookSignature( - 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 4f905fd79..9d9dba980 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,7 +208,7 @@ importers: specifier: workspace:* version: link:../../packages/openweathermap '@corsair-dev/outlook': - specifier: workspace:s* + specifier: workspace:* version: link:../../packages/outlook '@corsair-dev/reddit': specifier: workspace:* From 7ca7bb8b8d233a62eb4fadbbe712840a7391221f Mon Sep 17 00:00:00 2001 From: Mayank Saini Date: Thu, 27 Aug 2026 21:30:32 +0530 Subject: [PATCH 6/6] Delete packages/griptape/README.md --- packages/griptape/README.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 packages/griptape/README.md diff --git a/packages/griptape/README.md b/packages/griptape/README.md deleted file mode 100644 index 85905c224..000000000 --- a/packages/griptape/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @corsair-dev/griptape - -Corsair integration for the Griptape Cloud API. - -## Authentication - -Griptape Cloud uses HTTP Bearer authentication. - -Provide a Griptape Cloud API key when creating the plugin: - -```ts -import { griptape } from '@corsair-dev/griptape'; - -const plugin = griptape({ - key: process.env.GT_CLOUD_API_KEY, -}); \ No newline at end of file