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

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 (123) hide show
  1. package/README.md +4 -3
  2. package/dist/boards/work.d.ts.map +1 -1
  3. package/dist/boards/work.js +37 -3
  4. package/dist/boards/work.js.map +1 -1
  5. package/dist/factory.d.ts +6 -3
  6. package/dist/factory.d.ts.map +1 -1
  7. package/dist/factory.js +15 -6
  8. package/dist/factory.js.map +1 -1
  9. package/dist/integrations/base.d.ts +2 -0
  10. package/dist/integrations/base.d.ts.map +1 -1
  11. package/dist/integrations/incidentio/agent-tools.d.ts +49 -0
  12. package/dist/integrations/incidentio/agent-tools.d.ts.map +1 -0
  13. package/dist/integrations/incidentio/agent-tools.js +90 -0
  14. package/dist/integrations/incidentio/agent-tools.js.map +1 -0
  15. package/dist/integrations/incidentio/claim.d.ts +8 -0
  16. package/dist/integrations/incidentio/claim.d.ts.map +1 -0
  17. package/dist/integrations/incidentio/claim.js +14 -0
  18. package/dist/integrations/incidentio/claim.js.map +1 -0
  19. package/dist/integrations/incidentio/default-rules.d.ts +46 -0
  20. package/dist/integrations/incidentio/default-rules.d.ts.map +1 -0
  21. package/dist/integrations/incidentio/default-rules.js +66 -0
  22. package/dist/integrations/incidentio/default-rules.js.map +1 -0
  23. package/dist/integrations/incidentio/integration.d.ts +48 -2
  24. package/dist/integrations/incidentio/integration.d.ts.map +1 -1
  25. package/dist/integrations/incidentio/integration.js +88 -2
  26. package/dist/integrations/incidentio/integration.js.map +1 -1
  27. package/dist/integrations/incidentio/issue-reconciler.d.ts +2 -0
  28. package/dist/integrations/incidentio/issue-reconciler.d.ts.map +1 -1
  29. package/dist/integrations/incidentio/issue-reconciler.js +38 -2
  30. package/dist/integrations/incidentio/issue-reconciler.js.map +1 -1
  31. package/dist/integrations/incidentio/routes.d.ts +71 -0
  32. package/dist/integrations/incidentio/routes.d.ts.map +1 -0
  33. package/dist/integrations/incidentio/routes.js +245 -0
  34. package/dist/integrations/incidentio/routes.js.map +1 -0
  35. package/dist/integrations/incidentio/rules.d.ts +55 -0
  36. package/dist/integrations/incidentio/rules.d.ts.map +1 -0
  37. package/dist/integrations/incidentio/rules.js +175 -0
  38. package/dist/integrations/incidentio/rules.js.map +1 -0
  39. package/dist/integrations/jira/adf.d.ts +26 -0
  40. package/dist/integrations/jira/adf.d.ts.map +1 -0
  41. package/dist/integrations/jira/adf.js +73 -0
  42. package/dist/integrations/jira/adf.js.map +1 -0
  43. package/dist/integrations/jira/agent-tools.d.ts +45 -0
  44. package/dist/integrations/jira/agent-tools.d.ts.map +1 -0
  45. package/dist/integrations/jira/agent-tools.js +87 -0
  46. package/dist/integrations/jira/agent-tools.js.map +1 -0
  47. package/dist/integrations/jira/api.d.ts +157 -0
  48. package/dist/integrations/jira/api.d.ts.map +1 -0
  49. package/dist/integrations/jira/api.js +141 -0
  50. package/dist/integrations/jira/api.js.map +1 -0
  51. package/dist/integrations/jira/claim.d.ts +9 -0
  52. package/dist/integrations/jira/claim.d.ts.map +1 -0
  53. package/dist/integrations/jira/claim.js +15 -0
  54. package/dist/integrations/jira/claim.js.map +1 -0
  55. package/dist/integrations/jira/default-rules.d.ts +47 -0
  56. package/dist/integrations/jira/default-rules.d.ts.map +1 -0
  57. package/dist/integrations/jira/default-rules.js +67 -0
  58. package/dist/integrations/jira/default-rules.js.map +1 -0
  59. package/dist/integrations/jira/integration.d.ts +114 -0
  60. package/dist/integrations/jira/integration.d.ts.map +1 -0
  61. package/dist/integrations/jira/integration.js +401 -0
  62. package/dist/integrations/jira/integration.js.map +1 -0
  63. package/dist/integrations/jira/issue-reconciler.d.ts +10 -0
  64. package/dist/integrations/jira/issue-reconciler.d.ts.map +1 -0
  65. package/dist/integrations/jira/issue-reconciler.js +70 -0
  66. package/dist/integrations/jira/issue-reconciler.js.map +1 -0
  67. package/dist/integrations/jira/reconciliation-config.d.ts +3 -0
  68. package/dist/integrations/jira/reconciliation-config.d.ts.map +1 -0
  69. package/dist/integrations/jira/reconciliation-config.js +17 -0
  70. package/dist/integrations/jira/reconciliation-config.js.map +1 -0
  71. package/dist/integrations/jira/routes.d.ts +54 -0
  72. package/dist/integrations/jira/routes.d.ts.map +1 -0
  73. package/dist/integrations/jira/routes.js +260 -0
  74. package/dist/integrations/jira/routes.js.map +1 -0
  75. package/dist/integrations/jira/rules.d.ts +57 -0
  76. package/dist/integrations/jira/rules.d.ts.map +1 -0
  77. package/dist/integrations/jira/rules.js +175 -0
  78. package/dist/integrations/jira/rules.js.map +1 -0
  79. package/dist/integrations/platform/connect/routes.d.ts +39 -0
  80. package/dist/integrations/platform/connect/routes.d.ts.map +1 -0
  81. package/dist/integrations/platform/connect/routes.js +129 -0
  82. package/dist/integrations/platform/connect/routes.js.map +1 -0
  83. package/dist/integrations/platform/github/event-worker.js +1 -1
  84. package/dist/integrations/platform/github/integration.js +1 -1
  85. package/dist/integrations/platform/incidentio/integration.d.ts +61 -4
  86. package/dist/integrations/platform/incidentio/integration.d.ts.map +1 -1
  87. package/dist/integrations/platform/incidentio/integration.js +371 -22
  88. package/dist/integrations/platform/incidentio/integration.js.map +1 -1
  89. package/dist/integrations/platform/jira/agent-tools.d.ts +36 -0
  90. package/dist/integrations/platform/jira/agent-tools.d.ts.map +1 -0
  91. package/dist/integrations/platform/jira/agent-tools.js +85 -0
  92. package/dist/integrations/platform/jira/agent-tools.js.map +1 -0
  93. package/dist/integrations/platform/jira/integration.d.ts +65 -0
  94. package/dist/integrations/platform/jira/integration.d.ts.map +1 -0
  95. package/dist/integrations/platform/jira/integration.js +540 -0
  96. package/dist/integrations/platform/jira/integration.js.map +1 -0
  97. package/dist/integrations/platform/jira/routes.d.ts +55 -0
  98. package/dist/integrations/platform/jira/routes.d.ts.map +1 -0
  99. package/dist/integrations/platform/jira/routes.js +274 -0
  100. package/dist/integrations/platform/jira/routes.js.map +1 -0
  101. package/dist/routes/oauth.js +2 -5
  102. package/dist/routes/oauth.js.map +1 -1
  103. package/dist/routes/surface.d.ts.map +1 -1
  104. package/dist/routes/surface.js +24 -4
  105. package/dist/routes/surface.js.map +1 -1
  106. package/dist/rules/dispatcher.d.ts.map +1 -1
  107. package/dist/rules/dispatcher.js +1 -1
  108. package/dist/rules/dispatcher.js.map +1 -1
  109. package/dist/rules/index.d.ts +2 -2
  110. package/dist/rules/index.d.ts.map +1 -1
  111. package/dist/rules/index.js +2 -2
  112. package/dist/rules/types.d.ts +57 -3
  113. package/dist/rules/types.d.ts.map +1 -1
  114. package/dist/rules/types.js +9 -1
  115. package/dist/rules/types.js.map +1 -1
  116. package/dist/rules/validation.d.ts.map +1 -1
  117. package/dist/rules/validation.js +2 -0
  118. package/dist/rules/validation.js.map +1 -1
  119. package/dist/work-item-branch.d.ts +1 -1
  120. package/dist/work-item-branch.d.ts.map +1 -1
  121. package/dist/work-item-branch.js +4 -2
  122. package/dist/work-item-branch.js.map +1 -1
  123. package/package.json +7 -7
