- packages/core/api: HttpApi contract with ToolsApi + SecretsApi groups SDK tagged errors pass through directly with status annotations (ToolNotFoundError→404, PolicyDeniedError→403, etc.) - apps/server: API handler implementation via HttpApiBuilder Exports createApiHandler() for reuse (no baked-in server runtime) Handlers are pure pass-through — no manual error catching - apps/web: Vite + React frontend served on same port as API Vite plugin forwards /v1/*, /docs, /openapi.json to Effect handler effect-atom integration: AtomHttpApi.Tag typed client, query/mutation atoms RegistryProvider + useAtomValue with Result pattern matching - Catalog pins @effect/platform, platform-bun, platform-node, cluster - All 9 packages typecheck clean
Showing the changes to apps/server/src (5 of 26 files). Show all
Unified · Split
| @@ -0,0 +1,63 @@ | |||
| 1 | import { HttpApiBuilder } from "@effect/platform"; | ||
| 2 | import { Effect, Option } from "effect"; | ||
| 3 | import type { SecretId, ScopeId } from "@executor/sdk"; | ||
| 4 | |||
| 5 | import { ExecutorApi } from "@executor/api"; | ||
| 6 | import { ExecutorService } from "../services/executor"; | ||
| 7 | |||
| 8 | const refToResponse = (ref: { | ||
| 9 | id: SecretId; | ||
| 10 | scopeId: ScopeId; | ||
| 11 | name: string; | ||
| 12 | provider: Option.Option<string>; | ||
| 13 | purpose?: string; | ||
| 14 | createdAt: Date; | ||
| 15 | }) => ({ | ||
| 16 | id: ref.id, | ||
| 17 | scopeId: ref.scopeId, | ||
| 18 | name: ref.name, | ||
| 19 | provider: Option.getOrUndefined(ref.provider), | ||
| 20 | purpose: ref.purpose, | ||
| 21 | createdAt: ref.createdAt.getTime(), | ||
| 22 | }); | ||
| 23 | |||
| 24 | export const SecretsHandlers = HttpApiBuilder.group( | ||
| 25 | ExecutorApi, | ||
| 26 | "secrets", | ||
| 27 | (handlers) => | ||
| 28 | handlers | ||
| 29 | .handle("list", ({ path }) => | ||
| 30 | Effect.gen(function* () { | ||
| 31 | const executor = yield* ExecutorService; | ||
| 32 | const refs = yield* executor.secrets.list(); | ||
| 33 | return refs.map(refToResponse); | ||
| 34 | }), | ||
| 35 | ) | ||
| 36 | .handle("status", ({ path }) => | ||
| 37 | Effect.gen(function* () { | ||
| 38 | const executor = yield* ExecutorService; | ||
| 39 | const status = yield* executor.secrets.status(path.secretId); | ||
| 40 | return { secretId: path.secretId, status }; | ||
| 41 | }), | ||
| 42 | ) | ||
| 43 | .handle("set", ({ path, payload }) => | ||
| 44 | Effect.gen(function* () { | ||
| 45 | const executor = yield* ExecutorService; | ||
| 46 | const ref = yield* executor.secrets.set({ | ||
| 47 | id: payload.id, | ||
| 48 | name: payload.name, | ||
| 49 | value: payload.value, | ||
| 50 | purpose: payload.purpose, | ||
| 51 | provider: payload.provider, | ||
| 52 | }); | ||
| 53 | return refToResponse(ref); | ||
| 54 | }), | ||
| 55 | ) | ||
| 56 | .handle("remove", ({ path }) => | ||
| 57 | Effect.gen(function* () { | ||
| 58 | const executor = yield* ExecutorService; | ||
| 59 | const removed = yield* executor.secrets.remove(path.secretId); | ||
| 60 | return { removed }; | ||
| 61 | }), | ||
| 62 | ), | ||
| 63 | ); | ||
| @@ -0,0 +1,37 @@ | |||
| 1 | import { HttpApiBuilder } from "@effect/platform"; | ||
| 2 | import { Effect } from "effect"; | ||
| 3 | |||
| 4 | import { ExecutorApi } from "@executor/api"; | ||
| 5 | import { ExecutorService } from "../services/executor"; | ||
| 6 | |||
| 7 | export const ToolsHandlers = HttpApiBuilder.group( | ||
| 8 | ExecutorApi, | ||
| 9 | "tools", | ||
| 10 | (handlers) => | ||
| 11 | handlers | ||
| 12 | .handle("list", ({ path }) => | ||
| 13 | Effect.gen(function* () { | ||
| 14 | const executor = yield* ExecutorService; | ||
| 15 | const tools = yield* executor.tools.list(); | ||
| 16 | return tools.map((t) => ({ | ||
| 17 | id: t.id, | ||
| 18 | name: t.name, | ||
| 19 | description: t.description, | ||
| 20 | tags: [...t.tags], | ||
| 21 | mayElicit: t.mayElicit, | ||
| 22 | })); | ||
| 23 | }), | ||
| 24 | ) | ||
| 25 | .handle("schema", ({ path }) => | ||
| 26 | Effect.gen(function* () { | ||
| 27 | const executor = yield* ExecutorService; | ||
| 28 | return yield* executor.tools.schema(path.toolId); | ||
| 29 | }), | ||
| 30 | ) | ||
| 31 | .handle("invoke", ({ path, payload }) => | ||
| 32 | Effect.gen(function* () { | ||
| 33 | const executor = yield* ExecutorService; | ||
| 34 | return yield* executor.tools.invoke(path.toolId, payload.args); | ||
| 35 | }), | ||
| 36 | ), | ||
| 37 | ); | ||
| @@ -0,0 +1,2 @@ | |||
| 1 | export { createApiHandler, type ApiHandler } from "./main"; | ||
| 2 | export { ExecutorServiceLive } from "./services/executor"; | ||
| @@ -0,0 +1,40 @@ | |||
| 1 | import { | ||
| 2 | HttpApiBuilder, | ||
| 3 | HttpApiSwagger, | ||
| 4 | HttpMiddleware, | ||
| 5 | HttpServer, | ||
| 6 | } from "@effect/platform"; | ||
| 7 | import { Layer } from "effect"; | ||
| 8 | |||
| 9 | import { ExecutorApi } from "@executor/api"; | ||
| 10 | import { ToolsHandlers } from "./handlers/tools"; | ||
| 11 | import { SecretsHandlers } from "./handlers/secrets"; | ||
| 12 | import { ExecutorServiceLive } from "./services/executor"; | ||
| 13 | |||
| 14 | // --------------------------------------------------------------------------- | ||
| 15 | // API layer — wire handlers | ||
| 16 | // --------------------------------------------------------------------------- | ||
| 17 | |||
| 18 | const ApiLive = HttpApiBuilder.api(ExecutorApi).pipe( | ||
| 19 | Layer.provide([ToolsHandlers, SecretsHandlers]), | ||
| 20 | Layer.provide(ExecutorServiceLive), | ||
| 21 | ); | ||
| 22 | |||
| 23 | // --------------------------------------------------------------------------- | ||
| 24 | // Web handler — usable by Vite plugin, standalone server, tests, etc. | ||
| 25 | // --------------------------------------------------------------------------- | ||
| 26 | |||
| 27 | export const createApiHandler = () => | ||
| 28 | HttpApiBuilder.toWebHandler( | ||
| 29 | HttpApiSwagger.layer().pipe( | ||
| 30 | Layer.provideMerge(HttpApiBuilder.middlewareOpenApi()), | ||
| 31 | Layer.provideMerge(HttpApiBuilder.middlewareCors()), | ||
| 32 | Layer.provideMerge(ApiLive), | ||
| 33 | Layer.provideMerge(HttpServer.layerContext), | ||
| 34 | ), | ||
| 35 | { middleware: HttpMiddleware.logger }, | ||
| 36 | ); | ||
| 37 | |||
| 38 | export type ApiHandler = ReturnType<typeof createApiHandler>; | ||
| 39 | |||
| 40 | export { ExecutorServiceLive } from "./services/executor"; | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | import { Context, Effect, Layer } from "effect"; | ||
| 2 | import { createExecutor, makeTestConfig } from "@executor/sdk"; | ||
| 3 | |||
| 4 | import type { Executor } from "@executor/sdk"; | ||
| 5 | |||
| 6 | // --------------------------------------------------------------------------- | ||
| 7 | // Service tag — provides the executor instance to HTTP handlers | ||
| 8 | // --------------------------------------------------------------------------- | ||
| 9 | |||
| 10 | export class ExecutorService extends Context.Tag("ExecutorService")< | ||
| 11 | ExecutorService, | ||
| 12 | Executor | ||
| 13 | >() {} | ||
| 14 | |||
| 15 | // --------------------------------------------------------------------------- | ||
| 16 | // Default layer — creates an in-memory executor | ||
| 17 | // --------------------------------------------------------------------------- | ||
| 18 | |||
| 19 | export const ExecutorServiceLive = Layer.effect( | ||
| 20 | ExecutorService, | ||
| 21 | createExecutor(makeTestConfig()), | ||
| 22 | ); | ||