Skip to content

Commit 4153df4

Browse files
authored
merge @executor/ui + @executor/react into packages/react (RhysSullivan#88)
- Merge UI components, hooks, lib, styles from packages/ui - Merge API client, atoms, provider from packages/clients/react - Move shared pages and components from apps/local/src/web - Direct subpath exports: @executor/react/{api,plugins,pages,components,hooks,lib}/* - No barrel files, no backwards compat shims - Each app owns its own shell/router, imports pages from package - Delete packages/ui and packages/clients/react
2 parents c3f0b23 + 7cb48ec commit 4153df4

133 files changed

Lines changed: 877 additions & 498 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/config.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
"@executor/plugin-keychain",
2727
"@executor/plugin-mcp",
2828
"@executor/plugin-onepassword",
29-
"@executor/plugin-openapi",
30-
"@executor/ui",
31-
"@executor/marketing"
29+
"@executor/plugin-openapi"
3230
]
3331
}

apps/cloud/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"@executor/react": "workspace:*",
1616
"@executor/sdk": "workspace:*",
1717
"@executor/storage-postgres": "workspace:*",
18-
"@executor/ui": "workspace:*",
1918
"@tanstack/react-router": "^1.168.10",
2019
"@workos-inc/node": "^7.0.0",
2120
"drizzle-orm": "catalog:",

apps/cloud/src/api.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// ---------------------------------------------------------------------------
2-
// Cloud API — core handlers from @executor/api + cloud-specific plugins
2+
// Cloud API — core handlers from @executor/api + cloud-specific plugins + auth
33
// ---------------------------------------------------------------------------
44

55
import {
@@ -20,16 +20,20 @@ import { GraphqlGroup, GraphqlExtensionService, GraphqlHandlers } from "@executo
2020

2121
import { createTeamExecutor } from "./services/executor";
2222
import { authenticateRequest } from "./auth/workos";
23+
import { CloudAuthApi } from "./auth/api";
24+
import { CloudAuthHandlers } from "./auth/handlers";
25+
import { AuthContext, UserStoreService } from "./auth/context";
2326
import type { DrizzleDb } from "./services/db";
2427

2528
// ---------------------------------------------------------------------------
26-
// Cloud API — core + cloud plugins (no onepassword)
29+
// Cloud API — core + cloud plugins + cloud auth (no onepassword)
2730
// ---------------------------------------------------------------------------
2831

2932
const CloudApi = addGroup(OpenApiGroup)
3033
.add(McpGroup)
3134
.add(GoogleDiscoveryGroup)
32-
.add(GraphqlGroup);
35+
.add(GraphqlGroup)
36+
.add(CloudAuthApi);
3337

3438
const CloudApiBase = HttpApiBuilder.api(CloudApi).pipe(
3539
Layer.provide(CoreHandlers),
@@ -38,6 +42,7 @@ const CloudApiBase = HttpApiBuilder.api(CloudApi).pipe(
3842
McpHandlers,
3943
GoogleDiscoveryHandlers,
4044
GraphqlHandlers,
45+
CloudAuthHandlers,
4146
)),
4247
);
4348

@@ -96,13 +101,29 @@ export const createCloudApiHandler = (db: DrizzleDb, encryptionKey: string) => {
96101
Layer.provideMerge(pluginExtensions),
97102
Layer.provideMerge(Layer.succeed(ExecutorService, executor)),
98103
Layer.provideMerge(Layer.succeed(ExecutionEngineService, engine)),
104+
Layer.provideMerge(Layer.succeed(AuthContext, {
105+
userId: auth.userId,
106+
email: auth.email,
107+
teamId,
108+
name: `${auth.firstName ?? ""} ${auth.lastName ?? ""}`.trim() || null,
109+
avatarUrl: auth.avatarUrl,
110+
})),
111+
Layer.provideMerge(Layer.succeed(UserStoreService, userStore)),
99112
Layer.provideMerge(HttpServer.layerContext),
100113
),
101114
{ middleware: HttpMiddleware.logger },
102115
);
103116

104117
try {
105-
return await handler.handler(request);
118+
const response = await handler.handler(request);
119+
120+
if (auth.refreshedCookie) {
121+
const newResponse = new Response(response.body, response);
122+
newResponse.headers.append("Set-Cookie", auth.refreshedCookie);
123+
return newResponse;
124+
}
125+
126+
return response;
106127
} finally {
107128
await Effect.runPromise(executor.close()).catch(() => undefined);
108129
handler.dispose();

apps/cloud/src/auth/api.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
2+
import { Schema } from "effect";
3+
4+
const AuthUser = Schema.Struct({
5+
id: Schema.String,
6+
email: Schema.String,
7+
name: Schema.NullOr(Schema.String),
8+
avatarUrl: Schema.NullOr(Schema.String),
9+
});
10+
11+
const AuthTeam = Schema.Struct({
12+
id: Schema.String,
13+
name: Schema.String,
14+
});
15+
16+
const AuthMeResponse = Schema.Struct({
17+
user: AuthUser,
18+
team: Schema.NullOr(AuthTeam),
19+
});
20+
21+
export class CloudAuthApi extends HttpApiGroup.make("cloudAuth")
22+
.add(
23+
HttpApiEndpoint.get("me")`/auth/me`
24+
.addSuccess(AuthMeResponse),
25+
) {}

apps/cloud/src/auth/context.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { Context } from "effect";
2+
import type { makeUserStore } from "@executor/storage-postgres";
23

34
export class AuthContext extends Context.Tag("@executor/cloud/AuthContext")<
45
AuthContext,
56
{
67
readonly userId: string;
78
readonly teamId: string;
89
readonly email: string;
10+
readonly name: string | null;
11+
readonly avatarUrl: string | null;
912
}
1013
>() {}
14+
15+
export class UserStoreService extends Context.Tag("@executor/cloud/UserStoreService")<
16+
UserStoreService,
17+
ReturnType<typeof makeUserStore>
18+
>() {}

apps/cloud/src/auth/handlers.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { HttpApiBuilder } from "@effect/platform";
2+
import { Effect } from "effect";
3+
4+
import { addGroup } from "@executor/api";
5+
import { CloudAuthApi } from "./api";
6+
import { AuthContext, UserStoreService } from "./context";
7+
8+
const ApiWithCloudAuth = addGroup(CloudAuthApi);
9+
10+
export const CloudAuthHandlers = HttpApiBuilder.group(
11+
ApiWithCloudAuth,
12+
"cloudAuth",
13+
(handlers) =>
14+
handlers.handle("me", () =>
15+
Effect.gen(function* () {
16+
const auth = yield* AuthContext;
17+
const userStore = yield* UserStoreService;
18+
19+
const team = yield* Effect.tryPromise(() =>
20+
userStore.getTeam(auth.teamId),
21+
).pipe(Effect.orDie);
22+
23+
return {
24+
user: {
25+
id: auth.userId,
26+
email: auth.email,
27+
name: auth.name,
28+
avatarUrl: auth.avatarUrl,
29+
},
30+
team: team ? { id: team.id, name: team.name } : null,
31+
};
32+
}),
33+
),
34+
);

apps/cloud/src/auth/workos.ts

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export const authenticateWithCode = async (code: string) => {
4848
* Authenticate a request using the sealed session cookie.
4949
* Returns user info or null if not authenticated.
5050
*/
51+
/**
52+
* Authenticate a request using the sealed session cookie.
53+
* Automatically refreshes expired tokens and returns a new cookie if needed.
54+
*/
5155
export const authenticateRequest = async (request: Request) => {
5256
const cookieHeader = request.headers.get("cookie");
5357
const sessionData = parseCookie(cookieHeader, COOKIE_NAME);
@@ -60,39 +64,38 @@ export const authenticateRequest = async (request: Request) => {
6064
});
6165

6266
const result = await session.authenticate();
63-
if (!result.authenticated) return null;
64-
65-
return {
66-
userId: result.user.id,
67-
email: result.user.email,
68-
firstName: result.user.firstName,
69-
lastName: result.user.lastName,
70-
avatarUrl: result.user.profilePictureUrl,
71-
sessionId: result.sessionId,
72-
};
73-
};
74-
75-
/**
76-
* Refresh the sealed session cookie. Returns new cookie value or null.
77-
*/
78-
export const refreshSession = async (request: Request) => {
79-
const cookieHeader = request.headers.get("cookie");
80-
const sessionData = parseCookie(cookieHeader, COOKIE_NAME);
81-
if (!sessionData) return null;
8267

83-
const wos = getWorkOS();
84-
const session = wos.userManagement.loadSealedSession({
85-
sessionData,
86-
cookiePassword: getCookiePassword(),
87-
});
88-
89-
const result = await session.refresh();
90-
if (!result.authenticated || !result.sealedSession) return null;
68+
if (result.authenticated) {
69+
return {
70+
userId: result.user.id,
71+
email: result.user.email,
72+
firstName: result.user.firstName,
73+
lastName: result.user.lastName,
74+
avatarUrl: result.user.profilePictureUrl,
75+
sessionId: result.sessionId,
76+
refreshedCookie: undefined as string | undefined,
77+
};
78+
}
9179

92-
return {
93-
sealedSession: result.sealedSession,
94-
cookie: makeSessionCookie(result.sealedSession),
95-
};
80+
// Token expired — try refreshing
81+
if (result.reason === "no_session_cookie_provided") return null;
82+
83+
try {
84+
const refreshed = await session.refresh();
85+
if (!refreshed.authenticated || !refreshed.sealedSession) return null;
86+
87+
return {
88+
userId: refreshed.user.id,
89+
email: refreshed.user.email,
90+
firstName: refreshed.user.firstName,
91+
lastName: refreshed.user.lastName,
92+
avatarUrl: refreshed.user.profilePictureUrl,
93+
sessionId: refreshed.sessionId,
94+
refreshedCookie: makeSessionCookie(refreshed.sealedSession),
95+
};
96+
} catch {
97+
return null;
98+
}
9699
};
97100

