Repository navigation
fix(console): a share link's password travels in a header, and a sign-in-required link shows the sign-in path (objectui#11649) - #11757
Merged
objectstack-fleet[bot] merged 2 commits intoOct 7, 2026
Conversation
…-in-required link shows the sign-in path The share-link landing page sent a link's password as a `?password=` search param on the resolve request. It now sends it in the `X-Share-Password` request header, the name both producers read, and never in a request URL. The conversation `/messages` request carries the same header, so a protected conversation no longer reads as empty. A 401 from resolve is read by its `error.code`: `NEEDS_PASSWORD` and `WRONG_PASSWORD` show the password prompt, `SIGN_IN_REQUIRED` shows the console's sign-in path with `?redirect=` back to the link, and any other code shows the server's message. A password with a character above U+00FF, which a header cannot carry, is not sent; the prompt says why. Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…e console spy args Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-11649-share-link-password-header
branch
October 7, 2026 03:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11649
Clause-②: no
The console half of objectstack-ai/objectstack#21839, built against
@objectstack/*17.7.0 (the release this tree installs; it carriesf5b8e29e37).What changed
All in
apps/console/src/pages/, plus one changeset.SharedRecordPage'sfetchResolveseturl.searchParams.set('password', pw)onGET /api/v1/share-links/TOKEN/resolve. It now sends the password in theX-Share-Passwordrequest header and nothing in the URL. The header name and the request headers live inshared-record-shape.tsasSHARE_PASSWORD_HEADERandshareRequestHeaders./messagesrequest carries the header too. That route re-checks the password, and the page sent it nothing. So a password-protected shared conversation got404and showed "This conversation has no messages yet." The page keeps the password that unlocked the link and sends it in the same header. This is the same defect class, in the same file, under the same gates. The pin is the third case below.401is read by itserror.code.resolveGateOfmaps the three codes both producers answer with401.NEEDS_PASSWORDshows the prompt.WRONG_PASSWORDshows the prompt with "Wrong password.".SIGN_IN_REQUIREDshows "Sign in required" and a "Sign in" link to/login?redirect=back to the link, with no password input. Any other code shows the server's message instead of a prompt. The code map is typedsatisfies Partial of Record of (RegisteredErrorCode from @objectstack/spec/api, ResolveGate), so a code the spec ledger does not register fails to compile. Reverse-verified below.fetchthrowsTypeError: ... String contains non ISO-8859-1 code pointfor a password such as a CJK string or one with an emoji, before any request leaves.caféis sent and arrives intact.canSendSharePasswordrefuses a character above U+00FF. The prompt then says the password cannot be sent, the request is not built, and the page does not fall back to the URL. See the open question below.The server contract consumed (objectstack at the 17.7.0 tag,
4e4e881427)x-share-password. The sharing plugin reads it inpresentedPassword(packages/plugins/plugin-sharing/src/share-link-routes.ts) for both/resolveand/messages. The runtime dispatcher twin reads it asheaderOf('x-share-password')(packages/runtime/src/domains/share-links.ts) on both routes.X-Share-Passwordis in the Hono adapter's default CORS allow-list.?password=, and they read it before the header. The console now sends only the header and keeps no fallback.401with{ success: false, error: { code, message } }. The plugin writes it throughsendErrorin@objectstack/types. The dispatcher writes{ code }throughapiErrorResponse, andsplitSemanticCodepromotes it toerror.code. The codes areNEEDS_PASSWORD(protected, nothing sent),WRONG_PASSWORD(protected, a password sent) andSIGN_IN_REQUIRED(signed-in audience, anonymous caller). All three are in the spec'sERROR_CODE_LEDGERunder both@objectstack/plugin-sharingand@objectstack/runtime.The PM's mechanism assumptions, measured
c0862c1./login?redirect=built from the router location (location.pathname + location.search), asLoginRedirectdoes. The page's requests are a barefetchwith no bearer token, so they carry a session only as a same-origin cookie. Where that cookie does not reach the server, a visitor who has just signed in still getsSIGN_IN_REQUIRED.LoginPagesends a signed-in visitor straight back toredirect, so an automatic redirect would bounce between the two pages forever, and a link cannot. The card's ruling is "shows the sign-in path", and the link meets it.console.log/info/warn/error/debugfor every case and asserts that no call carries the password.Tests (all at
126c0cb)pnpm exec vitest run apps/console/src/pages/shared-record-shape.test.ts apps/console/src/pages/SharedRecordPage-11649.test.tsx:Test Files 2 passed (2),Tests 16 passed (16).pnpm exec vitest run --maxWorkers=2 apps/console/(the whole console project):Test Files 157 passed (157),Tests 1832 passed (1832).pnpm --filter @object-ui/console type-check: exit 0, afterturbo run build --filter=@object-ui/console^...(34 tasks successful). The type-check includes the test files. An earlier run reported TS7006 insideSharedRecordPage-11649.test.tsx, which shows they are in its file set.eslint .inapps/console(the package'slint): exit 0, 262 files, 0 errors.SharedRecordPage.tsxkeeps the same 3 warnings it has atc0862c1: twono-explicit-anyand oneset-state-in-effect. The new and edited files have none.pnpm check:eager-closureon a fresh console build: green, aggregate headroom 44.2 KB. The page is imported eagerly, so this was measured, not assumed.check:new-line-citations(0 new),check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:spec-symbols,check:phantom-deps,check:unreferenced-sources,check:installed-pin-claims,changeset:check(no major),check-changeset-presence.mjsandcheck-changeset-fixed.mjs: exit 0.check-governed-queue-guard.mjs --teston the five paths: NOT GOVERNED.check:spec-floors. Reason: it exits 1 on@object-ui/cli [no-artifact], which needs a full-workspace build, andcliis outside this build closure. It judges published.d.tsartifacts. The console publishes a bundle, and this PR's only spec import isimport type, which the bundle erases.The pins
SharedRecordPage-11649.test.tsxhas a stub server that answers both routes the way both producers do. It reads?password=first and the header otherwise, and refuses with the coded401envelope./messagesrequest carries the header and is answered 200./login?redirect=%2Fs%2FTOKENand no password input.401with an unknown code shows the server's message, with no prompt and no sign-in link.Ablation (one leg, as ordered)
The search-param line was restored through
ablation-replace.mjs(WRAP mode) and the page test was run under it.1 → 0, blob5a0ea29292af → e7085412f585, and the planted line counted 1 on disk.Tests 3 failed | 3 passed (6). The protected-link, wrong-password and/messagescases failed, each at its "no URL carries the password" assertion. The other three stayed green.blob after restore 5a0ea29292af == blob at HEAD, andgit diff HEADwas empty.Reverse verification of the ledger typing
NEEDS_PASSWORDwas planted asNEEDS_PASSWRDandtsc --noEmitwas run. It failed withTS2561 ... 'NEEDS_PASSWRD' does not exist in type 'Partial of Record of (... 271 more ..., ResolveGate)'. Did you mean to write 'NEEDS_PASSWORD'?. Restored: blob4753fc201fbf == HEAD,git diff HEADempty.Changeset
'@object-ui/console': patch. That is the level of the console-only fixes pending in.changeset/, for example the verify-email and audit-actor ones. No export, prop, type member or i18n key is added or removed.Open question for the seat
At 17.7.0 the server declares no encoding for
X-Share-Password, and a browser cannot send a header value with a character above U+00FF. So a link whose password has such a character could be opened before this PR, through the query parameter, and cannot be opened from the console after it. Such a password can be minted:ShareDialogandcreateLinkaccept any string. The page now says why instead of throwing aTypeError. The ruling (no password in any URL) is executed as written, but this regression may need the seat's call. The two choices are to land now and let a server-side encoding follow, or to hold. The report carries the finding.Acceptance notes (not filed: read off the code, not measured)
audience: signed_in,resolveTokenchecks the audience before the password, while the route's refusal probe checks the password first. An anonymous visitor who enters the right password would readWRONG_PASSWORD. The console renders what the server says. Read at4e4e881427, not reproduced. Carrier: none.VITE_SERVER_URL), a signed-in visitor may still getSIGN_IN_REQUIRED. This predates this PR. Carrier: none.Headers.ShareDialogtrims on create, so this only bites a password minted through the API with edge whitespace. It is the same family as the open question.Generated by Claude Code