Skip to content

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 into
mainfrom
claude/issue-11649-share-link-password-header
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11649-share-link-password-header

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11649

Clause-②: no

The console half of objectstack-ai/objectstack#21839, built against @objectstack/* 17.7.0 (the release this tree installs; it carries f5b8e29e37).

What changed

All in apps/console/src/pages/, plus one changeset.

  1. The password leaves the request URL. SharedRecordPage's fetchResolve set url.searchParams.set('password', pw) on GET /api/v1/share-links/TOKEN/resolve. It now sends the password in the X-Share-Password request header and nothing in the URL. The header name and the request headers live in shared-record-shape.ts as SHARE_PASSWORD_HEADER and shareRequestHeaders.
  2. The /messages request carries the header too. That route re-checks the password, and the page sent it nothing. So a password-protected shared conversation got 404 and 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.
  3. A 401 is read by its error.code. resolveGateOf maps the three codes both producers answer with 401. NEEDS_PASSWORD shows the prompt. WRONG_PASSWORD shows the prompt with "Wrong password.". SIGN_IN_REQUIRED shows "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 typed satisfies 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.
  4. A password the header cannot carry is not sent. A header value is a byte string. Measured in Chromium, fetch throws TypeError: ... String contains non ISO-8859-1 code point for a password such as a CJK string or one with an emoji, before any request leaves. café is sent and arrives intact. canSendSharePassword refuses 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)

  • Header: x-share-password. The sharing plugin reads it in presentedPassword (packages/plugins/plugin-sharing/src/share-link-routes.ts) for both /resolve and /messages. The runtime dispatcher twin reads it as headerOf('x-share-password') (packages/runtime/src/domains/share-links.ts) on both routes. X-Share-Password is in the Hono adapter's default CORS allow-list.
  • The query parameter: both mounts still accept ?password=, and they read it before the header. The console now sends only the header and keeps no fallback.
  • Refusals: 401 with { success: false, error: { code, message } }. The plugin writes it through sendError in @objectstack/types. The dispatcher writes { code } through apiErrorResponse, and splitSemanticCode promotes it to error.code. The codes are NEEDS_PASSWORD (protected, nothing sent), WRONG_PASSWORD (protected, a password sent) and SIGN_IN_REQUIRED (signed-in audience, anonymous caller). All three are in the spec's ERROR_CODE_LEDGER under both @objectstack/plugin-sharing and @objectstack/runtime.

The PM's mechanism assumptions, measured

  • The current request: confirmed at c0862c1.
  • The server contract: as above.
  • The prompt: the order assumed that a sign-in-required answer "routes to" sign-in. This PR shows the sign-in path as a link instead of redirecting on its own. It uses the console's convention: /login?redirect= built from the router location (location.pathname + location.search), as LoginRedirect does. The page's requests are a bare fetch with 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 gets SIGN_IN_REQUIRED. LoginPage sends a signed-in visitor straight back to redirect, 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.
  • Logging: the page writes no console line. The page test spies on console.log/info/warn/error/debug for 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, after turbo run build --filter=@object-ui/console^... (34 tasks successful). The type-check includes the test files. An earlier run reported TS7006 inside SharedRecordPage-11649.test.tsx, which shows they are in its file set.
  • eslint . in apps/console (the package's lint): exit 0, 262 files, 0 errors. SharedRecordPage.tsx keeps the same 3 warnings it has at c0862c1: two no-explicit-any and one set-state-in-effect. The new and edited files have none.
  • pnpm check:eager-closure on 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.mjs and check-changeset-fixed.mjs: exit 0. check-governed-queue-guard.mjs --test on the five paths: NOT GOVERNED.
  • NOT MEASURED: check:spec-floors. Reason: it exits 1 on @object-ui/cli [no-artifact], which needs a full-workspace build, and cli is outside this build closure. It judges published .d.ts artifacts. The console publishes a bundle, and this PR's only spec import is import type, which the bundle erases.

The pins

SharedRecordPage-11649.test.tsx has a stub server that answers both routes the way both producers do. It reads ?password= first and the header otherwise, and refuses with the coded 401 envelope.

  • A protected link prompts. The password reaches resolve in the header, the server answers 200, and no request URL carries the password in any spelling.
  • A wrong password shows "Wrong password." on the prompt.
  • An unlocked conversation's /messages request carries the header and is answered 200.
  • A password with a character the header cannot carry is never sent. The prompt says why, and only the first password-less resolve goes out.
  • A sign-in-required link shows the "Sign in" link to /login?redirect=%2Fs%2FTOKEN and no password input.
  • A 401 with 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.

  • The first attempt was a no-op. The replacement contained the anchor, so the tool refused it: "the anchor count moved 1 → 1". It was re-run with an anchor spanning the line boundary.
  • The landed mutation: anchor 1 → 0, blob 5a0ea29292af → e7085412f585, and the planted line counted 1 on disk.
  • Predicted direction: red on exactly the three cases that assert on URLs. Observed: Tests 3 failed | 3 passed (6). The protected-link, wrong-password and /messages cases failed, each at its "no URL carries the password" assertion. The other three stayed green.
  • Restore: blob after restore 5a0ea29292af == blob at HEAD, and git diff HEAD was empty.

Reverse verification of the ledger typing

NEEDS_PASSWORD was planted as NEEDS_PASSWRD and tsc --noEmit was run. It failed with TS2561 ... 'NEEDS_PASSWRD' does not exist in type 'Partial of Record of (... 271 more ..., ResolveGate)'. Did you mean to write 'NEEDS_PASSWORD'?. Restored: blob 4753fc201fbf == HEAD, git diff HEAD empty.

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: ShareDialog and createLink accept any string. The page now says why instead of throwing a TypeError. 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)

  • A password plus signed-in link. For a link with both a password and audience: signed_in, resolveToken checks the audience before the password, while the route's refusal probe checks the password first. An anonymous visitor who enters the right password would read WRONG_PASSWORD. The console renders what the server says. Read at 4e4e881427, not reproduced. Carrier: none.
  • Cross-origin deployments. The landing page's requests carry a session only as a same-origin cookie. In a deployment that serves the API cross-origin (VITE_SERVER_URL), a signed-in visitor may still get SIGN_IN_REQUIRED. This predates this PR. Carrier: none.
  • Whitespace. Header normalization strips a password's leading and trailing whitespace. Measured in Node's Headers. ShareDialog trims 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

claude added 2 commits October 7, 2026 02:42
…-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>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 332 chunks) 3507.5 KB 3551.8 KB
Main entry chunk (gzip) 155.3 KB 350 KB
Entry file index-BSRoUIJ-.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 578.94KB 139.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.88KB 67.46KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 37.51KB 10.04KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.22KB 38.99KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.73KB 65.20KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.12KB 45.90KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.64KB 22.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 03:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 03:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 77c12b9 Oct 7, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11649-share-link-password-header branch October 7, 2026 03:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants