@desplega.ai/agent-swarm 1.63.0 → 1.64.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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Anonymized telemetry for agent-swarm.
3
+ *
4
+ * - Opt-out via ANONYMIZED_TELEMETRY=false
5
+ * - Fire-and-forget: never throws, never blocks
6
+ * - No external dependencies (uses global fetch + node:crypto)
7
+ * - Importable from both API server and workers
8
+ */
9
+ import { randomUUID } from "node:crypto";
10
+
11
+ const TELEMETRY_ENDPOINT = "https://proxy.desplega.sh/v1/events";
12
+ const PRODUCT = "agent-swarm";
13
+ const TIMEOUT_MS = 5_000;
14
+
15
+ let installationId: string | null = null;
16
+ let source = "unknown";
17
+
18
+ function isEnabled(): boolean {
19
+ return process.env.ANONYMIZED_TELEMETRY !== "false";
20
+ }
21
+
22
+ /**
23
+ * Initialize telemetry. Call once at startup.
24
+ * @param sourceId - "api-server" or "worker"
25
+ * @param getConfig - reads a key from swarm_config (global scope)
26
+ * @param setConfig - writes a key to swarm_config (global scope)
27
+ */
28
+ export async function initTelemetry(
29
+ sourceId: string,
30
+ getConfig: (key: string) => Promise<string | undefined> | string | undefined,
31
+ setConfig: (key: string, value: string) => Promise<void> | void,
32
+ ): Promise<void> {
33
+ if (!isEnabled()) return;
34
+ source = sourceId;
35
+ try {
36
+ const existing = await getConfig("telemetry_installation_id");
37
+ if (existing) {
38
+ installationId = existing;
39
+ } else {
40
+ installationId = `install_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
41
+ await setConfig("telemetry_installation_id", installationId);
42
+ }
43
+ } catch {
44
+ // Config access failed — generate ephemeral ID so telemetry still works this session
45
+ installationId = `ephemeral_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
46
+ }
47
+ }
48
+
49
+ interface TrackOptions {
50
+ event: string;
51
+ properties?: Record<string, unknown>;
52
+ metadata?: Record<string, unknown>;
53
+ }
54
+
55
+ /** Fire-and-forget telemetry event. Never throws, never blocks. */
56
+ export function track(options: TrackOptions): void {
57
+ if (!isEnabled() || !installationId) return;
58
+ try {
59
+ const payload = {
60
+ product: PRODUCT,
61
+ event: options.event,
62
+ occurred_at: new Date().toISOString(),
63
+ source,
64
+ actor_mode: "anonymous" as const,
65
+ actor_anonymous_id: installationId,
66
+ properties: options.properties ?? {},
67
+ metadata: {
68
+ transport: "https",
69
+ schema_version: 1,
70
+ environment: process.env.NODE_ENV ?? "production",
71
+ ...options.metadata,
72
+ },
73
+ };
74
+ fetch(TELEMETRY_ENDPOINT, {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify(payload),
78
+ signal: AbortSignal.timeout(TIMEOUT_MS),
79
+ }).catch(() => {});
80
+ } catch {
81
+ // Never throw
82
+ }
83
+ }
84
+
85
+ export const telemetry = {
86
+ taskEvent(
87
+ event: string,
88
+ props: {
89
+ taskId: string;
90
+ source?: string;
91
+ tags?: string[];
92
+ durationMs?: number;
93
+ hasParent?: boolean;
94
+ agentId?: string;
95
+ priority?: number;
96
+ [k: string]: unknown;
97
+ },
98
+ ): void {
99
+ track({ event: `task.${event}`, properties: props });
100
+ },
101
+
102
+ server(event: string, props?: Record<string, unknown>): void {
103
+ track({ event: `server.${event}`, properties: props ?? {} });
104
+ },
105
+
106
+ session(event: string, props: { agentId: string; taskId?: string; [k: string]: unknown }): void {
107
+ track({ event: `session.${event}`, properties: props });
108
+ },
109
+ };
@@ -0,0 +1,155 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+
3
+ import { resolveCodexLoginConfig, runCodexLogin } from "../commands/codex-login.js";
4
+
5
+ describe("resolveCodexLoginConfig", () => {
6
+ it("uses defaults without prompts when not interactive", async () => {
7
+ const promptText = mock(async () => {
8
+ throw new Error("should not prompt for text");
9
+ });
10
+ const promptSecret = mock(async () => {
11
+ throw new Error("should not prompt for secret");
12
+ });
13
+
14
+ const result = await resolveCodexLoginConfig([], {
15
+ env: {},
16
+ isInteractive: false,
17
+ promptText,
18
+ promptSecret,
19
+ });
20
+
21
+ expect(result).toEqual({
22
+ apiUrl: "http://localhost:3013",
23
+ apiKey: "123123",
24
+ });
25
+ expect(promptText).not.toHaveBeenCalled();
26
+ expect(promptSecret).not.toHaveBeenCalled();
27
+ });
28
+
29
+ it("prompts for api url and api key in interactive mode", async () => {
30
+ const promptText = mock(async () => "https://swarm.example.com");
31
+ const promptSecret = mock(async () => "super-secret");
32
+
33
+ const result = await resolveCodexLoginConfig([], {
34
+ env: {},
35
+ isInteractive: true,
36
+ promptText,
37
+ promptSecret,
38
+ });
39
+
40
+ expect(result).toEqual({
41
+ apiUrl: "https://swarm.example.com",
42
+ apiKey: "super-secret",
43
+ });
44
+ expect(promptText).toHaveBeenCalledWith("Swarm API URL", "http://localhost:3013");
45
+ expect(promptSecret).toHaveBeenCalledWith(
46
+ "Swarm API key",
47
+ "123123",
48
+ "Press Enter to use the default local API key",
49
+ );
50
+ });
51
+
52
+ it("uses environment defaults when interactive prompts are left blank", async () => {
53
+ const promptText = mock(async () => "");
54
+ const promptSecret = mock(async () => "");
55
+
56
+ const result = await resolveCodexLoginConfig([], {
57
+ env: {
58
+ MCP_BASE_URL: "https://env.example.com",
59
+ API_KEY: "env-secret",
60
+ },
61
+ isInteractive: true,
62
+ promptText,
63
+ promptSecret,
64
+ });
65
+
66
+ expect(result).toEqual({
67
+ apiUrl: "https://env.example.com",
68
+ apiKey: "env-secret",
69
+ });
70
+ expect(promptSecret).toHaveBeenCalledWith(
71
+ "Swarm API key",
72
+ "env-secret",
73
+ "Press Enter to use API_KEY from the environment",
74
+ );
75
+ });
76
+
77
+ it("does not prompt when flags are provided", async () => {
78
+ const promptText = mock(async () => {
79
+ throw new Error("should not prompt for text");
80
+ });
81
+ const promptSecret = mock(async () => {
82
+ throw new Error("should not prompt for secret");
83
+ });
84
+
85
+ const result = await resolveCodexLoginConfig(
86
+ ["--api-url", "https://flag.example.com", "--api-key", "flag-secret"],
87
+ {
88
+ env: {
89
+ MCP_BASE_URL: "https://env.example.com",
90
+ API_KEY: "env-secret",
91
+ },
92
+ isInteractive: true,
93
+ promptText,
94
+ promptSecret,
95
+ },
96
+ );
97
+
98
+ expect(result).toEqual({
99
+ apiUrl: "https://flag.example.com",
100
+ apiKey: "flag-secret",
101
+ });
102
+ expect(promptText).not.toHaveBeenCalled();
103
+ expect(promptSecret).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it("prompts only for the missing value when one flag is provided", async () => {
107
+ const promptText = mock(async () => {
108
+ throw new Error("should not prompt for api url");
109
+ });
110
+ const promptSecret = mock(async () => "prompted-secret");
111
+
112
+ const result = await resolveCodexLoginConfig(["--api-url", "https://flag.example.com"], {
113
+ env: {},
114
+ isInteractive: true,
115
+ promptText,
116
+ promptSecret,
117
+ });
118
+
119
+ expect(result).toEqual({
120
+ apiUrl: "https://flag.example.com",
121
+ apiKey: "prompted-secret",
122
+ });
123
+ expect(promptText).not.toHaveBeenCalled();
124
+ expect(promptSecret).toHaveBeenCalledTimes(1);
125
+ });
126
+ });
127
+
128
+ describe("runCodexLogin", () => {
129
+ it("handles prompt cancellation cleanly before starting OAuth", async () => {
130
+ const error = mock(() => {});
131
+ const exit = mock(() => {});
132
+ const login = mock(async () => {
133
+ throw new Error("should not start oauth");
134
+ });
135
+ const store = mock(async () => {
136
+ throw new Error("should not store");
137
+ });
138
+
139
+ await runCodexLogin([], {
140
+ resolveConfig: async () => {
141
+ throw new Error("Aborted");
142
+ },
143
+ login,
144
+ store,
145
+ log: () => {},
146
+ error,
147
+ exit,
148
+ });
149
+
150
+ expect(error).toHaveBeenCalledWith("\nError: Aborted");
151
+ expect(exit).toHaveBeenCalledWith(1);
152
+ expect(login).not.toHaveBeenCalled();
153
+ expect(store).not.toHaveBeenCalled();
154
+ });
155
+ });
@@ -0,0 +1,306 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { resetFetchForTesting, setFetchForTesting } from "../providers/codex-oauth/flow.js";
3
+ import {
4
+ deleteCodexOAuth,
5
+ getValidCodexOAuth,
6
+ loadCodexOAuth,
7
+ storeCodexOAuth,
8
+ } from "../providers/codex-oauth/storage.js";
9
+ import type { CodexOAuthCredentials } from "../providers/codex-oauth/types.js";
10
+
11
+ const MOCK_API_URL = "http://localhost:3013";
12
+ const MOCK_API_KEY = "test-api-key";
13
+
14
+ const mockCreds: CodexOAuthCredentials = {
15
+ access: "at_test123",
16
+ refresh: "rt_test456",
17
+ expires: Date.now() + 3600000,
18
+ accountId: "acc-test-789",
19
+ };
20
+
21
+ describe("storeCodexOAuth", () => {
22
+ const originalFetch = globalThis.fetch;
23
+
24
+ afterEach(() => {
25
+ globalThis.fetch = originalFetch;
26
+ });
27
+
28
+ it("sends correct PUT request with isSecret: true", async () => {
29
+ let capturedBody: unknown = null;
30
+ globalThis.fetch = async (_url: string | URL | Request, init?: RequestInit) => {
31
+ capturedBody = JSON.parse(init?.body as string);
32
+ return new Response(
33
+ JSON.stringify({ id: "cfg-1", key: "codex_oauth", scope: "global", value: "stored" }),
34
+ { status: 200, headers: { "Content-Type": "application/json" } },
35
+ );
36
+ };
37
+
38
+ await storeCodexOAuth(MOCK_API_URL, MOCK_API_KEY, mockCreds);
39
+
40
+ expect(capturedBody).not.toBeNull();
41
+ const body = capturedBody as Record<string, unknown>;
42
+ expect(body.scope).toBe("global");
43
+ expect(body.key).toBe("codex_oauth");
44
+ expect(body.isSecret).toBe(true);
45
+ expect(JSON.parse(body.value as string)).toEqual(mockCreds);
46
+ });
47
+
48
+ it("throws on HTTP error", async () => {
49
+ globalThis.fetch = async () => new Response("Server Error", { status: 500 });
50
+
51
+ await expect(storeCodexOAuth(MOCK_API_URL, MOCK_API_KEY, mockCreds)).rejects.toThrow(
52
+ "Failed to store codex_oauth config",
53
+ );
54
+ });
55
+ });
56
+
57
+ describe("loadCodexOAuth", () => {
58
+ const originalFetch = globalThis.fetch;
59
+
60
+ afterEach(() => {
61
+ globalThis.fetch = originalFetch;
62
+ });
63
+
64
+ it("parses config response correctly", async () => {
65
+ globalThis.fetch = async () =>
66
+ new Response(
67
+ JSON.stringify({
68
+ configs: [
69
+ {
70
+ id: "cfg-1",
71
+ key: "codex_oauth",
72
+ value: JSON.stringify(mockCreds),
73
+ scope: "global",
74
+ },
75
+ ],
76
+ }),
77
+ { status: 200, headers: { "Content-Type": "application/json" } },
78
+ );
79
+
80
+ const result = await loadCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
81
+ expect(result).not.toBeNull();
82
+ expect(result!.access).toBe(mockCreds.access);
83
+ expect(result!.refresh).toBe(mockCreds.refresh);
84
+ expect(result!.accountId).toBe(mockCreds.accountId);
85
+ });
86
+
87
+ it("returns null when no config found", async () => {
88
+ globalThis.fetch = async () =>
89
+ new Response(JSON.stringify({ configs: [] }), {
90
+ status: 200,
91
+ headers: { "Content-Type": "application/json" },
92
+ });
93
+
94
+ const result = await loadCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
95
+ expect(result).toBeNull();
96
+ });
97
+
98
+ it("returns null on HTTP error", async () => {
99
+ globalThis.fetch = async () => new Response("Not Found", { status: 404 });
100
+
101
+ const result = await loadCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
102
+ expect(result).toBeNull();
103
+ });
104
+
105
+ it("returns null on network error", async () => {
106
+ globalThis.fetch = async () => {
107
+ throw new Error("ConnectionRefused");
108
+ };
109
+
110
+ const result = await loadCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
111
+ expect(result).toBeNull();
112
+ });
113
+
114
+ it("returns null on invalid JSON value", async () => {
115
+ globalThis.fetch = async () =>
116
+ new Response(
117
+ JSON.stringify({
118
+ configs: [{ id: "cfg-1", key: "codex_oauth", value: "not-json", scope: "global" }],
119
+ }),
120
+ { status: 200, headers: { "Content-Type": "application/json" } },
121
+ );
122
+
123
+ const result = await loadCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
124
+ expect(result).toBeNull();
125
+ });
126
+ });
127
+
128
+ describe("deleteCodexOAuth", () => {
129
+ const originalFetch = globalThis.fetch;
130
+
131
+ afterEach(() => {
132
+ globalThis.fetch = originalFetch;
133
+ });
134
+
135
+ it("sends DELETE request for the config entry", async () => {
136
+ let deleteUrl = "";
137
+ globalThis.fetch = async (url: string | URL | Request, init?: RequestInit) => {
138
+ const urlStr = typeof url === "string" ? url : url.toString();
139
+ const method = init?.method || "GET";
140
+
141
+ if (method === "DELETE") {
142
+ deleteUrl = urlStr;
143
+ }
144
+
145
+ if (urlStr.includes("config/resolved")) {
146
+ return new Response(
147
+ JSON.stringify({
148
+ configs: [{ id: "cfg-123", key: "codex_oauth", value: "{}", scope: "global" }],
149
+ }),
150
+ { status: 200, headers: { "Content-Type": "application/json" } },
151
+ );
152
+ }
153
+
154
+ return new Response(JSON.stringify({ success: true }), {
155
+ status: 200,
156
+ headers: { "Content-Type": "application/json" },
157
+ });
158
+ };
159
+
160
+ await deleteCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
161
+ expect(deleteUrl).toContain("cfg-123");
162
+ });
163
+
164
+ it("does nothing when no config found", async () => {
165
+ let deleteCalled = false;
166
+ globalThis.fetch = async (url: string | URL | Request, init?: RequestInit) => {
167
+ const method = init?.method || "GET";
168
+ if (method === "DELETE") {
169
+ deleteCalled = true;
170
+ }
171
+ return new Response(JSON.stringify({ configs: [] }), {
172
+ status: 200,
173
+ headers: { "Content-Type": "application/json" },
174
+ });
175
+ };
176
+
177
+ await deleteCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
178
+ expect(deleteCalled).toBe(false);
179
+ });
180
+ });
181
+
182
+ describe("getValidCodexOAuth", () => {
183
+ const originalFetch = globalThis.fetch;
184
+
185
+ afterEach(() => {
186
+ globalThis.fetch = originalFetch;
187
+ resetFetchForTesting();
188
+ });
189
+
190
+ it("returns cached credentials when not expired", async () => {
191
+ globalThis.fetch = async () =>
192
+ new Response(
193
+ JSON.stringify({
194
+ configs: [
195
+ {
196
+ id: "cfg-1",
197
+ key: "codex_oauth",
198
+ value: JSON.stringify({ ...mockCreds, expires: Date.now() + 3600000 }),
199
+ scope: "global",
200
+ },
201
+ ],
202
+ }),
203
+ { status: 200, headers: { "Content-Type": "application/json" } },
204
+ );
205
+
206
+ const result = await getValidCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
207
+ expect(result).not.toBeNull();
208
+ expect(result!.access).toBe(mockCreds.access);
209
+ });
210
+
211
+ it("refreshes expired tokens and re-stores", async () => {
212
+ let putCalled = false;
213
+ const expiredCreds = { ...mockCreds, expires: Date.now() - 1000 };
214
+
215
+ globalThis.fetch = async (url: string | URL | Request, init?: RequestInit) => {
216
+ const urlStr = typeof url === "string" ? url : url.toString();
217
+ const method = init?.method || "GET";
218
+
219
+ if (method === "GET" && urlStr.includes("config/resolved")) {
220
+ return new Response(
221
+ JSON.stringify({
222
+ configs: [
223
+ {
224
+ id: "cfg-1",
225
+ key: "codex_oauth",
226
+ value: JSON.stringify(expiredCreds),
227
+ scope: "global",
228
+ },
229
+ ],
230
+ }),
231
+ { status: 200, headers: { "Content-Type": "application/json" } },
232
+ );
233
+ }
234
+
235
+ if (method === "PUT") {
236
+ putCalled = true;
237
+ return new Response(JSON.stringify({ id: "cfg-1", key: "codex_oauth", scope: "global" }), {
238
+ status: 200,
239
+ headers: { "Content-Type": "application/json" },
240
+ });
241
+ }
242
+
243
+ return new Response("Not Found", { status: 404 });
244
+ };
245
+
246
+ setFetchForTesting(
247
+ async () =>
248
+ new Response(
249
+ JSON.stringify({
250
+ access_token: "at_refreshed",
251
+ refresh_token: "rt_refreshed",
252
+ expires_in: 3600,
253
+ }),
254
+ { status: 200, headers: { "Content-Type": "application/json" } },
255
+ ),
256
+ );
257
+
258
+ const result = await getValidCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
259
+ expect(result).not.toBeNull();
260
+ expect(result!.access).toBe("at_refreshed");
261
+ expect(result!.refresh).toBe("rt_refreshed");
262
+ expect(putCalled).toBe(true);
263
+ });
264
+
265
+ it("returns null when refresh fails", async () => {
266
+ const expiredCreds = { ...mockCreds, expires: Date.now() - 1000 };
267
+
268
+ globalThis.fetch = async (url: string | URL | Request) => {
269
+ const urlStr = typeof url === "string" ? url : url.toString();
270
+
271
+ if (urlStr.includes("config/resolved")) {
272
+ return new Response(
273
+ JSON.stringify({
274
+ configs: [
275
+ {
276
+ id: "cfg-1",
277
+ key: "codex_oauth",
278
+ value: JSON.stringify(expiredCreds),
279
+ scope: "global",
280
+ },
281
+ ],
282
+ }),
283
+ { status: 200, headers: { "Content-Type": "application/json" } },
284
+ );
285
+ }
286
+
287
+ return new Response("Not Found", { status: 404 });
288
+ };
289
+
290
+ setFetchForTesting(() => new Response("Unauthorized", { status: 401 }));
291
+
292
+ const result = await getValidCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
293
+ expect(result).toBeNull();
294
+ });
295
+
296
+ it("returns null when no credentials stored", async () => {
297
+ globalThis.fetch = async () =>
298
+ new Response(JSON.stringify({ configs: [] }), {
299
+ status: 200,
300
+ headers: { "Content-Type": "application/json" },
301
+ });
302
+
303
+ const result = await getValidCodexOAuth(MOCK_API_URL, MOCK_API_KEY);
304
+ expect(result).toBeNull();
305
+ });
306
+ });