fix(web): generate sync ids on loopback HTTP
This commit is contained in:
parent
05c93777d6
commit
99e6c0b073
3 changed files with 34 additions and 2 deletions
|
|
@ -17,6 +17,9 @@
|
|||
- Made relocatable launchers resolve stable user-bin symlinks to their physical
|
||||
versioned bundle before loading matching binaries and helpers.
|
||||
|
||||
- Made the Web sync button generate cryptographic UUIDv4 request IDs on the
|
||||
loopback HTTP hostname where `crypto.randomUUID()` is unavailable.
|
||||
|
||||
- Closed the native candidate warning gate with checked path and HTTP response
|
||||
bounds, strict GCC and Clang builds, full Clang ASan/UBSan validation, and a
|
||||
stable concurrent object-I/O MTP double. Rebuilt the desktop, current Android
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
import { beforeEach,describe,expect,it,vi } from 'vitest'
|
||||
import { fetchSyncStatus,startSync,syncIsActive } from './sync'
|
||||
import { fetchSyncStatus, newRequestId, startSync, syncIsActive } from './sync'
|
||||
const token='b'.repeat(64)
|
||||
function response(value:unknown,status=200){return {ok:status>=200&&status<300,status,headers:new Headers({'X-Trainlog-CSRF-Token':token}),json:()=>Promise.resolve(value)}}
|
||||
describe('sync API',()=>{beforeEach(()=>vi.restoreAllMocks())
|
||||
it('obtient passivement le statut et utilise le jeton pour la mutation',async()=>{const fetch=vi.fn().mockResolvedValueOnce(response({phase:'idle',result:'disabled'})).mockResolvedValueOnce(response({phase:'requested',result:'running'},202));vi.stubGlobal('fetch',fetch);await fetchSyncStatus();await startSync('sy_11111111-1111-4111-8111-111111111111');expect(fetch).toHaveBeenCalledTimes(2);const init=fetch.mock.calls[1][1];expect(init.method).toBe('POST');expect(init.headers['X-Trainlog-CSRF-Token']).toBe(token)})
|
||||
it('rejette un statut inconnu et classe les phases actives',async()=>{vi.stubGlobal('fetch',vi.fn().mockResolvedValue(response({phase:'invented',result:'ok'})));await expect(fetchSyncStatus()).rejects.toThrow();expect(syncIsActive('waiting_acknowledgement')).toBe(true);expect(syncIsActive('completed')).toBe(false)})
|
||||
it('crée un UUIDv4 sans dépendre de randomUUID', () => {
|
||||
vi.stubGlobal('crypto', {
|
||||
getRandomValues: (bytes: Uint8Array) => {
|
||||
bytes.set(Array.from({ length: 16 }, (_, index) => index))
|
||||
return bytes
|
||||
},
|
||||
})
|
||||
expect(newRequestId()).toBe('sy_00010203-0405-4607-8809-0a0b0c0d0e0f')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,5 +6,25 @@ const phases=new Set<SyncPhase>(['idle','requested','waiting_android_publication
|
|||
function parse(value:unknown):SyncStatus { if(typeof value!=='object'||value===null)throw new Error('statut sync invalide');const status=value as Partial<SyncStatus>;if(typeof status.phase!=='string'||!phases.has(status.phase as SyncPhase)||typeof status.result!=='string')throw new Error('statut sync invalide');if(status.drafts!==undefined&&(!Array.isArray(status.drafts)||status.drafts.length>32))throw new Error('résumé des brouillons invalide');return status as SyncStatus }
|
||||
export async function fetchSyncStatus(signal?:AbortSignal):Promise<SyncStatus>{const response=await fetch('/api/v1/sync/status',{headers:{Accept:'application/json'},signal});if(!response.ok)throw new Error(`sync status HTTP ${response.status}`);const token=response.headers.get('X-Trainlog-CSRF-Token')??'';if(!/^[0-9a-f]{64}$/.test(token))throw new Error('jeton CSRF absent');csrfToken=token;return parse(await response.json())}
|
||||
export async function startSync(requestId:string):Promise<SyncStatus>{if(!/^sy_[0-9a-f-]{36}$/.test(requestId)||csrfToken.length!==64)throw new Error('requête sync invalide');const response=await fetch('/api/v1/sync',{method:'POST',headers:{Accept:'application/json','Content-Type':'application/json','X-Trainlog-CSRF-Token':csrfToken},body:JSON.stringify({request_id:requestId,trigger:'web'})});const value=await response.json();if(!response.ok)throw new Error(typeof value?.error==='string'?value.error:`sync HTTP ${response.status}`);return parse(value)}
|
||||
export function newRequestId():string{return `sy_${crypto.randomUUID()}`}
|
||||
export function newRequestId(): string {
|
||||
/*
|
||||
* WHY: trainlog.perf is loopback-routed HTTP but is not a browser secure
|
||||
* context, so randomUUID() is legitimately unavailable there.
|
||||
* CONTRACT: getRandomValues() supplies the UUID entropy; Trainlog sets the
|
||||
* RFC 4122 version/variant bits and never falls back to Math.random().
|
||||
*/
|
||||
const bytes = new Uint8Array(16)
|
||||
crypto.getRandomValues(bytes)
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
const hexadecimal = Array.from(bytes, (value) => value.toString(16).padStart(2, '0'))
|
||||
const uuid = [
|
||||
hexadecimal.slice(0, 4).join(''),
|
||||
hexadecimal.slice(4, 6).join(''),
|
||||
hexadecimal.slice(6, 8).join(''),
|
||||
hexadecimal.slice(8, 10).join(''),
|
||||
hexadecimal.slice(10, 16).join(''),
|
||||
].join('-')
|
||||
return `sy_${uuid}`
|
||||
}
|
||||
export function syncIsActive(phase:SyncPhase):boolean{return !['idle','completed','failed','interrupted','explicitly_degraded'].includes(phase)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue