@mastra/factory 0.26.0-alpha.2 → 0.26.0-alpha.3

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 (154) hide show
  1. package/README.md +74 -7
  2. package/dist/boards/define-board.d.ts +19 -0
  3. package/dist/boards/define-board.d.ts.map +1 -1
  4. package/dist/boards/define-board.js +41 -9
  5. package/dist/boards/define-board.js.map +1 -1
  6. package/dist/boards/project-boards.d.ts +78 -0
  7. package/dist/boards/project-boards.d.ts.map +1 -0
  8. package/dist/boards/project-boards.js +194 -0
  9. package/dist/boards/project-boards.js.map +1 -0
  10. package/dist/boards/review.js +2 -2
  11. package/dist/boards/review.js.map +1 -1
  12. package/dist/boards/stored-board.d.ts +101 -0
  13. package/dist/boards/stored-board.d.ts.map +1 -0
  14. package/dist/boards/stored-board.js +139 -0
  15. package/dist/boards/stored-board.js.map +1 -0
  16. package/dist/boards/work.d.ts.map +1 -1
  17. package/dist/boards/work.js +6 -3
  18. package/dist/boards/work.js.map +1 -1
  19. package/dist/capabilities/intake.d.ts +6 -0
  20. package/dist/capabilities/intake.d.ts.map +1 -1
  21. package/dist/factory.d.ts +15 -2
  22. package/dist/factory.d.ts.map +1 -1
  23. package/dist/factory.js +49 -10
  24. package/dist/factory.js.map +1 -1
  25. package/dist/integrations/jira/binding-scope.d.ts +30 -0
  26. package/dist/integrations/jira/binding-scope.d.ts.map +1 -0
  27. package/dist/integrations/jira/binding-scope.js +50 -0
  28. package/dist/integrations/jira/binding-scope.js.map +1 -0
  29. package/dist/integrations/jira/claim.d.ts +12 -0
  30. package/dist/integrations/jira/claim.d.ts.map +1 -1
  31. package/dist/integrations/jira/claim.js +24 -1
  32. package/dist/integrations/jira/claim.js.map +1 -1
  33. package/dist/integrations/jira/intake-jql.d.ts +32 -0
  34. package/dist/integrations/jira/intake-jql.d.ts.map +1 -0
  35. package/dist/integrations/jira/intake-jql.js +57 -0
  36. package/dist/integrations/jira/intake-jql.js.map +1 -0
  37. package/dist/integrations/jira/integration.d.ts +4 -3
  38. package/dist/integrations/jira/integration.d.ts.map +1 -1
  39. package/dist/integrations/jira/integration.js +14 -21
  40. package/dist/integrations/jira/integration.js.map +1 -1
  41. package/dist/integrations/jira/routes.d.ts +3 -0
  42. package/dist/integrations/jira/routes.d.ts.map +1 -1
  43. package/dist/integrations/jira/routes.js +16 -29
  44. package/dist/integrations/jira/routes.js.map +1 -1
  45. package/dist/integrations/jira/rules.d.ts.map +1 -1
  46. package/dist/integrations/jira/rules.js +8 -13
  47. package/dist/integrations/jira/rules.js.map +1 -1
  48. package/dist/integrations/platform/jira/integration.d.ts +7 -1
  49. package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
  50. package/dist/integrations/platform/jira/integration.js +26 -24
  51. package/dist/integrations/platform/jira/integration.js.map +1 -1
  52. package/dist/integrations/platform/jira/routes.d.ts +3 -0
  53. package/dist/integrations/platform/jira/routes.d.ts.map +1 -1
  54. package/dist/integrations/platform/jira/routes.js +17 -28
  55. package/dist/integrations/platform/jira/routes.js.map +1 -1
  56. package/dist/routes/attention-providers.d.ts +2 -1
  57. package/dist/routes/attention-providers.d.ts.map +1 -1
  58. package/dist/routes/attention-providers.js +3 -2
  59. package/dist/routes/attention-providers.js.map +1 -1
  60. package/dist/routes/attention.d.ts +2 -1
  61. package/dist/routes/attention.d.ts.map +1 -1
  62. package/dist/routes/attention.js.map +1 -1
  63. package/dist/routes/boards.d.ts +30 -0
  64. package/dist/routes/boards.d.ts.map +1 -0
  65. package/dist/routes/boards.js +272 -0
  66. package/dist/routes/boards.js.map +1 -0
  67. package/dist/routes/contracts.d.ts +249 -0
  68. package/dist/routes/contracts.d.ts.map +1 -1
  69. package/dist/routes/contracts.js +107 -35
  70. package/dist/routes/contracts.js.map +1 -1
  71. package/dist/routes/intake.d.ts +15 -10
  72. package/dist/routes/intake.d.ts.map +1 -1
  73. package/dist/routes/intake.js +109 -22
  74. package/dist/routes/intake.js.map +1 -1
  75. package/dist/routes/projects.d.ts +3 -0
  76. package/dist/routes/projects.d.ts.map +1 -1
  77. package/dist/routes/projects.js +10 -1
  78. package/dist/routes/projects.js.map +1 -1
  79. package/dist/routes/surface.d.ts +10 -3
  80. package/dist/routes/surface.d.ts.map +1 -1
  81. package/dist/routes/surface.js +12 -1
  82. package/dist/routes/surface.js.map +1 -1
  83. package/dist/routes/tenant-credentials.d.ts +9 -0
  84. package/dist/routes/tenant-credentials.d.ts.map +1 -1
  85. package/dist/routes/tenant-credentials.js +24 -2
  86. package/dist/routes/tenant-credentials.js.map +1 -1
  87. package/dist/routes/work-items.d.ts +3 -1
  88. package/dist/routes/work-items.d.ts.map +1 -1
  89. package/dist/routes/work-items.js +64 -42
  90. package/dist/routes/work-items.js.map +1 -1
  91. package/dist/rules/dispatcher.d.ts +7 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +70 -34
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/processor.d.ts +3 -1
  96. package/dist/rules/processor.d.ts.map +1 -1
  97. package/dist/rules/processor.js +11 -6
  98. package/dist/rules/processor.js.map +1 -1
  99. package/dist/rules/tools.d.ts +2 -1
  100. package/dist/rules/tools.d.ts.map +1 -1
  101. package/dist/rules/tools.js +2 -1
  102. package/dist/rules/tools.js.map +1 -1
  103. package/dist/rules/transition-service.d.ts +3 -1
  104. package/dist/rules/transition-service.d.ts.map +1 -1
  105. package/dist/rules/transition-service.js +12 -12
  106. package/dist/rules/transition-service.js.map +1 -1
  107. package/dist/storage/domains/audit/actions.d.ts +1 -0
  108. package/dist/storage/domains/audit/actions.d.ts.map +1 -1
  109. package/dist/storage/domains/audit/actions.js +6 -1
  110. package/dist/storage/domains/audit/actions.js.map +1 -1
  111. package/dist/storage/domains/boards/base.d.ts +110 -0
  112. package/dist/storage/domains/boards/base.d.ts.map +1 -0
  113. package/dist/storage/domains/boards/base.js +292 -0
  114. package/dist/storage/domains/boards/base.js.map +1 -0
  115. package/dist/storage/domains/boards/schemas.d.ts +11 -0
  116. package/dist/storage/domains/boards/schemas.d.ts.map +1 -0
  117. package/dist/storage/domains/boards/schemas.js +106 -0
  118. package/dist/storage/domains/boards/schemas.js.map +1 -0
  119. package/dist/storage/domains/credentials/base.d.ts +4 -1
  120. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  121. package/dist/storage/domains/credentials/base.js +5 -2
  122. package/dist/storage/domains/credentials/base.js.map +1 -1
  123. package/dist/storage/domains/intake/base.d.ts +86 -5
  124. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  125. package/dist/storage/domains/intake/base.js +298 -59
  126. package/dist/storage/domains/intake/base.js.map +1 -1
  127. package/dist/storage/domains/projects/base.d.ts +2 -1
  128. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  129. package/dist/storage/domains/projects/base.js +15 -10
  130. package/dist/storage/domains/projects/base.js.map +1 -1
  131. package/dist/storage/domains/work-items/base.d.ts +14 -0
  132. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  133. package/dist/storage/domains/work-items/base.js +39 -11
  134. package/dist/storage/domains/work-items/base.js.map +1 -1
  135. package/dist/tools/read-tools.d.ts +3 -1
  136. package/dist/tools/read-tools.d.ts.map +1 -1
  137. package/dist/tools/read-tools.js +3 -2
  138. package/dist/tools/read-tools.js.map +1 -1
  139. package/dist/tools/session-tools.d.ts +3 -1
  140. package/dist/tools/session-tools.d.ts.map +1 -1
  141. package/dist/tools/session-tools.js +2 -1
  142. package/dist/tools/session-tools.js.map +1 -1
  143. package/dist/tools/sessions-state-processor.d.ts +3 -1
  144. package/dist/tools/sessions-state-processor.d.ts.map +1 -1
  145. package/dist/tools/sessions-state-processor.js +2 -1
  146. package/dist/tools/sessions-state-processor.js.map +1 -1
  147. package/dist/tools/work-item-tools.d.ts +3 -1
  148. package/dist/tools/work-item-tools.d.ts.map +1 -1
  149. package/dist/tools/work-item-tools.js +3 -2
  150. package/dist/tools/work-item-tools.js.map +1 -1
  151. package/dist/workspace.js +1 -1
  152. package/factory-skills/factory-plan/SKILL.md +1 -1
  153. package/factory-skills/factory-triage/SKILL.md +18 -7
  154. package/package.json +9 -9
