@dainprotocol/service-sdk 2.0.83 → 2.0.85
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/dist/client/client.d.ts +29 -5
- package/dist/client/client.js +226 -6
- package/dist/client/client.js.map +1 -1
- package/dist/client/types.d.ts +271 -6
- package/dist/client/types.js +128 -5
- package/dist/client/types.js.map +1 -1
- package/dist/extensions/telegram-oauth.js +1 -276
- package/dist/extensions/telegram-oauth.js.map +1 -1
- package/dist/lib/schemaStructure.js +1 -0
- package/dist/lib/schemaStructure.js.map +1 -1
- package/dist/service/auth.d.ts +2 -2
- package/dist/service/auth.js +29 -7
- package/dist/service/auth.js.map +1 -1
- package/dist/service/nodeService.js +2 -2
- package/dist/service/nodeService.js.map +1 -1
- package/dist/service/oauth2Manager.js +0 -1
- package/dist/service/oauth2Manager.js.map +1 -1
- package/dist/service/server.js +515 -86
- package/dist/service/server.js.map +1 -1
- package/dist/service/types.d.ts +132 -4
- package/dist/service/webhooks.d.ts +8 -8
- package/dist/service/webhooks.js +2 -1
- package/dist/service/webhooks.js.map +1 -1
- package/package.json +4 -4
package/dist/service/server.js
CHANGED
|
@@ -32,6 +32,21 @@ function debugWarn(...args) {
|
|
|
32
32
|
console.warn(...args);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
function getFirstForwardedValue(value) {
|
|
36
|
+
if (!value)
|
|
37
|
+
return undefined;
|
|
38
|
+
const first = value.split(",")[0]?.trim();
|
|
39
|
+
return first || undefined;
|
|
40
|
+
}
|
|
41
|
+
function normalizeForwardedPrefix(prefix) {
|
|
42
|
+
if (!prefix)
|
|
43
|
+
return undefined;
|
|
44
|
+
const trimmed = prefix.trim();
|
|
45
|
+
if (!trimmed || trimmed === "/")
|
|
46
|
+
return undefined;
|
|
47
|
+
const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
48
|
+
return normalized.replace(/\/+$/, "");
|
|
49
|
+
}
|
|
35
50
|
/**
|
|
36
51
|
* Safely parse JSON body from request, returning empty object on failure.
|
|
37
52
|
*/
|
|
@@ -63,23 +78,30 @@ function requireScope(requiredScope) {
|
|
|
63
78
|
// Helper to sign and stream SSE events
|
|
64
79
|
function signedStreamSSE(c, privateKey, config, handler) {
|
|
65
80
|
return (0, streaming_1.streamSSE)(c, async (stream) => {
|
|
81
|
+
const requestSignal = c.req.raw?.signal;
|
|
82
|
+
const isAborted = () => stream.aborted || stream.closed || requestSignal?.aborted === true;
|
|
66
83
|
const signedStream = {
|
|
67
84
|
writeSSE: async (event) => {
|
|
85
|
+
if (isAborted())
|
|
86
|
+
return;
|
|
68
87
|
const timestamp = Date.now().toString();
|
|
69
88
|
const message = `${event.data}:${timestamp}`;
|
|
70
89
|
const messageHash = (0, sha256_1.sha256)(message);
|
|
71
90
|
const signatureBytes = ed25519_1.ed25519.sign(messageHash, privateKey);
|
|
72
91
|
const signature = (0, utils_1.bytesToHex)(signatureBytes);
|
|
73
92
|
// Fast path for non-critical events (progress/UI updates)
|
|
74
|
-
const isCriticalEvent = event.event === 'result' ||
|
|
93
|
+
const isCriticalEvent = event.event === 'result' ||
|
|
94
|
+
event.event === 'process-created' ||
|
|
95
|
+
event.event === 'datasource-update';
|
|
75
96
|
const dataWithSignature = isCriticalEvent
|
|
76
97
|
? JSON.stringify({
|
|
77
98
|
data: JSON.parse(event.data),
|
|
78
99
|
_signature: { signature, timestamp, agentId: config.identity.agentId, orgId: config.identity.orgId, address: config.identity.publicKey }
|
|
79
100
|
})
|
|
80
101
|
: `{"data":${event.data},"_signature":{"signature":"${signature}","timestamp":"${timestamp}","agentId":"${config.identity.agentId}","orgId":"${config.identity.orgId}","address":"${config.identity.publicKey}"}}`;
|
|
81
|
-
|
|
82
|
-
}
|
|
102
|
+
await stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
|
|
103
|
+
},
|
|
104
|
+
isAborted,
|
|
83
105
|
};
|
|
84
106
|
await handler(signedStream);
|
|
85
107
|
});
|
|
@@ -163,7 +185,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
163
185
|
c.req.path.startsWith("/getSkills") ||
|
|
164
186
|
c.req.path.startsWith("/getWebhookTriggers") ||
|
|
165
187
|
c.req.path.startsWith("/metadata") ||
|
|
166
|
-
c.req.path.startsWith("/recommendations") ||
|
|
167
188
|
c.req.path.startsWith("/ping") ||
|
|
168
189
|
isWebhookPath ||
|
|
169
190
|
isHITLPath) {
|
|
@@ -177,8 +198,9 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
177
198
|
debugLog(`[Auth Middleware] JWT present: ${!!jwtToken}, API Key present: ${!!apiKey}`);
|
|
178
199
|
if (jwtToken) {
|
|
179
200
|
// JWT Authentication (Users)
|
|
180
|
-
// Auto-detect JWT audience from
|
|
181
|
-
const host = c.req.header("x-forwarded-host") ||
|
|
201
|
+
// Auto-detect JWT audience from forwarded headers (works with localhost, path-based tunnels, production)
|
|
202
|
+
const host = getFirstForwardedValue(c.req.header("x-forwarded-host")) ||
|
|
203
|
+
getFirstForwardedValue(c.req.header("host"));
|
|
182
204
|
if (!host) {
|
|
183
205
|
throw new http_exception_1.HTTPException(400, { message: "Unable to determine service URL. Host header is required." });
|
|
184
206
|
}
|
|
@@ -186,22 +208,33 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
186
208
|
if (host.includes(' ') || host.includes('\n') || host.includes('\r')) {
|
|
187
209
|
throw new http_exception_1.HTTPException(400, { message: "Invalid Host header format" });
|
|
188
210
|
}
|
|
189
|
-
const xForwardedProto = c.req.header("x-forwarded-proto");
|
|
190
|
-
const protocol = xForwardedProto
|
|
191
|
-
|
|
211
|
+
const xForwardedProto = getFirstForwardedValue(c.req.header("x-forwarded-proto"));
|
|
212
|
+
const protocol = xForwardedProto === "http" || xForwardedProto === "https"
|
|
213
|
+
? xForwardedProto
|
|
214
|
+
: (host.includes("localhost") || host.startsWith("127.") ? "http" : "https");
|
|
215
|
+
const forwardedPrefix = normalizeForwardedPrefix(getFirstForwardedValue(c.req.header("x-forwarded-prefix")));
|
|
216
|
+
const jwtAudiences = [`${protocol}://${host}`];
|
|
217
|
+
if (forwardedPrefix) {
|
|
218
|
+
jwtAudiences.push(`${protocol}://${host}${forwardedPrefix}`);
|
|
219
|
+
}
|
|
192
220
|
const publicKeyOrUrl = config.jwtPublicKey || config.dainIdUrl || "https://id.dain.org";
|
|
193
221
|
const issuer = config.jwtIssuer || "dainid-oauth";
|
|
194
222
|
const result = await (0, auth_1.verifyJWT)(jwtToken, publicKeyOrUrl, {
|
|
195
223
|
issuer: issuer,
|
|
196
|
-
audience:
|
|
224
|
+
audience: jwtAudiences,
|
|
197
225
|
});
|
|
198
226
|
if (!result.valid) {
|
|
199
227
|
throw new http_exception_1.HTTPException(401, { message: `JWT verification failed: ${result.error}` });
|
|
200
228
|
}
|
|
201
229
|
// Defense in depth: double-check audience claim
|
|
202
|
-
|
|
230
|
+
const tokenAudiences = Array.isArray(result.payload?.aud)
|
|
231
|
+
? result.payload.aud
|
|
232
|
+
: [result.payload?.aud];
|
|
233
|
+
const expectedAudienceSet = new Set(jwtAudiences);
|
|
234
|
+
const audienceMatched = tokenAudiences.some((aud) => aud && expectedAudienceSet.has(aud));
|
|
235
|
+
if (!audienceMatched) {
|
|
203
236
|
throw new http_exception_1.HTTPException(403, {
|
|
204
|
-
message: `JWT audience mismatch. Expected: ${
|
|
237
|
+
message: `JWT audience mismatch. Expected one of: ${jwtAudiences.join(", ")}, Got: ${tokenAudiences.filter(Boolean).join(", ")}`
|
|
205
238
|
});
|
|
206
239
|
}
|
|
207
240
|
c.set('authMethod', 'jwt');
|
|
@@ -279,9 +312,49 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
279
312
|
app.get("/metadata", (c) => {
|
|
280
313
|
// Compute service-level capability: does ANY tool support user actions (HITL)?
|
|
281
314
|
const supportsUserActions = tools.some((tool) => tool.supportsUserActions === true);
|
|
315
|
+
const requestedContract = c.req.header("x-butterfly-contract") || c.req.header("X-Butterfly-Contract");
|
|
316
|
+
const sdkMajor = Number.parseInt(String(package_json_1.default.version).split(".")[0] || "0", 10);
|
|
317
|
+
const contractVersion = Number.isFinite(sdkMajor) && sdkMajor > 0 ? `${sdkMajor}.0.0` : "0.0.0";
|
|
318
|
+
const capabilities = {
|
|
319
|
+
tools: true,
|
|
320
|
+
contexts: true,
|
|
321
|
+
widgets: true,
|
|
322
|
+
datasources: true,
|
|
323
|
+
streamingTools: true,
|
|
324
|
+
streamingDatasources: true,
|
|
325
|
+
datasourcePolicy: true,
|
|
326
|
+
widgetPolicy: true,
|
|
327
|
+
toolSafety: true,
|
|
328
|
+
};
|
|
329
|
+
const compatibility = (() => {
|
|
330
|
+
if (!requestedContract)
|
|
331
|
+
return undefined;
|
|
332
|
+
const requestedMajor = Number.parseInt(String(requestedContract).split(".")[0] || "0", 10);
|
|
333
|
+
if (!Number.isFinite(requestedMajor) || requestedMajor <= 0) {
|
|
334
|
+
return {
|
|
335
|
+
requested: requestedContract,
|
|
336
|
+
contractVersion,
|
|
337
|
+
ok: false,
|
|
338
|
+
reason: "Invalid requested contract version",
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const ok = requestedMajor === sdkMajor;
|
|
342
|
+
return ok
|
|
343
|
+
? { requested: requestedContract, contractVersion, ok: true }
|
|
344
|
+
: {
|
|
345
|
+
requested: requestedContract,
|
|
346
|
+
contractVersion,
|
|
347
|
+
ok: false,
|
|
348
|
+
reason: `Requested major ${requestedMajor} does not match service major ${sdkMajor}`,
|
|
349
|
+
};
|
|
350
|
+
})();
|
|
282
351
|
return c.json({
|
|
283
352
|
...metadata,
|
|
284
353
|
supportsUserActions,
|
|
354
|
+
dainSdkVersion: package_json_1.default.version,
|
|
355
|
+
contractVersion,
|
|
356
|
+
capabilities,
|
|
357
|
+
...(compatibility ? { compatibility } : {}),
|
|
285
358
|
});
|
|
286
359
|
});
|
|
287
360
|
// Tools list endpoint
|
|
@@ -300,6 +373,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
300
373
|
outputSchema,
|
|
301
374
|
interface: tool.interface,
|
|
302
375
|
suggestConfirmation: tool.suggestConfirmation,
|
|
376
|
+
sideEffectClass: tool.sideEffectClass,
|
|
377
|
+
supportsParallel: tool.supportsParallel,
|
|
378
|
+
idempotencyScope: tool.idempotencyScope,
|
|
379
|
+
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
303
380
|
supportsUserActions: tool.supportsUserActions,
|
|
304
381
|
};
|
|
305
382
|
});
|
|
@@ -400,14 +477,26 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
400
477
|
oauth2Client,
|
|
401
478
|
app
|
|
402
479
|
});
|
|
403
|
-
|
|
480
|
+
const response = {
|
|
404
481
|
id: widget.id,
|
|
405
482
|
name: widget.name,
|
|
406
483
|
description: widget.description,
|
|
407
484
|
icon: widget.icon,
|
|
408
485
|
size: widget.size || "sm",
|
|
486
|
+
refreshIntervalMs: widget.refreshIntervalMs,
|
|
409
487
|
...widgetData
|
|
410
488
|
};
|
|
489
|
+
if (!("freshness" in response)) {
|
|
490
|
+
response.freshness = {
|
|
491
|
+
freshAt: Date.now(),
|
|
492
|
+
transport: "poll",
|
|
493
|
+
requestedPolicy: {
|
|
494
|
+
refreshIntervalMs: widget.refreshIntervalMs,
|
|
495
|
+
},
|
|
496
|
+
scope: "account",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return response;
|
|
411
500
|
}));
|
|
412
501
|
const validWidgets = widgetsFull.filter(w => w !== null);
|
|
413
502
|
const processedResponse = await processPluginsForResponse(validWidgets, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
@@ -448,8 +537,19 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
448
537
|
description: widget.description,
|
|
449
538
|
icon: widget.icon,
|
|
450
539
|
size: widget.size || "sm",
|
|
540
|
+
refreshIntervalMs: widget.refreshIntervalMs,
|
|
451
541
|
...widgetData
|
|
452
542
|
};
|
|
543
|
+
if (!("freshness" in response)) {
|
|
544
|
+
response.freshness = {
|
|
545
|
+
freshAt: Date.now(),
|
|
546
|
+
transport: "poll",
|
|
547
|
+
requestedPolicy: {
|
|
548
|
+
refreshIntervalMs: widget.refreshIntervalMs,
|
|
549
|
+
},
|
|
550
|
+
scope: "account",
|
|
551
|
+
};
|
|
552
|
+
}
|
|
453
553
|
const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
454
554
|
return c.json(processedResponse);
|
|
455
555
|
});
|
|
@@ -479,6 +579,12 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
479
579
|
name: datasource.name,
|
|
480
580
|
description: datasource.description,
|
|
481
581
|
type: datasource.type,
|
|
582
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
583
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
584
|
+
transport: datasource.transport,
|
|
585
|
+
priority: datasource.priority,
|
|
586
|
+
scope: datasource.scope,
|
|
587
|
+
dataClass: datasource.dataClass,
|
|
482
588
|
inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(datasource.input),
|
|
483
589
|
};
|
|
484
590
|
}
|
|
@@ -516,12 +622,32 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
516
622
|
plugins: pluginsData,
|
|
517
623
|
oauth2Client
|
|
518
624
|
});
|
|
625
|
+
const requestedPolicy = {
|
|
626
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
627
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
628
|
+
transport: datasource.transport,
|
|
629
|
+
priority: datasource.priority,
|
|
630
|
+
scope: datasource.scope,
|
|
631
|
+
dataClass: datasource.dataClass,
|
|
632
|
+
};
|
|
519
633
|
const response = {
|
|
520
634
|
id: datasource.id,
|
|
521
635
|
name: datasource.name,
|
|
522
636
|
description: datasource.description,
|
|
523
637
|
type: datasource.type,
|
|
638
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
639
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
640
|
+
transport: datasource.transport,
|
|
641
|
+
priority: datasource.priority,
|
|
642
|
+
scope: datasource.scope,
|
|
643
|
+
dataClass: datasource.dataClass,
|
|
524
644
|
data,
|
|
645
|
+
freshness: {
|
|
646
|
+
freshAt: Date.now(),
|
|
647
|
+
transport: datasource.transport || "poll",
|
|
648
|
+
requestedPolicy,
|
|
649
|
+
scope: datasource.scope,
|
|
650
|
+
},
|
|
525
651
|
};
|
|
526
652
|
const processedResponse = await processPluginsForResponse(response, { plugins: pluginsData }, { extraData: { plugins: pluginsData } });
|
|
527
653
|
return c.json(processedResponse);
|
|
@@ -539,6 +665,149 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
539
665
|
throw error;
|
|
540
666
|
}
|
|
541
667
|
});
|
|
668
|
+
// Stream datasource updates over SSE. This is primarily used for "fresh" UIs
|
|
669
|
+
// (positions/orders) where clients want stream-first updates with a poll fallback.
|
|
670
|
+
//
|
|
671
|
+
// Note: This does not require services to implement a separate streaming backend.
|
|
672
|
+
// The server re-runs the datasource handler on an interval and streams results.
|
|
673
|
+
app.post("/datasources/:datasourceId/stream", async (c) => {
|
|
674
|
+
const datasource = datasources.find((ds) => ds.id === c.req.param("datasourceId"));
|
|
675
|
+
if (!datasource) {
|
|
676
|
+
throw new http_exception_1.HTTPException(404, { message: "Datasource not found" });
|
|
677
|
+
}
|
|
678
|
+
const agentInfo = await getAgentInfo(c);
|
|
679
|
+
const rawBody = await safeParseBody(c);
|
|
680
|
+
const hasWrappedParams = rawBody &&
|
|
681
|
+
typeof rawBody === "object" &&
|
|
682
|
+
!Array.isArray(rawBody) &&
|
|
683
|
+
"params" in rawBody;
|
|
684
|
+
const requestedIntervalMsRaw = hasWrappedParams ? rawBody.intervalMs : undefined;
|
|
685
|
+
const requestParamsRaw = hasWrappedParams ? rawBody.params : rawBody;
|
|
686
|
+
let params = await processPluginsForRequest(requestParamsRaw && typeof requestParamsRaw === "object" ? requestParamsRaw : {}, agentInfo);
|
|
687
|
+
const pluginsData = (params.plugins && typeof params.plugins === "object"
|
|
688
|
+
? params.plugins
|
|
689
|
+
: {});
|
|
690
|
+
delete params.plugins;
|
|
691
|
+
let parsedParams;
|
|
692
|
+
try {
|
|
693
|
+
parsedParams = datasource.input.parse(params);
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
if (error instanceof zod_1.z.ZodError) {
|
|
697
|
+
const missingParams = error.issues
|
|
698
|
+
.map((issue) => issue.path.join("."))
|
|
699
|
+
.join(", ");
|
|
700
|
+
return c.json({
|
|
701
|
+
error: `Missing or invalid parameters: ${missingParams}`,
|
|
702
|
+
code: "INVALID_PARAMS"
|
|
703
|
+
}, 400);
|
|
704
|
+
}
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
const pluginRequestContext = hasWrappedParams
|
|
708
|
+
? {
|
|
709
|
+
params: parsedParams,
|
|
710
|
+
intervalMs: requestedIntervalMsRaw,
|
|
711
|
+
plugins: pluginsData,
|
|
712
|
+
}
|
|
713
|
+
: {
|
|
714
|
+
...(typeof parsedParams === "object" && parsedParams !== null
|
|
715
|
+
? parsedParams
|
|
716
|
+
: {}),
|
|
717
|
+
plugins: pluginsData,
|
|
718
|
+
};
|
|
719
|
+
const oauth2Client = app.oauth2?.getClient();
|
|
720
|
+
const requestedPolicy = {
|
|
721
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
722
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
723
|
+
transport: datasource.transport,
|
|
724
|
+
priority: datasource.priority,
|
|
725
|
+
scope: datasource.scope,
|
|
726
|
+
dataClass: datasource.dataClass,
|
|
727
|
+
};
|
|
728
|
+
const requestedIntervalMs = typeof requestedIntervalMsRaw === "number" && Number.isFinite(requestedIntervalMsRaw) && requestedIntervalMsRaw > 0
|
|
729
|
+
? requestedIntervalMsRaw
|
|
730
|
+
: null;
|
|
731
|
+
const baseIntervalMs = requestedIntervalMs ??
|
|
732
|
+
(typeof datasource.refreshIntervalMs === "number" && datasource.refreshIntervalMs > 0
|
|
733
|
+
? datasource.refreshIntervalMs
|
|
734
|
+
: 15_000);
|
|
735
|
+
const intervalMs = Math.max(1_000, Math.floor(baseIntervalMs));
|
|
736
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
737
|
+
debugLog(`[SSE] Datasource ${datasource.id} stream start (${intervalMs}ms interval)`);
|
|
738
|
+
return signedStreamSSE(c, privateKey, config, async (stream) => {
|
|
739
|
+
let eventId = 0;
|
|
740
|
+
const emitUpdate = async () => {
|
|
741
|
+
const data = await datasource.getDatasource(agentInfo, parsedParams, {
|
|
742
|
+
plugins: pluginsData,
|
|
743
|
+
oauth2Client,
|
|
744
|
+
});
|
|
745
|
+
const response = {
|
|
746
|
+
id: datasource.id,
|
|
747
|
+
name: datasource.name,
|
|
748
|
+
description: datasource.description,
|
|
749
|
+
type: datasource.type,
|
|
750
|
+
refreshIntervalMs: datasource.refreshIntervalMs,
|
|
751
|
+
maxStalenessMs: datasource.maxStalenessMs,
|
|
752
|
+
transport: datasource.transport,
|
|
753
|
+
priority: datasource.priority,
|
|
754
|
+
scope: datasource.scope,
|
|
755
|
+
dataClass: datasource.dataClass,
|
|
756
|
+
data,
|
|
757
|
+
freshness: {
|
|
758
|
+
freshAt: Date.now(),
|
|
759
|
+
transport: "stream",
|
|
760
|
+
requestedPolicy,
|
|
761
|
+
scope: datasource.scope,
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
const processedResponse = await processPluginsForResponse(response, pluginRequestContext, { extraData: { plugins: pluginsData } });
|
|
765
|
+
await stream.writeSSE({
|
|
766
|
+
event: "datasource-update",
|
|
767
|
+
data: JSON.stringify(processedResponse),
|
|
768
|
+
id: String(eventId++),
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
// Send initial snapshot immediately.
|
|
772
|
+
try {
|
|
773
|
+
await emitUpdate();
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
console.error(`[SSE] Datasource ${datasource.id} initial update failed:`, error);
|
|
777
|
+
if (!stream.isAborted()) {
|
|
778
|
+
await stream.writeSSE({
|
|
779
|
+
event: "error",
|
|
780
|
+
data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
// Continue sending updates until client disconnects.
|
|
786
|
+
// Hono exposes the underlying Request via c.req.raw, which includes an AbortSignal.
|
|
787
|
+
const signal = c.req.raw?.signal;
|
|
788
|
+
while (!stream.isAborted() && !signal?.aborted) {
|
|
789
|
+
await sleep(intervalMs);
|
|
790
|
+
if (stream.isAborted() || signal?.aborted)
|
|
791
|
+
break;
|
|
792
|
+
try {
|
|
793
|
+
await emitUpdate();
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
if (stream.isAborted() || signal?.aborted)
|
|
797
|
+
break;
|
|
798
|
+
console.error(`[SSE] Datasource ${datasource.id} update failed:`, error);
|
|
799
|
+
// Keep the stream alive; clients should rely on poll fallback if needed.
|
|
800
|
+
if (!stream.isAborted()) {
|
|
801
|
+
await stream.writeSSE({
|
|
802
|
+
event: "error",
|
|
803
|
+
data: JSON.stringify({ message: error?.message || "Datasource stream error" }),
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
debugLog(`[SSE] Datasource ${datasource.id} stream end`);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
542
811
|
function mapAgentInfo(agent) {
|
|
543
812
|
return {
|
|
544
813
|
id: agent.id,
|
|
@@ -615,6 +884,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
615
884
|
outputSchema,
|
|
616
885
|
interface: tool.interface,
|
|
617
886
|
suggestConfirmation: tool.suggestConfirmation,
|
|
887
|
+
sideEffectClass: tool.sideEffectClass,
|
|
888
|
+
supportsParallel: tool.supportsParallel,
|
|
889
|
+
idempotencyScope: tool.idempotencyScope,
|
|
890
|
+
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
618
891
|
supportsUserActions: tool.supportsUserActions,
|
|
619
892
|
};
|
|
620
893
|
}
|
|
@@ -819,6 +1092,10 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
819
1092
|
outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
|
|
820
1093
|
interface: tool.interface,
|
|
821
1094
|
suggestConfirmation: tool.suggestConfirmation,
|
|
1095
|
+
sideEffectClass: tool.sideEffectClass,
|
|
1096
|
+
supportsParallel: tool.supportsParallel,
|
|
1097
|
+
idempotencyScope: tool.idempotencyScope,
|
|
1098
|
+
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
822
1099
|
};
|
|
823
1100
|
return c.json(toolDetails);
|
|
824
1101
|
}
|
|
@@ -846,6 +1123,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
846
1123
|
pricing: tool.pricing,
|
|
847
1124
|
inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.input),
|
|
848
1125
|
outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
|
|
1126
|
+
suggestConfirmation: tool.suggestConfirmation,
|
|
1127
|
+
sideEffectClass: tool.sideEffectClass,
|
|
1128
|
+
supportsParallel: tool.supportsParallel,
|
|
1129
|
+
idempotencyScope: tool.idempotencyScope,
|
|
1130
|
+
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
849
1131
|
}
|
|
850
1132
|
: {
|
|
851
1133
|
id: toolId,
|
|
@@ -924,31 +1206,11 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
924
1206
|
}
|
|
925
1207
|
try {
|
|
926
1208
|
await app.oauth2.handleCallback(code, state);
|
|
927
|
-
return c.html(
|
|
928
|
-
<html>
|
|
929
|
-
<body>
|
|
930
|
-
<script>
|
|
931
|
-
window.opener.postMessage({ type: 'oauth2-success', provider: '${provider}' }, '*');
|
|
932
|
-
window.close();
|
|
933
|
-
</script>
|
|
934
|
-
<h1>Authentication successful! You can close this window.</h1>
|
|
935
|
-
</body>
|
|
936
|
-
</html>
|
|
937
|
-
`);
|
|
1209
|
+
return c.html("<html><body><script>window.opener.postMessage({ type: 'oauth2-success', provider: '" + provider + "' }, '*');window.close();</script><h1>Authentication successful! You can close this window.</h1></body></html>");
|
|
938
1210
|
}
|
|
939
1211
|
catch (error) {
|
|
940
1212
|
console.error("OAuth callback error:", error);
|
|
941
|
-
return c.html(
|
|
942
|
-
<html>
|
|
943
|
-
<body>
|
|
944
|
-
<script>
|
|
945
|
-
window.opener.postMessage({ type: 'oauth2-error', provider: '${provider}', error: '${error.message}' }, '*');
|
|
946
|
-
window.close();
|
|
947
|
-
</script>
|
|
948
|
-
<h1>Authentication failed! You can close this window.</h1>
|
|
949
|
-
</body>
|
|
950
|
-
</html>
|
|
951
|
-
`);
|
|
1213
|
+
return c.html("<html><body><script>window.opener.postMessage({ type: 'oauth2-error', provider: '" + provider + "', error: '" + error.message + "' }, '*');window.close();</script><h1>Authentication failed! You can close this window.</h1></body></html>");
|
|
952
1214
|
}
|
|
953
1215
|
});
|
|
954
1216
|
// Make oauth2Handler available to tools
|
|
@@ -1175,64 +1437,231 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
1175
1437
|
});
|
|
1176
1438
|
// Toolboxes list endpoint
|
|
1177
1439
|
app.get("/toolboxes", (c) => c.json(toolboxes));
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1440
|
+
function normalizeRecommendationText(value) {
|
|
1441
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
1442
|
+
}
|
|
1443
|
+
function tokenizeRecommendationQuery(query) {
|
|
1444
|
+
const normalized = normalizeRecommendationText(query);
|
|
1445
|
+
if (!normalized)
|
|
1446
|
+
return [];
|
|
1447
|
+
return Array.from(new Set(normalized.split(/\s+/).filter((token) => token.length >= 2)));
|
|
1448
|
+
}
|
|
1449
|
+
function buildRecommendationIndex() {
|
|
1450
|
+
const index = new Map();
|
|
1181
1451
|
for (const tool of tools) {
|
|
1182
|
-
if (tool.recommendations?.length)
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
ui: hasDynamicSchema ? undefined : card.ui,
|
|
1189
|
-
inputSchema: hasDynamicSchema ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined,
|
|
1190
|
-
toolId: tool.id,
|
|
1191
|
-
toolName: tool.name,
|
|
1192
|
-
});
|
|
1452
|
+
if (!tool.recommendations?.length)
|
|
1453
|
+
continue;
|
|
1454
|
+
for (const card of tool.recommendations) {
|
|
1455
|
+
const scopedCardId = `${tool.id}:${card.id}`;
|
|
1456
|
+
if (index.has(scopedCardId)) {
|
|
1457
|
+
throw new Error(`Duplicate recommendation card ID "${scopedCardId}". Card IDs must be unique per tool.`);
|
|
1193
1458
|
}
|
|
1459
|
+
const isDynamic = !!card.inputSchema;
|
|
1460
|
+
const inputSchema = isDynamic ? (0, schemaStructure_1.zodToJsonSchema)(card.inputSchema) : undefined;
|
|
1461
|
+
const searchText = normalizeRecommendationText([tool.id, tool.name, card.id, ...card.tags].join(" "));
|
|
1462
|
+
index.set(scopedCardId, {
|
|
1463
|
+
cardId: scopedCardId,
|
|
1464
|
+
localCardId: card.id,
|
|
1465
|
+
toolId: tool.id,
|
|
1466
|
+
toolName: tool.name,
|
|
1467
|
+
tags: card.tags,
|
|
1468
|
+
inputSchema,
|
|
1469
|
+
ui: isDynamic ? undefined : card.ui,
|
|
1470
|
+
isDynamic,
|
|
1471
|
+
card,
|
|
1472
|
+
searchText,
|
|
1473
|
+
});
|
|
1194
1474
|
}
|
|
1195
1475
|
}
|
|
1196
|
-
return
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
const
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1476
|
+
return index;
|
|
1477
|
+
}
|
|
1478
|
+
function scoreRecommendationCard(card, tokens) {
|
|
1479
|
+
if (tokens.length === 0)
|
|
1480
|
+
return 0;
|
|
1481
|
+
let score = 0;
|
|
1482
|
+
const tagSet = new Set(card.tags.map((tag) => normalizeRecommendationText(tag)));
|
|
1483
|
+
const scopedId = normalizeRecommendationText(card.cardId);
|
|
1484
|
+
const toolName = normalizeRecommendationText(card.toolName);
|
|
1485
|
+
for (const token of tokens) {
|
|
1486
|
+
if (scopedId === token || card.localCardId.toLowerCase() === token) {
|
|
1487
|
+
score += 6;
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (tagSet.has(token)) {
|
|
1491
|
+
score += 4;
|
|
1492
|
+
}
|
|
1493
|
+
if (toolName.includes(token)) {
|
|
1494
|
+
score += 3;
|
|
1495
|
+
}
|
|
1496
|
+
if (card.searchText.includes(token)) {
|
|
1497
|
+
score += 1;
|
|
1212
1498
|
}
|
|
1213
1499
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1500
|
+
return score;
|
|
1501
|
+
}
|
|
1502
|
+
const recommendationCardsById = buildRecommendationIndex();
|
|
1503
|
+
const recommendationSearchSchema = zod_1.z.object({
|
|
1504
|
+
query: zod_1.z.string().optional(),
|
|
1505
|
+
limit: zod_1.z.number().int().min(1).max(50).optional().default(12),
|
|
1506
|
+
toolIds: zod_1.z.array(zod_1.z.string()).optional(),
|
|
1507
|
+
includeStaticUI: zod_1.z.boolean().optional().default(false),
|
|
1508
|
+
});
|
|
1509
|
+
app.post("/recommendations/search", async (c) => {
|
|
1510
|
+
const agentInfo = await getAgentInfo(c);
|
|
1511
|
+
const body = await safeParseBody(c);
|
|
1512
|
+
const processedPluginData = await processPluginsForRequest(body, agentInfo);
|
|
1513
|
+
const requestData = { ...processedPluginData };
|
|
1514
|
+
delete requestData.plugins;
|
|
1515
|
+
const parsed = recommendationSearchSchema.safeParse(requestData);
|
|
1516
|
+
if (!parsed.success) {
|
|
1517
|
+
return c.json({
|
|
1518
|
+
error: "Invalid recommendation search request",
|
|
1519
|
+
details: parsed.error.format(),
|
|
1520
|
+
}, 400);
|
|
1521
|
+
}
|
|
1522
|
+
const { query, limit, toolIds, includeStaticUI, } = parsed.data;
|
|
1523
|
+
const toolIdSet = toolIds?.length ? new Set(toolIds) : null;
|
|
1524
|
+
const filteredCards = Array.from(recommendationCardsById.values()).filter((card) => !toolIdSet || toolIdSet.has(card.toolId));
|
|
1525
|
+
const tokens = tokenizeRecommendationQuery(query ?? "");
|
|
1526
|
+
const ranked = filteredCards
|
|
1527
|
+
.map((card, index) => ({
|
|
1528
|
+
...card,
|
|
1529
|
+
score: scoreRecommendationCard(card, tokens),
|
|
1530
|
+
index,
|
|
1531
|
+
}))
|
|
1532
|
+
.sort((a, b) => {
|
|
1533
|
+
if (tokens.length === 0)
|
|
1534
|
+
return a.index - b.index;
|
|
1535
|
+
if (b.score !== a.score)
|
|
1536
|
+
return b.score - a.score;
|
|
1537
|
+
if (a.toolName !== b.toolName)
|
|
1538
|
+
return a.toolName.localeCompare(b.toolName);
|
|
1539
|
+
return a.localCardId.localeCompare(b.localCardId);
|
|
1540
|
+
})
|
|
1541
|
+
.slice(0, limit);
|
|
1542
|
+
const cards = ranked.map((card) => ({
|
|
1543
|
+
cardId: card.cardId,
|
|
1544
|
+
localCardId: card.localCardId,
|
|
1545
|
+
toolId: card.toolId,
|
|
1546
|
+
toolName: card.toolName,
|
|
1547
|
+
tags: card.tags,
|
|
1548
|
+
score: card.score,
|
|
1549
|
+
inputSchema: card.inputSchema,
|
|
1550
|
+
ui: includeStaticUI && !card.isDynamic ? card.ui : undefined,
|
|
1551
|
+
isDynamic: card.isDynamic,
|
|
1552
|
+
}));
|
|
1553
|
+
const response = {
|
|
1554
|
+
cards,
|
|
1555
|
+
count: cards.length,
|
|
1556
|
+
query,
|
|
1557
|
+
};
|
|
1558
|
+
const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
1559
|
+
return c.json(processedResponse);
|
|
1560
|
+
});
|
|
1561
|
+
const recommendationRenderSchema = zod_1.z.object({
|
|
1562
|
+
cards: zod_1.z
|
|
1563
|
+
.array(zod_1.z.object({
|
|
1564
|
+
cardId: zod_1.z.string().min(1),
|
|
1565
|
+
params: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
1566
|
+
context: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
1567
|
+
}))
|
|
1568
|
+
.min(1)
|
|
1569
|
+
.max(8),
|
|
1570
|
+
continueOnError: zod_1.z.boolean().optional().default(true),
|
|
1571
|
+
});
|
|
1572
|
+
app.post("/recommendations/render", async (c) => {
|
|
1573
|
+
const agentInfo = await getAgentInfo(c);
|
|
1574
|
+
const body = await safeParseBody(c);
|
|
1575
|
+
const processedPluginData = await processPluginsForRequest(body, agentInfo);
|
|
1576
|
+
const requestData = { ...processedPluginData };
|
|
1577
|
+
delete requestData.plugins;
|
|
1578
|
+
const parsed = recommendationRenderSchema.safeParse(requestData);
|
|
1579
|
+
if (!parsed.success) {
|
|
1580
|
+
return c.json({
|
|
1581
|
+
error: "Invalid recommendation render request",
|
|
1582
|
+
details: parsed.error.format(),
|
|
1583
|
+
}, 400);
|
|
1584
|
+
}
|
|
1585
|
+
const { cards: requestedCards, continueOnError } = parsed.data;
|
|
1586
|
+
const renderedCards = [];
|
|
1587
|
+
const renderErrors = [];
|
|
1588
|
+
for (const requested of requestedCards) {
|
|
1589
|
+
const card = recommendationCardsById.get(requested.cardId);
|
|
1590
|
+
if (!card) {
|
|
1591
|
+
renderErrors.push({
|
|
1592
|
+
cardId: requested.cardId,
|
|
1593
|
+
code: "not_found",
|
|
1594
|
+
message: `Card not found: ${requested.cardId}`,
|
|
1595
|
+
});
|
|
1596
|
+
if (!continueOnError)
|
|
1597
|
+
break;
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
if (!card.isDynamic) {
|
|
1601
|
+
renderedCards.push({
|
|
1602
|
+
cardId: card.cardId,
|
|
1603
|
+
localCardId: card.localCardId,
|
|
1604
|
+
toolId: card.toolId,
|
|
1605
|
+
toolName: card.toolName,
|
|
1606
|
+
tags: card.tags,
|
|
1607
|
+
ui: card.card.ui,
|
|
1608
|
+
});
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const paramsInput = requested.params ?? {};
|
|
1612
|
+
const parseResult = card.card.inputSchema.safeParse(paramsInput);
|
|
1613
|
+
if (!parseResult.success) {
|
|
1614
|
+
renderErrors.push({
|
|
1615
|
+
cardId: card.cardId,
|
|
1616
|
+
code: "invalid_params",
|
|
1617
|
+
message: "Invalid parameters",
|
|
1618
|
+
details: parseResult.error.format(),
|
|
1619
|
+
});
|
|
1620
|
+
if (!continueOnError)
|
|
1621
|
+
break;
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (typeof card.card.ui !== "function") {
|
|
1625
|
+
renderErrors.push({
|
|
1626
|
+
cardId: card.cardId,
|
|
1627
|
+
code: "render_failed",
|
|
1628
|
+
message: `Card "${card.cardId}" is dynamic but has no UI generator function`,
|
|
1629
|
+
});
|
|
1630
|
+
if (!continueOnError)
|
|
1631
|
+
break;
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
const ui = await card.card.ui(parseResult.data, requested.context ?? {});
|
|
1636
|
+
renderedCards.push({
|
|
1637
|
+
cardId: card.cardId,
|
|
1638
|
+
localCardId: card.localCardId,
|
|
1639
|
+
toolId: card.toolId,
|
|
1640
|
+
toolName: card.toolName,
|
|
1641
|
+
tags: card.tags,
|
|
1642
|
+
ui,
|
|
1643
|
+
params: parseResult.data,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
catch (error) {
|
|
1647
|
+
renderErrors.push({
|
|
1648
|
+
cardId: card.cardId,
|
|
1649
|
+
code: "render_failed",
|
|
1650
|
+
message: error instanceof Error ? error.message : "Failed to render recommendation card",
|
|
1651
|
+
});
|
|
1652
|
+
if (!continueOnError)
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1235
1655
|
}
|
|
1656
|
+
const response = {
|
|
1657
|
+
cards: renderedCards,
|
|
1658
|
+
errors: renderErrors,
|
|
1659
|
+
renderedCount: renderedCards.length,
|
|
1660
|
+
requestedCount: requestedCards.length,
|
|
1661
|
+
};
|
|
1662
|
+
const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
1663
|
+
const statusCode = !continueOnError && renderErrors.length > 0 ? 400 : 200;
|
|
1664
|
+
return c.json(processedResponse, statusCode);
|
|
1236
1665
|
});
|
|
1237
1666
|
// Process request with plugins
|
|
1238
1667
|
async function processPluginsForRequest(request, agentInfo) {
|