Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 9 additions & 19 deletions apps/cloud/src/auth/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context, Effect, Layer } from "effect";
import { makeUserStore } from "../services/user-store";
import { DbService } from "../services/db";
import { UserStoreError } from "./errors";
import { UserStoreError, withServiceLogging } from "./errors";

// AuthContext is defined in ./middleware.ts to keep middleware-related types together.
export { AuthContext } from "./middleware";
Expand All @@ -12,24 +12,14 @@ export { AuthContext } from "./middleware";

type RawStore = ReturnType<typeof makeUserStore>;

const makeService = (store: RawStore) => {
const use = <A>(fn: (s: RawStore) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(store),
catch: (cause) => cause,
}).pipe(
Effect.tapError((cause) =>
Effect.sync(() => {
// eslint-disable-next-line no-console
console.error("[user_store] query failed:", cause);
}),
),
Effect.mapError(() => new UserStoreError()),
Effect.withSpan("user_store"),
);

return { use };
};
const makeService = (store: RawStore) => ({
use: <A>(fn: (s: RawStore) => Promise<A>) =>
withServiceLogging(
"user_store",
() => new UserStoreError(),
Effect.tryPromise({ try: () => fn(store), catch: (e) => e }),
),
});

type UserStoreServiceType = ReturnType<typeof makeService>;

Expand Down
29 changes: 24 additions & 5 deletions apps/cloud/src/auth/errors.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { HttpApiSchema } from "@effect/platform";
import { Schema } from "effect";
import { Effect, Schema } from "effect";

