Repository navigation
fix(rest): the environment-scoped ?layers=true successor Link names the request path, not the route template (#20508) - #20528
Conversation
…ath, not the route template RestServer built the deprecated spelling's successor Link from metaPath, which on the environment-scoped mount is the route template, so the header named /environments/:environmentId/.../layers. It now passes the request's own path (IHttpRequest.path), parsed as a URL path the way the runtime dispatcher reads its request URL: without a trailing slash, and percent-encoded again where the Hono adapter hands the path over decoded. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 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 837b533b0e16b3aa6cd5b40a0e9c2c33b09871a5 && git checkout 837b533b0e16b3aa6cd5b40a0e9c2c33b09871a5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fb386074f57b234c98c40938aae7a0486ad50e8b 439dd81bd37090d5175b79c654413f22d7a13a92 && git checkout -B drift-repro fb386074f57b234c98c40938aae7a0486ad50e8b && git merge --no-ff 439dd81bd37090d5175b79c654413f22d7a13a92
node scripts/docs-audit/affected-docs.mjs --json fb386074f57b234c98c40938aae7a0486ad50e8b
|
Contract reviewServed-tier: Inputs read: card #20508 body and its three comments (triage grade Check-runs on the head at read: 33 runs; 25 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke), 5 in progress (Lint & Repo Gates, Test Core 1/6, 2/6, 4/6, 5/6), 0 failure. Success includes Test Core 3/6 and 6/6, all three Dogfood Regression Gate shards, Temporal Conformance, Build Core, Dogfood Verify CLI, all four Type Check jobs, Check Changeset, Check PR Size, Governed Surface Queue Guard and both claim guards. The five in progress are recorded as running; no verdict is inferred for them. ① Derived judgmentsThe card's binding direction (triage grade (1) The scoped (2) The encoded-name behaviour on the unscoped mount ( (3) (4) The edited existing test keeps its assertion: RIGHT. Further derived judgments:
② Semver level
③ Boundary flags
The PASS below is the contract verdict on the diff at this head; 5 check-runs were still in progress at read and none had failed, and their conclusions are not inferred here. Implemented-by: VERDICT: PASS |
Fixes #20508
Clause-②: no
What was wrong
On
RestServer's environment-scoped mount (api.enableProjectScoping), the deprecated?layers=truespelling of the item read answeredDeprecation: trueand a successor-versionLinkbuilt frommetaPath. On that mountmetaPathis the route template, so theLinknamed the path/api/v1/environments/:environmentId/meta/view/lead_all/layers, with a literal:environmentIdin it. A client that followed the header requested that path.What changed
packages/rest/src/rest-server.ts, the?layers=truecall site ofmetaItemLayersDeprecationHeadersonly.RestServernow passes the request's own path (IHttpRequest.path), parsed as a URL path the way the runtime dispatcher'srequestedItemPathreads its request URL: without the trailing slash. The shared helper inmeta-item-read-gate.tsis untouched (no template replace anywhere).The parse matters for one measured reason: the Hono adapter hands handlers a
decodeURI'd path (c.req.path). A name requested aslead%20allreaches the handler aslead all. The parse percent-encodes it again, so theLinkstays a valid URI reference. A request that carries no path getsDeprecation: truealone, which is what the helper prescribes for a transport that cannot say where it serves the item.Pins (triage grade
5879492075):Linknames/api/v1/environments/env_1/meta/view/lead_all/layers;Linkstill names/api/v1/meta/view/lead_all/layers.Both pins run through the real
HonoHttpServer(packages/rest/src/meta-item-layers-deprecation-link.test.ts), because the path comes from the adapter. The same file pins the encoded-name case. The mock-harness filemeta-item-layered-route.test.tsnow passes thepathfield thatIHttpRequestdeclares required (its hand-built request omitted it). It also pins the no-path branch.Measurements (PM mechanism hypotheses)
All at
fb386074f5(unfixed) or439dd81bd3(fixed), through the realHonoHttpServerwithenableProjectScoping: true, projectResolution: 'optional'.H0: confirmed. Unfixed,
GET /api/v1/environments/env_1/meta/view/lead_all?layers=trueanswered200,deprecation=true, with aLinknaming/api/v1/environments/:environmentId/meta/view/lead_all/layers. The unscoped controlGET /api/v1/meta/view/lead_all?layers=truenamed/api/v1/meta/view/lead_all/layers, which is correct.H1: half confirmed. The request's own path is available at the call site as
req.path. The Hono adapter sets it fromc.req.path, and the Node conformance port fromurl.pathname. It does not carry a percent-encoded name unchanged under Hono:c.req.pathisdecodeURI'd. A probe route read the following for the scoped path:req.pathsegmentrequestedItemPathlead_alllead_alllead_alllead_alllead%20alllead alllead%20alllead%20alllead%2Falllead%2Falllead%2Falllead%2Falllead%25alllead%25alllead%25alllead%25alll%C3%A9adléadl%C3%A9adl%C3%A9adlead%5Falllead_alllead_alllead%5Fallenv%201env 1env%201env%201After the parse, six of the seven rows match the dispatcher byte for byte. The seventh is
%5F, a percent-encoded unreserved character, which Hono decodes to_. The two spellings are equivalent under RFC 3986 section 6.2.2.2, and they name the same route.H2 (ablation): the red set is larger than predicted. The mutation put back the template argument (
${metaPath}/${req.params.type}/${req.params.name}) throughscripts/ablation-replace.mjs, which confirmed it landed (anchor 1 to 0, blobcb57de8250e2to004a11651dba). The result was 3 red and 13 green of 16: the scoped pin, the scoped encoded-name pin, and the mock no-path pin. The PM predicted the scoped pin alone. The two extra reds are pins this PR adds, and each is a scoped or no-path case the template argument gets wrong. Both unscoped controls (Hono and mock) stayed green. Restore was proven: blob after restorecb57de8250e2equals the blob atHEAD, andgit diff HEADwas empty.Second ablation, of the parse. It replaced the parse with the raw
req.path. The first attempt was a no-op: the tool refused because the replacement textrequestPathalready occurred inside the anchor, so the count did not rise, and it restored without running the tests. The re-run used a unique marker and landed (blobcb57de8250e2to48fa04adf547). Exactly 1 test went red: the encoded-name pin, which received a raw space inlead all/layers. Restore was proven the same way.Verification
At
439dd81bd3:pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: 222 files, 4235 passed, 40 skipped.pnpm --filter @objectstack/rest typecheck: exit 0, which includescheck:test-typecheck.tsc -p tsconfig.test.json --listFileslists both edited test files.pnpm lint(full repo, not narrowed): exit 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 62 commands. 60 exited 0. Two exited 3 withPREREQUISITE NOT MET, because both need the whole workspace built:check:dual-build-cjs-loadsandcheck:type-check-debt. Those two are NOT MEASURED. The whole-workspace build was not run on this shared host.--ranreconciliation: 62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN.check-changeset-fixed,check:error-code-casingandcheck:filter-alias-parityall exited 0.node scripts/check-issue-citations.mjs --base origin/main, after mergingorigin/main(already up to date atfb386074f5): exit 0.Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.
Acceptance notes
Linkwas assembled from the decoded route parameters. Measured onfb386074f5through Hono:lead%2Fallnamed/api/v1/meta/view/lead/all/layers, a different path;lead%25allnamedlead%all, an invalid percent-encoding;lead%20allandl%C3%A9adput a raw space and a raw non-ASCII character in the header. After the fix, each keeps its encoding. For every name that needs no encoding, the unscopedLinkis byte-identical. The changeset says so.requestedItemPathinpackages/runtime/src/domains/meta.ts, and once is inline at this call site. Moving one copy into the shared seam,meta-item-read-gate.ts, is outside this claim's file surface: that file andpackages/runtime/**are read-only here, and PR fix(rest): the layered view answers an absent name with the plain read's 404, as it answers an unpublished app (#20507) #20527 is editing the seam. carrier: none. Noted, not filed.Generated by Claude Code