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.
- 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 +427 -0
- package/esm/src/channels/invoke.d.ts.map +1 -0
- package/esm/src/channels/invoke.js +401 -0
- package/esm/src/embedding/embedding.d.ts.map +1 -1
- package/esm/src/embedding/embedding.js +5 -1
- package/esm/src/embedding/rag-store.js +2 -0
- package/esm/src/embedding/vector-store.d.ts.map +1 -1
- package/esm/src/embedding/vector-store.js +2 -0
- package/esm/src/embedding/veryfront-cloud/rag-store.d.ts.map +1 -1
- package/esm/src/embedding/veryfront-cloud/rag-store.js +2 -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-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 +2 -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 +521 -0
- package/src/src/embedding/embedding.ts +5 -1
- package/src/src/embedding/rag-store.ts +1 -0
- package/src/src/embedding/vector-store.ts +1 -0
- package/src/src/embedding/veryfront-cloud/rag-store.ts +1 -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-invoke.handler.ts +95 -0
- package/src/src/server/runtime-handler/index.ts +2 -0
- package/src/src/transforms/md/compiler/md-compiler.ts +27 -1
|
@@ -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,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,7 @@ 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 { ChannelInvokeHandler } from "../handlers/request/channel-invoke.handler.js";
|
|
56
57
|
import { DevDashboardHandler } from "../handlers/dev/dashboard/index.js";
|
|
57
58
|
import { ProjectsHandler } from "../handlers/dev/projects/index.js";
|
|
58
59
|
|
|
@@ -204,6 +205,7 @@ export function createVeryfrontHandler(
|
|
|
204
205
|
new DebugContextHandler(),
|
|
205
206
|
new OpenAPIHandler(),
|
|
206
207
|
new OpenAPIDocsHandler(),
|
|
208
|
+
new ChannelInvokeHandler(),
|
|
207
209
|
new DevDashboardHandler(),
|
|
208
210
|
new ProjectsHandler(),
|
|
209
211
|
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
|
|