diff --git a/apps/worker/src/oauth.ts b/apps/worker/src/oauth.ts new file mode 100644 index 0000000..46c887a --- /dev/null +++ b/apps/worker/src/oauth.ts @@ -0,0 +1,66 @@ +// Worker-side singleton NodeOAuthClient. +// +// Mirrors apps/web/lib/atproto.ts. Both call buildOAuthBundle() from +// @yominoki/atproto/oauth so the keyset-build / loopback-vs-confidential +// branching lives in one place. This module just caches the bundle for +// the lifetime of the worker isolate. +// +// IMPORTANT: NodeOAuthClient relies on Node APIs (crypto, jose) that +// require Workers' nodejs_compat compatibility flag. wrangler.jsonc +// already sets it. If construction starts failing on workerd this is +// the first place to look — check the @atproto/oauth-client-node +// changelog for new required Node APIs. + +import type { Env } from "./index" +import type { StoreOps } from "@easier-idx/core" +import { + buildOAuthBundle, + type NodeOAuthClient, + type OAuthBundle, +} from "@yominoki/atproto" + +let cached: OAuthBundle | null = null +let inflight: Promise | null = null + +export class OAuthEnvMissingError extends Error { + constructor(missing: string) { + super(`OAuth env missing: ${missing}`) + this.name = "OAuthEnvMissingError" + } +} + +function isLoopback(host: string): boolean { + return ( + host.startsWith("http://localhost") || host.startsWith("http://127.0.0.1") + ) +} + +export async function getOAuthClient( + env: Env, + ops: StoreOps +): Promise { + if (cached) return cached.client + if (!inflight) { + if (!env.OAUTH_PUBLIC_HOST) { + throw new OAuthEnvMissingError("OAUTH_PUBLIC_HOST") + } + const baseUrl = env.OAUTH_PUBLIC_HOST + const jwksJson = env.OAUTH_PRIVATE_JWKS ?? null + if (!isLoopback(baseUrl) && !jwksJson) { + throw new OAuthEnvMissingError("OAUTH_PRIVATE_JWKS") + } + inflight = buildOAuthBundle({ baseUrl, jwksJson, ops }).then( + (bundle) => { + cached = bundle + inflight = null + return bundle + }, + (err: unknown) => { + inflight = null + throw err + } + ) + } + const bundle = await inflight + return bundle.client +}