Repository navigation
feat(service-print): print a record's print page to a PDF in headless Chromium as the requesting user, and archive it on the record - #23811
Conversation
Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…acements, and their pins Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…for its archived PDF Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…registry; a print dogfood test on the showcase Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…geset and the lockstep count Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…int-render-service
Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…adings Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…w docs page owe Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…its slot lookups Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 61 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 146 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 004ff06ee7fbd9d3d625a8474749bfba56342475 && git checkout 004ff06ee7fbd9d3d625a8474749bfba56342475
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 161aee10478de783f37320d4f665153488571569 5a16f29db960f6d14e058af2b926d959c8992a7e && git checkout -B drift-repro 161aee10478de783f37320d4f665153488571569 && git merge --no-ff 5a16f29db960f6d14e058af2b926d959c8992a7e
node scripts/docs-audit/affected-docs.mjs --json 161aee10478de783f37320d4f665153488571569
|
…int-render-service
…mintPrincipalSession Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…e session is gone after the print Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · REWORK (patch round 1) ·
|
…int-render-service
…user — mayAttach and attachFileAs (WIP) Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…equesting user The file-attachments writer gains a caller-scoped face (ICallerFileAttachmentService: assertMayAttach, attachFileAs) on the same slot; the print service asks it in the files step, before any mint or launch, and archives through it. Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…nts face Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
… caller-scoped attach Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · patch round 1, item 1 ·
|
…int-render-service
… the requester, with the session gone after Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…d hold its glob by that test Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #23811 (card #22269). The review is owed by the face of the
None of the 41 paths is governed, and
The five required contexts are green. ① Derived judgments1.
2. The config options and env names: right.
3.
4.
5. The render's identity: right, with one pin gap.
6.
7. New names, and where they are registered: right.
8. The refusal order: right, with one wording gap.
9. Notes on the code for the seat, none of them blocking:
10. The showcase and the tests: right.
② Semver level
③ Boundary flagsEscalated to the maintainer, not judged. Both items are
What the render pin's CI skip leaves unmeasured. These claims are measured in CI neither on the PR nor in main's full battery:
They are measured only in the dev's container: green 6/6 at What carries CI on its own:
Dev report
Dev report
Dev report
Dev report
Routed elsewhere, not this PR's:
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · REWORK (patch round 2) ·
|
|
Amendment to REWORK
Generated by Claude Code |
…, a quiet revoke, re-probe after the refusals, the caller's organization only Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #23811 (card #22269). This is a same-form re-review after patch round 2. The review is owed by the face of the What was read:
The gate verdicts are the head's 43 check-runs, latest per name:
The five required contexts are green, and so is ① Derived judgmentsPatch round 2, item by item, read from the diff at this head
The whole PR at this head 1.
2. The config options and env names: right. The round narrows a new input only.
3.
4.
5. The render's identity: right. The pin gap is closed.
6.
7. New names, and where they are registered: right. The missing doc row is added.
8. The refusal order: right, and now exact.
9. The previous record's code notes, re-judged:
10. The showcase and the tests: right, unchanged by the round.
② Semver levelUnchanged:
The
③ Boundary flagsEscalated to the maintainer, not judged. Both are
No workflow sets Dev addendum
Carried from the earlier reports, unchanged.
For the seat, at ACCEPT.
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · REWORK (patch round 3, small) ·
|
…RL, the revoke log pinned through the plugin Claude-Session: https://claude.ai/code/session_01W2XFCTBfK4LabgqovmvSgK Co-authored-by: Claude <noreply@anthropic.com>
…int-render-service
维护者速读改了什么
为什么改
风险与代价(含回滚)
席位意见
你要做的(一个动作)
席位核对 · ACCEPT ·
|
Fixes #22269
Clause-②: yes (widening)
Print page ③ of the B′ split:
@objectstack/service-print, a render service with one engine (headless Chromium) that renders a record's print page under the requesting user's own principal and archives the PDF, as that user, as asys_fileattached to the record. It builds on the landedfile-attachmentsarchive, which it widens with a caller-scoped face, and binds to the principal-session mint of the seam card #23772.Binding state: bound. #23772 has landed (PR #23786), and so has the fix that lets a minted session boot the console (#23813). The service reads
IAuthService.mintPrincipalSessionthrough ONE package-local adapter,packages/services/service-print/src/principal-session.ts(principalSessionMinterOf), which is typed by the contract's ownIAuthServiceandPrincipalSession.status()reportsunavailable, naming that member, only where the composed auth service does not offer it.What it adds
PrintServicePluginregisters the in-processprintservice (IPrintService):status()andprintRecordToFile({ app, object, recordId, page, filename? }, context). Explicit composition; no route, no metadata key, no error code, norequirestoken.One engine, two placements (ruling of record on this card, OQ5 → B):
OS_PRINT_CHROMIUM_PATH/chromiumPath. Per render,chromium.launchServer({ executablePath, headless: true })andchromium.connectto it, one browser process per render. Disposal closes the connection, then the server, and kills the process if a close does not settle within 2 s;OS_PRINT_BROWSER_ENDPOINT/browserEndpoint, a DevTools WebSocket endpoint,chromium.connectOverCDPper render. Its context closes first, then the connection, each bounded the same way;OS_PRINT_CONSOLE_URL/consoleUrl, the console URL that browser reaches (/_consoleincluded). A console URL carrying credentials (user:password@) is refused, and so is configuring both placements at once; each is reported as misconfigured.playwright-core1.63.0, pinned exact on this package only (already in the lockfile).Loud at
start():warnnaming the setting;status()reportsready | unconfigured | misconfigured | unavailablewith the reason;status()never shows more: the endpoint may carry a token, and a value that does not parse is described as unparseable, never echoed.ICallerFileAttachmentService(@objectstack/service-storage, exported type,minor): thefile-attachmentsservice's caller-scoped face, on the same slot and the same one writer.IFileAttachmentServiceandattachFileare unchanged.assertMayAttach(parentObject, parentId, context)throws the insert's own refusal without writing:PERMISSION_DENIED/ 403 whenISecurityService.explainsays the caller may not createsys_attachment;ATTACHMENT_PARENT_ACCESS/ 403 when the attach rule (parent EDIT) refuses.attachFileAs(input, context):sys_fileowned by the caller, and onesys_attachmentinserted under the caller's own context, so every insert gate judges the caller anduploaded_byis stamped;organizationIdother than the context'stenantId(ornullwhen it names none) isPERMISSION_DENIED/ 403, before anything is read or written.PERMISSION_DENIED/ 403 on both verbs.Refusals, in the ruled order, all before any mint or launch. Each pin asserts
code+status, and that mint, render and archive were never called:authGatehold)PERMISSION_DENIED/ 403ready(unconfigured, misconfigured — e.g. both placements, or a console URL with credentials — never probed, or a service a print needs not composed)SERVICE_UNAVAILABLE/ 503RESOURCE_NOT_FOUND/ 404print, or not the object's default record pageVALIDATION_ERROR/ 400RECORD_NOT_FOUND/ 404enable: { files: true }FILES_DISABLED/ 403assertMayAttach)PERMISSION_DENIED/ 403assertMayAttach)ATTACHMENT_PARENT_ACCESS/ 403An engine whose last probe failed, with every service composed, is probed once more only after step 5. If that probe fails too, the answer is
SERVICE_UNAVAILABLE/ 503. So no browser is launched or connected before any of the refusals above.Then:
page.pdf, so a stall answersTIMEOUT/ 504; the cleanup after it is bounded, and a browser that arrives late is closed;attachFileAsas the caller;finally(pinned after a failed render and after a refused archive).erroron the plugin's logger, naming the user and a 12-hex fingerprint of the session (never the token or the cookie), and the 60 s TTL ends the session (pinned through the plugin).The page binding: the browser loads
CONSOLE/apps/APP/OBJECT/record/IDwith the minted cookie, waits for the print root (data-print-page) ANDwindow.__objectui.idlefor 3 consecutive polls, thendocument.fonts.ready, thenpage.pdf({ preferCSSPageSize: true })— the print keys are mapped to CSS once, by the console.Showcase:
showcase_invoice_document, the invoice's default record page, declaresprint;showcase_invoicedeclaresenable: { files: true }. Section headings and the page label are translated (en, zh-CN).Docs:
content/docs/ui/print-pages.mdx(+ the ui landing row);OS_PRINT_*rows incontent/docs/deployment/environment-variables.mdx;packages.mdxrow;Measured (mechanism assumptions)
file-attachmentswriter as it is, run AS THE CALLER throughattachFileAs; there is no second archive entry, and noIFileAttachmentServiceorpackages/specchange.RECORD_NOT_FOUND), theFILES_DISABLEDpre-check, andassertMayAttach.sys_file.owner_idandsys_attachment.uploaded_bynull and skipped the parent-EDIT gate. Both rows now name the requester, and the attach rule judges them. This is pinned on a real showcase boot (print-service-archive.dogfood.test.ts, and the real render inprint-service-render.dogfood.test.ts).PERMISSION_DENIED/ 403 before any mint or render..objectui-shaf96f771140, re-read atdaeb77fc84dd, the pin at this head.RecordDetailViewcallsusePageAssignment(objectName)withoutpageName,RecordDetailView.tsx:640at the pin).PageRendererstampsdata-print-pageon a print page.pdfinfo: 1 page, 594.96 x 841.92 pts; fonts includeWenQuanYiZenHei). The run through the service itself with a minted session is item 3.IAuthServicemember with itsAuthManagerimplementation, the seam #22269's print service consumes (S2 of #22269) #23772) against the real console: boots, after plugin-auth: a session minted bymintPrincipalSessionnever boots the console — the console loops on boot, so #22269 cannot render under the requester #23813.get-sessionread.0cfd0c49bd),print-service-render.dogfood.test.tsprints through the service's own path, with a session minted byIAuthService.mintPrincipalSession, against the console built atdaeb77fc84dd, on both placements. It asserts:9252cd175e, and again at78e50355a2after the round-3 driver changes.launchServer({ executablePath })+connect. Remote:connectOverCDP(endpoint). Both work on Chromium 141.pdftotextreturns the Chinese text from a tofu render). The pin asserts every Chinese code point is a ToUnicode target of an embedded font. Ablation (Chromium narrowed to a Latin-only fontconfig, no code change), both placements red:expected [ '发', '票', '客', '户', '名', '称', …(12) ] to deeply equal []; restored green.Tests
@objectstack/service-print: 79 tests.start()loudness against a missing executable and a dead endpoint carrying a token, and the plugin-level revoke-log pin;@page, the CJK pin, timeout →TIMEOUT/ 504, remote cleanup.OS_TEST_PRINT_CHROMIUM_PATHorPLAYWRIGHT_BROWSERS_PATH/chromiumexists, and says it was skipped otherwise.@objectstack/service-storage: 1029 tests, including the caller-face pins against a real ObjectQL engine and its insert gates:owner_id/uploaded_by= the caller;ATTACHMENT_PARENT_ACCESSfrom the real insert gate;PERMISSION_DENIEDfromexplain;FILES_DISABLEDas the caller;packages/qa/dogfood/test/print-service-archive.dogfood.test.ts: 9 tests.sys_file+ onesys_attachmenton the invoice, both naming the requester; the real mint revoked at the wire.packages/qa/dogfood/test/print-service-render.dogfood.test.ts: 6 tests, 3 per placement. The real render through the real console with a minted session, as Measured 3 describes.5a16f29db9; 9 and 6 at78e50355a2. In CI the two Chromium-gated files skip, and the dogfood suite runs only in main's full battery.Acceptance notes
packages/spec/liveness'spage.printrow was flipped fromplannedtoliveonmainbefore this head (PR fix(spec): page.print and the flow terminal-toast translations read live in the liveness ledger, so a print page is no longer warned that nothing applies it #23776, closing spec liveness: the console pin hop to objectui 1f23d5b070fa makespage.printandtranslation.flows.successMessage/errorMessagereadplannedwhile the shipped console applies them (family card; theparentDataSourceleg is #23585) #23710). The finding first noted here is resolved, and needs no spec-lane follow-up.record:line_itemsdraws a column exactly as declared, and does not hydrate a label from the child field (packages/spec/src/ui/component.zod.ts:2494). The showcase page's columns declare nolabel, so the follow-up is in this repo's showcase.Surface deviations (declared)
packages/spec/llms.txt: the package count 71 → 72, forced by the new package (check:llms-txt), with a@objectstack/specpatch line in the changeset. No otherpackages/specchange.turbo.jsonandscripts/cross-package-test-inputs.mjs: the dogfood tests' declared inputs gainpackages/services/service-print/src/**andpackages/console/dist/index.html, the latter withheldBy(check:cross-package-test-inputs).content/docs/permissions/system-context.mdx: census rows 47d (the print service's system-context refusal) and 47e (the caller-scoped attach verbs' refusal) (check:system-context-census).scripts/docs-audit/handwritten-docs.json: the new docs page (check:docs-audit-scope).packages/services/service-storage/src/**: the caller-scoped face, as REWORK6208156693item 2 orders.storage-service-plugin.tsandattachment-access-hooks.tswere edited only after PR feat(service-storage): the sys_attachment read gate writes one ReadableIds leaf per parent object and does no pre-scan; D5's three classes keep the probe (ADR-0139 E7, attachments) #23767 merged. The hooks change is an extraction of the attach rule's verdict, with behaviour unchanged.Seat edit ·
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T14:56Z: stale lines corrected to head5a16f29db.Generated by Claude Code