import { env } from "cloudflare:workers";import { Cause, Effect } from "effect";import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";import { autumnHandler } from "autumn-js/backend"; import { WorkOSClient } from "../../auth/workos";import { ORG_SELECTOR_HEADER, authorizeOrganizationSelector } from "../../auth/organization";import { HttpResponseError, isServerError, toErrorServerResponseEffect,} from "../../api/error-response"; type BillingSession = { readonly userId: string; readonly organizationId?: string | null;}; export const resolveBillingOrganization = (request: Request, session: BillingSession) => Effect.gen(function* () { const selector = request.headers.get(ORG_SELECTOR_HEADER) ?? session.organizationId; if (!selector) { return yield* new HttpResponseError({ status: 401, code: "unauthorized", message: "Unauthorized", }); } const org = yield* authorizeOrganizationSelector(session.userId, selector); if (!org) { return yield* new HttpResponseError({ status: 403, code: "forbidden", message: "Forbidden", }); } return org; }); const handler = Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; const webRequest = yield* Effect.mapError( HttpServerRequest.toWeb(request), () => new HttpResponseError({ status: 500, code: "invalid_request", message: "Invalid request", }), ); const workos = yield* WorkOSClient; const session = yield* workos.authenticateRequest(webRequest); if (!session) { return yield* new HttpResponseError({ status: 401, code: "unauthorized", message: "Unauthorized", }); } const org = yield* resolveBillingOrganization(webRequest, session); const url = new URL(webRequest.url); const body = request.method !== "GET" && request.method !== "HEAD" ? yield* Effect.mapError( request.json, () => new HttpResponseError({ status: 400, code: "invalid_json", message: "Invalid request body", }), ) : undefined; const { statusCode, response } = yield* Effect.promise(() => autumnHandler({ request: { url: url.pathname, method: request.method, body, }, customerId: org.id, customerData: { name: session.email, email: session.email, }, clientOptions: { secretKey: env.AUTUMN_SECRET_KEY ?? "", // autumn-js's handler reads `baseURL` to override the Autumn endpoint // (not `serverURL`, which it silently ignores). Without this, a non-prod // AUTUMN_API_URL (the e2e emulator, a self-hosted Autumn) is dropped and // every billing request goes to production Autumn and 401s. ...(env.AUTUMN_API_URL ? { baseURL: env.AUTUMN_API_URL } : {}), }, pathPrefix: "/api/billing", }), ); if (statusCode >= 400) { console.error("[autumn] upstream error:", statusCode, response); return yield* new HttpResponseError({ status: statusCode, code: "billing_request_failed", message: "Billing request failed", }); } return HttpServerResponse.jsonUnsafe(response, { status: statusCode });}).pipe( Effect.catchCause((err) => { if (isServerError(err)) { console.error("[autumn] request failed:", Cause.pretty(err)); } return toErrorServerResponseEffect(err); }),); export const AutumnRoutesLive = HttpRouter.add("*", "/api/billing/*", handler);
import { env } from "cloudflare:workers";
import { Cause, Effect } from "effect";
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
import { autumnHandler } from "autumn-js/backend";
import { WorkOSClient } from "../../auth/workos";
import { ORG_SELECTOR_HEADER, authorizeOrganizationSelector } from "../../auth/organization";
import {
HttpResponseError,
isServerError,
toErrorServerResponseEffect,
} from "../../api/error-response";
type BillingSession = {
readonly userId: string;
readonly organizationId?: string | null;
};
export const resolveBillingOrganization = (request: Request, session: BillingSession) =>
Effect.gen(function* () {
const selector = request.headers.get(ORG_SELECTOR_HEADER) ?? session.organizationId;
if (!selector) {
return yield* new HttpResponseError({
status: 401,
code: "unauthorized",
message: "Unauthorized",
});
}
const org = yield* authorizeOrganizationSelector(session.userId, selector);
if (!org) {
return yield* new HttpResponseError({
status: 403,
code: "forbidden",
message: "Forbidden",
});
}
return org;
});
const handler = Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const webRequest = yield* Effect.mapError(
HttpServerRequest.toWeb(request),
() =>
new HttpResponseError({
status: 500,
code: "invalid_request",
message: "Invalid request",
}),
);
const workos = yield* WorkOSClient;
const session = yield* workos.authenticateRequest(webRequest);
if (!session) {
return yield* new HttpResponseError({
status: 401,
code: "unauthorized",
message: "Unauthorized",
});
}
const org = yield* resolveBillingOrganization(webRequest, session);
const url = new URL(webRequest.url);
const body =
request.method !== "GET" && request.method !== "HEAD"
? yield* Effect.mapError(
request.json,
() =>
new HttpResponseError({
status: 400,
code: "invalid_json",
message: "Invalid request body",
}),
)
: undefined;
const { statusCode, response } = yield* Effect.promise(() =>
autumnHandler({
request: {
url: url.pathname,
method: request.method,
body,
},
customerId: org.id,
customerData: {
name: session.email,
email: session.email,
},
clientOptions: {
secretKey: env.AUTUMN_SECRET_KEY ?? "",
// autumn-js's handler reads `baseURL` to override the Autumn endpoint
// (not `serverURL`, which it silently ignores). Without this, a non-prod
// AUTUMN_API_URL (the e2e emulator, a self-hosted Autumn) is dropped and
// every billing request goes to production Autumn and 401s.
...(env.AUTUMN_API_URL ? { baseURL: env.AUTUMN_API_URL } : {}),
},
pathPrefix: "/api/billing",
}),
);
if (statusCode >= 400) {
console.error("[autumn] upstream error:", statusCode, response);
return yield* new HttpResponseError({
status: statusCode,
code: "billing_request_failed",
message: "Billing request failed",
});
}
return HttpServerResponse.jsonUnsafe(response, { status: statusCode });
}).pipe(
Effect.catchCause((err) => {
if (isServerError(err)) {
console.error("[autumn] request failed:", Cause.pretty(err));
}
return toErrorServerResponseEffect(err);
}),
);
export const AutumnRoutesLive = HttpRouter.add("*", "/api/billing/*", handler);