69 lines
2.0 KiB
TypeScript
69 lines
2.0 KiB
TypeScript
import { Transform } from "node:stream";
|
|
|
|
/**
|
|
* Regex matching `<script` opening tags.
|
|
* Captures everything between `<script` and `>` to check for existing nonce.
|
|
*/
|
|
const SCRIPT_OPEN_RE = /<script(\s[^>]*)?\s*>/gi;
|
|
|
|
/**
|
|
* Wraps an SSR HTML stream to inject `nonce="..."` on every `<script>` tag
|
|
* that doesn't already have a nonce attribute.
|
|
*
|
|
* Workaround for React issue #24883: renderToPipeableStream({ nonce }) only
|
|
* applies the nonce to inline bootstrapScriptContent, not to external
|
|
* bootstrapScripts src URLs.
|
|
*/
|
|
export function wrapSsrStreamWithNonce(
|
|
stream: NodeJS.ReadableStream,
|
|
nonce: string,
|
|
): NodeJS.ReadableStream {
|
|
let buffer = "";
|
|
|
|
const transform = new Transform({
|
|
decodeStrings: false,
|
|
|
|
transform(chunk: Buffer | string, _encoding, callback) {
|
|
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
|
|
// Keep a trailing partial `<script...` that might be split across chunks.
|
|
// Find the last `<scr` that might be an incomplete tag.
|
|
const lastOpenBracket = buffer.lastIndexOf("<scr");
|
|
if (lastOpenBracket !== -1 && !buffer.includes(">", lastOpenBracket)) {
|
|
// Incomplete tag — hold the partial in the buffer
|
|
const ready = buffer.slice(0, lastOpenBracket);
|
|
buffer = buffer.slice(lastOpenBracket);
|
|
this.push(injectNonce(ready, nonce));
|
|
} else {
|
|
// No incomplete tag — flush everything
|
|
this.push(injectNonce(buffer, nonce));
|
|
buffer = "";
|
|
}
|
|
|
|
callback();
|
|
},
|
|
|
|
flush(callback) {
|
|
if (buffer.length > 0) {
|
|
this.push(injectNonce(buffer, nonce));
|
|
buffer = "";
|
|
}
|
|
callback();
|
|
},
|
|
});
|
|
|
|
stream.pipe(transform);
|
|
return transform;
|
|
}
|
|
|
|
function injectNonce(html: string, nonce: string): string {
|
|
return html.replace(SCRIPT_OPEN_RE, (match, attrs: string | undefined) => {
|
|
const attrStr = attrs ?? "";
|
|
// Don't inject if nonce already present
|
|
if (/\bnonce\s*=/i.test(attrStr)) {
|
|
return match;
|
|
}
|
|
return `<script nonce="${nonce}"${attrStr}>`;
|
|
});
|
|
}
|