@desplega.ai/agent-swarm 1.129.0 → 1.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-kfv2dawe.js → actions-w53mbns9.js} +11 -11
  3. package/dist/{anthropic-messages-3xgme8my.js → anthropic-messages-rjmd2e7n.js} +5 -5
  4. package/dist/{app-q8vjk6e8.js → app-m9gwqsqv.js} +8 -8
  5. package/dist/{artifact-hg4t1nx7.js → artifact-jysj3wrg.js} +2 -2
  6. package/dist/{assistant-g037r345.js → assistant-bp65m6f3.js} +13 -13
  7. package/dist/{azure-openai-responses-xa4sh9f6.js → azure-openai-responses-c0yjmbfd.js} +8 -5
  8. package/dist/{boot-reembed-18pzsrxa.js → boot-reembed-62p0at2k.js} +6 -6
  9. package/dist/{boot-reembed-wpks2cj8.js → boot-reembed-xmfp6wdh.js} +7 -7
  10. package/dist/{boot-scrub-logs-n7szd57r.js → boot-scrub-logs-rr8q2dms.js} +5 -5
  11. package/dist/{claude-adapter-qz4r0ajt.js → claude-adapter-2y47mrkd.js} +9 -9
  12. package/dist/{cli-mhjnbgb6.js → cli-0rcyq6d9.js} +450 -186
  13. package/dist/{cli-ttkmehy0.js → cli-2q72qz2s.js} +274 -25
  14. package/dist/{cli-gztgc3hz.js → cli-2wadx7p2.js} +2 -2
  15. package/dist/cli-3fj60etf.js +153 -0
  16. package/dist/{cli-7ecgm5v0.js → cli-3kf51j15.js} +1 -1
  17. package/dist/{cli-vgexpfsr.js → cli-57khth04.js} +6 -3
  18. package/dist/{cli-2mbe7vk0.js → cli-5dsbcjjw.js} +1 -1
  19. package/dist/{cli-etr62zy6.js → cli-5kx36evn.js} +2 -2
  20. package/dist/{cli-v2tr6amf.js → cli-5n6e5yfn.js} +30 -8
  21. package/dist/{cli-hx02s9zq.js → cli-5v4rw6yd.js} +2 -2
  22. package/dist/{cli-v37yr6mf.js → cli-5ze9rv97.js} +4 -4
  23. package/dist/{cli-ch7t5v7n.js → cli-6map995v.js} +9 -4
  24. package/dist/{cli-h0457a67.js → cli-70xqj0z3.js} +3 -3
  25. package/dist/{cli-kk4f9r17.js → cli-72q8m8gk.js} +8 -6
  26. package/dist/{cli-7padk69a.js → cli-78xwvkhj.js} +2 -2
  27. package/dist/{cli-dxtxacy7.js → cli-9dr5qzc0.js} +45 -8
  28. package/dist/{cli-17zxdgsq.js → cli-a40rnf71.js} +6 -6
  29. package/dist/{cli-t53vnnxz.js → cli-a4jzdpa3.js} +2514 -1055
  30. package/dist/{cli-w10fkrf1.js → cli-akdvwc9v.js} +354 -115
  31. package/dist/{cli-1qkk5er0.js → cli-bmze986d.js} +1 -1
  32. package/dist/{cli-ga12dc1d.js → cli-bqw0mt1n.js} +3 -3
  33. package/dist/{cli-0sf92efp.js → cli-cdeg08wx.js} +1 -1
  34. package/dist/{cli-a8ynhkyj.js → cli-cvgjwnh1.js} +17 -15
  35. package/dist/{cli-9fxg3v5m.js → cli-d2tfkcga.js} +4 -4
  36. package/dist/{cli-nhcd7x43.js → cli-d3x3x1c9.js} +2 -2
  37. package/dist/{cli-6nh2qxvh.js → cli-darcwax3.js} +1 -1
  38. package/dist/{cli-yjbgmysx.js → cli-e7ckcg4v.js} +12 -12
  39. package/dist/{cli-4n2gcjpd.js → cli-efxfnq6q.js} +4 -3
  40. package/dist/{cli-z9xqqhfk.js → cli-er3a4bb0.js} +1 -1
  41. package/dist/{cli-xg2eezyv.js → cli-f1am9szv.js} +1 -1
  42. package/dist/{cli-xy4cwsm9.js → cli-fywms69x.js} +1 -1
  43. package/dist/{cli-33jj76y8.js → cli-g6b8x1h8.js} +1 -1
  44. package/dist/{cli-epsvdckx.js → cli-gm9cbhbq.js} +20 -10
  45. package/dist/{cli-rkndnn90.js → cli-k9qkf1hw.js} +6 -4
  46. package/dist/{cli-zejvt395.js → cli-kh3ba2zf.js} +1 -1
  47. package/dist/{cli-gb25p1h2.js → cli-nm5gtp1x.js} +2 -2
  48. package/dist/{cli-akxnpx17.js → cli-pm6xbh84.js} +1829 -1629
  49. package/dist/{cli-2k0ztbfp.js → cli-psh3c4va.js} +1 -1
  50. package/dist/{cli-39vgrsep.js → cli-qv61hp37.js} +2 -2
  51. package/dist/{cli-hv2fndhm.js → cli-r6kqqbj0.js} +6 -4
  52. package/dist/{cli-1y1cm570.js → cli-rzbpmxdr.js} +1 -1
  53. package/dist/{cli-mmrk00vp.js → cli-t1xze5qh.js} +1 -1
  54. package/dist/{cli-g252a9ky.js → cli-t55yw3a1.js} +2 -2
  55. package/dist/{cli-7bz8dzqa.js → cli-t76vw5db.js} +1184 -1522
  56. package/dist/{cli-4n4mdv21.js → cli-xpxde7rm.js} +7 -3
  57. package/dist/{cli-fpaa5k2q.js → cli-yw8vghx2.js} +54 -18
  58. package/dist/cli-zsspvvm2.js +490 -0
  59. package/dist/cli.js +27 -25
  60. package/dist/{codex-adapter-7v6t7ff6.js → codex-adapter-d24q75wk.js} +7 -7
  61. package/dist/{codex-hook-amndawyb.js → codex-hook-hq54t9ft.js} +2 -2
  62. package/dist/{codex-session-runner-e7j4khe2.js → codex-session-runner-g50vcsex.js} +7 -7
  63. package/dist/{commands-ex0k5tbf.js → commands-c3nrc5hg.js} +5 -5
  64. package/dist/{db-df7jatfc.js → db-nt7fg3tw.js} +5 -5
  65. package/dist/{google-generative-ai-y42hnw9p.js → google-generative-ai-hysrd8qx.js} +7 -5
  66. package/dist/{google-vertex-4sg66gb6.js → google-vertex-p5arrga0.js} +7 -5
  67. package/dist/{handlers-1j2cfqt2.js → handlers-fkr6swat.js} +13 -13
  68. package/dist/{hook-nx9a0mj3.js → hook-77wpvnjc.js} +7 -7
  69. package/dist/{hook-w6yaf6sy.js → hook-g019yehy.js} +8 -8
  70. package/dist/{http-4tz5h8wg.js → http-jewj0syk.js} +2993 -716
  71. package/dist/{index-0bpkzb09.js → index-070v4eyb.js} +3 -3
  72. package/dist/{index-0s67qv7h.js → index-6gm5he8h.js} +11 -11
  73. package/dist/{index-35gtv6gj.js → index-7n2xpbd4.js} +310 -40
  74. package/dist/{index-ker3gccb.js → index-9znhar3x.js} +4 -4
  75. package/dist/{index-5n2c8yay.js → index-hase6zbe.js} +2 -2
  76. package/dist/{index-kcf3tm9h.js → index-hfvggd0z.js} +12 -12
  77. package/dist/{index-7c0jqna0.js → index-hp1kdbjj.js} +13 -13
  78. package/dist/{keepalive-3j4a9168.js → keepalive-j0fjvqfd.js} +7 -7
  79. package/dist/{lead-6kpevymb.js → lead-1n8x30xe.js} +33 -33
  80. package/dist/{maintenance-m1k82ean.js → maintenance-gvev11sa.js} +7 -7
  81. package/dist/{mistral-conversations-bjkdfb6z.js → mistral-conversations-81fwav0j.js} +6 -6
  82. package/dist/{oauth-refresh-sweep-881k8e4q.js → oauth-refresh-sweep-s9ecsvh0.js} +6 -6
  83. package/dist/{onboard-c0ykffjn.js → onboard-pvm5b38w.js} +2 -2
  84. package/dist/{openai-codex-responses-xetseqvt.js → openai-codex-responses-5g27pd6f.js} +43 -23
  85. package/dist/{openai-completions-5r2g5axa.js → openai-completions-3vkmfz2a.js} +55 -11
  86. package/dist/{openai-responses-ce67hd23.js → openai-responses-dg4t7j7a.js} +8 -5
  87. package/dist/{openrouter-images-pzfp22jb.js → openrouter-images-g3gm8931.js} +1 -1
  88. package/dist/{otel-impl-sqagjerv.js → otel-impl-p6jvgd9c.js} +6 -4
  89. package/dist/{otel-3mp81ppd.js → otel-nqa12wf3.js} +2 -2
  90. package/dist/{pi-mono-adapter-k584k16g.js → pi-mono-adapter-t091s27g.js} +17117 -7517
  91. package/dist/{pricing-refresh-y82whbf9.js → pricing-refresh-k49rcyed.js} +7 -7
  92. package/dist/{rbac-roles-nvkqmzyd.js → rbac-roles-30pspvy7.js} +7 -7
  93. package/dist/{rbac-roles-4ht8gegc.js → rbac-roles-cr80g96t.js} +6 -6
  94. package/dist/{render-v2-wvqdgvvp.js → render-v2-xm457ng1.js} +8 -8
  95. package/dist/{seed-pricing-fy3rxhde.js → seed-pricing-yhpv35fr.js} +6 -6
  96. package/dist/{setup-ak5cnc6f.js → setup-nytnmwwr.js} +2 -2
  97. package/dist/{worker-d8769q8a.js → worker-msytr2cw.js} +33 -33
  98. package/openapi.json +34039 -11036
  99. package/package.json +8 -6
  100. package/src/apps/definition.ts +368 -26
  101. package/src/apps/row-store.ts +66 -10
  102. package/src/apps/schema-migrate.ts +95 -1
  103. package/src/apps/store.ts +13 -4
  104. package/src/apps/sync-run-as.ts +18 -0
  105. package/src/apps/sync.ts +934 -0
  106. package/src/apps/version.ts +16 -2
  107. package/src/assets/key.ts +8 -1
  108. package/src/be/asset-key-audit.ts +3 -1
  109. package/src/be/db-queries/mcp-oauth.ts +297 -24
  110. package/src/be/db.ts +42 -1
  111. package/src/be/migrations/129_asset_keys_apps_scripts.sql +161 -0
  112. package/src/be/scripts/db.ts +5 -2
  113. package/src/be/scripts/typecheck.ts +1 -0
  114. package/src/be/seed-scripts/catalog/app-sync-run.ts +32 -0
  115. package/src/be/seed-scripts/catalog/github-issues-pull.ts +102 -0
  116. package/src/be/seed-scripts/index.ts +18 -0
  117. package/src/be/seed-skills/index.ts +7 -0
  118. package/src/http/active-sessions.ts +26 -18
  119. package/src/http/agents.ts +94 -27
  120. package/src/http/api-keys.ts +91 -17
  121. package/src/http/approval-requests.ts +87 -10
  122. package/src/http/apps.ts +361 -53
  123. package/src/http/assets.ts +165 -31
  124. package/src/http/budgets.ts +7 -5
  125. package/src/http/codex-oauth-keep-warm.ts +21 -3
  126. package/src/http/config.ts +46 -16
  127. package/src/http/context.ts +33 -6
  128. package/src/http/ecosystem.ts +20 -6
  129. package/src/http/events.ts +24 -9
  130. package/src/http/favorites.ts +20 -6
  131. package/src/http/fs.ts +60 -17
  132. package/src/http/heartbeat.ts +18 -4
  133. package/src/http/inbox-state.ts +12 -6
  134. package/src/http/integrations.ts +25 -5
  135. package/src/http/kv.ts +36 -15
  136. package/src/http/mcp-bridge.ts +5 -1
  137. package/src/http/mcp-oauth.ts +332 -17
  138. package/src/http/mcp-servers.ts +56 -18
  139. package/src/http/memory.ts +282 -29
  140. package/src/http/metrics.ts +80 -17
  141. package/src/http/oauth-callback.ts +10 -4
  142. package/src/http/oauth-generic.ts +5 -1
  143. package/src/http/oauth-locks.ts +6 -3
  144. package/src/http/openapi.ts +57 -9
  145. package/src/http/page-proxy.ts +25 -5
  146. package/src/http/pages-public.ts +37 -20
  147. package/src/http/pages.ts +93 -28
  148. package/src/http/poll.ts +89 -5
  149. package/src/http/pricing.ts +9 -6
  150. package/src/http/prompt-templates.ts +85 -21
  151. package/src/http/route-def.ts +67 -4
  152. package/src/http/schedules.ts +63 -14
  153. package/src/http/script-connection-proxy.ts +30 -4
  154. package/src/http/script-connections.ts +393 -43
  155. package/src/http/script-runs.ts +73 -19
  156. package/src/http/scripts.ts +257 -39
  157. package/src/http/session-data.ts +87 -14
  158. package/src/http/sessions.ts +80 -5
  159. package/src/http/skills.ts +115 -35
  160. package/src/http/stats.ts +94 -13
  161. package/src/http/task-templates.ts +6 -4
  162. package/src/http/tasks.ts +230 -61
  163. package/src/http/trackers/jira.ts +90 -30
  164. package/src/http/trackers/linear.ts +46 -12
  165. package/src/http/users.ts +162 -25
  166. package/src/http/utils.ts +12 -2
  167. package/src/http/webhooks.ts +30 -16
  168. package/src/http/workflow-events.ts +18 -5
  169. package/src/http/workflows.ts +128 -39
  170. package/src/http/x.ts +37 -6
  171. package/src/oauth/ensure-mcp-token.ts +16 -1
  172. package/src/oauth/mcp-wrapper.ts +281 -26
  173. package/src/providers/pi-mono-adapter.ts +1 -1
  174. package/src/scripts-runtime/credential-broker/README.md +1 -1
  175. package/src/scripts-runtime/credential-broker/fetch-patch.ts +12 -2
  176. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  177. package/src/scripts-runtime/stdlib/fetch.ts +6 -0
  178. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  179. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  180. package/src/server.ts +2 -0
  181. package/src/tests/apps-rbac.test.ts +110 -0
  182. package/src/tests/apps-spike2.test.ts +4 -1
  183. package/src/tests/apps-sync-engine.test.ts +2540 -0
  184. package/src/tests/apps-sync.test.ts +1627 -0
  185. package/src/tests/asset-key-api.test.ts +48 -2
  186. package/src/tests/asset-key-invariants.test.ts +92 -0
  187. package/src/tests/asset-key-migration.test.ts +174 -8
  188. package/src/tests/asset-key-normalization.test.ts +2 -0
  189. package/src/tests/credential-broker.test.ts +27 -2
  190. package/src/tests/mcp-oauth-dcr-reuse.test.ts +743 -0
  191. package/src/tests/mcp-oauth-ensure-token.test.ts +45 -0
  192. package/src/tests/mcp-oauth-manual-client.test.ts +135 -2
  193. package/src/tests/mcp-oauth-queries.test.ts +153 -0
  194. package/src/tests/mcp-oauth-wrapper.test.ts +432 -0
  195. package/src/tests/memory-http-recall-gating.test.ts +2 -0
  196. package/src/tests/openapi-response-contract.test.ts +138 -0
  197. package/src/tests/rbac-admission-e2e.test.ts +68 -0
  198. package/src/tests/script-connections.test.ts +3 -0
  199. package/src/tests/scripts-runtime-fetch.test.ts +121 -0
  200. package/src/tests/scripts-runtime-secret-egress.test.ts +4 -2
  201. package/src/tests/seed-scripts.test.ts +2 -2
  202. package/src/tests/swarm-tool-result-gate.test.ts +32 -0
  203. package/src/tests/task-working-dir.test.ts +8 -4
  204. package/src/tests/whoami-route.test.ts +127 -0
  205. package/src/tests/workflow-engine-v2.test.ts +80 -7
  206. package/src/tests/workflow-swarm-script.test.ts +104 -0
  207. package/src/tools/app-get.ts +10 -2
  208. package/src/tools/app-rollback.ts +1 -0
  209. package/src/tools/app-sync.ts +112 -0
  210. package/src/tools/script-common.ts +20 -0
  211. package/src/tools/tool-config.ts +2 -1
  212. package/src/tools/utils.ts +11 -0
  213. package/src/tools/workflows/create-workflow.ts +4 -1
  214. package/src/types.ts +1394 -1206
  215. package/src/utils/template.ts +8 -1
  216. package/src/utils/zod-openapi.ts +11 -0
  217. package/src/workflows/engine.ts +27 -4
  218. package/src/workflows/executors/swarm-script.ts +28 -0
  219. package/src/workflows/retry-poller.ts +19 -2
  220. package/templates/skills/apps/content.md +94 -5
  221. package/templates/skills/asset-namespaces/content.md +2 -2
  222. package/templates/skills/scheduled-task-resilience/SKILL.md +102 -0
  223. package/templates/skills/scheduled-task-resilience/config.json +14 -0
  224. package/templates/skills/scheduled-task-resilience/content.md +97 -0
  225. package/dist/cli-7wmybrt5.js +0 -115
  226. package/dist/cli-d91pvmq1.js +0 -14
