@mastra/factory 0.26.0-alpha.1 → 0.26.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -7
- package/dist/boards/define-board.d.ts +19 -0
- package/dist/boards/define-board.d.ts.map +1 -1
- package/dist/boards/define-board.js +41 -9
- package/dist/boards/define-board.js.map +1 -1
- package/dist/boards/project-boards.d.ts +78 -0
- package/dist/boards/project-boards.d.ts.map +1 -0
- package/dist/boards/project-boards.js +194 -0
- package/dist/boards/project-boards.js.map +1 -0
- package/dist/boards/review.js +2 -2
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/stored-board.d.ts +101 -0
- package/dist/boards/stored-board.d.ts.map +1 -0
- package/dist/boards/stored-board.js +139 -0
- package/dist/boards/stored-board.js.map +1 -0
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +6 -3
- package/dist/boards/work.js.map +1 -1
- package/dist/capabilities/intake.d.ts +6 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/environment/build.d.ts +1 -1
- package/dist/environment/build.d.ts.map +1 -1
- package/dist/environment/build.js +2 -0
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts +15 -2
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +51 -12
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/integration.d.ts +5 -4
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +10 -8
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +2 -2
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +6 -1
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/jira/binding-scope.d.ts +30 -0
- package/dist/integrations/jira/binding-scope.d.ts.map +1 -0
- package/dist/integrations/jira/binding-scope.js +50 -0
- package/dist/integrations/jira/binding-scope.js.map +1 -0
- package/dist/integrations/jira/claim.d.ts +12 -0
- package/dist/integrations/jira/claim.d.ts.map +1 -1
- package/dist/integrations/jira/claim.js +24 -1
- package/dist/integrations/jira/claim.js.map +1 -1
- package/dist/integrations/jira/intake-jql.d.ts +32 -0
- package/dist/integrations/jira/intake-jql.d.ts.map +1 -0
- package/dist/integrations/jira/intake-jql.js +57 -0
- package/dist/integrations/jira/intake-jql.js.map +1 -0
- package/dist/integrations/jira/integration.d.ts +4 -3
- package/dist/integrations/jira/integration.d.ts.map +1 -1
- package/dist/integrations/jira/integration.js +14 -21
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/routes.d.ts +3 -0
- package/dist/integrations/jira/routes.d.ts.map +1 -1
- package/dist/integrations/jira/routes.js +16 -29
- package/dist/integrations/jira/routes.js.map +1 -1
- package/dist/integrations/jira/rules.d.ts.map +1 -1
- package/dist/integrations/jira/rules.js +8 -13
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +5 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +7 -0
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.d.ts +7 -1
- package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
- package/dist/integrations/platform/jira/integration.js +26 -24
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/jira/routes.d.ts +3 -0
- package/dist/integrations/platform/jira/routes.d.ts.map +1 -1
- package/dist/integrations/platform/jira/routes.js +17 -28
- package/dist/integrations/platform/jira/routes.js.map +1 -1
- package/dist/routes/attention-providers.d.ts +2 -1
- package/dist/routes/attention-providers.d.ts.map +1 -1
- package/dist/routes/attention-providers.js +3 -2
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/attention.d.ts +2 -1
- package/dist/routes/attention.d.ts.map +1 -1
- package/dist/routes/attention.js.map +1 -1
- package/dist/routes/boards.d.ts +30 -0
- package/dist/routes/boards.d.ts.map +1 -0
- package/dist/routes/boards.js +272 -0
- package/dist/routes/boards.js.map +1 -0
- package/dist/routes/contracts.d.ts +249 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +107 -35
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/intake.d.ts +15 -10
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +109 -22
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +10 -1
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/surface.d.ts +10 -3
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +13 -1
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.d.ts +9 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -1
- package/dist/routes/tenant-credentials.js +24 -2
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.d.ts +3 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +64 -42
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts.map +1 -1
- package/dist/rules/dispatch-errors.js +4 -0
- package/dist/rules/dispatch-errors.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +7 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +70 -34
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts +3 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +11 -6
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/tools.d.ts +2 -1
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +2 -1
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +3 -1
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +12 -12
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.d.ts +4 -1
- package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/storage/domains/audit/actions.d.ts +1 -0
- package/dist/storage/domains/audit/actions.d.ts.map +1 -1
- package/dist/storage/domains/audit/actions.js +6 -1
- package/dist/storage/domains/audit/actions.js.map +1 -1
- package/dist/storage/domains/boards/base.d.ts +110 -0
- package/dist/storage/domains/boards/base.d.ts.map +1 -0
- package/dist/storage/domains/boards/base.js +292 -0
- package/dist/storage/domains/boards/base.js.map +1 -0
- package/dist/storage/domains/boards/schemas.d.ts +11 -0
- package/dist/storage/domains/boards/schemas.d.ts.map +1 -0
- package/dist/storage/domains/boards/schemas.js +106 -0
- package/dist/storage/domains/boards/schemas.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +4 -1
- package/dist/storage/domains/credentials/base.d.ts.map +1 -1
- package/dist/storage/domains/credentials/base.js +5 -2
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/intake/base.d.ts +86 -5
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +298 -59
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/projects/base.d.ts +2 -1
- package/dist/storage/domains/projects/base.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.js +15 -10
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +16 -2
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +40 -11
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/tools/read-tools.d.ts +3 -1
- package/dist/tools/read-tools.d.ts.map +1 -1
- package/dist/tools/read-tools.js +3 -2
- package/dist/tools/read-tools.js.map +1 -1
- package/dist/tools/session-tools.d.ts +3 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +2 -1
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.d.ts +3 -1
- package/dist/tools/sessions-state-processor.d.ts.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -1
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/tools/work-item-tools.d.ts +3 -1
- package/dist/tools/work-item-tools.d.ts.map +1 -1
- package/dist/tools/work-item-tools.js +3 -2
- package/dist/tools/work-item-tools.js.map +1 -1
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +14 -5
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-plan/SKILL.md +1 -1
- package/factory-skills/factory-triage/SKILL.md +18 -7
- package/package.json +9 -9
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JQL intake reads issues with, shared by the direct and Platform Jira
|
|
3
|
+
* integrations.
|
|
4
|
+
*
|
|
5
|
+
* Several Factory projects may bind the same Jira project, each to a disjoint
|
|
6
|
+
* slice of its issues chosen by label (see `IntakeStorage.setSharedBinding`).
|
|
7
|
+
* The slicing happens here, in the query Jira runs, so a Factory only ever
|
|
8
|
+
* fetches the issues its binding owns and no two Factories race to file the
|
|
9
|
+
* same issue.
|
|
10
|
+
*/
|
|
11
|
+
/** One Jira project a Factory reads, narrowed to the slice its binding owns. */
|
|
12
|
+
export interface JiraIntakeScope {
|
|
13
|
+
projectId: string;
|
|
14
|
+
/** Labels the binding owns; `null` takes every issue no label binding claims. */
|
|
15
|
+
include: string[] | null;
|
|
16
|
+
/**
|
|
17
|
+
* Labels the issue must not carry: for a label binding, those owned by older
|
|
18
|
+
* label bindings on the project (oldest binding wins an issue both match);
|
|
19
|
+
* for an all-issues binding, those owned by every label binding.
|
|
20
|
+
*/
|
|
21
|
+
exclude: string[];
|
|
22
|
+
}
|
|
23
|
+
/** Unfiltered scopes for plain project ids, the shape every caller used before label filters. */
|
|
24
|
+
export declare function unfilteredJiraScopes(projectIds: readonly string[]): JiraIntakeScope[];
|
|
25
|
+
/**
|
|
26
|
+
* Build the intake JQL for `scopes`, optionally narrowed to issues carrying one
|
|
27
|
+
* of `labels`. Returns `null` when `scopes` names no usable project, so callers
|
|
28
|
+
* skip the request instead of reading the whole site. `scopes` omitted reads
|
|
29
|
+
* every project.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildIntakeJql(scopes?: readonly JiraIntakeScope[], labels?: readonly string[]): string | null;
|
|
32
|
+
//# sourceMappingURL=intake-jql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intake-jql.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/intake-jql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,iGAAiG;AACjG,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,eAAe,EAAE,CAErF;AAgBD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,SAAS,eAAe,EAAE,EAAE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CA8B7G"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/integrations/jira/intake-jql.ts
|
|
2
|
+
/** Unfiltered scopes for plain project ids, the shape every caller used before label filters. */
|
|
3
|
+
function unfilteredJiraScopes(projectIds) {
|
|
4
|
+
return projectIds.map((projectId) => ({
|
|
5
|
+
projectId,
|
|
6
|
+
include: null,
|
|
7
|
+
exclude: []
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
/** Numeric ids pass bare, keys are quoted, and anything outside a safe charset is dropped. */
|
|
11
|
+
function safeProject(projectId) {
|
|
12
|
+
const id = projectId.trim();
|
|
13
|
+
if (!/^[A-Za-z0-9_]+$/.test(id)) return null;
|
|
14
|
+
return /^\d+$/.test(id) ? id : `"${id}"`;
|
|
15
|
+
}
|
|
16
|
+
/** Quote labels for JQL, stripping the characters that could break out of the string. */
|
|
17
|
+
function safeLabels(labels) {
|
|
18
|
+
return [...new Set(labels.map((label) => label.trim().replace(/["\\]/g, "")).filter(Boolean))].map((label) => `"${label}"`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the intake JQL for `scopes`, optionally narrowed to issues carrying one
|
|
22
|
+
* of `labels`. Returns `null` when `scopes` names no usable project, so callers
|
|
23
|
+
* skip the request instead of reading the whole site. `scopes` omitted reads
|
|
24
|
+
* every project.
|
|
25
|
+
*/
|
|
26
|
+
function buildIntakeJql(scopes, labels) {
|
|
27
|
+
const clauses = [];
|
|
28
|
+
if (scopes) {
|
|
29
|
+
const plain = [];
|
|
30
|
+
const filtered = [];
|
|
31
|
+
for (const scope of scopes) {
|
|
32
|
+
const project = safeProject(scope.projectId);
|
|
33
|
+
if (!project) continue;
|
|
34
|
+
const exclude = safeLabels(scope.exclude);
|
|
35
|
+
if (scope.include === null) {
|
|
36
|
+
if (exclude.length === 0) plain.push(project);
|
|
37
|
+
else filtered.push(`(project = ${project} AND (labels IS EMPTY OR labels NOT IN (${exclude.join(", ")})))`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const include = safeLabels(scope.include);
|
|
41
|
+
if (include.length === 0) continue;
|
|
42
|
+
const older = exclude.length > 0 ? ` AND labels NOT IN (${exclude.join(", ")})` : "";
|
|
43
|
+
filtered.push(`(project = ${project} AND labels IN (${include.join(", ")})${older})`);
|
|
44
|
+
}
|
|
45
|
+
const uniquePlain = [...new Set(plain)];
|
|
46
|
+
const projectClauses = [...uniquePlain.length > 0 ? [`project IN (${uniquePlain.join(", ")})`] : [], ...filtered];
|
|
47
|
+
if (projectClauses.length === 0) return null;
|
|
48
|
+
clauses.push(projectClauses.length === 1 ? projectClauses[0] : `(${projectClauses.join(" OR ")})`);
|
|
49
|
+
}
|
|
50
|
+
clauses.push("statusCategory != Done");
|
|
51
|
+
const narrowed = safeLabels(labels ?? []);
|
|
52
|
+
if (narrowed.length > 0) clauses.push(`labels IN (${narrowed.join(", ")})`);
|
|
53
|
+
return `${clauses.join(" AND ")} ORDER BY updated DESC`;
|
|
54
|
+
}
|
|
55
|
+
export { buildIntakeJql, unfilteredJiraScopes };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=intake-jql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intake-jql.js","names":[],"sources":["../../../src/integrations/jira/intake-jql.ts"],"sourcesContent":["/**\n * The JQL intake reads issues with, shared by the direct and Platform Jira\n * integrations.\n *\n * Several Factory projects may bind the same Jira project, each to a disjoint\n * slice of its issues chosen by label (see `IntakeStorage.setSharedBinding`).\n * The slicing happens here, in the query Jira runs, so a Factory only ever\n * fetches the issues its binding owns and no two Factories race to file the\n * same issue.\n */\n\n/** One Jira project a Factory reads, narrowed to the slice its binding owns. */\nexport interface JiraIntakeScope {\n projectId: string;\n /** Labels the binding owns; `null` takes every issue no label binding claims. */\n include: string[] | null;\n /**\n * Labels the issue must not carry: for a label binding, those owned by older\n * label bindings on the project (oldest binding wins an issue both match);\n * for an all-issues binding, those owned by every label binding.\n */\n exclude: string[];\n}\n\n/** Unfiltered scopes for plain project ids, the shape every caller used before label filters. */\nexport function unfilteredJiraScopes(projectIds: readonly string[]): JiraIntakeScope[] {\n return projectIds.map(projectId => ({ projectId, include: null, exclude: [] }));\n}\n\n/** Numeric ids pass bare, keys are quoted, and anything outside a safe charset is dropped. */\nfunction safeProject(projectId: string): string | null {\n const id = projectId.trim();\n if (!/^[A-Za-z0-9_]+$/.test(id)) return null;\n return /^\\d+$/.test(id) ? id : `\"${id}\"`;\n}\n\n/** Quote labels for JQL, stripping the characters that could break out of the string. */\nfunction safeLabels(labels: readonly string[]): string[] {\n return [...new Set(labels.map(label => label.trim().replace(/[\"\\\\]/g, '')).filter(Boolean))].map(\n label => `\"${label}\"`,\n );\n}\n\n/**\n * Build the intake JQL for `scopes`, optionally narrowed to issues carrying one\n * of `labels`. Returns `null` when `scopes` names no usable project, so callers\n * skip the request instead of reading the whole site. `scopes` omitted reads\n * every project.\n */\nexport function buildIntakeJql(scopes?: readonly JiraIntakeScope[], labels?: readonly string[]): string | null {\n const clauses: string[] = [];\n if (scopes) {\n const plain: string[] = [];\n const filtered: string[] = [];\n for (const scope of scopes) {\n const project = safeProject(scope.projectId);\n if (!project) continue;\n const exclude = safeLabels(scope.exclude);\n if (scope.include === null) {\n if (exclude.length === 0) plain.push(project);\n // `labels NOT IN` drops unlabelled issues, which an all-issues binding must keep.\n else filtered.push(`(project = ${project} AND (labels IS EMPTY OR labels NOT IN (${exclude.join(', ')})))`);\n continue;\n }\n const include = safeLabels(scope.include);\n if (include.length === 0) continue;\n // The issue carries an owned label, so plain `NOT IN` cannot drop an unlabelled one here.\n const older = exclude.length > 0 ? ` AND labels NOT IN (${exclude.join(', ')})` : '';\n filtered.push(`(project = ${project} AND labels IN (${include.join(', ')})${older})`);\n }\n const uniquePlain = [...new Set(plain)];\n const projectClauses = [...(uniquePlain.length > 0 ? [`project IN (${uniquePlain.join(', ')})`] : []), ...filtered];\n if (projectClauses.length === 0) return null;\n clauses.push(projectClauses.length === 1 ? projectClauses[0]! : `(${projectClauses.join(' OR ')})`);\n }\n clauses.push('statusCategory != Done');\n const narrowed = safeLabels(labels ?? []);\n if (narrowed.length > 0) clauses.push(`labels IN (${narrowed.join(', ')})`);\n return `${clauses.join(' AND ')} ORDER BY updated DESC`;\n}\n"],"mappings":";;AAyBA,SAAgB,qBAAqB,YAAkD;CACrF,OAAO,WAAW,KAAI,eAAc;EAAE;EAAW,SAAS;EAAM,SAAS,CAAC;CAAE,EAAE;AAChF;;AAGA,SAAS,YAAY,WAAkC;CACrD,MAAM,KAAK,UAAU,KAAK;CAC1B,IAAI,CAAC,kBAAkB,KAAK,EAAE,GAAG,OAAO;CACxC,OAAO,QAAQ,KAAK,EAAE,IAAI,KAAK,IAAI,GAAG;AACxC;;AAGA,SAAS,WAAW,QAAqC;CACvD,OAAO,CAAC,GAAG,IAAI,IAAI,OAAO,KAAI,UAAS,MAAM,KAAK,CAAC,CAAC,QAAQ,UAAU,EAAE,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,KAC3F,UAAS,IAAI,MAAM,EACrB;AACF;;;;;;;AAQA,SAAgB,eAAe,QAAqC,QAA2C;CAC7G,MAAM,UAAoB,CAAC;CAC3B,IAAI,QAAQ;EACV,MAAM,QAAkB,CAAC;EACzB,MAAM,WAAqB,CAAC;EAC5B,KAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,UAAU,YAAY,MAAM,SAAS;GAC3C,IAAI,CAAC,SAAS;GACd,MAAM,UAAU,WAAW,MAAM,OAAO;GACxC,IAAI,MAAM,YAAY,MAAM;IAC1B,IAAI,QAAQ,WAAW,GAAG,MAAM,KAAK,OAAO;SAEvC,SAAS,KAAK,cAAc,QAAQ,0CAA0C,QAAQ,KAAK,IAAI,EAAE,IAAI;IAC1G;GACF;GACA,MAAM,UAAU,WAAW,MAAM,OAAO;GACxC,IAAI,QAAQ,WAAW,GAAG;GAE1B,MAAM,QAAQ,QAAQ,SAAS,IAAI,uBAAuB,QAAQ,KAAK,IAAI,EAAE,KAAK;GAClF,SAAS,KAAK,cAAc,QAAQ,kBAAkB,QAAQ,KAAK,IAAI,EAAE,GAAG,MAAM,EAAE;EACtF;EACA,MAAM,cAAc,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC;EACtC,MAAM,iBAAiB,CAAC,GAAI,YAAY,SAAS,IAAI,CAAC,eAAe,YAAY,KAAK,IAAI,EAAE,EAAE,IAAI,CAAC,GAAI,GAAG,QAAQ;EAClH,IAAI,eAAe,WAAW,GAAG,OAAO;EACxC,QAAQ,KAAK,eAAe,WAAW,IAAI,eAAe,KAAM,IAAI,eAAe,KAAK,MAAM,EAAE,EAAE;CACpG;CACA,QAAQ,KAAK,wBAAwB;CACrC,MAAM,WAAW,WAAW,UAAU,CAAC,CAAC;CACxC,IAAI,SAAS,SAAS,GAAG,QAAQ,KAAK,cAAc,SAAS,KAAK,IAAI,EAAE,EAAE;CAC1E,OAAO,GAAG,QAAQ,KAAK,OAAO,EAAE;AAClC"}
|
|
@@ -24,6 +24,7 @@ import type { FactoryProjectsStorage } from '../../storage/domains/projects/base
|
|
|
24
24
|
import type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';
|
|
25
25
|
import { JiraApiClient } from './api.js';
|
|
26
26
|
import type { JiraEventRules, JiraRuleOverrides } from './default-rules.js';
|
|
27
|
+
import type { JiraIntakeScope } from './intake-jql.js';
|
|
27
28
|
/** Deployment-global Jira Cloud credentials. All credential fields are required. */
|
|
28
29
|
export interface JiraIntegrationConfig {
|
|
29
30
|
/** Site base URL, e.g. `https://acme.atlassian.net`, used for browser links. */
|
|
@@ -81,10 +82,10 @@ export declare class JiraIntegration implements FactoryIntegration {
|
|
|
81
82
|
readonly intake: Intake;
|
|
82
83
|
/**
|
|
83
84
|
* One page of active issues (status category not Done), most recently
|
|
84
|
-
* updated first, optionally
|
|
85
|
-
* capability cursor is Jira's opaque `nextPageToken` passed through.
|
|
85
|
+
* updated first, optionally narrowed to the given project scopes and labels.
|
|
86
|
+
* The capability cursor is Jira's opaque `nextPageToken` passed through.
|
|
86
87
|
*/
|
|
87
|
-
listActiveIssues(cursor?: string,
|
|
88
|
+
listActiveIssues(cursor?: string, scopes?: JiraIntakeScope[], labels?: string[]): Promise<{
|
|
88
89
|
issues: Array<IntakeIssue & {
|
|
89
90
|
sourceId: string;
|
|
90
91
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,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;AAExD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAC9E,OAAO,KAAK,EAIV,MAAM,EACN,WAAW,EASZ,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAK3F,OAAO,EAAE,aAAa,EAAgB,MAAM,UAAU,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/integration.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@ import { IssueReconcileWorker } from "../issue-reconcile-worker.js";
|
|
|
2
2
|
import { adfToText } from "./adf.js";
|
|
3
3
|
import { JiraApiClient, JiraApiError } from "./api.js";
|
|
4
4
|
import { resolveJiraRules } from "./default-rules.js";
|
|
5
|
+
import { buildIntakeJql, unfilteredJiraScopes } from "./intake-jql.js";
|
|
5
6
|
import { attachJiraRules } from "./rules.js";
|
|
6
7
|
import { attachJiraIssueReconciler } from "./issue-reconciler.js";
|
|
7
8
|
import { jiraReconciliationEnabled, jiraReconciliationInterval } from "./reconciliation-config.js";
|
|
@@ -130,6 +131,7 @@ var JiraIntegration = class {
|
|
|
130
131
|
return this.#baseUrl;
|
|
131
132
|
}
|
|
132
133
|
intake = {
|
|
134
|
+
bindingLabelFilters: true,
|
|
133
135
|
resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
|
|
134
136
|
listSources: () => this.#listSources(),
|
|
135
137
|
listItems: (input) => this.#listItems(input),
|
|
@@ -173,7 +175,7 @@ var JiraIntegration = class {
|
|
|
173
175
|
items: [],
|
|
174
176
|
nextCursor: null
|
|
175
177
|
};
|
|
176
|
-
const page = await this.listActiveIssues(cursor, sourceIds);
|
|
178
|
+
const page = await this.listActiveIssues(cursor, unfilteredJiraScopes(sourceIds));
|
|
177
179
|
return {
|
|
178
180
|
items: page.issues.map((issue) => ({
|
|
179
181
|
source: {
|
|
@@ -208,7 +210,7 @@ var JiraIntegration = class {
|
|
|
208
210
|
};
|
|
209
211
|
}
|
|
210
212
|
async #listIssues(input) {
|
|
211
|
-
const page = await this.listActiveIssues(input.cursor, input.sourceIds, input.labels);
|
|
213
|
+
const page = await this.listActiveIssues(input.cursor, input.sourceIds ? unfilteredJiraScopes(input.sourceIds) : void 0, input.labels);
|
|
212
214
|
return {
|
|
213
215
|
issues: page.issues,
|
|
214
216
|
nextCursor: page.nextCursor
|
|
@@ -216,12 +218,17 @@ var JiraIntegration = class {
|
|
|
216
218
|
}
|
|
217
219
|
/**
|
|
218
220
|
* One page of active issues (status category not Done), most recently
|
|
219
|
-
* updated first, optionally
|
|
220
|
-
* capability cursor is Jira's opaque `nextPageToken` passed through.
|
|
221
|
+
* updated first, optionally narrowed to the given project scopes and labels.
|
|
222
|
+
* The capability cursor is Jira's opaque `nextPageToken` passed through.
|
|
221
223
|
*/
|
|
222
|
-
async listActiveIssues(cursor,
|
|
224
|
+
async listActiveIssues(cursor, scopes, labels) {
|
|
225
|
+
const jql = buildIntakeJql(scopes, labels);
|
|
226
|
+
if (jql === null) return {
|
|
227
|
+
issues: [],
|
|
228
|
+
nextCursor: null
|
|
229
|
+
};
|
|
223
230
|
const page = await this.api.searchIssues({
|
|
224
|
-
jql
|
|
231
|
+
jql,
|
|
225
232
|
...cursor ? { nextPageToken: cursor } : {}
|
|
226
233
|
});
|
|
227
234
|
return {
|
|
@@ -338,6 +345,7 @@ var JiraIntegration = class {
|
|
|
338
345
|
jira: this,
|
|
339
346
|
auth: ctx.auth,
|
|
340
347
|
intake: ctx.storage.intake,
|
|
348
|
+
workItems: ctx.runtime?.workItems,
|
|
341
349
|
ingestFactoryIssues: attachJiraRules(this, ctx)
|
|
342
350
|
});
|
|
343
351
|
}
|
|
@@ -383,21 +391,6 @@ function maskEmail(email) {
|
|
|
383
391
|
if (at <= 0) return "***";
|
|
384
392
|
return `${email[0]}***${email.slice(at)}`;
|
|
385
393
|
}
|
|
386
|
-
/**
|
|
387
|
-
* Build the intake JQL. Source ids and labels come from stored org config /
|
|
388
|
-
* request input, so both are sanitized before interpolation: numeric project
|
|
389
|
-
* ids pass through bare, project keys are restricted to a safe charset, and
|
|
390
|
-
* label quotes/backslashes are stripped.
|
|
391
|
-
*/
|
|
392
|
-
function buildIntakeJql(projectIds, labels) {
|
|
393
|
-
const clauses = [];
|
|
394
|
-
const safeProjects = (projectIds ?? []).map((id) => id.trim()).filter((id) => /^[A-Za-z0-9_]+$/.test(id)).map((id) => /^\d+$/.test(id) ? id : `"${id}"`);
|
|
395
|
-
if (safeProjects.length > 0) clauses.push(`project IN (${safeProjects.join(", ")})`);
|
|
396
|
-
clauses.push("statusCategory != Done");
|
|
397
|
-
const safeLabels = [...new Set((labels ?? []).map((label) => label.trim().replace(/["\\]/g, "")).filter(Boolean))];
|
|
398
|
-
if (safeLabels.length > 0) clauses.push(`labels IN (${safeLabels.map((label) => `"${label}"`).join(", ")})`);
|
|
399
|
-
return `${clauses.join(" AND ")} ORDER BY updated DESC`;
|
|
400
|
-
}
|
|
401
394
|
/** Whether the issue is already in the requested target state. */
|
|
402
395
|
function currentStatusMatches(issue, target) {
|
|
403
396
|
const status = issue.fields.status;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.js","names":[],"sources":["../../../src/integrations/jira/integration.ts"],"sourcesContent":["/**\n * `JiraIntegration` — the self-contained Jira Cloud integration.\n *\n * Implements the system-wide `FactoryIntegration` contract\n * (`../base.ts`): the deploy entry reads the `JIRA_*` env vars ONCE,\n * constructs an instance with explicit credentials, and passes it to\n * `MastraFactory`. Everything Jira-flavored the system does — project/issue\n * reads for Intake and the agent's issue tools — flows through this\n * instance. No other module reads `JIRA_*` env vars.\n *\n * Unlike Linear, credentials are **deployment-global** (Basic auth with\n * `email:apiToken` from the environment): there is no OAuth flow, no state\n * signer, and no per-org connection storage. The `IntegrationConnection`\n * arguments required by the `Intake` contract are accepted but not consulted\n * — the instance's own credentials authenticate every call.\n */\n\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { MastraWorker } from '@mastra/core/worker';\n\nimport type { IntegrationConnection } from '../../capabilities/connection.js';\nimport type {\n CreateIntakeCommentInput,\n CreatedIntakeComment,\n GetIntakeIssueInput,\n Intake,\n IntakeIssue,\n IntakeIssueDetail,\n IntakeItemPage,\n IntakeSource,\n ListIntakeIssuesInput,\n ListIntakeItemsInput,\n ResolveIntakeDispatchInput,\n ResolvedIntakeDispatch,\n UpdateIntakeIssueInput,\n} from '../../capabilities/intake.js';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';\nimport { IssueReconcileWorker } from '../issue-reconcile-worker.js';\nimport { adfToText } from './adf.js';\nimport { buildJiraAgentTools } from './agent-tools.js';\nimport type { JiraComment, JiraIssue, JiraTransition } from './api.js';\nimport { JiraApiClient, JiraApiError } from './api.js';\nimport type { JiraEventRules, JiraRuleOverrides } from './default-rules.js';\nimport { resolveJiraRules } from './default-rules.js';\nimport { attachJiraIssueReconciler } from './issue-reconciler.js';\nimport { jiraReconciliationEnabled, jiraReconciliationInterval } from './reconciliation-config.js';\nimport { buildJiraRoutes } from './routes.js';\nimport { attachJiraRules } from './rules.js';\n\n/** Deployment-global Jira Cloud credentials. All credential fields are required. */\nexport interface JiraIntegrationConfig {\n /** Site base URL, e.g. `https://acme.atlassian.net`, used for browser links. */\n baseUrl: string;\n /** API base URL for scoped tokens. Defaults to `baseUrl`. */\n apiBaseUrl?: string;\n /** Atlassian account email the API token belongs to. */\n email: string;\n /** API token from id.atlassian.com → Security → API tokens. */\n apiToken: string;\n /** Per-event replacements; omitted events retain defaults, null disables. */\n rules?: JiraRuleOverrides;\n}\n\n/** Hard stop for comment pagination so a misbehaving `total` can't loop forever. */\nconst ISSUE_COMMENTS_MAX_PAGES = 20;\n\n/** Hard stop for project pagination so a misbehaving `isLast` can't loop forever. */\nconst PROJECT_SEARCH_MAX_PAGES = 50;\n\n/**\n * The `Intake` contract requires a connection argument, but Jira credentials\n * live on the integration instance — this inert placeholder is accepted and\n * ignored everywhere, and keeps the real API token out of resolved dispatches\n * and agent-tool calls.\n */\nexport const DEPLOYMENT_CONNECTION: IntegrationConnection = { type: 'oauth', accessToken: 'deployment-global' };\n\n/** Jira issue keys look like `ENG-42`. */\nconst ISSUE_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*-\\d+$/;\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nfunction normalizeSiteBaseUrl(baseUrl: string): string {\n let parsed: URL;\n try {\n parsed = new URL(baseUrl);\n } catch {\n throw new Error(`JiraIntegration baseUrl is not an absolute URL: \"${baseUrl}\".`);\n }\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new Error(`JiraIntegration baseUrl must be an http(s) URL: \"${baseUrl}\".`);\n }\n return baseUrl.replace(/\\/+$/, '');\n}\n\n/**\n * Capability state types (`capabilities/intake.ts`) → Jira status-category\n * keys. `canceled` has no Jira category — it is resolved separately against\n * `done`-category transitions whose status name contains \"cancel\".\n */\nconst STATE_TYPE_TO_CATEGORY: Record<'unstarted' | 'started' | 'completed', string> = {\n unstarted: 'new',\n started: 'indeterminate',\n completed: 'done',\n};\n\n/** Jira status-category keys → capability state types. */\nfunction stateTypeFromCategory(key: string | undefined): string | null {\n switch (key) {\n case 'new':\n return 'unstarted';\n case 'indeterminate':\n return 'started';\n case 'done':\n return 'completed';\n default:\n return null;\n }\n}\n\nexport class JiraIntegration implements FactoryIntegration {\n /** Stable integration identifier (see `../base.ts`). */\n readonly id = 'jira';\n\n readonly #config: JiraIntegrationConfig;\n readonly #baseUrl: string;\n /** Typed REST client bound to the deployment credentials. */\n readonly api: JiraApiClient;\n\n /** Bound once by the factory via `initialize()` before any surface is used. */\n #projects: FactoryProjectsStorage | undefined;\n #auth: RouteAuth | undefined;\n readonly #orgIdByResourceId = new Map<string, string | null>();\n\n readonly #rules: JiraEventRules;\n\n get rules(): JiraEventRules {\n return this.#rules;\n }\n\n constructor(config: JiraIntegrationConfig) {\n this.#rules = resolveJiraRules(config.rules);\n this.#baseUrl = normalizeSiteBaseUrl(config.baseUrl);\n // JiraApiClient validates the credential group and normalizes the API URL.\n this.api = new JiraApiClient({ ...config, baseUrl: config.apiBaseUrl ?? config.baseUrl });\n this.#config = config;\n }\n\n /**\n * Bind the projects domain and the host auth seam. Jira has no per-org\n * connection rows, so the generic storage handle is unused — credentials\n * are deployment-global constructor config.\n */\n initialize({ projects, auth }: { projects: FactoryProjectsStorage; auth: RouteAuth }): void {\n this.#projects = projects;\n this.#auth = auth;\n }\n\n /** Factory projects domain — maps a session's resourceId to its owning org. */\n get projects(): FactoryProjectsStorage {\n if (!this.#projects) {\n throw new Error('JiraIntegration is not initialized — the factory binds storage during prepare().');\n }\n return this.#projects;\n }\n\n /**\n * Whether the host runs with web auth enabled. Intake selections are\n * org-owned, so every Jira surface is inert without a tenant auth seam.\n */\n get authEnabled(): boolean {\n return this.#auth?.enabled() ?? false;\n }\n\n /**\n * Map a session's resourceId (the factory project id) to its owning org.\n * Same caching semantics as the Linear integration: definitive misses are\n * cached, transient database failures are not.\n */\n async resolveOrgId(resourceId: string): Promise<string | null> {\n const cached = this.#orgIdByResourceId.get(resourceId);\n if (cached !== undefined) return cached;\n // Non-UUID resource ids (local/dev resources) would make the uuid column\n // comparison throw — they're definitively \"not a project\", so cache that.\n if (!UUID_PATTERN.test(resourceId)) {\n this.#orgIdByResourceId.set(resourceId, null);\n return null;\n }\n let orgId: string | null;\n try {\n await this.projects.ensureReady();\n const project = await this.projects.getById({ id: resourceId });\n orgId = project?.orgId ?? null;\n } catch {\n // Transient database failure: skip the tools for this request but don't\n // cache the miss, so the next request retries the lookup.\n return null;\n }\n this.#orgIdByResourceId.set(resourceId, orgId);\n return orgId;\n }\n\n /** Test hook: clear the org cache between specs. */\n clearCaches(): void {\n this.#orgIdByResourceId.clear();\n }\n\n /** Normalized site base URL (no trailing slash) — `/browse/<key>` links hang off this. */\n get baseUrl(): string {\n return this.#baseUrl;\n }\n\n readonly intake: Intake = {\n resolveIntakeDispatch: input => this.#resolveIntakeDispatch(input),\n listSources: () => this.#listSources(),\n listItems: input => this.#listItems(input),\n listIssues: input => this.#listIssues(input),\n getIssue: input => this.#getIssue(input),\n createComment: input => this.#createComment(input),\n updateIssue: input => this.#updateIssue(input),\n };\n\n /**\n * Background-dispatch context. Credentials are deployment-global, so the\n * returned connection is a formality the Jira intake methods ignore; the\n * issue key is stored directly as the work item's `externalId` (mirroring\n * Linear, which stores the issue UUID).\n */\n async #resolveIntakeDispatch({ externalSource }: ResolveIntakeDispatchInput): Promise<ResolvedIntakeDispatch | null> {\n if (externalSource.type !== 'issue') return null;\n return {\n // Inert placeholder, never the real token: the Jira intake methods read\n // credentials from the client, and the resolved dispatch travels through\n // generic reconciler/dispatcher code that must stay secret-free.\n connection: DEPLOYMENT_CONNECTION,\n issueId: externalSource.externalId,\n };\n }\n\n /** All the site's projects (Settings intake-source picker), following `isLast` paging. */\n async #listSources(): Promise<IntakeSource[]> {\n const sources: IntakeSource[] = [];\n let startAt = 0;\n for (let pageCount = 0; pageCount < PROJECT_SEARCH_MAX_PAGES; pageCount++) {\n const page = await this.api.listProjects({ startAt });\n for (const project of page.values) {\n sources.push({\n id: project.id,\n name: project.name,\n type: 'project',\n metadata: { key: project.key },\n });\n }\n if (page.isLast || page.values.length === 0) break;\n startAt += page.values.length;\n }\n return sources;\n }\n\n async #listItems({ sourceIds, cursor }: ListIntakeItemsInput): Promise<IntakeItemPage> {\n if (sourceIds.length === 0) return { items: [], nextCursor: null };\n const page = await this.listActiveIssues(cursor, sourceIds);\n return {\n items: page.issues.map(issue => ({\n source: { type: 'issue', externalId: issue.identifier, url: issue.url },\n sourceId: issue.sourceId,\n title: `${issue.identifier}: ${issue.title}`,\n status: issue.state ?? undefined,\n labels: issue.labels,\n assignee: issue.assignee,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n metadata: {\n identifier: issue.identifier,\n issueRef: issue.identifier,\n state: issue.state,\n stateType: issue.stateType,\n priority: issue.priority,\n project: issue.source,\n assignee: issue.assignee,\n assignees: issue.assignees ?? [],\n creator: issue.author,\n author: issue.author,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n },\n })),\n nextCursor: page.nextCursor,\n };\n }\n\n async #listIssues(input: ListIntakeIssuesInput): Promise<{ issues: IntakeIssue[]; nextCursor: string | null }> {\n const page = await this.listActiveIssues(input.cursor, input.sourceIds, input.labels);\n return { issues: page.issues, nextCursor: page.nextCursor };\n }\n\n /**\n * One page of active issues (status category not Done), most recently\n * updated first, optionally filtered to projects and labels. The\n * capability cursor is Jira's opaque `nextPageToken` passed through.\n */\n async listActiveIssues(\n cursor?: string,\n projectIds?: string[],\n labels?: string[],\n ): Promise<{ issues: Array<IntakeIssue & { sourceId: string }>; nextCursor: string | null }> {\n const page = await this.api.searchIssues({\n jql: buildIntakeJql(projectIds, labels),\n ...(cursor ? { nextPageToken: cursor } : {}),\n });\n return {\n issues: page.issues.map(issue => ({\n ...this.#toIntakeIssue(issue),\n sourceId: issue.fields.project?.id ?? '',\n })),\n nextCursor: page.nextPageToken ?? null,\n };\n }\n\n async #getIssue({ issueId }: GetIntakeIssueInput): Promise<IntakeIssueDetail | null> {\n let issue: JiraIssue;\n try {\n issue = await this.api.getIssue(issueId);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n const { comments, total } = await this.#listAllComments(issue.key);\n return {\n ...this.#toIntakeIssue(issue),\n commentCount: total,\n description: issue.fields.description ? adfToText(issue.fields.description) || null : null,\n comments: comments.map(comment => ({\n author: comment.author?.displayName ?? null,\n body: adfToText(comment.body),\n createdAt: comment.created,\n })),\n };\n }\n\n /** Every comment on the issue, oldest first, bounded by {@link ISSUE_COMMENTS_MAX_PAGES}. */\n async #listAllComments(keyOrId: string): Promise<{ comments: JiraComment[]; total: number }> {\n const comments: JiraComment[] = [];\n let total = 0;\n for (let page = 0; page < ISSUE_COMMENTS_MAX_PAGES; page++) {\n const result = await this.api.listComments(keyOrId, { startAt: comments.length });\n comments.push(...result.comments);\n total = result.total;\n if (result.comments.length === 0 || comments.length >= total) break;\n }\n return { comments, total };\n }\n\n async #createComment({ issueId, body }: CreateIntakeCommentInput): Promise<CreatedIntakeComment | null> {\n // The browse URL needs the issue key; resolve it when the caller passed a\n // numeric id instead.\n let key = issueId;\n if (!ISSUE_KEY_PATTERN.test(issueId)) {\n try {\n key = (await this.api.getIssue(issueId)).key;\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n }\n let comment;\n try {\n comment = await this.api.createComment(key, body);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n return {\n id: comment.id,\n url: `${this.baseUrl}/browse/${key}?focusedCommentId=${comment.id}`,\n };\n }\n\n /**\n * Move an issue to a target state by applying a workflow transition. Jira\n * only exposes transitions that are currently legal, so a policy miss (no\n * transition reaches the target) returns `null` per the `Intake` contract —\n * infrastructure errors (network, auth) still throw.\n */\n async #updateIssue(input: UpdateIntakeIssueInput): Promise<IntakeIssue | null> {\n let issue: JiraIssue;\n try {\n issue = await this.api.getIssue(input.issueId);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n if (currentStatusMatches(issue, input.state)) {\n return this.#toIntakeIssue(issue);\n }\n const transitions = await this.api.listTransitions(issue.key);\n const transition = resolveTransition(transitions, input.state);\n if (!transition) return null;\n await this.api.applyTransition(issue.key, transition.id);\n const fresh = await this.api.getIssue(issue.key);\n return this.#toIntakeIssue(fresh);\n }\n\n #toIntakeIssue(issue: JiraIssue): IntakeIssue {\n const { fields } = issue;\n return {\n id: issue.id,\n identifier: issue.key,\n title: fields.summary ?? issue.key,\n url: `${this.baseUrl}/browse/${issue.key}`,\n author: fields.reporter?.displayName ?? null,\n state: fields.status?.name ?? null,\n stateType: stateTypeFromCategory(fields.status?.statusCategory?.key),\n priority: fields.priority?.name ?? null,\n assignee: fields.assignee?.displayName ?? null,\n source: fields.project?.key ?? null,\n sourceId: fields.project?.id ?? null,\n labels: fields.labels ?? [],\n commentCount: null,\n createdAt: fields.created ?? '',\n updatedAt: fields.updated ?? '',\n };\n }\n\n // ── FactoryIntegration surface ───────────────────────────────────────────\n\n /**\n * The integration's HTTP surface: `/web/jira/*` Mastra `apiRoutes` (status,\n * projects + issues for Intake). No OAuth routes — credentials are\n * deployment-global. Handlers operate on this instance.\n */\n routes(ctx: IntegrationContext): ApiRoute[] {\n return buildJiraRoutes({\n jira: this,\n auth: ctx.auth,\n intake: ctx.storage.intake,\n ingestFactoryIssues: attachJiraRules(this, ctx),\n });\n }\n\n /**\n * Background reconciliation, matching the Platform integration: keeps filed\n * Jira cards' metadata fresh and replays close events through the rules\n * ingress. Registered only when reconciliation is enabled.\n */\n workers(ctx: IntegrationContext): MastraWorker[] {\n if (!jiraReconciliationEnabled()) return [];\n const reconcile = attachJiraIssueReconciler(this, ctx);\n if (!reconcile) return [];\n const intervalMs = jiraReconciliationInterval();\n return [\n new IssueReconcileWorker({\n integrationId: this.id,\n reconcile,\n ...(intervalMs ? { intervalMs } : {}),\n }),\n ];\n }\n\n /**\n * Org-scoped agent tools: the read-only issue-detail tool for sessions whose\n * resource is a factory project (Jira credentials are deployment-global,\n * so no per-org connection gates apply). v1 exposes no mutating Jira tools.\n */\n async agentTools(args: { requestContext: RequestContext }): Promise<IntegrationTools> {\n return buildJiraAgentTools({ requestContext: args.requestContext, jira: this });\n }\n\n /** Non-secret config snapshot for system diagnostics/startup logs. */\n diagnostics(): Record<string, unknown> {\n return {\n configured: true,\n site: new URL(this.baseUrl).host,\n email: maskEmail(this.#config.email),\n };\n }\n}\n\n/** `ops@acme.test` → `o***@acme.test` — enough to identify, never the value. */\nfunction maskEmail(email: string): string {\n const at = email.indexOf('@');\n if (at <= 0) return '***';\n return `${email[0]}***${email.slice(at)}`;\n}\n\n/**\n * Build the intake JQL. Source ids and labels come from stored org config /\n * request input, so both are sanitized before interpolation: numeric project\n * ids pass through bare, project keys are restricted to a safe charset, and\n * label quotes/backslashes are stripped.\n */\nfunction buildIntakeJql(projectIds?: string[], labels?: string[]): string {\n const clauses: string[] = [];\n const safeProjects = (projectIds ?? [])\n .map(id => id.trim())\n .filter(id => /^[A-Za-z0-9_]+$/.test(id))\n .map(id => (/^\\d+$/.test(id) ? id : `\"${id}\"`));\n if (safeProjects.length > 0) {\n clauses.push(`project IN (${safeProjects.join(', ')})`);\n }\n clauses.push('statusCategory != Done');\n const safeLabels = [...new Set((labels ?? []).map(label => label.trim().replace(/[\"\\\\]/g, '')).filter(Boolean))];\n if (safeLabels.length > 0) {\n clauses.push(`labels IN (${safeLabels.map(label => `\"${label}\"`).join(', ')})`);\n }\n return `${clauses.join(' AND ')} ORDER BY updated DESC`;\n}\n\n/** Whether the issue is already in the requested target state. */\nfunction currentStatusMatches(issue: JiraIssue, target: UpdateIntakeIssueInput['state']): boolean {\n const status = issue.fields.status;\n if (!status) return false;\n if (target.kind === 'byName') {\n return status.name.toLowerCase() === target.name.toLowerCase();\n }\n if (target.stateType === 'canceled') {\n return status.statusCategory?.key === 'done' && /cancel/i.test(status.name);\n }\n return status.statusCategory?.key === STATE_TYPE_TO_CATEGORY[target.stateType];\n}\n\n/** Resolve the target state against the issue's currently-legal transitions. */\nfunction resolveTransition(\n transitions: JiraTransition[],\n target: UpdateIntakeIssueInput['state'],\n): JiraTransition | null {\n if (target.kind === 'byName') {\n const wanted = target.name.toLowerCase();\n return transitions.find(transition => transition.to?.name.toLowerCase() === wanted) ?? null;\n }\n if (target.stateType === 'canceled') {\n return (\n transitions.find(\n transition => transition.to?.statusCategory?.key === 'done' && /cancel/i.test(transition.to.name),\n ) ?? null\n );\n }\n const category = STATE_TYPE_TO_CATEGORY[target.stateType];\n return transitions.find(transition => transition.to?.statusCategory?.key === category) ?? null;\n}\n"],"mappings":";;;;;;;;;;;AAmEA,MAAM,2BAA2B;;AAGjC,MAAM,2BAA2B;;;;;;;AAQjC,MAAa,wBAA+C;CAAE,MAAM;CAAS,aAAa;AAAoB;;AAG9G,MAAM,oBAAoB;AAE1B,MAAM,eAAe;AAErB,SAAS,qBAAqB,SAAyB;CACrD,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,OAAO;CAC1B,QAAQ;EACN,MAAM,IAAI,MAAM,oDAAoD,QAAQ,GAAG;CACjF;CACA,IAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SACtD,MAAM,IAAI,MAAM,oDAAoD,QAAQ,GAAG;CAEjF,OAAO,QAAQ,QAAQ,QAAQ,EAAE;AACnC;;;;;;AAOA,MAAM,yBAAgF;CACpF,WAAW;CACX,SAAS;CACT,WAAW;AACb;;AAGA,SAAS,sBAAsB,KAAwC;CACrE,QAAQ,KAAR;EACE,KAAK,OACH,OAAO;EACT,KAAK,iBACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,SACE,OAAO;CACX;AACF;AAEA,IAAa,kBAAb,MAA2D;;CAEzD,KAAc;CAEd;CACA;;CAEA;;CAGA;CACA;CACA,qCAA8B,IAAI,IAA2B;CAE7D;CAEA,IAAI,QAAwB;EAC1B,OAAO,KAAK;CACd;CAEA,YAAY,QAA+B;EACzC,KAAK,SAAS,iBAAiB,OAAO,KAAK;EAC3C,KAAK,WAAW,qBAAqB,OAAO,OAAO;EAEnD,KAAK,MAAM,IAAI,cAAc;GAAE,GAAG;GAAQ,SAAS,OAAO,cAAc,OAAO;EAAQ,CAAC;EACxF,KAAK,UAAU;CACjB;;;;;;CAOA,WAAW,EAAE,UAAU,QAAqE;EAC1F,KAAK,YAAY;EACjB,KAAK,QAAQ;CACf;;CAGA,IAAI,WAAmC;EACrC,IAAI,CAAC,KAAK,WACR,MAAM,IAAI,MAAM,kFAAkF;EAEpG,OAAO,KAAK;CACd;;;;;CAMA,IAAI,cAAuB;EACzB,OAAO,KAAK,OAAO,QAAQ,KAAK;CAClC;;;;;;CAOA,MAAM,aAAa,YAA4C;EAC7D,MAAM,SAAS,KAAK,mBAAmB,IAAI,UAAU;EACrD,IAAI,WAAW,KAAA,GAAW,OAAO;EAGjC,IAAI,CAAC,aAAa,KAAK,UAAU,GAAG;GAClC,KAAK,mBAAmB,IAAI,YAAY,IAAI;GAC5C,OAAO;EACT;EACA,IAAI;EACJ,IAAI;GACF,MAAM,KAAK,SAAS,YAAY;GAEhC,SAAQ,MADc,KAAK,SAAS,QAAQ,EAAE,IAAI,WAAW,CAAC,EAAA,EAC7C,SAAS;EAC5B,QAAQ;GAGN,OAAO;EACT;EACA,KAAK,mBAAmB,IAAI,YAAY,KAAK;EAC7C,OAAO;CACT;;CAGA,cAAoB;EAClB,KAAK,mBAAmB,MAAM;CAChC;;CAGA,IAAI,UAAkB;EACpB,OAAO,KAAK;CACd;CAEA,SAA0B;EACxB,wBAAuB,UAAS,KAAK,uBAAuB,KAAK;EACjE,mBAAmB,KAAK,aAAa;EACrC,YAAW,UAAS,KAAK,WAAW,KAAK;EACzC,aAAY,UAAS,KAAK,YAAY,KAAK;EAC3C,WAAU,UAAS,KAAK,UAAU,KAAK;EACvC,gBAAe,UAAS,KAAK,eAAe,KAAK;EACjD,cAAa,UAAS,KAAK,aAAa,KAAK;CAC/C;;;;;;;CAQA,MAAM,uBAAuB,EAAE,kBAAsF;EACnH,IAAI,eAAe,SAAS,SAAS,OAAO;EAC5C,OAAO;GAIL,YAAY;GACZ,SAAS,eAAe;EAC1B;CACF;;CAGA,MAAM,eAAwC;EAC5C,MAAM,UAA0B,CAAC;EACjC,IAAI,UAAU;EACd,KAAK,IAAI,YAAY,GAAG,YAAY,0BAA0B,aAAa;GACzE,MAAM,OAAO,MAAM,KAAK,IAAI,aAAa,EAAE,QAAQ,CAAC;GACpD,KAAK,MAAM,WAAW,KAAK,QACzB,QAAQ,KAAK;IACX,IAAI,QAAQ;IACZ,MAAM,QAAQ;IACd,MAAM;IACN,UAAU,EAAE,KAAK,QAAQ,IAAI;GAC/B,CAAC;GAEH,IAAI,KAAK,UAAU,KAAK,OAAO,WAAW,GAAG;GAC7C,WAAW,KAAK,OAAO;EACzB;EACA,OAAO;CACT;CAEA,MAAM,WAAW,EAAE,WAAW,UAAyD;EACrF,IAAI,UAAU,WAAW,GAAG,OAAO;GAAE,OAAO,CAAC;GAAG,YAAY;EAAK;EACjE,MAAM,OAAO,MAAM,KAAK,iBAAiB,QAAQ,SAAS;EAC1D,OAAO;GACL,OAAO,KAAK,OAAO,KAAI,WAAU;IAC/B,QAAQ;KAAE,MAAM;KAAS,YAAY,MAAM;KAAY,KAAK,MAAM;IAAI;IACtE,UAAU,MAAM;IAChB,OAAO,GAAG,MAAM,WAAW,IAAI,MAAM;IACrC,QAAQ,MAAM,SAAS,KAAA;IACvB,QAAQ,MAAM;IACd,UAAU,MAAM;IAChB,WAAW,MAAM;IACjB,WAAW,MAAM;IACjB,UAAU;KACR,YAAY,MAAM;KAClB,UAAU,MAAM;KAChB,OAAO,MAAM;KACb,WAAW,MAAM;KACjB,UAAU,MAAM;KAChB,SAAS,MAAM;KACf,UAAU,MAAM;KAChB,WAAW,MAAM,aAAa,CAAC;KAC/B,SAAS,MAAM;KACf,QAAQ,MAAM;KACd,QAAQ,MAAM;KACd,WAAW,MAAM;KACjB,WAAW,MAAM;IACnB;GACF,EAAE;GACF,YAAY,KAAK;EACnB;CACF;CAEA,MAAM,YAAY,OAA6F;EAC7G,MAAM,OAAO,MAAM,KAAK,iBAAiB,MAAM,QAAQ,MAAM,WAAW,MAAM,MAAM;EACpF,OAAO;GAAE,QAAQ,KAAK;GAAQ,YAAY,KAAK;EAAW;CAC5D;;;;;;CAOA,MAAM,iBACJ,QACA,YACA,QAC2F;EAC3F,MAAM,OAAO,MAAM,KAAK,IAAI,aAAa;GACvC,KAAK,eAAe,YAAY,MAAM;GACtC,GAAI,SAAS,EAAE,eAAe,OAAO,IAAI,CAAC;EAC5C,CAAC;EACD,OAAO;GACL,QAAQ,KAAK,OAAO,KAAI,WAAU;IAChC,GAAG,KAAK,eAAe,KAAK;IAC5B,UAAU,MAAM,OAAO,SAAS,MAAM;GACxC,EAAE;GACF,YAAY,KAAK,iBAAiB;EACpC;CACF;CAEA,MAAM,UAAU,EAAE,WAAmE;EACnF,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,KAAK,IAAI,SAAS,OAAO;EACzC,SAAS,KAAK;GACZ,IAAI,eAAe,gBAAgB,IAAI,WAAW,KAAK,OAAO;GAC9D,MAAM;EACR;EACA,MAAM,EAAE,UAAU,UAAU,MAAM,KAAK,iBAAiB,MAAM,GAAG;EACjE,OAAO;GACL,GAAG,KAAK,eAAe,KAAK;GAC5B,cAAc;GACd,aAAa,MAAM,OAAO,cAAc,UAAU,MAAM,OAAO,WAAW,KAAK,OAAO;GACtF,UAAU,SAAS,KAAI,aAAY;IACjC,QAAQ,QAAQ,QAAQ,eAAe;IACvC,MAAM,UAAU,QAAQ,IAAI;IAC5B,WAAW,QAAQ;GACrB,EAAE;EACJ;CACF;;CAGA,MAAM,iBAAiB,SAAsE;EAC3F,MAAM,WAA0B,CAAC;EACjC,IAAI,QAAQ;EACZ,KAAK,IAAI,OAAO,GAAG,OAAO,0BAA0B,QAAQ;GAC1D,MAAM,SAAS,MAAM,KAAK,IAAI,aAAa,SAAS,EAAE,SAAS,SAAS,OAAO,CAAC;GAChF,SAAS,KAAK,GAAG,OAAO,QAAQ;GAChC,QAAQ,OAAO;GACf,IAAI,OAAO,SAAS,WAAW,KAAK,SAAS,UAAU,OAAO;EAChE;EACA,OAAO;GAAE;GAAU;EAAM;CAC3B;CAEA,MAAM,eAAe,EAAE,SAAS,QAAwE;EAGtG,IAAI,MAAM;EACV,IAAI,CAAC,kBAAkB,KAAK,OAAO,GACjC,IAAI;GACF,OAAO,MAAM,KAAK,IAAI,SAAS,OAAO,EAAA,CAAG;EAC3C,SAAS,KAAK;GACZ,IAAI,eAAe,gBAAgB,IAAI,WAAW,KAAK,OAAO;GAC9D,MAAM;EACR;EAEF,IAAI;EACJ,IAAI;GACF,UAAU,MAAM,KAAK,IAAI,cAAc,KAAK,IAAI;EAClD,SAAS,KAAK;GACZ,IAAI,eAAe,gBAAgB,IAAI,WAAW,KAAK,OAAO;GAC9D,MAAM;EACR;EACA,OAAO;GACL,IAAI,QAAQ;GACZ,KAAK,GAAG,KAAK,QAAQ,UAAU,IAAI,oBAAoB,QAAQ;EACjE;CACF;;;;;;;CAQA,MAAM,aAAa,OAA4D;EAC7E,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,KAAK,IAAI,SAAS,MAAM,OAAO;EAC/C,SAAS,KAAK;GACZ,IAAI,eAAe,gBAAgB,IAAI,WAAW,KAAK,OAAO;GAC9D,MAAM;EACR;EACA,IAAI,qBAAqB,OAAO,MAAM,KAAK,GACzC,OAAO,KAAK,eAAe,KAAK;EAGlC,MAAM,aAAa,kBAAkB,MADX,KAAK,IAAI,gBAAgB,MAAM,GAAG,GACV,MAAM,KAAK;EAC7D,IAAI,CAAC,YAAY,OAAO;EACxB,MAAM,KAAK,IAAI,gBAAgB,MAAM,KAAK,WAAW,EAAE;EACvD,MAAM,QAAQ,MAAM,KAAK,IAAI,SAAS,MAAM,GAAG;EAC/C,OAAO,KAAK,eAAe,KAAK;CAClC;CAEA,eAAe,OAA+B;EAC5C,MAAM,EAAE,WAAW;EACnB,OAAO;GACL,IAAI,MAAM;GACV,YAAY,MAAM;GAClB,OAAO,OAAO,WAAW,MAAM;GAC/B,KAAK,GAAG,KAAK,QAAQ,UAAU,MAAM;GACrC,QAAQ,OAAO,UAAU,eAAe;GACxC,OAAO,OAAO,QAAQ,QAAQ;GAC9B,WAAW,sBAAsB,OAAO,QAAQ,gBAAgB,GAAG;GACnE,UAAU,OAAO,UAAU,QAAQ;GACnC,UAAU,OAAO,UAAU,eAAe;GAC1C,QAAQ,OAAO,SAAS,OAAO;GAC/B,UAAU,OAAO,SAAS,MAAM;GAChC,QAAQ,OAAO,UAAU,CAAC;GAC1B,cAAc;GACd,WAAW,OAAO,WAAW;GAC7B,WAAW,OAAO,WAAW;EAC/B;CACF;;;;;;CASA,OAAO,KAAqC;EAC1C,OAAO,gBAAgB;GACrB,MAAM;GACN,MAAM,IAAI;GACV,QAAQ,IAAI,QAAQ;GACpB,qBAAqB,gBAAgB,MAAM,GAAG;EAChD,CAAC;CACH;;;;;;CAOA,QAAQ,KAAyC;EAC/C,IAAI,CAAC,0BAA0B,GAAG,OAAO,CAAC;EAC1C,MAAM,YAAY,0BAA0B,MAAM,GAAG;EACrD,IAAI,CAAC,WAAW,OAAO,CAAC;EACxB,MAAM,aAAa,2BAA2B;EAC9C,OAAO,CACL,IAAI,qBAAqB;GACvB,eAAe,KAAK;GACpB;GACA,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;EACrC,CAAC,CACH;CACF;;;;;;CAOA,MAAM,WAAW,MAAqE;EACpF,OAAO,oBAAoB;GAAE,gBAAgB,KAAK;GAAgB,MAAM;EAAK,CAAC;CAChF;;CAGA,cAAuC;EACrC,OAAO;GACL,YAAY;GACZ,MAAM,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC;GAC5B,OAAO,UAAU,KAAK,QAAQ,KAAK;EACrC;CACF;AACF;;AAGA,SAAS,UAAU,OAAuB;CACxC,MAAM,KAAK,MAAM,QAAQ,GAAG;CAC5B,IAAI,MAAM,GAAG,OAAO;CACpB,OAAO,GAAG,MAAM,GAAG,KAAK,MAAM,MAAM,EAAE;AACxC;;;;;;;AAQA,SAAS,eAAe,YAAuB,QAA2B;CACxE,MAAM,UAAoB,CAAC;CAC3B,MAAM,gBAAgB,cAAc,CAAC,EAAA,CAClC,KAAI,OAAM,GAAG,KAAK,CAAC,CAAC,CACpB,QAAO,OAAM,kBAAkB,KAAK,EAAE,CAAC,CAAC,CACxC,KAAI,OAAO,QAAQ,KAAK,EAAE,IAAI,KAAK,IAAI,GAAG,EAAG;CAChD,IAAI,aAAa,SAAS,GACxB,QAAQ,KAAK,eAAe,aAAa,KAAK,IAAI,EAAE,EAAE;CAExD,QAAQ,KAAK,wBAAwB;CACrC,MAAM,aAAa,CAAC,GAAG,IAAI,KAAK,UAAU,CAAC,EAAA,CAAG,KAAI,UAAS,MAAM,KAAK,CAAC,CAAC,QAAQ,UAAU,EAAE,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC;CAC/G,IAAI,WAAW,SAAS,GACtB,QAAQ,KAAK,cAAc,WAAW,KAAI,UAAS,IAAI,MAAM,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;CAEhF,OAAO,GAAG,QAAQ,KAAK,OAAO,EAAE;AAClC;;AAGA,SAAS,qBAAqB,OAAkB,QAAkD;CAChG,MAAM,SAAS,MAAM,OAAO;CAC5B,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI,OAAO,SAAS,UAClB,OAAO,OAAO,KAAK,YAAY,MAAM,OAAO,KAAK,YAAY;CAE/D,IAAI,OAAO,cAAc,YACvB,OAAO,OAAO,gBAAgB,QAAQ,UAAU,UAAU,KAAK,OAAO,IAAI;CAE5E,OAAO,OAAO,gBAAgB,QAAQ,uBAAuB,OAAO;AACtE;;AAGA,SAAS,kBACP,aACA,QACuB;CACvB,IAAI,OAAO,SAAS,UAAU;EAC5B,MAAM,SAAS,OAAO,KAAK,YAAY;EACvC,OAAO,YAAY,MAAK,eAAc,WAAW,IAAI,KAAK,YAAY,MAAM,MAAM,KAAK;CACzF;CACA,IAAI,OAAO,cAAc,YACvB,OACE,YAAY,MACV,eAAc,WAAW,IAAI,gBAAgB,QAAQ,UAAU,UAAU,KAAK,WAAW,GAAG,IAAI,CAClG,KAAK;CAGT,MAAM,WAAW,uBAAuB,OAAO;CAC/C,OAAO,YAAY,MAAK,eAAc,WAAW,IAAI,gBAAgB,QAAQ,QAAQ,KAAK;AAC5F"}
|
|
1
|
+
{"version":3,"file":"integration.js","names":[],"sources":["../../../src/integrations/jira/integration.ts"],"sourcesContent":["/**\n * `JiraIntegration` — the self-contained Jira Cloud integration.\n *\n * Implements the system-wide `FactoryIntegration` contract\n * (`../base.ts`): the deploy entry reads the `JIRA_*` env vars ONCE,\n * constructs an instance with explicit credentials, and passes it to\n * `MastraFactory`. Everything Jira-flavored the system does — project/issue\n * reads for Intake and the agent's issue tools — flows through this\n * instance. No other module reads `JIRA_*` env vars.\n *\n * Unlike Linear, credentials are **deployment-global** (Basic auth with\n * `email:apiToken` from the environment): there is no OAuth flow, no state\n * signer, and no per-org connection storage. The `IntegrationConnection`\n * arguments required by the `Intake` contract are accepted but not consulted\n * — the instance's own credentials authenticate every call.\n */\n\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { MastraWorker } from '@mastra/core/worker';\n\nimport type { IntegrationConnection } from '../../capabilities/connection.js';\nimport type {\n CreateIntakeCommentInput,\n CreatedIntakeComment,\n GetIntakeIssueInput,\n Intake,\n IntakeIssue,\n IntakeIssueDetail,\n IntakeItemPage,\n IntakeSource,\n ListIntakeIssuesInput,\n ListIntakeItemsInput,\n ResolveIntakeDispatchInput,\n ResolvedIntakeDispatch,\n UpdateIntakeIssueInput,\n} from '../../capabilities/intake.js';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';\nimport { IssueReconcileWorker } from '../issue-reconcile-worker.js';\nimport { adfToText } from './adf.js';\nimport { buildJiraAgentTools } from './agent-tools.js';\nimport type { JiraComment, JiraIssue, JiraTransition } from './api.js';\nimport { JiraApiClient, JiraApiError } from './api.js';\nimport type { JiraEventRules, JiraRuleOverrides } from './default-rules.js';\nimport { resolveJiraRules } from './default-rules.js';\nimport { buildIntakeJql, unfilteredJiraScopes } from './intake-jql.js';\nimport type { JiraIntakeScope } from './intake-jql.js';\nimport { attachJiraIssueReconciler } from './issue-reconciler.js';\nimport { jiraReconciliationEnabled, jiraReconciliationInterval } from './reconciliation-config.js';\nimport { buildJiraRoutes } from './routes.js';\nimport { attachJiraRules } from './rules.js';\n\n/** Deployment-global Jira Cloud credentials. All credential fields are required. */\nexport interface JiraIntegrationConfig {\n /** Site base URL, e.g. `https://acme.atlassian.net`, used for browser links. */\n baseUrl: string;\n /** API base URL for scoped tokens. Defaults to `baseUrl`. */\n apiBaseUrl?: string;\n /** Atlassian account email the API token belongs to. */\n email: string;\n /** API token from id.atlassian.com → Security → API tokens. */\n apiToken: string;\n /** Per-event replacements; omitted events retain defaults, null disables. */\n rules?: JiraRuleOverrides;\n}\n\n/** Hard stop for comment pagination so a misbehaving `total` can't loop forever. */\nconst ISSUE_COMMENTS_MAX_PAGES = 20;\n\n/** Hard stop for project pagination so a misbehaving `isLast` can't loop forever. */\nconst PROJECT_SEARCH_MAX_PAGES = 50;\n\n/**\n * The `Intake` contract requires a connection argument, but Jira credentials\n * live on the integration instance — this inert placeholder is accepted and\n * ignored everywhere, and keeps the real API token out of resolved dispatches\n * and agent-tool calls.\n */\nexport const DEPLOYMENT_CONNECTION: IntegrationConnection = { type: 'oauth', accessToken: 'deployment-global' };\n\n/** Jira issue keys look like `ENG-42`. */\nconst ISSUE_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*-\\d+$/;\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nfunction normalizeSiteBaseUrl(baseUrl: string): string {\n let parsed: URL;\n try {\n parsed = new URL(baseUrl);\n } catch {\n throw new Error(`JiraIntegration baseUrl is not an absolute URL: \"${baseUrl}\".`);\n }\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new Error(`JiraIntegration baseUrl must be an http(s) URL: \"${baseUrl}\".`);\n }\n return baseUrl.replace(/\\/+$/, '');\n}\n\n/**\n * Capability state types (`capabilities/intake.ts`) → Jira status-category\n * keys. `canceled` has no Jira category — it is resolved separately against\n * `done`-category transitions whose status name contains \"cancel\".\n */\nconst STATE_TYPE_TO_CATEGORY: Record<'unstarted' | 'started' | 'completed', string> = {\n unstarted: 'new',\n started: 'indeterminate',\n completed: 'done',\n};\n\n/** Jira status-category keys → capability state types. */\nfunction stateTypeFromCategory(key: string | undefined): string | null {\n switch (key) {\n case 'new':\n return 'unstarted';\n case 'indeterminate':\n return 'started';\n case 'done':\n return 'completed';\n default:\n return null;\n }\n}\n\nexport class JiraIntegration implements FactoryIntegration {\n /** Stable integration identifier (see `../base.ts`). */\n readonly id = 'jira';\n\n readonly #config: JiraIntegrationConfig;\n readonly #baseUrl: string;\n /** Typed REST client bound to the deployment credentials. */\n readonly api: JiraApiClient;\n\n /** Bound once by the factory via `initialize()` before any surface is used. */\n #projects: FactoryProjectsStorage | undefined;\n #auth: RouteAuth | undefined;\n readonly #orgIdByResourceId = new Map<string, string | null>();\n\n readonly #rules: JiraEventRules;\n\n get rules(): JiraEventRules {\n return this.#rules;\n }\n\n constructor(config: JiraIntegrationConfig) {\n this.#rules = resolveJiraRules(config.rules);\n this.#baseUrl = normalizeSiteBaseUrl(config.baseUrl);\n // JiraApiClient validates the credential group and normalizes the API URL.\n this.api = new JiraApiClient({ ...config, baseUrl: config.apiBaseUrl ?? config.baseUrl });\n this.#config = config;\n }\n\n /**\n * Bind the projects domain and the host auth seam. Jira has no per-org\n * connection rows, so the generic storage handle is unused — credentials\n * are deployment-global constructor config.\n */\n initialize({ projects, auth }: { projects: FactoryProjectsStorage; auth: RouteAuth }): void {\n this.#projects = projects;\n this.#auth = auth;\n }\n\n /** Factory projects domain — maps a session's resourceId to its owning org. */\n get projects(): FactoryProjectsStorage {\n if (!this.#projects) {\n throw new Error('JiraIntegration is not initialized — the factory binds storage during prepare().');\n }\n return this.#projects;\n }\n\n /**\n * Whether the host runs with web auth enabled. Intake selections are\n * org-owned, so every Jira surface is inert without a tenant auth seam.\n */\n get authEnabled(): boolean {\n return this.#auth?.enabled() ?? false;\n }\n\n /**\n * Map a session's resourceId (the factory project id) to its owning org.\n * Same caching semantics as the Linear integration: definitive misses are\n * cached, transient database failures are not.\n */\n async resolveOrgId(resourceId: string): Promise<string | null> {\n const cached = this.#orgIdByResourceId.get(resourceId);\n if (cached !== undefined) return cached;\n // Non-UUID resource ids (local/dev resources) would make the uuid column\n // comparison throw — they're definitively \"not a project\", so cache that.\n if (!UUID_PATTERN.test(resourceId)) {\n this.#orgIdByResourceId.set(resourceId, null);\n return null;\n }\n let orgId: string | null;\n try {\n await this.projects.ensureReady();\n const project = await this.projects.getById({ id: resourceId });\n orgId = project?.orgId ?? null;\n } catch {\n // Transient database failure: skip the tools for this request but don't\n // cache the miss, so the next request retries the lookup.\n return null;\n }\n this.#orgIdByResourceId.set(resourceId, orgId);\n return orgId;\n }\n\n /** Test hook: clear the org cache between specs. */\n clearCaches(): void {\n this.#orgIdByResourceId.clear();\n }\n\n /** Normalized site base URL (no trailing slash) — `/browse/<key>` links hang off this. */\n get baseUrl(): string {\n return this.#baseUrl;\n }\n\n readonly intake: Intake = {\n bindingLabelFilters: true,\n resolveIntakeDispatch: input => this.#resolveIntakeDispatch(input),\n listSources: () => this.#listSources(),\n listItems: input => this.#listItems(input),\n listIssues: input => this.#listIssues(input),\n getIssue: input => this.#getIssue(input),\n createComment: input => this.#createComment(input),\n updateIssue: input => this.#updateIssue(input),\n };\n\n /**\n * Background-dispatch context. Credentials are deployment-global, so the\n * returned connection is a formality the Jira intake methods ignore; the\n * issue key is stored directly as the work item's `externalId` (mirroring\n * Linear, which stores the issue UUID).\n */\n async #resolveIntakeDispatch({ externalSource }: ResolveIntakeDispatchInput): Promise<ResolvedIntakeDispatch | null> {\n if (externalSource.type !== 'issue') return null;\n return {\n // Inert placeholder, never the real token: the Jira intake methods read\n // credentials from the client, and the resolved dispatch travels through\n // generic reconciler/dispatcher code that must stay secret-free.\n connection: DEPLOYMENT_CONNECTION,\n issueId: externalSource.externalId,\n };\n }\n\n /** All the site's projects (Settings intake-source picker), following `isLast` paging. */\n async #listSources(): Promise<IntakeSource[]> {\n const sources: IntakeSource[] = [];\n let startAt = 0;\n for (let pageCount = 0; pageCount < PROJECT_SEARCH_MAX_PAGES; pageCount++) {\n const page = await this.api.listProjects({ startAt });\n for (const project of page.values) {\n sources.push({\n id: project.id,\n name: project.name,\n type: 'project',\n metadata: { key: project.key },\n });\n }\n if (page.isLast || page.values.length === 0) break;\n startAt += page.values.length;\n }\n return sources;\n }\n\n async #listItems({ sourceIds, cursor }: ListIntakeItemsInput): Promise<IntakeItemPage> {\n if (sourceIds.length === 0) return { items: [], nextCursor: null };\n const page = await this.listActiveIssues(cursor, unfilteredJiraScopes(sourceIds));\n return {\n items: page.issues.map(issue => ({\n source: { type: 'issue', externalId: issue.identifier, url: issue.url },\n sourceId: issue.sourceId,\n title: `${issue.identifier}: ${issue.title}`,\n status: issue.state ?? undefined,\n labels: issue.labels,\n assignee: issue.assignee,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n metadata: {\n identifier: issue.identifier,\n issueRef: issue.identifier,\n state: issue.state,\n stateType: issue.stateType,\n priority: issue.priority,\n project: issue.source,\n assignee: issue.assignee,\n assignees: issue.assignees ?? [],\n creator: issue.author,\n author: issue.author,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n },\n })),\n nextCursor: page.nextCursor,\n };\n }\n\n async #listIssues(input: ListIntakeIssuesInput): Promise<{ issues: IntakeIssue[]; nextCursor: string | null }> {\n const page = await this.listActiveIssues(\n input.cursor,\n input.sourceIds ? unfilteredJiraScopes(input.sourceIds) : undefined,\n input.labels,\n );\n return { issues: page.issues, nextCursor: page.nextCursor };\n }\n\n /**\n * One page of active issues (status category not Done), most recently\n * updated first, optionally narrowed to the given project scopes and labels.\n * The capability cursor is Jira's opaque `nextPageToken` passed through.\n */\n async listActiveIssues(\n cursor?: string,\n scopes?: JiraIntakeScope[],\n labels?: string[],\n ): Promise<{ issues: Array<IntakeIssue & { sourceId: string }>; nextCursor: string | null }> {\n const jql = buildIntakeJql(scopes, labels);\n if (jql === null) return { issues: [], nextCursor: null };\n const page = await this.api.searchIssues({\n jql,\n ...(cursor ? { nextPageToken: cursor } : {}),\n });\n return {\n issues: page.issues.map(issue => ({\n ...this.#toIntakeIssue(issue),\n sourceId: issue.fields.project?.id ?? '',\n })),\n nextCursor: page.nextPageToken ?? null,\n };\n }\n\n async #getIssue({ issueId }: GetIntakeIssueInput): Promise<IntakeIssueDetail | null> {\n let issue: JiraIssue;\n try {\n issue = await this.api.getIssue(issueId);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n const { comments, total } = await this.#listAllComments(issue.key);\n return {\n ...this.#toIntakeIssue(issue),\n commentCount: total,\n description: issue.fields.description ? adfToText(issue.fields.description) || null : null,\n comments: comments.map(comment => ({\n author: comment.author?.displayName ?? null,\n body: adfToText(comment.body),\n createdAt: comment.created,\n })),\n };\n }\n\n /** Every comment on the issue, oldest first, bounded by {@link ISSUE_COMMENTS_MAX_PAGES}. */\n async #listAllComments(keyOrId: string): Promise<{ comments: JiraComment[]; total: number }> {\n const comments: JiraComment[] = [];\n let total = 0;\n for (let page = 0; page < ISSUE_COMMENTS_MAX_PAGES; page++) {\n const result = await this.api.listComments(keyOrId, { startAt: comments.length });\n comments.push(...result.comments);\n total = result.total;\n if (result.comments.length === 0 || comments.length >= total) break;\n }\n return { comments, total };\n }\n\n async #createComment({ issueId, body }: CreateIntakeCommentInput): Promise<CreatedIntakeComment | null> {\n // The browse URL needs the issue key; resolve it when the caller passed a\n // numeric id instead.\n let key = issueId;\n if (!ISSUE_KEY_PATTERN.test(issueId)) {\n try {\n key = (await this.api.getIssue(issueId)).key;\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n }\n let comment;\n try {\n comment = await this.api.createComment(key, body);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n return {\n id: comment.id,\n url: `${this.baseUrl}/browse/${key}?focusedCommentId=${comment.id}`,\n };\n }\n\n /**\n * Move an issue to a target state by applying a workflow transition. Jira\n * only exposes transitions that are currently legal, so a policy miss (no\n * transition reaches the target) returns `null` per the `Intake` contract —\n * infrastructure errors (network, auth) still throw.\n */\n async #updateIssue(input: UpdateIntakeIssueInput): Promise<IntakeIssue | null> {\n let issue: JiraIssue;\n try {\n issue = await this.api.getIssue(input.issueId);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n if (currentStatusMatches(issue, input.state)) {\n return this.#toIntakeIssue(issue);\n }\n const transitions = await this.api.listTransitions(issue.key);\n const transition = resolveTransition(transitions, input.state);\n if (!transition) return null;\n await this.api.applyTransition(issue.key, transition.id);\n const fresh = await this.api.getIssue(issue.key);\n return this.#toIntakeIssue(fresh);\n }\n\n #toIntakeIssue(issue: JiraIssue): IntakeIssue {\n const { fields } = issue;\n return {\n id: issue.id,\n identifier: issue.key,\n title: fields.summary ?? issue.key,\n url: `${this.baseUrl}/browse/${issue.key}`,\n author: fields.reporter?.displayName ?? null,\n state: fields.status?.name ?? null,\n stateType: stateTypeFromCategory(fields.status?.statusCategory?.key),\n priority: fields.priority?.name ?? null,\n assignee: fields.assignee?.displayName ?? null,\n source: fields.project?.key ?? null,\n sourceId: fields.project?.id ?? null,\n labels: fields.labels ?? [],\n commentCount: null,\n createdAt: fields.created ?? '',\n updatedAt: fields.updated ?? '',\n };\n }\n\n // ── FactoryIntegration surface ───────────────────────────────────────────\n\n /**\n * The integration's HTTP surface: `/web/jira/*` Mastra `apiRoutes` (status,\n * projects + issues for Intake). No OAuth routes — credentials are\n * deployment-global. Handlers operate on this instance.\n */\n routes(ctx: IntegrationContext): ApiRoute[] {\n return buildJiraRoutes({\n jira: this,\n auth: ctx.auth,\n intake: ctx.storage.intake,\n workItems: ctx.runtime?.workItems,\n ingestFactoryIssues: attachJiraRules(this, ctx),\n });\n }\n\n /**\n * Background reconciliation, matching the Platform integration: keeps filed\n * Jira cards' metadata fresh and replays close events through the rules\n * ingress. Registered only when reconciliation is enabled.\n */\n workers(ctx: IntegrationContext): MastraWorker[] {\n if (!jiraReconciliationEnabled()) return [];\n const reconcile = attachJiraIssueReconciler(this, ctx);\n if (!reconcile) return [];\n const intervalMs = jiraReconciliationInterval();\n return [\n new IssueReconcileWorker({\n integrationId: this.id,\n reconcile,\n ...(intervalMs ? { intervalMs } : {}),\n }),\n ];\n }\n\n /**\n * Org-scoped agent tools: the read-only issue-detail tool for sessions whose\n * resource is a factory project (Jira credentials are deployment-global,\n * so no per-org connection gates apply). v1 exposes no mutating Jira tools.\n */\n async agentTools(args: { requestContext: RequestContext }): Promise<IntegrationTools> {\n return buildJiraAgentTools({ requestContext: args.requestContext, jira: this });\n }\n\n /** Non-secret config snapshot for system diagnostics/startup logs. */\n diagnostics(): Record<string, unknown> {\n return {\n configured: true,\n site: new URL(this.baseUrl).host,\n email: maskEmail(this.#config.email),\n };\n }\n}\n\n/** `ops@acme.test` → `o***@acme.test` — enough to identify, never the value. */\nfunction maskEmail(email: string): string {\n const at = email.indexOf('@');\n if (at <= 0) return '***';\n return `${email[0]}***${email.slice(at)}`;\n}\n\n/** Whether the issue is already in the requested target state. */\nfunction currentStatusMatches(issue: JiraIssue, target: UpdateIntakeIssueInput['state']): boolean {\n const status = issue.fields.status;\n if (!status) return false;\n if (target.kind === 'byName') {\n return status.name.toLowerCase() === target.name.toLowerCase();\n }\n if (target.stateType === 'canceled') {\n return status.statusCategory?.key === 'done' && /cancel/i.test(status.name);\n }\n return status.statusCategory?.key === STATE_TYPE_TO_CATEGORY[target.stateType];\n}\n\n/** Resolve the target state against the issue's currently-legal transitions. */\nfunction resolveTransition(\n transitions: JiraTransition[],\n target: UpdateIntakeIssueInput['state'],\n): JiraTransition | null {\n if (target.kind === 'byName') {\n const wanted = target.name.toLowerCase();\n return transitions.find(transition => transition.to?.name.toLowerCase() === wanted) ?? null;\n }\n if (target.stateType === 'canceled') {\n return (\n transitions.find(\n transition => transition.to?.statusCategory?.key === 'done' && /cancel/i.test(transition.to.name),\n ) ?? null\n );\n }\n const category = STATE_TYPE_TO_CATEGORY[target.stateType];\n return transitions.find(transition => transition.to?.statusCategory?.key === category) ?? null;\n}\n"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import type { ApiRoute } from '@mastra/core/server';
|
|
16
16
|
import type { RouteAuth } from '../../routes/route.js';
|
|
17
17
|
import type { IntakeStorage } from '../../storage/domains/intake/base.js';
|
|
18
|
+
import type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';
|
|
18
19
|
import type { JiraIntegration } from './integration.js';
|
|
19
20
|
import type { JiraRulesIngress } from './rules.js';
|
|
20
21
|
/**
|
|
@@ -45,6 +46,8 @@ export interface MountJiraRoutesOptions {
|
|
|
45
46
|
* the same automatic intake Linear has.
|
|
46
47
|
*/
|
|
47
48
|
ingestFactoryIssues?: (input: JiraRulesIngress) => Promise<unknown>;
|
|
49
|
+
/** Filed cards, so a Factory keeps reading issues it filed after a label filter moves them to another slice. */
|
|
50
|
+
workItems?: Pick<WorkItemsStorage, 'getByClaimKey' | 'listBySource'>;
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
50
53
|
* Build the Jira routes as Mastra `apiRoutes`. When the feature is disabled,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAMjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAYnD;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,2FAA2F;IAC3F,IAAI,EAAE,SAAS,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpE,gHAAgH;IAChH,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,GAAG,cAAc,CAAC,CAAC;CACtE;AAqDD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,QAAQ,EAAE,CAgO3E"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { JiraApiError } from "./api.js";
|
|
2
|
+
import { jiraIssueFiledIn } from "./claim.js";
|
|
3
|
+
import { unfilteredJiraScopes } from "./intake-jql.js";
|
|
4
|
+
import { scopeJiraSourcesToProject } from "./binding-scope.js";
|
|
2
5
|
import { DEPLOYMENT_CONNECTION } from "./integration.js";
|
|
3
6
|
import { registerApiRoute } from "@mastra/core/server";
|
|
4
7
|
// src/integrations/jira/routes.ts
|
|
@@ -9,27 +12,6 @@ function loose(c) {
|
|
|
9
12
|
return c;
|
|
10
13
|
}
|
|
11
14
|
/**
|
|
12
|
-
* Narrow the caller's selected Jira projects to the ones that feed this
|
|
13
|
-
* Factory project.
|
|
14
|
-
*
|
|
15
|
-
* A Jira issue carries no Factory project of its own, so without a binding
|
|
16
|
-
* every board view would ingest every selected project's issues into whichever
|
|
17
|
-
* Factory happened to be on screen. Routing is explicit: a project feeds this
|
|
18
|
-
* Factory only when its binding names both the Factory project and a board.
|
|
19
|
-
* Returns the bound board per source so consumers can land cards where the
|
|
20
|
-
* user asked. Mirrors the Linear routes' scoping semantics locally — the
|
|
21
|
-
* generic binding storage is provider-neutral.
|
|
22
|
-
*/
|
|
23
|
-
async function scopeSourceIdsToProject({ intake, orgId, factoryProjectId, selectedIds }) {
|
|
24
|
-
const selected = new Set(selectedIds);
|
|
25
|
-
const intakeBoards = {};
|
|
26
|
-
for (const binding of await intake.listBindings({
|
|
27
|
-
orgId,
|
|
28
|
-
integrationId: "jira"
|
|
29
|
-
})) if (binding.factoryProjectId === factoryProjectId && binding.board && selected.has(binding.sourceId)) intakeBoards[binding.sourceId] = binding.board;
|
|
30
|
-
return intakeBoards;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
15
|
* Resolve the org-scoped tenant for a Jira request. Intake selections are
|
|
34
16
|
* org-owned, so it requires both a signed-in user and an organization — same
|
|
35
17
|
* tenancy rules as the Linear routes.
|
|
@@ -74,7 +56,7 @@ function jiraFetchError(c, err) {
|
|
|
74
56
|
*/
|
|
75
57
|
function buildJiraRoutes(options) {
|
|
76
58
|
const routes = [];
|
|
77
|
-
const { jira, auth, intake } = options;
|
|
59
|
+
const { jira, auth, intake, workItems } = options;
|
|
78
60
|
const enabled = Boolean(jira) && auth.enabled();
|
|
79
61
|
const diagnostics = () => ({
|
|
80
62
|
jiraConfigured: Boolean(jira),
|
|
@@ -158,19 +140,20 @@ function buildJiraRoutes(options) {
|
|
|
158
140
|
message: "Jira intake is turned off in Settings."
|
|
159
141
|
}, 404);
|
|
160
142
|
const selectedIds = selection.sourceIds ?? [];
|
|
161
|
-
const
|
|
143
|
+
const projectScope = factoryProjectId ? await scopeJiraSourcesToProject({
|
|
162
144
|
intake,
|
|
163
145
|
orgId: resolved.tenant.orgId,
|
|
164
146
|
factoryProjectId,
|
|
165
147
|
selectedIds
|
|
166
148
|
}) : null;
|
|
167
|
-
const
|
|
168
|
-
|
|
149
|
+
const intakeBoards = projectScope?.intakeBoards ?? null;
|
|
150
|
+
const scopes = projectScope?.scopes ?? unfilteredJiraScopes(selectedIds);
|
|
151
|
+
if (scopes.length === 0) return c.json({
|
|
169
152
|
issues: [],
|
|
170
153
|
nextCursor: null
|
|
171
154
|
});
|
|
172
155
|
try {
|
|
173
|
-
const { issues, nextCursor } = await jira.listActiveIssues(after,
|
|
156
|
+
const { issues, nextCursor } = await jira.listActiveIssues(after, scopes);
|
|
174
157
|
const issuePayload = issues.map((issue) => ({
|
|
175
158
|
id: issue.id,
|
|
176
159
|
identifier: issue.identifier,
|
|
@@ -227,19 +210,23 @@ function buildJiraRoutes(options) {
|
|
|
227
210
|
error: "jira_intake_disabled",
|
|
228
211
|
message: "Jira intake is turned off in Settings."
|
|
229
212
|
}, 404);
|
|
230
|
-
const
|
|
213
|
+
const projectScope = await scopeJiraSourcesToProject({
|
|
231
214
|
intake,
|
|
232
215
|
orgId: resolved.tenant.orgId,
|
|
233
216
|
factoryProjectId,
|
|
234
217
|
selectedIds: selection.sourceIds ?? []
|
|
235
218
|
});
|
|
236
|
-
if (
|
|
219
|
+
if (projectScope.scopes.length === 0) return c.json({ error: "issue_not_found" }, 404);
|
|
237
220
|
try {
|
|
238
221
|
const issue = await jira.intake.getIssue({
|
|
239
222
|
connection: DEPLOYMENT_CONNECTION,
|
|
240
223
|
issueId: issueRef
|
|
241
224
|
});
|
|
242
|
-
const routed = issue?.sourceId != null && issue.sourceId in intakeBoards
|
|
225
|
+
const routed = issue?.sourceId != null && issue.sourceId in projectScope.intakeBoards && (projectScope.takes(issue.sourceId, issue.labels) || await jiraIssueFiledIn(workItems, {
|
|
226
|
+
orgId: resolved.tenant.orgId,
|
|
227
|
+
factoryProjectId,
|
|
228
|
+
issueRef: issue.id
|
|
229
|
+
}));
|
|
243
230
|
if (!issue || issue.identifier.toUpperCase() !== identifier.toUpperCase() || !routed) return c.json({ error: "issue_not_found" }, 404);
|
|
244
231
|
return c.json({
|
|
245
232
|
identifier: issue.identifier,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/jira/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the Jira intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the host auth gate.\n * Mirrors the Linear module minus everything OAuth: there is no\n * connect/callback flow, no state signer, and no per-org connection storage —\n * Jira credentials are deployment-global constructor config on the\n * integration instance. Every route still re-resolves the authenticated user\n * from the request and scopes intake selections by the caller's org.\n *\n * When the feature is disabled (no auth, or no intake storage),\n * `buildJiraRoutes` returns only `GET /web/jira/status`, which reports\n * `enabled:false` so the SPA can cleanly hide all Jira UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport { JiraApiError } from './api.js';\nimport { DEPLOYMENT_CONNECTION } from './integration.js';\nimport type { JiraIntegration } from './integration.js';\nimport type { JiraRulesIngress } from './rules.js';\n\ntype RouteContext = Context;\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst ISSUE_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*-\\d+$/;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\n/**\n * Non-secret diagnostic snapshot of every Jira feature gate, mirroring the\n * Linear diagnostics shape. Only booleans — never values.\n */\nexport interface JiraFeatureDiagnostics {\n jiraConfigured: boolean;\n factoryAuthEnabled: boolean;\n appDbConfigured: boolean;\n}\n\nexport interface MountJiraRoutesOptions {\n /**\n * The integration instance providing REST access. Required for everything\n * beyond the disabled `status` route.\n */\n jira?: JiraIntegration;\n /** Host auth seam. Intake selections are org-owned, so the feature is inert without it. */\n auth: RouteAuth;\n /**\n * Cross-integration intake selection domain. Required for the issues route's\n * project filter; when absent, only the disabled `status` route is served.\n */\n intake?: IntakeStorage;\n /**\n * Factory rules ingress for observed issues. When present, a board-scoped\n * issue listing also materializes new cards through the Jira event rules —\n * the same automatic intake Linear has.\n */\n ingestFactoryIssues?: (input: JiraRulesIngress) => Promise<unknown>;\n}\n\n/**\n * Narrow the caller's selected Jira projects to the ones that feed this\n * Factory project.\n *\n * A Jira issue carries no Factory project of its own, so without a binding\n * every board view would ingest every selected project's issues into whichever\n * Factory happened to be on screen. Routing is explicit: a project feeds this\n * Factory only when its binding names both the Factory project and a board.\n * Returns the bound board per source so consumers can land cards where the\n * user asked. Mirrors the Linear routes' scoping semantics locally — the\n * generic binding storage is provider-neutral.\n */\nasync function scopeSourceIdsToProject({\n intake,\n orgId,\n factoryProjectId,\n selectedIds,\n}: {\n intake: IntakeStorage;\n orgId: string;\n factoryProjectId: string;\n selectedIds: string[];\n}): Promise<Record<string, string>> {\n const selected = new Set(selectedIds);\n const intakeBoards: Record<string, string> = {};\n for (const binding of await intake.listBindings({ orgId, integrationId: 'jira' })) {\n if (binding.factoryProjectId === factoryProjectId && binding.board && selected.has(binding.sourceId)) {\n intakeBoards[binding.sourceId] = binding.board;\n }\n }\n return intakeBoards;\n}\n\n/**\n * Resolve the org-scoped tenant for a Jira request. Intake selections are\n * org-owned, so it requires both a signed-in user and an organization — same\n * tenancy rules as the Linear routes.\n */\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Jira intake requires an organization. Personal accounts cannot use Jira intake.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque Jira pagination cursor from the query string. Cursors are\n * server-issued (`nextPageToken`), so anything outside a conservative\n * charset/length is rejected rather than forwarded to Jira.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 512 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Map a Jira read failure to the API response for the SPA. */\nfunction jiraFetchError(c: RouteContext, err: unknown) {\n if (err instanceof JiraApiError && err.code === 'jira_auth_failed') {\n return c.json(\n {\n error: 'jira_auth_failed',\n message: 'Jira rejected the configured credentials. Check JIRA_EMAIL and JIRA_API_TOKEN.',\n },\n 409,\n );\n }\n return c.json({ error: 'jira_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the Jira routes as Mastra `apiRoutes`. When the feature is disabled,\n * returns only the `status` route so the SPA can detect the disabled state.\n */\nexport function buildJiraRoutes(options: MountJiraRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { jira, auth, intake } = options;\n const enabled = Boolean(jira) && auth.enabled();\n const diagnostics = (): JiraFeatureDiagnostics => ({\n jiraConfigured: Boolean(jira),\n factoryAuthEnabled: auth.enabled(),\n appDbConfigured: true,\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/jira/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !jira || !intake) {\n return c.json({\n enabled: false,\n configured: Boolean(jira),\n mode: 'direct',\n site: null,\n reason: 'missing_config',\n diagnostics: diagnostics(),\n });\n }\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized', reason: 'auth_required' }, 401);\n\n const site = new URL(jira.baseUrl).host;\n if (!tenant.orgId) {\n return c.json({\n enabled: true,\n configured: true,\n mode: 'direct',\n organizationRequired: true,\n site,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n\n // Deployment-global credentials: configured means ready — there is no\n // per-org connection step like Linear's OAuth.\n return c.json({\n enabled: true,\n configured: true,\n mode: 'direct',\n site,\n reason: 'ready',\n diagnostics: diagnostics(),\n });\n },\n }),\n );\n\n // Without the integration instance or the intake domain the feature can't\n // serve org-scoped data — serve only the disabled `status` route.\n if (!enabled || !jira || !intake) {\n return routes;\n }\n\n // ── List the site's projects (Settings intake-source picker) ────────────\n routes.push(\n registerApiRoute('/web/jira/projects', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n try {\n const sources = await jira.intake.listSources(resolved.tenant);\n return c.json({\n projects: sources.map(source => ({\n id: source.id,\n name: source.name,\n key: typeof source.metadata?.key === 'string' ? source.metadata.key : null,\n })),\n });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the site's active issues (cursor-paged) ────────────────────────\n // Respects the caller's intake config: disabled Jira intake 404s the\n // source, and an explicit project selection narrows the issue filter.\n routes.push(\n registerApiRoute('/web/jira/issues', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const after = parseAfterCursor(c.req.query('after'));\n if (after === null) return c.json({ error: 'invalid_cursor' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['jira'] });\n const selection = config.jira!;\n if (!selection.enabled) {\n return c.json({ error: 'jira_intake_disabled', message: 'Jira intake is turned off in Settings.' }, 404);\n }\n\n // No projects selected means nothing is synced — don't fan out to Jira.\n const selectedIds = selection.sourceIds ?? [];\n // A board request only ever sees the projects routed to a board of\n // that Factory project; unscoped listing stays available to callers\n // that don't view a specific board.\n const intakeBoards = factoryProjectId\n ? await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds,\n })\n : null;\n const projectIds = intakeBoards ? Object.keys(intakeBoards) : selectedIds;\n if (projectIds.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const { issues, nextCursor } = await jira.listActiveIssues(after, projectIds);\n const issuePayload = issues.map(issue => ({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n author: issue.author,\n state: issue.state ?? '',\n stateType: issue.stateType ?? '',\n priorityLabel: issue.priority ?? '',\n assignee: issue.assignee,\n project: issue.source,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n sourceId: issue.sourceId || null,\n }));\n if (factoryProjectId && intakeBoards && options.ingestFactoryIssues) {\n await options.ingestFactoryIssues({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n factoryProjectId,\n issues: issuePayload.map(issue => ({ ...issue, site: null })),\n intakeBoards,\n });\n }\n return c.json({ issues: issuePayload, nextCursor });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n routes.push(\n registerApiRoute('/web/jira/issues/:identifier', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const identifier = c.req.param('identifier');\n const factoryProjectId = c.req.query('factoryProjectId');\n const issueRef = c.req.query('issueRef');\n if (!ISSUE_KEY_RE.test(identifier)) return c.json({ error: 'invalid_issue_identifier' }, 400);\n if (!factoryProjectId || !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n if (!issueRef || issueRef.length > 2_048) return c.json({ error: 'invalid_issue_ref' }, 400);\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['jira'] });\n const selection = config.jira!;\n if (!selection.enabled) {\n return c.json({ error: 'jira_intake_disabled', message: 'Jira intake is turned off in Settings.' }, 404);\n }\n const intakeBoards = await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds: selection.sourceIds ?? [],\n });\n if (Object.keys(intakeBoards).length === 0) return c.json({ error: 'issue_not_found' }, 404);\n\n try {\n const issue = await jira.intake.getIssue({ connection: DEPLOYMENT_CONNECTION, issueId: issueRef });\n // The issue's project must be a source routed to this Factory — an\n // issue from an unbound project reads exactly like one that doesn't\n // exist (same stance as the Linear detail route).\n const routed = issue?.sourceId != null && issue.sourceId in intakeBoards;\n if (!issue || issue.identifier.toUpperCase() !== identifier.toUpperCase() || !routed) {\n return c.json({ error: 'issue_not_found' }, 404);\n }\n return c.json({\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n description: issue.description,\n });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/jira/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the Jira intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the host auth gate.\n * Mirrors the Linear module minus everything OAuth: there is no\n * connect/callback flow, no state signer, and no per-org connection storage —\n * Jira credentials are deployment-global constructor config on the\n * integration instance. Every route still re-resolves the authenticated user\n * from the request and scopes intake selections by the caller's org.\n *\n * When the feature is disabled (no auth, or no intake storage),\n * `buildJiraRoutes` returns only `GET /web/jira/status`, which reports\n * `enabled:false` so the SPA can cleanly hide all Jira UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport { JiraApiError } from './api.js';\nimport { scopeJiraSourcesToProject } from './binding-scope.js';\nimport { jiraIssueFiledIn } from './claim.js';\nimport { unfilteredJiraScopes } from './intake-jql.js';\nimport { DEPLOYMENT_CONNECTION } from './integration.js';\nimport type { JiraIntegration } from './integration.js';\nimport type { JiraRulesIngress } from './rules.js';\n\ntype RouteContext = Context;\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst ISSUE_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*-\\d+$/;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\n/**\n * Non-secret diagnostic snapshot of every Jira feature gate, mirroring the\n * Linear diagnostics shape. Only booleans — never values.\n */\nexport interface JiraFeatureDiagnostics {\n jiraConfigured: boolean;\n factoryAuthEnabled: boolean;\n appDbConfigured: boolean;\n}\n\nexport interface MountJiraRoutesOptions {\n /**\n * The integration instance providing REST access. Required for everything\n * beyond the disabled `status` route.\n */\n jira?: JiraIntegration;\n /** Host auth seam. Intake selections are org-owned, so the feature is inert without it. */\n auth: RouteAuth;\n /**\n * Cross-integration intake selection domain. Required for the issues route's\n * project filter; when absent, only the disabled `status` route is served.\n */\n intake?: IntakeStorage;\n /**\n * Factory rules ingress for observed issues. When present, a board-scoped\n * issue listing also materializes new cards through the Jira event rules —\n * the same automatic intake Linear has.\n */\n ingestFactoryIssues?: (input: JiraRulesIngress) => Promise<unknown>;\n /** Filed cards, so a Factory keeps reading issues it filed after a label filter moves them to another slice. */\n workItems?: Pick<WorkItemsStorage, 'getByClaimKey' | 'listBySource'>;\n}\n\n/**\n * Resolve the org-scoped tenant for a Jira request. Intake selections are\n * org-owned, so it requires both a signed-in user and an organization — same\n * tenancy rules as the Linear routes.\n */\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Jira intake requires an organization. Personal accounts cannot use Jira intake.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque Jira pagination cursor from the query string. Cursors are\n * server-issued (`nextPageToken`), so anything outside a conservative\n * charset/length is rejected rather than forwarded to Jira.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 512 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Map a Jira read failure to the API response for the SPA. */\nfunction jiraFetchError(c: RouteContext, err: unknown) {\n if (err instanceof JiraApiError && err.code === 'jira_auth_failed') {\n return c.json(\n {\n error: 'jira_auth_failed',\n message: 'Jira rejected the configured credentials. Check JIRA_EMAIL and JIRA_API_TOKEN.',\n },\n 409,\n );\n }\n return c.json({ error: 'jira_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the Jira routes as Mastra `apiRoutes`. When the feature is disabled,\n * returns only the `status` route so the SPA can detect the disabled state.\n */\nexport function buildJiraRoutes(options: MountJiraRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { jira, auth, intake, workItems } = options;\n const enabled = Boolean(jira) && auth.enabled();\n const diagnostics = (): JiraFeatureDiagnostics => ({\n jiraConfigured: Boolean(jira),\n factoryAuthEnabled: auth.enabled(),\n appDbConfigured: true,\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/jira/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !jira || !intake) {\n return c.json({\n enabled: false,\n configured: Boolean(jira),\n mode: 'direct',\n site: null,\n reason: 'missing_config',\n diagnostics: diagnostics(),\n });\n }\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized', reason: 'auth_required' }, 401);\n\n const site = new URL(jira.baseUrl).host;\n if (!tenant.orgId) {\n return c.json({\n enabled: true,\n configured: true,\n mode: 'direct',\n organizationRequired: true,\n site,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n\n // Deployment-global credentials: configured means ready — there is no\n // per-org connection step like Linear's OAuth.\n return c.json({\n enabled: true,\n configured: true,\n mode: 'direct',\n site,\n reason: 'ready',\n diagnostics: diagnostics(),\n });\n },\n }),\n );\n\n // Without the integration instance or the intake domain the feature can't\n // serve org-scoped data — serve only the disabled `status` route.\n if (!enabled || !jira || !intake) {\n return routes;\n }\n\n // ── List the site's projects (Settings intake-source picker) ────────────\n routes.push(\n registerApiRoute('/web/jira/projects', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n try {\n const sources = await jira.intake.listSources(resolved.tenant);\n return c.json({\n projects: sources.map(source => ({\n id: source.id,\n name: source.name,\n key: typeof source.metadata?.key === 'string' ? source.metadata.key : null,\n })),\n });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the site's active issues (cursor-paged) ────────────────────────\n // Respects the caller's intake config: disabled Jira intake 404s the\n // source, and an explicit project selection narrows the issue filter.\n routes.push(\n registerApiRoute('/web/jira/issues', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const after = parseAfterCursor(c.req.query('after'));\n if (after === null) return c.json({ error: 'invalid_cursor' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['jira'] });\n const selection = config.jira!;\n if (!selection.enabled) {\n return c.json({ error: 'jira_intake_disabled', message: 'Jira intake is turned off in Settings.' }, 404);\n }\n\n // No projects selected means nothing is synced — don't fan out to Jira.\n const selectedIds = selection.sourceIds ?? [];\n // A board request only ever sees the projects routed to a board of\n // that Factory project; unscoped listing stays available to callers\n // that don't view a specific board.\n const projectScope = factoryProjectId\n ? await scopeJiraSourcesToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds,\n })\n : null;\n const intakeBoards = projectScope?.intakeBoards ?? null;\n const scopes = projectScope?.scopes ?? unfilteredJiraScopes(selectedIds);\n if (scopes.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const { issues, nextCursor } = await jira.listActiveIssues(after, scopes);\n const issuePayload = issues.map(issue => ({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n author: issue.author,\n state: issue.state ?? '',\n stateType: issue.stateType ?? '',\n priorityLabel: issue.priority ?? '',\n assignee: issue.assignee,\n project: issue.source,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n sourceId: issue.sourceId || null,\n }));\n if (factoryProjectId && intakeBoards && options.ingestFactoryIssues) {\n await options.ingestFactoryIssues({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n factoryProjectId,\n issues: issuePayload.map(issue => ({ ...issue, site: null })),\n intakeBoards,\n });\n }\n return c.json({ issues: issuePayload, nextCursor });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n routes.push(\n registerApiRoute('/web/jira/issues/:identifier', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const identifier = c.req.param('identifier');\n const factoryProjectId = c.req.query('factoryProjectId');\n const issueRef = c.req.query('issueRef');\n if (!ISSUE_KEY_RE.test(identifier)) return c.json({ error: 'invalid_issue_identifier' }, 400);\n if (!factoryProjectId || !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n if (!issueRef || issueRef.length > 2_048) return c.json({ error: 'invalid_issue_ref' }, 400);\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['jira'] });\n const selection = config.jira!;\n if (!selection.enabled) {\n return c.json({ error: 'jira_intake_disabled', message: 'Jira intake is turned off in Settings.' }, 404);\n }\n const projectScope = await scopeJiraSourcesToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds: selection.sourceIds ?? [],\n });\n if (projectScope.scopes.length === 0) return c.json({ error: 'issue_not_found' }, 404);\n\n try {\n const issue = await jira.intake.getIssue({ connection: DEPLOYMENT_CONNECTION, issueId: issueRef });\n // An unbound project's issue, or one in another Factory's slice that this\n // Factory never filed, reads like one that doesn't exist (as on Linear).\n const routed =\n issue?.sourceId != null &&\n issue.sourceId in projectScope.intakeBoards &&\n (projectScope.takes(issue.sourceId, issue.labels) ||\n (await jiraIssueFiledIn(workItems, { orgId: resolved.tenant.orgId, factoryProjectId, issueRef: issue.id })));\n if (!issue || issue.identifier.toUpperCase() !== identifier.toUpperCase() || !routed) {\n return c.json({ error: 'issue_not_found' }, 404);\n }\n return c.json({\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n description: issue.description,\n });\n } catch (err) {\n return jiraFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/rules.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAoBzD,MAAM,WAAW,gBAAgB;IAC/B,uFAAuF;IACvF,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACjD;AAED,KAAK,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1D,qBAAa,SAAS;;IACR,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,gBAAgB,EAAI;IAEpD,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAuB1F;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/jira/rules.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAoBzD,MAAM,WAAW,gBAAgB;IAC/B,uFAAuF;IACvF,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,cAAc,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACjD;AAED,KAAK,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1D,qBAAa,SAAS;;IACR,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,gBAAgB,EAAI;IAEpD,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAuB1F;CAyHF;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;CAAE,EACxC,OAAO,EAAE,kBAAkB,GAC1B,CAAC,CAAC,KAAK,EAAE,gBAAgB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAU7D"}
|