fix(ui): support secure UUIDs on LAN HTTP

Avoid eager consent component crashes and registration approval failures where crypto.randomUUID is unavailable. Share the companion audio CSPRNG fallback, preserve UUIDv4/session semantics and fail closed without secure randomness.

Validation: 24 focused UUID, consent mount, registration recovery and companion audio tests passed. Independent review passed; full UI typecheck/build qualification remains in progress.
This commit is contained in:
archipelago
2026-10-08 12:00:17 -04:00
parent cea4fa1a5a
commit 0df423a84f
7 changed files with 79 additions and 13 deletions
@@ -41,6 +41,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { secureUuid } from '@/utils/secureUuid'
import { fileBrowserClient, type FileBrowserItem } from '@/api/filebrowser-client'
import { useModalKeyboard } from '@/composables/useModalKeyboard'
import type { RegistrationRequest, CloudSelection } from '@/composables/useMediaRegistrationBridge'
@@ -48,7 +49,7 @@ const props = defineProps<{ request: RegistrationRequest | null; phase: 'select'
const emit = defineEmits<{ approve: [selection: CloudSelection]; cancel: []; resolve: [] }>()
const modal = ref<HTMLElement | null>(null), directory = ref('/'), files = ref<FileBrowserItem[]>([])
const selected = ref<FileBrowserItem | null>(null), loading = ref(false), localError = ref('')
const titleId = `media-registration-${crypto.randomUUID()}`
const titleId = `media-registration-${secureUuid()}`
let generation = 0
const visible = computed(() => files.value.filter(item => item.isDir || /\.(mp4|m4v|webm|mov)$/i.test(item.name)))
const duration = computed(() => { const seconds = props.request?.intent.viewingSeconds ?? 0; return seconds % 3600 === 0 ? `${seconds / 3600} hours` : `${Math.ceil(seconds / 60)} minutes` })
@@ -0,0 +1,19 @@
import { mount, flushPromises } from '@vue/test-utils'
import { afterEach, expect, it, vi } from 'vitest'
import { webcrypto } from 'node:crypto'
const files = vi.hoisted(() => ({ login: vi.fn(async () => true), listDirectory: vi.fn(async () => []) }))
vi.mock('@/api/filebrowser-client', () => ({ fileBrowserClient: files }))
import MediaRegistrationConsent from '../MediaRegistrationConsent.vue'
afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks() })
it('mounts the inactive dashboard consent and opens its labelled dialog on LAN HTTP', async () => {
vi.stubGlobal('crypto', { getRandomValues: webcrypto.getRandomValues.bind(webcrypto) })
const wrapper = mount(MediaRegistrationConsent, { props: { request: null, phase: 'select', error: '' } })
expect(files.login).not.toHaveBeenCalled()
await wrapper.setProps({ request: { intent: { viewingSeconds: 3600, priceSats: 15 } } as any })
await flushPromises()
const dialog = wrapper.get('[role="dialog"]')
const label = dialog.attributes('aria-labelledby')
expect(label).toMatch(/^media-registration-[0-9a-f-]{36}$/)
expect(wrapper.get('#' + label).text()).toBeTruthy()
wrapper.unmount()
})