@slashfi/agents-sdk 0.8.0 → 0.10.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 (59) hide show
  1. package/dist/agent-definitions/auth.d.ts +17 -0
  2. package/dist/agent-definitions/auth.d.ts.map +1 -1
  3. package/dist/agent-definitions/auth.js +135 -1
  4. package/dist/agent-definitions/auth.js.map +1 -1
  5. package/dist/agent-definitions/integrations.d.ts +19 -0
  6. package/dist/agent-definitions/integrations.d.ts.map +1 -1
  7. package/dist/agent-definitions/integrations.js +218 -5
  8. package/dist/agent-definitions/integrations.js.map +1 -1
  9. package/dist/index.d.ts +9 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/integration-interface.d.ts +37 -0
  14. package/dist/integration-interface.d.ts.map +1 -0
  15. package/dist/integration-interface.js +94 -0
  16. package/dist/integration-interface.js.map +1 -0
  17. package/dist/integrations-store.d.ts +33 -0
  18. package/dist/integrations-store.d.ts.map +1 -0
  19. package/dist/integrations-store.js +50 -0
  20. package/dist/integrations-store.js.map +1 -0
  21. package/dist/jwt.d.ts +86 -17
  22. package/dist/jwt.d.ts.map +1 -1
  23. package/dist/jwt.js +140 -17
  24. package/dist/jwt.js.map +1 -1
  25. package/dist/registry.d.ts +7 -0
  26. package/dist/registry.d.ts.map +1 -1
  27. package/dist/registry.js +8 -21
  28. package/dist/registry.js.map +1 -1
  29. package/dist/secret-collection.d.ts +37 -0
  30. package/dist/secret-collection.d.ts.map +1 -0
  31. package/dist/secret-collection.js +37 -0
  32. package/dist/secret-collection.js.map +1 -0
  33. package/dist/server.d.ts +41 -44
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +236 -592
  36. package/dist/server.js.map +1 -1
  37. package/dist/types.d.ts +7 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +5 -2
  40. package/src/agent-definitions/auth.ts +187 -1
  41. package/src/agent-definitions/integrations.ts +260 -5
  42. package/src/index.ts +18 -4
  43. package/src/integration-interface.ts +118 -0
  44. package/src/integrations-store.ts +84 -0
  45. package/src/jwt.ts +233 -65
  46. package/src/registry.ts +17 -2
  47. package/src/secret-collection.ts +66 -0
  48. package/src/server.ts +272 -681
  49. package/src/types.ts +8 -1
  50. package/dist/slack-oauth.d.ts +0 -27
  51. package/dist/slack-oauth.d.ts.map +0 -1
  52. package/dist/slack-oauth.js +0 -48
  53. package/dist/slack-oauth.js.map +0 -1
  54. package/dist/web-pages.d.ts +0 -8
  55. package/dist/web-pages.d.ts.map +0 -1
  56. package/dist/web-pages.js +0 -169
  57. package/dist/web-pages.js.map +0 -1
  58. package/src/slack-oauth.ts +0 -66
  59. package/src/web-pages.ts +0 -178
