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