Skip to content

Commit ad702a9

Browse files
authored
Add registered first-party OAuth providers (#1717)
* Add registered first-party OAuth providers * Redact submitted client secret from token-request failures
1 parent c1f51b7 commit ad702a9

11 files changed

Lines changed: 901 additions & 150 deletions

File tree

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

Lines changed: 3 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,14 @@ 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 { cloudEnterpriseManagedRollout } from "../analytics/ema-rollout";
5650
import { DbService } from "../db/db";
5751
import { cloudDbProviderLayer } from "../db/fuma";
52+
import { firstPartyOAuthClientsFor } from "./first-party-oauth-clients";
5853

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

@@ -95,98 +90,6 @@ export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed(
9590
*/
9691
export const CLOUD_MOUNT_PREFIX = "/api" as const;
9792

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

‎apps/cloud/src/engine/first-party-oauth-clients.test.ts‎

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,100 @@
1-
import { env } from "cloudflare:workers";
2-
import { beforeAll, describe, expect, it } from "@effect/vitest";
1+
import { describe, expect, it } from "@effect/vitest";
32

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

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

15-
describe("cloud first-party oauth clients", () => {
16-
beforeAll(() => {
17-
env.FIRST_PARTY_GOOGLE_CLIENT_ID = "test-google-client";
18-
env.FIRST_PARTY_GOOGLE_CLIENT_SECRET = "test-google-secret";
19-
});
20-
21-
const google = () => cloudFirstPartyOAuthClients().find((client) => client.name === "google");
108+
describe("cloud first-party Google app", () => {
109+
const google = () =>
110+
firstPartyOAuthClientsFor(completeEnv).find((client) => client.name === "google");
22111

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

0 commit comments

Comments
 (0)