@@ -19,6 +19,61 @@ import { deriveApiBaseUrl, parseQueryParams } from "../utils";
19
19
  const MANUAL_WEBHOOK_INSTRUCTIONS =
20
20
  "See docs-site/.../guides/jira-integration.mdx for manual webhook registration steps.";
21
21
 
22
+ // ─── Response Schemas ────────────────────────────────────────────────────────
23
+
24
+ const JiraWebhookIdEntrySchema = z.object({
25
+ id: z.number().int(),
26
+ expiresAt: z.string(),
27
+ jql: z.string(),
28
+ });
29
+
30
+ /** Shape returned by both GET /status and POST /refresh (buildJiraStatusPayload). */
31
+ const JiraStatusPayloadSchema = z.object({
32
+ provider: z.literal("jira"),
33
+ connected: z.boolean(),
34
+ cloudId: z.string().nullable(),
35
+ siteUrl: z.string().nullable(),
36
+ tokenExpiresAt: z.string().nullable(),
37
+ scope: z.string().nullable(),
38
+ hasManageWebhookScope: z.boolean(),
39
+ webhookTokenConfigured: z.boolean(),
40
+ webhookUrl: z.string(),
41
+ redirectUri: z.string(),
42
+ webhookIds: z.array(JiraWebhookIdEntrySchema),
43
+ // Only present when the OAuth grant lacks `manage:jira-webhook` scope.
44
+ manualWebhookInstructions: z.string().optional(),
45
+ });
46
+
47
+ type JiraStatusPayload = z.infer<typeof JiraStatusPayloadSchema>;
48
+
49
+ /** Body shape of handleJiraWebhook's WebhookResult when status === 200. */
50
+ const JiraWebhookEventResultSchema = z.union([
51
+ z.object({ status: z.literal("accepted") }),
52
+ z.object({ status: z.literal("duplicate") }),
53
+ z.object({ status: z.literal("ignored"), reason: z.literal("invalid-json") }),
54
+ ]);
55
+
56
+ type JiraWebhookEventResult = z.infer<typeof JiraWebhookEventResultSchema>;
57
+
58
+ const JiraWebhookRegisteredSchema = z.object({
59
+ webhookId: z.number().int(),
60
+ expiresAt: z.string(),
61
+ jql: z.string(),
62
+ });
63
+
64
+ const JiraWebhookDeletedSchema = z.object({
65
+ deleted: z.literal(true),
66
+ webhookId: z.number().int(),
67
+ });
68
+
69
+ const JiraDisconnectResultSchema = z.object({
70
+ disconnected: z.literal(true),
71
+ webhooksDeleted: z.number().int().nonnegative(),
72
+ webhooksTotal: z.number().int().nonnegative(),
73
+ webhookFailures: z.array(z.object({ id: z.number().int(), error: z.string() })),
74
+ revokeNote: z.string(),
75
+ });
76
+
22
77
  // ─── Route Definitions ───────────────────────────────────────────────────────
