A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 4.2 kB View raw
1/** 2 * Pure, dependency-free OAuth configuration helpers (Decision 0004). 3 * 4 * Kept free of `@atproto/*` imports so it's unit-testable in Node and importable 5 * from anywhere. The browser-only client construction lives in `oauth.ts`. 6 */ 7 8/** Scope for SP1 — establishes a session. Granular write scopes are added in SP2. */ 9export const OAUTH_SCOPE = 'atproto transition:generic'; 10 11/** Handle resolver service (a `com.atproto.identity.resolveHandle` XRPC endpoint). */ 12export const HANDLE_RESOLVER = 'https://bsky.social'; 13 14const LOOPBACK_HOSTS = new Set( [ 'localhost', '127.0.0.1', '[::1]', '::1' ] ); 15 16/** 17 * Loopback hosts use atproto's development client (auto-generated metadata); 18 * everything else uses a hosted `client-metadata.json`. 19 */ 20export function getClientMode( hostname: string ): 'loopback' | 'hosted' { 21 return LOOPBACK_HOSTS.has( hostname ) ? 'loopback' : 'hosted'; 22} 23 24/** The hosted client metadata URL — the `client_id` for production. */ 25export function clientMetadataUrl( origin: string ): string { 26 return `${ origin.replace( /\/$/, '' ) }/client-metadata.json`; 27} 28 29/** 30 * The routes that mount the OAuth island and may therefore be the page sign-in starts from 31 * (and returns to). Every entry MUST appear in `client-metadata.json`'s `redirect_uris` 32 * (it's generated from this list) — atproto only redirects back to a registered URI, and 33 * the browser client matches the callback page against the same list to exchange the code. 34 * 35 * `/editor/` stays first: it's the historical default and the safe fallback for any other 36 * page that ever reaches sign-in. (Decision 0020 — the writing-first `/write` flow shares 37 * the editor island, so signing in from there must come back to `/write/`, not `/editor/`.) 38 */ 39export const OAUTH_REDIRECT_PATHS = [ '/editor/', '/write/' ] as const; 40 41/** 42 * The registered redirect URI for the page sign-in is starting on, so the full-page 43 * OAuth round-trip returns the writer to where they were (carrying the draft + publish 44 * intent that `draft-store` persisted). Unknown paths fall back to `/editor/` — passing 45 * an unregistered URI to `signIn()` would make atproto throw. 46 */ 47export function redirectUriForLocation( origin: string, pathname: string ): string { 48 const base = origin.replace( /\/$/, '' ); 49 const normalized = pathname.endsWith( '/' ) ? pathname : `${ pathname }/`; 50 const match = OAUTH_REDIRECT_PATHS.find( ( path ) => path === normalized ); 51 return `${ base }${ match ?? OAUTH_REDIRECT_PATHS[ 0 ] }`; 52} 53 54/** Normalise a handle: trim, drop a leading `@`, lowercase. */ 55export function normalizeHandle( input: string ): string { 56 return input.trim().replace( /^@/, '' ).toLowerCase(); 57} 58 59const HANDLE_RE = /^([a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/; 60const DID_RE = /^did:[a-z]+:[a-zA-Z0-9._:%-]+$/; 61 62/** 63 * Accept the three things `signIn()` can resolve from: a handle (`alice.bsky.social`), 64 * a DID (`did:plc:…`), or a PDS/entryway URL (`https://…`). Rejects empty input and 65 * bare words with no domain. 66 */ 67export function isValidAccountInput( input: string ): boolean { 68 const value = input.trim(); 69 if ( ! value ) { 70 return false; 71 } 72 if ( DID_RE.test( value ) ) { 73 return true; 74 } 75 if ( /^https?:\/\//.test( value ) ) { 76 try { 77 return Boolean( new URL( value ).hostname ); 78 } catch { 79 return false; 80 } 81 } 82 return HANDLE_RE.test( normalizeHandle( value ) ); 83} 84 85/** 86 * Stricter sibling of `isValidAccountInput` for the public READ path. Accepts only a 87 * syntactic handle (`alice.bsky.social`) or DID (`did:plc:…`) — and, unlike 88 * `isValidAccountInput`, rejects `https://…` URLs and anything carrying a path, port, 89 * query or scheme. The renderer takes the author straight from the URL and uses it as a 90 * resolver fetch host, so this stops a value like `evil.com/x?y=` from smuggling into the 91 * outbound request (`safeFetch` still blocks internal hosts; this gates the syntax). 92 */ 93export function isValidHandleOrDid( input: string ): boolean { 94 const value = input.trim(); 95 if ( ! value ) { 96 return false; 97 } 98 if ( DID_RE.test( value ) ) { 99 return true; 100 } 101 return HANDLE_RE.test( normalizeHandle( value ) ); 102}