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
30 path: string;30 path: string;
31 query: string;31 query: string;
32 body: string;32 body: string;
33 headers: Record<string, string>;
33 }>;34 }>;
34 close: () => Promise<void>;35 close: () => Promise<void>;
35};36};
@@ -53,6 +54,7 @@ const makeEffectServerHandler = ( · 17 unmodified lines
53 path: `/repos/${path.owner}/${path.repo}`,54 path: `/repos/${path.owner}/${path.repo}`,
54 query: include ? `?include=${encodeURIComponent(include)}` : "",55 query: include ? `?include=${encodeURIComponent(include)}` : "",
55 body: "",56 body: "",
57 headers: {},
56 });58 });
57 59
58 return {60 return {
@@ -69,6 +71,7 @@ const makeEffectServerHandler = ( · 10 unmodified lines
69 path: `/repos/${path.owner}/${path.repo}/issues`,71 path: `/repos/${path.owner}/${path.repo}/issues`,
70 query: "",72 query: "",
71 body: bodyText,73 body: bodyText,
74 headers: {},
72 });75 });
73 76
74 return {77 return {
@@ -207,11 +210,21 @@ const makeTestServer = Effect.acquireRelease( · 132 unmodified lines
207 catch: (error: unknown) =>210 catch: (error: unknown) =>
208 error instanceof Error ? error : new Error(String(error)),211 error instanceof Error ? error : new Error(String(error)),
209 }).pipe(Effect.orDie),212 }).pipe(Effect.orDie),
210);213 );
211 214
212const ownerParam = HttpApiSchema.param("owner", Schema.String);215const ownerParam = HttpApiSchema.param("owner", Schema.String);
213const repoParam = HttpApiSchema.param("repo", Schema.String);216const repoParam = HttpApiSchema.param("repo", Schema.String);
214 217
218const 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
215class GeneratedReposApi extends HttpApiGroup.make("repos")228class GeneratedReposApi extends HttpApiGroup.make("repos")
216 .add(229 .add(
217 HttpApiEndpoint.get("getRepo")`/repos/${ownerParam}/${repoParam}`230 HttpApiEndpoint.get("getRepo")`/repos/${ownerParam}/${repoParam}`
@@ -475,6 +488,117 @@ describe("openapi-tools", () => { · 257 unmodified lines
475 }),488 }),
476 );489 );
477 490
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
478 it.scoped("falls back to path-template args when extracted path parameters are missing", () =>602 it.scoped("falls back to path-template args when extracted path parameters are missing", () =>
479 Effect.gen(function* () {603 Effect.gen(function* () {
480 const server = yield* makeTestServer;604 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
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9 9
10import {10import {
11 applyCookiePlacementsToHeaders,
12 applyHttpQueryPlacementsToUrl,
13 applyJsonBodyPlacements,
11 standardSchemaFromJsonSchema,14 standardSchemaFromJsonSchema,
12 toTool,15 toTool,
16 type HttpRequestPlacements,
13 type ToolMap,17 type ToolMap,
14 unknownInputSchema,18 unknownInputSchema,
15} from "@executor/codemode-core";19} from "@executor/codemode-core";
@@ -288,6 +292,7 @@ export type CreateOpenApiToolsFromManifestInput = { · 272 unmodified lines
288 sourceKey?: string;292 sourceKey?: string;
289 defaultHeaders?: Readonly<Record<string, string>>;293 defaultHeaders?: Readonly<Record<string, string>>;
290 credentialHeaders?: Readonly<Record<string, string>>;294 credentialHeaders?: Readonly<Record<string, string>>;
295 credentialPlacements?: HttpRequestPlacements;
291 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;296 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;
292};297};
293 298
@@ -317,7 +322,7 @@ const buildFetchRequest = (input: { · 23 unmodified lines
317 args: OpenApiToolArgs;322 args: OpenApiToolArgs;
318 baseUrl: string;323 baseUrl: string;
319 defaultHeaders: Readonly<Record<string, string>>;324 defaultHeaders: Readonly<Record<string, string>>;
320 credentialHeaders: Readonly<Record<string, string>>;325 credentialPlacements: HttpRequestPlacements;
321}): {326}): {
322 url: URL;327 url: URL;
323 method: string;328 method: string;
@@ -369,9 +374,11 @@ const buildFetchRequest = (input: { · 45 unmodified lines
369 }374 }
370 375
371 let body: string | undefined;376 let body: string | undefined;
377 const bodyValues = input.credentialPlacements.bodyValues ?? {};
378 const hasCredentialBodyValues = Object.keys(bodyValues).length > 0;
372 379
373 if (input.payload.requestBody !== null) {380 if (input.payload.requestBody !== null) {
374 if (!hasRequestBody(input.args)) {381 if (!hasRequestBody(input.args) && !hasCredentialBodyValues) {
375 if (input.payload.requestBody.required) {382 if (input.payload.requestBody.required) {
376 throw new OpenApiToolInvocationError({383 throw new OpenApiToolInvocationError({
377 operation: "validate_args",384 operation: "validate_args",
@@ -380,7 +387,13 @@ const buildFetchRequest = (input: { · 2 unmodified lines
380 });387 });
381 }388 }
382 } else {389 } 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 );
384 397
385 const preferredContentType = input.payload.requestBody.contentTypes[0];398 const preferredContentType = input.payload.requestBody.contentTypes[0];
386 if (preferredContentType) {399 if (preferredContentType) {
@@ -391,14 +404,23 @@ const buildFetchRequest = (input: { · 4 unmodified lines
391 }404 }
392 }405 }
393 406
394 for (const [key, value] of Object.entries(input.credentialHeaders)) {407 const urlWithAuth = applyHttpQueryPlacementsToUrl({
395 headers[key] = value;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;
396 }418 }
397 419
398 return {420 return {
399 url,421 url: urlWithAuth,
400 method: input.payload.method.toUpperCase(),422 method: input.payload.method.toUpperCase(),
401 headers,423 headers: headersWithAuthCookies,
402 body,424 body,
403 };425 };
404};426};
@@ -413,15 +435,32 @@ export type CreateOpenApiToolFromDefinitionInput = { · 8 unmodified lines
413 baseUrl: string;435 baseUrl: string;
414 defaultHeaders?: Readonly<Record<string, string>>;436 defaultHeaders?: Readonly<Record<string, string>>;
415 credentialHeaders?: Readonly<Record<string, string>>;437 credentialHeaders?: Readonly<Record<string, string>>;
438 credentialPlacements?: HttpRequestPlacements;
416 refHintTable?: Readonly<OpenApiRefHintTable>;439 refHintTable?: Readonly<OpenApiRefHintTable>;
417 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;440 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;
418};441};
419 442
443const 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
420export const createOpenApiToolFromDefinition = (456export const createOpenApiToolFromDefinition = (
421 input: CreateOpenApiToolFromDefinitionInput,457 input: CreateOpenApiToolFromDefinitionInput,
422) => {458) => {
423 const defaultHeaders = input.defaultHeaders ?? {};459 const defaultHeaders = input.defaultHeaders ?? {};
424 const credentialHeaders = input.credentialHeaders ?? {};460 const credentialPlacements = normalizeCredentialPlacements({
461 credentialHeaders: input.credentialHeaders,
462 credentialPlacements: input.credentialPlacements,
463 });
425 const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer;464 const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer;
426 const presentation = buildOpenApiToolPresentation({465 const presentation = buildOpenApiToolPresentation({
427 definition: input.definition,466 definition: input.definition,
@@ -441,7 +480,7 @@ export const createOpenApiToolFromDefinition = ( · 13 unmodified lines
441 args: decodedArgs,480 args: decodedArgs,
442 baseUrl: input.baseUrl,481 baseUrl: input.baseUrl,
443 defaultHeaders,482 defaultHeaders,
444 credentialHeaders,483 credentialPlacements,
445 });484 });
446 485
447 return Effect.runPromise(486 return Effect.runPromise(
@@ -503,7 +542,6 @@ export const createOpenApiToolsFromManifest = ( · 55 unmodified lines
503 const baseUrl = normalizeHttpUrl(input.baseUrl);542 const baseUrl = normalizeHttpUrl(input.baseUrl);
504 const sourceKey = input.sourceKey ?? "openapi.generated";543 const sourceKey = input.sourceKey ?? "openapi.generated";
505 const defaultHeaders = input.defaultHeaders ?? {};544 const defaultHeaders = input.defaultHeaders ?? {};
506 const credentialHeaders = input.credentialHeaders ?? {};
507 const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer;545 const httpClientLayer = input.httpClientLayer ?? FetchHttpClient.layer;
508 546
509 const definitions = compileOpenApiToolDefinitions(input.manifest);547 const definitions = compileOpenApiToolDefinitions(input.manifest);
@@ -517,7 +555,8 @@ export const createOpenApiToolsFromManifest = ( · 7 unmodified lines
517 sourceKey,555 sourceKey,
518 baseUrl,556 baseUrl,
519 defaultHeaders,557 defaultHeaders,
520 credentialHeaders,558 credentialHeaders: input.credentialHeaders,
559 credentialPlacements: input.credentialPlacements,
521 refHintTable: input.manifest.refHintTable,560 refHintTable: input.manifest.refHintTable,
522 httpClientLayer,561 httpClientLayer,
523 });562 });
@@ -534,6 +573,7 @@ export const createOpenApiToolsFromSpec = (input: { · 10 unmodified lines
534 sourceKey?: string;573 sourceKey?: string;
535 defaultHeaders?: Readonly<Record<string, string>>;574 defaultHeaders?: Readonly<Record<string, string>>;
536 credentialHeaders?: Readonly<Record<string, string>>;575 credentialHeaders?: Readonly<Record<string, string>>;
576 credentialPlacements?: HttpRequestPlacements;
537 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;577 httpClientLayer?: Layer.Layer<HttpClient.HttpClient, never, never>;
538}): Effect.Effect<578}): Effect.Effect<
539 { manifest: OpenApiToolManifest; definitions: Array<OpenApiToolDefinition>; tools: ToolMap },579 { manifest: OpenApiToolManifest; definitions: Array<OpenApiToolDefinition>; tools: ToolMap },
@@ -551,6 +591,7 @@ export const createOpenApiToolsFromSpec = (input: { · 11 unmodified lines
551 sourceKey: input.sourceKey,591 sourceKey: input.sourceKey,
552 defaultHeaders: input.defaultHeaders,592 defaultHeaders: input.defaultHeaders,
553 credentialHeaders: input.credentialHeaders,593 credentialHeaders: input.credentialHeaders,
594 credentialPlacements: input.credentialPlacements,
554 httpClientLayer: input.httpClientLayer,595 httpClientLayer: input.httpClientLayer,
555 }),596 }),
556 }),597 }),