@agentforge-io/sdk-server 4.0.18

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,71 @@
1
+ "use strict";
2
+ // secrets resource — gobernanza de los secrets COMPANY-scoped del tenant
3
+ // desde una app externa (ScrappyDoc). Espeja el patrón de connectors: el
4
+ // tenant sale SIEMPRE del ak_live_, la app hija decide qué valores custom
5
+ // setear/rotar/borrar para su propio workspace, y el platform los guarda
6
+ // cifrados en `af_platform_secrets` (scope 'company').
7
+ //
8
+ // Para qué sirve: un tenant puede sobrescribir keys o crear valores custom
9
+ // (API keys de terceros, tokens, flags) que luego consumen sus agents/tools
10
+ // en runtime — sin que la app hija tenga que guardarlos ella misma. La
11
+ // plataforma madre es la única fuente de verdad y los sirve cifrados.
12
+ //
13
+ // Auth: SIEMPRE ak_live_ tenant-wide — estos endpoints usan `ApiKeyGuard` en
14
+ // el server (ver ApiKeySecretsController) y rechazan ChatToken, aunque el
15
+ // `AgentForgeClient` también tenga uno configurado (ver `requestWithApiKey`).
16
+ //
17
+ // SEGURIDAD: esta superficie SOLO ve/toca los secrets 'company' del tenant.
18
+ // Los secrets 'instance' de la plataforma (ANTHROPIC_API_KEY, STRIPE_*, OAuth
19
+ // de plataforma) NUNCA se exponen acá — el server fuerza scope 'company'.
20
+ // Los valores en claro NUNCA viajan de vuelta: `list()` solo devuelve un hint
21
+ // (últimos 4 chars) + metadata.
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.SecretsResource = void 0;
24
+ class SecretsResource {
25
+ constructor(client) {
26
+ this.client = client;
27
+ }
28
+ /**
29
+ * `GET /tenant-secrets` — metadata de TODOS los secrets company-scoped que
30
+ * este tenant ha configurado. Nunca devuelve el valor en claro, solo
31
+ * `valueHint` (últimos 4 chars) y flags — suficiente para renderizar una
32
+ * lista "configurado / no configurado" en la UI de la app hija.
33
+ *
34
+ * @example
35
+ * const { items } = await af.secrets.list();
36
+ * const configured = items.filter((s) => s.configured);
37
+ */
38
+ async list() {
39
+ return this.client.requestWithApiKey('GET', '/tenant-secrets');
40
+ }
41
+ /**
42
+ * `PUT /tenant-secrets/:key` — crea o sobrescribe UN secret company-scoped
43
+ * para este tenant. El valor viaja cifrado al vault del platform; la
44
+ * respuesta trae solo metadata (nunca el valor en claro). La `key` debe
45
+ * matchear `/^[A-Z][A-Z0-9_]*$/` (mayúsculas, dígitos, guión bajo; empieza
46
+ * con letra) — el server responde 400 si no.
47
+ *
48
+ * El caller es responsable de gatear quién puede llamar esto (ej. solo
49
+ * admin/owner del workspace) — el server no impone rol propio más allá de
50
+ * la ak_live_ tenant-wide.
51
+ *
52
+ * @example
53
+ * await af.secrets.set('OPENAI_API_KEY', 'sk-...');
54
+ */
55
+ async set(key, value) {
56
+ return this.client.requestWithApiKey('PUT', `/tenant-secrets/${encodeURIComponent(key)}`, { value });
57
+ }
58
+ /**
59
+ * `DELETE /tenant-secrets/:key` — borra el override company-scoped de este
60
+ * tenant para `key`. No toca el valor 'instance' de la plataforma (si lo
61
+ * hubiera): tras esto, la resolución en runtime cae al fallback de
62
+ * plataforma. Devuelve 204 (sin body).
63
+ *
64
+ * @example
65
+ * await af.secrets.remove('OPENAI_API_KEY');
66
+ */
67
+ async remove(key) {
68
+ await this.client.requestWithApiKey('DELETE', `/tenant-secrets/${encodeURIComponent(key)}`);
69
+ }
70
+ }
71
+ exports.SecretsResource = SecretsResource;
@@ -0,0 +1,58 @@
1
+ import type { AgentForgeClient } from '../client';
2
+ import type { ToolCallbackPayload } from '../types';
3
+ export interface VerifyCallbackInput {
4
+ /** El body EXACTO tal como llegó por la red (antes de cualquier
5
+ * JSON.parse) — HMAC es sensible a cualquier diferencia de bytes. */
6
+ rawBody: string;
7
+ /** Valor crudo del header `X-AgentForge-Signature`. */
8
+ signatureHeader: string;
9
+ /** El secret `whsec_...` que recibiste al registrar esta tool. */
10
+ secretPlaintext: string;
11
+ }
12
+ export interface RespondToCallbackInput {
13
+ /** El `resultUrl` que trajo el `ToolCallbackPayload` — path relativo,
14
+ * se resuelve contra el mismo `baseURL` del client. */
15
+ resultUrl: string;
16
+ tenantId: string;
17
+ /** El mismo secret `whsec_...` usado para verificar el callback entrante. */
18
+ secret: string;
19
+ /** Texto que el LLM verá como tool_result. Ignorado si `errorMessage` viene. */
20
+ result?: string;
21
+ /** Si la ejecución falló, el platform marca la ToolCall como failed y el
22
+ * LLM ve este mensaje como el resultado. */
23
+ errorMessage?: string;
24
+ }
25
+ export declare class ToolsResource {
26
+ private readonly client;
27
+ constructor(client: AgentForgeClient);
28
+ /**
29
+ * Verifica la firma HMAC de un callback ENTRANTE del platform y devuelve
30
+ * el payload ya parseado. Tira `ToolCallbackSignatureError` (código
31
+ * 'malformed' | 'stale' | 'mismatch') si la firma no es válida — tu
32
+ * endpoint debería responder 401 en ese caso, nunca ejecutar la tool.
33
+ *
34
+ * @example
35
+ * const payload = af.tools.verifyCallback({
36
+ * rawBody: req.rawBody!.toString('utf8'),
37
+ * signatureHeader: req.headers['x-agentforge-signature'] as string,
38
+ * secretPlaintext: process.env.MY_TOOL_SECRET!,
39
+ * });
40
+ * // payload.toolName, payload.input, payload.resultUrl…
41
+ */
42
+ verifyCallback(input: VerifyCallbackInput): ToolCallbackPayload;
43
+ /**
44
+ * Responde el resultado de una tool 'remote' ejecutada localmente —
45
+ * firma el body y lo POSTea a `resultUrl`. El platform reanuda el turno
46
+ * del LLM con ese resultado (o lo marca failed si mandaste
47
+ * `errorMessage`). 204 en éxito — no hay nada que leer del response.
48
+ *
49
+ * @example
50
+ * await af.tools.respondToCallback({
51
+ * resultUrl: payload.resultUrl,
52
+ * tenantId: payload.tenantId,
53
+ * secret: process.env.MY_TOOL_SECRET!,
54
+ * result: JSON.stringify({ ok: true }),
55
+ * });
56
+ */
57
+ respondToCallback(input: RespondToCallbackInput): Promise<void>;
58
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ // tools resource — el lado CLIENTE del ciclo de vida de una tool 'remote'
3
+ // registrada vía `agents.registerTools({ tools: [{ executionMode: 'remote', ... }] })`:
4
+ //
5
+ // 1. El LLM invoca la tool → el platform POSTea (firmado) tu propio
6
+ // `callbackUrl` público con un `ToolCallbackPayload`.
7
+ // 2. Tu endpoint verifica la firma (`verifyCallback`) y ejecuta la tool
8
+ // localmente — nunca corre en el platform.
9
+ // 3. Devolvés el resultado con `respondToCallback()`, firmado con el MISMO
10
+ // secret — el platform reanuda el turno del LLM con ese resultado.
11
+ //
12
+ // Este resource NO expone un servidor HTTP (eso lo monta tu framework — Nest,
13
+ // Express, lo que sea); solo la verificación/firma/POST del protocolo.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ToolsResource = void 0;
16
+ const webhooks_1 = require("../webhooks");
17
+ class ToolsResource {
18
+ constructor(client) {
19
+ this.client = client;
20
+ }
21
+ /**
22
+ * Verifica la firma HMAC de un callback ENTRANTE del platform y devuelve
23
+ * el payload ya parseado. Tira `ToolCallbackSignatureError` (código
24
+ * 'malformed' | 'stale' | 'mismatch') si la firma no es válida — tu
25
+ * endpoint debería responder 401 en ese caso, nunca ejecutar la tool.
26
+ *
27
+ * @example
28
+ * const payload = af.tools.verifyCallback({
29
+ * rawBody: req.rawBody!.toString('utf8'),
30
+ * signatureHeader: req.headers['x-agentforge-signature'] as string,
31
+ * secretPlaintext: process.env.MY_TOOL_SECRET!,
32
+ * });
33
+ * // payload.toolName, payload.input, payload.resultUrl…
34
+ */
35
+ verifyCallback(input) {
36
+ (0, webhooks_1.verifyToolCallbackSignature)({
37
+ rawBody: input.rawBody,
38
+ signatureHeader: input.signatureHeader,
39
+ secretPlaintext: input.secretPlaintext,
40
+ });
41
+ return JSON.parse(input.rawBody);
42
+ }
43
+ /**
44
+ * Responde el resultado de una tool 'remote' ejecutada localmente —
45
+ * firma el body y lo POSTea a `resultUrl`. El platform reanuda el turno
46
+ * del LLM con ese resultado (o lo marca failed si mandaste
47
+ * `errorMessage`). 204 en éxito — no hay nada que leer del response.
48
+ *
49
+ * @example
50
+ * await af.tools.respondToCallback({
51
+ * resultUrl: payload.resultUrl,
52
+ * tenantId: payload.tenantId,
53
+ * secret: process.env.MY_TOOL_SECRET!,
54
+ * result: JSON.stringify({ ok: true }),
55
+ * });
56
+ */
57
+ async respondToCallback(input) {
58
+ const rawBody = JSON.stringify(input.errorMessage
59
+ ? { result: '', errorMessage: input.errorMessage }
60
+ : { result: input.result ?? '' });
61
+ const signature = (0, webhooks_1.signToolCallbackPayload)(input.secret, rawBody);
62
+ await this.client.requestRaw('POST', input.resultUrl, {
63
+ rawBody,
64
+ headers: {
65
+ [webhooks_1.TOOL_CALLBACK_HEADERS.signature]: signature,
66
+ [webhooks_1.TOOL_CALLBACK_HEADERS.tenant]: input.tenantId,
67
+ },
68
+ timeoutMs: 10000,
69
+ });
70
+ }
71
+ }
72
+ exports.ToolsResource = ToolsResource;
@@ -0,0 +1,355 @@
1
+ export type ToolPermissionMode = 'allow' | 'approval' | 'blocked';
2
+ export interface ToolPermission {
3
+ name: string;
4
+ mode: ToolPermissionMode;
5
+ }
6
+ export interface ExternalToolSummary {
7
+ name: string;
8
+ description: string;
9
+ inputSchema: Record<string, unknown>;
10
+ executionMode: 'client' | 'remote';
11
+ /** Solo presente cuando `executionMode === 'remote'`. */
12
+ callbackUrl?: string | null;
13
+ /** Prefijo del whsec_ — útil para identificar qué secret tiene el cliente
14
+ * en su .env. El plaintext nunca se devuelve en el endpoint. */
15
+ secretPrefix?: string | null;
16
+ source: 'client-registered';
17
+ lastUpsertedAt: string | Date;
18
+ }
19
+ export interface McpServerConfig {
20
+ name: string;
21
+ transport: 'http' | 'sse';
22
+ url: string;
23
+ headers?: Record<string, string>;
24
+ tenantId?: string;
25
+ }
26
+ export interface AgentMe {
27
+ id: string;
28
+ slug: string;
29
+ name: string;
30
+ description: string | null;
31
+ model: string | null;
32
+ systemPrompt: string;
33
+ context: string | null;
34
+ temperature: number | null;
35
+ topP: number | null;
36
+ maxTokens: number | null;
37
+ tools: ToolPermission[];
38
+ mcpServers: McpServerConfig[];
39
+ skills: string[];
40
+ timezone: string | null;
41
+ isActive: boolean;
42
+ updatedAt: string | Date;
43
+ externalTools: ExternalToolSummary[];
44
+ }
45
+ export interface RegisterToolInput {
46
+ name: string;
47
+ description: string;
48
+ inputSchema: Record<string, unknown>;
49
+ /** 'client' (default) = la app cliente ejecuta la tool in-process, solo
50
+ * declara el shape para visibilidad/auditoría. 'remote' = el platform
51
+ * invoca `callbackUrl` cuando el LLM la usa y espera el resultado en
52
+ * POST /api/v1/agents/me/tool-results/:toolUseId (ver ToolsResource). */
53
+ executionMode?: 'client' | 'remote';
54
+ /** Requerido cuando `executionMode === 'remote'`. */
55
+ callbackUrl?: string;
56
+ }
57
+ export interface RegisterToolsInput {
58
+ tools: RegisterToolInput[];
59
+ }
60
+ export interface RegisterToolsResult {
61
+ agentId: string;
62
+ count: number;
63
+ tools: Array<{
64
+ name: string;
65
+ lastUpsertedAt: string | Date;
66
+ /** Solo presente cuando esta tool (o su callbackUrl-group entero, ver
67
+ * ExternalToolsRepository) acaba de rotar su secret HMAC — el
68
+ * plaintext SOLO se muestra esta vez, guárdalo. Ausente si el secret
69
+ * no cambió respecto al registro anterior. */
70
+ secret?: string;
71
+ }>;
72
+ }
73
+ export interface AgentSummary {
74
+ id: string;
75
+ slug: string;
76
+ name: string;
77
+ description: string | null;
78
+ model: string | null;
79
+ systemPrompt: string;
80
+ context: string | null;
81
+ temperature: number | null;
82
+ topP: number | null;
83
+ maxTokens: number | null;
84
+ timezone: string | null;
85
+ isActive: boolean;
86
+ createdAt: string | Date;
87
+ updatedAt: string | Date;
88
+ }
89
+ export interface CreateAgentInput {
90
+ slug: string;
91
+ name: string;
92
+ systemPrompt?: string;
93
+ description?: string;
94
+ model?: string;
95
+ context?: string;
96
+ temperature?: number;
97
+ topP?: number;
98
+ maxTokens?: number;
99
+ timezone?: string;
100
+ isActive?: boolean;
101
+ metadata?: Record<string, unknown>;
102
+ tools?: Array<string | {
103
+ name: string;
104
+ mode: ToolPermissionMode;
105
+ }>;
106
+ }
107
+ export interface UpdateAgentInput {
108
+ name?: string;
109
+ description?: string;
110
+ model?: string;
111
+ systemPrompt?: string;
112
+ context?: string;
113
+ temperature?: number;
114
+ topP?: number;
115
+ maxTokens?: number;
116
+ timezone?: string;
117
+ isActive?: boolean;
118
+ metadata?: Record<string, unknown>;
119
+ /** Set completo de permisos de tool. Pass-through al backend — cada item
120
+ * es `{name, mode}` (a diferencia de `CreateAgentInput.tools`, que además
121
+ * acepta strings sueltos con mode='allow' implícito). */
122
+ tools?: Array<string | {
123
+ name: string;
124
+ mode: ToolPermissionMode;
125
+ }>;
126
+ }
127
+ export interface ListAgentsResult {
128
+ items: AgentSummary[];
129
+ total: number;
130
+ }
131
+ export interface ChatTokenRecord {
132
+ token: string;
133
+ tenantId: string;
134
+ agentId: string;
135
+ name: string;
136
+ allowedOrigins?: string[];
137
+ isActive: boolean;
138
+ createdByUserId?: string;
139
+ lastUsedAt?: string | Date;
140
+ createdAt: string | Date;
141
+ updatedAt: string | Date;
142
+ }
143
+ export type MessageRole = 'user' | 'assistant' | 'system';
144
+ export type ConversationStatus = 'active' | 'completed' | 'error' | 'paused';
145
+ export interface TokenUsage {
146
+ inputTokens: number;
147
+ outputTokens: number;
148
+ totalTokens: number;
149
+ cacheCreationInputTokens?: number;
150
+ cacheReadInputTokens?: number;
151
+ }
152
+ export interface ToolCallRecord {
153
+ toolName: string;
154
+ toolUseId: string;
155
+ input: Record<string, unknown>;
156
+ output?: string;
157
+ error?: string;
158
+ durationMs?: number;
159
+ }
160
+ export interface MessageRecord {
161
+ id: string;
162
+ role: MessageRole;
163
+ content: string;
164
+ toolCalls?: ToolCallRecord[];
165
+ usage?: TokenUsage;
166
+ metadata?: Record<string, unknown>;
167
+ createdAt: string | Date;
168
+ }
169
+ export interface ConversationHistory {
170
+ conversationId: string;
171
+ agentId: string;
172
+ userId: string;
173
+ title?: string;
174
+ status: ConversationStatus;
175
+ messages: MessageRecord[];
176
+ createdAt: string | Date;
177
+ updatedAt: string | Date;
178
+ }
179
+ export interface ApprovalCopyBundle {
180
+ title: string;
181
+ body: string;
182
+ approveLabel: string;
183
+ approveBusyLabel: string;
184
+ denyLabel: string;
185
+ denyBusyLabel: string;
186
+ approvedPill: string;
187
+ deniedPill: string;
188
+ readOnlyHint: string;
189
+ blockedTitle: string;
190
+ blockedBody: string;
191
+ expiresPrefix: string;
192
+ }
193
+ interface ActingAgentTag {
194
+ /** Presente cuando un team orchestrator delegó el turno a un miembro —
195
+ * el id de quién habla. Ausente = el agent primario de la sesión. */
196
+ actingAgentId?: string;
197
+ }
198
+ /** Unión completa de chunks que puede traer un turno de chat-stream. */
199
+ export type ChatStreamChunk = ({
200
+ type: 'text_delta';
201
+ delta: string;
202
+ } & ActingAgentTag) | ({
203
+ type: 'tool_use_start';
204
+ toolName: string;
205
+ toolUseId: string;
206
+ input?: Record<string, unknown>;
207
+ } & ActingAgentTag) | ({
208
+ type: 'tool_result';
209
+ toolName: string;
210
+ result: string;
211
+ } & ActingAgentTag) | ({
212
+ type: 'respond_data';
213
+ internalData?: unknown;
214
+ } & ActingAgentTag) | {
215
+ type: 'delegation_start';
216
+ subAgentId: string;
217
+ task: string;
218
+ subAgentName?: string;
219
+ subAgentAvatarUrl?: string;
220
+ } | {
221
+ type: 'delegation_result';
222
+ subAgentId: string;
223
+ result: string;
224
+ } | ({
225
+ type: 'usage';
226
+ usage: TokenUsage;
227
+ } & ActingAgentTag) | {
228
+ type: 'awaiting_approval';
229
+ approvalId: string;
230
+ toolName: string;
231
+ expiresAt: string;
232
+ connectorName?: string;
233
+ toolDescription?: string;
234
+ copy?: ApprovalCopyBundle;
235
+ } | {
236
+ type: 'tool_blocked';
237
+ toolName: string;
238
+ reason?: string;
239
+ connectorName?: string;
240
+ toolDescription?: string;
241
+ copy?: ApprovalCopyBundle;
242
+ } | {
243
+ type: 'done';
244
+ messageId: string;
245
+ };
246
+ /**
247
+ * Evento tipado que `ChatResource.streamMessage()` yieldea — envuelve el
248
+ * frame crudo SSE (`event:`/`data:`) que emite chat-stream.controller.ts:
249
+ * - `conversation`: primer frame, siempre. Trae el conversationId (nuevo o
250
+ * el que pasaste) para poder persistirlo (ej. en la URL) desde el turno 1.
251
+ * - `chunk`: cualquier StreamChunk del turno del agente (incluye su propio
252
+ * `{type:'done', messageId}` cuando el agente terminó su respuesta).
253
+ * - `stream_end`: el frame `{ok:true}` que cierra la conexión HTTP —
254
+ * DISTINTO del `chunk` de tipo 'done' de arriba (el wire manda AMBOS,
255
+ * con el mismo `event: done` pero shapes distintos — se discriminan acá
256
+ * por la presencia de `.type` en el JSON).
257
+ * - `error`: el turno falló server-side (tool remota caída, LLM error, etc).
258
+ */
259
+ export type ChatStreamEvent = {
260
+ kind: 'conversation';
261
+ conversationId: string;
262
+ } | {
263
+ kind: 'chunk';
264
+ chunk: ChatStreamChunk;
265
+ } | {
266
+ kind: 'stream_end';
267
+ ok: boolean;
268
+ } | {
269
+ kind: 'error';
270
+ message: string;
271
+ };
272
+ export interface ConnectorStatus {
273
+ connectorId: string;
274
+ /** ¿El tenant ya completó el OAuth/API-key setup de este connector? */
275
+ connected: boolean;
276
+ /** ¿Tiene selector de recursos real implementado (nivel 2 del picker)?
277
+ * false = se puede conectar pero `listResources()` devuelve `unsupported`. */
278
+ browsable: boolean;
279
+ }
280
+ export interface ConnectorResourceItem {
281
+ id: string;
282
+ name: string;
283
+ description?: string;
284
+ }
285
+ export interface ListConnectorResourcesResult {
286
+ items: ConnectorResourceItem[];
287
+ /** true cuando el connector está conectado pero `browsable` es false —
288
+ * no es un error, la UI debe mostrar un estado "sin selector todavía". */
289
+ unsupported?: true;
290
+ }
291
+ export interface CreateClickupTaskInput {
292
+ /** Id de la lista de ClickUp (`ConnectorResourceItem.id` del picker). */
293
+ listId: string;
294
+ name: string;
295
+ description?: string;
296
+ }
297
+ export interface CreateClickupTaskResult {
298
+ id: string;
299
+ /** URL de la tarea en la app de ClickUp, para enlazar desde el caller. */
300
+ url: string;
301
+ name: string;
302
+ }
303
+ /** De dónde viene el valor resuelto: override guardado por el tenant,
304
+ * template del autor del connector, o fallback allow-all implícito. */
305
+ export type ConnectorDefaultsSource = 'tenant' | 'template' | 'fallback';
306
+ export interface ConnectorToolInfo {
307
+ name: string;
308
+ description?: string;
309
+ }
310
+ export interface ConnectorDefaultsResult {
311
+ tools: ConnectorToolInfo[];
312
+ permissions: ToolPermission[];
313
+ source: ConnectorDefaultsSource;
314
+ }
315
+ /** Body que el platform POSTea al `callbackUrl` de una tool 'remote' cuando
316
+ * el LLM la invoca. Firmado con HMAC (ver webhooks.ts) — usa
317
+ * `ToolsResource.verifyCallback()` para verificar + parsear en un paso. */
318
+ export interface ToolCallbackPayload {
319
+ toolUseId: string;
320
+ tenantId: string;
321
+ agentId: string;
322
+ conversationId: string;
323
+ toolName: string;
324
+ input: Record<string, unknown>;
325
+ /** ISO timestamp — el platform deja de esperar la respuesta después de
326
+ * esto (default 30s desde que se disparó el callback). */
327
+ expiresAt: string;
328
+ /** Path relativo (no absoluto) — pasalo tal cual a
329
+ * `ToolsResource.respondToCallback({ resultUrl, ... })`. */
330
+ resultUrl: string;
331
+ }
332
+ export interface IssueChatTokenInput {
333
+ /** Etiqueta humana. Si se omite, el server asigna un default con fecha. */
334
+ name?: string;
335
+ /** Si se omite, el token acepta cualquier Origin (útil en dev). */
336
+ allowedOrigins?: string[];
337
+ }
338
+ /** Metadata pública de un secret company-scoped del tenant, tal como la
339
+ * devuelve `af.secrets.list()` / `af.secrets.set()`. NUNCA incluye el valor
340
+ * en claro ni el ciphertext — solo un hint (últimos 4 chars) + flags. */
341
+ export interface TenantSecretEntry {
342
+ /** Nombre de la key (ej. `OPENAI_API_KEY`). */
343
+ key: string;
344
+ /** Descripción libre; el server pone un placeholder si no se dio ninguna. */
345
+ description: string;
346
+ /** `true` cuando el tenant tiene un valor guardado para esta key. */
347
+ configured: boolean;
348
+ /** Últimos 4 chars del valor (o `••••` si es muy corto). Ausente si no hay
349
+ * valor persistido. Nunca el valor completo. */
350
+ valueHint?: string;
351
+ /** ISO timestamp de la última escritura (serializado por el JSON transport).
352
+ * Ausente si nunca se configuró. */
353
+ updatedAt?: string;
354
+ }
355
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ // Tipos compartidos del SDK. Espejan los shapes que devuelven los endpoints
3
+ // públicos del platform (ProxyPublicController). NO importamos del server
4
+ // para mantener el SDK desacoplado del runtime de NestJS.
5
+ //
6
+ // Cuando el server cambia un shape público, esto debe actualizarse. Eventual
7
+ // migración a generación automática desde OpenAPI queda como follow-up.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,22 @@
1
+ export declare const TOOL_CALLBACK_HEADERS: {
2
+ readonly signature: "x-agentforge-signature";
3
+ readonly tenant: "x-agentforge-tenant";
4
+ };
5
+ export declare class ToolCallbackSignatureError extends Error {
6
+ readonly code: 'malformed' | 'stale' | 'mismatch';
7
+ constructor(code: 'malformed' | 'stale' | 'mismatch', message: string);
8
+ }
9
+ /** Firma un body para enviar al peer. Devuelve el valor del header
10
+ * `X-AgentForge-Signature`. */
11
+ export declare function signToolCallbackPayload(secretPlaintext: string, rawBody: string): string;
12
+ /** Verifica una firma contra el secret plaintext + raw body. Constant-time.
13
+ * Tira `ToolCallbackSignatureError` si la firma es inválida, vieja, o
14
+ * malformed. El `rawBody` debe ser EXACTAMENTE el string que llegó por la
15
+ * red (no un objeto re-serializado) — HMAC es sensible a cualquier byte de
16
+ * diferencia (orden de keys, espacios, etc). */
17
+ export declare function verifyToolCallbackSignature(input: {
18
+ rawBody: string;
19
+ signatureHeader: string;
20
+ secretPlaintext: string;
21
+ nowSeconds?: number;
22
+ }): void;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ // Helpers de HMAC para autenticación de callbacks Platform ↔ Cliente — MISMO
3
+ // algoritmo exacto que agentforge-platform/apps/server/src/modules/proxy/
4
+ // hmac.ts (portado acá, no importado, por la misma razón de desacople del
5
+ // resto del SDK: esta app cliente firma/verifica en AMBAS direcciones y
6
+ // necesita el algoritmo disponible sin arrastrar el runtime del server).
7
+ //
8
+ // Modelo (estilo Stripe webhook):
9
+ // - Cliente y platform comparten un secret HMAC `whsec_*` — el cliente lo
10
+ // recibe plaintext en `RegisterToolsResult.tools[].secret` (solo la
11
+ // primera vez, o cuando rota — ver AgentsResource.registerTools).
12
+ // - Cada mensaje firmado lleva header:
13
+ // X-AgentForge-Signature: t=<timestamp>,v1=<hex>
14
+ // donde:
15
+ // timestamp = unix segundos
16
+ // v1 = HMAC_SHA256(secret, `${timestamp}.${raw_body}`)
17
+ // - El receptor: extrae timestamp + v1, recomputa, compara constant-time.
18
+ // - Defensa contra replay: timestamp debe estar dentro de ±5 min (300 s).
19
+ //
20
+ // Direcciones de uso:
21
+ // - El platform FIRMA cuando POSTea el callback de una tool 'remote' al
22
+ // cliente — el cliente VERIFICA con `verifyToolCallbackSignature`.
23
+ // - El cliente FIRMA cuando responde con el resultado — ver
24
+ // `ToolsResource.respondToCallback`, que llama `signToolCallbackPayload`
25
+ // internamente (no hace falta invocarlo a mano).
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.ToolCallbackSignatureError = exports.TOOL_CALLBACK_HEADERS = void 0;
28
+ exports.signToolCallbackPayload = signToolCallbackPayload;
29
+ exports.verifyToolCallbackSignature = verifyToolCallbackSignature;
30
+ const node_crypto_1 = require("node:crypto");
31
+ const MAX_AGE_SECONDS = 300;
32
+ exports.TOOL_CALLBACK_HEADERS = {
33
+ signature: 'x-agentforge-signature',
34
+ tenant: 'x-agentforge-tenant',
35
+ };
36
+ class ToolCallbackSignatureError extends Error {
37
+ constructor(code, message) {
38
+ super(message);
39
+ this.name = 'ToolCallbackSignatureError';
40
+ this.code = code;
41
+ }
42
+ }
43
+ exports.ToolCallbackSignatureError = ToolCallbackSignatureError;
44
+ /** Firma un body para enviar al peer. Devuelve el valor del header
45
+ * `X-AgentForge-Signature`. */
46
+ function signToolCallbackPayload(secretPlaintext, rawBody) {
47
+ const ts = Math.floor(Date.now() / 1000);
48
+ return `t=${ts},v1=${computeHmac(secretPlaintext, ts, rawBody)}`;
49
+ }
50
+ /** Verifica una firma contra el secret plaintext + raw body. Constant-time.
51
+ * Tira `ToolCallbackSignatureError` si la firma es inválida, vieja, o
52
+ * malformed. El `rawBody` debe ser EXACTAMENTE el string que llegó por la
53
+ * red (no un objeto re-serializado) — HMAC es sensible a cualquier byte de
54
+ * diferencia (orden de keys, espacios, etc). */
55
+ function verifyToolCallbackSignature(input) {
56
+ const parsed = parseSignatureHeader(input.signatureHeader);
57
+ if (!parsed) {
58
+ throw new ToolCallbackSignatureError('malformed', 'Signature header is malformed');
59
+ }
60
+ const now = input.nowSeconds ?? Math.floor(Date.now() / 1000);
61
+ if (Math.abs(now - parsed.timestamp) > MAX_AGE_SECONDS) {
62
+ throw new ToolCallbackSignatureError('stale', `Signature timestamp is more than ${MAX_AGE_SECONDS}s old or in the future`);
63
+ }
64
+ const expected = computeHmac(input.secretPlaintext, parsed.timestamp, input.rawBody);
65
+ const a = Buffer.from(parsed.v1, 'hex');
66
+ const b = Buffer.from(expected, 'hex');
67
+ if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b)) {
68
+ throw new ToolCallbackSignatureError('mismatch', 'Signature does not match');
69
+ }
70
+ }
71
+ function parseSignatureHeader(raw) {
72
+ if (!raw)
73
+ return null;
74
+ let timestamp;
75
+ let v1;
76
+ for (const part of raw.split(',').map((p) => p.trim())) {
77
+ if (part.startsWith('t='))
78
+ timestamp = Number(part.slice(2));
79
+ else if (part.startsWith('v1='))
80
+ v1 = part.slice(3);
81
+ }
82
+ if (!timestamp || !v1 || Number.isNaN(timestamp))
83
+ return null;
84
+ return { timestamp, v1 };
85
+ }
86
+ function computeHmac(secretPlaintext, timestamp, rawBody) {
87
+ return (0, node_crypto_1.createHmac)('sha256', secretPlaintext)
88
+ .update(`${timestamp}.${rawBody}`)
89
+ .digest('hex');
90
+ }