import { packageContext } from 'kody:runtime' import { JWKS } from '../src/lib/jwks.ts' import { getCaller, humanPlayer } from '../src/lib/identity.ts' import { signPowerSyncToken } from '../src/lib/token.ts' import { POWERSYNC_URL } from '../src/lib/config.ts' import { WorldError, applyBlockOps, parseBlockOp, requireWorldId, updateCursor, createRoom, getRoom, parseRoomRef, renameRoom, upsertPlayer, type BlockOp, } from '../src/lib/world-ops.ts' import { DEFAULT_WORLD_ID, LIMITS, blockId } from '../src/shared/world-rules.ts' import { renderShell } from './shell.ts' const json = (body: unknown, status = 200, headers: Record = {}) => Response.json(body, { status, headers: { 'cache-control': 'no-store', ...headers }, }) function relPath(pathname: string) { const base = String(packageContext?.appBasePath ?? '') const assets = String(packageContext?.assetBasePath ?? `${base}/_assets`).replace(/\/+$/, '') if (base && pathname.startsWith(base)) pathname = pathname.slice(base.length) return pathname.replace(/\/+$/, '') || '/' } async function requireHuman() { const caller = await getCaller() if (!caller) throw new WorldError('Sign in with Kody to build.', 401) return humanPlayer(caller) } async function session(url: URL) { const caller = await getCaller() if (!caller) return json({ signedIn: false, worldId: DEFAULT_WORLD_ID, powersyncUrl: POWERSYNC_URL }) const player = humanPlayer(caller) const worldId = requireWorldId(url.searchParams.get('world')) let ready = true try { await upsertPlayer(player, worldId) } catch (error) { ready = false console.warn('player upsert failed', error instanceof Error ? error.message : error) } return json({ signedIn: true, ready, worldId, powersyncUrl: POWERSYNC_URL, player: { id: player.id, displayName: player.displayName, color: player.color }, }) } function roomJson(room: { slug: string; name: string; id: string; ownerId: string }, username: string | null) { return { slug: room.slug, name: room.name, worldId: room.id, isOwner: !!username && room.ownerId === username } } async function rooms(request: Request, path: string, method: string) { const caller = await getCaller() if (path === '/api/rooms' && method === 'POST') { if (!caller) throw new WorldError('Sign in with Kody to create a room.', 401) return json({ room: roomJson(await createRoom(caller.username), caller.username) }, 201) } const slug = parseRoomRef(path.slice('/api/rooms/'.length)) if (!slug) throw new WorldError('That room link is not valid.', 404) if (method === 'GET') { try { return json({ room: roomJson(await getRoom(slug), caller?.username ?? null) }) } catch (error) { // Kody resolves credentials only for a signed-in person today, so a // signed-out visitor can't read the world database yet. if (!caller && !(error instanceof WorldError)) { throw new WorldError('Watching without an account is not available yet. Sign in to watch and build.', 503) } throw error } } if (method === 'PATCH') { if (!caller) throw new WorldError('Sign in with Kody to rename the room.', 401) const body = (await request.json().catch(() => null)) as { name?: unknown } | null return json({ room: roomJson(await renameRoom(slug, caller.username, body?.name), caller.username) }) } return json({ error: 'Method not allowed' }, 405) } /** * PowerSync sync token. Signed-in people get `sub = u:`; signed-out * visitors get a random read-only spectator subject. A PowerSync token can only * download (sync rules are read-only buckets); every write goes through * `POST /api/upload`, which requires a Kody session, so a spectator token never * grants write access. */ async function token() { const caller = await getCaller() const subject = caller ? humanPlayer(caller).id : `anon:${crypto.randomUUID()}` try { return json({ ...(await signPowerSyncToken(subject)), readOnly: !caller }) } catch (error) { const message = error instanceof Error ? error.message : String(error) console.error('world-builder token signing failed', message) // Signing errors describe configuration (approval, host), never key material. throw new WorldError(`Could not sign a sync token: ${message.slice(0, 300)}`, 503) } } type CrudOp = { op?: string; type?: string; id?: string; data?: Record } /** PowerSync uploadData target. Invalid ops are rejected (and revert on the client); a 429 asks the client to retry. */ async function upload(request: Request) { const player = await requireHuman() const len = Number(request.headers.get('content-length') ?? 0) if (len > 256_000) throw new WorldError('Upload too large.', 413) const body = (await request.json().catch(() => null)) as { ops?: Array } | null const ops = Array.isArray(body?.ops) ? body!.ops : [] if (ops.length > LIMITS.maxUploadOps) throw new WorldError('Too many changes in one upload.', 413) const byWorld = new Map>() let rejected = 0 let cursor: { worldId: string; pos: { x: number; y: number; z: number } | null } | null = null for (const op of ops) { if (op.type === 'blocks' && (op.op === 'PUT' || op.op === 'PATCH')) { const d = op.data ?? {} let worldId: string try { worldId = requireWorldId(d.world_id) } catch { rejected++ continue } const parsed = parseBlockOp(d, 'put') if (!parsed || op.id !== blockId(worldId, parsed.x, parsed.y, parsed.z)) { rejected++ continue } byWorld.set(worldId, [...(byWorld.get(worldId) ?? []), parsed]) } else if (op.type === 'blocks' && op.op === 'DELETE') { const m = /^([0-9a-f-]{36}):(-?\d+):(-?\d+):(-?\d+)$/i.exec(String(op.id ?? '')) const parsed = m ? parseBlockOp({ x: m[2], y: m[3], z: m[4] }, 'del') : null if (!m || !parsed) { rejected++ continue } const worldId = m[1]!.toLowerCase() byWorld.set(worldId, [...(byWorld.get(worldId) ?? []), parsed]) } else if (op.type === 'players' && op.op === 'PATCH' && op.id === player.id) { const d = op.data ?? {} const has = d.cursor_x != null && d.cursor_y != null && d.cursor_z != null let worldId = DEFAULT_WORLD_ID try { worldId = requireWorldId(d.world_id) } catch {} cursor = { worldId, pos: has ? { x: Number(d.cursor_x), y: Number(d.cursor_y), z: Number(d.cursor_z) } : null, } } else { rejected++ } } let placed = 0 let removed = 0 for (const [worldId, worldOps] of byWorld) { try { const out = await applyBlockOps({ player, worldId, ops: worldOps }) placed += out.placed removed += out.removed } catch (error) { // 429 should be retried by the client; other world errors drop the ops. if (error instanceof WorldError && error.status !== 429) { rejected += worldOps.length continue } throw error } } if (cursor) { try { await updateCursor(player, cursor.worldId, cursor.pos) } catch (error) { if (!(error instanceof WorldError && error.status === 429)) throw error } } return json({ ok: true, placed, removed, rejected }) } async function dispatch(request: Request) { const url = new URL(request.url) const path = relPath(url.pathname) const method = request.method.toUpperCase() if (path === '/health') return json({ ok: true, app: 'world-builder' }) if (path === '/.well-known/jwks.json' || path === '/jwks.json') { return json(JWKS, 200, { 'cache-control': 'public, max-age=300' }) } if (path === '/api/session' && method === 'GET') return session(url) if (path === '/api/me' && method === 'GET') { const caller = await getCaller() return json(caller ? { signedIn: true, displayName: caller.displayName } : { signedIn: false }) } if (path === '/api/token' && method === 'GET') return token() if (path === '/api/upload') { if (method !== 'POST') return json({ error: 'Method not allowed' }, 405) return upload(request) } if (path === '/api/rooms' || path.startsWith('/api/rooms/')) return rooms(request, path, method) if (path === '/manifest.webmanifest') { const base = String(packageContext?.appBasePath ?? '') const assets = String(packageContext?.assetBasePath ?? `${base}/_assets`) return json( { name: 'Kody World Builder', short_name: 'World Builder', start_url: `${base}/`, scope: `${base}/`, display: 'standalone', background_color: '#0f1420', theme_color: '#0f1420', icons: [{ src: `${assets}/icon.png`, sizes: '512x512', type: 'image/png' }], }, 200, { 'content-type': 'application/manifest+json' }, ) } if (path.startsWith('/api/')) return json({ error: 'Not found' }, 404) if (method === 'GET' || method === 'HEAD') return renderShell() return new Response('Method Not Allowed', { status: 405 }) } export default { async fetch(request: Request) { try { return await dispatch(request) } catch (error) { if (error instanceof WorldError) return json({ error: error.message }, error.status) console.error('world-builder request failed', error instanceof Error ? error.message : error) return json({ error: 'Something went wrong. Try again.' }, 500) } }, }