extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288import { type RequestEvent } from "@sveltejs/kit";import type { App } from '@sveltejs/kit';import { OAuthClient, MemoryStore, type StoredState, type OAuthSession,} from "@atcute/oauth-node-client";import type { ClientAssertionPrivateJwk } from "@atcute/oauth-crypto";import { CompositeDidDocumentResolver, CompositeHandleResolver, LocalActorResolver, WellKnownHandleResolver,} from "@atcute/identity-resolver";import type { Did } from "@atcute/lexicons/syntax";import { Client } from "@atcute/client";
// Fetch-based DNS resolver for Cloudflare Workersclass FetchDnsHandleResolver { async resolve(handle: string): Promise<string | null> { try { const dnsUrl = `https://cloudflare-dns.com/dns-query?name=_atproto.${handle}&type=TXT`; const response = await fetch(dnsUrl, { headers: { 'Accept': 'application/dns-json' } });
if (!response.ok) return null;
const data: any = await response.json(); const txtRecords = data.Answer?.filter((a: any) => a.type === 16) || [];
for (const record of txtRecords) { const text = record.data.replace(/"/g, ''); if (text.startsWith('did=')) { return text.substring(4); } } return null; } catch { return null; } }}
// Custom PLC DID resolver with better error handling for Cloudflare Workersclass CloudflarePlcDidDocumentResolver { private plcUrl = 'https://plc.directory';
async resolve(did: string): Promise<any> { if (!did.startsWith('did:plc:')) { return undefined; }
try { const url = `${this.plcUrl}/${did}`; const response = await fetch(url, { headers: { 'Accept': 'application/json', 'User-Agent': 'CloudGo/1.0', }, });
if (!response.ok) { console.error('[PLC Resolver] HTTP error:', response.status, did); return undefined; }
return await response.json(); } catch (err) { console.error('[PLC Resolver] Fetch error:', err); return undefined; } }}
// Custom Web DID resolver for Cloudflare Workersclass CloudflareWebDidDocumentResolver { async resolve(did: string): Promise<any> { if (!did.startsWith('did:web:')) { return undefined; }
try { // did:web:example.com -> https://example.com/.well-known/did.json // did:web:example.com:path:to -> https://example.com/path/to/did.json const domainAndPath = did.slice('did:web:'.length); const parts = domainAndPath.split(':').map(decodeURIComponent); const domain = parts[0]; const path = parts.length > 1 ? '/' + parts.slice(1).join('/') : '/.well-known'; const url = `https://${domain}${path}/did.json`;
const response = await fetch(url, { headers: { 'Accept': 'application/json', 'User-Agent': 'CloudGo/1.0', }, });
if (!response.ok) { console.error('[Web Resolver] HTTP error:', response.status, did); return undefined; }
return await response.json(); } catch (err) { console.error('[Web Resolver] Fetch error:', err); return undefined; } }}
// KV-backed state store for Cloudflare Workersclass KVStateStore<K extends string = string, V = any> { constructor( private kv: KVNamespace, private prefix: string, private ttlSeconds?: number ) {}
async set(key: K, value: V): Promise<void> { await this.kv.put( `${this.prefix}:${key}`, JSON.stringify(value), this.ttlSeconds ? { expirationTtl: this.ttlSeconds } : undefined ); }
async get(key: K): Promise<V | undefined> { const value = await this.kv.get(`${this.prefix}:${key}`, 'json'); return value || undefined; }
async delete(key: K): Promise<void> { await this.kv.delete(`${this.prefix}:${key}`); }}
const ONE_MINUTE_MS = 60_000;const TEN_MINUTES_MS = 10 * ONE_MINUTE_MS;
// Module-level cache for OAuth client instancesconst oauthClients = new Map<string, OAuthClient>();
export async function getOAuthClient(platform: App.Platform | undefined): Promise<OAuthClient> { // Get env vars from platform (Cloudflare) or process.env (local dev) const env = platform?.env; const PRIVATE_KEY_JWK = env?.PRIVATE_KEY_JWK ?? (typeof process !== 'undefined' ? process.env?.PRIVATE_KEY_JWK : undefined); const PUBLIC_BASE_URL = env?.PUBLIC_BASE_URL ?? (typeof process !== 'undefined' ? process.env?.PUBLIC_BASE_URL : undefined);
if (!PRIVATE_KEY_JWK) { throw new Error( "PRIVATE_KEY_JWK environment variable is required for OAuth", ); }
if (!PUBLIC_BASE_URL) { throw new Error( "PUBLIC_BASE_URL environment variable is required for OAuth. Check your configuration.", ); }
// Use a cache key based on the base URL const cacheKey = PUBLIC_BASE_URL; const cached = oauthClients.get(cacheKey); if (cached) { return cached; }
const publicUrl = new URL(PUBLIC_BASE_URL);
// Parse the JWK directly - the new API accepts JWK objects const privateJwk = JSON.parse(PRIVATE_KEY_JWK) as ClientAssertionPrivateJwk;
const client = new OAuthClient({ metadata: { client_id: new URL("/oauth-client-metadata.json", publicUrl).href, client_name: "Cloud Go", redirect_uris: [new URL("/auth/callback", publicUrl).href], scope: "atproto repo:app.bsky.feed.post?action=create com.atproto.repo.uploadBlob blob:image/png repo:boo.sky.go.game?action=create repo:boo.sky.go.game?action=update repo:boo.sky.go.move?action=create repo:boo.sky.go.pass?action=create repo:boo.sky.go.resign?action=create repo:boo.sky.go.reaction?action=create repo:boo.sky.go.profile?action=create repo:boo.sky.go.profile?action=update", jwks_uri: new URL("/jwks.json", publicUrl).href, },
keyset: [privateJwk],
actorResolver: new LocalActorResolver({ handleResolver: new CompositeHandleResolver({ methods: { dns: new FetchDnsHandleResolver() as any, // Type cast for compatibility http: new WellKnownHandleResolver(), }, }), didDocumentResolver: new CompositeDidDocumentResolver({ methods: { plc: new CloudflarePlcDidDocumentResolver() as any, web: new CloudflareWebDidDocumentResolver() as any, }, }), }),
stores: platform?.env?.SESSIONS_KV && platform?.env?.STATES_KV ? { // Production: Use KV storage sessions: new KVStateStore(platform.env.SESSIONS_KV, 'session') as any, states: new KVStateStore<string, StoredState>(platform.env.STATES_KV, 'state', 600) as any, // 10 min TTL } : { // Local development: Use MemoryStore sessions: new MemoryStore({ maxSize: 10_000 }) as any, states: new MemoryStore<string, StoredState>({ maxSize: 10_000, ttl: TEN_MINUTES_MS, ttlAutopurge: true, }) as any, }, });
oauthClients.set(cacheKey, client); return client;}
export interface Session { did: string; handle?: string;}
const COOKIE_NAME = "go_session";
export async function getSession(event: RequestEvent): Promise<Session | null> { const did = event.cookies.get(COOKIE_NAME); if (!did) return null;
try { const oauth = await getOAuthClient(event.platform); const oauthSession = await oauth.restore(did as Did, { refresh: "auto" });
// Return session with DID and optionally fetch handle if needed return { did: oauthSession.did, }; } catch (error) { // If session restore fails, clear the cookie event.cookies.delete(COOKIE_NAME, { path: "/" }); return null; }}
export async function setSession(event: RequestEvent, session: Session) { const PUBLIC_BASE_URL = event.platform?.env?.PUBLIC_BASE_URL; const secure = PUBLIC_BASE_URL?.startsWith("https:") ?? false;
event.cookies.set(COOKIE_NAME, session.did, { path: "/", httpOnly: true, sameSite: "lax", secure, maxAge: 60 * 60 * 24 * 7, // 7 days });}
export async function clearSession(event: RequestEvent) { const PUBLIC_BASE_URL = event.platform?.env?.PUBLIC_BASE_URL; const secure = PUBLIC_BASE_URL?.startsWith("https:") ?? false;
event.cookies.delete(COOKIE_NAME, { path: "/", httpOnly: true, sameSite: "lax", secure, });}
export async function getAgent(event: RequestEvent): Promise<Client | null> { const did = event.cookies.get(COOKIE_NAME); if (!did) return null;
try { const oauth = await getOAuthClient(event.platform); const oauthSession = await oauth.restore(did as Did, { refresh: "auto" });
// Create a client using the OAuth session as the handler const client = new Client({ handler: oauthSession }); return client; } catch (error) { console.error("Failed to get agent:", error); return null; }}