sukovanej / effect-http

Declarative HTTP API library for effect-ts
https://sukovanej.github.io/effect-http
MIT License
218 stars 16 forks source link

Services in Security implementation is leaked into the client #569

Closed Almaju closed 1 month ago

Almaju commented 1 month ago

When using a service inside a custom Security, it becomes a requirement of the client endpoint although it should be only used on the server side.

Look below how the effect has the type: Effect.Effect<string, ClientError<number>, UserStorage>

import { Schema } from '@effect/schema';
import { Effect, Layer, pipe } from 'effect';
import {
  Api,
  ApiEndpoint,
  Client,
  Middlewares,
  RouterBuilder,
  Security,
} from 'effect-http';

interface UserInfo {
  email: string;
}

class UserStorage extends Effect.Tag('UserStorage')<
  UserStorage,
  { getInfo: (user: string) => Effect.Effect<UserInfo> }
>() {
  static dummy = Layer.succeed(
    UserStorage,
    UserStorage.of({
      getInfo: (_: string) => Effect.succeed({ email: 'email@gmail.com' }),
    }),
  );
}

const mySecurity = pipe(
  Security.basic({ description: 'My basic auth' }),
  Security.map((creds) => creds.user),
  Security.mapEffect((user) => UserStorage.getInfo(user)),
);

const api = Api.make().pipe(
  Api.addEndpoint(
    Api.post('endpoint', '/my-secured-endpoint').pipe(
      Api.setResponseBody(Schema.String),
      Api.setSecurity(mySecurity),
    ),
  ),
);

const app = RouterBuilder.make(api).pipe(
  RouterBuilder.handle('endpoint', (_, security) =>
    Effect.succeed(`Logged in`),
  ),
  RouterBuilder.build,
  Middlewares.errorLog,
);

const client = Client.make(api);

// BAD: Effect.Effect<string, ClientError<number>, UserStorage>
const effect = client.endpoint({});

I have written a quick type helper as a workaround:

type ApiClient<T> =
  T extends Api.Api<infer Endpoints> ? Api.Api<MapEndpoints<Endpoints>> : never;

type MapEndpoints<T> =
  T extends ApiEndpoint.ApiEndpoint<
    infer Endpoint,
    infer Req,
    infer Resp,
    Security.Security<infer A, infer E, unknown>
  >
    ? ApiEndpoint.ApiEndpoint<
        Endpoint,
        Req,
        Resp,
        Security.Security<A, E, never>
      >
    : never;

const client = Client.make(api as ApiClient<typeof api>);

// GOOD: Effect.Effect<string, ClientError<number>, never>
const effect = client.endpoint({});

I will try to do a PR when I find the time.

sukovanej commented 1 month ago

Nice catch! I'll take a look.