veryfront 0.1.61 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/esm/cli/templates/manifest.js +37 -37
  2. package/esm/deno.d.ts +3 -0
  3. package/esm/deno.js +6 -3
  4. package/esm/src/agent/composition/composition.d.ts.map +1 -1
  5. package/esm/src/agent/composition/composition.js +13 -3
  6. package/esm/src/agent/factory.d.ts.map +1 -1
  7. package/esm/src/agent/factory.js +3 -3
  8. package/esm/src/agent/middleware/security/validator.d.ts +92 -0
  9. package/esm/src/agent/middleware/security/validator.d.ts.map +1 -0
  10. package/esm/src/agent/middleware/security/validator.js +187 -0
  11. package/esm/src/agent/runtime/index.d.ts +3 -2
  12. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  13. package/esm/src/agent/runtime/index.js +16 -8
  14. package/esm/src/agent/types.d.ts +4 -0
  15. package/esm/src/agent/types.d.ts.map +1 -1
  16. package/esm/src/channels/invoke.d.ts +427 -0
  17. package/esm/src/channels/invoke.d.ts.map +1 -0
  18. package/esm/src/channels/invoke.js +401 -0
  19. package/esm/src/embedding/embedding.d.ts.map +1 -1
  20. package/esm/src/embedding/embedding.js +5 -1
  21. package/esm/src/embedding/rag-store.js +2 -0
  22. package/esm/src/embedding/vector-store.d.ts.map +1 -1
  23. package/esm/src/embedding/vector-store.js +2 -0
  24. package/esm/src/embedding/veryfront-cloud/rag-store.d.ts.map +1 -1
  25. package/esm/src/embedding/veryfront-cloud/rag-store.js +2 -0
  26. package/esm/src/integrations/schema.d.ts +2 -2
  27. package/esm/src/oauth/handlers/init-handler.d.ts +6 -2
  28. package/esm/src/oauth/handlers/init-handler.d.ts.map +1 -1
  29. package/esm/src/oauth/handlers/init-handler.js +8 -2
  30. package/esm/src/platform/compat/opaque-deps.d.ts.map +1 -1
  31. package/esm/src/platform/compat/opaque-deps.js +10 -1
  32. package/esm/src/prompt/factory.d.ts.map +1 -1
  33. package/esm/src/prompt/factory.js +9 -1
  34. package/esm/src/react/components/ai/markdown.d.ts.map +1 -1
  35. package/esm/src/react/components/ai/markdown.js +4 -4
  36. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  37. package/esm/src/server/handlers/dev/framework-candidates.generated.js +5 -4
  38. package/esm/src/server/handlers/preview/markdown-html-generator.js +1 -1
  39. package/esm/src/server/handlers/request/api/api-handler-wrapper.d.ts.map +1 -1
  40. package/esm/src/server/handlers/request/api/api-handler-wrapper.js +1 -74
  41. package/esm/src/server/handlers/request/api/project-discovery.d.ts +9 -0
  42. package/esm/src/server/handlers/request/api/project-discovery.d.ts.map +1 -0
  43. package/esm/src/server/handlers/request/api/project-discovery.js +74 -0
  44. package/esm/src/server/handlers/request/channel-invoke.handler.d.ts +11 -0
  45. package/esm/src/server/handlers/request/channel-invoke.handler.d.ts.map +1 -0
  46. package/esm/src/server/handlers/request/channel-invoke.handler.js +72 -0
  47. package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
  48. package/esm/src/server/runtime-handler/index.js +2 -0
  49. package/esm/src/transforms/md/compiler/md-compiler.d.ts.map +1 -1
  50. package/esm/src/transforms/md/compiler/md-compiler.js +25 -1
  51. package/package.json +3 -1
  52. package/src/cli/templates/manifest.js +37 -37
  53. package/src/deno.js +6 -3
  54. package/src/src/agent/composition/composition.ts +15 -3
  55. package/src/src/agent/factory.ts +19 -6
  56. package/src/src/agent/middleware/security/validator.ts +288 -0
  57. package/src/src/agent/runtime/index.ts +26 -3
  58. package/src/src/agent/types.ts +4 -0
  59. package/src/src/channels/invoke.ts +521 -0
  60. package/src/src/embedding/embedding.ts +5 -1
  61. package/src/src/embedding/rag-store.ts +1 -0
  62. package/src/src/embedding/vector-store.ts +1 -0
  63. package/src/src/embedding/veryfront-cloud/rag-store.ts +1 -0
  64. package/src/src/oauth/handlers/init-handler.ts +20 -5
  65. package/src/src/platform/compat/opaque-deps.ts +19 -4
  66. package/src/src/prompt/factory.ts +10 -1
  67. package/src/src/react/components/ai/markdown.tsx +5 -4
  68. package/src/src/server/handlers/dev/framework-candidates.generated.ts +5 -4
  69. package/src/src/server/handlers/preview/markdown-html-generator.ts +1 -1
  70. package/src/src/server/handlers/request/api/api-handler-wrapper.ts +1 -85
  71. package/src/src/server/handlers/request/api/project-discovery.ts +86 -0
  72. package/src/src/server/handlers/request/channel-invoke.handler.ts +95 -0
  73. package/src/src/server/runtime-handler/index.ts +2 -0
  74. package/src/src/transforms/md/compiler/md-compiler.ts +27 -1
