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.
- package/esm/cli/templates/manifest.js +37 -37
- package/esm/deno.d.ts +3 -0
- package/esm/deno.js +6 -3
- package/esm/src/agent/composition/composition.d.ts.map +1 -1
- package/esm/src/agent/composition/composition.js +13 -3
- package/esm/src/agent/factory.d.ts.map +1 -1
- package/esm/src/agent/factory.js +3 -3
- package/esm/src/agent/middleware/security/validator.d.ts +92 -0
- package/esm/src/agent/middleware/security/validator.d.ts.map +1 -0
- package/esm/src/agent/middleware/security/validator.js +187 -0
- package/esm/src/agent/runtime/index.d.ts +3 -2
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +16 -8
- package/esm/src/agent/types.d.ts +4 -0
- package/esm/src/agent/types.d.ts.map +1 -1
- package/esm/src/channels/invoke.d.ts +491 -0
- package/esm/src/channels/invoke.d.ts.map +1 -0
- package/esm/src/channels/invoke.js +417 -0
- package/esm/src/embedding/embedding.js +2 -2
- package/esm/src/integrations/endpoint-executor.d.ts +1 -0
- package/esm/src/integrations/endpoint-executor.d.ts.map +1 -1
- package/esm/src/integrations/endpoint-executor.js +44 -0
- package/esm/src/integrations/schema.d.ts +2 -2
- package/esm/src/oauth/handlers/init-handler.d.ts +6 -2
- package/esm/src/oauth/handlers/init-handler.d.ts.map +1 -1
- package/esm/src/oauth/handlers/init-handler.js +8 -2
- package/esm/src/platform/compat/opaque-deps.d.ts.map +1 -1
- package/esm/src/platform/compat/opaque-deps.js +10 -1
- package/esm/src/prompt/factory.d.ts.map +1 -1
- package/esm/src/prompt/factory.js +9 -1
- package/esm/src/react/components/ai/markdown.d.ts.map +1 -1
- package/esm/src/react/components/ai/markdown.js +4 -4
- package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/framework-candidates.generated.js +5 -4
- package/esm/src/server/handlers/preview/markdown-html-generator.js +1 -1
- package/esm/src/server/handlers/request/api/api-handler-wrapper.d.ts.map +1 -1
- package/esm/src/server/handlers/request/api/api-handler-wrapper.js +1 -74
- package/esm/src/server/handlers/request/api/project-discovery.d.ts +9 -0
- package/esm/src/server/handlers/request/api/project-discovery.d.ts.map +1 -0
- package/esm/src/server/handlers/request/api/project-discovery.js +74 -0
- package/esm/src/server/handlers/request/channel-assistants.handler.d.ts +11 -0
- package/esm/src/server/handlers/request/channel-assistants.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/channel-assistants.handler.js +71 -0
- package/esm/src/server/handlers/request/channel-invoke.handler.d.ts +11 -0
- package/esm/src/server/handlers/request/channel-invoke.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/channel-invoke.handler.js +72 -0
- package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/index.js +4 -0
- package/esm/src/transforms/md/compiler/md-compiler.d.ts.map +1 -1
- package/esm/src/transforms/md/compiler/md-compiler.js +25 -1
- package/package.json +3 -1
- package/src/cli/templates/manifest.js +37 -37
- package/src/deno.js +6 -3
- package/src/src/agent/composition/composition.ts +15 -3
- package/src/src/agent/factory.ts +19 -6
- package/src/src/agent/middleware/security/validator.ts +288 -0
- package/src/src/agent/runtime/index.ts +26 -3
- package/src/src/agent/types.ts +4 -0
- package/src/src/channels/invoke.ts +546 -0
- package/src/src/embedding/embedding.ts +2 -2
- package/src/src/integrations/endpoint-executor.ts +51 -0
- package/src/src/oauth/handlers/init-handler.ts +20 -5
- package/src/src/platform/compat/opaque-deps.ts +19 -4
- package/src/src/prompt/factory.ts +10 -1
- package/src/src/react/components/ai/markdown.tsx +5 -4
- package/src/src/server/handlers/dev/framework-candidates.generated.ts +5 -4
- package/src/src/server/handlers/preview/markdown-html-generator.ts +1 -1
- package/src/src/server/handlers/request/api/api-handler-wrapper.ts +1 -85
- package/src/src/server/handlers/request/api/project-discovery.ts +86 -0
- package/src/src/server/handlers/request/channel-assistants.handler.ts +94 -0
- package/src/src/server/handlers/request/channel-invoke.handler.ts +95 -0
- package/src/src/server/runtime-handler/index.ts +4 -0
- 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
|
-
|
|
62
|
-
|
|
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 =
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
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 {
|
|
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(
|
|
131
|
+
.use(rehypeSanitize, sanitizeSchema)
|
|
132
|
+
.use(rehypeStringify)
|
|
107
133
|
.process(body);
|
|
108
134
|
const html = String(result);
|
|
109
135
|
|