@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.
- package/README.md +2 -2
- package/dist/{actions-qe5zjcbr.js → actions-mwshtkf7.js} +6 -6
- package/dist/{anthropic-messages-qjxzsabf.js → anthropic-messages-4gn944cn.js} +29 -13
- package/dist/{app-31x7k3vj.js → app-skbty91x.js} +4 -4
- package/dist/{assistant-4crt9dve.js → assistant-tg7dhfj8.js} +13 -13
- package/dist/{azure-openai-responses-ntfd05zd.js → azure-openai-responses-79zc0wyy.js} +28 -13
- package/dist/{boot-reembed-8g69ttz4.js → boot-reembed-ntjmeyh2.js} +4 -4
- package/dist/{boot-reembed-q648j733.js → boot-reembed-q5x0xpcp.js} +5 -5
- package/dist/{boot-scrub-logs-sfmzd2hj.js → boot-scrub-logs-5gznsfbe.js} +4 -4
- package/dist/{claude-adapter-9n3nzt65.js → claude-adapter-vxvcym3r.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-g807894s.js} +2 -2
- package/dist/{cli-bpvgs2y6.js → cli-072t49hf.js} +2123 -1533
- package/dist/{cli-g6qz2kts.js → cli-0df2g86x.js} +336 -18
- package/dist/cli-1728qh70.js +791 -0
- package/dist/{cli-vg1jxfqz.js → cli-3b1ayz83.js} +5 -5
- package/dist/{cli-r93nz80j.js → cli-3fxqwfh3.js} +1 -1
- package/dist/{cli-w6rhc30s.js → cli-3jrdwnn9.js} +20 -5
- package/dist/{cli-pasyk3vc.js → cli-46etyprn.js} +10 -5
- package/dist/{cli-433c0f4b.js → cli-4pfweq4x.js} +8 -8
- package/dist/{cli-2r7s8jhp.js → cli-5c4327dx.js} +7 -1
- package/dist/{cli-4s84mw72.js → cli-5hpvd15x.js} +2 -2
- package/dist/{cli-branks1w.js → cli-6s3ytc5x.js} +1 -1
- package/dist/{cli-da3x3btq.js → cli-7pt9sc83.js} +2 -2
- package/dist/{cli-2gv8ygwr.js → cli-7pzae66m.js} +2 -2
- package/dist/{cli-qs7nb3zn.js → cli-8wrympd7.js} +27 -1
- package/dist/{cli-qw1ccvxm.js → cli-95rbe11h.js} +70 -51
- package/dist/{cli-6rvm3jkk.js → cli-agdrxyex.js} +1 -1
- package/dist/{cli-wr706gez.js → cli-av2s7my4.js} +167 -66
- package/dist/{cli-59jy217p.js → cli-avw9ev3x.js} +55 -21
- package/dist/{cli-fkxxh5d4.js → cli-b2mqkg1h.js} +2 -2
- package/dist/{cli-1arw00dp.js → cli-ccjwgarc.js} +12 -3
- package/dist/{cli-pscrc6tw.js → cli-ch7t5v7n.js} +5 -0
- package/dist/{cli-tghre7g6.js → cli-cw70hymf.js} +1 -0
- package/dist/{cli-n61x2aph.js → cli-dmzv0986.js} +816 -1335
- package/dist/{cli-d1reabed.js → cli-g9vva2qh.js} +2 -2
- package/dist/{cli-ner6af7m.js → cli-gdpx70nc.js} +24048 -23715
- package/dist/{cli-96feg24n.js → cli-ghq3v4c9.js} +2 -2
- package/dist/{cli-wxbnjgce.js → cli-ky8esb8t.js} +3 -3
- package/dist/{cli-gdqdkr3j.js → cli-nf7sadev.js} +2 -2
- package/dist/{cli-h6cf69x5.js → cli-p9w7kb3h.js} +1 -1
- package/dist/{cli-xeatwfbe.js → cli-ry656xem.js} +24 -17
- package/dist/{cli-q8dway4w.js → cli-s8bdcke2.js} +1 -1
- package/dist/cli-s8s499xy.js +81 -0
- package/dist/{cli-sbkwvn5e.js → cli-srjqn7fd.js} +1 -1
- package/dist/cli-tv7vekye.js +367 -0
- package/dist/{cli-gg5bd32n.js → cli-vg3zrw6h.js} +4 -4
- package/dist/{cli-vx0y68jv.js → cli-vp9r4ap4.js} +5 -4
- package/dist/{cli-mg280wsv.js → cli-we156bmz.js} +1 -1
- package/dist/{cli-86fdnerz.js → cli-wwsaj97q.js} +9 -4
- package/dist/{cli-e0y603nn.js → cli-x0e0yvnq.js} +101 -1
- package/dist/{cli-mw3xpgwb.js → cli-x4apc9t3.js} +4 -4
- package/dist/cli.js +26 -21
- package/dist/{codex-adapter-6j2b38tg.js → codex-adapter-mmy741zd.js} +8 -9
- package/dist/{codex-session-runner-vz215wgw.js → codex-session-runner-sazmx9zt.js} +8 -9
- package/dist/{commands-6xr20jbe.js → commands-cxxye5e4.js} +3 -3
- package/dist/{db-1mmvk1z4.js → db-ab1954b7.js} +5 -3
- package/dist/{google-generative-ai-n1d3xt4n.js → google-generative-ai-wwa2ked5.js} +14 -18
- package/dist/{google-vertex-mxst23gy.js → google-vertex-71xmdd7n.js} +14 -18
- package/dist/{handlers-xvrknk8w.js → handlers-fmfe2343.js} +13 -13
- package/dist/{hook-qm7he8wn.js → hook-ygnw3cvt.js} +7 -8
- package/dist/{http-hj08zq67.js → http-kactcc0z.js} +1128 -536
- package/dist/{index-688jph6a.js → index-6erq0wvk.js} +11 -11
- package/dist/{index-gze4fg2w.js → index-g86j2yt5.js} +9 -9
- package/dist/{index-etz7d8md.js → index-gmzs6a2q.js} +10 -10
- package/dist/{index-wj8dfpwh.js → index-hmp82wn0.js} +12 -12
- package/dist/{keepalive-94wn9yxw.js → keepalive-3cvt26dw.js} +18 -14
- package/dist/{lead-rvcqkp11.js → lead-ysf26cjr.js} +31 -32
- package/dist/{maintenance-qhfc4dnp.js → maintenance-9npwptbb.js} +5 -5
- package/dist/{mistral-conversations-ykcg66za.js → mistral-conversations-26ya4yah.js} +17 -13
- package/dist/{oauth-refresh-sweep-rj8wgvsw.js → oauth-refresh-sweep-0r2v76vw.js} +23 -17
- package/dist/{onboard-8bv8q1wc.js → onboard-ddb1674a.js} +2 -2
- package/dist/{openai-codex-responses-zy0907j9.js → openai-codex-responses-6rr2ye0s.js} +78 -38
- package/dist/{openai-completions-xk4p56n9.js → openai-completions-zfa9nkvt.js} +130 -33
- package/dist/{openai-responses-4gv3ca5d.js → openai-responses-9wmdks0g.js} +38 -14
- package/dist/{opencode-adapter-5bwzvbwj.js → opencode-adapter-h6rkxwrk.js} +2 -2
- package/dist/{openrouter-images-4fsrrzes.js → openrouter-images-dgq98729.js} +10 -3
- package/dist/{otel-impl-1kf6fr2m.js → otel-impl-b43bkq8e.js} +4 -4
- package/dist/{pi-messages-zb8v76wx.js → pi-messages-nzjbbz47.js} +4 -6
- package/dist/{pi-mono-adapter-9sg69kg3.js → pi-mono-adapter-ke789738.js} +2328 -1281
- package/dist/{pricing-refresh-e84tar2s.js → pricing-refresh-2rqf114z.js} +5 -5
- package/dist/{rbac-roles-atytq5ch.js → rbac-roles-5wkhc8xv.js} +5 -5
- package/dist/{rbac-roles-pg0zvnwn.js → rbac-roles-6vtafvzb.js} +4 -4
- package/dist/{seed-pricing-xd6nxq7k.js → seed-pricing-23wrb873.js} +4 -4
- package/dist/{setup-4z389yk0.js → setup-6rny4fk6.js} +2 -2
- package/dist/{worker-m61r0t7m.js → worker-yapewe14.js} +31 -32
- package/openapi.json +2671 -1811
- package/package.json +10 -5
- package/plugin/commands/swarm-chat.md +2 -0
- package/plugin/pi-skills/swarm-chat/SKILL.md +2 -0
- package/src/be/connection-bindings-blob-migration.ts +123 -0
- package/src/be/db-queries/mcp-oauth.ts +409 -247
- package/src/be/db-queries/oauth.ts +813 -131
- package/src/be/db.ts +97 -6
- package/src/be/migrations/117_unified_oauth.sql +624 -0
- package/src/be/migrations/119_agent_avatar.sql +7 -0
- package/src/be/migrations/120_task_title.sql +3 -0
- package/src/be/oauth-credential-bindings.ts +48 -14
- package/src/be/oauth-encryption-backfill.ts +84 -0
- package/src/be/oauth-refresh-sweep.ts +38 -23
- package/src/be/script-connections.ts +1130 -163
- package/src/be/script-credential-broker.ts +37 -27
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-capabilities.ts +62 -0
- package/src/be/seed-pricing.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/vendored-openapi.ts +98 -0
- package/src/commands/runner.ts +69 -6
- package/src/http/agents.ts +23 -5
- package/src/http/all-routes.ts +1 -0
- package/src/http/core.ts +8 -5
- package/src/http/index.ts +18 -5
- package/src/http/mcp-bridge.ts +5 -2
- package/src/http/mcp-oauth.ts +103 -101
- package/src/http/oauth-callback.ts +332 -0
- package/src/http/oauth-generic.ts +14 -54
- package/src/http/poll.ts +5 -1
- package/src/http/script-connections.ts +651 -170
- package/src/http/scripts.ts +4 -2
- package/src/http/sessions.ts +1 -1
- package/src/http/tasks.ts +35 -0
- package/src/http/trackers/jira.ts +22 -34
- package/src/http/trackers/linear.ts +21 -36
- package/src/http/x.ts +6 -2
- package/src/jira/app.ts +12 -6
- package/src/jira/oauth.ts +48 -48
- package/src/linear/app.ts +44 -2
- package/src/linear/oauth.ts +12 -37
- package/src/oauth/app-validation.ts +27 -9
- package/src/oauth/ensure-mcp-token.ts +1 -0
- package/src/oauth/ensure-token.ts +331 -108
- package/src/oauth/identity-capture.ts +103 -0
- package/src/oauth/keepalive.ts +14 -9
- package/src/oauth/presets.ts +242 -0
- package/src/oauth/wrapper.ts +197 -76
- package/src/prompts/base-prompt.ts +48 -4
- package/src/prompts/session-templates.ts +13 -1
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/rbac/legacy-policy.ts +2 -1
- package/src/rbac/permissions.ts +7 -3
- package/src/scripts-runtime/credential-broker/broker.ts +51 -8
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +30 -3
- package/src/scripts-runtime/credential-broker/index.ts +1 -1
- package/src/scripts-runtime/credential-broker/types.ts +55 -8
- package/src/scripts-runtime/egress-secrets.ts +6 -2
- package/src/scripts-runtime/eval-harness.ts +2 -2
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -0
- package/src/scripts-runtime/types/stdlib.d.ts +11 -0
- package/src/server.ts +230 -126
- package/src/tests/agent-avatar-resolution.test.ts +123 -0
- package/src/tests/agents-list-model-display.test.ts +10 -0
- package/src/tests/base-prompt.test.ts +72 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/connection-embedded-auth.test.ts +709 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/credential-broker.test.ts +123 -125
- package/src/tests/db-queries-oauth.test.ts +126 -1
- package/src/tests/ensure-token.test.ts +44 -2
- package/src/tests/list-endpoint-slimming.test.ts +45 -0
- package/src/tests/mcp-oauth-manual-client.test.ts +20 -1
- package/src/tests/mcp-oauth-queries.test.ts +154 -8
- package/src/tests/oauth-access-token-tool.test.ts +37 -1
- package/src/tests/oauth-callback-flow.test.ts +534 -0
- package/src/tests/oauth-credential-bindings.test.ts +50 -22
- package/src/tests/oauth-presets.test.ts +275 -0
- package/src/tests/oauth-refresh-failure.test.ts +263 -0
- package/src/tests/oauth-refresh-sweep.test.ts +83 -3
- package/src/tests/oauth-wrapper.test.ts +77 -55
- package/src/tests/prompt-template-session.test.ts +6 -5
- package/src/tests/rbac-charact-misc-tools.test.ts +60 -0
- package/src/tests/rbac-engine.test.ts +2 -1
- package/src/tests/script-connections-http.test.ts +356 -11
- package/src/tests/script-connections.test.ts +102 -0
- package/src/tests/sdk-allowlist.test.ts +4 -1
- package/src/tests/seed-capabilities.test.ts +92 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/sessions.test.ts +46 -0
- package/src/tests/spec-base-url.test.ts +266 -0
- package/src/tests/status.test.ts +3 -4
- package/src/tests/swarm-config-reserved-keys.test.ts +4 -2
- package/src/tests/task-title-route.test.ts +106 -0
- package/src/tests/tool-annotations.test.ts +6 -3
- package/src/tests/tracker-fold.test.ts +412 -0
- package/src/tests/unified-oauth-migration.test.ts +455 -0
- package/src/tests/vendored-openapi.test.ts +263 -0
- package/src/tools/credential-bindings/tool.ts +232 -77
- package/src/tools/oauth-access-token.ts +55 -6
- package/src/tools/script-connections/tool.ts +73 -53
- package/src/tools/swarm-config/set-config.ts +24 -50
- package/src/tools/update-profile.ts +15 -5
- package/src/tracker/types.ts +12 -0
- package/src/types.ts +29 -0
- package/src/utils/context-window.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +6 -2
- package/vendored-openapi/README.md +22 -0
- package/vendored-openapi/github.json +7759 -0
- package/vendored-openapi/gmail.json +393 -0
- package/vendored-openapi/jira.json +3396 -0
- package/vendored-openapi/linear.json +252 -0
- package/vendored-openapi/manifest.json +128 -0
- package/vendored-openapi/slack.json +2895 -0
- package/dist/cli-0pygq9r1.js +0 -429
- package/dist/cli-8hhms4hp.js +0 -393
- package/dist/cli-z9gzq07x.js +0 -27
- package/src/scripts-runtime/credential-broker/store.ts +0 -72
- package/dist/{otel-g299k41b.js → otel-63jr5v38.js} +1 -1
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { closeDb, createAgent, getDb, initDb } from "../be/db";
|
|
11
11
|
import {
|
|
12
|
+
deleteOAuthTokens,
|
|
12
13
|
getOAuthApp,
|
|
13
14
|
getOAuthTokens,
|
|
14
15
|
storeOAuthTokens,
|
|
@@ -41,8 +42,8 @@ type ToolResult = {
|
|
|
41
42
|
bindings: Array<{
|
|
42
43
|
configKey: string;
|
|
43
44
|
authKind?: "config" | "oauth";
|
|
44
|
-
|
|
45
|
-
tokenStatus?: "ok" | "expiring" | "missing";
|
|
45
|
+
oauthAuthorizationId?: string;
|
|
46
|
+
tokenStatus?: "ok" | "expiring" | "refresh-failed" | "revoked" | "missing";
|
|
46
47
|
}>;
|
|
47
48
|
};
|
|
48
49
|
};
|
|
@@ -100,7 +101,6 @@ async function listen(server: Server): Promise<number> {
|
|
|
100
101
|
function cleanupRows() {
|
|
101
102
|
const db = getDb();
|
|
102
103
|
db.run("DELETE FROM script_credential_bindings WHERE config_key LIKE 'PHASE2_%'");
|
|
103
|
-
db.run("DELETE FROM oauth_tokens WHERE provider LIKE 'phase2-%'");
|
|
104
104
|
db.run("DELETE FROM oauth_apps WHERE provider LIKE 'phase2-%'");
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -147,24 +147,31 @@ describe("OAuth credential bindings", () => {
|
|
|
147
147
|
.all()
|
|
148
148
|
.map((column) => column.name);
|
|
149
149
|
expect(columns).toContain("auth_kind");
|
|
150
|
-
expect(columns).toContain("
|
|
150
|
+
expect(columns).toContain("oauth_authorization_id");
|
|
151
|
+
|
|
152
|
+
upsertOAuthApp("phase2-roundtrip", testApp("phase2-roundtrip"));
|
|
153
|
+
storeOAuthTokens("phase2-roundtrip", {
|
|
154
|
+
accessToken: "roundtrip-access",
|
|
155
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
156
|
+
});
|
|
157
|
+
const authorizationId = getOAuthTokens("phase2-roundtrip")!.id;
|
|
151
158
|
|
|
152
159
|
const binding = upsertCredentialBinding({
|
|
153
160
|
configKey: "PHASE2_ROUNDTRIP_OAUTH",
|
|
154
161
|
allowedHosts: ["api.vendor.test"],
|
|
155
162
|
headerTemplate: "Authorization: Bearer [REDACTED:PHASE2_ROUNDTRIP_OAUTH]",
|
|
156
163
|
authKind: "oauth",
|
|
157
|
-
|
|
164
|
+
oauthAuthorizationId: authorizationId,
|
|
158
165
|
});
|
|
159
166
|
|
|
160
167
|
expect(binding.authKind).toBe("oauth");
|
|
161
|
-
expect(binding.
|
|
168
|
+
expect(binding.oauthAuthorizationId).toBe(authorizationId);
|
|
162
169
|
|
|
163
170
|
const listed = listRelationalCredentialBindings({ includeInactive: true }).find(
|
|
164
171
|
(item) => item.id === binding.id,
|
|
165
172
|
);
|
|
166
173
|
expect(listed?.authKind).toBe("oauth");
|
|
167
|
-
expect(listed?.
|
|
174
|
+
expect(listed?.oauthAuthorizationId).toBe(authorizationId);
|
|
168
175
|
});
|
|
169
176
|
|
|
170
177
|
test("OAuth binding resolves through the stored access token", async () => {
|
|
@@ -179,7 +186,7 @@ describe("OAuth credential bindings", () => {
|
|
|
179
186
|
allowedHosts: ["api.vendor.test"],
|
|
180
187
|
headerTemplate: "Authorization: Bearer [REDACTED:PHASE2_RESOLVE_OAUTH]",
|
|
181
188
|
authKind: "oauth",
|
|
182
|
-
|
|
189
|
+
oauthAuthorizationId: getOAuthTokens("phase2-resolve")!.id,
|
|
183
190
|
});
|
|
184
191
|
process.env.PHASE2_RESOLVE_OAUTH = "env-must-not-win";
|
|
185
192
|
|
|
@@ -205,7 +212,7 @@ describe("OAuth credential bindings", () => {
|
|
|
205
212
|
allowedHosts: ["api.vendor.test"],
|
|
206
213
|
headerTemplate: "Authorization: Bearer [REDACTED:PHASE2_REFRESH_OAUTH]",
|
|
207
214
|
authKind: "oauth",
|
|
208
|
-
|
|
215
|
+
oauthAuthorizationId: getOAuthTokens("phase2-refresh")!.id,
|
|
209
216
|
});
|
|
210
217
|
|
|
211
218
|
const fetchSpy = mock((input: string | URL | Request, init?: RequestInit) => {
|
|
@@ -254,7 +261,7 @@ describe("OAuth credential bindings", () => {
|
|
|
254
261
|
allowedHosts: ["api.vendor.test"],
|
|
255
262
|
headerTemplate: "Authorization: Bearer [REDACTED:PHASE2_BASIC_OAUTH]",
|
|
256
263
|
authKind: "oauth",
|
|
257
|
-
|
|
264
|
+
oauthAuthorizationId: getOAuthTokens("phase2-basic")!.id,
|
|
258
265
|
});
|
|
259
266
|
|
|
260
267
|
const config = getOAuthProviderConfig("phase2-basic");
|
|
@@ -308,7 +315,16 @@ describe("OAuth credential bindings", () => {
|
|
|
308
315
|
expect(url).not.toContain("scope=read%2Cwrite");
|
|
309
316
|
});
|
|
310
317
|
|
|
311
|
-
test("
|
|
318
|
+
test("metadata actor remains an authorization parameter without lifted extras", () => {
|
|
319
|
+
upsertOAuthApp("phase2-actor", {
|
|
320
|
+
...testApp("phase2-actor"),
|
|
321
|
+
metadata: JSON.stringify({ actor: "app" }),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(getOAuthProviderConfig("phase2-actor")?.extraParams).toEqual({ actor: "app" });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("credential-bindings tool now accepts tracker providers (reserved carve-out removed in step-8)", async () => {
|
|
312
328
|
const result = (await credentialBindingsTool().handler(
|
|
313
329
|
{
|
|
314
330
|
action: "oauth-app-upsert",
|
|
@@ -322,9 +338,10 @@ describe("OAuth credential bindings", () => {
|
|
|
322
338
|
meta(),
|
|
323
339
|
)) as ToolResult;
|
|
324
340
|
|
|
325
|
-
|
|
326
|
-
expect(result.structuredContent.
|
|
327
|
-
expect(
|
|
341
|
+
// linear/jira are ordinary oauth_apps rows now — no reserved-provider block.
|
|
342
|
+
expect(result.structuredContent.success).toBe(true);
|
|
343
|
+
expect(result.structuredContent.message).not.toContain("dedicated tracker");
|
|
344
|
+
expect(getOAuthApp("jira")?.clientId).toBe("jira-client");
|
|
328
345
|
});
|
|
329
346
|
|
|
330
347
|
test("credential-bindings tool validates OAuth app URLs in production", async () => {
|
|
@@ -376,7 +393,7 @@ describe("OAuth credential bindings", () => {
|
|
|
376
393
|
allowedHosts: ["api.vendor.test"],
|
|
377
394
|
headerTemplate: "Authorization: Bearer [REDACTED:PHASE2_BROKEN_OAUTH]",
|
|
378
395
|
authKind: "oauth",
|
|
379
|
-
|
|
396
|
+
oauthAuthorizationId: getOAuthTokens("phase2-broken")!.id,
|
|
380
397
|
});
|
|
381
398
|
upsertCredentialBinding({
|
|
382
399
|
configKey: "PHASE2_HEALTHY_CONFIG",
|
|
@@ -406,15 +423,21 @@ describe("OAuth credential bindings", () => {
|
|
|
406
423
|
delete process.env.PHASE2_HEALTHY_CONFIG;
|
|
407
424
|
});
|
|
408
425
|
|
|
409
|
-
test("
|
|
426
|
+
test("revoked OAuth token skips binding resolution and list reports revoked", async () => {
|
|
410
427
|
upsertOAuthApp("phase2-missing", testApp("phase2-missing"));
|
|
428
|
+
storeOAuthTokens("phase2-missing", {
|
|
429
|
+
accessToken: "soon-deleted",
|
|
430
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
431
|
+
});
|
|
432
|
+
const authorizationId = getOAuthTokens("phase2-missing")!.id;
|
|
411
433
|
upsertCredentialBinding({
|
|
412
434
|
configKey: "PHASE2_MISSING_OAUTH",
|
|
413
435
|
allowedHosts: ["api.vendor.test"],
|
|
414
436
|
headerTemplate: "Authorization: Bearer [REDACTED:PHASE2_MISSING_OAUTH]",
|
|
415
437
|
authKind: "oauth",
|
|
416
|
-
|
|
438
|
+
oauthAuthorizationId: authorizationId,
|
|
417
439
|
});
|
|
440
|
+
deleteOAuthTokens("phase2-missing");
|
|
418
441
|
process.env.PHASE2_MISSING_OAUTH = "env-must-not-win";
|
|
419
442
|
|
|
420
443
|
const bindings = await buildScriptCredentialBindings({});
|
|
@@ -428,7 +451,9 @@ describe("OAuth credential bindings", () => {
|
|
|
428
451
|
(binding) => binding.configKey === "PHASE2_MISSING_OAUTH",
|
|
429
452
|
);
|
|
430
453
|
expect(result.structuredContent.success).toBe(true);
|
|
431
|
-
|
|
454
|
+
// Disconnect revokes the authorization in place (row kept for referential
|
|
455
|
+
// continuity), so the binding surfaces as `revoked`, not `missing`.
|
|
456
|
+
expect(listed?.tokenStatus).toBe("revoked");
|
|
432
457
|
});
|
|
433
458
|
|
|
434
459
|
test("generic OAuth callback exchanges code and stores tokens", async () => {
|
|
@@ -472,17 +497,20 @@ describe("OAuth credential bindings", () => {
|
|
|
472
497
|
}
|
|
473
498
|
});
|
|
474
499
|
|
|
475
|
-
test("generic OAuth callback
|
|
500
|
+
test("generic OAuth callback no longer special-cases linear (unified routing)", async () => {
|
|
501
|
+
// step-4 dropped the DEDICATED_CALLBACK_PROVIDERS 409 asymmetry: every
|
|
502
|
+
// provider routes through the unified, state-keyed handler. An unknown
|
|
503
|
+
// state now reports the same invalid-state error as any other provider.
|
|
476
504
|
const server = createOAuthServer();
|
|
477
505
|
const port = await listen(server);
|
|
478
506
|
try {
|
|
479
507
|
const res = await fetch(
|
|
480
|
-
`http://localhost:${port}/api/oauth/linear/callback?code=callback-code&state=state`,
|
|
508
|
+
`http://localhost:${port}/api/oauth/linear/callback?code=callback-code&state=unknown-state`,
|
|
481
509
|
);
|
|
482
510
|
|
|
483
|
-
expect(res.status).toBe(
|
|
511
|
+
expect(res.status).toBe(400);
|
|
484
512
|
const body = (await res.json()) as { error: string };
|
|
485
|
-
expect(body.error).toContain("
|
|
513
|
+
expect(body.error).toContain("Invalid or expired OAuth state");
|
|
486
514
|
} finally {
|
|
487
515
|
server.close();
|
|
488
516
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { closeDb, createAgent, initDb } from "../be/db";
|
|
6
|
+
import { getOAuthApp } from "../be/db-queries/oauth";
|
|
7
|
+
import { handleScriptConnections } from "../http/script-connections";
|
|
8
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
9
|
+
import { assertOAuthAppUrlsSafe } from "../oauth/app-validation";
|
|
10
|
+
import {
|
|
11
|
+
getOAuthPreset,
|
|
12
|
+
hydrateOAuthAppFromPreset,
|
|
13
|
+
listOAuthPresetIds,
|
|
14
|
+
listOAuthPresets,
|
|
15
|
+
} from "../oauth/presets";
|
|
16
|
+
import { buildAuthorizationUrl } from "../oauth/wrapper";
|
|
17
|
+
|
|
18
|
+
const TEST_DB_PATH = "./test-oauth-presets.sqlite";
|
|
19
|
+
|
|
20
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
21
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
22
|
+
try {
|
|
23
|
+
await unlink(path + suffix);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type TestResponse = { status: number; text: string; json: () => Promise<unknown> };
|
|
31
|
+
|
|
32
|
+
async function dispatch(
|
|
33
|
+
path: string,
|
|
34
|
+
init: { method?: string; body?: unknown; agentId?: string } = {},
|
|
35
|
+
): Promise<TestResponse> {
|
|
36
|
+
const req = Readable.from(
|
|
37
|
+
init.body === undefined ? [] : [Buffer.from(JSON.stringify(init.body))],
|
|
38
|
+
) as IncomingMessage;
|
|
39
|
+
req.method = init.method ?? "GET";
|
|
40
|
+
req.url = path;
|
|
41
|
+
req.headers = init.agentId
|
|
42
|
+
? { "x-agent-id": init.agentId, "content-type": "application/json" }
|
|
43
|
+
: { "content-type": "application/json" };
|
|
44
|
+
|
|
45
|
+
let status = 200;
|
|
46
|
+
let text = "";
|
|
47
|
+
const res = {
|
|
48
|
+
headersSent: false,
|
|
49
|
+
writableEnded: false,
|
|
50
|
+
setHeader() {},
|
|
51
|
+
writeHead(code: number) {
|
|
52
|
+
status = code;
|
|
53
|
+
this.headersSent = true;
|
|
54
|
+
return this;
|
|
55
|
+
},
|
|
56
|
+
end(chunk?: unknown) {
|
|
57
|
+
if (chunk !== undefined) text += String(chunk);
|
|
58
|
+
this.writableEnded = true;
|
|
59
|
+
return this;
|
|
60
|
+
},
|
|
61
|
+
} as unknown as ServerResponse;
|
|
62
|
+
|
|
63
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
64
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
65
|
+
if (!(await handleScriptConnections(req, res, pathSegments, queryParams, init.agentId))) {
|
|
66
|
+
res.writeHead(404);
|
|
67
|
+
res.end("Not Found");
|
|
68
|
+
}
|
|
69
|
+
return { status, text, json: async () => JSON.parse(text) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let leadAgentId: string;
|
|
73
|
+
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
76
|
+
initDb(TEST_DB_PATH);
|
|
77
|
+
leadAgentId = createAgent({ name: "presets-lead", isLead: true, status: "idle" }).id;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterAll(async () => {
|
|
81
|
+
closeDb();
|
|
82
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("oauth presets — pure data", () => {
|
|
86
|
+
test("exposes the expected curated preset ids", () => {
|
|
87
|
+
expect(listOAuthPresetIds().sort()).toEqual(
|
|
88
|
+
["github", "google", "jira", "linear", "notion", "slack"].sort(),
|
|
89
|
+
);
|
|
90
|
+
expect(listOAuthPresets()).toHaveLength(6);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("every preset carries endpoints, setup hints, and SSRF-safe URLs", () => {
|
|
94
|
+
for (const preset of listOAuthPresets()) {
|
|
95
|
+
expect(preset.authorizeUrl.startsWith("https://")).toBe(true);
|
|
96
|
+
expect(preset.tokenUrl.startsWith("https://")).toBe(true);
|
|
97
|
+
// Parse without throwing.
|
|
98
|
+
expect(() => new URL(preset.authorizeUrl)).not.toThrow();
|
|
99
|
+
expect(() => new URL(preset.tokenUrl)).not.toThrow();
|
|
100
|
+
if (preset.revocationUrl) expect(() => new URL(preset.revocationUrl as string)).not.toThrow();
|
|
101
|
+
if (preset.userinfoUrl) expect(() => new URL(preset.userinfoUrl as string)).not.toThrow();
|
|
102
|
+
expect(preset.setupHints.length).toBeGreaterThan(0);
|
|
103
|
+
// Defense in depth: the merged endpoints must survive the SSRF gate.
|
|
104
|
+
expect(() =>
|
|
105
|
+
assertOAuthAppUrlsSafe({ authorizeUrl: preset.authorizeUrl, tokenUrl: preset.tokenUrl }),
|
|
106
|
+
).not.toThrow();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("provider-specific quirks are encoded", () => {
|
|
111
|
+
const google = getOAuthPreset("google");
|
|
112
|
+
expect(google?.extraParams).toMatchObject({ access_type: "offline", prompt: "consent" });
|
|
113
|
+
expect(google?.scopeSeparator).toBe(" ");
|
|
114
|
+
|
|
115
|
+
const notion = getOAuthPreset("notion");
|
|
116
|
+
expect(notion?.tokenAuthStyle).toBe("basic");
|
|
117
|
+
expect(notion?.tokenBodyFormat).toBe("json");
|
|
118
|
+
|
|
119
|
+
const jira = getOAuthPreset("jira");
|
|
120
|
+
expect(jira?.requiresRefreshTokenRotation).toBe(true);
|
|
121
|
+
|
|
122
|
+
const linear = getOAuthPreset("linear");
|
|
123
|
+
expect(linear?.scopeSeparator).toBe(",");
|
|
124
|
+
|
|
125
|
+
const slack = getOAuthPreset("slack");
|
|
126
|
+
expect(slack?.scopeSeparator).toBe(",");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("hydration marks curated-prefill and lets explicit fields win", () => {
|
|
130
|
+
const preset = getOAuthPreset("google");
|
|
131
|
+
if (!preset) throw new Error("google preset missing");
|
|
132
|
+
|
|
133
|
+
const merged = hydrateOAuthAppFromPreset(preset, {
|
|
134
|
+
tokenUrl: "https://custom.example.com/token",
|
|
135
|
+
scopes: ["custom.scope"],
|
|
136
|
+
extraParams: { prompt: "select_account" },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(merged.source).toBe("curated-prefill");
|
|
140
|
+
expect(merged.authorizeUrl).toBe(preset.authorizeUrl); // filled from preset
|
|
141
|
+
expect(merged.tokenUrl).toBe("https://custom.example.com/token"); // explicit wins
|
|
142
|
+
expect(merged.scopes).toEqual(["custom.scope"]); // explicit wins
|
|
143
|
+
// extraParams merge per-key: explicit prompt wins, preset access_type stays.
|
|
144
|
+
expect(merged.extraParams).toMatchObject({ access_type: "offline", prompt: "select_account" });
|
|
145
|
+
expect(merged.setupHints).toEqual(preset.setupHints);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("google hydration yields offline params in the built authorization URL", async () => {
|
|
149
|
+
const preset = getOAuthPreset("google");
|
|
150
|
+
if (!preset) throw new Error("google preset missing");
|
|
151
|
+
const hydrated = hydrateOAuthAppFromPreset(preset, {});
|
|
152
|
+
|
|
153
|
+
// buildAuthorizationUrl now persists a PKCE pending row keyed to a real app
|
|
154
|
+
// (step-4), so the app must exist first.
|
|
155
|
+
const created = await dispatch("/api/oauth-apps", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
agentId: leadAgentId,
|
|
158
|
+
body: { presetId: "google", clientId: "google-client", clientSecret: "google-secret" },
|
|
159
|
+
});
|
|
160
|
+
expect(created.status).toBe(200);
|
|
161
|
+
|
|
162
|
+
const { url } = await buildAuthorizationUrl(
|
|
163
|
+
{
|
|
164
|
+
provider: hydrated.provider,
|
|
165
|
+
clientId: "google-client",
|
|
166
|
+
clientSecret: "google-secret",
|
|
167
|
+
authorizeUrl: hydrated.authorizeUrl,
|
|
168
|
+
tokenUrl: hydrated.tokenUrl,
|
|
169
|
+
redirectUri: "https://api.public.test/api/oauth/callback",
|
|
170
|
+
scopes: hydrated.scopes,
|
|
171
|
+
scopeSeparator: hydrated.scopeSeparator,
|
|
172
|
+
extraParams: hydrated.extraParams,
|
|
173
|
+
},
|
|
174
|
+
{ flow: "generic" },
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const parsed = new URL(url);
|
|
178
|
+
expect(parsed.searchParams.get("access_type")).toBe("offline");
|
|
179
|
+
expect(parsed.searchParams.get("prompt")).toBe("consent");
|
|
180
|
+
// Space separator surfaces as `+` / `%20` in the encoded scope.
|
|
181
|
+
expect(parsed.searchParams.get("scope")).toBe("openid email profile");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("oauth presets — HTTP", () => {
|
|
186
|
+
test("GET /api/oauth-presets lists all six presets with hints", async () => {
|
|
187
|
+
const res = await dispatch("/api/oauth-presets");
|
|
188
|
+
expect(res.status).toBe(200);
|
|
189
|
+
const body = (await res.json()) as {
|
|
190
|
+
presets: Array<{ id: string; authorizeUrl: string; setupHints: string[] }>;
|
|
191
|
+
};
|
|
192
|
+
expect(body.presets).toHaveLength(6);
|
|
193
|
+
expect(body.presets.map((p) => p.id).sort()).toEqual(
|
|
194
|
+
["github", "google", "jira", "linear", "notion", "slack"].sort(),
|
|
195
|
+
);
|
|
196
|
+
for (const preset of body.presets) {
|
|
197
|
+
expect(preset.authorizeUrl.startsWith("https://")).toBe(true);
|
|
198
|
+
expect(preset.setupHints.length).toBeGreaterThan(0);
|
|
199
|
+
}
|
|
200
|
+
// No secrets ever live in presets.
|
|
201
|
+
expect(res.text).not.toContain("clientSecret");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("POST /api/oauth-apps with presetId 'google' hydrates endpoints + source", async () => {
|
|
205
|
+
const res = await dispatch("/api/oauth-apps", {
|
|
206
|
+
method: "POST",
|
|
207
|
+
agentId: leadAgentId,
|
|
208
|
+
body: { presetId: "google", clientId: "google-client", clientSecret: "google-secret" },
|
|
209
|
+
});
|
|
210
|
+
expect(res.status).toBe(200);
|
|
211
|
+
const body = (await res.json()) as {
|
|
212
|
+
oauthApp: { provider: string; authorizeUrl: string; source: string };
|
|
213
|
+
redirectUri: string;
|
|
214
|
+
setupHints: string[];
|
|
215
|
+
};
|
|
216
|
+
expect(body.redirectUri).toContain("/api/oauth/callback");
|
|
217
|
+
expect(body.setupHints.length).toBeGreaterThan(0);
|
|
218
|
+
expect(body.oauthApp.source).toBe("curated-prefill");
|
|
219
|
+
expect(res.text).not.toContain("google-secret");
|
|
220
|
+
|
|
221
|
+
const stored = getOAuthApp("google");
|
|
222
|
+
expect(stored?.source).toBe("curated-prefill");
|
|
223
|
+
expect(stored?.authorizeUrl).toBe("https://accounts.google.com/o/oauth2/v2/auth");
|
|
224
|
+
expect(stored?.tokenUrl).toBe("https://oauth2.googleapis.com/token");
|
|
225
|
+
expect(stored?.scopeSeparator).toBe(" ");
|
|
226
|
+
expect(stored?.revocationUrl).toBe("https://oauth2.googleapis.com/revoke");
|
|
227
|
+
expect(stored?.extraParamsJson ?? "").toContain("access_type");
|
|
228
|
+
expect(stored?.clientSecret).toBe("google-secret");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("no presetId + missing required endpoint fields is rejected with 400", async () => {
|
|
232
|
+
// provider/authorizeUrl/tokenUrl are optional in the schema (preset can
|
|
233
|
+
// supply them) — but with no presetId the handler must still reject a
|
|
234
|
+
// request that omits them, not write a half-configured app.
|
|
235
|
+
const res = await dispatch("/api/oauth-apps", {
|
|
236
|
+
method: "POST",
|
|
237
|
+
agentId: leadAgentId,
|
|
238
|
+
body: { provider: "bare_provider", clientId: "c", clientSecret: "s" },
|
|
239
|
+
});
|
|
240
|
+
expect(res.status).toBe(400);
|
|
241
|
+
expect(res.text).toContain("authorizeUrl");
|
|
242
|
+
expect(getOAuthApp("bare_provider")).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("explicit fields override the preset during hydration", async () => {
|
|
246
|
+
const res = await dispatch("/api/oauth-apps", {
|
|
247
|
+
method: "POST",
|
|
248
|
+
agentId: leadAgentId,
|
|
249
|
+
body: {
|
|
250
|
+
presetId: "google",
|
|
251
|
+
provider: "google_custom",
|
|
252
|
+
clientId: "gc",
|
|
253
|
+
clientSecret: "gcs",
|
|
254
|
+
tokenUrl: "https://custom.example.com/token",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
expect(res.status).toBe(200);
|
|
258
|
+
const stored = getOAuthApp("google_custom");
|
|
259
|
+
expect(stored?.tokenUrl).toBe("https://custom.example.com/token");
|
|
260
|
+
expect(stored?.authorizeUrl).toBe("https://accounts.google.com/o/oauth2/v2/auth");
|
|
261
|
+
expect(stored?.source).toBe("curated-prefill");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("unknown presetId is rejected with 400 listing valid ids", async () => {
|
|
265
|
+
const res = await dispatch("/api/oauth-apps", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
agentId: leadAgentId,
|
|
268
|
+
body: { presetId: "not-a-real-preset", clientId: "c", clientSecret: "s" },
|
|
269
|
+
});
|
|
270
|
+
expect(res.status).toBe(400);
|
|
271
|
+
const body = (await res.json()) as { error: string };
|
|
272
|
+
expect(body.error).toContain("Unknown presetId");
|
|
273
|
+
expect(body.error).toContain("google");
|
|
274
|
+
});
|
|
275
|
+
});
|