@desplega.ai/agent-swarm 1.119.6 → 1.121.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 (207) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-qe5zjcbr.js → actions-mwshtkf7.js} +6 -6
  3. package/dist/{anthropic-messages-qjxzsabf.js → anthropic-messages-4gn944cn.js} +29 -13
  4. package/dist/{app-31x7k3vj.js → app-skbty91x.js} +4 -4
  5. package/dist/{assistant-4crt9dve.js → assistant-tg7dhfj8.js} +13 -13
  6. package/dist/{azure-openai-responses-ntfd05zd.js → azure-openai-responses-79zc0wyy.js} +28 -13
  7. package/dist/{boot-reembed-8g69ttz4.js → boot-reembed-ntjmeyh2.js} +4 -4
  8. package/dist/{boot-reembed-q648j733.js → boot-reembed-q5x0xpcp.js} +5 -5
  9. package/dist/{boot-scrub-logs-sfmzd2hj.js → boot-scrub-logs-5gznsfbe.js} +4 -4
  10. package/dist/{claude-adapter-9n3nzt65.js → claude-adapter-vxvcym3r.js} +3 -3
  11. package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-g807894s.js} +2 -2
  12. package/dist/{cli-bpvgs2y6.js → cli-072t49hf.js} +2123 -1533
  13. package/dist/{cli-g6qz2kts.js → cli-0df2g86x.js} +336 -18
  14. package/dist/cli-1728qh70.js +791 -0
  15. package/dist/{cli-vg1jxfqz.js → cli-3b1ayz83.js} +5 -5
  16. package/dist/{cli-r93nz80j.js → cli-3fxqwfh3.js} +1 -1
  17. package/dist/{cli-w6rhc30s.js → cli-3jrdwnn9.js} +20 -5
  18. package/dist/{cli-pasyk3vc.js → cli-46etyprn.js} +10 -5
  19. package/dist/{cli-433c0f4b.js → cli-4pfweq4x.js} +8 -8
  20. package/dist/{cli-2r7s8jhp.js → cli-5c4327dx.js} +7 -1
  21. package/dist/{cli-4s84mw72.js → cli-5hpvd15x.js} +2 -2
  22. package/dist/{cli-branks1w.js → cli-6s3ytc5x.js} +1 -1
  23. package/dist/{cli-da3x3btq.js → cli-7pt9sc83.js} +2 -2
  24. package/dist/{cli-2gv8ygwr.js → cli-7pzae66m.js} +2 -2
  25. package/dist/{cli-qs7nb3zn.js → cli-8wrympd7.js} +27 -1
  26. package/dist/{cli-qw1ccvxm.js → cli-95rbe11h.js} +70 -51
  27. package/dist/{cli-6rvm3jkk.js → cli-agdrxyex.js} +1 -1
  28. package/dist/{cli-wr706gez.js → cli-av2s7my4.js} +167 -66
  29. package/dist/{cli-59jy217p.js → cli-avw9ev3x.js} +55 -21
  30. package/dist/{cli-fkxxh5d4.js → cli-b2mqkg1h.js} +2 -2
  31. package/dist/{cli-1arw00dp.js → cli-ccjwgarc.js} +12 -3
  32. package/dist/{cli-pscrc6tw.js → cli-ch7t5v7n.js} +5 -0
  33. package/dist/{cli-tghre7g6.js → cli-cw70hymf.js} +1 -0
  34. package/dist/{cli-n61x2aph.js → cli-dmzv0986.js} +816 -1335
  35. package/dist/{cli-d1reabed.js → cli-g9vva2qh.js} +2 -2
  36. package/dist/{cli-ner6af7m.js → cli-gdpx70nc.js} +24048 -23715
  37. package/dist/{cli-96feg24n.js → cli-ghq3v4c9.js} +2 -2
  38. package/dist/{cli-wxbnjgce.js → cli-ky8esb8t.js} +3 -3
  39. package/dist/{cli-gdqdkr3j.js → cli-nf7sadev.js} +2 -2
  40. package/dist/{cli-h6cf69x5.js → cli-p9w7kb3h.js} +1 -1
  41. package/dist/{cli-xeatwfbe.js → cli-ry656xem.js} +24 -17
  42. package/dist/{cli-q8dway4w.js → cli-s8bdcke2.js} +1 -1
  43. package/dist/cli-s8s499xy.js +81 -0
  44. package/dist/{cli-sbkwvn5e.js → cli-srjqn7fd.js} +1 -1
  45. package/dist/cli-tv7vekye.js +367 -0
  46. package/dist/{cli-gg5bd32n.js → cli-vg3zrw6h.js} +4 -4
  47. package/dist/{cli-vx0y68jv.js → cli-vp9r4ap4.js} +5 -4
  48. package/dist/{cli-mg280wsv.js → cli-we156bmz.js} +1 -1
  49. package/dist/{cli-86fdnerz.js → cli-wwsaj97q.js} +9 -4
  50. package/dist/{cli-e0y603nn.js → cli-x0e0yvnq.js} +101 -1
  51. package/dist/{cli-mw3xpgwb.js → cli-x4apc9t3.js} +4 -4
  52. package/dist/cli.js +26 -21
  53. package/dist/{codex-adapter-6j2b38tg.js → codex-adapter-mmy741zd.js} +8 -9
  54. package/dist/{codex-session-runner-vz215wgw.js → codex-session-runner-sazmx9zt.js} +8 -9
  55. package/dist/{commands-6xr20jbe.js → commands-cxxye5e4.js} +3 -3
  56. package/dist/{db-1mmvk1z4.js → db-ab1954b7.js} +5 -3
  57. package/dist/{google-generative-ai-n1d3xt4n.js → google-generative-ai-wwa2ked5.js} +14 -18
  58. package/dist/{google-vertex-mxst23gy.js → google-vertex-71xmdd7n.js} +14 -18
  59. package/dist/{handlers-xvrknk8w.js → handlers-fmfe2343.js} +13 -13
  60. package/dist/{hook-qm7he8wn.js → hook-ygnw3cvt.js} +7 -8
  61. package/dist/{http-hj08zq67.js → http-kactcc0z.js} +1128 -536
  62. package/dist/{index-688jph6a.js → index-6erq0wvk.js} +11 -11
  63. package/dist/{index-gze4fg2w.js → index-g86j2yt5.js} +9 -9
  64. package/dist/{index-etz7d8md.js → index-gmzs6a2q.js} +10 -10
  65. package/dist/{index-wj8dfpwh.js → index-hmp82wn0.js} +12 -12
  66. package/dist/{keepalive-94wn9yxw.js → keepalive-3cvt26dw.js} +18 -14
  67. package/dist/{lead-rvcqkp11.js → lead-ysf26cjr.js} +31 -32
  68. package/dist/{maintenance-qhfc4dnp.js → maintenance-9npwptbb.js} +5 -5
  69. package/dist/{mistral-conversations-ykcg66za.js → mistral-conversations-26ya4yah.js} +17 -13
  70. package/dist/{oauth-refresh-sweep-rj8wgvsw.js → oauth-refresh-sweep-0r2v76vw.js} +23 -17
  71. package/dist/{onboard-8bv8q1wc.js → onboard-ddb1674a.js} +2 -2
  72. package/dist/{openai-codex-responses-zy0907j9.js → openai-codex-responses-6rr2ye0s.js} +78 -38
  73. package/dist/{openai-completions-xk4p56n9.js → openai-completions-zfa9nkvt.js} +130 -33
  74. package/dist/{openai-responses-4gv3ca5d.js → openai-responses-9wmdks0g.js} +38 -14
  75. package/dist/{opencode-adapter-5bwzvbwj.js → opencode-adapter-h6rkxwrk.js} +2 -2
  76. package/dist/{openrouter-images-4fsrrzes.js → openrouter-images-dgq98729.js} +10 -3
  77. package/dist/{otel-impl-1kf6fr2m.js → otel-impl-b43bkq8e.js} +4 -4
  78. package/dist/{pi-messages-zb8v76wx.js → pi-messages-nzjbbz47.js} +4 -6
  79. package/dist/{pi-mono-adapter-9sg69kg3.js → pi-mono-adapter-ke789738.js} +2328 -1281
  80. package/dist/{pricing-refresh-e84tar2s.js → pricing-refresh-2rqf114z.js} +5 -5
  81. package/dist/{rbac-roles-atytq5ch.js → rbac-roles-5wkhc8xv.js} +5 -5
  82. package/dist/{rbac-roles-pg0zvnwn.js → rbac-roles-6vtafvzb.js} +4 -4
  83. package/dist/{seed-pricing-xd6nxq7k.js → seed-pricing-23wrb873.js} +4 -4
  84. package/dist/{setup-4z389yk0.js → setup-6rny4fk6.js} +2 -2
  85. package/dist/{worker-m61r0t7m.js → worker-yapewe14.js} +31 -32
  86. package/openapi.json +2671 -1811
  87. package/package.json +10 -5
  88. package/plugin/commands/swarm-chat.md +2 -0
  89. package/plugin/pi-skills/swarm-chat/SKILL.md +2 -0
  90. package/src/be/connection-bindings-blob-migration.ts +123 -0
  91. package/src/be/db-queries/mcp-oauth.ts +409 -247
  92. package/src/be/db-queries/oauth.ts +813 -131
  93. package/src/be/db.ts +97 -6
  94. package/src/be/migrations/117_unified_oauth.sql +624 -0
  95. package/src/be/migrations/119_agent_avatar.sql +7 -0
  96. package/src/be/migrations/120_task_title.sql +3 -0
  97. package/src/be/oauth-credential-bindings.ts +48 -14
  98. package/src/be/oauth-encryption-backfill.ts +84 -0
  99. package/src/be/oauth-refresh-sweep.ts +38 -23
  100. package/src/be/script-connections.ts +1130 -163
  101. package/src/be/script-credential-broker.ts +37 -27
  102. package/src/be/scripts/typecheck.ts +3 -2
  103. package/src/be/seed-capabilities.ts +62 -0
  104. package/src/be/seed-pricing.ts +1 -1
  105. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  106. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  107. package/src/be/vendored-openapi.ts +98 -0
  108. package/src/commands/runner.ts +69 -6
  109. package/src/http/agents.ts +23 -5
  110. package/src/http/all-routes.ts +1 -0
  111. package/src/http/core.ts +8 -5
  112. package/src/http/index.ts +18 -5
  113. package/src/http/mcp-bridge.ts +5 -2
  114. package/src/http/mcp-oauth.ts +103 -101
  115. package/src/http/oauth-callback.ts +332 -0
  116. package/src/http/oauth-generic.ts +14 -54
  117. package/src/http/poll.ts +5 -1
  118. package/src/http/script-connections.ts +651 -170
  119. package/src/http/scripts.ts +4 -2
  120. package/src/http/sessions.ts +1 -1
  121. package/src/http/tasks.ts +35 -0
  122. package/src/http/trackers/jira.ts +22 -34
  123. package/src/http/trackers/linear.ts +21 -36
  124. package/src/http/x.ts +6 -2
  125. package/src/jira/app.ts +12 -6
  126. package/src/jira/oauth.ts +48 -48
  127. package/src/linear/app.ts +44 -2
  128. package/src/linear/oauth.ts +12 -37
  129. package/src/oauth/app-validation.ts +27 -9
  130. package/src/oauth/ensure-mcp-token.ts +1 -0
  131. package/src/oauth/ensure-token.ts +331 -108
  132. package/src/oauth/identity-capture.ts +103 -0
  133. package/src/oauth/keepalive.ts +14 -9
  134. package/src/oauth/presets.ts +242 -0
  135. package/src/oauth/wrapper.ts +197 -76
  136. package/src/prompts/base-prompt.ts +48 -4
  137. package/src/prompts/session-templates.ts +13 -1
  138. package/src/providers/claude-managed-models.ts +8 -0
  139. package/src/rbac/legacy-policy.ts +2 -1
  140. package/src/rbac/permissions.ts +7 -3
  141. package/src/scripts-runtime/credential-broker/broker.ts +51 -8
  142. package/src/scripts-runtime/credential-broker/fetch-patch.ts +30 -3
  143. package/src/scripts-runtime/credential-broker/index.ts +1 -1
  144. package/src/scripts-runtime/credential-broker/types.ts +55 -8
  145. package/src/scripts-runtime/egress-secrets.ts +6 -2
  146. package/src/scripts-runtime/eval-harness.ts +2 -2
  147. package/src/scripts-runtime/executors/types.ts +2 -1
  148. package/src/scripts-runtime/loader.ts +3 -0
  149. package/src/scripts-runtime/types/stdlib.d.ts +11 -0
  150. package/src/server.ts +230 -126
  151. package/src/tests/agent-avatar-resolution.test.ts +123 -0
  152. package/src/tests/agents-list-model-display.test.ts +10 -0
  153. package/src/tests/base-prompt.test.ts +72 -0
  154. package/src/tests/claude-managed-adapter.test.ts +6 -0
  155. package/src/tests/connection-embedded-auth.test.ts +709 -0
  156. package/src/tests/context-window.test.ts +1 -0
  157. package/src/tests/credential-broker.test.ts +123 -125
  158. package/src/tests/db-queries-oauth.test.ts +126 -1
  159. package/src/tests/ensure-token.test.ts +44 -2
  160. package/src/tests/list-endpoint-slimming.test.ts +45 -0
  161. package/src/tests/mcp-oauth-manual-client.test.ts +20 -1
  162. package/src/tests/mcp-oauth-queries.test.ts +154 -8
  163. package/src/tests/oauth-access-token-tool.test.ts +37 -1
  164. package/src/tests/oauth-callback-flow.test.ts +534 -0
  165. package/src/tests/oauth-credential-bindings.test.ts +50 -22
  166. package/src/tests/oauth-presets.test.ts +275 -0
  167. package/src/tests/oauth-refresh-failure.test.ts +263 -0
  168. package/src/tests/oauth-refresh-sweep.test.ts +83 -3
  169. package/src/tests/oauth-wrapper.test.ts +77 -55
  170. package/src/tests/prompt-template-session.test.ts +6 -5
  171. package/src/tests/rbac-charact-misc-tools.test.ts +60 -0
  172. package/src/tests/rbac-engine.test.ts +2 -1
  173. package/src/tests/script-connections-http.test.ts +356 -11
  174. package/src/tests/script-connections.test.ts +102 -0
  175. package/src/tests/sdk-allowlist.test.ts +4 -1
  176. package/src/tests/seed-capabilities.test.ts +92 -0
  177. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  178. package/src/tests/sessions.test.ts +46 -0
  179. package/src/tests/spec-base-url.test.ts +266 -0
  180. package/src/tests/status.test.ts +3 -4
  181. package/src/tests/swarm-config-reserved-keys.test.ts +4 -2
  182. package/src/tests/task-title-route.test.ts +106 -0
  183. package/src/tests/tool-annotations.test.ts +6 -3
  184. package/src/tests/tracker-fold.test.ts +412 -0
  185. package/src/tests/unified-oauth-migration.test.ts +455 -0
  186. package/src/tests/vendored-openapi.test.ts +263 -0
  187. package/src/tools/credential-bindings/tool.ts +232 -77
  188. package/src/tools/oauth-access-token.ts +55 -6
  189. package/src/tools/script-connections/tool.ts +73 -53
  190. package/src/tools/swarm-config/set-config.ts +24 -50
  191. package/src/tools/update-profile.ts +15 -5
  192. package/src/tracker/types.ts +12 -0
  193. package/src/types.ts +29 -0
  194. package/src/utils/context-window.ts +1 -0
  195. package/src/workflows/executors/swarm-script.ts +6 -2
  196. package/vendored-openapi/README.md +22 -0
  197. package/vendored-openapi/github.json +7759 -0
  198. package/vendored-openapi/gmail.json +393 -0
  199. package/vendored-openapi/jira.json +3396 -0
  200. package/vendored-openapi/linear.json +252 -0
  201. package/vendored-openapi/manifest.json +128 -0
  202. package/vendored-openapi/slack.json +2895 -0
  203. package/dist/cli-0pygq9r1.js +0 -429
  204. package/dist/cli-8hhms4hp.js +0 -393
  205. package/dist/cli-z9gzq07x.js +0 -27
  206. package/src/scripts-runtime/credential-broker/store.ts +0 -72
  207. package/dist/{otel-g299k41b.js → otel-63jr5v38.js} +1 -1
