Infrastructure build to support third-party app integrations

This commit is contained in:
KS Jannette
2026-08-01 07:35:01 -04:00
parent b4666c5439
commit 15af3465e2
53 changed files with 5864 additions and 659 deletions

View File

@@ -0,0 +1,113 @@
import type { NormalizedDelivery, NoteProvenance } from '../types/integration.js';
import type { NoteInput } from '../types/domain.js';
export class NormalizationError extends Error {
constructor(message: string) {
super(message);
this.name = 'NormalizationError';
}
}
const asRecord = (value: unknown): Record<string, unknown> => {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new NormalizationError('Payload is not an object');
}
return value as Record<string, unknown>;
};
const asString = (value: unknown): string | undefined =>
typeof value === 'string' && value.length > 0 ? value : undefined;
/** notes.id is VARCHAR(64), so composite keys have to be trimmed to fit. */
const noteId = (provider: string, externalId: string): string =>
`${provider}_${externalId}`.slice(0, 64);
const provenance = (
meta: Omit<NoteProvenance, 'receivedAt'>
): Record<string, unknown> => ({
...meta,
receivedAt: new Date().toISOString(),
});
const isNoteInput = (value: unknown): value is NoteInput => {
if (typeof value !== 'object' || value === null) return false;
const note = value as Record<string, unknown>;
if (typeof note.id !== 'string' || note.id.length === 0) return false;
if (typeof note.text !== 'string' || note.text.length === 0) return false;
if (typeof note.author !== 'string' || note.author.length === 0) return false;
if (note.x !== undefined && typeof note.x !== 'number') return false;
if (note.y !== undefined && typeof note.y !== 'number') return false;
if (note.color !== undefined && typeof note.color !== 'string') return false;
return true;
};
export const isNoteInputArray = (value: unknown): value is NoteInput[] =>
Array.isArray(value) && value.every(isNoteInput);
/**
* Direct push: the caller already speaks our note shape, so normalization is
* a validation step rather than a translation.
*/
export const normalizeRest = (payload: unknown): NormalizedDelivery => {
const body = asRecord(payload);
if (!isNoteInputArray(body.notes)) {
throw new NormalizationError('Expected a "notes" array of note objects');
}
const externalId = asString(body.batchId) ?? randomBatchId();
return {
externalId,
notes: body.notes.map((note) => ({
...note,
sourceMeta: provenance({ provider: 'rest', externalId }),
})),
};
};
const randomBatchId = (): string =>
`batch_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
/**
* Linear comment webhook. Kept minimal on purpose: this proves the webhook
* pipeline end to end. Label filtering and issue-thread context belong to the
* Linear integration proper, not to this infrastructure.
*/
export const normalizeLinear = (payload: unknown): NormalizedDelivery => {
const body = asRecord(payload);
const data = asRecord(body.data);
const externalId = asString(data.id);
if (!externalId) {
throw new NormalizationError('Linear payload is missing data.id');
}
const text = asString(data.body);
if (!text) {
return { externalId, notes: [] };
}
const user = typeof data.user === 'object' && data.user !== null
? (data.user as Record<string, unknown>)
: {};
return {
externalId,
notes: [
{
id: noteId('linear', externalId),
text,
author: asString(user.name) ?? 'unknown',
sourceMeta: provenance({
provider: 'linear',
externalId,
permalink: asString(data.url),
authorHandle: asString(user.name),
}),
},
],
};
};

View File

@@ -0,0 +1,95 @@
import type {
NormalizedDelivery,
ProviderSlug,
SignatureScheme,
} from '../types/integration.js';
import { normalizeLinear, normalizeRest } from './normalizers.js';
export type ChallengeResponse = { status: number; body: unknown };
export type ProviderConfig = {
slug: ProviderSlug;
signatureScheme: SignatureScheme;
/** Header carrying the signature. Empty only when the scheme is 'none'. */
signatureHeader: string;
timestampHeader?: string;
/** Environment variable holding the shared secret when the install has none. */
secretEnvVar?: string;
/** Returns a response when the request is a handshake rather than an event. */
challenge?: (body: unknown) => ChallengeResponse | null;
/**
* Absent until that provider's integration is built. The transport above is
* infrastructure; the payload mapping belongs to the integration itself.
*/
normalize?: (payload: unknown) => NormalizedDelivery;
oauth?: {
authorizeUrl: string;
tokenUrl: string;
scopes: string[];
};
};
const slackChallenge = (body: unknown): ChallengeResponse | null => {
if (typeof body !== 'object' || body === null) return null;
const candidate = body as { type?: unknown; challenge?: unknown };
if (candidate.type !== 'url_verification') return null;
if (typeof candidate.challenge !== 'string') return null;
return { status: 200, body: { challenge: candidate.challenge } };
};
export const providers: Record<ProviderSlug, ProviderConfig> = {
rest: {
slug: 'rest',
signatureScheme: 'none',
signatureHeader: '',
normalize: normalizeRest,
},
linear: {
slug: 'linear',
signatureScheme: 'linear-sha256',
signatureHeader: 'linear-signature',
secretEnvVar: 'LINEAR_SIGNING_SECRET',
normalize: normalizeLinear,
oauth: {
authorizeUrl: 'https://linear.app/oauth/authorize',
tokenUrl: 'https://api.linear.app/oauth/token',
scopes: ['read'],
},
},
slack: {
slug: 'slack',
signatureScheme: 'slack-v0',
signatureHeader: 'x-slack-signature',
timestampHeader: 'x-slack-request-timestamp',
secretEnvVar: 'SLACK_SIGNING_SECRET',
challenge: slackChallenge,
oauth: {
authorizeUrl: 'https://slack.com/oauth/v2/authorize',
tokenUrl: 'https://slack.com/api/oauth.v2.access',
scopes: ['channels:history', 'reactions:read', 'users:read'],
},
},
github: {
slug: 'github',
signatureScheme: 'github-sha256',
signatureHeader: 'x-hub-signature-256',
secretEnvVar: 'GITHUB_WEBHOOK_SECRET',
oauth: {
authorizeUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
scopes: ['repo', 'read:discussion'],
},
},
jira: {
slug: 'jira',
signatureScheme: 'none',
signatureHeader: '',
oauth: {
authorizeUrl: 'https://auth.atlassian.com/authorize',
tokenUrl: 'https://auth.atlassian.com/oauth/token',
scopes: ['read:jira-work', 'offline_access'],
},
},
};
export const getProvider = (slug: ProviderSlug): ProviderConfig => providers[slug];