Skip to content

Commit efa9d4a

Browse files
committed
Add registered first-party OAuth providers
1 parent 73cbba9 commit efa9d4a

11 files changed

Lines changed: 708 additions & 114 deletions

File tree

‎apps/cloud/src/engine/execution-stack.ts‎

Lines changed: 3 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,13 @@ import {
4242
PluginsProvider,
4343
collectTables,
4444
} from "@executor-js/api/server";
45-
import { googleCatalogOAuthScopesForPreset } from "@executor-js/plugin-openapi/providers/google";
46-
import { slackMcpUserScopes } from "@executor-js/react/lib/slack-mcp-oauth";
4745
import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker";
48-
import {
49-
IntegrationSlug,
50-
type AnyPlugin,
51-
type FirstPartyOAuthClientConfig,
52-
} from "@executor-js/sdk";
46+
import { type AnyPlugin } from "@executor-js/sdk";
5347

5448
import executorConfig from "../../executor.config";
5549
import { DbService } from "../db/db";
5650
import { cloudDbProviderLayer } from "../db/fuma";
51+
import { firstPartyOAuthClientsFor } from "./first-party-oauth-clients";
5752

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

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

97-
// Consumer Google launch boundary. Keep this list aligned with the scopes
98-
// submitted for the Executor-owned production app: ordinary Workspace services
99-
// plus Photos, Meet, and Search Console. Admin, Classroom, YouTube, Apps Script,
100-
// BigQuery, and Cloud Resource Manager have materially different audiences or
101-
// provider requirements and remain BYO OAuth. The same scope source builds each
102-
// catalog auth template, preventing picker/start drift.
103-
const GOOGLE_FIRST_PARTY_PRESET_IDS = [
104-
"google-calendar",
105-
"google-meet",
106-
"google-gmail",
107-
"google-sheets",
108-
"google-drive",
109-
"google-docs",
110-
"google-slides",
111-
"google-forms",
112-
"google-tasks",
113-
"google-people",
114-
"google-photos-library",
115-
"google-photos-picker",
116-
"google-search-console",
117-
] as const;
118-
119-
const GOOGLE_FIRST_PARTY_ALLOWED_SCOPES: readonly string[] = [
120-
...new Set([
121-
...GOOGLE_FIRST_PARTY_PRESET_IDS.flatMap(googleCatalogOAuthScopesForPreset),
122-
// Connections created before the full-Gmail review retain this declared
123-
// scope on reconnect. New Gmail presets request `mail.google.com`.
124-
"https://www.googleapis.com/auth/gmail.modify",
125-
]),
126-
];
127-
128-
// Executor-owned provider apps, enabled per provider by setting BOTH env vars
129-
// (id + secret). Each provider-side registration must list
130-
// `${VITE_PUBLIC_SITE_URL}/api/oauth/callback` as its callback; the org slug
131-
// travels inside OAuth `state`, so the single static callback serves every org.
132-
//
133-
// The endpoint URLs default to the real provider; the `_AUTHORIZE_URL` /
134-
// `_TOKEN_URL` overrides exist so tests and dev instances can point the app at
135-
// an emulated provider (`@executor-js/emulate`) and run the complete flow.
136-
// Production leaves them unset.
137-
const cloudFirstPartyOAuthClients = (): readonly FirstPartyOAuthClientConfig[] => [
138-
...(env.FIRST_PARTY_GITHUB_CLIENT_ID && env.FIRST_PARTY_GITHUB_CLIENT_SECRET
139-
? [
140-
{
141-
name: "github",
142-
authorizationUrl:
143-
env.FIRST_PARTY_GITHUB_AUTHORIZE_URL ?? "https://github.com/login/oauth/authorize",
144-
tokenUrl:
145-
env.FIRST_PARTY_GITHUB_TOKEN_URL ?? "https://github.com/login/oauth/access_token",
146-
clientId: env.FIRST_PARTY_GITHUB_CLIENT_ID,
147-
clientSecret: env.FIRST_PARTY_GITHUB_CLIENT_SECRET,
148-
integrations: [IntegrationSlug.make("github_rest")],
149-
// GitHub App user access tokens do not use classic OAuth scopes;
150-
// their capabilities come from the app's registered permissions.
151-
authorizationScopes: [],
152-
},
153-
]
154-
: []),
155-
...(env.FIRST_PARTY_GOOGLE_CLIENT_ID && env.FIRST_PARTY_GOOGLE_CLIENT_SECRET
156-
? [
157-
{
158-
name: "google",
159-
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
160-
tokenUrl: "https://oauth2.googleapis.com/token",
161-
clientId: env.FIRST_PARTY_GOOGLE_CLIENT_ID,
162-
clientSecret: env.FIRST_PARTY_GOOGLE_CLIENT_SECRET,
163-
allowedScopes: GOOGLE_FIRST_PARTY_ALLOWED_SCOPES,
164-
},
165-
]
166-
: []),
167-
...(env.FIRST_PARTY_SLACK_CLIENT_ID && env.FIRST_PARTY_SLACK_CLIENT_SECRET
168-
? [
169-
{
170-
name: "slack",
171-
authorizationUrl: "https://slack.com/oauth/v2_user/authorize",
172-
tokenUrl: "https://slack.com/api/oauth.v2.user.access",
173-
resource: "https://mcp.slack.com",
174-
clientId: env.FIRST_PARTY_SLACK_CLIENT_ID,
175-
clientSecret: env.FIRST_PARTY_SLACK_CLIENT_SECRET,
176-
integrations: [IntegrationSlug.make("slack")],
177-
allowedScopes: slackMcpUserScopes,
178-
},
179-
]
180-
: []),
181-
];
182-
18392
export const CloudHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig, () => ({
18493
// SSRF / private-network egress guard. Config-driven, NOT a test flag:
18594
// production leaves `ALLOW_LOCAL_NETWORK` unset so the guard stays ON (`false`);
@@ -191,7 +100,7 @@ export const CloudHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig, (
191100
// WorkOS Vault is cloud's credential storage implementation detail, not a
192101
// user-selectable provider surface.
193102
exposeCredentialProviders: false,
194-
firstPartyOAuthClients: cloudFirstPartyOAuthClients(),
103+
firstPartyOAuthClients: firstPartyOAuthClientsFor(env),
195104
}));
196105

