Add cors support 9f834bf6
Co-Authored-By: @stevedylan.dev
Heath Stewart · 2026-02-25 23:14 4 file(s) · +64 −3
docs/src/index.ts +10 −0
1 1
import { Hono } from "hono";
2 +
import { cors } from "hono/cors";
2 3
import auth from "./routes/auth";
3 4
import subscribe from "./routes/subscribe";
5 +
import "./lib/path-redirect";
4 6
5 7
type Bindings = {
6 8
	ASSETS: Fetcher;
12 14
13 15
app.route("/oauth", auth);
14 16
app.route("/subscribe", subscribe);
17 +
app.use("/subscribe", cors({
18 +
	origin: (origin) => origin,
19 +
	credentials: true,
20 +
}));
21 +
app.use("/subscribe/*", cors({
22 +
	origin: (origin) => origin,
23 +
	credentials: true,
24 +
}));
15 25
16 26
app.get("/api/health", (c) => {
17 27
	return c.json({ status: "ok" });
docs/src/lib/oauth-client.ts +1 −1
19 19
			redirect_uris: [redirectUri],
20 20
			grant_types: ["authorization_code", "refresh_token"],
21 21
			response_types: ["code"],
22 -
			scope: "atproto transition:generic",
22 +
			scope: "atproto site.standard.graph.subscription",
23 23
			token_endpoint_auth_method: "none",
24 24
			application_type: "web",
25 25
			dpop_bound_access_tokens: true,
docs/src/lib/path-redirect.ts (added) +51 −0
1 +
// Cloudflare Workers compatibility patches for @atproto libraries.
2 +
//
3 +
// 1. Workers don't support `redirect: 'error'` — simulate it with 'manual'.
4 +
// 2. Workers don't support the standard `cache` option in Request — strip it.
5 +
6 +
function sanitizeInit(init?: RequestInit): RequestInit | undefined {
7 +
	if (!init) return init;
8 +
	const { cache, redirect, ...rest } = init;
9 +
	return {
10 +
		...rest,
11 +
		// Workers only support 'follow' and 'manual'
12 +
		redirect: redirect === "error" ? "manual" : redirect,
13 +
		// Workers don't support standard cache modes — omit entirely
14 +
		...(cache ? {} : {}),
15 +
	};
16 +
}
17 +
18 +
const errorRedirectRequests = new WeakSet<Request>();
19 +
const OriginalRequest = globalThis.Request;
20 +
21 +
globalThis.Request = class extends OriginalRequest {
22 +
	constructor(
23 +
		input: RequestInfo | URL,
24 +
		init?: RequestInit,
25 +
	) {
26 +
		super(input, sanitizeInit(init));
27 +
		if (init?.redirect === "error") {
28 +
			errorRedirectRequests.add(this);
29 +
		}
30 +
	}
31 +
} as typeof Request;
32 +
33 +
const originalFetch = globalThis.fetch;
34 +
globalThis.fetch = (async (
35 +
	input: RequestInfo | URL,
36 +
	init?: RequestInit,
37 +
): Promise<Response> => {
38 +
	const cleanInit = sanitizeInit(init);
39 +
	const response = await originalFetch(input, cleanInit);
40 +
41 +
	// Simulate redirect: 'error' — throw on 3xx
42 +
	const wantsRedirectError =
43 +
		init?.redirect === "error" ||
44 +
		(input instanceof Request && errorRedirectRequests.has(input));
45 +
46 +
	if (wantsRedirectError && response.status >= 300 && response.status < 400) {
47 +
		throw new TypeError("unexpected redirect");
48 +
	}
49 +
50 +
	return response;
51 +
}) as typeof fetch;
docs/src/routes/auth.ts +2 −2
27 27
		redirect_uris: [redirectUri],
28 28
		grant_types: ["authorization_code", "refresh_token"],
29 29
		response_types: ["code"],
30 -
		scope: "atproto transition:generic",
30 +
		scope: "atproto site.standard.graph.subscription",
31 31
		token_endpoint_auth_method: "none",
32 32
		application_type: "web",
33 33
		dpop_bound_access_tokens: true,
44 44
45 45
		const client = createOAuthClient(c.env.SEQUOIA_SESSIONS, c.env.CLIENT_URL);
46 46
		const authUrl = await client.authorize(handle, {
47 -
			scope: "atproto transition:generic",
47 +
			scope: "atproto site.standard.graph.subscription",
48 48
		});
49 49
50 50
		return c.redirect(authUrl.toString());