History

Clean cloud auth and Autumn boundaries

Rhys Sullivan committed 6 months ago

Commit d401d651 parent b039b6e4 files · +36 −22Browse files

Unified · Split

4 files
  1. apps/cloud/src/api/autumn.ts +12 −16
  2. apps/cloud/src/auth/handlers.node.test.ts +11 −4
  3. apps/cloud/src/auth/workos.ts +10 −2
  4. apps/cloud/src/observability.test.ts +3 −0
apps/cloud/src/api/autumn.ts+12−16
@@ -1,5 +1,5 @@
11 import { env } from "cloudflare:workers";
2−import { Effect } from "effect";
2+import { Cause, Effect } from "effect";
33 import {
44 HttpRouter,
55 HttpServerRequest,
@@ -30,13 +30,11 @@ const handler = Effect.gen(function* () { · 24 unmodified lines
3030 const session = yield* workos.authenticateRequest(webRequest);
3131
3232 if (!session || !session.organizationId) {
33− return yield* Effect.fail(
34− new HttpResponseError({
35− status: 401,
36− code: "unauthorized",
37− message: "Unauthorized",
38− }),
39− );
33+ return yield* new HttpResponseError({
34+ status: 401,
35+ code: "unauthorized",
36+ message: "Unauthorized",
37+ });
4038 }
4139
4240 const url = new URL(webRequest.url);
@@ -74,20 +72,18 @@ const handler = Effect.gen(function* () { · 31 unmodified lines
7472
7573 if (statusCode >= 400) {
7674 console.error("[autumn] upstream error:", statusCode, response);
77− return yield* Effect.fail(
78− new HttpResponseError({
79− status: statusCode,
80− code: "billing_request_failed",
81− message: "Billing request failed",
82− }),
83− );
75+ return yield* new HttpResponseError({
76+ status: statusCode,
77+ code: "billing_request_failed",
78+ message: "Billing request failed",
79+ });
8480 }
8581
8682 return HttpServerResponse.jsonUnsafe(response, { status: statusCode });
8783 }).pipe(
8884 Effect.catchCause((err) => {
8985 if (isServerError(err)) {
90− console.error("[autumn] request failed:", err instanceof Error ? err.stack : err);
86+ console.error("[autumn] request failed:", Cause.pretty(err));
9187 }
9288 return Effect.succeed(toErrorServerResponse(err));
9389 }),
apps/cloud/src/auth/handlers.node.test.ts+11−4
@@ -1,7 +1,7 @@
11 import { HttpApiBuilder, HttpApi } from "effect/unstable/httpapi";
22 import { HttpRouter, HttpServer } from "effect/unstable/http";
33 import { describe, expect, it } from "@effect/vitest";
4−import { Effect, Layer } from "effect";
4+import { Data, Effect, Layer } from "effect";
55 import type { Effect as EffectType } from "effect/Effect";
66
77 import { CloudAuthPublicApi } from "./api";
@@ -31,15 +31,22 @@ const fakeUser: AuthenticateWithCodeResult["user"] = { · 23 unmodified lines
3131 metadata: {},
3232 };
3333
34+class UnstubbedWorkOSMethod extends Data.TaggedError("UnstubbedWorkOSMethod")<{
35+ method: string;
36+}> {}
37+
3438 const makeAuthFetch = (workos: Partial<WorkOSAuth["Service"]>) => {
3539 const WorkOSTest = Layer.succeed(
3640 WorkOSAuth,
3741 new Proxy(workos as WorkOSAuth["Service"], {
3842 get: (target, prop) => {
3943 if (prop in target) return target[prop as keyof typeof target];
40− return () => {
41− throw new Error(`WorkOSAuth.${String(prop)} not stubbed`);
42− };
44+ return () =>
45+ Effect.fail(
46+ new UnstubbedWorkOSMethod({
47+ method: typeof prop === "string" ? prop : (prop.description ?? "symbol"),
48+ }),
49+ );
4350 },
4451 }),
4552 );
apps/cloud/src/auth/workos.ts+10−2
@@ -3,11 +3,17 @@ · 2 unmodified lines
33 // ---------------------------------------------------------------------------
44
55 import { env } from "cloudflare:workers";
6−import { Context, Effect, Layer } from "effect";
6+import { Context, Data, Effect, Layer } from "effect";
77 import { GeneratePortalLinkIntent, WorkOS } from "@workos-inc/node/worker";
88 import { WorkOSError, tryPromiseService, withServiceLogging } from "./errors";
99
1010 const COOKIE_NAME = "wos-session";
11+const INVALID_COOKIE_PASSWORD_MESSAGE =
12+ "WORKOS_COOKIE_PASSWORD must be at least 32 characters";
13+
14+class WorkOSAuthConfigurationError extends Data.TaggedError("WorkOSAuthConfigurationError")<{
15+ readonly message: string;
16+}> {}
1117
1218 // ---------------------------------------------------------------------------
1319 // Service
@@ -19,7 +25,9 @@ const make = Effect.gen(function* () { · 5 unmodified lines
1925 const cookiePassword = env.WORKOS_COOKIE_PASSWORD;
2026
2127 if (!cookiePassword || cookiePassword.length < 32) {
22− return yield* Effect.die(new Error("WORKOS_COOKIE_PASSWORD must be at least 32 characters"));
28+ return yield* new WorkOSAuthConfigurationError({
29+ message: INVALID_COOKIE_PASSWORD_MESSAGE,
30+ });
2331 }
2432
2533 const workos = new WorkOS({ apiKey, clientId });
apps/cloud/src/observability.test.ts+3−0
@@ -15,6 +15,7 @@ describe("sentryPayloadForCause", () => { · 14 unmodified lines
1515 // Reproduces the production chain: an inner runPromise rejects with a
1616 // CauseImpl (from Effect v4's causeSquash), Effect.promise re-wraps it
1717 // as Die(CauseImpl), and the outer catchCause receives this shape.
18+ // oxlint-disable-next-line executor/no-error-constructor -- boundary: observability test must build a real Error for Sentry-compatible payload assertions
1819 const innerCause = Cause.fail(new Error("inner failure"));
1920 const outerCause = Cause.die(innerCause);
2021
@@ -25,11 +26,13 @@ describe("sentryPayloadForCause", () => { · 4 unmodified lines
2526 });
2627
2728 it("hands Sentry a real Error for an ordinary failed Cause", () => {
29+ // oxlint-disable-next-line executor/no-error-constructor -- boundary: observability test must build a real Error for Sentry-compatible payload assertions
2830 const { primary } = sentryPayloadForCause(Cause.fail(new Error("plain failure")));
2931 expect(looksLikeErrorToSentry(primary)).toBe(true);
3032 });
3133
3234 it("forwards non-Cause inputs as-is with no pretty cause attached", () => {
35+ // oxlint-disable-next-line executor/no-error-constructor -- boundary: observability test must build a real Error for Sentry-compatible payload assertions
3336 const err = new Error("raw");
3437 const { primary, pretty } = sentryPayloadForCause(err);
3538 expect(primary).toBe(err);