/** * GitHub event ingest with afterWrite race-safe idempotency. * * Mirrors src/connectors/slack/ingest.ts. The dedupe key is sha256(eventName + * ':' + rawBody) (codex P0 #3) — derived from the signed body, not from the * unsigned X-GitHub-Delivery header, so a replay attacker cannot bypass * idempotency by rotating the delivery UUID. * * Race semantics (codex P1 #6): * - Fast path: hasSeenKey pre-check returns 'duplicate' for the common case. * - Slow path: hasSeenKey passes (no row yet). Two workers may race into * remember() concurrently. Inside the writeEntry SAVEPOINT, INSERT OR * IGNORE on github_event_log either inserts (changes=1, commit) or * collides (changes=0, throw DuplicateIdempotencyError -> SAVEPOINT * rolls back this worker's memory row). Exactly one memory exists per * idempotency_key. * * The Slack precedent's race test was insufficient — it tested the fast path, * not the SAVEPOINT collision. The `__testInjectBeforeLog` hook below lets * tests pre-populate github_event_log inside the SAVEPOINT to actually * exercise the changes=0 -> rollback path. */ import { type Context } from '../../api.js'; import { type DatabaseSyncLike } from '../../db.js'; import type { GitHubIssueEvent, GitHubIssueCommentEvent, GitHubPullRequestEvent, GitHubPullRequestReviewCommentEvent } from './types.js'; export type IngestStatus = 'ingested' | 'duplicate' | 'skipped' | 'skipped_duplicate'; export interface IngestResult { status: IngestStatus; memoryId: string | null; } /** * Discriminated union of the four event shapes V1 ingests. The eventName * field MUST equal the X-GitHub-Event header value; computeIdempotencyKey * folds it into the dedupe key so the same body posted under two different * X-GitHub-Event headers produces two distinct keys (test 7). */ export type IngestEvent = { eventName: 'issues'; payload: GitHubIssueEvent; } | { eventName: 'issue_comment'; payload: GitHubIssueCommentEvent; } | { eventName: 'pull_request'; payload: GitHubPullRequestEvent; } | { eventName: 'pull_request_review_comment'; payload: GitHubPullRequestReviewCommentEvent; }; export interface IngestInput { /** The X-GitHub-Event header value + parsed body, discriminated. */ event: IngestEvent; /** The raw HTTP body — used for the idempotency key (replay-safe per codex P0 #3). */ rawBody: string; /** X-GitHub-Delivery header value, audit metadata only. */ deliveryId: string; /** * Test-only hook fired inside the SAVEPOINT, AFTER the memory row is * inserted but BEFORE the github_event_log INSERT OR IGNORE. Tests pass * a function that pre-inserts the github_event_log row using the * provided db handle to simulate a concurrent worker winning the race. * Production callers leave this undefined. */ __testInjectBeforeLog?: (db: DatabaseSyncLike, idempotencyKey: string) => void; } export declare function ingestEvent(ctx: Context, input: IngestInput): IngestResult; //# sourceMappingURL=ingest.d.ts.map