package/src/jwt.ts CHANGED
@@ -1,9 +1,223 @@
1
1
  /**
2
2
  * JWT utilities for auth tokens.
3
3
  *
4
- * Minimal JWT implementation using Web Crypto API (HMAC-SHA256).
5
- * No external dependencies.
4
+ * Supports two modes:
5
+ * - ES256 (asymmetric) — for production / cross-registry trust
6
+ * - HS256 (HMAC) — for backward compat / simple single-server setups
7
+ *
8
+ * Uses `jose` library for all crypto operations.
9
+ */
10
+
11
+ import {
12
+ SignJWT,
13
+ jwtVerify,
14
+ generateKeyPair,
15
+ exportJWK,
16
+ importJWK,
17
+ createRemoteJWKSet,
18
+ type JWTPayload,
19
+
20
+ type JWK,
21
+ } from "jose";
22
+
23
+ // ============================================
24
+ // Types
25
+ // ============================================
26
+
27
+ /** JWT payload for auth tokens */
28
+ export interface AgentJwtPayload {
29
+ /** Subject - the client ID or user ID */
30
+ sub: string;
31
+ /** Client/user name */
32
+ name: string;
33
+ /** Issuer URL */
34
+ iss?: string;
35
+ /** Tenant ID */
36
+ tenantId?: string;
37
+ /** User ID (when acting on behalf of a user) */
38
+ userId?: string;
39
+ /** Scopes */
40
+ scopes: string[];
41
+ /** Identities (for cross-registry provisioning) */
42
+ identities?: Array<{ provider: string; id: string; [key: string]: unknown }>;
43
+ /** Issued at (unix seconds) */
44
+ iat: number;
45
+ /** Expires at (unix seconds) */
46
+ exp: number;
47
+ }
48
+
49
+ /** A signing key with metadata */
50
+ export interface SigningKey {
51
+ /** Unique key ID */
52
+ kid: string;
53
+ /** Private key (for signing) */
54
+ privateKey: CryptoKey;
55
+ /** Public key (for verification + JWKS) */
56
+ publicKey: CryptoKey;
57
+ /** Algorithm */
58
+ alg: string;
59
+ /** Status */
60
+ status: "active" | "deprecated" | "revoked";
61
+ /** When this key was created */
62
+ createdAt: number;
63
+ }
64
+
65
+ /** Exported key pair for storage */
66
+ export interface ExportedKeyPair {
67
+ kid: string;
68
+ alg: string;
69
+ privateKeyJwk: JWK;
70
+ publicKeyJwk: JWK;
71
+ status: "active" | "deprecated" | "revoked";
72
+ createdAt: number;
73
+ }
74
+
75
+ // ============================================
76
+ // Key Generation
77
+ // ============================================
78
+
79
+ /**
80
+ * Generate a new ES256 signing key pair.
81
+ */
82
+ export async function generateSigningKey(kid?: string): Promise<SigningKey> {
83
+ const { privateKey, publicKey } = await generateKeyPair("ES256");
84
+ return {
85
+ kid: kid ?? `key-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
86
+ privateKey,
87
+ publicKey,
88
+ alg: "ES256",
89
+ status: "active",
90
+ createdAt: Date.now(),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Export a signing key to JWK format (for storage).
96
+ */
97
+ export async function exportSigningKey(key: SigningKey): Promise<ExportedKeyPair> {
98
+ const privateKeyJwk = await exportJWK(key.privateKey);
99
+ const publicKeyJwk = await exportJWK(key.publicKey);
100
+ return {
101
+ kid: key.kid,
102
+ alg: key.alg,
103
+ privateKeyJwk,
104
+ publicKeyJwk,
105
+ status: key.status,
106
+ createdAt: key.createdAt,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Import a signing key from stored JWK format.
112
+ */
113
+ export async function importSigningKey(exported: ExportedKeyPair): Promise<SigningKey> {
114
+ const privateKey = await importJWK(exported.privateKeyJwk, exported.alg) as CryptoKey;
115
+ const publicKey = await importJWK(exported.publicKeyJwk, exported.alg) as CryptoKey;
116
+ return {
117
+ kid: exported.kid,
118
+ privateKey,
119
+ publicKey,
120
+ alg: exported.alg,
121
+ status: exported.status,
122
+ createdAt: exported.createdAt,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Build a JWKS (JSON Web Key Set) from signing keys.
128
+ * Only includes public keys.
129
+ */
130
+ export async function buildJwks(keys: SigningKey[]): Promise<{ keys: JWK[] }> {
131
+ const jwks: JWK[] = [];
132
+ for (const key of keys) {
133
+ if (key.status === "revoked") continue;
134
+ const jwk = await exportJWK(key.publicKey);
135
+ jwk.kid = key.kid;
136
+ jwk.alg = key.alg;
137
+ jwk.use = "sig";
138
+ jwks.push(jwk);
139
+ }
140
+ return { keys: jwks };
141
+ }
142
+
143
+ // ============================================
144
+ // Signing (ES256)
145
+ // ============================================
146
+
147
+ /**
148
+ * Sign a JWT with ES256 using the server's private key.
6
149
  */
150
+ export async function signJwtES256(
151
+ payload: Omit<AgentJwtPayload, "iat" | "exp"> & { iat?: number; exp?: number },
152
+ privateKey: CryptoKey,
153
+ kid: string,
154
+ issuer?: string,
155
+ expiresIn?: string,
156
+ ): Promise<string> {
157
+ let builder = new SignJWT(payload as unknown as JWTPayload)
158
+ .setProtectedHeader({ alg: "ES256", kid })
159
+ .setIssuedAt();
160
+
161
+ if (issuer) builder = builder.setIssuer(issuer);
162
+ if (payload.sub) builder = builder.setSubject(payload.sub);
163
+ if (expiresIn) {
164
+ builder = builder.setExpirationTime(expiresIn);
165
+ } else if (payload.exp) {
166
+ builder = builder.setExpirationTime(payload.exp);
167
+ } else {
168
+ builder = builder.setExpirationTime("1h");
169
+ }
170
+
171
+ return builder.sign(privateKey);
172
+ }
173
+
174
+ // ============================================
175
+ // Verification
176
+ // ============================================
177
+
178
+ /**
179
+ * Verify a JWT against a local public key.
180
+ */
181
+ export async function verifyJwtLocal(
182
+ token: string,
183
+ publicKey: CryptoKey,
184
+ ): Promise<AgentJwtPayload | null> {
185
+ try {
186
+ const { payload } = await jwtVerify(token, publicKey);
187
+ return payload as unknown as AgentJwtPayload;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /** JWKS cache for remote issuers */
194
+ const jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
195
+
196
+ /**
197
+ * Verify a JWT against a remote issuer's JWKS.
198
+ * Fetches and caches the JWKS from the issuer's /.well-known/jwks.json
199
+ */
200
+ export async function verifyJwtFromIssuer(
201
+ token: string,
202
+ issuerUrl: string,
203
+ ): Promise<AgentJwtPayload | null> {
204
+ try {
205
+ const jwksUrl = issuerUrl.replace(/\/$/, "") + "/.well-known/jwks.json";
206
+ let jwks = jwksCache.get(jwksUrl);
207
+ if (!jwks) {
208
+ jwks = createRemoteJWKSet(new URL(jwksUrl));
209
+ jwksCache.set(jwksUrl, jwks);
210
+ }
211
+ const { payload } = await jwtVerify(token, jwks);
212
+ return payload as unknown as AgentJwtPayload;
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+
218
+ // ============================================
219
+ // Legacy HMAC (backward compat)
220
+ // ============================================
7
221
 
8
222
  const encoder = new TextEncoder();
9
223
 
@@ -20,106 +234,60 @@ function base64UrlDecode(str: string): Uint8Array {
20
234
 
21
235
  async function hmacSign(data: string, secret: string): Promise<Uint8Array> {
22
236
  const key = await crypto.subtle.importKey(
23
- "raw",
24
- encoder.encode(secret),
25
- { name: "HMAC", hash: "SHA-256" },
26
- false,
27
- ["sign"],
237
+ "raw", encoder.encode(secret),
238
+ { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
28
239
  );
29
240
  const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
30
241
  return new Uint8Array(sig);
31
242
  }
32
243
 
33
- async function hmacVerify(
34
- data: string,
35
- signature: Uint8Array,
36
- secret: string,
37
- ): Promise<boolean> {
244
+ async function hmacVerify(data: string, signature: Uint8Array, secret: string): Promise<boolean> {
38
245
  const key = await crypto.subtle.importKey(
39
- "raw",
40
- encoder.encode(secret),
41
- { name: "HMAC", hash: "SHA-256" },
42
- false,
43
- ["verify"],
44
- );
45
- return crypto.subtle.verify(
46
- "HMAC",
47
- key,
48
- signature.buffer as ArrayBuffer,
49
- encoder.encode(data),
246
+ "raw", encoder.encode(secret),
247
+ { name: "HMAC", hash: "SHA-256" }, false, ["verify"],
50
248
  );
249
+ return crypto.subtle.verify("HMAC", key, signature.buffer as ArrayBuffer, encoder.encode(data));
51
250
  }
52
251
 
53
- /** JWT payload for auth tokens */
54
- export interface JwtPayload {
55
- /** Subject - the client ID */
56
- sub: string;
57
- /** Client name */
58
- name: string;
59
- /** Tenant ID */
60
- tenantId?: string;
61
- /** Scopes */
62
- scopes: string[];
63
- /** Issued at (unix seconds) */
64
- iat: number;
65
- /** Expires at (unix seconds) */
66
- exp: number;
67
- }
252
+ /** @deprecated Use AgentJwtPayload instead */
253
+ export type JwtPayload = AgentJwtPayload;
68
254
 
69
255
  /**
70
- * Sign a JWT with HMAC-SHA256.
71
- *
72
- * @param payload - JWT payload (client_id, scopes, etc.)
73
- * @param secret - Signing secret (the client's secret hash)
74
- * @returns Signed JWT string
256
+ * Sign a JWT with HMAC-SHA256 (legacy).
257
+ * @deprecated Use signJwtES256 for new code.
75
258
  */
76
259
  export async function signJwt(
77
- payload: JwtPayload,
260
+ payload: AgentJwtPayload,
78
261
  secret: string,
79
262
  ): Promise<string> {
80
263
  const header = { alg: "HS256", typ: "JWT" };
81
-
82
264
  const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header)));
83
265
  const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));
84
266
  const signingInput = `${headerB64}.${payloadB64}`;
85
-
86
267
  const signature = await hmacSign(signingInput, secret);
87
- const signatureB64 = base64UrlEncode(signature);
88
-
89
- return `${signingInput}.${signatureB64}`;
268
+ return `${signingInput}.${base64UrlEncode(signature)}`;
90
269
  }
91
270
 
92
271
  /**
93
- * Verify and decode a JWT.
94
- *
95
- * @param token - JWT string
96
- * @param secret - Signing secret to verify against
97
- * @returns Decoded payload, or null if invalid/expired
272
+ * Verify and decode a JWT (HMAC-SHA256, legacy).
273
+ * @deprecated Use verifyJwtLocal or verifyJwtFromIssuer for new code.
98
274
  */
99
275
  export async function verifyJwt(
100
276
  token: string,
101
277
  secret: string,
102
- ): Promise<JwtPayload | null> {
278
+ ): Promise<AgentJwtPayload | null> {
103
279
  const parts = token.split(".");
104
280
  if (parts.length !== 3) return null;
105
-
106
281
  const [headerB64, payloadB64, signatureB64] = parts;
107
282
  const signingInput = `${headerB64}.${payloadB64}`;
108
-
109
283
  try {
110
284
  const signature = base64UrlDecode(signatureB64);
111
285
  const valid = await hmacVerify(signingInput, signature, secret);
112
286
  if (!valid) return null;
113
-
114
287
  const payload = JSON.parse(
115
288
  new TextDecoder().decode(base64UrlDecode(payloadB64)),
116
- ) as JwtPayload;
117
-
118
- // Check expiration
119
- if (payload.exp && payload.exp < Date.now() / 1000) {
120
- return null;
121
- }
122
-
289
+ ) as AgentJwtPayload;
290
+ if (payload.exp && payload.exp < Date.now() / 1000) return null;
123
291
  return payload;
124
292
  } catch {
125
293
  return null;
package/src/registry.ts CHANGED
@@ -37,6 +37,8 @@ const DEFAULT_SUPPORTED_ACTIONS: AgentAction[] = [
37
37
  export interface AgentRegistryOptions {
38
38
  /** Default visibility for agents without explicit visibility */
39
39
  defaultVisibility?: Visibility;
40
+ /** Factory to enrich ToolContext with application-specific data */
41
+ contextFactory?: ContextFactory;
40
42
  }
41
43
 
42
44
  /**
@@ -82,6 +84,12 @@ export interface AgentRegistry {
82
84
  * });
83
85
  * ```
84
86
  */
87
+ /**
88
+ * Factory function that enriches the base ToolContext with application-specific data.
89
+ * Called before every tool execution.
90
+ */
91
+ export type ContextFactory = (baseCtx: import("./types.js").ToolContext) => import("./types.js").ToolContext;
92
+
85
93
  export function createAgentRegistry(
86
94
  options: AgentRegistryOptions = {},
87
95
  ): AgentRegistry {
@@ -165,6 +173,8 @@ export function createAgentRegistry(
165
173
  return (
166
174
  callerType === "agent" || (callerType != null && callerId != null)
167
175
  );
176
+ case "authenticated":
177
+ return callerId != null && callerId !== "anonymous";
168
178
  case "private":
169
179
  return callerId === agent.path;
170
180
  default:
@@ -276,12 +286,12 @@ export function createAgentRegistry(
276
286
  ) {
277
287
  return {
278
288
  success: false,
279
- error: `Access denied to tool: ${request.tool}`,
289
+ error: `Access denied to tool: ${request.tool} (visibility=${tool.visibility}, callerId=${request.callerId}, callerType=${request.callerType})`,
280
290
  code: "ACCESS_DENIED",
281
291
  } as CallAgentErrorResponse;
282
292
  }
283
293
 
284
- const ctx: ToolContext = {
294
+ let ctx: ToolContext = {
285
295
  tenantId: "default",
286
296
  agentPath: agent.path,
287
297
  callerId: request.callerId ?? "unknown",
@@ -289,6 +299,11 @@ export function createAgentRegistry(
289
299
  metadata: request.metadata,
290
300
  };
291
301
 
302
+ // Apply contextFactory if provided
303
+ if (options.contextFactory) {
304
+ ctx = options.contextFactory(ctx);
305
+ }
306
+
292
307
  try {
293
308
  if (!tool.execute) {
294
309
  return {
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Secret Collection
3
+ *
4
+ * Manages pending secret collection forms (one-time tokens).
5
+ * Used by @integrations collect_secrets and the server's /secrets/collect endpoint.
6
+ *
7
+ * Exported for use in custom server implementations.
8
+ */
9
+
10
+ // ============================================
11
+ // Types
12
+ // ============================================
13
+
14
+ export interface PendingCollectionField {
15
+ name: string;
16
+ description?: string;
17
+ required: boolean;
18
+ secret: boolean;
19
+ }
20
+
21
+ export interface PendingCollection {
22
+ agent: string;
23
+ tool: string;
24
+ params: Record<string, unknown>;
25
+ fields: PendingCollectionField[];
26
+ auth?: {
27
+ callerId: string;
28
+ callerType: string;
29
+ scopes?: string[];
30
+ isRoot?: boolean;
31
+ };
32
+ createdAt: number;
33
+ }
34
+
35
+ // ============================================
36
+ // Storage (with TTL cleanup)
37
+ // ============================================
38
+
39
+ /** Default TTL for pending collections: 15 minutes */
40
+ const COLLECTION_TTL_MS = 15 * 60 * 1000;
41
+
42
+ /** Pending secret collection forms, keyed by one-time token */
43
+ export const pendingCollections = new Map<string, PendingCollection>();
44
+
45
+ /** Generate a random one-time token for secret collection */
46
+ export function generateCollectionToken(): string {
47
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
48
+ let token = "";
49
+ for (let i = 0; i < 48; i++) {
50
+ token += chars[Math.floor(Math.random() * chars.length)];
51
+ }
52
+ return token;
53
+ }
54
+
55
+ /**
56
+ * Clean up expired pending collections.
57
+ * Call this periodically or before lookups.
58
+ */
59
+ export function cleanupExpiredCollections(): void {
60
+ const now = Date.now();
61
+ for (const [token, pending] of pendingCollections) {
62
+ if (now - pending.createdAt > COLLECTION_TTL_MS) {
63
+ pendingCollections.delete(token);
64
+ }
65
+ }
66
+ }