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
44 // single org.
55
66 import { describe, expect, it } from "@effect/vitest";
7−import { Effect } from "effect";
7+import { Effect, Result, Schema } from "effect";
88 import http from "node:http";
99 import { readFileSync } from "node:fs";
1010 import type { AddressInfo } from "node:net";
@@ -173,6 +173,11 @@ const GRAPHQL_INTROSPECTION_RESPONSE = { · 162 unmodified lines
173173 },
174174 };
175175
176+const GraphqlRequestSchema = Schema.Struct({
177+ query: Schema.optional(Schema.String),
178+ variables: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
179+});
180+
176181 const startGraphqlServer = () => {
177182 const requests: Array<{ readonly query: string; readonly variables: unknown }> = [];
178183 const server = http.createServer(async (req, res) => {
@@ -182,10 +187,9 @@ const startGraphqlServer = () => { · 3 unmodified lines
182187 return;
183188 }
184189
185− const parsed = JSON.parse(await readBody(req)) as {
186− readonly query?: string;
187− readonly variables?: Record<string, unknown>;
188− };
190+ const parsed = await Schema.decodeUnknownPromise(Schema.fromJsonString(GraphqlRequestSchema))(
191+ await readBody(req),
192+ );
189193 const query = parsed.query ?? "";
190194 requests.push({ query, variables: parsed.variables ?? null });
191195
@@ -411,9 +415,8 @@ describe("sources api (HTTP)", () => { · 219 unmodified lines
411415 }),
412416 );
413417
414− if (execution.status !== "completed") {
415− throw new Error(`Expected completed execution, got ${execution.status}`);
416− }
418+ expect(execution.status).toBe("completed");
419+ if (execution.status !== "completed") return;
417420 expect(execution.isError).toBe(false);
418421 expect(execution.structured).toMatchObject({
419422 status: "completed",
@@ -448,7 +451,7 @@ describe("sources api (HTTP)", () => { · 28 unmodified lines
448451 })
449452 .pipe(Effect.result),
450453 );
451− expect(addResult._tag).toBe("Failure");
454+ expect(Result.isFailure(addResult)).toBe(true);
452455
453456 const fetched = yield* asOrg(org, (client) =>
454457 client.mcp.getSource({ params: { scopeId, namespace } }),
@@ -518,9 +521,8 @@ describe("sources api (HTTP)", () => { · 63 unmodified lines
518521 }),
519522 );
520523
521− if (execution.status !== "completed") {
522− throw new Error(`Expected completed execution, got ${execution.status}`);
523− }
524+ expect(execution.status).toBe("completed");
525+ if (execution.status !== "completed") return;
524526 expect(execution.isError).toBe(false);
525527 expect(execution.structured).toMatchObject({
526528 status: "completed",
@@ -587,9 +589,8 @@ describe("sources api (HTTP)", () => { · 60 unmodified lines
587589 }),
588590 );
589591
590− if (execution.status !== "completed") {
591− throw new Error(`Expected completed execution, got ${execution.status}`);
592− }
592+ expect(execution.status).toBe("completed");
593+ if (execution.status !== "completed") return;
593594 expect(execution.isError).toBe(false);
594595 expect(execution.structured).toMatchObject({
595596 status: "completed",
@@ -635,7 +636,7 @@ describe("sources api (HTTP)", () => { · 39 unmodified lines
635636 .remove({ params: { scopeId: ScopeId.make(org), sourceId: ghost } })
636637 .pipe(Effect.result),
637638 );
638− expect(result._tag).toBe("Success");
639+ expect(Result.isSuccess(result)).toBe(true);
639640 }),
640641 );
641642
@@ -651,7 +652,7 @@ describe("sources api (HTTP)", () => { · 9 unmodified lines
651652 .remove({ params: { scopeId: ScopeId.make(org), sourceId: "openapi" } })
652653 .pipe(Effect.result),
653654 );
654− expect(result._tag).toBe("Failure");
655+ expect(Result.isFailure(result)).toBe(true);
655656 }),
656657 );
657658
packages/core/api/src/handlers/oauth.ts+27−9
@@ -6,7 +6,7 @@ · 5 unmodified lines
66
77 import { HttpApiBuilder } from "effect/unstable/httpapi";
88 import { HttpServerResponse } from "effect/unstable/http";
9−import { Effect } from "effect";
9+import { Effect, Option, Predicate, Schema } from "effect";
1010
1111 import { runOAuthCallback } from "../oauth-popup";
1212 import {
@@ -39,19 +39,35 @@ const resolveOAuthSecretBackedMap = <E extends OAuthProbeError | OAuthStartError · 26 unmodified lines
3939 onError: (_error, name) => makeError(`Secret not found for "${name}"`),
4040 }).pipe(
4141 Effect.mapError((error) =>
42− error instanceof OAuthProbeError || error instanceof OAuthStartError
42+ Predicate.isTagged(error, "OAuthProbeError") || Predicate.isTagged(error, "OAuthStartError")
4343 ? (error as E)
4444 : makeError("Secret resolution failed"),
4545 ),
4646 );
4747
48+const decodeOAuthStartError = Schema.decodeUnknownOption(OAuthStartError);
49+const decodeOAuthCompleteError = Schema.decodeUnknownOption(OAuthCompleteError);
50+const decodeOAuthProbeError = Schema.decodeUnknownOption(OAuthProbeError);
51+const decodeOAuthSessionNotFoundError = Schema.decodeUnknownOption(OAuthSessionNotFoundError);
52+
53+const 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+
4862 const toPopupErrorMessage = (error: unknown): string => {
49− if (error instanceof OAuthStartError) return error.message;
50− if (error instanceof OAuthCompleteError) return error.message;
51− if (error instanceof OAuthProbeError) return error.message;
52− if (error instanceof OAuthSessionNotFoundError) {
53− return `OAuth session not found: ${error.sessionId}`;
54− }
63+ const message =
64+ getOAuthErrorMessage(error, decodeOAuthStartError) ??
65+ getOAuthErrorMessage(error, decodeOAuthCompleteError) ??
66+ getOAuthErrorMessage(error, decodeOAuthProbeError);
67+ if (message) return message;
68+
69+ const sessionNotFound = decodeOAuthSessionNotFoundError(error);
70+ if (Option.isSome(sessionNotFound)) return `OAuth session not found: ${sessionNotFound.value.sessionId}`;
5571 return "Authentication failed";
5672 };
5773
@@ -147,7 +163,9 @@ export const OAuthHandlers = HttpApiBuilder.group(ExecutorApi, "oauth", (handler · 89 unmodified lines
147163 Effect.tapError((cause) =>
148164 Effect.logError("OAuth callback completion failed", cause),
149165 ),
150− Effect.catchCause(() => Effect.fail(new Error("Authentication failed"))),
166+ Effect.catchCause(() =>
167+ Effect.fail(new OAuthCompleteError({ message: "Authentication failed" })),
168+ ),
151169 ),
152170 urlParams,
153171 toErrorMessage: toPopupErrorMessage,