// Tagged errors returned to HTTP clients. The response payload must NEVER
// include internal details (SQL, stack traces, WorkOS error bodies) — those
// can leak schema + secrets. Keep the schema empty and log the `cause`
// server-side via Effect's logger when the error is constructed.
export class UserStoreError extends Schema.TaggedError<UserStoreError>()(
"UserStoreError",
{},
Expand All @@ -16,3 +12,26 @@ export class WorkOSError extends Schema.TaggedError<WorkOSError>()(
{},
HttpApiSchema.annotations({ status: 500 }),
) {}

/**
* Service-boundary error wrapper. Logs the full Cause chain (drizzle
* query/params, pg error codes, nested Error.cause, etc.) via Effect's
* structured logger, then maps to a tagged error so the HTTP wire
* response contains only safe fields.
*
* Use this whenever a Promise-based API gets lifted into an Effect and
* its failure needs both debuggable server-side logging and a safe
* public shape.
*/
export const withServiceLogging = <A, E, R>(
name: string,
publicError: () => E,
effect: Effect.Effect<A, unknown, R>,
): Effect.Effect<A, E, R> =>
effect.pipe(
Effect.tapErrorCause((cause) =>
Effect.logError(`${name} failed`, cause),
),
Effect.mapError(publicError),
Effect.withSpan(name),
);
18 changes: 5 additions & 13 deletions apps/cloud/src/auth/workos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { Context, Effect, Layer } from "effect";
import { WorkOS } from "@workos-inc/node/worker";
import { WorkOSError } from "./errors";
import { WorkOSError, withServiceLogging } from "./errors";
import { server } from "../env";

const COOKIE_NAME = "wos-session";
Expand All @@ -26,18 +26,10 @@ const make = Effect.gen(function* () {
const workos = new WorkOS({ apiKey, clientId });

const use = <A>(fn: (wos: WorkOS) => Promise<A>) =>
Effect.tryPromise({
try: () => fn(workos),
catch: (cause) => cause,
}).pipe(
Effect.tapError((cause) =>
Effect.sync(() => {
// eslint-disable-next-line no-console
console.error("[workos] call failed:", cause);
}),
),
Effect.mapError(() => new WorkOSError()),
Effect.withSpan("workos"),
withServiceLogging(
"workos",
() => new WorkOSError(),
Effect.tryPromise({ try: () => fn(workos), catch: (e) => e }),
);

const authenticateSealedSession = (sessionData: string) =>
Expand Down
15 changes: 7 additions & 8 deletions packages/core/sdk/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Schema } from "effect";
import { Data, Schema } from "effect";

import { ToolId, SecretId, PolicyId } from "./ids";

Expand All @@ -7,14 +7,13 @@ export class ToolNotFoundError extends Schema.TaggedError<ToolNotFoundError>()(
{ toolId: ToolId },
) {}

export class ToolInvocationError extends Schema.TaggedError<ToolInvocationError>()(
export class ToolInvocationError extends Data.TaggedError(
"ToolInvocationError",
{
toolId: ToolId,
message: Schema.String,
cause: Schema.optional(Schema.Unknown),
},
) {}
)<{
readonly toolId: ToolId;
readonly message: string;
readonly cause?: unknown;
}> {}

export class SecretNotFoundError extends Schema.TaggedError<SecretNotFoundError>()(
"SecretNotFoundError",
Expand Down
6 changes: 2 additions & 4 deletions packages/plugins/google-discovery/src/sdk/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ const DiscoveryDocumentModel = Schema.Struct({
});
type DiscoveryDocument = typeof DiscoveryDocumentModel.Type;

const toParseError = (message: string, error: unknown) =>
new GoogleDiscoveryParseError({ message, error });
const toParseError = (message: string, cause: unknown) =>
new GoogleDiscoveryParseError({ message, cause });

const decodeUnknownWith = <A>(
message: string,
Expand Down Expand Up @@ -436,7 +436,6 @@ const manifestMethodFromMethod = (input: {
if (!method.httpMethod) {
return yield* new GoogleDiscoveryParseError({
message: `Google Discovery method '${methodId}' is missing httpMethod`,
error: new Error(`Missing httpMethod for ${methodId}`),
});
}

Expand Down Expand Up @@ -509,7 +508,6 @@ export const extractGoogleDiscoveryManifest = Effect.fn(
return yield* new GoogleDiscoveryParseError({
message:
"Google Discovery document is missing one of: name, version, rootUrl",
error: new Error("Invalid document"),
});
}

Expand Down
27 changes: 13 additions & 14 deletions packages/plugins/google-discovery/src/sdk/errors.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { Schema } from "effect";
import { Data, Schema } from "effect";
import type { Option } from "effect";

export class GoogleDiscoveryParseError extends Schema.TaggedError<GoogleDiscoveryParseError>()(
export class GoogleDiscoveryParseError extends Data.TaggedError(
"GoogleDiscoveryParseError",
{
message: Schema.String,
error: Schema.Defect,
},
) {}
)<{
readonly message: string;
readonly cause?: unknown;
}> {}

export class GoogleDiscoveryInvocationError extends Schema.TaggedError<GoogleDiscoveryInvocationError>()(
export class GoogleDiscoveryInvocationError extends Data.TaggedError(
"GoogleDiscoveryInvocationError",
{
message: Schema.String,
statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }),
error: Schema.Defect,
},
) {}
)<{
readonly message: string;
readonly statusCode: Option.Option<number>;
readonly cause?: unknown;
}> {}

export class GoogleDiscoveryOAuthError extends Schema.TaggedError<GoogleDiscoveryOAuthError>()(
"GoogleDiscoveryOAuthError",
Expand Down
3 changes: 1 addition & 2 deletions packages/plugins/google-discovery/src/sdk/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ const invoke = Effect.fn("GoogleDiscovery.invoke")(function* (input: {
return yield* new GoogleDiscoveryInvocationError({
message: `Missing required ${parameter.location} parameter: ${parameter.name}`,
statusCode: Option.none(),
error: undefined,
});
}
continue;
Expand Down Expand Up @@ -327,7 +326,7 @@ const invoke = Effect.fn("GoogleDiscovery.invoke")(function* (input: {
new GoogleDiscoveryInvocationError({
message: `HTTP request failed: ${err.message}`,
statusCode: Option.none(),
error: err,
cause: err,
}),
),
);
Expand Down
17 changes: 8 additions & 9 deletions packages/plugins/graphql/src/sdk/errors.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Schema } from "effect";
import { Data, Schema } from "effect";
import type { Option } from "effect";

export class GraphqlIntrospectionError extends Schema.TaggedError<GraphqlIntrospectionError>()(
"GraphqlIntrospectionError",
{
message: Schema.String,
error: Schema.Defect,
},
) {}

Expand All @@ -15,11 +15,10 @@ export class GraphqlExtractionError extends Schema.TaggedError<GraphqlExtraction
},
) {}

export class GraphqlInvocationError extends Schema.TaggedError<GraphqlInvocationError>()(
export class GraphqlInvocationError extends Data.TaggedError(
"GraphqlInvocationError",
{
message: Schema.String,
statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }),
error: Schema.Defect,
},
) {}
)<{
readonly message: string;
readonly statusCode: Option.Option<number>;
readonly cause?: unknown;
}> {}
20 changes: 9 additions & 11 deletions packages/plugins/graphql/src/sdk/introspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,29 +149,31 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* (
}

const response = yield* client.execute(request).pipe(
Effect.tapErrorCause((cause) =>
Effect.logError("graphql introspection request failed", cause),
),
Effect.mapError(
(err) =>
new GraphqlIntrospectionError({
message: `Failed to reach GraphQL endpoint: ${err.message}`,
error: err,
}),
),
);

if (response.status !== 200) {
const body = yield* response.text.pipe(Effect.catchAll(() => Effect.succeed("")));
return yield* new GraphqlIntrospectionError({
message: `Introspection failed with status ${response.status}: ${body}`,
error: undefined,
message: `Introspection failed with status ${response.status}`,
});
}

const raw = yield* response.json.pipe(
Effect.tapErrorCause((cause) =>
Effect.logError("graphql introspection JSON parse failed", cause),
),
Effect.mapError(
(err) =>
() =>
new GraphqlIntrospectionError({
message: `Failed to parse introspection response as JSON`,
error: err,
}),
),
);
Expand All @@ -180,16 +182,13 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* (

if (json.errors && Array.isArray(json.errors) && json.errors.length > 0) {
return yield* new GraphqlIntrospectionError({
// @effect-diagnostics-next-line preferSchemaOverJson:off
message: `Introspection returned errors: ${JSON.stringify(json.errors)}`,
error: undefined,
message: `Introspection returned ${json.errors.length} error(s)`,
});
}

if (!json.data?.__schema) {
return yield* new GraphqlIntrospectionError({
message: "Introspection response missing __schema",
error: undefined,
});
}

Expand All @@ -216,6 +215,5 @@ export const parseIntrospectionJson = (
catch: (err) =>
new GraphqlIntrospectionError({
message: `Failed to parse introspection JSON: ${err instanceof Error ? err.message : String(err)}`,
error: err,
}),
});
2 changes: 1 addition & 1 deletion packages/plugins/graphql/src/sdk/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* (
new GraphqlInvocationError({
message: `GraphQL request failed: ${err.message}`,
statusCode: Option.none(),
error: err,
cause: err,
}),
),
);
Expand Down
13 changes: 5 additions & 8 deletions packages/plugins/keychain/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Schema } from "effect";
import { Data } from "effect";

export class KeychainError extends Schema.TaggedError<KeychainError>()(
"KeychainError",
{
message: Schema.String,
cause: Schema.optional(Schema.Unknown),
},
) {}
export class KeychainError extends Data.TaggedError("KeychainError")<{
readonly message: string;
readonly cause?: unknown;
}> {}
17 changes: 8 additions & 9 deletions packages/plugins/openapi/src/sdk/errors.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Schema } from "effect";
import { Data, Schema } from "effect";
import type { Option } from "effect";

export class OpenApiParseError extends Schema.TaggedError<OpenApiParseError>()(
"OpenApiParseError",
{
message: Schema.String,
error: Schema.Defect,
},
) {}

Expand All @@ -15,11 +15,10 @@ export class OpenApiExtractionError extends Schema.TaggedError<OpenApiExtraction
},
) {}

export class OpenApiInvocationError extends Schema.TaggedError<OpenApiInvocationError>()(
export class OpenApiInvocationError extends Data.TaggedError(
"OpenApiInvocationError",
{
message: Schema.String,
statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }),
error: Schema.Defect,
},
) {}
)<{
readonly message: string;
readonly statusCode: Option.Option<number>;
readonly cause?: unknown;
}> {}
4 changes: 1 addition & 3 deletions packages/plugins/openapi/src/sdk/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* (
return yield* new OpenApiInvocationError({
message: `Missing required path parameter: ${param.name}`,
statusCode: Option.none(),
error: undefined,
});
}
continue;
Expand Down Expand Up @@ -111,7 +110,6 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* (
return yield* new OpenApiInvocationError({
message: `Unresolved path parameters: ${[...new Set(unresolved)].join(", ")}`,
statusCode: Option.none(),
error: undefined,
});
}

Expand Down Expand Up @@ -245,7 +243,7 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* (
new OpenApiInvocationError({
message: `HTTP request failed: ${err.message}`,
statusCode: Option.none(),
error: err,
cause: err,
}),
),
);
Expand Down
Loading