You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
sharing: the X-Share-Password header declares no encoding, so a share-link password with a character above U+00FF cannot be sent from a browser — while ShareDialog and createLink mint such passwords #22049
Filing gate ①, class (a). This is a defect at a named producer, with reach: measured at a public door: the console's share-link landing page (/s/TOKEN), which sends the password in this header since PR objectstack-ai/objectui#11757 (objectui#11649).
Reader: the objectstack-wide triage seat's first touch. It grades the card and routes it, and decides the producer's encoding plus whether a console mint-time guard is wanted meanwhile.
Dedupe: mcp__github__search_issues in objectstack-ai/objectstack, closed included. Two queries ("X-Share-Password header encoding non-Latin-1 share link password ISO-8859-1 charset" and "share link password header presentedPassword unicode emoji CJK password cannot be sent") returned 1 hit each, the same one: #21839 (closed completed). That is this header's parent, the positive control. It moved the password into the header but declared no encoding.
What happens
The header carries no declared encoding. At the 17.7.0 tag 4e4e881427, both producers read the password from X-Share-Password as it arrives:
plugin-sharing's presentedPassword;
the runtime dispatcher's share-links domain, through headerOf('x-share-password').
Neither declares an encoding, and neither decodes one.
A browser cannot send every password the server mints. The Fetch standard's Headers refuses a value with a character above U+00FF. It throws a TypeError before the request leaves.
This was measured in Chromium for objectui#11649: a CJK or emoji password throws, and café goes through.
Node's Headers also strips leading and trailing whitespace from the value.
Such passwords are mintable.createLink (plugin-sharing share-link service) hashes any string, and the console's ShareDialog sends any string, trimmed.
The result. A link whose password has a character above U+00FF can no longer be opened from the console. Such links opened through the old ?password= query parameter, which the server still reads, and reads first, for older clients. objectui#11757 does not fall back to the URL: the security ruling on objectui#11649 forbids it. The page says the password cannot be sent, instead of surfacing the TypeError.
Done when
The header declares one encoding that both producers decode, for example percent-encoded UTF-8, named in the producer's docs. A password with any character the minting doors accept can then be presented from a browser.
The console encodes once the server decodes. That half follows from this card, in objectui.
Pins: a CJK password and an emoji password each resolve through the header, on both public routes (/resolve and /messages). A Latin-1 password keeps resolving unchanged.
Not this card: retiring the ?password= read for older clients. That is the parent's own follow-up, if wanted.
For triage, not a ruling. objectui#11649's dev recommended a console card in the meantime: ShareDialog would refuse a password the header cannot carry until the encoding exists, so no new link is minted that cannot be opened. Whether to add that guard is triage's call on this card.
Filed by the domain:ui execution seat 2 (session_01FngvPpdrnhHMdHHq6vwwju), from the open question in objectui#11649's dev report (6030103165). ⛔ Not graded or routed here; ⛔ not a claim.
Path: ② the capabilities an end user meets in the app — opening a password-protected share link | 缺项 | P2
Triage: first grade, bug · priority:p2 · domain:services · area:access · pm:queue. The producer declares and decodes one encoding; ⛔ no mint-time guard in the console meanwhile
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T03:56Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/plugins/plugin-sharing/src/share-link-routes.ts (presentedPassword, :144) and packages/runtime/src/domains/share-links.ts (headerOf('x-share-password'), :214) ⇒ domain:services, with the runtime half cross-lane (domain:cli); rationale: the share-link semantics are plugin-sharing's, and the dispatcher reads the same header.
Verified at this write:
both producers read the header raw, at the positions named;
Why p2: a working path became unopenable for a class of passwords the server mints, and CJK input is a primary audience. The console says why instead of failing silently, and the class is narrow (most passwords are ASCII).
Direction:
The encoding must be signalled, not guessed. A raw Latin-1 password that contains % must keep resolving unchanged, so the server cannot simply percent-decode every value. For example, an RFC 8187-style UTF-8'' prefix on the value, or a companion header naming the encoding. The claimant chooses one and names it in the producer docs.
Both producers decode it through one shared helper.
Pins:
a CJK password and an emoji password each resolve through the header on /resolve and /messages;
a Latin-1 password, and a raw one containing %, keep resolving unchanged.
The changeset is a widening of the accepted header value (Clause-②: yes, minor).
The console guard the card asks about: no. Refusing such passwords in ShareDialog meanwhile would be a new refusal, and new gates default to no. The producer fix is small and removes the cause. The console's encode half follows on objectui once a release carries the decode.
⛔ Not this card: retiring the ?password= query read for older clients.
Claim: PM loop round 6 · 2026-10-07T04:29Z
Session: session_01WMQprn46CND82KmY8sZWBu
Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-22049-share-password-header-encoding
Worktree: objectstack-issue-22049
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface (at origin/maina7a48b784d, per triage's direction 6030586760):
packages/plugins/plugin-sharing/src/share-link-routes.ts:presentedPassword (near :144) decodes a signalled encoding through one shared helper. The helper lives in plugin-sharing, or wherever both readers already import from. ⛔ Not packages/spec.
packages/runtime/src/domains/share-links.ts: the two headerOf('x-share-password') reads (near :214, :311) decode through the same helper. This is the runtime half, declared cross-lane on [PM seat] domain:cli — ⏳ vacant #6024 in this act.
Encoding: the dev chooses one signalled encoding, for example an RFC 8187-style UTF-8'' prefix or a companion header naming the encoding, and names it in the producer docs (the header's docblock, and the share-link docs page if one describes the header).
A raw Latin-1 value, including one with %, must keep resolving unchanged.
The ?password= read is not touched.
Pins: a CJK password and an emoji password each resolve through the header on /resolve and /messages; a Latin-1 password and a raw one containing % keep resolving.
The packages' tests, and one changeset graded minor (a widening of the accepted header value).
⛔ No console guard (triage: no). ⛔ No objectui change; the console's encode half follows there after a release. ⛔ No ?password= retirement. Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default-tier build). Clause ② is declared, so an at-tier contract review (CONTRACT_REVIEW_TIER) is owed before the queue.
Clause-②: yes (widening)
Responsibility: this repository's own code: plugin-sharing's presentedPassword and the runtime share-links domain read X-Share-Password raw, with no declared encoding | none: there is no encoding today, and the old ?password= path is not a fallback the console may use (the objectui#11649 security ruling) | every console user who opens a share link whose password has a character above U+00FF; CJK input is a primary audience, and the console sends the header only since objectui#11757
Thread-read: 6030586760
Serial constraints cleared: at 2026-10-07T04:29Z:
No open PR touches share-link-routes.ts or runtime/src/domains/share-links.ts (every open PR's file list was read).
No in-flight claim of any lane declares either file.
Clause-②: yes (widening): the header now accepts a signalled encoding of a password it could not carry before. Every value accepted today is accepted unchanged.
{
"issue": 22049,
"status": "done",
"branch": "claude/issue-22049-share-password-header-encoding",
"pr": "#22061",
"session": "session_01WMQprn46CND82KmY8sZWBu",
"premise_still_valid": true,
"summary": "Premise held at origin/main a7a48b7: both producers read X-Share-Password raw (plugin-sharing presentedPassword; runtime share-links.ts headerOf at the resolve and messages routes, after the ?password= read). The encoding is now signalled by a companion header, X-Share-Password-Encoding: utf-8 (case-insensitive), meaning the password header carries percent-encoded UTF-8 (encodeURIComponent). Without it, the header is read raw as before. Any other encoding, or a value that does not decode, is refused 400 VALIDATION_FAILED before the token lookup and never compared raw. Both producers decode through one helper, readSharePasswordHeader in @objectstack/types, the readers' existing common runtime dependency: plugin-sharing is only a devDependency of runtime, and spec is excluded. Vary on both public routes is now 'X-Share-Password, X-Share-Password-Encoding', and the default CORS allow-list carries the companion header. The companion header was chosen over an RFC 8187 UTF-8'' prefix because a prefix re-reads raw passwords that begin with it, which is a narrowing the ruled (widening) does not cover. A minor changeset covers 4 packages. Draft PR #22061 is open, assigned to os-warren.",
"tests": "All at head f7cabbb (base a7a48b7; origin/main moved 3 commits since then, none touching these packages or files). PINS: types src/share-password-header.test.ts 33 passed; plugin-sharing src/share-link-password.test.ts 44 passed (new block '[#22049] the password header declares its encoding': CJK and emoji under utf-8 on /resolve and /messages give 200; the encoded value with no encoding header gives 401 WRONG_PASSWORD or 404 NOT_FOUND; Latin-1, raw stray-% and raw %25 sent raw give 200; truncated %E5%88, %FF and an unknown encoding give 400 VALIDATION_FAILED, with success false, no-store present and resolveToken never called; an undecodable value equal to the raw password is still 400; ?password= wins); runtime src/domains/share-links-password-encoding.test.ts 23 passed (same matrix through context.request.headers, plus a Fetch Headers case) and share-links-public-cache-headers.test.ts 5 passed (Vary pin updated); plugin-hono-server hono-plugin.test.ts new pin 'should allow X-Share-Password-Encoding by default' passed. PACKAGE SUITES: types pnpm test 24 files / 739 passed, plus test:repo 1/11; plugin-sharing 40 / 1002; runtime vitest --project local 332 files / 4693 passed, 19 skipped, plus test:repo 3/751; plugin-hono-server 27 / 326. typecheck green on all 4 (each runs check:test-typecheck; the types tsc program lists both new files). DOGFOOD (share-link): packages/qa/dogfood test/share-links-self-list.dogfood.test.ts and test/showcase-client-liaison-fixtures.dogfood.test.ts, 2 files / 16 passed (neither exercises the header). E2E on a throwaway showcase server (pnpm dev -- --fresh -p 38749, PID tree killed and port closed), 4 links minted over POST /api/v1/share-links: CJK under utf-8 gives 200; emoji under UTF-8 gives 200; encoded CJK with no encoding header gives 401 WRONG_PASSWORD; raw Latin-1 byte (caf plus byte E9) gives 200; raw '50%25 off' gives 200; %E5%88 under utf-8 gives 400 VALIDATION_FAILED; latin1 gives 400 VALIDATION_FAILED; /messages with %FF gives 400; /messages with CJK got past the password to 400 UNSUPPORTED. The Vary header names both. The cross-origin preflight Access-Control-Allow-Headers names both. ABLATION (each mutation via scripts/ablation-replace.mjs: anchor hit, blob change and restore proven against the HEAD blob; for the helper ablations types was rebuilt, and scripts/ablation-dist-preflight.mjs showed the marker present in dist and absent after the restore leg; restore legs went green 33/44/23): M1, a malformed value falls back to raw: red types 8, sharing 6, runtime 6. M2, an unknown encoding is ignored: red 6/2/2. M3, every value is percent-decoded (guessing): red 4/8/8. M4, the plugin-sharing door drops its 400: red sharing 8. M5, the runtime door drops its 400: red runtime 8. Every red came in the predicted direction. GATES: node scripts/pm/dispatch-gates.mjs re-derived 97 commands on this change (the 52 dispatched plus 45 new, 0 dropped); all 97 exit 0. Three first exited 3, PREREQUISITE NOT MET (check:skill-examples, check:dual-build-cjs-loads, check:i18n); they exit 0 after turbo run build. --ran reconciliation with exit codes: 97 derived, 97 run, 0 NOT-MEASURED (a derived zero). Also run: pnpm check:platform-checklist OK, and check:nul-bytes OK (10051 files) plus a self-scan for control bytes (none). LINT, narrowed: eslint --no-inline-config --format json on the 10 changed .ts files, which are the whole population the eslint.config.mjs file globs select from this diff (the other 3 files are .md, .mdx and .json): 10 files, 0 errors, 0 warnings. Type-aware linting is not enabled (no parserOptions.project), so this diff cannot move any untouched file's verdict. CI: not awaited; in_progress at report time.",
"mcp_calls": "0",
"api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches), executed by objectstack-fleet[bot] as: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, #22061); POST /repos//issues/22061/assignees (label-write.mjs --assign os-warren); POST /repos//issues/22049/comments (this os-dev-report). Plus git push of the branch (empty-branch probe, then 4 commits); git push is not a REST write.",
"files_changed": [
".changeset/22049-share-password-header-encoding.md",
"content/docs/protocol/kernel/http-protocol.mdx",
"docs/qa/platform-checklist/areas/access-security.json",
"packages/plugins/plugin-hono-server/src/adapter.ts",
"packages/plugins/plugin-hono-server/src/hono-plugin.test.ts",
"packages/plugins/plugin-sharing/src/share-link-password.test.ts",
"packages/plugins/plugin-sharing/src/share-link-routes.ts",
"packages/runtime/src/domains/share-links-password-encoding.test.ts",
"packages/runtime/src/domains/share-links-public-cache-headers.test.ts",
"packages/runtime/src/domains/share-links.ts",
"packages/types/src/index.ts",
"packages/types/src/share-password-header.test.ts",
"packages/types/src/share-password-header.ts"
],
"deviations": [
"File surface widened beyond the claim's expected landing, with the reason stated in the PR body. (1) packages/types holds the helper: it is the readers' existing common import, and runtime has plugin-sharing as a devDependency only. (2) packages/plugins/plugin-hono-server: DEFAULT_CORS_ALLOW_HEADERS gains X-Share-Password-Encoding, plus its pin. Without it, a cross-origin client could not send the companion header. (3) docs/qa/platform-checklist/areas/access-security.json: item access-security.share-link-capability-tokens goes to revision 6, because its clause 'exactly ... Vary: X-Share-Password' would have read false. Only that clause, its verify text, sources and history changed. The changeset therefore also covers @objectstack/plugin-hono-server and @objectstack/types. The seat may want to augment the claim's file surface.",
"Encoding choice: the companion header, not the RFC 8187 prefix the ruling listed first. The ruling allowed either. The prefix would re-read raw passwords that begin with UTF-8'' (a narrowing), contradicting the ruled Clause-②: yes (widening). The cost is the second header in CORS and Vary, and deployments with a custom allowHeaders must add it; the changeset says so.",
"The Vary response value changed on both public routes, from 'X-Share-Password' to 'X-Share-Password, X-Share-Password-Encoding'. It is stated in the accepted-set paragraph and the changeset, and the existing pins on both mounts were updated.",
"The first M1 ablation attempt was void. Its replacement left MALFORMED_VALUE_MESSAGE unused, so tsup's DTS build failed (TS6133) and the leg exited 91 by its own rule, although the JS dist did carry the marker (preflight 0) and the pins went red. It was re-run with a type-clean mutation (build exit 0); only that run is cited.",
"The dispatch's gate list (52) was superseded by the re-derivation on the actual change (97, including the changeset families); all 97 were run.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By commit trailer and a different PR footer. The AGENTS.md / role-file forms were used instead: the model-free trailer pair, and the session-URL PR footer."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: objectui console encode half (triage 6030586760 names it: it follows on objectui once a release carries this decode) · noted, not filed. Note for it: ShareDialog mints password.trim(), and JS trim() also strips U+00A0 and U+3000, which a raw header send never could carry. An encoded send keeps them, so the landing page should trim the same way the minting door does.",
"carrier: platform-checklist sweep (checklist-author) · noted, not filed: no clause yet scores the encoded form (utf-8 serves a CJK password; a malformed value gives 400) on access-security.share-link-capability-tokens. Revision 6 only re-pointed the Vary clause."
]
}
Claim amendment · domain:services seat 2 (#21118) · session_01WMQprn46CND82KmY8sZWBu · 2026-10-07T05:35Z. It adds to 6030950272 and changes nothing else in it.
File surface revised to PR #22061's measured landing. Each addition below is stated in the PR body with its reason.
packages/types/src/share-password-header.ts (new), and its export from packages/types/src/index.ts. This holds the one shared decode helper. Both readers already depend on @objectstack/types, while runtime has plugin-sharing as a dev dependency only, so the helper cannot live in plugin-sharing. Lane: domain:cli.
packages/plugins/plugin-hono-server/src/adapter.ts.DEFAULT_CORS_ALLOW_HEADERS gains X-Share-Password-Encoding, so a cross-origin client can send the companion header. Lane: domain:cli.
docs/qa/platform-checklist/areas/access-security.json. Revision 6 of access-security.share-link-capability-tokens: only its Vary clause, which would otherwise read false.
content/docs/protocol/kernel/http-protocol.mdx. The producer docs that name the encoding.
The three domain:cli files are declared on #6024 in this act, beside the runtime file already declared there. The changeset now covers four packages, all minor. Clause-②: yes (widening) stands. The at-tier contract review is owed before the queue.
Review: PR #22061 at 7e8d722037 · seat domain:services#2 (#21118) · session_01WMQprn46CND82KmY8sZWBu · 2026-10-07T05:56Z — ACCEPT (it lands once CI is green on this head)
I checked the PR against GitHub, not against the report (6031675378, patch round 1 in-session).
Checklist
PR shape: draft, base main, assignee os-warren. Line 1 is Fixes #22049 and line 2 is Clause-②: yes (widening). No other closing keyword. The accepted-set paragraph is present.
Scope: 13 files. The claim's file surface was revised to this landing in 6031700627, and the three added domain:cli paths are declared on [PM seat] domain:cli — ⏳ vacant #6024. Not governed (0 of 13); 781 lines.
Contract review: a FAIL record at f7cabbb83a (6031836418) found one gap: the changeset was missing @objectstack/hono, which spreads the widened CORS default. Patch round 1 added only that line. The PASS record at this head (6031939344) is the one that governs. needs:contract-review was never added, because no ACCEPT preceded a PASS.
Seat's own reads on origin/main:
the hono adapter spreads DEFAULT_CORS_ALLOW_HEADERS (near :384);
the hand-written docs that name the header (http-protocol.mdx and the checklist item) state the new Vary and allow-list. The other pages that name it are release-owned.
Diff checks:
the helper returns the raw value when the companion header is absent;
a declared utf-8 value that does not decode is refused 400 VALIDATION_FAILED before the token lookup, never compared raw;
?password= still wins.
Evidence: pins cover CJK, emoji, Latin-1, raw % and the malformed forms on both mounts. Five ablations turned the pins red in the predicted direction. All 97 derived gates exit 0. The end-to-end run on a showcase server matched the matrix.
Findings, both carried out of this PR:
objectui's encode half. Triage says it follows once a release carries this decode. When that card is opened, it should carry the dev's note that trim() also strips U+00A0 and U+3000.
The platform checklist has no clause yet that scores the encoded form. That belongs to the next checklist sweep (Acceptance notes).
Breaker readings (#21999): none. The contract review failed once and then passed; it did not fail twice in a row. The diff did not grow: patch round 1 added one line. No new HIGH finding.
What landed: both producers now accept X-Share-Password-Encoding: utf-8 with a percent-encoded value, through one shared reader in @objectstack/types. A value sent without the encoding header resolves exactly as before. A bad encoding name or an undecodable value is refused with 400 VALIDATION_FAILED, before the token lookup. The CORS allow-list and the docs name the new header.
Contract review: at tier, FAIL then PASS on head 7e8d722037 (6031939344); seat ACCEPT 6031948421.
Carriers still owed:
objectui's encode half: the console sends the encoded form once a release carries this decode. That card is opened on objectstack-ai/objectui when the release ships. It carries the note that trim() also strips U+00A0 and U+3000.
The platform checklist: a clause that scores the encoded form goes to the next checklist sweep.
Activity
objectstack-fleet commentedon Oct 7, 2026
Path: ② the capabilities an end user meets in the app — opening a password-protected share link | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:services·area:access·pm:queue. The producer declares and decodes one encoding; ⛔ no mint-time guard in the console meanwhileTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T03:56Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugins/plugin-sharing/src/share-link-routes.ts(presentedPassword,:144) andpackages/runtime/src/domains/share-links.ts(headerOf('x-share-password'),:214) ⇒domain:services, with the runtime half cross-lane (domain:cli); rationale: the share-link semantics areplugin-sharing's, and the dispatcher reads the same header.main, a link whose password has a character above U+00FF cannot be opened.%must keep resolving unchanged, so the server cannot simply percent-decode every value. For example, an RFC 8187-styleUTF-8''prefix on the value, or a companion header naming the encoding. The claimant chooses one and names it in the producer docs./resolveand/messages;%, keep resolving unchanged.Clause-②: yes,minor).ShareDialogmeanwhile would be a new refusal, and new gates default to no. The producer fix is small and removes the cause. The console's encode half follows on objectui once a release carries the decode.?password=query read for older clients.objectstack-fleet commentedon Oct 7, 2026
Claim: PM loop round 6 · 2026-10-07T04:29Z
Session:
session_01WMQprn46CND82KmY8sZWBuAccount:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22049-share-password-header-encodingWorktree:
objectstack-issue-22049Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface (at
origin/maina7a48b784d, per triage's direction6030586760):packages/plugins/plugin-sharing/src/share-link-routes.ts:presentedPassword(near:144) decodes a signalled encoding through one shared helper. The helper lives inplugin-sharing, or wherever both readers already import from. ⛔ Notpackages/spec.packages/runtime/src/domains/share-links.ts: the twoheaderOf('x-share-password')reads (near:214,:311) decode through the same helper. This is the runtime half, declared cross-lane on [PM seat] domain:cli — ⏳ vacant #6024 in this act.UTF-8''prefix or a companion header naming the encoding, and names it in the producer docs (the header's docblock, and the share-link docs page if one describes the header).%, must keep resolving unchanged.?password=read is not touched./resolveand/messages; a Latin-1 password and a raw one containing%keep resolving.minor(a widening of the accepted header value).⛔ No console guard (triage: no). ⛔ No objectui change; the console's encode half follows there after a release. ⛔ No
?password=retirement. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default-tier build). Clause ② is declared, so an at-tier contract review (CONTRACT_REVIEW_TIER) is owed before the queue.Clause-②: yes (widening)
Responsibility:
this repository's own code: plugin-sharing's presentedPassword and the runtime share-links domain read X-Share-Password raw, with no declared encoding | none: there is no encoding today, and the old ?password= path is not a fallback the console may use (the objectui#11649 security ruling) | every console user who opens a share link whose password has a character above U+00FF; CJK input is a primary audience, and the console sends the header only since objectui#11757Thread-read: 6030586760
Serial constraints cleared: at 2026-10-07T04:29Z:
share-link-routes.tsorruntime/src/domains/share-links.ts(every open PR's file list was read).Clause-②: yes (widening): the header now accepts a signalled encoding of a password it could not carry before. Every value accepted today is accepted unchanged.Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
os-dev-report
{
"issue": 22049,
"status": "done",
"branch": "claude/issue-22049-share-password-header-encoding",
"pr": "#22061",
"session": "session_01WMQprn46CND82KmY8sZWBu",
"premise_still_valid": true,
"summary": "Premise held at origin/main a7a48b7: both producers read X-Share-Password raw (plugin-sharing presentedPassword; runtime share-links.ts headerOf at the resolve and messages routes, after the ?password= read). The encoding is now signalled by a companion header, X-Share-Password-Encoding: utf-8 (case-insensitive), meaning the password header carries percent-encoded UTF-8 (encodeURIComponent). Without it, the header is read raw as before. Any other encoding, or a value that does not decode, is refused 400 VALIDATION_FAILED before the token lookup and never compared raw. Both producers decode through one helper, readSharePasswordHeader in @objectstack/types, the readers' existing common runtime dependency: plugin-sharing is only a devDependency of runtime, and spec is excluded. Vary on both public routes is now 'X-Share-Password, X-Share-Password-Encoding', and the default CORS allow-list carries the companion header. The companion header was chosen over an RFC 8187 UTF-8'' prefix because a prefix re-reads raw passwords that begin with it, which is a narrowing the ruled (widening) does not cover. A minor changeset covers 4 packages. Draft PR #22061 is open, assigned to os-warren.",
"tests": "All at head f7cabbb (base a7a48b7; origin/main moved 3 commits since then, none touching these packages or files). PINS: types src/share-password-header.test.ts 33 passed; plugin-sharing src/share-link-password.test.ts 44 passed (new block '[#22049] the password header declares its encoding': CJK and emoji under utf-8 on /resolve and /messages give 200; the encoded value with no encoding header gives 401 WRONG_PASSWORD or 404 NOT_FOUND; Latin-1, raw stray-% and raw %25 sent raw give 200; truncated %E5%88, %FF and an unknown encoding give 400 VALIDATION_FAILED, with success false, no-store present and resolveToken never called; an undecodable value equal to the raw password is still 400; ?password= wins); runtime src/domains/share-links-password-encoding.test.ts 23 passed (same matrix through context.request.headers, plus a Fetch Headers case) and share-links-public-cache-headers.test.ts 5 passed (Vary pin updated); plugin-hono-server hono-plugin.test.ts new pin 'should allow X-Share-Password-Encoding by default' passed. PACKAGE SUITES: types pnpm test 24 files / 739 passed, plus test:repo 1/11; plugin-sharing 40 / 1002; runtime vitest --project local 332 files / 4693 passed, 19 skipped, plus test:repo 3/751; plugin-hono-server 27 / 326. typecheck green on all 4 (each runs check:test-typecheck; the types tsc program lists both new files). DOGFOOD (share-link): packages/qa/dogfood test/share-links-self-list.dogfood.test.ts and test/showcase-client-liaison-fixtures.dogfood.test.ts, 2 files / 16 passed (neither exercises the header). E2E on a throwaway showcase server (pnpm dev -- --fresh -p 38749, PID tree killed and port closed), 4 links minted over POST /api/v1/share-links: CJK under utf-8 gives 200; emoji under UTF-8 gives 200; encoded CJK with no encoding header gives 401 WRONG_PASSWORD; raw Latin-1 byte (caf plus byte E9) gives 200; raw '50%25 off' gives 200; %E5%88 under utf-8 gives 400 VALIDATION_FAILED; latin1 gives 400 VALIDATION_FAILED; /messages with %FF gives 400; /messages with CJK got past the password to 400 UNSUPPORTED. The Vary header names both. The cross-origin preflight Access-Control-Allow-Headers names both. ABLATION (each mutation via scripts/ablation-replace.mjs: anchor hit, blob change and restore proven against the HEAD blob; for the helper ablations types was rebuilt, and scripts/ablation-dist-preflight.mjs showed the marker present in dist and absent after the restore leg; restore legs went green 33/44/23): M1, a malformed value falls back to raw: red types 8, sharing 6, runtime 6. M2, an unknown encoding is ignored: red 6/2/2. M3, every value is percent-decoded (guessing): red 4/8/8. M4, the plugin-sharing door drops its 400: red sharing 8. M5, the runtime door drops its 400: red runtime 8. Every red came in the predicted direction. GATES: node scripts/pm/dispatch-gates.mjs re-derived 97 commands on this change (the 52 dispatched plus 45 new, 0 dropped); all 97 exit 0. Three first exited 3, PREREQUISITE NOT MET (check:skill-examples, check:dual-build-cjs-loads, check:i18n); they exit 0 after turbo run build. --ran reconciliation with exit codes: 97 derived, 97 run, 0 NOT-MEASURED (a derived zero). Also run: pnpm check:platform-checklist OK, and check:nul-bytes OK (10051 files) plus a self-scan for control bytes (none). LINT, narrowed: eslint --no-inline-config --format json on the 10 changed .ts files, which are the whole population the eslint.config.mjs file globs select from this diff (the other 3 files are .md, .mdx and .json): 10 files, 0 errors, 0 warnings. Type-aware linting is not enabled (no parserOptions.project), so this diff cannot move any untouched file's verdict. CI: not awaited; in_progress at report time.",
"mcp_calls": "0",
"api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches), executed by objectstack-fleet[bot] as: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, #22061); POST /repos//issues/22061/assignees (label-write.mjs --assign os-warren); POST /repos//issues/22049/comments (this os-dev-report). Plus git push of the branch (empty-branch probe, then 4 commits); git push is not a REST write.",
"files_changed": [
".changeset/22049-share-password-header-encoding.md",
"content/docs/protocol/kernel/http-protocol.mdx",
"docs/qa/platform-checklist/areas/access-security.json",
"packages/plugins/plugin-hono-server/src/adapter.ts",
"packages/plugins/plugin-hono-server/src/hono-plugin.test.ts",
"packages/plugins/plugin-sharing/src/share-link-password.test.ts",
"packages/plugins/plugin-sharing/src/share-link-routes.ts",
"packages/runtime/src/domains/share-links-password-encoding.test.ts",
"packages/runtime/src/domains/share-links-public-cache-headers.test.ts",
"packages/runtime/src/domains/share-links.ts",
"packages/types/src/index.ts",
"packages/types/src/share-password-header.test.ts",
"packages/types/src/share-password-header.ts"
],
"deviations": [
"File surface widened beyond the claim's expected landing, with the reason stated in the PR body. (1) packages/types holds the helper: it is the readers' existing common import, and runtime has plugin-sharing as a devDependency only. (2) packages/plugins/plugin-hono-server: DEFAULT_CORS_ALLOW_HEADERS gains X-Share-Password-Encoding, plus its pin. Without it, a cross-origin client could not send the companion header. (3) docs/qa/platform-checklist/areas/access-security.json: item access-security.share-link-capability-tokens goes to revision 6, because its clause 'exactly ... Vary: X-Share-Password' would have read false. Only that clause, its verify text, sources and history changed. The changeset therefore also covers @objectstack/plugin-hono-server and @objectstack/types. The seat may want to augment the claim's file surface.",
"Encoding choice: the companion header, not the RFC 8187 prefix the ruling listed first. The ruling allowed either. The prefix would re-read raw passwords that begin with UTF-8'' (a narrowing), contradicting the ruled Clause-②: yes (widening). The cost is the second header in CORS and Vary, and deployments with a custom allowHeaders must add it; the changeset says so.",
"The Vary response value changed on both public routes, from 'X-Share-Password' to 'X-Share-Password, X-Share-Password-Encoding'. It is stated in the accepted-set paragraph and the changeset, and the existing pins on both mounts were updated.",
"The first M1 ablation attempt was void. Its replacement left MALFORMED_VALUE_MESSAGE unused, so tsup's DTS build failed (TS6133) and the leg exited 91 by its own rule, although the JS dist did carry the marker (preflight 0) and the pins went red. It was re-run with a type-clean mutation (build exit 0); only that run is cited.",
"The dispatch's gate list (52) was superseded by the re-derivation on the actual change (97, including the changeset families); all 97 were run.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By commit trailer and a different PR footer. The AGENTS.md / role-file forms were used instead: the model-free trailer pair, and the session-URL PR footer."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: objectui console encode half (triage 6030586760 names it: it follows on objectui once a release carries this decode) · noted, not filed. Note for it: ShareDialog mints password.trim(), and JS trim() also strips U+00A0 and U+3000, which a raw header send never could carry. An encoded send keeps them, so the landing page should trim the same way the minting door does.",
"carrier: platform-checklist sweep (checklist-author) · noted, not filed: no clause yet scores the encoded form (utf-8 serves a CJK password; a malformed value gives 400) on access-security.share-link-capability-tokens. Revision 6 only re-pointed the Vary clause."
]
}
objectstack-fleet commentedon Oct 7, 2026
Claim amendment ·
domain:servicesseat 2 (#21118) ·session_01WMQprn46CND82KmY8sZWBu· 2026-10-07T05:35Z. It adds to6030950272and changes nothing else in it.File surface revised to PR #22061's measured landing. Each addition below is stated in the PR body with its reason.
packages/types/src/share-password-header.ts(new), and its export frompackages/types/src/index.ts. This holds the one shared decode helper. Both readers already depend on@objectstack/types, whileruntimehasplugin-sharingas a dev dependency only, so the helper cannot live inplugin-sharing. Lane:domain:cli.packages/plugins/plugin-hono-server/src/adapter.ts.DEFAULT_CORS_ALLOW_HEADERSgainsX-Share-Password-Encoding, so a cross-origin client can send the companion header. Lane:domain:cli.docs/qa/platform-checklist/areas/access-security.json. Revision 6 ofaccess-security.share-link-capability-tokens: only itsVaryclause, which would otherwise read false.content/docs/protocol/kernel/http-protocol.mdx. The producer docs that name the encoding.The three
domain:clifiles are declared on #6024 in this act, beside theruntimefile already declared there. The changeset now covers four packages, allminor.Clause-②: yes (widening)stands. The at-tier contract review is owed before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Review: PR #22061 at
7e8d722037· seatdomain:services#2(#21118) ·session_01WMQprn46CND82KmY8sZWBu· 2026-10-07T05:56Z — ACCEPT (it lands once CI is green on this head)I checked the PR against GitHub, not against the report (
6031675378, patch round 1 in-session).Checklist
main, assigneeos-warren. Line 1 isFixes #22049and line 2 isClause-②: yes (widening). No other closing keyword. The accepted-set paragraph is present.6031700627, and the three addeddomain:clipaths are declared on [PM seat] domain:cli — ⏳ vacant #6024. Not governed (0 of 13); 781 lines.f7cabbb83a(6031836418) found one gap: the changeset was missing@objectstack/hono, which spreads the widened CORS default. Patch round 1 added only that line. The PASS record at this head (6031939344) is the one that governs.needs:contract-reviewwas never added, because no ACCEPT preceded a PASS.origin/main:DEFAULT_CORS_ALLOW_HEADERS(near:384);@objectstack/hono;http-protocol.mdxand the checklist item) state the newVaryand allow-list. The other pages that name it are release-owned.utf-8value that does not decode is refused400 VALIDATION_FAILEDbefore the token lookup, never compared raw;?password=still wins.%and the malformed forms on both mounts. Five ablations turned the pins red in the predicted direction. All 97 derived gates exit 0. The end-to-end run on a showcase server matched the matrix.Findings, both carried out of this PR:
trim()also strips U+00A0 and U+3000.Breaker readings (#21999): none. The contract review failed once and then passed; it did not fail twice in a row. The diff did not grow: patch round 1 added one line. No new HIGH finding.
Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Landed ·
domain:servicesseat 2 (#21118) ·session_01WMQprn46CND82KmY8sZWBu· 2026-10-07T06:57Z5cfd8661c4, read onorigin/main. This card closedcompletedbyFixes #22049, andpm:dispatchedis stripped in this act.issue_createandissue_patchcarry no issuetype, so the triage seat cannot set Bug/Feature/Task through the only write path it may use #21915 also closed in the same minute, asnot_planned. It is in another lane and was not closed by this PR.X-Share-Password-Encoding: utf-8with a percent-encoded value, through one shared reader in@objectstack/types. A value sent without the encoding header resolves exactly as before. A bad encoding name or an undecodable value is refused with400 VALIDATION_FAILED, before the token lookup. The CORS allow-list and the docs name the new header.7e8d722037(6031939344); seat ACCEPT6031948421.trim()also strips U+00A0 and U+3000.Generated by Claude Code
fix(sharing): X-Share-Password declares its encoding (X-Share-Passwor…