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
109 });109 });
110 }),110 }),
111);111);
112
113scenario(
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
77 expect(transformed.url).toBe("https://executor.example/api/graphql/sources");77 expect(transformed.url).toBe("https://executor.example/api/graphql/sources");
78 expect(transformed.headers.authorization).toBe("Bearer key_123");78 expect(transformed.headers.authorization).toBe("Bearer key_123");
79 });79 });
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 });
80});93});
packages/core/sdk/src/client.ts+19−2
@@ -238,11 +238,18 @@ export interface CreatePluginAtomClientOptions { · 237 unmodified lines
238 /** Optional dynamic Authorization header for hosts whose active238 /** Optional dynamic Authorization header for hosts whose active
239 * Executor Server Connection requires Basic or Bearer auth. */239 * Executor Server Connection requires Basic or Bearer auth. */
240 readonly authorizationHeader?: string | null | (() => string | null);240 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>>);
241}245}
242 246
243export interface PluginAtomClientRequestTransformOptions {247export interface PluginAtomClientRequestTransformOptions {
244 readonly baseUrl?: () => string;248 readonly baseUrl?: () => string;
245 readonly authorizationHeader?: string | null | (() => string | null);249 readonly authorizationHeader?: string | null | (() => string | null);
250 readonly headers?:
251 | Readonly<Record<string, string | null | undefined>>
252 | (() => Readonly<Record<string, string | null | undefined>>);
246}253}
247 254
248/** @internal */255/** @internal */
@@ -258,6 +265,14 @@ export const applyPluginAtomClientRequestTransform = ( · 9 unmodified lines
258 if (authorization) {265 if (authorization) {
259 next = HttpClientRequest.setHeader(next, "authorization", authorization);266 next = HttpClientRequest.setHeader(next, "authorization", authorization);
260 }267 }
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 }
261 return next;276 return next;
262};277};
263 278
@@ -278,7 +293,7 @@ export const createPluginAtomClient = < · 14 unmodified lines
278 group: G,293 group: G,
279 options: CreatePluginAtomClientOptions = {},294 options: CreatePluginAtomClientOptions = {},
280) => {295) => {
281 const { baseUrl = "/api", authorizationHeader } = options;296 const { baseUrl = "/api", authorizationHeader, headers } = options;
282 const pluginId = group.identifier;297 const pluginId = group.identifier;
283 const bundle = HttpApi.make(`plugin-${pluginId}`).add(group);298 const bundle = HttpApi.make(`plugin-${pluginId}`).add(group);
284 const getBaseUrl = typeof baseUrl === "function" ? baseUrl : null;299 const getBaseUrl = typeof baseUrl === "function" ? baseUrl : null;
@@ -286,8 +301,9 @@ export const createPluginAtomClient = < · 1 unmodified line
286 const getAuthorizationHeader =301 const getAuthorizationHeader =
287 typeof authorizationHeader === "function" ? authorizationHeader : null;302 typeof authorizationHeader === "function" ? authorizationHeader : null;
288 const hasAuthorization = authorizationHeader !== undefined && authorizationHeader !== null;303 const hasAuthorization = authorizationHeader !== undefined && authorizationHeader !== null;
304 const hasHeaders = headers !== undefined;
289 const transformClient =305 const transformClient =
290 getBaseUrl || hasAuthorization306 getBaseUrl || hasAuthorization || hasHeaders
291 ? HttpClient.mapRequest((request) =>307 ? HttpClient.mapRequest((request) =>
292 applyPluginAtomClientRequestTransform(request, {308 applyPluginAtomClientRequestTransform(request, {
293 ...(getBaseUrl ? { baseUrl: getBaseUrl } : {}),309 ...(getBaseUrl ? { baseUrl: getBaseUrl } : {}),
@@ -296,6 +312,7 @@ export const createPluginAtomClient = < · 2 unmodified lines
296 : authorizationHeader !== undefined312 : authorizationHeader !== undefined
297 ? { authorizationHeader }313 ? { authorizationHeader }
298 : {}),314 : {}),
315 ...(headers !== undefined ? { headers } : {}),
299 }),316 }),
300 )317 )
301 : undefined;318 : undefined;
packages/plugins/graphql/src/react/client.ts+2−0
@@ -1,5 +1,6 @@
1import { createPluginAtomClient } from "@executor-js/sdk/client";1import { createPluginAtomClient } from "@executor-js/sdk/client";
2import {2import {
3 getExecutorOrganizationHeaders,
3 getExecutorApiBaseUrl,4 getExecutorApiBaseUrl,
4 getExecutorServerAuthorizationHeader,5 getExecutorServerAuthorizationHeader,
5} from "@executor-js/react/api/server-connection";6} from "@executor-js/react/api/server-connection";
@@ -8,4 +9,5 @@ import { GraphqlGroup } from "../api/group"; · 2 unmodified lines
8export const GraphqlClient = createPluginAtomClient(GraphqlGroup, {9export const GraphqlClient = createPluginAtomClient(GraphqlGroup, {
9 baseUrl: getExecutorApiBaseUrl,10 baseUrl: getExecutorApiBaseUrl,
10 authorizationHeader: getExecutorServerAuthorizationHeader,11 authorizationHeader: getExecutorServerAuthorizationHeader,
12 headers: getExecutorOrganizationHeaders,
11});13});
packages/plugins/mcp/src/react/client.ts+2−0
@@ -1,5 +1,6 @@
1import { createPluginAtomClient } from "@executor-js/sdk/client";1import { createPluginAtomClient } from "@executor-js/sdk/client";
2import {2import {
3 getExecutorOrganizationHeaders,
3 getExecutorApiBaseUrl,4 getExecutorApiBaseUrl,
4 getExecutorServerAuthorizationHeader,5 getExecutorServerAuthorizationHeader,
5} from "@executor-js/react/api/server-connection";6} from "@executor-js/react/api/server-connection";
@@ -8,4 +9,5 @@ import { McpGroup } from "../api/group"; · 2 unmodified lines
8export const McpClient = createPluginAtomClient(McpGroup, {9export const McpClient = createPluginAtomClient(McpGroup, {
9 baseUrl: getExecutorApiBaseUrl,10 baseUrl: getExecutorApiBaseUrl,
10 authorizationHeader: getExecutorServerAuthorizationHeader,11 authorizationHeader: getExecutorServerAuthorizationHeader,
12 headers: getExecutorOrganizationHeaders,
11});13});
packages/plugins/onepassword/src/react/client.ts+2−0
@@ -1,5 +1,6 @@
1import { createPluginAtomClient } from "@executor-js/sdk/client";1import { createPluginAtomClient } from "@executor-js/sdk/client";
2import {2import {
3 getExecutorOrganizationHeaders,
3 getExecutorApiBaseUrl,4 getExecutorApiBaseUrl,
4 getExecutorServerAuthorizationHeader,5 getExecutorServerAuthorizationHeader,
5} from "@executor-js/react/api/server-connection";6} from "@executor-js/react/api/server-connection";
@@ -8,4 +9,5 @@ import { OnePasswordGroup } from "../api/group"; · 2 unmodified lines
8export const OnePasswordClient = createPluginAtomClient(OnePasswordGroup, {9export const OnePasswordClient = createPluginAtomClient(OnePasswordGroup, {
9 baseUrl: getExecutorApiBaseUrl,10 baseUrl: getExecutorApiBaseUrl,
10 authorizationHeader: getExecutorServerAuthorizationHeader,11 authorizationHeader: getExecutorServerAuthorizationHeader,
12 headers: getExecutorOrganizationHeaders,
11});13});
packages/plugins/openapi/src/react/client.ts+2−0
@@ -1,5 +1,6 @@
1import { createPluginAtomClient } from "@executor-js/sdk/client";1import { createPluginAtomClient } from "@executor-js/sdk/client";
2import {2import {
3 getExecutorOrganizationHeaders,
3 getExecutorApiBaseUrl,4 getExecutorApiBaseUrl,
4 getExecutorServerAuthorizationHeader,5 getExecutorServerAuthorizationHeader,
5} from "@executor-js/react/api/server-connection";6} from "@executor-js/react/api/server-connection";
@@ -8,4 +9,5 @@ import { OpenApiGroup } from "../api/group"; · 2 unmodified lines
8export const OpenApiClient = createPluginAtomClient(OpenApiGroup, {9export const OpenApiClient = createPluginAtomClient(OpenApiGroup, {
9 baseUrl: getExecutorApiBaseUrl,10 baseUrl: getExecutorApiBaseUrl,
10 authorizationHeader: getExecutorServerAuthorizationHeader,11 authorizationHeader: getExecutorServerAuthorizationHeader,
12 headers: getExecutorOrganizationHeaders,
11});13});
packages/react/src/api/server-connection.tsx+5−0
@@ -96,6 +96,11 @@ export const getActiveOrgSlug = (): string | null => { · 95 unmodified lines
96 return first && isValidOrgSlug(first) ? first : null;96 return first && isValidOrgSlug(first) ? first : null;
97};97};
98 98
99export const getExecutorOrganizationHeaders = (): Readonly<Record<string, string>> => {
100 const orgSlug = getActiveOrgSlug();
101 return orgSlug ? { [EXECUTOR_ORG_HEADER]: orgSlug } : {};
102};
103
99export const getExecutorServerConnection = (): ExecutorServerConnection => activeConnection;104export const getExecutorServerConnection = (): ExecutorServerConnection => activeConnection;
100 105
101export const setExecutorServerConnection = (input: ExecutorServerConnectionInput): void => {106export const setExecutorServerConnection = (input: ExecutorServerConnectionInput): void => {