Skip to content

Commit 4091f8b

Browse files
authored
[codex] Fix plugin API org scoping (#1043)
1 parent b039157 commit 4091f8b

8 files changed

Lines changed: 93 additions & 2 deletions

File tree

e2e/cloud/org-multitab-cookie.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,51 @@ scenario(
109109
});
110110
}),
111111
);
112+
113+
scenario(
114+
"Org scope · plugin API requests carry the URL organization selector",
115+
{},
116+
Effect.gen(function* () {
117+
const target = yield* Target;
118+
const browser = yield* Browser;
119+
const identity = yield* target.newIdentity();
120+
121+
const spec = JSON.stringify({
122+
openapi: "3.0.3",
123+
info: { title: "Header Probe", version: "1.0.0" },
124+
servers: [{ url: "https://api.example.com" }],
125+
paths: {
126+
"/ping": {
127+
get: {
128+
operationId: "ping",
129+
responses: { "200": { description: "ok" } },
130+
},
131+
},
132+
},
133+
});
134+
135+
yield* browser.session(identity, async ({ page, step }) => {
136+
await step("Open the OpenAPI add form", async () => {
137+
await page.goto("/integrations/add/openapi", { waitUntil: "networkidle" });
138+
await page.getByPlaceholder("https://api.example.com/openapi.json").waitFor();
139+
});
140+
141+
const orgSlug = new URL(page.url()).pathname.split("/")[1];
142+
expect(orgSlug, "the add form landed on an org-scoped URL").toBeTruthy();
143+
144+
await step("Paste an inline spec", async () => {
145+
const previewRequest = page.waitForRequest(
146+
(request) => request.url().includes("/api/openapi/preview"),
147+
{ timeout: 15_000 },
148+
);
149+
150+
await page.getByPlaceholder("https://api.example.com/openapi.json").fill(spec);
151+
152+
expect(
153+
(await previewRequest).headers()["x-executor-organization"],
154+
"plugin-owned preview requests use the URL's org selector",
155+
).toBe(orgSlug);
156+
});
157+
});
158+
}),
159+
);

packages/core/sdk/src/client.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,17 @@ describe("createPluginAtomClient", () => {
7777
expect(transformed.url).toBe("https://executor.example/api/graphql/sources");
7878
expect(transformed.headers.authorization).toBe("Bearer key_123");
7979
});
80+
81+
it("can apply host-supplied dynamic request headers to plugin requests", () => {
82+
const request = HttpClientRequest.get("/openapi/integrations/acme");
83+
const transformed = applyPluginAtomClientRequestTransform(request, {
84+
headers: () => ({
85+
"x-executor-organization": "newvale",
86+
"x-empty": null,
87+
}),
88+
});
89+
90+
expect(transformed.headers["x-executor-organization"]).toBe("newvale");
91+
expect(transformed.headers["x-empty"]).toBeUndefined();
92+
});
8093
});

packages/core/sdk/src/client.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,18 @@ export interface CreatePluginAtomClientOptions {
238238
/** Optional dynamic Authorization header for hosts whose active
239239
* Executor Server Connection requires Basic or Bearer auth. */
240240
readonly authorizationHeader?: string | null | (() => string | null);
241+
/** Optional dynamic request headers supplied by the host shell. */
242+
readonly headers?:
243+
| Readonly<Record<string, string | null | undefined>>
244+
| (() => Readonly<Record<string, string | null | undefined>>);
241245
}
242246

243247
export interface PluginAtomClientRequestTransformOptions {
244248
readonly baseUrl?: () => string;
245249
readonly authorizationHeader?: string | null | (() => string | null);
250+
readonly headers?:
251+
| Readonly<Record<string, string | null | undefined>>
252+
| (() => Readonly<Record<string, string | null | undefined>>);
246253
}
247254

248255
/** @internal */
@@ -258,6 +265,14 @@ export const applyPluginAtomClientRequestTransform = (
258265
if (authorization) {
259266
next = HttpClientRequest.setHeader(next, "authorization", authorization);
260267
}
268+
const headers = typeof options.headers === "function" ? options.headers() : options.headers;
269+
if (headers) {
270+
for (const [name, value] of Object.entries(headers)) {
271+
if (value !== undefined && value !== null) {
272+
next = HttpClientRequest.setHeader(next, name, value);
273+
}
274+
}
275+
}
261276
return next;
262277
};
263278

@@ -278,16 +293,17 @@ export const createPluginAtomClient = <
278293
group: G,
279294
options: CreatePluginAtomClientOptions = {},
280295
) => {
281-
const { baseUrl = "/api", authorizationHeader } = options;
296+
const { baseUrl = "/api", authorizationHeader, headers } = options;
282297
const pluginId = group.identifier;
283298
const bundle = HttpApi.make(`plugin-${pluginId}`).add(group);
284299
const getBaseUrl = typeof baseUrl === "function" ? baseUrl : null;
285300
const staticBaseUrl = typeof baseUrl === "function" ? undefined : baseUrl;
286301
const getAuthorizationHeader =
287302
typeof authorizationHeader === "function" ? authorizationHeader : null;
288303
const hasAuthorization = authorizationHeader !== undefined && authorizationHeader !== null;
304+
const hasHeaders = headers !== undefined;
289305
const transformClient =
290-
getBaseUrl || hasAuthorization
306+
getBaseUrl || hasAuthorization || hasHeaders
291307
? HttpClient.mapRequest((request) =>
292308
applyPluginAtomClientRequestTransform(request, {
293309
...(getBaseUrl ? { baseUrl: getBaseUrl } : {}),
@@ -296,6 +312,7 @@ export const createPluginAtomClient = <
296312
: authorizationHeader !== undefined
297313
? { authorizationHeader }
298314
: {}),
315+
...(headers !== undefined ? { headers } : {}),
299316
}),
300317
)
301318
: undefined;

packages/plugins/graphql/src/react/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createPluginAtomClient } from "@executor-js/sdk/client";
22
import {
3+
getExecutorOrganizationHeaders,
34
getExecutorApiBaseUrl,
45
getExecutorServerAuthorizationHeader,
56
} from "@executor-js/react/api/server-connection";
@@ -8,4 +9,5 @@ import { GraphqlGroup } from "../api/group";
89
export const GraphqlClient = createPluginAtomClient(GraphqlGroup, {
910
baseUrl: getExecutorApiBaseUrl,
1011
authorizationHeader: getExecutorServerAuthorizationHeader,
12+
headers: getExecutorOrganizationHeaders,
1113
});

packages/plugins/mcp/src/react/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createPluginAtomClient } from "@executor-js/sdk/client";
22
import {
3+
getExecutorOrganizationHeaders,
34
getExecutorApiBaseUrl,
45
getExecutorServerAuthorizationHeader,
56
} from "@executor-js/react/api/server-connection";
@@ -8,4 +9,5 @@ import { McpGroup } from "../api/group";
89
export const McpClient = createPluginAtomClient(McpGroup, {
910
baseUrl: getExecutorApiBaseUrl,
1011
authorizationHeader: getExecutorServerAuthorizationHeader,
12+
headers: getExecutorOrganizationHeaders,
1113
});

packages/plugins/onepassword/src/react/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createPluginAtomClient } from "@executor-js/sdk/client";
22
import {
3+
getExecutorOrganizationHeaders,
34
getExecutorApiBaseUrl,
45
getExecutorServerAuthorizationHeader,
56
} from "@executor-js/react/api/server-connection";
@@ -8,4 +9,5 @@ import { OnePasswordGroup } from "../api/group";
89
export const OnePasswordClient = createPluginAtomClient(OnePasswordGroup, {
910
baseUrl: getExecutorApiBaseUrl,
1011
authorizationHeader: getExecutorServerAuthorizationHeader,
12+
headers: getExecutorOrganizationHeaders,
1113
});

packages/plugins/openapi/src/react/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createPluginAtomClient } from "@executor-js/sdk/client";
22
import {
3+
getExecutorOrganizationHeaders,
34
getExecutorApiBaseUrl,
45
getExecutorServerAuthorizationHeader,
56
} from "@executor-js/react/api/server-connection";
@@ -8,4 +9,5 @@ import { OpenApiGroup } from "../api/group";
89
export const OpenApiClient = createPluginAtomClient(OpenApiGroup, {
910
baseUrl: getExecutorApiBaseUrl,
1011
authorizationHeader: getExecutorServerAuthorizationHeader,
12+
headers: getExecutorOrganizationHeaders,
1113
});

packages/react/src/api/server-connection.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ export const getActiveOrgSlug = (): string | null => {
9696
return first && isValidOrgSlug(first) ? first : null;
9797
};
9898

99+
export const getExecutorOrganizationHeaders = (): Readonly<Record<string, string>> => {
100+
const orgSlug = getActiveOrgSlug();
101+
return orgSlug ? { [EXECUTOR_ORG_HEADER]: orgSlug } : {};
102+
};
103+
99104
export const getExecutorServerConnection = (): ExecutorServerConnection => activeConnection;
100105

101106
export const setExecutorServerConnection = (input: ExecutorServerConnectionInput): void => {

0 commit comments

Comments
 (0)