mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-22 12:28:29 +00:00
Feat: add mcp oauth2.1 support
This commit is contained in:
parent
e6da6ad7c5
commit
8ec974f942
@ -36,6 +36,10 @@ setInterval(
|
|||||||
// MCP request body schema
|
// MCP request body schema
|
||||||
const MCPRequestSchema = z.object({}).passthrough();
|
const MCPRequestSchema = z.object({}).passthrough();
|
||||||
|
|
||||||
|
const SourceParams = z.object({
|
||||||
|
source: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
// Search parameters schema for MCP tool
|
// Search parameters schema for MCP tool
|
||||||
const SearchParamsSchema = z.object({
|
const SearchParamsSchema = z.object({
|
||||||
query: z.string().describe("The search query in third person perspective"),
|
query: z.string().describe("The search query in third person perspective"),
|
||||||
@ -55,10 +59,11 @@ const handleMCPRequest = async (
|
|||||||
request: Request,
|
request: Request,
|
||||||
body: any,
|
body: any,
|
||||||
authentication: any,
|
authentication: any,
|
||||||
|
params: z.infer<typeof SourceParams>,
|
||||||
) => {
|
) => {
|
||||||
const sessionId = request.headers.get("mcp-session-id") as string | undefined;
|
const sessionId = request.headers.get("mcp-session-id") as string | undefined;
|
||||||
const source = request.headers.get("source") as string | undefined;
|
const source =
|
||||||
|
request.headers.get("source") || (params.source as string | undefined);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return json(
|
return json(
|
||||||
{
|
{
|
||||||
@ -241,17 +246,18 @@ const handleDelete = async (request: Request, authentication: any) => {
|
|||||||
const { action, loader } = createHybridActionApiRoute(
|
const { action, loader } = createHybridActionApiRoute(
|
||||||
{
|
{
|
||||||
body: MCPRequestSchema,
|
body: MCPRequestSchema,
|
||||||
|
searchParams: SourceParams,
|
||||||
allowJWT: true,
|
allowJWT: true,
|
||||||
authorization: {
|
authorization: {
|
||||||
action: "mcp",
|
action: "mcp",
|
||||||
},
|
},
|
||||||
corsStrategy: "all",
|
corsStrategy: "all",
|
||||||
},
|
},
|
||||||
async ({ body, authentication, request }) => {
|
async ({ body, authentication, request, searchParams }) => {
|
||||||
const method = request.method;
|
const method = request.method;
|
||||||
|
|
||||||
if (method === "POST") {
|
if (method === "POST") {
|
||||||
return await handleMCPRequest(request, body, authentication);
|
return await handleMCPRequest(request, body, authentication, searchParams);
|
||||||
} else if (method === "DELETE") {
|
} else if (method === "DELETE") {
|
||||||
return await handleDelete(request, authentication);
|
return await handleDelete(request, authentication);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
let scopeParam = url.searchParams.get("scope") || undefined;
|
let scopeParam = url.searchParams.get("scope") || "mcp";
|
||||||
|
|
||||||
// If scope is present, normalize it to comma-separated format
|
// If scope is present, normalize it to comma-separated format
|
||||||
// Handle both space-separated (from URL encoding) and comma-separated scopes
|
// Handle both space-separated (from URL encoding) and comma-separated scopes
|
||||||
|
|||||||
59
apps/webapp/app/routes/oauth.register.tsx
Normal file
59
apps/webapp/app/routes/oauth.register.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { json } from "@remix-run/node";
|
||||||
|
import { type ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { oauth2Service } from "~/services/oauth2.server";
|
||||||
|
|
||||||
|
// Dynamic Client Registration for MCP clients (Claude, etc.)
|
||||||
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
|
if (request.method !== "POST") {
|
||||||
|
throw new Response("Method Not Allowed", { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { client_name, redirect_uris, grant_types, response_types } = body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!redirect_uris || !Array.isArray(redirect_uris) || redirect_uris.length === 0) {
|
||||||
|
return json(
|
||||||
|
{ error: "invalid_request", error_description: "redirect_uris is required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MCP client with special handling
|
||||||
|
const client = await oauth2Service.createDynamicClient({
|
||||||
|
name: client_name || "MCP Client",
|
||||||
|
redirectUris: redirect_uris,
|
||||||
|
grantTypes: grant_types || ["authorization_code"],
|
||||||
|
responseTypes: response_types || ["code"],
|
||||||
|
clientType: "mcp", // Special flag for MCP clients
|
||||||
|
requirePkce: true,
|
||||||
|
allowedScopes: "mcp",
|
||||||
|
});
|
||||||
|
|
||||||
|
return json ({
|
||||||
|
client_id: client.clientId,
|
||||||
|
client_secret: client.clientSecret, // Include if confidential client
|
||||||
|
client_id_issued_at: Math.floor(Date.now() / 1000),
|
||||||
|
grant_types: client.grantTypes.split(","),
|
||||||
|
response_types: ["code"],
|
||||||
|
redirect_uris: client.redirectUris.split(","),
|
||||||
|
scope: client.allowedScopes,
|
||||||
|
token_endpoint_auth_method: "client_secret_post",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Dynamic client registration error:", error);
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
error: "invalid_request",
|
||||||
|
error_description: "Failed to register client"
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent GET requests
|
||||||
|
export async function loader() {
|
||||||
|
throw new Response("Method Not Allowed", { status: 405 });
|
||||||
|
}
|
||||||
@ -691,6 +691,50 @@ export class OAuth2Service {
|
|||||||
scope: storedRefreshToken.scope || undefined,
|
scope: storedRefreshToken.scope || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createDynamicClient(params: {
|
||||||
|
name: string;
|
||||||
|
redirectUris: string[];
|
||||||
|
grantTypes?: string[];
|
||||||
|
clientType?: string;
|
||||||
|
responseTypes?: string[];
|
||||||
|
requirePkce?: boolean;
|
||||||
|
allowedScopes?: string;
|
||||||
|
description?: string;
|
||||||
|
workspaceId?: string;
|
||||||
|
createdById?: string;
|
||||||
|
}) {
|
||||||
|
// Generate secure client credentials
|
||||||
|
const clientId = crypto.randomBytes(16).toString("hex");
|
||||||
|
const clientSecret = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
|
// Default values for MCP clients
|
||||||
|
const grantTypes = params.grantTypes || [
|
||||||
|
"authorization_code",
|
||||||
|
"refresh_token",
|
||||||
|
];
|
||||||
|
const allowedScopes = params.allowedScopes || "mcp";
|
||||||
|
const requirePkce = params.requirePkce ?? true; // Default to true for security
|
||||||
|
|
||||||
|
const client = await prisma.oAuthClient.create({
|
||||||
|
data: {
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
name: params.name,
|
||||||
|
description:
|
||||||
|
params.description ||
|
||||||
|
`Dynamically registered ${params.clientType || "client"}`,
|
||||||
|
redirectUris: params.redirectUris.join(","),
|
||||||
|
grantTypes: grantTypes.join(","),
|
||||||
|
allowedScopes,
|
||||||
|
requirePkce,
|
||||||
|
clientType: "mcp",
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const oauth2Service = new OAuth2Service();
|
export const oauth2Service = new OAuth2Service();
|
||||||
|
|||||||
@ -635,11 +635,13 @@ async function wrapResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New hybrid authentication types and functions
|
// New hybrid authentication types and functions
|
||||||
export type HybridAuthenticationResult = ApiAuthenticationResultSuccess | {
|
export type HybridAuthenticationResult =
|
||||||
ok: true;
|
| ApiAuthenticationResultSuccess
|
||||||
type: "COOKIE";
|
| {
|
||||||
userId: string;
|
ok: true;
|
||||||
};
|
type: "COOKIE";
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
async function authenticateHybridRequest(
|
async function authenticateHybridRequest(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -766,10 +768,9 @@ export function createHybridActionApiRoute<
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authenticationResult = await authenticateHybridRequest(
|
const authenticationResult = await authenticateHybridRequest(request, {
|
||||||
request,
|
allowJWT,
|
||||||
{ allowJWT },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!authenticationResult) {
|
if (!authenticationResult) {
|
||||||
return await wrapResponse(
|
return await wrapResponse(
|
||||||
|
|||||||
@ -44,10 +44,27 @@ async function init() {
|
|||||||
|
|
||||||
app.use(morgan("tiny"));
|
app.use(morgan("tiny"));
|
||||||
|
|
||||||
|
app.get("/.well-known/oauth-authorization-server", (req, res) => {
|
||||||
|
res.json({
|
||||||
|
issuer: process.env.APP_ORIGIN,
|
||||||
|
authorization_endpoint: `${process.env.APP_ORIGIN}/oauth/authorize`,
|
||||||
|
token_endpoint: `${process.env.APP_ORIGIN}/oauth/token`,
|
||||||
|
registration_endpoint: `${process.env.APP_ORIGIN}/oauth/register`,
|
||||||
|
scopes_supported: ["mcp"],
|
||||||
|
response_types_supported: ["code"],
|
||||||
|
grant_types_supported: [
|
||||||
|
"authorization_code",
|
||||||
|
"refresh_token",
|
||||||
|
"client_credentials",
|
||||||
|
],
|
||||||
|
code_challenge_methods_supported: ["S256"],
|
||||||
|
token_endpoint_auth_methods_supported: ["client_secret_basic", "none"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// handle SSR requests
|
// handle SSR requests
|
||||||
app.all("*", remixHandler);
|
app.all("*", remixHandler);
|
||||||
|
|
||||||
|
|
||||||
const port = process.env.REMIX_APP_PORT || 3000;
|
const port = process.env.REMIX_APP_PORT || 3000;
|
||||||
app.listen(port, () =>
|
app.listen(port, () =>
|
||||||
console.log(`Express server listening at http://localhost:${port}`),
|
console.log(`Express server listening at http://localhost:${port}`),
|
||||||
|
|||||||
207
docs/oauth-integration-webhook-implementation.md
Normal file
207
docs/oauth-integration-webhook-implementation.md
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# OAuth Integration Webhook Implementation
|
||||||
|
|
||||||
|
This document describes the implementation of webhook notifications for OAuth applications when users connect new integrations, following the existing trigger-based architecture.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The implementation follows the established pattern used in the Echo system:
|
||||||
|
|
||||||
|
- **Integration Creation**: Happens in `integration-run` trigger
|
||||||
|
- **Webhook Delivery**: Uses dedicated trigger task for asynchronous processing
|
||||||
|
- **Error Handling**: Non-blocking - webhook failures don't affect integration creation
|
||||||
|
|
||||||
|
## Implementation Components
|
||||||
|
|
||||||
|
### 1. OAuth Integration Webhook Delivery Task
|
||||||
|
|
||||||
|
**File**: `apps/webapp/app/trigger/webhooks/oauth-integration-webhook-delivery.ts`
|
||||||
|
|
||||||
|
This is a dedicated trigger task that handles webhook delivery to OAuth applications:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const oauthIntegrationWebhookTask = task({
|
||||||
|
id: "oauth-integration-webhook-delivery",
|
||||||
|
queue: oauthIntegrationWebhookQueue,
|
||||||
|
run: async (payload: OAuthIntegrationWebhookPayload) => {
|
||||||
|
// Implementation
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Features**:
|
||||||
|
|
||||||
|
- Finds OAuth clients with `integration` scope for the user
|
||||||
|
- Sends webhook notifications with integration details
|
||||||
|
- Includes HMAC signature verification
|
||||||
|
- Provides detailed delivery status tracking
|
||||||
|
- Non-blocking error handling
|
||||||
|
|
||||||
|
### 2. Integration into Integration-Run Trigger
|
||||||
|
|
||||||
|
**File**: `apps/webapp/app/trigger/integrations/integration-run.ts`
|
||||||
|
|
||||||
|
Modified the `handleAccountMessage` function to trigger webhook notifications:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function handleAccountMessage(...) {
|
||||||
|
// Create integration account
|
||||||
|
const integrationAccount = await createIntegrationAccount({...});
|
||||||
|
|
||||||
|
// Trigger OAuth integration webhook notifications
|
||||||
|
try {
|
||||||
|
await triggerOAuthIntegrationWebhook(integrationAccount.id, userId);
|
||||||
|
} catch (error) {
|
||||||
|
// Log error but don't fail integration creation
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAccount;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Integration Points**:
|
||||||
|
|
||||||
|
- Triggered after successful integration account creation
|
||||||
|
- Works for all integration types (OAuth, API key, MCP)
|
||||||
|
- Maintains existing integration creation flow
|
||||||
|
|
||||||
|
## Webhook Flow
|
||||||
|
|
||||||
|
### 1. Integration Connection
|
||||||
|
|
||||||
|
When a user connects a new integration:
|
||||||
|
|
||||||
|
1. Integration runs through `IntegrationEventType.SETUP`
|
||||||
|
2. CLI returns "account" message
|
||||||
|
3. `handleAccountMessage` creates integration account
|
||||||
|
4. `triggerOAuthIntegrationWebhook` is called
|
||||||
|
5. Webhook delivery task is queued
|
||||||
|
|
||||||
|
### 2. Webhook Delivery
|
||||||
|
|
||||||
|
The webhook delivery task:
|
||||||
|
|
||||||
|
1. Queries OAuth clients with:
|
||||||
|
- `integration` scope in `allowedScopes`
|
||||||
|
- Active `OAuthIntegrationGrant` for the user
|
||||||
|
- Configured `webhookUrl`
|
||||||
|
2. Sends HTTP POST to each webhook URL
|
||||||
|
3. Logs delivery results
|
||||||
|
|
||||||
|
### 3. Webhook Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "integration.connected",
|
||||||
|
"user_id": "user_uuid",
|
||||||
|
"integration": {
|
||||||
|
"id": "integration_account_uuid",
|
||||||
|
"provider": "linear",
|
||||||
|
"account_id": "external_account_id",
|
||||||
|
"mcp_endpoint": "mcp://core.ai/linear/external_account_id",
|
||||||
|
"name": "Linear",
|
||||||
|
"icon": "https://example.com/linear-icon.png"
|
||||||
|
},
|
||||||
|
"timestamp": "2024-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### HMAC Signature
|
||||||
|
|
||||||
|
If OAuth client has `webhookSecret` configured:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const signature = crypto
|
||||||
|
.createHmac("sha256", client.webhookSecret)
|
||||||
|
.update(payloadString)
|
||||||
|
.digest("hex");
|
||||||
|
headers["X-Webhook-Secret"] = signature;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- `User-Agent: Echo-OAuth-Webhooks/1.0`
|
||||||
|
- `X-Webhook-Delivery: ${deliveryId}`
|
||||||
|
- `X-Webhook-Event: integration.connected`
|
||||||
|
- `X-Webhook-Secret: ${signature}` (if secret configured)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Non-Blocking Design
|
||||||
|
|
||||||
|
- Webhook delivery failures do NOT affect integration creation
|
||||||
|
- Errors are logged but don't throw exceptions
|
||||||
|
- Integration process continues normally
|
||||||
|
|
||||||
|
### Retry Strategy
|
||||||
|
|
||||||
|
Currently, the system uses Trigger.dev's built-in retry mechanism:
|
||||||
|
|
||||||
|
- Failed webhook deliveries will be retried automatically
|
||||||
|
- Exponential backoff for temporary failures
|
||||||
|
- Dead letter queue for permanent failures
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Comprehensive logging includes:
|
||||||
|
|
||||||
|
- Integration account details
|
||||||
|
- OAuth client information
|
||||||
|
- HTTP response status and body
|
||||||
|
- Error messages and stack traces
|
||||||
|
- Delivery success/failure counts
|
||||||
|
|
||||||
|
## Database Requirements
|
||||||
|
|
||||||
|
The implementation requires these existing database relationships:
|
||||||
|
|
||||||
|
### OAuthClient
|
||||||
|
|
||||||
|
- `webhookUrl`: Target URL for notifications
|
||||||
|
- `webhookSecret`: Optional HMAC secret
|
||||||
|
- `allowedScopes`: Must include "integration"
|
||||||
|
|
||||||
|
### OAuthIntegrationGrant
|
||||||
|
|
||||||
|
- Links OAuth clients to users
|
||||||
|
- `isActive`: Must be true for notifications
|
||||||
|
- `userId`: Target user for the integration
|
||||||
|
|
||||||
|
### IntegrationAccount
|
||||||
|
|
||||||
|
- Created during integration setup
|
||||||
|
- Includes `integrationDefinition` relationship
|
||||||
|
- Contains provider-specific configuration
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To test the webhook delivery:
|
||||||
|
|
||||||
|
1. **Create OAuth Client** with integration scope:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE "OAuthClient"
|
||||||
|
SET "allowedScopes" = 'profile,email,openid,integration',
|
||||||
|
"webhookUrl" = 'https://your-webhook-endpoint.com/webhooks'
|
||||||
|
WHERE "clientId" = 'your-client-id';
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Grant Integration Access** through OAuth flow with `integration` scope
|
||||||
|
|
||||||
|
3. **Connect Integration** (Linear, Slack, etc.) - webhooks will be triggered automatically
|
||||||
|
|
||||||
|
4. **Monitor Logs** for delivery status and any errors
|
||||||
|
|
||||||
|
## Advantages of This Approach
|
||||||
|
|
||||||
|
1. **Follows Existing Patterns**: Uses the same trigger-based architecture as other webhook systems
|
||||||
|
2. **Scalable**: Leverages Trigger.dev's queue system for handling high volumes
|
||||||
|
3. **Reliable**: Built-in retry and error handling
|
||||||
|
4. **Non-Blocking**: Integration creation is never blocked by webhook issues
|
||||||
|
5. **Comprehensive**: Works with all integration types and OAuth flows
|
||||||
|
6. **Secure**: Includes HMAC signature verification and proper headers
|
||||||
|
7. **Observable**: Detailed logging for monitoring and debugging
|
||||||
|
|
||||||
|
This implementation ensures that OAuth applications are immediately notified when users connect new integrations, while maintaining the reliability and scalability of the existing system architecture.
|
||||||
159
docs/webhook-delivery-architecture.md
Normal file
159
docs/webhook-delivery-architecture.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# Webhook Delivery Architecture
|
||||||
|
|
||||||
|
This document describes the refactored webhook delivery system that eliminates code duplication by using common utilities.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The webhook delivery system now follows a clean separation of concerns:
|
||||||
|
|
||||||
|
1. **Common Utilities** (`webhook-delivery-utils.ts`) - Shared HTTP delivery logic
|
||||||
|
2. **Activity Webhooks** (`webhook-delivery.ts`) - Workspace-based activity notifications
|
||||||
|
3. **OAuth Integration Webhooks** (`oauth-integration-webhook-delivery.ts`) - OAuth app integration notifications
|
||||||
|
|
||||||
|
## Common Utilities (`webhook-delivery-utils.ts`)
|
||||||
|
|
||||||
|
### Core Function: `deliverWebhook()`
|
||||||
|
|
||||||
|
Handles the common HTTP delivery logic for both webhook types:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function deliverWebhook(params: WebhookDeliveryParams): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
deliveryResults: DeliveryResult[];
|
||||||
|
summary: { total: number; successful: number; failed: number };
|
||||||
|
}>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Generic payload support (works with any webhook structure)
|
||||||
|
- Configurable User-Agent strings
|
||||||
|
- HMAC signature verification with different header formats
|
||||||
|
- 30-second timeout
|
||||||
|
- Comprehensive error handling and logging
|
||||||
|
- Detailed delivery results
|
||||||
|
|
||||||
|
### Helper Function: `prepareWebhookTargets()`
|
||||||
|
|
||||||
|
Converts simple webhook configurations to the standardized target format:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function prepareWebhookTargets(
|
||||||
|
webhooks: Array<{ url: string; secret?: string | null }>
|
||||||
|
): WebhookTarget[];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Activity Webhooks (`webhook-delivery.ts`)
|
||||||
|
|
||||||
|
**Purpose:** Send notifications to workspace webhook configurations when activities are created.
|
||||||
|
|
||||||
|
**Payload Structure:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "activity.created",
|
||||||
|
"timestamp": "2024-01-15T10:30:00.000Z",
|
||||||
|
"data": {
|
||||||
|
"id": "activity_id",
|
||||||
|
"text": "Activity content",
|
||||||
|
"sourceURL": "https://source.url",
|
||||||
|
"integrationAccount": { ... },
|
||||||
|
"workspace": { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
|
||||||
|
- Uses `X-Hub-Signature-256` header for HMAC verification
|
||||||
|
- Logs delivery results to `WebhookDeliveryLog` table
|
||||||
|
- Targets all active workspace webhook configurations
|
||||||
|
|
||||||
|
## OAuth Integration Webhooks (`oauth-integration-webhook-delivery.ts`)
|
||||||
|
|
||||||
|
**Purpose:** Notify OAuth applications when users connect new integrations.
|
||||||
|
|
||||||
|
**Payload Structure:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "integration.connected",
|
||||||
|
"user_id": "user_uuid",
|
||||||
|
"integration": {
|
||||||
|
"id": "integration_account_id",
|
||||||
|
"provider": "linear",
|
||||||
|
"account_id": "external_account_id",
|
||||||
|
"mcp_endpoint": "mcp://core.ai/linear/external_account_id",
|
||||||
|
"name": "Linear",
|
||||||
|
"icon": "https://example.com/icon.png"
|
||||||
|
},
|
||||||
|
"timestamp": "2024-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
|
||||||
|
- Uses `X-Webhook-Secret` header for HMAC verification
|
||||||
|
- Custom User-Agent: `Echo-OAuth-Webhooks/1.0`
|
||||||
|
- Targets OAuth clients with `integration` scope and webhook URLs
|
||||||
|
|
||||||
|
## Shared Features
|
||||||
|
|
||||||
|
Both webhook types benefit from the common utilities:
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- HMAC-SHA256 signature verification
|
||||||
|
- Configurable secrets per webhook target
|
||||||
|
- Proper HTTP headers for identification
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
|
||||||
|
- 30-second request timeout
|
||||||
|
- Comprehensive error handling
|
||||||
|
- Non-blocking webhook failures
|
||||||
|
|
||||||
|
### Observability
|
||||||
|
|
||||||
|
- Detailed logging at each step
|
||||||
|
- Delivery success/failure tracking
|
||||||
|
- Response status and body capture (limited)
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Parallel webhook delivery
|
||||||
|
- Efficient target preparation
|
||||||
|
- Minimal memory footprint
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### Activity Webhooks
|
||||||
|
|
||||||
|
- Triggered from: `apps/webapp/app/routes/api.v1.activity.tsx`
|
||||||
|
- Function: `triggerWebhookDelivery(activityId, workspaceId)`
|
||||||
|
|
||||||
|
### OAuth Integration Webhooks
|
||||||
|
|
||||||
|
- Triggered from: `apps/webapp/app/trigger/integrations/integration-run.ts`
|
||||||
|
- Function: `triggerOAuthIntegrationWebhook(integrationAccountId, userId)`
|
||||||
|
|
||||||
|
## Benefits of This Architecture
|
||||||
|
|
||||||
|
1. **Code Reuse**: Common HTTP delivery logic eliminates duplication
|
||||||
|
2. **Maintainability**: Single place to update delivery logic
|
||||||
|
3. **Consistency**: Same headers, timeouts, and error handling across webhook types
|
||||||
|
4. **Flexibility**: Easy to add new webhook types by reusing common utilities
|
||||||
|
5. **Testing**: Easier to test common logic independently
|
||||||
|
6. **Security**: Consistent HMAC implementation across all webhook types
|
||||||
|
|
||||||
|
## Adding New Webhook Types
|
||||||
|
|
||||||
|
To add a new webhook type:
|
||||||
|
|
||||||
|
1. Create a new trigger task file (e.g., `new-webhook-delivery.ts`)
|
||||||
|
2. Define your payload structure
|
||||||
|
3. Use `deliverWebhook()` with your payload and targets
|
||||||
|
4. Add your event type to `WebhookEventType` in utils
|
||||||
|
5. Update HMAC header logic in `deliverWebhook()` if needed
|
||||||
|
|
||||||
|
This architecture provides a solid foundation for webhook delivery that can easily scale to support additional webhook types while maintaining code quality and consistency.
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "OAuthClient" DROP CONSTRAINT "OAuthClient_createdById_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "OAuthClient" ADD COLUMN "clientType" TEXT NOT NULL DEFAULT 'regular',
|
||||||
|
ALTER COLUMN "workspaceId" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "createdById" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OAuthClient" ADD CONSTRAINT "OAuthClient_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@ -287,6 +287,8 @@ model OAuthClient {
|
|||||||
// PKCE support
|
// PKCE support
|
||||||
requirePkce Boolean @default(false)
|
requirePkce Boolean @default(false)
|
||||||
|
|
||||||
|
clientType String @default("regular")
|
||||||
|
|
||||||
// Client metadata
|
// Client metadata
|
||||||
logoUrl String?
|
logoUrl String?
|
||||||
homepageUrl String?
|
homepageUrl String?
|
||||||
@ -299,12 +301,12 @@ model OAuthClient {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
// Workspace relationship (like GitHub orgs)
|
// Workspace relationship (like GitHub orgs)
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
workspaceId String
|
workspaceId String?
|
||||||
|
|
||||||
// Created by user (for audit trail)
|
// Created by user (for audit trail)
|
||||||
createdBy User @relation(fields: [createdById], references: [id])
|
createdBy User? @relation(fields: [createdById], references: [id])
|
||||||
createdById String
|
createdById String?
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
oauthAuthorizationCodes OAuthAuthorizationCode[]
|
oauthAuthorizationCodes OAuthAuthorizationCode[]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user