Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/cloud/src/auth/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ const AuthOrganizationsResponse = Schema.Struct({
activeOrganizationId: Schema.NullOr(Schema.String),
});

const SwitchOrganizationBody = Schema.Struct({
export const SwitchOrganizationBody = Schema.Struct({
organizationId: Schema.String,
});

const CreateOrganizationBody = Schema.Struct({
export const CreateOrganizationBody = Schema.Struct({
name: Schema.String,
});

Expand Down Expand Up @@ -69,7 +69,7 @@ const PendingInvitationsResponse = Schema.Struct({
invitations: Schema.Array(PendingInvitation),
});

const AcceptInvitationBody = Schema.Struct({
export const AcceptInvitationBody = Schema.Struct({
invitationId: Schema.String,
});

Expand All @@ -83,7 +83,7 @@ const McpSessionExecutionParams = {
executionId: Schema.String,
};

const ResumeMcpExecutionBody = Schema.Struct({
export const ResumeMcpExecutionBody = Schema.Struct({
action: Schema.Literals(["accept", "decline", "cancel"]),
content: Schema.optional(Schema.Unknown),
});
Expand Down
85 changes: 57 additions & 28 deletions apps/cloud/src/auth/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { Duration, Effect, Predicate } from "effect";

import {
AUTH_PATHS,
type AcceptInvitationBody,
CloudAuthApi,
CloudAuthPublicApi,
type CreateOrganizationBody,
McpExecutionNotFoundError,
McpSessionForbiddenError,
type ResumeMcpExecutionBody,
type SwitchOrganizationBody,
} from "./api";
import { NoOrganization } from "@executor-js/api/server";
import { SessionContext, SessionCookies } from "./middleware";
Expand Down Expand Up @@ -233,8 +237,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
"cloudAuth",
(handlers) =>
handlers
.handle("me", () =>
Effect.gen(function* () {
.handle(
"me",
Effect.fn("cloudAuth.me")(function* () {
const session = yield* SessionContext;
const org = session.organizationId
? yield* authorizeOrganization(session.accountId, session.organizationId)
Expand All @@ -256,8 +261,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
deleteResponseCookie(HttpServerResponse.redirect("/", { status: 302 }), "wos-session"),
),
)
.handle("organizations", () =>
Effect.gen(function* () {
.handle(
"organizations",
Effect.fn("cloudAuth.organizations")(function* () {
const workos = yield* WorkOSClient;
const session = yield* SessionContext;

Expand All @@ -278,28 +284,34 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
};
}),
)
.handle("switchOrganization", ({ payload }) =>
Effect.gen(function* () {
.handle(
"switchOrganization",
Effect.fn("cloudAuth.switchOrganization")(function* (ctx: {
payload: typeof SwitchOrganizationBody.Type;
}) {
const workos = yield* WorkOSClient;
const session = yield* SessionContext;

const refreshed = yield* workos.refreshSession(
session.sealedSession,
payload.organizationId,
ctx.payload.organizationId,
);
if (refreshed) {
(yield* SessionCookies).set("wos-session", refreshed, RESPONSE_COOKIE_OPTIONS);
}
}),
)
.handle("createOrganization", ({ payload }) =>
Effect.gen(function* () {
.handle(
"createOrganization",
Effect.fn("cloudAuth.createOrganization")(function* (ctx: {
payload: typeof CreateOrganizationBody.Type;
}) {
const workos = yield* WorkOSClient;
const users = yield* UserStoreService;
const session = yield* SessionContext;
const autumn = yield* AutumnService;

const name = payload.name.trim();
const name = ctx.payload.name.trim();
const memberships = yield* workos.listUserMemberships(session.accountId);
const activeMemberships = memberships.data.filter(
(membership) => membership.status === "active",
Expand Down Expand Up @@ -363,8 +375,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
return { id: org.id, name: org.name };
}),
)
.handle("pendingInvitations", () =>
Effect.gen(function* () {
.handle(
"pendingInvitations",
Effect.fn("cloudAuth.pendingInvitations")(function* () {
const workos = yield* WorkOSClient;
const session = yield* SessionContext;

Expand Down Expand Up @@ -412,19 +425,22 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
};
}),
)
.handle("acceptInvitation", ({ payload }) =>
Effect.gen(function* () {
.handle(
"acceptInvitation",
Effect.fn("cloudAuth.acceptInvitation")(function* (ctx: {
payload: typeof AcceptInvitationBody.Type;
}) {
const workos = yield* WorkOSClient;
const users = yield* UserStoreService;
const session = yield* SessionContext;

const invitation = yield* workos.acceptInvitation(payload.invitationId);
const invitation = yield* workos.acceptInvitation(ctx.payload.invitationId);

// Defensive: invitations created without an org shouldn't reach
// this UI, but the SDK type allows null so guard anyway.
if (!invitation.organizationId) {
yield* Effect.logWarning("acceptInvitation: invitation has no organizationId", {
invitationId: payload.invitationId,
invitationId: ctx.payload.invitationId,
});
return yield* new WorkOSError();
}
Expand Down Expand Up @@ -456,20 +472,26 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
return { id: org.id, name: org.name };
}),
)
.handle("getMcpPaused", ({ params }) =>
Effect.gen(function* () {
.handle(
"getMcpPaused",
Effect.fn("cloudAuth.getMcpPaused")(function* (ctx: {
params: { mcpSessionId: string; executionId: string };
}) {
const owner = yield* requireSessionOrganizationId;
const stub = yield* requireMcpSessionStub(params.mcpSessionId, params.executionId);
const stub = yield* requireMcpSessionStub(
ctx.params.mcpSessionId,
ctx.params.executionId,
);
const result = yield* Effect.promise(
() =>
stub.getPausedExecutionForApproval(params.executionId, {
stub.getPausedExecutionForApproval(ctx.params.executionId, {
accountId: owner.accountId,
organizationId: owner.organizationId,
}) as Promise<McpSessionApprovalResult>,
);

if (result.status !== "ok") {
return yield* failMcpApprovalResult(result, params);
return yield* failMcpApprovalResult(result, ctx.params);
}

return {
Expand All @@ -478,27 +500,34 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
};
}),
)
.handle("resumeMcpExecution", ({ params, payload }) =>
Effect.gen(function* () {
.handle(
"resumeMcpExecution",
Effect.fn("cloudAuth.resumeMcpExecution")(function* (ctx: {
params: { mcpSessionId: string; executionId: string };
payload: typeof ResumeMcpExecutionBody.Type;
}) {
const owner = yield* requireSessionOrganizationId;
const stub = yield* requireMcpSessionStub(params.mcpSessionId, params.executionId);
const stub = yield* requireMcpSessionStub(
ctx.params.mcpSessionId,
ctx.params.executionId,
);
const result = yield* Effect.promise(
() =>
stub.resumeExecutionForApproval(
params.executionId,
ctx.params.executionId,
{
accountId: owner.accountId,
organizationId: owner.organizationId,
},
{
action: payload.action,
content: payload.content as Record<string, unknown> | undefined,
action: ctx.payload.action,
content: ctx.payload.content as Record<string, unknown> | undefined,
},
) as Promise<McpSessionResumeApprovalResult>,
);

if (result.status !== "ok") {
return yield* failMcpApprovalResult(result, params);
return yield* failMcpApprovalResult(result, ctx.params);
}

if (result.executionStatus === "paused") {
Expand Down
19 changes: 11 additions & 8 deletions apps/cloud/src/org/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ const assertDomainInSessionOrg = (domainId: string) =>

export const OrgHandlers = HttpApiBuilder.group(OrgHttpApi, "org", (handlers) =>
handlers
.handle("listDomains", () =>
Effect.gen(function* () {
.handle(
"listDomains",
Effect.fn("org.listDomains")(function* () {
const auth = yield* AuthContext;
const workos = yield* WorkOSClient;
const org = yield* workos.getOrganization(auth.organizationId);
Expand All @@ -70,8 +71,9 @@ export const OrgHandlers = HttpApiBuilder.group(OrgHttpApi, "org", (handlers) =>
return { domains };
}),
)
.handle("getDomainVerificationLink", () =>
Effect.gen(function* () {
.handle(
"getDomainVerificationLink",
Effect.fn("org.getDomainVerificationLink")(function* () {
yield* requireAdmin;
const auth = yield* AuthContext;

Expand All @@ -97,12 +99,13 @@ export const OrgHandlers = HttpApiBuilder.group(OrgHttpApi, "org", (handlers) =>
return { link };
}),
)
.handle("deleteDomain", ({ params }) =>
Effect.gen(function* () {
.handle(
"deleteDomain",
Effect.fn("org.deleteDomain")(function* (ctx: { params: { domainId: string } }) {
yield* requireAdmin;
yield* assertDomainInSessionOrg(params.domainId);
yield* assertDomainInSessionOrg(ctx.params.domainId);
const workos = yield* WorkOSClient;
yield* workos.deleteOrganizationDomain(params.domainId);
yield* workos.deleteOrganizationDomain(ctx.params.domainId);
return { success: true };
}),
),
Expand Down
24 changes: 14 additions & 10 deletions apps/host-selfhost/src/admin/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AdminForbidden,
AdminHttpApi,
AdminUnauthorized,
type CreateInviteBody,
type InviteCode as InviteCodeSchema,
} from "./api";
import { BetterAuth, type BetterAuthHandle } from "../auth/better-auth";
Expand Down Expand Up @@ -62,8 +63,9 @@ const toWire = (row: InviteCodeRow): typeof InviteCodeSchema.Type => ({

export const AdminHandlers = HttpApiBuilder.group(AdminHttpApi, "admin", (handlers) =>
handlers
.handle("listInvites", () =>
Effect.gen(function* () {
.handle(
"listInvites",
Effect.fn("admin.listInvites")(function* () {
yield* requireAdmin(yield* requestHeaders);
const { client } = yield* SelfHostDb;
const rows = yield* Effect.tryPromise({
Expand All @@ -73,32 +75,34 @@ export const AdminHandlers = HttpApiBuilder.group(AdminHttpApi, "admin", (handle
return { invites: rows.map(toWire) };
}),
)
.handle("createInvite", ({ payload }) =>
Effect.gen(function* () {
.handle(
"createInvite",
Effect.fn("admin.createInvite")(function* (ctx: { payload: typeof CreateInviteBody.Type }) {
const member = yield* requireAdmin(yield* requestHeaders);
const { client } = yield* SelfHostDb;
const days = payload.expiresInDays ?? null;
const days = ctx.payload.expiresInDays ?? null;
const expiresAt =
days && days > 0 ? new Date(Date.now() + days * 86_400_000).toISOString() : null;
const row = yield* Effect.tryPromise({
try: () =>
createInviteCode(client, {
createdBy: member.userId,
role: narrowRole(payload.role),
label: payload.label?.trim() ? payload.label.trim() : null,
role: narrowRole(ctx.payload.role),
label: ctx.payload.label?.trim() ? ctx.payload.label.trim() : null,
expiresAt,
}),
catch: () => new AdminError({ message: "Failed to create invite" }),
});
return toWire(row);
}),
)
.handle("revokeInvite", ({ params }) =>
Effect.gen(function* () {
.handle(
"revokeInvite",
Effect.fn("admin.revokeInvite")(function* (ctx: { params: { inviteId: string } }) {
yield* requireAdmin(yield* requestHeaders);
const { client } = yield* SelfHostDb;
yield* Effect.tryPromise({
try: () => revokeInviteCode(client, params.inviteId),
try: () => revokeInviteCode(client, ctx.params.inviteId),
catch: () => new AdminError({ message: "Failed to revoke invite" }),
});
return { success: true };
Expand Down
10 changes: 6 additions & 4 deletions apps/host-selfhost/src/system/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db";

export const SystemHandlers = HttpApiBuilder.group(SystemHttpApi, "system", (handlers) =>
handlers
.handle("health", () =>
Effect.gen(function* () {
.handle(
"health",
Effect.fn("system.health")(function* () {
const { client } = yield* SelfHostDb;
const status = yield* Effect.tryPromise({
try: () => client.execute("SELECT 1"),
Expand All @@ -27,8 +28,9 @@ export const SystemHandlers = HttpApiBuilder.group(SystemHttpApi, "system", (han
return { status };
}),
)
.handle("setupStatus", () =>
Effect.gen(function* () {
.handle(
"setupStatus",
Effect.fn("system.setupStatus")(function* () {
const { auth, organizationId } = yield* BetterAuth;
// Count via Better Auth's adapter (see countOrgMembers) so this read is
// consistent with how memberships are written.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/api/src/executions/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { InternalError } from "@executor-js/sdk/shared";
// Schemas
// ---------------------------------------------------------------------------

const ExecuteRequest = Schema.Struct({
export const ExecuteRequest = Schema.Struct({
code: Schema.String,
});

Expand All @@ -26,7 +26,7 @@ const PausedResult = Schema.Struct({

const ExecuteResponse = Schema.Union([CompletedResult, PausedResult]);

const ResumeRequest = Schema.Struct({
export const ResumeRequest = Schema.Struct({
action: Schema.Literals(["accept", "decline", "cancel"]),
content: Schema.optional(Schema.Unknown),
});
Expand Down
Loading
Loading