ReviewOS

also looking at this

stacks/wildloop

feat: native trail maps and complete native mobile screens

#1017
Draft chris wants to merge codex/trail-experience into main
60 files +2505 -823

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

tests/unit/native-api.test.tsmodified+92-0
Changes to tests/unit/native-api.test.ts
@@ -288,3 +288,95 @@ describe('signed-out native mutation security', () => {
288288 } finally {globalThis.fetch=previous}
289289 })
290290})
291
292describe('native account-bound operations', () => {
293 it('requires a warm screen to reload after its account changes before an operation begins', async () => {
294 let account: unknown = {user:{id:7},version:1}
295 let token = 'account-seven'
296 const fetched = recorder()
297 try {
298 const api = createNativeApi({snapshots:{get:()=>account},secureStorage:{get:()=>token}})
299 await api.send('/api/me')
300 account = {user:{id:8},version:2}
301 token = 'account-eight'
302 await expect(api.bindSession()).rejects.toThrow('Your account changed')
303 expect(fetched.calls).toHaveLength(1)
304 }
305 finally { fetched.restore() }
306 })
307
308 it('refuses a warm screen after the bearer changes before the account snapshot arrives', async () => {
309 let token = 'account-seven'
310 const fetched = recorder()
311 try {
312 const api = createNativeApi({snapshots:{get:()=>({user:{id:7},version:1})},secureStorage:{get:()=>token}})
313 await api.send('/api/me')
314 token = 'account-eight'
315 await expect(api.bindSession()).rejects.toThrow('Your account changed')
316 expect(fetched.calls).toHaveLength(1)
317 }
318 finally { fetched.restore() }
319 })
320
321 it('keeps an identity check and its upload in the same account session', async () => {
322 let account: unknown = {user:{id:7},version:1}
323 let token = 'account-seven'
324 const fetched = recorder({body:'{"user":{"id":7}}'})
325 try {
326 const api = await createNativeApi({snapshots:{get:()=>account},secureStorage:{get:()=>token}}).bindSession()
327 await api.send('/api/me')
328 account = {user:{id:8},version:2}
329 token = 'account-eight'
330 await expect(api.send('/api/activities','POST',{upload_id:'run:owned'})).rejects.toThrow('Your account changed')
331 expect(fetched.calls).toHaveLength(1)
332 expect(fetched.calls[0].headers.Authorization).toBe('Bearer account-seven')
333 }
334 finally { fetched.restore() }
335 })
336
337 it('works on a cold start without an account snapshot and detects a changed keychain', async () => {
338 let token = 'cold-start-token'
339 const fetched = recorder()
340 try {
341 const api = await createNativeApi({secureStorage:{get:()=>token}}).bindSession()
342 await api.send('/api/me')
343 token = 'replacement-token'
344 await expect(api.send('/api/activities','POST',{})).rejects.toThrow('Your account changed')
345 expect(fetched.calls).toHaveLength(1)
346 }
347 finally { fetched.restore() }
348 })
349
350 it('does not bind a signed-out session or silently retarget after a keychain read', async () => {
351 let account: unknown = {user:{id:7},version:1}
352 const fetched = recorder()
353 try {
354 await expect(createNativeApi({secureStorage:{get:()=>null}}).bindSession()).rejects.toThrow('Sign in')
355 const api = createNativeApi({snapshots:{get:()=>account},secureStorage:{get:async()=>{
356 account={user:{id:8},version:2}
357 return 'previous-token'
358 }}})
359 await expect(api.bindSession()).rejects.toThrow('Your account changed')
360 expect(fetched.calls).toHaveLength(0)
361 }
362 finally { fetched.restore() }
363 })
364
365 it('does not expose an old response after a cold-start bearer changes in flight', async () => {
366 let token = 'account-seven'
367 const previous = globalThis.fetch
368 const bearers: string[] = []
369 globalThis.fetch = (async (url: any, init: any) => {
370 expect(String(url)).toBe(`${WILDLOOP_ORIGIN}/api/me`)
371 bearers.push(init.headers.Authorization)
372 token = 'account-eight'
373 return new Response('{"user":{"id":7}}')
374 }) as typeof fetch
375 try {
376 const api = await createNativeApi({secureStorage:{get:()=>token}}).bindSession()
377 await expect(api.send('/api/me')).rejects.toThrow('Your account changed')
378 expect(bearers).toEqual(['Bearer account-seven'])
379 }
380 finally { globalThis.fetch = previous }
381 })
382})