23
78
 
24
79
  const jiraAuthorize = route({
@@ -47,7 +102,11 @@ const jiraCallback = route({
47
102
  state: z.string(),
48
103
  }),
49
104
  responses: {
50
- 200: { description: "OAuth complete" },
105
+ 200: {
106
+ description: "OAuth complete",
107
+ unstructured:
108
+ "Delegates to completeGenericOAuthCallback (src/http/oauth-callback.ts), which sends an HTML success page or a redirect on success — never a fixed JSON shape",
109
+ },
51
110
  400: { description: "Invalid state or code" },
52
111
  500: { description: "Token exchange or accessible-resources fetch failed" },
53
112
  },
@@ -61,7 +120,7 @@ const jiraStatus = route({
61
120
  "Jira connection status, cloudId/siteUrl, token expiry, expected webhook URL, scope/token-config flags",
62
121
  tags: ["Trackers"],
63
122
  responses: {
64
- 200: { description: "Connection status" },
123
+ 200: { description: "Connection status", schema: JiraStatusPayloadSchema },
65
124
  503: { description: "Jira integration not configured" },
66
125
  },
67
126
  });
@@ -74,7 +133,10 @@ const jiraRefresh = route({
74
133
  "Force a Jira OAuth token refresh and return the updated status payload. Useful when an agent observes an expired token via tracker-status / db-query and wants to recover without restarting the server or re-running 3LO.",
75
134
  tags: ["Trackers"],
76
135
  responses: {
77
- 200: { description: "Token refreshed; returns same shape as /status" },
136
+ 200: {
137
+ description: "Token refreshed; returns same shape as /status",
138
+ schema: JiraStatusPayloadSchema,
139
+ },
78
140
  409: { description: "Jira not connected (no refresh token stored)" },
79
141
  500: { description: "Refresh failed (e.g. revoked grant, network error)" },
80
142
  503: { description: "Jira integration not configured" },
@@ -91,7 +153,7 @@ const jiraWebhook = route({
91
153
  auth: { apiKey: false },
92
154
  params: z.object({ token: z.string() }),
93
155
  responses: {
94
- 200: { description: "Event accepted" },
156
+ 200: { description: "Event accepted", schema: JiraWebhookEventResultSchema },
95
157
  401: { description: "Invalid URL token" },
96
158
  503: { description: "Jira webhook handler not configured" },
97
159
  },
@@ -110,7 +172,7 @@ const jiraWebhookRegister = route({
110
172
  jqlFilter: z.string().min(1),
111
173
  }),
112
174
  responses: {
113
- 200: { description: "Webhook registered" },
175
+ 200: { description: "Webhook registered", schema: JiraWebhookRegisteredSchema },
114
176
  400: { description: "Invalid jqlFilter" },
115
177
  503: { description: "Jira not connected or JIRA_WEBHOOK_TOKEN missing" },
116
178
  },
@@ -128,7 +190,7 @@ const jiraWebhookDelete = route({
128
190
  id: z.coerce.number().int().positive(),
129
191
  }),
130
192
  responses: {
131
- 200: { description: "Webhook deleted" },
193
+ 200: { description: "Webhook deleted", schema: JiraWebhookDeletedSchema },
132
194
  400: { description: "Invalid webhook id" },
133
195
  503: { description: "Jira not connected" },
134
196
  },
@@ -145,7 +207,7 @@ const jiraDisconnect = route({
145
207
  summary: "Fully disconnect Jira: delete all webhooks, drop tokens, clear metadata",
146
208
  tags: ["Trackers"],
147
209
  responses: {
148
- 200: { description: "Disconnected" },
210
+ 200: { description: "Disconnected", schema: JiraDisconnectResultSchema },
149
211
  503: { description: "Jira not configured" },
150
212
  },
151
213
  });
@@ -166,7 +228,7 @@ function getRedirectUri(req: IncomingMessage): string {
166
228
  return `${deriveApiBaseUrl(req)}/api/trackers/jira/callback`;
167
229
  }
168
230
 
169
- function buildJiraStatusPayload(req: IncomingMessage): Record<string, unknown> {
231
+ function buildJiraStatusPayload(req: IncomingMessage): JiraStatusPayload {
170
232
  const tokens = getOAuthTokens("jira");
171
233
  const meta = getJiraMetadata();
172
234
  const scope = tokens?.scope ?? null;
@@ -174,7 +236,7 @@ function buildJiraStatusPayload(req: IncomingMessage): Record<string, unknown> {
174
236
  const scopeList = scope ? scope.split(/[\s,]+/).filter(Boolean) : [];
175
237
  const hasManageWebhookScope = scopeList.includes("manage:jira-webhook");
176
238
 
177
- const status: Record<string, unknown> = {
239
+ const status: JiraStatusPayload = {
178
240
  provider: "jira",
179
241
  connected: !!tokens,
180
242
  cloudId: meta.cloudId ?? null,
@@ -258,8 +320,7 @@ export async function handleJiraTracker(
258
320
  return true;
259
321
  }
260
322
 
261
- res.writeHead(200, { "Content-Type": "application/json" });
262
- res.end(JSON.stringify(buildJiraStatusPayload(req)));
323
+ jiraStatus.respond(res, 200, buildJiraStatusPayload(req));
263
324
  return true;
264
325
  }
265
326
 
@@ -296,8 +357,7 @@ export async function handleJiraTracker(
296
357
  return true;
297
358
  }
298
359
 
299
- res.writeHead(200, { "Content-Type": "application/json" });
300
- res.end(JSON.stringify(buildJiraStatusPayload(req)));
360
+ jiraRefresh.respond(res, 200, buildJiraStatusPayload(req));
301
361
  return true;
302
362
  }
303
363
 
@@ -332,6 +392,11 @@ export async function handleJiraTracker(
332
392
  return true;
333
393
  }
334
394
 
395
+ if (result.status === 200) {
396
+ jiraWebhook.respond(res, 200, result.body as JiraWebhookEventResult);
397
+ return true;
398
+ }
399
+
335
400
  res.writeHead(result.status, { "Content-Type": "application/json" });
336
401
  res.end(typeof result.body === "string" ? result.body : JSON.stringify(result.body));
337
402
  return true;
@@ -356,8 +421,7 @@ export async function handleJiraTracker(
356
421
 
357
422
  try {
358
423
  const result = await registerJiraWebhook(parsed.body.jqlFilter);
359
- res.writeHead(200, { "Content-Type": "application/json" });
360
- res.end(JSON.stringify(result));
424
+ jiraWebhookRegister.respond(res, 200, result);
361
425
  } catch (err) {
362
426
  const message = err instanceof Error ? err.message : String(err);
363
427
  console.error("[Jira] Webhook register failed:", message);
@@ -383,8 +447,7 @@ export async function handleJiraTracker(
383
447
 
384
448
  try {
385
449
  await deleteJiraWebhook(parsed.params.id);
386
- res.writeHead(200, { "Content-Type": "application/json" });
387
- res.end(JSON.stringify({ deleted: true, webhookId: parsed.params.id }));
450
+ jiraWebhookDelete.respond(res, 200, { deleted: true, webhookId: parsed.params.id });
388
451
  } catch (err) {
389
452
  const message = err instanceof Error ? err.message : String(err);
390
453
  console.error(`[Jira] Webhook delete failed (id=${parsed.params.id}):`, message);
@@ -425,19 +488,16 @@ export async function handleJiraTracker(
425
488
  `[Jira] Disconnected: ${webhooksDeleted}/${ids.length} webhooks deleted, tokens cleared, metadata reset`,
426
489
  );
427
490
 
428
- res.writeHead(200, { "Content-Type": "application/json" });
429
- res.end(
430
- JSON.stringify({
431
- disconnected: true,
432
- webhooksDeleted,
433
- webhooksTotal: ids.length,
434
- webhookFailures,
435
- // Atlassian 3LO has no token revocation endpoint — surface this so
436
- // the UI can prompt the user to revoke the grant manually if desired.
437
- revokeNote:
438
- "Atlassian OAuth grants must be revoked manually at https://id.atlassian.com/manage/connected-apps if you want to fully sever the consent.",
439
- }),
440
- );
491
+ jiraDisconnect.respond(res, 200, {
492
+ disconnected: true,
493
+ webhooksDeleted,
494
+ webhooksTotal: ids.length,
495
+ webhookFailures,
496
+ // Atlassian 3LO has no token revocation endpoint — surface this so
497
+ // the UI can prompt the user to revoke the grant manually if desired.
498
+ revokeNote:
499
+ "Atlassian OAuth grants must be revoked manually at https://id.atlassian.com/manage/connected-apps if you want to fully sever the consent.",
500
+ });
441
501
  return true;
442
502
  }
443
503
 
@@ -14,6 +14,28 @@ import { completeGenericOAuthCallback } from "../oauth-callback";
14
14
  import { route } from "../route-def";
15
15
  import { deriveApiBaseUrl, parseQueryParams } from "../utils";
16
16
 
17
+ // ─── Schemas ─────────────────────────────────────────────────────────────────
18
+
19
+ /** Shape returned by `buildLinearStatusPayload` — shared by /status and /refresh. */
20
+ const LinearStatusSchema = z.object({
21
+ provider: z.literal("linear"),
22
+ connected: z.boolean(),
23
+ tokenExpiry: z.string().nullable(),
24
+ scope: z.string().nullable(),
25
+ webhookUrl: z.string(),
26
+ redirectUri: z.string(),
27
+ });
28
+
29
+ /** The two `body` shapes `handleLinearWebhook` (../../linear/webhook.ts) sends for its 200 code. */
30
+ const LinearWebhookAcceptedSchema = z.object({
31
+ status: z.enum(["duplicate", "accepted"]),
32
+ });
33
+
34
+ const LinearDisconnectSchema = z.object({
35
+ disconnected: z.literal(true),
36
+ revoked: z.boolean(),
37
+ });
38
+
17
39
  // ─── Route Definitions ───────────────────────────────────────────────────────
18
40
 
19
41
  const linearAuthorize = route({
@@ -42,7 +64,11 @@ const linearCallback = route({
42
64
  state: z.string(),
43
65
  }),
44
66
  responses: {
45
- 200: { description: "OAuth complete" },
67
+ 200: {
68
+ description: "OAuth complete",
69
+ unstructured:
70
+ "HTML success page (sendAuthorizedHtml) sent by completeGenericOAuthCallback in ../oauth-callback.ts — shape not owned by this file",
71
+ },
46
72
  400: { description: "Invalid state or code" },
47
73
  500: { description: "Token exchange failed" },
48
74
  },
@@ -55,7 +81,7 @@ const linearStatus = route({
55
81
  summary: "Linear connection status, token expiry, workspace info, expected webhook URL",
56
82
  tags: ["Trackers"],
57
83
  responses: {
58
- 200: { description: "Connection status" },
84
+ 200: { description: "Connection status", schema: LinearStatusSchema },
59
85
  503: { description: "Linear integration not configured" },
60
86
  },
61
87
  });
@@ -68,7 +94,10 @@ const linearRefresh = route({
68
94
  "Force a Linear OAuth token refresh and return the updated status payload. Useful when an agent observes an expired token and wants to recover without restarting the server or re-running OAuth.",
69
95
  tags: ["Trackers"],
70
96
  responses: {
71
- 200: { description: "Token refreshed; returns same shape as /status" },
97
+ 200: {
98
+ description: "Token refreshed; returns same shape as /status",
99
+ schema: LinearStatusSchema,
100
+ },
72
101
  409: { description: "Linear not connected (no refresh token stored)" },
73
102
  500: { description: "Refresh failed" },
74
103
  503: { description: "Linear integration not configured" },
@@ -83,7 +112,7 @@ const linearWebhook = route({
83
112
  tags: ["Trackers"],
84
113
  auth: { apiKey: false },
85
114
  responses: {
86
- 200: { description: "Event accepted" },
115
+ 200: { description: "Event accepted", schema: LinearWebhookAcceptedSchema },
87
116
  401: { description: "Invalid signature" },
88
117
  503: { description: "Linear integration not configured" },
89
118
  },
@@ -99,14 +128,14 @@ const linearDisconnect = route({
99
128
  summary: "Fully disconnect Linear: revoke OAuth grant + drop tokens",
100
129
  tags: ["Trackers"],
101
130
  responses: {
102
- 200: { description: "Disconnected" },
131
+ 200: { description: "Disconnected", schema: LinearDisconnectSchema },
103
132
  503: { description: "Linear not configured" },
104
133
  },
105
134
  });
106
135
 
107
136
  // ─── Helpers ─────────────────────────────────────────────────────────────────
108
137
 
109
- function buildLinearStatusPayload(req: IncomingMessage): Record<string, unknown> {
138
+ function buildLinearStatusPayload(req: IncomingMessage): z.infer<typeof LinearStatusSchema> {
110
139
  const tokens = getOAuthTokens("linear");
111
140
  const baseUrl = deriveApiBaseUrl(req);
112
141
 
@@ -180,8 +209,7 @@ export async function handleLinearTracker(
180
209
  return true;
181
210
  }
182
211
 
183
- res.writeHead(200, { "Content-Type": "application/json" });
184
- res.end(JSON.stringify(buildLinearStatusPayload(req)));
212
+ linearStatus.respond(res, 200, buildLinearStatusPayload(req));
185
213
  return true;
186
214
  }
187
215
 
@@ -217,8 +245,7 @@ export async function handleLinearTracker(
217
245
  return true;
218
246
  }
219
247
 
220
- res.writeHead(200, { "Content-Type": "application/json" });
221
- res.end(JSON.stringify(buildLinearStatusPayload(req)));
248
+ linearRefresh.respond(res, 200, buildLinearStatusPayload(req));
222
249
  return true;
223
250
  }
224
251
 
@@ -239,6 +266,14 @@ export async function handleLinearTracker(
239
266
  };
240
267
 
241
268
  const result = await handleLinearWebhook(rawBody, headers);
269
+ // handleLinearWebhook's return type is `{ status: number; body: unknown }` (it
270
+ // can send 503/401/200 — all declared on this route); narrow to the literal
271
+ // 200 code to use the typed egress for the one 2xx case, keep the raw
272
+ // passthrough (same status/body/header as before) for 401/503.
273
+ if (result.status === 200) {
274
+ linearWebhook.respond(res, 200, result.body as z.infer<typeof LinearWebhookAcceptedSchema>);
275
+ return true;
276
+ }
242
277
  res.writeHead(result.status, { "Content-Type": "application/json" });
243
278
  res.end(JSON.stringify(result.body));
244
279
  return true;
@@ -262,8 +297,7 @@ export async function handleLinearTracker(
262
297
 
263
298
  console.log(`[Linear] Disconnected: revoke=${revoked}, tokens cleared`);
264
299
 
265
- res.writeHead(200, { "Content-Type": "application/json" });
266
- res.end(JSON.stringify({ disconnected: true, revoked }));
300
+ linearDisconnect.respond(res, 200, { disconnected: true, revoked });
267
301
  return true;
268
302
  }
269
303