@mastra/factory 0.2.0 → 0.2.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/capabilities/intake.d.ts +45 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/factory.js +920 -549
- package/dist/factory.js.map +1 -1
- package/dist/index.js +920 -549
- package/dist/index.js.map +1 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +126 -11
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/project-lock.d.ts +24 -2
- package/dist/integrations/github/project-lock.d.ts.map +1 -1
- package/dist/integrations/github/project-lock.js +43 -10
- package/dist/integrations/github/project-lock.js.map +1 -1
- package/dist/integrations/github/routes.js +41 -10
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +60 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +7 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +104 -12
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +91 -0
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/routes/fs.d.ts +16 -0
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +103 -3
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/oauth.js +564 -478
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +1 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +717 -526
- package/dist/routes/surface.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +0 -1
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +6 -7
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/integrations/platform/github/event-worker.ts","../../../../src/integrations/github/subscriptions.ts","../../../../src/integrations/github/webhook.ts","../../../../src/integrations/platform/api-client.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type { IntegrationStorageHandle } from '../../../storage/domains/integrations/base.js';\nimport type { GithubRepositoryPermission } from '../../github/integration.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from '../../github/subscriptions.js';\nimport type { GithubSubscriptionStorage } from '../../github/subscriptions.js';\nimport { dispatchGithubWebhook } from '../../github/webhook.js';\nimport type { GithubWebhookNotification, ParsedGithubWebhook } from '../../github/webhook.js';\nimport type { PlatformApiClient } from '../api-client.js';\nimport { PlatformApiError } from '../api-client.js';\n\nconst API_PREFIX = '/v1/server/github-app';\nconst DEFAULT_POLL_INTERVAL_MS = 5_000;\nconst EVENT_PAGE_SIZE = 500;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst CURSOR_ORG_ID = '__platform_github_event_worker__';\nconst CURSOR_USER_ID = 'worker';\nconst SUPPORTED_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n]);\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment',\n 'pull-request-comment',\n 'pull-request-review',\n 'pull-request-review-comment',\n]);\nconst AUTHORIZED_BOTS = new Set(['coderabbitai[bot]', 'devin-ai-integration[bot]']);\nconst AUTHORIZED_PERMISSIONS = new Set<GithubRepositoryPermission>(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\n\ntype EventCursor = { afterEventId: string } | { afterTimestamp: number };\ntype PlatformGithubEventWorkerSettings = {\n version: 1;\n repositories: Record<string, EventCursor>;\n};\n\nexport type PlatformGithubEventStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n PlatformGithubEventWorkerSettings,\n Record<string, unknown>\n>;\n\ntype EventLogEntry = {\n id: string;\n deliveryId: string;\n event: string;\n payload: unknown;\n};\n\ntype Repository = { id: number };\n\nexport interface PlatformGithubEventDispatchIntegration {\n readonly integrationStorage: GithubSubscriptionStorage;\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n signal?: AbortSignal,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface PlatformGithubEventWorkerConfig {\n client: PlatformApiClient;\n controller: MountedMastraCode['controller'];\n github: PlatformGithubEventDispatchIntegration;\n storage: PlatformGithubEventStorage;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n intervalMs?: number;\n now?: () => number;\n dispatch?: typeof dispatchGithubWebhook;\n}\n\nexport class PlatformGithubEventWorker extends MastraWorker {\n readonly name = 'platform-github-events';\n\n readonly #client: PlatformApiClient;\n readonly #controller: MountedMastraCode['controller'];\n readonly #github: PlatformGithubEventDispatchIntegration;\n readonly #storage: PlatformGithubEventStorage;\n readonly #ingestFactoryEvent: ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined;\n readonly #intervalMs: number;\n readonly #now: () => number;\n readonly #dispatch: typeof dispatchGithubWebhook;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #leaseRenewalTimer: ReturnType<typeof setInterval> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n #leaseTtlMs: number;\n #hasLease = false;\n #startedAt = 0;\n #settings: PlatformGithubEventWorkerSettings = { version: 1, repositories: {} };\n\n constructor(config: PlatformGithubEventWorkerConfig) {\n super();\n this.#client = config.client;\n this.#controller = config.controller;\n this.#github = config.github;\n this.#storage = config.storage;\n this.#ingestFactoryEvent = config.ingestFactoryEvent;\n this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('Platform GitHub event polling interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n this.#now = config.now ?? Date.now;\n this.#dispatch = config.dispatch ?? dispatchGithubWebhook;\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('PlatformGithubEventWorker: call init() before start()');\n\n this.#startedAt = this.#now() - 1;\n this.#settings = normalizeSettings(await this.#storage.settings.get(CURSOR_ORG_ID, CURSOR_USER_ID));\n this.#running = true;\n this.deps.logger.info('Platform GitHub event polling started', {\n intervalMs: this.#intervalMs,\n leaseTtlMs: this.#leaseTtlMs,\n });\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n this.#stopLeaseRenewal();\n await this.#inFlight;\n if (this.#hasLease) {\n await this.#leaseProvider.releaseLease(this.#leaseKey(), this.#leaseOwner).catch(() => undefined);\n this.#hasLease = false;\n }\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick();\n this.#inFlight = run;\n void run.finally(() => {\n if (this.#inFlight === run) this.#inFlight = undefined;\n });\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n let nextDelay = this.#intervalMs;\n try {\n if (!(await this.#ensureLease())) return;\n nextDelay = await this.#poll();\n } catch (error) {\n nextDelay = retryDelay(error, this.#intervalMs);\n this.deps?.logger.error('Platform GitHub event polling cycle failed', {\n error: error instanceof Error ? error.message : String(error),\n retryInMs: nextDelay,\n });\n } finally {\n this.#schedule(nextDelay);\n }\n }\n\n async #ensureLease(): Promise<boolean> {\n if (this.#hasLease) return true;\n const result = await this.#leaseProvider.acquireLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs);\n this.#hasLease = result.acquired;\n if (this.#hasLease) this.#startLeaseRenewal();\n return this.#hasLease;\n }\n\n #startLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) return;\n this.#leaseRenewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs)\n .then(renewed => {\n if (!renewed) {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n }\n })\n .catch(error => {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n this.deps?.logger.warn('Platform GitHub event polling lease renewal failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.floor(this.#leaseTtlMs / 3),\n );\n this.#leaseRenewalTimer.unref?.();\n }\n\n #stopLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) clearInterval(this.#leaseRenewalTimer);\n this.#leaseRenewalTimer = undefined;\n }\n\n async #poll(): Promise<number> {\n const repositories = await this.#discoverRepositories();\n let retryInMs = this.#intervalMs;\n\n for (const repository of repositories) {\n if (!this.#running || !this.#hasLease) break;\n try {\n await this.#pollRepository(repository.id);\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform GitHub repository event polling failed', {\n repositoryId: repository.id,\n error: error instanceof Error ? error.message : String(error),\n retryInMs: delay,\n });\n if (error instanceof PlatformApiError && error.status === 429) break;\n }\n }\n\n return retryInMs;\n }\n\n async #discoverRepositories(): Promise<Repository[]> {\n const result = await this.#client.request<{\n installations: Array<{\n installationId: number;\n usable: boolean;\n suspendedAt: string | null;\n }>;\n }>('GET', `${API_PREFIX}/installations`);\n const repositories = new Map<number, Repository>();\n\n for (const installation of result.installations) {\n if (!installation.usable || installation.suspendedAt) continue;\n const page = await this.#client.request<{ repositories: Repository[] }>(\n 'GET',\n `${API_PREFIX}/installations/${installation.installationId}/repositories`,\n );\n for (const repository of page.repositories) repositories.set(repository.id, repository);\n }\n\n return [...repositories.values()];\n }\n\n async #pollRepository(repositoryId: number): Promise<void> {\n const key = String(repositoryId);\n if (!this.#settings.repositories[key]) {\n this.#settings.repositories[key] = { afterTimestamp: this.#startedAt };\n await this.#saveSettings();\n }\n\n while (this.#running && this.#hasLease) {\n const cursor: EventCursor = this.#settings.repositories[key]!;\n const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE) });\n if ('afterEventId' in cursor) query.set('afterEventId', cursor.afterEventId);\n else query.set('afterTimestamp', String(cursor.afterTimestamp));\n\n const page = await this.#client.request<{ events: EventLogEntry[]; nextCursor: string | null }>(\n 'GET',\n `${API_PREFIX}/repositories/${repositoryId}/events?${query}`,\n );\n if (page.events.length === 0 || !page.nextCursor) return;\n\n for (const event of page.events) {\n if (!this.#running || !this.#hasLease) return;\n const parsed = parseEvent(event);\n if (!parsed) {\n this.deps?.logger.warn('Platform GitHub event log returned a malformed event', {\n repositoryId,\n eventId: event.id,\n });\n continue;\n }\n if (isFactoryClosureEvent(parsed)) {\n await this.#ingestFactoryEvent?.(parsed);\n }\n const result = await this.#dispatch(parsed, {\n controller: this.#controller,\n listSubscriptions: (target, options) =>\n listPullRequestSubscriptionsForWebhook(target, options, this.#github.integrationStorage),\n retireSubscription: (id, status) =>\n retirePullRequestSubscription(id, status, this.#github.integrationStorage),\n isAuthorizedSender: notification => this.#isAuthorizedSender(notification),\n onTargetError: (subscription, error) => {\n this.deps?.logger.error('Platform GitHub event delivery failed for a subscription', {\n subscriptionId: subscription.id,\n resourceId: subscription.resourceId,\n threadId: subscription.threadId,\n error: error instanceof Error ? error.message : String(error),\n });\n },\n });\n if (result.failed > 0) {\n throw new Error(\n `Platform GitHub event ${event.deliveryId} failed for ${result.failed} subscribed target(s).`,\n );\n }\n }\n\n if (page.nextCursor === ('afterEventId' in cursor ? cursor.afterEventId : undefined)) return;\n this.#settings.repositories[key] = { afterEventId: page.nextCursor };\n await this.#saveSettings();\n }\n }\n\n async #isAuthorizedSender(notification: GithubWebhookNotification): Promise<boolean> {\n if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;\n const sender = notification.metadata.sender;\n const repository = notification.metadata.repository;\n if (!sender || !repository) return false;\n if (AUTHORIZED_BOTS.has(sender)) return true;\n\n const abortController = new AbortController();\n const timeout = setTimeout(() => abortController.abort(), PERMISSION_CHECK_TIMEOUT_MS);\n try {\n const permission = await this.#github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n );\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n async #saveSettings(): Promise<void> {\n await this.#storage.settings.save(CURSOR_ORG_ID, CURSOR_USER_ID, this.#settings);\n }\n\n #leaseKey(): string {\n return `${this.name}:${this.#storage.integrationId}`;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n\nfunction normalizeSettings(value: PlatformGithubEventWorkerSettings | null): PlatformGithubEventWorkerSettings {\n if (!value || value.version !== 1 || !value.repositories || typeof value.repositories !== 'object') {\n return { version: 1, repositories: {} };\n }\n return { version: 1, repositories: { ...value.repositories } };\n}\n\nfunction isFactoryClosureEvent(event: ParsedGithubWebhook): boolean {\n return (event.event === 'issues' || event.event === 'pull_request') && event.payload.action === 'closed';\n}\n\nfunction parseEvent(event: EventLogEntry): ParsedGithubWebhook | null {\n if (\n !event.id ||\n !event.deliveryId ||\n !SUPPORTED_EVENTS.has(event.event) ||\n !event.payload ||\n typeof event.payload !== 'object' ||\n Array.isArray(event.payload)\n ) {\n return null;\n }\n return {\n event: event.event,\n deliveryId: event.deliveryId,\n payload: event.payload as Record<string, unknown>,\n };\n}\n\nfunction retryDelay(error: unknown, fallbackMs: number): number {\n if (error instanceof PlatformApiError && error.status === 429 && error.retryAfterSeconds !== null) {\n return Math.max(fallbackMs, error.retryAfterSeconds * 1_000);\n }\n return fallbackMs;\n}\n","import type { IntegrationStorageHandle, IntegrationSubscription } from '../../storage/domains/integrations/base.js';\n\nexport type GithubSignalSubscriptionSource = 'auto-gh-pr-create' | 'factory-pr-create' | 'explicit-tool';\nexport type GithubSignalSubscriptionStatus = 'open' | 'closed' | 'merged';\n\nexport interface GithubSignalSubscriptionData {\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n ownerId: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId: string | null;\n}\n\nexport type GithubSignalSubscriptionRow = IntegrationSubscription<GithubSignalSubscriptionData>;\nexport type GithubSubscriptionStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n GithubSignalSubscriptionData\n>;\n\nexport interface SubscribeToPullRequestInput {\n orgId: string;\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n sessionId: string;\n ownerId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId?: string;\n}\n\nexport interface ThreadSubscriptionTarget {\n orgId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n}\n\nexport interface PullRequestSubscriptionTarget {\n orgId: string;\n installationExternalId: string;\n repositoryExternalId: string;\n changeRequestId: string;\n}\n\nexport type GithubWebhookPullRequestTarget = Omit<PullRequestSubscriptionTarget, 'orgId'>;\n\nexport function changeRequestTargetKey(input: GithubWebhookPullRequestTarget): string {\n return `change-request:${input.installationExternalId}:${input.repositoryExternalId}:${input.changeRequestId}`;\n}\n\nfunction sameSession(row: GithubSignalSubscriptionRow, input: SubscribeToPullRequestInput): boolean {\n return (\n row.orgId === input.orgId &&\n row.sessionId === input.sessionId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? '')\n );\n}\n\nexport async function subscribeToPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow> {\n const targetKey = changeRequestTargetKey(input);\n const existing = (await storage.subscriptions.listByTarget(targetKey)).find(row => sameSession(row, input));\n if (existing) {\n if (existing.status !== 'open') await storage.subscriptions.updateStatus(existing.id, 'open');\n return { ...existing, status: 'open' };\n }\n\n return storage.subscriptions.create({\n orgId: input.orgId,\n targetKey,\n sessionId: input.sessionId,\n resourceId: input.resourceId,\n threadId: input.threadId,\n sessionScope: input.sessionScope ?? '',\n status: 'open',\n data: {\n installationExternalId: input.installationExternalId,\n projectRepositoryId: input.projectRepositoryId,\n repositoryExternalId: input.repositoryExternalId,\n repositorySlug: input.repositorySlug,\n changeRequestId: input.changeRequestId,\n ownerId: input.ownerId,\n source: input.source,\n subscribedByUserId: input.subscribedByUserId ?? null,\n },\n });\n}\n\nexport async function unsubscribeFromPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n await Promise.all(rows.filter(row => sameSession(row, input)).map(row => storage.subscriptions.delete(row.id)));\n}\n\nexport async function listPullRequestSubscriptionsForThread(\n input: ThreadSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByThread(input.resourceId, input.threadId);\n return rows.filter(\n row =>\n row.orgId === input.orgId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? ''),\n );\n}\n\nexport async function listPullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return rows.filter(row => row.orgId === input.orgId && row.status === 'open');\n}\n\nexport async function listPullRequestSubscriptionsForWebhook(\n input: GithubWebhookPullRequestTarget,\n options: { includeTerminal?: boolean } | undefined,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return options?.includeTerminal ? rows : rows.filter(row => row.status === 'open');\n}\n\nexport function retirePullRequestSubscription(\n id: string,\n status: GithubSignalSubscriptionStatus,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n return storage.subscriptions.updateStatus(id, status);\n}\n\nexport async function retirePullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await listPullRequestSubscriptions(input, storage);\n await Promise.all(rows.map(row => storage.subscriptions.updateStatus(row.id, 'closed')));\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport type { NotificationPriority } from '@mastra/core/notifications';\nimport type { Context } from 'hono';\nimport type { GithubIntegration } from './integration.js';\nimport type { GithubIssueTriageInput, GithubIssueTriageResult } from './issue-triage.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from './subscriptions.js';\nimport type { GithubSignalSubscriptionRow, GithubWebhookPullRequestTarget } from './subscriptions.js';\n\nexport type GithubIssueTriageRunInput = GithubIssueTriageInput;\nexport type GithubIssueTriageRunResult = GithubIssueTriageResult;\n\nexport interface GithubWebhookHandlerOptions {\n /** Integration providing webhook-secret verification + collaborator permission checks. */\n github: GithubIntegration;\n runIssueTriage?: (input: GithubIssueTriageRunInput) => Promise<GithubIssueTriageRunResult>;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n}\n\nconst SUPPORTED_GITHUB_WEBHOOK_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n]);\n\nexport interface GithubWebhookMetadata {\n event: string;\n action?: string;\n deliveryId: string;\n repository?: string;\n repositoryId?: number;\n issueNumber?: number;\n pullRequestNumber?: number;\n sender?: string;\n senderType?: string;\n installationId?: number;\n}\n\nexport interface ParsedGithubWebhook {\n event: string;\n deliveryId: string;\n payload: Record<string, unknown>;\n}\n\nexport type GithubWebhookResult =\n | { status: 202; body: { ok: true; ignored?: true } }\n | { status: 400; body: { error: 'bad_request'; message: string } }\n | { status: 401; body: { error: 'unauthorized'; message: string } };\n\nexport interface GithubWebhookNotification {\n action: string;\n kind: string;\n priority: NotificationPriority;\n summary: string;\n terminal: boolean;\n metadata: GithubWebhookMetadata & { pullRequestNumber: number; repositoryId: number; installationId: number };\n payload: Record<string, unknown>;\n}\n\nexport interface GithubWebhookDispatchDependencies {\n controller: MountedMastraCode['controller'];\n /**\n * Integration used by the default sender-authorization check (collaborator\n * permission lookup). Author-gated notifications fail closed when neither\n * this nor an `isAuthorizedSender` override is supplied.\n */\n github?: GithubIntegration;\n listSubscriptions?: (\n target: GithubWebhookPullRequestTarget,\n options?: { includeTerminal?: boolean },\n ) => Promise<GithubSignalSubscriptionRow[]>;\n retireSubscription?: (id: string, status: 'open' | 'closed' | 'merged') => Promise<void>;\n isAuthorizedSender?: (notification: GithubWebhookNotification) => Promise<boolean>;\n onTargetError?: (subscription: GithubSignalSubscriptionRow, error: unknown) => void;\n}\n\nfunction normalizeHeader(value: string | undefined | null): string | null {\n if (!value) return null;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction verifySignature(rawBody: string, signature: string, secret: string): boolean {\n if (!signature.startsWith('sha256=')) return false;\n const signatureHex = signature.slice('sha256='.length);\n if (!/^[a-fA-F0-9]{64}$/.test(signatureHex)) return false;\n\n const expectedHex = createHmac('sha256', secret).update(rawBody).digest('hex');\n const received = Buffer.from(signatureHex, 'hex');\n const expected = Buffer.from(expectedHex, 'hex');\n return received.length === expected.length && timingSafeEqual(received, expected);\n}\n\nasync function parseGithubWebhook(\n c: Context,\n secret: string | undefined,\n): Promise<ParsedGithubWebhook | GithubWebhookResult> {\n if (!secret) {\n return { status: 401, body: { error: 'unauthorized', message: 'GitHub webhook secret is not configured' } };\n }\n\n const event = normalizeHeader(c.req.header('x-github-event'));\n const deliveryId = normalizeHeader(c.req.header('x-github-delivery'));\n const signature = normalizeHeader(c.req.header('x-hub-signature-256'));\n\n if (!event) return { status: 400, body: { error: 'bad_request', message: 'Missing x-github-event header' } };\n if (!deliveryId) return { status: 400, body: { error: 'bad_request', message: 'Missing x-github-delivery header' } };\n if (!signature)\n return { status: 401, body: { error: 'unauthorized', message: 'Missing x-hub-signature-256 header' } };\n\n const rawBody = await c.req.text();\n if (!verifySignature(rawBody, signature, secret)) {\n return { status: 401, body: { error: 'unauthorized', message: 'Invalid GitHub webhook signature' } };\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(rawBody);\n } catch {\n return { status: 400, body: { error: 'bad_request', message: 'Malformed JSON payload' } };\n }\n\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n return { status: 400, body: { error: 'bad_request', message: 'Payload must be a JSON object' } };\n }\n\n return { event, deliveryId, payload: payload as Record<string, unknown> };\n}\n\nfunction getObject(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction getString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction getNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction getBoolean(value: unknown): boolean | undefined {\n return typeof value === 'boolean' ? value : undefined;\n}\n\nfunction getLabels(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value\n .map(label => (typeof label === 'string' ? label : getString(getObject(label)?.name)))\n .filter((label): label is string => Boolean(label));\n}\n\nfunction getIssueTriageRunInput(parsed: ParsedGithubWebhook): GithubIssueTriageRunInput | null {\n if (parsed.event !== 'issues' || getString(parsed.payload.action) !== 'opened') return null;\n const repository = getString(getObject(parsed.payload.repository)?.full_name);\n const issue = getObject(parsed.payload.issue);\n const sender = getString(getObject(parsed.payload.sender)?.login);\n const installationId = getNumber(getObject(parsed.payload.installation)?.id);\n const issueNumber = getNumber(issue?.number);\n const issueTitle = getString(issue?.title);\n const issueUrl = getString(issue?.html_url);\n if (!repository || !installationId || !issueNumber || !issueTitle || !issueUrl) return null;\n return {\n repository,\n issueNumber,\n issueTitle,\n issueUrl,\n labels: getLabels(issue?.labels),\n sender,\n installationId,\n };\n}\n\nexport function normalizeGithubWebhookMetadata(parsed: ParsedGithubWebhook): GithubWebhookMetadata {\n const { event, deliveryId, payload } = parsed;\n const repository = getObject(payload.repository);\n const issue = getObject(payload.issue);\n const pullRequest = getObject(payload.pull_request);\n const sender = getObject(payload.sender);\n const installation = getObject(payload.installation);\n const issuePullRequest = getObject(issue?.pull_request);\n\n return {\n event,\n action: getString(payload.action),\n deliveryId,\n repository: getString(repository?.full_name),\n repositoryId: getNumber(repository?.id),\n issueNumber: getNumber(issue?.number),\n pullRequestNumber:\n getNumber(pullRequest?.number) ??\n (event === 'issue_comment' && issuePullRequest ? getNumber(issue?.number) : undefined),\n sender: getString(sender?.login),\n senderType: getString(sender?.type),\n installationId: getNumber(installation?.id),\n };\n}\n\nfunction notificationSummary(metadata: GithubWebhookMetadata, label: string): string {\n const actor = metadata.sender ? `${metadata.sender} ` : '';\n return `${actor}${label} on ${metadata.repository}#${metadata.pullRequestNumber}`;\n}\n\nfunction notificationTargetUrl(event: string, payload: Record<string, unknown>): string | undefined {\n if (event === 'issue_comment' || event === 'pull_request_review_comment') {\n return getString(getObject(payload.comment)?.html_url);\n }\n if (event === 'pull_request_review') {\n return getString(getObject(payload.review)?.html_url);\n }\n return getString(getObject(payload.pull_request)?.html_url);\n}\n\nexport function classifyGithubWebhook(parsed: ParsedGithubWebhook): GithubWebhookNotification | undefined {\n const metadata = normalizeGithubWebhookMetadata(parsed);\n const { event, payload } = parsed;\n const action = metadata.action;\n if (\n !action ||\n !metadata.repositoryId ||\n !metadata.installationId ||\n !metadata.pullRequestNumber ||\n !metadata.repository\n ) {\n return undefined;\n }\n\n let priority: NotificationPriority;\n let kind: string;\n let label: string;\n let terminal = false;\n\n if (event === 'pull_request_review' && action === 'submitted') {\n const state = getString(getObject(payload.review)?.state)?.toLowerCase().replaceAll('_', '-');\n priority = state === 'approved' || state === 'changes-requested' ? 'urgent' : 'high';\n kind =\n state === 'approved'\n ? 'review-approved'\n : state === 'changes-requested'\n ? 'review-changes-requested'\n : 'review-submitted';\n label =\n state === 'approved'\n ? 'approved the pull request'\n : state === 'changes-requested'\n ? 'requested changes'\n : 'submitted a review';\n } else if (event === 'pull_request' && action === 'closed') {\n const merged = getBoolean(getObject(payload.pull_request)?.merged) === true;\n priority = 'urgent';\n kind = merged ? 'pull-request-merged' : 'pull-request-closed';\n label = merged ? 'merged the pull request' : 'closed the pull request';\n terminal = true;\n } else if (event === 'issue_comment' && action === 'created') {\n priority = 'high';\n kind = 'issue-comment-created';\n label = 'commented';\n } else if (event === 'pull_request_review_comment' && action === 'created') {\n priority = 'high';\n kind = 'review-comment-created';\n label = 'left a review comment';\n } else if (event === 'pull_request' && action === 'reopened') {\n priority = 'high';\n kind = 'pull-request-reopened';\n label = 'reopened the pull request';\n } else if (event === 'pull_request_review' && action === 'dismissed') {\n priority = 'high';\n kind = 'review-dismissed';\n label = 'dismissed a review';\n } else if (\n event === 'pull_request' &&\n [\n 'synchronize',\n 'ready_for_review',\n 'converted_to_draft',\n 'assigned',\n 'unassigned',\n 'review_requested',\n 'review_request_removed',\n ].includes(action)\n ) {\n priority = 'medium';\n kind = `pull-request-${action.replaceAll('_', '-')}`;\n label = action.replaceAll('_', ' ');\n } else if (\n event === 'pull_request' &&\n ['edited', 'labeled', 'unlabeled', 'milestoned', 'demilestoned'].includes(action)\n ) {\n priority = 'low';\n kind = `pull-request-${action.replaceAll('_', '-')}`;\n label = action.replaceAll('_', ' ');\n } else {\n return undefined;\n }\n\n return {\n action,\n kind,\n priority,\n summary: notificationSummary(metadata, label),\n terminal,\n metadata: {\n ...metadata,\n pullRequestNumber: metadata.pullRequestNumber,\n repositoryId: metadata.repositoryId,\n installationId: metadata.installationId,\n },\n payload,\n };\n}\n\nasync function resolveSubscriptionSession(\n controller: MountedMastraCode['controller'],\n subscription: GithubSignalSubscriptionRow,\n) {\n const { sessionId, resourceId, threadId } = subscription;\n if (!sessionId || !resourceId || !threadId) {\n throw new Error(`GitHub subscription ${subscription.id} is missing its session binding.`);\n }\n const scope = subscription.sessionScope || undefined;\n let session = await controller.getSessionByResource(resourceId, scope);\n if (!session) {\n const tags = {\n factoryProjectId: resourceId,\n projectRepositoryId: subscription.data.projectRepositoryId,\n ...(scope ? { worktreePath: scope } : {}),\n };\n session = await controller.createSession({\n id: sessionId,\n ownerId: subscription.data.ownerId,\n resourceId,\n scope,\n tags,\n });\n }\n if (session.thread.getId() !== threadId) {\n await session.thread.switch({ threadId, emitEvent: false });\n }\n if (session.thread.getId() !== threadId) {\n throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n }\n return session;\n}\n\nconst AUTHORIZED_BOTS = new Set(['coderabbitai[bot]', 'devin-ai-integration[bot]']);\nconst AUTHORIZED_PERMISSIONS = new Set(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment-created',\n 'review-comment-created',\n 'review-submitted',\n 'review-approved',\n 'review-changes-requested',\n 'review-dismissed',\n]);\n\nasync function isAuthorizedGithubSender(\n notification: GithubWebhookNotification,\n github: GithubIntegration | undefined,\n): Promise<boolean> {\n if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;\n const sender = notification.metadata.sender;\n const repository = notification.metadata.repository;\n if (!sender || !repository) return false;\n const normalizedSender = sender.toLowerCase();\n if (notification.metadata.senderType?.toLowerCase() === 'bot' || normalizedSender.endsWith('[bot]')) {\n return AUTHORIZED_BOTS.has(normalizedSender);\n }\n if (!github) return false;\n const abortController = new AbortController();\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n const permission = await Promise.race([\n github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n ),\n new Promise<undefined>(resolve => {\n timeout = setTimeout(() => {\n abortController.abort();\n resolve(undefined);\n }, PERMISSION_CHECK_TIMEOUT_MS);\n }),\n ]);\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nexport async function dispatchGithubWebhook(\n parsed: ParsedGithubWebhook,\n dependencies: GithubWebhookDispatchDependencies,\n): Promise<{ delivered: number; failed: number; ignored: boolean }> {\n const notification = classifyGithubWebhook(parsed);\n if (!notification) return { delivered: 0, failed: 0, ignored: true };\n const isAuthorizedSender =\n dependencies.isAuthorizedSender ??\n ((n: GithubWebhookNotification) => isAuthorizedGithubSender(n, dependencies.github));\n if (!(await isAuthorizedSender(notification))) {\n return { delivered: 0, failed: 0, ignored: true };\n }\n\n const target = {\n installationExternalId: notification.metadata.installationId.toString(),\n repositoryExternalId: notification.metadata.repositoryId.toString(),\n changeRequestId: notification.metadata.pullRequestNumber.toString(),\n };\n const listSubscriptions =\n dependencies.listSubscriptions ??\n ((subscriptionTarget: GithubWebhookPullRequestTarget, options?: { includeTerminal?: boolean }) => {\n if (!dependencies.github) throw new Error('GitHub integration is required to load webhook subscriptions.');\n return listPullRequestSubscriptionsForWebhook(\n subscriptionTarget,\n options,\n dependencies.github.integrationStorage,\n );\n });\n const retireSubscription =\n dependencies.retireSubscription ??\n ((id: string, status: 'open' | 'closed' | 'merged') => {\n if (!dependencies.github) throw new Error('GitHub integration is required to retire webhook subscriptions.');\n return retirePullRequestSubscription(id, status, dependencies.github.integrationStorage);\n });\n const subscriptions = await listSubscriptions(target, { includeTerminal: notification.action === 'reopened' });\n let delivered = 0;\n let failed = 0;\n\n for (const subscription of subscriptions) {\n try {\n const session = await resolveSubscriptionSession(dependencies.controller, subscription);\n const result = await session.sendNotificationSignal({\n source: 'github',\n kind: notification.kind,\n summary: notification.summary,\n priority: notification.priority,\n payload: notification.payload,\n sourceId: parsed.deliveryId,\n dedupeKey: `${parsed.deliveryId}:${subscription.sessionId}:${subscription.threadId}`,\n coalesceKey: `github:${subscription.data.repositoryExternalId}:pull-request:${subscription.data.changeRequestId}`,\n metadata: {\n event: notification.metadata.event,\n action: notification.action,\n repository: notification.metadata.repository,\n issueNumber: notification.metadata.issueNumber,\n pullRequestNumber: notification.metadata.pullRequestNumber,\n targetUrl: notificationTargetUrl(parsed.event, parsed.payload),\n deliveryId: parsed.deliveryId,\n },\n });\n await Promise.all([result.persisted, result.accepted].filter(Boolean));\n if (notification.terminal) {\n await retireSubscription(subscription.id, notification.kind === 'pull-request-merged' ? 'merged' : 'closed');\n } else if (notification.action === 'reopened') {\n await retireSubscription(subscription.id, 'open');\n }\n delivered += 1;\n } catch (error) {\n failed += 1;\n dependencies.onTargetError?.(subscription, error);\n }\n }\n\n return { delivered, failed, ignored: false };\n}\n\nexport async function handleGithubWebhook(\n c: Context,\n options: GithubWebhookHandlerOptions & Partial<Omit<GithubWebhookDispatchDependencies, 'github'>>,\n): Promise<GithubWebhookResult> {\n const parsed = await parseGithubWebhook(c, options.github.webhookSecret);\n if ('status' in parsed) return parsed;\n\n if (!SUPPORTED_GITHUB_WEBHOOK_EVENTS.has(parsed.event)) {\n return { status: 202, body: { ok: true, ignored: true } };\n }\n\n const metadata = normalizeGithubWebhookMetadata(parsed);\n console.info('[GitHub Webhook]', metadata);\n\n if (options.ingestFactoryEvent) {\n await options.ingestFactoryEvent(parsed);\n } else {\n const issueTriageRun = getIssueTriageRunInput(parsed);\n if (issueTriageRun && options.runIssueTriage) {\n void options.runIssueTriage(issueTriageRun).catch((error: unknown) => {\n console.error('[GitHub Webhook] Failed to run issue triage', {\n deliveryId: metadata.deliveryId,\n repository: metadata.repository,\n issueNumber: metadata.issueNumber,\n error: error instanceof Error ? error.message : String(error),\n });\n });\n }\n }\n\n if (!options.controller) {\n return { status: 202, body: { ok: true } };\n }\n\n const result = await dispatchGithubWebhook(parsed, options as GithubWebhookDispatchDependencies);\n if (result.failed > 0) {\n console.warn(`[GitHub Webhook] ${result.failed} subscribed target(s) failed for delivery ${parsed.deliveryId}.`);\n }\n return { status: 202, body: { ok: true, ...(result.ignored ? { ignored: true as const } : {}) } };\n}\n","export interface PlatformApiClientConfig {\n baseUrl: string;\n accessToken: string;\n fetchImpl?: typeof fetch;\n}\n\nexport function platformApiClientConfigFromEnv(): PlatformApiClientConfig {\n const sharedApiUrl = process.env.MASTRA_SHARED_API_URL?.trim() || 'https://platform.mastra.ai/v1';\n const accessToken = process.env.MASTRA_PLATFORM_SECRET_KEY?.trim();\n if (!accessToken) {\n throw new Error('Platform integration: missing required environment variable MASTRA_PLATFORM_SECRET_KEY.');\n }\n return { baseUrl: normalizeSharedApiUrl(sharedApiUrl), accessToken };\n}\n\nfunction normalizeSharedApiUrl(sharedApiUrl: string): string {\n return sharedApiUrl.replace(/\\/+$/, '').replace(/\\/v1$/, '');\n}\n\nexport class PlatformApiError extends Error {\n readonly status: number;\n readonly retryAfterSeconds: number | null;\n\n constructor(message: string, status: number, retryAfterSeconds: number | null = null) {\n super(message);\n this.name = 'PlatformApiError';\n this.status = status;\n this.retryAfterSeconds = retryAfterSeconds;\n }\n}\n\nexport class PlatformApiClient {\n readonly #baseUrl: string;\n readonly #accessToken: string;\n readonly #fetch: typeof fetch;\n\n constructor(config: PlatformApiClientConfig) {\n const missing = ['baseUrl', 'accessToken'].filter(field => !config[field as keyof PlatformApiClientConfig]);\n if (missing.length > 0) {\n throw new Error(`Platform integration: missing required config field(s): ${missing.join(', ')}.`);\n }\n this.#baseUrl = config.baseUrl.replace(/\\/+$/, '');\n this.#accessToken = config.accessToken;\n this.#fetch = config.fetchImpl ?? globalThis.fetch;\n }\n\n async request<T>(\n method: string,\n path: string,\n body?: unknown,\n options?: { signal?: AbortSignal; actingUserId?: string },\n ): Promise<T> {\n const response = await this.#send(method, path, body, options);\n if (!response.ok) {\n const message = redact(await extractError(response), this.#accessToken);\n const retryAfterSeconds = parseRetryAfter(response.headers.get('retry-after'));\n logPlatformError('Platform API request failed', {\n method,\n path,\n status: response.status,\n retryAfterSeconds,\n message,\n });\n throw new PlatformApiError(message, response.status, retryAfterSeconds);\n }\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n }\n\n async requestRedirect(method: string, path: string, options?: { signal?: AbortSignal }): Promise<string> {\n const response = await this.#send(method, path, undefined, options, 'manual');\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('location');\n if (location) return location;\n }\n if (!response.ok) {\n const message = redact(await extractError(response), this.#accessToken);\n const retryAfterSeconds = parseRetryAfter(response.headers.get('retry-after'));\n logPlatformError('Platform API redirect request failed', {\n method,\n path,\n status: response.status,\n retryAfterSeconds,\n message,\n });\n throw new PlatformApiError(message, response.status, retryAfterSeconds);\n }\n logPlatformError('Platform API request did not return a redirect', { method, path, status: response.status });\n throw new PlatformApiError('Platform API request did not return a redirect.', response.status);\n }\n\n async #send(\n method: string,\n path: string,\n body?: unknown,\n options?: { signal?: AbortSignal; actingUserId?: string },\n redirect?: RequestInit['redirect'],\n ): Promise<Response> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n authorization: `Bearer ${this.#accessToken}`,\n };\n // Acting end-user for platform GitHub writes: the platform resolves this\n // user's GitHub OAuth connection (org-scoped) so issues/PRs are authored\n // by the human instead of the App bot. Ignored by older platforms.\n if (options?.actingUserId) {\n headers['x-acting-user-id'] = options.actingUserId;\n }\n const timeoutSignal = AbortSignal.timeout(15_000);\n const init: RequestInit = {\n method,\n headers,\n redirect,\n signal: options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal,\n };\n if (body !== undefined) {\n headers['content-type'] = 'application/json';\n init.body = JSON.stringify(body);\n }\n\n try {\n return await this.#fetch(`${this.#baseUrl}${path}`, init);\n } catch (error) {\n if (error instanceof Error && error.message.includes(this.#accessToken)) {\n const redacted = new Error(redact(error.message, this.#accessToken));\n redacted.name = error.name;\n logPlatformError('Platform API transport error', {\n method,\n path,\n name: redacted.name,\n message: redacted.message,\n });\n throw redacted;\n }\n logPlatformError('Platform API transport error', {\n method,\n path,\n name: error instanceof Error ? error.name : undefined,\n message: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n }\n}\n\nasync function extractError(response: Response): Promise<string> {\n try {\n const data = (await response.clone().json()) as Record<string, unknown>;\n for (const field of ['detail', 'error', 'title']) {\n if (typeof data[field] === 'string' && data[field]) return data[field];\n }\n } catch {\n // Fall through to the status-based message.\n }\n return `Platform API request failed (${response.status})`;\n}\n\nfunction redact(message: string, accessToken: string): string {\n return message.split(accessToken).join('[REDACTED]');\n}\n\nexport function logPlatformInfo(message: string, fields?: Record<string, unknown>): void {\n writePlatformLog('info', message, fields);\n}\n\nexport function logPlatformWarn(message: string, fields?: Record<string, unknown>): void {\n writePlatformLog('warn', message, fields);\n}\n\nexport function logPlatformError(message: string, fields?: Record<string, unknown>): void {\n writePlatformLog('error', message, fields);\n}\n\nfunction writePlatformLog(level: 'info' | 'warn' | 'error', message: string, fields?: Record<string, unknown>): void {\n const metadata = fields ? ` ${JSON.stringify(stripUndefined(fields))}` : '';\n process.stderr.write(`[Mastra Factory] ${level.toUpperCase()} ${message}${metadata}\\n`);\n}\n\nfunction stripUndefined(fields: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));\n}\n\nfunction parseRetryAfter(value: string | null): number | null {\n if (!value) return null;\n const seconds = Number.parseInt(value, 10);\n return Number.isSafeInteger(seconds) && seconds >= 0 ? seconds : null;\n}\n"],"mappings":"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","names":["AUTHOR_GATED_KINDS","AUTHORIZED_BOTS","AUTHORIZED_PERMISSIONS","PERMISSION_CHECK_TIMEOUT_MS"]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/integrations/platform/github/event-worker.ts","../../../../src/integrations/github/subscriptions.ts","../../../../src/integrations/github/webhook.ts","../../../../src/integrations/platform/api-client.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type { IntegrationStorageHandle } from '../../../storage/domains/integrations/base.js';\nimport type { GithubRepositoryPermission } from '../../github/integration.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from '../../github/subscriptions.js';\nimport type { GithubSubscriptionStorage } from '../../github/subscriptions.js';\nimport { dispatchGithubWebhook } from '../../github/webhook.js';\nimport type { GithubWebhookNotification, ParsedGithubWebhook } from '../../github/webhook.js';\nimport type { PlatformApiClient } from '../api-client.js';\nimport { PlatformApiError } from '../api-client.js';\n\nconst API_PREFIX = '/v1/server/github-app';\nconst DEFAULT_POLL_INTERVAL_MS = 20_000;\nconst EVENT_PAGE_SIZE = 500;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst CURSOR_ORG_ID = '__platform_github_event_worker__';\nconst CURSOR_USER_ID = 'worker';\nconst SUPPORTED_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n]);\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment',\n 'pull-request-comment',\n 'pull-request-review',\n 'pull-request-review-comment',\n]);\nconst AUTHORIZED_BOTS = new Set(['coderabbitai[bot]', 'devin-ai-integration[bot]']);\nconst AUTHORIZED_PERMISSIONS = new Set<GithubRepositoryPermission>(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\n\ntype EventCursor = { afterEventId: string } | { afterTimestamp: number };\ntype PlatformGithubEventWorkerSettings = {\n version: 1;\n repositories: Record<string, EventCursor>;\n};\n\nexport type PlatformGithubEventStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n PlatformGithubEventWorkerSettings,\n Record<string, unknown>\n>;\n\ntype EventLogEntry = {\n id: string;\n deliveryId: string;\n event: string;\n payload: unknown;\n};\n\ntype Repository = { id: number };\n\nexport interface PlatformGithubEventDispatchIntegration {\n readonly integrationStorage: GithubSubscriptionStorage;\n getRepositoryCollaboratorPermission(\n installationId: number,\n repoFullName: string,\n username: string,\n signal?: AbortSignal,\n ): Promise<GithubRepositoryPermission | undefined>;\n}\n\nexport interface PlatformGithubEventWorkerConfig {\n client: PlatformApiClient;\n controller: MountedMastraCode['controller'];\n github: PlatformGithubEventDispatchIntegration;\n storage: PlatformGithubEventStorage;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n intervalMs?: number;\n now?: () => number;\n dispatch?: typeof dispatchGithubWebhook;\n}\n\nexport class PlatformGithubEventWorker extends MastraWorker {\n readonly name = 'platform-github-events';\n\n readonly #client: PlatformApiClient;\n readonly #controller: MountedMastraCode['controller'];\n readonly #github: PlatformGithubEventDispatchIntegration;\n readonly #storage: PlatformGithubEventStorage;\n readonly #ingestFactoryEvent: ((event: ParsedGithubWebhook) => Promise<unknown>) | undefined;\n readonly #intervalMs: number;\n readonly #now: () => number;\n readonly #dispatch: typeof dispatchGithubWebhook;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #leaseRenewalTimer: ReturnType<typeof setInterval> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n #leaseTtlMs: number;\n #hasLease = false;\n #startedAt = 0;\n #settings: PlatformGithubEventWorkerSettings = { version: 1, repositories: {} };\n\n constructor(config: PlatformGithubEventWorkerConfig) {\n super();\n this.#client = config.client;\n this.#controller = config.controller;\n this.#github = config.github;\n this.#storage = config.storage;\n this.#ingestFactoryEvent = config.ingestFactoryEvent;\n this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('Platform GitHub event polling interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n this.#now = config.now ?? Date.now;\n this.#dispatch = config.dispatch ?? dispatchGithubWebhook;\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('PlatformGithubEventWorker: call init() before start()');\n\n this.#startedAt = this.#now() - 1;\n this.#settings = normalizeSettings(await this.#storage.settings.get(CURSOR_ORG_ID, CURSOR_USER_ID));\n this.#running = true;\n this.deps.logger.info('Platform GitHub event polling started', {\n intervalMs: this.#intervalMs,\n leaseTtlMs: this.#leaseTtlMs,\n });\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n this.#stopLeaseRenewal();\n await this.#inFlight;\n if (this.#hasLease) {\n await this.#leaseProvider.releaseLease(this.#leaseKey(), this.#leaseOwner).catch(() => undefined);\n this.#hasLease = false;\n }\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick();\n this.#inFlight = run;\n void run.finally(() => {\n if (this.#inFlight === run) this.#inFlight = undefined;\n });\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n let nextDelay = this.#intervalMs;\n try {\n if (!(await this.#ensureLease())) return;\n nextDelay = await this.#poll();\n } catch (error) {\n nextDelay = retryDelay(error, this.#intervalMs);\n this.deps?.logger.error('Platform GitHub event polling cycle failed', {\n error: error instanceof Error ? error.message : String(error),\n retryInMs: nextDelay,\n });\n } finally {\n this.#schedule(nextDelay);\n }\n }\n\n async #ensureLease(): Promise<boolean> {\n if (this.#hasLease) return true;\n const result = await this.#leaseProvider.acquireLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs);\n this.#hasLease = result.acquired;\n if (this.#hasLease) this.#startLeaseRenewal();\n return this.#hasLease;\n }\n\n #startLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) return;\n this.#leaseRenewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs)\n .then(renewed => {\n if (!renewed) {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n }\n })\n .catch(error => {\n this.#hasLease = false;\n this.#stopLeaseRenewal();\n this.deps?.logger.warn('Platform GitHub event polling lease renewal failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.floor(this.#leaseTtlMs / 3),\n );\n this.#leaseRenewalTimer.unref?.();\n }\n\n #stopLeaseRenewal(): void {\n if (this.#leaseRenewalTimer) clearInterval(this.#leaseRenewalTimer);\n this.#leaseRenewalTimer = undefined;\n }\n\n async #poll(): Promise<number> {\n const repositories = await this.#discoverRepositories();\n let retryInMs = this.#intervalMs;\n\n for (const repository of repositories) {\n if (!this.#running || !this.#hasLease) break;\n try {\n await this.#pollRepository(repository.id);\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform GitHub repository event polling failed', {\n repositoryId: repository.id,\n error: error instanceof Error ? error.message : String(error),\n retryInMs: delay,\n });\n if (error instanceof PlatformApiError && error.status === 429) break;\n }\n }\n\n return retryInMs;\n }\n\n async #discoverRepositories(): Promise<Repository[]> {\n const result = await this.#client.request<{\n installations: Array<{\n installationId: number;\n usable: boolean;\n suspendedAt: string | null;\n }>;\n }>('GET', `${API_PREFIX}/installations`);\n const repositories = new Map<number, Repository>();\n\n for (const installation of result.installations) {\n if (!installation.usable || installation.suspendedAt) continue;\n const page = await this.#client.request<{ repositories: Repository[] }>(\n 'GET',\n `${API_PREFIX}/installations/${installation.installationId}/repositories`,\n );\n for (const repository of page.repositories) repositories.set(repository.id, repository);\n }\n\n return [...repositories.values()];\n }\n\n async #pollRepository(repositoryId: number): Promise<void> {\n const key = String(repositoryId);\n if (!this.#settings.repositories[key]) {\n this.#settings.repositories[key] = { afterTimestamp: this.#startedAt };\n await this.#saveSettings();\n }\n\n while (this.#running && this.#hasLease) {\n const cursor: EventCursor = this.#settings.repositories[key]!;\n const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE) });\n if ('afterEventId' in cursor) query.set('afterEventId', cursor.afterEventId);\n else query.set('afterTimestamp', String(cursor.afterTimestamp));\n\n const pollStartedAt = performance.now();\n const page = await this.#client.request<{ events: EventLogEntry[]; nextCursor: string | null }>(\n 'GET',\n `${API_PREFIX}/repositories/${repositoryId}/events?${query}`,\n );\n this.deps?.logger.info('Platform GitHub repository event poll completed', {\n repositoryId,\n eventCount: page.events.length,\n latencyMs: Math.round(performance.now() - pollStartedAt),\n });\n if (page.events.length === 0 || !page.nextCursor) return;\n\n for (const event of page.events) {\n if (!this.#running || !this.#hasLease) return;\n const parsed = parseEvent(event);\n if (!parsed) {\n this.deps?.logger.warn('Platform GitHub event log returned a malformed event', {\n repositoryId,\n eventId: event.id,\n });\n continue;\n }\n if (isFactoryClosureEvent(parsed)) {\n await this.#ingestFactoryEvent?.(parsed);\n }\n const result = await this.#dispatch(parsed, {\n controller: this.#controller,\n listSubscriptions: (target, options) =>\n listPullRequestSubscriptionsForWebhook(target, options, this.#github.integrationStorage),\n retireSubscription: (id, status) =>\n retirePullRequestSubscription(id, status, this.#github.integrationStorage),\n isAuthorizedSender: notification => this.#isAuthorizedSender(notification),\n onTargetError: (subscription, error) => {\n this.deps?.logger.error('Platform GitHub event delivery failed for a subscription', {\n subscriptionId: subscription.id,\n resourceId: subscription.resourceId,\n threadId: subscription.threadId,\n error: error instanceof Error ? error.message : String(error),\n });\n },\n });\n if (result.failed > 0) {\n throw new Error(\n `Platform GitHub event ${event.deliveryId} failed for ${result.failed} subscribed target(s).`,\n );\n }\n }\n\n if (page.nextCursor === ('afterEventId' in cursor ? cursor.afterEventId : undefined)) return;\n this.#settings.repositories[key] = { afterEventId: page.nextCursor };\n await this.#saveSettings();\n }\n }\n\n async #isAuthorizedSender(notification: GithubWebhookNotification): Promise<boolean> {\n if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;\n const sender = notification.metadata.sender;\n const repository = notification.metadata.repository;\n if (!sender || !repository) return false;\n if (AUTHORIZED_BOTS.has(sender)) return true;\n\n const abortController = new AbortController();\n const timeout = setTimeout(() => abortController.abort(), PERMISSION_CHECK_TIMEOUT_MS);\n try {\n const permission = await this.#github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n );\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n async #saveSettings(): Promise<void> {\n await this.#storage.settings.save(CURSOR_ORG_ID, CURSOR_USER_ID, this.#settings);\n }\n\n #leaseKey(): string {\n return `${this.name}:${this.#storage.integrationId}`;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n\nfunction normalizeSettings(value: PlatformGithubEventWorkerSettings | null): PlatformGithubEventWorkerSettings {\n if (!value || value.version !== 1 || !value.repositories || typeof value.repositories !== 'object') {\n return { version: 1, repositories: {} };\n }\n return { version: 1, repositories: { ...value.repositories } };\n}\n\nfunction isFactoryClosureEvent(event: ParsedGithubWebhook): boolean {\n return (event.event === 'issues' || event.event === 'pull_request') && event.payload.action === 'closed';\n}\n\nfunction parseEvent(event: EventLogEntry): ParsedGithubWebhook | null {\n if (\n !event.id ||\n !event.deliveryId ||\n !SUPPORTED_EVENTS.has(event.event) ||\n !event.payload ||\n typeof event.payload !== 'object' ||\n Array.isArray(event.payload)\n ) {\n return null;\n }\n return {\n event: event.event,\n deliveryId: event.deliveryId,\n payload: event.payload as Record<string, unknown>,\n };\n}\n\nfunction retryDelay(error: unknown, fallbackMs: number): number {\n if (error instanceof PlatformApiError && error.status === 429 && error.retryAfterSeconds !== null) {\n return Math.max(fallbackMs, error.retryAfterSeconds * 1_000);\n }\n return fallbackMs;\n}\n","import type { IntegrationStorageHandle, IntegrationSubscription } from '../../storage/domains/integrations/base.js';\n\nexport type GithubSignalSubscriptionSource = 'auto-gh-pr-create' | 'factory-pr-create' | 'explicit-tool';\nexport type GithubSignalSubscriptionStatus = 'open' | 'closed' | 'merged';\n\nexport interface GithubSignalSubscriptionData {\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n ownerId: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId: string | null;\n}\n\nexport type GithubSignalSubscriptionRow = IntegrationSubscription<GithubSignalSubscriptionData>;\nexport type GithubSubscriptionStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n GithubSignalSubscriptionData\n>;\n\nexport interface SubscribeToPullRequestInput {\n orgId: string;\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n sessionId: string;\n ownerId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId?: string;\n}\n\nexport interface ThreadSubscriptionTarget {\n orgId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n}\n\nexport interface PullRequestSubscriptionTarget {\n orgId: string;\n installationExternalId: string;\n repositoryExternalId: string;\n changeRequestId: string;\n}\n\nexport type GithubWebhookPullRequestTarget = Omit<PullRequestSubscriptionTarget, 'orgId'>;\n\nexport function changeRequestTargetKey(input: GithubWebhookPullRequestTarget): string {\n return `change-request:${input.installationExternalId}:${input.repositoryExternalId}:${input.changeRequestId}`;\n}\n\nfunction sameSession(row: GithubSignalSubscriptionRow, input: SubscribeToPullRequestInput): boolean {\n return (\n row.orgId === input.orgId &&\n row.sessionId === input.sessionId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? '')\n );\n}\n\nexport async function subscribeToPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow> {\n const targetKey = changeRequestTargetKey(input);\n const existing = (await storage.subscriptions.listByTarget(targetKey)).find(row => sameSession(row, input));\n if (existing) {\n if (existing.status !== 'open') await storage.subscriptions.updateStatus(existing.id, 'open');\n return { ...existing, status: 'open' };\n }\n\n return storage.subscriptions.create({\n orgId: input.orgId,\n targetKey,\n sessionId: input.sessionId,\n resourceId: input.resourceId,\n threadId: input.threadId,\n sessionScope: input.sessionScope ?? '',\n status: 'open',\n data: {\n installationExternalId: input.installationExternalId,\n projectRepositoryId: input.projectRepositoryId,\n repositoryExternalId: input.repositoryExternalId,\n repositorySlug: input.repositorySlug,\n changeRequestId: input.changeRequestId,\n ownerId: input.ownerId,\n source: input.source,\n subscribedByUserId: input.subscribedByUserId ?? null,\n },\n });\n}\n\nexport async function unsubscribeFromPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n await Promise.all(rows.filter(row => sameSession(row, input)).map(row => storage.subscriptions.delete(row.id)));\n}\n\nexport async function listPullRequestSubscriptionsForThread(\n input: ThreadSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByThread(input.resourceId, input.threadId);\n return rows.filter(\n row =>\n row.orgId === input.orgId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? ''),\n );\n}\n\nexport async function listPullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return rows.filter(row => row.orgId === input.orgId && row.status === 'open');\n}\n\nexport async function listPullRequestSubscriptionsForWebhook(\n input: GithubWebhookPullRequestTarget,\n options: { includeTerminal?: boolean } | undefined,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return options?.includeTerminal ? rows : rows.filter(row => row.status === 'open');\n}\n\nexport function retirePullRequestSubscription(\n id: string,\n status: GithubSignalSubscriptionStatus,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n return storage.subscriptions.updateStatus(id, status);\n}\n\nexport async function retirePullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await listPullRequestSubscriptions(input, storage);\n await Promise.all(rows.map(row => storage.subscriptions.updateStatus(row.id, 'closed')));\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { MountedMastraCode } from '@mastra/code-sdk';\nimport type { NotificationPriority } from '@mastra/core/notifications';\nimport type { Context } from 'hono';\nimport type { GithubIntegration } from './integration.js';\nimport type { GithubIssueTriageInput, GithubIssueTriageResult } from './issue-triage.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from './subscriptions.js';\nimport type { GithubSignalSubscriptionRow, GithubWebhookPullRequestTarget } from './subscriptions.js';\n\nexport type GithubIssueTriageRunInput = GithubIssueTriageInput;\nexport type GithubIssueTriageRunResult = GithubIssueTriageResult;\n\nexport interface GithubWebhookHandlerOptions {\n /** Integration providing webhook-secret verification + collaborator permission checks. */\n github: GithubIntegration;\n runIssueTriage?: (input: GithubIssueTriageRunInput) => Promise<GithubIssueTriageRunResult>;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n}\n\nconst SUPPORTED_GITHUB_WEBHOOK_EVENTS = new Set([\n 'issues',\n 'issue_comment',\n 'pull_request',\n 'pull_request_review',\n 'pull_request_review_comment',\n]);\n\nexport interface GithubWebhookMetadata {\n event: string;\n action?: string;\n deliveryId: string;\n repository?: string;\n repositoryId?: number;\n issueNumber?: number;\n pullRequestNumber?: number;\n sender?: string;\n senderType?: string;\n installationId?: number;\n}\n\nexport interface ParsedGithubWebhook {\n event: string;\n deliveryId: string;\n payload: Record<string, unknown>;\n}\n\nexport type GithubWebhookResult =\n | { status: 202; body: { ok: true; ignored?: true } }\n | { status: 400; body: { error: 'bad_request'; message: string } }\n | { status: 401; body: { error: 'unauthorized'; message: string } };\n\nexport interface GithubWebhookNotification {\n action: string;\n kind: string;\n priority: NotificationPriority;\n summary: string;\n terminal: boolean;\n metadata: GithubWebhookMetadata & { pullRequestNumber: number; repositoryId: number; installationId: number };\n payload: Record<string, unknown>;\n}\n\nexport interface GithubWebhookDispatchDependencies {\n controller: MountedMastraCode['controller'];\n /**\n * Integration used by the default sender-authorization check (collaborator\n * permission lookup). Author-gated notifications fail closed when neither\n * this nor an `isAuthorizedSender` override is supplied.\n */\n github?: GithubIntegration;\n listSubscriptions?: (\n target: GithubWebhookPullRequestTarget,\n options?: { includeTerminal?: boolean },\n ) => Promise<GithubSignalSubscriptionRow[]>;\n retireSubscription?: (id: string, status: 'open' | 'closed' | 'merged') => Promise<void>;\n isAuthorizedSender?: (notification: GithubWebhookNotification) => Promise<boolean>;\n onTargetError?: (subscription: GithubSignalSubscriptionRow, error: unknown) => void;\n}\n\nfunction normalizeHeader(value: string | undefined | null): string | null {\n if (!value) return null;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction verifySignature(rawBody: string, signature: string, secret: string): boolean {\n if (!signature.startsWith('sha256=')) return false;\n const signatureHex = signature.slice('sha256='.length);\n if (!/^[a-fA-F0-9]{64}$/.test(signatureHex)) return false;\n\n const expectedHex = createHmac('sha256', secret).update(rawBody).digest('hex');\n const received = Buffer.from(signatureHex, 'hex');\n const expected = Buffer.from(expectedHex, 'hex');\n return received.length === expected.length && timingSafeEqual(received, expected);\n}\n\nasync function parseGithubWebhook(\n c: Context,\n secret: string | undefined,\n): Promise<ParsedGithubWebhook | GithubWebhookResult> {\n if (!secret) {\n return { status: 401, body: { error: 'unauthorized', message: 'GitHub webhook secret is not configured' } };\n }\n\n const event = normalizeHeader(c.req.header('x-github-event'));\n const deliveryId = normalizeHeader(c.req.header('x-github-delivery'));\n const signature = normalizeHeader(c.req.header('x-hub-signature-256'));\n\n if (!event) return { status: 400, body: { error: 'bad_request', message: 'Missing x-github-event header' } };\n if (!deliveryId) return { status: 400, body: { error: 'bad_request', message: 'Missing x-github-delivery header' } };\n if (!signature)\n return { status: 401, body: { error: 'unauthorized', message: 'Missing x-hub-signature-256 header' } };\n\n const rawBody = await c.req.text();\n if (!verifySignature(rawBody, signature, secret)) {\n return { status: 401, body: { error: 'unauthorized', message: 'Invalid GitHub webhook signature' } };\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(rawBody);\n } catch {\n return { status: 400, body: { error: 'bad_request', message: 'Malformed JSON payload' } };\n }\n\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n return { status: 400, body: { error: 'bad_request', message: 'Payload must be a JSON object' } };\n }\n\n return { event, deliveryId, payload: payload as Record<string, unknown> };\n}\n\nfunction getObject(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction getString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction getNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction getBoolean(value: unknown): boolean | undefined {\n return typeof value === 'boolean' ? value : undefined;\n}\n\nfunction getLabels(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value\n .map(label => (typeof label === 'string' ? label : getString(getObject(label)?.name)))\n .filter((label): label is string => Boolean(label));\n}\n\nfunction getIssueTriageRunInput(parsed: ParsedGithubWebhook): GithubIssueTriageRunInput | null {\n if (parsed.event !== 'issues' || getString(parsed.payload.action) !== 'opened') return null;\n const repository = getString(getObject(parsed.payload.repository)?.full_name);\n const issue = getObject(parsed.payload.issue);\n const sender = getString(getObject(parsed.payload.sender)?.login);\n const installationId = getNumber(getObject(parsed.payload.installation)?.id);\n const issueNumber = getNumber(issue?.number);\n const issueTitle = getString(issue?.title);\n const issueUrl = getString(issue?.html_url);\n if (!repository || !installationId || !issueNumber || !issueTitle || !issueUrl) return null;\n return {\n repository,\n issueNumber,\n issueTitle,\n issueUrl,\n labels: getLabels(issue?.labels),\n sender,\n installationId,\n };\n}\n\nexport function normalizeGithubWebhookMetadata(parsed: ParsedGithubWebhook): GithubWebhookMetadata {\n const { event, deliveryId, payload } = parsed;\n const repository = getObject(payload.repository);\n const issue = getObject(payload.issue);\n const pullRequest = getObject(payload.pull_request);\n const sender = getObject(payload.sender);\n const installation = getObject(payload.installation);\n const issuePullRequest = getObject(issue?.pull_request);\n\n return {\n event,\n action: getString(payload.action),\n deliveryId,\n repository: getString(repository?.full_name),\n repositoryId: getNumber(repository?.id),\n issueNumber: getNumber(issue?.number),\n pullRequestNumber:\n getNumber(pullRequest?.number) ??\n (event === 'issue_comment' && issuePullRequest ? getNumber(issue?.number) : undefined),\n sender: getString(sender?.login),\n senderType: getString(sender?.type),\n installationId: getNumber(installation?.id),\n };\n}\n\nfunction notificationSummary(metadata: GithubWebhookMetadata, label: string): string {\n const actor = metadata.sender ? `${metadata.sender} ` : '';\n return `${actor}${label} on ${metadata.repository}#${metadata.pullRequestNumber}`;\n}\n\nfunction notificationTargetUrl(event: string, payload: Record<string, unknown>): string | undefined {\n if (event === 'issue_comment' || event === 'pull_request_review_comment') {\n return getString(getObject(payload.comment)?.html_url);\n }\n if (event === 'pull_request_review') {\n return getString(getObject(payload.review)?.html_url);\n }\n return getString(getObject(payload.pull_request)?.html_url);\n}\n\nexport function classifyGithubWebhook(parsed: ParsedGithubWebhook): GithubWebhookNotification | undefined {\n const metadata = normalizeGithubWebhookMetadata(parsed);\n const { event, payload } = parsed;\n const action = metadata.action;\n if (\n !action ||\n !metadata.repositoryId ||\n !metadata.installationId ||\n !metadata.pullRequestNumber ||\n !metadata.repository\n ) {\n return undefined;\n }\n\n let priority: NotificationPriority;\n let kind: string;\n let label: string;\n let terminal = false;\n\n if (event === 'pull_request_review' && action === 'submitted') {\n const state = getString(getObject(payload.review)?.state)?.toLowerCase().replaceAll('_', '-');\n priority = state === 'approved' || state === 'changes-requested' ? 'urgent' : 'high';\n kind =\n state === 'approved'\n ? 'review-approved'\n : state === 'changes-requested'\n ? 'review-changes-requested'\n : 'review-submitted';\n label =\n state === 'approved'\n ? 'approved the pull request'\n : state === 'changes-requested'\n ? 'requested changes'\n : 'submitted a review';\n } else if (event === 'pull_request' && action === 'closed') {\n const merged = getBoolean(getObject(payload.pull_request)?.merged) === true;\n priority = 'urgent';\n kind = merged ? 'pull-request-merged' : 'pull-request-closed';\n label = merged ? 'merged the pull request' : 'closed the pull request';\n terminal = true;\n } else if (event === 'issue_comment' && action === 'created') {\n priority = 'high';\n kind = 'issue-comment-created';\n label = 'commented';\n } else if (event === 'pull_request_review_comment' && action === 'created') {\n priority = 'high';\n kind = 'review-comment-created';\n label = 'left a review comment';\n } else if (event === 'pull_request' && action === 'reopened') {\n priority = 'high';\n kind = 'pull-request-reopened';\n label = 'reopened the pull request';\n } else if (event === 'pull_request_review' && action === 'dismissed') {\n priority = 'high';\n kind = 'review-dismissed';\n label = 'dismissed a review';\n } else if (\n event === 'pull_request' &&\n [\n 'synchronize',\n 'ready_for_review',\n 'converted_to_draft',\n 'assigned',\n 'unassigned',\n 'review_requested',\n 'review_request_removed',\n ].includes(action)\n ) {\n priority = 'medium';\n kind = `pull-request-${action.replaceAll('_', '-')}`;\n label = action.replaceAll('_', ' ');\n } else if (\n event === 'pull_request' &&\n ['edited', 'labeled', 'unlabeled', 'milestoned', 'demilestoned'].includes(action)\n ) {\n priority = 'low';\n kind = `pull-request-${action.replaceAll('_', '-')}`;\n label = action.replaceAll('_', ' ');\n } else {\n return undefined;\n }\n\n return {\n action,\n kind,\n priority,\n summary: notificationSummary(metadata, label),\n terminal,\n metadata: {\n ...metadata,\n pullRequestNumber: metadata.pullRequestNumber,\n repositoryId: metadata.repositoryId,\n installationId: metadata.installationId,\n },\n payload,\n };\n}\n\nasync function resolveSubscriptionSession(\n controller: MountedMastraCode['controller'],\n subscription: GithubSignalSubscriptionRow,\n) {\n const { sessionId, resourceId, threadId } = subscription;\n if (!sessionId || !resourceId || !threadId) {\n throw new Error(`GitHub subscription ${subscription.id} is missing its session binding.`);\n }\n const scope = subscription.sessionScope || undefined;\n let session = await controller.getSessionByResource(resourceId, scope);\n if (!session) {\n const tags = {\n factoryProjectId: resourceId,\n projectRepositoryId: subscription.data.projectRepositoryId,\n ...(scope ? { worktreePath: scope } : {}),\n };\n session = await controller.createSession({\n id: sessionId,\n ownerId: subscription.data.ownerId,\n resourceId,\n scope,\n tags,\n });\n }\n if (session.thread.getId() !== threadId) {\n await session.thread.switch({ threadId, emitEvent: false });\n }\n if (session.thread.getId() !== threadId) {\n throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n }\n return session;\n}\n\nconst AUTHORIZED_BOTS = new Set(['coderabbitai[bot]', 'devin-ai-integration[bot]']);\nconst AUTHORIZED_PERMISSIONS = new Set(['admin', 'maintain', 'write']);\nconst PERMISSION_CHECK_TIMEOUT_MS = 5_000;\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment-created',\n 'review-comment-created',\n 'review-submitted',\n 'review-approved',\n 'review-changes-requested',\n 'review-dismissed',\n]);\n\nasync function isAuthorizedGithubSender(\n notification: GithubWebhookNotification,\n github: GithubIntegration | undefined,\n): Promise<boolean> {\n if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;\n const sender = notification.metadata.sender;\n const repository = notification.metadata.repository;\n if (!sender || !repository) return false;\n const normalizedSender = sender.toLowerCase();\n if (notification.metadata.senderType?.toLowerCase() === 'bot' || normalizedSender.endsWith('[bot]')) {\n return AUTHORIZED_BOTS.has(normalizedSender);\n }\n if (!github) return false;\n const abortController = new AbortController();\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n const permission = await Promise.race([\n github.getRepositoryCollaboratorPermission(\n notification.metadata.installationId,\n repository,\n sender,\n abortController.signal,\n ),\n new Promise<undefined>(resolve => {\n timeout = setTimeout(() => {\n abortController.abort();\n resolve(undefined);\n }, PERMISSION_CHECK_TIMEOUT_MS);\n }),\n ]);\n return permission !== undefined && AUTHORIZED_PERMISSIONS.has(permission);\n } catch {\n return false;\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nexport async function dispatchGithubWebhook(\n parsed: ParsedGithubWebhook,\n dependencies: GithubWebhookDispatchDependencies,\n): Promise<{ delivered: number; failed: number; ignored: boolean }> {\n const notification = classifyGithubWebhook(parsed);\n if (!notification) return { delivered: 0, failed: 0, ignored: true };\n const isAuthorizedSender =\n dependencies.isAuthorizedSender ??\n ((n: GithubWebhookNotification) => isAuthorizedGithubSender(n, dependencies.github));\n if (!(await isAuthorizedSender(notification))) {\n return { delivered: 0, failed: 0, ignored: true };\n }\n\n const target = {\n installationExternalId: notification.metadata.installationId.toString(),\n repositoryExternalId: notification.metadata.repositoryId.toString(),\n changeRequestId: notification.metadata.pullRequestNumber.toString(),\n };\n const listSubscriptions =\n dependencies.listSubscriptions ??\n ((subscriptionTarget: GithubWebhookPullRequestTarget, options?: { includeTerminal?: boolean }) => {\n if (!dependencies.github) throw new Error('GitHub integration is required to load webhook subscriptions.');\n return listPullRequestSubscriptionsForWebhook(\n subscriptionTarget,\n options,\n dependencies.github.integrationStorage,\n );\n });\n const retireSubscription =\n dependencies.retireSubscription ??\n ((id: string, status: 'open' | 'closed' | 'merged') => {\n if (!dependencies.github) throw new Error('GitHub integration is required to retire webhook subscriptions.');\n return retirePullRequestSubscription(id, status, dependencies.github.integrationStorage);\n });\n const subscriptions = await listSubscriptions(target, { includeTerminal: notification.action === 'reopened' });\n let delivered = 0;\n let failed = 0;\n\n for (const subscription of subscriptions) {\n try {\n const session = await resolveSubscriptionSession(dependencies.controller, subscription);\n const result = await session.sendNotificationSignal({\n source: 'github',\n kind: notification.kind,\n summary: notification.summary,\n priority: notification.priority,\n payload: notification.payload,\n sourceId: parsed.deliveryId,\n dedupeKey: `${parsed.deliveryId}:${subscription.sessionId}:${subscription.threadId}`,\n coalesceKey: `github:${subscription.data.repositoryExternalId}:pull-request:${subscription.data.changeRequestId}`,\n metadata: {\n event: notification.metadata.event,\n action: notification.action,\n repository: notification.metadata.repository,\n issueNumber: notification.metadata.issueNumber,\n pullRequestNumber: notification.metadata.pullRequestNumber,\n targetUrl: notificationTargetUrl(parsed.event, parsed.payload),\n deliveryId: parsed.deliveryId,\n },\n });\n await Promise.all([result.persisted, result.accepted].filter(Boolean));\n if (notification.terminal) {\n await retireSubscription(subscription.id, notification.kind === 'pull-request-merged' ? 'merged' : 'closed');\n } else if (notification.action === 'reopened') {\n await retireSubscription(subscription.id, 'open');\n }\n delivered += 1;\n } catch (error) {\n failed += 1;\n dependencies.onTargetError?.(subscription, error);\n }\n }\n\n return { delivered, failed, ignored: false };\n}\n\nexport async function handleGithubWebhook(\n c: Context,\n options: GithubWebhookHandlerOptions & Partial<Omit<GithubWebhookDispatchDependencies, 'github'>>,\n): Promise<GithubWebhookResult> {\n const parsed = await parseGithubWebhook(c, options.github.webhookSecret);\n if ('status' in parsed) return parsed;\n\n if (!SUPPORTED_GITHUB_WEBHOOK_EVENTS.has(parsed.event)) {\n return { status: 202, body: { ok: true, ignored: true } };\n }\n\n const metadata = normalizeGithubWebhookMetadata(parsed);\n console.info('[GitHub Webhook]', metadata);\n\n if (options.ingestFactoryEvent) {\n await options.ingestFactoryEvent(parsed);\n } else {\n const issueTriageRun = getIssueTriageRunInput(parsed);\n if (issueTriageRun && options.runIssueTriage) {\n void options.runIssueTriage(issueTriageRun).catch((error: unknown) => {\n console.error('[GitHub Webhook] Failed to run issue triage', {\n deliveryId: metadata.deliveryId,\n repository: metadata.repository,\n issueNumber: metadata.issueNumber,\n error: error instanceof Error ? error.message : String(error),\n });\n });\n }\n }\n\n if (!options.controller) {\n return { status: 202, body: { ok: true } };\n }\n\n const result = await dispatchGithubWebhook(parsed, options as GithubWebhookDispatchDependencies);\n if (result.failed > 0) {\n console.warn(`[GitHub Webhook] ${result.failed} subscribed target(s) failed for delivery ${parsed.deliveryId}.`);\n }\n return { status: 202, body: { ok: true, ...(result.ignored ? { ignored: true as const } : {}) } };\n}\n","export interface PlatformApiClientConfig {\n baseUrl: string;\n accessToken: string;\n fetchImpl?: typeof fetch;\n}\n\nexport function platformApiClientConfigFromEnv(): PlatformApiClientConfig {\n const sharedApiUrl = process.env.MASTRA_SHARED_API_URL?.trim() || 'https://platform.mastra.ai/v1';\n const accessToken = process.env.MASTRA_PLATFORM_SECRET_KEY?.trim();\n if (!accessToken) {\n throw new Error('Platform integration: missing required environment variable MASTRA_PLATFORM_SECRET_KEY.');\n }\n return { baseUrl: normalizeSharedApiUrl(sharedApiUrl), accessToken };\n}\n\nfunction normalizeSharedApiUrl(sharedApiUrl: string): string {\n return sharedApiUrl.replace(/\\/+$/, '').replace(/\\/v1$/, '');\n}\n\nexport class PlatformApiError extends Error {\n readonly status: number;\n readonly retryAfterSeconds: number | null;\n\n constructor(message: string, status: number, retryAfterSeconds: number | null = null) {\n super(message);\n this.name = 'PlatformApiError';\n this.status = status;\n this.retryAfterSeconds = retryAfterSeconds;\n }\n}\n\nexport class PlatformApiClient {\n readonly #baseUrl: string;\n readonly #accessToken: string;\n readonly #fetch: typeof fetch;\n\n constructor(config: PlatformApiClientConfig) {\n const missing = ['baseUrl', 'accessToken'].filter(field => !config[field as keyof PlatformApiClientConfig]);\n if (missing.length > 0) {\n throw new Error(`Platform integration: missing required config field(s): ${missing.join(', ')}.`);\n }\n this.#baseUrl = config.baseUrl.replace(/\\/+$/, '');\n this.#accessToken = config.accessToken;\n this.#fetch = config.fetchImpl ?? globalThis.fetch;\n }\n\n async request<T>(\n method: string,\n path: string,\n body?: unknown,\n options?: { signal?: AbortSignal; actingUserId?: string },\n ): Promise<T> {\n const response = await this.#send(method, path, body, options);\n if (!response.ok) {\n const message = redact(await extractError(response), this.#accessToken);\n const retryAfterSeconds = parseRetryAfter(response.headers.get('retry-after'));\n logPlatformError('Platform API request failed', {\n method,\n path,\n status: response.status,\n retryAfterSeconds,\n message,\n });\n throw new PlatformApiError(message, response.status, retryAfterSeconds);\n }\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n }\n\n async requestRedirect(method: string, path: string, options?: { signal?: AbortSignal }): Promise<string> {\n const response = await this.#send(method, path, undefined, options, 'manual');\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('location');\n if (location) return location;\n }\n if (!response.ok) {\n const message = redact(await extractError(response), this.#accessToken);\n const retryAfterSeconds = parseRetryAfter(response.headers.get('retry-after'));\n logPlatformError('Platform API redirect request failed', {\n method,\n path,\n status: response.status,\n retryAfterSeconds,\n message,\n });\n throw new PlatformApiError(message, response.status, retryAfterSeconds);\n }\n logPlatformError('Platform API request did not return a redirect', { method, path, status: response.status });\n throw new PlatformApiError('Platform API request did not return a redirect.', response.status);\n }\n\n async #send(\n method: string,\n path: string,\n body?: unknown,\n options?: { signal?: AbortSignal; actingUserId?: string },\n redirect?: RequestInit['redirect'],\n ): Promise<Response> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n authorization: `Bearer ${this.#accessToken}`,\n };\n // Acting end-user for platform GitHub writes: the platform resolves this\n // user's GitHub OAuth connection (org-scoped) so issues/PRs are authored\n // by the human instead of the App bot. Ignored by older platforms.\n if (options?.actingUserId) {\n headers['x-acting-user-id'] = options.actingUserId;\n }\n const timeoutSignal = AbortSignal.timeout(15_000);\n const init: RequestInit = {\n method,\n headers,\n redirect,\n signal: options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal,\n };\n if (body !== undefined) {\n headers['content-type'] = 'application/json';\n init.body = JSON.stringify(body);\n }\n\n try {\n return await this.#fetch(`${this.#baseUrl}${path}`, init);\n } catch (error) {\n if (error instanceof Error && error.message.includes(this.#accessToken)) {\n const redacted = new Error(redact(error.message, this.#accessToken));\n redacted.name = error.name;\n logPlatformError('Platform API transport error', {\n method,\n path,\n name: redacted.name,\n message: redacted.message,\n });\n throw redacted;\n }\n logPlatformError('Platform API transport error', {\n method,\n path,\n name: error instanceof Error ? error.name : undefined,\n message: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n }\n}\n\nasync function extractError(response: Response): Promise<string> {\n try {\n const data = (await response.clone().json()) as Record<string, unknown>;\n for (const field of ['detail', 'error', 'title']) {\n if (typeof data[field] === 'string' && data[field]) return data[field];\n }\n } catch {\n // Fall through to the status-based message.\n }\n return `Platform API request failed (${response.status})`;\n}\n\nfunction redact(message: string, accessToken: string): string {\n return message.split(accessToken).join('[REDACTED]');\n}\n\nexport function logPlatformInfo(message: string, fields?: Record<string, unknown>): void {\n writePlatformLog('info', message, fields);\n}\n\nexport function logPlatformWarn(message: string, fields?: Record<string, unknown>): void {\n writePlatformLog('warn', message, fields);\n}\n\nexport function logPlatformError(message: string, fields?: Record<string, unknown>): void {\n writePlatformLog('error', message, fields);\n}\n\nfunction writePlatformLog(level: 'info' | 'warn' | 'error', message: string, fields?: Record<string, unknown>): void {\n const metadata = fields ? ` ${JSON.stringify(stripUndefined(fields))}` : '';\n process.stderr.write(`[Mastra Factory] ${level.toUpperCase()} ${message}${metadata}\\n`);\n}\n\nfunction stripUndefined(fields: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));\n}\n\nfunction parseRetryAfter(value: string | null): number | null {\n if (!value) return null;\n const seconds = Number.parseInt(value, 10);\n return Number.isSafeInteger(seconds) && seconds >= 0 ? seconds : null;\n}\n"],"mappings":"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","names":["AUTHOR_GATED_KINDS","AUTHORIZED_BOTS","AUTHORIZED_PERMISSIONS","PERMISSION_CHECK_TIMEOUT_MS"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/github/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAKpD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/github/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAKpD,OAAO,KAAK,EAEV,MAAM,EAIP,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAuBV,cAAc,EACf,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+CAA+C,CAAC;AAC9F,OAAO,KAAK,EAEV,0BAA0B,EAC3B,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAO9G,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAQ/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AA4F9D,qBAAa,yBAA0B,YAAW,kBAAkB;;IAClE,QAAQ,CAAC,EAAE,YAAY;IAQvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAsHrB;IAEF,QAAQ,CAAC,cAAc,EAAE,cAAc,CAuErC;;IAUF,IAAI,OAAO,IAAI,0BAA0B,CAGxC;IAED,IAAI,oBAAoB,IAAI,0BAA0B,CAErD;IAED,IAAI,kBAAkB,IAAI,yBAAyB,CAKlD;IAuBD,UAAU,CAAC,EAAE,OAAO,EAAE,EAAE;QAAE,OAAO,EAAE,wBAAwB,CAAA;KAAE,GAAG,IAAI;IASpE,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IA8K3C,OAAO,CAAC,GAAG,EAAE,kBAAkB,GAAG,yBAAyB,EAAE;IAiB7D,YAAY,CAAC,EAAE,cAAc,EAAE,EAAE;QAAE,cAAc,EAAE,cAAc,CAAA;KAAE,GAAG,gBAAgB;IAIhF,gBAAgB,CAAC,EACrB,WAAW,EACX,cAAc,GACf,EAAE,UAAU,CAAC,WAAW,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrF,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAWhC,mCAAmC,CACvC,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,0BAA0B,GAAG,SAAS,CAAC;IAoB5C,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAcrE,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAa9D,cAAc,CAClB,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EAAE,GACf,OAAO,CAAC,MAAM,EAAE,CAAC;IASpB,sBAAsB,CAAC,eAAe,EAAE,MAAM,GAAG,UAAU,CAAC,iBAAiB,CAAC,wBAAwB,CAAC,CAAC;CA6WzG"}
|
|
@@ -96,6 +96,34 @@ async function clearGithubPat(storage, orgId, kind = "default") {
|
|
|
96
96
|
// src/integrations/github/project-lock.ts
|
|
97
97
|
import { createHash } from "crypto";
|
|
98
98
|
var inProcessLocks = /* @__PURE__ */ new Map();
|
|
99
|
+
var DEFAULT_PROJECT_LOCK_TIMEOUT_MS = 6e4;
|
|
100
|
+
var ProjectLockTimeoutError = class extends Error {
|
|
101
|
+
key;
|
|
102
|
+
timeoutMs;
|
|
103
|
+
constructor(key, timeoutMs) {
|
|
104
|
+
super(`Project lock critical section for "${key}" exceeded ${timeoutMs}ms`);
|
|
105
|
+
this.name = "ProjectLockTimeoutError";
|
|
106
|
+
this.key = key;
|
|
107
|
+
this.timeoutMs = timeoutMs;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
async function runWithTimeout(key, timeoutMs, fn) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
113
|
+
const abortError = new Promise((_, reject) => {
|
|
114
|
+
controller.signal.addEventListener("abort", () => reject(new ProjectLockTimeoutError(key, timeoutMs)), {
|
|
115
|
+
once: true
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
const work = fn(controller.signal);
|
|
119
|
+
work.catch(() => {
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
return await Promise.race([work, abortError]);
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
99
127
|
function hashKey(key) {
|
|
100
128
|
const digest = createHash("sha256").update(key).digest();
|
|
101
129
|
const a = digest.readInt32BE(0);
|
|
@@ -103,9 +131,9 @@ function hashKey(key) {
|
|
|
103
131
|
return [a, b];
|
|
104
132
|
}
|
|
105
133
|
function withProjectLock(options) {
|
|
106
|
-
const { key, storage, fn, pool } = options;
|
|
134
|
+
const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
|
|
107
135
|
const prev = inProcessLocks.get(key) ?? Promise.resolve();
|
|
108
|
-
const run = () => withDbAdvisoryLock({ key, storage, fn, pool });
|
|
136
|
+
const run = () => withDbAdvisoryLock({ key, storage, fn, pool, timeoutMs });
|
|
109
137
|
const next = prev.then(run, run);
|
|
110
138
|
const tail = next.then(
|
|
111
139
|
() => void 0,
|
|
@@ -120,28 +148,31 @@ function withProjectLock(options) {
|
|
|
120
148
|
return next;
|
|
121
149
|
}
|
|
122
150
|
async function withDbAdvisoryLock(options) {
|
|
123
|
-
const { key, storage, fn, pool } = options;
|
|
151
|
+
const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
|
|
124
152
|
if (process.env.MASTRACODE_DISTRIBUTED_LOCK === "0") {
|
|
125
|
-
return fn
|
|
153
|
+
return runWithTimeout(key, timeoutMs, fn);
|
|
126
154
|
}
|
|
127
|
-
if (pool) return advisoryLockOver(pool, key, fn);
|
|
155
|
+
if (pool) return advisoryLockOver(pool, key, timeoutMs, fn);
|
|
128
156
|
if (typeof storage?.withDistributedLock !== "function") {
|
|
129
|
-
return fn
|
|
157
|
+
return runWithTimeout(key, timeoutMs, fn);
|
|
130
158
|
}
|
|
131
|
-
return storage.withDistributedLock(key, fn);
|
|
159
|
+
return storage.withDistributedLock(key, () => runWithTimeout(key, timeoutMs, fn));
|
|
132
160
|
}
|
|
133
|
-
async function advisoryLockOver(pool, key, fn) {
|
|
161
|
+
async function advisoryLockOver(pool, key, timeoutMs, fn) {
|
|
134
162
|
const [k1, k2] = hashKey(key);
|
|
135
163
|
const client = await pool.connect();
|
|
136
164
|
try {
|
|
137
165
|
await client.query("BEGIN");
|
|
138
166
|
await client.query("SELECT pg_advisory_xact_lock($1, $2)", [k1, k2]);
|
|
139
167
|
try {
|
|
140
|
-
const result = await fn
|
|
168
|
+
const result = await runWithTimeout(key, timeoutMs, fn);
|
|
141
169
|
await client.query("COMMIT");
|
|
142
170
|
return result;
|
|
143
171
|
} catch (err) {
|
|
144
|
-
|
|
172
|
+
try {
|
|
173
|
+
await client.query("ROLLBACK");
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
145
176
|
throw err;
|
|
146
177
|
}
|
|
147
178
|
} finally {
|
|
@@ -2275,7 +2306,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
2275
2306
|
import { isLeaseProvider, NoopLeaseProvider } from "@mastra/core/events";
|
|
2276
2307
|
import { MastraWorker } from "@mastra/core/worker";
|
|
2277
2308
|
var API_PREFIX = "/v1/server/github-app";
|
|
2278
|
-
var DEFAULT_POLL_INTERVAL_MS =
|
|
2309
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e4;
|
|
2279
2310
|
var EVENT_PAGE_SIZE = 500;
|
|
2280
2311
|
var MIN_LEASE_TTL_MS = 3e4;
|
|
2281
2312
|
var CURSOR_ORG_ID = "__platform_github_event_worker__";
|
|
@@ -2465,10 +2496,16 @@ var PlatformGithubEventWorker = class extends MastraWorker {
|
|
|
2465
2496
|
const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE) });
|
|
2466
2497
|
if ("afterEventId" in cursor) query.set("afterEventId", cursor.afterEventId);
|
|
2467
2498
|
else query.set("afterTimestamp", String(cursor.afterTimestamp));
|
|
2499
|
+
const pollStartedAt = performance.now();
|
|
2468
2500
|
const page = await this.#client.request(
|
|
2469
2501
|
"GET",
|
|
2470
2502
|
`${API_PREFIX}/repositories/${repositoryId}/events?${query}`
|
|
2471
2503
|
);
|
|
2504
|
+
this.deps?.logger.info("Platform GitHub repository event poll completed", {
|
|
2505
|
+
repositoryId,
|
|
2506
|
+
eventCount: page.events.length,
|
|
2507
|
+
latencyMs: Math.round(performance.now() - pollStartedAt)
|
|
2508
|
+
});
|
|
2472
2509
|
if (page.events.length === 0 || !page.nextCursor) return;
|
|
2473
2510
|
for (const event of page.events) {
|
|
2474
2511
|
if (!this.#running || !this.#hasLease) return;
|
|
@@ -2591,6 +2628,7 @@ var PlatformGithubIntegration = class {
|
|
|
2591
2628
|
#storage;
|
|
2592
2629
|
#integrationStorage;
|
|
2593
2630
|
intake = {
|
|
2631
|
+
resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
|
|
2594
2632
|
listSources: async ({ orgId, userId }) => {
|
|
2595
2633
|
const installations = await this.#client.request("GET", `${API_PREFIX2}/github-app/installations`);
|
|
2596
2634
|
const usable = installations.installations.filter(
|
|
@@ -2689,7 +2727,8 @@ var PlatformGithubIntegration = class {
|
|
|
2689
2727
|
return this.#listIssues(input.connection, sourceId, parsePositiveCursor(input.cursor), input.labels);
|
|
2690
2728
|
},
|
|
2691
2729
|
getIssue: (input) => this.#getIssue(input.connection, input.sourceId, input.issueId),
|
|
2692
|
-
createComment: (input) => this.#createIssueComment(input)
|
|
2730
|
+
createComment: (input) => this.#createIssueComment(input),
|
|
2731
|
+
updateIssue: (input) => this.#updateIntakeIssue(input)
|
|
2693
2732
|
};
|
|
2694
2733
|
versionControl = {
|
|
2695
2734
|
initialize: ({ storage }) => {
|
|
@@ -2781,6 +2820,21 @@ var PlatformGithubIntegration = class {
|
|
|
2781
2820
|
}
|
|
2782
2821
|
return this.#integrationStorage;
|
|
2783
2822
|
}
|
|
2823
|
+
/** Resolve a stored GitHub locator without scanning installations or repositories. */
|
|
2824
|
+
async #resolveIntakeDispatch({
|
|
2825
|
+
orgId,
|
|
2826
|
+
externalSource
|
|
2827
|
+
}) {
|
|
2828
|
+
const target = parseGithubExternalTarget(externalSource.externalId);
|
|
2829
|
+
if (!target) return null;
|
|
2830
|
+
const repository = target.repository.includes("/") ? target.repository : (await this.storage.repositories.findByExternalId({ orgId, externalId: target.repository }))?.slug;
|
|
2831
|
+
if (!repository) return null;
|
|
2832
|
+
return {
|
|
2833
|
+
connection: { type: "app-installation", installationId: 1 },
|
|
2834
|
+
sourceId: repository,
|
|
2835
|
+
issueId: target.issueId
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2784
2838
|
initialize({ storage }) {
|
|
2785
2839
|
this.#integrationStorage = storage;
|
|
2786
2840
|
logPlatformInfo("Platform GitHub integration initialized", {
|
|
@@ -3079,6 +3133,39 @@ var PlatformGithubIntegration = class {
|
|
|
3079
3133
|
throw error;
|
|
3080
3134
|
}
|
|
3081
3135
|
}
|
|
3136
|
+
async #updateIntakeIssue(input) {
|
|
3137
|
+
requireGithubConnection(input.connection);
|
|
3138
|
+
const repository = requireSource(input.sourceId, "GitHub Intake requires a repository source.");
|
|
3139
|
+
const issueNumber = requirePositiveId(input.issueId, "issue");
|
|
3140
|
+
if (input.state.kind === "byName") {
|
|
3141
|
+
logPlatformWarn(`Platform GitHub: updateIssue byName is not supported (name=${input.state.name}); ignoring.`);
|
|
3142
|
+
return null;
|
|
3143
|
+
}
|
|
3144
|
+
const targetState = input.state.stateType === "unstarted" || input.state.stateType === "started" ? "open" : "closed";
|
|
3145
|
+
const stateReason = targetState === "closed" ? input.state.stateType === "canceled" ? "not_planned" : "completed" : null;
|
|
3146
|
+
try {
|
|
3147
|
+
await this.#client.request("GET", repositoryPath(repository, `pulls/${issueNumber}`));
|
|
3148
|
+
logPlatformWarn(`Platform GitHub: updateIssue rejected \u2014 target ${repository}#${issueNumber} is a pull request.`);
|
|
3149
|
+
return null;
|
|
3150
|
+
} catch (error) {
|
|
3151
|
+
if (!isNotFound(error)) throw error;
|
|
3152
|
+
}
|
|
3153
|
+
try {
|
|
3154
|
+
const issue = await this.#client.request(
|
|
3155
|
+
"PATCH",
|
|
3156
|
+
repositoryPath(repository, `issues/${issueNumber}`),
|
|
3157
|
+
{
|
|
3158
|
+
state: targetState,
|
|
3159
|
+
...stateReason ? { state_reason: stateReason } : {}
|
|
3160
|
+
},
|
|
3161
|
+
{ actingUserId: input.actingUserId }
|
|
3162
|
+
);
|
|
3163
|
+
return parseIntakeIssue(repository, issue);
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
if (isNotFound(error)) return null;
|
|
3166
|
+
throw error;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3082
3169
|
async #createIssueComment(input) {
|
|
3083
3170
|
requireGithubConnection(input.connection);
|
|
3084
3171
|
const repository = requireSource(input.sourceId, "GitHub Intake requires a repository source.");
|
|
@@ -3463,6 +3550,11 @@ function parsePositiveInteger(value) {
|
|
|
3463
3550
|
const parsed = Number(value);
|
|
3464
3551
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
3465
3552
|
}
|
|
3553
|
+
function parseGithubExternalTarget(externalId) {
|
|
3554
|
+
const match = externalId.match(/^(.+\/.+):(\d+)$/) ?? externalId.match(/^github:(\d+):(?:issue|pull-request):(\d+)$/) ?? externalId.match(/^(\d+):(\d+)$/);
|
|
3555
|
+
if (!match?.[1] || !match[2] || parsePositiveInteger(match[2]) === null) return null;
|
|
3556
|
+
return { repository: match[1], issueId: match[2] };
|
|
3557
|
+
}
|
|
3466
3558
|
function optionalPositiveIntegerEnv(name) {
|
|
3467
3559
|
const value = process.env[name]?.trim();
|
|
3468
3560
|
if (!value) return void 0;
|