@mastra/factory 0.24.0-alpha.8 → 0.24.0

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.
Files changed (301) hide show
  1. package/README.md +4 -4
  2. package/dist/environment/build-runner.js +2 -1
  3. package/dist/environment/build-runner.js.map +1 -1
  4. package/dist/environment/build-workflow.js +1 -1
  5. package/dist/environment/build-workflow.js.map +1 -1
  6. package/dist/environment/build.js +16 -14
  7. package/dist/environment/build.js.map +1 -1
  8. package/dist/factory.d.ts.map +1 -1
  9. package/dist/factory.js +111 -19
  10. package/dist/factory.js.map +1 -1
  11. package/dist/integrations/base.d.ts +1 -1
  12. package/dist/integrations/base.d.ts.map +1 -1
  13. package/dist/integrations/github/app-identity.js.map +1 -1
  14. package/dist/integrations/github/integration.js +1 -1
  15. package/dist/integrations/github/integration.js.map +1 -1
  16. package/dist/integrations/github/reconcile-worker.js +1 -1
  17. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  18. package/dist/integrations/github/routes.d.ts +1 -1
  19. package/dist/integrations/github/routes.d.ts.map +1 -1
  20. package/dist/integrations/github/routes.js +24 -13
  21. package/dist/integrations/github/routes.js.map +1 -1
  22. package/dist/integrations/github/rules.d.ts.map +1 -1
  23. package/dist/integrations/github/rules.js +53 -9
  24. package/dist/integrations/github/rules.js.map +1 -1
  25. package/dist/integrations/github/sandbox.js +2 -2
  26. package/dist/integrations/github/sandbox.js.map +1 -1
  27. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
  28. package/dist/integrations/github/session-subscriptions.js +1 -2
  29. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  30. package/dist/integrations/github/subscriptions.d.ts +7 -4
  31. package/dist/integrations/github/subscriptions.d.ts.map +1 -1
  32. package/dist/integrations/github/subscriptions.js +5 -5
  33. package/dist/integrations/github/subscriptions.js.map +1 -1
  34. package/dist/integrations/gitlab/api.js.map +1 -1
  35. package/dist/integrations/gitlab/integration.js.map +1 -1
  36. package/dist/integrations/gitlab/reconciliation-config.js +2 -1
  37. package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
  38. package/dist/integrations/gitlab/routes.d.ts.map +1 -1
  39. package/dist/integrations/gitlab/routes.js +10 -6
  40. package/dist/integrations/gitlab/routes.js.map +1 -1
  41. package/dist/integrations/gitlab/rules.js.map +1 -1
  42. package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
  43. package/dist/integrations/gitlab/session-subscriptions.js +1 -2
  44. package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
  45. package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
  46. package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
  47. package/dist/integrations/gitlab/subscriptions.js +5 -5
  48. package/dist/integrations/gitlab/subscriptions.js.map +1 -1
  49. package/dist/integrations/incidentio/api.js.map +1 -1
  50. package/dist/integrations/incidentio/integration.js.map +1 -1
  51. package/dist/integrations/incidentio/rules.js.map +1 -1
  52. package/dist/integrations/issue-reconcile-worker.js +1 -1
  53. package/dist/integrations/issue-reconcile-worker.js.map +1 -1
  54. package/dist/integrations/issue-reconciler.js +1 -1
  55. package/dist/integrations/issue-reconciler.js.map +1 -1
  56. package/dist/integrations/jira/api.js.map +1 -1
  57. package/dist/integrations/jira/integration.js.map +1 -1
  58. package/dist/integrations/jira/rules.js.map +1 -1
  59. package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
  60. package/dist/integrations/linear/agent-tools.js.map +1 -1
  61. package/dist/integrations/linear/integration.js.map +1 -1
  62. package/dist/integrations/linear/rules.js.map +1 -1
  63. package/dist/integrations/platform/api-client.js.map +1 -1
  64. package/dist/integrations/platform/github/event-worker.js +1 -1
  65. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  66. package/dist/integrations/platform/github/integration.js +2 -2
  67. package/dist/integrations/platform/github/integration.js.map +1 -1
  68. package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
  69. package/dist/integrations/platform/gitlab/integration.js.map +1 -1
  70. package/dist/integrations/platform/incidentio/integration.js +2 -1
  71. package/dist/integrations/platform/incidentio/integration.js.map +1 -1
  72. package/dist/integrations/platform/jira/integration.js.map +1 -1
  73. package/dist/integrations/platform/linear/event-worker.js +1 -1
  74. package/dist/integrations/platform/linear/event-worker.js.map +1 -1
  75. package/dist/integrations/platform/linear/integration.js.map +1 -1
  76. package/dist/integrations/slack/connect-route.js +1 -1
  77. package/dist/integrations/slack/connect-route.js.map +1 -1
  78. package/dist/integrations/slack/feed-publisher.js +1 -1
  79. package/dist/integrations/slack/feed-publisher.js.map +1 -1
  80. package/dist/integrations/slack/integration.js +1 -1
  81. package/dist/integrations/slack/integration.js.map +1 -1
  82. package/dist/integrations/slack/slack.d.ts +16 -5
  83. package/dist/integrations/slack/slack.d.ts.map +1 -1
  84. package/dist/integrations/slack/slack.js +87 -34
  85. package/dist/integrations/slack/slack.js.map +1 -1
  86. package/dist/integrations/subscription-session.d.ts +0 -1
  87. package/dist/integrations/subscription-session.d.ts.map +1 -1
  88. package/dist/integrations/subscription-session.js +1 -4
  89. package/dist/integrations/subscription-session.js.map +1 -1
  90. package/dist/integrations/workos/integration.js.map +1 -1
  91. package/dist/mcp/routes.d.ts.map +1 -1
  92. package/dist/mcp/routes.js +1 -1
  93. package/dist/mcp/routes.js.map +1 -1
  94. package/dist/packages/_internals/workspace/dist/index.js +18 -1
  95. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  96. package/dist/routes/attention-activity.d.ts.map +1 -1
  97. package/dist/routes/attention-activity.js +5 -2
  98. package/dist/routes/attention-activity.js.map +1 -1
  99. package/dist/routes/attention-parked.js.map +1 -1
  100. package/dist/routes/attention-providers.d.ts +0 -1
  101. package/dist/routes/attention-providers.d.ts.map +1 -1
  102. package/dist/routes/attention-providers.js +5 -6
  103. package/dist/routes/attention-providers.js.map +1 -1
  104. package/dist/routes/automation-runs.js.map +1 -1
  105. package/dist/routes/config.d.ts +6 -7
  106. package/dist/routes/config.d.ts.map +1 -1
  107. package/dist/routes/config.js +20 -19
  108. package/dist/routes/config.js.map +1 -1
  109. package/dist/routes/contracts.js +1 -1
  110. package/dist/routes/contracts.js.map +1 -1
  111. package/dist/routes/custom-provider-source.js.map +1 -1
  112. package/dist/routes/fs.d.ts +31 -23
  113. package/dist/routes/fs.d.ts.map +1 -1
  114. package/dist/routes/fs.js +218 -64
  115. package/dist/routes/fs.js.map +1 -1
  116. package/dist/routes/intake.js +2 -1
  117. package/dist/routes/intake.js.map +1 -1
  118. package/dist/routes/knowledge.js +1 -1
  119. package/dist/routes/knowledge.js.map +1 -1
  120. package/dist/routes/oauth.js +3 -2
  121. package/dist/routes/oauth.js.map +1 -1
  122. package/dist/routes/projects.d.ts +1 -1
  123. package/dist/routes/projects.d.ts.map +1 -1
  124. package/dist/routes/projects.js +33 -31
  125. package/dist/routes/projects.js.map +1 -1
  126. package/dist/routes/provider-credentials.d.ts.map +1 -1
  127. package/dist/routes/provider-credentials.js +6 -1
  128. package/dist/routes/provider-credentials.js.map +1 -1
  129. package/dist/routes/session-access.d.ts +55 -0
  130. package/dist/routes/session-access.d.ts.map +1 -0
  131. package/dist/routes/session-access.js +165 -0
  132. package/dist/routes/session-access.js.map +1 -0
  133. package/dist/routes/skills.d.ts +2 -4
  134. package/dist/routes/skills.d.ts.map +1 -1
  135. package/dist/routes/skills.js +19 -65
  136. package/dist/routes/skills.js.map +1 -1
  137. package/dist/routes/source-control-sessions.d.ts +6 -1
  138. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  139. package/dist/routes/source-control-sessions.js +95 -12
  140. package/dist/routes/source-control-sessions.js.map +1 -1
  141. package/dist/routes/surface.d.ts +5 -0
  142. package/dist/routes/surface.d.ts.map +1 -1
  143. package/dist/routes/surface.js +10 -4
  144. package/dist/routes/surface.js.map +1 -1
  145. package/dist/routes/tenant-credentials.js.map +1 -1
  146. package/dist/routes/work-items.js.map +1 -1
  147. package/dist/rules/binding-context.js +11 -9
  148. package/dist/rules/binding-context.js.map +1 -1
  149. package/dist/rules/dispatcher.d.ts +1 -1
  150. package/dist/rules/dispatcher.d.ts.map +1 -1
  151. package/dist/rules/dispatcher.js +10 -10
  152. package/dist/rules/dispatcher.js.map +1 -1
  153. package/dist/rules/start-coordinator.d.ts +4 -0
  154. package/dist/rules/start-coordinator.d.ts.map +1 -1
  155. package/dist/rules/start-coordinator.js +36 -4
  156. package/dist/rules/start-coordinator.js.map +1 -1
  157. package/dist/rules/tools.js +2 -2
  158. package/dist/rules/tools.js.map +1 -1
  159. package/dist/rules/transition-service.js.map +1 -1
  160. package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
  161. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  162. package/dist/sandbox/session-retirement.js.map +1 -1
  163. package/dist/secret-encryption.d.ts.map +1 -1
  164. package/dist/secret-encryption.js +65 -25
  165. package/dist/secret-encryption.js.map +1 -1
  166. package/dist/session/channel-reply-processor.d.ts +7 -6
  167. package/dist/session/channel-reply-processor.d.ts.map +1 -1
  168. package/dist/session/channel-reply-processor.js +9 -9
  169. package/dist/session/channel-reply-processor.js.map +1 -1
  170. package/dist/session/channel-session-link-processor.js +1 -0
  171. package/dist/session/channel-session-link-processor.js.map +1 -1
  172. package/dist/session/credential-policy.d.ts.map +1 -1
  173. package/dist/session/credential-policy.js +7 -6
  174. package/dist/session/credential-policy.js.map +1 -1
  175. package/dist/session/default-model-hydration.d.ts +1 -0
  176. package/dist/session/default-model-hydration.d.ts.map +1 -1
  177. package/dist/session/default-model-hydration.js +3 -2
  178. package/dist/session/default-model-hydration.js.map +1 -1
  179. package/dist/session/environment-state-processor.js +1 -1
  180. package/dist/session/environment-state-processor.js.map +1 -1
  181. package/dist/session/filesystem-capture.d.ts +3 -3
  182. package/dist/session/filesystem-capture.d.ts.map +1 -1
  183. package/dist/session/filesystem-capture.js +16 -17
  184. package/dist/session/filesystem-capture.js.map +1 -1
  185. package/dist/session/first-exec-capture.d.ts +1 -1
  186. package/dist/session/first-exec-capture.d.ts.map +1 -1
  187. package/dist/session/first-exec-capture.js +1 -1
  188. package/dist/session/first-exec-capture.js.map +1 -1
  189. package/dist/session/first-message-capture.d.ts +1 -1
  190. package/dist/session/first-message-capture.d.ts.map +1 -1
  191. package/dist/session/first-message-capture.js +1 -1
  192. package/dist/session/first-message-capture.js.map +1 -1
  193. package/dist/session/live-sessions.d.ts +1 -1
  194. package/dist/session/live-sessions.js +1 -1
  195. package/dist/session/live-sessions.js.map +1 -1
  196. package/dist/session/memory-settings-hydration.d.ts +1 -1
  197. package/dist/session/memory-settings-hydration.d.ts.map +1 -1
  198. package/dist/session/memory-settings-hydration.js +2 -1
  199. package/dist/session/memory-settings-hydration.js.map +1 -1
  200. package/dist/session/resource-migration.d.ts +105 -0
  201. package/dist/session/resource-migration.d.ts.map +1 -0
  202. package/dist/session/resource-migration.js +195 -0
  203. package/dist/session/resource-migration.js.map +1 -0
  204. package/dist/session/session-threads.d.ts +58 -0
  205. package/dist/session/session-threads.d.ts.map +1 -0
  206. package/dist/session/session-threads.js +90 -0
  207. package/dist/session/session-threads.js.map +1 -0
  208. package/dist/session/source-control-tools.js +8 -8
  209. package/dist/session/source-control-tools.js.map +1 -1
  210. package/dist/session/thinking-default.d.ts.map +1 -1
  211. package/dist/session/thinking-default.js +3 -4
  212. package/dist/session/thinking-default.js.map +1 -1
  213. package/dist/session/thread-session-stamp.d.ts +21 -0
  214. package/dist/session/thread-session-stamp.d.ts.map +1 -0
  215. package/dist/session/thread-session-stamp.js +20 -0
  216. package/dist/session/thread-session-stamp.js.map +1 -0
  217. package/dist/session/thread-title-mirror.d.ts +1 -1
  218. package/dist/session/thread-title-mirror.d.ts.map +1 -1
  219. package/dist/session/thread-title-mirror.js +1 -1
  220. package/dist/session/thread-title-mirror.js.map +1 -1
  221. package/dist/skills/service.d.ts +6 -8
  222. package/dist/skills/service.d.ts.map +1 -1
  223. package/dist/skills/service.js +3 -3
  224. package/dist/skills/service.js.map +1 -1
  225. package/dist/spa-static.js.map +1 -1
  226. package/dist/state-signing.js +1 -1
  227. package/dist/state-signing.js.map +1 -1
  228. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
  229. package/dist/storage/domains/audit/agent-audit.js +2 -2
  230. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  231. package/dist/storage/domains/audit/base.js.map +1 -1
  232. package/dist/storage/domains/audit/domain.js.map +1 -1
  233. package/dist/storage/domains/channel-identity/base.js.map +1 -1
  234. package/dist/storage/domains/comments/base.js.map +1 -1
  235. package/dist/storage/domains/comments/domain.js.map +1 -1
  236. package/dist/storage/domains/comments/feed-context.js.map +1 -1
  237. package/dist/storage/domains/credentials/base.js.map +1 -1
  238. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  239. package/dist/storage/domains/intake/base.js.map +1 -1
  240. package/dist/storage/domains/integrations/base.d.ts +8 -0
  241. package/dist/storage/domains/integrations/base.d.ts.map +1 -1
  242. package/dist/storage/domains/integrations/base.js +7 -0
  243. package/dist/storage/domains/integrations/base.js.map +1 -1
  244. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  245. package/dist/storage/domains/model-defaults/base.js.map +1 -1
  246. package/dist/storage/domains/projects/base.js.map +1 -1
  247. package/dist/storage/domains/queue-health/base.js.map +1 -1
  248. package/dist/storage/domains/source-control/base.d.ts +19 -0
  249. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  250. package/dist/storage/domains/source-control/base.js +42 -0
  251. package/dist/storage/domains/source-control/base.js.map +1 -1
  252. package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
  253. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  254. package/dist/storage/domains/source-control/inmemory.js +7 -0
  255. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  256. package/dist/storage/domains/work-items/base.d.ts +14 -0
  257. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  258. package/dist/storage/domains/work-items/base.js +25 -1
  259. package/dist/storage/domains/work-items/base.js.map +1 -1
  260. package/dist/storage/domains/work-items/board-version.js.map +1 -1
  261. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  262. package/dist/tools/approval-tools.js +1 -1
  263. package/dist/tools/approval-tools.js.map +1 -1
  264. package/dist/tools/deliver.d.ts +8 -5
  265. package/dist/tools/deliver.d.ts.map +1 -1
  266. package/dist/tools/deliver.js +22 -8
  267. package/dist/tools/deliver.js.map +1 -1
  268. package/dist/tools/messaging-tools.d.ts +1 -1
  269. package/dist/tools/messaging-tools.d.ts.map +1 -1
  270. package/dist/tools/messaging-tools.js +203 -72
  271. package/dist/tools/messaging-tools.js.map +1 -1
  272. package/dist/tools/permissions.d.ts +1 -1
  273. package/dist/tools/permissions.d.ts.map +1 -1
  274. package/dist/tools/permissions.js +1 -1
  275. package/dist/tools/permissions.js.map +1 -1
  276. package/dist/tools/reports.d.ts.map +1 -1
  277. package/dist/tools/reports.js +10 -3
  278. package/dist/tools/reports.js.map +1 -1
  279. package/dist/tools/run-watch.d.ts +2 -0
  280. package/dist/tools/run-watch.d.ts.map +1 -1
  281. package/dist/tools/run-watch.js +42 -2
  282. package/dist/tools/run-watch.js.map +1 -1
  283. package/dist/tools/scope.d.ts +2 -1
  284. package/dist/tools/scope.d.ts.map +1 -1
  285. package/dist/tools/scope.js +3 -2
  286. package/dist/tools/scope.js.map +1 -1
  287. package/dist/tools/session-tools.d.ts +1 -1
  288. package/dist/tools/session-tools.d.ts.map +1 -1
  289. package/dist/tools/session-tools.js +33 -16
  290. package/dist/tools/session-tools.js.map +1 -1
  291. package/dist/tools/sessions-state-processor.js +2 -2
  292. package/dist/tools/sessions-state-processor.js.map +1 -1
  293. package/dist/tools/side-sessions.d.ts +1 -1
  294. package/dist/tools/side-sessions.d.ts.map +1 -1
  295. package/dist/tools/side-sessions.js +1 -1
  296. package/dist/tools/side-sessions.js.map +1 -1
  297. package/dist/workspace.d.ts +5 -2
  298. package/dist/workspace.d.ts.map +1 -1
  299. package/dist/workspace.js +28 -15
  300. package/dist/workspace.js.map +1 -1
  301. package/package.json +13 -13
