Repository navigation
feat(spec): declare IApprovalService.handleActionPage, the optional transport-neutral ADR-0043 action-page member - #22608
Conversation
…ransport-neutral ADR-0043 action-page member One optional member on the approvals service contract: handleActionPage?(request: Request): Promise<Response>. GET renders the confirm page, POST redeems; the e-mail token is the only credential. Its one caller is the runtime dispatcher's /approvals/act domain, and an absent slot or member answers a typed 404 or 501, never ROUTE_NOT_FOUND. Pins: the member is optional at the web-standard signature, and a service object without it still satisfies IApprovalService. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 139 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 29e24b050aa505e732f52de48b748a9cb1c02fcf && git checkout 29e24b050aa505e732f52de48b748a9cb1c02fcf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d748ae80afe6ed63d1c61a51a69f4fb18280c844 63322fcf95afb83afb293192a98d69cd928eb374 && git checkout -B drift-repro d748ae80afe6ed63d1c61a51a69f4fb18280c844 && git merge --no-ff 63322fcf95afb83afb293192a98d69cd928eb374
node scripts/docs-audit/affected-docs.mjs --json d748ae80afe6ed63d1c61a51a69f4fb18280c844
|
Contract reviewServed-tier: Scope read: PR #22608 for card #22575 (segment 1 of ruling A on #22438, director record ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Landing pre-checks at
|
Fixes #22575
Clause-②: yes (widening)
What this does
Segment 1 of 4 of the maintainer's ruling A on #22438 (director record
6079645593, the maintainer's 「A + 扫类」). The two parts of that ruling this segment carries, verbatim: "plugin-approvals adds one transport-neutral member that renders onGETand redeems onPOST"; "an absent slot or member answers a typed 404 or 501, neverROUTE_NOT_FOUND".This PR declares the member on the contract. Nothing implements or calls it yet. Segment 2 (the runtime
/approvals/actdomain and the hono body fix) and segment 4 (the plugin-approvals implementation) are their own cards, eachBlocked-by:this one. Segment 3 (the ADR-0069 allow-list) is independent.packages/spec/src/contracts/approval-service.ts:IApprovalServicegains one optional member,handleActionPage?(request: Request), which returns aPromiseof a web-standardResponse(:1148). It sits after the existing optionalcontinueRestoredRun?(:1060). Its docblock states five things:GETrenders the confirm page from thetokenquery parameter and never decides.POSTredeems thetokenform field through ADR-0043's whole redemption chain. Both answer HTML, and a dead link is told on the page, never as an error status.ExecutionContextand reads no session, cookie orAuthorizationheader. The decision is audited as the approver the token binds.http.servertype in the signature.HttpDispatcher's/approvals/actdomain (segment 2). It forwards thePOSTbody unread, because the token rides in it. The plugin's self-hosted raw-app mount is not a caller, so the path is never mounted twice.approvalsslot or the member is absent, the caller answers a typed 404 or 501, neverROUTE_NOT_FOUND.packages/spec/src/contracts/approval-service.test.ts: the card's two pins.ActionPageIsOptionaluses{} extends Pick(toEqualTypeOfcannot tell an optional member from a requiredT | undefined).ActionPageTakesOneRequestasserts the parameters are exactly oneRequest.ActionPageAnswersAResponseasserts the return type is aPromiseofResponse.minimalService(), which declares every required member and nothing else. It is typed asIApprovalServiceand carries nohandleActionPage..changeset/22575-spec-approvals-action-page-member.md:@objectstack/specminor(pre modenext), with its ownClause-②: yes (widening)line.Why the name
handleActionPagehandle…is the one this package's contracts already use for a function from aRequestto aResponse:IAuthService.handleRequest(auth-service.ts:66) and the optionalIRealtimeService.handleUpgrade?(realtime-service.ts:127).mountActionPages, "Actionable-link pages (ADR-0043)") name ADR-0043's surface.peekActionToken/redeemActionToken) belong to the implementation and stay off the contract. The member is the page, not the token.The PM's mechanism assumptions, measured
Read at base
99801d831f, re-read at head63322fcf95(after mergingorigin/main76bc1e03a3).IApprovalServiceis atapproval-service.ts:845andcontinueRestoredRun?is at:1060. The new member follows that optional shape and its docblock style.packages/spec,git grep IApprovalServicenames:approval-service.ts(class ApprovalService implements IApprovalService,:1280),approval-node.ts(a comment only) andindex.ts(a re-export);packages/rest/src/rest-api-plugin.ts(ctx.getServicetyped to the contract).packages/spec. The only typed doubles areminimalService()and its spreads inapproval-service.test.ts;sharing-service.test.tsreadsParametersofdecideonly.2a48bd4has 0 hits (git grep exit 1; the control wordIAuthServicehas 2).handleActionPageexisted anywhere before this PR, so no implementer collides with it.packages/spec/tsconfig.jsonalready haslib: ["ES2020", "DOM", "DOM.Iterable"]andtypes: ["node"], so no new dependency is needed. The publisheddist/contracts/index.d.ts/.d.mtsalready carriesIAuthService.handleRequestat the sameRequest→Responseshape. A scratch consumer resolved@objectstack/spec/contractsthrough the package'sexports(symlinked,module/moduleResolutionNodeNext,skipLibCheck: true). It used two discriminators that compile only ifRequest/Responsefail to resolve inside the published declaration file: a@ts-expect-erroron a number assigned to the member's parameter type, and one on a string assigned to its awaited return type.lib: ["ES2020"]+types: ["node"](no DOM): exit 0, both discriminators consumed.lib: ["ES2020", "DOM"], no types: exit 0.@types/node: TS2304 and TS2578. The same TS2578 fires on the identical discriminator over the already-publishedIAuthService.handleRequest, so this member adds no requirement the entry did not already have.approvals-plugin.tsat head::411readsgetRawApp();:417is theGET: it readsc.req.query('token')and answers the confirm or result page;:426is thePOST: it readstokenfromparseBody()and callsredeemActionToken;The docblock describes that behaviour. The plugin is unchanged.
One Zone 3 expectation did not hold. "
check:api-surfacewill see the widened interface" is false.api-surface/contracts.jsonrecords each export's name and kind (IApprovalService (interface)), not its members. Socheck:api-surfaceanswers "public API surface + factory signatures unchanged",check:generatedreports all 15 artifacts up to date, and this PR commits no generated file. The widening is visible to the type programs (the reverse verification below), not to a snapshot. The member emits no runtime JavaScript:handleActionPagehas 0 hits inpackages/spec/dist/**/*.{js,mjs,cjs}and 2 hits indist/contracts/index.d.ts/.d.mts.Verification
Heavy runs went through
scripts/pm/os-verify-lock.sh. The lineVERDICT command-exit 0is quoted from each.pnpm --filter @objectstack/spec build: exit 0 atd661a20e97. After the merge,turbo run build --filter='@objectstack/plugin-approvals^...' --filter='@objectstack/rest^...' --concurrency=2rebuilt the consumers' closure, spec included:Tasks: 31 successful, 31 total.check:generatedat63322fcf95. "All 15 generated artifacts are up to date", against the dist that run built.63322fcf95.pnpm --filter @objectstack/spec --filter @objectstack/plugin-approvals --filter @objectstack/rest typecheck: exit 0.tsc --noEmit,check:scripts-typecheckandcheck:test-typecheck. That last one reports "OK", andapproval-service.test.tsholds no debt-ledger entry, so the new aliases compile with zero errors.check:test-typecheckreports "OK".check:test-typecheckreports "OK".pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/contracts/approval-service.test.tsat63322fcf95:Tests 8 passed (8)(7 existing + 1 new).vitest run --project local --maxWorkers=2) atd661a20e97:Test Files 638 passed (638),Tests 19035 passed | 1 todo. The only commit since is the merge oforigin/main, which touches no file of this diff.d661a20e97throughscripts/ablation-replace.mjs(wrap mode, trap restore):handleActionPage?(becamehandleActionPage(. On disk the anchor went 1 → 0, the replacement 0 → 1, and the blobc46255afcc33→ebd1be71170f. After a spec rebuild,ablation-dist-preflightfound the marker indist/contracts/index.d.tsand.d.mts.tsc --noEmitexited 1 withsrc/approval-service.ts(1280,14): error TS2420: Class 'ApprovalService' incorrectly implements interface 'IApprovalService'. The consumer reads the rebuilt.d.ts.approval-service.test.ts(175,49): TS2741 Property 'handleActionPage' is missing(the value pin) and(306,43): TS2344 Type 'false' does not satisfy the constraint 'true'(ActionPageIsOptional).c46255afcc33, equal to the HEAD blob, andgit diff HEADis empty. Spec was rebuilt.ablation-dist-preflight --absentreported the required spelling absent from all 232 built files, the optional spelling was present, and the tree was clean. plugin-approvalstsc --noEmitexited 0, and the spec test program reported 0 diagnostics inapproval-service.test.ts.63322fcf95.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths, change set derived from the merge base76bc1e03a) derived 85 commands. 84 ran and every one exited 0, includingcheck:api-surface,check:docs,check:authorable-surface,check:export-origins,check:exported-any,check:spec-docblock-symbol-anchors,check:adr-0087-registration,check:changeset-no-major,check:empty-changeset,check:doc-authoring,check:nul-bytes,check:issue-citationsandcheck:type-check-coverage/-debt.--ranreconciliation: "85 derived famil(ies) accounted for — 84 run, 1 NOT-MEASURED".check-closing-target-claim,check-partof-closing-keyword,check-single-claim-paths) are run against this PR once it exists, and their result is reported on the card.Acceptance notes
check:dual-build-cjs-loads. The dispatch rules out a whole-workspace build, which this gate needs. CI runs it.check:objectui-pin-citations. It exited 0, but its live objectui round-trip self-test was skipped because the local../objectuilacks the pinned commit20c6d351a. That is unrelated to this diff.check:console-injection. It exited 0 but skipped: there is nopackages/console/distin this worktree../examples/*,downstream-contract) and the path-scheduled CI jobs (Test Core, Dogfood, Build Core, Temporal Conformance). They are CI's. The consumer typechecks the card names were run (above).check:api-surfacedoes not record interface members (above). That is a fact about the gate's scope, not a defect of this PR, and nothing here changes it.200with an explanatory page for every dead-token shape. The docblock records that as the contract (ADR-0043: "this link is no longer valid", not an error code), so segment 4 implements the member with the plugin's current answers. The typed 404 / 501 of the absence rule is about a missing service or member, never about a dead token.Generated by Claude Code