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
97 changes: 3 additions & 94 deletions apps/cloud/src/engine/execution-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,13 @@ import {
PluginsProvider,
collectTables,
} from "@executor-js/api/server";
import { googleCatalogOAuthScopesForPreset } from "@executor-js/plugin-openapi/providers/google";
import { slackMcpUserScopes } from "@executor-js/react/lib/slack-mcp-oauth";
import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker";
import {
IntegrationSlug,
type AnyPlugin,
type FirstPartyOAuthClientConfig,
} from "@executor-js/sdk";
import { type AnyPlugin } from "@executor-js/sdk";

import executorConfig from "../../executor.config";
import { DbService } from "../db/db";
import { cloudDbProviderLayer } from "../db/fuma";
import { firstPartyOAuthClientsFor } from "./first-party-oauth-clients";

export { makeExecutionStack } from "@executor-js/api/server";

Expand Down Expand Up @@ -94,92 +89,6 @@ export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed(
*/
export const CLOUD_MOUNT_PREFIX = "/api" as const;

// Consumer Google launch boundary. Keep this list aligned with the scopes
// submitted for the Executor-owned production app: ordinary Workspace services
// plus Photos, Meet, and Search Console. Admin, Classroom, YouTube, Apps Script,
// BigQuery, and Cloud Resource Manager have materially different audiences or
// provider requirements and remain BYO OAuth. The same scope source builds each
// catalog auth template, preventing picker/start drift.
const GOOGLE_FIRST_PARTY_PRESET_IDS = [
"google-calendar",
"google-meet",
"google-gmail",
"google-sheets",
"google-drive",
"google-docs",
"google-slides",
"google-forms",
"google-tasks",
"google-people",
"google-photos-library",
"google-photos-picker",
"google-search-console",
] as const;

const GOOGLE_FIRST_PARTY_ALLOWED_SCOPES: readonly string[] = [
...new Set([
...GOOGLE_FIRST_PARTY_PRESET_IDS.flatMap(googleCatalogOAuthScopesForPreset),
// Connections created before the full-Gmail review retain this declared
// scope on reconnect. New Gmail presets request `mail.google.com`.
"https://www.googleapis.com/auth/gmail.modify",
]),
];

// Executor-owned provider apps, enabled per provider by setting BOTH env vars
// (id + secret). Each provider-side registration must list
// `${VITE_PUBLIC_SITE_URL}/api/oauth/callback` as its callback; the org slug
// travels inside OAuth `state`, so the single static callback serves every org.
//
// The endpoint URLs default to the real provider; the `_AUTHORIZE_URL` /
// `_TOKEN_URL` overrides exist so tests and dev instances can point the app at
// an emulated provider (`@executor-js/emulate`) and run the complete flow.
// Production leaves them unset.
const cloudFirstPartyOAuthClients = (): readonly FirstPartyOAuthClientConfig[] => [
...(env.FIRST_PARTY_GITHUB_CLIENT_ID && env.FIRST_PARTY_GITHUB_CLIENT_SECRET
? [
{
name: "github",
authorizationUrl:
env.FIRST_PARTY_GITHUB_AUTHORIZE_URL ?? "https://github.com/login/oauth/authorize",
tokenUrl:
env.FIRST_PARTY_GITHUB_TOKEN_URL ?? "https://github.com/login/oauth/access_token",
clientId: env.FIRST_PARTY_GITHUB_CLIENT_ID,
clientSecret: env.FIRST_PARTY_GITHUB_CLIENT_SECRET,
integrations: [IntegrationSlug.make("github_rest")],
// GitHub App user access tokens do not use classic OAuth scopes;
// their capabilities come from the app's registered permissions.
authorizationScopes: [],
},
]
: []),
...(env.FIRST_PARTY_GOOGLE_CLIENT_ID && env.FIRST_PARTY_GOOGLE_CLIENT_SECRET
? [
{
name: "google",
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
clientId: env.FIRST_PARTY_GOOGLE_CLIENT_ID,
clientSecret: env.FIRST_PARTY_GOOGLE_CLIENT_SECRET,
allowedScopes: GOOGLE_FIRST_PARTY_ALLOWED_SCOPES,
},
]
: []),
...(env.FIRST_PARTY_SLACK_CLIENT_ID && env.FIRST_PARTY_SLACK_CLIENT_SECRET
? [
{
name: "slack",
authorizationUrl: "https://slack.com/oauth/v2_user/authorize",
tokenUrl: "https://slack.com/api/oauth.v2.user.access",
resource: "https://mcp.slack.com",
clientId: env.FIRST_PARTY_SLACK_CLIENT_ID,
clientSecret: env.FIRST_PARTY_SLACK_CLIENT_SECRET,
integrations: [IntegrationSlug.make("slack")],
allowedScopes: slackMcpUserScopes,
},
]
: []),
];

export const CloudHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig, () => ({
// SSRF / private-network egress guard. Config-driven, NOT a test flag:
// production leaves `ALLOW_LOCAL_NETWORK` unset so the guard stays ON (`false`);
Expand All @@ -191,7 +100,7 @@ export const CloudHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig, (
// WorkOS Vault is cloud's credential storage implementation detail, not a
// user-selectable provider surface.
exposeCredentialProviders: false,
firstPartyOAuthClients: cloudFirstPartyOAuthClients(),
firstPartyOAuthClients: firstPartyOAuthClientsFor(env),
}));

export const CloudCodeExecutorProvider: Layer.Layer<CodeExecutorProvider> = Layer.sync(
Expand Down
97 changes: 97 additions & 0 deletions apps/cloud/src/engine/first-party-oauth-clients.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, expect, it } from "@effect/vitest";

import {
firstPartyOAuthClientsFor,
type FirstPartyOAuthClientEnv,
} from "./first-party-oauth-clients";

const completeEnv: FirstPartyOAuthClientEnv = {
FIRST_PARTY_AIRTABLE_CLIENT_ID: "airtable-id",
FIRST_PARTY_AIRTABLE_CLIENT_SECRET: "airtable-secret",
FIRST_PARTY_ATLASSIAN_CLIENT_ID: "atlassian-id",
FIRST_PARTY_ATLASSIAN_CLIENT_SECRET: "atlassian-secret",
FIRST_PARTY_BOX_CLIENT_ID: "box-id",
FIRST_PARTY_BOX_CLIENT_SECRET: "box-secret",
FIRST_PARTY_CLICKUP_CLIENT_ID: "clickup-id",
FIRST_PARTY_CLICKUP_CLIENT_SECRET: "clickup-secret",
FIRST_PARTY_FIGMA_CLIENT_ID: "figma-id",
FIRST_PARTY_FIGMA_CLIENT_SECRET: "figma-secret",
FIRST_PARTY_GITHUB_CLIENT_ID: "github-id",
FIRST_PARTY_GITHUB_CLIENT_SECRET: "github-secret",
FIRST_PARTY_GITLAB_CLIENT_ID: "gitlab-id",
FIRST_PARTY_GITLAB_CLIENT_SECRET: "gitlab-secret",
FIRST_PARTY_GOOGLE_CLIENT_ID: "google-id",
FIRST_PARTY_GOOGLE_CLIENT_SECRET: "google-secret",
FIRST_PARTY_HUBSPOT_CLIENT_ID: "hubspot-id",
FIRST_PARTY_HUBSPOT_CLIENT_SECRET: "hubspot-secret",
FIRST_PARTY_LINEAR_CLIENT_ID: "linear-id",
FIRST_PARTY_LINEAR_CLIENT_SECRET: "linear-secret",
FIRST_PARTY_MICROSOFT_CLIENT_ID: "microsoft-id",
FIRST_PARTY_MICROSOFT_CLIENT_SECRET: "microsoft-secret",
FIRST_PARTY_NOTION_CLIENT_ID: "notion-id",
FIRST_PARTY_NOTION_CLIENT_SECRET: "notion-secret",
FIRST_PARTY_SLACK_CLIENT_ID: "slack-id",
FIRST_PARTY_SLACK_CLIENT_SECRET: "slack-secret",
};

describe("cloud first-party OAuth clients", () => {
it("enables every registered OAuth 2 provider from complete secret pairs", () => {
const clients = firstPartyOAuthClientsFor(completeEnv);

expect(clients.map((client) => client.name)).toEqual([
"airtable",
"atlassian",
"box",
"clickup",
"figma",
"github",
"gitlab",
"google",
"hubspot",
"linear",
"microsoft",
"notion",
"slack",
]);
});

it("fails closed when either half of a provider secret pair is absent", () => {
expect(firstPartyOAuthClientsFor({ FIRST_PARTY_AIRTABLE_CLIENT_ID: "id" })).toEqual([]);
expect(firstPartyOAuthClientsFor({ FIRST_PARTY_AIRTABLE_CLIENT_SECRET: "secret" })).toEqual([]);
});

it("carries provider-specific authorization and token contracts", () => {
const byName = new Map(
firstPartyOAuthClientsFor(completeEnv).map((client) => [client.name, client]),
);

expect(byName.get("airtable")).toMatchObject({
tokenEndpointAuthMethod: "basic",
});
expect(byName.get("atlassian")).toMatchObject({
tokenRequestFormat: "json",
authorizationExtraParams: { audience: "api.atlassian.com", prompt: "consent" },
});
expect(byName.get("figma")).toMatchObject({
tokenEndpointAuthMethod: "basic",
allowedScopes: expect.arrayContaining(["folder_metadata:read", "folders:read"]),
});
expect(byName.get("hubspot")).toMatchObject({
tokenUrl: "https://api.hubapi.com/oauth/v3/token",
authorizationExtraParams: {
optional_scope: "content crm.objects.custom.read crm.schemas.custom.read",
},
});
expect(byName.get("linear")).toMatchObject({ authorizationScopeSeparator: "," });
expect(byName.get("microsoft")).toMatchObject({
additionalAuthorizationScopes: ["offline_access"],
allowedScopes: expect.arrayContaining(["Mail.ReadWrite", "Files.ReadWrite.All"]),
});
expect(byName.get("notion")).toMatchObject({
authorizationScopes: [],
authorizationExtraParams: { owner: "user" },
tokenEndpointAuthMethod: "basic",
tokenRequestFormat: "json",
});
});
});
Loading
Loading