@@ -1 +1 @@
1
- {"version":3,"file":"event-worker.js","names":["#client","#linear","#storage","#projects","#workItems","#ingestFactoryIssue","#reconcileFactoryState","#pollEventsEnabled","#intervalMs","#reconcileIntervalMs","#now","#leaseOwner","#leaseTtlMs","#leaseProvider","#running","#startedAt","#settings","#schedule","#timer","#stopLeaseRenewal","#inFlight","#hasLease","#tick","#ensureLease","#poll","#startLeaseRenewal","#leaseRenewalTimer","#pollWorkspace","#maybeReconcile","#lastReconcileAt","#saveSettings","#dispatchEvent"],"sources":["../../../../src/integrations/platform/linear/event-worker.ts"],"sourcesContent":["import { 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 { FactoryProjectsStorage } from '../../../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../../../storage/domains/work-items/base.js';\nimport type { IssueReconciler } from '../../issue-reconciler.js';\nimport type { LinearIssueIngress, LinearRulesIngress } from '../../linear/rules.js';\nimport type { PlatformApiClient} from '../api-client.js';\nimport { PlatformApiError } from '../api-client.js';\n\nconst API_PREFIX = '/v1/server/linear';\nconst DEFAULT_POLL_INTERVAL_MS = 20_000;\nconst DEFAULT_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst EVENT_PAGE_SIZE = 500;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst CURSOR_ORG_ID = '__platform_linear_event_worker__';\nconst CURSOR_USER_ID = 'worker';\nconst LEASE_KEY = 'platform-linear-events:linear';\n\ntype EventCursor = { afterEventId: string } | { afterTimestamp: number };\ntype PlatformLinearEventWorkerSettings = {\n version: 1;\n workspaces: Record<string, EventCursor>;\n};\n\nexport type PlatformLinearEventStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n PlatformLinearEventWorkerSettings,\n Record<string, unknown>\n>;\n\n/**\n * Minimal envelope shape mirrored from `@platform/linear`. Kept opaque\n * (`data: unknown`) — every consumer demuxes by `type`.\n */\nexport interface LinearWebhookEnvelope {\n type: string;\n action: string | null;\n createdAt: string | null;\n webhookTimestamp: number | null;\n linearOrganizationId: string | null;\n oauthClientId: string | null;\n url: string | null;\n data: unknown;\n}\n\nexport interface LinearEventLogEntry {\n id: string;\n timestamp: number;\n envelope: LinearWebhookEnvelope;\n}\n\nexport interface PlatformLinearWorkspace {\n linearWorkspaceId: string;\n linearWorkspaceName?: string;\n}\n\n/**\n * Everything the worker needs to drive ingest + reconciliation without\n * pulling in the full `PlatformLinearIntegration` type (circular).\n */\nexport interface PlatformLinearEventDispatchIntegration {\n listWorkspaces(): Promise<PlatformLinearWorkspace[]>;\n}\n\nexport interface PlatformLinearEventWorkerConfig {\n client: PlatformApiClient;\n linear: PlatformLinearEventDispatchIntegration;\n storage: PlatformLinearEventStorage;\n projects: Pick<FactoryProjectsStorage, 'listAll'>;\n /**\n * Used to scope event dispatch to `(orgId, factoryProjectId)` pairs that\n * already have a work item linked to the incoming Linear issue. Without\n * this scoping, a workspace-scoped event would fan out to every project in\n * every org and materialize a triage card in each — a cross-tenant leak.\n * First-observation of a Linear issue happens through the user-authenticated\n * `/web/linear/issues?factoryProjectId=...` intake path instead.\n */\n workItems: Pick<WorkItemsStorage, 'list'>;\n /** Called with a single-issue rules ingress derived from an `Issue` webhook. */\n ingestFactoryIssue?: (input: LinearRulesIngress) => Promise<unknown>;\n reconcileFactoryState?: IssueReconciler;\n /** When false the worker skips event tailing and only runs the reconcile sweep. */\n pollEventsEnabled?: boolean;\n intervalMs?: number;\n reconcileIntervalMs?: number;\n now?: () => number;\n}\n\n/**\n * Tails the Platform Linear event stream (Redis-backed, per workspace) and\n * runs the same folded issue-reconcile sweep pattern as\n * `PlatformGithubEventWorker`. Issue events are translated into a normal\n * `LinearRulesIngress` so downstream rule dispatch is identical to the\n * polling path.\n *\n * Cursor state is persisted per workspace via the integration's generic\n * settings surface, keyed by a well-known worker identity. Cold start uses\n * `afterTimestamp: now - 1` to avoid replaying the 14-day stream backlog.\n *\n * ## Delivery semantics\n *\n * **At-most-once** per event. The cursor advances to the last event ID in a\n * page after all dispatch attempts on that page complete, regardless of\n * whether individual ingest calls threw. Ingest failures are logged, the\n * offending event is not retried, and drift is caught by the folded\n * `LinearIssueReconciler` sweep on its own cadence (default 5 minutes). This\n * matches `PlatformGithubEventWorker` and avoids poison-pill events blocking\n * the whole stream. Any consumer that needs exactly-once must idempotently\n * handle the same issue arriving via both the event path and the reconcile\n * path.\n */\nexport class PlatformLinearEventWorker extends MastraWorker {\n readonly name = 'platform-linear-events';\n\n readonly #client: PlatformApiClient;\n readonly #linear: PlatformLinearEventDispatchIntegration;\n readonly #storage: PlatformLinearEventStorage;\n readonly #projects: Pick<FactoryProjectsStorage, 'listAll'>;\n readonly #workItems: Pick<WorkItemsStorage, 'list'>;\n readonly #ingestFactoryIssue: ((input: LinearRulesIngress) => Promise<unknown>) | undefined;\n readonly #reconcileFactoryState: IssueReconciler | undefined;\n readonly #pollEventsEnabled: boolean;\n readonly #intervalMs: number;\n readonly #reconcileIntervalMs: number;\n readonly #now: () => number;\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 #lastReconcileAt = 0;\n #settings: PlatformLinearEventWorkerSettings = { version: 1, workspaces: {} };\n\n constructor(config: PlatformLinearEventWorkerConfig) {\n super();\n this.#client = config.client;\n this.#linear = config.linear;\n this.#storage = config.storage;\n this.#projects = config.projects;\n this.#workItems = config.workItems;\n this.#ingestFactoryIssue = config.ingestFactoryIssue;\n this.#reconcileFactoryState = config.reconcileFactoryState;\n this.#pollEventsEnabled = config.pollEventsEnabled ?? true;\n this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n this.#reconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('Platform Linear event polling interval must be a positive number.');\n }\n if (!Number.isFinite(this.#reconcileIntervalMs) || this.#reconcileIntervalMs <= 0) {\n throw new Error('Platform Linear reconcile interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n this.#now = config.now ?? Date.now;\n }\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('PlatformLinearEventWorker: 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 Linear 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(LEASE_KEY, 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 Linear 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(LEASE_KEY, 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(LEASE_KEY, 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 Linear 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 // Reconcile-only mode has no event tail to keep fresh, so tick on the\n // slower reconcile cadence instead of the polling interval.\n let retryInMs = this.#pollEventsEnabled ? this.#intervalMs : this.#reconcileIntervalMs;\n\n if (this.#pollEventsEnabled) {\n // Only list workspaces when actually tailing events: reconciliation\n // does not need the workspace list, and folding this call into\n // reconcile-only mode would let one workspace-listing outage take\n // down the reconcile sweep as well.\n let workspaces: PlatformLinearWorkspace[] = [];\n try {\n workspaces = await this.#linear.listWorkspaces();\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform Linear workspace listing failed', {\n error: error instanceof Error ? error.message : String(error),\n retryInMs: delay,\n });\n }\n\n for (const workspace of workspaces) {\n if (!this.#running || !this.#hasLease) break;\n try {\n await this.#pollWorkspace(workspace.linearWorkspaceId);\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform Linear workspace event polling failed', {\n linearWorkspaceId: workspace.linearWorkspaceId,\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();\n\n return retryInMs;\n }\n\n async #maybeReconcile(): Promise<void> {\n if (!this.#reconcileFactoryState || !this.#running || !this.#hasLease) return;\n const now = this.#now();\n if (now - this.#lastReconcileAt < this.#reconcileIntervalMs) return;\n // Advance the clock before sweeping so a persistently failing sweep stays\n // on cadence instead of retrying every poll tick.\n this.#lastReconcileAt = now;\n const startedAt = Date.now();\n try {\n const { errors, ...counts } = await this.#reconcileFactoryState();\n const context = { ...counts, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform Linear issue reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.updated > 0) {\n this.deps?.logger.info('Platform Linear issue reconcile patched stale metadata', context);\n } else {\n this.deps?.logger.debug('Platform Linear issue reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform Linear issue reconcile failed', {\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n async #pollWorkspace(linearWorkspaceId: string): Promise<void> {\n if (!this.#settings.workspaces[linearWorkspaceId]) {\n this.#settings.workspaces[linearWorkspaceId] = { afterTimestamp: this.#startedAt };\n await this.#saveSettings();\n }\n\n while (this.#running && this.#hasLease) {\n const cursor: EventCursor = this.#settings.workspaces[linearWorkspaceId]!;\n const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE), type: 'Issue' });\n if ('afterEventId' in cursor) query.set('afterEventId', cursor.afterEventId);\n else query.set('after', String(cursor.afterTimestamp));\n\n const pollStartedAt = performance.now();\n const page = await this.#client.request<{ events: LinearEventLogEntry[] }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(linearWorkspaceId)}/events?${query}`,\n );\n this.deps?.logger.debug('Platform Linear workspace event poll completed', {\n linearWorkspaceId,\n eventCount: page.events.length,\n latencyMs: Math.round(performance.now() - pollStartedAt),\n });\n if (page.events.length === 0) return;\n\n const lastId = page.events[page.events.length - 1]!.id;\n\n // Resolve the project set once per page instead of once per event.\n // The event stream can return up to EVENT_PAGE_SIZE (500) events; the\n // project list rarely changes within the second or two it takes to\n // dispatch a single page, and #dispatchEvent still consults storage\n // per-project to check for a linked work item.\n const projects = await this.#projects.listAll();\n\n for (const event of page.events) {\n if (!this.#running || !this.#hasLease) return;\n await this.#dispatchEvent(linearWorkspaceId, event, projects);\n }\n\n if ('afterEventId' in cursor && cursor.afterEventId === lastId) return;\n this.#settings.workspaces[linearWorkspaceId] = { afterEventId: lastId };\n await this.#saveSettings();\n\n // Short pages mean we've caught up; wait for the next tick.\n if (page.events.length < EVENT_PAGE_SIZE) return;\n }\n }\n\n async #dispatchEvent(\n linearWorkspaceId: string,\n event: LinearEventLogEntry,\n projects: readonly { id: string; orgId: string }[],\n ): Promise<void> {\n if (event.envelope.type !== 'Issue') return;\n if (!this.#ingestFactoryIssue) return;\n const issue = parseIssueEnvelope(event.envelope);\n if (!issue) {\n this.deps?.logger.warn('Platform Linear event log returned an unusable Issue payload', {\n linearWorkspaceId,\n eventId: event.id,\n });\n return;\n }\n\n // The event stream is workspace-scoped and Platform Linear has no\n // workspace→org mapping we can trust. To avoid cross-tenant fan-out (an\n // Issue from org A materializing a triage card in org B via the default\n // `linearIssueObserved` rule), only dispatch to `(orgId, factoryProjectId)`\n // pairs that already have a persisted work item for this Linear issue.\n // First-observation of a Linear issue happens through the\n // user-authenticated `/web/linear/issues?factoryProjectId=...` intake path.\n const sourceKey = `linear:${issue.identifier}`;\n let dispatched = 0;\n for (const project of projects) {\n let items;\n try {\n items = await this.#workItems.list({ orgId: project.orgId, factoryProjectId: project.id });\n } catch (error) {\n this.deps?.logger.error('Platform Linear work-item lookup failed', {\n linearWorkspaceId,\n eventId: event.id,\n projectId: project.id,\n error: error instanceof Error ? error.message : String(error),\n });\n continue;\n }\n const linked = items.some(\n item =>\n item.externalSource?.integrationId === 'linear' &&\n item.externalSource.type === 'issue' &&\n item.externalSource.externalId === sourceKey,\n );\n if (!linked) continue;\n try {\n await this.#ingestFactoryIssue({\n orgId: project.orgId,\n userId: 'platform-linear-event-worker',\n factoryProjectId: project.id,\n issues: [issue],\n });\n dispatched += 1;\n } catch (error) {\n this.deps?.logger.error('Platform Linear issue ingest failed', {\n linearWorkspaceId,\n eventId: event.id,\n projectId: project.id,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n if (dispatched === 0) {\n this.deps?.logger.debug('Platform Linear Issue event had no linked work items to update', {\n linearWorkspaceId,\n eventId: event.id,\n sourceKey,\n });\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\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: PlatformLinearEventWorkerSettings | null): PlatformLinearEventWorkerSettings {\n if (!value || value.version !== 1 || !value.workspaces || typeof value.workspaces !== 'object') {\n return { version: 1, workspaces: {} };\n }\n return { version: 1, workspaces: { ...value.workspaces } };\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\n/**\n * Translate an Issue webhook envelope into the rules-ingress shape.\n * Returns undefined when the payload lacks the identifiers we need to\n * match a work item.\n */\nfunction parseIssueEnvelope(envelope: LinearWebhookEnvelope): LinearIssueIngress | undefined {\n const data = envelope.data;\n if (!data || typeof data !== 'object' || Array.isArray(data)) return undefined;\n const raw = data as Record<string, unknown>;\n const id = typeof raw.id === 'string' ? raw.id : undefined;\n const identifier = typeof raw.identifier === 'string' ? raw.identifier : undefined;\n const title = typeof raw.title === 'string' ? raw.title : undefined;\n const url = typeof raw.url === 'string' ? raw.url : (envelope.url ?? undefined);\n if (!id || !identifier || !title || !url) return undefined;\n\n const state = optionalObject(raw.state);\n const team = optionalObject(raw.team);\n const assignee = optionalObject(raw.assignee);\n const creator = optionalObject(raw.user) ?? optionalObject(raw.creator);\n const labels = Array.isArray(raw.labels)\n ? raw.labels.flatMap(label => {\n const obj = optionalObject(label);\n return obj && typeof obj.name === 'string' ? [obj.name] : [];\n })\n : [];\n\n const createdAt = typeof raw.createdAt === 'string' ? raw.createdAt : (envelope.createdAt ?? '');\n const updatedAt = typeof raw.updatedAt === 'string' ? raw.updatedAt : createdAt;\n\n return {\n id,\n identifier,\n title,\n url,\n state: (state && typeof state.name === 'string' ? state.name : null) ?? '',\n stateType: (state && typeof state.type === 'string' ? state.type : null) ?? '',\n priorityLabel: typeof raw.priorityLabel === 'string' ? raw.priorityLabel : '',\n assignee: assignee && typeof assignee.name === 'string' ? assignee.name : null,\n creator: creator && typeof creator.name === 'string' ? creator.name : null,\n team: team && typeof team.name === 'string' ? team.name : null,\n labels,\n createdAt,\n updatedAt,\n };\n}\n\nfunction optionalObject(value: unknown): Record<string, unknown> | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;\n return value as Record<string, unknown>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"event-worker.js","names":[],"sources":["../../../../src/integrations/platform/linear/event-worker.ts"],"sourcesContent":["import { 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 { FactoryProjectsStorage } from '../../../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../../../storage/domains/work-items/base.js';\nimport type { IssueReconciler } from '../../issue-reconciler.js';\nimport type { LinearIssueIngress, LinearRulesIngress } from '../../linear/rules.js';\nimport type { PlatformApiClient} from '../api-client.js';\nimport { PlatformApiError } from '../api-client.js';\n\nconst API_PREFIX = '/v1/server/linear';\nconst DEFAULT_POLL_INTERVAL_MS = 20_000;\nconst DEFAULT_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst EVENT_PAGE_SIZE = 500;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst CURSOR_ORG_ID = '__platform_linear_event_worker__';\nconst CURSOR_USER_ID = 'worker';\nconst LEASE_KEY = 'platform-linear-events:linear';\n\ntype EventCursor = { afterEventId: string } | { afterTimestamp: number };\ntype PlatformLinearEventWorkerSettings = {\n version: 1;\n workspaces: Record<string, EventCursor>;\n};\n\nexport type PlatformLinearEventStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n PlatformLinearEventWorkerSettings,\n Record<string, unknown>\n>;\n\n/**\n * Minimal envelope shape mirrored from `@platform/linear`. Kept opaque\n * (`data: unknown`) — every consumer demuxes by `type`.\n */\nexport interface LinearWebhookEnvelope {\n type: string;\n action: string | null;\n createdAt: string | null;\n webhookTimestamp: number | null;\n linearOrganizationId: string | null;\n oauthClientId: string | null;\n url: string | null;\n data: unknown;\n}\n\nexport interface LinearEventLogEntry {\n id: string;\n timestamp: number;\n envelope: LinearWebhookEnvelope;\n}\n\nexport interface PlatformLinearWorkspace {\n linearWorkspaceId: string;\n linearWorkspaceName?: string;\n}\n\n/**\n * Everything the worker needs to drive ingest + reconciliation without\n * pulling in the full `PlatformLinearIntegration` type (circular).\n */\nexport interface PlatformLinearEventDispatchIntegration {\n listWorkspaces(): Promise<PlatformLinearWorkspace[]>;\n}\n\nexport interface PlatformLinearEventWorkerConfig {\n client: PlatformApiClient;\n linear: PlatformLinearEventDispatchIntegration;\n storage: PlatformLinearEventStorage;\n projects: Pick<FactoryProjectsStorage, 'listAll'>;\n /**\n * Used to scope event dispatch to `(orgId, factoryProjectId)` pairs that\n * already have a work item linked to the incoming Linear issue. Without\n * this scoping, a workspace-scoped event would fan out to every project in\n * every org and materialize a triage card in each — a cross-tenant leak.\n * First-observation of a Linear issue happens through the user-authenticated\n * `/web/linear/issues?factoryProjectId=...` intake path instead.\n */\n workItems: Pick<WorkItemsStorage, 'list'>;\n /** Called with a single-issue rules ingress derived from an `Issue` webhook. */\n ingestFactoryIssue?: (input: LinearRulesIngress) => Promise<unknown>;\n reconcileFactoryState?: IssueReconciler;\n /** When false the worker skips event tailing and only runs the reconcile sweep. */\n pollEventsEnabled?: boolean;\n intervalMs?: number;\n reconcileIntervalMs?: number;\n now?: () => number;\n}\n\n/**\n * Tails the Platform Linear event stream (Redis-backed, per workspace) and\n * runs the same folded issue-reconcile sweep pattern as\n * `PlatformGithubEventWorker`. Issue events are translated into a normal\n * `LinearRulesIngress` so downstream rule dispatch is identical to the\n * polling path.\n *\n * Cursor state is persisted per workspace via the integration's generic\n * settings surface, keyed by a well-known worker identity. Cold start uses\n * `afterTimestamp: now - 1` to avoid replaying the 14-day stream backlog.\n *\n * ## Delivery semantics\n *\n * **At-most-once** per event. The cursor advances to the last event ID in a\n * page after all dispatch attempts on that page complete, regardless of\n * whether individual ingest calls threw. Ingest failures are logged, the\n * offending event is not retried, and drift is caught by the folded\n * `LinearIssueReconciler` sweep on its own cadence (default 5 minutes). This\n * matches `PlatformGithubEventWorker` and avoids poison-pill events blocking\n * the whole stream. Any consumer that needs exactly-once must idempotently\n * handle the same issue arriving via both the event path and the reconcile\n * path.\n */\nexport class PlatformLinearEventWorker extends MastraWorker {\n readonly name = 'platform-linear-events';\n\n readonly #client: PlatformApiClient;\n readonly #linear: PlatformLinearEventDispatchIntegration;\n readonly #storage: PlatformLinearEventStorage;\n readonly #projects: Pick<FactoryProjectsStorage, 'listAll'>;\n readonly #workItems: Pick<WorkItemsStorage, 'list'>;\n readonly #ingestFactoryIssue: ((input: LinearRulesIngress) => Promise<unknown>) | undefined;\n readonly #reconcileFactoryState: IssueReconciler | undefined;\n readonly #pollEventsEnabled: boolean;\n readonly #intervalMs: number;\n readonly #reconcileIntervalMs: number;\n readonly #now: () => number;\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 #lastReconcileAt = 0;\n #settings: PlatformLinearEventWorkerSettings = { version: 1, workspaces: {} };\n\n constructor(config: PlatformLinearEventWorkerConfig) {\n super();\n this.#client = config.client;\n this.#linear = config.linear;\n this.#storage = config.storage;\n this.#projects = config.projects;\n this.#workItems = config.workItems;\n this.#ingestFactoryIssue = config.ingestFactoryIssue;\n this.#reconcileFactoryState = config.reconcileFactoryState;\n this.#pollEventsEnabled = config.pollEventsEnabled ?? true;\n this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n this.#reconcileIntervalMs = config.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('Platform Linear event polling interval must be a positive number.');\n }\n if (!Number.isFinite(this.#reconcileIntervalMs) || this.#reconcileIntervalMs <= 0) {\n throw new Error('Platform Linear reconcile interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n this.#now = config.now ?? Date.now;\n }\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('PlatformLinearEventWorker: 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 Linear 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(LEASE_KEY, 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 Linear 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(LEASE_KEY, 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(LEASE_KEY, 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 Linear 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 // Reconcile-only mode has no event tail to keep fresh, so tick on the\n // slower reconcile cadence instead of the polling interval.\n let retryInMs = this.#pollEventsEnabled ? this.#intervalMs : this.#reconcileIntervalMs;\n\n if (this.#pollEventsEnabled) {\n // Only list workspaces when actually tailing events: reconciliation\n // does not need the workspace list, and folding this call into\n // reconcile-only mode would let one workspace-listing outage take\n // down the reconcile sweep as well.\n let workspaces: PlatformLinearWorkspace[] = [];\n try {\n workspaces = await this.#linear.listWorkspaces();\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform Linear workspace listing failed', {\n error: error instanceof Error ? error.message : String(error),\n retryInMs: delay,\n });\n }\n\n for (const workspace of workspaces) {\n if (!this.#running || !this.#hasLease) break;\n try {\n await this.#pollWorkspace(workspace.linearWorkspaceId);\n } catch (error) {\n const delay = retryDelay(error, this.#intervalMs);\n retryInMs = Math.max(retryInMs, delay);\n this.deps?.logger.error('Platform Linear workspace event polling failed', {\n linearWorkspaceId: workspace.linearWorkspaceId,\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();\n\n return retryInMs;\n }\n\n async #maybeReconcile(): Promise<void> {\n if (!this.#reconcileFactoryState || !this.#running || !this.#hasLease) return;\n const now = this.#now();\n if (now - this.#lastReconcileAt < this.#reconcileIntervalMs) return;\n // Advance the clock before sweeping so a persistently failing sweep stays\n // on cadence instead of retrying every poll tick.\n this.#lastReconcileAt = now;\n const startedAt = Date.now();\n try {\n const { errors, ...counts } = await this.#reconcileFactoryState();\n const context = { ...counts, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('Platform Linear issue reconcile sweep completed with failures', {\n ...context,\n errors,\n });\n } else if (counts.updated > 0) {\n this.deps?.logger.info('Platform Linear issue reconcile patched stale metadata', context);\n } else {\n this.deps?.logger.debug('Platform Linear issue reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.error('Platform Linear issue reconcile failed', {\n durationMs: Date.now() - startedAt,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n async #pollWorkspace(linearWorkspaceId: string): Promise<void> {\n if (!this.#settings.workspaces[linearWorkspaceId]) {\n this.#settings.workspaces[linearWorkspaceId] = { afterTimestamp: this.#startedAt };\n await this.#saveSettings();\n }\n\n while (this.#running && this.#hasLease) {\n const cursor: EventCursor = this.#settings.workspaces[linearWorkspaceId]!;\n const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE), type: 'Issue' });\n if ('afterEventId' in cursor) query.set('afterEventId', cursor.afterEventId);\n else query.set('after', String(cursor.afterTimestamp));\n\n const pollStartedAt = performance.now();\n const page = await this.#client.request<{ events: LinearEventLogEntry[] }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(linearWorkspaceId)}/events?${query}`,\n );\n this.deps?.logger.debug('Platform Linear workspace event poll completed', {\n linearWorkspaceId,\n eventCount: page.events.length,\n latencyMs: Math.round(performance.now() - pollStartedAt),\n });\n if (page.events.length === 0) return;\n\n const lastId = page.events[page.events.length - 1]!.id;\n\n // Resolve the project set once per page instead of once per event.\n // The event stream can return up to EVENT_PAGE_SIZE (500) events; the\n // project list rarely changes within the second or two it takes to\n // dispatch a single page, and #dispatchEvent still consults storage\n // per-project to check for a linked work item.\n const projects = await this.#projects.listAll();\n\n for (const event of page.events) {\n if (!this.#running || !this.#hasLease) return;\n await this.#dispatchEvent(linearWorkspaceId, event, projects);\n }\n\n if ('afterEventId' in cursor && cursor.afterEventId === lastId) return;\n this.#settings.workspaces[linearWorkspaceId] = { afterEventId: lastId };\n await this.#saveSettings();\n\n // Short pages mean we've caught up; wait for the next tick.\n if (page.events.length < EVENT_PAGE_SIZE) return;\n }\n }\n\n async #dispatchEvent(\n linearWorkspaceId: string,\n event: LinearEventLogEntry,\n projects: readonly { id: string; orgId: string }[],\n ): Promise<void> {\n if (event.envelope.type !== 'Issue') return;\n if (!this.#ingestFactoryIssue) return;\n const issue = parseIssueEnvelope(event.envelope);\n if (!issue) {\n this.deps?.logger.warn('Platform Linear event log returned an unusable Issue payload', {\n linearWorkspaceId,\n eventId: event.id,\n });\n return;\n }\n\n // The event stream is workspace-scoped and Platform Linear has no\n // workspace→org mapping we can trust. To avoid cross-tenant fan-out (an\n // Issue from org A materializing a triage card in org B via the default\n // `linearIssueObserved` rule), only dispatch to `(orgId, factoryProjectId)`\n // pairs that already have a persisted work item for this Linear issue.\n // First-observation of a Linear issue happens through the\n // user-authenticated `/web/linear/issues?factoryProjectId=...` intake path.\n const sourceKey = `linear:${issue.identifier}`;\n let dispatched = 0;\n for (const project of projects) {\n let items;\n try {\n items = await this.#workItems.list({ orgId: project.orgId, factoryProjectId: project.id });\n } catch (error) {\n this.deps?.logger.error('Platform Linear work-item lookup failed', {\n linearWorkspaceId,\n eventId: event.id,\n projectId: project.id,\n error: error instanceof Error ? error.message : String(error),\n });\n continue;\n }\n const linked = items.some(\n item =>\n item.externalSource?.integrationId === 'linear' &&\n item.externalSource.type === 'issue' &&\n item.externalSource.externalId === sourceKey,\n );\n if (!linked) continue;\n try {\n await this.#ingestFactoryIssue({\n orgId: project.orgId,\n userId: 'platform-linear-event-worker',\n factoryProjectId: project.id,\n issues: [issue],\n });\n dispatched += 1;\n } catch (error) {\n this.deps?.logger.error('Platform Linear issue ingest failed', {\n linearWorkspaceId,\n eventId: event.id,\n projectId: project.id,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n if (dispatched === 0) {\n this.deps?.logger.debug('Platform Linear Issue event had no linked work items to update', {\n linearWorkspaceId,\n eventId: event.id,\n sourceKey,\n });\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\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: PlatformLinearEventWorkerSettings | null): PlatformLinearEventWorkerSettings {\n if (!value || value.version !== 1 || !value.workspaces || typeof value.workspaces !== 'object') {\n return { version: 1, workspaces: {} };\n }\n return { version: 1, workspaces: { ...value.workspaces } };\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\n/**\n * Translate an Issue webhook envelope into the rules-ingress shape.\n * Returns undefined when the payload lacks the identifiers we need to\n * match a work item.\n */\nfunction parseIssueEnvelope(envelope: LinearWebhookEnvelope): LinearIssueIngress | undefined {\n const data = envelope.data;\n if (!data || typeof data !== 'object' || Array.isArray(data)) return undefined;\n const raw = data as Record<string, unknown>;\n const id = typeof raw.id === 'string' ? raw.id : undefined;\n const identifier = typeof raw.identifier === 'string' ? raw.identifier : undefined;\n const title = typeof raw.title === 'string' ? raw.title : undefined;\n const url = typeof raw.url === 'string' ? raw.url : (envelope.url ?? undefined);\n if (!id || !identifier || !title || !url) return undefined;\n\n const state = optionalObject(raw.state);\n const team = optionalObject(raw.team);\n const assignee = optionalObject(raw.assignee);\n const creator = optionalObject(raw.user) ?? optionalObject(raw.creator);\n const labels = Array.isArray(raw.labels)\n ? raw.labels.flatMap(label => {\n const obj = optionalObject(label);\n return obj && typeof obj.name === 'string' ? [obj.name] : [];\n })\n : [];\n\n const createdAt = typeof raw.createdAt === 'string' ? raw.createdAt : (envelope.createdAt ?? '');\n const updatedAt = typeof raw.updatedAt === 'string' ? raw.updatedAt : createdAt;\n\n return {\n id,\n identifier,\n title,\n url,\n state: (state && typeof state.name === 'string' ? state.name : null) ?? '',\n stateType: (state && typeof state.type === 'string' ? state.type : null) ?? '',\n priorityLabel: typeof raw.priorityLabel === 'string' ? raw.priorityLabel : '',\n assignee: assignee && typeof assignee.name === 'string' ? assignee.name : null,\n creator: creator && typeof creator.name === 'string' ? creator.name : null,\n team: team && typeof team.name === 'string' ? team.name : null,\n labels,\n createdAt,\n updatedAt,\n };\n}\n\nfunction optionalObject(value: unknown): Record<string, unknown> | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;\n return value as Record<string, unknown>;\n}\n"],"mappings":";;;;AAaA,MAAM,aAAa;AACnB,MAAM,2BAA2B;AACjC,MAAM,gCAAgC;AACtC,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AACzB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;AA+FlB,IAAa,4BAAb,cAA+C,aAAa;CAC1D,OAAgB;CAEhB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,cAAuB,WAAW,OAAO,WAAW;CAEpD,WAAW;CACX;CACA;CACA;CACA,iBAAgC;CAChC;CACA,YAAY;CACZ,aAAa;CACb,mBAAmB;CACnB,YAA+C;EAAE,SAAS;EAAG,YAAY,CAAC;CAAE;CAE5E,YAAY,QAAyC;EACnD,MAAM;EACN,KAAK,UAAU,OAAO;EACtB,KAAK,UAAU,OAAO;EACtB,KAAK,WAAW,OAAO;EACvB,KAAK,YAAY,OAAO;EACxB,KAAK,aAAa,OAAO;EACzB,KAAK,sBAAsB,OAAO;EAClC,KAAK,yBAAyB,OAAO;EACrC,KAAK,qBAAqB,OAAO,qBAAqB;EACtD,KAAK,cAAc,OAAO,cAAc;EACxC,KAAK,uBAAuB,OAAO,uBAAuB;EAC1D,IAAI,CAAC,OAAO,SAAS,KAAK,WAAW,KAAK,KAAK,eAAe,GAC5D,MAAM,IAAI,MAAM,mEAAmE;EAErF,IAAI,CAAC,OAAO,SAAS,KAAK,oBAAoB,KAAK,KAAK,wBAAwB,GAC9E,MAAM,IAAI,MAAM,+DAA+D;EAEjF,KAAK,cAAc,KAAK,IAAI,kBAAkB,KAAK,cAAc,CAAC;EAClE,KAAK,OAAO,OAAO,OAAO,KAAK;CACjC;CAEA,MAAM,KAAK,MAAiC;EAC1C,MAAM,MAAM,KAAK,IAAI;EACrB,KAAK,iBAAiB,iBAAiB,KAAK,MAAM;CACpD;CAEA,MAAM,QAAuB;EAC3B,IAAI,KAAK,UAAU;EACnB,IAAI,CAAC,KAAK,MAAM,MAAM,IAAI,MAAM,uDAAuD;EAEvF,KAAK,aAAa,KAAK,KAAK,IAAI;EAChC,KAAK,YAAY,kBAAkB,MAAM,KAAK,SAAS,SAAS,IAAI,eAAe,cAAc,CAAC;EAClG,KAAK,WAAW;EAChB,KAAK,KAAK,OAAO,KAAK,yCAAyC;GAC7D,YAAY,KAAK;GACjB,YAAY,KAAK;EACnB,CAAC;EACD,KAAK,UAAU,CAAC;CAClB;CAEA,MAAM,OAAsB;EAC1B,IAAI,CAAC,KAAK,UAAU;EACpB,KAAK,WAAW;EAChB,IAAI,KAAK,QAAQ,aAAa,KAAK,MAAM;EACzC,KAAK,SAAS,KAAA;EACd,KAAK,kBAAkB;EACvB,MAAM,KAAK;EACX,IAAI,KAAK,WAAW;GAClB,MAAM,KAAK,eAAe,aAAa,WAAW,KAAK,WAAW,CAAC,CAAC,YAAY,KAAA,CAAS;GACzF,KAAK,YAAY;EACnB;CACF;CAEA,IAAI,YAAqB;EACvB,OAAO,KAAK;CACd;CAEA,UAAU,SAAuB;EAC/B,IAAI,CAAC,KAAK,UAAU;EACpB,KAAK,SAAS,iBAAiB;GAC7B,KAAK,SAAS,KAAA;GACd,MAAM,MAAM,KAAK,MAAM;GACvB,KAAK,YAAY;GACjB,IAAS,cAAc;IACrB,IAAI,KAAK,cAAc,KAAK,KAAK,YAAY,KAAA;GAC/C,CAAC;EACH,GAAG,OAAO;EACV,KAAK,OAAO,QAAQ;CACtB;CAEA,MAAM,QAAuB;EAC3B,IAAI,YAAY,KAAK;EACrB,IAAI;GACF,IAAI,CAAE,MAAM,KAAK,aAAa,GAAI;GAClC,YAAY,MAAM,KAAK,MAAM;EAC/B,SAAS,OAAO;GACd,YAAY,WAAW,OAAO,KAAK,WAAW;GAC9C,KAAK,MAAM,OAAO,MAAM,8CAA8C;IACpE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC5D,WAAW;GACb,CAAC;EACH,UAAU;GACR,KAAK,UAAU,SAAS;EAC1B;CACF;CAEA,MAAM,eAAiC;EACrC,IAAI,KAAK,WAAW,OAAO;EAC3B,MAAM,SAAS,MAAM,KAAK,eAAe,aAAa,WAAW,KAAK,aAAa,KAAK,WAAW;EACnG,KAAK,YAAY,OAAO;EACxB,IAAI,KAAK,WAAW,KAAK,mBAAmB;EAC5C,OAAO,KAAK;CACd;CAEA,qBAA2B;EACzB,IAAI,KAAK,oBAAoB;EAC7B,KAAK,qBAAqB,kBAClB;GACJ,KAAU,eACP,WAAW,WAAW,KAAK,aAAa,KAAK,WAAW,CAAC,CACzD,MAAK,YAAW;IACf,IAAI,CAAC,SAAS;KACZ,KAAK,YAAY;KACjB,KAAK,kBAAkB;IACzB;GACF,CAAC,CAAC,CACD,OAAM,UAAS;IACd,KAAK,YAAY;IACjB,KAAK,kBAAkB;IACvB,KAAK,MAAM,OAAO,KAAK,sDAAsD,EAC3E,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;GACH,CAAC;EACL,GACA,KAAK,MAAM,KAAK,cAAc,CAAC,CACjC;EACA,KAAK,mBAAmB,QAAQ;CAClC;CAEA,oBAA0B;EACxB,IAAI,KAAK,oBAAoB,cAAc,KAAK,kBAAkB;EAClE,KAAK,qBAAqB,KAAA;CAC5B;CAEA,MAAM,QAAyB;EAG7B,IAAI,YAAY,KAAK,qBAAqB,KAAK,cAAc,KAAK;EAElE,IAAI,KAAK,oBAAoB;GAK3B,IAAI,aAAwC,CAAC;GAC7C,IAAI;IACF,aAAa,MAAM,KAAK,QAAQ,eAAe;GACjD,SAAS,OAAO;IACd,MAAM,QAAQ,WAAW,OAAO,KAAK,WAAW;IAChD,YAAY,KAAK,IAAI,WAAW,KAAK;IACrC,KAAK,MAAM,OAAO,MAAM,4CAA4C;KAClE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KAC5D,WAAW;IACb,CAAC;GACH;GAEA,KAAK,MAAM,aAAa,YAAY;IAClC,IAAI,CAAC,KAAK,YAAY,CAAC,KAAK,WAAW;IACvC,IAAI;KACF,MAAM,KAAK,eAAe,UAAU,iBAAiB;IACvD,SAAS,OAAO;KACd,MAAM,QAAQ,WAAW,OAAO,KAAK,WAAW;KAChD,YAAY,KAAK,IAAI,WAAW,KAAK;KACrC,KAAK,MAAM,OAAO,MAAM,kDAAkD;MACxE,mBAAmB,UAAU;MAC7B,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;MAC5D,WAAW;KACb,CAAC;KACD,IAAI,iBAAiB,oBAAoB,MAAM,WAAW,KAAK;IACjE;GACF;EACF;EAEA,MAAM,KAAK,gBAAgB;EAE3B,OAAO;CACT;CAEA,MAAM,kBAAiC;EACrC,IAAI,CAAC,KAAK,0BAA0B,CAAC,KAAK,YAAY,CAAC,KAAK,WAAW;EACvE,MAAM,MAAM,KAAK,KAAK;EACtB,IAAI,MAAM,KAAK,mBAAmB,KAAK,sBAAsB;EAG7D,KAAK,mBAAmB;EACxB,MAAM,YAAY,KAAK,IAAI;EAC3B,IAAI;GACF,MAAM,EAAE,QAAQ,GAAG,WAAW,MAAM,KAAK,uBAAuB;GAChE,MAAM,UAAU;IAAE,GAAG;IAAQ,YAAY,KAAK,IAAI,IAAI;GAAU;GAChE,IAAI,OAAO,SAAS,GAClB,KAAK,MAAM,OAAO,KAAK,iEAAiE;IACtF,GAAG;IACH;GACF,CAAC;QACI,IAAI,OAAO,UAAU,GAC1B,KAAK,MAAM,OAAO,KAAK,0DAA0D,OAAO;QAExF,KAAK,MAAM,OAAO,MAAM,mDAAmD,OAAO;EAEtF,SAAS,OAAO;GACd,KAAK,MAAM,OAAO,MAAM,0CAA0C;IAChE,YAAY,KAAK,IAAI,IAAI;IACzB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC9D,CAAC;EACH;CACF;CAEA,MAAM,eAAe,mBAA0C;EAC7D,IAAI,CAAC,KAAK,UAAU,WAAW,oBAAoB;GACjD,KAAK,UAAU,WAAW,qBAAqB,EAAE,gBAAgB,KAAK,WAAW;GACjF,MAAM,KAAK,cAAc;EAC3B;EAEA,OAAO,KAAK,YAAY,KAAK,WAAW;GACtC,MAAM,SAAsB,KAAK,UAAU,WAAW;GACtD,MAAM,QAAQ,IAAI,gBAAgB;IAAE,OAAO,OAAO,eAAe;IAAG,MAAM;GAAQ,CAAC;GACnF,IAAI,kBAAkB,QAAQ,MAAM,IAAI,gBAAgB,OAAO,YAAY;QACtE,MAAM,IAAI,SAAS,OAAO,OAAO,cAAc,CAAC;GAErD,MAAM,gBAAgB,YAAY,IAAI;GACtC,MAAM,OAAO,MAAM,KAAK,QAAQ,QAC9B,OACA,GAAG,WAAW,cAAc,mBAAmB,iBAAiB,EAAE,UAAU,OAC9E;GACA,KAAK,MAAM,OAAO,MAAM,kDAAkD;IACxE;IACA,YAAY,KAAK,OAAO;IACxB,WAAW,KAAK,MAAM,YAAY,IAAI,IAAI,aAAa;GACzD,CAAC;GACD,IAAI,KAAK,OAAO,WAAW,GAAG;GAE9B,MAAM,SAAS,KAAK,OAAO,KAAK,OAAO,SAAS,EAAE,CAAE;GAOpD,MAAM,WAAW,MAAM,KAAK,UAAU,QAAQ;GAE9C,KAAK,MAAM,SAAS,KAAK,QAAQ;IAC/B,IAAI,CAAC,KAAK,YAAY,CAAC,KAAK,WAAW;IACvC,MAAM,KAAK,eAAe,mBAAmB,OAAO,QAAQ;GAC9D;GAEA,IAAI,kBAAkB,UAAU,OAAO,iBAAiB,QAAQ;GAChE,KAAK,UAAU,WAAW,qBAAqB,EAAE,cAAc,OAAO;GACtE,MAAM,KAAK,cAAc;GAGzB,IAAI,KAAK,OAAO,SAAS,iBAAiB;EAC5C;CACF;CAEA,MAAM,eACJ,mBACA,OACA,UACe;EACf,IAAI,MAAM,SAAS,SAAS,SAAS;EACrC,IAAI,CAAC,KAAK,qBAAqB;EAC/B,MAAM,QAAQ,mBAAmB,MAAM,QAAQ;EAC/C,IAAI,CAAC,OAAO;GACV,KAAK,MAAM,OAAO,KAAK,gEAAgE;IACrF;IACA,SAAS,MAAM;GACjB,CAAC;GACD;EACF;EASA,MAAM,YAAY,UAAU,MAAM;EAClC,IAAI,aAAa;EACjB,KAAK,MAAM,WAAW,UAAU;GAC9B,IAAI;GACJ,IAAI;IACF,QAAQ,MAAM,KAAK,WAAW,KAAK;KAAE,OAAO,QAAQ;KAAO,kBAAkB,QAAQ;IAAG,CAAC;GAC3F,SAAS,OAAO;IACd,KAAK,MAAM,OAAO,MAAM,2CAA2C;KACjE;KACA,SAAS,MAAM;KACf,WAAW,QAAQ;KACnB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC9D,CAAC;IACD;GACF;GAOA,IAAI,CANW,MAAM,MACnB,SACE,KAAK,gBAAgB,kBAAkB,YACvC,KAAK,eAAe,SAAS,WAC7B,KAAK,eAAe,eAAe,SAE7B,GAAG;GACb,IAAI;IACF,MAAM,KAAK,oBAAoB;KAC7B,OAAO,QAAQ;KACf,QAAQ;KACR,kBAAkB,QAAQ;KAC1B,QAAQ,CAAC,KAAK;IAChB,CAAC;IACD,cAAc;GAChB,SAAS,OAAO;IACd,KAAK,MAAM,OAAO,MAAM,uCAAuC;KAC7D;KACA,SAAS,MAAM;KACf,WAAW,QAAQ;KACnB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC9D,CAAC;GACH;EACF;EACA,IAAI,eAAe,GACjB,KAAK,MAAM,OAAO,MAAM,kEAAkE;GACxF;GACA,SAAS,MAAM;GACf;EACF,CAAC;CAEL;CAEA,MAAM,gBAA+B;EACnC,MAAM,KAAK,SAAS,SAAS,KAAK,eAAe,gBAAgB,KAAK,SAAS;CACjF;AACF;AAEA,SAAS,iBAAiB,QAA+B;CACvD,MAAM,cAAe,OAA2E;CAChG,IAAI,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,MAAM,KAAK;CAC1E,OAAO,gBAAgB,MAAM,IAAI,SAAS;AAC5C;AAEA,SAAS,kBAAkB,OAAoF;CAC7G,IAAI,CAAC,SAAS,MAAM,YAAY,KAAK,CAAC,MAAM,cAAc,OAAO,MAAM,eAAe,UACpF,OAAO;EAAE,SAAS;EAAG,YAAY,CAAC;CAAE;CAEtC,OAAO;EAAE,SAAS;EAAG,YAAY,EAAE,GAAG,MAAM,WAAW;CAAE;AAC3D;AAEA,SAAS,WAAW,OAAgB,YAA4B;CAC9D,IAAI,iBAAiB,oBAAoB,MAAM,WAAW,OAAO,MAAM,sBAAsB,MAC3F,OAAO,KAAK,IAAI,YAAY,MAAM,oBAAoB,GAAK;CAE7D,OAAO;AACT;;;;;;AAOA,SAAS,mBAAmB,UAAiE;CAC3F,MAAM,OAAO,SAAS;CACtB,IAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAA;CACrE,MAAM,MAAM;CACZ,MAAM,KAAK,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK,KAAA;CACjD,MAAM,aAAa,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa,KAAA;CACzE,MAAM,QAAQ,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,KAAA;CAC1D,MAAM,MAAM,OAAO,IAAI,QAAQ,WAAW,IAAI,MAAO,SAAS,OAAO,KAAA;CACrE,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,SAAS,CAAC,KAAK,OAAO,KAAA;CAEjD,MAAM,QAAQ,eAAe,IAAI,KAAK;CACtC,MAAM,OAAO,eAAe,IAAI,IAAI;CACpC,MAAM,WAAW,eAAe,IAAI,QAAQ;CAC5C,MAAM,UAAU,eAAe,IAAI,IAAI,KAAK,eAAe,IAAI,OAAO;CACtE,MAAM,SAAS,MAAM,QAAQ,IAAI,MAAM,IACnC,IAAI,OAAO,SAAQ,UAAS;EAC1B,MAAM,MAAM,eAAe,KAAK;EAChC,OAAO,OAAO,OAAO,IAAI,SAAS,WAAW,CAAC,IAAI,IAAI,IAAI,CAAC;CAC7D,CAAC,IACD,CAAC;CAEL,MAAM,YAAY,OAAO,IAAI,cAAc,WAAW,IAAI,YAAa,SAAS,aAAa;CAC7F,MAAM,YAAY,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;CAEtE,OAAO;EACL;EACA;EACA;EACA;EACA,QAAQ,SAAS,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,SAAS;EACxE,YAAY,SAAS,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,SAAS;EAC5E,eAAe,OAAO,IAAI,kBAAkB,WAAW,IAAI,gBAAgB;EAC3E,UAAU,YAAY,OAAO,SAAS,SAAS,WAAW,SAAS,OAAO;EAC1E,SAAS,WAAW,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO;EACtE,MAAM,QAAQ,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;EAC1D;EACA;EACA;CACF;AACF;AAEA,SAAS,eAAe,OAAqD;CAC3E,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG,OAAO,KAAA;CACxE,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"integration.js","names":["#client","#endpointHost","#workflowStatesByTeam","#resolveIntakeDispatch","#listProjectSources","#listTeamSources","#listIssues","#findIssue","#loadComments","#resolveWorkspaceForIssue","#listWorkflowStates","#rules","#projects","#auth","#listWorkspaces","#connectRoute","#candidateWorkspaceIds"],"sources":["../../../../src/integrations/platform/linear/integration.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { MastraWorker } from '@mastra/core/worker';\nimport type { Context } from 'hono';\n\nimport type { IntegrationConnection } from '../../../capabilities/connection.js';\nimport type { Intake, IntakeIssue, IntakeIssueDetail, UpdateIntakeIssueInput } from '../../../capabilities/intake.js';\nimport type { RouteAuth } from '../../../routes/route.js';\nimport type { FactoryProjectsStorage } from '../../../storage/domains/projects/base.js';\nimport type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../../base.js';\nimport { buildLinearAgentTools } from '../../linear/agent-tools.js';\nimport type { LinearEventRules, LinearRuleOverrides } from '../../linear/default-rules.js';\nimport { resolveLinearRules } from '../../linear/default-rules.js';\nimport type {\n LinearConnectionCheck,\n LinearIntegration,\n LinearIssueDetail as LinearRouteIssueDetail,\n} from '../../linear/integration.js';\nimport { attachLinearIssueReconciler } from '../../linear/issue-reconciler.js';\nimport {\n linearIssueReconciliationEnabled,\n linearIssueReconciliationInterval,\n} from '../../linear/reconciliation-config.js';\nimport { buildLinearRoutes } from '../../linear/routes.js';\nimport { attachLinearRules } from '../../linear/rules.js';\nimport type { LinearConnectionData, LinearConnectionRow, LinearStorageHandle } from '../../linear/storage.js';\nimport {\n logPlatformInfo,\n logPlatformWarn,\n PlatformApiClient,\n PlatformApiError,\n platformApiClientConfigFromEnv,\n} from '../api-client.js';\nimport { PlatformLinearEventWorker } from './event-worker.js';\nimport type { PlatformLinearEventStorage } from './event-worker.js';\n\ntype PageInfo = { hasNextPage: boolean; endCursor: string | null };\ntype LinearUser = {\n id: string;\n name: string;\n displayName: string;\n email: string | null;\n avatarUrl: string | null;\n};\ntype LinearIssue = {\n id: string;\n identifier: string;\n number: number;\n title: string;\n description: string | null;\n url: string;\n priority: number;\n priorityLabel: string;\n labels: Array<{ id: string; name: string }>;\n state: { id: string; name: string; type: string };\n team: { id: string; key: string; name: string };\n project: { id: string } | null;\n assignee: LinearUser | null;\n creator: LinearUser | null;\n createdAt: string;\n updatedAt: string;\n archivedAt: string | null;\n};\ntype LinearComment = {\n id: string;\n body: string;\n url: string;\n issue: { id: string; identifier: string };\n user: LinearUser | null;\n parent: { id: string } | null;\n createdAt: string;\n updatedAt: string;\n};\ntype LinearWorkspace = {\n linearWorkspaceId: string;\n linearWorkspaceName: string;\n urlKey: string | null;\n connected: boolean;\n};\ntype LinearProject = {\n id: string;\n name: string;\n state: string;\n teams: Array<{ id: string; key: string; name: string }>;\n};\ntype LinearTeam = { id: string; key: string; name: string };\ntype ProjectSource = { workspace: LinearWorkspace; project: LinearProject };\ntype TeamSource = { workspace: LinearWorkspace; team: LinearTeam };\ntype LinearWorkflowState = {\n id: string;\n name: string;\n type: string;\n position: number;\n teamId: string;\n};\n\nconst API_PREFIX = '/v1/server/linear';\nconst PAGE_SIZE = 30;\nconst MAX_REFERENCE_PAGES = 20;\nconst MAX_COMMENT_PAGES = 20;\nconst PLATFORM_MANAGED_CONNECTION_TOKEN = 'platform-managed';\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\nfunction routeBaseUrl(ctx: IntegrationContext, requestUrl: string): string {\n return (ctx.baseUrl || new URL(requestUrl).origin).replace(/\\/+$/, '');\n}\n\nexport class PlatformLinearIntegration implements FactoryIntegration {\n readonly id = 'linear';\n readonly #client: PlatformApiClient;\n readonly #endpointHost: string;\n #projects: FactoryProjectsStorage | undefined;\n #auth: RouteAuth | undefined;\n /**\n * Per-instance workflow-state cache keyed by `${workspaceId}:${teamId}`. Scoped to the process\n * lifetime; not shared across requests intentionally to keep failure modes simple (stale states\n * clear on process restart). Populated lazily on first `updateIssue` per team.\n */\n readonly #workflowStatesByTeam = new Map<string, LinearWorkflowState[]>();\n\n readonly intake: Intake = {\n resolveIntakeDispatch: input => this.#resolveIntakeDispatch(input),\n listSources: async () => {\n const [projectSources, teamSources] = await Promise.all([this.#listProjectSources(), this.#listTeamSources()]);\n const projects = projectSources.map(({ workspace, project }) => ({\n id: encodeSourceId(workspace.linearWorkspaceId, project.id),\n name: project.name,\n type: 'project' as const,\n metadata: {\n workspaceId: workspace.linearWorkspaceId,\n workspaceName: workspace.linearWorkspaceName,\n workspaceUrlKey: workspace.urlKey,\n state: project.state,\n teams: project.teams,\n },\n }));\n const teams = teamSources.map(({ workspace, team }) => ({\n id: encodeTeamSourceId(workspace.linearWorkspaceId, team.id),\n name: team.name,\n type: 'team' as const,\n metadata: {\n workspaceId: workspace.linearWorkspaceId,\n workspaceName: workspace.linearWorkspaceName,\n workspaceUrlKey: workspace.urlKey,\n teamKey: team.key,\n },\n }));\n return [...projects, ...teams];\n },\n listItems: async ({ sourceIds, attributionSourceIds, cursor }) => {\n const result = await this.#listIssues(sourceIds, cursor, undefined, attributionSourceIds);\n return {\n items: result.issues.map(({ issue, sourceId, workspace }) => ({\n source: { type: 'issue', externalId: issue.id, url: issue.url },\n sourceId,\n title: issue.title,\n status: issue.state.name,\n labels: issue.labels.map(label => label.name),\n assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n metadata: {\n identifier: issue.identifier,\n workspaceId: workspace.linearWorkspaceId,\n workspaceName: workspace.linearWorkspaceName,\n projectId: issue.project?.id ?? null,\n team: issue.team.key,\n priority: issue.priorityLabel,\n },\n })),\n nextCursor: result.nextCursor,\n };\n },\n listIssues: async ({ connection, sourceIds, attributionSourceIds, labels, cursor }) => {\n requireLinearConnection(connection);\n const result = await this.#listIssues(sourceIds, cursor, labels, attributionSourceIds);\n return {\n issues: result.issues.map(({ issue, sourceId, workspace }) => ({\n ...parseIssue(issue, workspace.linearWorkspaceId),\n sourceId,\n })),\n nextCursor: result.nextCursor,\n };\n },\n getIssue: async ({ connection, sourceId, issueId }) => {\n requireLinearConnection(connection);\n const located = await this.#findIssue(sourceId, issueId);\n if (!located) return null;\n const comments = await this.#loadComments(located.workspaceId, issueId, located.issue.comments);\n return parseIssueDetail(located.issue, comments, located.workspaceId);\n },\n createComment: async ({ connection, sourceId, issueId, body }) => {\n requireLinearConnection(connection);\n const workspaceId = await this.#resolveWorkspaceForIssue(sourceId, issueId);\n if (!workspaceId) return null;\n try {\n const comment = await this.#client.request<LinearComment>(\n 'POST',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments`,\n { body },\n );\n return { id: comment.id, url: comment.url };\n } catch (error) {\n if (isNotFound(error)) return null;\n throw error;\n }\n },\n updateIssue: async (input: UpdateIntakeIssueInput): Promise<IntakeIssue | null> => {\n requireLinearConnection(input.connection);\n const located = await this.#findIssue(input.sourceId, input.issueId);\n if (!located) return null;\n const { workspaceId, issue } = located;\n\n const target = input.state;\n // Idempotency: skip the write entirely if the issue is already at the target state.\n if (target.kind === 'byType' && issue.state.type === target.stateType) {\n return parseIssue(issue, workspaceId);\n }\n if (target.kind === 'byName' && issue.state.name.toLowerCase() === target.name.toLowerCase()) {\n return parseIssue(issue, workspaceId);\n }\n\n let states: LinearWorkflowState[];\n try {\n states = await this.#listWorkflowStates(workspaceId, issue.team.id);\n } catch (error) {\n if (isNotFound(error)) {\n // Platform companion endpoint not deployed yet — degrade to policy skip so PR 1\n // is standalone-mergeable ahead of the platform-side workflow-states route landing.\n logPlatformWarn('Platform Linear: workflow-states endpoint not available; skipping updateIssue.', {\n workspaceId,\n teamId: issue.team.id,\n });\n return null;\n }\n throw error;\n }\n let matched: LinearWorkflowState | undefined;\n if (target.kind === 'byType') {\n matched = states\n .slice()\n .sort((a, b) => a.position - b.position)\n .find(s => s.type === target.stateType);\n } else {\n const wanted = target.name.toLowerCase();\n matched = states.find(s => s.name.toLowerCase() === wanted);\n }\n if (!matched) {\n logPlatformWarn('Platform Linear: no workflow state matched target; skipping updateIssue.', {\n workspaceId,\n teamId: issue.team.id,\n target,\n });\n return null;\n }\n\n try {\n await this.#client.request<LinearIssue>(\n 'PATCH',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(input.issueId)}`,\n { stateId: matched.id },\n );\n } catch (error) {\n if (isNotFound(error)) return null;\n throw error;\n }\n\n const refreshed = await this.#findIssue(input.sourceId, input.issueId);\n return refreshed ? parseIssue(refreshed.issue, refreshed.workspaceId) : null;\n },\n };\n\n readonly #rules: LinearEventRules;\n\n get rules(): LinearEventRules {\n return this.#rules;\n }\n\n constructor({ rules }: { rules?: LinearRuleOverrides } = {}) {\n this.#rules = resolveLinearRules(rules);\n const config = platformApiClientConfigFromEnv();\n this.#client = new PlatformApiClient(config);\n this.#endpointHost = new URL(config.baseUrl).host;\n }\n\n get storage(): LinearStorageHandle {\n const now = new Date();\n return {\n integrationId: this.id,\n connections: {\n get: async (orgId: string) => ({\n id: `platform-linear:${orgId}`,\n orgId,\n userId: null,\n data: {\n accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,\n refreshToken: null,\n expiresAtMs: null,\n scope: 'read,comments:create',\n workspaceName: null,\n workspaceUrlKey: null,\n } satisfies LinearConnectionData,\n metadata: {},\n createdAt: now,\n updatedAt: now,\n }),\n },\n } as unknown as LinearStorageHandle;\n }\n\n get projects(): FactoryProjectsStorage {\n if (!this.#projects) throw new Error('PlatformLinearIntegration projects storage has not been initialized.');\n return this.#projects;\n }\n\n initialize({ projects, auth }: { projects: FactoryProjectsStorage; auth?: RouteAuth }): void {\n this.#projects = projects;\n this.#auth = auth;\n logPlatformInfo('Platform Linear integration initialized', { endpointHost: this.#endpointHost });\n }\n\n get authEnabled(): boolean {\n return this.#auth?.enabled() ?? false;\n }\n\n async resolveOrgId(resourceId: string): Promise<string | null> {\n try {\n const project = await this.projects.getById({ id: resourceId });\n return project?.orgId ?? null;\n } catch {\n return null;\n }\n }\n\n async loadConnection(orgId: string): Promise<LinearConnectionRow | null> {\n const workspace = (await this.#listWorkspaces())[0];\n if (!workspace) return null;\n const now = new Date();\n return {\n id: `platform-linear:${orgId}`,\n orgId,\n userId: null,\n accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,\n scope: 'read,comments:create',\n refreshToken: null,\n expiresAt: null,\n workspaceName: workspace.linearWorkspaceName,\n workspaceUrlKey: workspace.urlKey,\n createdAt: now,\n updatedAt: now,\n };\n }\n\n async getFreshAccessToken(_connection: LinearConnectionRow): Promise<string> {\n return PLATFORM_MANAGED_CONNECTION_TOKEN;\n }\n\n /**\n * Background-dispatch context: platform-managed connection token when the\n * org has a connected workspace. Linear work items store the issue UUID\n * directly as `externalId`.\n */\n async #resolveIntakeDispatch({\n orgId,\n externalSource,\n }: {\n orgId: string;\n externalSource: { type: string; externalId: string };\n }): Promise<{ connection: IntegrationConnection; issueId: string } | null> {\n if (externalSource.type !== 'issue') return null;\n const connection = await this.loadConnection(orgId);\n if (!connection) return null;\n return {\n connection: { type: 'oauth', accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN },\n issueId: externalSource.externalId,\n };\n }\n\n canPostComments(connection: LinearConnectionRow): boolean {\n const scopes = (connection.scope ?? '').split(/[\\s,]+/).filter(Boolean);\n return scopes.some(scope => scope === 'comments:create' || scope === 'write' || scope === 'admin');\n }\n\n async checkConnection(orgId: string): Promise<LinearConnectionCheck> {\n const connection = await this.loadConnection(orgId);\n return {\n connected: connection !== null,\n canComment: connection !== null && this.canPostComments(connection),\n checkedAt: Date.now(),\n };\n }\n\n workers(ctx: IntegrationContext): MastraWorker[] {\n const pollingEnabled = process.env.MASTRACODE_PLATFORM_LINEAR_POLLING_ENABLED?.trim().toLowerCase() !== 'false';\n const reconcileEnabled = linearIssueReconciliationEnabled();\n if (!pollingEnabled && !reconcileEnabled) return [];\n\n const ingest = attachLinearRules(this, ctx);\n const reconcile = reconcileEnabled ? attachLinearIssueReconciler(this, ctx) : undefined;\n const workItems = ctx.runtime?.workItems;\n if (!workItems) return [];\n if (!ingest && !reconcile) return [];\n\n const pollIntervalMs = optionalPositiveIntegerEnv('MASTRACODE_PLATFORM_LINEAR_POLLING_INTERVAL_MS');\n const reconcileIntervalMs = linearIssueReconciliationInterval();\n\n return [\n new PlatformLinearEventWorker({\n client: this.#client,\n linear: { listWorkspaces: () => this.listWorkspaces() },\n storage: ctx.storage.generic as unknown as PlatformLinearEventStorage,\n projects: ctx.storage.projects,\n workItems,\n ...(ingest ? { ingestFactoryIssue: ingest } : {}),\n ...(reconcile ? { reconcileFactoryState: reconcile } : {}),\n pollEventsEnabled: pollingEnabled,\n ...(pollIntervalMs !== undefined ? { intervalMs: pollIntervalMs } : {}),\n ...(reconcileIntervalMs !== undefined ? { reconcileIntervalMs } : {}),\n }),\n ];\n }\n\n routes(ctx: IntegrationContext): ApiRoute[] {\n return [\n this.#connectRoute(ctx),\n ...buildLinearRoutes({\n auth: ctx.auth,\n linear: this as unknown as LinearIntegration,\n stateSigner: ctx.stateSigner,\n baseUrl: ctx.baseUrl,\n intake: ctx.storage.intake,\n projects: ctx.storage.projects,\n ingestFactoryIssues: attachLinearRules(this, ctx),\n workItems: ctx.runtime?.workItems,\n boards: ctx.runtime?.boards,\n }).filter(route => !route.path.startsWith('/auth/linear/')),\n ];\n }\n\n #connectRoute(ctx: IntegrationContext): ApiRoute {\n return registerApiRoute('/auth/linear/connect', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n await ctx.auth.ensureUser(loose(c));\n const tenant = ctx.auth.tenant(loose(c));\n if (!tenant?.orgId) return c.json({ error: 'unauthorized' }, 401);\n\n const returnTo = c.req.query('return_to') || '/';\n const originator = routeBaseUrl(ctx, c.req.url);\n logPlatformInfo('Starting Platform Linear connect flow', {\n orgId: tenant.orgId,\n returnTo,\n originator,\n });\n const query = new URLSearchParams({ return_to: returnTo, originator });\n const location = await this.#client.requestRedirect('GET', `${API_PREFIX}/authorize?${query}`);\n return c.redirect(location);\n },\n });\n }\n\n async agentTools({ requestContext }: { requestContext: RequestContext }): Promise<IntegrationTools> {\n return buildLinearAgentTools({ requestContext, linear: this as unknown as LinearIntegration });\n }\n\n diagnostics(): Record<string, unknown> {\n return { mode: 'platform', endpointHost: this.#endpointHost };\n }\n\n async listProjects(): Promise<Array<LinearProject & { workspaceId: string }>> {\n return (await this.#listProjectSources()).map(({ workspace, project }) => ({\n ...project,\n id: encodeSourceId(workspace.linearWorkspaceId, project.id),\n workspaceId: workspace.linearWorkspaceId,\n }));\n }\n\n async listTeams(_accessToken: string): Promise<Array<LinearTeam & { workspaceId: string; sourceId: string }>> {\n return (await this.#listTeamSources()).map(({ workspace, team }) => ({\n ...team,\n workspaceId: workspace.linearWorkspaceId,\n sourceId: encodeTeamSourceId(workspace.linearWorkspaceId, team.id),\n }));\n }\n\n async fetchIssueDetail(\n _accessToken: string,\n idOrIdentifier: string,\n sourceIds?: string[],\n routedSourceIds?: string[],\n ): Promise<LinearRouteIssueDetail | null> {\n const located = await this.#findIssue(undefined, idOrIdentifier, sourceIds, routedSourceIds);\n if (!located) return null;\n const comments = await this.#loadComments(located.workspaceId, located.issue.id, located.issue.comments);\n const issue = located.issue;\n return {\n id: issue.id,\n projectId: issue.project?.id ?? null,\n workspaceId: located.workspaceId,\n teamId: issue.team.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description?.trim() ? issue.description : null,\n url: issue.url,\n state: issue.state.name,\n stateType: issue.state.type,\n priorityLabel: issue.priorityLabel,\n assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,\n creator: issue.creator?.displayName ?? issue.creator?.name ?? null,\n team: issue.team.key,\n labels: issue.labels.map(label => label.name),\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n comments: comments.map(comment => ({\n author: comment.user?.displayName ?? comment.user?.name ?? null,\n body: comment.body,\n createdAt: comment.createdAt,\n })),\n };\n }\n\n sourceMatchesIssue(\n sourceId: string,\n issue: Pick<LinearRouteIssueDetail, 'workspaceId' | 'projectId' | 'teamId'>,\n ): boolean {\n const source = parseSourceId(sourceId);\n if (issue.workspaceId !== source.workspaceId) return false;\n return source.kind === 'team' ? issue.teamId === source.teamId : issue.projectId === source.projectId;\n }\n\n async #listProjectSources(workspaceIds?: ReadonlySet<string>): Promise<ProjectSource[]> {\n const workspaces = await this.#listWorkspaces();\n const scopedWorkspaces = workspaceIds\n ? workspaces.filter(workspace => workspaceIds.has(workspace.linearWorkspaceId))\n : workspaces;\n const projectGroups = await Promise.all(\n scopedWorkspaces.map(async workspace => {\n const projects: LinearProject[] = [];\n let after: string | undefined;\n for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {\n const query = new URLSearchParams({ first: '200' });\n if (after) query.set('after', after);\n const result = await this.#client.request<{ projects: LinearProject[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/projects?${query}`,\n );\n projects.push(...result.projects);\n if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;\n // A page that hands back the cursor it was asked for would replay forever.\n if (result.pageInfo.endCursor === after) throw invalidLinearCursor();\n after = result.pageInfo.endCursor;\n }\n return projects.map(project => ({ workspace, project }));\n }),\n );\n return projectGroups.flat();\n }\n\n async #listTeamSources(workspaceIds?: ReadonlySet<string>): Promise<TeamSource[]> {\n const workspaces = await this.#listWorkspaces();\n const scopedWorkspaces = workspaceIds\n ? workspaces.filter(workspace => workspaceIds.has(workspace.linearWorkspaceId))\n : workspaces;\n const teamGroups = await Promise.all(\n scopedWorkspaces.map(async workspace => {\n const teams: LinearTeam[] = [];\n let after: string | undefined;\n for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {\n const query = new URLSearchParams({ first: '200' });\n if (after) query.set('after', after);\n const result = await this.#client.request<{ teams: LinearTeam[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/teams?${query}`,\n );\n teams.push(...result.teams);\n if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;\n // A page that hands back the cursor it was asked for would replay forever.\n if (result.pageInfo.endCursor === after) throw invalidLinearCursor();\n after = result.pageInfo.endCursor;\n }\n return teams.map(team => ({ workspace, team }));\n }),\n );\n return teamGroups.flat();\n }\n\n async listWorkspaces(): Promise<LinearWorkspace[]> {\n return this.#listWorkspaces();\n }\n\n async #listWorkspaces(): Promise<LinearWorkspace[]> {\n const result = await this.#client.request<{ workspaces: LinearWorkspace[] }>('GET', `${API_PREFIX}/workspaces`);\n return result.workspaces.filter(workspace => workspace.connected);\n }\n\n async #listIssues(sourceIds: string[], cursor?: string, labels?: string[], attributionSourceIds = sourceIds) {\n type ListedIssue = { issue: LinearIssue; sourceId: string; workspace: LinearWorkspace };\n if (sourceIds.length === 0) return { issues: [] as ListedIssue[], nextCursor: null };\n\n // Resolve every selected source to a concrete listing descriptor. Project\n // and team sources are listed separately: a project source filters by\n // `projectIds`, a team source filters by `teamId` (and returns projectless\n // issues too). Filters are never combined on one request.\n const parsedSources = sourceIds.map(sourceId => parseSourceId(sourceId));\n const projectWorkspaceIds = new Set(\n parsedSources.flatMap(source => (source.kind === 'project' ? [source.workspaceId] : [])),\n );\n const teamWorkspaceIds = new Set(\n parsedSources.flatMap(source => (source.kind === 'team' ? [source.workspaceId] : [])),\n );\n const [projectSources, teamSources] = await Promise.all([\n projectWorkspaceIds.size > 0\n ? this.#listProjectSources(projectWorkspaceIds)\n : Promise.resolve([] as ProjectSource[]),\n teamWorkspaceIds.size > 0 ? this.#listTeamSources(teamWorkspaceIds) : Promise.resolve([] as TeamSource[]),\n ]);\n const projectMap = new Map(\n projectSources.map(source => [encodeSourceId(source.workspace.linearWorkspaceId, source.project.id), source]),\n );\n const teamMap = new Map(\n teamSources.map(source => [encodeTeamSourceId(source.workspace.linearWorkspaceId, source.team.id), source]),\n );\n\n type Descriptor = {\n kind: 'project' | 'team';\n sourceId: string;\n workspace: LinearWorkspace;\n query: URLSearchParams;\n };\n const descriptors: Descriptor[] = [];\n const selectedProjectIdsByWorkspace = new Map<string, Set<string>>();\n for (const attributionSourceId of attributionSourceIds) {\n const source = parseSourceId(attributionSourceId);\n if (source.kind !== 'project') continue;\n const selectedProjectIds = selectedProjectIdsByWorkspace.get(source.workspaceId) ?? new Set<string>();\n selectedProjectIds.add(source.projectId);\n selectedProjectIdsByWorkspace.set(source.workspaceId, selectedProjectIds);\n }\n for (const sourceId of sourceIds) {\n const projectSource = projectMap.get(sourceId);\n if (projectSource) {\n const query = new URLSearchParams({\n first: String(PAGE_SIZE),\n projectIds: projectSource.project.id,\n stateType: 'triage,backlog,unstarted,started',\n orderBy: 'updatedAt',\n });\n const workspaceId = projectSource.workspace.linearWorkspaceId;\n const selectedProjectIds = selectedProjectIdsByWorkspace.get(workspaceId) ?? new Set<string>();\n selectedProjectIds.add(projectSource.project.id);\n selectedProjectIdsByWorkspace.set(workspaceId, selectedProjectIds);\n descriptors.push({ kind: 'project', sourceId, workspace: projectSource.workspace, query });\n continue;\n }\n const teamSource = teamMap.get(sourceId);\n if (teamSource) {\n const query = new URLSearchParams({\n first: String(PAGE_SIZE),\n teamId: teamSource.team.id,\n stateType: 'triage,backlog,unstarted,started',\n orderBy: 'updatedAt',\n });\n descriptors.push({ kind: 'team', sourceId, workspace: teamSource.workspace, query });\n }\n }\n\n const cursors = await decodeCursor(cursor, sourceIds, attributionSourceIds);\n const normalizedLabels = normalizeLabels(labels);\n const nextState: Record<string, string | null> = {};\n let hasNextPage = false;\n const pages = await Promise.all(\n descriptors.map(async ({ kind, sourceId, workspace, query }) => {\n if (cursors[sourceId] === null) {\n nextState[sourceId] = null;\n return [] as ListedIssue[];\n }\n const after = cursors[sourceId];\n if (after) query.set('after', after);\n const result = await this.#client.request<{ issues: LinearIssue[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/issues?${query}`,\n );\n const next = result.pageInfo.hasNextPage ? result.pageInfo.endCursor : null;\n // A page that hands back the cursor it was asked for would replay forever.\n if (next !== null && next === after) throw invalidLinearCursor();\n nextState[sourceId] = next;\n hasNextPage ||= next !== null;\n return result.issues\n .filter(\n issue => normalizedLabels.length === 0 || issue.labels.some(label => normalizedLabels.includes(label.name)),\n )\n .filter(\n issue =>\n kind === 'project' ||\n issue.project === null ||\n !selectedProjectIdsByWorkspace.get(workspace.linearWorkspaceId)?.has(issue.project.id),\n )\n .map(issue => ({ issue, sourceId, workspace }));\n }),\n );\n return {\n issues: dedupeIssuesBySource(pages.flat(), sourceIds),\n nextCursor: hasNextPage ? await encodeCursor(nextState, sourceIds, attributionSourceIds) : null,\n };\n }\n\n async #findIssue(\n sourceId: string | undefined,\n issueId: string,\n sourceIds?: string[],\n routedSourceIds?: string[],\n ): Promise<{\n workspaceId: string;\n issue: LinearIssue & { comments?: { nodes: LinearComment[]; pageInfo: PageInfo } };\n } | null> {\n const scopedSourceIds = sourceIds ?? (sourceId ? [sourceId] : undefined);\n const scopedSources = scopedSourceIds?.map(sourceKey => ({\n sourceKey,\n source: parseSourceId(sourceKey),\n }));\n const routedSourceSet = routedSourceIds ? new Set(routedSourceIds) : undefined;\n const workspaceIds = routedSourceIds\n ? [...new Set(routedSourceIds.map(sourceKey => parseSourceId(sourceKey).workspaceId))]\n : scopedSources\n ? [...new Set(scopedSources.map(({ source }) => source.workspaceId))]\n : await this.#candidateWorkspaceIds(undefined);\n for (const workspaceId of workspaceIds) {\n try {\n const issue = await this.#client.request<\n LinearIssue & { comments?: { nodes: LinearComment[]; pageInfo: PageInfo } }\n >(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}?include=comments`,\n );\n if (scopedSources) {\n const matching = scopedSources.filter(\n ({ source }) =>\n source.workspaceId === workspaceId &&\n (source.kind === 'team' ? issue.team.id === source.teamId : issue.project?.id === source.projectId),\n );\n const winner = matching.find(({ source }) => source.kind === 'project') ?? matching[0];\n if (!winner || (routedSourceSet && !routedSourceSet.has(winner.sourceKey))) continue;\n }\n return { workspaceId, issue };\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n }\n return null;\n }\n\n async #resolveWorkspaceForIssue(sourceId: string | undefined, issueId: string): Promise<string | null> {\n const workspaceIds = await this.#candidateWorkspaceIds(sourceId);\n if (workspaceIds.length === 1) return workspaceIds[0]!;\n for (const workspaceId of workspaceIds) {\n try {\n await this.#client.request<LinearIssue>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}`,\n );\n return workspaceId;\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n }\n return null;\n }\n\n async #listWorkflowStates(workspaceId: string, teamId: string): Promise<LinearWorkflowState[]> {\n const cacheKey = `${workspaceId}:${teamId}`;\n const cached = this.#workflowStatesByTeam.get(cacheKey);\n if (cached) return cached;\n const result = await this.#client.request<{ workflowStates: LinearWorkflowState[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/workflow-states?teamId=${encodeURIComponent(teamId)}&first=100`,\n );\n this.#workflowStatesByTeam.set(cacheKey, result.workflowStates);\n return result.workflowStates;\n }\n\n async #candidateWorkspaceIds(sourceId: string | undefined): Promise<string[]> {\n if (sourceId) return [parseSourceId(sourceId).workspaceId];\n return (await this.#listWorkspaces()).map(workspace => workspace.linearWorkspaceId);\n }\n\n async #loadComments(\n workspaceId: string,\n issueId: string,\n embedded: { nodes: LinearComment[]; pageInfo: PageInfo } | undefined,\n ): Promise<LinearComment[]> {\n const comments = [...(embedded?.nodes ?? [])];\n let pageInfo = embedded?.pageInfo;\n let page = 0;\n while (pageInfo?.hasNextPage && pageInfo.endCursor && page < MAX_COMMENT_PAGES) {\n const result = await this.#client.request<{ comments: LinearComment[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments?first=200&after=${encodeURIComponent(pageInfo.endCursor)}`,\n );\n comments.push(...result.comments);\n if (result.pageInfo.hasNextPage && result.pageInfo.endCursor === pageInfo.endCursor) throw invalidLinearCursor();\n pageInfo = result.pageInfo;\n page += 1;\n }\n return comments;\n }\n}\n\nfunction parseIssue(issue: LinearIssue, workspaceId: string): IntakeIssue {\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n author: issue.creator?.displayName ?? issue.creator?.name ?? null,\n state: issue.state.name,\n stateType: issue.state.type,\n priority: issue.priorityLabel,\n assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,\n source: issue.team.key,\n // Use the same workspace-scoped identity as listProjects and intake bindings.\n // The source may be a team, so it cannot stand in for the issue's project.\n projectId: issue.project ? encodeSourceId(workspaceId, issue.project.id) : null,\n labels: issue.labels.map(label => label.name),\n commentCount: null,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n };\n}\n\nfunction parseIssueDetail(issue: LinearIssue, comments: LinearComment[], workspaceId: string): IntakeIssueDetail {\n return {\n ...parseIssue(issue, workspaceId),\n commentCount: comments.length,\n description: issue.description?.trim() ? issue.description : null,\n comments: comments.map(comment => ({\n author: comment.user?.displayName ?? comment.user?.name ?? null,\n body: comment.body,\n createdAt: comment.createdAt,\n })),\n };\n}\n\n/**\n * Deduplicate issues that surfaced from more than one selected source (a team\n * source and one of its projects both select the same issue). Keeps one entry\n * per Linear issue UUID.\n *\n * Tie-breaker — MOST-SPECIFIC-WINS: a project source beats a team source, so an\n * issue that belongs to a selected project is attributed to that project's\n * source (and therefore routes to the project's board). The team source covers\n * only the remainder — projectless issues and issues in projects that were not\n * separately selected. `selectionOrder` breaks a same-kind tie deterministically\n * by the order the sources were selected.\n *\n * This function is the single precedence point; change the comparison here to\n * change the routing policy.\n */\nfunction dedupeIssuesBySource<T extends { issue: { id: string }; sourceId: string }>(\n entries: T[],\n selectionOrder: string[],\n): T[] {\n const rank = new Map(selectionOrder.map((id, index) => [id, index]));\n const specificity = (sourceId: string): number => (parseSourceId(sourceId).kind === 'project' ? 0 : 1);\n const winners = new Map<string, T>();\n for (const entry of entries) {\n const existing = winners.get(entry.issue.id);\n if (!existing) {\n winners.set(entry.issue.id, entry);\n continue;\n }\n const bySpecificity = specificity(entry.sourceId) - specificity(existing.sourceId);\n const better =\n bySpecificity < 0 ||\n (bySpecificity === 0 &&\n (rank.get(entry.sourceId) ?? Number.MAX_SAFE_INTEGER) <\n (rank.get(existing.sourceId) ?? Number.MAX_SAFE_INTEGER));\n if (better) winners.set(entry.issue.id, entry);\n }\n // Preserve first-seen order of the surviving issues.\n const seen = new Set<string>();\n const ordered: T[] = [];\n for (const entry of entries) {\n if (seen.has(entry.issue.id)) continue;\n seen.add(entry.issue.id);\n ordered.push(winners.get(entry.issue.id)!);\n }\n return ordered;\n}\n\nexport function encodeSourceId(workspaceId: string, projectId: string): string {\n return `linear-project:${Buffer.from(JSON.stringify({ workspaceId, projectId })).toString('base64url')}`;\n}\n\nfunction decodeSourceId(sourceId: string): { workspaceId: string; projectId: string } {\n if (!sourceId.startsWith('linear-project:')) throw new Error('Linear project source id is invalid.');\n try {\n const parsed = JSON.parse(Buffer.from(sourceId.slice('linear-project:'.length), 'base64url').toString('utf8')) as {\n workspaceId?: unknown;\n projectId?: unknown;\n };\n if (typeof parsed.workspaceId !== 'string' || !parsed.workspaceId) throw new Error();\n if (typeof parsed.projectId !== 'string' || !parsed.projectId) throw new Error();\n return { workspaceId: parsed.workspaceId, projectId: parsed.projectId };\n } catch {\n throw new Error('Linear project source id is invalid.');\n }\n}\n\nconst TEAM_SOURCE_PREFIX = 'linear-team:';\n\nexport function encodeTeamSourceId(workspaceId: string, teamId: string): string {\n return `${TEAM_SOURCE_PREFIX}${Buffer.from(JSON.stringify({ workspaceId, teamId })).toString('base64url')}`;\n}\n\nexport function decodeTeamSourceId(sourceId: string): { workspaceId: string; teamId: string } {\n if (!sourceId.startsWith(TEAM_SOURCE_PREFIX)) throw new Error('Linear team source id is invalid.');\n try {\n const parsed = JSON.parse(Buffer.from(sourceId.slice(TEAM_SOURCE_PREFIX.length), 'base64url').toString('utf8')) as {\n workspaceId?: unknown;\n teamId?: unknown;\n };\n if (typeof parsed.workspaceId !== 'string' || !parsed.workspaceId) throw new Error();\n if (typeof parsed.teamId !== 'string' || !parsed.teamId) throw new Error();\n return { workspaceId: parsed.workspaceId, teamId: parsed.teamId };\n } catch {\n throw new Error('Linear team source id is invalid.');\n }\n}\n\n/**\n * A decoded intake source: either a Linear project or a whole Linear team.\n * Downstream code discriminates on `kind` to build the right issue-listing\n * filter and to apply most-specific-wins routing (project beats team).\n */\nexport type DecodedSource =\n { kind: 'project'; workspaceId: string; projectId: string } | { kind: 'team'; workspaceId: string; teamId: string };\n\n/** Discriminate any Linear source id by its prefix and decode it. */\nexport function parseSourceId(sourceId: string): DecodedSource {\n if (sourceId.startsWith(TEAM_SOURCE_PREFIX)) {\n return { kind: 'team', ...decodeTeamSourceId(sourceId) };\n }\n return { kind: 'project', ...decodeSourceId(sourceId) };\n}\n\nfunction normalizeLabels(labels: string[] | undefined): string[] {\n return [...new Set((labels ?? []).map(label => label.trim()).filter(Boolean))];\n}\n\ntype PlatformListCursor = { v: 1; sourceSet: string; cursors: Array<string | null> };\n\nfunction canonicalSourceIds(sourceIds: string[]): string[] {\n return [...new Set(sourceIds)].sort();\n}\n\nasync function linearSourceSetFingerprint(sourceIds: string[], attributionSourceIds: string[]): Promise<string> {\n const scope = {\n sourceIds: canonicalSourceIds(sourceIds),\n attributionSourceIds: canonicalSourceIds(attributionSourceIds),\n };\n const bytes = new TextEncoder().encode(JSON.stringify(scope));\n return Buffer.from(await globalThis.crypto.subtle.digest('SHA-256', bytes)).toString('base64url');\n}\n\nfunction invalidLinearCursor(): Error {\n return Object.assign(new Error('Linear cursor is invalid or stale.'), { code: 'invalid_cursor' as const });\n}\n\nasync function decodeCursor(\n cursor: string | undefined,\n sourceIds: string[],\n attributionSourceIds: string[],\n): Promise<Record<string, string | null | undefined>> {\n if (!cursor) return {};\n try {\n const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')) as Partial<PlatformListCursor>;\n const canonical = canonicalSourceIds(sourceIds);\n if (\n parsed.v !== 1 ||\n parsed.sourceSet !== (await linearSourceSetFingerprint(sourceIds, attributionSourceIds)) ||\n !Array.isArray(parsed.cursors) ||\n parsed.cursors.length !== canonical.length ||\n !parsed.cursors.every(value => value === null || typeof value === 'string')\n ) {\n throw invalidLinearCursor();\n }\n return Object.fromEntries(canonical.map((sourceId, index) => [sourceId, parsed.cursors![index]]));\n } catch {\n throw invalidLinearCursor();\n }\n}\n\nasync function encodeCursor(\n state: Record<string, string | null>,\n sourceIds: string[],\n attributionSourceIds: string[],\n): Promise<string> {\n const canonical = canonicalSourceIds(sourceIds);\n const cursor: PlatformListCursor = {\n v: 1,\n sourceSet: await linearSourceSetFingerprint(sourceIds, attributionSourceIds),\n cursors: canonical.map(sourceId => state[sourceId] ?? null),\n };\n return Buffer.from(JSON.stringify(cursor)).toString('base64url');\n}\n\nfunction requireLinearConnection(connection: IntegrationConnection): void {\n if (connection.type !== 'oauth') {\n throw new Error('Linear capabilities require an OAuth connection.');\n }\n}\n\nfunction isNotFound(error: unknown): boolean {\n return error instanceof PlatformApiError && error.status === 404;\n}\n\nfunction optionalPositiveIntegerEnv(name: string): number | undefined {\n const value = process.env[name]?.trim();\n if (!value) return undefined;\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed <= 0) {\n throw new Error(`${name} must be a positive integer.`);\n }\n return parsed;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"integration.js","names":[],"sources":["../../../../src/integrations/platform/linear/integration.ts"],"sourcesContent":["import type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { MastraWorker } from '@mastra/core/worker';\nimport type { Context } from 'hono';\n\nimport type { IntegrationConnection } from '../../../capabilities/connection.js';\nimport type { Intake, IntakeIssue, IntakeIssueDetail, UpdateIntakeIssueInput } from '../../../capabilities/intake.js';\nimport type { RouteAuth } from '../../../routes/route.js';\nimport type { FactoryProjectsStorage } from '../../../storage/domains/projects/base.js';\nimport type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../../base.js';\nimport { buildLinearAgentTools } from '../../linear/agent-tools.js';\nimport type { LinearEventRules, LinearRuleOverrides } from '../../linear/default-rules.js';\nimport { resolveLinearRules } from '../../linear/default-rules.js';\nimport type {\n LinearConnectionCheck,\n LinearIntegration,\n LinearIssueDetail as LinearRouteIssueDetail,\n} from '../../linear/integration.js';\nimport { attachLinearIssueReconciler } from '../../linear/issue-reconciler.js';\nimport {\n linearIssueReconciliationEnabled,\n linearIssueReconciliationInterval,\n} from '../../linear/reconciliation-config.js';\nimport { buildLinearRoutes } from '../../linear/routes.js';\nimport { attachLinearRules } from '../../linear/rules.js';\nimport type { LinearConnectionData, LinearConnectionRow, LinearStorageHandle } from '../../linear/storage.js';\nimport {\n logPlatformInfo,\n logPlatformWarn,\n PlatformApiClient,\n PlatformApiError,\n platformApiClientConfigFromEnv,\n} from '../api-client.js';\nimport { PlatformLinearEventWorker } from './event-worker.js';\nimport type { PlatformLinearEventStorage } from './event-worker.js';\n\ntype PageInfo = { hasNextPage: boolean; endCursor: string | null };\ntype LinearUser = {\n id: string;\n name: string;\n displayName: string;\n email: string | null;\n avatarUrl: string | null;\n};\ntype LinearIssue = {\n id: string;\n identifier: string;\n number: number;\n title: string;\n description: string | null;\n url: string;\n priority: number;\n priorityLabel: string;\n labels: Array<{ id: string; name: string }>;\n state: { id: string; name: string; type: string };\n team: { id: string; key: string; name: string };\n project: { id: string } | null;\n assignee: LinearUser | null;\n creator: LinearUser | null;\n createdAt: string;\n updatedAt: string;\n archivedAt: string | null;\n};\ntype LinearComment = {\n id: string;\n body: string;\n url: string;\n issue: { id: string; identifier: string };\n user: LinearUser | null;\n parent: { id: string } | null;\n createdAt: string;\n updatedAt: string;\n};\ntype LinearWorkspace = {\n linearWorkspaceId: string;\n linearWorkspaceName: string;\n urlKey: string | null;\n connected: boolean;\n};\ntype LinearProject = {\n id: string;\n name: string;\n state: string;\n teams: Array<{ id: string; key: string; name: string }>;\n};\ntype LinearTeam = { id: string; key: string; name: string };\ntype ProjectSource = { workspace: LinearWorkspace; project: LinearProject };\ntype TeamSource = { workspace: LinearWorkspace; team: LinearTeam };\ntype LinearWorkflowState = {\n id: string;\n name: string;\n type: string;\n position: number;\n teamId: string;\n};\n\nconst API_PREFIX = '/v1/server/linear';\nconst PAGE_SIZE = 30;\nconst MAX_REFERENCE_PAGES = 20;\nconst MAX_COMMENT_PAGES = 20;\nconst PLATFORM_MANAGED_CONNECTION_TOKEN = 'platform-managed';\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\nfunction routeBaseUrl(ctx: IntegrationContext, requestUrl: string): string {\n return (ctx.baseUrl || new URL(requestUrl).origin).replace(/\\/+$/, '');\n}\n\nexport class PlatformLinearIntegration implements FactoryIntegration {\n readonly id = 'linear';\n readonly #client: PlatformApiClient;\n readonly #endpointHost: string;\n #projects: FactoryProjectsStorage | undefined;\n #auth: RouteAuth | undefined;\n /**\n * Per-instance workflow-state cache keyed by `${workspaceId}:${teamId}`. Scoped to the process\n * lifetime; not shared across requests intentionally to keep failure modes simple (stale states\n * clear on process restart). Populated lazily on first `updateIssue` per team.\n */\n readonly #workflowStatesByTeam = new Map<string, LinearWorkflowState[]>();\n\n readonly intake: Intake = {\n resolveIntakeDispatch: input => this.#resolveIntakeDispatch(input),\n listSources: async () => {\n const [projectSources, teamSources] = await Promise.all([this.#listProjectSources(), this.#listTeamSources()]);\n const projects = projectSources.map(({ workspace, project }) => ({\n id: encodeSourceId(workspace.linearWorkspaceId, project.id),\n name: project.name,\n type: 'project' as const,\n metadata: {\n workspaceId: workspace.linearWorkspaceId,\n workspaceName: workspace.linearWorkspaceName,\n workspaceUrlKey: workspace.urlKey,\n state: project.state,\n teams: project.teams,\n },\n }));\n const teams = teamSources.map(({ workspace, team }) => ({\n id: encodeTeamSourceId(workspace.linearWorkspaceId, team.id),\n name: team.name,\n type: 'team' as const,\n metadata: {\n workspaceId: workspace.linearWorkspaceId,\n workspaceName: workspace.linearWorkspaceName,\n workspaceUrlKey: workspace.urlKey,\n teamKey: team.key,\n },\n }));\n return [...projects, ...teams];\n },\n listItems: async ({ sourceIds, attributionSourceIds, cursor }) => {\n const result = await this.#listIssues(sourceIds, cursor, undefined, attributionSourceIds);\n return {\n items: result.issues.map(({ issue, sourceId, workspace }) => ({\n source: { type: 'issue', externalId: issue.id, url: issue.url },\n sourceId,\n title: issue.title,\n status: issue.state.name,\n labels: issue.labels.map(label => label.name),\n assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n metadata: {\n identifier: issue.identifier,\n workspaceId: workspace.linearWorkspaceId,\n workspaceName: workspace.linearWorkspaceName,\n projectId: issue.project?.id ?? null,\n team: issue.team.key,\n priority: issue.priorityLabel,\n },\n })),\n nextCursor: result.nextCursor,\n };\n },\n listIssues: async ({ connection, sourceIds, attributionSourceIds, labels, cursor }) => {\n requireLinearConnection(connection);\n const result = await this.#listIssues(sourceIds, cursor, labels, attributionSourceIds);\n return {\n issues: result.issues.map(({ issue, sourceId, workspace }) => ({\n ...parseIssue(issue, workspace.linearWorkspaceId),\n sourceId,\n })),\n nextCursor: result.nextCursor,\n };\n },\n getIssue: async ({ connection, sourceId, issueId }) => {\n requireLinearConnection(connection);\n const located = await this.#findIssue(sourceId, issueId);\n if (!located) return null;\n const comments = await this.#loadComments(located.workspaceId, issueId, located.issue.comments);\n return parseIssueDetail(located.issue, comments, located.workspaceId);\n },\n createComment: async ({ connection, sourceId, issueId, body }) => {\n requireLinearConnection(connection);\n const workspaceId = await this.#resolveWorkspaceForIssue(sourceId, issueId);\n if (!workspaceId) return null;\n try {\n const comment = await this.#client.request<LinearComment>(\n 'POST',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments`,\n { body },\n );\n return { id: comment.id, url: comment.url };\n } catch (error) {\n if (isNotFound(error)) return null;\n throw error;\n }\n },\n updateIssue: async (input: UpdateIntakeIssueInput): Promise<IntakeIssue | null> => {\n requireLinearConnection(input.connection);\n const located = await this.#findIssue(input.sourceId, input.issueId);\n if (!located) return null;\n const { workspaceId, issue } = located;\n\n const target = input.state;\n // Idempotency: skip the write entirely if the issue is already at the target state.\n if (target.kind === 'byType' && issue.state.type === target.stateType) {\n return parseIssue(issue, workspaceId);\n }\n if (target.kind === 'byName' && issue.state.name.toLowerCase() === target.name.toLowerCase()) {\n return parseIssue(issue, workspaceId);\n }\n\n let states: LinearWorkflowState[];\n try {\n states = await this.#listWorkflowStates(workspaceId, issue.team.id);\n } catch (error) {\n if (isNotFound(error)) {\n // Platform companion endpoint not deployed yet — degrade to policy skip so PR 1\n // is standalone-mergeable ahead of the platform-side workflow-states route landing.\n logPlatformWarn('Platform Linear: workflow-states endpoint not available; skipping updateIssue.', {\n workspaceId,\n teamId: issue.team.id,\n });\n return null;\n }\n throw error;\n }\n let matched: LinearWorkflowState | undefined;\n if (target.kind === 'byType') {\n matched = states\n .slice()\n .sort((a, b) => a.position - b.position)\n .find(s => s.type === target.stateType);\n } else {\n const wanted = target.name.toLowerCase();\n matched = states.find(s => s.name.toLowerCase() === wanted);\n }\n if (!matched) {\n logPlatformWarn('Platform Linear: no workflow state matched target; skipping updateIssue.', {\n workspaceId,\n teamId: issue.team.id,\n target,\n });\n return null;\n }\n\n try {\n await this.#client.request<LinearIssue>(\n 'PATCH',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(input.issueId)}`,\n { stateId: matched.id },\n );\n } catch (error) {\n if (isNotFound(error)) return null;\n throw error;\n }\n\n const refreshed = await this.#findIssue(input.sourceId, input.issueId);\n return refreshed ? parseIssue(refreshed.issue, refreshed.workspaceId) : null;\n },\n };\n\n readonly #rules: LinearEventRules;\n\n get rules(): LinearEventRules {\n return this.#rules;\n }\n\n constructor({ rules }: { rules?: LinearRuleOverrides } = {}) {\n this.#rules = resolveLinearRules(rules);\n const config = platformApiClientConfigFromEnv();\n this.#client = new PlatformApiClient(config);\n this.#endpointHost = new URL(config.baseUrl).host;\n }\n\n get storage(): LinearStorageHandle {\n const now = new Date();\n return {\n integrationId: this.id,\n connections: {\n get: async (orgId: string) => ({\n id: `platform-linear:${orgId}`,\n orgId,\n userId: null,\n data: {\n accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,\n refreshToken: null,\n expiresAtMs: null,\n scope: 'read,comments:create',\n workspaceName: null,\n workspaceUrlKey: null,\n } satisfies LinearConnectionData,\n metadata: {},\n createdAt: now,\n updatedAt: now,\n }),\n },\n } as unknown as LinearStorageHandle;\n }\n\n get projects(): FactoryProjectsStorage {\n if (!this.#projects) throw new Error('PlatformLinearIntegration projects storage has not been initialized.');\n return this.#projects;\n }\n\n initialize({ projects, auth }: { projects: FactoryProjectsStorage; auth?: RouteAuth }): void {\n this.#projects = projects;\n this.#auth = auth;\n logPlatformInfo('Platform Linear integration initialized', { endpointHost: this.#endpointHost });\n }\n\n get authEnabled(): boolean {\n return this.#auth?.enabled() ?? false;\n }\n\n async resolveOrgId(resourceId: string): Promise<string | null> {\n try {\n const project = await this.projects.getById({ id: resourceId });\n return project?.orgId ?? null;\n } catch {\n return null;\n }\n }\n\n async loadConnection(orgId: string): Promise<LinearConnectionRow | null> {\n const workspace = (await this.#listWorkspaces())[0];\n if (!workspace) return null;\n const now = new Date();\n return {\n id: `platform-linear:${orgId}`,\n orgId,\n userId: null,\n accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,\n scope: 'read,comments:create',\n refreshToken: null,\n expiresAt: null,\n workspaceName: workspace.linearWorkspaceName,\n workspaceUrlKey: workspace.urlKey,\n createdAt: now,\n updatedAt: now,\n };\n }\n\n async getFreshAccessToken(_connection: LinearConnectionRow): Promise<string> {\n return PLATFORM_MANAGED_CONNECTION_TOKEN;\n }\n\n /**\n * Background-dispatch context: platform-managed connection token when the\n * org has a connected workspace. Linear work items store the issue UUID\n * directly as `externalId`.\n */\n async #resolveIntakeDispatch({\n orgId,\n externalSource,\n }: {\n orgId: string;\n externalSource: { type: string; externalId: string };\n }): Promise<{ connection: IntegrationConnection; issueId: string } | null> {\n if (externalSource.type !== 'issue') return null;\n const connection = await this.loadConnection(orgId);\n if (!connection) return null;\n return {\n connection: { type: 'oauth', accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN },\n issueId: externalSource.externalId,\n };\n }\n\n canPostComments(connection: LinearConnectionRow): boolean {\n const scopes = (connection.scope ?? '').split(/[\\s,]+/).filter(Boolean);\n return scopes.some(scope => scope === 'comments:create' || scope === 'write' || scope === 'admin');\n }\n\n async checkConnection(orgId: string): Promise<LinearConnectionCheck> {\n const connection = await this.loadConnection(orgId);\n return {\n connected: connection !== null,\n canComment: connection !== null && this.canPostComments(connection),\n checkedAt: Date.now(),\n };\n }\n\n workers(ctx: IntegrationContext): MastraWorker[] {\n const pollingEnabled = process.env.MASTRACODE_PLATFORM_LINEAR_POLLING_ENABLED?.trim().toLowerCase() !== 'false';\n const reconcileEnabled = linearIssueReconciliationEnabled();\n if (!pollingEnabled && !reconcileEnabled) return [];\n\n const ingest = attachLinearRules(this, ctx);\n const reconcile = reconcileEnabled ? attachLinearIssueReconciler(this, ctx) : undefined;\n const workItems = ctx.runtime?.workItems;\n if (!workItems) return [];\n if (!ingest && !reconcile) return [];\n\n const pollIntervalMs = optionalPositiveIntegerEnv('MASTRACODE_PLATFORM_LINEAR_POLLING_INTERVAL_MS');\n const reconcileIntervalMs = linearIssueReconciliationInterval();\n\n return [\n new PlatformLinearEventWorker({\n client: this.#client,\n linear: { listWorkspaces: () => this.listWorkspaces() },\n storage: ctx.storage.generic as unknown as PlatformLinearEventStorage,\n projects: ctx.storage.projects,\n workItems,\n ...(ingest ? { ingestFactoryIssue: ingest } : {}),\n ...(reconcile ? { reconcileFactoryState: reconcile } : {}),\n pollEventsEnabled: pollingEnabled,\n ...(pollIntervalMs !== undefined ? { intervalMs: pollIntervalMs } : {}),\n ...(reconcileIntervalMs !== undefined ? { reconcileIntervalMs } : {}),\n }),\n ];\n }\n\n routes(ctx: IntegrationContext): ApiRoute[] {\n return [\n this.#connectRoute(ctx),\n ...buildLinearRoutes({\n auth: ctx.auth,\n linear: this as unknown as LinearIntegration,\n stateSigner: ctx.stateSigner,\n baseUrl: ctx.baseUrl,\n intake: ctx.storage.intake,\n projects: ctx.storage.projects,\n ingestFactoryIssues: attachLinearRules(this, ctx),\n workItems: ctx.runtime?.workItems,\n boards: ctx.runtime?.boards,\n }).filter(route => !route.path.startsWith('/auth/linear/')),\n ];\n }\n\n #connectRoute(ctx: IntegrationContext): ApiRoute {\n return registerApiRoute('/auth/linear/connect', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n await ctx.auth.ensureUser(loose(c));\n const tenant = ctx.auth.tenant(loose(c));\n if (!tenant?.orgId) return c.json({ error: 'unauthorized' }, 401);\n\n const returnTo = c.req.query('return_to') || '/';\n const originator = routeBaseUrl(ctx, c.req.url);\n logPlatformInfo('Starting Platform Linear connect flow', {\n orgId: tenant.orgId,\n returnTo,\n originator,\n });\n const query = new URLSearchParams({ return_to: returnTo, originator });\n const location = await this.#client.requestRedirect('GET', `${API_PREFIX}/authorize?${query}`);\n return c.redirect(location);\n },\n });\n }\n\n async agentTools({ requestContext }: { requestContext: RequestContext }): Promise<IntegrationTools> {\n return buildLinearAgentTools({ requestContext, linear: this as unknown as LinearIntegration });\n }\n\n diagnostics(): Record<string, unknown> {\n return { mode: 'platform', endpointHost: this.#endpointHost };\n }\n\n async listProjects(): Promise<Array<LinearProject & { workspaceId: string }>> {\n return (await this.#listProjectSources()).map(({ workspace, project }) => ({\n ...project,\n id: encodeSourceId(workspace.linearWorkspaceId, project.id),\n workspaceId: workspace.linearWorkspaceId,\n }));\n }\n\n async listTeams(_accessToken: string): Promise<Array<LinearTeam & { workspaceId: string; sourceId: string }>> {\n return (await this.#listTeamSources()).map(({ workspace, team }) => ({\n ...team,\n workspaceId: workspace.linearWorkspaceId,\n sourceId: encodeTeamSourceId(workspace.linearWorkspaceId, team.id),\n }));\n }\n\n async fetchIssueDetail(\n _accessToken: string,\n idOrIdentifier: string,\n sourceIds?: string[],\n routedSourceIds?: string[],\n ): Promise<LinearRouteIssueDetail | null> {\n const located = await this.#findIssue(undefined, idOrIdentifier, sourceIds, routedSourceIds);\n if (!located) return null;\n const comments = await this.#loadComments(located.workspaceId, located.issue.id, located.issue.comments);\n const issue = located.issue;\n return {\n id: issue.id,\n projectId: issue.project?.id ?? null,\n workspaceId: located.workspaceId,\n teamId: issue.team.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description?.trim() ? issue.description : null,\n url: issue.url,\n state: issue.state.name,\n stateType: issue.state.type,\n priorityLabel: issue.priorityLabel,\n assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,\n creator: issue.creator?.displayName ?? issue.creator?.name ?? null,\n team: issue.team.key,\n labels: issue.labels.map(label => label.name),\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n comments: comments.map(comment => ({\n author: comment.user?.displayName ?? comment.user?.name ?? null,\n body: comment.body,\n createdAt: comment.createdAt,\n })),\n };\n }\n\n sourceMatchesIssue(\n sourceId: string,\n issue: Pick<LinearRouteIssueDetail, 'workspaceId' | 'projectId' | 'teamId'>,\n ): boolean {\n const source = parseSourceId(sourceId);\n if (issue.workspaceId !== source.workspaceId) return false;\n return source.kind === 'team' ? issue.teamId === source.teamId : issue.projectId === source.projectId;\n }\n\n async #listProjectSources(workspaceIds?: ReadonlySet<string>): Promise<ProjectSource[]> {\n const workspaces = await this.#listWorkspaces();\n const scopedWorkspaces = workspaceIds\n ? workspaces.filter(workspace => workspaceIds.has(workspace.linearWorkspaceId))\n : workspaces;\n const projectGroups = await Promise.all(\n scopedWorkspaces.map(async workspace => {\n const projects: LinearProject[] = [];\n let after: string | undefined;\n for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {\n const query = new URLSearchParams({ first: '200' });\n if (after) query.set('after', after);\n const result = await this.#client.request<{ projects: LinearProject[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/projects?${query}`,\n );\n projects.push(...result.projects);\n if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;\n // A page that hands back the cursor it was asked for would replay forever.\n if (result.pageInfo.endCursor === after) throw invalidLinearCursor();\n after = result.pageInfo.endCursor;\n }\n return projects.map(project => ({ workspace, project }));\n }),\n );\n return projectGroups.flat();\n }\n\n async #listTeamSources(workspaceIds?: ReadonlySet<string>): Promise<TeamSource[]> {\n const workspaces = await this.#listWorkspaces();\n const scopedWorkspaces = workspaceIds\n ? workspaces.filter(workspace => workspaceIds.has(workspace.linearWorkspaceId))\n : workspaces;\n const teamGroups = await Promise.all(\n scopedWorkspaces.map(async workspace => {\n const teams: LinearTeam[] = [];\n let after: string | undefined;\n for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {\n const query = new URLSearchParams({ first: '200' });\n if (after) query.set('after', after);\n const result = await this.#client.request<{ teams: LinearTeam[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/teams?${query}`,\n );\n teams.push(...result.teams);\n if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;\n // A page that hands back the cursor it was asked for would replay forever.\n if (result.pageInfo.endCursor === after) throw invalidLinearCursor();\n after = result.pageInfo.endCursor;\n }\n return teams.map(team => ({ workspace, team }));\n }),\n );\n return teamGroups.flat();\n }\n\n async listWorkspaces(): Promise<LinearWorkspace[]> {\n return this.#listWorkspaces();\n }\n\n async #listWorkspaces(): Promise<LinearWorkspace[]> {\n const result = await this.#client.request<{ workspaces: LinearWorkspace[] }>('GET', `${API_PREFIX}/workspaces`);\n return result.workspaces.filter(workspace => workspace.connected);\n }\n\n async #listIssues(sourceIds: string[], cursor?: string, labels?: string[], attributionSourceIds = sourceIds) {\n type ListedIssue = { issue: LinearIssue; sourceId: string; workspace: LinearWorkspace };\n if (sourceIds.length === 0) return { issues: [] as ListedIssue[], nextCursor: null };\n\n // Resolve every selected source to a concrete listing descriptor. Project\n // and team sources are listed separately: a project source filters by\n // `projectIds`, a team source filters by `teamId` (and returns projectless\n // issues too). Filters are never combined on one request.\n const parsedSources = sourceIds.map(sourceId => parseSourceId(sourceId));\n const projectWorkspaceIds = new Set(\n parsedSources.flatMap(source => (source.kind === 'project' ? [source.workspaceId] : [])),\n );\n const teamWorkspaceIds = new Set(\n parsedSources.flatMap(source => (source.kind === 'team' ? [source.workspaceId] : [])),\n );\n const [projectSources, teamSources] = await Promise.all([\n projectWorkspaceIds.size > 0\n ? this.#listProjectSources(projectWorkspaceIds)\n : Promise.resolve([] as ProjectSource[]),\n teamWorkspaceIds.size > 0 ? this.#listTeamSources(teamWorkspaceIds) : Promise.resolve([] as TeamSource[]),\n ]);\n const projectMap = new Map(\n projectSources.map(source => [encodeSourceId(source.workspace.linearWorkspaceId, source.project.id), source]),\n );\n const teamMap = new Map(\n teamSources.map(source => [encodeTeamSourceId(source.workspace.linearWorkspaceId, source.team.id), source]),\n );\n\n type Descriptor = {\n kind: 'project' | 'team';\n sourceId: string;\n workspace: LinearWorkspace;\n query: URLSearchParams;\n };\n const descriptors: Descriptor[] = [];\n const selectedProjectIdsByWorkspace = new Map<string, Set<string>>();\n for (const attributionSourceId of attributionSourceIds) {\n const source = parseSourceId(attributionSourceId);\n if (source.kind !== 'project') continue;\n const selectedProjectIds = selectedProjectIdsByWorkspace.get(source.workspaceId) ?? new Set<string>();\n selectedProjectIds.add(source.projectId);\n selectedProjectIdsByWorkspace.set(source.workspaceId, selectedProjectIds);\n }\n for (const sourceId of sourceIds) {\n const projectSource = projectMap.get(sourceId);\n if (projectSource) {\n const query = new URLSearchParams({\n first: String(PAGE_SIZE),\n projectIds: projectSource.project.id,\n stateType: 'triage,backlog,unstarted,started',\n orderBy: 'updatedAt',\n });\n const workspaceId = projectSource.workspace.linearWorkspaceId;\n const selectedProjectIds = selectedProjectIdsByWorkspace.get(workspaceId) ?? new Set<string>();\n selectedProjectIds.add(projectSource.project.id);\n selectedProjectIdsByWorkspace.set(workspaceId, selectedProjectIds);\n descriptors.push({ kind: 'project', sourceId, workspace: projectSource.workspace, query });\n continue;\n }\n const teamSource = teamMap.get(sourceId);\n if (teamSource) {\n const query = new URLSearchParams({\n first: String(PAGE_SIZE),\n teamId: teamSource.team.id,\n stateType: 'triage,backlog,unstarted,started',\n orderBy: 'updatedAt',\n });\n descriptors.push({ kind: 'team', sourceId, workspace: teamSource.workspace, query });\n }\n }\n\n const cursors = await decodeCursor(cursor, sourceIds, attributionSourceIds);\n const normalizedLabels = normalizeLabels(labels);\n const nextState: Record<string, string | null> = {};\n let hasNextPage = false;\n const pages = await Promise.all(\n descriptors.map(async ({ kind, sourceId, workspace, query }) => {\n if (cursors[sourceId] === null) {\n nextState[sourceId] = null;\n return [] as ListedIssue[];\n }\n const after = cursors[sourceId];\n if (after) query.set('after', after);\n const result = await this.#client.request<{ issues: LinearIssue[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/issues?${query}`,\n );\n const next = result.pageInfo.hasNextPage ? result.pageInfo.endCursor : null;\n // A page that hands back the cursor it was asked for would replay forever.\n if (next !== null && next === after) throw invalidLinearCursor();\n nextState[sourceId] = next;\n hasNextPage ||= next !== null;\n return result.issues\n .filter(\n issue => normalizedLabels.length === 0 || issue.labels.some(label => normalizedLabels.includes(label.name)),\n )\n .filter(\n issue =>\n kind === 'project' ||\n issue.project === null ||\n !selectedProjectIdsByWorkspace.get(workspace.linearWorkspaceId)?.has(issue.project.id),\n )\n .map(issue => ({ issue, sourceId, workspace }));\n }),\n );\n return {\n issues: dedupeIssuesBySource(pages.flat(), sourceIds),\n nextCursor: hasNextPage ? await encodeCursor(nextState, sourceIds, attributionSourceIds) : null,\n };\n }\n\n async #findIssue(\n sourceId: string | undefined,\n issueId: string,\n sourceIds?: string[],\n routedSourceIds?: string[],\n ): Promise<{\n workspaceId: string;\n issue: LinearIssue & { comments?: { nodes: LinearComment[]; pageInfo: PageInfo } };\n } | null> {\n const scopedSourceIds = sourceIds ?? (sourceId ? [sourceId] : undefined);\n const scopedSources = scopedSourceIds?.map(sourceKey => ({\n sourceKey,\n source: parseSourceId(sourceKey),\n }));\n const routedSourceSet = routedSourceIds ? new Set(routedSourceIds) : undefined;\n const workspaceIds = routedSourceIds\n ? [...new Set(routedSourceIds.map(sourceKey => parseSourceId(sourceKey).workspaceId))]\n : scopedSources\n ? [...new Set(scopedSources.map(({ source }) => source.workspaceId))]\n : await this.#candidateWorkspaceIds(undefined);\n for (const workspaceId of workspaceIds) {\n try {\n const issue = await this.#client.request<\n LinearIssue & { comments?: { nodes: LinearComment[]; pageInfo: PageInfo } }\n >(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}?include=comments`,\n );\n if (scopedSources) {\n const matching = scopedSources.filter(\n ({ source }) =>\n source.workspaceId === workspaceId &&\n (source.kind === 'team' ? issue.team.id === source.teamId : issue.project?.id === source.projectId),\n );\n const winner = matching.find(({ source }) => source.kind === 'project') ?? matching[0];\n if (!winner || (routedSourceSet && !routedSourceSet.has(winner.sourceKey))) continue;\n }\n return { workspaceId, issue };\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n }\n return null;\n }\n\n async #resolveWorkspaceForIssue(sourceId: string | undefined, issueId: string): Promise<string | null> {\n const workspaceIds = await this.#candidateWorkspaceIds(sourceId);\n if (workspaceIds.length === 1) return workspaceIds[0]!;\n for (const workspaceId of workspaceIds) {\n try {\n await this.#client.request<LinearIssue>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}`,\n );\n return workspaceId;\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n }\n return null;\n }\n\n async #listWorkflowStates(workspaceId: string, teamId: string): Promise<LinearWorkflowState[]> {\n const cacheKey = `${workspaceId}:${teamId}`;\n const cached = this.#workflowStatesByTeam.get(cacheKey);\n if (cached) return cached;\n const result = await this.#client.request<{ workflowStates: LinearWorkflowState[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/workflow-states?teamId=${encodeURIComponent(teamId)}&first=100`,\n );\n this.#workflowStatesByTeam.set(cacheKey, result.workflowStates);\n return result.workflowStates;\n }\n\n async #candidateWorkspaceIds(sourceId: string | undefined): Promise<string[]> {\n if (sourceId) return [parseSourceId(sourceId).workspaceId];\n return (await this.#listWorkspaces()).map(workspace => workspace.linearWorkspaceId);\n }\n\n async #loadComments(\n workspaceId: string,\n issueId: string,\n embedded: { nodes: LinearComment[]; pageInfo: PageInfo } | undefined,\n ): Promise<LinearComment[]> {\n const comments = [...(embedded?.nodes ?? [])];\n let pageInfo = embedded?.pageInfo;\n let page = 0;\n while (pageInfo?.hasNextPage && pageInfo.endCursor && page < MAX_COMMENT_PAGES) {\n const result = await this.#client.request<{ comments: LinearComment[]; pageInfo: PageInfo }>(\n 'GET',\n `${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments?first=200&after=${encodeURIComponent(pageInfo.endCursor)}`,\n );\n comments.push(...result.comments);\n if (result.pageInfo.hasNextPage && result.pageInfo.endCursor === pageInfo.endCursor) throw invalidLinearCursor();\n pageInfo = result.pageInfo;\n page += 1;\n }\n return comments;\n }\n}\n\nfunction parseIssue(issue: LinearIssue, workspaceId: string): IntakeIssue {\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n author: issue.creator?.displayName ?? issue.creator?.name ?? null,\n state: issue.state.name,\n stateType: issue.state.type,\n priority: issue.priorityLabel,\n assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,\n source: issue.team.key,\n // Use the same workspace-scoped identity as listProjects and intake bindings.\n // The source may be a team, so it cannot stand in for the issue's project.\n projectId: issue.project ? encodeSourceId(workspaceId, issue.project.id) : null,\n labels: issue.labels.map(label => label.name),\n commentCount: null,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n };\n}\n\nfunction parseIssueDetail(issue: LinearIssue, comments: LinearComment[], workspaceId: string): IntakeIssueDetail {\n return {\n ...parseIssue(issue, workspaceId),\n commentCount: comments.length,\n description: issue.description?.trim() ? issue.description : null,\n comments: comments.map(comment => ({\n author: comment.user?.displayName ?? comment.user?.name ?? null,\n body: comment.body,\n createdAt: comment.createdAt,\n })),\n };\n}\n\n/**\n * Deduplicate issues that surfaced from more than one selected source (a team\n * source and one of its projects both select the same issue). Keeps one entry\n * per Linear issue UUID.\n *\n * Tie-breaker — MOST-SPECIFIC-WINS: a project source beats a team source, so an\n * issue that belongs to a selected project is attributed to that project's\n * source (and therefore routes to the project's board). The team source covers\n * only the remainder — projectless issues and issues in projects that were not\n * separately selected. `selectionOrder` breaks a same-kind tie deterministically\n * by the order the sources were selected.\n *\n * This function is the single precedence point; change the comparison here to\n * change the routing policy.\n */\nfunction dedupeIssuesBySource<T extends { issue: { id: string }; sourceId: string }>(\n entries: T[],\n selectionOrder: string[],\n): T[] {\n const rank = new Map(selectionOrder.map((id, index) => [id, index]));\n const specificity = (sourceId: string): number => (parseSourceId(sourceId).kind === 'project' ? 0 : 1);\n const winners = new Map<string, T>();\n for (const entry of entries) {\n const existing = winners.get(entry.issue.id);\n if (!existing) {\n winners.set(entry.issue.id, entry);\n continue;\n }\n const bySpecificity = specificity(entry.sourceId) - specificity(existing.sourceId);\n const better =\n bySpecificity < 0 ||\n (bySpecificity === 0 &&\n (rank.get(entry.sourceId) ?? Number.MAX_SAFE_INTEGER) <\n (rank.get(existing.sourceId) ?? Number.MAX_SAFE_INTEGER));\n if (better) winners.set(entry.issue.id, entry);\n }\n // Preserve first-seen order of the surviving issues.\n const seen = new Set<string>();\n const ordered: T[] = [];\n for (const entry of entries) {\n if (seen.has(entry.issue.id)) continue;\n seen.add(entry.issue.id);\n ordered.push(winners.get(entry.issue.id)!);\n }\n return ordered;\n}\n\nexport function encodeSourceId(workspaceId: string, projectId: string): string {\n return `linear-project:${Buffer.from(JSON.stringify({ workspaceId, projectId })).toString('base64url')}`;\n}\n\nfunction decodeSourceId(sourceId: string): { workspaceId: string; projectId: string } {\n if (!sourceId.startsWith('linear-project:')) throw new Error('Linear project source id is invalid.');\n try {\n const parsed = JSON.parse(Buffer.from(sourceId.slice('linear-project:'.length), 'base64url').toString('utf8')) as {\n workspaceId?: unknown;\n projectId?: unknown;\n };\n if (typeof parsed.workspaceId !== 'string' || !parsed.workspaceId) throw new Error();\n if (typeof parsed.projectId !== 'string' || !parsed.projectId) throw new Error();\n return { workspaceId: parsed.workspaceId, projectId: parsed.projectId };\n } catch {\n throw new Error('Linear project source id is invalid.');\n }\n}\n\nconst TEAM_SOURCE_PREFIX = 'linear-team:';\n\nexport function encodeTeamSourceId(workspaceId: string, teamId: string): string {\n return `${TEAM_SOURCE_PREFIX}${Buffer.from(JSON.stringify({ workspaceId, teamId })).toString('base64url')}`;\n}\n\nexport function decodeTeamSourceId(sourceId: string): { workspaceId: string; teamId: string } {\n if (!sourceId.startsWith(TEAM_SOURCE_PREFIX)) throw new Error('Linear team source id is invalid.');\n try {\n const parsed = JSON.parse(Buffer.from(sourceId.slice(TEAM_SOURCE_PREFIX.length), 'base64url').toString('utf8')) as {\n workspaceId?: unknown;\n teamId?: unknown;\n };\n if (typeof parsed.workspaceId !== 'string' || !parsed.workspaceId) throw new Error();\n if (typeof parsed.teamId !== 'string' || !parsed.teamId) throw new Error();\n return { workspaceId: parsed.workspaceId, teamId: parsed.teamId };\n } catch {\n throw new Error('Linear team source id is invalid.');\n }\n}\n\n/**\n * A decoded intake source: either a Linear project or a whole Linear team.\n * Downstream code discriminates on `kind` to build the right issue-listing\n * filter and to apply most-specific-wins routing (project beats team).\n */\nexport type DecodedSource =\n { kind: 'project'; workspaceId: string; projectId: string } | { kind: 'team'; workspaceId: string; teamId: string };\n\n/** Discriminate any Linear source id by its prefix and decode it. */\nexport function parseSourceId(sourceId: string): DecodedSource {\n if (sourceId.startsWith(TEAM_SOURCE_PREFIX)) {\n return { kind: 'team', ...decodeTeamSourceId(sourceId) };\n }\n return { kind: 'project', ...decodeSourceId(sourceId) };\n}\n\nfunction normalizeLabels(labels: string[] | undefined): string[] {\n return [...new Set((labels ?? []).map(label => label.trim()).filter(Boolean))];\n}\n\ntype PlatformListCursor = { v: 1; sourceSet: string; cursors: Array<string | null> };\n\nfunction canonicalSourceIds(sourceIds: string[]): string[] {\n return [...new Set(sourceIds)].sort();\n}\n\nasync function linearSourceSetFingerprint(sourceIds: string[], attributionSourceIds: string[]): Promise<string> {\n const scope = {\n sourceIds: canonicalSourceIds(sourceIds),\n attributionSourceIds: canonicalSourceIds(attributionSourceIds),\n };\n const bytes = new TextEncoder().encode(JSON.stringify(scope));\n return Buffer.from(await globalThis.crypto.subtle.digest('SHA-256', bytes)).toString('base64url');\n}\n\nfunction invalidLinearCursor(): Error {\n return Object.assign(new Error('Linear cursor is invalid or stale.'), { code: 'invalid_cursor' as const });\n}\n\nasync function decodeCursor(\n cursor: string | undefined,\n sourceIds: string[],\n attributionSourceIds: string[],\n): Promise<Record<string, string | null | undefined>> {\n if (!cursor) return {};\n try {\n const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')) as Partial<PlatformListCursor>;\n const canonical = canonicalSourceIds(sourceIds);\n if (\n parsed.v !== 1 ||\n parsed.sourceSet !== (await linearSourceSetFingerprint(sourceIds, attributionSourceIds)) ||\n !Array.isArray(parsed.cursors) ||\n parsed.cursors.length !== canonical.length ||\n !parsed.cursors.every(value => value === null || typeof value === 'string')\n ) {\n throw invalidLinearCursor();\n }\n return Object.fromEntries(canonical.map((sourceId, index) => [sourceId, parsed.cursors![index]]));\n } catch {\n throw invalidLinearCursor();\n }\n}\n\nasync function encodeCursor(\n state: Record<string, string | null>,\n sourceIds: string[],\n attributionSourceIds: string[],\n): Promise<string> {\n const canonical = canonicalSourceIds(sourceIds);\n const cursor: PlatformListCursor = {\n v: 1,\n sourceSet: await linearSourceSetFingerprint(sourceIds, attributionSourceIds),\n cursors: canonical.map(sourceId => state[sourceId] ?? null),\n };\n return Buffer.from(JSON.stringify(cursor)).toString('base64url');\n}\n\nfunction requireLinearConnection(connection: IntegrationConnection): void {\n if (connection.type !== 'oauth') {\n throw new Error('Linear capabilities require an OAuth connection.');\n }\n}\n\nfunction isNotFound(error: unknown): boolean {\n return error instanceof PlatformApiError && error.status === 404;\n}\n\nfunction optionalPositiveIntegerEnv(name: string): number | undefined {\n const value = process.env[name]?.trim();\n if (!value) return undefined;\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed <= 0) {\n throw new Error(`${name} must be a positive integer.`);\n }\n return parsed;\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ const SLACK_TOKEN_URL = "https://slack.com/api/openid.connect.token";
5
5
  const OIDC_CALLBACK_PATH = "/connect/slack/oidc/callback";
6
6
  const SLACK_TOKEN_TIMEOUT_MS = 1e4;
7
7
  /** Matches the signer's own `state` lifetime — past it, `verify` rejects anyway. */
8
- const STATE_REPLAY_WINDOW_MS = 600 * 1e3;
8
+ const STATE_REPLAY_WINDOW_MS = 6e5;
9
9
  /**
10
10
  * Decode a JWT's payload WITHOUT signature verification. Safe here because the
11
11
  * `id_token` arrives directly from Slack's token endpoint over TLS in a
@@ -1 +1 @@
1
- {"version":3,"file":"connect-route.js","names":[],"sources":["../../../src/integrations/slack/connect-route.ts"],"sourcesContent":["import { registerApiRoute } from '@mastra/core/server';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { StateSigner } from '../../state-signing.js';\nimport type { ChannelIdentityStorage } from '../../storage/domains/channel-identity/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\n\n/**\n * Payload shape for the Connections list: the platform sender key +\n * link time, without the tenant ids (the caller IS the tenant).\n */\ninterface ConnectedChannelAccountPayload {\n platform: string;\n externalTeamId: string;\n externalUserId: string;\n /** Display names captured at link time (OIDC profile claims); ids fall back. */\n externalTeamName?: string;\n externalUserName?: string;\n /** Which Factory project this link's channel runs route to (unset = not picked yet). */\n defaultFactoryProjectId?: string;\n linkedAt: string;\n}\n\n/**\n * Config for the web-initiated \"Sign in with Slack\" (OIDC) connect flow. All\n * values come from the env-supplied Slack app. Absent → the OIDC routes\n * respond with an error redirect and the list endpoint reports\n * `canConnect: false` so the UI hides its Connect button.\n */\ninterface SlackOidcConfig {\n clientId: string;\n clientSecret: string;\n /**\n * Public HTTPS origin the OIDC `redirect_uri` is built from (Slack rejects\n * plain-http redirect URLs, so locally this is the tunnel origin). Must also\n * be registered as a redirect URL on the Slack app.\n */\n redirectBaseUrl: string;\n /** Browser-facing origin post-connect redirects land on (the SPA host). */\n uiOrigin?: string;\n}\n\nconst SLACK_AUTHORIZE_URL = 'https://slack.com/openid/connect/authorize';\nconst SLACK_TOKEN_URL = 'https://slack.com/api/openid.connect.token';\nconst OIDC_CALLBACK_PATH = '/connect/slack/oidc/callback';\nconst SLACK_TOKEN_TIMEOUT_MS = 10_000;\n/** Matches the signer's own `state` lifetime — past it, `verify` rejects anyway. */\nconst STATE_REPLAY_WINDOW_MS = 10 * 60 * 1000;\n\n/**\n * Decode a JWT's payload WITHOUT signature verification. Safe here because the\n * `id_token` arrives directly from Slack's token endpoint over TLS in a\n * confidential-client code exchange — the transport authenticates the issuer,\n * which is the standard OIDC allowance for this flow. `iss`/`aud`/`exp` are\n * still checked by the caller.\n */\nfunction decodeJwtPayload(jwt: string): Record<string, unknown> | null {\n const payload = jwt.split('.')[1];\n if (!payload) return null;\n try {\n return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/**\n * Slack account-linking routes.\n *\n * A link is only ever written by the OIDC flow below, where Slack itself\n * asserts the `(team, user)` pair in the id_token — so the web user has to\n * actually control the Slack account they bind. `/connect/slack` is just the\n * Slack-side entry point: it carries no identity and writes nothing, it only\n * sends the visitor to Connections, where the flow starts.\n */\nexport function createSlackConnectRoutes(deps: {\n auth: RouteAuth;\n accountLinks: ChannelIdentityStorage;\n /** Signs the OIDC `state`, binding the round-trip to the initiating tenant. */\n tenantStateSigner?: StateSigner;\n oidc?: SlackOidcConfig;\n /**\n * Factory projects domain, for validating (and listing to) the per-link\n * default factory. Unset → the default-factory PATCH route rejects.\n */\n projects?: FactoryProjectsStorage;\n}): ApiRoute[] {\n const { auth, accountLinks, tenantStateSigner, oidc, projects } = deps;\n const oidcEnabled = Boolean(oidc && tenantStateSigner);\n const uiOrigin = oidc?.uiOrigin?.replace(/\\/$/, '') ?? '';\n\n // A signed `state` stays valid for its whole 10-minute window, so on its own\n // it authorizes the binding repeatedly: anyone who captures a callback URL\n // can re-run the exchange with their own fresh Slack `code` and bind their\n // Slack account to the tenant that started the flow. Burning the nonce makes\n // the state single-use. Scope note: this is per-process, so it does not stop\n // a replay routed to a different replica — narrowing that further needs\n // shared storage, which is out of proportion to a 10-minute window here.\n const consumedNonces = new Map<string, number>();\n const consumeNonce = (nonce: string): boolean => {\n const now = Date.now();\n for (const [seen, expiresAt] of consumedNonces) {\n if (expiresAt <= now) consumedNonces.delete(seen);\n }\n if (consumedNonces.has(nonce)) return false;\n consumedNonces.set(nonce, now + STATE_REPLAY_WINDOW_MS);\n return true;\n };\n\n // mc-web can resolve a different hono version than @mastra/factory, so the\n // registerApiRoute handler `Context` and the factory `RouteAuth` `Context`\n // are structurally incompatible (private `[GET_MATCH_RESULT]` symbol). Cast\n // through `unknown` at the seam — same `loose()` workaround the factory\n // routes use internally.\n type RouteAuthContext = Parameters<RouteAuth['ensureUser']>[0];\n const loose = (c: unknown): RouteAuthContext => c as RouteAuthContext;\n\n return [\n registerApiRoute('/connect/slack', {\n method: 'GET',\n // No auth: nothing here reads a tenant or writes a link. The SPA route\n // this lands on requires a session and bounces through login itself.\n requiresAuth: false,\n handler: async c => c.redirect(`${uiOrigin}/settings/connections`),\n }),\n // Web-initiated connect: \"Sign in with Slack\" (OIDC). The only route that\n // writes a link, because it is the only one that proves the signed-in web\n // user actually CONTROLS the Slack account: Slack itself asserts the\n // (team, user) pair in the id_token.\n registerApiRoute('/connect/slack/oidc/start', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!oidcEnabled) return c.redirect(`${uiOrigin}/?slack=error`);\n\n const factoryProjectId = c.req.query('factoryId')?.trim() || undefined;\n const startPath = factoryProjectId\n ? `/connect/slack/oidc/start?factoryId=${encodeURIComponent(factoryProjectId)}`\n : '/connect/slack/oidc/start';\n\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) {\n return c.redirect(`/auth/login?returnTo=${encodeURIComponent(startPath)}`);\n }\n\n const params = new URLSearchParams({\n response_type: 'code',\n // `profile` adds display-name claims (user name, team name) to the\n // id_token so Connections can show names instead of ids.\n scope: 'openid profile',\n client_id: oidc!.clientId,\n // Personal accounts have no org; the signer requires a string, so an\n // empty org round-trips and is mapped back to undefined on save.\n state: tenantStateSigner!.sign(tenant.orgId ?? '', tenant.userId, { factoryProjectId }),\n redirect_uri: `${oidc!.redirectBaseUrl.replace(/\\/$/, '')}${OIDC_CALLBACK_PATH}`,\n });\n return c.redirect(`${SLACK_AUTHORIZE_URL}?${params.toString()}`);\n },\n }),\n // OIDC callback. Authenticates via the signed `state` (which carries the\n // initiating tenant) rather than the session cookie — the callback may\n // arrive on a different origin (the public tunnel) where the SPA's\n // host-scoped cookie is not sent.\n registerApiRoute(OIDC_CALLBACK_PATH, {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!oidcEnabled) return c.redirect(`${uiOrigin}/?slack=error`);\n\n const tenant = tenantStateSigner!.verify(c.req.query('state'));\n const code = c.req.query('code');\n if (!tenant || !code) return c.redirect(`${uiOrigin}/?slack=error`);\n // Burn the state before spending the code, not after saving: a replay\n // must be refused even if the first attempt fails partway through.\n if (!consumeNonce(tenant.nonce)) return c.redirect(`${uiOrigin}/?slack=error`);\n\n // Bound the exchange: without a deadline a slow token endpoint parks the\n // request until the platform's own timeout, and this route is reachable\n // by anyone holding a signed state.\n const tokenRes = await fetch(SLACK_TOKEN_URL, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: oidc!.clientId,\n client_secret: oidc!.clientSecret,\n code,\n redirect_uri: `${oidc!.redirectBaseUrl.replace(/\\/$/, '')}${OIDC_CALLBACK_PATH}`,\n }),\n signal: AbortSignal.timeout(SLACK_TOKEN_TIMEOUT_MS),\n }).catch(() => null);\n const token = (await tokenRes?.json().catch(() => null)) as { ok?: boolean; id_token?: string } | null;\n if (!tokenRes?.ok || !token?.ok || typeof token.id_token !== 'string') {\n return c.redirect(`${uiOrigin}/?slack=error`);\n }\n\n const claims = decodeJwtPayload(token.id_token);\n const teamId = claims?.['https://slack.com/team_id'];\n const slackUserId = claims?.['https://slack.com/user_id'];\n // Display-only claims from the `profile` scope — absent claims just\n // mean the settings list falls back to ids.\n const teamName = claims?.['https://slack.com/team_name'];\n const userName = claims?.name;\n // `exp` is seconds since the epoch. A missing or past `exp` means the\n // assertion is not currently valid, so it cannot bind an account.\n const expiresAt = typeof claims?.exp === 'number' ? claims.exp * 1000 : null;\n if (\n !claims ||\n claims.iss !== 'https://slack.com' ||\n claims.aud !== oidc!.clientId ||\n expiresAt === null ||\n expiresAt <= Date.now() ||\n typeof teamId !== 'string' ||\n typeof slackUserId !== 'string'\n ) {\n return c.redirect(`${uiOrigin}/?slack=error`);\n }\n\n await accountLinks.saveAccountLink({\n platform: 'slack',\n externalTeamId: teamId,\n externalUserId: slackUserId,\n orgId: tenant.orgId || undefined,\n userId: tenant.userId,\n externalTeamName: typeof teamName === 'string' ? teamName : undefined,\n externalUserName: typeof userName === 'string' ? userName : undefined,\n });\n\n const successPath = tenant.factoryProjectId\n ? `/factories/${encodeURIComponent(tenant.factoryProjectId)}/settings/connections/slack`\n : '/settings/connections';\n return c.redirect(`${uiOrigin}${successPath}?slack=connected`);\n },\n }),\n // The caller's own linked channel accounts for Connections. Tenant-scoped:\n // you only ever see your own links.\n registerApiRoute('/web/channel-accounts', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const links = await accountLinks.listAccountLinksForUser(tenant.userId);\n const accounts: ConnectedChannelAccountPayload[] = links.map(link => ({\n platform: link.platform,\n externalTeamId: link.externalTeamId,\n externalUserId: link.externalUserId,\n externalTeamName: link.externalTeamName,\n externalUserName: link.externalUserName,\n defaultFactoryProjectId: link.defaultFactoryProjectId,\n linkedAt: link.linkedAt.toISOString(),\n }));\n // `canConnect` tells the settings UI whether the web-initiated OIDC\n // connect flow is configured (vs. Slack-side Connect card only).\n return c.json({ accounts, canConnect: oidcEnabled });\n },\n }),\n // Self-service disconnect. The storage delete is guarded by the caller's\n // tenant userId, so a known sender key alone can never sever someone\n // else's link.\n registerApiRoute('/web/channel-accounts', {\n method: 'DELETE',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const body = (await c.req.json().catch(() => null)) as {\n platform?: string;\n externalTeamId?: string;\n externalUserId?: string;\n } | null;\n if (!body?.platform || !body.externalTeamId || !body.externalUserId) {\n return c.json({ error: 'platform, externalTeamId and externalUserId are required' }, 400);\n }\n\n const deleted = await accountLinks.deleteAccountLinkForUser({\n userId: tenant.userId,\n platform: body.platform,\n externalTeamId: body.externalTeamId,\n externalUserId: body.externalUserId,\n });\n return c.json({ deleted });\n },\n }),\n // Set (or clear, with `factoryProjectId: null`) which Factory project a\n // link's channel runs route to. The storage write is guarded by the\n // caller's tenant userId — a known sender key alone can never repoint\n // someone else's link — and a non-null factory must exist in the caller's\n // org.\n registerApiRoute('/web/channel-accounts/default-factory', {\n method: 'PATCH',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const body = (await c.req.json().catch(() => null)) as {\n platform?: string;\n externalTeamId?: string;\n externalUserId?: string;\n factoryProjectId?: string | null;\n } | null;\n if (!body?.platform || !body.externalTeamId || !body.externalUserId || body.factoryProjectId === undefined) {\n return c.json(\n { error: 'platform, externalTeamId, externalUserId and factoryProjectId (nullable) are required' },\n 400,\n );\n }\n\n if (body.factoryProjectId !== null) {\n if (!projects) return c.json({ error: 'factory routing is not configured' }, 400);\n const factory = await projects.get({ orgId: tenant.orgId ?? '', id: body.factoryProjectId });\n if (!factory) return c.json({ error: 'unknown factory' }, 400);\n }\n\n const updated = await accountLinks.setDefaultFactory({\n userId: tenant.userId,\n platform: body.platform,\n externalTeamId: body.externalTeamId,\n externalUserId: body.externalUserId,\n factoryProjectId: body.factoryProjectId,\n });\n return c.json({ updated });\n },\n }),\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"connect-route.js","names":[],"sources":["../../../src/integrations/slack/connect-route.ts"],"sourcesContent":["import { registerApiRoute } from '@mastra/core/server';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { StateSigner } from '../../state-signing.js';\nimport type { ChannelIdentityStorage } from '../../storage/domains/channel-identity/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\n\n/**\n * Payload shape for the Connections list: the platform sender key +\n * link time, without the tenant ids (the caller IS the tenant).\n */\ninterface ConnectedChannelAccountPayload {\n platform: string;\n externalTeamId: string;\n externalUserId: string;\n /** Display names captured at link time (OIDC profile claims); ids fall back. */\n externalTeamName?: string;\n externalUserName?: string;\n /** Which Factory project this link's channel runs route to (unset = not picked yet). */\n defaultFactoryProjectId?: string;\n linkedAt: string;\n}\n\n/**\n * Config for the web-initiated \"Sign in with Slack\" (OIDC) connect flow. All\n * values come from the env-supplied Slack app. Absent → the OIDC routes\n * respond with an error redirect and the list endpoint reports\n * `canConnect: false` so the UI hides its Connect button.\n */\ninterface SlackOidcConfig {\n clientId: string;\n clientSecret: string;\n /**\n * Public HTTPS origin the OIDC `redirect_uri` is built from (Slack rejects\n * plain-http redirect URLs, so locally this is the tunnel origin). Must also\n * be registered as a redirect URL on the Slack app.\n */\n redirectBaseUrl: string;\n /** Browser-facing origin post-connect redirects land on (the SPA host). */\n uiOrigin?: string;\n}\n\nconst SLACK_AUTHORIZE_URL = 'https://slack.com/openid/connect/authorize';\nconst SLACK_TOKEN_URL = 'https://slack.com/api/openid.connect.token';\nconst OIDC_CALLBACK_PATH = '/connect/slack/oidc/callback';\nconst SLACK_TOKEN_TIMEOUT_MS = 10_000;\n/** Matches the signer's own `state` lifetime — past it, `verify` rejects anyway. */\nconst STATE_REPLAY_WINDOW_MS = 10 * 60 * 1000;\n\n/**\n * Decode a JWT's payload WITHOUT signature verification. Safe here because the\n * `id_token` arrives directly from Slack's token endpoint over TLS in a\n * confidential-client code exchange — the transport authenticates the issuer,\n * which is the standard OIDC allowance for this flow. `iss`/`aud`/`exp` are\n * still checked by the caller.\n */\nfunction decodeJwtPayload(jwt: string): Record<string, unknown> | null {\n const payload = jwt.split('.')[1];\n if (!payload) return null;\n try {\n return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/**\n * Slack account-linking routes.\n *\n * A link is only ever written by the OIDC flow below, where Slack itself\n * asserts the `(team, user)` pair in the id_token — so the web user has to\n * actually control the Slack account they bind. `/connect/slack` is just the\n * Slack-side entry point: it carries no identity and writes nothing, it only\n * sends the visitor to Connections, where the flow starts.\n */\nexport function createSlackConnectRoutes(deps: {\n auth: RouteAuth;\n accountLinks: ChannelIdentityStorage;\n /** Signs the OIDC `state`, binding the round-trip to the initiating tenant. */\n tenantStateSigner?: StateSigner;\n oidc?: SlackOidcConfig;\n /**\n * Factory projects domain, for validating (and listing to) the per-link\n * default factory. Unset → the default-factory PATCH route rejects.\n */\n projects?: FactoryProjectsStorage;\n}): ApiRoute[] {\n const { auth, accountLinks, tenantStateSigner, oidc, projects } = deps;\n const oidcEnabled = Boolean(oidc && tenantStateSigner);\n const uiOrigin = oidc?.uiOrigin?.replace(/\\/$/, '') ?? '';\n\n // A signed `state` stays valid for its whole 10-minute window, so on its own\n // it authorizes the binding repeatedly: anyone who captures a callback URL\n // can re-run the exchange with their own fresh Slack `code` and bind their\n // Slack account to the tenant that started the flow. Burning the nonce makes\n // the state single-use. Scope note: this is per-process, so it does not stop\n // a replay routed to a different replica — narrowing that further needs\n // shared storage, which is out of proportion to a 10-minute window here.\n const consumedNonces = new Map<string, number>();\n const consumeNonce = (nonce: string): boolean => {\n const now = Date.now();\n for (const [seen, expiresAt] of consumedNonces) {\n if (expiresAt <= now) consumedNonces.delete(seen);\n }\n if (consumedNonces.has(nonce)) return false;\n consumedNonces.set(nonce, now + STATE_REPLAY_WINDOW_MS);\n return true;\n };\n\n // mc-web can resolve a different hono version than @mastra/factory, so the\n // registerApiRoute handler `Context` and the factory `RouteAuth` `Context`\n // are structurally incompatible (private `[GET_MATCH_RESULT]` symbol). Cast\n // through `unknown` at the seam — same `loose()` workaround the factory\n // routes use internally.\n type RouteAuthContext = Parameters<RouteAuth['ensureUser']>[0];\n const loose = (c: unknown): RouteAuthContext => c as RouteAuthContext;\n\n return [\n registerApiRoute('/connect/slack', {\n method: 'GET',\n // No auth: nothing here reads a tenant or writes a link. The SPA route\n // this lands on requires a session and bounces through login itself.\n requiresAuth: false,\n handler: async c => c.redirect(`${uiOrigin}/settings/connections`),\n }),\n // Web-initiated connect: \"Sign in with Slack\" (OIDC). The only route that\n // writes a link, because it is the only one that proves the signed-in web\n // user actually CONTROLS the Slack account: Slack itself asserts the\n // (team, user) pair in the id_token.\n registerApiRoute('/connect/slack/oidc/start', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!oidcEnabled) return c.redirect(`${uiOrigin}/?slack=error`);\n\n const factoryProjectId = c.req.query('factoryId')?.trim() || undefined;\n const startPath = factoryProjectId\n ? `/connect/slack/oidc/start?factoryId=${encodeURIComponent(factoryProjectId)}`\n : '/connect/slack/oidc/start';\n\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) {\n return c.redirect(`/auth/login?returnTo=${encodeURIComponent(startPath)}`);\n }\n\n const params = new URLSearchParams({\n response_type: 'code',\n // `profile` adds display-name claims (user name, team name) to the\n // id_token so Connections can show names instead of ids.\n scope: 'openid profile',\n client_id: oidc!.clientId,\n // Personal accounts have no org; the signer requires a string, so an\n // empty org round-trips and is mapped back to undefined on save.\n state: tenantStateSigner!.sign(tenant.orgId ?? '', tenant.userId, { factoryProjectId }),\n redirect_uri: `${oidc!.redirectBaseUrl.replace(/\\/$/, '')}${OIDC_CALLBACK_PATH}`,\n });\n return c.redirect(`${SLACK_AUTHORIZE_URL}?${params.toString()}`);\n },\n }),\n // OIDC callback. Authenticates via the signed `state` (which carries the\n // initiating tenant) rather than the session cookie — the callback may\n // arrive on a different origin (the public tunnel) where the SPA's\n // host-scoped cookie is not sent.\n registerApiRoute(OIDC_CALLBACK_PATH, {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!oidcEnabled) return c.redirect(`${uiOrigin}/?slack=error`);\n\n const tenant = tenantStateSigner!.verify(c.req.query('state'));\n const code = c.req.query('code');\n if (!tenant || !code) return c.redirect(`${uiOrigin}/?slack=error`);\n // Burn the state before spending the code, not after saving: a replay\n // must be refused even if the first attempt fails partway through.\n if (!consumeNonce(tenant.nonce)) return c.redirect(`${uiOrigin}/?slack=error`);\n\n // Bound the exchange: without a deadline a slow token endpoint parks the\n // request until the platform's own timeout, and this route is reachable\n // by anyone holding a signed state.\n const tokenRes = await fetch(SLACK_TOKEN_URL, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: oidc!.clientId,\n client_secret: oidc!.clientSecret,\n code,\n redirect_uri: `${oidc!.redirectBaseUrl.replace(/\\/$/, '')}${OIDC_CALLBACK_PATH}`,\n }),\n signal: AbortSignal.timeout(SLACK_TOKEN_TIMEOUT_MS),\n }).catch(() => null);\n const token = (await tokenRes?.json().catch(() => null)) as { ok?: boolean; id_token?: string } | null;\n if (!tokenRes?.ok || !token?.ok || typeof token.id_token !== 'string') {\n return c.redirect(`${uiOrigin}/?slack=error`);\n }\n\n const claims = decodeJwtPayload(token.id_token);\n const teamId = claims?.['https://slack.com/team_id'];\n const slackUserId = claims?.['https://slack.com/user_id'];\n // Display-only claims from the `profile` scope — absent claims just\n // mean the settings list falls back to ids.\n const teamName = claims?.['https://slack.com/team_name'];\n const userName = claims?.name;\n // `exp` is seconds since the epoch. A missing or past `exp` means the\n // assertion is not currently valid, so it cannot bind an account.\n const expiresAt = typeof claims?.exp === 'number' ? claims.exp * 1000 : null;\n if (\n !claims ||\n claims.iss !== 'https://slack.com' ||\n claims.aud !== oidc!.clientId ||\n expiresAt === null ||\n expiresAt <= Date.now() ||\n typeof teamId !== 'string' ||\n typeof slackUserId !== 'string'\n ) {\n return c.redirect(`${uiOrigin}/?slack=error`);\n }\n\n await accountLinks.saveAccountLink({\n platform: 'slack',\n externalTeamId: teamId,\n externalUserId: slackUserId,\n orgId: tenant.orgId || undefined,\n userId: tenant.userId,\n externalTeamName: typeof teamName === 'string' ? teamName : undefined,\n externalUserName: typeof userName === 'string' ? userName : undefined,\n });\n\n const successPath = tenant.factoryProjectId\n ? `/factories/${encodeURIComponent(tenant.factoryProjectId)}/settings/connections/slack`\n : '/settings/connections';\n return c.redirect(`${uiOrigin}${successPath}?slack=connected`);\n },\n }),\n // The caller's own linked channel accounts for Connections. Tenant-scoped:\n // you only ever see your own links.\n registerApiRoute('/web/channel-accounts', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const links = await accountLinks.listAccountLinksForUser(tenant.userId);\n const accounts: ConnectedChannelAccountPayload[] = links.map(link => ({\n platform: link.platform,\n externalTeamId: link.externalTeamId,\n externalUserId: link.externalUserId,\n externalTeamName: link.externalTeamName,\n externalUserName: link.externalUserName,\n defaultFactoryProjectId: link.defaultFactoryProjectId,\n linkedAt: link.linkedAt.toISOString(),\n }));\n // `canConnect` tells the settings UI whether the web-initiated OIDC\n // connect flow is configured (vs. Slack-side Connect card only).\n return c.json({ accounts, canConnect: oidcEnabled });\n },\n }),\n // Self-service disconnect. The storage delete is guarded by the caller's\n // tenant userId, so a known sender key alone can never sever someone\n // else's link.\n registerApiRoute('/web/channel-accounts', {\n method: 'DELETE',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const body = (await c.req.json().catch(() => null)) as {\n platform?: string;\n externalTeamId?: string;\n externalUserId?: string;\n } | null;\n if (!body?.platform || !body.externalTeamId || !body.externalUserId) {\n return c.json({ error: 'platform, externalTeamId and externalUserId are required' }, 400);\n }\n\n const deleted = await accountLinks.deleteAccountLinkForUser({\n userId: tenant.userId,\n platform: body.platform,\n externalTeamId: body.externalTeamId,\n externalUserId: body.externalUserId,\n });\n return c.json({ deleted });\n },\n }),\n // Set (or clear, with `factoryProjectId: null`) which Factory project a\n // link's channel runs route to. The storage write is guarded by the\n // caller's tenant userId — a known sender key alone can never repoint\n // someone else's link — and a non-null factory must exist in the caller's\n // org.\n registerApiRoute('/web/channel-accounts/default-factory', {\n method: 'PATCH',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const body = (await c.req.json().catch(() => null)) as {\n platform?: string;\n externalTeamId?: string;\n externalUserId?: string;\n factoryProjectId?: string | null;\n } | null;\n if (!body?.platform || !body.externalTeamId || !body.externalUserId || body.factoryProjectId === undefined) {\n return c.json(\n { error: 'platform, externalTeamId, externalUserId and factoryProjectId (nullable) are required' },\n 400,\n );\n }\n\n if (body.factoryProjectId !== null) {\n if (!projects) return c.json({ error: 'factory routing is not configured' }, 400);\n const factory = await projects.get({ orgId: tenant.orgId ?? '', id: body.factoryProjectId });\n if (!factory) return c.json({ error: 'unknown factory' }, 400);\n }\n\n const updated = await accountLinks.setDefaultFactory({\n userId: tenant.userId,\n platform: body.platform,\n externalTeamId: body.externalTeamId,\n externalUserId: body.externalUserId,\n factoryProjectId: body.factoryProjectId,\n });\n return c.json({ updated });\n },\n }),\n ];\n}\n"],"mappings":"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"}
@@ -22,7 +22,7 @@ var SlackFeedPublisher = class {
22
22
  };
23
23
  function fitSlackMarkdown(markdown) {
24
24
  if (markdown.length <= MAX_SLACK_MARKDOWN_CHARS) return markdown;
25
- return `${markdown.slice(0, MAX_SLACK_MARKDOWN_CHARS - 1).replace(/[\uD800-\uDBFF]$/, "")}…`;
25
+ return `${markdown.slice(0, 11999).replace(/[\uD800-\uDBFF]$/, "")}…`;
26
26
  }
27
27
  /**
28
28
  * The one key both sync directions stamp on a mirrored message, so an ingested
@@ -1 +1 @@
1
- {"version":3,"file":"feed-publisher.js","names":["#controller"],"sources":["../../../src/integrations/slack/feed-publisher.ts"],"sourcesContent":["/**\n * Mirrors work-item feed comments into the Slack thread the card was born from.\n * The bot cannot post as the commenter, so attribution is textual: `**Name**: body`.\n */\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\n\nimport type { WorkItemCommentRow } from '../../storage/domains/comments/base.js';\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\nimport type { ExternalWorkItemSource, WorkItemRow } from '../../storage/domains/work-items/base.js';\n\n/** Slack rejects a `markdown_text` block over this; comments allow 16k. */\nconst MAX_SLACK_MARKDOWN_CHARS = 12_000;\n\nexport class SlackFeedPublisher implements WorkItemFeedPublisher {\n readonly id = 'slack';\n\n readonly #controller: MountedMastraCode['controller'] | undefined;\n\n constructor({ controller }: { controller?: MountedMastraCode['controller'] }) {\n this.#controller = controller;\n }\n\n async publish(\n comment: WorkItemCommentRow,\n workItem: WorkItemRow,\n ): Promise<{ source: ExternalWorkItemSource } | null> {\n const source = workItem.externalSource;\n if (source?.integrationId !== this.id || source.type !== 'slack-thread') return null;\n\n // Null until the channels' lazy `initialize()` resolves; no outbox retries.\n const sdk = this.#controller?.getChannels()?.sdk;\n if (!sdk) {\n console.warn('[slack] feed mirror skipped, channels not initialized yet', { commentId: comment.id });\n return null;\n }\n\n const author = comment.author.displayName ?? comment.author.id;\n const sent = await sdk.thread(source.externalId).post({\n markdown: fitSlackMarkdown(`**${author}**: ${comment.body}`),\n });\n return { source: slackCommentSource(source.externalId, sent.id, source.workspaceId) };\n }\n}\n\nfunction fitSlackMarkdown(markdown: string): string {\n if (markdown.length <= MAX_SLACK_MARKDOWN_CHARS) return markdown;\n // `slice` counts UTF-16 units, so a cut inside an emoji leaves half a pair.\n return `${markdown.slice(0, MAX_SLACK_MARKDOWN_CHARS - 1).replace(/[\\uD800-\\uDBFF]$/, '')}…`;\n}\n\n/**\n * The one key both sync directions stamp on a mirrored message, so an ingested\n * aside and a mirrored comment cannot diverge. Workspace-scoped like the card's\n * own key: a channel id and a `ts` only identify a message inside the team that\n * issued them, and one project can hold two Slack workspaces.\n */\nexport function slackCommentSource(threadId: string, messageTs: string, teamId?: string): ExternalWorkItemSource {\n return {\n integrationId: 'slack',\n type: 'message',\n ...(teamId ? { workspaceId: teamId } : {}),\n externalId: `${threadId.split(':')[1] ?? ''}:${messageTs}`,\n };\n}\n"],"mappings":";;AAYA,MAAM,2BAA2B;AAEjC,IAAa,qBAAb,MAAiE;CAC/D,KAAc;CAEd;CAEA,YAAY,EAAE,cAAgE;EAC5E,KAAKA,cAAc;CACrB;CAEA,MAAM,QACJ,SACA,UACoD;EACpD,MAAM,SAAS,SAAS;EACxB,IAAI,QAAQ,kBAAkB,KAAK,MAAM,OAAO,SAAS,gBAAgB,OAAO;EAGhF,MAAM,MAAM,KAAKA,aAAa,YAAY,CAAC,EAAE;EAC7C,IAAI,CAAC,KAAK;GACR,QAAQ,KAAK,6DAA6D,EAAE,WAAW,QAAQ,GAAG,CAAC;GACnG,OAAO;EACT;EAEA,MAAM,SAAS,QAAQ,OAAO,eAAe,QAAQ,OAAO;EAC5D,MAAM,OAAO,MAAM,IAAI,OAAO,OAAO,UAAU,CAAC,CAAC,KAAK,EACpD,UAAU,iBAAiB,KAAK,OAAO,MAAM,QAAQ,MAAM,EAC7D,CAAC;EACD,OAAO,EAAE,QAAQ,mBAAmB,OAAO,YAAY,KAAK,IAAI,OAAO,WAAW,EAAE;CACtF;AACF;AAEA,SAAS,iBAAiB,UAA0B;CAClD,IAAI,SAAS,UAAU,0BAA0B,OAAO;CAExD,OAAO,GAAG,SAAS,MAAM,GAAG,2BAA2B,CAAC,CAAC,CAAC,QAAQ,oBAAoB,EAAE,EAAE;AAC5F;;;;;;;AAQA,SAAgB,mBAAmB,UAAkB,WAAmB,QAAyC;CAC/G,OAAO;EACL,eAAe;EACf,MAAM;EACN,GAAI,SAAS,EAAE,aAAa,OAAO,IAAI,CAAC;EACxC,YAAY,GAAG,SAAS,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG,GAAG;CACjD;AACF"}
1
+ {"version":3,"file":"feed-publisher.js","names":[],"sources":["../../../src/integrations/slack/feed-publisher.ts"],"sourcesContent":["/**\n * Mirrors work-item feed comments into the Slack thread the card was born from.\n * The bot cannot post as the commenter, so attribution is textual: `**Name**: body`.\n */\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\n\nimport type { WorkItemCommentRow } from '../../storage/domains/comments/base.js';\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\nimport type { ExternalWorkItemSource, WorkItemRow } from '../../storage/domains/work-items/base.js';\n\n/** Slack rejects a `markdown_text` block over this; comments allow 16k. */\nconst MAX_SLACK_MARKDOWN_CHARS = 12_000;\n\nexport class SlackFeedPublisher implements WorkItemFeedPublisher {\n readonly id = 'slack';\n\n readonly #controller: MountedMastraCode['controller'] | undefined;\n\n constructor({ controller }: { controller?: MountedMastraCode['controller'] }) {\n this.#controller = controller;\n }\n\n async publish(\n comment: WorkItemCommentRow,\n workItem: WorkItemRow,\n ): Promise<{ source: ExternalWorkItemSource } | null> {\n const source = workItem.externalSource;\n if (source?.integrationId !== this.id || source.type !== 'slack-thread') return null;\n\n // Null until the channels' lazy `initialize()` resolves; no outbox retries.\n const sdk = this.#controller?.getChannels()?.sdk;\n if (!sdk) {\n console.warn('[slack] feed mirror skipped, channels not initialized yet', { commentId: comment.id });\n return null;\n }\n\n const author = comment.author.displayName ?? comment.author.id;\n const sent = await sdk.thread(source.externalId).post({\n markdown: fitSlackMarkdown(`**${author}**: ${comment.body}`),\n });\n return { source: slackCommentSource(source.externalId, sent.id, source.workspaceId) };\n }\n}\n\nfunction fitSlackMarkdown(markdown: string): string {\n if (markdown.length <= MAX_SLACK_MARKDOWN_CHARS) return markdown;\n // `slice` counts UTF-16 units, so a cut inside an emoji leaves half a pair.\n return `${markdown.slice(0, MAX_SLACK_MARKDOWN_CHARS - 1).replace(/[\\uD800-\\uDBFF]$/, '')}…`;\n}\n\n/**\n * The one key both sync directions stamp on a mirrored message, so an ingested\n * aside and a mirrored comment cannot diverge. Workspace-scoped like the card's\n * own key: a channel id and a `ts` only identify a message inside the team that\n * issued them, and one project can hold two Slack workspaces.\n */\nexport function slackCommentSource(threadId: string, messageTs: string, teamId?: string): ExternalWorkItemSource {\n return {\n integrationId: 'slack',\n type: 'message',\n ...(teamId ? { workspaceId: teamId } : {}),\n externalId: `${threadId.split(':')[1] ?? ''}:${messageTs}`,\n };\n}\n"],"mappings":";;AAYA,MAAM,2BAA2B;AAEjC,IAAa,qBAAb,MAAiE;CAC/D,KAAc;CAEd;CAEA,YAAY,EAAE,cAAgE;EAC5E,KAAK,cAAc;CACrB;CAEA,MAAM,QACJ,SACA,UACoD;EACpD,MAAM,SAAS,SAAS;EACxB,IAAI,QAAQ,kBAAkB,KAAK,MAAM,OAAO,SAAS,gBAAgB,OAAO;EAGhF,MAAM,MAAM,KAAK,aAAa,YAAY,CAAC,EAAE;EAC7C,IAAI,CAAC,KAAK;GACR,QAAQ,KAAK,6DAA6D,EAAE,WAAW,QAAQ,GAAG,CAAC;GACnG,OAAO;EACT;EAEA,MAAM,SAAS,QAAQ,OAAO,eAAe,QAAQ,OAAO;EAC5D,MAAM,OAAO,MAAM,IAAI,OAAO,OAAO,UAAU,CAAC,CAAC,KAAK,EACpD,UAAU,iBAAiB,KAAK,OAAO,MAAM,QAAQ,MAAM,EAC7D,CAAC;EACD,OAAO,EAAE,QAAQ,mBAAmB,OAAO,YAAY,KAAK,IAAI,OAAO,WAAW,EAAE;CACtF;AACF;AAEA,SAAS,iBAAiB,UAA0B;CAClD,IAAI,SAAS,UAAU,0BAA0B,OAAO;CAExD,OAAO,GAAG,SAAS,MAAM,GAAG,KAA4B,CAAC,CAAC,QAAQ,oBAAoB,EAAE,EAAE;AAC5F;;;;;;;AAQA,SAAgB,mBAAmB,UAAkB,WAAmB,QAAyC;CAC/G,OAAO;EACL,eAAe;EACf,MAAM;EACN,GAAI,SAAS,EAAE,aAAa,OAAO,IAAI,CAAC;EACxC,YAAY,GAAG,SAAS,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG,GAAG;CACjD;AACF"}