197106
export const CloudCodeExecutorProvider: Layer.Layer<CodeExecutorProvider> = Layer.sync(
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { describe, expect, it } from "@effect/vitest";
2+
3+
import {
4+
firstPartyOAuthClientsFor,
5+
type FirstPartyOAuthClientEnv,
6+
} from "./first-party-oauth-clients";
7+
8+
const completeEnv: FirstPartyOAuthClientEnv = {
9+
FIRST_PARTY_AIRTABLE_CLIENT_ID: "airtable-id",
10+
FIRST_PARTY_AIRTABLE_CLIENT_SECRET: "airtable-secret",
11+
FIRST_PARTY_ATLASSIAN_CLIENT_ID: "atlassian-id",
12+
FIRST_PARTY_ATLASSIAN_CLIENT_SECRET: "atlassian-secret",
13+
FIRST_PARTY_BOX_CLIENT_ID: "box-id",
14+
FIRST_PARTY_BOX_CLIENT_SECRET: "box-secret",
15+
FIRST_PARTY_CLICKUP_CLIENT_ID: "clickup-id",
16+
FIRST_PARTY_CLICKUP_CLIENT_SECRET: "clickup-secret",
17+
FIRST_PARTY_FIGMA_CLIENT_ID: "figma-id",
18+
FIRST_PARTY_FIGMA_CLIENT_SECRET: "figma-secret",
19+
FIRST_PARTY_GITHUB_CLIENT_ID: "github-id",
20+
FIRST_PARTY_GITHUB_CLIENT_SECRET: "github-secret",
21+
FIRST_PARTY_GITLAB_CLIENT_ID: "gitlab-id",
22+
FIRST_PARTY_GITLAB_CLIENT_SECRET: "gitlab-secret",
23+
FIRST_PARTY_GOOGLE_CLIENT_ID: "google-id",
24+
FIRST_PARTY_GOOGLE_CLIENT_SECRET: "google-secret",
25+
FIRST_PARTY_HUBSPOT_CLIENT_ID: "hubspot-id",
26+
FIRST_PARTY_HUBSPOT_CLIENT_SECRET: "hubspot-secret",
27+
FIRST_PARTY_LINEAR_CLIENT_ID: "linear-id",
28+
FIRST_PARTY_LINEAR_CLIENT_SECRET: "linear-secret",
29+
FIRST_PARTY_MICROSOFT_CLIENT_ID: "microsoft-id",
30+
FIRST_PARTY_MICROSOFT_CLIENT_SECRET: "microsoft-secret",
31+
FIRST_PARTY_NOTION_CLIENT_ID: "notion-id",
32+
FIRST_PARTY_NOTION_CLIENT_SECRET: "notion-secret",
33+
FIRST_PARTY_SLACK_CLIENT_ID: "slack-id",
34+
FIRST_PARTY_SLACK_CLIENT_SECRET: "slack-secret",
35+
};
36+
37+
describe("cloud first-party OAuth clients", () => {
38+
it("enables every registered OAuth 2 provider from complete secret pairs", () => {
39+
const clients = firstPartyOAuthClientsFor(completeEnv);
40+
41+
expect(clients.map((client) => client.name)).toEqual([
42+
"airtable",
43+
"atlassian",
44+
"box",
45+
"clickup",
46+
"figma",
47+
"github",
48+
"gitlab",
49+
"google",
50+
"hubspot",
51+
"linear",
52+
"microsoft",
53+
"notion",
54+
"slack",
55+
]);
56+
});
57+
58+
it("fails closed when either half of a provider secret pair is absent", () => {
59+
expect(firstPartyOAuthClientsFor({ FIRST_PARTY_AIRTABLE_CLIENT_ID: "id" })).toEqual([]);
60+
expect(firstPartyOAuthClientsFor({ FIRST_PARTY_AIRTABLE_CLIENT_SECRET: "secret" })).toEqual([]);
61+
});
62+
63+
it("carries provider-specific authorization and token contracts", () => {
64+
const byName = new Map(
65+
firstPartyOAuthClientsFor(completeEnv).map((client) => [client.name, client]),
66+
);
67+
68+
expect(byName.get("airtable")).toMatchObject({
69+
tokenEndpointAuthMethod: "basic",
70+
});
71+
expect(byName.get("atlassian")).toMatchObject({
72+
tokenRequestFormat: "json",
73+
authorizationExtraParams: { audience: "api.atlassian.com", prompt: "consent" },
74+
});
75+
expect(byName.get("figma")).toMatchObject({
76+
tokenEndpointAuthMethod: "basic",
77+
allowedScopes: expect.arrayContaining(["folder_metadata:read", "folders:read"]),
78+
});
79+
expect(byName.get("hubspot")).toMatchObject({
80+
tokenUrl: "https://api.hubapi.com/oauth/v3/token",
81+
authorizationExtraParams: {
82+
optional_scope: "content crm.objects.custom.read crm.schemas.custom.read",
83+
},
84+
});
85+
expect(byName.get("linear")).toMatchObject({ authorizationScopeSeparator: "," });
86+
expect(byName.get("microsoft")).toMatchObject({
87+
additionalAuthorizationScopes: ["offline_access"],
88+
allowedScopes: expect.arrayContaining(["Mail.ReadWrite", "Files.ReadWrite.All"]),
89+
});
90+
expect(byName.get("notion")).toMatchObject({
91+
authorizationScopes: [],
92+
authorizationExtraParams: { owner: "user" },
93+
tokenEndpointAuthMethod: "basic",
94+
tokenRequestFormat: "json",
95+
});
96+
});
97+
});

0 commit comments

Comments
 (0)