History

Merge remote-tracking branch 'origin/codex/effect-lint-core-api-oauth-sources' into codex/effect-lint-integration

Rhys Sullivan committed 6 months ago

Commit e03dd712 parents eb2f5a4 ff6dbd32 files · +45 −26Browse files

Unified · Split

2 files
  1. apps/cloud/src/services/sources-api.node.test.ts +18 −17
  2. packages/core/api/src/handlers/oauth.ts +27 −9
apps/cloud/src/services/sources-api.node.test.ts+18−17
@@ -4,7 +4,7 @@ · 3 unmodified lines
4// single org.4// single org.
5 5
6import { describe, expect, it } from "@effect/vitest";6import { describe, expect, it } from "@effect/vitest";
7import { Effect } from "effect";7import { Effect, Result, Schema } from "effect";
8import http from "node:http";8import http from "node:http";
9import { readFileSync } from "node:fs";9import { readFileSync } from "node:fs";
10import type { AddressInfo } from "node:net";10import type { AddressInfo } from "node:net";
@@ -173,6 +173,11 @@ const GRAPHQL_INTROSPECTION_RESPONSE = { · 162 unmodified lines
173 },173 },
174};174};
175 175
176const GraphqlRequestSchema = Schema.Struct({
177 query: Schema.optional(Schema.String),
178 variables: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
179});
180
176const startGraphqlServer = () => {181const startGraphqlServer = () => {
177 const requests: Array<{ readonly query: string; readonly variables: unknown }> = [];182 const requests: Array<{ readonly query: string; readonly variables: unknown }> = [];
178 const server = http.createServer(async (req, res) => {183 const server = http.createServer(async (req, res) => {
@@ -182,10 +187,9 @@ const startGraphqlServer = () => { · 3 unmodified lines
182 return;187 return;
183 }188 }
184 189
185 const parsed = JSON.parse(await readBody(req)) as {190 const parsed = await Schema.decodeUnknownPromise(Schema.fromJsonString(GraphqlRequestSchema))(
186 readonly query?: string;191 await readBody(req),
187 readonly variables?: Record<string, unknown>;192 );
188 };
189 const query = parsed.query ?? "";193 const query = parsed.query ?? "";
190 requests.push({ query, variables: parsed.variables ?? null });194 requests.push({ query, variables: parsed.variables ?? null });
191 195
@@ -411,9 +415,8 @@ describe("sources api (HTTP)", () => { · 219 unmodified lines
411 }),415 }),
412 );416 );
413 417
414 if (execution.status !== "completed") {418 expect(execution.status).toBe("completed");
415 throw new Error(`Expected completed execution, got ${execution.status}`);419 if (execution.status !== "completed") return;
416 }
417 expect(execution.isError).toBe(false);420 expect(execution.isError).toBe(false);
418 expect(execution.structured).toMatchObject({421 expect(execution.structured).toMatchObject({
419 status: "completed",422 status: "completed",
@@ -448,7 +451,7 @@ describe("sources api (HTTP)", () => { · 28 unmodified lines
448 })451 })
449 .pipe(Effect.result),452 .pipe(Effect.result),
450 );453 );
451 expect(addResult._tag).toBe("Failure");454 expect(Result.isFailure(addResult)).toBe(true);
452 455
453 const fetched = yield* asOrg(org, (client) =>456 const fetched = yield* asOrg(org, (client) =>
454 client.mcp.getSource({ params: { scopeId, namespace } }),457 client.mcp.getSource({ params: { scopeId, namespace } }),
@@ -518,9 +521,8 @@ describe("sources api (HTTP)", () => { · 63 unmodified lines
518 }),521 }),
519 );522 );
520 523
521 if (execution.status !== "completed") {524 expect(execution.status).toBe("completed");
522 throw new Error(`Expected completed execution, got ${execution.status}`);525 if (execution.status !== "completed") return;
523 }
524 expect(execution.isError).toBe(false);526 expect(execution.isError).toBe(false);
525 expect(execution.structured).toMatchObject({527 expect(execution.structured).toMatchObject({
526 status: "completed",528 status: "completed",
@@ -587,9 +589,8 @@ describe("sources api (HTTP)", () => { · 60 unmodified lines
587 }),589 }),
588 );590 );
589 591
590 if (execution.status !== "completed") {592 expect(execution.status).toBe("completed");
591 throw new Error(`Expected completed execution, got ${execution.status}`);593 if (execution.status !== "completed") return;
592 }
593 expect(execution.isError).toBe(false);594 expect(execution.isError).toBe(false);
594 expect(execution.structured).toMatchObject({595 expect(execution.structured).toMatchObject({
595 status: "completed",596 status: "completed",
@@ -635,7 +636,7 @@ describe("sources api (HTTP)", () => { · 39 unmodified lines
635 .remove({ params: { scopeId: ScopeId.make(org), sourceId: ghost } })636 .remove({ params: { scopeId: ScopeId.make(org), sourceId: ghost } })
636 .pipe(Effect.result),637 .pipe(Effect.result),
637 );638 );
638 expect(result._tag).toBe("Success");639 expect(Result.isSuccess(result)).toBe(true);
639 }),640 }),
640 );641 );
641 642
@@ -651,7 +652,7 @@ describe("sources api (HTTP)", () => { · 9 unmodified lines
651 .remove({ params: { scopeId: ScopeId.make(org), sourceId: "openapi" } })652 .remove({ params: { scopeId: ScopeId.make(org), sourceId: "openapi" } })
652 .pipe(Effect.result),653 .pipe(Effect.result),
653 );654 );
654 expect(result._tag).toBe("Failure");655 expect(Result.isFailure(result)).toBe(true);
655 }),656 }),
656 );657 );
657 658
packages/core/api/src/handlers/oauth.ts+27−9
@@ -6,7 +6,7 @@ · 5 unmodified lines
6 6
7import { HttpApiBuilder } from "effect/unstable/httpapi";7import { HttpApiBuilder } from "effect/unstable/httpapi";
8import { HttpServerResponse } from "effect/unstable/http";8import { HttpServerResponse } from "effect/unstable/http";
9import { Effect } from "effect";9import { Effect, Option, Predicate, Schema } from "effect";
10 10
11import { runOAuthCallback } from "../oauth-popup";11import { runOAuthCallback } from "../oauth-popup";
12import {12import {
@@ -39,19 +39,35 @@ const resolveOAuthSecretBackedMap = <E extends OAuthProbeError | OAuthStartError · 26 unmodified lines
39 onError: (_error, name) => makeError(`Secret not found for "${name}"`),39 onError: (_error, name) => makeError(`Secret not found for "${name}"`),
40 }).pipe(40 }).pipe(
41 Effect.mapError((error) =>41 Effect.mapError((error) =>
42 error instanceof OAuthProbeError || error instanceof OAuthStartError42 Predicate.isTagged(error, "OAuthProbeError") || Predicate.isTagged(error, "OAuthStartError")
43 ? (error as E)43 ? (error as E)
44 : makeError("Secret resolution failed"),44 : makeError("Secret resolution failed"),
45 ),45 ),
46 );46 );
47 47
48const decodeOAuthStartError = Schema.decodeUnknownOption(OAuthStartError);
49const decodeOAuthCompleteError = Schema.decodeUnknownOption(OAuthCompleteError);
50const decodeOAuthProbeError = Schema.decodeUnknownOption(OAuthProbeError);
51const decodeOAuthSessionNotFoundError = Schema.decodeUnknownOption(OAuthSessionNotFoundError);
52
53const getOAuthErrorMessage = <A extends { readonly message: string }>(
54 error: unknown,
55 decode: (input: unknown) => Option.Option<A>,
56): string | undefined =>
57 Option.match(decode(error), {
58 onNone: () => undefined,
59 onSome: (oauthError) => oauthError.message,
60 });
61
48const toPopupErrorMessage = (error: unknown): string => {62const toPopupErrorMessage = (error: unknown): string => {
49 if (error instanceof OAuthStartError) return error.message;63 const message =
50 if (error instanceof OAuthCompleteError) return error.message;64 getOAuthErrorMessage(error, decodeOAuthStartError) ??
51 if (error instanceof OAuthProbeError) return error.message;65 getOAuthErrorMessage(error, decodeOAuthCompleteError) ??
52 if (error instanceof OAuthSessionNotFoundError) {66 getOAuthErrorMessage(error, decodeOAuthProbeError);
53 return `OAuth session not found: ${error.sessionId}`;67 if (message) return message;
54 }68
69 const sessionNotFound = decodeOAuthSessionNotFoundError(error);
70 if (Option.isSome(sessionNotFound)) return `OAuth session not found: ${sessionNotFound.value.sessionId}`;
55 return "Authentication failed";71 return "Authentication failed";
56};72};
57 73
@@ -147,7 +163,9 @@ export const OAuthHandlers = HttpApiBuilder.group(ExecutorApi, "oauth", (handler · 89 unmodified lines
147 Effect.tapError((cause) =>163 Effect.tapError((cause) =>
148 Effect.logError("OAuth callback completion failed", cause),164 Effect.logError("OAuth callback completion failed", cause),
149 ),165 ),
150 Effect.catchCause(() => Effect.fail(new Error("Authentication failed"))),166 Effect.catchCause(() =>
167 Effect.fail(new OAuthCompleteError({ message: "Authentication failed" })),
168 ),
151 ),169 ),
152 urlParams,170 urlParams,
153 toErrorMessage: toPopupErrorMessage,171 toErrorMessage: toPopupErrorMessage,