// ---------------------------------------------------------------------------// OAuth HTTP handlers — thin forwarders over `executor.oauth.*` (v2).//// `createClient` / `cancel` / `probe` are implemented in the SDK;// `start` / `complete` are STUBBED there (milestone 2) and fail at runtime —// the handlers are wired to call them so the surface is complete.// --------------------------------------------------------------------------- import { HttpApiBuilder } from "effect/unstable/httpapi";import { HttpServerResponse } from "effect/unstable/http";import { Effect, Option, Schema } from "effect"; import { runOAuthCallback, type PopupErrorMessage } from "../oauth-popup";import { OAUTH_POPUP_MESSAGE_TYPE, OAuthCompleteError, OAuthProbeError, OAuthSessionNotFoundError, OAuthStartError, OAuthState, type Connection, type ConnectResult,} from "@executor-js/sdk"; import { ExecutorApi } from "../api";import { capture } from "../observability";import { ExecutorService } from "../services"; const OAUTH_POPUP_CHANNEL = OAUTH_POPUP_MESSAGE_TYPE; const decodeOAuthStartError = Schema.decodeUnknownOption(OAuthStartError);const decodeOAuthCompleteError = Schema.decodeUnknownOption(OAuthCompleteError);const decodeOAuthProbeError = Schema.decodeUnknownOption(OAuthProbeError);const decodeOAuthSessionNotFoundError = Schema.decodeUnknownOption(OAuthSessionNotFoundError); const connectionToResponse = (c: Connection) => ({ owner: c.owner, name: c.name, integration: c.integration, template: c.template, provider: c.provider, address: c.address, identityLabel: c.identityLabel ?? null, expiresAt: c.expiresAt ?? null, oauthClient: c.oauthClient ?? null, oauthClientOwner: c.oauthClientOwner ?? null, oauthScope: c.oauthScope ?? null, missingOAuthScopes: c.missingOAuthScopes ?? [],}); const startResultToResponse = (result: ConnectResult) => result.status === "connected" ? { status: "connected" as const, connection: connectionToResponse(result.connection) } : { status: "redirect" as const, authorizationUrl: result.authorizationUrl, state: result.state, }; const toPopupErrorMessage = (error: unknown): PopupErrorMessage => { const completeError = decodeOAuthCompleteError(error); if (Option.isSome(completeError)) return { short: "Could not complete authentication", details: completeError.value.message, }; const startError = decodeOAuthStartError(error); if (Option.isSome(startError)) return { short: "Could not start authentication", details: startError.value.message, }; const probeError = decodeOAuthProbeError(error); if (Option.isSome(probeError)) return { short: "Could not discover authentication endpoint", details: probeError.value.message, }; const sessionNotFound = decodeOAuthSessionNotFoundError(error); if (Option.isSome(sessionNotFound)) return { short: "OAuth session expired or not found", details: `State: ${sessionNotFound.value.state}`, }; return { short: "Authentication failed" };}; export const OAuthHandlers = HttpApiBuilder.group(ExecutorApi, "oauth", (handlers) => handlers .handle("createClient", ({ payload }) => capture( Effect.gen(function* () { const executor = yield* ExecutorService; const client = yield* executor.oauth.createClient({ owner: payload.owner, slug: payload.slug, authorizationUrl: payload.authorizationUrl, tokenUrl: payload.tokenUrl, grant: payload.grant, clientId: payload.clientId, clientSecret: payload.clientSecret, resource: payload.resource ?? null, origin: { kind: "manual", integration: payload.originIntegration ?? null }, }); return { client }; }), ), ) .handle("registerDynamic", ({ payload }) => capture( Effect.gen(function* () { const executor = yield* ExecutorService; const client = yield* executor.oauth.registerDynamicClient({ owner: payload.owner, slug: payload.slug, issuer: payload.issuer ?? null, registrationEndpoint: payload.registrationEndpoint, authorizationUrl: payload.authorizationUrl, tokenUrl: payload.tokenUrl, resource: payload.resource ?? null, scopes: payload.scopes, tokenEndpointAuthMethodsSupported: payload.tokenEndpointAuthMethodsSupported, clientName: payload.clientName, redirectUri: payload.redirectUri, originIntegration: payload.originIntegration ?? null, }); return { client }; }), ), ) .handle("listClients", () => capture( Effect.gen(function* () { const executor = yield* ExecutorService; return yield* executor.oauth.listClients(); }), ), ) .handle("removeClient", ({ params: path, payload }) => capture( Effect.gen(function* () { const executor = yield* ExecutorService; yield* executor.oauth.removeClient(payload.owner, path.slug); return { removed: true }; }), ), ) .handle("start", ({ payload }) => capture( Effect.gen(function* () { const executor = yield* ExecutorService; const result = yield* executor.oauth.start({ client: payload.client, clientOwner: payload.clientOwner, owner: payload.owner, name: payload.name, integration: payload.integration, template: payload.template, identityLabel: payload.identityLabel, newConnection: payload.newConnection, redirectUri: payload.redirectUri, }); return startResultToResponse(result); }), ), ) .handle("complete", ({ payload }) => capture( Effect.gen(function* () { const executor = yield* ExecutorService; const connection = yield* executor.oauth.complete({ state: payload.state, code: payload.code, callbackDomain: payload.callbackDomain ?? null, }); return connectionToResponse(connection); }), ), ) .handle("cancel", ({ payload }) => capture( Effect.gen(function* () { const executor = yield* ExecutorService; yield* executor.oauth.cancel(payload.state); return { cancelled: true }; }), ), ) .handle("probe", ({ payload }) => capture( Effect.gen(function* () { const executor = yield* ExecutorService; return yield* executor.oauth.probe({ url: payload.url }); }), ), ) .handle("callback", ({ query: urlParams }) => // The callback always renders HTML, even on failure — the popup shows the // error + messages it back to the opener. capture( Effect.gen(function* () { const executor = yield* ExecutorService; const html = yield* runOAuthCallback({ complete: ({ state, code, callbackDomain }) => executor.oauth .complete({ // `runOAuthCallback`'s `state` is a raw string from the URL; // the SDK speaks the branded `OAuthState` (nominal brand). state: OAuthState.make(state), code: code ?? "", callbackDomain, }) .pipe( Effect.tapError((cause: unknown) => Effect.logError("OAuth callback completion failed", cause), ), ), urlParams, toErrorMessage: toPopupErrorMessage, channelName: OAUTH_POPUP_CHANNEL, }); return HttpServerResponse.html(html); }), ), ),);
// ---------------------------------------------------------------------------
// OAuth HTTP handlers — thin forwarders over `executor.oauth.*` (v2).
//
// `createClient` / `cancel` / `probe` are implemented in the SDK;
// `start` / `complete` are STUBBED there (milestone 2) and fail at runtime —
// the handlers are wired to call them so the surface is complete.
// ---------------------------------------------------------------------------
import { HttpApiBuilder } from "effect/unstable/httpapi";
import { HttpServerResponse } from "effect/unstable/http";
import { Effect, Option, Schema } from "effect";
import { runOAuthCallback, type PopupErrorMessage } from "../oauth-popup";
import {
OAUTH_POPUP_MESSAGE_TYPE,
OAuthCompleteError,
OAuthProbeError,
OAuthSessionNotFoundError,
OAuthStartError,
OAuthState,
type Connection,
type ConnectResult,
} from "@executor-js/sdk";
import { ExecutorApi } from "../api";
import { capture } from "../observability";
import { ExecutorService } from "../services";
const OAUTH_POPUP_CHANNEL = OAUTH_POPUP_MESSAGE_TYPE;
const decodeOAuthStartError = Schema.decodeUnknownOption(OAuthStartError);
const decodeOAuthCompleteError = Schema.decodeUnknownOption(OAuthCompleteError);
const decodeOAuthProbeError = Schema.decodeUnknownOption(OAuthProbeError);
const decodeOAuthSessionNotFoundError = Schema.decodeUnknownOption(OAuthSessionNotFoundError);
const connectionToResponse = (c: Connection) => ({
owner: c.owner,
name: c.name,
integration: c.integration,
template: c.template,
provider: c.provider,
address: c.address,
identityLabel: c.identityLabel ?? null,
expiresAt: c.expiresAt ?? null,
oauthClient: c.oauthClient ?? null,
oauthClientOwner: c.oauthClientOwner ?? null,
oauthScope: c.oauthScope ?? null,
missingOAuthScopes: c.missingOAuthScopes ?? [],
});
const startResultToResponse = (result: ConnectResult) =>
result.status === "connected"
? { status: "connected" as const, connection: connectionToResponse(result.connection) }
: {
status: "redirect" as const,
authorizationUrl: result.authorizationUrl,
state: result.state,
};
const toPopupErrorMessage = (error: unknown): PopupErrorMessage => {
const completeError = decodeOAuthCompleteError(error);
if (Option.isSome(completeError))
return {
short: "Could not complete authentication",
details: completeError.value.message,
};
const startError = decodeOAuthStartError(error);
if (Option.isSome(startError))
return {
short: "Could not start authentication",
details: startError.value.message,
};
const probeError = decodeOAuthProbeError(error);
if (Option.isSome(probeError))
return {
short: "Could not discover authentication endpoint",
details: probeError.value.message,
};
const sessionNotFound = decodeOAuthSessionNotFoundError(error);
if (Option.isSome(sessionNotFound))
return {
short: "OAuth session expired or not found",
details: `State: ${sessionNotFound.value.state}`,
};
return { short: "Authentication failed" };
};
export const OAuthHandlers = HttpApiBuilder.group(ExecutorApi, "oauth", (handlers) =>
handlers
.handle("createClient", ({ payload }) =>
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
const client = yield* executor.oauth.createClient({
owner: payload.owner,
slug: payload.slug,
authorizationUrl: payload.authorizationUrl,
tokenUrl: payload.tokenUrl,
grant: payload.grant,
clientId: payload.clientId,
clientSecret: payload.clientSecret,
resource: payload.resource ?? null,
origin: { kind: "manual", integration: payload.originIntegration ?? null },
});
return { client };
}),
),
)
.handle("registerDynamic", ({ payload }) =>
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
const client = yield* executor.oauth.registerDynamicClient({
owner: payload.owner,
slug: payload.slug,
issuer: payload.issuer ?? null,
registrationEndpoint: payload.registrationEndpoint,
authorizationUrl: payload.authorizationUrl,
tokenUrl: payload.tokenUrl,
resource: payload.resource ?? null,
scopes: payload.scopes,
tokenEndpointAuthMethodsSupported: payload.tokenEndpointAuthMethodsSupported,
clientName: payload.clientName,
redirectUri: payload.redirectUri,
originIntegration: payload.originIntegration ?? null,
});
return { client };
}),
),
)
.handle("listClients", () =>
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
return yield* executor.oauth.listClients();
}),
),
)
.handle("removeClient", ({ params: path, payload }) =>
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
yield* executor.oauth.removeClient(payload.owner, path.slug);
return { removed: true };
}),
),
)
.handle("start", ({ payload }) =>
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
const result = yield* executor.oauth.start({
client: payload.client,
clientOwner: payload.clientOwner,
owner: payload.owner,
name: payload.name,
integration: payload.integration,
template: payload.template,
identityLabel: payload.identityLabel,
newConnection: payload.newConnection,
redirectUri: payload.redirectUri,
});
return startResultToResponse(result);
}),
),
)
.handle("complete", ({ payload }) =>
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
const connection = yield* executor.oauth.complete({
state: payload.state,
code: payload.code,
callbackDomain: payload.callbackDomain ?? null,
});
return connectionToResponse(connection);
}),
),
)
.handle("cancel", ({ payload }) =>
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
yield* executor.oauth.cancel(payload.state);
return { cancelled: true };
}),
),
)
.handle("probe", ({ payload }) =>
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
return yield* executor.oauth.probe({ url: payload.url });
}),
),
)
.handle("callback", ({ query: urlParams }) =>
// The callback always renders HTML, even on failure — the popup shows the
// error + messages it back to the opener.
capture(
Effect.gen(function* () {
const executor = yield* ExecutorService;
const html = yield* runOAuthCallback({
complete: ({ state, code, callbackDomain }) =>
executor.oauth
.complete({
// `runOAuthCallback`'s `state` is a raw string from the URL;
// the SDK speaks the branded `OAuthState` (nominal brand).
state: OAuthState.make(state),
code: code ?? "",
callbackDomain,
})
.pipe(
Effect.tapError((cause: unknown) =>
Effect.logError("OAuth callback completion failed", cause),
),
),
urlParams,
toErrorMessage: toPopupErrorMessage,
channelName: OAUTH_POPUP_CHANNEL,
});
return HttpServerResponse.html(html);
}),
),
),
);