A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import {
2 BrowserOAuthClient,
3 atprotoLoopbackClientMetadata,
4} from '@atproto/oauth-client-browser';
5import { getClientMode, clientMetadataUrl, HANDLE_RESOLVER, OAUTH_SCOPE } from './config';
6
7/**
8 * Create the browser OAuth client (Decision 0004). BROWSER-ONLY — reads
9 * `window.location` and uses WebCrypto/IndexedDB, so call it from an effect in a
10 * client-only island, never during SSR.
11 *
12 * - Loopback (dev): an atproto loopback `client_id` must be **path-less**
13 * (`http://localhost?…`), but its `redirect_uri` may carry a path. The library's
14 * auto-builder derives both from `window.location`, which breaks when the app isn't
15 * at `/` (e.g. `/editor`). So we build it explicitly: a root client_id with the
16 * current page as the redirect target. Loopback origins must be `127.0.0.1`/`[::1]`.
17 * - Hosted (prod): loads `client-metadata.json` from the app origin (full scope).
18 */
19export async function createOAuthClient(): Promise< BrowserOAuthClient > {
20 if ( getClientMode( window.location.hostname ) === 'loopback' ) {
21 const redirectUri = `${ window.location.origin }${ window.location.pathname }`;
22 // Request the write scope in dev too (the default loopback scope is read-only
23 // `atproto`), so createRecord works without a hosted metadata file (Decision 0005).
24 const clientId =
25 `http://localhost?redirect_uri=${ encodeURIComponent( redirectUri ) }` +
26 `&scope=${ encodeURIComponent( OAUTH_SCOPE ) }`;
27 return new BrowserOAuthClient( {
28 clientMetadata: atprotoLoopbackClientMetadata( clientId ),
29 handleResolver: HANDLE_RESOLVER,
30 } );
31 }
32 return BrowserOAuthClient.load( {
33 clientId: clientMetadataUrl( window.location.origin ),
34 handleResolver: HANDLE_RESOLVER,
35 } );
36}