Files
flights_web/src/server/middleware/nonce-stream-transform.ts
T

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}>`;
});
}