import { Hono } from 'hono' import { useWorkersLogger } from 'workers-tagged-logger' import { withNotFound, withOnError } from '@repo/hono-helpers' import { validateAndGetAccountId } from './jwt' import type { Context } from 'hono' import type { App } from './context' /** * The only endpoint is auth-gated and returns 404 unconditionally — no DB * binding involved. */ /** * Resolve the account id from a Bearer token. Returns `null` when the header is * missing, the token is invalid, or the `sub` claim isn't an integer. */ async function authedId(c: Context): Promise { const authHeader = c.req.header('Authorization') ?? '' if (!authHeader.toLowerCase().startsWith('bearer ')) return null const token = authHeader.slice('Bearer '.length) const accountId = await validateAndGetAccountId(token, await c.env.JWT_SECRET.get()) if (!accountId) return null const id = Number.parseInt(accountId, 10) return Number.isNaN(id) ? null : id } const app = new Hono() .use( '*', // middleware (c, next) => useWorkersLogger(c.env.NAME, { environment: c.env.ENVIRONMENT, release: c.env.SENTRY_RELEASE, })(c, next) ) .onError(withOnError()) .notFound(withNotFound()) // Auth-gated → 401 without a valid token. A bare 404 here makes the client // treat it as an error, so we return an empty object stub. .get('/club/home/me', async (c) => { const id = await authedId(c) if (id === null) return c.body(null, 401) return c.json({}) }) // A real Rec Room client endpoint with no backing implementation yet. The // client calls it on the clubs host at /subscription/mine/member (no /club // prefix) and sends no auth header, so it isn't gated. Returns an empty // array = no club subscription memberships (the client chokes on null). .get('/subscription/mine/member', (c) => c.json([])) // Subscription details for an account (numeric id) — simulated: no club, no subs. .get('/subscription/details/:accountId{[0-9]+}', (c) => c.json({ accountId: Number.parseInt(c.req.param('accountId'), 10), clubId: 0, subscriberCount: 0, }) ) // Details for a named subscription (e.g. `rrplus`). The client deserializes this // into an object, so it must return `{}` (not `[]`). .get('/subscription/details/:subscription', (c) => c.json({})) // Subscriber count for an account. No club subscriptions yet → 0. .get('/subscription/subscriberCount/:accountId{[0-9]+}', (c) => c.json(0)) // The player's clubs that have unread announcements (MyClubsWithUnread- // Announcements). No DB → empty list. .get('/announcements/v2/mine/unread', (c) => c.json([])) // The clubs the player is a member of (GetMyMembershipClubs). No DB → empty. .get('/club/mine/member', (c) => c.json([])) // The clubs the player created (GetMyCreatedClubs). No DB → empty. .get('/club/mine/created', (c) => c.json([])) // The set of club category tags a club can be filed under — a fixed list. .get('/club/categoryTags', (c) => c.json(['Social', 'Creative', 'Competitive', 'Casual', 'Entertainment']) ) export default app