@slashfi/agents-sdk 0.11.2 → 0.13.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 (48) hide show
  1. package/dist/agent-definitions/auth.d.ts +17 -1
  2. package/dist/agent-definitions/auth.d.ts.map +1 -1
  3. package/dist/agent-definitions/auth.js +123 -4
  4. package/dist/agent-definitions/auth.js.map +1 -1
  5. package/dist/agent-definitions/integrations.d.ts +2 -14
  6. package/dist/agent-definitions/integrations.d.ts.map +1 -1
  7. package/dist/agent-definitions/integrations.js +64 -19
  8. package/dist/agent-definitions/integrations.js.map +1 -1
  9. package/dist/agent-definitions/remote-registry.d.ts +19 -14
  10. package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
  11. package/dist/agent-definitions/remote-registry.js +219 -381
  12. package/dist/agent-definitions/remote-registry.js.map +1 -1
  13. package/dist/agent-definitions/users.d.ts.map +1 -1
  14. package/dist/agent-definitions/users.js +29 -1
  15. package/dist/agent-definitions/users.js.map +1 -1
  16. package/dist/define.d.ts +6 -4
  17. package/dist/define.d.ts.map +1 -1
  18. package/dist/define.js +82 -3
  19. package/dist/define.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/jwt.js +1 -1
  25. package/dist/jwt.js.map +1 -1
  26. package/dist/key-manager.d.ts +76 -0
  27. package/dist/key-manager.d.ts.map +1 -0
  28. package/dist/key-manager.js +156 -0
  29. package/dist/key-manager.js.map +1 -0
  30. package/dist/server.d.ts +39 -0
  31. package/dist/server.d.ts.map +1 -1
  32. package/dist/server.js +228 -52
  33. package/dist/server.js.map +1 -1
  34. package/dist/types.d.ts +53 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/agent-definitions/auth.ts +165 -6
  38. package/src/agent-definitions/integrations.ts +59 -28
  39. package/src/agent-definitions/remote-registry.ts +219 -513
  40. package/src/agent-definitions/users.ts +35 -1
  41. package/src/define.ts +98 -6
  42. package/src/index.ts +3 -1
  43. package/src/jwt.ts +1 -1
  44. package/src/key-manager.test.ts +273 -0
  45. package/src/key-manager.ts +257 -0
  46. package/src/server.test.ts +284 -0
  47. package/src/server.ts +334 -60
  48. package/src/types.ts +44 -1
@@ -250,10 +250,30 @@ export function createUsersAgent(options: UsersAgentOptions): AgentDefinition {
250
250
  name: { type: "string", description: "Display name" },
251
251
  avatarUrl: { type: "string", description: "Avatar URL" },
252
252
  metadata: { type: "object", description: "Additional metadata" },
253
+ externalRef: {
254
+ type: "object",
255
+ description: "Link to a user on a remote system. Creates identity automatically.",
256
+ properties: {
257
+ issuer: { type: "string", description: "Issuer URL of the remote system" },
258
+ userId: { type: "string", description: "User ID on the remote system" },
259
+ },
260
+ },
253
261
  },
254
262
  required: ["tenantId"],
255
263
  },
256
264
  execute: async (input: any, __ctx: ToolContext) => {
265
+ // If externalRef provided, check if identity already exists
266
+ if (input.externalRef) {
267
+ const existing = await store.findIdentityByProviderUserId(
268
+ input.externalRef.issuer,
269
+ input.externalRef.userId,
270
+ );
271
+ if (existing) {
272
+ const user = await store.getUser(existing.userId);
273
+ return { success: true, user, identity: existing, alreadyLinked: true };
274
+ }
275
+ }
276
+
257
277
  const user = await store.createUser({
258
278
  id: input.id ?? generateId("user_"),
259
279
  tenantId: input.tenantId,
@@ -262,7 +282,21 @@ export function createUsersAgent(options: UsersAgentOptions): AgentDefinition {
262
282
  avatarUrl: input.avatarUrl,
263
283
  metadata: input.metadata,
264
284
  });
265
- return { success: true, user };
285
+
286
+ // Auto-create identity link if externalRef provided
287
+ let identity: UserIdentity | undefined;
288
+ if (input.externalRef) {
289
+ identity = await store.createIdentity({
290
+ id: generateId("uid_"),
291
+ userId: user.id,
292
+ provider: input.externalRef.issuer,
293
+ providerUserId: input.externalRef.userId,
294
+ email: input.email,
295
+ name: input.name,
296
+ });
297
+ }
298
+
299
+ return { success: true, user, identity };
266
300
  },
267
301
  });
268
302
 
package/src/define.ts CHANGED
@@ -5,10 +5,10 @@
5
5
  */
6
6
 
7
7
  import type {
8
+ IntegrationHooks,
8
9
  AgentConfig,
9
10
  AgentDefinition,
10
11
  AgentRuntime,
11
- IntegrationMethods,
12
12
  JsonSchema,
13
13
  ToolContext,
14
14
  ToolDefinition,
@@ -115,10 +115,18 @@ export interface DefineAgentOptions<
115
115
  allowedCallers?: string[];
116
116
 
117
117
  /**
118
+ * Integration hooks. When provided, defineAgent auto-generates
119
+ * setup_integration, connect_integration, etc. as tools.
120
+ */
121
+ integration?: IntegrationHooks;
122
+
123
+ /** Lazy loader for event listeners (e.g. entrypoint.ts) */
124
+ loadListeners?: () => Promise<unknown>;
125
+
126
+ /**
127
+ * @deprecated Use `integration` instead.
118
128
  * Integration method callbacks.
119
- * Implement these when this agent acts as an integration.
120
129
  */
121
- integrationMethods?: IntegrationMethods;
122
130
  }
123
131
 
124
132
  /**
@@ -148,14 +156,98 @@ export interface DefineAgentOptions<
148
156
  export function defineAgent<TContext extends ToolContext = ToolContext>(
149
157
  options: DefineAgentOptions<TContext>,
150
158
  ): AgentDefinition<TContext> {
159
+ const tools = [...(options.tools ?? [])];
160
+ let config = options.config;
161
+
162
+ // Auto-generate integration tools from hooks
163
+ if (options.integration) {
164
+ const h = options.integration;
165
+
166
+ // Set config.integration metadata if not already set
167
+ if (!config?.integration) {
168
+ config = {
169
+ ...config,
170
+ integration: {
171
+ provider: h.provider,
172
+ displayName: h.displayName,
173
+ icon: h.icon,
174
+ category: h.category,
175
+ description: h.description,
176
+ },
177
+ };
178
+ }
179
+
180
+ if (h.setup) {
181
+ const fn = h.setup;
182
+ tools.push(defineTool({
183
+ name: "setup_integration",
184
+ description: `Set up ${h.displayName} integration.`,
185
+ visibility: "public" as const,
186
+ inputSchema: { type: "object" as const, properties: { url: { type: "string" }, name: { type: "string" }, config: { type: "object" } } },
187
+ execute: (input: any, ctx: any) => fn(input, ctx),
188
+ }) as any);
189
+ }
190
+ if (h.connect) {
191
+ const fn = h.connect;
192
+ tools.push(defineTool({
193
+ name: "connect_integration",
194
+ description: `Connect a user to ${h.displayName}.`,
195
+ visibility: "public" as const,
196
+ inputSchema: { type: "object" as const, properties: { registryId: { type: "string" }, oidcUserId: { type: "string" }, redirectUri: { type: "string" } }, required: ["registryId"] as const },
197
+ execute: (input: any, ctx: any) => fn(input, ctx),
198
+ }) as any);
199
+ }
200
+ if (h.discover) {
201
+ const fn = h.discover;
202
+ tools.push(defineTool({
203
+ name: "discover_integrations",
204
+ description: `Discover available ${h.displayName} instances.`,
205
+ visibility: "public" as const,
206
+ inputSchema: { type: "object" as const, properties: { url: { type: "string" } } },
207
+ execute: (input: any, ctx: any) => fn(input, ctx),
208
+ }) as any);
209
+ }
210
+ if (h.list) {
211
+ const fn = h.list;
212
+ tools.push(defineTool({
213
+ name: "list_integrations",
214
+ description: `List connected ${h.displayName} instances.`,
215
+ visibility: "public" as const,
216
+ inputSchema: { type: "object" as const, properties: {} },
217
+ execute: (input: any, ctx: any) => fn(input, ctx),
218
+ }) as any);
219
+ }
220
+ if (h.get) {
221
+ const fn = h.get;
222
+ tools.push(defineTool({
223
+ name: "get_integration",
224
+ description: `Get details of a ${h.displayName} instance.`,
225
+ visibility: "public" as const,
226
+ inputSchema: { type: "object" as const, properties: { registryId: { type: "string" } }, required: ["registryId"] as const },
227
+ execute: (input: any, ctx: any) => fn(input, ctx),
228
+ }) as any);
229
+ }
230
+ if (h.update) {
231
+ const fn = h.update;
232
+ tools.push(defineTool({
233
+ name: "update_integration",
234
+ description: `Update a ${h.displayName} instance.`,
235
+ visibility: "public" as const,
236
+ inputSchema: { type: "object" as const, properties: { registryId: { type: "string" }, name: { type: "string" }, url: { type: "string" } }, required: ["registryId"] as const },
237
+ execute: (input: any, ctx: any) => fn(input, ctx),
238
+ }) as any);
239
+ }
240
+ }
241
+
242
+
151
243
  return {
152
244
  path: options.path,
153
245
  entrypoint: options.entrypoint,
154
- config: options.config,
155
- tools: options.tools ?? [],
246
+ config,
247
+ tools,
156
248
  runtime: options.runtime,
157
249
  visibility: options.visibility,
158
250
  allowedCallers: options.allowedCallers,
159
- integrationMethods: options.integrationMethods,
251
+ loadListeners: options.loadListeners,
160
252
  };
161
253
  }
package/src/index.ts CHANGED
@@ -85,6 +85,7 @@ export type {
85
85
  IntegrationMethods,
86
86
  IntegrationMethodResult,
87
87
  IntegrationMethodContext,
88
+ IntegrationHooks,
88
89
  Visibility,
89
90
  } from "./types.js";
90
91
 
@@ -98,7 +99,7 @@ export type { AgentRegistry, AgentRegistryOptions } from "./registry.js";
98
99
 
99
100
  // Server
100
101
  export { createAgentServer, detectAuth, resolveAuth, canSeeAgent } from "./server.js";
101
- export type { AgentServer, AgentServerOptions, AuthConfig, ResolvedAuth, TrustedIssuer } from "./server.js";
102
+ export type { AgentServer, AgentServerOptions, AuthConfig, OAuthIdentityProvider, ResolvedAuth, TrustedIssuer } from "./server.js";
102
103
 
103
104
  // Secret Collection
104
105
  export {
@@ -193,3 +194,4 @@ export type {
193
194
  export * from "./integrations-store.js";
194
195
  export * from "./integration-interface.js";
195
196
  export type { ContextFactory } from "./registry.js";
197
+ export { createKeyManager, type KeyManager, type KeyStore, type KeyManagerOptions, type StoredKey, type KeyStatus } from "./key-manager.js";
package/src/jwt.ts CHANGED
@@ -80,7 +80,7 @@ export interface ExportedKeyPair {
80
80
  * Generate a new ES256 signing key pair.
81
81
  */
