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,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateDeployMarkerRequest = {
version?: string;
/**
* Git SHA, tag or branch being deployed.
*/
ref?: string;
environment?: string;
note?: string;
};
@@ -0,0 +1,23 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateIntegrationRequest = {
name: string;
type: CreateIntegrationRequest.type;
/**
* Root URL of the self-hosted instance, e.g. https://analytics.example.com
*/
base_url: string;
/**
* Credential stored encrypted at rest; never returned by any endpoint.
*/
api_key?: string;
site_id?: string;
};
export namespace CreateIntegrationRequest {
export enum type {
RYBBIT = 'rybbit',
}
}
@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateWebhookRequest = {
url: string;
/**
* HMAC key for X-Primora-Signature. Generated when omitted.
*/
secret?: string;
events?: Array<'issue.created' | 'deploy.marker' | 'webhook.test'>;
enabled?: boolean;
};
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DeployMarker = {
version?: string;
ref?: string;
environment?: string;
note?: string;
recorded_at: string;
};
@@ -0,0 +1,30 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Integration = {
id: string;
project_id: string;
type: Integration.type;
name: string;
base_url: string;
/**
* Connector-specific target (Rybbit site id). Optional.
*/
site_id?: string;
status: Integration.status;
last_health_at?: string | null;
has_credentials: boolean;
created_at?: string;
};
export namespace Integration {
export enum type {
RYBBIT = 'rybbit',
}
export enum status {
UNKNOWN = 'unknown',
OK = 'ok',
ERROR = 'error',
}
}
@@ -0,0 +1,30 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RybbitMetricItem } from './RybbitMetricItem';
import type { RybbitSite } from './RybbitSite';
export type IntegrationAnalytics = {
integration_id: string;
site?: RybbitSite;
sites?: Array<RybbitSite>;
overview?: {
sessions?: number;
pageviews?: number;
users?: number;
pages_per_session?: number;
bounce_rate?: number;
session_duration?: number;
};
series?: Array<{
time?: string;
sessions?: number;
pageviews?: number;
users?: number;
bounce_rate?: number;
}>;
top_pages?: Array<RybbitMetricItem>;
top_referrers?: Array<RybbitMetricItem>;
window_days: number;
};
@@ -0,0 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type IntegrationID = string;
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Integration } from './Integration';
export type IntegrationListResponse = {
items: Array<Integration>;
};
@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type IntegrationTestResult = {
ok: boolean;
status_code?: number;
error?: string;
};
@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type RybbitMetricItem = {
value: string;
count: number;
percentage?: number;
};
@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type RybbitSite = {
site_id: string;
name: string;
domain?: string;
};
@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateWebhookRequest = {
url?: string;
/**
* Replace the signing secret. Omit to keep the current one.
*/
secret?: string;
events?: Array<'issue.created' | 'deploy.marker' | 'webhook.test'>;
enabled?: boolean;
};
@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Webhook = {
id: string;
project_id: string;
url: string;
/**
* Event filter (issue.created, deploy.marker, webhook.test). Empty means all events.
*/
events: Array<string>;
enabled: boolean;
has_secret: boolean;
created_at?: string;
};
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Webhook } from './Webhook';
export type WebhookCreateResponse = (Webhook & {
/**
* Plaintext secret — present only when Primora generated it. Store it; it cannot be recovered.
*/
secret?: string;
});
@@ -0,0 +1,25 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type WebhookDelivery = {
id: string;
webhook_id: string;
event_type: string;
payload?: Record<string, any>;
status: WebhookDelivery.status;
attempts: number;
last_status_code?: number | null;
last_error?: string;
delivered_at?: string | null;
next_retry_at?: string | null;
created_at?: string;
};
export namespace WebhookDelivery {
export enum status {
PENDING = 'pending',
DELIVERED = 'delivered',
FAILED = 'failed',
}
}
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { WebhookDelivery } from './WebhookDelivery';
export type WebhookDeliveryListResponse = {
items: Array<WebhookDelivery>;
};
@@ -0,0 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type WebhookID = string;
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Webhook } from './Webhook';
export type WebhookListResponse = {
items: Array<Webhook>;
};