98101
/**

apps/cloud/src/web/auth.tsx

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import React, { createContext, useContext, useEffect, useState } from "react";
1+
import React, { createContext, useContext } from "react";
2+
import { useAtomValue, Result } from "@effect-atom/atom-react";
3+
4+
import { CloudApiClient } from "./client";
5+
6+
// ---------------------------------------------------------------------------
7+
// Types (from CloudAuthApi response schema)
8+
// ---------------------------------------------------------------------------
29

310
type AuthUser = {
411
id: string;
@@ -12,40 +19,39 @@ type AuthTeam = {
1219
name: string;
1320
};
1421

22+
// ---------------------------------------------------------------------------
23+
// Auth atom — typed query against CloudAuthApi
24+
// ---------------------------------------------------------------------------
25+
26+
export const authAtom = CloudApiClient.query("cloudAuth", "me", {
27+
timeToLive: "5 minutes",
28+
});
29+
30+
// ---------------------------------------------------------------------------
31+
// Provider + hook
32+
// ---------------------------------------------------------------------------
33+
1534
type AuthState =
1635
| { status: "loading" }
1736
| { status: "unauthenticated" }
18-
| { status: "authenticated"; user: AuthUser; team: AuthTeam };
37+
| { status: "authenticated"; user: AuthUser; team: AuthTeam | null };
1938

2039
const AuthContext = createContext<AuthState>({ status: "loading" });
2140

2241
export const useAuth = () => useContext(AuthContext);
2342

2443
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
25-
const [state, setState] = useState<AuthState>({ status: "loading" });
26-
27-
useEffect(() => {
28-
fetch("/auth/me")
29-
.then((res) => {
30-
if (!res.ok) {
31-
setState({ status: "unauthenticated" });
32-
return;
33-
}
34-
return res.json();
35-
})
36-
.then((data) => {
37-
if (data?.user) {
38-
setState({
39-
status: "authenticated",
40-
user: data.user,
41-
team: data.team,
42-
});
43-
}
44-
})
45-
.catch(() => {
46-
setState({ status: "unauthenticated" });
47-
});
48-
}, []);
44+
const result = useAtomValue(authAtom);
45+
46+
const state: AuthState = Result.match(result, {
47+
onInitial: () => ({ status: "loading" as const }),
48+
onSuccess: ({ value }) => ({
49+
status: "authenticated" as const,
50+
user: value.user,
51+
team: value.team,
52+
}),
53+
onFailure: () => ({ status: "unauthenticated" as const }),
54+
});
4955

5056
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
5157
};

apps/cloud/src/web/client.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { AtomHttpApi } from "@effect-atom/atom-react";
2+
import { FetchHttpClient } from "@effect/platform";
3+
import { addGroup } from "@executor/api";
4+
import { getBaseUrl } from "@executor/react/api/base-url";
5+
import { CloudAuthApi } from "../auth/api";
6+
7+
// ---------------------------------------------------------------------------
8+
// Cloud API client — core API + cloud auth
9+
// ---------------------------------------------------------------------------
10+
11+
const CloudApi = addGroup(CloudAuthApi);
12+
13+
class CloudApiClient extends AtomHttpApi.Tag<CloudApiClient>()(
14+
"CloudApiClient",
15+
{
16+
api: CloudApi,
17+
httpClient: FetchHttpClient.layer,
18+
baseUrl: getBaseUrl(),
19+
},
20+
) {}
21+
22+
export { CloudApiClient };

apps/cloud/src/web/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22
import { createRoot } from "react-dom/client";
3-
import "@executor/ui/globals.css";
3+
import "@executor/react/globals.css";
44
import { App } from "./App";
55

66
const root = createRoot(document.getElementById("root")!);

0 commit comments

Comments
 (0)