82
82
  export async function generateSigningKey(kid?: string): Promise<SigningKey> {
83
- const { privateKey, publicKey } = await generateKeyPair("ES256");
83
+ const { privateKey, publicKey } = await generateKeyPair("ES256", { extractable: true });
84
84
  return {
85
85
  kid: kid ?? `key-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
86
86
  privateKey,
@@ -0,0 +1,273 @@
1
+ import { describe, test, expect, afterEach } from "bun:test";
2
+ import { createKeyManager, type KeyStore, type StoredKey, type KeyManager } from "./key-manager";
3
+ import { jwtVerify, createLocalJWKSet } from "jose";
4
+
5
+ // In-memory KeyStore for testing (no DB needed)
6
+ function createMemoryKeyStore(): KeyStore & { keys: StoredKey[] } {
7
+ const keys: StoredKey[] = [];
8
+ return {
9
+ keys,
10
+ async loadKeys() {
11
+ return keys.filter((k) => k.status !== "revoked");
12
+ },
13
+ async insertKey(key: StoredKey) {
14
+ keys.push(key);
15
+ },
16
+ async deprecateAllActive() {
17
+ for (const k of keys) {
18
+ if (k.status === "active") k.status = "deprecated";
19
+ }
20
+ },
21
+ async cleanupExpired() {
22
+ const now = Date.now();
23
+ const before = keys.length;
24
+ const remaining = keys.filter((k) => k.expiresAt.getTime() > now);
25
+ keys.length = 0;
26
+ keys.push(...remaining);
27
+ return before - remaining.length;
28
+ },
29
+ // Transaction support: snapshot + rollback on error
30
+ async transaction<T>(fn: () => Promise<T>): Promise<T> {
31
+ const snapshot = keys.map((k) => ({ ...k }));
32
+ try {
33
+ return await fn();
34
+ } catch (err) {
35
+ // Rollback
36
+ keys.length = 0;
37
+ keys.push(...snapshot);
38
+ throw err;
39
+ }
40
+ },
41
+ };
42
+ }
43
+
44
+ describe("KeyManager", () => {
45
+ let km: KeyManager;
46
+
47
+ afterEach(() => {
48
+ km?.stop();
49
+ });
50
+
51
+ test("creates initial key on startup", async () => {
52
+ const store = createMemoryKeyStore();
53
+ km = await createKeyManager({
54
+ store,
55
+ issuer: "http://test:3000",
56
+ checkIntervalMs: 60_000,
57
+ });
58
+
59
+ const jwks = km.getJwks();
60
+ expect(jwks.keys).toHaveLength(1);
61
+ expect(jwks.keys[0].alg).toBe("ES256");
62
+ expect(jwks.keys[0].kid).toMatch(/^key-/);
63
+ // No private key exposed
64
+ expect(jwks.keys[0].d).toBeUndefined();
65
+ });
66
+
67
+ test("signs a valid JWT", async () => {
68
+ const store = createMemoryKeyStore();
69
+ km = await createKeyManager({
70
+ store,
71
+ issuer: "http://test:3000",
72
+ checkIntervalMs: 60_000,
73
+ });
74
+
75
+ const token = await km.signJwt({ sub: "test-service" });
76
+ const parts = token.split(".");
77
+ expect(parts).toHaveLength(3);
78
+
79
+ const payload = JSON.parse(
80
+ Buffer.from(parts[1], "base64url").toString()
81
+ );
82
+ expect(payload.sub).toBe("test-service");
83
+ expect(payload.iss).toBe("http://test:3000");
84
+ expect(payload.exp - payload.iat).toBe(300); // default 5 min TTL
85
+ });
86
+
87
+ test("token verifies against JWKS", async () => {
88
+ const store = createMemoryKeyStore();
89
+ km = await createKeyManager({
90
+ store,
91
+ issuer: "http://test:3000",
92
+ checkIntervalMs: 60_000,
93
+ });
94
+
95
+ const token = await km.signJwt({ sub: "verify-me" });
96
+ const jwks = km.getJwks();
97
+ const JWKS = createLocalJWKSet(jwks);
98
+ const { payload } = await jwtVerify(token, JWKS);
99
+ expect(payload.sub).toBe("verify-me");
100
+ });
101
+
102
+ test("custom token TTL", async () => {
103
+ const store = createMemoryKeyStore();
104
+ km = await createKeyManager({
105
+ store,
106
+ issuer: "http://test:3000",
107
+ tokenTtlSeconds: 60,
108
+ checkIntervalMs: 60_000,
109
+ });
110
+
111
+ const token = await km.signJwt({ sub: "short-lived" });
112
+ const payload = JSON.parse(
113
+ Buffer.from(token.split(".")[1], "base64url").toString()
114
+ );
115
+ expect(payload.exp - payload.iat).toBe(60);
116
+ });
117
+
118
+ // ---- Rotation tests ----
119
+
120
+ test("rotation: creates new key when threshold exceeded", async () => {
121
+ const store = createMemoryKeyStore();
122
+ km = await createKeyManager({
123
+ store,
124
+ issuer: "http://test:3000",
125
+ rotationThresholdMs: 0, // immediate rotation
126
+ checkIntervalMs: 60_000,
127
+ });
128
+
129
+ const initialKid = km.getJwks().keys.find(
130
+ (k) => store.keys.find((sk) => sk.kid === k.kid)?.status === "active"
131
+ )?.kid;
132
+
133
+ // Force rotation
134
+ await km.rotate();
135
+
136
+ const activeKeys = store.keys.filter((k) => k.status === "active");
137
+ expect(activeKeys).toHaveLength(1);
138
+ expect(activeKeys[0].kid).not.toBe(initialKid);
139
+
140
+ const deprecatedKeys = store.keys.filter((k) => k.status === "deprecated");
141
+ expect(deprecatedKeys.length).toBeGreaterThanOrEqual(1);
142
+ });
143
+
144
+ test("rotation: deprecated keys still in JWKS for verification", async () => {
145
+ const store = createMemoryKeyStore();
146
+ km = await createKeyManager({
147
+ store,
148
+ issuer: "http://test:3000",
149
+ rotationThresholdMs: 0,
150
+ checkIntervalMs: 60_000,
151
+ });
152
+
153
+ await km.rotate();
154
+
155
+ const jwksKids = km.getJwks().keys.map((k) => k.kid);
156
+ const nonRevoked = store.keys.filter((k) => k.status !== "revoked");
157
+ for (const key of nonRevoked) {
158
+ expect(jwksKids).toContain(key.kid);
159
+ }
160
+ });
161
+
162
+ test("rotation: pre-rotation tokens still verify", async () => {
163
+ const store = createMemoryKeyStore();
164
+ km = await createKeyManager({
165
+ store,
166
+ issuer: "http://test:3000",
167
+ rotationThresholdMs: 0,
168
+ checkIntervalMs: 60_000,
169
+ });
170
+
171
+ const preRotationToken = await km.signJwt({ sub: "pre-rotate" });
172
+ await km.rotate();
173
+
174
+ // Pre-rotation token should still verify (deprecated key still in JWKS)
175
+ const jwks = km.getJwks();
176
+ const JWKS = createLocalJWKSet(jwks);
177
+ const { payload } = await jwtVerify(preRotationToken, JWKS);
178
+ expect(payload.sub).toBe("pre-rotate");
179
+ });
180
+
181
+ test("rotation: exactly 1 active key", async () => {
182
+ const store = createMemoryKeyStore();
183
+ km = await createKeyManager({
184
+ store,
185
+ issuer: "http://test:3000",
186
+ rotationThresholdMs: 0,
187
+ checkIntervalMs: 60_000,
188
+ });
189
+
190
+ await km.rotate();
191
+ await km.rotate();
192
+
193
+ const activeKeys = store.keys.filter((k) => k.status === "active");
194
+ expect(activeKeys).toHaveLength(1);
195
+ });
196
+
197
+ test("rotation: tokens fail verification after key expires and is cleaned up", async () => {
198
+ const store = createMemoryKeyStore();
199
+ km = await createKeyManager({
200
+ store,
201
+ issuer: "http://test:3000",
202
+ rotationThresholdMs: 0,
203
+ keyLifetimeMs: 1, // 1ms — keys expire almost immediately
204
+ checkIntervalMs: 60_000,
205
+ });
206
+
207
+ const token = await km.signJwt({ sub: "will-expire" });
208
+
209
+ // Token should verify now (key is active)
210
+ const JWKS = createLocalJWKSet(km.getJwks());
211
+ const { payload } = await jwtVerify(token, JWKS);
212
+ expect(payload.sub).toBe("will-expire");
213
+
214
+ // Wait for key to expire, then rotate (which cleans up expired keys)
215
+ await new Promise((r) => setTimeout(r, 5));
216
+ await km.rotate();
217
+
218
+ // Old key should be gone from JWKS — verification should fail
219
+ const jwksAfter = km.getJwks();
220
+ const JWKS2 = createLocalJWKSet(jwksAfter);
221
+ let failed = false;
222
+ try {
223
+ await jwtVerify(token, JWKS2);
224
+ } catch {
225
+ failed = true;
226
+ }
227
+ expect(failed).toBe(true);
228
+ });
229
+
230
+ // ---- Transaction tests ----
231
+
232
+ test("transaction: rotate is atomic (rollback on failure)", async () => {
233
+ const store = createMemoryKeyStore();
234
+ km = await createKeyManager({
235
+ store,
236
+ issuer: "http://test:3000",
237
+ checkIntervalMs: 60_000,
238
+ });
239
+
240
+ const initialKid = store.keys[0].kid;
241
+
242
+ // Monkey-patch insertKey to fail mid-transaction
243
+ const origInsert = store.insertKey;
244
+ store.insertKey = async () => { throw new Error("simulated failure"); };
245
+
246
+ // Rotation should fail, but state should be rolled back
247
+ try { await km.rotate(); } catch {}
248
+
249
+ // Original key should still be active (not deprecated)
250
+ const active = store.keys.filter((k) => k.status === "active");
251
+ expect(active).toHaveLength(1);
252
+ expect(active[0].kid).toBe(initialKid);
253
+
254
+ // Restore
255
+ store.insertKey = origInsert;
256
+ });
257
+
258
+ // ---- enableRotation option ----
259
+
260
+ test("enableRotation: false skips background interval", async () => {
261
+ const store = createMemoryKeyStore();
262
+ km = await createKeyManager({
263
+ store,
264
+ issuer: "http://test:3000",
265
+ enableRotation: false,
266
+ });
267
+
268
+ // Should still have an initial key
269
+ expect(km.getJwks().keys).toHaveLength(1);
270
+ // But stop() should be a no-op (no interval to clear)
271
+ km.stop();
272
+ });
273
+ });