Skip to content

fix(sharing): X-Share-Password declares its encoding (X-Share-Password-Encoding: utf-8), so any share-link password can be sent from a browser - #22061

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-22049-share-password-header-encoding
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-22049-share-password-header-encoding

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22049
Clause-②: yes (widening)

Accepted-set change. New: a request to GET /api/v1/share-links/:token/resolve or /:token/messages may carry X-Share-Password-Encoding: utf-8 (compared case-insensitively) beside X-Share-Password. The password header is then read as the password's UTF-8 bytes, percent-encoded (what encodeURIComponent produces), so a password with a character above U+00FF, or one that begins or ends with a space, can now be presented through the header. This holds on both mounts: the sharing plugin's routes and the runtime dispatcher's /share-links domain. Unchanged: a request without X-Share-Password-Encoding is read as before, with X-Share-Password raw as it arrives (a repeated header by its first value). Every value accepted until now, including a Latin-1 password and a raw password containing %, resolves to the same password. When ?password= is present it still wins, and the header pair is not read at all. Refused: X-Share-Password-Encoding with any other value is refused. Before this change that header meant nothing and no client sent it. Also refused is utf-8 over a password header that is not percent-encoded UTF-8: a % without two hex digits, octets that are not well-formed UTF-8, or any character outside visible ASCII. Both answer 400 with code VALIDATION_FAILED in the ADR-0112 envelope (success: false, error.code, error.message), on both routes and both mounts. The refusal comes before the token is looked up, and the value is never compared raw instead. VALIDATION_FAILED is already registered under both @objectstack/plugin-sharing and @objectstack/runtime, so no error code is added.

What changed

  • One reading, two producers. @objectstack/types exports readSharePasswordHeader(passwordHeader, encodingHeader) and the constants SHARE_PASSWORD_HEADER, SHARE_PASSWORD_ENCODING_HEADER, SHARE_PASSWORD_ENCODING_UTF8 and SHARE_PASSWORD_VARY. The sharing plugin's presentedPassword and the runtime domain's presentedPassword both read the header pair through it. On a refusal, each door answers 400 VALIDATION_FAILED in its own registered vocabulary. The helper spells no error code, because the error-code ledger registers codes per emitting package. It also never trims, logs or echoes the value.
  • Response headers. Both public routes answer Vary: X-Share-Password, X-Share-Password-Encoding, still beside Cache-Control: no-store.
  • CORS. DEFAULT_CORS_ALLOW_HEADERS (@objectstack/plugin-hono-server, also applied by @objectstack/hono) gains X-Share-Password-Encoding.
  • Producer docs. The header docblocks on both mounts, and the X-Share-Password-Encoding row in content/docs/protocol/kernel/http-protocol.mdx (CORS block and table). The platform checklist item access-security.share-link-capability-tokens moves to revision 6, because its Vary clause would otherwise read false.
  • One minor changeset for types, plugin-sharing, runtime and plugin-hono-server.

Why a companion header, not an in-value prefix

The ruling allowed either form. The RFC 8187-style UTF-8'' prefix would re-read every raw password that happens to begin with those seven characters. A previously accepted value would then resolve differently, or be refused, which is a narrowing that the declared (widening) does not cover. A companion header re-reads nothing that was sent before it existed. Its cost is the second header in the CORS allow-list and in Vary. A deployment that passes its own allowHeaders must add X-Share-Password-Encoding beside X-Share-Password before a cross-origin client can send an encoded password. The changeset says so.

Landing outside the expected file surface, and why

  • packages/types is the helper home. It is the readers' existing common import. @objectstack/plugin-sharing is only a dev dependency of the runtime, so importing from the plugin would add a dependency edge, and packages/spec is excluded. Both packages already depend on @objectstack/types, so the import adds no edge.
  • packages/plugins/plugin-hono-server: the default CORS allow-list and its pin. Without it, a cross-origin client could not send the companion header, and the capability would be declared but not delivered.
  • docs/qa/platform-checklist/areas/access-security.json. Its Vary clause said "exactly ... Vary: X-Share-Password" and would have read false. Only that clause, its verify text, two source lines and a history entry changed.

