Skip to content
Merged
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
103 changes: 3 additions & 100 deletions apps/cloud/src/engine/execution-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,14 @@ 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 { cloudEnterpriseManagedRollout } from "../analytics/ema-rollout";
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 @@ -95,98 +90,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.
export 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,
// Withdrawn from the connect picker: no new connection is offered the
// Executor-owned Google app. The entry stays declared on purpose —
// every connection already minted against it keeps refreshing and
// reconnecting through it. Deleting this block, or unsetting the env
// vars, would strand those connections instead.
unlisted: true,
},
]
: []),
...(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 @@ -198,7 +101,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),
// Workers cancel request-scoped I/O once the response settles; the ambient
// `waitUntil` binds to the in-flight invocation (HTTP request or DO call),
// so stale tool-catalog rebuilds that outlive a read still converge.
Expand Down
109 changes: 99 additions & 10 deletions apps/cloud/src/engine/first-party-oauth-clients.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,100 @@
import { env } from "cloudflare:workers";
import { beforeAll, describe, expect, it } from "@effect/vitest";
import { describe, expect, it } from "@effect/vitest";

import { cloudFirstPartyOAuthClients } from "./execution-stack";
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",
});
});
});

// The reviewed consumer scope boundary of the Executor-owned Google app.
//
Expand All @@ -12,13 +105,9 @@ import { cloudFirstPartyOAuthClients } from "./execution-stack";
// scopes an `oauth.start` requests, and that admin scopes are refused).
const GOOGLE_SCOPE = (suffix: string) => `https://www.googleapis.com/auth/${suffix}`;

describe("cloud first-party oauth clients", () => {
beforeAll(() => {
env.FIRST_PARTY_GOOGLE_CLIENT_ID = "test-google-client";
env.FIRST_PARTY_GOOGLE_CLIENT_SECRET = "test-google-secret";
});

const google = () => cloudFirstPartyOAuthClients().find((client) => client.name === "google");
describe("cloud first-party Google app", () => {
const google = () =>
firstPartyOAuthClientsFor(completeEnv).find((client) => client.name === "google");

it("declares the Google app but withholds it from every listing", () => {
const client = google();
Expand Down
Loading
Loading