1import { RuntimeContext } from "alchemy";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Schema from "effect/Schema";
5import * as HttpApiMiddleware from "effect/http-api/HttpApiMiddleware";
6import { Auth } from "./auth.ts";
7import { CurrentUser } from "./current-user.ts";
8
9export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
10 "Unauthorized",
11 {},
12 { httpApiStatus: 401 },
13) {}
14
15export class AuthenticationUnavailable extends Schema.TaggedError<AuthenticationUnavailable>()(
16 "AuthenticationUnavailable",
17 {},
18 { httpApiStatus: 503 },
19) {}
20
21export class Authentication extends HttpApiMiddleware.Service<
22 Authentication,
23 { provides: CurrentUser }
24>()("app/Authentication", {
25 error: [Unauthorized, AuthenticationUnavailable],
26}) {
27 static readonly layer = Layer.effect(
28 Authentication,
29 Effect.gen(function* () {
30 const auth = yield* Auth;
31 return (httpEffect) =>
32 Effect.gen(function* () {
33 const session = yield* auth.getSession().pipe(
34 Effect.mapError(() => new AuthenticationUnavailable()),
35 Effect.catchDefect(() =>
36 Effect.fail(new AuthenticationUnavailable()),
37 ),
38 );
39 if (session === null) return yield* new Unauthorized();
40 return yield* Effect.provideService(
41 httpEffect,
42 CurrentUser,
43 session.user,
44 );
45 }).pipe(
46 // HttpApi does not carry Alchemy's runtime marker; the Worker owns request scope.
47 Effect.provide(RuntimeContext.phantom),
48 );
49 }),
50 );
51}