@@ -1,4 +1,7 @@
1
1
  import { JiraApiError } from "./api.js";
2
+ import { jiraIssueFiledIn } from "./claim.js";
3
+ import { unfilteredJiraScopes } from "./intake-jql.js";
4
+ import { scopeJiraSourcesToProject } from "./binding-scope.js";
2
5
  import { DEPLOYMENT_CONNECTION } from "./integration.js";
3
6
  import { registerApiRoute } from "@mastra/core/server";
4
7
  // src/integrations/jira/routes.ts
@@ -9,27 +12,6 @@ function loose(c) {
9
12
  return c;
10
13
  }
11
14
  /**
12
- * Narrow the caller's selected Jira projects to the ones that feed this
13
- * Factory project.
14
- *
15
- * A Jira issue carries no Factory project of its own, so without a binding
16
- * every board view would ingest every selected project's issues into whichever
17
- * Factory happened to be on screen. Routing is explicit: a project feeds this
18
- * Factory only when its binding names both the Factory project and a board.
19
- * Returns the bound board per source so consumers can land cards where the
20
- * user asked. Mirrors the Linear routes' scoping semantics locally — the
21
- * generic binding storage is provider-neutral.
22
- */
23
- async function scopeSourceIdsToProject({ intake, orgId, factoryProjectId, selectedIds }) {
24
- const selected = new Set(selectedIds);
25
- const intakeBoards = {};
26
- for (const binding of await intake.listBindings({
27
- orgId,
28
- integrationId: "jira"
29
- })) if (binding.factoryProjectId === factoryProjectId && binding.board && selected.has(binding.sourceId)) intakeBoards[binding.sourceId] = binding.board;
30
- return intakeBoards;
31
- }
32
- /**
33
15
  * Resolve the org-scoped tenant for a Jira request. Intake selections are
34
16
  * org-owned, so it requires both a signed-in user and an organization — same
35
17
  * tenancy rules as the Linear routes.
@@ -74,7 +56,7 @@ function jiraFetchError(c, err) {
74
56
  */
