diff --git a/src/mobile-web/tests/host-dialog-queue.test.mjs b/src/mobile-web/tests/host-dialog-queue.test.mjs index 0c7247cadf..259d864a06 100644 --- a/src/mobile-web/tests/host-dialog-queue.test.mjs +++ b/src/mobile-web/tests/host-dialog-queue.test.mjs @@ -143,3 +143,18 @@ test('a live send becomes recoverable only when its acknowledgement fails',async assert.equal(f.calls.find(call=>call.action==='submit').message.turnId,id); assert.equal(f.executions,1); }); + +test('a page without Web Crypto randomUUID still queues a send and an operation',async()=>{ + const f=fixture();const q=f.create(); + // The remote page of LAN mode is served over plain HTTP and older WebViews + // never expose `randomUUID`, while `getRandomValues` stays available. + const descriptor=Object.getOwnPropertyDescriptor(globalThis,'crypto');const original=globalThis.crypto; + Object.defineProperty(globalThis,'crypto',{configurable:true,value:{getRandomValues:original.getRandomValues.bind(original)}}); + try { + await q.submit(message);await q.act({turnId:'queued'},'promote'); + } finally {Object.defineProperty(globalThis,'crypto',descriptor);} + const uuid=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + assert.match(f.calls.find(call=>call.action==='submit').message.turnId,uuid); + assert.match(f.calls.find(call=>call.action==='promote').operationId,uuid); + assert.equal(f.executions,1); +}); diff --git a/src/shared/dialog-queue/HostDialogQueue.ts b/src/shared/dialog-queue/HostDialogQueue.ts index 9f615818c8..136e80cf4b 100644 --- a/src/shared/dialog-queue/HostDialogQueue.ts +++ b/src/shared/dialog-queue/HostDialogQueue.ts @@ -62,7 +62,16 @@ export const queueStorage: QueueStorage = { async remove(key) { await transaction('readwrite', store => store.delete(key)); }, }; export interface QueueView { snapshot: QueueSnapshot | null; error: string | null; pending: QueueOutboxRecord[] } -const newId = () => crypto.randomUUID(); +const newId = (): string => { + // `crypto.randomUUID` exists only in secure contexts, so a page served over + // plain HTTP (LAN remote control) or an older WebView must still queue sends. + if (typeof crypto.randomUUID === 'function') return crypto.randomUUID(); + const bytes = crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, value => value.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +}; function messageIdentity(message: Omit): string { // Reopening the composer can allocate fresh image IDs. An ambiguous retry // must send the stored payload, including its original IDs, unchanged.