History

refactor API routing and fix cloud auth sign-in flow

Rhys Sullivan committed 7 months ago

Commit 236b76e1 parent ffcfabb36 files · +471 −409Browse files

Unified · Split

36 files
  1. apps/cloud/src/api.ts +171 −86
  2. apps/cloud/src/auth/api.ts +19 −8
  3. apps/cloud/src/auth/context.ts +9 −22
  4. apps/cloud/src/auth/errors.ts +11 −0
  5. apps/cloud/src/auth/handlers.ts +61 −45
  6. apps/cloud/src/auth/workos.ts +3 −22
  7. apps/cloud/src/routeTree.gen.ts +17 −38
  8. apps/cloud/src/routes/api.$.ts +18 −0
  9. apps/cloud/src/routes/v1.$.ts +0 −14
  10. apps/cloud/src/server/api-handler.ts +8 −8
  11. apps/cloud/src/services/db.ts +36 −14
  12. apps/cloud/src/services/executor.ts +2 −2
  13. apps/cloud/src/services/schema.ts +40 −0
  14. apps/cloud/src/services/user-store.ts +1 −1
  15. apps/cloud/src/web/auth.tsx +14 −2
  16. apps/cloud/src/web/pages/login.tsx +2 −1
  17. apps/cloud/src/web/shell.tsx +2 −1
  18. apps/local/src/routeTree.gen.ts +21 −21
  19. apps/local/src/routes/api.$.ts +18 −0
  20. apps/local/src/routes/v1.$.ts +0 −14
  21. apps/local/src/server/api-handler.ts +5 −1
  22. apps/local/src/server/main.ts +1 −1
  23. packages/core/api/src/executions/api.ts +1 −1
  24. packages/core/api/src/scope/api.ts +1 −1
  25. packages/core/api/src/secrets/api.ts +1 −1
  26. packages/core/api/src/sources/api.ts +1 −1
  27. packages/core/api/src/tools/api.ts +1 −1
  28. packages/core/storage-postgres/src/index.test.ts +0 −55
  29. packages/core/storage-postgres/src/index.ts +0 −2
  30. packages/core/storage-postgres/src/schema.ts +0 −39
  31. packages/plugins/google-discovery/src/api/group.ts +1 −1
  32. packages/plugins/graphql/src/api/group.ts +1 −1
  33. packages/plugins/mcp/src/api/group.ts +1 −1
  34. packages/plugins/onepassword/src/api/group.ts +1 −1
  35. packages/plugins/openapi/src/api/group.ts +1 −1
  36. packages/react/src/api/base-url.tsx +2 −2
