Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,50 @@
import { describe, expect, it, vi } from "vitest";

import {
createOpenClawQrTerminalLoaderSource,
isQrcodePackage,
isQrcodeTerminalPackage,
isOpenClawQrTerminalRendererSource,
isReviewedOpenClawQrTerminalRendererIntegrity,
patchOpenClawQrTerminalRendererSource,
patchQrcode,
patchQrcodeTerminal,
REVIEWED_OPENCLAW_QR_TERMINAL_RENDERER_SHA256,
} from "./whatsapp-qr-compact";
import { makeQrcodeLoadHook } from "./whatsapp-qr-compact-test-helpers";

const OPENCLAW_QR_RENDERER_SOURCE = `
const COMPACT_MARGIN_MODULES = 1;
function readModule(modules, x, y) {
if (x < 0 || y < 0 || x >= modules.size || y >= modules.size) return false;
return Boolean(modules.data[y * modules.size + x]);
}
function compactBlock(top, bottom) {
if (top && bottom) return "█";
if (top) return "▀";
if (bottom) return "▄";
return " ";
}
function renderCompactTerminalQr(modules) {
const lines = [];
for (let y = -1; y < modules.size + COMPACT_MARGIN_MODULES; y += 2) {
let line = "";
for (let x = -1; x < modules.size + COMPACT_MARGIN_MODULES; x += 1) line += compactBlock(readModule(modules, x, y), readModule(modules, x, y + 1));
lines.push(line);
}
return lines.join("\\n");
}
async function renderQrTerminal(input, opts = {}) {
const text = normalizeQrText(input);
const qrCode = await loadQrCodeRuntime();
if (opts.small === true) return renderCompactTerminalQr(qrCode.create(text).modules);
return await qrCode.toString(text, {
small: false,
type: "terminal"
});
}
`;

// A fake of the `qrcode` package main: has its OWN toString + create().
function makeQrcodeFake() {
const calls: Array<{ text: unknown; opts: unknown; cb: unknown }> = [];
Expand Down Expand Up @@ -76,6 +113,61 @@ describe("isQrcodeTerminalPackage (#4522)", () => {
});
});

describe("patchOpenClawQrTerminalRendererSource (#4522)", () => {
it("detects the OpenClaw QR terminal renderer by the old patch selectors", () => {
expect(isOpenClawQrTerminalRendererSource(OPENCLAW_QR_RENDERER_SOURCE)).toBe(true);
expect(isOpenClawQrTerminalRendererSource("const unrelated = true;")).toBe(false);
});

it("recognizes the reviewed OpenClaw 2026.6.10 renderer integrity", () => {
expect(
isReviewedOpenClawQrTerminalRendererIntegrity(REVIEWED_OPENCLAW_QR_TERMINAL_RENDERER_SHA256),
).toBe(true);
expect(isReviewedOpenClawQrTerminalRendererIntegrity("0".repeat(64))).toBe(false);
});

it("widens the compact-renderer quiet zone on all four edges", () => {
const patched = patchOpenClawQrTerminalRendererSource(OPENCLAW_QR_RENDERER_SOURCE);

expect(patched).toContain("const COMPACT_MARGIN_MODULES = 4;");
expect(patched).toContain(
"for (let y = -COMPACT_MARGIN_MODULES; y < modules.size + COMPACT_MARGIN_MODULES; y += 2)",
);
expect(patched).toContain(
"for (let x = -COMPACT_MARGIN_MODULES; x < modules.size + COMPACT_MARGIN_MODULES; x += 1)",
);
expect(patched).not.toContain("toDataURL");
expect(patched).not.toContain("data:image/png");
});

it("fails closed for an unreviewed renderer integrity", () => {
expect(patchOpenClawQrTerminalRendererSource(OPENCLAW_QR_RENDERER_SOURCE, "0".repeat(64))).toBe(
OPENCLAW_QR_RENDERER_SOURCE,
);
});

it("fails closed instead of partially patching when a loop preimage drifts", () => {
const drifted = OPENCLAW_QR_RENDERER_SOURCE.replace("let x = -1;", "let x = 0;");

expect(patchOpenClawQrTerminalRendererSource(drifted)).toBe(drifted);
});

it("is idempotent once the source already has the four-edge quiet-zone patch", () => {
const patched = patchOpenClawQrTerminalRendererSource(OPENCLAW_QR_RENDERER_SOURCE);

expect(patchOpenClawQrTerminalRendererSource(patched)).toBe(patched);
});

it("loader source computes renderer integrity before applying the source rewrite", () => {
const loader = createOpenClawQrTerminalLoaderSource();

expect(loader).toContain('import { createHash } from "node:crypto";');
expect(loader).toContain(REVIEWED_OPENCLAW_QR_TERMINAL_RENDERER_SHA256);
expect(loader).toContain("const integrity = sha256Hex(source);");
expect(loader).toContain("patchOpenClawQrTerminalRendererSource(source, integrity)");
});
});

describe("patchQrcode (#4522)", () => {
it("forces small:true only for terminal renders", () => {
const mod = makeQrcodeFake();
Expand Down
119 changes: 119 additions & 0 deletions src/lib/messaging/channels/whatsapp/runtime/whatsapp-qr-compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
// left untouched, and a caller that already opts into `small` is a no-op.
// * `qrcode-terminal` (has `generate`): force `small: true` as well, so the
// fix also covers any agent/path that renders through that package.
// * the reviewed OpenClaw QR renderer ES module: widen the compact-renderer
// quiet zone on all four edges. This is hash-gated to the reviewed
// OpenClaw 2026.6.10 renderer so a drifted upstream bundle fails closed.
// The QR text and error-correction level are never altered — only the
// terminal cell packing — so the rendered code is identical apart from size.
//
Expand All @@ -57,6 +60,94 @@ function hasOwn(mod, name) {
return mod && Object.prototype.hasOwnProperty.call(mod, name);
}

function isOpenClawQrTerminalRendererSource(source) {
return (
typeof source === "string" &&
source.indexOf("renderCompactTerminalQr") !== -1 &&
source.indexOf("COMPACT_MARGIN_MODULES") !== -1 &&
source.indexOf("async function renderQrTerminal") !== -1 &&
source.indexOf("if (opts.small === true) return renderCompactTerminalQr") !== -1
);
}

var REVIEWED_OPENCLAW_QR_TERMINAL_RENDERER_SHA256 =
"f74865035a498389fe910b23537a7dffeaee1b05e044999d855b61c96af0ada7";

function isReviewedOpenClawQrTerminalRendererIntegrity(integrity: string | undefined) {
return integrity === REVIEWED_OPENCLAW_QR_TERMINAL_RENDERER_SHA256;
}

function patchOpenClawQrTerminalRendererSource(source: string, integrity?: string) {
if (!isOpenClawQrTerminalRendererSource(source)) return source;
if (
integrity !== undefined &&
integrity !== null &&
!isReviewedOpenClawQrTerminalRendererIntegrity(integrity)
) {
return source;
}

var marginFrom = "const COMPACT_MARGIN_MODULES = 1;";
var marginTo = "const COMPACT_MARGIN_MODULES = 4;";
var yLoopFrom = "for (let y = -1; y < modules.size + COMPACT_MARGIN_MODULES; y += 2)";
var yLoopTo =
"for (let y = -COMPACT_MARGIN_MODULES; y < modules.size + COMPACT_MARGIN_MODULES; y += 2)";
var xLoopFrom = "for (let x = -1; x < modules.size + COMPACT_MARGIN_MODULES; x += 1)";
var xLoopTo =
"for (let x = -COMPACT_MARGIN_MODULES; x < modules.size + COMPACT_MARGIN_MODULES; x += 1)";

var alreadyPatched =
source.indexOf(marginTo) !== -1 &&
source.indexOf(yLoopTo) !== -1 &&
source.indexOf(xLoopTo) !== -1;
if (alreadyPatched) return source;

var hasExactPreimage =
source.indexOf(marginFrom) !== -1 &&
source.indexOf(yLoopFrom) !== -1 &&
source.indexOf(xLoopFrom) !== -1;
if (!hasExactPreimage) return source;

return source
.replace(marginFrom, marginTo)
.replace(yLoopFrom, yLoopTo)
.replace(xLoopFrom, xLoopTo);
}

function createOpenClawQrTerminalLoaderSource() {
return `
import { createHash } from "node:crypto";

const REVIEWED_OPENCLAW_QR_TERMINAL_RENDERER_SHA256 = ${JSON.stringify(
REVIEWED_OPENCLAW_QR_TERMINAL_RENDERER_SHA256,
)};
const isOpenClawQrTerminalRendererSource = ${isOpenClawQrTerminalRendererSource.toString()};
const isReviewedOpenClawQrTerminalRendererIntegrity = ${isReviewedOpenClawQrTerminalRendererIntegrity.toString()};
const patchOpenClawQrTerminalRendererSource = ${patchOpenClawQrTerminalRendererSource.toString()};

function decodeSource(source) {
if (typeof source === "string") return source;
if (source && typeof Buffer !== "undefined") return Buffer.from(source).toString("utf8");
return "";
}

function sha256Hex(source) {
return createHash("sha256").update(source).digest("hex");
}

export async function load(url, context, nextLoad) {
const result = await nextLoad(url, context);
if (!result || result.format !== "module") return result;
const source = decodeSource(result.source);
if (!isOpenClawQrTerminalRendererSource(source)) return result;
const integrity = sha256Hex(source);
const patched = patchOpenClawQrTerminalRendererSource(source, integrity);
if (patched === source) return result;
return { ...result, source: patched };
}
`;
}

// `qrcode` package main: renderQrTerminal() calls qrcode.toString(text, opts).
// Require an OWN toString (every object inherits Object.prototype.toString, so
// a plain `typeof mod.toString` check would also match qrcode's internal
Expand Down Expand Up @@ -155,11 +246,38 @@ export {
hasOwn,
isQrcodePackage,
isQrcodeTerminalPackage,
isReviewedOpenClawQrTerminalRendererIntegrity,
isOpenClawQrTerminalRendererSource,
patchOpenClawQrTerminalRendererSource,
REVIEWED_OPENCLAW_QR_TERMINAL_RENDERER_SHA256,
createOpenClawQrTerminalLoaderSource,
patchQrcode,
patchQrcodeTerminal,
resolvePatchedModule,
};

function installOpenClawQrTerminalSourceLoader(Module) {
if (process.__nemoclawWhatsappQrCompactSourceLoaderInstalled) return;
if (!Module || typeof Module.register !== "function") return;
try {
Object.defineProperty(process, "__nemoclawWhatsappQrCompactSourceLoaderInstalled", {
value: true,
});
} catch (_e) {
process.__nemoclawWhatsappQrCompactSourceLoaderInstalled = true;
}

try {
var loaderSource = createOpenClawQrTerminalLoaderSource();
var loaderUrl =
"data:text/javascript;base64," + Buffer.from(loaderSource, "utf8").toString("base64");
Module.register(loaderUrl);
} catch (_e) {
// The qrcode small-mode hook below is still useful even if this Node
// runtime cannot register ESM loader hooks.
}
}

// Install the Module._load hook that patches qrcode / qrcode-terminal on load.
// Guarded so double-require is a no-op. Runs on import (the file is loaded via
// `--require`/preload), preserving the previous self-installing IIFE behavior.
Expand All @@ -172,6 +290,7 @@ function installWhatsappQrCompactHook() {
}

var Module = require("module");
installOpenClawQrTerminalSourceLoader(Module);
var origLoad = Module._load;

Module._load = function (request, _parent, _isMain) {
Expand Down
Loading
Loading