Whitespace (mechanism hypothesis 4), measured on Node 22.22.0

  • new Headers() turns ' a b \t' into 'a b', and throws a TypeError on a CJK value.
  • Node's http server receives ' p w \t' as 'p w'.
  • The console mints trimmed: ShareDialog sends password.trim(). The landing page sends the typed value, and the browser's Headers strips its leading and trailing HTTP whitespace.
  • The server compares exactly what arrives: createLink hashes the input as is, resolveToken verifies it as is, and the new reading does not trim.
  • So the raw path behaves as before for a space-edged password. Under utf-8, a space-edged password now survives transport as %20.

Verification (head f7cabbb83a)

  • Pins. On both mounts, /resolve and /messages serve a CJK and an emoji password sent percent-encoded under utf-8. The same encoded value with no encoding header is compared raw (401 WRONG_PASSWORD / 404 NOT_FOUND): the server does not guess. A Latin-1 password, a raw password with a stray %, and a raw password with a clean %25 escape each resolve sent raw. A truncated UTF-8 sequence, %FF, and an unknown encoding each get 400 VALIDATION_FAILED, with success false, the public headers present, and resolveToken never called. A value that IS the raw password but does not decode under utf-8 is still refused. ?password= still wins.
  • Files. packages/types/src/share-password-header.test.ts (33 passed), packages/plugins/plugin-sharing/src/share-link-password.test.ts (44 passed), packages/runtime/src/domains/share-links-password-encoding.test.ts (23 passed), share-links-public-cache-headers.test.ts (5 passed), and hono-plugin.test.ts (the new CORS pin).
  • Package suites. types 24 files / 739 passed; plugin-sharing 40 / 1002; runtime (--project local) 332 / 4693 passed, 19 skipped; plugin-hono-server 27 / 326. typecheck is green on all four, including each package's check:test-typecheck.
  • Ablations. Each mutation was landed by scripts/ablation-replace.mjs (anchor hit and blob change proven, restore proven against the HEAD blob), with @objectstack/types rebuilt and scripts/ablation-dist-preflight.mjs proving the marker in dist/, then absent after the restore leg. Every leg turned red as predicted and went back to green:
    • malformed value falls through to a raw compare: types 8, sharing 6, runtime 6 red;
    • unknown encoding ignored: 6 / 2 / 2 red;
    • every value percent-decoded (guessing): 4 / 8 / 8 red;
    • the sharing door drops its 400: sharing 8 red;
    • the runtime door drops its 400: runtime 8 red.
  • End to end on a throwaway showcase server (pnpm dev -- --fresh, random port, torn down). Four links were minted over POST /api/v1/share-links.
    • Served (200): a CJK and an emoji password under utf-8 / UTF-8, a raw Latin-1 byte (caf plus byte E9), and a raw 50%25 off.
    • Not guessed: the encoded CJK value with no encoding header gets 401 WRONG_PASSWORD.
    • Refused: %E5%88 under utf-8, the latin1 encoding, and %FF on /messages each get 400 VALIDATION_FAILED.
    • /messages with the CJK password got past the password to 400 UNSUPPORTED, because the record is not a conversation.
    • A cross-origin preflight answers Access-Control-Allow-Headers naming both headers.
  • Gates. node scripts/pm/dispatch-gates.mjs re-derived 97 commands on this change and all 97 exit 0. Three first exited 3 (PREREQUISITE NOT MET: check:skill-examples, check:dual-build-cjs-loads, check:i18n), and after turbo run build they exit 0. --ran reconciliation: 97 derived, 97 run, 0 NOT-MEASURED, a derived zero. The share-link dogfood files share-links-self-list.dogfood.test.ts and showcase-client-liaison-fixtures.dogfood.test.ts pass (16 tests).
  • Lint, narrowed. The 10 changed .ts files are the whole population the **/*.{ts,…} globs in eslint.config.mjs select from this diff; the other 3 files are .md, .mdx and .json. Run with --no-inline-config --format json: 10 files, 0 errors, 0 warnings. Type-aware linting is not enabled in that config (no parserOptions.project), so this diff cannot move any untouched file's verdict.
  • origin/main moved 3 commits past the base a7a48b784d, and none of them touches these packages or files.

Acceptance notes


Generated by Claude Code

claude added 4 commits October 7, 2026 04:41
… a signalled encoding

X-Share-Password-Encoding: utf-8 declares X-Share-Password percent-encoded
UTF-8; without it the password header is read raw, unchanged. A declared
encoding that does not hold is a refusal, never a raw compare.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…-Password-Encoding declares it

plugin-sharing's presentedPassword and the runtime /share-links domain decode
the header pair through readSharePasswordHeader; a declared encoding that does
not hold answers 400 VALIDATION_FAILED before the token is looked up. Vary
names both headers; the default CORS allow-list carries the companion.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…a guessing reader cannot pass

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…ing; checklist Vary clause re-pointed

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/plugin-hono-server, @objectstack/plugin-sharing, @objectstack/runtime, @objectstack/types, touching 20 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/types/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 2015c540287ee136ec0226ec7e4fb9b22f8b1351.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/types/src/index.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 35 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2015c540287ee136ec0226ec7e4fb9b22f8b1351 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d2d4b1282892116667fadea4819e17c0bec9b9ab — the merge of head 7e8d722037fa3f20bb056dffcd44673cda892163 into base 2015c540287ee136ec0226ec7e4fb9b22f8b1351, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d2d4b1282892116667fadea4819e17c0bec9b9ab && git checkout d2d4b1282892116667fadea4819e17c0bec9b9ab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2015c540287ee136ec0226ec7e4fb9b22f8b1351 7e8d722037fa3f20bb056dffcd44673cda892163 && git checkout -B drift-repro 2015c540287ee136ec0226ec7e4fb9b22f8b1351 && git merge --no-ff 7e8d722037fa3f20bb056dffcd44673cda892163

node scripts/docs-audit/affected-docs.mjs --json 2015c540287ee136ec0226ec7e4fb9b22f8b1351

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2015c540287ee136ec0226ec7e4fb9b22f8b1351 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f7cabbb83a43dee23907ce06dbf9a727d39f29aa
Local-runs: none

Inputs read: card #22049 (body and all four comments: triage 6030586760, claim 6030950272, os-dev-report 6031675378, claim amendment 6031700627); PR #22061 body, its 13-file list and the net diff a7a48b784d..f7cabbb83a (746 added, 34 deleted); the check-runs on the head, read at 2026-10-07T05:42:01Z. Read-only shape: the head and main were fetched into owned refs and read with git show / git grep; nothing was built, run or re-run.

① Derived judgments

Each accept-set or public-surface change the diff implies, with its judgment:

  1. New request header X-Share-Password-Encoding on GET /api/v1/share-links/:token/resolve and /:token/messages, both mounts. One value, utf-8, compared case-insensitively; under it X-Share-Password is read as percent-encoded UTF-8 (the encodeURIComponent form). RIGHT. The encoding is signalled, never guessed, as triage required. Both producers decode through the one helper readSharePasswordHeader (@objectstack/types): plugin-sharing's presentedPassword and the runtime domain's presentedPassword closure both call it, and git grep at the head finds no third reader of the header.
  2. Raw read unchanged without the companion header. An absent encoding header returns the first value of the password header untouched, as the old read did. RIGHT. A Latin-1 password, a raw password with a stray % and one with a clean %25 escape are pinned as served on both routes and both mounts; an encoded value sent without the companion is compared raw (401 on /resolve, 404 on /messages), so the server does not guess.
  3. ?password= still wins, and the header pair is then not read. RIGHT, pinned on both mounts; the card's stated non-scope is untouched.
  4. Refusal 400 VALIDATION_FAILED for any other encoding, or for a value that is not percent-encoded UTF-8 under utf-8, before the token is looked up, never a raw compare. RIGHT. VALIDATION_FAILED is registered in the ADR-0112 ledger under both emitters (packages/spec/src/api/error-code-ledger.zod.ts: the @objectstack/runtime block and the @objectstack/plugin-sharing block), so no code is added and the helper spells none. Pins on both mounts assert success: false, error.code, resolveToken never called, the public headers present, and the presented value absent from the body. The refusal messages carry no tracker number and never the value. This is the new header's own closed value set, declared on the day it lands; no request shape that had a meaning before this diff changes meaning, so the ruled (widening) stands. A joined repeated companion (utf-8, utf-8) is refused as unknown, which is acceptable under a signalled-not-guessed contract and is pinned as such.
  5. Visible-ASCII precheck (0x21 to 0x7E) on the encoded value. RIGHT. encodeURIComponent output lies inside that range, so a raw non-ASCII or space-bearing value under a declared utf-8 is refused rather than passed through; decodeURIComponent's URIError covers truncated, overlong, surrogate and non-hex forms, each pinned.
  6. Vary on both public routes becomes X-Share-Password, X-Share-Password-Encoding, from one constant SHARE_PASSWORD_VARY. RIGHT. The runtime wrapper handleShareLinksRequest merges PUBLIC_RESPONSE_HEADERS onto every public outcome, the body-returned 400 included; the plugin mount calls setPublicResponseHeaders(res) before the refusal on both routes. The pins on both mounts are updated. At the head, the only other spellings of the old value are RELEASE-OWNED (packages/*/CHANGELOG.md, content/docs/releases/v17/17-7.mdx) and the checklist's revision-5 history line, all correctly left verbatim; no pin outside the four packages names the old value.
  7. DEFAULT_CORS_ALLOW_HEADERS gains X-Share-Password-Encoding. RIGHT and necessary: without it a cross-origin client could not send the companion, and the capability would be declared but not delivered. @objectstack/hono spreads the same constant (packages/adapters/hono/src/index.ts:384), so both hosts widen with no second edit. A deployment passing its own allowHeaders must add it; the changeset says so. See ② for the package the changeset does not name.
  8. @objectstack/types public surface: readSharePasswordHeader, four constants, three types. Additive, so a widening. The home is right: runtime has plugin-sharing as a devDependency only, both readers already depend on @objectstack/types, and packages/spec was excluded by the claim. packages/types has no api-surface snapshot (its scripts are build, typecheck, test), and its tsconfig include: src/**/* reaches the new test file, so nothing is owed to a generator.
  9. Docs and checklist. content/docs/protocol/kernel/http-protocol.mdx is a hand-written tree; the new row and the count (four to five) are right. docs/qa/platform-checklist/areas/access-security.json goes to revision 6 on the Vary clause only; docs/qa/ is not in the GOVERNED_SURFACES register, and the Governed Surface Queue Guard concluded success.
  10. No new query parameter, so the closed-query-set rule is not triggered. No OpenAPI, discovery, skills or client surface enumerates the header at the head (git grep finds none). Of the ADRs whose text names share links, ADR-0111 (the share-link doors) and ADR-0121 (the /share-links namespace) govern these routes; neither fixes the header's transport encoding.
  11. Scope held: no console guard (triage: no), no objectui change, no ?password= retirement, no packages/spec edit. main moved three commits past the merge-base (d5a14dd5c0, 5bd8cb100b, 93125aeeb8); none touches these packages or files.

② Semver level

  • Clause-②: yes (widening) is on the PR body and in the changeset body. RIGHT, for the reasons in ① 4: every value accepted before resolves unchanged, and the only new refusal is the new header's own closed set.
  • Level minor for @objectstack/types, @objectstack/plugin-sharing, @objectstack/runtime and @objectstack/plugin-hono-server. RIGHT: new exports, a new accepted header pair, a widened default allow-list; nothing removed or renamed, so no migration text and no ADR-0087 marker is owed. Check Changeset concluded success.
  • WRONG, one package short. The diff changes what @objectstack/hono publishes: its default CORS allow-list widens through the constant it spreads (packages/adapters/hono/src/index.ts:384), and the changeset's own sentence for operators with a custom allowHeaders is addressed to exactly that adapter's users, yet the frontmatter does not name @objectstack/hono. The precedent on this very surface named it: the security(sharing): share-link password handling falls short of the platform's credential rules (response shape, hashing strength, transport) — detail withheld pending maintainer #21839 changeset (commit f5b8e29e37, PR fix(plugin-sharing): share-link password never leaves the server, is stored with the platform slow hash, and is accepted in a header #21890) listed '@objectstack/hono': patch beside plugin-hono-server, which is why packages/adapters/hono/CHANGELOG.md 17.7.0 carries the X-Share-Password CORS note as a direct entry. The fixed group bumps the adapter's version regardless, and updateInternalDependencies: patch yields only an Updated dependencies line, so the sentence an upgrading @objectstack/hono consumer greps will not be in their CHANGELOG, and after release it can be amended only by a dedicated docs-only PR. Check Changeset cannot see this, since the adapter's source is untouched. Fix: add "@objectstack/hono": minor to .changeset/22049-share-password-header-encoding.md; nothing else in the diff changes.