@@ -0,0 +1,521 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import type { Agent, AgentMessage as Message, AgentResponse } from "../agent/index.js";
3
+ import { fromError } from "../errors/veryfront-error.js";
4
+ import type { HandlerContext } from "../types/index.js";
5
+ import { serverLogger } from "../utils/index.js";
6
+ import { base64urlEncodeBytes } from "../utils/base64url.js";
7
+ import { z } from "zod";
8
+ import {
9
+ getAgent as getRegisteredAgent,
10
+ getAllAgentIds as getRegisteredAgentIds,
11
+ } from "../agent/composition/composition.js";
12
+ import { ensureProjectDiscovery as ensureProjectDiscoveryForProject } from "../server/handlers/request/api/project-discovery.js";
13
+
14
+ const logger = serverLogger.component("channels-invoke");
15
+ const SIGNATURE_SKEW_SECONDS = 5;
16
+
17
+ const rawHistoryPartSchema = z.object({
18
+ type: z.string(),
19
+ }).passthrough();
20
+
21
+ const channelAttachmentSchema = z.object({
22
+ id: z.string(),
23
+ kind: z.enum(["image", "file"]),
24
+ filename: z.string().optional(),
25
+ mediaType: z.string().optional(),
26
+ privateUrl: z.string().optional(),
27
+ });
28
+
29
+ const channelInvokeHistoryMessageSchema = z.object({
30
+ id: z.string(),
31
+ role: z.enum(["user", "assistant", "system", "tool"]),
32
+ parts: z.array(rawHistoryPartSchema),
33
+ metadata: z.record(z.unknown()).optional(),
34
+ createdAt: z.string().optional(),
35
+ });
36
+
37
+ export const ChannelInvokeRequestSchema = z.object({
38
+ dispatchId: z.string().min(1),
39
+ conversationId: z.string().min(1),
40
+ projectId: z.string().min(1),
41
+ agentConfigId: z.string().min(1),
42
+ platform: z.literal("slack"),
43
+ inboundMessage: z.object({
44
+ text: z.string(),
45
+ userId: z.string(),
46
+ userName: z.string(),
47
+ isDirectMessage: z.boolean(),
48
+ attachments: z.array(channelAttachmentSchema).optional(),
49
+ }),
50
+ conversationHistory: z.array(channelInvokeHistoryMessageSchema),
51
+ generation: z.object({
52
+ maxResponseTokens: z.number().int().positive().max(16384).optional(),
53
+ }).optional(),
54
+ });
55
+
56
+ const channelTextPartSchema = z.object({
57
+ type: z.literal("text"),
58
+ text: z.string(),
59
+ });
60
+
61
+ const channelToolCallPartSchema = z.object({
62
+ type: z.literal("tool_call"),
63
+ id: z.string(),
64
+ name: z.string(),
65
+ input: z.record(z.unknown()),
66
+ state: z.enum(["streaming", "pending", "completed", "error"]),
67
+ });
68
+
69
+ const channelToolResultPartSchema = z.object({
70
+ type: z.literal("tool_result"),
71
+ tool_call_id: z.string(),
72
+ output: z.unknown(),
73
+ is_error: z.boolean().optional(),
74
+ });
75
+
76
+ const channelReasoningPartSchema = z.object({
77
+ type: z.literal("reasoning"),
78
+ text: z.string(),
79
+ });
80
+
81
+ const channelErrorPartSchema = z.object({
82
+ type: z.literal("error"),
83
+ code: z.string(),
84
+ message: z.string(),
85
+ });
86
+
87
+ export const ChannelResponsePartSchema = z.discriminatedUnion("type", [
88
+ channelTextPartSchema,
89
+ channelToolCallPartSchema,
90
+ channelToolResultPartSchema,
91
+ channelReasoningPartSchema,
92
+ channelErrorPartSchema,
93
+ ]);
94
+
95
+ export const ChannelInvokeResponseSchema = z.object({
96
+ ignored: z.boolean(),
97
+ responseParts: z.array(ChannelResponsePartSchema).optional(),
98
+ tokenUsage: z.object({
99
+ inputTokens: z.number().int().nonnegative().optional(),
100
+ outputTokens: z.number().int().nonnegative().optional(),
101
+ totalTokens: z.number().int().nonnegative().optional(),
102
+ }).optional(),
103
+ error: z.object({
104
+ code: z.enum(["provider_error", "internal_error"]),
105
+ retryable: z.boolean(),
106
+ }).optional(),
107
+ });
108
+
109
+ const dispatchHeaderSchema = z.object({
110
+ alg: z.literal("EdDSA"),
111
+ typ: z.string().optional(),
112
+ kid: z.string().optional(),
113
+ });
114
+
115
+ const dispatchClaimsSchema = z.object({
116
+ iss: z.string(),
117
+ aud: z.string(),
118
+ sub: z.string(),
119
+ project_id: z.string(),
120
+ platform: z.string(),
121
+ body_sha256: z.string(),
122
+ iat: z.number().int(),
123
+ exp: z.number().int(),
124
+ });
125
+
126
+ export type ChannelInvokeRequest = z.infer<typeof ChannelInvokeRequestSchema>;
127
+ export type ChannelInvokeResponse = z.infer<typeof ChannelInvokeResponseSchema>;
128
+
129
+ type ChannelResponsePart = z.infer<typeof ChannelResponsePartSchema>;
130
+ type DispatchClaims = z.infer<typeof dispatchClaimsSchema>;
131
+
132
+ export interface ChannelInvokeDeps {
133
+ ensureProjectDiscovery: (ctx: HandlerContext) => Promise<void>;
134
+ getAgent: (id: string) => Agent | undefined;
135
+ getAllAgentIds: () => string[];
136
+ }
137
+
138
+ export const defaultChannelInvokeDeps: ChannelInvokeDeps = {
139
+ ensureProjectDiscovery: ensureProjectDiscoveryForProject,
140
+ getAgent: getRegisteredAgent,
141
+ getAllAgentIds: getRegisteredAgentIds,
142
+ };
143
+
144
+ function base64urlDecodeToBytes(input: string): ArrayBuffer {
145
+ const normalized = input
146
+ .replaceAll("-", "+")
147
+ .replaceAll("_", "/")
148
+ .padEnd(Math.ceil(input.length / 4) * 4, "=");
149
+
150
+ return toArrayBuffer(Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0)));
151
+ }
152
+
153
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
154
+ const buffer = new ArrayBuffer(bytes.byteLength);
155
+ new Uint8Array(buffer).set(bytes);
156
+ return buffer;
157
+ }
158
+
159
+ function pemToDer(pem: string, label: string): ArrayBuffer {
160
+ const body = pem
161
+ .replace(`-----BEGIN ${label}-----`, "")
162
+ .replace(`-----END ${label}-----`, "")
163
+ .replace(/\s/g, "");
164
+
165
+ return toArrayBuffer(Uint8Array.from(atob(body), (char) => char.charCodeAt(0)));
166
+ }
167
+
168
+ async function importEd25519PublicKey(pem: string): Promise<dntShim.CryptoKey> {
169
+ return dntShim.crypto.subtle.importKey(
170
+ "spki",
171
+ pemToDer(pem, "PUBLIC KEY"),
172
+ "Ed25519",
173
+ false,
174
+ ["verify"],
175
+ );
176
+ }
177
+
178
+ async function sha256Base64url(body: string): Promise<string> {
179
+ const hash = await dntShim.crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
180
+ return base64urlEncodeBytes(new Uint8Array(hash));
181
+ }
182
+
183
+ export async function verifyDispatchJws(
184
+ jws: string,
185
+ body: string,
186
+ options: {
187
+ audience: string;
188
+ publicKeyPem: string;
189
+ maxAgeSeconds: number;
190
+ expectedProjectId?: string;
191
+ },
192
+ ): Promise<DispatchClaims> {
193
+ const parts = jws.split(".");
194
+ if (parts.length !== 3) {
195
+ throw new Error("Channel dispatch signature must be a compact JWS");
196
+ }
197
+
198
+ const encodedHeader = parts[0];
199
+ const encodedPayload = parts[1];
200
+ const encodedSignature = parts[2];
201
+ if (!encodedHeader || !encodedPayload || !encodedSignature) {
202
+ throw new Error("Channel dispatch signature must include header, payload, and signature");
203
+ }
204
+ const header = dispatchHeaderSchema.parse(
205
+ JSON.parse(new TextDecoder().decode(base64urlDecodeToBytes(encodedHeader))),
206
+ );
207
+ const claims = dispatchClaimsSchema.parse(
208
+ JSON.parse(new TextDecoder().decode(base64urlDecodeToBytes(encodedPayload))),
209
+ );
210
+
211
+ if (header.alg !== "EdDSA") {
212
+ throw new Error("Unsupported channel dispatch JWS algorithm");
213
+ }
214
+
215
+ const signingInput = new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`);
216
+ const signature = base64urlDecodeToBytes(encodedSignature);
217
+ const publicKey = await importEd25519PublicKey(options.publicKeyPem);
218
+ const verified = await dntShim.crypto.subtle.verify("Ed25519", publicKey, signature, signingInput);
219
+
220
+ if (!verified) {
221
+ throw new Error("Channel dispatch signature verification failed");
222
+ }
223
+
224
+ if (claims.aud !== options.audience) {
225
+ throw new Error("Channel dispatch audience mismatch");
226
+ }
227
+
228
+ if (options.expectedProjectId && claims.project_id !== options.expectedProjectId) {
229
+ throw new Error("Channel dispatch project mismatch");
230
+ }
231
+
232
+ const now = Math.floor(Date.now() / 1000);
233
+ if (claims.exp <= now) {
234
+ throw new Error("Channel dispatch signature expired");
235
+ }
236
+
237
+ if (claims.iat > now + SIGNATURE_SKEW_SECONDS) {
238
+ throw new Error("Channel dispatch signature issued in the future");
239
+ }
240
+
241
+ if (now - claims.iat > options.maxAgeSeconds) {
242
+ throw new Error("Channel dispatch signature is too old");
243
+ }
244
+
245
+ const bodySha256 = await sha256Base64url(body);
246
+ if (claims.body_sha256 !== bodySha256) {
247
+ throw new Error("Channel dispatch body hash mismatch");
248
+ }
249
+
250
+ return claims;
251
+ }
252
+
253
+ function normalizeConversationPart(
254
+ part: z.infer<typeof rawHistoryPartSchema>,
255
+ ): Message["parts"][number] | null {
256
+ if (part.type === "text" && typeof part.text === "string") {
257
+ return { type: "text", text: part.text };
258
+ }
259
+
260
+ if (
261
+ part.type === "tool_call" &&
262
+ typeof part.id === "string" &&
263
+ typeof part.name === "string" &&
264
+ part.input &&
265
+ typeof part.input === "object" &&
266
+ !Array.isArray(part.input)
267
+ ) {
268
+ return {
269
+ type: `tool-${part.name}`,
270
+ toolCallId: part.id,
271
+ toolName: part.name,
272
+ args: part.input as Record<string, unknown>,
273
+ };
274
+ }
275
+
276
+ if (part.type === "tool_result" && typeof part.tool_call_id === "string") {
277
+ return {
278
+ type: "tool-result",
279
+ toolCallId: part.tool_call_id,
280
+ toolName: typeof part.tool_name === "string" ? part.tool_name : "unknown",
281
+ result: "output" in part ? part.output : undefined,
282
+ };
283
+ }
284
+
285
+ return null;
286
+ }
287
+
288
+ export function normalizeConversationHistoryForRuntime(
289
+ messages: ChannelInvokeRequest["conversationHistory"],
290
+ ): Message[] {
291
+ return messages.map((message) => ({
292
+ id: message.id,
293
+ role: message.role,
294
+ parts: message.parts
295
+ .map((part) => normalizeConversationPart(part))
296
+ .filter((part): part is NonNullable<typeof part> => part !== null),
297
+ ...(message.createdAt ? { timestamp: Date.parse(message.createdAt) || undefined } : {}),
298
+ ...(message.metadata ? { metadata: message.metadata } : {}),
299
+ }));
300
+ }
301
+
302
+ export function resolveChannelInvokeAgent(
303
+ agentConfigId: string,
304
+ deps: Pick<ChannelInvokeDeps, "getAgent" | "getAllAgentIds">,
305
+ ): Agent | undefined {
306
+ const exactAgent = deps.getAgent(agentConfigId);
307
+ if (exactAgent) {
308
+ return exactAgent;
309
+ }
310
+
311
+ const agentIds = deps.getAllAgentIds();
312
+ if (agentIds.length !== 1) {
313
+ return undefined;
314
+ }
315
+
316
+ const onlyAgentId = agentIds[0];
317
+ if (!onlyAgentId) {
318
+ return undefined;
319
+ }
320
+ const onlyAgent = deps.getAgent(onlyAgentId);
321
+ if (onlyAgent) {
322
+ logger.warn(
323
+ "Channel invoke fell back to the only discovered runtime agent because agentConfigId did not match a registry id",
324
+ {
325
+ requestedAgentConfigId: agentConfigId,
326
+ resolvedAgentId: onlyAgentId,
327
+ },
328
+ );
329
+ }
330
+
331
+ return onlyAgent;
332
+ }
333
+
334
+ function normalizeToolCallState(status: string): "pending" | "completed" | "error" {
335
+ switch (status) {
336
+ case "completed":
337
+ return "completed";
338
+ case "error":
339
+ return "error";
340
+ default:
341
+ return "pending";
342
+ }
343
+ }
344
+
345
+ function convertAssistantPartToChannelResponsePart(
346
+ part: Message["parts"][number],
347
+ knownToolCallIds: Set<string>,
348
+ ): ChannelResponsePart | null {
349
+ if (part.type === "text" && "text" in part) {
350
+ return channelTextPartSchema.parse({
351
+ type: "text",
352
+ text: part.text,
353
+ });
354
+ }
355
+
356
+ const isToolCallPart = part.type === "tool-call" ||
357
+ (part.type.startsWith("tool-") && part.type !== "tool-result");
358
+ if (
359
+ isToolCallPart &&
360
+ "toolCallId" in part &&
361
+ "toolName" in part &&
362
+ !knownToolCallIds.has(part.toolCallId)
363
+ ) {
364
+ return channelToolCallPartSchema.parse({
365
+ type: "tool_call",
366
+ id: part.toolCallId,
367
+ name: part.toolName,
368
+ input: "args" in part ? part.args : ("input" in part ? part.input : {}),
369
+ state: "pending",
370
+ });
371
+ }
372
+
373
+ return null;
374
+ }
375
+
376
+ function findLastAssistantMessage(messages: Message[]): Message | undefined {
377
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
378
+ if (messages[index]?.role === "assistant") {
379
+ return messages[index];
380
+ }
381
+ }
382
+
383
+ return undefined;
384
+ }
385
+
386
+ export function buildChannelResponseParts(response: AgentResponse): ChannelResponsePart[] {
387
+ const responseParts: ChannelResponsePart[] = [];
388
+ const knownToolCallIds = new Set<string>();
389
+
390
+ if (response.thinking?.trim()) {
391
+ responseParts.push(channelReasoningPartSchema.parse({
392
+ type: "reasoning",
393
+ text: response.thinking,
394
+ }));
395
+ }
396
+
397
+ for (const toolCall of response.toolCalls) {
398
+ knownToolCallIds.add(toolCall.id);
399
+ responseParts.push(channelToolCallPartSchema.parse({
400
+ type: "tool_call",
401
+ id: toolCall.id,
402
+ name: toolCall.name,
403
+ input: toolCall.args,
404
+ state: normalizeToolCallState(toolCall.status),
405
+ }));
406
+
407
+ if (toolCall.status === "completed" || toolCall.status === "error") {
408
+ responseParts.push(channelToolResultPartSchema.parse({
409
+ type: "tool_result",
410
+ tool_call_id: toolCall.id,
411
+ output: toolCall.status === "error"
412
+ ? { error: toolCall.error ?? "Tool execution failed" }
413
+ : toolCall.result,
414
+ ...(toolCall.status === "error" ? { is_error: true } : {}),
415
+ }));
416
+ }
417
+ }
418
+
419
+ const lastAssistantMessage = findLastAssistantMessage(response.messages);
420
+ if (lastAssistantMessage) {
421
+ for (const part of lastAssistantMessage.parts) {
422
+ const converted = convertAssistantPartToChannelResponsePart(part, knownToolCallIds);
423
+ if (converted) {
424
+ responseParts.push(converted);
425
+ }
426
+ }
427
+ } else if (response.text.trim()) {
428
+ responseParts.push(channelTextPartSchema.parse({
429
+ type: "text",
430
+ text: response.text,
431
+ }));
432
+ }
433
+
434
+ return responseParts;
435
+ }
436
+
437
+ function classifyRuntimeError(error: unknown): ChannelInvokeResponse["error"] {
438
+ const veryfrontError = fromError(error);
439
+
440
+ if (veryfrontError?.type === "no_ai_available") {
441
+ return { code: "provider_error", retryable: false };
442
+ }
443
+
444
+ if (veryfrontError?.type === "api" || veryfrontError?.type === "network") {
445
+ return { code: "provider_error", retryable: true };
446
+ }
447
+
448
+ return { code: "internal_error", retryable: true };
449
+ }
450
+
451
+ export async function executeChannelInvoke(
452
+ payload: ChannelInvokeRequest,
453
+ ctx: HandlerContext,
454
+ deps: ChannelInvokeDeps,
455
+ ): Promise<ChannelInvokeResponse> {
456
+ await deps.ensureProjectDiscovery(ctx);
457
+
458
+ const agent = resolveChannelInvokeAgent(payload.agentConfigId, deps);
459
+ if (!agent) {
460
+ logger.error("Channel invoke could not resolve a runtime agent for the request", {
461
+ requestedAgentConfigId: payload.agentConfigId,
462
+ discoveredAgentIds: deps.getAllAgentIds(),
463
+ projectSlug: ctx.projectSlug,
464
+ projectId: ctx.projectId,
465
+ });
466
+ return {
467
+ ignored: false,
468
+ error: {
469
+ code: "internal_error",
470
+ retryable: false,
471
+ },
472
+ };
473
+ }
474
+
475
+ const messages = normalizeConversationHistoryForRuntime(payload.conversationHistory);
476
+ await agent.clearMemory();
477
+
478
+ try {
479
+ const result = await agent.generate({
480
+ input: messages,
481
+ context: {
482
+ requestId: payload.dispatchId,
483
+ dispatchId: payload.dispatchId,
484
+ conversationId: payload.conversationId,
485
+ projectId: payload.projectId,
486
+ agentConfigId: payload.agentConfigId,
487
+ channel: payload.inboundMessage,
488
+ },
489
+ ...(payload.generation?.maxResponseTokens
490
+ ? {
491
+ maxOutputTokens: payload.generation.maxResponseTokens,
492
+ }
493
+ : {}),
494
+ });
495
+
496
+ return ChannelInvokeResponseSchema.parse({
497
+ ignored: false,
498
+ responseParts: buildChannelResponseParts(result),
499
+ tokenUsage: result.usage
500
+ ? {
501
+ inputTokens: result.usage.promptTokens,
502
+ outputTokens: result.usage.completionTokens,
503
+ totalTokens: result.usage.totalTokens,
504
+ }
505
+ : undefined,
506
+ });
507
+ } catch (error) {
508
+ logger.error("Channel invoke runtime execution failed", {
509
+ error: error instanceof Error ? error.message : String(error),
510
+ stack: error instanceof Error ? error.stack : undefined,
511
+ projectSlug: ctx.projectSlug,
512
+ projectId: ctx.projectId,
513
+ dispatchId: payload.dispatchId,
514
+ });
515
+
516
+ return {
517
+ ignored: false,
518
+ error: classifyRuntimeError(error),
519
+ };
520
+ }
521
+ }
@@ -35,7 +35,11 @@ export function embedding(config: EmbeddingConfig): Embedding {
35
35
  model: modelId,
36
36
 
37
37
  async embed(text: string): Promise<number[]> {
38
- const result = await embed({ model, value: queryPrefix + text });
38
+ if (!text.trim()) {
39
+ throw new Error("Cannot embed an empty string");
40
+ }
41
+ const value = queryPrefix + text;
42
+ const result = await embed({ model, value });
39
43
  return result.embedding;
40
44
  },
41
45
 
@@ -321,6 +321,7 @@ function createLocalJsonRagStore(config: ResolvedRagStoreConfig): RagStore {
321
321
  query: string,
322
322
  options?: RagSearchOptions,
323
323
  ): Promise<RagSearchResult[]> {
324
+ if (!query.trim()) return [];
324
325
  return withLock(async () => {
325
326
  const data = await load();
326
327
  if (data.chunks.length === 0) return [];
@@ -60,6 +60,7 @@ export function vectorStore(config: VectorStoreConfig): VectorStore {
60
60
  },
61
61
 
62
62
  async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
63
+ if (!query.trim()) return [];
63
64
  if (entries.length === 0) return [];
64
65
 
65
66
  const topK = options?.topK ?? 5;
@@ -492,6 +492,7 @@ export function createVeryfrontCloudRagStore(config: ResolvedCloudRagStoreConfig
492
492
  query: string,
493
493
  options?: RagSearchOptions,
494
494
  ): Promise<RagSearchResult[]> {
495
+ if (!query.trim()) return [];
495
496
  const context = getCloudStoreContext(config);
496
497
  const queryEmbedder = createEmbedder(config);
497
498
  const vector = await queryEmbedder.embed(query);
@@ -77,16 +77,23 @@ export interface OAuthStatusHandlerOptions {
77
77
 
78
78
  /** EnvReader for dynamic env vars (defaults to getEnv) */
79
79
  envReader?: EnvReader;
80
+
81
+ /** Optional authentication check — return true if the request is authenticated */
82
+ isAuthenticated?: (req: dntShim.Request) => boolean | Promise<boolean>;
80
83
  }
81
84
 
82
85
  export function createOAuthStatusHandler(
83
86
  config: OAuthServiceConfig,
84
87
  options: OAuthStatusHandlerOptions = {},
85
- ): () => Promise<dntShim.Response> {
88
+ ): (req: dntShim.Request) => Promise<dntShim.Response> {
86
89
  const tokenStore = options.tokenStore ?? memoryTokenStore;
87
90
  const envReader = options.envReader ?? getEnv;
88
91
 
89
- return async function handler(): Promise<dntShim.Response> {
92
+ return async function handler(req: dntShim.Request): Promise<dntShim.Response> {
93
+ if (options.isAuthenticated && !(await options.isAuthenticated(req))) {
94
+ return dntShim.Response.json({ error: "Unauthorized" }, { status: 401 });
95
+ }
96
+
90
97
  const tokens = await tokenStore.getTokens(config.serviceId);
91
98
 
92
99
  const isConnected = !!tokens?.accessToken;
@@ -106,11 +113,19 @@ export function createOAuthStatusHandler(
106
113
 
107
114
  export function createOAuthDisconnectHandler(
108
115
  config: OAuthServiceConfig,
109
- options: { tokenStore?: TokenStore } = {},
110
- ): () => Promise<dntShim.Response> {
116
+ options: {
117
+ tokenStore?: TokenStore;
118
+ /** Optional authentication check — return true if the request is authenticated */
119
+ isAuthenticated?: (req: dntShim.Request) => boolean | Promise<boolean>;
120
+ } = {},
121
+ ): (req: dntShim.Request) => Promise<dntShim.Response> {
111
122
  const tokenStore = options.tokenStore ?? memoryTokenStore;
112
123
 
113
- return async function handler(): Promise<dntShim.Response> {
124
+ return async function handler(req: dntShim.Request): Promise<dntShim.Response> {
125
+ if (options.isAuthenticated && !(await options.isAuthenticated(req))) {
126
+ return dntShim.Response.json({ error: "Unauthorized" }, { status: 401 });
127
+ }
128
+
114
129
  await tokenStore.clearTokens(config.serviceId);
115
130
 
116
131
  return dntShim.Response.json({
@@ -17,7 +17,7 @@
17
17
  import * as dntShim from "../../../_dnt.shims.js";
18
18
 
19
19
 
20
- import { isDeno } from "./runtime.js";
20
+ import { isDeno, isDenoCompiled } from "./runtime.js";
21
21
  import { dynamicImport } from "./dynamic-import.js";
22
22
 
23
23
  function resolve(pkg: string, version: string): string {
@@ -27,6 +27,14 @@ function resolve(pkg: string, version: string): string {
27
27
  // deno-lint-ignore no-explicit-any -- callers assign to their own typed variable; any allows implicit narrowing at each call site
28
28
  type OpaqueModule = any;
29
29
 
30
+ type KreuzbergModule = {
31
+ initWasm?: () => Promise<void>;
32
+ extractBytes: (
33
+ data: Uint8Array,
34
+ mimeType: string,
35
+ ) => Promise<{ content: string }>;
36
+ };
37
+
30
38
  /** Lazily import `@huggingface/transformers` (+ onnxruntime, ~500MB). */
31
39
  export function importTransformers(): Promise<OpaqueModule> {
32
40
  return dynamicImport(resolve("@huggingface/transformers", "3.4.2"));
@@ -57,9 +65,16 @@ export async function importKreuzberg(): Promise<{
57
65
  }> {
58
66
  if (isDeno) {
59
67
  // Regular import — visible to deno compile, resolved via deno.json import map
60
- const mod = await import("@kreuzberg/wasm") as unknown as
61
- & { initWasm?: () => Promise<void> }
62
- & { extractBytes: (data: Uint8Array, mimeType: string) => Promise<{ content: string }> };
68
+ const mod = await import("@kreuzberg/wasm") as unknown as KreuzbergModule;
69
+ if (isDenoCompiled) {
70
+ // Kreuzberg's initWasm() internally uses a computed dynamic import() to
71
+ // load the WASM glue module (kreuzberg_wasm.js). deno compile cannot
72
+ // trace computed import() paths, so the glue module is absent from the
73
+ // binary's embedded module graph. Pre-importing it here populates Deno's
74
+ // in-process module cache so the subsequent import() inside initWasm()
75
+ // resolves from cache instead of hitting the missing file.
76
+ await import("@kreuzberg/wasm/dist/pkg/kreuzberg_wasm.js");
77
+ }
63
78
  await mod.initWasm?.();
64
79
  return mod;
65
80
  }
@@ -1,5 +1,6 @@
1
1
  import type { Prompt, PromptConfig } from "./types.js";
2
2
  import { createError, toError } from "../errors/veryfront-error.js";
3
+ import { COMMON_BLOCKED_PATTERNS } from "../agent/middleware/security/validator.js";
3
4
 
4
5
  export function prompt(config: PromptConfig): Prompt {
5
6
  const id = config.id ?? generatePromptId();
@@ -35,12 +36,20 @@ function generatePromptId(): string {
35
36
  return `prompt_${Date.now()}_${promptIdCounter++}`;
36
37
  }
37
38
 
39
+ function sanitizeVariableValue(value: string): string {
40
+ let sanitized = value;
41
+ for (const pattern of COMMON_BLOCKED_PATTERNS.promptInjection) {
42
+ sanitized = sanitized.replace(pattern, "");
43
+ }
44
+ return sanitized;
45
+ }
46
+
38
47
  function interpolateVariables(
39
48
  template: string,
40
49
  variables: Record<string, unknown>,
41
50
  ): string {
42
51
  return template.replace(/\{(\w+)\}/g, (match, key) => {
43
52
  const value = variables[key];
44
- return value != null ? String(value) : match;
53
+ return value != null ? sanitizeVariableValue(String(value)) : match;
45
54
  });
46
55
  }
@@ -21,10 +21,11 @@ export interface CodeBlockProps {
21
21
  inline?: boolean;
22
22
  }
23
23
 
24
- const ESM_REACT_MARKDOWN = "https://esm.sh/react-markdown@9?external=react&target=es2022";
25
- const ESM_REMARK_GFM = "https://esm.sh/remark-gfm@4?target=es2022";
26
- const ESM_REHYPE_HIGHLIGHT = "https://esm.sh/rehype-highlight@7?target=es2022";
27
- const ESM_MERMAID = "https://esm.sh/mermaid@11";
24
+ const ESM_REACT_MARKDOWN =
25
+ "https://esm.sh/react-markdown@9.0.3?external=react&target=es2022&pin=v135";
26
+ const ESM_REMARK_GFM = "https://esm.sh/remark-gfm@4.0.1?target=es2022&pin=v135";
27
+ const ESM_REHYPE_HIGHLIGHT = "https://esm.sh/rehype-highlight@7.0.2?target=es2022&pin=v135";
28
+ const ESM_MERMAID = "https://esm.sh/mermaid@11.4.1?pin=v135";
28
29
 
29
30
  const dynamicImport = new Function("url", "return import(url)") as (url: string) => Promise<any>;
30
31