All files / lib/api auth.ts

85.71% Statements 6/7
100% Branches 0/0
80% Functions 4/5
85.71% Lines 6/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35                                  1x         4x       4x 4x   1x 1x        
import { Context, Effect, Layer, Option } from 'effect';
 
/**
 * Where `ApiClient` gets the bearer token, read again for every request so a sign-in or a
 * sign-out takes effect on the next call. The template has no sign-in: `AuthToken.none` sends no
 * `Authorization` header. An app with sign-in swaps it in `src/lib/runtime.ts` for
 * `AuthToken.fromGetter(() => <the token your session holds>, onRejected)`; a 401 still reaches
 * the UI as `UnauthorizedError`, and `rejected` is told which token the API refused, so the
 * session can drop that token (and only that one: a sign-in may have replaced it meanwhile).
 */
export class AuthToken extends Context.Service<
  AuthToken,
  {
    readonly get: Effect.Effect<Option.Option<string>>;
    readonly rejected: (token: string) => Effect.Effect<void>;
  }
>()('app/lib/api/AuthToken') {
  static readonly none = Layer.succeed(AuthToken, {
    get: Effect.succeed(Option.none()),
    rejected: () => Effect.void,
  });
 
  static readonly fromGetter = (
    getToken: () => string | null | undefined,
    onRejected?: (token: string) => void,
  ) =>
    Layer.succeed(AuthToken, {
      get: Effect.sync(() => Option.fromNullishOr(getToken())),
      rejected: (token) =>
        Effect.sync(() => {
          onRejected?.(token);
        }),
    });
}