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>
席位核对 · 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