-
Notifications
You must be signed in to change notification settings - Fork 177
Expand file tree
/
Copy pathroute.ts
More file actions
118 lines (106 loc) · 3.57 KB
/
Copy pathroute.ts
File metadata and controls
118 lines (106 loc) · 3.57 KB
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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, toErrorServerResponse } 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 Effect.succeed(toErrorServerResponse(err));
}),
);
export const AutumnRoutesLive = HttpRouter.add("*", "/api/billing/*", handler);