@desplega.ai/agent-swarm 1.73.1 → 1.73.2
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/openapi.json +1 -1
- package/package.json +1 -1
- package/src/oauth/ensure-token.ts +26 -9
- package/src/oauth/keepalive.ts +21 -11
- package/src/tests/ensure-token.test.ts +33 -1
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.73.
|
|
5
|
+
"version": "1.73.2",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
package/package.json
CHANGED
|
@@ -26,11 +26,35 @@ function getOAuthConfig(provider: string): OAuthProviderConfig | null {
|
|
|
26
26
|
* If the token is expiring soon, attempt to refresh it.
|
|
27
27
|
* Call this before any API interaction with an OAuth-protected service.
|
|
28
28
|
*
|
|
29
|
+
* Reactive variant — never throws. Refresh failures are logged so a single
|
|
30
|
+
* dead-token incident doesn't tear down an unrelated request path. Use
|
|
31
|
+
* {@link ensureTokenOrThrow} from keepalive contexts where you want a dead
|
|
32
|
+
* refresh token to surface as an alert.
|
|
33
|
+
*
|
|
29
34
|
* @param bufferMs - How far ahead to check for expiry. Default 5 min (reactive use).
|
|
30
35
|
* Keepalive callers should pass a larger value (e.g. 13h) to force
|
|
31
36
|
* a proactive refresh well before the token actually expires.
|
|
32
37
|
*/
|
|
33
38
|
export async function ensureToken(provider: string, bufferMs?: number): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await ensureTokenOrThrow(provider, bufferMs);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(
|
|
43
|
+
`[OAuth] Failed to refresh ${provider} token:`,
|
|
44
|
+
err instanceof Error ? err.message : err,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Strict variant of {@link ensureToken}: throws on refresh failure of a
|
|
51
|
+
* configured provider so callers (keepalive, alerting) can react.
|
|
52
|
+
*
|
|
53
|
+
* Stays silent (no throw) when the provider isn't configured or no refresh
|
|
54
|
+
* token is stored — those are "not connected" states, not failures, and
|
|
55
|
+
* shouldn't page anyone.
|
|
56
|
+
*/
|
|
57
|
+
export async function ensureTokenOrThrow(provider: string, bufferMs?: number): Promise<void> {
|
|
34
58
|
if (!isTokenExpiringSoon(provider, bufferMs)) return;
|
|
35
59
|
|
|
36
60
|
const config = getOAuthConfig(provider);
|
|
@@ -42,13 +66,6 @@ export async function ensureToken(provider: string, bufferMs?: number): Promise<
|
|
|
42
66
|
return;
|
|
43
67
|
}
|
|
44
68
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.log(`[OAuth] ${provider} token refreshed successfully`);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.error(
|
|
50
|
-
`[OAuth] Failed to refresh ${provider} token:`,
|
|
51
|
-
err instanceof Error ? err.message : err,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
69
|
+
await refreshAccessToken(config, tokens.refreshToken);
|
|
70
|
+
console.log(`[OAuth] ${provider} token refreshed successfully`);
|
|
54
71
|
}
|
package/src/oauth/keepalive.ts
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ensureTokenOrThrow } from "./ensure-token";
|
|
2
2
|
|
|
3
3
|
const TWELVE_HOURS_MS = 12 * 60 * 60 * 1000;
|
|
4
4
|
const THIRTEEN_HOURS_MS = 13 * 60 * 60 * 1000;
|
|
5
5
|
const SLACK_ALERTS_CHANNEL = process.env.SLACK_ALERTS_CHANNEL || "C08JCRURPBV";
|
|
6
6
|
|
|
7
|
+
const KEEPALIVE_PROVIDERS = ["linear", "jira"] as const;
|
|
8
|
+
|
|
7
9
|
let keepaliveInterval: ReturnType<typeof setInterval> | null = null;
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Proactively refresh OAuth tokens on a schedule to prevent expiry.
|
|
11
13
|
* If refresh fails, posts a Slack notification so someone can re-auth manually.
|
|
14
|
+
*
|
|
15
|
+
* Why both Linear and Jira: Atlassian rotates refresh tokens and expires them
|
|
16
|
+
* after 90 days of inactivity, so a swarm that doesn't touch Jira for a long
|
|
17
|
+
* stretch will silently lose the ability to refresh. Touching the token every
|
|
18
|
+
* 12h keeps the refresh token alive and surfaces a dead one as an alert
|
|
19
|
+
* instead of a runtime 401.
|
|
12
20
|
*/
|
|
13
21
|
async function runKeepalive(): Promise<void> {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
for (const provider of KEEPALIVE_PROVIDERS) {
|
|
23
|
+
console.log(`[OAuth Keepalive] Running scheduled token refresh for ${provider}...`);
|
|
24
|
+
try {
|
|
25
|
+
await ensureTokenOrThrow(provider, THIRTEEN_HOURS_MS);
|
|
26
|
+
console.log(`[OAuth Keepalive] ${provider} token check completed successfully`);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
console.error(`[OAuth Keepalive] Failed to refresh ${provider} token: ${message}`);
|
|
30
|
+
await notifySlack(
|
|
31
|
+
`⚠️ *OAuth Keepalive Failed*\nProvider: \`${provider}\`\nError: ${message}\n\nManual re-authorization may be required.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
36
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
storeOAuthTokens,
|
|
8
8
|
upsertOAuthApp,
|
|
9
9
|
} from "../be/db-queries/oauth";
|
|
10
|
-
import { ensureToken } from "../oauth/ensure-token";
|
|
10
|
+
import { ensureToken, ensureTokenOrThrow } from "../oauth/ensure-token";
|
|
11
11
|
|
|
12
12
|
const TEST_DB_PATH = "./test-ensure-token.sqlite";
|
|
13
13
|
|
|
@@ -202,3 +202,35 @@ describe("ensureToken", () => {
|
|
|
202
202
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
|
+
|
|
206
|
+
describe("ensureTokenOrThrow", () => {
|
|
207
|
+
test("throws when refresh fails for a configured provider (so keepalive can alert)", async () => {
|
|
208
|
+
storeOAuthTokens("test-provider", {
|
|
209
|
+
accessToken: "old-token",
|
|
210
|
+
refreshToken: "refresh-token",
|
|
211
|
+
expiresAt: new Date(Date.now() + 60 * 1000).toISOString(),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
globalThis.fetch = mock(() =>
|
|
215
|
+
Promise.resolve(
|
|
216
|
+
new Response('{"error":"invalid_grant"}', {
|
|
217
|
+
status: 400,
|
|
218
|
+
headers: { "Content-Type": "application/json" },
|
|
219
|
+
}),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await expect(ensureTokenOrThrow("test-provider")).rejects.toThrow(/Token refresh failed/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("stays silent (no throw) when no refresh token is stored", async () => {
|
|
227
|
+
deleteOAuthTokens("test-provider");
|
|
228
|
+
|
|
229
|
+
// "Not connected" should not page anyone
|
|
230
|
+
await expect(ensureTokenOrThrow("test-provider")).resolves.toBeUndefined();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("stays silent (no throw) when provider is not configured", async () => {
|
|
234
|
+
await expect(ensureTokenOrThrow("nonexistent-provider")).resolves.toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
});
|