apps/cloud/src/api.ts+171−86
@@ -1,40 +1,45 @@
1// ---------------------------------------------------------------------------1// ---------------------------------------------------------------------------
2// Cloud API — core handlers from @executor/api + cloud-specific plugins + auth2// Cloud API — protected core API + public auth endpoints
3// ---------------------------------------------------------------------------3// ---------------------------------------------------------------------------
4 4
5import {5import {
6 HttpApi,
6 HttpApiBuilder,7 HttpApiBuilder,
7 HttpApiSwagger,8 HttpApiSwagger,
8 HttpMiddleware,9 HttpMiddleware,
9 HttpServer,10 HttpServer,
10} from "@effect/platform";11} from "@effect/platform";
11import { Effect, Layer } from "effect";12import { Effect, Layer } from "effect";
13import { setCookie } from "@tanstack/react-start/server";
12 14
13import { addGroup, CoreHandlers, ExecutorService, ExecutionEngineService } from "@executor/api";15import { addGroup, CoreHandlers, ExecutorService, ExecutionEngineService } from "@executor/api";
14import { createExecutionEngine } from "@executor/execution";16import { createExecutionEngine } from "@executor/execution";
15import { OpenApiGroup, OpenApiExtensionService, OpenApiHandlers } from "@executor/plugin-openapi/api";17import { OpenApiGroup, OpenApiExtensionService, OpenApiHandlers } from "@executor/plugin-openapi/api";
16import { McpGroup, McpExtensionService, McpHandlers } from "@executor/plugin-mcp/api";18import { McpGroup, McpExtensionService, McpHandlers } from "@executor/plugin-mcp/api";
17import { GoogleDiscoveryGroup, GoogleDiscoveryExtensionService, GoogleDiscoveryHandlers } from "@executor/plugin-google-discovery/api";19import {
20 GoogleDiscoveryGroup,
21 GoogleDiscoveryExtensionService,
22 GoogleDiscoveryHandlers,
23} from "@executor/plugin-google-discovery/api";
18import { GraphqlGroup, GraphqlExtensionService, GraphqlHandlers } from "@executor/plugin-graphql/api";24import { GraphqlGroup, GraphqlExtensionService, GraphqlHandlers } from "@executor/plugin-graphql/api";
19 25
20import { createTeamExecutor } from "./services/executor";26import { CloudAuthApi, CloudAuthPublicApi } from "./auth/api";
21import { CloudAuthApi } from "./auth/api";
22import { CloudAuthHandlers } from "./auth/handlers";
23import { AuthContext, UserStoreService } from "./auth/context";27import { AuthContext, UserStoreService } from "./auth/context";
28import { CloudAuthHandlers, CloudAuthPublicHandlers } from "./auth/handlers";
24import { WorkOSAuth } from "./auth/workos";29import { WorkOSAuth } from "./auth/workos";
25import type { DrizzleDb } from "./services/db";30import { DbService } from "./services/db";
26 31import { createTeamExecutor } from "./services/executor";
27// ---------------------------------------------------------------------------
28// Cloud API — core + cloud plugins + cloud auth (no onepassword)
29// ---------------------------------------------------------------------------
30 32
31const CloudApi = addGroup(OpenApiGroup)33const ProtectedCloudApi = addGroup(OpenApiGroup)
32 .add(McpGroup)34 .add(McpGroup)
33 .add(GoogleDiscoveryGroup)35 .add(GoogleDiscoveryGroup)
34 .add(GraphqlGroup)36 .add(GraphqlGroup)
35 .add(CloudAuthApi);37 .add(CloudAuthApi);
36 38
37const CloudApiBase = HttpApiBuilder.api(CloudApi).pipe(39const PublicCloudApi = HttpApi.make("cloudPublic")
40 .add(CloudAuthPublicApi);
41
42const ProtectedCloudApiLive = HttpApiBuilder.api(ProtectedCloudApi).pipe(
38 Layer.provide(CoreHandlers),43 Layer.provide(CoreHandlers),
39 Layer.provide(Layer.mergeAll(44 Layer.provide(Layer.mergeAll(
40 OpenApiHandlers,45 OpenApiHandlers,
@@ -45,100 +50,180 @@ const CloudApiBase = HttpApiBuilder.api(CloudApi).pipe( · 4 unmodified lines
45 )),50 )),
46);51);
47 52
48// ---------------------------------------------------------------------------53const PublicCloudApiLive = HttpApiBuilder.api(PublicCloudApi).pipe(
49// Cookie parser54 Layer.provide(CloudAuthPublicHandlers),
50// ---------------------------------------------------------------------------55);
56
57const DbLive = DbService.Live;
58const UserStoreLive = UserStoreService.Live.pipe(
59 Layer.provide(DbLive),
60);
61
62const SharedServices = Layer.mergeAll(
63 DbLive,
64 UserStoreLive,
65 WorkOSAuth.Default,
66 HttpServer.layerContext,
67);
68
69const publicApiHandler = HttpApiBuilder.toWebHandler(
70 PublicCloudApiLive.pipe(
71 Layer.provideMerge(SharedServices),
72 ),
73 { middleware: HttpMiddleware.logger },
74);
51 75
52const parseCookie = (cookieHeader: string | null, name: string): string | null => {76const parseCookie = (cookieHeader: string | null, name: string): string | null => {
53 if (!cookieHeader) return null;77 if (!cookieHeader) return null;
54 const match = cookieHeader78 const match = cookieHeader
55 .split(";")79 .split(";")
56 .map((c) => c.trim())80 .map((value) => value.trim())
57 .find((c) => c.startsWith(`${name}=`));81 .find((value) => value.startsWith(`${name}=`));
58 if (!match) return null;82 if (!match) return null;
59 return match.slice(name.length + 1) || null;83 return match.slice(name.length + 1) || null;
60};84};
61 85
62// ---------------------------------------------------------------------------86const isPublicPath = (pathname: string): boolean =>
63// Create API handler with auth-based executor resolution87 pathname === "/auth/login" || pathname === "/auth/callback";
64// ---------------------------------------------------------------------------
65 88
66export const createCloudApiHandler = (db: DrizzleDb, encryptionKey: string) => {89const unauthorized = (message: string): Response =>
67 return async (request: Request): Promise<Response> => {90 Response.json({ error: message }, { status: 401 });
68 // Authenticate via WorkOS sealed session
69 const auth = await Effect.runPromise(
70 Effect.gen(function* () {
71 const workos = yield* WorkOSAuth;
72 return yield* workos.authenticateRequest(request);
73 }).pipe(Effect.provide(WorkOSAuth.Default)),
74 );
75 91
76 if (!auth) {92const COOKIE_OPTIONS = {
77 return Response.json({ error: "Unauthorized" }, { status: 401 });93 path: "/",
78 }94 httpOnly: true,
95 sameSite: "lax" as const,
96 maxAge: 60 * 60 * 24 * 7,
97 secure: process.env.NODE_ENV === "production",
98};
79 99
80 const teamId = parseCookie(request.headers.get("cookie"), "executor_team");100const resolveAuth = (request: Request) =>
81 if (!teamId) {101 Effect.gen(function* () {
82 return Response.json({ error: "No team selected" }, { status: 401 });102 const workos = yield* WorkOSAuth;
83 }103 return yield* workos.authenticateRequest(request);
104 }).pipe(
105 Effect.provide(SharedServices),
106 Effect.runPromise,
107 );
108
109const resolveTeamId = (
110 auth: {
111 readonly userId: string;
112 readonly email: string;
113 readonly firstName: string | null | undefined;
114 readonly lastName: string | null | undefined;
115 readonly avatarUrl: string | null | undefined;
116 },
117 cookieTeamId: string | null,
118) =>
119 Effect.gen(function* () {
120 if (cookieTeamId) return cookieTeamId;
121
122 const users = yield* UserStoreService;
123 const teams = yield* users.use((store) => store.getTeamsForUser(auth.userId));
124 if (teams.length > 0) return teams[0]!.teamId;
125
126 const user = yield* users.use((store) =>
127 store.upsertUser({
128 id: auth.userId,
129 email: auth.email,
130 name: `${auth.firstName ?? ""} ${auth.lastName ?? ""}`.trim() || undefined,
131 avatarUrl: auth.avatarUrl ?? undefined,
132 }),
133 );
84 134
85 // Resolve team name135 const team = yield* users.use((store) =>
86 const userStore = UserStoreService.layer(db);136 store.createTeam(`${user.name ?? user.email}'s Team`),
87 const team = await Effect.runPromise(
88 Effect.gen(function* () {
89 const users = yield* UserStoreService;
90 return yield* users.use((s) => s.getTeam(teamId));
91 }).pipe(Effect.provide(userStore)),
92 );137 );
138 yield* users.use((store) => store.addMember(team.id, user.id, "owner"));
139 return team.id;
140 }).pipe(
141 Effect.provide(SharedServices),
142 Effect.runPromise,
143 );
144
145const resolveExecutor = (teamId: string) =>
146 Effect.gen(function* () {
147 const users = yield* UserStoreService;
148 const team = yield* users.use((store) => store.getTeam(teamId));
93 const teamName = team?.name ?? "Unknown Team";149 const teamName = team?.name ?? "Unknown Team";
150 const encryptionKey = process.env.ENCRYPTION_KEY ?? "local-dev-encryption-key";
151 return yield* createTeamExecutor(teamId, teamName, encryptionKey);
152 }).pipe(
153 Effect.provide(SharedServices),
154 Effect.runPromise,
155 );
156
157type TeamExecutor = Awaited<ReturnType<typeof resolveExecutor>>;
158
159const createProtectedHandler = (
160 auth: {
161 readonly userId: string;
162 readonly email: string;
163 readonly firstName: string | null | undefined;
164 readonly lastName: string | null | undefined;
165 readonly avatarUrl: string | null | undefined;
166 },
167 teamId: string,
168 executor: TeamExecutor,
169) => {
170 const engine = createExecutionEngine({ executor });
171
172 const requestServices = Layer.mergeAll(
173 Layer.succeed(AuthContext, {
174 userId: auth.userId,
175 teamId,
176 email: auth.email,
177 name: `${auth.firstName ?? ""} ${auth.lastName ?? ""}`.trim() || null,
178 avatarUrl: auth.avatarUrl ?? null,
179 }),
180 Layer.succeed(ExecutorService, executor),
181 Layer.succeed(ExecutionEngineService, engine),
182 Layer.succeed(OpenApiExtensionService, executor.openapi),
183 Layer.succeed(McpExtensionService, executor.mcp),
184 Layer.succeed(GoogleDiscoveryExtensionService, executor.googleDiscovery),
185 Layer.succeed(GraphqlExtensionService, executor.graphql),
186 );
187
188 return HttpApiBuilder.toWebHandler(
189 HttpApiSwagger.layer({ path: "/docs" }).pipe(
190 Layer.provideMerge(HttpApiBuilder.middlewareOpenApi()),
191 Layer.provideMerge(ProtectedCloudApiLive),
192 Layer.provideMerge(requestServices),
193 Layer.provideMerge(SharedServices),
194 ),
195 { middleware: HttpMiddleware.logger },
196 );
197};
94 198
95 const executor = await Effect.runPromise(199export const handleApiRequest = async (request: Request): Promise<Response> => {
96 createTeamExecutor(db, teamId, teamName, encryptionKey),200 const pathname = new URL(request.url).pathname;
97 );
98 201
99 const pluginExtensions = Layer.mergeAll(202 if (isPublicPath(pathname)) {
100 Layer.succeed(OpenApiExtensionService, executor.openapi),203 return publicApiHandler.handler(request);
101 Layer.succeed(McpExtensionService, executor.mcp),204 }
102 Layer.succeed(GoogleDiscoveryExtensionService, executor.googleDiscovery),
103 Layer.succeed(GraphqlExtensionService, executor.graphql),
104 );
105 205
106 const engine = createExecutionEngine({ executor });206 const auth = await resolveAuth(request);
107 207 if (!auth) return unauthorized("Unauthorized");
108 const handler = HttpApiBuilder.toWebHandler(208
109 HttpApiSwagger.layer().pipe(209 const cookieTeamId = parseCookie(request.headers.get("cookie"), "executor_team");
110 Layer.provideMerge(HttpApiBuilder.middlewareOpenApi()),210 const teamId = await resolveTeamId(auth, cookieTeamId);
111 Layer.provideMerge(CloudApiBase),
112 Layer.provideMerge(pluginExtensions),
113 Layer.provideMerge(Layer.succeed(ExecutorService, executor)),
114 Layer.provideMerge(Layer.succeed(ExecutionEngineService, engine)),
115 Layer.provideMerge(Layer.succeed(AuthContext, {
116 userId: auth.userId,
117 email: auth.email,
118 teamId,
119 name: `${auth.firstName ?? ""} ${auth.lastName ?? ""}`.trim() || null,
120 avatarUrl: auth.avatarUrl,
121 })),
122 Layer.provideMerge(UserStoreService.layer(db)),
123 Layer.provideMerge(WorkOSAuth.Default),
124 Layer.provideMerge(HttpServer.layerContext),
125 ),
126 { middleware: HttpMiddleware.logger },
127 );
128 211
129 try {212 const executor = await resolveExecutor(teamId);
130 const response = await handler.handler(request);213 const handler = createProtectedHandler(auth, teamId, executor);
131 214
132 if (auth.refreshedCookie) {215 try {
133 const newResponse = new Response(response.body, response);216 const response = await handler.handler(request);
134 newResponse.headers.append("Set-Cookie", auth.refreshedCookie);
135 return newResponse;
136 }
137 217
138 return response;218 if (auth.refreshedSession) {
139 } finally {219 setCookie("wos-session", auth.refreshedSession, COOKIE_OPTIONS);
140 await Effect.runPromise(executor.close()).catch(() => undefined);220 }
141 handler.dispose();221 if (!cookieTeamId) {
222 setCookie("executor_team", teamId, COOKIE_OPTIONS);
142 }223 }
143 };224 return response;
225 } finally {
226 await Effect.runPromise(executor.close()).catch(() => undefined);
227 await handler.dispose().catch(() => undefined);
228 }
144};229};
apps/cloud/src/auth/api.ts+19−8
@@ -1,6 +1,6 @@
1import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform";1import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
2import { Schema } from "effect";2import { Schema } from "effect";
3import { UserStoreError, WorkOSError } from "./context";3import { UserStoreError, WorkOSError } from "./errors";
4 4
5const AuthUser = Schema.Struct({5const AuthUser = Schema.Struct({
6 id: Schema.String,6 id: Schema.String,
@@ -23,12 +23,14 @@ const AuthCallbackSearch = Schema.Struct({ · 16 unmodified lines
23 code: Schema.String,23 code: Schema.String,
24});24});
25 25
26export class CloudAuthApi extends HttpApiGroup.make("cloudAuth")26export const AUTH_PATHS = {
27 .add(27 login: "/api/auth/login",
28 HttpApiEndpoint.get("me")`/auth/me`28 logout: "/api/auth/logout",
29 .addSuccess(AuthMeResponse)29 callback: "/api/auth/callback",
30 .addError(UserStoreError),30} as const;
31 )31
32/** Public auth endpoints — no authentication required */
33export class CloudAuthPublicApi extends HttpApiGroup.make("cloudAuthPublic")
32 .add(34 .add(
33 HttpApiEndpoint.get("login")`/auth/login`,35 HttpApiEndpoint.get("login")`/auth/login`,
34 )36 )
@@ -37,7 +39,16 @@ export class CloudAuthApi extends HttpApiGroup.make("cloudAuth") · 2 unmodified lines
37 .setUrlParams(AuthCallbackSearch)39 .setUrlParams(AuthCallbackSearch)
38 .addError(UserStoreError)40 .addError(UserStoreError)
39 .addError(WorkOSError),41 .addError(WorkOSError),
42 ) {}
43
44/** Protected auth endpoints — require authentication */
45export class CloudAuthApi extends HttpApiGroup.make("cloudAuth")
46 .add(
47 HttpApiEndpoint.get("me")`/auth/me`
48 .addSuccess(AuthMeResponse)
49 .addError(UserStoreError),
40 )50 )
41 .add(51 .add(
42 HttpApiEndpoint.post("logout")`/auth/logout`,52 HttpApiEndpoint.post("logout")`/auth/logout`,
43 ) {}53 )
54{}
apps/cloud/src/auth/context.ts+9−22
@@ -1,20 +1,7 @@
1import { Context, Effect, Layer, Schema } from "effect";1import { Context, Effect, Layer } from "effect";
2import { makeUserStore } from "@executor/storage-postgres";2import { makeUserStore } from "../services/user-store";
3import type { DrizzleDb } from "../services/db";3import { DbService } from "../services/db";
4 4import { UserStoreError } from "./errors";
5// ---------------------------------------------------------------------------
6// Errors
7// ---------------------------------------------------------------------------
8
9export class UserStoreError extends Schema.TaggedError<UserStoreError>()(
10 "UserStoreError",
11 { cause: Schema.Unknown },
12) {}
13
14export class WorkOSError extends Schema.TaggedError<WorkOSError>()(
15 "WorkOSError",
16 { cause: Schema.Unknown },
17) {}
18 5
19// ---------------------------------------------------------------------------6// ---------------------------------------------------------------------------
20// AuthContext — resolved per-request from sealed session7// AuthContext — resolved per-request from sealed session
@@ -37,9 +24,7 @@ export class AuthContext extends Context.Tag("@executor/cloud/AuthContext")< · 16 unmodified lines
37 24
38type RawStore = ReturnType<typeof makeUserStore>;25type RawStore = ReturnType<typeof makeUserStore>;
39 26
40const makeService = (db: DrizzleDb) => {27const makeService = (store: RawStore) => {
41 const store = makeUserStore(db);
42
43 const use = <A>(fn: (s: RawStore) => Promise<A>) =>28 const use = <A>(fn: (s: RawStore) => Promise<A>) =>
44 Effect.tryPromise({29 Effect.tryPromise({
45 try: () => fn(store),30 try: () => fn(store),
@@ -55,6 +40,8 @@ export class UserStoreService extends Context.Tag("@executor/cloud/UserStoreServ · 9 unmodified lines
55 UserStoreService,40 UserStoreService,
56 UserStoreServiceType41 UserStoreServiceType
57>() {42>() {
58 static layer = (db: DrizzleDb) =>43 static Live = Layer.effect(
59 Layer.succeed(this, makeService(db));44 this,
45 Effect.map(DbService, (db) => makeService(makeUserStore(db))),
46 );
60}47}
apps/cloud/src/auth/errors.ts+11−0
@@ -0,0 +1,11 @@
1import { Schema } from "effect";
2
3export class UserStoreError extends Schema.TaggedError<UserStoreError>()(
4 "UserStoreError",
5 { cause: Schema.Unknown },
6) {}
7
8export class WorkOSError extends Schema.TaggedError<WorkOSError>()(
9 "WorkOSError",
10 { cause: Schema.Unknown },
11) {}
apps/cloud/src/auth/handlers.ts+61−45
@@ -1,40 +1,38 @@
1import { HttpApiBuilder, HttpServerResponse } from "@effect/platform";1import { HttpApi, HttpApiBuilder, HttpServerRequest, HttpServerResponse } from "@effect/platform";
2import { Effect } from "effect";2import { Effect } from "effect";
3import { deleteCookie, setCookie } from "@tanstack/react-start/server";
3 4
4import { addGroup } from "@executor/api";5import { addGroup } from "@executor/api";
5import { CloudAuthApi } from "./api";6import { AUTH_PATHS, CloudAuthApi, CloudAuthPublicApi } from "./api";
6import { AuthContext, UserStoreService } from "./context";7import { AuthContext, UserStoreService } from "./context";
7import { WorkOSAuth, makeSessionCookie, clearSessionCookie } from "./workos";8import { WorkOSAuth } from "./workos";
8 9
9const ApiWithCloudAuth = addGroup(CloudAuthApi);10const COOKIE_OPTIONS = {
11 path: "/",
12 httpOnly: true,
13 sameSite: "lax" as const,
14 maxAge: 60 * 60 * 24 * 7,
15 secure: process.env.NODE_ENV === "production",
16};
10 17
11export const CloudAuthHandlers = HttpApiBuilder.group(18// ---------------------------------------------------------------------------
12 ApiWithCloudAuth,19// Public auth handlers (no authentication required)
13 "cloudAuth",20// ---------------------------------------------------------------------------
21
22const PublicAuthApi = HttpApi.make("cloudPublic")
23 .add(CloudAuthPublicApi);
24
25export const CloudAuthPublicHandlers = HttpApiBuilder.group(
26 PublicAuthApi,
27 "cloudAuthPublic",
14 (handlers) =>28 (handlers) =>
15 handlers29 handlers
16 .handle("me", () =>
17 Effect.gen(function* () {
18 const auth = yield* AuthContext;
19 const users = yield* UserStoreService;
20 const team = yield* users.use((s) => s.getTeam(auth.teamId));
21
22 return {
23 user: {
24 id: auth.userId,
25 email: auth.email,
26 name: auth.name,
27 avatarUrl: auth.avatarUrl,
28 },
29 team: team ? { id: team.id, name: team.name } : null,
30 };
31 }),
32 )
33 .handleRaw("login", () =>30 .handleRaw("login", () =>
34 Effect.gen(function* () {31 Effect.gen(function* () {
35 const workos = yield* WorkOSAuth;32 const workos = yield* WorkOSAuth;
36 const baseUrl = process.env.APP_URL ?? `http://localhost:${process.env.PORT ?? "3000"}`;33 const req = yield* HttpServerRequest.HttpServerRequest;
37 const url = workos.getAuthorizationUrl(`${baseUrl}/auth/callback`);34 const origin = new URL(req.url, `http://${req.headers["host"]}`).origin;
35 const url = workos.getAuthorizationUrl(`${origin}${AUTH_PATHS.callback}`);
38 return HttpServerResponse.redirect(url, { status: 302 });36 return HttpServerResponse.redirect(url, { status: 302 });
39 }),37 }),
40 )38 )
@@ -81,28 +79,46 @@ export const CloudAuthHandlers = HttpApiBuilder.group( · 40 unmodified lines
81 return HttpServerResponse.text("Failed to create session", { status: 500 });79 return HttpServerResponse.text("Failed to create session", { status: 500 });
82 }80 }
83 81
84 return HttpServerResponse.redirect("/", {82 setCookie("wos-session", sealedSession, COOKIE_OPTIONS);
85 status: 302,83 setCookie("executor_team", teamId, COOKIE_OPTIONS);
86 headers: {84 return HttpServerResponse.redirect("/", { status: 302 });
87 "Set-Cookie": [85 }),
88 makeSessionCookie(sealedSession),86 ),
89 `executor_team=${teamId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800`,87);
90 ].join(", "),88
89// ---------------------------------------------------------------------------
90// Protected auth handlers (require authentication via middleware)
91// ---------------------------------------------------------------------------
92
93const ApiWithCloudAuth = addGroup(CloudAuthApi);
94
95export const CloudAuthHandlers = HttpApiBuilder.group(
96 ApiWithCloudAuth,
97 "cloudAuth",
98 (handlers) =>
99 handlers
100 .handle("me", () =>
101 Effect.gen(function* () {
102 const auth = yield* AuthContext;
103 const users = yield* UserStoreService;
104 const team = yield* users.use((s) => s.getTeam(auth.teamId));
105
106 return {
107 user: {
108 id: auth.userId,
109 email: auth.email,
110 name: auth.name,
111 avatarUrl: auth.avatarUrl,
91 },112 },
92 });113 team: team ? { id: team.id, name: team.name } : null,
114 };
93 }),115 }),
94 )116 )
95 .handleRaw("logout", () =>117 .handleRaw("logout", () =>
96 Effect.succeed(118 Effect.sync(() => {
97 HttpServerResponse.redirect("/", {119 deleteCookie("wos-session", { path: "/" });
98 status: 302,120 deleteCookie("executor_team", { path: "/" });
99 headers: {121 return HttpServerResponse.redirect("/", { status: 302 });
100 "Set-Cookie": [122 }),
101 clearSessionCookie(),
102 "executor_team=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0",
103 ].join(", "),
104 },
105 }),
106 ),
107 ),123 ),
108);124);
apps/cloud/src/auth/workos.ts+3−22
@@ -4,7 +4,7 @@ · 3 unmodified lines
4 4
5import { Context, Effect, Layer } from "effect";5import { Context, Effect, Layer } from "effect";
6import { WorkOS } from "@workos-inc/node";6import { WorkOS } from "@workos-inc/node";
7import { WorkOSError } from "./context";7import { WorkOSError } from "./errors";
8 8
9const COOKIE_NAME = "wos-session";9const COOKIE_NAME = "wos-session";
10 10
@@ -67,7 +67,7 @@ const make = Effect.gen(function* () { · 56 unmodified lines
67 lastName: result.user.lastName,67 lastName: result.user.lastName,
68 avatarUrl: result.user.profilePictureUrl,68 avatarUrl: result.user.profilePictureUrl,
69 sessionId: result.sessionId,69 sessionId: result.sessionId,
70 refreshedCookie: undefined as string | undefined,70 refreshedSession: undefined as string | undefined,
71 };71 };
72 }72 }
73 73
@@ -87,7 +87,7 @@ const make = Effect.gen(function* () { · 13 unmodified lines
87 lastName: refreshed.user.lastName,87 lastName: refreshed.user.lastName,
88 avatarUrl: refreshed.user.profilePictureUrl,88 avatarUrl: refreshed.user.profilePictureUrl,
89 sessionId: refreshed.sessionId,89 sessionId: refreshed.sessionId,
90 refreshedCookie: makeSessionCookie(refreshed.sealedSession),90 refreshedSession: refreshed.sealedSession,
91 };91 };
92 }),92 }),
93 };93 };
@@ -104,25 +104,6 @@ export class WorkOSAuth extends Context.Tag("@executor/cloud/WorkOSAuth")< · 10 unmodified lines
104 );104 );
105}105}
106 106
107// ---------------------------------------------------------------------------
108// Cookie helpers
109// ---------------------------------------------------------------------------
110
111export const makeSessionCookie = (sealedSession: string): string => {
112 const parts = [
113 `${COOKIE_NAME}=${sealedSession}`,
114 "Path=/",
115 "HttpOnly",
116 "SameSite=Lax",
117 "Max-Age=604800",
118 ];
119 if (process.env.NODE_ENV === "production") parts.push("Secure");
120 return parts.join("; ");
121};
122
123export const clearSessionCookie = (): string =>
124 `${COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
125
126const parseCookie = (cookieHeader: string | null, name: string): string | null => {107const parseCookie = (cookieHeader: string | null, name: string): string | null => {
127 if (!cookieHeader) return null;108 if (!cookieHeader) return null;
128 const match = cookieHeader109 const match = cookieHeader
apps/cloud/src/routeTree.gen.ts+17−38
@@ -12,9 +12,8 @@ import { Route as rootRouteImport } from './routes/__root' · 11 unmodified lines
12import { Route as ToolsRouteImport } from './routes/tools'12import { Route as ToolsRouteImport } from './routes/tools'
13import { Route as SecretsRouteImport } from './routes/secrets'13import { Route as SecretsRouteImport } from './routes/secrets'
14import { Route as IndexRouteImport } from './routes/index'14import { Route as IndexRouteImport } from './routes/index'
15import { Route as V1SplatRouteImport } from './routes/v1.$'
16import { Route as SourcesNamespaceRouteImport } from './routes/sources.$namespace'15import { Route as SourcesNamespaceRouteImport } from './routes/sources.$namespace'
17import { Route as AuthSplatRouteImport } from './routes/auth.$'16import { Route as ApiSplatRouteImport } from './routes/api.$'
18import { Route as SourcesAddPluginKeyRouteImport } from './routes/sources.add.$pluginKey'17import { Route as SourcesAddPluginKeyRouteImport } from './routes/sources.add.$pluginKey'
19 18
20const ToolsRoute = ToolsRouteImport.update({19const ToolsRoute = ToolsRouteImport.update({
@@ -32,19 +31,14 @@ const IndexRoute = IndexRouteImport.update({ · 11 unmodified lines
32 path: '/',31 path: '/',
33 getParentRoute: () => rootRouteImport,32 getParentRoute: () => rootRouteImport,
34} as any)33} as any)
35const V1SplatRoute = V1SplatRouteImport.update({
36 id: '/v1/$',
37 path: '/v1/$',
38 getParentRoute: () => rootRouteImport,
39} as any)
40const SourcesNamespaceRoute = SourcesNamespaceRouteImport.update({34const SourcesNamespaceRoute = SourcesNamespaceRouteImport.update({
41 id: '/sources/$namespace',35 id: '/sources/$namespace',
42 path: '/sources/$namespace',36 path: '/sources/$namespace',
43 getParentRoute: () => rootRouteImport,37 getParentRoute: () => rootRouteImport,
44} as any)38} as any)
45const AuthSplatRoute = AuthSplatRouteImport.update({39const ApiSplatRoute = ApiSplatRouteImport.update({
46 id: '/auth/$',40 id: '/api/$',
47 path: '/auth/$',41 path: '/api/$',
48 getParentRoute: () => rootRouteImport,42 getParentRoute: () => rootRouteImport,
49} as any)43} as any)
50const SourcesAddPluginKeyRoute = SourcesAddPluginKeyRouteImport.update({44const SourcesAddPluginKeyRoute = SourcesAddPluginKeyRouteImport.update({
@@ -57,18 +51,16 @@ export interface FileRoutesByFullPath { · 6 unmodified lines
57 '/': typeof IndexRoute51 '/': typeof IndexRoute
58 '/secrets': typeof SecretsRoute52 '/secrets': typeof SecretsRoute
59 '/tools': typeof ToolsRoute53 '/tools': typeof ToolsRoute
60 '/auth/$': typeof AuthSplatRoute54 '/api/$': typeof ApiSplatRoute
61 '/sources/$namespace': typeof SourcesNamespaceRoute55 '/sources/$namespace': typeof SourcesNamespaceRoute
62 '/v1/$': typeof V1SplatRoute
63 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute56 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
64}57}
65export interface FileRoutesByTo {58export interface FileRoutesByTo {
66 '/': typeof IndexRoute59 '/': typeof IndexRoute
67 '/secrets': typeof SecretsRoute60 '/secrets': typeof SecretsRoute
68 '/tools': typeof ToolsRoute61 '/tools': typeof ToolsRoute
69 '/auth/$': typeof AuthSplatRoute62 '/api/$': typeof ApiSplatRoute
70 '/sources/$namespace': typeof SourcesNamespaceRoute63 '/sources/$namespace': typeof SourcesNamespaceRoute
71 '/v1/$': typeof V1SplatRoute
72 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute64 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
73}65}
74export interface FileRoutesById {66export interface FileRoutesById {
@@ -76,9 +68,8 @@ export interface FileRoutesById { · 1 unmodified line
76 '/': typeof IndexRoute68 '/': typeof IndexRoute
77 '/secrets': typeof SecretsRoute69 '/secrets': typeof SecretsRoute
78 '/tools': typeof ToolsRoute70 '/tools': typeof ToolsRoute
79 '/auth/$': typeof AuthSplatRoute71 '/api/$': typeof ApiSplatRoute
80 '/sources/$namespace': typeof SourcesNamespaceRoute72 '/sources/$namespace': typeof SourcesNamespaceRoute
81 '/v1/$': typeof V1SplatRoute
82 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute73 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
83}74}
84export interface FileRouteTypes {75export interface FileRouteTypes {
@@ -87,27 +78,24 @@ export interface FileRouteTypes { · 2 unmodified lines
87 | '/'78 | '/'
88 | '/secrets'79 | '/secrets'
89 | '/tools'80 | '/tools'
90 | '/auth/$'81 | '/api/$'
91 | '/sources/$namespace'82 | '/sources/$namespace'
92 | '/v1/$'
93 | '/sources/add/$pluginKey'83 | '/sources/add/$pluginKey'
94 fileRoutesByTo: FileRoutesByTo84 fileRoutesByTo: FileRoutesByTo
95 to:85 to:
96 | '/'86 | '/'
97 | '/secrets'87 | '/secrets'
98 | '/tools'88 | '/tools'
99 | '/auth/$'89 | '/api/$'
100 | '/sources/$namespace'90 | '/sources/$namespace'
101 | '/v1/$'
102 | '/sources/add/$pluginKey'91 | '/sources/add/$pluginKey'
103 id:92 id:
104 | '__root__'93 | '__root__'
105 | '/'94 | '/'
106 | '/secrets'95 | '/secrets'
107 | '/tools'96 | '/tools'
108 | '/auth/$'97 | '/api/$'
109 | '/sources/$namespace'98 | '/sources/$namespace'
110 | '/v1/$'
111 | '/sources/add/$pluginKey'99 | '/sources/add/$pluginKey'
112 fileRoutesById: FileRoutesById100 fileRoutesById: FileRoutesById
113}101}
@@ -115,9 +103,8 @@ export interface RootRouteChildren { · 1 unmodified line
115 IndexRoute: typeof IndexRoute103 IndexRoute: typeof IndexRoute
116 SecretsRoute: typeof SecretsRoute104 SecretsRoute: typeof SecretsRoute
117 ToolsRoute: typeof ToolsRoute105 ToolsRoute: typeof ToolsRoute
118 AuthSplatRoute: typeof AuthSplatRoute106 ApiSplatRoute: typeof ApiSplatRoute
119 SourcesNamespaceRoute: typeof SourcesNamespaceRoute107 SourcesNamespaceRoute: typeof SourcesNamespaceRoute
120 V1SplatRoute: typeof V1SplatRoute
121 SourcesAddPluginKeyRoute: typeof SourcesAddPluginKeyRoute108 SourcesAddPluginKeyRoute: typeof SourcesAddPluginKeyRoute
122}109}
123 110
@@ -144,13 +131,6 @@ declare module '@tanstack/react-router' { · 20 unmodified lines
144 preLoaderRoute: typeof IndexRouteImport131 preLoaderRoute: typeof IndexRouteImport
145 parentRoute: typeof rootRouteImport132 parentRoute: typeof rootRouteImport
146 }133 }
147 '/v1/$': {
148 id: '/v1/$'
149 path: '/v1/$'
150 fullPath: '/v1/$'
151 preLoaderRoute: typeof V1SplatRouteImport
152 parentRoute: typeof rootRouteImport
153 }
154 '/sources/$namespace': {134 '/sources/$namespace': {
155 id: '/sources/$namespace'135 id: '/sources/$namespace'
156 path: '/sources/$namespace'136 path: '/sources/$namespace'
@@ -158,11 +138,11 @@ declare module '@tanstack/react-router' { · 1 unmodified line
158 preLoaderRoute: typeof SourcesNamespaceRouteImport138 preLoaderRoute: typeof SourcesNamespaceRouteImport
159 parentRoute: typeof rootRouteImport139 parentRoute: typeof rootRouteImport
160 }140 }
161 '/auth/$': {141 '/api/$': {
162 id: '/auth/$'142 id: '/api/$'
163 path: '/auth/$'143 path: '/api/$'
164 fullPath: '/auth/$'144 fullPath: '/api/$'
165 preLoaderRoute: typeof AuthSplatRouteImport145 preLoaderRoute: typeof ApiSplatRouteImport
166 parentRoute: typeof rootRouteImport146 parentRoute: typeof rootRouteImport
167 }147 }
168 '/sources/add/$pluginKey': {148 '/sources/add/$pluginKey': {
@@ -179,9 +159,8 @@ const rootRouteChildren: RootRouteChildren = { · 10 unmodified lines
179 IndexRoute: IndexRoute,159 IndexRoute: IndexRoute,
180 SecretsRoute: SecretsRoute,160 SecretsRoute: SecretsRoute,
181 ToolsRoute: ToolsRoute,161 ToolsRoute: ToolsRoute,
182 AuthSplatRoute: AuthSplatRoute,162 ApiSplatRoute: ApiSplatRoute,
183 SourcesNamespaceRoute: SourcesNamespaceRoute,163 SourcesNamespaceRoute: SourcesNamespaceRoute,
184 V1SplatRoute: V1SplatRoute,
185 SourcesAddPluginKeyRoute: SourcesAddPluginKeyRoute,164 SourcesAddPluginKeyRoute: SourcesAddPluginKeyRoute,
186}165}
187export const routeTree = rootRouteImport166export const routeTree = rootRouteImport
apps/cloud/src/routes/api.$.ts+18−0
@@ -0,0 +1,18 @@
1import { createFileRoute } from "@tanstack/react-router";
2
3const handle = async (request: Request) => {
4 const { handleApiRequest } = await import("../server/api-handler");
5 return handleApiRequest(request);
6};
7
8export const Route = createFileRoute("/api/$")({
9 server: {
10 handlers: {
11 GET: ({ request }) => handle(request),
12 POST: ({ request }) => handle(request),
13 PUT: ({ request }) => handle(request),
14 DELETE: ({ request }) => handle(request),
15 PATCH: ({ request }) => handle(request),
16 },
17 },
18});
apps/cloud/src/routes/v1.$.ts+0−14
@@ -1,14 +0,0 @@
1import { createFileRoute } from "@tanstack/react-router";
2import { handleApiRequest } from "../server/api-handler";
3
4export const Route = createFileRoute("/v1/$")({
5 server: {
6 handlers: {
7 GET: ({ request }) => handleApiRequest(request),
8 POST: ({ request }) => handleApiRequest(request),
9 PUT: ({ request }) => handleApiRequest(request),
10 DELETE: ({ request }) => handleApiRequest(request),
11 PATCH: ({ request }) => handleApiRequest(request),
12 },
13 },
14});
apps/cloud/src/server/api-handler.ts+8−8
@@ -1,9 +1,9 @@
1import { createCloudApiHandler } from "../api";1import { handleApiRequest as _handleApiRequest } from "../api";
2import { getDb } from "../services/db";
3 2
4const handlerPromise = getDb().then((db) =>3export const handleApiRequest = (request: Request) => {
5 createCloudApiHandler(db, process.env.ENCRYPTION_KEY ?? "local-dev-encryption-key"),4 // Strip /api prefix — route catch-all (api.$.ts) matches /api/*
6);5 // but Effect endpoints are defined without the prefix
7 6 const url = new URL(request.url);
8export const handleApiRequest = async (request: Request) =>7 url.pathname = url.pathname.replace(/^\/api/, "");
9 (await handlerPromise)(request);8 return _handleApiRequest(new Request(url, request));
9};
apps/cloud/src/services/db.ts+36−14
@@ -2,10 +2,14 @@ · 1 unmodified line
2// Database service — PGlite for dev, node-postgres for prod2// Database service — PGlite for dev, node-postgres for prod
3// ---------------------------------------------------------------------------3// ---------------------------------------------------------------------------
4 4
5import { Context, Effect, Layer } from "effect";
5import { resolve } from "node:path";6import { resolve } from "node:path";
6import * as schema from "@executor/storage-postgres";7import * as sharedSchema from "@executor/storage-postgres";
8import * as cloudSchema from "./schema";
7import type { DrizzleDb } from "@executor/storage-postgres";9import type { DrizzleDb } from "@executor/storage-postgres";
8 10
11const schema = { ...sharedSchema, ...cloudSchema };
12
9export type { DrizzleDb };13export type { DrizzleDb };
10 14
11const MIGRATIONS_DIR = resolve(15const MIGRATIONS_DIR = resolve(
@@ -13,27 +17,45 @@ const MIGRATIONS_DIR = resolve( · 1 unmodified line
13 "../../../../packages/core/storage-postgres/drizzle",17 "../../../../packages/core/storage-postgres/drizzle",
14);18);
15 19
16let db: DrizzleDb | null = null;20let dbPromise: Promise<DrizzleDb> | undefined;
17
18export const getDb = async (): Promise<DrizzleDb> => {
19 if (db) return db;
20 21
22const createDb = async (): Promise<DrizzleDb> => {
21 if (process.env.DATABASE_URL) {23 if (process.env.DATABASE_URL) {
22 const { drizzle } = await import("drizzle-orm/node-postgres");24 const { drizzle } = await import("drizzle-orm/node-postgres");
23 const { migrate } = await import("drizzle-orm/node-postgres/migrator");25 const { migrate } = await import("drizzle-orm/node-postgres/migrator");
24 const { Pool } = await import("pg");26 const { Pool } = await import("pg");
25 const pool = new Pool({ connectionString: process.env.DATABASE_URL });27 const pool = new Pool({ connectionString: process.env.DATABASE_URL });
26 db = drizzle(pool, { schema }) as DrizzleDb;28 const db = drizzle(pool, { schema }) as DrizzleDb;
27 await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR });
28 } else {
29 const { PGlite } = await import("@electric-sql/pglite");
30 const { drizzle } = await import("drizzle-orm/pglite");
31 const { migrate } = await import("drizzle-orm/pglite/migrator");
32 const dataDir = process.env.PGLITE_DATA_DIR ?? ".pglite";
33 const client = new PGlite(dataDir);
34 db = drizzle(client, { schema }) as DrizzleDb;
35 await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR });29 await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR });
30 return db;
36 }31 }
37 32
33 const { PGlite } = await import("@electric-sql/pglite");
34 const { drizzle } = await import("drizzle-orm/pglite");
35 const { migrate } = await import("drizzle-orm/pglite/migrator");
36 const dataDir = process.env.PGLITE_DATA_DIR ?? ".pglite";
37 const client = new PGlite(dataDir);
38 const db = drizzle(client, { schema }) as DrizzleDb;
39 await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR });
38 return db;40 return db;
39};41};
42
43const getOrCreateDb = async (): Promise<DrizzleDb> => {
44 if (!dbPromise) {
45 dbPromise = createDb().catch((error) => {
46 dbPromise = undefined;
47 throw error;
48 });
49 }
50 return dbPromise;
51};
52
53export class DbService extends Context.Tag("@executor/cloud/DbService")<
54 DbService,
55 DrizzleDb
56>() {
57 static Live = Layer.effect(
58 this,
59 Effect.promise(getOrCreateDb),
60 );
61}
apps/cloud/src/services/executor.ts+2−2
@@ -5,7 +5,6 @@ · 4 unmodified lines
5import { Effect } from "effect";5import { Effect } from "effect";
6 6
7import { createExecutor } from "@executor/sdk";7import { createExecutor } from "@executor/sdk";
8import type { DrizzleDb } from "@executor/storage-postgres";
9import { makePgConfig, makePgKv } from "@executor/storage-postgres";8import { makePgConfig, makePgKv } from "@executor/storage-postgres";
10import {9import {
11 openApiPlugin,10 openApiPlugin,
@@ -23,18 +22,19 @@ import { · 11 unmodified lines
23 graphqlPlugin,22 graphqlPlugin,
24 makeKvOperationStore as makeKvGraphqlOperationStore,23 makeKvOperationStore as makeKvGraphqlOperationStore,
25} from "@executor/plugin-graphql";24} from "@executor/plugin-graphql";
25import { DbService } from "./db";
26 26
27// ---------------------------------------------------------------------------27// ---------------------------------------------------------------------------
28// Create a fresh executor for a team (stateless, per-request)28// Create a fresh executor for a team (stateless, per-request)
29// ---------------------------------------------------------------------------29// ---------------------------------------------------------------------------
30 30
31export const createTeamExecutor = (31export const createTeamExecutor = (
32 db: DrizzleDb,
33 teamId: string,32 teamId: string,
34 teamName: string,33 teamName: string,
35 encryptionKey: string,34 encryptionKey: string,
36) =>35) =>
37 Effect.gen(function* () {36 Effect.gen(function* () {
37 const db = yield* DbService;
38 const kv = makePgKv(db, teamId);38 const kv = makePgKv(db, teamId);
39 const config = makePgConfig(db, {39 const config = makePgConfig(db, {
40 teamId,40 teamId,
apps/cloud/src/services/schema.ts+40−0
@@ -0,0 +1,40 @@
1// ---------------------------------------------------------------------------
2// Cloud-specific identity & multi-tenancy tables
3// ---------------------------------------------------------------------------
4
5import { pgTable, text, timestamp, primaryKey } from "drizzle-orm/pg-core";
6
7export const users = pgTable("users", {
8 id: text("id").primaryKey(),
9 email: text("email").notNull().unique(),
10 name: text("name"),
11 avatarUrl: text("avatar_url"),
12 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
13});
14
15export const teams = pgTable("teams", {
16 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
17 name: text("name").notNull(),
18 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
19});
20
21export const teamMembers = pgTable(
22 "team_members",
23 {
24 teamId: text("team_id").notNull(),
25 userId: text("user_id").notNull(),
26 role: text("role").notNull().$default(() => "member"),
27 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
28 },
29 (table) => [primaryKey({ columns: [table.teamId, table.userId] })],
30);
31
32export const invitations = pgTable("invitations", {
33 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
34 teamId: text("team_id").notNull(),
35 email: text("email").notNull(),
36 invitedBy: text("invited_by").notNull(),
37 status: text("status").notNull().$default(() => "pending"),
38 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
39 expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
40});
apps/cloud/src/services/user-store.tsrenamed from packages/core/storage-postgres/src/user-store.ts+1−1
@@ -5,7 +5,7 @@ · 4 unmodified lines
5import { eq, and } from "drizzle-orm";5import { eq, and } from "drizzle-orm";
6 6
7import { users, teams, teamMembers, invitations } from "./schema";7import { users, teams, teamMembers, invitations } from "./schema";
8import type { DrizzleDb } from "./types";8import type { DrizzleDb } from "@executor/storage-postgres";
9 9
10export type User = typeof users.$inferSelect;10export type User = typeof users.$inferSelect;
11export type Team = typeof teams.$inferSelect;11export type Team = typeof teams.$inferSelect;
apps/cloud/src/web/auth.tsx+14−2
@@ -1,5 +1,5 @@
1import React, { createContext, useContext } from "react";1import React, { createContext, useContext } from "react";
2import { useAtomValue, Result } from "@effect-atom/atom-react";2import { useAtomMount, useAtomValue, Result } from "@effect-atom/atom-react";
3 3
4import { CloudApiClient } from "./client";4import { CloudApiClient } from "./client";
5 5
@@ -40,7 +40,8 @@ const AuthContext = createContext<AuthState>({ status: "loading" }); · 34 unmodified lines
40 40
41export const useAuth = () => useContext(AuthContext);41export const useAuth = () => useContext(AuthContext);
42 42
43export const AuthProvider = ({ children }: { children: React.ReactNode }) => {43const AuthProviderClient = ({ children }: { children: React.ReactNode }) => {
44 useAtomMount(authAtom);
44 const result = useAtomValue(authAtom);45 const result = useAtomValue(authAtom);
45 46
46 const state: AuthState = Result.match(result, {47 const state: AuthState = Result.match(result, {
@@ -55,3 +56,14 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { · 8 unmodified lines
55 56
56 return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;57 return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
57};58};
59
60export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
61 if (typeof window === "undefined") {
62 return (
63 <AuthContext.Provider value={{ status: "loading" }}>
64 {children}
65 </AuthContext.Provider>
66 );
67 }
68 return <AuthProviderClient>{children}</AuthProviderClient>;
69};
apps/cloud/src/web/pages/login.tsx+2−1
@@ -1,4 +1,5 @@
1import React from "react";1import React from "react";
2import { AUTH_PATHS } from "../../auth/api";
2 3
3export const LoginPage = () => {4export const LoginPage = () => {
4 return (5 return (
@@ -11,7 +12,7 @@ export const LoginPage = () => { · 6 unmodified lines
11 </p>12 </p>
12 </div>13 </div>
13 <a14 <a
14 href="/auth/login"15 href={AUTH_PATHS.login}
15 className="inline-flex h-10 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 transition-colors"16 className="inline-flex h-10 w-full items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 transition-colors"
16 >17 >
17 Sign in18 Sign in
apps/cloud/src/web/shell.tsx+2−1
@@ -4,6 +4,7 @@ import { useAtomRefresh, useAtomValue, Result } from "@effect-atom/atom-react"; · 3 unmodified lines
4import { sourcesAtom, toolsAtom } from "@executor/react/api/atoms";4import { sourcesAtom, toolsAtom } from "@executor/react/api/atoms";
5import { useScope } from "@executor/react/api/scope-context";5import { useScope } from "@executor/react/api/scope-context";
6import { Button } from "@executor/react/components/button";6import { Button } from "@executor/react/components/button";
7import { AUTH_PATHS } from "../auth/api";
7import { useAuth } from "./auth";8import { useAuth } from "./auth";
8 9
9// ── NavItem ──────────────────────────────────────────────────────────────10// ── NavItem ──────────────────────────────────────────────────────────────
@@ -109,7 +110,7 @@ function UserFooter() { · 99 unmodified lines
109 </p>110 </p>
110 )}111 )}
111 </div>112 </div>
112 <form action="/auth/logout" method="post">113 <form action={AUTH_PATHS.logout} method="post">
113 <button114 <button
114 type="submit"115 type="submit"
115 className="shrink-0 rounded-md p-1 text-muted-foreground/50 transition-colors hover:bg-sidebar-active hover:text-foreground"116 className="shrink-0 rounded-md p-1 text-muted-foreground/50 transition-colors hover:bg-sidebar-active hover:text-foreground"
apps/local/src/routeTree.gen.ts+21−21
@@ -12,9 +12,9 @@ import { Route as rootRouteImport } from './routes/__root' · 11 unmodified lines
12import { Route as ToolsRouteImport } from './routes/tools'12import { Route as ToolsRouteImport } from './routes/tools'
13import { Route as SecretsRouteImport } from './routes/secrets'13import { Route as SecretsRouteImport } from './routes/secrets'
14import { Route as IndexRouteImport } from './routes/index'14import { Route as IndexRouteImport } from './routes/index'
15import { Route as V1SplatRouteImport } from './routes/v1.$'
16import { Route as SourcesNamespaceRouteImport } from './routes/sources.$namespace'15import { Route as SourcesNamespaceRouteImport } from './routes/sources.$namespace'
17import { Route as McpSplatRouteImport } from './routes/mcp.$'16import { Route as McpSplatRouteImport } from './routes/mcp.$'
17import { Route as ApiSplatRouteImport } from './routes/api.$'
18import { Route as SourcesAddPluginKeyRouteImport } from './routes/sources.add.$pluginKey'18import { Route as SourcesAddPluginKeyRouteImport } from './routes/sources.add.$pluginKey'
19 19
20const ToolsRoute = ToolsRouteImport.update({20const ToolsRoute = ToolsRouteImport.update({
@@ -32,11 +32,6 @@ const IndexRoute = IndexRouteImport.update({ · 11 unmodified lines
32 path: '/',32 path: '/',
33 getParentRoute: () => rootRouteImport,33 getParentRoute: () => rootRouteImport,
34} as any)34} as any)
35const V1SplatRoute = V1SplatRouteImport.update({
36 id: '/v1/$',
37 path: '/v1/$',
38 getParentRoute: () => rootRouteImport,
39} as any)
40const SourcesNamespaceRoute = SourcesNamespaceRouteImport.update({35const SourcesNamespaceRoute = SourcesNamespaceRouteImport.update({
41 id: '/sources/$namespace',36 id: '/sources/$namespace',
42 path: '/sources/$namespace',37 path: '/sources/$namespace',
@@ -47,6 +42,11 @@ const McpSplatRoute = McpSplatRouteImport.update({ · 4 unmodified lines
47 path: '/mcp/$',42 path: '/mcp/$',
48 getParentRoute: () => rootRouteImport,43 getParentRoute: () => rootRouteImport,
49} as any)44} as any)
45const ApiSplatRoute = ApiSplatRouteImport.update({
46 id: '/api/$',
47 path: '/api/$',
48 getParentRoute: () => rootRouteImport,
49} as any)
50const SourcesAddPluginKeyRoute = SourcesAddPluginKeyRouteImport.update({50const SourcesAddPluginKeyRoute = SourcesAddPluginKeyRouteImport.update({
51 id: '/sources/add/$pluginKey',51 id: '/sources/add/$pluginKey',
52 path: '/sources/add/$pluginKey',52 path: '/sources/add/$pluginKey',
@@ -57,18 +57,18 @@ export interface FileRoutesByFullPath { · 4 unmodified lines
57 '/': typeof IndexRoute57 '/': typeof IndexRoute
58 '/secrets': typeof SecretsRoute58 '/secrets': typeof SecretsRoute
59 '/tools': typeof ToolsRoute59 '/tools': typeof ToolsRoute
60 '/api/$': typeof ApiSplatRoute
60 '/mcp/$': typeof McpSplatRoute61 '/mcp/$': typeof McpSplatRoute
61 '/sources/$namespace': typeof SourcesNamespaceRoute62 '/sources/$namespace': typeof SourcesNamespaceRoute
62 '/v1/$': typeof V1SplatRoute
63 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute63 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
64}64}
65export interface FileRoutesByTo {65export interface FileRoutesByTo {
66 '/': typeof IndexRoute66 '/': typeof IndexRoute
67 '/secrets': typeof SecretsRoute67 '/secrets': typeof SecretsRoute
68 '/tools': typeof ToolsRoute68 '/tools': typeof ToolsRoute
69 '/api/$': typeof ApiSplatRoute
69 '/mcp/$': typeof McpSplatRoute70 '/mcp/$': typeof McpSplatRoute
70 '/sources/$namespace': typeof SourcesNamespaceRoute71 '/sources/$namespace': typeof SourcesNamespaceRoute
71 '/v1/$': typeof V1SplatRoute
72 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute72 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
73}73}
74export interface FileRoutesById {74export interface FileRoutesById {
@@ -76,9 +76,9 @@ export interface FileRoutesById { · 1 unmodified line
76 '/': typeof IndexRoute76 '/': typeof IndexRoute
77 '/secrets': typeof SecretsRoute77 '/secrets': typeof SecretsRoute
78 '/tools': typeof ToolsRoute78 '/tools': typeof ToolsRoute
79 '/api/$': typeof ApiSplatRoute
79 '/mcp/$': typeof McpSplatRoute80 '/mcp/$': typeof McpSplatRoute
80 '/sources/$namespace': typeof SourcesNamespaceRoute81 '/sources/$namespace': typeof SourcesNamespaceRoute
81 '/v1/$': typeof V1SplatRoute
82 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute82 '/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
83}83}
84export interface FileRouteTypes {84export interface FileRouteTypes {
@@ -87,27 +87,27 @@ export interface FileRouteTypes { · 2 unmodified lines
87 | '/'87 | '/'
88 | '/secrets'88 | '/secrets'
89 | '/tools'89 | '/tools'
90 | '/api/$'
90 | '/mcp/$'91 | '/mcp/$'
91 | '/sources/$namespace'92 | '/sources/$namespace'
92 | '/v1/$'
93 | '/sources/add/$pluginKey'93 | '/sources/add/$pluginKey'
94 fileRoutesByTo: FileRoutesByTo94 fileRoutesByTo: FileRoutesByTo
95 to:95 to:
96 | '/'96 | '/'
97 | '/secrets'97 | '/secrets'
98 | '/tools'98 | '/tools'
99 | '/api/$'
99 | '/mcp/$'100 | '/mcp/$'
100 | '/sources/$namespace'101 | '/sources/$namespace'
101 | '/v1/$'
102 | '/sources/add/$pluginKey'102 | '/sources/add/$pluginKey'
103 id:103 id:
104 | '__root__'104 | '__root__'
105 | '/'105 | '/'
106 | '/secrets'106 | '/secrets'
107 | '/tools'107 | '/tools'
108 | '/api/$'
108 | '/mcp/$'109 | '/mcp/$'
109 | '/sources/$namespace'110 | '/sources/$namespace'
110 | '/v1/$'
111 | '/sources/add/$pluginKey'111 | '/sources/add/$pluginKey'
112 fileRoutesById: FileRoutesById112 fileRoutesById: FileRoutesById
113}113}
@@ -115,9 +115,9 @@ export interface RootRouteChildren { · 1 unmodified line
115 IndexRoute: typeof IndexRoute115 IndexRoute: typeof IndexRoute
116 SecretsRoute: typeof SecretsRoute116 SecretsRoute: typeof SecretsRoute
117 ToolsRoute: typeof ToolsRoute117 ToolsRoute: typeof ToolsRoute
118 ApiSplatRoute: typeof ApiSplatRoute
118 McpSplatRoute: typeof McpSplatRoute119 McpSplatRoute: typeof McpSplatRoute
119 SourcesNamespaceRoute: typeof SourcesNamespaceRoute120 SourcesNamespaceRoute: typeof SourcesNamespaceRoute
120 V1SplatRoute: typeof V1SplatRoute
121 SourcesAddPluginKeyRoute: typeof SourcesAddPluginKeyRoute121 SourcesAddPluginKeyRoute: typeof SourcesAddPluginKeyRoute
122}122}
123 123
@@ -144,13 +144,6 @@ declare module '@tanstack/react-router' { · 20 unmodified lines
144 preLoaderRoute: typeof IndexRouteImport144 preLoaderRoute: typeof IndexRouteImport
145 parentRoute: typeof rootRouteImport145 parentRoute: typeof rootRouteImport
146 }146 }
147 '/v1/$': {
148 id: '/v1/$'
149 path: '/v1/$'
150 fullPath: '/v1/$'
151 preLoaderRoute: typeof V1SplatRouteImport
152 parentRoute: typeof rootRouteImport
153 }
154 '/sources/$namespace': {147 '/sources/$namespace': {
155 id: '/sources/$namespace'148 id: '/sources/$namespace'
156 path: '/sources/$namespace'149 path: '/sources/$namespace'
@@ -165,6 +158,13 @@ declare module '@tanstack/react-router' { · 8 unmodified lines
165 preLoaderRoute: typeof McpSplatRouteImport158 preLoaderRoute: typeof McpSplatRouteImport
166 parentRoute: typeof rootRouteImport159 parentRoute: typeof rootRouteImport
167 }160 }
161 '/api/$': {
162 id: '/api/$'
163 path: '/api/$'
164 fullPath: '/api/$'
165 preLoaderRoute: typeof ApiSplatRouteImport
166 parentRoute: typeof rootRouteImport
167 }
168 '/sources/add/$pluginKey': {168 '/sources/add/$pluginKey': {
169 id: '/sources/add/$pluginKey'169 id: '/sources/add/$pluginKey'
170 path: '/sources/add/$pluginKey'170 path: '/sources/add/$pluginKey'
@@ -179,9 +179,9 @@ const rootRouteChildren: RootRouteChildren = { · 8 unmodified lines
179 IndexRoute: IndexRoute,179 IndexRoute: IndexRoute,
180 SecretsRoute: SecretsRoute,180 SecretsRoute: SecretsRoute,
181 ToolsRoute: ToolsRoute,181 ToolsRoute: ToolsRoute,
182 ApiSplatRoute: ApiSplatRoute,
182 McpSplatRoute: McpSplatRoute,183 McpSplatRoute: McpSplatRoute,
183 SourcesNamespaceRoute: SourcesNamespaceRoute,184 SourcesNamespaceRoute: SourcesNamespaceRoute,
184 V1SplatRoute: V1SplatRoute,
185 SourcesAddPluginKeyRoute: SourcesAddPluginKeyRoute,185 SourcesAddPluginKeyRoute: SourcesAddPluginKeyRoute,
186}186}
187export const routeTree = rootRouteImport187export const routeTree = rootRouteImport
apps/local/src/routes/api.$.ts+18−0
@@ -0,0 +1,18 @@
1import { createFileRoute } from "@tanstack/react-router";
2
3const handle = async (request: Request) => {
4 const { handleApiRequest } = await import("../server/api-handler");
5 return handleApiRequest(request);
6};
7
8export const Route = createFileRoute("/api/$")({
9 server: {
10 handlers: {
11 GET: ({ request }) => handle(request),
12 POST: ({ request }) => handle(request),
13 PUT: ({ request }) => handle(request),
14 DELETE: ({ request }) => handle(request),
15 PATCH: ({ request }) => handle(request),
16 },
17 },
18});
apps/local/src/routes/v1.$.ts+0−14
@@ -1,14 +0,0 @@
1import { createFileRoute } from "@tanstack/react-router";
2import { handleApiRequest } from "../server/api-handler";
3
4export const Route = createFileRoute("/v1/$")({
5 server: {
6 handlers: {
7 GET: ({ request }) => handleApiRequest(request),
8 POST: ({ request }) => handleApiRequest(request),
9 PUT: ({ request }) => handleApiRequest(request),
10 DELETE: ({ request }) => handleApiRequest(request),
11 PATCH: ({ request }) => handleApiRequest(request),
12 },
13 },
14});
apps/local/src/server/api-handler.ts+5−1
@@ -20,7 +20,11 @@ const getHandlers = () => { · 19 unmodified lines
20 20
21export const handleApiRequest = async (request: Request): Promise<Response> => {21export const handleApiRequest = async (request: Request): Promise<Response> => {
22 const handlers = await getHandlers();22 const handlers = await getHandlers();
23 return handlers.api.handler(request);23 // Strip /api prefix — the route catch-all (api.$.ts) matches /api/*
24 // but the Effect handler endpoints are defined without the prefix
25 const url = new URL(request.url);
26 url.pathname = url.pathname.replace(/^\/api/, "");
27 return handlers.api.handler(new Request(url, request));
24};28};
25 29
26export const handleMcpRequest = async (request: Request): Promise<Response> => {30export const handleMcpRequest = async (request: Request): Promise<Response> => {
apps/local/src/server/main.ts+1−1
@@ -62,7 +62,7 @@ export const createServerHandlers = async (): Promise<ServerHandlers> => { · 61 unmodified lines
62 );62 );
63 63
64 const api = HttpApiBuilder.toWebHandler(64 const api = HttpApiBuilder.toWebHandler(
65 HttpApiSwagger.layer().pipe(65 HttpApiSwagger.layer({ path: "/docs" }).pipe(
66 Layer.provideMerge(HttpApiBuilder.middlewareOpenApi()),66 Layer.provideMerge(HttpApiBuilder.middlewareOpenApi()),
67 Layer.provideMerge(LocalApiBase),67 Layer.provideMerge(LocalApiBase),
68 Layer.provideMerge(pluginExtensions),68 Layer.provideMerge(pluginExtensions),
packages/core/api/src/executions/api.ts+1−1
@@ -61,4 +61,4 @@ export class ExecutionsApi extends HttpApiGroup.make("executions") · 60 unmodified lines
61 .addSuccess(ResumeResponse)61 .addSuccess(ResumeResponse)
62 .addError(ExecutionNotFoundError),62 .addError(ExecutionNotFoundError),
63 )63 )
64 .prefix("/v1") {}64 {}
packages/core/api/src/scope/api.ts+1−1
@@ -21,4 +21,4 @@ export class ScopeApi extends HttpApiGroup.make("scope") · 20 unmodified lines
21 HttpApiEndpoint.get("info")`/scope`21 HttpApiEndpoint.get("info")`/scope`
22 .addSuccess(ScopeInfoResponse),22 .addSuccess(ScopeInfoResponse),
23 )23 )
24 .prefix("/v1") {}24 {}
packages/core/api/src/secrets/api.ts+1−1
@@ -86,4 +86,4 @@ export class SecretsApi extends HttpApiGroup.make("secrets") · 85 unmodified lines
86 .addSuccess(Schema.Struct({ removed: Schema.Boolean }))86 .addSuccess(Schema.Struct({ removed: Schema.Boolean }))
87 .addError(SecretNotFound),87 .addError(SecretNotFound),
88 )88 )
89 .prefix("/v1") {}89 {}
packages/core/api/src/sources/api.ts+1−1
@@ -77,4 +77,4 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 76 unmodified lines
77 .setPayload(DetectRequest)77 .setPayload(DetectRequest)
78 .addSuccess(Schema.Array(DetectResultResponse)),78 .addSuccess(Schema.Array(DetectResultResponse)),
79 )79 )
80 .prefix("/v1") {}80 {}
packages/core/api/src/tools/api.ts+1−1
@@ -57,4 +57,4 @@ export class ToolsApi extends HttpApiGroup.make("tools") · 56 unmodified lines
57 .addSuccess(ToolSchemaResponse)57 .addSuccess(ToolSchemaResponse)
58 .addError(ToolNotFound),58 .addError(ToolNotFound),
59 )59 )
60 .prefix("/v1") {}60 {}
packages/core/storage-postgres/src/index.test.ts+0−55
@@ -21,7 +21,6 @@ import { · 20 unmodified lines
21 21
22import { makePgConfig } from "./index";22import { makePgConfig } from "./index";
23import { makePgKv } from "./pg-kv";23import { makePgKv } from "./pg-kv";
24import { makeUserStore } from "./user-store";
25import * as schema from "./schema";24import * as schema from "./schema";
26 25
27// ---------------------------------------------------------------------------26// ---------------------------------------------------------------------------
@@ -355,57 +354,3 @@ describe("Executor with Postgres storage", () => { · 327 unmodified lines
355 );354 );
356});355});
357 356
358// ---------------------------------------------------------------------------
359// User Store (not part of Executor, tested directly)
360// ---------------------------------------------------------------------------
361
362describe("UserStore", () => {
363 it("upsert and get user", async () => {
364 const store = makeUserStore(db);
365 const user = await store.upsertUser({
366 id: "user-1",
367 email: "test@example.com",
368 name: "Test User",
369 });
370 expect(user.id).toBe("user-1");
371
372 const fetched = await store.getUser("user-1");
373 expect(fetched!.name).toBe("Test User");
374
375 await store.upsertUser({ id: "user-1", email: "test@example.com", name: "Updated" });
376 expect((await store.getUser("user-1"))!.name).toBe("Updated");
377 });
378
379 it("create team and manage members", async () => {
380 const store = makeUserStore(db);
381 await store.upsertUser({ id: "u1", email: "a@example.com", name: "A" });
382 await store.upsertUser({ id: "u2", email: "b@example.com", name: "B" });
383
384 const team = await store.createTeam("My Team");
385 await store.addMember(team.id, "u1", "owner");
386 await store.addMember(team.id, "u2", "member");
387
388 expect(await store.listMembers(team.id)).toHaveLength(2);
389
390 const teams = await store.getTeamsForUser("u1");
391 expect(teams[0]!.teamName).toBe("My Team");
392
393 await store.removeMember(team.id, "u2");
394 expect(await store.listMembers(team.id)).toHaveLength(1);
395 });
396
397 it("invitations workflow", async () => {
398 const store = makeUserStore(db);
399 await store.upsertUser({ id: "u1", email: "owner@example.com" });
400 const team = await store.createTeam("Team");
401 await store.addMember(team.id, "u1", "owner");
402
403 const invitation = await store.createInvitation(team.id, "new@example.com", "u1");
404 expect(invitation.status).toBe("pending");
405 expect(await store.getPendingInvitations("new@example.com")).toHaveLength(1);
406
407 await store.acceptInvitation(invitation.id);
408 expect(await store.getPendingInvitations("new@example.com")).toHaveLength(0);
409 });
410
411});
packages/core/storage-postgres/src/index.ts+0−2
@@ -29,8 +29,6 @@ export { makePgKv } from "./pg-kv"; · 28 unmodified lines
29export { makePgToolRegistry } from "./tool-registry";29export { makePgToolRegistry } from "./tool-registry";
30export { makePgSecretStore } from "./secret-store";30export { makePgSecretStore } from "./secret-store";
31export { makePgPolicyEngine } from "./policy-engine";31export { makePgPolicyEngine } from "./policy-engine";
32export { makeUserStore } from "./user-store";
33export type { User, Team, TeamMember, Invitation } from "./user-store";
34export { encrypt, decrypt } from "./crypto";32export { encrypt, decrypt } from "./crypto";
35export type { DrizzleDb } from "./types";33export type { DrizzleDb } from "./types";
36export * from "./schema";34export * from "./schema";
packages/core/storage-postgres/src/schema.ts+0−39
@@ -19,45 +19,6 @@ const bytea = customType<{ data: Buffer }>({ · 18 unmodified lines
19 },19 },
20});20});
21 21
22// ---------------------------------------------------------------------------
23// Identity & multi-tenancy
24// ---------------------------------------------------------------------------
25
26export const users = pgTable("users", {
27 id: text("id").primaryKey(),
28 email: text("email").notNull().unique(),
29 name: text("name"),
30 avatarUrl: text("avatar_url"),
31 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
32});
33
34export const teams = pgTable("teams", {
35 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
36 name: text("name").notNull(),
37 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
38});
39
40export const teamMembers = pgTable(
41 "team_members",
42 {
43 teamId: text("team_id").notNull(),
44 userId: text("user_id").notNull(),
45 role: text("role").notNull().$default(() => "member"),
46 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
47 },
48 (table) => [primaryKey({ columns: [table.teamId, table.userId] })],
49);
50
51export const invitations = pgTable("invitations", {
52 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
53 teamId: text("team_id").notNull(),
54 email: text("email").notNull(),
55 invitedBy: text("invited_by").notNull(),
56 status: text("status").notNull().$default(() => "pending"),
57 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
58 expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
59});
60
61// ---------------------------------------------------------------------------22// ---------------------------------------------------------------------------
62// Domain data — all team-scoped23// Domain data — all team-scoped
63// ---------------------------------------------------------------------------24// ---------------------------------------------------------------------------
packages/plugins/google-discovery/src/api/group.ts+1−1
@@ -125,4 +125,4 @@ export class GoogleDiscoveryGroup extends HttpApiGroup.make("googleDiscovery") · 124 unmodified lines
125 .addSuccess(HtmlResponse)125 .addSuccess(HtmlResponse)
126 .addError(ApiError),126 .addError(ApiError),
127 )127 )
128 .prefix("/v1") {}128 {}
packages/plugins/graphql/src/api/group.ts+1−1
@@ -58,4 +58,4 @@ export class GraphqlGroup extends HttpApiGroup.make("graphql") · 57 unmodified lines
58 .addError(IntrospectionError)58 .addError(IntrospectionError)
59 .addError(ExtractionError),59 .addError(ExtractionError),
60 )60 )
61 .prefix("/v1") {}61 {}
packages/plugins/mcp/src/api/group.ts+1−1
@@ -194,4 +194,4 @@ export class McpGroup extends HttpApiGroup.make("mcp") · 193 unmodified lines
194 .addSuccess(HtmlResponse)194 .addSuccess(HtmlResponse)
195 .addError(McpApiError),195 .addError(McpApiError),
196 )196 )
197 .prefix("/v1") {}197 {}
packages/plugins/onepassword/src/api/group.ts+1−1
@@ -70,4 +70,4 @@ export class OnePasswordGroup extends HttpApiGroup.make("onepassword") · 69 unmodified lines
70 .addSuccess(ListVaultsResponse)70 .addSuccess(ListVaultsResponse)
71 .addError(OpError),71 .addError(OpError),
72 )72 )
73 .prefix("/v1") {}73 {}
packages/plugins/openapi/src/api/group.ts+1−1
@@ -70,4 +70,4 @@ export class OpenApiGroup extends HttpApiGroup.make("openapi") · 69 unmodified lines
70 .addError(ParseError)70 .addError(ParseError)
71 .addError(ExtractionError),71 .addError(ExtractionError),
72 )72 )
73 .prefix("/v1") {}73 {}
packages/react/src/api/base-url.tsx+2−2
@@ -2,8 +2,8 @@ const DEFAULT_BASE_URL = "http://127.0.0.1:4000"; · 1 unmodified line
2 2
3let baseUrl =3let baseUrl =
4 typeof window !== "undefined" && typeof window.location?.origin === "string"4 typeof window !== "undefined" && typeof window.location?.origin === "string"
5 ? window.location.origin5 ? `${window.location.origin}/api`
6 : DEFAULT_BASE_URL;6 : `${DEFAULT_BASE_URL}/api`;
7 7
8export const getBaseUrl = (): string => baseUrl;8export const getBaseUrl = (): string => baseUrl;
9 9