@dainprotocol/service-sdk 2.0.85 → 2.0.87
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 +5 -29
- package/dist/client/client.js +6 -226
- package/dist/client/client.js.map +1 -1
- package/dist/client/types.d.ts +6 -271
- package/dist/client/types.js +5 -128
- package/dist/client/types.js.map +1 -1
- package/dist/extensions/telegram-oauth.js +276 -1
- package/dist/extensions/telegram-oauth.js.map +1 -1
- package/dist/lib/schemaStructure.js +0 -1
- package/dist/lib/schemaStructure.js.map +1 -1
- package/dist/service/auth.d.ts +2 -2
- package/dist/service/auth.js +7 -29
- 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 +1 -0
- package/dist/service/oauth2Manager.js.map +1 -1
- package/dist/service/server.js +86 -515
- package/dist/service/server.js.map +1 -1
- package/dist/service/types.d.ts +4 -132
- package/dist/service/webhooks.d.ts +8 -8
- package/dist/service/webhooks.js +1 -2
- package/dist/service/webhooks.js.map +1 -1
- package/package.json +13 -14
package/dist/service/server.js
CHANGED
|
@@ -32,21 +32,6 @@ 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
|
-
}
|
|
50
35
|
/**
|
|
51
36
|
* Safely parse JSON body from request, returning empty object on failure.
|
|
52
37
|
*/
|
|
@@ -78,30 +63,23 @@ function requireScope(requiredScope) {
|
|
|
78
63
|
// Helper to sign and stream SSE events
|
|
79
64
|
function signedStreamSSE(c, privateKey, config, handler) {
|
|
80
65
|
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;
|
|
83
66
|
const signedStream = {
|
|
84
67
|
writeSSE: async (event) => {
|
|
85
|
-
if (isAborted())
|
|
86
|
-
return;
|
|
87
68
|
const timestamp = Date.now().toString();
|
|
88
69
|
const message = `${event.data}:${timestamp}`;
|
|
89
70
|
const messageHash = (0, sha256_1.sha256)(message);
|
|
90
71
|
const signatureBytes = ed25519_1.ed25519.sign(messageHash, privateKey);
|
|
91
72
|
const signature = (0, utils_1.bytesToHex)(signatureBytes);
|
|
92
73
|
// Fast path for non-critical events (progress/UI updates)
|
|
93
|
-
const isCriticalEvent = event.event === 'result' ||
|
|
94
|
-
event.event === 'process-created' ||
|
|
95
|
-
event.event === 'datasource-update';
|
|
74
|
+
const isCriticalEvent = event.event === 'result' || event.event === 'process-created';
|
|
96
75
|
const dataWithSignature = isCriticalEvent
|
|
97
76
|
? JSON.stringify({
|
|
98
77
|
data: JSON.parse(event.data),
|
|
99
78
|
_signature: { signature, timestamp, agentId: config.identity.agentId, orgId: config.identity.orgId, address: config.identity.publicKey }
|
|
100
79
|
})
|
|
101
80
|
: `{"data":${event.data},"_signature":{"signature":"${signature}","timestamp":"${timestamp}","agentId":"${config.identity.agentId}","orgId":"${config.identity.orgId}","address":"${config.identity.publicKey}"}}`;
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
isAborted,
|
|
81
|
+
return stream.writeSSE({ event: event.event, data: dataWithSignature, id: event.id });
|
|
82
|
+
}
|
|
105
83
|
};
|
|
106
84
|
await handler(signedStream);
|
|
107
85
|
});
|
|
@@ -185,6 +163,7 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
185
163
|
c.req.path.startsWith("/getSkills") ||
|
|
186
164
|
c.req.path.startsWith("/getWebhookTriggers") ||
|
|
187
165
|
c.req.path.startsWith("/metadata") ||
|
|
166
|
+
c.req.path.startsWith("/recommendations") ||
|
|
188
167
|
c.req.path.startsWith("/ping") ||
|
|
189
168
|
isWebhookPath ||
|
|
190
169
|
isHITLPath) {
|
|
@@ -198,9 +177,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
198
177
|
debugLog(`[Auth Middleware] JWT present: ${!!jwtToken}, API Key present: ${!!apiKey}`);
|
|
199
178
|
if (jwtToken) {
|
|
200
179
|
// JWT Authentication (Users)
|
|
201
|
-
// Auto-detect JWT audience from
|
|
202
|
-
const host =
|
|
203
|
-
getFirstForwardedValue(c.req.header("host"));
|
|
180
|
+
// Auto-detect JWT audience from Host header (works with localhost, tunnels, production)
|
|
181
|
+
const host = c.req.header("x-forwarded-host") || c.req.header("host");
|
|
204
182
|
if (!host) {
|
|
205
183
|
throw new http_exception_1.HTTPException(400, { message: "Unable to determine service URL. Host header is required." });
|
|
206
184
|
}
|
|
@@ -208,33 +186,22 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
208
186
|
if (host.includes(' ') || host.includes('\n') || host.includes('\r')) {
|
|
209
187
|
throw new http_exception_1.HTTPException(400, { message: "Invalid Host header format" });
|
|
210
188
|
}
|
|
211
|
-
const xForwardedProto =
|
|
212
|
-
const protocol = xForwardedProto
|
|
213
|
-
|
|
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
|
-
}
|
|
189
|
+
const xForwardedProto = c.req.header("x-forwarded-proto");
|
|
190
|
+
const protocol = xForwardedProto || (host.includes("localhost") ? "http" : "https");
|
|
191
|
+
const jwtAudience = `${protocol}://${host}`;
|
|
220
192
|
const publicKeyOrUrl = config.jwtPublicKey || config.dainIdUrl || "https://id.dain.org";
|
|
221
193
|
const issuer = config.jwtIssuer || "dainid-oauth";
|
|
222
194
|
const result = await (0, auth_1.verifyJWT)(jwtToken, publicKeyOrUrl, {
|
|
223
195
|
issuer: issuer,
|
|
224
|
-
audience:
|
|
196
|
+
audience: jwtAudience,
|
|
225
197
|
});
|
|
226
198
|
if (!result.valid) {
|
|
227
199
|
throw new http_exception_1.HTTPException(401, { message: `JWT verification failed: ${result.error}` });
|
|
228
200
|
}
|
|
229
201
|
// Defense in depth: double-check audience claim
|
|
230
|
-
|
|
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) {
|
|
202
|
+
if (result.payload.aud !== jwtAudience) {
|
|
236
203
|
throw new http_exception_1.HTTPException(403, {
|
|
237
|
-
message: `JWT audience mismatch. Expected
|
|
204
|
+
message: `JWT audience mismatch. Expected: ${jwtAudience}, Got: ${result.payload.aud}`
|
|
238
205
|
});
|
|
239
206
|
}
|
|
240
207
|
c.set('authMethod', 'jwt');
|
|
@@ -312,49 +279,9 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
312
279
|
app.get("/metadata", (c) => {
|
|
313
280
|
// Compute service-level capability: does ANY tool support user actions (HITL)?
|
|
314
281
|
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
|
-
})();
|
|
351
282
|
return c.json({
|
|
352
283
|
...metadata,
|
|
353
284
|
supportsUserActions,
|
|
354
|
-
dainSdkVersion: package_json_1.default.version,
|
|
355
|
-
contractVersion,
|
|
356
|
-
capabilities,
|
|
357
|
-
...(compatibility ? { compatibility } : {}),
|
|
358
285
|
});
|
|
359
286
|
});
|
|
360
287
|
// Tools list endpoint
|
|
@@ -373,10 +300,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
373
300
|
outputSchema,
|
|
374
301
|
interface: tool.interface,
|
|
375
302
|
suggestConfirmation: tool.suggestConfirmation,
|
|
376
|
-
sideEffectClass: tool.sideEffectClass,
|
|
377
|
-
supportsParallel: tool.supportsParallel,
|
|
378
|
-
idempotencyScope: tool.idempotencyScope,
|
|
379
|
-
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
380
303
|
supportsUserActions: tool.supportsUserActions,
|
|
381
304
|
};
|
|
382
305
|
});
|
|
@@ -477,26 +400,14 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
477
400
|
oauth2Client,
|
|
478
401
|
app
|
|
479
402
|
});
|
|
480
|
-
|
|
403
|
+
return {
|
|
481
404
|
id: widget.id,
|
|
482
405
|
name: widget.name,
|
|
483
406
|
description: widget.description,
|
|
484
407
|
icon: widget.icon,
|
|
485
408
|
size: widget.size || "sm",
|
|
486
|
-
refreshIntervalMs: widget.refreshIntervalMs,
|
|
487
409
|
...widgetData
|
|
488
410
|
};
|
|
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;
|
|
500
411
|
}));
|
|
501
412
|
const validWidgets = widgetsFull.filter(w => w !== null);
|
|
502
413
|
const processedResponse = await processPluginsForResponse(validWidgets, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
@@ -537,19 +448,8 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
537
448
|
description: widget.description,
|
|
538
449
|
icon: widget.icon,
|
|
539
450
|
size: widget.size || "sm",
|
|
540
|
-
refreshIntervalMs: widget.refreshIntervalMs,
|
|
541
451
|
...widgetData
|
|
542
452
|
};
|
|
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
|
-
}
|
|
553
453
|
const processedResponse = await processPluginsForResponse(response, body, { extraData: { plugins: processedPluginData.plugins } });
|
|
554
454
|
return c.json(processedResponse);
|
|
555
455
|
});
|
|
@@ -579,12 +479,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
579
479
|
name: datasource.name,
|
|
580
480
|
description: datasource.description,
|
|
581
481
|
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,
|
|
588
482
|
inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(datasource.input),
|
|
589
483
|
};
|
|
590
484
|
}
|
|
@@ -622,32 +516,12 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
622
516
|
plugins: pluginsData,
|
|
623
517
|
oauth2Client
|
|
624
518
|
});
|
|
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
|
-
};
|
|
633
519
|
const response = {
|
|
634
520
|
id: datasource.id,
|
|
635
521
|
name: datasource.name,
|
|
636
522
|
description: datasource.description,
|
|
637
523
|
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,
|
|
644
524
|
data,
|
|
645
|
-
freshness: {
|
|
646
|
-
freshAt: Date.now(),
|
|
647
|
-
transport: datasource.transport || "poll",
|
|
648
|
-
requestedPolicy,
|
|
649
|
-
scope: datasource.scope,
|
|
650
|
-
},
|
|
651
525
|
};
|
|
652
526
|
const processedResponse = await processPluginsForResponse(response, { plugins: pluginsData }, { extraData: { plugins: pluginsData } });
|
|
653
527
|
return c.json(processedResponse);
|
|
@@ -665,149 +539,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
665
539
|
throw error;
|
|
666
540
|
}
|
|
667
541
|
});
|
|
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
|
-
});
|
|
811
542
|
function mapAgentInfo(agent) {
|
|
812
543
|
return {
|
|
813
544
|
id: agent.id,
|
|
@@ -884,10 +615,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
884
615
|
outputSchema,
|
|
885
616
|
interface: tool.interface,
|
|
886
617
|
suggestConfirmation: tool.suggestConfirmation,
|
|
887
|
-
sideEffectClass: tool.sideEffectClass,
|
|
888
|
-
supportsParallel: tool.supportsParallel,
|
|
889
|
-
idempotencyScope: tool.idempotencyScope,
|
|
890
|
-
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
891
618
|
supportsUserActions: tool.supportsUserActions,
|
|
892
619
|
};
|
|
893
620
|
}
|
|
@@ -1092,10 +819,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
1092
819
|
outputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.output),
|
|
1093
820
|
interface: tool.interface,
|
|
1094
821
|
suggestConfirmation: tool.suggestConfirmation,
|
|
1095
|
-
sideEffectClass: tool.sideEffectClass,
|
|
1096
|
-
supportsParallel: tool.supportsParallel,
|
|
1097
|
-
idempotencyScope: tool.idempotencyScope,
|
|
1098
|
-
maxConcurrencyHint: tool.maxConcurrencyHint,
|
|
1099
822
|
};
|
|
1100
823
|
return c.json(toolDetails);
|
|
1101
824
|
}
|
|
@@ -1123,11 +846,6 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
1123
846
|
pricing: tool.pricing,
|
|
1124
847
|
inputSchema: (0, schemaStructure_1.getDetailedSchemaStructure)(tool.input),
|
|
1125
848
|
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,
|
|
1131
849
|
}
|
|
1132
850
|
: {
|
|
1133
851
|
id: toolId,
|
|
@@ -1206,11 +924,31 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
1206
924
|
}
|
|
1207
925
|
try {
|
|
1208
926
|
await app.oauth2.handleCallback(code, state);
|
|
1209
|
-
return c.html(
|
|
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
|
+
`);
|
|
1210
938
|
}
|
|
1211
939
|
catch (error) {
|
|
1212
940
|
console.error("OAuth callback error:", error);
|
|
1213
|
-
return c.html(
|
|
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
|
+
`);
|
|
1214
952
|
}
|
|
1215
953
|
});
|
|
1216
954
|
// Make oauth2Handler available to tools
|
|
@@ -1437,231 +1175,64 @@ function setupHttpServer(config, tools, services, toolboxes, metadata, privateKe
|
|
|
1437
1175
|
});
|
|
1438
1176
|
// Toolboxes list endpoint
|
|
1439
1177
|
app.get("/toolboxes", (c) => c.json(toolboxes));
|
|
1440
|
-
|
|
1441
|
-
|
|
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();
|
|
1178
|
+
// Recommendations endpoint - returns cards for AI selection
|
|
1179
|
+
app.get("/recommendations", (c) => {
|
|
1180
|
+
const cards = [];
|
|
1451
1181
|
for (const tool of tools) {
|
|
1452
|
-
if (
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1182
|
+
if (tool.recommendations?.length) {
|
|
1183
|
+
for (const card of tool.recommendations) {
|
|
1184
|
+
const hasDynamicSchema = !!card.inputSchema;
|
|
1185
|
+
cards.push({
|
|
1186
|
+
id: card.id,
|
|
1187
|
+
tags: card.tags,
|
|
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
|
+
});
|
|
1458
1193
|
}
|
|
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
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
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;
|
|
1498
1194
|
}
|
|
1499
1195
|
}
|
|
1500
|
-
return
|
|
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),
|
|
1196
|
+
return c.json({ cards, count: cards.length });
|
|
1508
1197
|
});
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const
|
|
1512
|
-
const
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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;
|
|
1198
|
+
// Generate recommendation card UI
|
|
1199
|
+
app.post("/recommendations/:id/generate", async (c) => {
|
|
1200
|
+
const cardId = c.req.param("id");
|
|
1201
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1202
|
+
const params = body.params ?? {};
|
|
1203
|
+
const context = body.context ?? {};
|
|
1204
|
+
let foundCard;
|
|
1205
|
+
let foundTool;
|
|
1206
|
+
for (const tool of tools) {
|
|
1207
|
+
const card = tool.recommendations?.find((r) => r.id === cardId);
|
|
1208
|
+
if (card) {
|
|
1209
|
+
foundCard = card;
|
|
1210
|
+
foundTool = tool;
|
|
1211
|
+
break;
|
|
1654
1212
|
}
|
|
1655
1213
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1214
|
+
if (!foundCard) {
|
|
1215
|
+
return c.json({ error: `Card not found: ${cardId}` }, 404);
|
|
1216
|
+
}
|
|
1217
|
+
const baseResponse = { id: cardId, toolId: foundTool?.id, toolName: foundTool?.name };
|
|
1218
|
+
if (!foundCard.inputSchema) {
|
|
1219
|
+
return c.json({ ...baseResponse, ui: foundCard.ui });
|
|
1220
|
+
}
|
|
1221
|
+
const parseResult = foundCard.inputSchema.safeParse(params);
|
|
1222
|
+
if (!parseResult.success) {
|
|
1223
|
+
return c.json({ error: "Invalid parameters", details: parseResult.error.format() }, 400);
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof foundCard.ui !== "function") {
|
|
1226
|
+
return c.json({ error: `Card ${cardId} has inputSchema but ui is not a function` }, 500);
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
const ui = await foundCard.ui(parseResult.data, context);
|
|
1230
|
+
return c.json({ ...baseResponse, ui, params: parseResult.data });
|
|
1231
|
+
}
|
|
1232
|
+
catch (e) {
|
|
1233
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
1234
|
+
return c.json({ error: `Failed to generate UI: ${message}` }, 500);
|
|
1235
|
+
}
|
|
1665
1236
|
});
|
|
1666
1237
|
// Process request with plugins
|
|
1667
1238
|
async function processPluginsForRequest(request, agentInfo) {
|