@mastra/factory 0.24.0-alpha.11 → 0.24.0-alpha.13
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/README.md +4 -4
- package/dist/environment/build-runner.js +2 -1
- package/dist/environment/build-runner.js.map +1 -1
- package/dist/environment/build-workflow.js +1 -1
- package/dist/environment/build-workflow.js.map +1 -1
- package/dist/environment/build.js +16 -14
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +25 -1
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/app-identity.js.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +6 -3
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +3 -2
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/integration.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js +2 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/rules.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -1
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +2 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
- package/dist/integrations/platform/gitlab/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +2 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/connect-route.js +1 -1
- package/dist/integrations/slack/connect-route.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.js +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/integration.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.js +2 -1
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +18 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.js.map +1 -1
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/automation-runs.js.map +1 -1
- package/dist/routes/contracts.js +1 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +218 -64
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +2 -1
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.js +1 -1
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.js +3 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +32 -30
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/session-access.d.ts +55 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +165 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +5 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +87 -10
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +4 -2
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.js +4 -4
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +2 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.js +2 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/resource-migration.d.ts +18 -0
- package/dist/session/resource-migration.d.ts.map +1 -1
- package/dist/session/resource-migration.js +24 -1
- package/dist/session/resource-migration.js.map +1 -1
- package/dist/session/session-threads.d.ts +9 -0
- package/dist/session/session-threads.d.ts.map +1 -1
- package/dist/session/session-threads.js +24 -4
- package/dist/session/session-threads.js.map +1 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +1 -1
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +15 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +29 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.js +2 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/board-version.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/tools/messaging-tools.d.ts.map +1 -1
- package/dist/tools/messaging-tools.js +201 -73
- package/dist/tools/messaging-tools.js.map +1 -1
- package/dist/tools/run-watch.d.ts +2 -0
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js +42 -2
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +25 -10
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -2
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/workspace.d.ts +5 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +26 -13
- package/dist/workspace.js.map +1 -1
- package/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","names":["#heldElsewhere","#ingestIssue"],"sources":["../../../src/integrations/linear/rules.ts"],"sourcesContent":["import {\n boardEventHandlers,\n boardForWorkItem,\n eventBoardId,\n isTerminalWorkItem,\n rulePhaseField,\n withBoardClosure,\n} from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport { selectEventRule } from '../../rules/event-rules.js';\nimport type { FactoryLinearRuleContext, FactoryRuleDecision } from '../../rules/types.js';\nimport { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport { linearClaimKey } from './claim.js';\nimport type { LinearEventRules } from './default-rules.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nconst MAX_STALE_RULE_ATTEMPTS = 3;\n\nexport interface LinearIssueIngress {\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Linear user who created the issue, when available. */\n creator: string | null;\n team: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** Linear source the issue was read from (project or team); resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n projectId?: string | null;\n}\n\nexport interface LinearRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n linearRules: LinearEventRules;\n}\n\nexport interface LinearRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: LinearIssueIngress[];\n /** Board id per bound Linear source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class LinearRules {\n constructor(private readonly options: LinearRulesOptions) {}\n\n async ingest(input: LinearRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const itemsByClaimKey = new Map(items.filter(item => item.claimKey).map(item => [item.claimKey, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n // The stable issue id finds the card even after Linear renamed the\n // identifier; the identifier covers cards filed before claims existed.\n const relatedItem =\n itemsByClaimKey.get(linearClaimKey(issue.id)) ?? itemsBySourceKey.get(`linear:${issue.identifier}`);\n // One live card per Linear issue per org. When the winning source for an\n // issue moves to a source routed elsewhere (a project deselected under a\n // selected team, or the reverse), the card that already exists keeps the\n // issue; this Factory must not mint a second one. The store's claim index\n // is the guarantee; this check spares the dispatcher a refused upsert.\n if (!relatedItem && (await this.#heldElsewhere(input, issue))) {\n statuses.push('missing');\n continue;\n }\n statuses.push(await this.#ingestIssue(input, issue, relatedItem));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #heldElsewhere(input: LinearRulesIngress, issue: LinearIssueIngress): Promise<boolean> {\n const heldLive = (row: WorkItemRow) =>\n row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);\n const claimant = await this.options.storage.getByClaimKey({\n orgId: input.orgId,\n claimKey: linearClaimKey(issue.id),\n });\n if (claimant && heldLive(claimant)) return true;\n // Cards filed before claims existed carry only the identifier.\n const legacy = await this.options.storage.listBySource({\n orgId: input.orgId,\n source: { integrationId: 'linear', type: 'issue', externalId: `linear:${issue.identifier}` },\n });\n return legacy.some(heldLive);\n }\n\n async #ingestIssue(\n input: LinearRulesIngress,\n issue: LinearIssueIngress,\n relatedItem: WorkItemRow | undefined,\n attempt = 1,\n ): Promise<IngressStatus> {\n const ingressId = `linear:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryLinearRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'linear', id: ingressId },\n cause: `linear.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'linear-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n acceptedAt: relatedItem.acceptedAt,\n metadata: relatedItem.metadata,\n },\n board: boardForWorkItem(relatedItem),\n itemRevision: relatedItem.revision,\n ...withBoardClosure(this.options.boards, relatedItem),\n ...rulePhaseField(this.options.boards, relatedItem),\n }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = selectEventRule(\n this.options.linearRules,\n boardEventHandlers(this.options.boards, eventBoardId(context), 'linear'),\n context.event,\n );\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => {\n assertFactoryDecisionTarget(\n entry,\n this.options.boards,\n relatedItem ? boardForWorkItem(relatedItem) : undefined,\n );\n return { ...entry };\n });\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Linear rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'linear.issueObserved' },\n configVersion: this.options.configVersion,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n if (committed.status === 'stale') {\n // Nothing was persisted: re-read the item and evaluate again at its fresh revision.\n if (attempt >= MAX_STALE_RULE_ATTEMPTS) {\n throw new Error(`Factory Linear rule evaluation for ${ingressId} kept losing revision races.`);\n }\n const fresh = relatedItem ? await this.options.storage.get({ orgId: input.orgId, id: relatedItem.id }) : null;\n if (relatedItem && !fresh) return 'missing';\n return this.#ingestIssue(input, issue, fresh ?? undefined, attempt + 1);\n }\n return committed.status;\n }\n}\n\nexport function attachLinearRules(\n linear: { readonly rules: LinearEventRules },\n context: IntegrationContext,\n): ((input: LinearRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new LinearRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n linearRules: linear.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rules.js","names":[],"sources":["../../../src/integrations/linear/rules.ts"],"sourcesContent":["import {\n boardEventHandlers,\n boardForWorkItem,\n eventBoardId,\n isTerminalWorkItem,\n rulePhaseField,\n withBoardClosure,\n} from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport { selectEventRule } from '../../rules/event-rules.js';\nimport type { FactoryLinearRuleContext, FactoryRuleDecision } from '../../rules/types.js';\nimport { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport { linearClaimKey } from './claim.js';\nimport type { LinearEventRules } from './default-rules.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nconst MAX_STALE_RULE_ATTEMPTS = 3;\n\nexport interface LinearIssueIngress {\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Linear user who created the issue, when available. */\n creator: string | null;\n team: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** Linear source the issue was read from (project or team); resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n projectId?: string | null;\n}\n\nexport interface LinearRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n linearRules: LinearEventRules;\n}\n\nexport interface LinearRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: LinearIssueIngress[];\n /** Board id per bound Linear source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class LinearRules {\n constructor(private readonly options: LinearRulesOptions) {}\n\n async ingest(input: LinearRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const itemsByClaimKey = new Map(items.filter(item => item.claimKey).map(item => [item.claimKey, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n // The stable issue id finds the card even after Linear renamed the\n // identifier; the identifier covers cards filed before claims existed.\n const relatedItem =\n itemsByClaimKey.get(linearClaimKey(issue.id)) ?? itemsBySourceKey.get(`linear:${issue.identifier}`);\n // One live card per Linear issue per org. When the winning source for an\n // issue moves to a source routed elsewhere (a project deselected under a\n // selected team, or the reverse), the card that already exists keeps the\n // issue; this Factory must not mint a second one. The store's claim index\n // is the guarantee; this check spares the dispatcher a refused upsert.\n if (!relatedItem && (await this.#heldElsewhere(input, issue))) {\n statuses.push('missing');\n continue;\n }\n statuses.push(await this.#ingestIssue(input, issue, relatedItem));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #heldElsewhere(input: LinearRulesIngress, issue: LinearIssueIngress): Promise<boolean> {\n const heldLive = (row: WorkItemRow) =>\n row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);\n const claimant = await this.options.storage.getByClaimKey({\n orgId: input.orgId,\n claimKey: linearClaimKey(issue.id),\n });\n if (claimant && heldLive(claimant)) return true;\n // Cards filed before claims existed carry only the identifier.\n const legacy = await this.options.storage.listBySource({\n orgId: input.orgId,\n source: { integrationId: 'linear', type: 'issue', externalId: `linear:${issue.identifier}` },\n });\n return legacy.some(heldLive);\n }\n\n async #ingestIssue(\n input: LinearRulesIngress,\n issue: LinearIssueIngress,\n relatedItem: WorkItemRow | undefined,\n attempt = 1,\n ): Promise<IngressStatus> {\n const ingressId = `linear:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryLinearRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'linear', id: ingressId },\n cause: `linear.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'linear-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n acceptedAt: relatedItem.acceptedAt,\n metadata: relatedItem.metadata,\n },\n board: boardForWorkItem(relatedItem),\n itemRevision: relatedItem.revision,\n ...withBoardClosure(this.options.boards, relatedItem),\n ...rulePhaseField(this.options.boards, relatedItem),\n }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = selectEventRule(\n this.options.linearRules,\n boardEventHandlers(this.options.boards, eventBoardId(context), 'linear'),\n context.event,\n );\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => {\n assertFactoryDecisionTarget(\n entry,\n this.options.boards,\n relatedItem ? boardForWorkItem(relatedItem) : undefined,\n );\n return { ...entry };\n });\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Linear rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'linear.issueObserved' },\n configVersion: this.options.configVersion,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n if (committed.status === 'stale') {\n // Nothing was persisted: re-read the item and evaluate again at its fresh revision.\n if (attempt >= MAX_STALE_RULE_ATTEMPTS) {\n throw new Error(`Factory Linear rule evaluation for ${ingressId} kept losing revision races.`);\n }\n const fresh = relatedItem ? await this.options.storage.get({ orgId: input.orgId, id: relatedItem.id }) : null;\n if (relatedItem && !fresh) return 'missing';\n return this.#ingestIssue(input, issue, fresh ?? undefined, attempt + 1);\n }\n return committed.status;\n }\n}\n\nexport function attachLinearRules(\n linear: { readonly rules: LinearEventRules },\n context: IntegrationContext,\n): ((input: LinearRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new LinearRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n linearRules: linear.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.js","names":["#baseUrl","#accessToken","#projectId","#fetch","#send"],"sources":["../../../src/integrations/platform/api-client.ts"],"sourcesContent":["export interface PlatformApiClientConfig {\n baseUrl: string;\n accessToken: string;\n /**\n * Mastra project this deployment belongs to. Sent as `x-mastra-project-id`\n * so the platform can attribute proxied provider activity to the project;\n * the platform strips the header before forwarding anything upstream.\n */\n projectId?: string;\n fetchImpl?: typeof fetch;\n}\n\nconst DEFAULT_INTEGRATIONS_URL = 'https://integrations.mastra.ai';\nconst REGIONAL_INTEGRATIONS_URLS: Record<'us' | 'eu', string> = {\n us: 'https://integrations.us.mastra.ai',\n eu: 'https://integrations.eu.mastra.ai',\n};\n\nexport function platformApiClientConfigFromEnv(): PlatformApiClientConfig {\n // MASTRA_INTEGRATIONS_API_URL is the dedicated override for the\n // integrations service and takes precedence over MASTRA_PLATFORM_REGION.\n // MASTRA_SHARED_API_URL is deliberately not consulted: it configures the\n // shared platform API, and integrations routing is independent of it.\n const integrationsApiUrl = process.env.MASTRA_INTEGRATIONS_API_URL?.trim() || resolveIntegrationsUrl();\n // MASTRA_PLATFORM_ACCESS_TOKEN is the credential Mastra Platform injects\n // into deployed projects; MASTRA_PLATFORM_SECRET_KEY is the org secret key\n // written by project scaffolding. The platform API accepts both forms.\n const accessToken =\n process.env.MASTRA_PLATFORM_ACCESS_TOKEN?.trim() || process.env.MASTRA_PLATFORM_SECRET_KEY?.trim();\n if (!accessToken) {\n throw new Error(\n 'Platform integration: missing required environment variable MASTRA_PLATFORM_ACCESS_TOKEN (or MASTRA_PLATFORM_SECRET_KEY).',\n );\n }\n const projectId = process.env.MASTRA_PROJECT_ID?.trim();\n return {\n baseUrl: normalizeIntegrationsApiUrl(integrationsApiUrl),\n accessToken,\n ...(projectId ? { projectId } : {}),\n };\n}\n\n/**\n * Resolves the integrations API base URL from `MASTRA_PLATFORM_REGION`\n * (case-insensitive `us` or `eu`), falling back to the global default.\n * Unknown region values fall through to the global default.\n */\nfunction resolveIntegrationsUrl(): string {\n const region = process.env.MASTRA_PLATFORM_REGION?.trim().toLowerCase();\n if (region === 'us' || region === 'eu') return REGIONAL_INTEGRATIONS_URLS[region];\n return DEFAULT_INTEGRATIONS_URL;\n}\n\n/**\n * Normalizes an integrations API URL to a bare origin. Callers pass fully\n * versioned paths (`/v1/server/...`, `/v2/...`), so a trailing `/v1` — as in\n * legacy platform API URLs like `https://platform.mastra.ai/v1` — is stripped\n * to avoid duplicated version segments.\n */\nfunction normalizeIntegrationsApiUrl(integrationsApiUrl: string): string {\n return integrationsApiUrl.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 #projectId: string | null;\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.#projectId = config.projectId?.trim() || null;\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 // Project attribution for proxied provider calls. Org-scoped machine\n // tokens carry no project claim, so without this header the platform\n // records proxy activity with no project.\n if (this.#projectId) {\n headers['x-mastra-project-id'] = this.#projectId;\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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"}
|
|
1
|
+
{"version":3,"file":"api-client.js","names":[],"sources":["../../../src/integrations/platform/api-client.ts"],"sourcesContent":["export interface PlatformApiClientConfig {\n baseUrl: string;\n accessToken: string;\n /**\n * Mastra project this deployment belongs to. Sent as `x-mastra-project-id`\n * so the platform can attribute proxied provider activity to the project;\n * the platform strips the header before forwarding anything upstream.\n */\n projectId?: string;\n fetchImpl?: typeof fetch;\n}\n\nconst DEFAULT_INTEGRATIONS_URL = 'https://integrations.mastra.ai';\nconst REGIONAL_INTEGRATIONS_URLS: Record<'us' | 'eu', string> = {\n us: 'https://integrations.us.mastra.ai',\n eu: 'https://integrations.eu.mastra.ai',\n};\n\nexport function platformApiClientConfigFromEnv(): PlatformApiClientConfig {\n // MASTRA_INTEGRATIONS_API_URL is the dedicated override for the\n // integrations service and takes precedence over MASTRA_PLATFORM_REGION.\n // MASTRA_SHARED_API_URL is deliberately not consulted: it configures the\n // shared platform API, and integrations routing is independent of it.\n const integrationsApiUrl = process.env.MASTRA_INTEGRATIONS_API_URL?.trim() || resolveIntegrationsUrl();\n // MASTRA_PLATFORM_ACCESS_TOKEN is the credential Mastra Platform injects\n // into deployed projects; MASTRA_PLATFORM_SECRET_KEY is the org secret key\n // written by project scaffolding. The platform API accepts both forms.\n const accessToken =\n process.env.MASTRA_PLATFORM_ACCESS_TOKEN?.trim() || process.env.MASTRA_PLATFORM_SECRET_KEY?.trim();\n if (!accessToken) {\n throw new Error(\n 'Platform integration: missing required environment variable MASTRA_PLATFORM_ACCESS_TOKEN (or MASTRA_PLATFORM_SECRET_KEY).',\n );\n }\n const projectId = process.env.MASTRA_PROJECT_ID?.trim();\n return {\n baseUrl: normalizeIntegrationsApiUrl(integrationsApiUrl),\n accessToken,\n ...(projectId ? { projectId } : {}),\n };\n}\n\n/**\n * Resolves the integrations API base URL from `MASTRA_PLATFORM_REGION`\n * (case-insensitive `us` or `eu`), falling back to the global default.\n * Unknown region values fall through to the global default.\n */\nfunction resolveIntegrationsUrl(): string {\n const region = process.env.MASTRA_PLATFORM_REGION?.trim().toLowerCase();\n if (region === 'us' || region === 'eu') return REGIONAL_INTEGRATIONS_URLS[region];\n return DEFAULT_INTEGRATIONS_URL;\n}\n\n/**\n * Normalizes an integrations API URL to a bare origin. Callers pass fully\n * versioned paths (`/v1/server/...`, `/v2/...`), so a trailing `/v1` — as in\n * legacy platform API URLs like `https://platform.mastra.ai/v1` — is stripped\n * to avoid duplicated version segments.\n */\nfunction normalizeIntegrationsApiUrl(integrationsApiUrl: string): string {\n return integrationsApiUrl.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 #projectId: string | null;\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.#projectId = config.projectId?.trim() || null;\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 // Project attribution for proxied provider calls. Org-scoped machine\n // tokens carry no project claim, so without this header the platform\n // records proxy activity with no project.\n if (this.#projectId) {\n headers['x-mastra-project-id'] = this.#projectId;\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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"}
|
|
@@ -6,7 +6,7 @@ import { MastraWorker } from "@mastra/core/worker";
|
|
|
6
6
|
//#region src/integrations/platform/github/event-worker.ts
|
|
7
7
|
const API_PREFIX = "/v1/server/github-app";
|
|
8
8
|
const DEFAULT_POLL_INTERVAL_MS = 2e4;
|
|
9
|
-
const DEFAULT_RECONCILE_INTERVAL_MS =
|
|
9
|
+
const DEFAULT_RECONCILE_INTERVAL_MS = 36e5;
|
|
10
10
|
const EVENT_PAGE_SIZE = 500;
|
|
11
11
|
const MIN_LEASE_TTL_MS = 3e4;
|
|
12
12
|
const CURSOR_ORG_ID = "__platform_github_event_worker__";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-worker.js","names":["#client","#controller","#github","#storage","#ingestFactoryEvent","#reconcileFactoryState","#reconcileIssuesFactoryState","#pollEventsEnabled","#pullRequestReconcileIntervalMs","#issueReconcileIntervalMs","#intervalMs","#now","#dispatch","#sourceControl","#leaseOwner","#leaseTtlMs","#leaseProvider","#running","#startedAt","#settings","#schedule","#timer","#stopLeaseRenewal","#inFlight","#hasLease","#leaseKey","#tick","#ensureLease","#poll","#startLeaseRenewal","#leaseRenewalTimer","#discoverRepositories","#reconcileCadence","#pollRepository","#maybeReconcile","#lastPullRequestReconcileAt","#lastIssueReconcileAt","#saveSettings","#isAuthorizedSender"],"sources":["../../../../src/integrations/platform/github/event-worker.ts"],"sourcesContent":["import 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 type { GithubIssueReconciler } from '../../github/issue-reconciler.js';\nimport type { GithubReconcileRepositorySource } from '../../github/reconcile-worker.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from '../../github/rules.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from '../../github/subscriptions.js';\nimport { dispatchGithubWebhook, isFactoryAppSender, resolveAuthorizedBots } from '../../github/webhook.js';\nimport type {\n GithubWebhookDispatchIntegration,\n GithubWebhookNotification,\n ParsedGithubWebhook,\n} 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;\n// Event polling is the primary sync; the sweeps only catch drift (missed\n// events, cards ingested before polling was linked), so they run hourly.\nconst DEFAULT_RECONCILE_INTERVAL_MS = 60 * 60_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 // Pushes never reach the subscription dispatcher; they feed the factory\n // ingest path so base checkpoints rebuild on default-branch updates.\n 'push',\n]);\n// Kinds emitted by `classifyGithubWebhook` that carry untrusted sender-authored\n// content and must pass the sender authorization gate below.\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment-created',\n 'review-comment-created',\n 'review-approved',\n 'review-changes-requested',\n 'review-submitted',\n 'review-dismissed',\n]);\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; fullName?: string; installationId: number };\n\nexport type PlatformGithubEventDispatchIntegration = GithubWebhookDispatchIntegration;\n\nexport interface PlatformGithubEventWorkerConfig {\n client: PlatformApiClient;\n controller: MountedMastraCode['controller'];\n github: PlatformGithubEventDispatchIntegration;\n storage: PlatformGithubEventStorage;\n /**\n * Source-control storage used to resolve the set of repositories the worker\n * should poll. The worker restricts itself to `(installation, repository)`\n * pairs linked to a factory project — the same set the reconciler uses —\n * so polling scales with Factory usage, not with the size of the underlying\n * GitHub org.\n */\n sourceControl: GithubReconcileRepositorySource;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n reconcileFactoryState?: GithubPullRequestReconciler;\n reconcileIssuesFactoryState?: GithubIssueReconciler;\n /** When false the worker skips event tailing and only runs enabled reconciliation sweeps. */\n pollEventsEnabled?: boolean;\n intervalMs?: number;\n /** Legacy shared reconciliation cadence. */\n reconcileIntervalMs?: number;\n pullRequestReconcileIntervalMs?: number;\n issueReconcileIntervalMs?: 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 #reconcileFactoryState: GithubPullRequestReconciler | undefined;\n readonly #reconcileIssuesFactoryState: GithubIssueReconciler | undefined;\n readonly #pollEventsEnabled: boolean;\n readonly #pullRequestReconcileIntervalMs: number;\n readonly #issueReconcileIntervalMs: number;\n readonly #intervalMs: number;\n readonly #now: () => number;\n readonly #dispatch: typeof dispatchGithubWebhook;\n readonly #sourceControl: GithubReconcileRepositorySource;\n readonly #leaseOwner = globalThis.crypto.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 // Negative infinity so the first tick always sweeps, whatever the clock reads.\n #lastPullRequestReconcileAt = Number.NEGATIVE_INFINITY;\n #lastIssueReconcileAt = Number.NEGATIVE_INFINITY;\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.#reconcileFactoryState = config.reconcileFactoryState;\n this.#reconcileIssuesFactoryState = config.reconcileIssuesFactoryState;\n this.#pollEventsEnabled = config.pollEventsEnabled ?? true;\n const legacyReconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;\n this.#pullRequestReconcileIntervalMs = config.pullRequestReconcileIntervalMs ?? legacyReconcileIntervalMs;\n this.#issueReconcileIntervalMs = config.issueReconcileIntervalMs ?? legacyReconcileIntervalMs;\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 if (!Number.isFinite(this.#pullRequestReconcileIntervalMs) || this.#pullRequestReconcileIntervalMs <= 0) {\n throw new Error('Platform GitHub pull request reconcile interval must be a positive number.');\n }\n if (!Number.isFinite(this.#issueReconcileIntervalMs) || this.#issueReconcileIntervalMs <= 0) {\n throw new Error('Platform GitHub issue reconcile interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(\n MIN_LEASE_TTL_MS,\n Math.min(this.#intervalMs, this.#pullRequestReconcileIntervalMs, this.#issueReconcileIntervalMs) * 3,\n );\n this.#now = config.now ?? Date.now;\n this.#dispatch = config.dispatch ?? dispatchGithubWebhook;\n this.#sourceControl = config.sourceControl;\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.#pollEventsEnabled ? this.#intervalMs : this.#reconcileCadence();\n\n if (this.#pollEventsEnabled) {\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\n await this.#maybeReconcile(repositories);\n return retryInMs;\n }\n\n #reconcileCadence(): number {\n const cadences = [\n this.#reconcileFactoryState ? this.#pullRequestReconcileIntervalMs : undefined,\n this.#reconcileIssuesFactoryState ? this.#issueReconcileIntervalMs : undefined,\n ].filter((interval): interval is number => interval !== undefined);\n return cadences.length > 0 ? Math.min(...cadences) : this.#intervalMs;\n }\n\n async #maybeReconcile(repositories: Repository[]): Promise<void> {\n if (!this.#running || !this.#hasLease) return;\n const now = this.#now();\n const reconcilePullRequests = Boolean(\n this.#reconcileFactoryState && now - this.#lastPullRequestReconcileAt >= this.#pullRequestReconcileIntervalMs,\n );\n const reconcileIssues = Boolean(\n this.#reconcileIssuesFactoryState && now - this.#lastIssueReconcileAt >= this.#issueReconcileIntervalMs,\n );\n if (!reconcilePullRequests && !reconcileIssues) return;\n if (reconcilePullRequests) this.#lastPullRequestReconcileAt = now;\n if (reconcileIssues) this.#lastIssueReconcileAt = now;\n\n const targets: ReconcileRepository[] = repositories.flatMap(repository =>\n repository.fullName\n ? [{ id: repository.id, fullName: repository.fullName, installationId: repository.installationId }]\n : [],\n );\n if (targets.length === 0) {\n this.deps?.logger.debug('Platform GitHub reconciliation skipped: no named repositories');\n return;\n }\n\n if (reconcilePullRequests && this.#reconcileFactoryState) {\n const startedAt = Date.now();\n try {\n const { errors, ...counts } = await this.#reconcileFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub pull request reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('Platform GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.info('Platform GitHub pull request reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub pull request reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n if (reconcileIssues && this.#reconcileIssuesFactoryState && this.#hasLease) {\n const startedAt = Date.now();\n try {\n const { errors, ...counts } = await this.#reconcileIssuesFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub issue reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.created > 0) {\n this.deps?.logger.info('Platform GitHub issue reconcile replayed missed opens', context);\n } else if (counts.closed > 0) {\n this.deps?.logger.info('Platform GitHub issue reconcile replayed closed work items', context);\n } else if (counts.updated > 0) {\n this.deps?.logger.info('Platform GitHub issue reconcile patched stale metadata', context);\n } else {\n this.deps?.logger.debug('Platform GitHub issue reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub issue reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n /**\n * Configured `(installation, repository)` pairs — the same set the\n * reconciler sweeps — resolved to the numeric ids and slug the poll\n * addresses GitHub with. Repositories not linked to any factory project\n * cannot produce work for Factory, so polling and reconciling them is\n * wasted `/events` bandwidth and platform load.\n */\n async #discoverRepositories(): Promise<Repository[]> {\n const keys = await this.#sourceControl.projectRepositories.listConfiguredExternalKeys();\n const repositories = new Map<number, Repository>();\n for (const key of keys) {\n const installationId = Number(key.installationExternalId);\n const repositoryId = Number(key.repositoryExternalId);\n if (\n !Number.isSafeInteger(installationId) ||\n installationId <= 0 ||\n !Number.isSafeInteger(repositoryId) ||\n repositoryId <= 0\n ) {\n continue;\n }\n if (repositories.has(repositoryId)) continue;\n // A configured key exists per project link, so `listByExternalRepository`\n // always yields at least one row; the first row's orgId is enough to\n // look up the repository row for its slug.\n const projects = await this.#sourceControl.projectRepositories.listByExternalRepository(key);\n const orgId = projects[0]?.orgId;\n const repository = orgId\n ? await this.#sourceControl.repositories.findByExternalId({ orgId, externalId: key.repositoryExternalId })\n : null;\n repositories.set(repositoryId, {\n id: repositoryId,\n installationId,\n fullName: repository?.slug,\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.debug('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 (isFactoryIngestedEvent(parsed)) {\n await this.#ingestFactoryEvent?.(parsed);\n } else {\n this.deps?.logger.debug('Platform GitHub event not forwarded to Factory ingress', {\n repositoryId,\n deliveryId: parsed.deliveryId,\n event: parsed.event,\n action: parsed.payload.action,\n });\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 github: this.#github,\n isAuthorizedSender: notification => this.#isAuthorizedSender(notification),\n onTargetSkipped: subscription => {\n // Routine when a subscription's thread belongs to another\n // deployment, so this stays at debug rather than warning on a loop.\n this.deps?.logger.debug('Platform GitHub event skipped: thread is not held here', {\n deliveryId: event.deliveryId,\n subscriptionId: subscription.id,\n threadId: subscription.threadId,\n });\n },\n onSenderRejected: notification => {\n this.deps?.logger.debug('Platform GitHub event dropped: sender not authorized', {\n deliveryId: event.deliveryId,\n repository: notification.metadata.repository,\n sender: notification.metadata.sender,\n kind: notification.kind,\n });\n },\n onTargetError: (subscription, error) => {\n this.deps?.logger.error('Platform GitHub event delivery failed for a subscription', {\n deliveryId: event.deliveryId,\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 this.deps?.logger.warn('Platform GitHub event completed with failed subscription deliveries', {\n repositoryId,\n deliveryId: event.deliveryId,\n delivered: result.delivered,\n failed: result.failed,\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 // Factory's own app has to clear the gate before the bot rules below, which\n // fail closed for every bot that is not explicitly allowlisted. GitHub\n // forbids an app from reviewing its own pull request, so `factory-review`\n // posts its verdict as a comment under this login and that comment is the\n // handoff the authoring agent wakes on.\n if (this.#github.identity?.matches(sender)) return true;\n if (isFactoryAppSender(sender, this.#github.slug)) return true;\n const normalizedSender = sender.toLowerCase();\n const authorizedBots = resolveAuthorizedBots(this.#github.authorizedBots);\n if (authorizedBots.has(normalizedSender)) return true;\n // Any other bot is gated purely by the allowlist: a GitHub App never holds a\n // collaborator permission under its sender login, so falling through to the\n // permission lookup would only fail closed after a wasted API call.\n if (notification.metadata.senderType?.toLowerCase() === 'bot' || normalizedSender.endsWith('[bot]')) {\n return false;\n }\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\n// Events the polling worker forwards to the factory rules engine. Closures\n// let the reconciler finalize cards; `synchronize` and `review_requested` on a\n// pull request are the triggers the review board's re-review path listens for;\n// submitted reviews and pull request comments are how review feedback reaches\n// the agent that authored the branch. Direct-webhook consumers ingest every\n// parsed event; the platform path gates because the remaining events (issue\n// edits, comment edits and deletions) only interest the subscription\n// dispatcher, not the factory rules.\nconst ISSUE_INGESTED_ACTIONS = new Set(['opened', 'reopened', 'edited', 'labeled', 'unlabeled']);\n\nfunction isFactoryIngestedEvent(event: ParsedGithubWebhook): boolean {\n if ((event.event === 'issues' || event.event === 'pull_request') && event.payload.action === 'closed') {\n return true;\n }\n if (event.event === 'pull_request') {\n const action = event.payload.action;\n // `opened` is what mints the Review card for a pull request. Without it a\n // factory-authored PR never gets reviewed on the polling path, which is the\n // only path a local deployment has.\n if (action === 'opened' || action === 'synchronize' || action === 'review_requested') return true;\n }\n // Issue opens, reopens, edits and label changes mint and update Work cards.\n // Without them a new issue never becomes a card on the polling path (#24972).\n if (event.event === 'issues' && ISSUE_INGESTED_ACTIONS.has(String(event.payload.action))) return true;\n if (event.event === 'pull_request_review' && event.payload.action === 'submitted') return true;\n if (event.event === 'issue_comment' && event.payload.action === 'created') return true;\n // Default-branch pushes drive base-checkpoint rebuilds\n // (`withBaseCheckpointWebhookTrigger` wraps the ingest callback).\n if (event.event === 'push') return true;\n return false;\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"event-worker.js","names":[],"sources":["../../../../src/integrations/platform/github/event-worker.ts"],"sourcesContent":["import 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 type { GithubIssueReconciler } from '../../github/issue-reconciler.js';\nimport type { GithubReconcileRepositorySource } from '../../github/reconcile-worker.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from '../../github/rules.js';\nimport { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from '../../github/subscriptions.js';\nimport { dispatchGithubWebhook, isFactoryAppSender, resolveAuthorizedBots } from '../../github/webhook.js';\nimport type {\n GithubWebhookDispatchIntegration,\n GithubWebhookNotification,\n ParsedGithubWebhook,\n} 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;\n// Event polling is the primary sync; the sweeps only catch drift (missed\n// events, cards ingested before polling was linked), so they run hourly.\nconst DEFAULT_RECONCILE_INTERVAL_MS = 60 * 60_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 // Pushes never reach the subscription dispatcher; they feed the factory\n // ingest path so base checkpoints rebuild on default-branch updates.\n 'push',\n]);\n// Kinds emitted by `classifyGithubWebhook` that carry untrusted sender-authored\n// content and must pass the sender authorization gate below.\nconst AUTHOR_GATED_KINDS = new Set([\n 'issue-comment-created',\n 'review-comment-created',\n 'review-approved',\n 'review-changes-requested',\n 'review-submitted',\n 'review-dismissed',\n]);\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; fullName?: string; installationId: number };\n\nexport type PlatformGithubEventDispatchIntegration = GithubWebhookDispatchIntegration;\n\nexport interface PlatformGithubEventWorkerConfig {\n client: PlatformApiClient;\n controller: MountedMastraCode['controller'];\n github: PlatformGithubEventDispatchIntegration;\n storage: PlatformGithubEventStorage;\n /**\n * Source-control storage used to resolve the set of repositories the worker\n * should poll. The worker restricts itself to `(installation, repository)`\n * pairs linked to a factory project — the same set the reconciler uses —\n * so polling scales with Factory usage, not with the size of the underlying\n * GitHub org.\n */\n sourceControl: GithubReconcileRepositorySource;\n ingestFactoryEvent?: (event: ParsedGithubWebhook) => Promise<unknown>;\n reconcileFactoryState?: GithubPullRequestReconciler;\n reconcileIssuesFactoryState?: GithubIssueReconciler;\n /** When false the worker skips event tailing and only runs enabled reconciliation sweeps. */\n pollEventsEnabled?: boolean;\n intervalMs?: number;\n /** Legacy shared reconciliation cadence. */\n reconcileIntervalMs?: number;\n pullRequestReconcileIntervalMs?: number;\n issueReconcileIntervalMs?: 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 #reconcileFactoryState: GithubPullRequestReconciler | undefined;\n readonly #reconcileIssuesFactoryState: GithubIssueReconciler | undefined;\n readonly #pollEventsEnabled: boolean;\n readonly #pullRequestReconcileIntervalMs: number;\n readonly #issueReconcileIntervalMs: number;\n readonly #intervalMs: number;\n readonly #now: () => number;\n readonly #dispatch: typeof dispatchGithubWebhook;\n readonly #sourceControl: GithubReconcileRepositorySource;\n readonly #leaseOwner = globalThis.crypto.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 // Negative infinity so the first tick always sweeps, whatever the clock reads.\n #lastPullRequestReconcileAt = Number.NEGATIVE_INFINITY;\n #lastIssueReconcileAt = Number.NEGATIVE_INFINITY;\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.#reconcileFactoryState = config.reconcileFactoryState;\n this.#reconcileIssuesFactoryState = config.reconcileIssuesFactoryState;\n this.#pollEventsEnabled = config.pollEventsEnabled ?? true;\n const legacyReconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;\n this.#pullRequestReconcileIntervalMs = config.pullRequestReconcileIntervalMs ?? legacyReconcileIntervalMs;\n this.#issueReconcileIntervalMs = config.issueReconcileIntervalMs ?? legacyReconcileIntervalMs;\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 if (!Number.isFinite(this.#pullRequestReconcileIntervalMs) || this.#pullRequestReconcileIntervalMs <= 0) {\n throw new Error('Platform GitHub pull request reconcile interval must be a positive number.');\n }\n if (!Number.isFinite(this.#issueReconcileIntervalMs) || this.#issueReconcileIntervalMs <= 0) {\n throw new Error('Platform GitHub issue reconcile interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(\n MIN_LEASE_TTL_MS,\n Math.min(this.#intervalMs, this.#pullRequestReconcileIntervalMs, this.#issueReconcileIntervalMs) * 3,\n );\n this.#now = config.now ?? Date.now;\n this.#dispatch = config.dispatch ?? dispatchGithubWebhook;\n this.#sourceControl = config.sourceControl;\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.#pollEventsEnabled ? this.#intervalMs : this.#reconcileCadence();\n\n if (this.#pollEventsEnabled) {\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\n await this.#maybeReconcile(repositories);\n return retryInMs;\n }\n\n #reconcileCadence(): number {\n const cadences = [\n this.#reconcileFactoryState ? this.#pullRequestReconcileIntervalMs : undefined,\n this.#reconcileIssuesFactoryState ? this.#issueReconcileIntervalMs : undefined,\n ].filter((interval): interval is number => interval !== undefined);\n return cadences.length > 0 ? Math.min(...cadences) : this.#intervalMs;\n }\n\n async #maybeReconcile(repositories: Repository[]): Promise<void> {\n if (!this.#running || !this.#hasLease) return;\n const now = this.#now();\n const reconcilePullRequests = Boolean(\n this.#reconcileFactoryState && now - this.#lastPullRequestReconcileAt >= this.#pullRequestReconcileIntervalMs,\n );\n const reconcileIssues = Boolean(\n this.#reconcileIssuesFactoryState && now - this.#lastIssueReconcileAt >= this.#issueReconcileIntervalMs,\n );\n if (!reconcilePullRequests && !reconcileIssues) return;\n if (reconcilePullRequests) this.#lastPullRequestReconcileAt = now;\n if (reconcileIssues) this.#lastIssueReconcileAt = now;\n\n const targets: ReconcileRepository[] = repositories.flatMap(repository =>\n repository.fullName\n ? [{ id: repository.id, fullName: repository.fullName, installationId: repository.installationId }]\n : [],\n );\n if (targets.length === 0) {\n this.deps?.logger.debug('Platform GitHub reconciliation skipped: no named repositories');\n return;\n }\n\n if (reconcilePullRequests && this.#reconcileFactoryState) {\n const startedAt = Date.now();\n try {\n const { errors, ...counts } = await this.#reconcileFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub pull request reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('Platform GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.info('Platform GitHub pull request reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub pull request reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n if (reconcileIssues && this.#reconcileIssuesFactoryState && this.#hasLease) {\n const startedAt = Date.now();\n try {\n const { errors, ...counts } = await this.#reconcileIssuesFactoryState(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform GitHub issue reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.created > 0) {\n this.deps?.logger.info('Platform GitHub issue reconcile replayed missed opens', context);\n } else if (counts.closed > 0) {\n this.deps?.logger.info('Platform GitHub issue reconcile replayed closed work items', context);\n } else if (counts.updated > 0) {\n this.deps?.logger.info('Platform GitHub issue reconcile patched stale metadata', context);\n } else {\n this.deps?.logger.debug('Platform GitHub issue reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform GitHub issue reconcile failed', {\n repositories: targets.length,\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n /**\n * Configured `(installation, repository)` pairs — the same set the\n * reconciler sweeps — resolved to the numeric ids and slug the poll\n * addresses GitHub with. Repositories not linked to any factory project\n * cannot produce work for Factory, so polling and reconciling them is\n * wasted `/events` bandwidth and platform load.\n */\n async #discoverRepositories(): Promise<Repository[]> {\n const keys = await this.#sourceControl.projectRepositories.listConfiguredExternalKeys();\n const repositories = new Map<number, Repository>();\n for (const key of keys) {\n const installationId = Number(key.installationExternalId);\n const repositoryId = Number(key.repositoryExternalId);\n if (\n !Number.isSafeInteger(installationId) ||\n installationId <= 0 ||\n !Number.isSafeInteger(repositoryId) ||\n repositoryId <= 0\n ) {\n continue;\n }\n if (repositories.has(repositoryId)) continue;\n // A configured key exists per project link, so `listByExternalRepository`\n // always yields at least one row; the first row's orgId is enough to\n // look up the repository row for its slug.\n const projects = await this.#sourceControl.projectRepositories.listByExternalRepository(key);\n const orgId = projects[0]?.orgId;\n const repository = orgId\n ? await this.#sourceControl.repositories.findByExternalId({ orgId, externalId: key.repositoryExternalId })\n : null;\n repositories.set(repositoryId, {\n id: repositoryId,\n installationId,\n fullName: repository?.slug,\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.debug('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 (isFactoryIngestedEvent(parsed)) {\n await this.#ingestFactoryEvent?.(parsed);\n } else {\n this.deps?.logger.debug('Platform GitHub event not forwarded to Factory ingress', {\n repositoryId,\n deliveryId: parsed.deliveryId,\n event: parsed.event,\n action: parsed.payload.action,\n });\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 github: this.#github,\n isAuthorizedSender: notification => this.#isAuthorizedSender(notification),\n onTargetSkipped: subscription => {\n // Routine when a subscription's thread belongs to another\n // deployment, so this stays at debug rather than warning on a loop.\n this.deps?.logger.debug('Platform GitHub event skipped: thread is not held here', {\n deliveryId: event.deliveryId,\n subscriptionId: subscription.id,\n threadId: subscription.threadId,\n });\n },\n onSenderRejected: notification => {\n this.deps?.logger.debug('Platform GitHub event dropped: sender not authorized', {\n deliveryId: event.deliveryId,\n repository: notification.metadata.repository,\n sender: notification.metadata.sender,\n kind: notification.kind,\n });\n },\n onTargetError: (subscription, error) => {\n this.deps?.logger.error('Platform GitHub event delivery failed for a subscription', {\n deliveryId: event.deliveryId,\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 this.deps?.logger.warn('Platform GitHub event completed with failed subscription deliveries', {\n repositoryId,\n deliveryId: event.deliveryId,\n delivered: result.delivered,\n failed: result.failed,\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 // Factory's own app has to clear the gate before the bot rules below, which\n // fail closed for every bot that is not explicitly allowlisted. GitHub\n // forbids an app from reviewing its own pull request, so `factory-review`\n // posts its verdict as a comment under this login and that comment is the\n // handoff the authoring agent wakes on.\n if (this.#github.identity?.matches(sender)) return true;\n if (isFactoryAppSender(sender, this.#github.slug)) return true;\n const normalizedSender = sender.toLowerCase();\n const authorizedBots = resolveAuthorizedBots(this.#github.authorizedBots);\n if (authorizedBots.has(normalizedSender)) return true;\n // Any other bot is gated purely by the allowlist: a GitHub App never holds a\n // collaborator permission under its sender login, so falling through to the\n // permission lookup would only fail closed after a wasted API call.\n if (notification.metadata.senderType?.toLowerCase() === 'bot' || normalizedSender.endsWith('[bot]')) {\n return false;\n }\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\n// Events the polling worker forwards to the factory rules engine. Closures\n// let the reconciler finalize cards; `synchronize` and `review_requested` on a\n// pull request are the triggers the review board's re-review path listens for;\n// submitted reviews and pull request comments are how review feedback reaches\n// the agent that authored the branch. Direct-webhook consumers ingest every\n// parsed event; the platform path gates because the remaining events (issue\n// edits, comment edits and deletions) only interest the subscription\n// dispatcher, not the factory rules.\nconst ISSUE_INGESTED_ACTIONS = new Set(['opened', 'reopened', 'edited', 'labeled', 'unlabeled']);\n\nfunction isFactoryIngestedEvent(event: ParsedGithubWebhook): boolean {\n if ((event.event === 'issues' || event.event === 'pull_request') && event.payload.action === 'closed') {\n return true;\n }\n if (event.event === 'pull_request') {\n const action = event.payload.action;\n // `opened` is what mints the Review card for a pull request. Without it a\n // factory-authored PR never gets reviewed on the polling path, which is the\n // only path a local deployment has.\n if (action === 'opened' || action === 'synchronize' || action === 'review_requested') return true;\n }\n // Issue opens, reopens, edits and label changes mint and update Work cards.\n // Without them a new issue never becomes a card on the polling path (#24972).\n if (event.event === 'issues' && ISSUE_INGESTED_ACTIONS.has(String(event.payload.action))) return true;\n if (event.event === 'pull_request_review' && event.payload.action === 'submitted') return true;\n if (event.event === 'issue_comment' && event.payload.action === 'created') return true;\n // Default-branch pushes drive base-checkpoint rebuilds\n // (`withBaseCheckpointWebhookTrigger` wraps the ingest callback).\n if (event.event === 'push') return true;\n return false;\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"],"mappings":"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"}
|
|
@@ -35,7 +35,7 @@ const INSTALLATION_REPOS_CACHE_TTL_MS = 3e4;
|
|
|
35
35
|
* installation tokens live ~60 minutes; 5 minutes keeps a wide validity
|
|
36
36
|
* margin while collapsing the per-session-materialization mint round trip.
|
|
37
37
|
*/
|
|
38
|
-
const REPOSITORY_ACCESS_CACHE_TTL_MS =
|
|
38
|
+
const REPOSITORY_ACCESS_CACHE_TTL_MS = 3e5;
|
|
39
39
|
/**
|
|
40
40
|
* How long a collaborator permission lookup may be reused. The reconcile
|
|
41
41
|
* sweep re-stamps every open card's author and the event tail gates every
|
|
@@ -43,7 +43,7 @@ const REPOSITORY_ACCESS_CACHE_TTL_MS = 5 * 6e4;
|
|
|
43
43
|
* requests per cycle against the installation's REST budget. Revoked access
|
|
44
44
|
* reads as trusted for at most this long.
|
|
45
45
|
*/
|
|
46
|
-
const COLLABORATOR_PERMISSION_CACHE_TTL_MS =
|
|
46
|
+
const COLLABORATOR_PERMISSION_CACHE_TTL_MS = 18e5;
|
|
47
47
|
/** Bound on the shared upstream lookup; callers race their own signal against it. */
|
|
48
48
|
const COLLABORATOR_PERMISSION_LOOKUP_TIMEOUT_MS = 1e4;
|
|
49
49
|
/**
|