@@ -0,0 +1,455 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+ import { decryptSecret, encryptSecret, getEncryptionKey } from "../be/crypto";
4
+ import { runMigrations } from "../be/migrations/runner";
5
+ import { autoEncryptLegacyOAuthSecrets } from "../be/oauth-encryption-backfill";
6
+
7
+ const NOW = "2026-07-21T12:00:00.000Z";
8
+
9
+ async function pre117Database(path = ":memory:"): Promise<Database> {
10
+ const database = new Database(path, { create: true });
11
+
12
+ // Mark the consolidated connections-redesign migration as already applied so
13
+ // the real runner constructs an exact pre-redesign schema through 116.
14
+ // Removing the sentinel later makes the same runner apply migration 117.
15
+ database.run(`
16
+ CREATE TABLE _migrations (
17
+ version INTEGER PRIMARY KEY,
18
+ name TEXT NOT NULL,
19
+ applied_at TEXT NOT NULL,
20
+ checksum TEXT NOT NULL
21
+ )
22
+ `);
23
+ const migrationsDir = `${import.meta.dir}/../be/migrations`;
24
+ const post116Files = (await Array.fromAsync(new Bun.Glob("*.sql").scan(migrationsDir)))
25
+ .filter((file) => parseInt(file.split("_")[0] ?? "0", 10) >= 117)
26
+ .sort();
27
+ const insertSentinel = database.query(
28
+ "INSERT INTO _migrations (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
29
+ );
30
+ for (const file of post116Files) {
31
+ const sql = await Bun.file(`${migrationsDir}/${file}`).text();
32
+ const checksum = new Bun.CryptoHasher("sha256").update(sql).digest("hex");
33
+ insertSentinel.run(
34
+ parseInt(file.split("_")[0] ?? "0", 10),
35
+ file.replace(".sql", ""),
36
+ NOW,
37
+ checksum,
38
+ );
39
+ }
40
+ runMigrations(database);
41
+
42
+ const appRows = [
43
+ {
44
+ id: "app-linear",
45
+ provider: "linear",
46
+ clientId: "linear-client",
47
+ clientSecret: "linear-client-secret",
48
+ authorizeUrl: "https://linear.test/authorize",
49
+ tokenUrl: "https://linear.test/token",
50
+ redirectUri: "https://swarm.test/api/oauth/linear/callback",
51
+ scopes: "read,write",
52
+ metadata: JSON.stringify({
53
+ actor: "app",
54
+ extraParams: { prompt: "consent" },
55
+ tokenAuthStyle: "body",
56
+ tokenBodyFormat: "form",
57
+ }),
58
+ },
59
+ {
60
+ id: "app-jira",
61
+ provider: "jira",
62
+ clientId: "jira-client",
63
+ clientSecret: "jira-client-secret",
64
+ authorizeUrl: "https://jira.test/authorize",
65
+ tokenUrl: "https://jira.test/token",
66
+ redirectUri: "https://swarm.test/api/oauth/jira/callback",
67
+ scopes: "read:jira-user,offline_access",
68
+ metadata: JSON.stringify({
69
+ cloudId: "cloud-1",
70
+ webhookIds: ["hook-1"],
71
+ revocationUrl: "https://jira.test/revoke",
72
+ tokenAuthStyle: "basic",
73
+ tokenBodyFormat: "json",
74
+ }),
75
+ },
76
+ {
77
+ id: "app-vendor",
78
+ provider: "vendor",
79
+ clientId: "vendor-client",
80
+ clientSecret: "vendor-client-secret",
81
+ authorizeUrl: "https://vendor.test/authorize",
82
+ tokenUrl: "https://vendor.test/token",
83
+ redirectUri: "https://swarm.test/api/oauth/vendor/callback",
84
+ scopes: "profile,email",
85
+ metadata: "{}",
86
+ },
87
+ ];
88
+ const insertApp = database.query(`
89
+ INSERT INTO oauth_apps (
90
+ id, provider, clientId, clientSecret, authorizeUrl, tokenUrl,
91
+ redirectUri, scopes, metadata, createdAt, updatedAt
92
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
93
+ `);
94
+ for (const app of appRows) {
95
+ insertApp.run(
96
+ app.id,
97
+ app.provider,
98
+ app.clientId,
99
+ app.clientSecret,
100
+ app.authorizeUrl,
101
+ app.tokenUrl,
102
+ app.redirectUri,
103
+ app.scopes,
104
+ app.metadata,
105
+ NOW,
106
+ NOW,
107
+ );
108
+ }
109
+
110
+ const insertToken = database.query(`
111
+ INSERT INTO oauth_tokens (
112
+ id, provider, accessToken, refreshToken, expiresAt, scope, createdAt, updatedAt
113
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
114
+ `);
115
+ for (const provider of ["linear", "jira", "vendor"]) {
116
+ insertToken.run(
117
+ `token-${provider}`,
118
+ provider,
119
+ `${provider}-access-token`,
120
+ `${provider}-refresh-token`,
121
+ "2026-08-01T00:00:00.000Z",
122
+ "read write",
123
+ NOW,
124
+ NOW,
125
+ );
126
+ }
127
+
128
+ const insertServer = database.query(`
129
+ INSERT INTO mcp_servers (
130
+ id, name, scope, transport, url, isEnabled, version, createdAt, lastUpdatedAt
131
+ ) VALUES (?, ?, 'global', 'http', ?, 1, 1, ?, ?)
132
+ `);
133
+ insertServer.run("mcp-dcr", "Migration DCR", "https://mcp-dcr.test", NOW, NOW);
134
+ insertServer.run("mcp-manual", "Migration Manual", "https://mcp-manual.test", NOW, NOW);
135
+
136
+ const key = getEncryptionKey();
137
+ const insertMcpToken = database.query(`
138
+ INSERT INTO mcp_oauth_tokens (
139
+ id, mcpServerId, userId, accessToken, refreshToken, tokenType, expiresAt, scope,
140
+ resourceUrl, authorizationServerIssuer, authorizeUrl, tokenUrl, revocationUrl,
141
+ dcrClientId, dcrClientSecret, clientSource, status, lastErrorMessage,
142
+ lastRefreshedAt, connectedByUserId, createdAt, updatedAt
143
+ ) VALUES (?, ?, NULL, ?, ?, 'Bearer', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, NULL, ?, ?)
144
+ `);
145
+ insertMcpToken.run(
146
+ "mcp-token-dcr",
147
+ "mcp-dcr",
148
+ encryptSecret("mcp-dcr-access", key),
149
+ encryptSecret("mcp-dcr-refresh", key),
150
+ "2026-08-02T00:00:00.000Z",
151
+ "mcp read",
152
+ "https://mcp-dcr.test",
153
+ "https://issuer-dcr.test",
154
+ "https://issuer-dcr.test/authorize",
155
+ "https://issuer-dcr.test/token",
156
+ "https://issuer-dcr.test/revoke",
157
+ "dcr-client",
158
+ encryptSecret("dcr-client-secret", key),
159
+ "dcr",
160
+ "connected",
161
+ NOW,
162
+ NOW,
163
+ NOW,
164
+ );
165
+ insertMcpToken.run(
166
+ "mcp-token-manual",
167
+ "mcp-manual",
168
+ encryptSecret("mcp-manual-access", key),
169
+ null,
170
+ "2026-08-03T00:00:00.000Z",
171
+ "manual read",
172
+ "https://mcp-manual.test",
173
+ "https://issuer-manual.test",
174
+ "https://issuer-manual.test/authorize",
175
+ "https://issuer-manual.test/token",
176
+ null,
177
+ "manual-client",
178
+ encryptSecret("manual-client-secret", key),
179
+ "manual",
180
+ "error",
181
+ NOW,
182
+ NOW,
183
+ NOW,
184
+ );
185
+
186
+ database
187
+ .query(`
188
+ INSERT INTO mcp_oauth_pending (
189
+ state, mcpServerId, userId, codeVerifier, nonce, resourceUrl,
190
+ authorizationServerIssuer, authorizeUrl, tokenUrl, revocationUrl, scopes,
191
+ dcrClientId, dcrClientSecret, redirectUri, finalRedirect, createdAt
192
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
193
+ `)
194
+ .run(
195
+ "pending-state",
196
+ "mcp-dcr",
197
+ encryptSecret("pending-verifier", key),
198
+ "pending-nonce",
199
+ "https://pending-resource.test",
200
+ "https://pending-issuer.test",
201
+ "https://pending-issuer.test/authorize",
202
+ "https://pending-issuer.test/token",
203
+ "https://pending-issuer.test/revoke",
204
+ "pending scope",
205
+ "pending-client",
206
+ encryptSecret("pending-client-secret", key),
207
+ "https://swarm.test/api/oauth/callback",
208
+ "/connections",
209
+ NOW,
210
+ );
211
+
212
+ const insertBinding = database.query(`
213
+ INSERT INTO script_credential_bindings (
214
+ id, config_key, allowed_hosts_json, header_template, query_template,
215
+ scope, scope_id, active, source, created_at, updated_at,
216
+ auth_kind, oauth_provider
217
+ ) VALUES (?, ?, ?, ?, ?, 'global', NULL, 1, 'user', ?, ?, ?, ?)
218
+ `);
219
+ insertBinding.run(
220
+ "binding-config",
221
+ "PLAIN_SECRET",
222
+ '["api.example.test"]',
223
+ "Authorization: Bearer [REDACTED:PLAIN_SECRET]",
224
+ null,
225
+ NOW,
226
+ NOW,
227
+ "config",
228
+ null,
229
+ );
230
+ insertBinding.run(
231
+ "binding-oauth",
232
+ "VENDOR_OAUTH",
233
+ '["api.vendor.test"]',
234
+ "Authorization: Bearer [REDACTED:VENDOR_OAUTH]",
235
+ null,
236
+ NOW,
237
+ NOW,
238
+ "oauth",
239
+ "vendor",
240
+ );
241
+ insertBinding.run(
242
+ "binding-query",
243
+ "QUERY_SECRET",
244
+ '["query.example.test"]',
245
+ null,
246
+ "token=[REDACTED:QUERY_SECRET]",
247
+ NOW,
248
+ NOW,
249
+ "config",
250
+ null,
251
+ );
252
+
253
+ const legacyBlob = JSON.stringify({
254
+ bindings: [
255
+ {
256
+ configKey: "VENDOR_OAUTH",
257
+ allowedHosts: ["api.vendor.test"],
258
+ headerTemplate: "Authorization: Bearer [REDACTED:VENDOR_OAUTH]",
259
+ authKind: "oauth",
260
+ oauthProvider: "vendor",
261
+ },
262
+ ],
263
+ });
264
+ database
265
+ .query(`
266
+ INSERT INTO swarm_config (
267
+ id, scope, scopeId, key, value, isSecret, description,
268
+ createdAt, lastUpdatedAt, encrypted
269
+ ) VALUES (?, 'global', NULL, 'SCRIPT_CREDENTIAL_BINDINGS', ?, 0, ?, ?, ?, 0)
270
+ `)
271
+ .run("legacy-binding-blob", legacyBlob, "migration fixture", NOW, NOW);
272
+
273
+ database
274
+ .query(
275
+ "INSERT INTO oauth_refresh_locks (provider, owner, expiresAt, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?)",
276
+ )
277
+ .run("linear", "migration-owner", "2026-08-01T00:00:00.000Z", NOW, NOW);
278
+
279
+ return database;
280
+ }
281
+
282
+ const qaFixturePath = process.env.UNIFIED_OAUTH_PRE117_FIXTURE_PATH;
283
+
284
+ describe("migration 117 unified OAuth storage", () => {
285
+ if (qaFixturePath) {
286
+ test("builds a populated pre-117 file fixture for boot QA", async () => {
287
+ const database = await pre117Database(qaFixturePath);
288
+ try {
289
+ expect(
290
+ database.query<{ count: number }, []>("SELECT count(*) AS count FROM oauth_tokens").get()
291
+ ?.count,
292
+ ).toBe(3);
293
+ expect(
294
+ database
295
+ .query<{ count: number }, []>("SELECT count(*) AS count FROM mcp_oauth_tokens")
296
+ .get()?.count,
297
+ ).toBe(2);
298
+ // The sentinel exists only to make runMigrations stop at 116. Remove it
299
+ // before handing the file to a real boot so migration 117 is pending.
300
+ database.query("DELETE FROM _migrations WHERE version >= 117").run();
301
+ } finally {
302
+ database.close();
303
+ }
304
+ });
305
+ return;
306
+ }
307
+
308
+ test("carries legacy rows, lifts quirks, re-keys bindings, and encrypts idempotently", async () => {
309
+ const database = await pre117Database();
310
+ try {
311
+ database.query("DELETE FROM _migrations WHERE version >= 117").run();
312
+ runMigrations(database);
313
+
314
+ expect(
315
+ database.query<{ count: number }, []>("SELECT count(*) AS count FROM oauth_apps").get()
316
+ ?.count,
317
+ ).toBe(5);
318
+ expect(
319
+ database
320
+ .query<{ count: number }, []>("SELECT count(*) AS count FROM oauth_authorizations")
321
+ .get()?.count,
322
+ ).toBe(5);
323
+ // Pending PKCE state is NOT carried over (10-min TTL makes it dead on
324
+ // arrival across an upgrade); the seeded mcp_oauth_pending row must
325
+ // vanish with its table instead of surfacing in oauth_pending.
326
+ expect(
327
+ database.query<{ count: number }, []>("SELECT count(*) AS count FROM oauth_pending").get()
328
+ ?.count,
329
+ ).toBe(0);
330
+
331
+ const linear = database
332
+ .query<
333
+ {
334
+ id: string;
335
+ scopeSeparator: string;
336
+ extraParamsJson: string | null;
337
+ metadata: string;
338
+ },
339
+ []
340
+ >(
341
+ "SELECT id, scopeSeparator, extraParamsJson, metadata FROM oauth_apps WHERE provider = 'linear'",
342
+ )
343
+ .get();
344
+ expect(linear?.id).toBe("app-linear");
345
+ expect(linear?.scopeSeparator).toBe(",");
346
+ expect(JSON.parse(linear?.extraParamsJson ?? "{}")).toEqual({ prompt: "consent" });
347
+ // Migration 121 backfills metadata.keepAlive on the migrated tracker apps.
348
+ expect(JSON.parse(linear?.metadata ?? "{}")).toEqual({ actor: "app", keepAlive: true });
349
+
350
+ const jira = database
351
+ .query<
352
+ {
353
+ tokenAuthStyle: string;
354
+ tokenBodyFormat: string;
355
+ requiresRefreshTokenRotation: number;
356
+ revocationUrl: string | null;
357
+ metadata: string;
358
+ },
359
+ []
360
+ >(
361
+ `SELECT tokenAuthStyle, tokenBodyFormat, requiresRefreshTokenRotation,
362
+ revocationUrl, metadata
363
+ FROM oauth_apps WHERE provider = 'jira'`,
364
+ )
365
+ .get();
366
+ expect(jira).toMatchObject({
367
+ tokenAuthStyle: "basic",
368
+ tokenBodyFormat: "json",
369
+ requiresRefreshTokenRotation: 1,
370
+ revocationUrl: "https://jira.test/revoke",
371
+ });
372
+ expect(JSON.parse(jira?.metadata ?? "{}")).toEqual({
373
+ cloudId: "cloud-1",
374
+ webhookIds: ["hook-1"],
375
+ keepAlive: true,
376
+ });
377
+
378
+ const manualMcp = database
379
+ .query<{ id: string; source: string; metadata: string; status: string }, []>(
380
+ `SELECT a.id, a.source, a.metadata, z.status
381
+ FROM oauth_apps a
382
+ JOIN oauth_authorizations z ON z.appId = a.id
383
+ WHERE a.mcpServerId = 'mcp-manual'`,
384
+ )
385
+ .get();
386
+ expect(manualMcp?.id).toBe("mcp-app-mcp-token-manual");
387
+ expect(manualMcp?.source).toBe("dcr");
388
+ expect(JSON.parse(manualMcp?.metadata ?? "{}").clientSource).toBe("manual");
389
+ expect(manualMcp?.status).toBe("refresh-failed");
390
+
391
+ const oauthBinding = database
392
+ .query<{ oauth_authorization_id: string | null }, []>(
393
+ "SELECT oauth_authorization_id FROM script_credential_bindings WHERE id = 'binding-oauth'",
394
+ )
395
+ .get();
396
+ expect(oauthBinding?.oauth_authorization_id).toBe("token-vendor");
397
+ expect(
398
+ database.query<{ lockKey: string }, []>("SELECT lockKey FROM oauth_refresh_locks").get()
399
+ ?.lockKey,
400
+ ).toBe("linear");
401
+
402
+ const legacyTables = database
403
+ .query<{ name: string }, []>(
404
+ `SELECT name FROM sqlite_master
405
+ WHERE type = 'table'
406
+ AND name IN ('oauth_tokens', 'mcp_oauth_tokens', 'mcp_oauth_pending')`,
407
+ )
408
+ .all();
409
+ expect(legacyTables).toEqual([]);
410
+ const bindingColumns = database
411
+ .query<{ name: string }, []>("PRAGMA table_info(script_credential_bindings)")
412
+ .all()
413
+ .map((row) => row.name);
414
+ expect(bindingColumns).toContain("oauth_authorization_id");
415
+ expect(bindingColumns).not.toContain("oauth_provider");
416
+
417
+ const firstBackfill = autoEncryptLegacyOAuthSecrets(database);
418
+ expect(firstBackfill).toEqual({ appsEncrypted: 3, authorizationsEncrypted: 3 });
419
+ const encryptedLinear = database
420
+ .query<{ clientSecret: string; clientSecretEncrypted: number }, []>(
421
+ "SELECT clientSecret, clientSecretEncrypted FROM oauth_apps WHERE id = 'app-linear'",
422
+ )
423
+ .get();
424
+ expect(encryptedLinear?.clientSecretEncrypted).toBe(1);
425
+ expect(encryptedLinear?.clientSecret).not.toBe("linear-client-secret");
426
+ expect(decryptSecret(encryptedLinear!.clientSecret, getEncryptionKey())).toBe(
427
+ "linear-client-secret",
428
+ );
429
+ const encryptedAuthorization = database
430
+ .query<{ accessToken: string; refreshToken: string | null; tokensEncrypted: number }, []>(
431
+ `SELECT accessToken, refreshToken, tokensEncrypted
432
+ FROM oauth_authorizations WHERE id = 'token-linear'`,
433
+ )
434
+ .get();
435
+ expect(encryptedAuthorization?.tokensEncrypted).toBe(1);
436
+ expect(decryptSecret(encryptedAuthorization!.accessToken, getEncryptionKey())).toBe(
437
+ "linear-access-token",
438
+ );
439
+ expect(decryptSecret(encryptedAuthorization!.refreshToken!, getEncryptionKey())).toBe(
440
+ "linear-refresh-token",
441
+ );
442
+ expect(autoEncryptLegacyOAuthSecrets(database)).toEqual({
443
+ appsEncrypted: 0,
444
+ authorizationsEncrypted: 0,
445
+ });
446
+
447
+ expect(database.query("PRAGMA foreign_key_check").all()).toEqual([]);
448
+ expect(
449
+ database.query<{ integrity_check: string }, []>("PRAGMA integrity_check").get(),
450
+ ).toEqual({ integrity_check: "ok" });
451
+ } finally {
452
+ database.close();
453
+ }
454
+ });
455
+ });