import { HttpApiBuilder } from "effect/unstable/httpapi";import { HttpRouter } from "effect/unstable/http";import { Effect, Layer } from "effect"; import { SystemError, SystemHttpApi } from "./api";import { BetterAuth, countOrgMembers, type BetterAuthHandle } from "../auth/better-auth";import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db";import { findRedeemableCode } from "../auth/invites"; // ---------------------------------------------------------------------------// Handlers for the public system API. Unauthenticated; every DB touch is an// Effect.tryPromise. `health` fails soft (a DB hiccup reports "degraded", it// never throws); `setup-status` reports whether the one org has zero members.// --------------------------------------------------------------------------- export const SystemHandlers = HttpApiBuilder.group(SystemHttpApi, "system", (handlers) => handlers .handle("health", () => Effect.gen(function* () { const { client } = yield* SelfHostDb; const status = yield* Effect.tryPromise({ try: () => client.execute("SELECT 1"), catch: () => new SystemError({ message: "database unreachable" }), }).pipe( Effect.as("ok"), Effect.orElseSucceed(() => "degraded"), ); return { status }; }), ) .handle("setupStatus", () => Effect.gen(function* () { const { auth, organizationId } = yield* BetterAuth; // Count via Better Auth's adapter (see countOrgMembers) so this read is // consistent with how memberships are written. const count = yield* Effect.tryPromise({ try: () => countOrgMembers(auth, organizationId), catch: () => new SystemError({ message: "failed to read setup status" }), }); return { needsSetup: count === 0 }; }), ) .handle("inviteStatus", ({ params }) => Effect.gen(function* () { const { client } = yield* SelfHostDb; const code = yield* Effect.tryPromise({ try: () => findRedeemableCode(client, params.code), catch: () => new SystemError({ message: "failed to read invite status" }), }); return { valid: code !== null }; }), ),); export interface SelfHostSystemApiDeps { readonly betterAuth: BetterAuthHandle; readonly db: SelfHostDbHandle; readonly mountPrefix: `/${string}`;} /** Mountable extension route layer (see makeSelfHostAdminApiLayer). */export const makeSelfHostSystemApiLayer = ({ betterAuth, db, mountPrefix,}: SelfHostSystemApiDeps) => { const prefixedRouter = Layer.effect(HttpRouter.HttpRouter)( Effect.map(HttpRouter.HttpRouter.asEffect(), (router) => router.prefixed(mountPrefix)), ); return HttpApiBuilder.layer(SystemHttpApi).pipe( Layer.provide(SystemHandlers), Layer.provide(prefixedRouter), HttpRouter.provideRequest( Layer.mergeAll(Layer.succeed(BetterAuth)(betterAuth), Layer.succeed(SelfHostDb)(db)), ), );};
import { HttpApiBuilder } from "effect/unstable/httpapi";
import { HttpRouter } from "effect/unstable/http";
import { Effect, Layer } from "effect";
import { SystemError, SystemHttpApi } from "./api";
import { BetterAuth, countOrgMembers, type BetterAuthHandle } from "../auth/better-auth";
import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db";
import { findRedeemableCode } from "../auth/invites";
// ---------------------------------------------------------------------------
// Handlers for the public system API. Unauthenticated; every DB touch is an
// Effect.tryPromise. `health` fails soft (a DB hiccup reports "degraded", it
// never throws); `setup-status` reports whether the one org has zero members.
// ---------------------------------------------------------------------------
export const SystemHandlers = HttpApiBuilder.group(SystemHttpApi, "system", (handlers) =>
handlers
.handle("health", () =>
Effect.gen(function* () {
const { client } = yield* SelfHostDb;
const status = yield* Effect.tryPromise({
try: () => client.execute("SELECT 1"),
catch: () => new SystemError({ message: "database unreachable" }),
}).pipe(
Effect.as("ok"),
Effect.orElseSucceed(() => "degraded"),
);
return { status };
}),
)
.handle("setupStatus", () =>
Effect.gen(function* () {
const { auth, organizationId } = yield* BetterAuth;
// Count via Better Auth's adapter (see countOrgMembers) so this read is
// consistent with how memberships are written.
const count = yield* Effect.tryPromise({
try: () => countOrgMembers(auth, organizationId),
catch: () => new SystemError({ message: "failed to read setup status" }),
});
return { needsSetup: count === 0 };
}),
)
.handle("inviteStatus", ({ params }) =>
Effect.gen(function* () {
const { client } = yield* SelfHostDb;
const code = yield* Effect.tryPromise({
try: () => findRedeemableCode(client, params.code),
catch: () => new SystemError({ message: "failed to read invite status" }),
});
return { valid: code !== null };
}),
),
);
export interface SelfHostSystemApiDeps {
readonly betterAuth: BetterAuthHandle;
readonly db: SelfHostDbHandle;
readonly mountPrefix: `/${string}`;
}
/** Mountable extension route layer (see makeSelfHostAdminApiLayer). */
export const makeSelfHostSystemApiLayer = ({
betterAuth,
db,
mountPrefix,
}: SelfHostSystemApiDeps) => {
const prefixedRouter = Layer.effect(HttpRouter.HttpRouter)(
Effect.map(HttpRouter.HttpRouter.asEffect(), (router) => router.prefixed(mountPrefix)),
);
return HttpApiBuilder.layer(SystemHttpApi).pipe(
Layer.provide(SystemHandlers),
Layer.provide(prefixedRouter),
HttpRouter.provideRequest(
Layer.mergeAll(Layer.succeed(BetterAuth)(betterAuth), Layer.succeed(SelfHostDb)(db)),
),
);
};