@@ -0,0 +1,245 @@
1
+ import { IncidentioApiError } from "./api.js";
2
+ import { registerApiRoute } from "@mastra/core/server";
3
+ //#region src/integrations/incidentio/routes.ts
4
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
+ const ITEM_REF_RE = /^incidentio:(?:follow-up|incident):[\w-]+$/;
6
+ const FOLLOW_UP_PREFIX = "incidentio:follow-up:";
7
+ /** Erase a route handler's path-parameterized context to a plain `Context`. */
8
+ function loose(c) {
9
+ return c;
10
+ }
11
+ /**
12
+ * Narrow the caller's selected incident.io sources to the ones that feed this
13
+ * Factory project. Routing is explicit: a source feeds this Factory only when
14
+ * its binding names both the Factory project and a board. Mirrors the Jira
15
+ * routes' scoping semantics — the generic binding storage is provider-neutral.
16
+ * Shared with the agent tools, which enforce the same source-level
17
+ * authorization before fetching item details.
18
+ */
19
+ async function scopeSourceIdsToProject({ intake, orgId, factoryProjectId, selectedIds }) {
20
+ const selected = new Set(selectedIds);
21
+ const intakeBoards = {};
22
+ for (const binding of await intake.listBindings({
23
+ orgId,
24
+ integrationId: "incidentio"
25
+ })) if (binding.factoryProjectId === factoryProjectId && binding.board && selected.has(binding.sourceId)) intakeBoards[binding.sourceId] = binding.board;
26
+ return intakeBoards;
27
+ }
28
+ /**
29
+ * Resolve the org-scoped tenant for an incident.io request. Intake selections
30
+ * are org-owned, so it requires both a signed-in user and an organization.
31
+ */
32
+ async function resolveOrgTenant(c, auth) {
33
+ await auth.ensureUser(c);
34
+ const tenant = auth.tenant(c);
35
+ if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
36
+ if (!tenant.orgId) return { response: c.json({
37
+ error: "organization_required",
38
+ message: "incident.io intake requires an organization. Personal accounts cannot use incident.io intake."
39
+ }, 403) };
40
+ return { tenant: {
41
+ orgId: tenant.orgId,
42
+ userId: tenant.userId
43
+ } };
44
+ }
45
+ /**
46
+ * Validate an opaque pagination cursor from the query string. Cursors are
47
+ * server-issued base64url blobs, so anything outside a conservative
48
+ * charset/length is rejected rather than decoded.
49
+ */
50
+ function parseAfterCursor(raw) {
51
+ if (raw === void 0 || raw === "") return void 0;
52
+ if (raw.length > 2048 || !/^[\w+/=.:-]+$/.test(raw)) return null;
53
+ return raw;
54
+ }
55
+ /** Map an incident.io read failure to the API response for the SPA. */
56
+ function incidentioFetchError(c, err) {
57
+ if (err instanceof IncidentioApiError && (err.status === 401 || err.status === 403)) return c.json({
58
+ error: "incidentio_auth_failed",
59
+ message: "incident.io rejected the configured credentials. Reconnect the incident.io account."
60
+ }, 409);
61
+ return c.json({
62
+ error: "incidentio_fetch_failed",
63
+ message: err instanceof Error ? err.message : String(err)
64
+ }, 502);
65
+ }
66
+ /**
67
+ * Build the incident.io routes as Mastra `apiRoutes`. When the feature is
68
+ * disabled, returns only the `status` route so the SPA can detect the
69
+ * disabled state.
70
+ */
71
+ function buildIncidentioRoutes(options) {
72
+ const routes = [];
73
+ const { incidentio, auth, intake } = options;
74
+ const enabled = Boolean(incidentio) && auth.enabled();
75
+ const diagnostics = () => ({
76
+ incidentioConfigured: Boolean(incidentio),
77
+ factoryAuthEnabled: auth.enabled()
78
+ });
79
+ routes.push(registerApiRoute("/web/incidentio/status", {
80
+ method: "GET",
81
+ requiresAuth: false,
82
+ handler: async (c) => {
83
+ if (!enabled || !incidentio || !intake) return c.json({
84
+ enabled: false,
85
+ configured: Boolean(incidentio),
86
+ reason: "missing_config",
87
+ diagnostics: diagnostics()
88
+ });
89
+ await auth.ensureUser(loose(c));
90
+ const tenant = auth.tenant(loose(c));
91
+ if (!tenant) return c.json({
92
+ error: "unauthorized",
93
+ reason: "auth_required"
94
+ }, 401);
95
+ if (!tenant.orgId) return c.json({
96
+ enabled: true,
97
+ configured: true,
98
+ organizationRequired: true,
99
+ reason: "organization_required",
100
+ diagnostics: diagnostics()
101
+ });
102
+ return c.json({
103
+ enabled: true,
104
+ configured: true,
105
+ reason: "ready",
106
+ diagnostics: diagnostics()
107
+ });
108
+ }
109
+ }));
110
+ if (!enabled || !incidentio || !intake) return routes;
111
+ routes.push(registerApiRoute("/web/incidentio/issues", {
112
+ method: "GET",
113
+ requiresAuth: false,
114
+ handler: async (c) => {
115
+ const resolved = await resolveOrgTenant(loose(c), auth);
116
+ if ("response" in resolved) return resolved.response;
117
+ const after = parseAfterCursor(c.req.query("after"));
118
+ if (after === null) return c.json({ error: "invalid_cursor" }, 400);
119
+ const factoryProjectId = c.req.query("factoryProjectId");
120
+ if (factoryProjectId && !UUID_RE.test(factoryProjectId)) return c.json({ error: "invalid_factory_project_id" }, 400);
121
+ await intake.ensureReady();
122
+ const selection = (await intake.getConfig({
123
+ orgId: resolved.tenant.orgId,
124
+ integrationIds: ["incidentio"]
125
+ })).incidentio;
126
+ if (!selection.enabled) return c.json({
127
+ error: "incidentio_intake_disabled",
128
+ message: "incident.io intake is turned off in Settings."
129
+ }, 404);
130
+ const selectedIds = selection.sourceIds ?? [];
131
+ const intakeBoards = factoryProjectId ? await scopeSourceIdsToProject({
132
+ intake,
133
+ orgId: resolved.tenant.orgId,
134
+ factoryProjectId,
135
+ selectedIds
136
+ }) : null;
137
+ const sourceIds = intakeBoards ? Object.keys(intakeBoards) : selectedIds;
138
+ if (sourceIds.length === 0) return c.json({
139
+ issues: [],
140
+ nextCursor: null
141
+ });
142
+ try {
143
+ const page = await incidentio.intake.listItems({
144
+ orgId: resolved.tenant.orgId,
145
+ userId: resolved.tenant.userId,
146
+ sourceIds,
147
+ ...after ? { cursor: after } : {}
148
+ });
149
+ const issuePayload = [];
150
+ for (const item of page.items) {
151
+ const externalId = item.source.externalId;
152
+ if (!externalId.startsWith(FOLLOW_UP_PREFIX)) continue;
153
+ const metadata = item.metadata ?? {};
154
+ const identifier = typeof metadata.identifier === "string" ? metadata.identifier : externalId;
155
+ const titlePrefix = `${identifier}: `;
156
+ issuePayload.push({
157
+ id: externalId,
158
+ identifier,
159
+ title: item.title.startsWith(titlePrefix) ? item.title.slice(titlePrefix.length) : item.title,
160
+ url: item.source.url ?? "",
161
+ author: typeof metadata.author === "string" ? metadata.author : null,
162
+ state: item.status ?? "",
163
+ stateType: typeof metadata.stateType === "string" ? metadata.stateType : "",
164
+ priorityLabel: typeof metadata.priority === "string" ? metadata.priority : "",
165
+ assignee: item.assignee ?? null,
166
+ incident: typeof metadata.incidentioIncidentId === "string" ? metadata.incidentioIncidentId : null,
167
+ labels: item.labels ?? [],
168
+ createdAt: item.createdAt ?? "",
169
+ updatedAt: item.updatedAt ?? "",
170
+ sourceId: item.sourceId || null
171
+ });
172
+ }
173
+ if (factoryProjectId && intakeBoards && options.ingestFactoryIssues) await options.ingestFactoryIssues({
174
+ orgId: resolved.tenant.orgId,
175
+ userId: resolved.tenant.userId,
176
+ factoryProjectId,
177
+ issues: issuePayload,
178
+ intakeBoards
179
+ });
180
+ return c.json({
181
+ issues: issuePayload,
182
+ nextCursor: page.nextCursor
183
+ });
184
+ } catch (err) {
185
+ return incidentioFetchError(loose(c), err);
186
+ }
187
+ }
188
+ }));
189
+ routes.push(registerApiRoute("/web/incidentio/issues/detail", {
190
+ method: "GET",
191
+ requiresAuth: false,
192
+ handler: async (c) => {
193
+ const resolved = await resolveOrgTenant(loose(c), auth);
194
+ if ("response" in resolved) return resolved.response;
195
+ const factoryProjectId = c.req.query("factoryProjectId");
196
+ const issueRef = c.req.query("issueRef");
197
+ if (!factoryProjectId || !UUID_RE.test(factoryProjectId)) return c.json({ error: "invalid_factory_project_id" }, 400);
198
+ if (!issueRef || issueRef.length > 2048 || !ITEM_REF_RE.test(issueRef)) return c.json({ error: "invalid_issue_ref" }, 400);
199
+ await intake.ensureReady();
200
+ const selection = (await intake.getConfig({
201
+ orgId: resolved.tenant.orgId,
202
+ integrationIds: ["incidentio"]
203
+ })).incidentio;
204
+ if (!selection.enabled) return c.json({
205
+ error: "incidentio_intake_disabled",
206
+ message: "incident.io intake is turned off in Settings."
207
+ }, 404);
208
+ const intakeBoards = await scopeSourceIdsToProject({
209
+ intake,
210
+ orgId: resolved.tenant.orgId,
211
+ factoryProjectId,
212
+ selectedIds: selection.sourceIds ?? []
213
+ });
214
+ if (Object.keys(intakeBoards).length === 0) return c.json({ error: "issue_not_found" }, 404);
215
+ try {
216
+ const dispatch = await incidentio.intake.resolveIntakeDispatch?.({
217
+ orgId: resolved.tenant.orgId,
218
+ externalSource: {
219
+ type: "issue",
220
+ externalId: issueRef
221
+ }
222
+ });
223
+ if (!dispatch || !dispatch.sourceId || !(dispatch.sourceId in intakeBoards)) return c.json({ error: "issue_not_found" }, 404);
224
+ const issue = await incidentio.intake.getIssue({
225
+ connection: dispatch.connection,
226
+ issueId: dispatch.issueId
227
+ });
228
+ if (!issue) return c.json({ error: "issue_not_found" }, 404);
229
+ return c.json({ issue: {
230
+ identifier: issue.identifier,
231
+ title: issue.title,
232
+ url: issue.url,
233
+ description: issue.description ?? null
234
+ } });
235
+ } catch (err) {
236
+ return incidentioFetchError(loose(c), err);
237
+ }
238
+ }
239
+ }));
240
+ return routes;
241
+ }
242
+ //#endregion
243
+ export { buildIncidentioRoutes, scopeSourceIdsToProject };
244
+
245
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/incidentio/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the incident.io intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the host auth gate.\n * Mirrors the Jira module: no connect/callback flow here — credentials are\n * either deployment-global constructor config (direct) or Platform-managed\n * connections discovered at runtime. Every route re-resolves the\n * authenticated user from the request and scopes intake selections by the\n * caller's org.\n *\n * When the feature is disabled (no auth, or no intake storage),\n * `buildIncidentioRoutes` returns only `GET /web/incidentio/status`, which\n * reports `enabled:false` so the SPA can cleanly hide all incident.io UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { Intake } from '../../capabilities/intake.js';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport { IncidentioApiError } from './api.js';\nimport type { IncidentioRulesIngress } 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 ITEM_REF_RE = /^incidentio:(?:follow-up|incident):[\\w-]+$/;\nconst FOLLOW_UP_PREFIX = 'incidentio:follow-up:';\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 incident.io feature gate, mirroring\n * the Jira diagnostics shape. Only booleans — never values.\n */\nexport interface IncidentioFeatureDiagnostics {\n incidentioConfigured: boolean;\n factoryAuthEnabled: boolean;\n}\n\nexport interface MountIncidentioRoutesOptions {\n /**\n * The integration's intake capability providing incident.io access. Required\n * for everything beyond the disabled `status` route.\n */\n incidentio?: { intake: Intake; id: string };\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 follow-ups. When present, a\n * board-scoped listing also materializes new cards through the incident.io\n * event rules — the same automatic intake Linear and Jira have.\n */\n ingestFactoryIssues?: (input: IncidentioRulesIngress) => Promise<unknown>;\n}\n\n/**\n * Narrow the caller's selected incident.io sources to the ones that feed this\n * Factory project. Routing is explicit: a source feeds this Factory only when\n * its binding names both the Factory project and a board. Mirrors the Jira\n * routes' scoping semantics — the generic binding storage is provider-neutral.\n * Shared with the agent tools, which enforce the same source-level\n * authorization before fetching item details.\n */\nexport async 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: 'incidentio' })) {\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 an incident.io request. Intake selections\n * are org-owned, so it requires both a signed-in user and an organization.\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: 'incident.io intake requires an organization. Personal accounts cannot use incident.io intake.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque pagination cursor from the query string. Cursors are\n * server-issued base64url blobs, so anything outside a conservative\n * charset/length is rejected rather than decoded.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 2_048 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Map an incident.io read failure to the API response for the SPA. */\nfunction incidentioFetchError(c: RouteContext, err: unknown) {\n if (err instanceof IncidentioApiError && (err.status === 401 || err.status === 403)) {\n return c.json(\n {\n error: 'incidentio_auth_failed',\n message: 'incident.io rejected the configured credentials. Reconnect the incident.io account.',\n },\n 409,\n );\n }\n return c.json({ error: 'incidentio_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\ninterface IncidentioIssuePayload {\n id: string;\n identifier: string;\n title: string;\n url: string;\n author: string | null;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n incident: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n sourceId: string | null;\n}\n\n/**\n * Build the incident.io routes as Mastra `apiRoutes`. When the feature is\n * disabled, returns only the `status` route so the SPA can detect the\n * disabled state.\n */\nexport function buildIncidentioRoutes(options: MountIncidentioRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { incidentio, auth, intake } = options;\n const enabled = Boolean(incidentio) && auth.enabled();\n const diagnostics = (): IncidentioFeatureDiagnostics => ({\n incidentioConfigured: Boolean(incidentio),\n factoryAuthEnabled: auth.enabled(),\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/incidentio/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !incidentio || !intake) {\n return c.json({\n enabled: false,\n configured: Boolean(incidentio),\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 if (!tenant.orgId) {\n return c.json({\n enabled: true,\n configured: true,\n organizationRequired: true,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n return c.json({ enabled: true, configured: true, reason: 'ready', diagnostics: diagnostics() });\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 || !incidentio || !intake) {\n return routes;\n }\n\n // ── List bound follow-ups (cursor-paged) ────────────────────────────────\n // Respects the caller's intake config: disabled incident.io intake 404s,\n // and only sources routed to the requested Factory project are listed.\n // Incidents never appear here — only follow-ups feed intake.\n routes.push(\n registerApiRoute('/web/incidentio/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: ['incidentio'] });\n const selection = config.incidentio!;\n if (!selection.enabled) {\n return c.json(\n { error: 'incidentio_intake_disabled', message: 'incident.io intake is turned off in Settings.' },\n 404,\n );\n }\n\n const selectedIds = selection.sourceIds ?? [];\n const intakeBoards = factoryProjectId\n ? await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds,\n })\n : null;\n const sourceIds = intakeBoards ? Object.keys(intakeBoards) : selectedIds;\n if (sourceIds.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const page = await incidentio.intake.listItems({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n sourceIds,\n ...(after ? { cursor: after } : {}),\n });\n const issuePayload: IncidentioIssuePayload[] = [];\n for (const item of page.items) {\n const externalId = item.source.externalId;\n // Incidents stay out of intake even when a stale binding selects\n // the incidents source — only follow-ups are served.\n if (!externalId.startsWith(FOLLOW_UP_PREFIX)) continue;\n const metadata = item.metadata ?? {};\n const identifier = typeof metadata.identifier === 'string' ? metadata.identifier : externalId;\n const titlePrefix = `${identifier}: `;\n issuePayload.push({\n id: externalId,\n identifier,\n title: item.title.startsWith(titlePrefix) ? item.title.slice(titlePrefix.length) : item.title,\n url: item.source.url ?? '',\n author: typeof metadata.author === 'string' ? metadata.author : null,\n state: item.status ?? '',\n stateType: typeof metadata.stateType === 'string' ? metadata.stateType : '',\n priorityLabel: typeof metadata.priority === 'string' ? metadata.priority : '',\n assignee: item.assignee ?? null,\n incident: typeof metadata.incidentioIncidentId === 'string' ? metadata.incidentioIncidentId : null,\n labels: item.labels ?? [],\n createdAt: item.createdAt ?? '',\n updatedAt: item.updatedAt ?? '',\n sourceId: item.sourceId || null,\n });\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,\n intakeBoards,\n });\n }\n return c.json({ issues: issuePayload, nextCursor: page.nextCursor });\n } catch (err) {\n return incidentioFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── Follow-up detail (description for the card panel) ───────────────────\n routes.push(\n registerApiRoute('/web/incidentio/issues/detail', {\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 factoryProjectId = c.req.query('factoryProjectId');\n const issueRef = c.req.query('issueRef');\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 || !ITEM_REF_RE.test(issueRef)) {\n return c.json({ error: 'invalid_issue_ref' }, 400);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['incidentio'] });\n const selection = config.incidentio!;\n if (!selection.enabled) {\n return c.json(\n { error: 'incidentio_intake_disabled', message: 'incident.io intake is turned off in Settings.' },\n 404,\n );\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 // The item must resolve through a source routed to this Factory —\n // an item from an unbound source reads exactly like one that\n // doesn't exist (same stance as the Jira detail route).\n const dispatch = await incidentio.intake.resolveIntakeDispatch?.({\n orgId: resolved.tenant.orgId,\n externalSource: { type: 'issue', externalId: issueRef },\n });\n if (!dispatch || !dispatch.sourceId || !(dispatch.sourceId in intakeBoards)) {\n return c.json({ error: 'issue_not_found' }, 404);\n }\n const issue = await incidentio.intake.getIssue({\n connection: dispatch.connection,\n issueId: dispatch.issueId,\n });\n if (!issue) return c.json({ error: 'issue_not_found' }, 404);\n return c.json({\n issue: {\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n description: issue.description ?? null,\n },\n });\n } catch (err) {\n return incidentioFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { BoardRegistry } from '../../boards/index.js';
2
+ import type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';
3
+ import type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';
4
+ import type { IntegrationContext } from '../base.js';
5
+ import type { IncidentioEventRules } from './default-rules.js';
6
+ export interface IncidentioIssueIngress {
7
+ /** Stable item reference — the prefixed incident.io follow-up id (`incidentio:follow-up:<ulid>`). */
8
+ id: string;
9
+ identifier: string;
10
+ title: string;
11
+ url: string;
12
+ state: string;
13
+ stateType: string;
14
+ priorityLabel: string;
15
+ assignee: string | null;
16
+ /** Display name of the follow-up creator, when available. */
17
+ author: string | null;
18
+ /** Reference of the incident this follow-up belongs to, when available. */
19
+ incident: string | null;
20
+ labels: string[];
21
+ createdAt: string;
22
+ updatedAt: string;
23
+ /** incident.io source the follow-up was read from; resolves the bound board via `intakeBoards`. */
24
+ sourceId?: string | null;
25
+ }
26
+ export interface IncidentioRulesOptions {
27
+ projects: Pick<FactoryProjectsStorage, 'get'>;
28
+ storage: WorkItemsStorage;
29
+ configVersion: string;
30
+ boards: BoardRegistry;
31
+ incidentioRules: IncidentioEventRules;
32
+ }
33
+ export interface IncidentioRulesIngress {
34
+ orgId: string;
35
+ userId: string;
36
+ factoryProjectId: string;
37
+ issues: IncidentioIssueIngress[];
38
+ /** Board id per bound incident.io source id, from the project's intake bindings. */
39
+ intakeBoards?: Readonly<Record<string, string>>;
40
+ }
41
+ type IngressStatus = 'committed' | 'replayed' | 'missing';
42
+ export declare class IncidentioRules {
43
+ #private;
44
+ private readonly options;
45
+ constructor(options: IncidentioRulesOptions);
46
+ ingest(input: IncidentioRulesIngress): Promise<{
47
+ status: IngressStatus;
48
+ ingested: number;
49
+ }>;
50
+ }
51
+ export declare function attachIncidentioRules(incidentio: {
52
+ readonly rules: IncidentioEventRules;
53
+ }, context: IntegrationContext): ((input: IncidentioRulesIngress) => Promise<unknown>) | undefined;
54
+ export {};
55
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/incidentio/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,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,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAkB/D,MAAM,WAAW,sBAAsB;IACrC,qGAAqG;IACrG,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,6DAA6D;IAC7D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,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,eAAe,EAAE,oBAAoB,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,sBAAsB,EAAE,CAAC;IACjC,oFAAoF;IACpF,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,eAAe;;IACd,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,sBAAsB;IAEtD,MAAM,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAqIlG;AAED,wBAAgB,qBAAqB,CACnC,UAAU,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAA;CAAE,EACpD,OAAO,EAAE,kBAAkB,GAC1B,CAAC,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAUnE"}
@@ -0,0 +1,175 @@
1
+ import { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from "../../rules/validation.js";
2
+ import { boardForWorkItem, isTerminalWorkItem } from "../../boards/semantics.js";
3
+ import "../../boards/index.js";
4
+ import { incidentioClaimKey } from "./claim.js";
5
+ //#region src/integrations/incidentio/rules.ts
6
+ const RULE_TIMEOUT_MS = 5e3;
7
+ async function withRuleTimeout(promise) {
8
+ let timeout;
9
+ try {
10
+ return await Promise.race([promise, new Promise((_, reject) => {
11
+ timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
12
+ })]);
13
+ } finally {
14
+ if (timeout) clearTimeout(timeout);
15
+ }
16
+ }
17
+ var IncidentioRules = class {
18
+ options;
19
+ constructor(options) {
20
+ this.options = options;
21
+ }
22
+ async ingest(input) {
23
+ if (!await this.options.projects.get({
24
+ orgId: input.orgId,
25
+ id: input.factoryProjectId
26
+ })) return {
27
+ status: "missing",
28
+ ingested: 0
29
+ };
30
+ const items = await this.options.storage.list({
31
+ orgId: input.orgId,
32
+ factoryProjectId: input.factoryProjectId
33
+ });
34
+ const itemsBySourceKey = new Map(items.map((item) => [item.externalSource?.externalId, item]));
35
+ const itemsByClaimKey = new Map(items.filter((item) => item.claimKey).map((item) => [item.claimKey, item]));
36
+ const statuses = [];
37
+ for (const issue of input.issues) {
38
+ const relatedItem = itemsByClaimKey.get(incidentioClaimKey(issue.id)) ?? itemsBySourceKey.get(issue.id);
39
+ if (!relatedItem && await this.#heldElsewhere(input, issue)) {
40
+ statuses.push("missing");
41
+ continue;
42
+ }
43
+ statuses.push(await this.#ingestIssue(input, issue, relatedItem));
44
+ }
45
+ if (statuses.some((status) => status === "committed")) return {
46
+ status: "committed",
47
+ ingested: statuses.length
48
+ };
49
+ if (statuses.some((status) => status === "replayed")) return {
50
+ status: "replayed",
51
+ ingested: statuses.length
52
+ };
53
+ return {
54
+ status: "missing",
55
+ ingested: statuses.length
56
+ };
57
+ }
58
+ async #heldElsewhere(input, issue) {
59
+ const heldLive = (row) => row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);
60
+ const claimant = await this.options.storage.getByClaimKey({
61
+ orgId: input.orgId,
62
+ claimKey: incidentioClaimKey(issue.id)
63
+ });
64
+ if (claimant && heldLive(claimant)) return true;
65
+ return (await this.options.storage.listBySource({
66
+ orgId: input.orgId,
67
+ source: {
68
+ integrationId: "incidentio",
69
+ type: "issue",
70
+ externalId: issue.id
71
+ }
72
+ })).some(heldLive);
73
+ }
74
+ async #ingestIssue(input, issue, relatedItem) {
75
+ const ingressId = `incidentio:${issue.id}:${issue.updatedAt}`;
76
+ const actor = {
77
+ type: "human",
78
+ id: input.userId
79
+ };
80
+ const isClosed = issue.stateType === "completed" || issue.stateType === "canceled";
81
+ if (isClosed && !relatedItem) return "missing";
82
+ const event = isClosed ? "followUpClosed" : "followUpObserved";
83
+ const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : void 0;
84
+ const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : void 0;
85
+ const context = {
86
+ tenant: {
87
+ orgId: input.orgId,
88
+ projectId: input.factoryProjectId
89
+ },
90
+ actor,
91
+ ingress: {
92
+ type: "incidentio",
93
+ id: ingressId
94
+ },
95
+ cause: `incidentio.${event}`,
96
+ causalChain: [],
97
+ configVersion: this.options.configVersion,
98
+ ...relatedItem ? {
99
+ item: {
100
+ id: relatedItem.id,
101
+ source: "incidentio-follow-up",
102
+ sourceKey: relatedItem.externalSource?.externalId ?? null,
103
+ parentWorkItemId: relatedItem.parentWorkItemId,
104
+ title: relatedItem.title,
105
+ url: relatedItem.externalSource?.url ?? null,
106
+ stages: relatedItem.stages,
107
+ acceptedAt: relatedItem.acceptedAt,
108
+ metadata: relatedItem.metadata
109
+ },
110
+ board: boardForWorkItem(relatedItem),
111
+ itemRevision: relatedItem.revision
112
+ } : {},
113
+ ...boundBoard ? { intake: {
114
+ board: boundBoard.id,
115
+ initialPhase: boundBoard.initialPhase
116
+ } } : {},
117
+ event,
118
+ issue
119
+ };
120
+ const rule = this.options.incidentioRules[context.event];
121
+ let decision;
122
+ let decisions = [];
123
+ let outcome = { status: "accepted" };
124
+ try {
125
+ decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
126
+ if (decision?.type === "reject") outcome = {
127
+ status: "rejected",
128
+ code: decision.code,
129
+ reason: decision.reason
130
+ };
131
+ else if (decision) decisions = validateFactoryRuleDecisions([decision]).map((entry) => {
132
+ assertFactoryDecisionTarget(entry, this.options.boards, relatedItem ? boardForWorkItem(relatedItem) : void 0);
133
+ return { ...entry };
134
+ });
135
+ } catch (error) {
136
+ const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
137
+ outcome = {
138
+ status: "rejected",
139
+ code: timedOut ? "timeout" : "rule_error",
140
+ reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory incident.io rule failed."
141
+ };
142
+ }
143
+ return (await this.options.storage.commitRuleEvaluation({
144
+ orgId: input.orgId,
145
+ factoryProjectId: input.factoryProjectId,
146
+ workItemId: relatedItem?.id ?? null,
147
+ ingress: {
148
+ identity: ingressId,
149
+ triggerType: `incidentio.${event}`
150
+ },
151
+ configVersion: this.options.configVersion,
152
+ expectedRevision: relatedItem?.revision ?? null,
153
+ actor,
154
+ outcome,
155
+ decisions,
156
+ causalChain: [],
157
+ now: /* @__PURE__ */ new Date()
158
+ })).status;
159
+ }
160
+ };
161
+ function attachIncidentioRules(incidentio, context) {
162
+ if (!context.runtime) return void 0;
163
+ const rules = new IncidentioRules({
164
+ projects: context.storage.projects,
165
+ storage: context.runtime.workItems,
166
+ configVersion: context.runtime.configVersion,
167
+ boards: context.runtime.boards,
168
+ incidentioRules: incidentio.rules
169
+ });
170
+ return (input) => rules.ingest(input);
171
+ }
172
+ //#endregion
173
+ export { IncidentioRules, attachIncidentioRules };
174
+
175
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","names":["#heldElsewhere","#ingestIssue"],"sources":["../../../src/integrations/incidentio/rules.ts"],"sourcesContent":["import { boardForWorkItem, isTerminalWorkItem } from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport type { FactoryIncidentioRuleContext, 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 { incidentioClaimKey } from './claim.js';\nimport type { IncidentioEventRules } 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 IncidentioIssueIngress {\n /** Stable item reference — the prefixed incident.io follow-up id (`incidentio:follow-up:<ulid>`). */\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 follow-up creator, when available. */\n author: string | null;\n /** Reference of the incident this follow-up belongs to, when available. */\n incident: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** incident.io source the follow-up was read from; resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n}\n\nexport interface IncidentioRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n incidentioRules: IncidentioEventRules;\n}\n\nexport interface IncidentioRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: IncidentioIssueIngress[];\n /** Board id per bound incident.io source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class IncidentioRules {\n constructor(private readonly options: IncidentioRulesOptions) {}\n\n async ingest(input: IncidentioRulesIngress): 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 item\n // reference covers cards filed before claims existed.\n const relatedItem = itemsByClaimKey.get(incidentioClaimKey(issue.id)) ?? itemsBySourceKey.get(issue.id);\n // One live card per follow-up 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: IncidentioRulesIngress, issue: IncidentioIssueIngress): 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: incidentioClaimKey(issue.id),\n });\n if (claimant && heldLive(claimant)) return true;\n // Cards filed before claims existed carry only the item reference.\n const legacy = await this.options.storage.listBySource({\n orgId: input.orgId,\n source: { integrationId: 'incidentio', type: 'issue', externalId: issue.id },\n });\n return legacy.some(heldLive);\n }\n\n async #ingestIssue(\n input: IncidentioRulesIngress,\n issue: IncidentioIssueIngress,\n relatedItem: WorkItemRow | undefined,\n ): Promise<IngressStatus> {\n const ingressId = `incidentio:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed follow-ups 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 ? 'followUpClosed' : 'followUpObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryIncidentioRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'incidentio', id: ingressId },\n cause: `incidentio.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'incidentio-follow-up',\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.incidentioRules[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 incident.io 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: `incidentio.${event}` },\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 attachIncidentioRules(\n incidentio: { readonly rules: IncidentioEventRules },\n context: IntegrationContext,\n): ((input: IncidentioRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new IncidentioRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n incidentioRules: incidentio.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Minimal Atlassian Document Format (ADF) helpers.
3
+ *
4
+ * Jira Cloud returns issue descriptions and comment bodies as ADF JSON and
5
+ * requires ADF when creating comments. The flattener here is a display/agent-
6
+ * context aid, not a fidelity converter: it covers the common nodes
7
+ * (paragraphs, text, hard breaks, lists, code blocks, headings, mentions,
8
+ * links) and degrades unknown nodes to their text content.
9
+ */
10
+ /** Loosely-typed ADF node — Jira emits many node types we don't enumerate. */
11
+ export interface AdfNode {
12
+ type?: string;
13
+ text?: string;
14
+ version?: number;
15
+ content?: AdfNode[];
16
+ attrs?: Record<string, unknown>;
17
+ }
18
+ /** Flatten an ADF document (or fragment) to plain text. */
19
+ export declare function adfToText(node: unknown): string;
20
+ /**
21
+ * Wrap plain text in a minimal ADF document for comment creation. Each line
22
+ * becomes a paragraph; blank lines produce empty paragraphs so spacing
23
+ * round-trips reasonably.
24
+ */
25
+ export declare function textToAdf(text: string): AdfNode;
26
+ //# sourceMappingURL=adf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adf.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/adf.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,2DAA2D;AAC3D,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAK/C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO/C"}
@@ -0,0 +1,73 @@
1
+ //#region src/integrations/jira/adf.ts
2
+ /** Flatten an ADF document (or fragment) to plain text. */
3
+ function adfToText(node) {
4
+ if (!node || typeof node !== "object") return "";
5
+ return render(node).replace(/\n{3,}/g, "\n\n").trim();
6
+ }
7
+ /**
8
+ * Wrap plain text in a minimal ADF document for comment creation. Each line
9
+ * becomes a paragraph; blank lines produce empty paragraphs so spacing
10
+ * round-trips reasonably.
11
+ */
12
+ function textToAdf(text) {
13
+ const content = text.split("\n").map((line) => ({
14
+ type: "paragraph",
15
+ content: line === "" ? [] : [{
16
+ type: "text",
17
+ text: line
18
+ }]
19
+ }));
20
+ return {
21
+ type: "doc",
22
+ version: 1,
23
+ content: content.length > 0 ? content : [{
24
+ type: "paragraph",
25
+ content: []
26
+ }]
27
+ };
28
+ }
29
+ function children(node) {
30
+ return Array.isArray(node.content) ? node.content : [];
31
+ }
32
+ function renderInline(nodes) {
33
+ return nodes.map(render).join("");
34
+ }
35
+ function renderBlocks(nodes, separator) {
36
+ return nodes.map(render).filter((text) => text !== "").join(separator);
37
+ }
38
+ function render(node) {
39
+ switch (node.type) {
40
+ case "text": return node.text ?? "";
41
+ case "hardBreak": return "\n";
42
+ case "mention": {
43
+ const label = String(node.attrs?.text ?? node.attrs?.displayName ?? "").trim();
44
+ if (!label) return "";
45
+ return label.startsWith("@") ? label : `@${label}`;
46
+ }
47
+ case "emoji": return String(node.attrs?.shortName ?? "");
48
+ case "inlineCard":
49
+ case "blockCard":
50
+ case "embedCard": return String(node.attrs?.url ?? "");
51
+ case "doc": return renderBlocks(children(node), "\n\n");
52
+ case "blockquote":
53
+ case "panel":
54
+ case "listItem":
55
+ case "expand":
56
+ case "nestedExpand": return renderBlocks(children(node), "\n");
57
+ case "paragraph":
58
+ case "heading": return renderInline(children(node));
59
+ case "codeBlock": return "```\n" + renderInline(children(node)) + "\n```";
60
+ case "bulletList": return children(node).map((item) => `- ${render(item).replace(/\n/g, "\n ")}`).join("\n");
61
+ case "orderedList": return children(node).map((item, index) => `${index + 1}. ${render(item).replace(/\n/g, "\n ")}`).join("\n");
62
+ case "rule": return "---";
63
+ default: {
64
+ if (node.text) return node.text;
65
+ const kids = children(node);
66
+ return kids.length > 0 ? renderBlocks(kids, "\n") : "";
67
+ }
68
+ }
69
+ }
70
+ //#endregion
71
+ export { adfToText, textToAdf };
72
+
73
+ //# sourceMappingURL=adf.js.map