veryfront 0.1.62 → 0.1.64

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 (73) 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 +491 -0
  17. package/esm/src/channels/invoke.d.ts.map +1 -0
  18. package/esm/src/channels/invoke.js +417 -0
  19. package/esm/src/embedding/embedding.js +2 -2
  20. package/esm/src/integrations/endpoint-executor.d.ts +1 -0
  21. package/esm/src/integrations/endpoint-executor.d.ts.map +1 -1
  22. package/esm/src/integrations/endpoint-executor.js +44 -0
  23. package/esm/src/integrations/schema.d.ts +2 -2
  24. package/esm/src/oauth/handlers/init-handler.d.ts +6 -2
  25. package/esm/src/oauth/handlers/init-handler.d.ts.map +1 -1
  26. package/esm/src/oauth/handlers/init-handler.js +8 -2
  27. package/esm/src/platform/compat/opaque-deps.d.ts.map +1 -1
  28. package/esm/src/platform/compat/opaque-deps.js +10 -1
  29. package/esm/src/prompt/factory.d.ts.map +1 -1
  30. package/esm/src/prompt/factory.js +9 -1
  31. package/esm/src/react/components/ai/markdown.d.ts.map +1 -1
  32. package/esm/src/react/components/ai/markdown.js +4 -4
  33. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  34. package/esm/src/server/handlers/dev/framework-candidates.generated.js +5 -4
  35. package/esm/src/server/handlers/preview/markdown-html-generator.js +1 -1
  36. package/esm/src/server/handlers/request/api/api-handler-wrapper.d.ts.map +1 -1
  37. package/esm/src/server/handlers/request/api/api-handler-wrapper.js +1 -74
  38. package/esm/src/server/handlers/request/api/project-discovery.d.ts +9 -0
  39. package/esm/src/server/handlers/request/api/project-discovery.d.ts.map +1 -0
  40. package/esm/src/server/handlers/request/api/project-discovery.js +74 -0
  41. package/esm/src/server/handlers/request/channel-assistants.handler.d.ts +11 -0
  42. package/esm/src/server/handlers/request/channel-assistants.handler.d.ts.map +1 -0
  43. package/esm/src/server/handlers/request/channel-assistants.handler.js +71 -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 +4 -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 +546 -0
  60. package/src/src/embedding/embedding.ts +2 -2
  61. package/src/src/integrations/endpoint-executor.ts +51 -0
  62. package/src/src/oauth/handlers/init-handler.ts +20 -5
  63. package/src/src/platform/compat/opaque-deps.ts +19 -4
  64. package/src/src/prompt/factory.ts +10 -1
  65. package/src/src/react/components/ai/markdown.tsx +5 -4
  66. package/src/src/server/handlers/dev/framework-candidates.generated.ts +5 -4
  67. package/src/src/server/handlers/preview/markdown-html-generator.ts +1 -1
  68. package/src/src/server/handlers/request/api/api-handler-wrapper.ts +1 -85
  69. package/src/src/server/handlers/request/api/project-discovery.ts +86 -0
  70. package/src/src/server/handlers/request/channel-assistants.handler.ts +94 -0
  71. package/src/src/server/handlers/request/channel-invoke.handler.ts +95 -0
  72. package/src/src/server/runtime-handler/index.ts +4 -0
  73. package/src/src/transforms/md/compiler/md-compiler.ts +27 -1
@@ -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
 
