feat: phase 4 integrations — connectors, rybbit analytics, webhooks

- core.integrations (migration 00005) with AES-256-GCM-encrypted
  credentials (PRIMORA_ENCRYPTION_KEY, hex/base64, dev fallback);
  has_credentials serialization only — secrets never leave the server
- /projects/:id/integrations list/create/delete/test; health check hits
  the stored base URL with a 5s timeout and records status
- Rybbit connector: GET …/integrations/:id/analytics proxies
  /api/organizations + /api/sites/* server-side into a normalized shape
- core.webhooks + core.webhook_deliveries; CRUD, deliveries, test, and
  POST …/deploy-markers. Events: issue.created (new fingerprint on
  ingest), deploy.marker, webhook.test. X-Primora-Signature HMAC-SHA256
  over the raw body; in-process dispatcher, 3 attempts, quadratic
  backoff, 10s timeout, pending-scan on boot
- HTTPS enforced for public targets; HTTP allowed for loopback/private/
  self-hosted sinks
- Integrations page (connectors + webhooks tabs, inline analytics,
  delivery log, secret-shown-once), demo-mode stubs
- 12 new primora_* MCP tools
- workspace 0.4.0

Authored By: TDvorak <[email protected]>
This commit is contained in:
Tomas Dvorak committed 2026-09-16 18:47:31 +02:00
1 parent 8cfddf9184
commit e25eb92339
82 files changed
+5710 -61

No files matched your search

@@ -0,0 +1,143 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CreateIntegrationRequest } from '../models/CreateIntegrationRequest';
import type { Integration } from '../models/Integration';
import type { IntegrationAnalytics } from '../models/IntegrationAnalytics';
import type { IntegrationListResponse } from '../models/IntegrationListResponse';
import type { IntegrationTestResult } from '../models/IntegrationTestResult';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class IntegrationsService {
/**
* @returns IntegrationListResponse External service integrations for the project
* @throws ApiError
*/
public static listIntegrations({
projectId,
}: {
projectId: string,
}): CancelablePromise<IntegrationListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/integrations',
path: {
'projectID': projectId,
},
errors: {
401: `Error response`,
},
});
}
/**
* @returns Integration Integration created
* @throws ApiError
*/
public static createIntegration({
projectId,
requestBody,
}: {
projectId: string,
requestBody: CreateIntegrationRequest,
}): CancelablePromise<Integration> {
return __request(OpenAPI, {
method: 'POST',
url: '/projects/{projectID}/integrations',
path: {
'projectID': projectId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
401: `Error response`,
409: `Error response`,
},
});
}
/**
* @returns void
* @throws ApiError
*/
public static deleteIntegration({
projectId,
integrationId,
}: {
projectId: string,
integrationId: string,
}): CancelablePromise<void> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/projects/{projectID}/integrations/{integrationID}',
path: {
'projectID': projectId,
'integrationID': integrationId,
},
errors: {
404: `Error response`,
},
});
}
/**
* Health check — HTTP GET against the integration's base URL with stored credentials (~5s timeout). Updates status and last_health_at.
* @returns IntegrationTestResult Connectivity probe result
* @throws ApiError
*/
public static testIntegration({
projectId,
integrationId,
}: {
projectId: string,
integrationId: string,
}): CancelablePromise<IntegrationTestResult> {
return __request(OpenAPI, {
method: 'POST',
url: '/projects/{projectID}/integrations/{integrationID}/test',
path: {
'projectID': projectId,
'integrationID': integrationId,
},
errors: {
404: `Error response`,
},
});
}
/**
* Read-only analytics pulled from the integration (Rybbit). The API key never leaves the server.
* @returns IntegrationAnalytics Normalized analytics payload
* @throws ApiError
*/
public static getIntegrationAnalytics({
projectId,
integrationId,
site,
days = 30,
}: {
projectId: string,
integrationId: string,
/**
* Rybbit site id override; defaults to the integration's configured site, else first accessible site.
*/
site?: string,
days?: number,
}): CancelablePromise<IntegrationAnalytics> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/integrations/{integrationID}/analytics',
path: {
'projectID': projectId,
'integrationID': integrationId,
},
query: {
'site': site,
'days': days,
},
errors: {
400: `Error response`,
404: `Error response`,
502: `Error response`,
},
});
}
}
@@ -0,0 +1,190 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CreateDeployMarkerRequest } from '../models/CreateDeployMarkerRequest';
import type { CreateWebhookRequest } from '../models/CreateWebhookRequest';
import type { DeployMarker } from '../models/DeployMarker';
import type { UpdateWebhookRequest } from '../models/UpdateWebhookRequest';
import type { WebhookCreateResponse } from '../models/WebhookCreateResponse';
import type { WebhookDelivery } from '../models/WebhookDelivery';
import type { WebhookDeliveryListResponse } from '../models/WebhookDeliveryListResponse';
import type { WebhookListResponse } from '../models/WebhookListResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class WebhooksService {
/**
* @returns WebhookListResponse Outbound webhooks for the project
* @throws ApiError
*/
public static listWebhooks({
projectId,
}: {
projectId: string,
}): CancelablePromise<WebhookListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/webhooks',
path: {
'projectID': projectId,
},
errors: {
401: `Error response`,
},
});
}
/**
* Deliveries are POSTed as JSON with X-Primora-Event and X-Primora-Signature (sha256=<HMAC-SHA256 hex of the raw body, keyed by the webhook secret>). If no secret is supplied one is generated and returned once.
* @returns WebhookCreateResponse Webhook created
* @throws ApiError
*/
public static createWebhook({
projectId,
requestBody,
}: {
projectId: string,
requestBody: CreateWebhookRequest,
}): CancelablePromise<WebhookCreateResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/projects/{projectID}/webhooks',
path: {
'projectID': projectId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
401: `Error response`,
409: `Error response`,
},
});
}
/**
* @returns WebhookCreateResponse Webhook updated
* @throws ApiError
*/
public static updateWebhook({
projectId,
webhookId,
requestBody,
}: {
projectId: string,
webhookId: string,
requestBody: UpdateWebhookRequest,
}): CancelablePromise<WebhookCreateResponse> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/projects/{projectID}/webhooks/{webhookID}',
path: {
'projectID': projectId,
'webhookID': webhookId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
404: `Error response`,
},
});
}
/**
* @returns void
* @throws ApiError
*/
public static deleteWebhook({
projectId,
webhookId,
}: {
projectId: string,
webhookId: string,
}): CancelablePromise<void> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/projects/{projectID}/webhooks/{webhookID}',
path: {
'projectID': projectId,
'webhookID': webhookId,
},
errors: {
404: `Error response`,
},
});
}
/**
* @returns WebhookDeliveryListResponse Recent delivery attempts, newest first
* @throws ApiError
*/
public static listWebhookDeliveries({
projectId,
webhookId,
limit = 50,
}: {
projectId: string,
webhookId: string,
limit?: number,
}): CancelablePromise<WebhookDeliveryListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/webhooks/{webhookID}/deliveries',
path: {
'projectID': projectId,
'webhookID': webhookId,
},
query: {
'limit': limit,
},
errors: {
404: `Error response`,
},
});
}
/**
* Queues a webhook.test ping delivery.
* @returns WebhookDelivery Test delivery queued
* @throws ApiError
*/
public static testWebhook({
projectId,
webhookId,
}: {
projectId: string,
webhookId: string,
}): CancelablePromise<WebhookDelivery> {
return __request(OpenAPI, {
method: 'POST',
url: '/projects/{projectID}/webhooks/{webhookID}/test',
path: {
'projectID': projectId,
'webhookID': webhookId,
},
errors: {
404: `Error response`,
},
});
}
/**
* Records a deployment marker in the audit log and fires the deploy.marker webhook event.
* @returns DeployMarker Marker recorded
* @throws ApiError
*/
public static createDeployMarker({
projectId,
requestBody,
}: {
projectId: string,
requestBody: CreateDeployMarkerRequest,
}): CancelablePromise<DeployMarker> {
return __request(OpenAPI, {
method: 'POST',
url: '/projects/{projectID}/deploy-markers',
path: {
'projectID': projectId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
401: `Error response`,
},
});
}
}