Allow passing CSP nonce to exported html (#12088)

* Allow passing CSP nonce to exported html

* test: Add nonce regression test, drop options from tags

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-04-17 09:00:34 -04:00
committed by GitHub
parent cbb53285a7
commit 60903fef84
4 changed files with 45 additions and 2 deletions
+10 -1
View File
@@ -33,10 +33,18 @@ const getBucketOrigin = () => {
}
};
interface CSPOptions {
/** Additional origins to allow as script sources. */
extraScriptSrc?: string[];
}
/**
* Create a Content Security Policy middleware for the application.
*
* @param options Optional configuration for the CSP middleware.
* @returns A Koa middleware function that applies the CSP headers.
*/
export default function createCSPMiddleware() {
export default function createCSPMiddleware(options?: CSPOptions) {
// Construct scripts CSP based on options in use
const defaultSrc: string[] = ["'self'"];
const scriptSrc: string[] = [];
@@ -83,6 +91,7 @@ export default function createCSPMiddleware() {
styleSrc,
scriptSrc: [
...uniq(scriptSrc),
...(options?.extraScriptSrc ?? []),
env.DEVELOPMENT_UNSAFE_INLINE_CSP
? "'unsafe-inline'"
: `'nonce-${ctx.state.cspNonce}'`,
@@ -100,6 +100,32 @@ describe("DocumentHelper", () => {
expect(result).toContain('<p dir="auto">This is a test paragraph</p>');
});
it("should apply the cspNonce to the injected mermaid script", async () => {
const document = await buildDocument({
text: "```mermaid\ngraph TD;\nA-->B;\n```",
});
const result = await DocumentHelper.toHTML(document, {
includeTitle: false,
includeStyles: false,
includeMermaid: true,
cspNonce: "test-nonce-123",
});
expect(result).toMatch(/<script[^>]*nonce="test-nonce-123"/);
expect(result).toContain('window.status = "ready"');
});
it("should not set a nonce attribute when cspNonce is not provided", async () => {
const document = await buildDocument({
text: "```mermaid\ngraph TD;\nA-->B;\n```",
});
const result = await DocumentHelper.toHTML(document, {
includeTitle: false,
includeStyles: false,
includeMermaid: true,
});
expect(result).not.toMatch(/<script[^>]*nonce="/);
});
it("should render diff classes when changes provided", async () => {
const doc1 = await buildDocument({ text: "Hello world" });
const doc2 = await buildDocument({ text: "Hello modified world" });
+3 -1
View File
@@ -68,6 +68,8 @@ type HTMLOptions = {
baseUrl?: string;
/** Changes to highlight in the document */
changes?: readonly ExtendedChange[];
/** CSP nonce to apply to injected inline scripts */
cspNonce?: string;
};
@trace()
@@ -257,12 +259,12 @@ export class DocumentHelper {
centered: options?.centered,
baseUrl: options?.baseUrl,
changes: options?.changes,
cspNonce: options?.cspNonce,
});
addTags({
collectionId: model instanceof Collection ? model.id : undefined,
documentId: !(model instanceof Collection) ? model.id : undefined,
options,
});
if (options?.signedUrls) {
@@ -50,6 +50,8 @@ export type HTMLOptions = {
baseUrl?: string;
/** Changes to highlight in the document */
changes?: readonly ExtendedChange[];
/** CSP nonce to apply to injected inline scripts */
cspNonce?: string;
};
export type MentionAttrs = {
@@ -558,6 +560,10 @@ export class ProsemirrorHelper extends SharedProsemirrorHelper {
const element = dom.window.document.createElement("script");
element.setAttribute("type", "module");
if (options?.cspNonce) {
element.setAttribute("nonce", options.cspNonce);
}
// Inject Mermaid script
if (mermaidElements.length) {
element.innerHTML = `