improved a lot of things

This commit is contained in:
SinachPat
2026-04-26 09:09:55 +01:00
parent 2911f22df8
commit 97315846be
70 changed files with 4967 additions and 10 deletions
@@ -0,0 +1,84 @@
import { z } from 'zod';
import type { OriginIngester, IngestionResult } from '../types.js';
// ── GitHub Webhook payload ────────────────────────────────────────────────────
// Handles pull_request events (opened, synchronize, reopened).
// Ref: https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request
const GitHubUserSchema = z.object({
login: z.string(),
html_url: z.string().url(),
});
const GitHubRepositorySchema = z.object({
id: z.number(),
full_name: z.string(),
html_url: z.string().url(),
default_branch: z.string(),
});
const GitHubPullRequestPayloadSchema = z.object({
action: z.enum(['opened', 'synchronize', 'reopened', 'closed']),
number: z.number().int(),
pull_request: z.object({
id: z.number(),
number: z.number().int(),
title: z.string(),
html_url: z.string().url(),
state: z.enum(['open', 'closed']),
head: z.object({
sha: z.string().length(40),
ref: z.string(),
label: z.string(),
}),
base: z.object({
sha: z.string().length(40),
ref: z.string(),
}),
user: GitHubUserSchema,
body: z.string().nullable().optional(),
draft: z.boolean().optional(),
}),
repository: GitHubRepositorySchema,
sender: GitHubUserSchema,
});
export type GitHubPullRequestPayload = z.infer<typeof GitHubPullRequestPayloadSchema>;
// ── Connector ─────────────────────────────────────────────────────────────────
export const githubIngester: OriginIngester<GitHubPullRequestPayload> = {
parsePayload(raw) {
return GitHubPullRequestPayloadSchema.parse(raw);
},
ingest(payload): IngestionResult {
const { pull_request: pr, repository } = payload;
// The render URL points to the head commit's deployed preview if available.
// Conventionally: https://<pr-number>.<preview-domain> — caller overrides as needed.
const renderUrl = pr.html_url;
return {
origin: {
type: 'GIT_COMMIT',
source_ref: pr.head.sha,
source_metadata_jsonb: {
pr_number: pr.number,
pr_title: pr.title,
pr_url: pr.html_url,
head_sha: pr.head.sha,
head_ref: pr.head.ref,
base_sha: pr.base.sha,
base_ref: pr.base.ref,
repo: repository.full_name,
author: pr.user.login,
...(pr.body !== undefined && pr.body !== null ? { body: pr.body } : {}),
...(pr.draft !== undefined ? { draft: pr.draft } : {}),
},
},
artboardTitle: `PR #${pr.number}: ${pr.title}`,
renderUrl,
};
},
};
@@ -0,0 +1,109 @@
import { z } from 'zod';
import type { OriginIngester, IngestionResult } from '../types.js';
// ── Intercom Webhook payload ──────────────────────────────────────────────────
// Handles conversation.user.created events where users report UI issues.
// Intercom sends annotated screenshots as file_url attachments.
// Ref: https://developers.intercom.com/docs/references/webhooks/conversation/
const IntercomAttachmentSchema = z.object({
type: z.literal('upload'),
name: z.string(),
url: z.string().url(),
content_type: z.string().optional(),
filesize: z.number().optional(),
width: z.number().optional(),
height: z.number().optional(),
});
const IntercomUserSchema = z.object({
type: z.enum(['user', 'lead']),
id: z.string(),
email: z.string().email().optional(),
name: z.string().optional(),
});
const IntercomConversationPartSchema = z.object({
type: z.literal('conversation_part'),
body: z.string().nullable().optional(),
attachments: z.array(IntercomAttachmentSchema).optional(),
author: z.object({
type: z.string(),
id: z.string(),
email: z.string().optional(),
name: z.string().optional(),
}).optional(),
});
const IntercomWebhookPayloadSchema = z.object({
type: z.literal('notification_event'),
topic: z.string(),
data: z.object({
type: z.literal('notification_event_data'),
item: z.object({
type: z.literal('conversation'),
id: z.string(),
created_at: z.number(),
source: z.object({
type: z.string(),
subject: z.string().optional(),
body: z.string().nullable().optional(),
attachments: z.array(IntercomAttachmentSchema).optional(),
author: IntercomUserSchema.optional(),
}),
conversation_parts: z.object({
conversation_parts: z.array(IntercomConversationPartSchema).optional(),
}).optional(),
}),
}),
});
export type IntercomWebhookPayload = z.infer<typeof IntercomWebhookPayloadSchema>;
// ── Connector ─────────────────────────────────────────────────────────────────
export const intercomIngester: OriginIngester<IntercomWebhookPayload> = {
parsePayload(raw) {
return IntercomWebhookPayloadSchema.parse(raw);
},
ingest(payload): IngestionResult {
const { item } = payload.data;
const { source } = item;
// Prefer annotated screenshot from the source message
const imageAttachment = source.attachments?.find(a =>
a.content_type?.startsWith('image/')
);
const authorName = source.author?.name ?? source.author?.email ?? 'Unknown user';
const subject = source.subject ?? `User report from ${authorName}`;
return {
origin: {
type: 'URL',
source_ref: item.id,
source_metadata_jsonb: {
conversation_id: item.id,
topic: payload.topic,
subject,
body: source.body ?? '',
created_at: item.created_at,
author: {
...(source.author?.id !== undefined ? { id: source.author.id } : {}),
...(source.author?.email !== undefined ? { email: source.author.email } : {}),
...(source.author?.name !== undefined ? { name: source.author.name } : {}),
},
...(imageAttachment !== undefined ? {
screenshot_url: imageAttachment.url,
screenshot_name: imageAttachment.name,
...(imageAttachment.width !== undefined ? { width: imageAttachment.width } : {}),
...(imageAttachment.height !== undefined ? { height: imageAttachment.height } : {}),
} : {}),
},
},
artboardTitle: `Intercom: ${subject}`,
...(imageAttachment !== undefined ? { renderUrl: imageAttachment.url } : {}),
};
},
};
@@ -0,0 +1,65 @@
import { z } from 'zod';
import type { OriginIngester, IngestionResult } from '../types.js';
// ── Linear webhook payload ────────────────────────────────────────────────────
// Fired on Issue create/update events.
const LinearAttachmentSchema = z.object({
url: z.string().url().optional(),
title: z.string().optional(),
});
const LinearIssuePayloadSchema = z.object({
action: z.enum(['create', 'update', 'remove']),
type: z.literal('Issue'),
data: z.object({
id: z.string(),
identifier: z.string(),
title: z.string(),
description: z.string().optional(),
url: z.string().url(),
priority: z.number().int().min(0).max(4).optional(),
state: z.object({ name: z.string() }).optional(),
assignee: z.object({ name: z.string(), email: z.string() }).optional(),
attachments: z.array(LinearAttachmentSchema).optional(),
team: z.object({ id: z.string(), name: z.string() }),
}),
updatedFrom: z.record(z.unknown()).optional(),
});
export type LinearIssuePayload = z.infer<typeof LinearIssuePayloadSchema>;
// ── Connector ─────────────────────────────────────────────────────────────────
export const linearIngester: OriginIngester<LinearIssuePayload> = {
parsePayload(raw) {
return LinearIssuePayloadSchema.parse(raw);
},
ingest(payload): IngestionResult {
const { data } = payload;
const attachment = data.attachments?.find(a => a.url !== undefined);
const renderUrl = attachment?.url ?? data.url;
return {
origin: {
type: 'LINEAR_ISSUE',
source_ref: data.identifier,
source_metadata_jsonb: {
id: data.id,
identifier: data.identifier,
title: data.title,
url: data.url,
...(data.description !== undefined ? { description: data.description } : {}),
...(data.priority !== undefined ? { priority: data.priority } : {}),
...(data.state !== undefined ? { state: data.state.name } : {}),
...(data.assignee !== undefined ? { assignee: data.assignee.name } : {}),
team: data.team.name,
},
},
artboardTitle: `${data.identifier}: ${data.title}`,
renderUrl,
};
},
};
@@ -0,0 +1,74 @@
import { z } from 'zod';
import type { OriginIngester, IngestionResult } from '../types.js';
// ── Slack Event API payload ───────────────────────────────────────────────────
// Sent when a message is posted to a channel the app is subscribed to.
// Ref: https://api.slack.com/events/message
const SlackFileSchema = z.object({
id: z.string(),
name: z.string().optional(),
mimetype: z.string().optional(),
url_private: z.string().optional(),
permalink: z.string().optional(),
});
const SlackMessageEventSchema = z.object({
type: z.literal('event_callback'),
event_id: z.string(),
team_id: z.string(),
event: z.object({
type: z.literal('message'),
channel: z.string(),
channel_name: z.string().optional(),
user: z.string(),
text: z.string(),
ts: z.string(),
thread_ts: z.string().optional(),
files: z.array(SlackFileSchema).optional(),
}),
authorizations: z.array(z.object({ user_id: z.string() })).optional(),
});
export type SlackMessagePayload = z.infer<typeof SlackMessageEventSchema>;
// ── Connector ─────────────────────────────────────────────────────────────────
export const slackIngester: OriginIngester<SlackMessagePayload> = {
parsePayload(raw) {
return SlackMessageEventSchema.parse(raw);
},
ingest(payload): IngestionResult {
const { event, team_id } = payload;
// Extract first image attachment as the render target
const imageFile = event.files?.find(f =>
f.mimetype?.startsWith('image/') && f.url_private !== undefined
);
const renderUrl = imageFile?.url_private ?? imageFile?.permalink;
const channelLabel = event.channel_name ?? event.channel;
const unixTs = parseFloat(event.ts);
const date = new Date(unixTs * 1000).toISOString().slice(0, 10);
return {
origin: {
type: 'SLACK_MESSAGE',
source_ref: `${event.channel}:${event.ts}`,
source_metadata_jsonb: {
team_id,
channel: event.channel,
channel_name: channelLabel,
user: event.user,
text: event.text,
ts: event.ts,
...(event.thread_ts !== undefined ? { thread_ts: event.thread_ts } : {}),
...(imageFile !== undefined ? { image_file_id: imageFile.id } : {}),
},
},
artboardTitle: `Slack: #${channelLabel} (${date})`,
...(renderUrl !== undefined ? { renderUrl } : {}),
};
},
};
+14 -1
View File
@@ -1 +1,14 @@
export {};
export type { IngestionResult, OriginIngester, WebhookEnvelope } from './types.js';
export { WebhookEnvelopeSchema } from './types.js';
export type { LinearIssuePayload } from './connectors/linear.js';
export { linearIngester } from './connectors/linear.js';
export type { SlackMessagePayload } from './connectors/slack.js';
export { slackIngester } from './connectors/slack.js';
export type { GitHubPullRequestPayload } from './connectors/github.js';
export { githubIngester } from './connectors/github.js';
export type { IntercomWebhookPayload } from './connectors/intercom.js';
export { intercomIngester } from './connectors/intercom.js';
+28
View File
@@ -0,0 +1,28 @@
import { z } from 'zod';
import type { InsertOrigin } from '@originmain/origin-graph';
// ── Core ingester interface ───────────────────────────────────────────────────
export interface IngestionResult {
origin: InsertOrigin;
/** Human-readable label used as the artboard title */
artboardTitle: string;
/** URL that the Live Artboard renderer should load, if applicable */
renderUrl?: string;
}
export interface OriginIngester<TPayload> {
/** Validates and parses the raw webhook payload. Throws ZodError on invalid input. */
parsePayload(raw: unknown): TPayload;
/** Converts a validated payload into an IngestionResult. */
ingest(payload: TPayload): IngestionResult;
}
// ── Shared helpers ────────────────────────────────────────────────────────────
export const WebhookEnvelopeSchema = z.object({
timestamp: z.string().optional(),
signature: z.string().optional(),
});
export type WebhookEnvelope = z.infer<typeof WebhookEnvelopeSchema>;