Skip to content

fix(app-shell): invitation links resolve through the console mount helper instead of rebuilding BASE_URL (#4472) - #4480

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4472-accept-url-mount
Aug 12, 2026
Merged

fix(app-shell): invitation links resolve through the console mount helper instead of rebuilding BASE_URL (#4472)#4480
yinlianghui merged 1 commit into
mainfrom
claude/issue-4472-accept-url-mount

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Closes#4472

The defect

Copying an invitation link from the workspace Invitations tab produced a URL the recipient cannot open:

http://localhost:8080./accept-invitation/bgn1XAfRuCQUS1l9NBqOMGgJDUYm5l56

Both copy sites carried their own buildAcceptUrl, glueing ${window.location.origin} onto import.meta.env.BASE_URL:

  • packages/app-shell/src/console/organizations/manage/InvitationsPage.tsx:32 — the Invitations tab's per-row copy button;
  • packages/app-shell/src/console/organizations/manage/InviteMemberDialog.tsx:39 — the freshly-created invitation's "Accept link" field (and its copy button).

In the portable build the console ships (base: './', apps/console/vite.config.ts) BASE_URL is the literal '.', so the concatenation put a dot straight after the authority — a trailing-dot host. Removing the dot by hand did not rescue the link either: the resulting /accept-invitation/:id skips the deployment mount, and a console served under /_console/ answers that path with {"error":"Not found"}. This is precisely the failure resolveHomeUrl.ts's own header records as retired.

The fix

Both local copies are deleted. The two sites call resolveConsoleUrl from packages/app-shell/src/console/organizations/resolveHomeUrl.ts — the mount-aware helper WorkspaceSwitcher and OrganizationsPage already use for full-page navigations. It resolves against the base-href tag (base href="/_console/") the server injects when it serves the console (objectstack packages/cli/src/utils/console.ts), so the copied link carries the mount in every deployment shape. One resolver, one answer: no URL assembly survives in either file. The invite dialog resolves once into a single acceptUrl, so its displayed field and its clipboard cannot drift apart.

No public surface moved: the emitted .d.ts for both modules is byte-identical to the origin/main build (measured, two builds diffed) — hence patch.

Red-first

packages/app-shell/src/console/organizations/__tests__/acceptInvitationLink.mount.test.tsx drives both copy sites through the real components and asserts the produced URL whole, in both deployment shapes (/_console/ mount and root mount).

Against the current builder the mount half failed exactly as reported:

AssertionError: expected '/accept-invitation/bgn1XAfRuCQUS1l9NB…' to be '/_console/accept-invitation/bgn1XAfRu…'
Expected: "/_console/accept-invitation/bgn1XAfRuCQUS1l9NBqOMGgJDUYm5l56"
Received: "/accept-invitation/bgn1XAfRuCQUS1l9NBqOMGgJDUYm5l56"

The trailing-dot half needed one measurement before it could be trusted, and the finding is worth recording: vi.stubEnv('BASE_URL', '.') reaches only the exact import.meta.env.BASE_URL spelling. The retired builders read (import.meta as any).env?.BASE_URL, and Vite inlines a bare import.meta.env as an object literal at transform time — so that read saw '/' in vitest no matter what was stubbed, and the portable-build condition could not be reproduced through it. Switching only the spelling in the two retired builders (temporarily, then reverted with git checkout) made the pin produce the bug report's string verbatim:

AssertionError: expected 'http://localhost:8080./accept-invitat…' not to match /:\d+\./
Received: "http://localhost:8080./accept-invitation/bgn1XAfRuCQUS1l9NBqOMGgJDUYm5l56"

So the shipped pin goes red on a reintroduced local builder via the mount assertion; the trailing-dot assertion is carried alongside it and documented in the test as unable to fail on its own in vitest, rather than left to read as coverage it does not have.

After the fix, all 4 cases green.

Verification

  • pnpm exec vitest run packages/app-shell/357 files, 3420 passed, 1 skipped
  • pnpm --filter @object-ui/app-shell type-check (tsc --noEmitandtsc -p tsconfig.test.json) — exit 0
  • pnpm exec eslint on the three changed files — 0 errors (26 pre-existing set-state-in-effect warnings in untouched code)
  • node scripts/check-changeset-presence.mjs, check-changeset-no-major.mjs, check-control-bytes.mjs — all green

Copy census

Swept for other accept-invitation/ assembly and BASE_URL-based origin concatenation in console code. No further in-family defects; two sites listed for the record, neither touched here:

  • packages/app-shell/src/console/ai/AiChatPage.tsx:986 (publicShareBase) — a third hand-rolled mount resolution, but a correct one: it reads the document's base-href tag and yields ${origin}/_console/s. Duplicated mechanism, no user-visible defect; a candidate to fold into resolveConsoleUrl separately.
  • apps/console/src/utils/consoleBase.ts:42 — reads import.meta.env.BASE_URL deliberately and never concatenates an origin; documented for all three mount shapes and covered by its own tests. Not in family.

Generated by Claude Code

…lper instead of rebuilding BASE_URL (#4472)
Both copy sites carried their own `buildAcceptUrl`, glueing
`${window.location.origin}` to `import.meta.env.BASE_URL`. In the portable
build the console ships (`base: './'`) BASE_URL is the literal '.', so the
copied link was `http://localhost:8080./accept-invitation/<id>` — a
trailing-dot host — and with the dot removed it still skipped the
`/_console/` mount, which the server answers with {"error":"Not found"}.
Both local copies are deleted; the two sites call `resolveConsoleUrl`, the
mount-aware helper WorkspaceSwitcher and OrganizationsPage already use. The
invite dialog resolves once so its displayed field and its clipboard cannot
drift apart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 12, 2026 7:00pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-D2k8nICs.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)489.32KB108.45KB
core (index.js)2.99KB1.14KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)153.42KB41.19KB
fields (index.js)228.99KB56.82KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.33KB1.20KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)45.24KB12.46KB
plugin-charts (index.js)62.01KB17.63KB
plugin-chatbot (index.js)181.17KB43.03KB
plugin-dashboard (index.js)120.75KB31.38KB
plugin-designer (index.js)212.58KB42.83KB
plugin-detail (index.js)239.03KB59.77KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)188.13KB50.00KB
plugin-kanban (index.js)48.62KB13.42KB
plugin-list (index.js)110.20KB26.79KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.99KB10.74KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.08KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.73KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — step-7 复核 by PM session session_017Qqyix2QcnpUC9XeYVDzx3 (focused review).

  • One-resolver landing exactly as ruled: both local builders deleted, resolveConsoleUrl serves both sites unchanged, resolveHomeUrl.ts untouched. Premise independently re-verified down to the serve-time base-href injection.
  • Red-first is the honest kind: the mount half red cleanly; the trailing-dot half was found to be unreachable in vitest (vi.stubEnv cannot move the Vite-inlined (import.meta as any).env?.BASE_URL spelling) — reproduced byte-for-byte via a temporary spelling switch instead, and the shipped pin states in-file which assertion can and cannot go red on its own. No pin claimed that cannot red. The stub-trap mechanism is recorded seat knowledge now — a test written against that spelling reads as coverage and is permanently green.
  • Copy census accepted: the two defects fixed; AiChatPage's publicShareBase (a third hand-rolled mount resolution, measured CORRECT today) is being filed as an observation card by the PM — duplicated mechanism is drift risk under the one-resolver doctrine even when currently right. consoleBase.ts correctly ruled not-in-family.
  • .d.ts diff EMPTY by build-and-diff, patch stands. Whole-package suite green (357 files), CI 18/18, bot comment correctly left alone.

Flipping ready + arming auto-merge. Note for the pool: #4474 and #4475 unlock when this lands (shared manage-area surface).


Generated by Claude Code


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: copied invitation link is unusable — buildAcceptUrl rebuilds the trailing-dot host that resolveHomeUrl already retired

2 participants

@yinlianghui@claude