agent-relay-server 0.61.2 → 0.62.1
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/docs/openapi.json +479 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-DVvlvi4B.js → MessageBubble-DRrxfjyT.js} +2 -2
- package/public/assets/{MessageBubble-DVvlvi4B.js.map → MessageBubble-DRrxfjyT.js.map} +1 -1
- package/public/assets/PlanGraphPanel-Dp2eYazz.js +2 -0
- package/public/assets/PlanGraphPanel-Dp2eYazz.js.map +1 -0
- package/public/assets/{activity-IYl3MEXC.js → activity--t0U2yzq.js} +2 -2
- package/public/assets/{activity-IYl3MEXC.js.map → activity--t0U2yzq.js.map} +1 -1
- package/public/assets/{agent-profiles-BfGTTbeY.js → agent-profiles-BMtJYzxn.js} +2 -2
- package/public/assets/{agent-profiles-BfGTTbeY.js.map → agent-profiles-BMtJYzxn.js.map} +1 -1
- package/public/assets/{agents-BquvREQr.js → agents-Ck9AePlc.js} +2 -2
- package/public/assets/{agents-BquvREQr.js.map → agents-Ck9AePlc.js.map} +1 -1
- package/public/assets/{analytics-De0cEZGx.js → analytics-CzfK387L.js} +3 -3
- package/public/assets/{analytics-De0cEZGx.js.map → analytics-CzfK387L.js.map} +1 -1
- package/public/assets/automation-Cgw8vtE8.js +2 -0
- package/public/assets/automation-Cgw8vtE8.js.map +1 -0
- package/public/assets/{badge-DhLyCPag.js → badge-CZhIdlTJ.js} +2 -2
- package/public/assets/{badge-DhLyCPag.js.map → badge-CZhIdlTJ.js.map} +1 -1
- package/public/assets/{branch-state-badge-DSyWKluD.js → branch-state-badge-DnEEPtOP.js} +2 -2
- package/public/assets/{branch-state-badge-DSyWKluD.js.map → branch-state-badge-DnEEPtOP.js.map} +1 -1
- package/public/assets/{button-K20wAZG1.js → button-2taQeNHW.js} +2 -2
- package/public/assets/{button-K20wAZG1.js.map → button-2taQeNHW.js.map} +1 -1
- package/public/assets/card-DzIsAI41.js +2 -0
- package/public/assets/{card-CcZ9hLI7.js.map → card-DzIsAI41.js.map} +1 -1
- package/public/assets/channels-Dc3L2MQs.js +2 -0
- package/public/assets/{channels-BG0Nib7y.js.map → channels-Dc3L2MQs.js.map} +1 -1
- package/public/assets/chat-Fc6iJ7sa.js +2 -0
- package/public/assets/{chat-Dx6qWNxb.js.map → chat-Fc6iJ7sa.js.map} +1 -1
- package/public/assets/{connectors-DWh7MwV_.js → connectors-Dc2ttGWs.js} +2 -2
- package/public/assets/{connectors-DWh7MwV_.js.map → connectors-Dc2ttGWs.js.map} +1 -1
- package/public/assets/copy-button-HEkwi6xX.js +2 -0
- package/public/assets/{copy-button-eF3cKp4Q.js.map → copy-button-HEkwi6xX.js.map} +1 -1
- package/public/assets/display-DzNvOu1j.js.map +1 -1
- package/public/assets/dist-1rXN5FN9.js +2 -0
- package/public/assets/{dist-YpHfnnIA.js.map → dist-1rXN5FN9.js.map} +1 -1
- package/public/assets/{dist-1dhfdhm9.js → dist-AbEQuFoK.js} +2 -2
- package/public/assets/{dist-1dhfdhm9.js.map → dist-AbEQuFoK.js.map} +1 -1
- package/public/assets/{dist-COQuunEv.js → dist-B7p1zkvf.js} +2 -2
- package/public/assets/{dist-COQuunEv.js.map → dist-B7p1zkvf.js.map} +1 -1
- package/public/assets/{dist-Ci5Y37nj.js → dist-CG10hM1-.js} +2 -2
- package/public/assets/{dist-Ci5Y37nj.js.map → dist-CG10hM1-.js.map} +1 -1
- package/public/assets/dist-Cfm5_zPB.js +2 -0
- package/public/assets/{dist-qd198ib9.js.map → dist-Cfm5_zPB.js.map} +1 -1
- package/public/assets/{dist-BrQsqBbc.js → dist-Cgw85UcE.js} +2 -2
- package/public/assets/{dist-BrQsqBbc.js.map → dist-Cgw85UcE.js.map} +1 -1
- package/public/assets/{dist-CoROafNg.js → dist-D3_TJKTC.js} +2 -2
- package/public/assets/{dist-CoROafNg.js.map → dist-D3_TJKTC.js.map} +1 -1
- package/public/assets/{dist-I-MFUv51.js → dist-DQkLq0hs.js} +2 -2
- package/public/assets/{dist-I-MFUv51.js.map → dist-DQkLq0hs.js.map} +1 -1
- package/public/assets/{dist-UQGKwqZH.js → dist-D_sl7u_f.js} +2 -2
- package/public/assets/{dist-UQGKwqZH.js.map → dist-D_sl7u_f.js.map} +1 -1
- package/public/assets/{dist-yGOq6sB3.js → dist-Ilb3r1im.js} +2 -2
- package/public/assets/{dist-yGOq6sB3.js.map → dist-Ilb3r1im.js.map} +1 -1
- package/public/assets/dist-Pf_3Gf38.js +2 -0
- package/public/assets/{dist-BLBF7qqr.js.map → dist-Pf_3Gf38.js.map} +1 -1
- package/public/assets/{es2015-CRxz9lOj.js → es2015-GSepNaIS.js} +2 -2
- package/public/assets/{es2015-CRxz9lOj.js.map → es2015-GSepNaIS.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DMm6jeqi.js → formatted-body-impl-Bg-gx7rG.js} +2 -2
- package/public/assets/{formatted-body-impl-DMm6jeqi.js.map → formatted-body-impl-Bg-gx7rG.js.map} +1 -1
- package/public/assets/index-BeDncjN4.js +22 -0
- package/public/assets/index-BeDncjN4.js.map +1 -0
- package/public/assets/index-DiR-fqy_.css +2 -0
- package/public/assets/{input-BHYNpXAG.js → input-BsNqlONJ.js} +2 -2
- package/public/assets/{input-BHYNpXAG.js.map → input-BsNqlONJ.js.map} +1 -1
- package/public/assets/{insights-C7wKt1_l.js → insights-c08aW-7n.js} +2 -2
- package/public/assets/{insights-C7wKt1_l.js.map → insights-c08aW-7n.js.map} +1 -1
- package/public/assets/{integrations-CG9qyHUf.js → integrations-BwpgcqnF.js} +2 -2
- package/public/assets/{integrations-CG9qyHUf.js.map → integrations-BwpgcqnF.js.map} +1 -1
- package/public/assets/{lib-Df4aph5u.js → lib-bXPu6xCL.js} +2 -2
- package/public/assets/{lib-Df4aph5u.js.map → lib-bXPu6xCL.js.map} +1 -1
- package/public/assets/lucide-react-DWYjU-AF.js +9 -0
- package/public/assets/{lucide-react-L2UxFDic.js.map → lucide-react-DWYjU-AF.js.map} +1 -1
- package/public/assets/{maintenance-CjpvnV_U.js → maintenance-CTKP0kWB.js} +2 -2
- package/public/assets/{maintenance-CjpvnV_U.js.map → maintenance-CTKP0kWB.js.map} +1 -1
- package/public/assets/{managed-agents-CdfuBKJA.js → managed-agents-BofCxo8d.js} +2 -2
- package/public/assets/{managed-agents-CdfuBKJA.js.map → managed-agents-BofCxo8d.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-Cf4xCmWt.js → markdown-preview-impl-CQ-6BIb8.js} +2 -2
- package/public/assets/{markdown-preview-impl-Cf4xCmWt.js.map → markdown-preview-impl-CQ-6BIb8.js.map} +1 -1
- package/public/assets/memory-DvfXjMoi.js +2 -0
- package/public/assets/{memory-xKWduhyr.js.map → memory-DvfXjMoi.js.map} +1 -1
- package/public/assets/{messages-BpZYOUtJ.js → messages-C9VsPzZ_.js} +2 -2
- package/public/assets/{messages-BpZYOUtJ.js.map → messages-C9VsPzZ_.js.map} +1 -1
- package/public/assets/orchestrators-MffVueeE.js +2 -0
- package/public/assets/{orchestrators-BrJvGIOc.js.map → orchestrators-MffVueeE.js.map} +1 -1
- package/public/assets/{overview-BUORwePi.js → overview-BSVajefM.js} +2 -2
- package/public/assets/{overview-BUORwePi.js.map → overview-BSVajefM.js.map} +1 -1
- package/public/assets/pairs-B5711-Cs.js +2 -0
- package/public/assets/{pairs-DR0GizDN.js.map → pairs-B5711-Cs.js.map} +1 -1
- package/public/assets/{react-dom-CrP_AtcL.js → react-dom-CPghDLpB.js} +2 -2
- package/public/assets/{react-dom-CrP_AtcL.js.map → react-dom-CPghDLpB.js.map} +1 -1
- package/public/assets/{security-DLwSqTc_.js → security-DkUwK9Vj.js} +2 -2
- package/public/assets/{security-DLwSqTc_.js.map → security-DkUwK9Vj.js.map} +1 -1
- package/public/assets/{select-pEru111r.js → select-DrM2RRiT.js} +2 -2
- package/public/assets/{select-pEru111r.js.map → select-DrM2RRiT.js.map} +1 -1
- package/public/assets/{settings-B-ueGvwV.js → settings-DdKCb-vy.js} +3 -3
- package/public/assets/{settings-B-ueGvwV.js.map → settings-DdKCb-vy.js.map} +1 -1
- package/public/assets/store-iTiheuTD.js +9 -0
- package/public/assets/store-iTiheuTD.js.map +1 -0
- package/public/assets/tasks-e1y1PFfO.js +2 -0
- package/public/assets/{tasks-C2rRQm6E.js.map → tasks-e1y1PFfO.js.map} +1 -1
- package/public/assets/teams-BUcxoGC_.js +2 -0
- package/public/assets/teams-BUcxoGC_.js.map +1 -0
- package/public/assets/{terminal-viewer-impl-BM_3qoeN.js → terminal-viewer-impl-CmZjQOB2.js} +3 -3
- package/public/assets/{terminal-viewer-impl-BM_3qoeN.js.map → terminal-viewer-impl-CmZjQOB2.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-DrRH_QzG.js → use-keyboard-viewport-NKw1O5gC.js} +2 -2
- package/public/assets/{use-keyboard-viewport-DrRH_QzG.js.map → use-keyboard-viewport-NKw1O5gC.js.map} +1 -1
- package/public/assets/{work-queue-CkIYDAZD.js → work-queue-CXBQx43n.js} +2 -2
- package/public/assets/{work-queue-CkIYDAZD.js.map → work-queue-CXBQx43n.js.map} +1 -1
- package/public/assets/{workspaces-CYm5bLg9.js → workspaces-D2EfZh9a.js} +3 -3
- package/public/assets/{workspaces-CYm5bLg9.js.map → workspaces-D2EfZh9a.js.map} +1 -1
- package/public/index.html +21 -21
- package/src/automations.ts +40 -7
- package/src/branch-landed.ts +5 -0
- package/src/bus.ts +6 -1
- package/src/cli/workspace.ts +9 -17
- package/src/db/activity.ts +191 -3
- package/src/db/agents.ts +14 -5
- package/src/db/integrations.ts +1 -0
- package/src/db/issue-associations.ts +128 -0
- package/src/db/issue-lifecycle.ts +187 -0
- package/src/db/issue-refs.ts +254 -0
- package/src/db/mappers.ts +2 -0
- package/src/db/migrations.ts +53 -0
- package/src/db/schema.ts +3 -2
- package/src/issue-tracker/adapter.ts +24 -0
- package/src/issue-tracker/github.ts +136 -0
- package/src/issue-tracker/index.ts +13 -0
- package/src/issue-tracker/registry.ts +26 -0
- package/src/maintenance.ts +22 -169
- package/src/mcp-issue-activity-tools.ts +114 -0
- package/src/mcp-issue-association-tools.ts +130 -0
- package/src/mcp-issue-lifecycle-tools.ts +55 -0
- package/src/mcp-issue-ref-tools.ts +174 -0
- package/src/mcp-team-tools.ts +1 -1
- package/src/mcp.ts +3 -3
- package/src/provider-config-store.ts +30 -8
- package/src/routes/activity.ts +6 -1
- package/src/routes/commands.ts +29 -0
- package/src/routes/index.ts +19 -0
- package/src/routes/issue-activity.ts +78 -0
- package/src/routes/issue-associations.ts +76 -0
- package/src/routes/issue-lifecycle.ts +34 -0
- package/src/routes/issue-refs.ts +137 -0
- package/src/routes/orchestrator-bootstrap.ts +2 -1
- package/src/routes/orchestrator.ts +42 -1
- package/src/routes/tokens.ts +1 -1
- package/src/runtime-tokens.ts +15 -11
- package/src/security.ts +51 -3
- package/src/services/issue-activity.ts +30 -0
- package/src/services/issue-associations.ts +72 -0
- package/src/services/issue-lifecycle.ts +308 -0
- package/src/services/issue-refs.ts +32 -0
- package/src/services/plan-graph.ts +40 -2
- package/src/services/team.ts +4 -0
- package/src/token-db.ts +27 -7
- package/src/workspace-actions.ts +8 -1
- package/src/workspace-auto-merge.ts +228 -0
- package/src/workspace-probe.ts +125 -0
- package/public/assets/PlanGraphPanel-Dby7wq9c.js +0 -2
- package/public/assets/PlanGraphPanel-Dby7wq9c.js.map +0 -1
- package/public/assets/automation-Ddbermbi.js +0 -2
- package/public/assets/automation-Ddbermbi.js.map +0 -1
- package/public/assets/card-CcZ9hLI7.js +0 -2
- package/public/assets/channels-BG0Nib7y.js +0 -2
- package/public/assets/chat-Dx6qWNxb.js +0 -2
- package/public/assets/copy-button-eF3cKp4Q.js +0 -2
- package/public/assets/dist-BLBF7qqr.js +0 -2
- package/public/assets/dist-YpHfnnIA.js +0 -2
- package/public/assets/dist-qd198ib9.js +0 -2
- package/public/assets/index-B3wOeAPO.js +0 -21
- package/public/assets/index-B3wOeAPO.js.map +0 -1
- package/public/assets/index-Dn_GDXbi.css +0 -2
- package/public/assets/lucide-react-L2UxFDic.js +0 -9
- package/public/assets/memory-xKWduhyr.js +0 -2
- package/public/assets/orchestrators-BrJvGIOc.js +0 -2
- package/public/assets/pairs-DR0GizDN.js +0 -2
- package/public/assets/store-B8xP6z6T.js +0 -9
- package/public/assets/store-B8xP6z6T.js.map +0 -1
- package/public/assets/tasks-C2rRQm6E.js +0 -2
- package/public/assets/teams-Bo0sdBTp.js +0 -2
- package/public/assets/teams-Bo0sdBTp.js.map +0 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ISSUE_ASSOCIATION_ENTITY_TYPES, ISSUE_PROVIDERS, isRecord, type IssueAssociationEntityType, type IssueProvider } from "../types";
|
|
2
|
+
import { associateIssueRef, listIssueEntities, listIssueRefsForEntity, removeIssueAssociation, resolveAssociationIssueRef } from "../services/issue-associations";
|
|
3
|
+
import { cleanEnum, cleanString, optionalEnum } from "../validation";
|
|
4
|
+
import { error, json, parseBody, type Handler } from "./_shared";
|
|
5
|
+
|
|
6
|
+
function cleanEntityType(value: unknown): IssueAssociationEntityType {
|
|
7
|
+
return cleanEnum(value, "entityType", ISSUE_ASSOCIATION_ENTITY_TYPES) as IssueAssociationEntityType;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cleanIssueInput(value: Record<string, unknown>) {
|
|
11
|
+
return {
|
|
12
|
+
issueRefId: cleanString(value.issueRefId, "issueRefId", { max: 120 }),
|
|
13
|
+
ref: cleanString(value.ref, "ref", { max: 1000 }),
|
|
14
|
+
defaultRepo: cleanString(value.defaultRepo, "defaultRepo", { max: 500 }),
|
|
15
|
+
provider: optionalEnum(value.provider, "provider", ISSUE_PROVIDERS) as IssueProvider | undefined,
|
|
16
|
+
repo: cleanString(value.repo, "repo", { max: 500 }),
|
|
17
|
+
externalKey: cleanString(value.externalKey, "externalKey", { max: 240 }),
|
|
18
|
+
url: cleanString(value.url, "url", { max: 2000 }),
|
|
19
|
+
title: cleanString(value.title, "title", { max: 500 }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanAssociationBody(body: unknown) {
|
|
24
|
+
if (!isRecord(body)) throw new Error("issue association body required");
|
|
25
|
+
return {
|
|
26
|
+
...cleanIssueInput(body),
|
|
27
|
+
entityType: cleanEntityType(body.entityType),
|
|
28
|
+
entityId: cleanString(body.entityId, "entityId", { required: true, max: 240 })!,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function routeError(err: unknown): Response {
|
|
33
|
+
return error(err instanceof Error ? err.message : "issue associations route failed");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const postIssueAssociationRoute: Handler = async (req) => {
|
|
37
|
+
const parsed = await parseBody<unknown>(req);
|
|
38
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
39
|
+
try {
|
|
40
|
+
return json(associateIssueRef(cleanAssociationBody(parsed.body)));
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return routeError(err);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const getIssueAssociationEntityIssuesRoute: Handler = (_req, params) => {
|
|
47
|
+
try {
|
|
48
|
+
return json({
|
|
49
|
+
issues: listIssueRefsForEntity(
|
|
50
|
+
cleanEntityType(params.entityType),
|
|
51
|
+
cleanString(params.entityId, "entityId", { required: true, max: 240 })!,
|
|
52
|
+
),
|
|
53
|
+
});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return routeError(err);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getIssueAssociationIssueEntitiesRoute: Handler = (_req, params) => {
|
|
60
|
+
try {
|
|
61
|
+
const issueRef = resolveAssociationIssueRef({ issueRefId: cleanString(params.issueRefId, "issueRefId", { required: true, max: 120 })! });
|
|
62
|
+
return json({ issueRef, entities: listIssueEntities(issueRef.id) });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return routeError(err);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const deleteIssueAssociationRoute: Handler = async (req) => {
|
|
69
|
+
const parsed = await parseBody<unknown>(req);
|
|
70
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
71
|
+
try {
|
|
72
|
+
return json(removeIssueAssociation(cleanAssociationBody(parsed.body)));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return routeError(err);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { reopenIssueLifecycle } from "../services/issue-lifecycle";
|
|
2
|
+
import { cleanEnum, cleanString, optionalEnum } from "../validation";
|
|
3
|
+
import { error, json, parseBody, type Handler } from "./_shared";
|
|
4
|
+
import { ISSUE_ASSOCIATION_ENTITY_TYPES, ISSUE_PROVIDERS, isRecord, type IssueAssociationEntityType, type IssueProvider } from "../types";
|
|
5
|
+
|
|
6
|
+
function cleanReopenBody(body: unknown) {
|
|
7
|
+
if (!isRecord(body)) throw new Error("issue lifecycle reopen body required");
|
|
8
|
+
return {
|
|
9
|
+
issueRefId: cleanString(body.issueRefId, "issueRefId", { max: 120 }),
|
|
10
|
+
ref: cleanString(body.ref, "ref", { max: 1000 }),
|
|
11
|
+
defaultRepo: cleanString(body.defaultRepo ?? body.repo, "defaultRepo", { max: 500 }),
|
|
12
|
+
provider: optionalEnum(body.provider, "provider", ISSUE_PROVIDERS) as IssueProvider | undefined,
|
|
13
|
+
repo: cleanString(body.repo, "repo", { max: 500 }),
|
|
14
|
+
externalKey: cleanString(body.externalKey, "externalKey", { max: 240 }),
|
|
15
|
+
url: cleanString(body.url, "url", { max: 2000 }),
|
|
16
|
+
title: cleanString(body.title, "title", { max: 500 }),
|
|
17
|
+
comment: cleanString(body.comment, "comment", { max: 2000 }),
|
|
18
|
+
agentId: cleanString(body.agentId, "agentId", { max: 200 }),
|
|
19
|
+
entityType: body.entityType === undefined ? undefined : cleanEnum(body.entityType, "entityType", ISSUE_ASSOCIATION_ENTITY_TYPES) as IssueAssociationEntityType,
|
|
20
|
+
entityId: cleanString(body.entityId, "entityId", { max: 240 }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const postIssueLifecycleReopenRoute: Handler = async (req) => {
|
|
25
|
+
const parsed = await parseBody<unknown>(req);
|
|
26
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
27
|
+
try {
|
|
28
|
+
const result = await reopenIssueLifecycle(cleanReopenBody(parsed.body));
|
|
29
|
+
if (result.action === "failed") return error(result.error, 502);
|
|
30
|
+
return json(result);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return error(err instanceof Error ? err.message : "issue lifecycle route failed");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { deleteIssueRefMapping, getIssueRef, listIssueRefs, patchIssueRefMapping, resolveIssueRefMapping, upsertIssueRefMapping, type ListIssueRefsOptions } from "../services/issue-refs";
|
|
2
|
+
import { cleanEnum, cleanString, optionalEnum } from "../validation";
|
|
3
|
+
import { error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
|
|
4
|
+
import { ISSUE_PROVIDERS, ISSUE_REF_STATUSES, isRecord, type IssueProvider, type IssueRefStatus } from "../types";
|
|
5
|
+
|
|
6
|
+
function cleanStatus(value: unknown): IssueRefStatus | undefined {
|
|
7
|
+
return optionalEnum(value, "status", ISSUE_REF_STATUSES) as IssueRefStatus | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cleanProvider(value: unknown): IssueProvider {
|
|
11
|
+
return cleanEnum(value, "provider", ISSUE_PROVIDERS) as IssueProvider;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cleanTimestamp(value: unknown): number | undefined {
|
|
15
|
+
if (value === undefined || value === null) return undefined;
|
|
16
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("statusSyncedAt must be a non-negative integer");
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cleanUpsertBody(body: unknown) {
|
|
21
|
+
if (!isRecord(body)) throw new Error("issue ref body required");
|
|
22
|
+
return {
|
|
23
|
+
provider: cleanProvider(body.provider),
|
|
24
|
+
repo: cleanString(body.repo, "repo", { required: true, max: 500 })!,
|
|
25
|
+
externalKey: cleanString(body.externalKey, "externalKey", { required: true, max: 240 })!,
|
|
26
|
+
url: cleanString(body.url, "url", { required: true, max: 2000 })!,
|
|
27
|
+
title: cleanString(body.title, "title", { max: 500 }),
|
|
28
|
+
status: cleanStatus(body.status),
|
|
29
|
+
statusSyncedAt: cleanTimestamp(body.statusSyncedAt),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cleanPatchBody(body: unknown) {
|
|
34
|
+
if (!isRecord(body)) throw new Error("issue ref patch body required");
|
|
35
|
+
return {
|
|
36
|
+
url: cleanString(body.url, "url", { max: 2000 }),
|
|
37
|
+
title: cleanString(body.title, "title", { max: 500 }),
|
|
38
|
+
status: cleanStatus(body.status),
|
|
39
|
+
statusSyncedAt: cleanTimestamp(body.statusSyncedAt),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cleanResolveBody(body: unknown) {
|
|
44
|
+
if (!isRecord(body)) throw new Error("issue ref resolve body required");
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const listIssueRefsRoute: Handler = (req) => {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(req.url);
|
|
51
|
+
const rawLimit = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500, clamp: true });
|
|
52
|
+
if (Number.isNaN(rawLimit)) return error("limit must be an integer");
|
|
53
|
+
const opts: ListIssueRefsOptions = {
|
|
54
|
+
provider: optionalEnum(url.searchParams.get("provider") ?? undefined, "provider", ISSUE_PROVIDERS) as IssueProvider | undefined,
|
|
55
|
+
repo: cleanString(url.searchParams.get("repo") ?? undefined, "repo", { max: 500 }),
|
|
56
|
+
status: optionalEnum(url.searchParams.get("status") ?? undefined, "status", ISSUE_REF_STATUSES) as IssueRefStatus | undefined,
|
|
57
|
+
limit: rawLimit ?? undefined,
|
|
58
|
+
};
|
|
59
|
+
return json({ issueRefs: listIssueRefs(opts) });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return error(err instanceof Error ? err.message : "issue refs route failed");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const postIssueRefRoute: Handler = async (req) => {
|
|
66
|
+
const parsed = await parseBody<unknown>(req);
|
|
67
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
68
|
+
try {
|
|
69
|
+
return json({ issueRef: upsertIssueRefMapping(cleanUpsertBody(parsed.body)) });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return error(err instanceof Error ? err.message : "issue refs route failed");
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const resolveIssueRefRoute: Handler = async (req) => {
|
|
76
|
+
const url = new URL(req.url);
|
|
77
|
+
const body = req.method === "POST" ? await parseBody<unknown>(req) : null;
|
|
78
|
+
if (body && !body.ok) return error(body.error, body.status);
|
|
79
|
+
try {
|
|
80
|
+
const value = body ? cleanResolveBody(body.body) : Object.fromEntries(url.searchParams.entries());
|
|
81
|
+
const ref = cleanString(value.ref, "ref", { max: 1000 });
|
|
82
|
+
const issueRef = ref
|
|
83
|
+
? resolveIssueRefMapping({
|
|
84
|
+
ref,
|
|
85
|
+
defaultRepo: cleanString(value.defaultRepo ?? value.repo, "defaultRepo", { max: 500 }),
|
|
86
|
+
provider: optionalEnum(value.provider, "provider", ISSUE_PROVIDERS, "github") as IssueProvider,
|
|
87
|
+
title: cleanString(value.title, "title", { max: 500 }),
|
|
88
|
+
status: cleanStatus(value.status),
|
|
89
|
+
statusSyncedAt: cleanTimestamp(value.statusSyncedAt),
|
|
90
|
+
})
|
|
91
|
+
: resolveIssueRefMapping({
|
|
92
|
+
provider: cleanProvider(value.provider),
|
|
93
|
+
repo: cleanString(value.repo, "repo", { required: true, max: 500 })!,
|
|
94
|
+
externalKey: cleanString(value.externalKey, "externalKey", { required: true, max: 240 })!,
|
|
95
|
+
url: cleanString(value.url, "url", { max: 2000 }),
|
|
96
|
+
title: cleanString(value.title, "title", { max: 500 }),
|
|
97
|
+
status: cleanStatus(value.status),
|
|
98
|
+
statusSyncedAt: cleanTimestamp(value.statusSyncedAt),
|
|
99
|
+
});
|
|
100
|
+
return json({ issueRef });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return error(err instanceof Error ? err.message : "issue refs route failed");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const getIssueRefRoute: Handler = (_req, params) => {
|
|
107
|
+
try {
|
|
108
|
+
const id = cleanString(params.id, "id", { required: true, max: 120 })!;
|
|
109
|
+
const issueRef = getIssueRef(id);
|
|
110
|
+
if (!issueRef) return error("issue ref not found", 404);
|
|
111
|
+
return json({ issueRef });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return error(err instanceof Error ? err.message : "issue refs route failed");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const patchIssueRefRoute: Handler = async (req, params) => {
|
|
118
|
+
const parsed = await parseBody<unknown>(req);
|
|
119
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
120
|
+
try {
|
|
121
|
+
const issueRef = patchIssueRefMapping(cleanString(params.id, "id", { required: true, max: 120 })!, cleanPatchBody(parsed.body));
|
|
122
|
+
if (!issueRef) return error("issue ref not found", 404);
|
|
123
|
+
return json({ issueRef });
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return error(err instanceof Error ? err.message : "issue refs route failed");
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const deleteIssueRefRoute: Handler = (_req, params) => {
|
|
130
|
+
try {
|
|
131
|
+
const deleted = deleteIssueRefMapping(cleanString(params.id, "id", { required: true, max: 120 })!);
|
|
132
|
+
if (!deleted) return error("issue ref not found", 404);
|
|
133
|
+
return json({ deleted });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return error(err instanceof Error ? err.message : "issue refs route failed");
|
|
136
|
+
}
|
|
137
|
+
};
|
|
@@ -85,7 +85,8 @@ export const postOrchestratorBootstrapExchange: Handler = async (req) => {
|
|
|
85
85
|
baseDir,
|
|
86
86
|
createdBy: auth.jti ? `bootstrap:${auth.jti}` : "bootstrap",
|
|
87
87
|
});
|
|
88
|
-
|
|
88
|
+
// One-shot bootstrap token consumed: hard-revoke (rotation), never re-mintable.
|
|
89
|
+
if (auth.jti) revokeToken(auth.jti, "rotation");
|
|
89
90
|
return json(runtimeToken, 201);
|
|
90
91
|
} catch (e) {
|
|
91
92
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
@@ -364,6 +364,26 @@ function orchestratorSelfUpgradeError(orch: ReturnType<typeof getOrchestrator>):
|
|
|
364
364
|
return undefined;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
// One-time, server-authority migration of host-local provider config files into the
|
|
368
|
+
// central provider-config rows (#465). The orchestrator reports its files via the
|
|
369
|
+
// command result; the server seeds the rows (see patchCommand). Capability-gated so
|
|
370
|
+
// pre-#465 orchestrators that lack the handler never get a command they'd reject.
|
|
371
|
+
function dispatchProviderConfigMigration(
|
|
372
|
+
orch: NonNullable<ReturnType<typeof getOrchestrator>>,
|
|
373
|
+
requestedBy = "system",
|
|
374
|
+
): ReturnType<typeof createCommand> | null {
|
|
375
|
+
if (!orch.capabilities?.migrateProviderConfig) return null;
|
|
376
|
+
const command = createCommand({
|
|
377
|
+
type: "orchestrator.migrate-provider-config",
|
|
378
|
+
source: "system",
|
|
379
|
+
target: orch.agentId,
|
|
380
|
+
ttlMs: 10 * 60_000,
|
|
381
|
+
params: { orchestratorId: orch.id, requestedBy },
|
|
382
|
+
});
|
|
383
|
+
emitCommand(command);
|
|
384
|
+
return command;
|
|
385
|
+
}
|
|
386
|
+
|
|
367
387
|
function reconcileOrchestratorUpgrade(orch: ReturnType<typeof getOrchestrator>): void {
|
|
368
388
|
if (!orch) return;
|
|
369
389
|
const up = orch.upgrade;
|
|
@@ -389,6 +409,9 @@ function reconcileOrchestratorUpgrade(orch: ReturnType<typeof getOrchestrator>):
|
|
|
389
409
|
view: "orchestrators",
|
|
390
410
|
metadata: { orchestratorId: orch.id, version: reported, commandId: up.commandId },
|
|
391
411
|
});
|
|
412
|
+
// Upgrade settled — the orchestrator is now running new-enough code to handle
|
|
413
|
+
// the provider-config migration, so seed the central rows from its local files (#465).
|
|
414
|
+
dispatchProviderConfigMigration(orch);
|
|
392
415
|
emitOrchestratorStatus(orch.id);
|
|
393
416
|
} else if (up.status === "pending" && Date.now() - up.requestedAt > ORCH_UPGRADE_DEADLINE_MS) {
|
|
394
417
|
const errMsg = `upgrade timed out; orchestrator still reports ${reported ?? "unknown"} (wanted ${up.desiredVersion})`;
|
|
@@ -416,9 +439,27 @@ export const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
416
439
|
if (!orch) return error("orchestrator not found", 404);
|
|
417
440
|
|
|
418
441
|
if (!isRecord(parsed.body)) return error("body required");
|
|
419
|
-
const action = optionalEnum(parsed.body.action, "action", ["restart", "shutdown", "upgrade"] as const);
|
|
442
|
+
const action = optionalEnum(parsed.body.action, "action", ["restart", "shutdown", "upgrade", "migrate-provider-config"] as const);
|
|
420
443
|
if (!action) return error("action required");
|
|
421
444
|
|
|
445
|
+
if (action === "migrate-provider-config") {
|
|
446
|
+
const denied = authorizeRoute(req, { scope: "command:write", resource: { orchestratorId: orch.id } });
|
|
447
|
+
if (denied) return denied;
|
|
448
|
+
const command = dispatchProviderConfigMigration(orch, "dashboard");
|
|
449
|
+
if (!command) return error(`orchestrator ${orch.id} does not support provider-config migration; upgrade it first`, 409);
|
|
450
|
+
auditEvent({
|
|
451
|
+
clientId: `server-orchestrator-migrate-provider-config-${orch.id}-${command.id}`,
|
|
452
|
+
kind: "state",
|
|
453
|
+
title: "Provider config migration requested",
|
|
454
|
+
body: orch.id,
|
|
455
|
+
meta: orch.id,
|
|
456
|
+
icon: "ti-arrow-up-circle",
|
|
457
|
+
view: "orchestrators",
|
|
458
|
+
metadata: { orchestratorId: orch.id, commandId: command.id, ...authAuditMetadata(req), ...dashboardAttribution(req, parsed.body.surface) },
|
|
459
|
+
});
|
|
460
|
+
return json({ ok: true, action, command }, 202);
|
|
461
|
+
}
|
|
462
|
+
|
|
422
463
|
if (action === "upgrade") {
|
|
423
464
|
const denied = authorizeRoute(req, { scope: "command:write", resource: { orchestratorId: orch.id } });
|
|
424
465
|
if (denied) return denied;
|
package/src/routes/tokens.ts
CHANGED
|
@@ -337,6 +337,6 @@ export const deleteTokenProfileRoute: Handler = (_req, params) => {
|
|
|
337
337
|
};
|
|
338
338
|
|
|
339
339
|
export const postTokenRevoke: Handler = (_req, params) => {
|
|
340
|
-
if (!revokeToken(params.jti
|
|
340
|
+
if (!revokeToken(params.jti!, "admin")) return error("token not found or already revoked", 404);
|
|
341
341
|
return json({ ok: true });
|
|
342
342
|
};
|
package/src/runtime-tokens.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createToken, getTokenProfile, revokeToken } from "./token-db";
|
|
1
|
+
import { createToken, getTokenProfile, hardenStaleRevocation, revokeToken } from "./token-db";
|
|
2
2
|
import { verifyComponentTokenAllowExpired } from "./security";
|
|
3
3
|
import { spawnGrantForProfile } from "./config-store";
|
|
4
4
|
import type { TokenRecord } from "./types";
|
|
@@ -26,20 +26,22 @@ interface RuntimeTokenResult {
|
|
|
26
26
|
// a long-dead token to a sane window — a live session re-mints well within this.
|
|
27
27
|
const REMINT_MAX_EXPIRED_AGE_SECONDS = 30 * 24 * 60 * 60; // 30 days
|
|
28
28
|
|
|
29
|
-
// Orchestrator-mediated re-mint: given a runner's current (possibly expired
|
|
30
|
-
// and the calling orchestrator's id, verify the token belongs
|
|
31
|
-
// orchestrator and issue a fresh provider-agent token cloning
|
|
32
|
-
// token is revoked
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// runner self-heal design: the relay
|
|
36
|
-
//
|
|
29
|
+
// Orchestrator-mediated re-mint: given a runner's current (possibly expired OR
|
|
30
|
+
// stale-revoked) token and the calling orchestrator's id, verify the token belongs
|
|
31
|
+
// to a runner of THIS orchestrator and issue a fresh provider-agent token cloning
|
|
32
|
+
// its scope. The old token is then hard-revoked so it cannot be re-minted twice.
|
|
33
|
+
// The caller MUST already be authenticated/authorized as the orchestrator — this
|
|
34
|
+
// only establishes that the presented token is a genuine runner token owned by that
|
|
35
|
+
// orchestrator. See approach #1/#2 in the runner self-heal design (#484): the relay
|
|
36
|
+
// stays strict, the orchestrator's standing privilege is the authorization, the
|
|
37
|
+
// signed token is the identity. A token revoked as "stale" (the restart-strand of a
|
|
38
|
+
// still-live process) is heal-able here; a hard kill (admin/rotation/removal) is not.
|
|
37
39
|
export function reissueRunnerRuntimeToken(input: {
|
|
38
40
|
expiredToken: string;
|
|
39
41
|
orchestratorId: string;
|
|
40
42
|
createdBy?: string;
|
|
41
43
|
}): RuntimeTokenResult | { error: string } {
|
|
42
|
-
const payload = verifyComponentTokenAllowExpired(input.expiredToken);
|
|
44
|
+
const payload = verifyComponentTokenAllowExpired(input.expiredToken, { allowStaleRevoked: true });
|
|
43
45
|
if (!payload) return { error: "invalid or revoked runner token" };
|
|
44
46
|
if (payload.role !== "provider" || !payload.sub.startsWith("runner:")) {
|
|
45
47
|
return { error: "not a runner token" };
|
|
@@ -60,7 +62,9 @@ export function reissueRunnerRuntimeToken(input: {
|
|
|
60
62
|
constraints: payload.constraints,
|
|
61
63
|
createdBy: input.createdBy ?? `remint:${payload.jti ?? "unknown"}`,
|
|
62
64
|
});
|
|
63
|
-
|
|
65
|
+
// Hard-revoke the consumed token so it can't mint again. revokeToken no-ops on an
|
|
66
|
+
// already-revoked (stale) token, so harden that case explicitly to "rotation".
|
|
67
|
+
if (payload.jti && !revokeToken(payload.jti, "rotation")) hardenStaleRevocation(payload.jti);
|
|
64
68
|
return reissued;
|
|
65
69
|
}
|
|
66
70
|
|
package/src/security.ts
CHANGED
|
@@ -188,6 +188,10 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
188
188
|
if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
|
|
189
189
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
190
190
|
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
191
|
+
if (pathname.startsWith("/api/issue-associations")) return method === "GET" ? "issue:read" : "issue:write";
|
|
192
|
+
if (pathname.startsWith("/api/issue-activity")) return method === "GET" ? "issue:read" : "issue:write";
|
|
193
|
+
if (pathname.startsWith("/api/issue-lifecycle")) return method === "GET" ? "issue:read" : "issue:write";
|
|
194
|
+
if (pathname.startsWith("/api/issue-refs")) return method === "GET" ? "issue:read" : "issue:write";
|
|
191
195
|
if (pathname.startsWith("/api/projects")) return method === "GET" ? "project:read" : "project:write";
|
|
192
196
|
// Insights config (the feature toggle) stays admin-only via the default; only the
|
|
193
197
|
// mechanical observation feed is writable by lower-privilege callers.
|
|
@@ -269,6 +273,10 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
269
273
|
if (pathname.startsWith("/api/plans")) return method === "GET" ? "plans:read" : "plans:write";
|
|
270
274
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
271
275
|
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
276
|
+
if (pathname.startsWith("/api/issue-associations")) return method === "GET" ? "issue:read" : "issue:write";
|
|
277
|
+
if (pathname.startsWith("/api/issue-activity")) return method === "GET" ? "issue:read" : "issue:write";
|
|
278
|
+
if (pathname.startsWith("/api/issue-lifecycle")) return method === "GET" ? "issue:read" : "issue:write";
|
|
279
|
+
if (pathname.startsWith("/api/issue-refs")) return method === "GET" ? "issue:read" : "issue:write";
|
|
272
280
|
// Fail-closed default returns system:admin, so this case is load-bearing: without it
|
|
273
281
|
// every /api/projects call from a scoped component token would 403-blackhole (#405).
|
|
274
282
|
if (pathname.startsWith("/api/projects")) return method === "GET" ? "project:read" : "project:write";
|
|
@@ -331,20 +339,60 @@ export function verifyComponentToken(token: string, nowSeconds = Math.floor(Date
|
|
|
331
339
|
return payload;
|
|
332
340
|
}
|
|
333
341
|
|
|
342
|
+
// Why a token was revoked (#484). The reason decides whether an orchestrator may
|
|
343
|
+
// re-mint it: a "stale" revocation (relay marked an agent stale across its own
|
|
344
|
+
// restart/reconnect) is recoverable for a process the orchestrator can confirm is
|
|
345
|
+
// alive; every other reason is a hard kill. This is the single home for the
|
|
346
|
+
// taxonomy — callers import the type, never re-declare the strings.
|
|
347
|
+
export type TokenRevokeReason = "stale" | "rotation" | "admin" | "agent-removed" | "pruned";
|
|
348
|
+
|
|
349
|
+
// The ONLY revocation reason an orchestrator-mediated re-mint may heal. A genuine
|
|
350
|
+
// kill — admin revoke, rotation, agent removal/prune, or a legacy NULL reason —
|
|
351
|
+
// stays an unconditional hard kill.
|
|
352
|
+
const REMINTABLE_REVOKE_REASONS: ReadonlySet<TokenRevokeReason> = new Set(["stale"]);
|
|
353
|
+
|
|
354
|
+
// Returns undefined if the token is not revoked, otherwise its reason string
|
|
355
|
+
// (NULL/legacy → null, treated as a hard kill).
|
|
356
|
+
function tokenRevocationReason(jti: string): string | null | undefined {
|
|
357
|
+
try {
|
|
358
|
+
const row = getDb().query("SELECT revoked_at, revoked_reason FROM tokens WHERE jti = ?").get(jti) as
|
|
359
|
+
| { revoked_at?: number | null; revoked_reason?: string | null }
|
|
360
|
+
| undefined;
|
|
361
|
+
if (!row?.revoked_at) return undefined;
|
|
362
|
+
return row.revoked_reason ?? null;
|
|
363
|
+
} catch {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
334
368
|
// Verify a component token's signature and revocation WITHOUT enforcing expiry.
|
|
335
369
|
// Used only for orchestrator-mediated runner-token re-minting: an expired runner
|
|
336
370
|
// token is unforgeable proof of identity (it is HMAC-signed by this relay), even
|
|
337
371
|
// though it can no longer authenticate a request. The caller (an authenticated
|
|
338
372
|
// orchestrator) supplies the authorization; this just establishes which runner.
|
|
339
|
-
// Revoked tokens are
|
|
340
|
-
|
|
373
|
+
// Revoked tokens are rejected as a hard kill EXCEPT when the caller opts into
|
|
374
|
+
// `allowStaleRevoked` AND the token was revoked as "stale" (#484) — that case is the
|
|
375
|
+
// restart-strand of a still-live process, which the orchestrator heals by re-minting.
|
|
376
|
+
export function verifyComponentTokenAllowExpired(
|
|
377
|
+
token: string,
|
|
378
|
+
options?: { allowStaleRevoked?: boolean },
|
|
379
|
+
): ComponentToken | null {
|
|
341
380
|
const parts = token.split(".");
|
|
342
381
|
if (parts.length !== 3) return null;
|
|
343
382
|
const [headerRaw, payloadRaw, signature] = parts as [string, string, string];
|
|
344
383
|
if (!safeEqual(signature, hmac(`${headerRaw}.${payloadRaw}`))) return null;
|
|
345
384
|
const payload = parseBase64urlJson(payloadRaw);
|
|
346
385
|
if (!isComponentToken(payload)) return null;
|
|
347
|
-
if (payload.jti
|
|
386
|
+
if (payload.jti) {
|
|
387
|
+
const reason = tokenRevocationReason(payload.jti);
|
|
388
|
+
if (reason !== undefined) {
|
|
389
|
+
const remintable =
|
|
390
|
+
options?.allowStaleRevoked === true &&
|
|
391
|
+
typeof reason === "string" &&
|
|
392
|
+
REMINTABLE_REVOKE_REASONS.has(reason as TokenRevokeReason);
|
|
393
|
+
if (!remintable) return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
348
396
|
return payload;
|
|
349
397
|
}
|
|
350
398
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ISSUE_ACTIVITY_ACTIONS,
|
|
3
|
+
listAgentIssueActivity,
|
|
4
|
+
listIssueActivityAgents,
|
|
5
|
+
recordIssueActivity,
|
|
6
|
+
resolveIssueActivityRef,
|
|
7
|
+
type IssueActivityAction,
|
|
8
|
+
type RecordIssueActivityInput,
|
|
9
|
+
} from "../db/activity";
|
|
10
|
+
import { emitActivityEvent } from "../sse";
|
|
11
|
+
import type { IssueProvider } from "../types";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
ISSUE_ACTIVITY_ACTIONS,
|
|
15
|
+
listAgentIssueActivity,
|
|
16
|
+
listIssueActivityAgents,
|
|
17
|
+
recordIssueActivity,
|
|
18
|
+
type IssueActivityAction,
|
|
19
|
+
type RecordIssueActivityInput,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function recordAndEmitIssueActivity(input: RecordIssueActivityInput) {
|
|
23
|
+
const event = recordIssueActivity(input);
|
|
24
|
+
emitActivityEvent(event);
|
|
25
|
+
return event;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveIssueActivityMapping(input: { issueRefId?: string; issueRef?: string; defaultRepo?: string; provider?: IssueProvider }) {
|
|
29
|
+
return resolveIssueActivityRef(input);
|
|
30
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
associateIssue,
|
|
3
|
+
listEntitiesForIssue,
|
|
4
|
+
listIssuesForEntity,
|
|
5
|
+
removeAssociation,
|
|
6
|
+
} from "../db/issue-associations";
|
|
7
|
+
import { getIssueRef, resolveIssueRefByExternalKey, resolveIssueRefFromString } from "../db/issue-refs";
|
|
8
|
+
import type { IssueAssociationEntityType, IssueAssociationIssue, IssueProvider } from "../types";
|
|
9
|
+
import { ValidationError } from "../db/connection";
|
|
10
|
+
|
|
11
|
+
export interface ResolveAssociationIssueInput {
|
|
12
|
+
issueRefId?: string;
|
|
13
|
+
ref?: string;
|
|
14
|
+
defaultRepo?: string;
|
|
15
|
+
provider?: IssueProvider;
|
|
16
|
+
repo?: string;
|
|
17
|
+
externalKey?: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AssociateIssueInput extends ResolveAssociationIssueInput {
|
|
23
|
+
entityType: IssueAssociationEntityType;
|
|
24
|
+
entityId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveAssociationIssueRef(input: ResolveAssociationIssueInput) {
|
|
28
|
+
if (input.issueRefId) {
|
|
29
|
+
const issueRef = getIssueRef(input.issueRefId);
|
|
30
|
+
if (!issueRef) throw new ValidationError(`issue ref ${input.issueRefId} not found`);
|
|
31
|
+
return issueRef;
|
|
32
|
+
}
|
|
33
|
+
if (input.ref) {
|
|
34
|
+
return resolveIssueRefFromString({
|
|
35
|
+
ref: input.ref,
|
|
36
|
+
defaultRepo: input.defaultRepo ?? input.repo,
|
|
37
|
+
provider: input.provider,
|
|
38
|
+
title: input.title,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (input.provider && input.repo && input.externalKey) {
|
|
42
|
+
return resolveIssueRefByExternalKey({
|
|
43
|
+
provider: input.provider,
|
|
44
|
+
repo: input.repo,
|
|
45
|
+
externalKey: input.externalKey,
|
|
46
|
+
url: input.url,
|
|
47
|
+
title: input.title,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
throw new ValidationError("issueRefId, ref, or provider/repo/externalKey required");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function associateIssueRef(input: AssociateIssueInput) {
|
|
54
|
+
const issueRef = resolveAssociationIssueRef(input);
|
|
55
|
+
return {
|
|
56
|
+
association: associateIssue(issueRef.id, input.entityType, input.entityId),
|
|
57
|
+
issueRef,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function listIssueRefsForEntity(entityType: IssueAssociationEntityType, entityId: string): IssueAssociationIssue[] {
|
|
62
|
+
return listIssuesForEntity(entityType, entityId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function listIssueEntities(issueRefId: string) {
|
|
66
|
+
return listEntitiesForIssue(issueRefId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function removeIssueAssociation(input: AssociateIssueInput): { deleted: boolean } {
|
|
70
|
+
const issueRef = resolveAssociationIssueRef(input);
|
|
71
|
+
return { deleted: removeAssociation(issueRef.id, input.entityType, input.entityId) };
|
|
72
|
+
}
|