③ Boundary flags

Dev flags (os-dev-report 6031675378: six deviations, open_questions empty) and the two out-of-scope findings:

  1. File surface widened (packages/types helper and its export, plugin-hono-server CORS default and pin, checklist revision 6, http-protocol.mdx): ANSWERED, accepted. The claim amendment 6031700627 adopted exactly these with lane declarations, and each reason is verified in ① (dependency edges, CORS necessity, the clause that would have read false).
  2. Companion header instead of the RFC 8187 UTF-8'' prefix: ANSWERED, right. Triage allowed either; the prefix would re-read a raw password that begins with those characters (the types test pins UTF-8''%E5%88%86 as raw-read-unchanged), which is the narrowing the ruled widening does not cover.
  3. Vary value changed on both routes: ANSWERED, accepted (① 6).
  4. First M1 ablation void, re-run type-clean: ANSWERED, a process note; nothing to act on.
  5. Dispatch gate list (52) superseded by re-derivation (97): ANSWERED; the check-runs on the head are the gate verdicts in any case.
  6. Attribution, AGENTS.md form over the harness reminder: ANSWERED, right. All four commits carry the model-free trailer pair, the PR body carries the session-URL footer, and no model identifier appears anywhere in the diff.
  7. Out of scope, carrier objectui (the console's encode half, with the trim() nuance for U+00A0 and U+3000): ESCALATED to the dispatching seat. Triage names it as following on objectui once a release carries this decode; a card there is owed at that moment and should carry the trim note. Not this PR; not a blocker.
  8. Out of scope, carrier checklist-author sweep (no clause yet scores the encoded form): ESCALATED as a sweep item; a coverage gap, not a defect. Not a blocker.

Check-runs on the head, read 2026-10-07T05:42:01Z: 25 success, 2 skipped by paths filter (Console Pin Gate, Packed-tarball smoke), 7 in progress (Lint & Repo Gates, Test Core 1 to 6), 0 failed. Of the seven required contexts, TypeScript Type Check, Build Core, Dogfood Regression Gate, Temporal Conformance (live PG + MySQL) and Governed Surface Queue Guard concluded success; Lint & Repo Gates and Test Core were still running at that read. The PR is a draft, not armed, a same-repo head, 780 changed lines, and touches no governed path.

Why FAIL: ② alone. The changeset must also name @objectstack/hono at minor, as the #21839 precedent did for the same CORS default. ① and ③ hold as written, so the re-review at the new head is scoped to the changeset frontmatter.

Implemented-by: claude/issue-22049-share-password-header-encoding
Reviewed-by: session_01WMQprn46CND82KmY8sZWBu

VERDICT: FAIL


Generated by Claude Code

…FAULT_CORS_ALLOW_HEADERS

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7e8d722037fa3f20bb056dffcd44673cda892163
Local-runs: none

Re-review after the FAIL record 6031836418 (head f7cabbb83a), which scoped it to the changeset frontmatter. Inputs read: card #22049 (body and its four comments, none added since); PR #22061 body, its 13-file list, the diff f7cabbb83a..7e8d722037 and the net diff against the merge-base a7a48b784d (747 added, 34 deleted); the check-runs on the new head, read at 2026-10-07T05:53:54Z. Read-only shape held: the head and main were fetched into owned refs and read with git show / git diff; nothing was built, run or re-run.

What the new head changes, confirmed from the diff rather than from the dispatch note: exactly one commit on top of f7cabbb83a (7e8d722037, the same branch, the model-free trailer pair), whose whole delta is one frontmatter line in .changeset/22049-share-password-header-encoding.md: "@objectstack/hono": minor. The changeset body is unchanged. The net diff against the merge-base with that one file excluded hashes byte-identical at both heads, so every other file is exactly as reviewed before. The PR file list is the same 13 files; the PR is a draft, not armed, a same-repo head, 781 changed lines, and touches no governed path.

① Derived judgments

Unchanged from 6031836418, items 1 to 11: the commit touches none of the files those judgments were made on, and the diff of every other file is byte-identical between the two heads. The new frontmatter line adds no code, no surface and no text beyond what ② judges.

② Semver level

  • Clause-②: yes (widening) is on the PR body and in the changeset body, unchanged. RIGHT, as before.
  • The frontmatter now names five packages at minor: @objectstack/types, @objectstack/plugin-sharing, @objectstack/runtime, @objectstack/plugin-hono-server and @objectstack/hono. RIGHT, and now complete. The fifth is the package the earlier record found short: @objectstack/hono spreads DEFAULT_CORS_ALLOW_HEADERS (packages/adapters/hono/src/index.ts:384), so its published default allow-list widens with the constant; minor matches the level of the package whose constant it spreads, and the entry matches the security(sharing): share-link password handling falls short of the platform's credential rules (response shape, hashing strength, transport) — detail withheld pending maintainer #21839 precedent (commit f5b8e29e37) in kind. The name resolves (packages/adapters/hono/package.json is @objectstack/hono, a member of the changeset fixed group), so changeset version will write the operator sentence about a custom allowHeaders into that adapter's own CHANGELOG, where its consumers grep. No further package is missing: the diff's code lives in the four named packages, and at the head git grep finds @objectstack/hono as the only other consumer of the CORS constant, so it is the only package whose published behaviour moves without a source change. Nothing is removed or renamed, so no migration text and no ADR-0087 marker is owed. Check Changeset concluded success on the new head.

③ Boundary flags

Unchanged from 6031836418, items 1 to 8: the six dev deviations answered, open_questions empty, the two out-of-scope carriers (the objectui encode half with the trim note; a checklist clause for the encoded form) escalated to the dispatching seat, neither blocking. No new flag, question or comment arrived with the commit: the card's comments are the same four, and the PR thread carries only the docs-drift bot comment and the earlier record.

Check-runs on the new head, read 2026-10-07T05:53:54Z: 14 success, 2 skipped by paths filter (Console Pin Gate, Packed-tarball smoke), 16 in progress, 0 failed. Of the seven required contexts, Governed Surface Queue Guard concluded success; Lint & Repo Gates, TypeScript Type Check (its source gates success; workspace, consumer gates and debt ledger still running), Test Core 1 to 6, Dogfood Regression Gate 1 to 3, Build Core and Temporal Conformance (live PG + MySQL) were still running at that read, so the landing waits for each to conclude success; this verdict is on the diff. main moved one more commit since the earlier record (2015c54028; four past the merge-base in all), and none of the four touches these packages or files.

Implemented-by: claude/issue-22049-share-password-header-encoding
Reviewed-by: session_01WMQprn46CND82KmY8sZWBu

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants