History

google oauth works

Rhys Sullivan committed 8 months ago

Commit 2e328ab1 parent 38327c077 files · +7814 −866Browse files

Showing the changes to packages/codemode-openapi/src (2 of 77 files). Show all

Unified · Split

2 files
  1. packages/codemode-openapi/src/openapi-tools.test.ts +125 −1
  2. packages/codemode-openapi/src/openapi-tools.ts +52 −11
packages/codemode-openapi/src/openapi-tools.test.ts+125−1
@@ -30,6 +30,7 @@ type TestServer = { · 29 unmodified lines
3030 path: string;
3131 query: string;
3232 body: string;
33+ headers: Record<string, string>;
3334 }>;
3435 close: () => Promise<void>;
3536 };
@@ -53,6 +54,7 @@ const makeEffectServerHandler = ( · 17 unmodified lines
5354 path: `/repos/${path.owner}/${path.repo}`,
5455 query: include ? `?include=${encodeURIComponent(include)}` : "",
5556 body: "",
57+ headers: {},
5658 });
5759
5860 return {
@@ -69,6 +71,7 @@ const makeEffectServerHandler = ( · 10 unmodified lines
6971 path: `/repos/${path.owner}/${path.repo}/issues`,
7072 query: "",
7173 body: bodyText,
74+ headers: {},
7275 });
7376
7477 return {
@@ -207,11 +210,21 @@ const makeTestServer = Effect.acquireRelease( · 132 unmodified lines
207210 catch: (error: unknown) =>
208211 error instanceof Error ? error : new Error(String(error)),
209212 }).pipe(Effect.orDie),
210−);
213+ );
211214
212215 const ownerParam = HttpApiSchema.param("owner", Schema.String);
213216 const repoParam = HttpApiSchema.param("repo", Schema.String);
214217
218+const requestHeadersFromNode = (
219+ request: IncomingMessage,
220+): Record<string, string> =>
221+ Object.fromEntries(
222+ Object.entries(request.headers).flatMap(([key, value]) =>
223+ value === undefined
224+ ? []
225+ : [[key, Array.isArray(value) ? value.join(",") : value]]),
226+ );
227+
215228 class GeneratedReposApi extends HttpApiGroup.make("repos")
216229 .add(
217230 HttpApiEndpoint.get("getRepo")`/repos/${ownerParam}/${repoParam}`
@@ -475,6 +488,117 @@ describe("openapi-tools", () => { · 257 unmodified lines
475488 }),
476489 );
477490
491+ it.scoped("applies credential placements for query params, cookies, and body values", () =>
492+ Effect.gen(function* () {
493+ const requests: TestServer["requests"] = [];
494+ const server = yield* Effect.acquireRelease(
495+ Effect.promise<TestServer>(
496+ () =>
497+ new Promise<TestServer>((resolve, reject) => {
498+ const httpServer = createServer((request, response) => {
499+ const url = new URL(
500+ request.url ?? "/",
501+ `http://${request.headers.host ?? "127.0.0.1"}`,
502+ );
503+ let body = "";
504+
505+ request.setEncoding("utf8");
506+ request.on("data", (chunk) => {
507+ body += chunk;
508+ });
509+ request.on("end", () => {
510+ requests.push({
511+ method: request.method ?? "GET",
512+ path: url.pathname,
513+ query: url.search,
514+ body,
515+ headers: requestHeadersFromNode(request),
516+ });
517+ response.statusCode = 200;
518+ response.setHeader("content-type", "application/json");
519+ response.end(JSON.stringify({ ok: true }));
520+ });
521+ });
522+
523+ httpServer.once("error", reject);
524+ httpServer.listen(0, "127.0.0.1", () => {
525+ const address = httpServer.address();
526+ if (!address || typeof address === "string") {
527+ reject(new Error("failed to resolve test server address"));
528+ return;
529+ }
530+
531+ resolve({
532+ baseUrl: `http://127.0.0.1:${address.port}`,
533+ requests,
534+ close: async () => {
535+ await new Promise<void>((closeResolve, closeReject) => {
536+ httpServer.close((error: Error | undefined) => {
537+ if (error) {
538+ closeReject(error);
539+ return;
540+ }
541+ closeResolve();
542+ });
543+ });
544+ },
545+ });
546+ });
547+ }),
548+ ),
549+ (resource) =>
550+ Effect.tryPromise({
551+ try: () => resource.close(),
552+ catch: (error: unknown) =>
553+ error instanceof Error ? error : new Error(String(error)),
554+ }).pipe(Effect.orDie),
555+ );
556+ const extracted = yield* createOpenApiToolsFromSpec({
557+ sourceName: "demo",
558+ openApiSpec: generatedOpenApiSpec,
559+ baseUrl: server.baseUrl,
560+ namespace: "source.demo",
561+ credentialPlacements: {
562+ headers: {
563+ authorization: "Bearer top-secret",
564+ },
565+ queryParams: {
566+ api_key: "secret-key",
567+ },
568+ cookies: {
569+ session: "abc123",
570+ },
571+ bodyValues: {
572+ "auth.token": "body-secret",
573+ },
574+ },
575+ });
576+
577+ const getRepo = resolveToolExecutor(extracted.tools, "source.demo.repos.getRepo");
578+ const createIssue = resolveToolExecutor(extracted.tools, "source.demo.repos.createIssue");
579+
580+ yield* Effect.promise(() => getRepo({ owner: "octocat", repo: "hello-world" }));
581+ yield* Effect.promise(() =>
582+ createIssue({
583+ owner: "octocat",
584+ repo: "hello-world",
585+ body: { title: "Bug report" },
586+ }),
587+ );
588+
589+ expect(server.requests).toHaveLength(2);
590+ expect(server.requests[0]?.query).toContain("api_key=secret-key");
591+ expect(server.requests[0]?.headers.authorization).toBe("Bearer top-secret");
592+ expect(server.requests[0]?.headers.cookie).toContain("session=abc123");
593+ expect(JSON.parse(server.requests[1]?.body ?? "{}")).toEqual({
594+ title: "Bug report",
595+ auth: {
596+ token: "body-secret",
597+ },
598+ });
599+ }),
600+ );
601+
478602 it.scoped("falls back to path-template args when extracted path parameters are missing", () =>
479603 Effect.gen(function* () {
480604 const server = yield* makeTestServer;
packages/codemode-openapi/src/openapi-tools.ts+52−11
@@ -8,8 +8,12 @@ import * as Effect from "effect/Effect"; · 7 unmodified lines
88 import * as Layer from "effect/Layer";
99
1010 import {
11+ applyCookiePlacementsToHeaders,
12+ applyHttpQueryPlacementsToUrl,
13+ applyJsonBodyPlacements,
1114 standardSchemaFromJsonSchema,
1215 toTool,
16+ type HttpRequestPlacements,
1317 type ToolMap,
1418 unknownInputSchema,
1519 } from "@executor/codemode-core";
@@ -288,6 +292,7 @@ export type CreateOpenApiToolsFromManifestInput = { · 272 unmodified lines
288292 sourceKey?: string;
289293 defaultHeaders?: Readonly<Record<string, string>>;
290294 credentialHeaders?: Readonly<Record<string, string>>;
295+ credentialPlacements?: HttpRequestPlacements;
291296 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;
292297 };
293298
@@ -317,7 +322,7 @@ const buildFetchRequest = (input: { · 23 unmodified lines
317322 args: OpenApiToolArgs;
318323 baseUrl: string;
319324 defaultHeaders: Readonly<Record<string, string>>;
320− credentialHeaders: Readonly<Record<string, string>>;
325+ credentialPlacements: HttpRequestPlacements;
321326 }): {
322327 url: URL;
323328 method: string;
@@ -369,9 +374,11 @@ const buildFetchRequest = (input: { · 45 unmodified lines
369374 }
370375
371376 let body: string | undefined;
377+ const bodyValues = input.credentialPlacements.bodyValues ?? {};
378+ const hasCredentialBodyValues = Object.keys(bodyValues).length > 0;
372379
373380 if (input.payload.requestBody !== null) {
374− if (!hasRequestBody(input.args)) {
381+ if (!hasRequestBody(input.args) && !hasCredentialBodyValues) {
375382 if (input.payload.requestBody.required) {
376383 throw new OpenApiToolInvocationError({
377384 operation: "validate_args",
@@ -380,7 +387,13 @@ const buildFetchRequest = (input: { · 2 unmodified lines
380387 });
381388 }
382389 } else {
383− body = JSON.stringify(input.args.body);
390+ body = JSON.stringify(
391+ applyJsonBodyPlacements({
392+ body: hasRequestBody(input.args) ? input.args.body : {},
393+ bodyValues,
394+ label: `${input.payload.method.toUpperCase()} ${input.payload.pathTemplate}`,
395+ }),
396+ );
384397
385398 const preferredContentType = input.payload.requestBody.contentTypes[0];
386399 if (preferredContentType) {
@@ -391,14 +404,23 @@ const buildFetchRequest = (input: { · 4 unmodified lines
391404 }
392405 }
393406
394− for (const [key, value] of Object.entries(input.credentialHeaders)) {
395− headers[key] = value;
407+ const urlWithAuth = applyHttpQueryPlacementsToUrl({
408+ url,
409+ queryParams: input.credentialPlacements.queryParams,
410+ });
411+ const headersWithAuthCookies = applyCookiePlacementsToHeaders({
412+ headers,
413+ cookies: input.credentialPlacements.cookies,
414+ });
415+
416+ for (const [key, value] of Object.entries(input.credentialPlacements.headers ?? {})) {
417+ headersWithAuthCookies[key] = value;
396418 }
397419
398420 return {
399− url,
421+ url: urlWithAuth,
400422 method: input.payload.method.toUpperCase(),
401− headers,
423+ headers: headersWithAuthCookies,
402424 body,
403425 };
404426 };
@@ -413,15 +435,32 @@ export type CreateOpenApiToolFromDefinitionInput = { · 8 unmodified lines
413435 baseUrl: string;
414436 defaultHeaders?: Readonly<Record<string, string>>;
415437 credentialHeaders?: Readonly<Record<string, string>>;
438+ credentialPlacements?: HttpRequestPlacements;
416439 refHintTable?: Readonly<OpenApiRefHintTable>;
417440 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;
418441 };
419442
443+const normalizeCredentialPlacements = (input: {
444+ credentialHeaders?: Readonly<Record<string, string>>;
445+ credentialPlacements?: HttpRequestPlacements;
446+}): HttpRequestPlacements => ({
447+ headers: {
448+ ...(input.credentialHeaders ?? {}),
449+ ...(input.credentialPlacements?.headers ?? {}),
450+ },
451+ queryParams: input.credentialPlacements?.queryParams,
452+ cookies: input.credentialPlacements?.cookies,
453+ bodyValues: input.credentialPlacements?.bodyValues,
454+});
455+
420456 export const createOpenApiToolFromDefinition = (
421457 input: CreateOpenApiToolFromDefinitionInput,
422458 ) => {
423459 const defaultHeaders = input.defaultHeaders ?? {};
424− const credentialHeaders = input.credentialHeaders ?? {};
460+ const credentialPlacements = normalizeCredentialPlacements({
461+ credentialHeaders: input.credentialHeaders,
462+ credentialPlacements: input.credentialPlacements,
463+ });
425464 const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer;
426465 const presentation = buildOpenApiToolPresentation({
427466 definition: input.definition,
@@ -441,7 +480,7 @@ export const createOpenApiToolFromDefinition = ( · 13 unmodified lines
441480 args: decodedArgs,
442481 baseUrl: input.baseUrl,
443482 defaultHeaders,
444− credentialHeaders,
483+ credentialPlacements,
445484 });
446485
447486 return Effect.runPromise(
@@ -503,7 +542,6 @@ export const createOpenApiToolsFromManifest = ( · 55 unmodified lines
503542 const baseUrl = normalizeHttpUrl(input.baseUrl);
504543 const sourceKey = input.sourceKey ?? "openapi.generated";
505544 const defaultHeaders = input.defaultHeaders ?? {};
506− const credentialHeaders = input.credentialHeaders ?? {};
507545 const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer;
508546
509547 const definitions = compileOpenApiToolDefinitions(input.manifest);
@@ -517,7 +555,8 @@ export const createOpenApiToolsFromManifest = ( · 7 unmodified lines
517555 sourceKey,
518556 baseUrl,
519557 defaultHeaders,
520− credentialHeaders,
558+ credentialHeaders: input.credentialHeaders,
559+ credentialPlacements: input.credentialPlacements,
521560 refHintTable: input.manifest.refHintTable,
522561 httpClientLayer,
523562 });
@@ -534,6 +573,7 @@ export const createOpenApiToolsFromSpec = (input: { · 10 unmodified lines
534573 sourceKey?: string;
535574 defaultHeaders?: Readonly<Record<string, string>>;
536575 credentialHeaders?: Readonly<Record<string, string>>;
576+ credentialPlacements?: HttpRequestPlacements;
537577 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;
538578 }): Effect.Effect<
539579 { manifest: OpenApiToolManifest; definitions: Array<OpenApiToolDefinition>; tools: ToolMap },
@@ -551,6 +591,7 @@ export const createOpenApiToolsFromSpec = (input: { · 11 unmodified lines
551591 sourceKey: input.sourceKey,
552592 defaultHeaders: input.defaultHeaders,
553593 credentialHeaders: input.credentialHeaders,
594+ credentialPlacements: input.credentialPlacements,
554595 httpClientLayer: input.httpClientLayer,
555596 }),
556597 }),