Skip to content

Commit bbf47a6

Browse files
committed
Rebase onto main: slug in the SSR auth hint, in-shell foreign-slug 404
Main's edge gate now renders the real shell from the hint, so the hint must carry the org slug (minted lazily like everywhere else) or the first paint has no slug to canonicalize onto. The foreign-slug state renders inside the shell chrome via the shared shell's content slot, matching main's in-shell route 404; connect-panel asserts the slug-form MCP command.
1 parent a95119c commit bbf47a6

7 files changed

Lines changed: 45 additions & 16 deletions

File tree

apps/cloud/src/auth/ssr-gate.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,26 +127,38 @@ const resolveAuthHint = async (
127127
avatarUrl: session.avatarUrl,
128128
},
129129
organization: session.organizationId
130-
? { id: session.organizationId, name: await organizationName(session.organizationId) }
130+
? {
131+
id: session.organizationId,
132+
...(await organizationDisplay(session.organizationId)),
133+
}
131134
: null,
132135
},
133136
mint: true,
134137
};
135138
};
136139

137-
// The sealed session carries the org ID but not its name; the local mirror
138-
// has it. Only consulted when minting (absent/mismatched hint) — never on the
139-
// steady-state path — and over per-request layers, because a connection cached
140-
// in the shared runtime would be reused across requests, which Cloudflare
141-
// forbids. A miss or failure reads as "" — display-only, corrected by the
140+
// The sealed session carries the org ID but not its name/slug; the local
141+
// mirror has both (the slug minted lazily for orgs that predate slugs). Only
142+
// consulted when minting (absent/mismatched hint) — never on the steady-state
143+
// path — and over per-request layers, because a connection cached in the
144+
// shared runtime would be reused across requests, which Cloudflare forbids. A
145+
// miss or failure reads as empty strings — display-only, corrected by the
142146
// client's /account/me write.
143-
const organizationName = async (organizationId: string): Promise<string> => {
147+
const organizationDisplay = async (
148+
organizationId: string,
149+
): Promise<{ name: string; slug: string }> => {
144150
const exit = await getRuntime().runPromiseExit(
145151
Effect.flatMap(UserStoreService.asEffect(), (users) =>
146-
users.use((store) => store.getOrganization(organizationId)),
152+
users.use(async (store) => {
153+
const org = await store.getOrganization(organizationId);
154+
if (!org) return null;
155+
return store.ensureOrganizationSlug(org);
156+
}),
147157
).pipe(Effect.provide(Layer.provide(makeUserStoreLayer(), makeDbLayer()))),
148158
);
149-
return Exit.isSuccess(exit) ? (exit.value?.name ?? "") : "";
159+
return Exit.isSuccess(exit)
160+
? { name: exit.value?.name ?? "", slug: exit.value?.slug ?? "" }
161+
: { name: "", slug: "" };
150162
};
151163

152164
const hintSetCookie = (hint: AuthHint) =>

apps/cloud/src/routes/__root.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,15 @@ function AuthGate() {
260260
>
261261
<OrgSlugGate
262262
activeSlug={activeSlug}
263-
foreignSlug={(slug) => <ForeignOrgSlug slug={slug} />}
263+
// Framed by the real shell: a foreign slug resolves (or
264+
// 404s) inside the app chrome, exactly like the route-level
265+
// not-found — never a bare full-page state.
266+
foreignSlug={(slug) => (
267+
<>
268+
<Shell content={<ForeignOrgSlug slug={slug} />} />
269+
<Toaster />
270+
</>
271+
)}
264272
>
265273
<Shell />
266274
<Toaster />

apps/cloud/src/web/components/foreign-org-slug.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function ForeignOrgSlug(props: { readonly slug: string }) {
5555
if (target === "none") return <OrgSlugNotFound />;
5656

5757
return (
58-
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
58+
<div className="flex min-h-full flex-1 items-center justify-center text-sm text-muted-foreground">
5959
{targetOrgId ? "Switching organization…" : ""}
6060
</div>
6161
);

apps/cloud/src/web/shell.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type React from "react";
2+
13
import { Shell as SharedShell, defaultShellNavItems } from "@executor-js/react/multiplayer/shell";
24
import { trackEvent } from "@executor-js/react/api/analytics";
35
import { AUTH_PATHS } from "../auth/api";
@@ -27,14 +29,15 @@ const signOut = async () => {
2729
window.location.href = "/";
2830
};
2931

30-
export function Shell() {
32+
export function Shell(props: { readonly content?: React.ReactNode }) {
3133
return (
3234
<SharedShell
3335
onSignOut={signOut}
3436
navItems={navItems}
3537
apiKeysTo="/api-keys"
3638
orgMenuSlot={<OrgMenuSlot />}
3739
supportSlot={<SupportSlot />}
40+
content={props.content}
3841
/>
3942
);
4043
}

e2e/cloud/connect-panel.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ scenario(
2828
});
2929
const httpCommand = await command();
3030
expect(httpCommand, "the default command adds the MCP server").toContain("npx add-mcp");
31-
expect(httpCommand, "the HTTP command is org-scoped").toMatch(/\/org_[^/]+\/mcp/);
31+
// Org-scoped via the org's URL slug, not the raw org_ id.
32+
expect(httpCommand, "the HTTP command is org-scoped").toMatch(/\/[a-z0-9-]+\/mcp/);
33+
expect(httpCommand, "the slug form, not the org_ id").not.toMatch(/\/org_[^/]+\/mcp/);
3234
expect(httpCommand).toContain("--transport http");
3335

3436
await step("Switch to Standard I/O", async () => {

packages/react/src/multiplayer/org-slug-gate.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ export interface OrgSlugGateProps {
4545

4646
/**
4747
* The default not-found page for a URL naming an org this session can't see.
48-
* The home link is BARE on purpose — it canonicalizes onto the active org.
48+
* Sized to fill its container (the shell's content area, or the viewport when
49+
* rendered bare). The home link is BARE on purpose — it canonicalizes onto
50+
* the active org.
4951
*/
5052
export function OrgSlugNotFound() {
5153
return (
52-
<main className="flex min-h-screen items-center justify-center bg-background px-6 py-10">
54+
<main className="flex min-h-full flex-1 items-center justify-center bg-background px-6 py-10">
5355
<section className="w-full max-w-md text-center">
5456
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">404</p>
5557
<h1 className="mt-2 text-xl font-semibold text-foreground">Page not found</h1>

packages/react/src/multiplayer/shell.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export interface ShellProps {
7777
readonly orgMenuSlot?: ReactNode;
7878
/** Injected support button above the account footer (cloud). */
7979
readonly supportSlot?: ReactNode;
80+
/** Replaces the routed `<Outlet />` (the org-slug gate's in-shell 404). */
81+
readonly content?: ReactNode;
8082
}
8183

8284
// ── Brand ────────────────────────────────────────────────────────────────
@@ -417,7 +419,7 @@ export function Shell(props: ShellProps) {
417419
<div className="w-8 shrink-0" />
418420
</div>
419421

420-
<Outlet />
422+
{props.content ?? <Outlet />}
421423
</main>
422424
</div>
423425
);

0 commit comments

Comments
 (0)