History

[codex] Fix plugin API org scoping (#1043)

Rhys Sullivan committed 4 months ago

Commit 4091f8b1 parent b0391578 files · +93 −2Browse files

Unified · Split

8 files
  1. e2e/cloud/org-multitab-cookie.test.ts +48 −0
  2. packages/core/sdk/src/client.test.ts +13 −0
  3. packages/core/sdk/src/client.ts +19 −2
  4. packages/plugins/graphql/src/react/client.ts +2 −0
  5. packages/plugins/mcp/src/react/client.ts +2 −0
  6. packages/plugins/onepassword/src/react/client.ts +2 −0
  7. packages/plugins/openapi/src/react/client.ts +2 −0
  8. packages/react/src/api/server-connection.tsx +5 −0
e2e/cloud/org-multitab-cookie.test.ts+48−0
@@ -109,3 +109,51 @@ scenario( · 108 unmodified lines
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+13−0
@@ -77,4 +77,17 @@ describe("createPluginAtomClient", () => { · 76 unmodified lines
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+19−2
@@ -238,11 +238,18 @@ export interface CreatePluginAtomClientOptions { · 237 unmodified lines
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 = ( · 9 unmodified lines
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,7 +293,7 @@ export const createPluginAtomClient = < · 14 unmodified lines
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;
@@ -286,8 +301,9 @@ export const createPluginAtomClient = < · 1 unmodified line
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 = < · 2 unmodified lines
296312 : authorizationHeader !== undefined
297313 ? { authorizationHeader }
298314 : {}),
315+ ...(headers !== undefined ? { headers } : {}),
299316 }),
300317 )
301318 : undefined;
packages/plugins/graphql/src/react/client.ts+2−0
@@ -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"; · 2 unmodified lines
89 export const GraphqlClient = createPluginAtomClient(GraphqlGroup, {
910 baseUrl: getExecutorApiBaseUrl,
1011 authorizationHeader: getExecutorServerAuthorizationHeader,
12+ headers: getExecutorOrganizationHeaders,
1113 });
packages/plugins/mcp/src/react/client.ts+2−0
@@ -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"; · 2 unmodified lines
89 export const McpClient = createPluginAtomClient(McpGroup, {
910 baseUrl: getExecutorApiBaseUrl,
1011 authorizationHeader: getExecutorServerAuthorizationHeader,
12+ headers: getExecutorOrganizationHeaders,
1113 });
packages/plugins/onepassword/src/react/client.ts+2−0
@@ -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"; · 2 unmodified lines
89 export const OnePasswordClient = createPluginAtomClient(OnePasswordGroup, {
910 baseUrl: getExecutorApiBaseUrl,
1011 authorizationHeader: getExecutorServerAuthorizationHeader,
12+ headers: getExecutorOrganizationHeaders,
1113 });
packages/plugins/openapi/src/react/client.ts+2−0
@@ -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"; · 2 unmodified lines
89 export const OpenApiClient = createPluginAtomClient(OpenApiGroup, {
910 baseUrl: getExecutorApiBaseUrl,
1011 authorizationHeader: getExecutorServerAuthorizationHeader,
12+ headers: getExecutorOrganizationHeaders,
1113 });
packages/react/src/api/server-connection.tsx+5−0
@@ -96,6 +96,11 @@ export const getActiveOrgSlug = (): string | null => { · 95 unmodified lines
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 => {