@mastra/factory 0.16.0-alpha.6 → 0.16.0-alpha.7

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 (87) hide show
  1. package/README.md +2 -1
  2. package/dist/boards/work.d.ts.map +1 -1
  3. package/dist/boards/work.js +20 -3
  4. package/dist/boards/work.js.map +1 -1
  5. package/dist/factory.d.ts +5 -3
  6. package/dist/factory.d.ts.map +1 -1
  7. package/dist/factory.js +13 -5
  8. package/dist/factory.js.map +1 -1
  9. package/dist/integrations/jira/adf.d.ts +26 -0
  10. package/dist/integrations/jira/adf.d.ts.map +1 -0
  11. package/dist/integrations/jira/adf.js +73 -0
  12. package/dist/integrations/jira/adf.js.map +1 -0
  13. package/dist/integrations/jira/agent-tools.d.ts +45 -0
  14. package/dist/integrations/jira/agent-tools.d.ts.map +1 -0
  15. package/dist/integrations/jira/agent-tools.js +87 -0
  16. package/dist/integrations/jira/agent-tools.js.map +1 -0
  17. package/dist/integrations/jira/api.d.ts +157 -0
  18. package/dist/integrations/jira/api.d.ts.map +1 -0
  19. package/dist/integrations/jira/api.js +141 -0
  20. package/dist/integrations/jira/api.js.map +1 -0
  21. package/dist/integrations/jira/claim.d.ts +9 -0
  22. package/dist/integrations/jira/claim.d.ts.map +1 -0
  23. package/dist/integrations/jira/claim.js +15 -0
  24. package/dist/integrations/jira/claim.js.map +1 -0
  25. package/dist/integrations/jira/default-rules.d.ts +47 -0
  26. package/dist/integrations/jira/default-rules.d.ts.map +1 -0
  27. package/dist/integrations/jira/default-rules.js +67 -0
  28. package/dist/integrations/jira/default-rules.js.map +1 -0
  29. package/dist/integrations/jira/integration.d.ts +114 -0
  30. package/dist/integrations/jira/integration.d.ts.map +1 -0
  31. package/dist/integrations/jira/integration.js +401 -0
  32. package/dist/integrations/jira/integration.js.map +1 -0
  33. package/dist/integrations/jira/issue-reconciler.d.ts +10 -0
  34. package/dist/integrations/jira/issue-reconciler.d.ts.map +1 -0
  35. package/dist/integrations/jira/issue-reconciler.js +70 -0
  36. package/dist/integrations/jira/issue-reconciler.js.map +1 -0
  37. package/dist/integrations/jira/reconciliation-config.d.ts +3 -0
  38. package/dist/integrations/jira/reconciliation-config.d.ts.map +1 -0
  39. package/dist/integrations/jira/reconciliation-config.js +17 -0
  40. package/dist/integrations/jira/reconciliation-config.js.map +1 -0
  41. package/dist/integrations/jira/routes.d.ts +54 -0
  42. package/dist/integrations/jira/routes.d.ts.map +1 -0
  43. package/dist/integrations/jira/routes.js +260 -0
  44. package/dist/integrations/jira/routes.js.map +1 -0
  45. package/dist/integrations/jira/rules.d.ts +57 -0
  46. package/dist/integrations/jira/rules.d.ts.map +1 -0
  47. package/dist/integrations/jira/rules.js +175 -0
  48. package/dist/integrations/jira/rules.js.map +1 -0
  49. package/dist/integrations/platform/connect/routes.d.ts +39 -0
  50. package/dist/integrations/platform/connect/routes.d.ts.map +1 -0
  51. package/dist/integrations/platform/connect/routes.js +123 -0
  52. package/dist/integrations/platform/connect/routes.js.map +1 -0
  53. package/dist/integrations/platform/github/event-worker.js +1 -1
  54. package/dist/integrations/platform/github/integration.js +1 -1
  55. package/dist/integrations/platform/jira/agent-tools.d.ts +36 -0
  56. package/dist/integrations/platform/jira/agent-tools.d.ts.map +1 -0
  57. package/dist/integrations/platform/jira/agent-tools.js +85 -0
  58. package/dist/integrations/platform/jira/agent-tools.js.map +1 -0
  59. package/dist/integrations/platform/jira/integration.d.ts +65 -0
  60. package/dist/integrations/platform/jira/integration.d.ts.map +1 -0
  61. package/dist/integrations/platform/jira/integration.js +540 -0
  62. package/dist/integrations/platform/jira/integration.js.map +1 -0
  63. package/dist/integrations/platform/jira/routes.d.ts +55 -0
  64. package/dist/integrations/platform/jira/routes.d.ts.map +1 -0
  65. package/dist/integrations/platform/jira/routes.js +274 -0
  66. package/dist/integrations/platform/jira/routes.js.map +1 -0
  67. package/dist/routes/surface.d.ts.map +1 -1
  68. package/dist/routes/surface.js +24 -4
  69. package/dist/routes/surface.js.map +1 -1
  70. package/dist/rules/dispatcher.d.ts.map +1 -1
  71. package/dist/rules/dispatcher.js +1 -1
  72. package/dist/rules/dispatcher.js.map +1 -1
  73. package/dist/rules/index.d.ts +2 -2
  74. package/dist/rules/index.d.ts.map +1 -1
  75. package/dist/rules/index.js +2 -2
  76. package/dist/rules/types.d.ts +30 -3
  77. package/dist/rules/types.d.ts.map +1 -1
  78. package/dist/rules/types.js +5 -1
  79. package/dist/rules/types.js.map +1 -1
  80. package/dist/rules/validation.d.ts.map +1 -1
  81. package/dist/rules/validation.js +1 -0
  82. package/dist/rules/validation.js.map +1 -1
  83. package/dist/work-item-branch.d.ts +1 -1
  84. package/dist/work-item-branch.d.ts.map +1 -1
  85. package/dist/work-item-branch.js +3 -2
  86. package/dist/work-item-branch.js.map +1 -1
  87. package/package.json +8 -8
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","names":["#heldElsewhere","#ingestIssue"],"sources":["../../../src/integrations/jira/rules.ts"],"sourcesContent":["import { boardForWorkItem, isTerminalWorkItem } from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.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\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 ): 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 }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = this.options.jiraRules[context.event];\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 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":";;;;;AAUA,MAAM,kBAAkB;AAExB,eAAe,gBAAmB,SAAiC;CACjE,IAAI;CACJ,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CACxB,SACA,IAAI,SAAgB,GAAG,WAAW;GAChC,UAAU,iBAAiB,uBAAO,IAAI,MAAM,sBAAsB,CAAC,GAAG,eAAe;EACvF,CAAC,CACH,CAAC;CACH,UAAU;EACR,IAAI,SAAS,aAAa,OAAO;CACnC;AACF;AA4CA,IAAa,YAAb,MAAuB;CACQ;CAA7B,YAAY,SAA4C;EAA3B,KAAA,UAAA;CAA4B;CAEzD,MAAM,OAAO,OAA+E;EAE1F,IAAI,CAAC,MADiB,KAAK,QAAQ,SAAS,IAAI;GAAE,OAAO,MAAM;GAAO,IAAI,MAAM;EAAiB,CAAC,GACpF,OAAO;GAAE,QAAQ;GAAW,UAAU;EAAE;EAEtD,MAAM,QAAQ,MAAM,KAAK,QAAQ,QAAQ,KAAK;GAAE,OAAO,MAAM;GAAO,kBAAkB,MAAM;EAAiB,CAAC;EAC9G,MAAM,mBAAmB,IAAI,IAAI,MAAM,KAAI,SAAQ,CAAC,KAAK,gBAAgB,YAAY,IAAI,CAAC,CAAC;EAC3F,MAAM,kBAAkB,IAAI,IAAI,MAAM,QAAO,SAAQ,KAAK,QAAQ,CAAC,CAAC,KAAI,SAAQ,CAAC,KAAK,UAAU,IAAI,CAAC,CAAC;EACtG,MAAM,WAA4B,CAAC;EACnC,KAAK,MAAM,SAAS,MAAM,QAAQ;GAGhC,MAAM,cAAc,gBAAgB,IAAI,aAAa,MAAM,EAAE,CAAC,KAAK,iBAAiB,IAAI,MAAM,EAAE;GAGhG,IAAI,CAAC,eAAgB,MAAM,KAAKA,eAAe,OAAO,KAAK,GAAI;IAC7D,SAAS,KAAK,SAAS;IACvB;GACF;GACA,SAAS,KAAK,MAAM,KAAKC,aAAa,OAAO,OAAO,WAAW,CAAC;EAClE;EACA,IAAI,SAAS,MAAK,WAAU,WAAW,WAAW,GAAG,OAAO;GAAE,QAAQ;GAAa,UAAU,SAAS;EAAO;EAC7G,IAAI,SAAS,MAAK,WAAU,WAAW,UAAU,GAAG,OAAO;GAAE,QAAQ;GAAY,UAAU,SAAS;EAAO;EAC3G,OAAO;GAAE,QAAQ;GAAW,UAAU,SAAS;EAAO;CACxD;CAEA,MAAMD,eAAe,OAAyB,OAA2C;EACvF,MAAM,YAAY,QAChB,IAAI,qBAAqB,MAAM,oBAAoB,CAAC,mBAAmB,KAAK,QAAQ,QAAQ,GAAG;EACjG,MAAM,WAAW,MAAM,KAAK,QAAQ,QAAQ,cAAc;GACxD,OAAO,MAAM;GACb,UAAU,aAAa,MAAM,EAAE;EACjC,CAAC;EACD,IAAI,YAAY,SAAS,QAAQ,GAAG,OAAO;EAM3C,QAAO,MAJc,KAAK,QAAQ,QAAQ,aAAa;GACrD,OAAO,MAAM;GACb,QAAQ;IAAE,eAAe;IAAQ,MAAM;IAAS,YAAY,MAAM;GAAG;EACvE,CAAC,EAAA,CACa,KAAK,QAAQ;CAC7B;CAEA,MAAMC,aACJ,OACA,OACA,aACwB;EACxB,MAAM,YAAY,QAAQ,MAAM,GAAG,GAAG,MAAM;EAC5C,MAAM,QAAQ;GAAE,MAAM;GAAkB,IAAI,MAAM;EAAO;EAGzD,MAAM,WAAW,MAAM,cAAc,eAAe,MAAM,cAAc;EACxE,IAAI,YAAY,CAAC,aACf,OAAO;EAGT,MAAM,QAAQ,WAAW,gBAAgB;EACzC,MAAM,eAAe,MAAM,WAAW,MAAM,eAAe,MAAM,YAAY,KAAA;EAC7E,MAAM,aAAa,eAAe,KAAK,QAAQ,OAAO,IAAI,YAAY,IAAI,KAAA;EAC1E,MAAM,UAAkC;GACtC,QAAQ;IAAE,OAAO,MAAM;IAAO,WAAW,MAAM;GAAiB;GAChE;GACA,SAAS;IAAE,MAAM;IAAQ,IAAI;GAAU;GACvC,OAAO,QAAQ;GACf,aAAa,CAAC;GACd,eAAe,KAAK,QAAQ;GAC5B,GAAI,cACA;IACE,MAAM;KACJ,IAAI,YAAY;KAChB,QAAQ;KACR,WAAW,YAAY,gBAAgB,cAAc;KACrD,kBAAkB,YAAY;KAC9B,OAAO,YAAY;KACnB,KAAK,YAAY,gBAAgB,OAAO;KACxC,QAAQ,YAAY;KACpB,YAAY,YAAY;KACxB,UAAU,YAAY;IACxB;IACA,OAAO,iBAAiB,WAAW;IACnC,cAAc,YAAY;GAC5B,IACA,CAAC;GACL,GAAI,aAAa,EAAE,QAAQ;IAAE,OAAO,WAAW;IAAI,cAAc,WAAW;GAAa,EAAE,IAAI,CAAC;GAChG;GACA;EACF;EAEA,MAAM,OAAO,KAAK,QAAQ,UAAU,QAAQ;EAC5C,IAAI;EACJ,IAAI,YAAuC,CAAC;EAC5C,IAAI,UAA+E,EAAE,QAAQ,WAAW;EACxG,IAAI;GACF,WAAW,OAAO,MAAM,gBAAgB,QAAQ,QAAQ,KAAK,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,KAAA;GACzF,IAAI,UAAU,SAAS,UACrB,UAAU;IAAE,QAAQ;IAAY,MAAM,SAAS;IAAM,QAAQ,SAAS;GAAO;QACxE,IAAI,UACT,YAAY,6BAA6B,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAI,UAAS;IAChE,4BACE,OACA,KAAK,QAAQ,QACb,cAAc,iBAAiB,WAAW,IAAI,KAAA,CAChD;IACA,OAAO,EAAE,GAAG,MAAM;GACpB,CAAC;EAEL,SAAS,OAAO;GACd,MAAM,WAAW,iBAAiB,SAAS,MAAM,YAAY;GAC7D,UAAU;IACR,QAAQ;IACR,MAAM,WAAW,YAAY;IAC7B,QAAQ,WACJ,uCACA,iBAAiB,QACf,MAAM,QAAQ,MAAM,GAAG,GAAK,IAC5B;GACR;EACF;EAeA,QAAO,MAbiB,KAAK,QAAQ,QAAQ,qBAAqB;GAChE,OAAO,MAAM;GACb,kBAAkB,MAAM;GACxB,YAAY,aAAa,MAAM;GAC/B,SAAS;IAAE,UAAU;IAAW,aAAa;GAAqB;GAClE,eAAe,KAAK,QAAQ;GAC5B,kBAAkB,aAAa,YAAY;GAC3C;GACA;GACA;GACA,aAAa,CAAC;GACd,qBAAK,IAAI,KAAK;EAChB,CAAC,EAAA,CACgB;CACnB;AACF;AAEA,SAAgB,gBACd,MACA,SAC6D;CAC7D,IAAI,CAAC,QAAQ,SAAS,OAAO,KAAA;CAC7B,MAAM,QAAQ,IAAI,UAAU;EAC1B,UAAU,QAAQ,QAAQ;EAC1B,SAAS,QAAQ,QAAQ;EACzB,eAAe,QAAQ,QAAQ;EAC/B,QAAQ,QAAQ,QAAQ;EACxB,WAAW,KAAK;CAClB,CAAC;CACD,QAAO,UAAS,MAAM,OAAO,KAAK;AACpC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Mastra `apiRoutes` that let the Factory SPA connect and reconnect
3
+ * Platform-managed provider accounts without leaving Factory.
4
+ *
5
+ * The browser cannot talk to the integrations service itself — it has no
6
+ * Platform session — so these routes mint Nango connect/reconnect sessions
7
+ * server-side with the deploy's machine credentials
8
+ * (`MASTRA_PLATFORM_ACCESS_TOKEN`/`MASTRA_PLATFORM_SECRET_KEY`) and hand the
9
+ * short-lived session token back to the SPA, which drives the provider's
10
+ * OAuth popup headlessly via `@nangohq/frontend`. Gating mirrors the
11
+ * Platform's own rule for these providers: a signed-in organization member
12
+ * (org:write parity with Linear), not an organization admin.
13
+ */
14
+ import type { ApiRoute } from '@mastra/core/server';
15
+ import type { RouteAuth } from '../../../routes/route.js';
16
+ import { PlatformApiClient } from '../api-client.js';
17
+ export interface PlatformConnectProvider {
18
+ /** Platform integration id used to mint connect sessions. */
19
+ integrationId: string;
20
+ /** Connection `integrationId` variants that belong to this provider. */
21
+ connectionIntegrationIds: readonly string[];
22
+ }
23
+ /**
24
+ * Providers the Factory SPA may connect through these routes. Keys are the
25
+ * SPA-facing provider slugs; `integrationId` is the Platform catalog id used
26
+ * for new connect sessions. Provider-specific PRs extend this registry when
27
+ * their Factory integrations are available.
28
+ */
29
+ export declare const PLATFORM_CONNECT_PROVIDERS: Record<string, PlatformConnectProvider>;
30
+ export interface BuildPlatformConnectRoutesOptions {
31
+ auth: RouteAuth;
32
+ client: PlatformApiClient;
33
+ }
34
+ /**
35
+ * Build the `/web/integrations/platform/*` routes. Callers only mount these
36
+ * when Platform machine credentials are configured.
37
+ */
38
+ export declare function buildPlatformConnectRoutes(options: BuildPlatformConnectRoutesOptions): ApiRoute[];
39
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/connect/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAoB,MAAM,kBAAkB,CAAC;AASvE,MAAM,WAAW,uBAAuB;IACtC,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAE9E,CAAC;AAmBF,MAAM,WAAW,iCAAiC;IAChD,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AA2CD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,QAAQ,EAAE,CAgFjG"}
@@ -0,0 +1,123 @@
1
+ import { PlatformApiError } from "../api-client.js";
2
+ import { registerApiRoute } from "@mastra/core/server";
3
+ //#region src/integrations/platform/connect/routes.ts
4
+ /** Erase a route handler's path-parameterized context to a plain `Context`. */
5
+ function loose(c) {
6
+ return c;
7
+ }
8
+ /**
9
+ * Providers the Factory SPA may connect through these routes. Keys are the
10
+ * SPA-facing provider slugs; `integrationId` is the Platform catalog id used
11
+ * for new connect sessions. Provider-specific PRs extend this registry when
12
+ * their Factory integrations are available.
13
+ */
14
+ const PLATFORM_CONNECT_PROVIDERS = { jira: {
15
+ integrationId: "jira",
16
+ connectionIntegrationIds: ["jira"]
17
+ } };
18
+ async function resolveOrgTenant(c, auth) {
19
+ if (!auth.enabled()) return { response: c.json({
20
+ error: "auth_disabled",
21
+ message: "Factory auth is not enabled."
22
+ }, 403) };
23
+ await auth.ensureUser(c);
24
+ const tenant = auth.tenant(c);
25
+ if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
26
+ if (!tenant.orgId) return { response: c.json({
27
+ error: "organization_required",
28
+ message: "Provider connections require an organization. Personal accounts cannot connect providers."
29
+ }, 403) };
30
+ return { tenant: {
31
+ orgId: tenant.orgId,
32
+ userId: tenant.userId
33
+ } };
34
+ }
35
+ function providerFromParam(c) {
36
+ const provider = c.req.param("provider");
37
+ return provider ? PLATFORM_CONNECT_PROVIDERS[provider] ?? null : null;
38
+ }
39
+ function platformError(c, err) {
40
+ if (err instanceof PlatformApiError) {
41
+ if (err.status === 409) return c.json({
42
+ error: "session_pending",
43
+ message: err.message
44
+ }, 409);
45
+ if (err.status === 403) return c.json({
46
+ error: "platform_forbidden",
47
+ message: err.message
48
+ }, 403);
49
+ }
50
+ return c.json({
51
+ error: "platform_request_failed",
52
+ message: err instanceof Error ? err.message : String(err)
53
+ }, 502);
54
+ }
55
+ /**
56
+ * Build the `/web/integrations/platform/*` routes. Callers only mount these
57
+ * when Platform machine credentials are configured.
58
+ */
59
+ function buildPlatformConnectRoutes(options) {
60
+ const { auth, client } = options;
61
+ return [
62
+ registerApiRoute("/web/integrations/platform/:provider/connections", {
63
+ method: "GET",
64
+ requiresAuth: false,
65
+ handler: async (rawContext) => {
66
+ const c = loose(rawContext);
67
+ const provider = providerFromParam(c);
68
+ if (!provider) return c.json({ error: "unknown_provider" }, 404);
69
+ const resolved = await resolveOrgTenant(c, auth);
70
+ if ("response" in resolved) return resolved.response;
71
+ try {
72
+ const connections = (await client.request("GET", "/v2/connections")).connections.filter((connection) => provider.connectionIntegrationIds.includes(connection.integrationId));
73
+ return c.json({ connections });
74
+ } catch (err) {
75
+ return platformError(c, err);
76
+ }
77
+ }
78
+ }),
79
+ registerApiRoute("/web/integrations/platform/:provider/connect-session", {
80
+ method: "POST",
81
+ requiresAuth: false,
82
+ handler: async (rawContext) => {
83
+ const c = loose(rawContext);
84
+ const provider = providerFromParam(c);
85
+ if (!provider) return c.json({ error: "unknown_provider" }, 404);
86
+ const resolved = await resolveOrgTenant(c, auth);
87
+ if ("response" in resolved) return resolved.response;
88
+ try {
89
+ const session = await client.request("POST", `/v2/integrations/${encodeURIComponent(provider.integrationId)}/connect-sessions`, {});
90
+ c.header("Cache-Control", "no-store");
91
+ return c.json(session, 201);
92
+ } catch (err) {
93
+ return platformError(c, err);
94
+ }
95
+ }
96
+ }),
97
+ registerApiRoute("/web/integrations/platform/:provider/connections/:connectionId/reconnect-session", {
98
+ method: "POST",
99
+ requiresAuth: false,
100
+ handler: async (rawContext) => {
101
+ const c = loose(rawContext);
102
+ const provider = providerFromParam(c);
103
+ if (!provider) return c.json({ error: "unknown_provider" }, 404);
104
+ const resolved = await resolveOrgTenant(c, auth);
105
+ if ("response" in resolved) return resolved.response;
106
+ const connectionId = c.req.param("connectionId");
107
+ if (!connectionId) return c.json({ error: "connection_required" }, 400);
108
+ try {
109
+ if (!(await client.request("GET", "/v2/connections")).connections.find((candidate) => candidate.id === connectionId && provider.connectionIntegrationIds.includes(candidate.integrationId))) return c.json({ error: "connection_not_found" }, 404);
110
+ const session = await client.request("POST", `/v2/connections/${encodeURIComponent(connectionId)}/reconnect-session`, {});
111
+ c.header("Cache-Control", "no-store");
112
+ return c.json(session, 201);
113
+ } catch (err) {
114
+ return platformError(c, err);
115
+ }
116
+ }
117
+ })
118
+ ];
119
+ }
120
+ //#endregion
121
+ export { PLATFORM_CONNECT_PROVIDERS, buildPlatformConnectRoutes };
122
+
123
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../../../src/integrations/platform/connect/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` that let the Factory SPA connect and reconnect\n * Platform-managed provider accounts without leaving Factory.\n *\n * The browser cannot talk to the integrations service itself — it has no\n * Platform session — so these routes mint Nango connect/reconnect sessions\n * server-side with the deploy's machine credentials\n * (`MASTRA_PLATFORM_ACCESS_TOKEN`/`MASTRA_PLATFORM_SECRET_KEY`) and hand the\n * short-lived session token back to the SPA, which drives the provider's\n * OAuth popup headlessly via `@nangohq/frontend`. Gating mirrors the\n * Platform's own rule for these providers: a signed-in organization member\n * (org:write parity with Linear), not an organization admin.\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 { PlatformApiClient, PlatformApiError } from '../api-client.js';\n\ntype RouteContext = Context;\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\nexport interface PlatformConnectProvider {\n /** Platform integration id used to mint connect sessions. */\n integrationId: string;\n /** Connection `integrationId` variants that belong to this provider. */\n connectionIntegrationIds: readonly string[];\n}\n\n/**\n * Providers the Factory SPA may connect through these routes. Keys are the\n * SPA-facing provider slugs; `integrationId` is the Platform catalog id used\n * for new connect sessions. Provider-specific PRs extend this registry when\n * their Factory integrations are available.\n */\nexport const PLATFORM_CONNECT_PROVIDERS: Record<string, PlatformConnectProvider> = {\n jira: { integrationId: 'jira', connectionIntegrationIds: ['jira'] },\n};\n\ninterface PlatformConnectionRow {\n id: string;\n integrationId: string;\n status: 'active' | 'needs_reauth';\n accountLabel: string | null;\n displayName?: string | null;\n connectedAt?: string;\n}\n\ninterface PlatformSessionResponse {\n connectionId: string;\n integrationId: string;\n connectUrl: string;\n sessionToken: string;\n expiresAt: string;\n}\n\nexport interface BuildPlatformConnectRoutesOptions {\n auth: RouteAuth;\n client: PlatformApiClient;\n}\n\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n if (!auth.enabled()) {\n return { response: c.json({ error: 'auth_disabled', message: 'Factory auth is not enabled.' }, 403) };\n }\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: 'Provider connections require an organization. Personal accounts cannot connect providers.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\nfunction providerFromParam(c: RouteContext): PlatformConnectProvider | null {\n const provider = c.req.param('provider');\n return provider ? (PLATFORM_CONNECT_PROVIDERS[provider] ?? null) : null;\n}\n\nfunction platformError(c: RouteContext, err: unknown) {\n if (err instanceof PlatformApiError) {\n if (err.status === 409) {\n return c.json({ error: 'session_pending', message: err.message }, 409);\n }\n if (err.status === 403) {\n return c.json({ error: 'platform_forbidden', message: err.message }, 403);\n }\n }\n return c.json({ error: 'platform_request_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the `/web/integrations/platform/*` routes. Callers only mount these\n * when Platform machine credentials are configured.\n */\nexport function buildPlatformConnectRoutes(options: BuildPlatformConnectRoutesOptions): ApiRoute[] {\n const { auth, client } = options;\n\n return [\n registerApiRoute('/web/integrations/platform/:provider/connections', {\n method: 'GET',\n requiresAuth: false,\n handler: async rawContext => {\n const c = loose(rawContext);\n const provider = providerFromParam(c);\n if (!provider) return c.json({ error: 'unknown_provider' }, 404);\n const resolved = await resolveOrgTenant(c, auth);\n if ('response' in resolved) return resolved.response;\n try {\n const result = await client.request<{ connections: PlatformConnectionRow[] }>('GET', '/v2/connections');\n const connections = result.connections.filter(connection =>\n provider.connectionIntegrationIds.includes(connection.integrationId),\n );\n return c.json({ connections });\n } catch (err) {\n return platformError(c, err);\n }\n },\n }),\n registerApiRoute('/web/integrations/platform/:provider/connect-session', {\n method: 'POST',\n requiresAuth: false,\n handler: async rawContext => {\n const c = loose(rawContext);\n const provider = providerFromParam(c);\n if (!provider) return c.json({ error: 'unknown_provider' }, 404);\n const resolved = await resolveOrgTenant(c, auth);\n if ('response' in resolved) return resolved.response;\n try {\n const session = await client.request<PlatformSessionResponse>(\n 'POST',\n `/v2/integrations/${encodeURIComponent(provider.integrationId)}/connect-sessions`,\n {},\n );\n c.header('Cache-Control', 'no-store');\n return c.json(session, 201);\n } catch (err) {\n return platformError(c, err);\n }\n },\n }),\n registerApiRoute('/web/integrations/platform/:provider/connections/:connectionId/reconnect-session', {\n method: 'POST',\n requiresAuth: false,\n handler: async rawContext => {\n const c = loose(rawContext);\n const provider = providerFromParam(c);\n if (!provider) return c.json({ error: 'unknown_provider' }, 404);\n const resolved = await resolveOrgTenant(c, auth);\n if ('response' in resolved) return resolved.response;\n const connectionId = c.req.param('connectionId');\n if (!connectionId) return c.json({ error: 'connection_required' }, 400);\n try {\n // Confirm the connection belongs to this provider before minting so\n // one provider's page cannot reconnect a different provider's\n // connection through this route family.\n const listed = await client.request<{ connections: PlatformConnectionRow[] }>('GET', '/v2/connections');\n const connection = listed.connections.find(\n candidate =>\n candidate.id === connectionId && provider.connectionIntegrationIds.includes(candidate.integrationId),\n );\n if (!connection) return c.json({ error: 'connection_not_found' }, 404);\n const session = await client.request<PlatformSessionResponse>(\n 'POST',\n `/v2/connections/${encodeURIComponent(connectionId)}/reconnect-session`,\n {},\n );\n c.header('Cache-Control', 'no-store');\n return c.json(session, 201);\n } catch (err) {\n return platformError(c, err);\n }\n },\n }),\n ];\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from "../../github/subscriptions.js";
2
- import { dispatchGithubWebhook, isFactoryAppSender, resolveAuthorizedBots } from "../../github/webhook.js";
3
2
  import { PlatformApiError } from "../api-client.js";
3
+ import { dispatchGithubWebhook, isFactoryAppSender, resolveAuthorizedBots } from "../../github/webhook.js";
4
4
  import { NoopLeaseProvider, isLeaseProvider } from "@mastra/core/events";
5
5
  import { randomUUID } from "crypto";
6
6
  import { MastraWorker } from "@mastra/core/worker";
@@ -1,4 +1,5 @@
1
1
  import { createGithubSubscriptionTools, parseCreatedPullRequest, subscribeCurrentSessionToPullRequest } from "../../github/session-subscriptions.js";
2
+ import { PlatformApiClient, PlatformApiError, logPlatformInfo, logPlatformWarn, platformApiClientConfigFromEnv } from "../api-client.js";
2
3
  import { GithubAppIdentity } from "../../github/app-identity.js";
3
4
  import { resolveGithubRules } from "../../github/default-rules.js";
4
5
  import { attachGithubReconciler, attachGithubRules } from "../../github/rules.js";
@@ -7,7 +8,6 @@ import { reconcileInterval, reconciliationEnabled } from "../../github/reconcili
7
8
  import { parseAuthorizedBotsEnv } from "../../github/webhook.js";
8
9
  import { buildGithubRoutes } from "../../github/routes.js";
9
10
  import { settleOrAbort } from "../../github/settle-or-abort.js";
10
- import { PlatformApiClient, PlatformApiError, logPlatformInfo, logPlatformWarn, platformApiClientConfigFromEnv } from "../api-client.js";
11
11
  import { PlatformGithubEventWorker } from "./event-worker.js";
12
12
  import { registerApiRoute } from "@mastra/core/server";
13
13
  //#region src/integrations/platform/github/integration.ts
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Jira tools exposed to the coding agent — `jira_get_issue` for reads and
3
+ * `jira_create_comment` for posting findings back to the issue, matching the
4
+ * Linear tool surface.
5
+ *
6
+ * Wired into the agent through the SDK's async `extraTools` provider: on each
7
+ * tool-set resolution we map the session's resourceId (the factory project
8
+ * id) to its owning org and only expose the Jira tools for real factory
9
+ * projects with an active Platform-managed Jira connection.
10
+ *
11
+ * Tenancy mirrors the Jira API routes: nothing is exposed without the host
12
+ * auth seam, and the session must resolve to an org-owned project.
13
+ */
14
+ import type { RequestContext } from '@mastra/core/request-context';
15
+ import type { PlatformJiraIntegration } from './integration.js';
16
+ declare function createJiraGetIssueTool(jira: PlatformJiraIntegration): import("@mastra/core/tools").Tool<{
17
+ issue: string;
18
+ }, unknown, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, "jira_get_issue", unknown>;
19
+ declare function createJiraCommentTool(jira: PlatformJiraIntegration): import("@mastra/core/tools").Tool<{
20
+ issue: string;
21
+ body: string;
22
+ }, unknown, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, "jira_create_comment", unknown>;
23
+ /**
24
+ * Async `extraTools` provider: expose the Jira tools only when the host runs
25
+ * with web auth and the session's resource is an org-owned factory project.
26
+ *
27
+ * Intake source bindings scope the board feed; both tools resolve the issue
28
+ * through one of the organization's active Platform Jira connections.
29
+ */
30
+ export declare function buildPlatformJiraAgentTools({ requestContext, jira, }: {
31
+ requestContext: RequestContext;
32
+ /** The integration instance providing the Jira API client. */
33
+ jira: PlatformJiraIntegration;
34
+ }): Promise<Record<string, ReturnType<typeof createJiraGetIssueTool> | ReturnType<typeof createJiraCommentTool>>>;
35
+ export {};
36
+ //# sourceMappingURL=agent-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-tools.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/jira/agent-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAOnE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAYhE,iBAAS,sBAAsB,CAAC,IAAI,EAAE,uBAAuB;;uIAuB5D;AAED,iBAAS,qBAAqB,CAAC,IAAI,EAAE,uBAAuB;;;4IAyB3D;AAED;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAAC,EAChD,cAAc,EACd,IAAI,GACL,EAAE;IACD,cAAc,EAAE,cAAc,CAAC;IAC/B,8DAA8D;IAC9D,IAAI,EAAE,uBAAuB,CAAC;CAC/B,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAmBhH"}
@@ -0,0 +1,85 @@
1
+ import { JiraApiError } from "../../jira/api.js";
2
+ import { JIRA_UNTRUSTED_CONTENT_NOTICE } from "../../jira/agent-tools.js";
3
+ import { createTool } from "@mastra/core/tools";
4
+ import { z } from "zod";
5
+ //#region src/integrations/platform/jira/agent-tools.ts
6
+ /** The `Intake` contract requires a connection argument; the Platform adapter resolves the real connection. */
7
+ const PLATFORM_CONNECTION = {
8
+ type: "oauth",
9
+ accessToken: "platform-managed"
10
+ };
11
+ function toolError(action, err) {
12
+ if (err instanceof JiraApiError && err.code === "jira_auth_failed") return { error: "Jira rejected the connected account. Reconnect it in Mastra Platform." };
13
+ return { error: `${action}: ${err instanceof Error ? err.message : String(err)}` };
14
+ }
15
+ function createJiraGetIssueTool(jira) {
16
+ return createTool({
17
+ id: "jira_get_issue",
18
+ description: "Fetch a Jira issue's full details — summary, description, status, assignee, labels, priority, and discussion comments. Use this whenever you're working on a Jira issue (e.g. ENG-123) to get its complete context.",
19
+ inputSchema: z.object({ issue: z.string().trim().min(1).describe("The Jira issue key (e.g. \"ENG-123\") or numeric issue id.") }),
20
+ execute: async ({ issue }) => {
21
+ try {
22
+ const detail = await jira.intake.getIssue({
23
+ connection: PLATFORM_CONNECTION,
24
+ issueId: issue
25
+ });
26
+ if (!detail) return { error: `Jira issue "${issue}" was not found on this site.` };
27
+ return {
28
+ notice: JIRA_UNTRUSTED_CONTENT_NOTICE,
29
+ ...detail
30
+ };
31
+ } catch (err) {
32
+ return toolError("Failed to fetch Jira issue", err);
33
+ }
34
+ }
35
+ });
36
+ }
37
+ function createJiraCommentTool(jira) {
38
+ return createTool({
39
+ id: "jira_create_comment",
40
+ description: "Post a comment on a Jira issue (e.g. to report investigation findings, link a PR, or ask a clarifying question). The comment is posted as the connected Jira account, so make clear it comes from the agent.",
41
+ inputSchema: z.object({
42
+ issue: z.string().trim().min(1).describe("The Jira issue key (e.g. \"ENG-123\") or numeric issue id."),
43
+ body: z.string().min(1).describe("The comment body, as plain text.")
44
+ }),
45
+ execute: async ({ issue, body }) => {
46
+ try {
47
+ const comment = await jira.intake.createComment({
48
+ connection: PLATFORM_CONNECTION,
49
+ issueId: issue,
50
+ body
51
+ });
52
+ if (!comment) return { error: `Jira issue "${issue}" was not found on this site.` };
53
+ return {
54
+ posted: true,
55
+ url: comment.url
56
+ };
57
+ } catch (err) {
58
+ return toolError("Failed to post Jira comment", err);
59
+ }
60
+ }
61
+ });
62
+ }
63
+ /**
64
+ * Async `extraTools` provider: expose the Jira tools only when the host runs
65
+ * with web auth and the session's resource is an org-owned factory project.
66
+ *
67
+ * Intake source bindings scope the board feed; both tools resolve the issue
68
+ * through one of the organization's active Platform Jira connections.
69
+ */
70
+ async function buildPlatformJiraAgentTools({ requestContext, jira }) {
71
+ if (!jira.authEnabled) return {};
72
+ const ctx = requestContext.get("controller");
73
+ if (!ctx) return {};
74
+ const projectId = ctx.getState().factoryProjectId ?? ctx.resourceId;
75
+ if (!projectId) return {};
76
+ if (!await jira.resolveOrgId(projectId) || !await jira.hasActiveConnections()) return {};
77
+ return {
78
+ jira_get_issue: createJiraGetIssueTool(jira),
79
+ jira_create_comment: createJiraCommentTool(jira)
80
+ };
81
+ }
82
+ //#endregion
83
+ export { buildPlatformJiraAgentTools };
84
+
85
+ //# sourceMappingURL=agent-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-tools.js","names":[],"sources":["../../../../src/integrations/platform/jira/agent-tools.ts"],"sourcesContent":["/**\n * Jira tools exposed to the coding agent — `jira_get_issue` for reads and\n * `jira_create_comment` for posting findings back to the issue, matching the\n * Linear tool surface.\n *\n * Wired into the agent through the SDK's async `extraTools` provider: on each\n * tool-set resolution we map the session's resourceId (the factory project\n * id) to its owning org and only expose the Jira tools for real factory\n * projects with an active Platform-managed Jira connection.\n *\n * Tenancy mirrors the Jira API routes: nothing is exposed without the host\n * auth seam, and the session must resolve to an org-owned project.\n */\n\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { IntegrationConnection } from '../../../capabilities/connection.js';\nimport { JIRA_UNTRUSTED_CONTENT_NOTICE } from '../../jira/agent-tools.js';\nimport { JiraApiError } from '../../jira/api.js';\nimport type { PlatformJiraIntegration } from './integration.js';\n\n/** The `Intake` contract requires a connection argument; the Platform adapter resolves the real connection. */\nconst PLATFORM_CONNECTION: IntegrationConnection = { type: 'oauth', accessToken: 'platform-managed' };\n\nfunction toolError(action: string, err: unknown): { error: string } {\n if (err instanceof JiraApiError && err.code === 'jira_auth_failed') {\n return { error: 'Jira rejected the connected account. Reconnect it in Mastra Platform.' };\n }\n return { error: `${action}: ${err instanceof Error ? err.message : String(err)}` };\n}\n\nfunction createJiraGetIssueTool(jira: PlatformJiraIntegration) {\n return createTool({\n id: 'jira_get_issue',\n description:\n \"Fetch a Jira issue's full details — summary, description, status, assignee, labels, priority, and discussion comments. Use this whenever you're working on a Jira issue (e.g. ENG-123) to get its complete context.\",\n inputSchema: z.object({\n issue: z.string().trim().min(1).describe('The Jira issue key (e.g. \"ENG-123\") or numeric issue id.'),\n }),\n execute: async ({ issue }: { issue: string }) => {\n try {\n const detail = await jira.intake.getIssue({\n connection: PLATFORM_CONNECTION,\n issueId: issue,\n });\n if (!detail) {\n return { error: `Jira issue \"${issue}\" was not found on this site.` };\n }\n return { notice: JIRA_UNTRUSTED_CONTENT_NOTICE, ...detail };\n } catch (err) {\n return toolError('Failed to fetch Jira issue', err);\n }\n },\n });\n}\n\nfunction createJiraCommentTool(jira: PlatformJiraIntegration) {\n return createTool({\n id: 'jira_create_comment',\n description:\n 'Post a comment on a Jira issue (e.g. to report investigation findings, link a PR, or ask a clarifying question). The comment is posted as the connected Jira account, so make clear it comes from the agent.',\n inputSchema: z.object({\n issue: z.string().trim().min(1).describe('The Jira issue key (e.g. \"ENG-123\") or numeric issue id.'),\n body: z.string().min(1).describe('The comment body, as plain text.'),\n }),\n execute: async ({ issue, body }: { issue: string; body: string }) => {\n try {\n const comment = await jira.intake.createComment({\n connection: PLATFORM_CONNECTION,\n issueId: issue,\n body,\n });\n if (!comment) {\n return { error: `Jira issue \"${issue}\" was not found on this site.` };\n }\n return { posted: true, url: comment.url };\n } catch (err) {\n return toolError('Failed to post Jira comment', err);\n }\n },\n });\n}\n\n/**\n * Async `extraTools` provider: expose the Jira tools only when the host runs\n * with web auth and the session's resource is an org-owned factory project.\n *\n * Intake source bindings scope the board feed; both tools resolve the issue\n * through one of the organization's active Platform Jira connections.\n */\nexport async function buildPlatformJiraAgentTools({\n requestContext,\n jira,\n}: {\n requestContext: RequestContext;\n /** The integration instance providing the Jira API client. */\n jira: PlatformJiraIntegration;\n}): Promise<Record<string, ReturnType<typeof createJiraGetIssueTool> | ReturnType<typeof createJiraCommentTool>>> {\n if (!jira.authEnabled) return {};\n\n const ctx = requestContext.get('controller') as\n AgentControllerRequestContext<{ factoryProjectId?: string }> | undefined;\n if (!ctx) return {};\n\n // Board-run resourceId is the work-item session id, not the project id stored\n // in factory_projects. Project-scoped sessions may not carry factoryProjectId.\n const projectId = ctx.getState().factoryProjectId ?? ctx.resourceId;\n if (!projectId) return {};\n\n const orgId = await jira.resolveOrgId(projectId);\n if (!orgId || !(await jira.hasActiveConnections())) return {};\n\n return {\n jira_get_issue: createJiraGetIssueTool(jira),\n jira_create_comment: createJiraCommentTool(jira),\n };\n}\n"],"mappings":";;;;;;AAyBA,MAAM,sBAA6C;CAAE,MAAM;CAAS,aAAa;AAAmB;AAEpG,SAAS,UAAU,QAAgB,KAAiC;CAClE,IAAI,eAAe,gBAAgB,IAAI,SAAS,oBAC9C,OAAO,EAAE,OAAO,wEAAwE;CAE1F,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,IAAI;AACnF;AAEA,SAAS,uBAAuB,MAA+B;CAC7D,OAAO,WAAW;EAChB,IAAI;EACJ,aACE;EACF,aAAa,EAAE,OAAO,EACpB,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,4DAA0D,EACrG,CAAC;EACD,SAAS,OAAO,EAAE,YAA+B;GAC/C,IAAI;IACF,MAAM,SAAS,MAAM,KAAK,OAAO,SAAS;KACxC,YAAY;KACZ,SAAS;IACX,CAAC;IACD,IAAI,CAAC,QACH,OAAO,EAAE,OAAO,eAAe,MAAM,+BAA+B;IAEtE,OAAO;KAAE,QAAQ;KAA+B,GAAG;IAAO;GAC5D,SAAS,KAAK;IACZ,OAAO,UAAU,8BAA8B,GAAG;GACpD;EACF;CACF,CAAC;AACH;AAEA,SAAS,sBAAsB,MAA+B;CAC5D,OAAO,WAAW;EAChB,IAAI;EACJ,aACE;EACF,aAAa,EAAE,OAAO;GACpB,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,4DAA0D;GACnG,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,kCAAkC;EACrE,CAAC;EACD,SAAS,OAAO,EAAE,OAAO,WAA4C;GACnE,IAAI;IACF,MAAM,UAAU,MAAM,KAAK,OAAO,cAAc;KAC9C,YAAY;KACZ,SAAS;KACT;IACF,CAAC;IACD,IAAI,CAAC,SACH,OAAO,EAAE,OAAO,eAAe,MAAM,+BAA+B;IAEtE,OAAO;KAAE,QAAQ;KAAM,KAAK,QAAQ;IAAI;GAC1C,SAAS,KAAK;IACZ,OAAO,UAAU,+BAA+B,GAAG;GACrD;EACF;CACF,CAAC;AACH;;;;;;;;AASA,eAAsB,4BAA4B,EAChD,gBACA,QAKgH;CAChH,IAAI,CAAC,KAAK,aAAa,OAAO,CAAC;CAE/B,MAAM,MAAM,eAAe,IAAI,YAAY;CAE3C,IAAI,CAAC,KAAK,OAAO,CAAC;CAIlB,MAAM,YAAY,IAAI,SAAS,CAAC,CAAC,oBAAoB,IAAI;CACzD,IAAI,CAAC,WAAW,OAAO,CAAC;CAGxB,IAAI,CAAC,MADe,KAAK,aAAa,SAAS,KACjC,CAAE,MAAM,KAAK,qBAAqB,GAAI,OAAO,CAAC;CAE5D,OAAO;EACL,gBAAgB,uBAAuB,IAAI;EAC3C,qBAAqB,sBAAsB,IAAI;CACjD;AACF"}
@@ -0,0 +1,65 @@
1
+ import type { RequestContext } from '@mastra/core/request-context';
2
+ import type { ApiRoute } from '@mastra/core/server';
3
+ import type { MastraWorker } from '@mastra/core/worker';
4
+ import type { Intake, IntakeIssue } from '../../../capabilities/intake.js';
5
+ import type { RouteAuth } from '../../../routes/route.js';
6
+ import type { FactoryProjectsStorage } from '../../../storage/domains/projects/base.js';
7
+ import type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../../base.js';
8
+ import type { JiraEventRules, JiraRuleOverrides } from '../../jira/default-rules.js';
9
+ import { type PlatformApiClientConfig } from '../api-client.js';
10
+ interface PlatformIntegrationConnection {
11
+ id: string;
12
+ integrationId: string;
13
+ status: 'active' | 'needs_reauth';
14
+ accountLabel: string | null;
15
+ }
16
+ interface JiraIssueReference {
17
+ connectionId: string;
18
+ issueId: string;
19
+ projectId?: string;
20
+ }
21
+ export interface PlatformJiraIntegrationConfig {
22
+ clientConfig?: PlatformApiClientConfig;
23
+ /** Per-event replacements; omitted events retain defaults, null disables. */
24
+ rules?: JiraRuleOverrides;
25
+ }
26
+ export declare class PlatformJiraIntegration implements FactoryIntegration {
27
+ #private;
28
+ readonly id = "jira";
29
+ get rules(): JiraEventRules;
30
+ constructor(config?: PlatformJiraIntegrationConfig);
31
+ initialize({ projects, auth }: {
32
+ projects: FactoryProjectsStorage;
33
+ auth: RouteAuth;
34
+ }): void;
35
+ get projects(): FactoryProjectsStorage;
36
+ get authEnabled(): boolean;
37
+ resolveOrgId(resourceId: string): Promise<string | null>;
38
+ clearCaches(): void;
39
+ listConnections(): Promise<PlatformIntegrationConnection[]>;
40
+ hasActiveConnections(): Promise<boolean>;
41
+ readonly intake: Intake;
42
+ listActiveIssues(cursor?: string, sourceIds?: string[], labels?: string[]): Promise<{
43
+ issues: Array<IntakeIssue & {
44
+ sourceId: string;
45
+ externalId: string;
46
+ site: string;
47
+ }>;
48
+ nextCursor: string | null;
49
+ }>;
50
+ workers(ctx: IntegrationContext): MastraWorker[];
51
+ routes(ctx: IntegrationContext): ApiRoute[];
52
+ agentTools(args: {
53
+ requestContext: RequestContext;
54
+ }): Promise<IntegrationTools>;
55
+ diagnostics(): Record<string, unknown>;
56
+ }
57
+ export declare function encodeSourceId(connectionId: string, projectId: string): string;
58
+ export declare function decodeSourceId(sourceId: string): {
59
+ connectionId: string;
60
+ projectId: string;
61
+ } | null;
62
+ export declare function encodeIssueReference(reference: JiraIssueReference): string;
63
+ export declare function decodeIssueReference(value: string): JiraIssueReference | null;
64
+ export {};
65
+ //# sourceMappingURL=integration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/jira/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,OAAO,KAAK,EAIV,MAAM,EACN,WAAW,EASZ,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACxF,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAK9F,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAKrF,OAAO,EAIL,KAAK,uBAAuB,EAC7B,MAAM,kBAAkB,CAAC;AAI1B,UAAU,6BAA6B;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,QAAQ,GAAG,cAAc,CAAC;IAClC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAaD,UAAU,kBAAkB;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqCD,MAAM,WAAW,6BAA6B;IAC5C,YAAY,CAAC,EAAE,uBAAuB,CAAC;IACvC,6EAA6E;IAC7E,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED,qBAAa,uBAAwB,YAAW,kBAAkB;;IAChE,QAAQ,CAAC,EAAE,UAAU;IAYrB,IAAI,KAAK,IAAI,cAAc,CAE1B;gBAEW,MAAM,GAAE,6BAAkC;IAOtD,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;QAAE,QAAQ,EAAE,sBAAsB,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,GAAG,IAAI;IAM3F,IAAI,QAAQ,IAAI,sBAAsB,CAIrC;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAEK,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAkB9D,WAAW,IAAI,IAAI;IAMb,eAAe,IAAI,OAAO,CAAC,6BAA6B,EAAE,CAAC;IAS3D,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;IAI9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAQrB;IA8EI,gBAAgB,CACpB,MAAM,CAAC,EAAE,MAAM,EACf,SAAS,GAAE,MAAM,EAAO,EACxB,MAAM,CAAC,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC;QACT,MAAM,EAAE,KAAK,CAAC,WAAW,GAAG;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACpF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,CAAC;IA8NF,OAAO,CAAC,GAAG,EAAE,kBAAkB,GAAG,YAAY,EAAE;IAchD,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IAUrC,UAAU,CAAC,IAAI,EAAE;QAAE,cAAc,EAAE,cAAc,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIrF,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAGvC;AAWD,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAYnG;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,kBAAkB,GAAG,MAAM,CAE1E;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAe7E"}