75
57
  function buildJiraRoutes(options) {
76
58
  const routes = [];
77
- const { jira, auth, intake } = options;
59
+ const { jira, auth, intake, workItems } = options;
78
60
  const enabled = Boolean(jira) && auth.enabled();
79
61
  const diagnostics = () => ({
80
62
  jiraConfigured: Boolean(jira),
@@ -158,19 +140,20 @@ function buildJiraRoutes(options) {
158
140
  message: "Jira intake is turned off in Settings."
159
141
  }, 404);
160
142
  const selectedIds = selection.sourceIds ?? [];
161
- const intakeBoards = factoryProjectId ? await scopeSourceIdsToProject({
143
+ const projectScope = factoryProjectId ? await scopeJiraSourcesToProject({
162
144
  intake,
163
145
  orgId: resolved.tenant.orgId,
164
146
  factoryProjectId,
165
147
  selectedIds
166
148
  }) : null;
167
- const projectIds = intakeBoards ? Object.keys(intakeBoards) : selectedIds;
168
- if (projectIds.length === 0) return c.json({
149
+ const intakeBoards = projectScope?.intakeBoards ?? null;
150
+ const scopes = projectScope?.scopes ?? unfilteredJiraScopes(selectedIds);
151
+ if (scopes.length === 0) return c.json({
169
152
  issues: [],
170
153
  nextCursor: null
171
154
  });
172
155
  try {
173
- const { issues, nextCursor } = await jira.listActiveIssues(after, projectIds);
156
+ const { issues, nextCursor } = await jira.listActiveIssues(after, scopes);
174
157
  const issuePayload = issues.map((issue) => ({
175
158
  id: issue.id,
176
159
  identifier: issue.identifier,
@@ -227,19 +210,23 @@ function buildJiraRoutes(options) {
227
210
  error: "jira_intake_disabled",
228
211
  message: "Jira intake is turned off in Settings."
229
212
  }, 404);
230
- const intakeBoards = await scopeSourceIdsToProject({
213
+ const projectScope = await scopeJiraSourcesToProject({
231
214
  intake,
232
215
  orgId: resolved.tenant.orgId,
233
216
  factoryProjectId,
234
217
  selectedIds: selection.sourceIds ?? []
235
218
  });
236
- if (Object.keys(intakeBoards).length === 0) return c.json({ error: "issue_not_found" }, 404);
219
+ if (projectScope.scopes.length === 0) return c.json({ error: "issue_not_found" }, 404);
237
220
  try {
238
221
  const issue = await jira.intake.getIssue({
239
222
  connection: DEPLOYMENT_CONNECTION,
240
223
  issueId: issueRef
241
224
  });
242
- const routed = issue?.sourceId != null && issue.sourceId in intakeBoards;
225
+ const routed = issue?.sourceId != null && issue.sourceId in projectScope.intakeBoards && (projectScope.takes(issue.sourceId, issue.labels) || await jiraIssueFiledIn(workItems, {
226
+ orgId: resolved.tenant.orgId,
227
+ factoryProjectId,
228
+ issueRef: issue.id
229
+ }));
243
230
  if (!issue || issue.identifier.toUpperCase() !== identifier.toUpperCase() || !routed) return c.json({ error: "issue_not_found" }, 404);
244
231
  return c.json({
245
232
  identifier: issue.identifier,
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/jira/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the Jira intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the host auth gate.\n * Mirrors the Linear module minus everything OAuth: there is no\n * connect/callback flow, no state signer, and no per-org connection storage —\n * Jira credentials are deployment-global constructor config on the\n * integration instance. Every route still re-resolves the authenticated user\n * from the request and scopes intake selections by the caller's org.\n *\n * When the feature is disabled (no auth, or no intake storage),\n * `buildJiraRoutes` returns only `GET /web/jira/status`, which reports\n * `enabled:false` so the SPA can cleanly hide all Jira UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport { JiraApiError } from './api.js';\nimport { DEPLOYMENT_CONNECTION } from './integration.js';\nimport type { JiraIntegration } from './integration.js';\nimport type { JiraRulesIngress } from './rules.js';\n\ntype RouteContext = Context;\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst ISSUE_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*-\\d+$/;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\n/**\n * Non-secret diagnostic snapshot of every Jira feature gate, mirroring the\n * Linear diagnostics shape. Only booleans — never values.\n */\nexport interface JiraFeatureDiagnostics {\n jiraConfigured: boolean;\n factoryAuthEnabled: boolean;\n appDbConfigured: boolean;\n}\n\nexport interface MountJiraRoutesOptions {\n /**\n * The integration instance providing REST access. Required for everything\n * beyond the disabled `status` route.\n */\n jira?: JiraIntegration;\n /** Host auth seam. Intake selections are org-owned, so the feature is inert without it. */\n auth: RouteAuth;\n /**\n * Cross-integration intake selection domain. Required for the issues route's\n * project filter; when absent, only the disabled `status` route is served.\n */\n intake?: IntakeStorage;\n /**\n * Factory rules ingress for observed issues. When present, a board-scoped\n * issue listing also materializes new cards through the Jira event rules —\n * the same automatic intake Linear has.\n */\n ingestFactoryIssues?: (input: JiraRulesIngress) => Promise<unknown>;\n}\n\n/**\n * Narrow the caller's selected Jira projects to the ones that feed this\n * Factory project.\n *\n * A Jira issue carries no Factory project of its own, so without a binding\n * every board view would ingest every selected project's issues into whichever\n * Factory happened to be on screen. Routing is explicit: a project feeds this\n * Factory only when its binding names both the Factory project and a board.\n * Returns the bound board per source so consumers can land cards where the\n * user asked. Mirrors the Linear routes' scoping semantics locally — the\n * generic binding storage is provider-neutral.\n */\nasync function scopeSourceIdsToProject({\n intake,\n orgId,\n factoryProjectId,\n selectedIds,\n}: {\n intake: IntakeStorage;\n orgId: string;\n factoryProjectId: string;\n selectedIds: string[];\n}): Promise<Record<string, string>> {\n const selected = new Set(selectedIds);\n const intakeBoards: Record<string, string> = {};\n for (const binding of await intake.listBindings({ orgId, integrationId: 'jira' })) {\n if (binding.factoryProjectId === factoryProjectId && binding.board && selected.has(binding.sourceId)) {\n intakeBoards[binding.sourceId] = binding.board;\n }\n }\n return intakeBoards;\n}\n\n/**\n * Resolve the org-scoped tenant for a Jira request. Intake selections are\n * org-owned, so it requires both a signed-in user and an organization — same\n * tenancy rules as the Linear routes.\n */\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Jira intake requires an organization. Personal accounts cannot use Jira intake.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque Jira pagination cursor from the query string. Cursors are\n * server-issued (`nextPageToken`), so anything outside a conservative\n * charset/length is rejected rather than forwarded to Jira.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 512 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Map a Jira read failure to the API response for the SPA. */\nfunction jiraFetchError(c: RouteContext, err: unknown) {\n if (err instanceof JiraApiError && err.code === 'jira_auth_failed') {\n return c.json(\n {\n error: 'jira_auth_failed',\n message: 'Jira rejected the configured credentials. Check JIRA_EMAIL and JIRA_API_TOKEN.',\n },\n 409,\n );\n }\n return c.json({ error: 'jira_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the Jira routes as Mastra `apiRoutes`. When the feature is disabled,\n * returns only the `status` route so the SPA can detect the disabled state.\n */\nexport function buildJiraRoutes(options: MountJiraRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { jira, auth, intake } = options;\n const enabled = Boolean(jira) && auth.enabled();\n const diagnostics = (): JiraFeatureDiagnostics => ({\n jiraConfigured: Boolean(jira),\n factoryAuthEnabled: auth.enabled(),\n appDbConfigured: true,\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/jira/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !jira || !intake) {\n return c.json({\n enabled: false,\n configured: Boolean(jira),\n mode: 'direct',\n site: null,\n reason: 'missing_config',\n diagnostics: diagnostics(),\n });\n }\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized', reason: 'auth_required' }, 401);\n\n const site = new URL(jira.baseUrl).host;\n if (!tenant.orgId) {\n return c.json({\n enabled: true,\n configured: true,\n mode: 'direct',\n organizationRequired: true,\n site,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n\n // Deployment-global credentials: configured means ready — there is no\n // per-org connection step like Linear's OAuth.\n return c.json({\n enabled: true,\n configured: true,\n mode: 'direct',\n site,\n reason: 'ready',\n diagnostics: diagnostics(),\n });\n },\n }),\n );\n\n // Without the integration instance or the intake domain the feature can't\n // serve org-scoped data — serve only the disabled `status` route.\n if (!enabled || !jira || !intake) {\n return routes;\n }\n\n // ── List the site's projects (Settings intake-source picker) ────────────\n routes.push(\n registerApiRoute('/web/jira/projects', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n try {\n const sources = await jira.intake.listSources(resolved.tenant);\n return c.json({\n projects: sources.map(source => ({\n id: source.id,\n name: source.name,\n key: typeof source.metadata?.key === 'string' ? source.metadata.key : null,\n })),\n });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the site's active issues (cursor-paged) ────────────────────────\n // Respects the caller's intake config: disabled Jira intake 404s the\n // source, and an explicit project selection narrows the issue filter.\n routes.push(\n registerApiRoute('/web/jira/issues', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const after = parseAfterCursor(c.req.query('after'));\n if (after === null) return c.json({ error: 'invalid_cursor' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['jira'] });\n const selection = config.jira!;\n if (!selection.enabled) {\n return c.json({ error: 'jira_intake_disabled', message: 'Jira intake is turned off in Settings.' }, 404);\n }\n\n // No projects selected means nothing is synced — don't fan out to Jira.\n const selectedIds = selection.sourceIds ?? [];\n // A board request only ever sees the projects routed to a board of\n // that Factory project; unscoped listing stays available to callers\n // that don't view a specific board.\n const intakeBoards = factoryProjectId\n ? await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds,\n })\n : null;\n const projectIds = intakeBoards ? Object.keys(intakeBoards) : selectedIds;\n if (projectIds.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const { issues, nextCursor } = await jira.listActiveIssues(after, projectIds);\n const issuePayload = issues.map(issue => ({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n author: issue.author,\n state: issue.state ?? '',\n stateType: issue.stateType ?? '',\n priorityLabel: issue.priority ?? '',\n assignee: issue.assignee,\n project: issue.source,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n sourceId: issue.sourceId || null,\n }));\n if (factoryProjectId && intakeBoards && options.ingestFactoryIssues) {\n await options.ingestFactoryIssues({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n factoryProjectId,\n issues: issuePayload.map(issue => ({ ...issue, site: null })),\n intakeBoards,\n });\n }\n return c.json({ issues: issuePayload, nextCursor });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n routes.push(\n registerApiRoute('/web/jira/issues/:identifier', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const identifier = c.req.param('identifier');\n const factoryProjectId = c.req.query('factoryProjectId');\n const issueRef = c.req.query('issueRef');\n if (!ISSUE_KEY_RE.test(identifier)) return c.json({ error: 'invalid_issue_identifier' }, 400);\n if (!factoryProjectId || !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n if (!issueRef || issueRef.length > 2_048) return c.json({ error: 'invalid_issue_ref' }, 400);\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['jira'] });\n const selection = config.jira!;\n if (!selection.enabled) {\n return c.json({ error: 'jira_intake_disabled', message: 'Jira intake is turned off in Settings.' }, 404);\n }\n const intakeBoards = await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds: selection.sourceIds ?? [],\n });\n if (Object.keys(intakeBoards).length === 0) return c.json({ error: 'issue_not_found' }, 404);\n\n try {\n const issue = await jira.intake.getIssue({ connection: DEPLOYMENT_CONNECTION, issueId: issueRef });\n // The issue's project must be a source routed to this Factory — an\n // issue from an unbound project reads exactly like one that doesn't\n // exist (same stance as the Linear detail route).\n const routed = issue?.sourceId != null && issue.sourceId in intakeBoards;\n if (!issue || issue.identifier.toUpperCase() !== identifier.toUpperCase() || !routed) {\n return c.json({ error: 'issue_not_found' }, 404);\n }\n return c.json({\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n description: issue.description,\n });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/jira/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the Jira intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the host auth gate.\n * Mirrors the Linear module minus everything OAuth: there is no\n * connect/callback flow, no state signer, and no per-org connection storage —\n * Jira credentials are deployment-global constructor config on the\n * integration instance. Every route still re-resolves the authenticated user\n * from the request and scopes intake selections by the caller's org.\n *\n * When the feature is disabled (no auth, or no intake storage),\n * `buildJiraRoutes` returns only `GET /web/jira/status`, which reports\n * `enabled:false` so the SPA can cleanly hide all Jira UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport { JiraApiError } from './api.js';\nimport { scopeJiraSourcesToProject } from './binding-scope.js';\nimport { jiraIssueFiledIn } from './claim.js';\nimport { unfilteredJiraScopes } from './intake-jql.js';\nimport { DEPLOYMENT_CONNECTION } from './integration.js';\nimport type { JiraIntegration } from './integration.js';\nimport type { JiraRulesIngress } from './rules.js';\n\ntype RouteContext = Context;\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst ISSUE_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*-\\d+$/;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\n/**\n * Non-secret diagnostic snapshot of every Jira feature gate, mirroring the\n * Linear diagnostics shape. Only booleans — never values.\n */\nexport interface JiraFeatureDiagnostics {\n jiraConfigured: boolean;\n factoryAuthEnabled: boolean;\n appDbConfigured: boolean;\n}\n\nexport interface MountJiraRoutesOptions {\n /**\n * The integration instance providing REST access. Required for everything\n * beyond the disabled `status` route.\n */\n jira?: JiraIntegration;\n /** Host auth seam. Intake selections are org-owned, so the feature is inert without it. */\n auth: RouteAuth;\n /**\n * Cross-integration intake selection domain. Required for the issues route's\n * project filter; when absent, only the disabled `status` route is served.\n */\n intake?: IntakeStorage;\n /**\n * Factory rules ingress for observed issues. When present, a board-scoped\n * issue listing also materializes new cards through the Jira event rules —\n * the same automatic intake Linear has.\n */\n ingestFactoryIssues?: (input: JiraRulesIngress) => Promise<unknown>;\n /** Filed cards, so a Factory keeps reading issues it filed after a label filter moves them to another slice. */\n workItems?: Pick<WorkItemsStorage, 'getByClaimKey' | 'listBySource'>;\n}\n\n/**\n * Resolve the org-scoped tenant for a Jira request. Intake selections are\n * org-owned, so it requires both a signed-in user and an organization — same\n * tenancy rules as the Linear routes.\n */\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Jira intake requires an organization. Personal accounts cannot use Jira intake.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque Jira pagination cursor from the query string. Cursors are\n * server-issued (`nextPageToken`), so anything outside a conservative\n * charset/length is rejected rather than forwarded to Jira.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 512 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Map a Jira read failure to the API response for the SPA. */\nfunction jiraFetchError(c: RouteContext, err: unknown) {\n if (err instanceof JiraApiError && err.code === 'jira_auth_failed') {\n return c.json(\n {\n error: 'jira_auth_failed',\n message: 'Jira rejected the configured credentials. Check JIRA_EMAIL and JIRA_API_TOKEN.',\n },\n 409,\n );\n }\n return c.json({ error: 'jira_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the Jira routes as Mastra `apiRoutes`. When the feature is disabled,\n * returns only the `status` route so the SPA can detect the disabled state.\n */\nexport function buildJiraRoutes(options: MountJiraRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { jira, auth, intake, workItems } = options;\n const enabled = Boolean(jira) && auth.enabled();\n const diagnostics = (): JiraFeatureDiagnostics => ({\n jiraConfigured: Boolean(jira),\n factoryAuthEnabled: auth.enabled(),\n appDbConfigured: true,\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/jira/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !jira || !intake) {\n return c.json({\n enabled: false,\n configured: Boolean(jira),\n mode: 'direct',\n site: null,\n reason: 'missing_config',\n diagnostics: diagnostics(),\n });\n }\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized', reason: 'auth_required' }, 401);\n\n const site = new URL(jira.baseUrl).host;\n if (!tenant.orgId) {\n return c.json({\n enabled: true,\n configured: true,\n mode: 'direct',\n organizationRequired: true,\n site,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n\n // Deployment-global credentials: configured means ready — there is no\n // per-org connection step like Linear's OAuth.\n return c.json({\n enabled: true,\n configured: true,\n mode: 'direct',\n site,\n reason: 'ready',\n diagnostics: diagnostics(),\n });\n },\n }),\n );\n\n // Without the integration instance or the intake domain the feature can't\n // serve org-scoped data — serve only the disabled `status` route.\n if (!enabled || !jira || !intake) {\n return routes;\n }\n\n // ── List the site's projects (Settings intake-source picker) ────────────\n routes.push(\n registerApiRoute('/web/jira/projects', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n try {\n const sources = await jira.intake.listSources(resolved.tenant);\n return c.json({\n projects: sources.map(source => ({\n id: source.id,\n name: source.name,\n key: typeof source.metadata?.key === 'string' ? source.metadata.key : null,\n })),\n });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the site's active issues (cursor-paged) ────────────────────────\n // Respects the caller's intake config: disabled Jira intake 404s the\n // source, and an explicit project selection narrows the issue filter.\n routes.push(\n registerApiRoute('/web/jira/issues', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const after = parseAfterCursor(c.req.query('after'));\n if (after === null) return c.json({ error: 'invalid_cursor' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['jira'] });\n const selection = config.jira!;\n if (!selection.enabled) {\n return c.json({ error: 'jira_intake_disabled', message: 'Jira intake is turned off in Settings.' }, 404);\n }\n\n // No projects selected means nothing is synced — don't fan out to Jira.\n const selectedIds = selection.sourceIds ?? [];\n // A board request only ever sees the projects routed to a board of\n // that Factory project; unscoped listing stays available to callers\n // that don't view a specific board.\n const projectScope = factoryProjectId\n ? await scopeJiraSourcesToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds,\n })\n : null;\n const intakeBoards = projectScope?.intakeBoards ?? null;\n const scopes = projectScope?.scopes ?? unfilteredJiraScopes(selectedIds);\n if (scopes.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const { issues, nextCursor } = await jira.listActiveIssues(after, scopes);\n const issuePayload = issues.map(issue => ({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n author: issue.author,\n state: issue.state ?? '',\n stateType: issue.stateType ?? '',\n priorityLabel: issue.priority ?? '',\n assignee: issue.assignee,\n project: issue.source,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n sourceId: issue.sourceId || null,\n }));\n if (factoryProjectId && intakeBoards && options.ingestFactoryIssues) {\n await options.ingestFactoryIssues({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n factoryProjectId,\n issues: issuePayload.map(issue => ({ ...issue, site: null })),\n intakeBoards,\n });\n }\n return c.json({ issues: issuePayload, nextCursor });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n routes.push(\n registerApiRoute('/web/jira/issues/:identifier', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const identifier = c.req.param('identifier');\n const factoryProjectId = c.req.query('factoryProjectId');\n const issueRef = c.req.query('issueRef');\n if (!ISSUE_KEY_RE.test(identifier)) return c.json({ error: 'invalid_issue_identifier' }, 400);\n if (!factoryProjectId || !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n if (!issueRef || issueRef.length > 2_048) return c.json({ error: 'invalid_issue_ref' }, 400);\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['jira'] });\n const selection = config.jira!;\n if (!selection.enabled) {\n return c.json({ error: 'jira_intake_disabled', message: 'Jira intake is turned off in Settings.' }, 404);\n }\n const projectScope = await scopeJiraSourcesToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds: selection.sourceIds ?? [],\n });\n if (projectScope.scopes.length === 0) return c.json({ error: 'issue_not_found' }, 404);\n\n try {\n const issue = await jira.intake.getIssue({ connection: DEPLOYMENT_CONNECTION, issueId: issueRef });\n // An unbound project's issue, or one in another Factory's slice that this\n // Factory never filed, reads like one that doesn't exist (as on Linear).\n const routed =\n issue?.sourceId != null &&\n issue.sourceId in projectScope.intakeBoards &&\n (projectScope.takes(issue.sourceId, issue.labels) ||\n (await jiraIssueFiledIn(workItems, { orgId: resolved.tenant.orgId, factoryProjectId, issueRef: issue.id })));\n if (!issue || issue.identifier.toUpperCase() !== identifier.toUpperCase() || !routed) {\n return c.json({ error: 'issue_not_found' }, 404);\n }\n return c.json({\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n description: issue.description,\n });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/rules.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAoBzD,MAAM,WAAW,gBAAgB;IAC/B,uFAAuF;IACvF,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACjD;AAED,KAAK,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1D,qBAAa,SAAS;;IACR,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,gBAAgB,EAAI;IAEpD,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAuB1F;CA8HF;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;CAAE,EACxC,OAAO,EAAE,kBAAkB,GAC1B,CAAC,CAAC,KAAK,EAAE,gBAAgB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAU7D"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/rules.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAoBzD,MAAM,WAAW,gBAAgB;IAC/B,uFAAuF;IACvF,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACjD;AAED,KAAK,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1D,qBAAa,SAAS;;IACR,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,gBAAgB,EAAI;IAEpD,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAuB1F;CAyHF;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;CAAE,EACxC,OAAO,EAAE,kBAAkB,GAC1B,CAAC,CAAC,KAAK,EAAE,gBAAgB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAU7D"}
@@ -2,7 +2,7 @@ import { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from "../..
2
2
  import { boardEventHandlers, boardForWorkItem, eventBoardId, isTerminalWorkItem, rulePhaseField, withBoardClosure } from "../../boards/semantics.js";
3
3
  import "../../boards/index.js";
4
4
  import { selectEventRule } from "../../rules/event-rules.js";
5
- import { jiraClaimKey } from "./claim.js";
5
+ import { jiraClaimKey, jiraIssueCards } from "./claim.js";
6
6
  // src/integrations/jira/rules.ts
7
7
  const RULE_TIMEOUT_MS = 5e3;
8
8
  async function withRuleTimeout(promise) {
@@ -58,20 +58,15 @@ var JiraRules = class {
58
58
  };
59
59
  }
60
60
  async #heldElsewhere(input, issue) {
61
- const heldLive = (row) => row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);
62
- const claimant = await this.options.storage.getByClaimKey({
61
+ const liveElsewhere = (await jiraIssueCards(this.options.storage, {
63
62
  orgId: input.orgId,
64
- claimKey: jiraClaimKey(issue.id)
65
- });
66
- if (claimant && heldLive(claimant)) return true;
67
- return (await this.options.storage.listBySource({
63
+ issueRef: issue.id
64
+ })).filter((row) => row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row));
65
+ for (const row of liveElsewhere) if (await this.options.projects.get({
68
66
  orgId: input.orgId,
69
- source: {
70
- integrationId: "jira",
71
- type: "issue",
72
- externalId: issue.id
73
- }
74
- })).some(heldLive);
67
+ id: row.factoryProjectId
68
+ })) return true;
69
+ return false;
75
70
  }
76
71
  async #ingestIssue(input, issue, relatedItem, attempt = 1) {
77
72
  const ingressId = `jira:${issue.id}:${issue.updatedAt}`;
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","names":[],"sources":["../../../src/integrations/jira/rules.ts"],"sourcesContent":["import {\n boardEventHandlers,\n boardForWorkItem,\n eventBoardId,\n isTerminalWorkItem,\n rulePhaseField,\n withBoardClosure,\n} from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport { selectEventRule } from '../../rules/event-rules.js';\nimport type { FactoryJiraRuleContext, FactoryRuleDecision } from '../../rules/types.js';\nimport { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport { jiraClaimKey } from './claim.js';\nimport type { JiraEventRules } from './default-rules.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nconst MAX_STALE_RULE_ATTEMPTS = 3;\n\nexport interface JiraIssueIngress {\n /** Stable issue reference — direct Jira id or the Platform-encoded issue reference. */\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Jira reporter, when available. */\n author: string | null;\n /** Jira project key the issue belongs to, when available. */\n project: string | null;\n /** Jira site host the issue was read from, when available. */\n site: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** Jira source the issue was read from; resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n}\n\nexport interface JiraRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n jiraRules: JiraEventRules;\n}\n\nexport interface JiraRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: JiraIssueIngress[];\n /** Board id per bound Jira source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class JiraRules {\n constructor(private readonly options: JiraRulesOptions) {}\n\n async ingest(input: JiraRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const itemsByClaimKey = new Map(items.filter(item => item.claimKey).map(item => [item.claimKey, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n // The claim finds the card even after a metadata drift; the raw issue\n // reference covers cards filed before claims existed.\n const relatedItem = itemsByClaimKey.get(jiraClaimKey(issue.id)) ?? itemsBySourceKey.get(issue.id);\n // One live card per Jira issue per org. The store's claim index is the\n // guarantee; this check spares the dispatcher a refused upsert.\n if (!relatedItem && (await this.#heldElsewhere(input, issue))) {\n statuses.push('missing');\n continue;\n }\n statuses.push(await this.#ingestIssue(input, issue, relatedItem));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #heldElsewhere(input: JiraRulesIngress, issue: JiraIssueIngress): Promise<boolean> {\n const heldLive = (row: WorkItemRow) =>\n row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);\n const claimant = await this.options.storage.getByClaimKey({\n orgId: input.orgId,\n claimKey: jiraClaimKey(issue.id),\n });\n if (claimant && heldLive(claimant)) return true;\n // Cards filed before claims existed carry only the issue reference.\n const legacy = await this.options.storage.listBySource({\n orgId: input.orgId,\n source: { integrationId: 'jira', type: 'issue', externalId: issue.id },\n });\n return legacy.some(heldLive);\n }\n\n async #ingestIssue(\n input: JiraRulesIngress,\n issue: JiraIssueIngress,\n relatedItem: WorkItemRow | undefined,\n attempt = 1,\n ): Promise<IngressStatus> {\n const ingressId = `jira:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryJiraRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'jira', id: ingressId },\n cause: `jira.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'jira-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n acceptedAt: relatedItem.acceptedAt,\n metadata: relatedItem.metadata,\n },\n board: boardForWorkItem(relatedItem),\n itemRevision: relatedItem.revision,\n ...withBoardClosure(this.options.boards, relatedItem),\n ...rulePhaseField(this.options.boards, relatedItem),\n }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = selectEventRule(\n this.options.jiraRules,\n boardEventHandlers(this.options.boards, eventBoardId(context), 'jira'),\n context.event,\n );\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => {\n assertFactoryDecisionTarget(\n entry,\n this.options.boards,\n relatedItem ? boardForWorkItem(relatedItem) : undefined,\n );\n return { ...entry };\n });\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Jira rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'jira.issueObserved' },\n configVersion: this.options.configVersion,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n if (committed.status === 'stale') {\n // Nothing was persisted: re-read the item and evaluate again at its fresh revision.\n if (attempt >= MAX_STALE_RULE_ATTEMPTS) {\n throw new Error(`Factory Jira rule evaluation for ${ingressId} kept losing revision races.`);\n }\n const fresh = relatedItem ? await this.options.storage.get({ orgId: input.orgId, id: relatedItem.id }) : null;\n if (relatedItem && !fresh) return 'missing';\n return this.#ingestIssue(input, issue, fresh ?? undefined, attempt + 1);\n }\n return committed.status;\n }\n}\n\nexport function attachJiraRules(\n jira: { readonly rules: JiraEventRules },\n context: IntegrationContext,\n): ((input: JiraRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new JiraRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n jiraRules: jira.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rules.js","names":[],"sources":["../../../src/integrations/jira/rules.ts"],"sourcesContent":["import {\n boardEventHandlers,\n boardForWorkItem,\n eventBoardId,\n isTerminalWorkItem,\n rulePhaseField,\n withBoardClosure,\n} from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport { selectEventRule } from '../../rules/event-rules.js';\nimport type { FactoryJiraRuleContext, FactoryRuleDecision } from '../../rules/types.js';\nimport { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport { jiraClaimKey, jiraIssueCards } from './claim.js';\nimport type { JiraEventRules } from './default-rules.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nconst MAX_STALE_RULE_ATTEMPTS = 3;\n\nexport interface JiraIssueIngress {\n /** Stable issue reference — direct Jira id or the Platform-encoded issue reference. */\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Jira reporter, when available. */\n author: string | null;\n /** Jira project key the issue belongs to, when available. */\n project: string | null;\n /** Jira site host the issue was read from, when available. */\n site: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** Jira source the issue was read from; resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n}\n\nexport interface JiraRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n jiraRules: JiraEventRules;\n}\n\nexport interface JiraRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: JiraIssueIngress[];\n /** Board id per bound Jira source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class JiraRules {\n constructor(private readonly options: JiraRulesOptions) {}\n\n async ingest(input: JiraRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const itemsByClaimKey = new Map(items.filter(item => item.claimKey).map(item => [item.claimKey, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n // The claim finds the card even after a metadata drift; the raw issue\n // reference covers cards filed before claims existed.\n const relatedItem = itemsByClaimKey.get(jiraClaimKey(issue.id)) ?? itemsBySourceKey.get(issue.id);\n // One live card per Jira issue per org. The store's claim index is the\n // guarantee; this check spares the dispatcher a refused upsert.\n if (!relatedItem && (await this.#heldElsewhere(input, issue))) {\n statuses.push('missing');\n continue;\n }\n statuses.push(await this.#ingestIssue(input, issue, relatedItem));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #heldElsewhere(input: JiraRulesIngress, issue: JiraIssueIngress): Promise<boolean> {\n const cards = await jiraIssueCards(this.options.storage, { orgId: input.orgId, issueRef: issue.id });\n const liveElsewhere = cards.filter(\n row => row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row),\n );\n for (const row of liveElsewhere) {\n if (await this.options.projects.get({ orgId: input.orgId, id: row.factoryProjectId })) return true;\n }\n return false;\n }\n\n async #ingestIssue(\n input: JiraRulesIngress,\n issue: JiraIssueIngress,\n relatedItem: WorkItemRow | undefined,\n attempt = 1,\n ): Promise<IngressStatus> {\n const ingressId = `jira:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryJiraRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'jira', id: ingressId },\n cause: `jira.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'jira-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n acceptedAt: relatedItem.acceptedAt,\n metadata: relatedItem.metadata,\n },\n board: boardForWorkItem(relatedItem),\n itemRevision: relatedItem.revision,\n ...withBoardClosure(this.options.boards, relatedItem),\n ...rulePhaseField(this.options.boards, relatedItem),\n }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = selectEventRule(\n this.options.jiraRules,\n boardEventHandlers(this.options.boards, eventBoardId(context), 'jira'),\n context.event,\n );\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => {\n assertFactoryDecisionTarget(\n entry,\n this.options.boards,\n relatedItem ? boardForWorkItem(relatedItem) : undefined,\n );\n return { ...entry };\n });\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Jira rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'jira.issueObserved' },\n configVersion: this.options.configVersion,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n if (committed.status === 'stale') {\n // Nothing was persisted: re-read the item and evaluate again at its fresh revision.\n if (attempt >= MAX_STALE_RULE_ATTEMPTS) {\n throw new Error(`Factory Jira rule evaluation for ${ingressId} kept losing revision races.`);\n }\n const fresh = relatedItem ? await this.options.storage.get({ orgId: input.orgId, id: relatedItem.id }) : null;\n if (relatedItem && !fresh) return 'missing';\n return this.#ingestIssue(input, issue, fresh ?? undefined, attempt + 1);\n }\n return committed.status;\n }\n}\n\nexport function attachJiraRules(\n jira: { readonly rules: JiraEventRules },\n context: IntegrationContext,\n): ((input: JiraRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new JiraRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n jiraRules: jira.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
@@ -6,6 +6,7 @@ import type { RouteAuth } from '../../../routes/route.js';
6
6
  import type { FactoryProjectsStorage } from '../../../storage/domains/projects/base.js';
7
7
  import type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../../base.js';
8
8
  import type { JiraEventRules, JiraRuleOverrides } from '../../jira/default-rules.js';
9
+ import type { JiraIntakeScope } from '../../jira/intake-jql.js';
9
10
  import type { PlatformApiClientConfig } from '../api-client.js';
10
11
  interface PlatformIntegrationConnection {
11
12
  id: string;
@@ -39,7 +40,12 @@ export declare class PlatformJiraIntegration implements FactoryIntegration {
39
40
  listConnections(): Promise<PlatformIntegrationConnection[]>;
40
41
  hasActiveConnections(): Promise<boolean>;
41
42
  readonly intake: Intake;
42
- listActiveIssues(cursor?: string, sourceIds?: string[], labels?: string[]): Promise<{
43
+ /**
44
+ * One page of active issues across connections. Each scope's `projectId` is
45
+ * an encoded Platform source id; sources are grouped per connection and each
46
+ * group runs one JQL built from only that connection's projects.
47
+ */
48
+ listActiveIssues(cursor?: string, scopes?: JiraIntakeScope[], labels?: string[]): Promise<{
43
49
  issues: Array<IntakeIssue & {
44
50
  sourceId: string;
45
51
  externalId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/jira/integration.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/jira/integration.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { IssueReconcileWorker } from "../../issue-reconcile-worker.js";
3
3
  import { adfToText } from "../../jira/adf.js";
4
4
  import { JiraApiClient, JiraApiError } from "../../jira/api.js";
5
5
  import { resolveJiraRules } from "../../jira/default-rules.js";
6
+ import { buildIntakeJql, unfilteredJiraScopes } from "../../jira/intake-jql.js";
6
7
  import { attachJiraRules } from "../../jira/rules.js";
7
8
  import { attachJiraIssueReconciler } from "../../jira/issue-reconciler.js";
8
9
  import { jiraReconciliationEnabled, jiraReconciliationInterval } from "../../jira/reconciliation-config.js";
@@ -92,6 +93,7 @@ var PlatformJiraIntegration = class {
92
93
  return (await this.#activeConnections()).length > 0;
93
94
  }
94
95
  intake = {
96
+ bindingLabelFilters: true,
95
97
  resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
96
98
  listSources: () => this.#listSources(),
97
99
  listItems: (input) => this.#listItems(input),
@@ -138,7 +140,7 @@ var PlatformJiraIntegration = class {
138
140
  items: [],
139
141
  nextCursor: null
140
142
  };
141
- const page = await this.listActiveIssues(cursor, sourceIds);
143
+ const page = await this.listActiveIssues(cursor, unfilteredJiraScopes(sourceIds));
142
144
  return {
143
145
  items: page.issues.map((issue) => ({
144
146
  source: {
@@ -174,14 +176,19 @@ var PlatformJiraIntegration = class {
174
176
  };
175
177
  }
176
178
  async #listIssues(input) {
177
- const page = await this.listActiveIssues(input.cursor, input.sourceIds, input.labels);
179
+ const page = await this.listActiveIssues(input.cursor, unfilteredJiraScopes(input.sourceIds ?? []), input.labels);
178
180
  return {
179
181
  issues: page.issues,
180
182
  nextCursor: page.nextCursor
181
183
  };
182
184
  }
183
- async listActiveIssues(cursor, sourceIds = [], labels) {
184
- const grouped = groupSources(sourceIds);
185
+ /**
186
+ * One page of active issues across connections. Each scope's `projectId` is
187
+ * an encoded Platform source id; sources are grouped per connection and each
188
+ * group runs one JQL built from only that connection's projects.
189
+ */
190
+ async listActiveIssues(cursor, scopes = [], labels) {
191
+ const grouped = groupSources(scopes);
185
192
  if (grouped.length === 0) return {
186
193
  issues: [],
187
194
  nextCursor: null
@@ -194,7 +201,7 @@ var PlatformJiraIntegration = class {
194
201
  for (let groupIndex = decodedCursor.group; groupIndex < grouped.length; groupIndex++) {
195
202
  const group = grouped[groupIndex];
196
203
  const context = await this.#connectionContextById(group.connectionId);
197
- const jql = buildIntakeJql(group.projectIds, labels);
204
+ const jql = buildIntakeJql(group.scopes, labels);
198
205
  if (jql === null) continue;
199
206
  const page = await context.api.searchIssues({
200
207
  jql,
@@ -206,7 +213,7 @@ var PlatformJiraIntegration = class {
206
213
  }) : groupIndex + 1 < grouped.length ? encodePageCursor({ group: groupIndex + 1 }) : null;
207
214
  return {
208
215
  issues: page.issues.map((issue) => {
209
- const projectId = issue.fields.project?.id ?? group.projectIds[0] ?? "";
216
+ const projectId = issue.fields.project?.id ?? group.scopes[0]?.projectId ?? "";
210
217
  return {
211
218
  ...this.#toIntakeIssue(issue, context.siteUrl),
212
219
  sourceId: encodeSourceId(group.connectionId, projectId),
@@ -411,6 +418,7 @@ var PlatformJiraIntegration = class {
411
418
  jira: this,
412
419
  auth: ctx.auth,
413
420
  intake: ctx.storage.intake,
421
+ workItems: ctx.runtime?.workItems,
414
422
  appDbConfigured: Boolean(ctx.factoryStorage),
415
423
  ingestFactoryIssues: attachJiraRules(this, ctx)
416
424
  });
@@ -474,18 +482,22 @@ function decodeIssueReference(value) {
474
482
  return null;
475
483
  }
476
484
  }
477
- function groupSources(sourceIds) {
485
+ /** A Platform source scope: `projectId` is the encoded source id, decoded per connection here. */
486
+ function groupSources(scopes) {
478
487
  const groups = /* @__PURE__ */ new Map();
479
- for (const sourceId of sourceIds) {
480
- const source = decodeSourceId(sourceId);
488
+ for (const scope of scopes) {
489
+ const source = decodeSourceId(scope.projectId);
481
490
  if (!source) continue;
482
- const projectIds = groups.get(source.connectionId) ?? /* @__PURE__ */ new Set();
483
- projectIds.add(source.projectId);
484
- groups.set(source.connectionId, projectIds);
491
+ const projects = groups.get(source.connectionId) ?? /* @__PURE__ */ new Map();
492
+ if (!projects.has(source.projectId)) projects.set(source.projectId, {
493
+ ...scope,
494
+ projectId: source.projectId
495
+ });
496
+ groups.set(source.connectionId, projects);
485
497
  }
486
- return [...groups.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([connectionId, projectIds]) => ({
498
+ return [...groups.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([connectionId, projects]) => ({
487
499
  connectionId,
488
- projectIds: [...projectIds].sort()
500
+ scopes: [...projects.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))
489
501
  }));
490
502
  }
491
503
  function encodePageCursor(cursor) {
@@ -515,16 +527,6 @@ function normalizeSiteUrl(value) {
515
527
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new JiraApiError("Jira site URL is invalid.", 502);
516
528
  return url.origin;
517
529
  }
518
- function buildIntakeJql(projectIds, labels) {
519
- const clauses = [];
520
- const safeProjects = (projectIds ?? []).map((id) => id.trim()).filter((id) => /^[A-Za-z0-9_]+$/.test(id)).map((id) => /^\d+$/.test(id) ? id : `"${id}"`);
521
- if (projectIds && safeProjects.length === 0) return null;
522
- if (safeProjects.length > 0) clauses.push(`project IN (${safeProjects.join(", ")})`);
523
- clauses.push("statusCategory != Done");
524
- const safeLabels = [...new Set((labels ?? []).map((label) => label.trim().replace(/["\\]/g, "")).filter(Boolean))];
525
- if (safeLabels.length > 0) clauses.push(`labels IN (${safeLabels.map((label) => `"${label}"`).join(", ")})`);
526
- return `${clauses.join(" AND ")} ORDER BY updated DESC`;
527
- }
528
530
  function currentStatusMatches(issue, target) {
529
531
  const status = issue.fields.status;
530
532
  if (!status) return false;