The Jev relay: POST /jev/v1/systemone from the page, forwarded to TypeSafe's API with the site's own key. TypeSafe refuses browser origins (CORS), so the page cannot call it directly; and visitors shouldn't have to hand their key to someone else's Worker, so the site pays.
Because anyone who finds this path spends that key, it forwards only the
question shapes TypeSafe defines (choice, score, noul) within TypeSafe's
own limits, for one model, in a bounded body, at a bounded rate per
visitor. Anything else is
refused before TypeSafe sees it. A request byte for byte the same as one
answered in the last hour gets that answer from KV instead
(answer-cache.ts), marked Jev-Cache: hit.
13import { d1Accounts, type Accounts, type User } from './accounts-store'; 14import { CACHE_HEADER, cached, cacheKey, keep } from './answer-cache'; 15import { signedIn } from './auth'; 16import type { Spend } from './budget-counter'; 17import type { Env } from './env'; 18import { event } from './events'; 19import config from '../wrangler.json';
The size bounds a call's cost (TypeSafe charges by input token), however many questions and options it holds; those two are TypeSafe's own limits.
The rate limit per visitor, set in wrangler.json (JSON, so its reasoning lives here). A song asks each of its jev()s once a section, so it makes (bpm / 4 cycles a minute) / (cycles a section) × (jev()s) calls a minute. The busiest, Task Failed Successfully, is at most 180 / 4 / 4 × 2 = 22.5 (played, 16: a section with one allowed next asks only its parts). The budget: that song in two tabs (45), plus the mood picker, the art check and the radio's next pick, about one a minute each (48). The limit is 60 a 60 s period, and allSongs.test.mjs measures every song and fails one above half of it, so one song can never starve the rest. The binding says only yes or no, so a refused caller is told to wait out a whole period.
TypeSafe's own rate-limit answer, passed back to the page as is.
48const PASS_HEADERS = /^(retry-after(-ms)?|ratelimit(-.*)?|x-ratelimit-.*)$/i;
A signed-in caller's daily budget (budget.ts), on every answer that
charged it: <used>/<limit>, or spent on the 429 that refuses a call
once it is spent (website/src/jev/ask.mjs reads both).
53export const BUDGET_HEADER = 'Jev-Budget';
55type Question = { type?: unknown; instructions?: unknown; criteria?: unknown }; 56 57const isObject = (x: unknown): x is Record<string, unknown> => typeof x === 'object' && x !== null && !Array.isArray(x); 58const allStrings = (xs: unknown[]) => xs.every((x) => typeof x === 'string'); 59 60function whyNotQuestion(q: Question): string | null { 61 if (typeof q?.instructions !== 'string') return 'instructions must be a string'; 62 switch (q.type) { 63 case 'choice': { 64 if (!isObject(q.criteria)) return 'a choice needs criteria: { option: description }'; 65 const options = Object.values(q.criteria); 66 if (options.length < 2 || options.length > MAX_OPTIONS) return `a choice has 2 to ${MAX_OPTIONS} options`; 67 return allStrings(options) ? null : 'choice descriptions must be strings'; 68 } 69 case 'score': { 70 const levels = q.criteria; 71 if (!Array.isArray(levels) || levels.length < 2 || levels.length > MAX_LEVELS) { 72 return `a score has 2 to ${MAX_LEVELS} levels`; 73 } 74 return allStrings(levels) ? null : 'score levels must be strings'; 75 } 76 case 'noul': { 77 if (q.criteria === undefined) return null; 78 if (!isObject(q.criteria)) return 'noul criteria must be { true, false }'; 79 const { true: yes, false: no, ...rest } = q.criteria; 80 if (Object.keys(rest).length || typeof yes !== 'string' || typeof no !== 'string') { 81 return 'noul criteria must be { true, false } strings'; 82 } 83 return null; 84 } 85 default: 86 return 'questions are choice, score or noul'; 87 } 88}
null when the body is a request the page could have sent; otherwise why not.
91export function whyNot(body: unknown): string | null { 92 if (!isObject(body)) return 'body must be a JSON object'; 93 const { model, questions, state, ...rest } = body; 94 if (Object.keys(rest).length) return `unexpected fields: ${Object.keys(rest).join(', ')}`; 95 if (model !== MODEL) return `model must be ${MODEL}`; 96 if (!isObject(state)) return 'state must be an object'; 97 if (!isObject(questions)) return 'questions must be an object'; 98 const qs = Object.values(questions) as Question[]; 99 if (qs.length < 1) return 'ask at least one question'; 100 for (const q of qs) { 101 const why = whyNotQuestion(q); 102 if (why) return why; 103 } 104 return null; 105}
The page's own timing (website/src/jev/ask.mjs), as headers the relay reads for its log and never forwards: seconds until the section asked about begins when the call went out (negative once it has begun), and which attempt this is (0, then its retries).
One structured line per call, for Workers Logs (wrangler.json's
observability) and wrangler tail. It holds timing and sizes only: no
address, no visitor key, nothing from the request's contents, and the
page's headers only as bounded numbers. colo is the Cloudflare data
centre that ran the call, which is where upstream latency is measured from.
outcome says who answered: the relay itself (refused), TypeSafe
(forwarded, whatever its status), the answer cache (cached), or nobody
(unreachable). The same
fields go to Analytics Engine as a jev.relay event (events-schema.ts),
where they can be counted and charted.
126export type RelayLog = { 127 event: 'jev.relay'; 128 outcome: 'refused' | 'forwarded' | 'cached' | 'unreachable'; 129 status: number; 130 upstreamMs: number | null; // until TypeSafe's response headers; null when refused before asking 131 requestBytes: number | null; // the body as forwarded; null when refused before reading it 132 sectionInS: number | null; 133 attempt: number | null; 134 colo: string | null; 135};
137export function pageTiming(headers: Headers): { sectionInS: number | null; attempt: number | null } { 138 const lead = Number(headers.get(LEAD_HEADER) ?? NaN); 139 const attempt = Number(headers.get(ATTEMPT_HEADER) ?? NaN); 140 return { 141 sectionInS: Number.isFinite(lead) && Math.abs(lead) <= MAX_LEAD_S ? Math.round(lead * 100) / 100 : null, 142 attempt: Number.isInteger(attempt) && attempt >= 0 && attempt <= MAX_ATTEMPT ? attempt : null, 143 }; 144}
The upstream path: one request to TypeSafe with the site's key, the body forwarded as given. The relay forwards the page's calls through it, and the Worker's own questions (content-jobs.ts: screening listeners' content and scoring their songs) go the same way, after the same whyNot check. Throws when TypeSafe cannot be reached; any status is returned as is.
151export function upstream(env: Pick<Env, 'JEVSTRUDEL_TYPESAFE_API_KEY'>, body: BodyInit): Promise<Response> { 152 return fetch(UPSTREAM, { 153 method: 'POST', 154 headers: { 155 Authorization: `Bearer ${env.JEVSTRUDEL_TYPESAFE_API_KEY}`, 156 'Content-Type': 'application/json', 157 }, 158 body, 159 }); 160}
Signed-in callers (a session cookie, auth.ts) are charged one call of their daily budget for each call forwarded to TypeSafe: a cached answer costs nothing, so it is not charged. Once spent, calls are refused with a 429 until UTC midnight. The per-minute JEV_LIMIT applies to everyone, keyed by the account when signed in (so listeners behind one address do not share it) and by the address otherwise, as before accounts.
168export async function relay( 169 request: Request, 170 env: Env, 171 ctx: Pick<ExecutionContext, 'waitUntil'>, 172 accounts: Accounts = d1Accounts(env.DB), 173): Promise<Response> { 174 const log = (outcome: RelayLog['outcome'], status: number, fields: Partial<RelayLog> = {}) => { 175 const colo = (request as { cf?: { colo?: unknown } }).cf?.colo; 176 const line: RelayLog = { 177 event: 'jev.relay', 178 outcome, 179 status, 180 upstreamMs: null, 181 requestBytes: null, 182 ...pageTiming(request.headers), 183 colo: typeof colo === 'string' ? colo : null, 184 ...fields, 185 }; 186 console.log(line); 187 event(env, 'jev.relay', { 188 blobs: { outcome: line.outcome, colo: line.colo }, 189 doubles: { 190 status: line.status, 191 upstreamMs: line.upstreamMs, 192 requestBytes: line.requestBytes, 193 sectionInS: line.sectionInS, 194 attempt: line.attempt, 195 }, 196 }); 197 }; 198 const refused = (status: number, why: string, headers: Record<string, string> = {}, fields: Partial<RelayLog> = {}) => { 199 log('refused', status, fields); 200 return refuse(status, why, headers); 201 }; 202 203 if (request.method !== 'POST') { 204 log('refused', 405); 205 return new Response('POST only', { status: 405, headers: { Allow: 'POST' } }); 206 } 207 if (!env.JEVSTRUDEL_TYPESAFE_API_KEY) return refused(503, 'the relay has no TypeSafe key'); 208 209 let user: User | null; 210 try { 211 user = await signedIn(request, accounts); 212 } catch (e) { 213 console.error({ event: 'jev.relay.session', error: (e as Error).message }); 214 return refused(503, 'sign-in is unavailable; try again in a minute', { 'Retry-After': '60' }); 215 } 216 const visitor = user ? `user:${user.id}` : (request.headers.get('CF-Connecting-IP') ?? 'local'); 217 const { success } = await env.JEV_LIMIT.limit({ key: visitor }); 218 if (!success) { 219 return refused(429, 'too many Jev calls; try again in a minute', { 'Retry-After': String(LIMIT_PERIOD_S) }); 220 } 221 222 const raw = await request.arrayBuffer(); 223 const requestBytes = raw.byteLength; 224 if (requestBytes > MAX_BYTES) return refused(413, 'request too large', {}, { requestBytes }); 225 let body: unknown; 226 try { 227 body = JSON.parse(new TextDecoder().decode(raw)); 228 } catch { 229 return refused(400, 'body must be JSON', {}, { requestBytes }); 230 } 231 const why = whyNot(body); 232 if (why) return refused(400, why, {}, { requestBytes }); 233 234 const key = await cacheKey(MODEL, raw); 235 const hit = await cached(env.CACHE, key); 236 if (hit) { 237 log('cached', 200, { requestBytes }); 238 return new Response(hit.body, { 239 status: 200, 240 headers: { 'Content-Type': hit.contentType, 'Cache-Control': 'no-store', [CACHE_HEADER]: 'hit' }, 241 }); 242 } 243 244 const budgetHeaders: Record<string, string> = {}; 245 if (user) { 246 let charge: Spend; 247 try { 248 charge = await env.BUDGET.get(env.BUDGET.idFromName(user.id)).spend(); 249 } catch (e) { 250 // not charged, so not forwarded: a budget that cannot be counted is not spent 251 console.error({ event: 'jev.budget', op: 'spend', error: (e as Error).message }); 252 return refused(503, 'your Jev budget cannot be checked right now', { 'Retry-After': '60' }, { requestBytes }); 253 } 254 const secondsToReset = Math.max(1, Math.ceil((charge.resetsAt - Date.now()) / 1000)); 255 if (!charge.ok) { 256 return refused( 257 429, 258 `your daily Jev budget (${charge.limit} calls) is spent; it resets at 00:00 UTC`, 259 { 'Retry-After': String(secondsToReset), [BUDGET_HEADER]: 'spent' }, 260 { requestBytes }, 261 ); 262 } 263 if (charge.exhausted) { 264 const colo = (request as { cf?: { colo?: unknown } }).cf?.colo; 265 console.log({ event: 'jev.budget.exhausted', limit: charge.limit, secondsToReset }); 266 event(env, 'jev.budget.exhausted', { 267 blobs: { colo: typeof colo === 'string' ? colo : null }, 268 doubles: { limit: charge.limit, secondsToReset }, 269 }); 270 } 271 budgetHeaders[BUDGET_HEADER] = `${charge.used}/${charge.limit}`; 272 } 273 274 const asked = Date.now(); 275 let res: Response; 276 try { 277 res = await upstream(env, raw); 278 } catch { 279 log('unreachable', 502, { requestBytes, upstreamMs: Date.now() - asked }); 280 return refuse(502, 'TypeSafe could not be reached'); 281 } 282 log('forwarded', res.status, { requestBytes, upstreamMs: Date.now() - asked }); 283 const contentType = res.headers.get('Content-Type') ?? 'application/json'; 284 const headers = new Headers({ 285 'Content-Type': contentType, 286 'Cache-Control': 'no-store', 287 [CACHE_HEADER]: 'miss', 288 ...budgetHeaders, 289 }); 290 res.headers.forEach((value, name) => { 291 if (PASS_HEADERS.test(name)) headers.set(name, value); 292 }); 293 let answer = res.body; 294 if (res.status === 200 && answer) { 295 const [page, store] = answer.tee(); 296 answer = page; 297 ctx.waitUntil(keep(env.CACHE, key, store, contentType)); 298 } 299 return new Response(answer, { status: res.status, headers }); 300}