@@ -3059,10 +3059,10 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [
3059
3059
  "html:",
3060
3060
  "html>",
3061
3061
  "https://ai-sdk.dev/elements)",
3062
- "https://esm.sh/mermaid@11",
3063
- "https://esm.sh/react-markdown@9?external=react",
3064
- "https://esm.sh/rehype-highlight@7?target=es2022",
3065
- "https://esm.sh/remark-gfm@4?target=es2022",
3062
+ "https://esm.sh/mermaid@11.4.1?pin=v135",
3063
+ "https://esm.sh/react-markdown@9.0.3?external=react",
3064
+ "https://esm.sh/rehype-highlight@7.0.2?target=es2022",
3065
+ "https://esm.sh/remark-gfm@4.0.1?target=es2022",
3066
3066
  "hydration",
3067
3067
  "i)",
3068
3068
  "icon",
@@ -4007,6 +4007,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [
4007
4007
  "persistThreads],",
4008
4008
  "pickers,",
4009
4009
  "pill",
4010
+ "pin=v135",
4010
4011
  "pl-2",
4011
4012
  "pl-2.5",
4012
4013
  "pl-3",
@@ -118,7 +118,7 @@ export function generateMarkdownHtml(options: MarkdownHtmlOptions): string {
118
118
  ${studioScript}
119
119
 
120
120
  <script type="module">
121
- import mermaid from 'https://esm.sh/mermaid@11';
121
+ import mermaid from 'https://esm.sh/mermaid@11.4.1?pin=v135';
122
122
 
123
123
  function getMermaidTheme() {
124
124
  return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'default';
@@ -9,9 +9,7 @@ import type {
9
9
  import { getApiHandler } from "./pages-api-handler.js";
10
10
  import { PRIORITY_MEDIUM_API } from "../../../../utils/constants/index.js";
11
11
  import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
12
- import { serverLogger } from "../../../../utils/index.js";
13
-
14
- const logger = serverLogger.component("api-wrapper");
12
+ import { ensureProjectDiscovery } from "./project-discovery.js";
15
13
 
16
14
  type FsWrapper = {
17
15
  isMultiProjectMode?: () => boolean;
@@ -24,88 +22,6 @@ type FsWrapper = {
24
22
  ) => Promise<T>;
25
23
  };
26
24
 
27
- /**
28
- * Tracks in-flight and completed AI discovery per project+release.
29
- *
30
- * Key: `{projectSlug}:{releaseId}` for production, `{projectSlug}:preview` for preview.
31
- * This ensures a new deployment triggers re-discovery of agents/tools.
32
- *
33
- * Using a Map<string, Promise> deduplicates concurrent requests and
34
- * allows retry on failure (the key is deleted if discovery rejects).
35
- */
36
- const discoveredProjects = new Map<string, Promise<void>>();
37
-
38
- /** Build a discovery cache key that incorporates the release/version. */
39
- function discoveryKey(ctx: HandlerContext): string {
40
- const slug = ctx.projectSlug ?? ctx.projectDir;
41
- const version = ctx.releaseId ?? "preview";
42
- return `${slug}:${version}`;
43
- }
44
-
45
- /**
46
- * Run AI discovery (agents, tools) for a project if not already done.
47
- * Must be called within a runWithContext scope so the VFS can resolve
48
- * the correct remote project files and the agent registry uses the
49
- * correct project scope.
50
- */
51
- async function ensureProjectDiscovery(ctx: HandlerContext): Promise<void> {
52
- const key = discoveryKey(ctx);
53
-
54
- const existing = discoveredProjects.get(key);
55
- if (existing) return existing;
56
-
57
- const promise = (async () => {
58
- const { discoverAll } = await import("../../../../discovery/index.js");
59
- const { agentRegistry } = await import(
60
- "../../../../agent/composition/composition.js"
61
- );
62
- const { toolRegistry } = await import("../../../../tool/registry.js");
63
-
64
- // Clear stale entries for this project scope before re-discovery.
65
- // This prevents agents/tools removed in a new release from lingering.
66
- agentRegistry.clear();
67
- toolRegistry.clear();
68
-
69
- const result = await discoverAll({
70
- baseDir: ctx.projectDir,
71
- fsAdapter: ctx.adapter.fs,
72
- verbose: false,
73
- });
74
-
75
- const logData = {
76
- projectSlug: ctx.projectSlug,
77
- releaseId: ctx.releaseId,
78
- agents: result.agents.size,
79
- tools: result.tools.size,
80
- errors: result.errors.length,
81
- };
82
-
83
- if (result.agents.size === 0 && result.tools.size === 0) {
84
- logger.warn("AI discovery found 0 agents and 0 tools", {
85
- ...logData,
86
- errorMessages: result.errors.map((e) => e.error.message).slice(0, 5),
87
- baseDir: ctx.projectDir,
88
- });
89
- } else {
90
- logger.info("AI discovery completed", logData);
91
- }
92
- })();
93
-
94
- discoveredProjects.set(key, promise);
95
-
96
- try {
97
- await promise;
98
- } catch (error) {
99
- // Allow retry on next request
100
- discoveredProjects.delete(key);
101
- logger.warn("AI discovery failed (will retry)", {
102
- projectSlug: ctx.projectSlug,
103
- error: error instanceof Error ? error.message : String(error),
104
- stack: error instanceof Error ? error.stack : undefined,
105
- });
106
- }
107
- }
108
-
109
25
  export class ApiHandlerWrapper extends BaseHandler {
110
26
  private projectDir: string;
111
27
  private adapter: import("../../../../platform/adapters/base.js").RuntimeAdapter;
@@ -0,0 +1,86 @@
1
+ import { serverLogger } from "../../../../utils/index.js";
2
+ import type { HandlerContext } from "../../types.js";
3
+
4
+ const logger = serverLogger.component("api-wrapper");
5
+
6
+ /**
7
+ * Tracks in-flight and completed AI discovery per project+release.
8
+ *
9
+ * Key: `{projectSlug}:{releaseId}` for production, `{projectSlug}:preview` for preview.
10
+ * This ensures a new deployment triggers re-discovery of agents/tools.
11
+ *
12
+ * Using a Map<string, Promise> deduplicates concurrent requests and
13
+ * allows retry on failure (the key is deleted if discovery rejects).
14
+ */
15
+ const discoveredProjects = new Map<string, Promise<void>>();
16
+
17
+ /** Build a discovery cache key that incorporates the release/version. */
18
+ function discoveryKey(ctx: HandlerContext): string {
19
+ const slug = ctx.projectSlug ?? ctx.projectDir;
20
+ const version = ctx.releaseId ?? "preview";
21
+ return `${slug}:${version}`;
22
+ }
23
+
24
+ /**
25
+ * Run AI discovery (agents, tools) for a project if not already done.
26
+ * Must be called within a runWithContext scope so the VFS can resolve
27
+ * the correct remote project files and the agent registry uses the
28
+ * correct project scope.
29
+ */
30
+ export async function ensureProjectDiscovery(ctx: HandlerContext): Promise<void> {
31
+ const key = discoveryKey(ctx);
32
+
33
+ const existing = discoveredProjects.get(key);
34
+ if (existing) return existing;
35
+
36
+ const promise = (async () => {
37
+ const { discoverAll } = await import("../../../../discovery/index.js");
38
+ const { agentRegistry } = await import(
39
+ "../../../../agent/composition/composition.js"
40
+ );
41
+ const { toolRegistry } = await import("../../../../tool/registry.js");
42
+
43
+ // Clear stale entries for this project scope before re-discovery.
44
+ // This prevents agents/tools removed in a new release from lingering.
45
+ agentRegistry.clear();
46
+ toolRegistry.clear();
47
+
48
+ const result = await discoverAll({
49
+ baseDir: ctx.projectDir,
50
+ fsAdapter: ctx.adapter.fs,
51
+ verbose: false,
52
+ });
53
+
54
+ const logData = {
55
+ projectSlug: ctx.projectSlug,
56
+ releaseId: ctx.releaseId,
57
+ agents: result.agents.size,
58
+ tools: result.tools.size,
59
+ errors: result.errors.length,
60
+ };
61
+
62
+ if (result.agents.size === 0 && result.tools.size === 0) {
63
+ logger.warn("AI discovery found 0 agents and 0 tools", {
64
+ ...logData,
65
+ errorMessages: result.errors.map((e) => e.error.message).slice(0, 5),
66
+ baseDir: ctx.projectDir,
67
+ });
68
+ } else {
69
+ logger.info("AI discovery completed", logData);
70
+ }
71
+ })();
72
+
73
+ discoveredProjects.set(key, promise);
74
+
75
+ try {
76
+ await promise;
77
+ } catch (error) {
78
+ // Allow retry on next request
79
+ discoveredProjects.delete(key);
80
+ logger.warn("AI discovery failed (will retry)", {
81
+ projectSlug: ctx.projectSlug,
82
+ error: error instanceof Error ? error.message : String(error),
83
+ stack: error instanceof Error ? error.stack : undefined,
84
+ });
85
+ }
86
+ }
@@ -0,0 +1,94 @@
1
+ import * as dntShim from "../../../../_dnt.shims.js";
2
+ import { BaseHandler } from "../response/base.js";
3
+ import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
4
+ import {
5
+ ChannelAssistantsRequestSchema,
6
+ type ChannelInvokeDeps,
7
+ defaultChannelInvokeDeps,
8
+ listChannelAssistants,
9
+ verifyDispatchJws,
10
+ } from "../../../channels/invoke.js";
11
+ import {
12
+ HTTP_INTERNAL_SERVER_ERROR,
13
+ PRIORITY_MEDIUM_API,
14
+ } from "../../../utils/constants/index.js";
15
+
16
+ const DISPATCH_JWS_HEADER = "x-veryfront-dispatch-jws";
17
+ const MAX_DISPATCH_SIGNATURE_AGE_SECONDS = 60;
18
+
19
+ export class ChannelAssistantsHandler extends BaseHandler {
20
+ metadata: HandlerMetadata = {
21
+ name: "ChannelAssistantsHandler",
22
+ priority: PRIORITY_MEDIUM_API as HandlerPriority,
23
+ patterns: [{ pattern: "/channels/assistants", exact: true, method: "POST" }],
24
+ };
25
+
26
+ constructor(private readonly deps: ChannelInvokeDeps = defaultChannelInvokeDeps) {
27
+ super();
28
+ }
29
+
30
+ async handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
31
+ if (!this.shouldHandle(req, ctx)) {
32
+ return this.continue();
33
+ }
34
+
35
+ return this.withProxyContext(ctx, async () => {
36
+ const builder = this.createResponseBuilder(ctx)
37
+ .withCORS(req, ctx.securityConfig?.cors)
38
+ .withSecurity(ctx.securityConfig ?? undefined, req);
39
+
40
+ const publicKeyPem = ctx.adapter.env.get("CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY");
41
+ if (!publicKeyPem) {
42
+ this.logWarn("Missing CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY for channel assistants endpoint");
43
+ return this.respond(
44
+ builder.json(
45
+ { error: "Channel dispatch verification is not configured" },
46
+ HTTP_INTERNAL_SERVER_ERROR,
47
+ ),
48
+ );
49
+ }
50
+
51
+ const projectSlug = ctx.projectSlug;
52
+ if (!projectSlug) {
53
+ this.logWarn("Channel assistants request arrived without resolved project slug");
54
+ return this.respond(builder.json({ error: "Project context is unavailable" }, 400));
55
+ }
56
+
57
+ const dispatchJws = req.headers.get(DISPATCH_JWS_HEADER);
58
+ if (!dispatchJws) {
59
+ return this.respond(builder.json({ error: "Missing dispatch signature" }, 401));
60
+ }
61
+
62
+ const rawBody = await req.text();
63
+ try {
64
+ await verifyDispatchJws(dispatchJws, rawBody, {
65
+ audience: projectSlug,
66
+ expectedProjectId: ctx.projectId,
67
+ publicKeyPem,
68
+ maxAgeSeconds: MAX_DISPATCH_SIGNATURE_AGE_SECONDS,
69
+ });
70
+ } catch (error) {
71
+ this.logWarn("Channel assistants signature verification failed", {
72
+ error: error instanceof Error ? error.message : String(error),
73
+ projectSlug,
74
+ projectId: ctx.projectId,
75
+ });
76
+ return this.respond(builder.json({ error: "Invalid dispatch signature" }, 401));
77
+ }
78
+
79
+ try {
80
+ ChannelAssistantsRequestSchema.parse(JSON.parse(rawBody));
81
+ } catch (error) {
82
+ this.logWarn("Channel assistants request validation failed", {
83
+ error: error instanceof Error ? error.message : String(error),
84
+ projectSlug,
85
+ projectId: ctx.projectId,
86
+ });
87
+ return this.respond(builder.json({ error: "Invalid channel assistants request" }, 400));
88
+ }
89
+
90
+ const response = await listChannelAssistants(ctx, this.deps);
91
+ return this.respond(builder.json(response, 200));
92
+ });
93
+ }
94
+ }
@@ -0,0 +1,95 @@
1
+ import * as dntShim from "../../../../_dnt.shims.js";
2
+ import { BaseHandler } from "../response/base.js";
3
+ import type { HandlerContext, HandlerMetadata, HandlerPriority, HandlerResult } from "../types.js";
4
+ import {
5
+ type ChannelInvokeDeps,
6
+ ChannelInvokeRequestSchema,
7
+ defaultChannelInvokeDeps,
8
+ executeChannelInvoke,
9
+ verifyDispatchJws,
10
+ } from "../../../channels/invoke.js";
11
+ import {
12
+ HTTP_INTERNAL_SERVER_ERROR,
13
+ PRIORITY_MEDIUM_API,
14
+ } from "../../../utils/constants/index.js";
15
+
16
+ const DISPATCH_JWS_HEADER = "x-veryfront-dispatch-jws";
17
+ const MAX_DISPATCH_SIGNATURE_AGE_SECONDS = 60;
18
+
19
+ export class ChannelInvokeHandler extends BaseHandler {
20
+ metadata: HandlerMetadata = {
21
+ name: "ChannelInvokeHandler",
22
+ priority: PRIORITY_MEDIUM_API as HandlerPriority,
23
+ patterns: [{ pattern: "/channels/invoke", exact: true, method: "POST" }],
24
+ };
25
+
26
+ constructor(private readonly deps: ChannelInvokeDeps = defaultChannelInvokeDeps) {
27
+ super();
28
+ }
29
+
30
+ async handle(req: dntShim.Request, ctx: HandlerContext): Promise<HandlerResult> {
31
+ if (!this.shouldHandle(req, ctx)) {
32
+ return this.continue();
33
+ }
34
+
35
+ return this.withProxyContext(ctx, async () => {
36
+ const builder = this.createResponseBuilder(ctx)
37
+ .withCORS(req, ctx.securityConfig?.cors)
38
+ .withSecurity(ctx.securityConfig ?? undefined, req);
39
+
40
+ const publicKeyPem = ctx.adapter.env.get("CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY");
41
+ if (!publicKeyPem) {
42
+ this.logWarn("Missing CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY for channel invoke endpoint");
43
+ return this.respond(
44
+ builder.json(
45
+ { error: "Channel dispatch verification is not configured" },
46
+ HTTP_INTERNAL_SERVER_ERROR,
47
+ ),
48
+ );
49
+ }
50
+
51
+ const projectSlug = ctx.projectSlug;
52
+ if (!projectSlug) {
53
+ this.logWarn("Channel invoke request arrived without resolved project slug");
54
+ return this.respond(builder.json({ error: "Project context is unavailable" }, 400));
55
+ }
56
+
57
+ const dispatchJws = req.headers.get(DISPATCH_JWS_HEADER);
58
+ if (!dispatchJws) {
59
+ return this.respond(builder.json({ error: "Missing dispatch signature" }, 401));
60
+ }
61
+
62
+ const rawBody = await req.text();
63
+ try {
64
+ await verifyDispatchJws(dispatchJws, rawBody, {
65
+ audience: projectSlug,
66
+ expectedProjectId: ctx.projectId,
67
+ publicKeyPem,
68
+ maxAgeSeconds: MAX_DISPATCH_SIGNATURE_AGE_SECONDS,
69
+ });
70
+ } catch (error) {
71
+ this.logWarn("Channel invoke signature verification failed", {
72
+ error: error instanceof Error ? error.message : String(error),
73
+ projectSlug,
74
+ projectId: ctx.projectId,
75
+ });
76
+ return this.respond(builder.json({ error: "Invalid dispatch signature" }, 401));
77
+ }
78
+
79
+ let payload;
80
+ try {
81
+ payload = ChannelInvokeRequestSchema.parse(JSON.parse(rawBody));
82
+ } catch (error) {
83
+ this.logWarn("Channel invoke request validation failed", {
84
+ error: error instanceof Error ? error.message : String(error),
85
+ projectSlug,
86
+ projectId: ctx.projectId,
87
+ });
88
+ return this.respond(builder.json({ error: "Invalid channel invoke request" }, 400));
89
+ }
90
+
91
+ const response = await executeChannelInvoke(payload, ctx, this.deps);
92
+ return this.respond(builder.json(response, 200));
93
+ });
94
+ }
95
+ }
@@ -53,6 +53,8 @@ import { HMRHandler } from "../handlers/preview/hmr.handler.js";
53
53
  import { MarkdownPreviewHandler } from "../handlers/preview/markdown-preview.handler.js";
54
54
  import { OpenAPIHandler } from "../handlers/request/openapi.handler.js";
55
55
  import { OpenAPIDocsHandler } from "../handlers/request/openapi-docs.handler.js";
56
+ import { ChannelAssistantsHandler } from "../handlers/request/channel-assistants.handler.js";
57
+ import { ChannelInvokeHandler } from "../handlers/request/channel-invoke.handler.js";
56
58
  import { DevDashboardHandler } from "../handlers/dev/dashboard/index.js";
57
59
  import { ProjectsHandler } from "../handlers/dev/projects/index.js";
58
60
 
@@ -204,6 +206,8 @@ export function createVeryfrontHandler(
204
206
  new DebugContextHandler(),
205
207
  new OpenAPIHandler(),
206
208
  new OpenAPIDocsHandler(),
209
+ new ChannelAssistantsHandler(),
210
+ new ChannelInvokeHandler(),
207
211
  new DevDashboardHandler(),
208
212
  new ProjectsHandler(),
209
213
  new StudioBridgeModulesHandler(),
@@ -5,6 +5,8 @@ import remarkFrontmatter from "remark-frontmatter";
5
5
  import remarkRehype from "remark-rehype";
6
6
  import rehypeStarryNight from "rehype-starry-night";
7
7
  import rehypeSlug from "rehype-slug";
8
+ import rehypeRaw from "rehype-raw";
9
+ import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
8
10
  import rehypeStringify from "rehype-stringify";
9
11
  import { visit } from "unist-util-visit";
10
12
  import { toString } from "mdast-util-to-string";
@@ -98,12 +100,36 @@ export function compileMarkdownRuntime(
98
100
  .use(rehypeStarryNight)
99
101
  .use(rehypeSlug);
100
102
 
103
+ // Parse raw HTML nodes into proper elements before sanitizing.
104
+ pipeline.use(rehypeRaw);
105
+
106
+ // Add node positions after rehype-raw so attributes survive re-parsing.
101
107
  if (studioEmbed && filePath) {
102
108
  pipeline.use(rehypeNodePositions, { filePath });
103
109
  }
104
110
 
111
+ // Extend the sanitize schema in studio embed mode to preserve
112
+ // data-node-* attributes used for element-to-source mapping.
113
+ const sanitizeSchema = studioEmbed
114
+ ? {
115
+ ...defaultSchema,
116
+ attributes: {
117
+ ...defaultSchema.attributes,
118
+ "*": [
119
+ ...(defaultSchema.attributes?.["*"] ?? []),
120
+ "data-node-file",
121
+ "data-node-name",
122
+ "data-node-line",
123
+ "data-node-column",
124
+ "data-node-source",
125
+ ],
126
+ },
127
+ }
128
+ : defaultSchema;
129
+
105
130
  const result = await pipeline
106
- .use(rehypeStringify, { allowDangerousHtml: true })
131
+ .use(rehypeSanitize, sanitizeSchema)
132
+ .use(rehypeStringify)
107
133
  .process(body);
108
134
  const html = String(result);
109
135