Repository navigation
fix(service-print): a server print works under the console's default CSP (the ready wait passes a function, not a string) - #26783
Conversation
…t CSP on both placements Claude-Session: https://claude.ai/code/session_01FEkQ5kpFFUuQY5FA7FCdwY Co-authored-by: Claude <noreply@anthropic.com>
…n with the stable-poll count Claude-Session: https://claude.ai/code/session_01FEkQ5kpFFUuQY5FA7FCdwY Co-authored-by: Claude <noreply@anthropic.com>
…, so a print runs under the console's default CSP playwright-core evaluates a string predicate with eval on every poll, from a page timer the page's CSP governs; without 'unsafe-eval' the second poll threw EvalError and every server print failed. A function predicate is evaluated once, inside the DevTools call that starts the wait, and each poll calls it. Same predicate, same timeout and polling; the stable-poll count is now its argument. Claude-Session: https://claude.ai/code/session_01FEkQ5kpFFUuQY5FA7FCdwY Co-authored-by: Claude <noreply@anthropic.com>
… CSP; keep the unit test inside the package's lib Claude-Session: https://claude.ai/code/session_01FEkQ5kpFFUuQY5FA7FCdwY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f23682269958467fb4e615289bbf80f46b26b81f && git checkout f23682269958467fb4e615289bbf80f46b26b81f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 785a7c3b3e31a669ef52e0d83bc18916d170ae9c dfd3fc087575ed9e16be6e1678acca0ab21e561d && git checkout -B drift-repro 785a7c3b3e31a669ef52e0d83bc18916d170ae9c && git merge --no-ff dfd3fc087575ed9e16be6e1678acca0ab21e561d
node scripts/docs-audit/affected-docs.mjs --json 785a7c3b3e31a669ef52e0d83bc18916d170ae9c |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #26664 under triage 6442489247 (p1). 4 files in packages/services/service-print/src/: chromium-driver.ts, its two tests, and a patch changeset. Clause-②: no and Metadata-protocol: none appear in both the body and the changeset. |
| Fix | The ready wait passes a function (printReady, with STABLE_POLLS as its argument) instead of a string. Playwright evaluates a string predicate again on every poll from a page timer, where the page's CSP applies, so under the console's default script-src (no 'unsafe-eval') every print failed with EvalError. A function's text is evaluated once inside the DevTools call that starts the wait, which the page's CSP does not govern. The predicate's meaning, timeout and polling are unchanged; the console CSP is untouched, as the triage requires. The FONTS_READY string stays: evaluate runs it once through DevTools, not from a page timer. |
| Evidence | CI green on head dfd3fc0875 (33 success, 11 skipped, 0 failed). Governed check: NOT governed, 142 changed lines. Red on main under a default-CSP fixture: the driver test 4 failed (EvalError, both placements) and the render dogfood 6 of 12; green at head: driver 20/20, dogfood 12/12, service-print 118/118, with OS_PAGE_REACT_ENABLED unset. 59 of 59 derived gates ran, 0 NOT-MEASURED. The unit test pins that the driver hands waitForFunction a function, so CI guards the regression although it skips the real render. |
| Downstream | With #26583 (PR #26662), this unblocks hotcrm #2312 once both ship in a release. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #26664
Clause-②: no
Metadata-protocol: none
维护者速读
'unsafe-eval',字符串谓词每次轮询都要在页面里 eval,第二次轮询即抛 EvalError,所有服务端打印都失败。chromium-driver.ts一处等待;就绪含义、超时、轮询间隔不变,控制台 CSP 不变。回滚即还原该文件。packages/services/service-print/src/chromium-driver.ts(printReady与waitForFunction那一行);chromium-driver.unit.test.ts末尾新增的两条。OS_TEST_PRINT_CHROMIUM_PATH=/opt/pw-browsers/chromium pnpm --filter @objectstack/service-print exec vitest run src/chromium-driver.test.ts,应见 CSP 那条在两种放置下都通过(main 上是 EvalError)。What changes
packages/services/service-print/src/chromium-driver.tswaited for the print page withpage.waitForFunction(PRINT_READY_PREDICATE, …), where the predicate was a string. It is now a function,printReady(stablePolls), passed aspage.waitForFunction(printReady, STABLE_POLLS, { timeout: remaining(), polling: POLL_INTERVAL_MS }).Which evaluation path, and why
script-srcdoes not block it (read in playwright-core 1.63.0,lib/coreBundle.jswaitForFunctionExpression, and measured below). The wait installs a poller through the injected script'sevaluateHandle, a DevToolsRuntime.callFunctionOn. The poller'spredicate()runsglobalThis.eval(expression):isFunction: false), on every poll. The first poll runs synchronously inside the DevTools call, where the inspector permits code generation; every later poll runs from a pagesetTimeout, where the page's CSP governs, and without'unsafe-eval'it throws EvalError. The predicate needs three stable polls, so the second poll always fails.isFunction: true), once: the first, synchronous poll caches the evaluated function (evaledExpression), and every later poll calls that cached function. No string is evaluated from a page timer, so the CSP has nothing to refuse.The readiness meaning is unchanged: the same three conditions (a
[data-print-page]root,window.__objectuipresent,idle === true), the same in-page run counter (__objectstackPrintStable), the sameSTABLE_POLLS(3), the same 100 ms polling and the same share of the one render budget. The page receives the function's text only, so the poll count it used to read from a template literal now arrives as its argument. The built output was read: tsup emitsprintReadyas plain ES2020 in bothdist/index.jsanddist/index.cjs, with no injected helper names.The console CSP (
packages/cli/src/utils/console.ts) is not touched.page.evaluate(FONTS_READY)stays a string: it is evaluated synchronously inside its DevTools call, and the CSP leg below shows it passing.A
patchchangeset on@objectstack/service-print.Tests
chromium-driver.unit.test.ts(runs in CI, no browser): the staged page'swaitForFunctionmust receive a function,STABLE_POLLSas its argument,polling: 100and a timeout inside the budget. A second case runs the received function against a stageddocument/__objectuiand pins the meaning: false with no console, false when idle with no root, false with the root while busy, then exactlySTABLE_POLLSidle polls to true, and a busy poll resets the run.chromium-driver.test.ts(real Chromium, both placements): the fixture now also serves the print page under the console's default policy (script-src 'self' 'nonce-…' 'wasm-unsafe-eval', the script carrying the nonce), and a new case per placement renders it.packages/qa/dogfood/test/print-service-render.dogfood.test.ts, which carries the named-page leg from feat(service-print): print a record on any print page of its object, not only its default record page #26662) is unchanged; it is the end-to-end reading below.Acceptance notes
describe.skipIf(!CHROMIUM)/!CHROMIUM || !CONSOLE_BUILT): Test Core has no Chromium and no console bundle. The unit test is the guard CI runs. Turning the render tests on in CI is a CI-provisioning question and is not done here.page.evaluate(FONTS_READY)and the predicate's first poll rely on DevTools-initiated evaluation being exempt from the page's'unsafe-eval'check, which is Chromium's behaviour today and is what playwright-core's own function-predicate path relies on.Verification
All readings at head
dfd3fc087, withOS_PAGE_REACT_ENABLED(and legacyOS_PAGE_REACT) unset, Chromium/opt/pw-browsers/chromium, console bundle frompnpm console:build.Red on main's driver (base 1685a06): EvalError
chromium-driver.tsbyte-equal toorigin/main, the new tests committed:vitest run src/chromium-driver.unit.test.ts src/chromium-driver.test.ts: Tests 4 failed, 16 passed (20). Both unit cases fail; the CSP case fails on both placements withThe print engine failed at the local executable /opt/pw-browsers/chromium: page.waitForFunction: EvalError: Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script: script-src 'self' 'nonce-print-fixture-nonce' 'wasm-unsafe-eval'(and the same at the remote endpoint).print-service-render.dogfood.test.ts+print-service-named-page.dogfood.test.ts: Tests 6 failed, 6 passed (12). All 6 render legs (invoice, named print page, revoke-after-failure; local and remote) fail with the EvalError under the console's real nonce policy. The named-page file uses a stand-in driver and passes either way.Green with the fix
pnpm --filter @objectstack/service-print build,typecheck: exit 0.test(with Chromium): Test Files 8 passed, Tests 118 passed (118).pnpm exec eslint --no-inline-config packages/services/service-print: exit 0.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatdfd3fc087(4 paths, +129 / -13): 59 commands, plus the 4 roster gates it marks as keeping a roster under one of these paths (check:authz-resolver,check:changeset-fixed,check:error-code-casing,check:filter-alias-parity). Each run alone, exit code captured before any pipe:node scripts/check-plugin-teardown-shape.mjs --self-test: exit 3 on the shallow clone (its pinned fixture commit621a4876was not in the object store). Aftergit fetch origin 621a487607881c66b2899b7e3477115229a156b4: exit 0, 48 cases pass.pnpm check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET (7 untouched packages had nodist/). After building those 7: exit 0, 112 require entry points across 69 packages load.dispatch-gates --ranwith every exit code recorded: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN (exit 0).origin/mainmoved 11 commits since the base, none touchingpackages/services/service-print, the print dogfood orpackages/cli/src/utils/console.ts;git merge-treeof head with it is clean. Every family CI owns (Lint & Repo Gates, Test Core, the type-check lanes) is read from this PR's checks.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
Generated by Claude Code