export type DiscoveredDevice = Readonly<{ origin: string; address: string; port: number; secure: boolean; httpStatus: number; identity: Readonly>; knownInstanceId: string | null; }>; export type DiscoverySession = Readonly<{ sessionId: string; status: 'scanning' | 'completed'; createdAt: string; expiresAt: string; scanned: number; total: number; ranges: readonly string[]; devices: readonly DiscoveredDevice[]; }>; export type DeviceProbe = Readonly<{ origin: string; reachable: boolean; httpStatus: number | null; identity: Readonly>; knownInstanceId: string | null; }>; export interface DiscoveryDataSource { start(signal?: AbortSignal): Promise; status(sessionId: string, signal?: AbortSignal): Promise; renew(sessionId: string, signal?: AbortSignal): Promise; stop(sessionId: string): Promise; probe(deviceUrl: string, signal?: AbortSignal): Promise; } const BASE = '/api/v1/discovery'; const MAX_TEXT = 200; const SESSION_ID = /^[0-9a-fA-F-]{1,64}$/u; const ORIGIN = /^https?:\/\/[A-Za-z0-9.[\]:_-]{1,120}(?::\d{1,5})?$/u; const IDENTITY_FIELDS: readonly string[] = [ 'model', 'manufacturer', 'brand', 'firmware_version', 'os_version', 'version', ]; const record = (value: unknown): Record | undefined => typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record) : undefined; const shortText = (value: unknown): value is string => typeof value === 'string' && value.length > 0 && value.length <= MAX_TEXT && !/[\x00-\x1F\x7F]/u.test(value); const integer = (value: unknown, minimum = 0, maximum = Number.MAX_SAFE_INTEGER): value is number => typeof value === 'number' && Number.isInteger(value) && value >= minimum && value <= maximum; function identity(value: unknown): Readonly> { const source = record(value); if (!source) return {}; const result: Record = {}; for (const field of IDENTITY_FIELDS) { const entry = source[field]; if (shortText(entry)) result[field] = entry; } return result; } function device(value: unknown): DiscoveredDevice | undefined { const source = record(value); if (!source) return undefined; const { origin, address, port, secure, httpStatus, knownInstanceId } = source; if (!shortText(origin) || !ORIGIN.test(origin)) return undefined; if (!shortText(address) || !integer(port, 1, 65_535) || typeof secure !== 'boolean') return undefined; if (!integer(httpStatus, 100, 599)) return undefined; if (knownInstanceId !== null && !shortText(knownInstanceId)) return undefined; return { origin, address, port, secure, httpStatus, identity: identity(source.identity), knownInstanceId: typeof knownInstanceId === 'string' ? knownInstanceId : null, }; } function session(value: unknown): DiscoverySession { const source = record(value); const sessionId = source?.sessionId; if (!source || !shortText(sessionId) || !SESSION_ID.test(sessionId)) throw new Error('Discovery response is invalid.'); const { status, createdAt, expiresAt, scanned, total, ranges } = source; if (status !== 'scanning' && status !== 'completed') throw new Error('Discovery response is invalid.'); if (!shortText(createdAt) || !shortText(expiresAt)) throw new Error('Discovery response is invalid.'); if (!integer(scanned) || !integer(total) || !Array.isArray(ranges)) throw new Error('Discovery response is invalid.'); const devices = ranges.every((entry) => shortText(entry)) ? source.devices : undefined; if (!Array.isArray(devices)) throw new Error('Discovery response is invalid.'); const parsed = devices.map(device); if (parsed.some((entry) => entry === undefined)) throw new Error('Discovery response is invalid.'); return { sessionId, status, createdAt, expiresAt, scanned, total, ranges: ranges as string[], devices: parsed as DiscoveredDevice[], }; } function probe(value: unknown): DeviceProbe { const source = record(value); if (!source) throw new Error('Discovery response is invalid.'); const { origin, reachable, httpStatus, knownInstanceId } = source; if (!shortText(origin) || !ORIGIN.test(origin) || typeof reachable !== 'boolean') throw new Error('Discovery response is invalid.'); if (httpStatus !== null && !integer(httpStatus, 100, 599)) throw new Error('Discovery response is invalid.'); if (knownInstanceId !== null && !shortText(knownInstanceId)) throw new Error('Discovery response is invalid.'); return { origin, reachable, httpStatus: typeof httpStatus === 'number' ? httpStatus : null, identity: identity(source.identity), knownInstanceId: typeof knownInstanceId === 'string' ? knownInstanceId : null, }; } export function createDiscoveryApiDataSource( fetcher: typeof fetch = globalThis.fetch, ): DiscoveryDataSource { const request = async ( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, signal?: AbortSignal, body?: unknown, ): Promise => { const response = await fetcher(`${BASE}${path}`, { method, credentials: 'same-origin', headers: body === undefined ? { accept: 'application/json' } : { accept: 'application/json', 'content-type': 'application/json' }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), ...(signal ? { signal } : {}), }); if (!response.ok) throw new Error('Discovery request failed.'); if (response.status === 204) return undefined; return (await response.json().catch(() => undefined)) as unknown; }; const data = async ( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, signal?: AbortSignal, body?: unknown, ): Promise> => { const payload = record(await request(method, path, signal, body)); const envelope = payload ? record(payload.data) : undefined; if (!envelope) throw new Error('Discovery response is invalid.'); return envelope; }; return { async start(signal) { return session(await data('POST', '/sessions', signal, {})); }, async status(sessionId, signal) { return session(await data('GET', `/sessions/${encodeURIComponent(sessionId)}`, signal)); }, async renew(sessionId, signal) { return session(await data('PUT', `/sessions/${encodeURIComponent(sessionId)}`, signal)); }, async stop(sessionId) { await request('DELETE', `/sessions/${encodeURIComponent(sessionId)}`); }, async probe(deviceUrl, signal) { return probe(await data('POST', '/probe', signal, { device_url: deviceUrl })); }, }; }