import { hostApiFetch } from '@/lib/host-api'; import type { DevicePackageIndexV1, DevicePackageKind, DevicePackageRecordV1, DevicePackageSourceKind, } from '../../shared/device-packages'; const PACKAGE_ID = /^[a-z0-9][a-z0-9._-]{0,127}$/u; function object(value: unknown): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error('device package projection is invalid'); } return value as Record; } function exact(value: Record, keys: readonly string[]): void { const actual = Object.keys(value).sort(); const expected = [...keys].sort(); if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { throw new Error('device package projection has unexpected fields'); } } function text(value: unknown, maximum: number): string { if (typeof value !== 'string' || !value || value.length > maximum) { throw new Error('device package text is invalid'); } return value; } function oneOf(value: unknown, choices: readonly T[]): T { if (typeof value !== 'string' || !choices.includes(value as T)) { throw new Error('device package value is invalid'); } return value as T; } function entry(value: unknown): DevicePackageRecordV1['skillEntries'][number] { const item = object(value); exact(item, ['id', 'entryPath']); return { id: text(item.id, 128), entryPath: text(item.entryPath, 1_024) }; } function record(value: unknown): DevicePackageRecordV1 { const item = object(value); exact(item, [ 'schemaVersion', 'packageId', 'displayName', 'resolvedVersion', 'source', 'kind', 'skillEntries', 'extensionEntries', 'enabled', 'confirmedExecutableCode', 'installedAt', ]); const source = object(item.source); exact(source, ['kind', 'requested', 'resolved']); if (item.schemaVersion !== 1 || typeof item.enabled !== 'boolean' || typeof item.confirmedExecutableCode !== 'boolean' || !Array.isArray(item.skillEntries) || item.skillEntries.length > 100 || !Array.isArray(item.extensionEntries) || item.extensionEntries.length > 100) { throw new Error('device package record is invalid'); } const packageId = text(item.packageId, 128); if (!PACKAGE_ID.test(packageId)) throw new Error('device package id is invalid'); return { schemaVersion: 1, packageId, displayName: text(item.displayName, 256), resolvedVersion: text(item.resolvedVersion, 128), source: { kind: oneOf(source.kind, ['npm', 'git', 'file']), requested: text(source.requested, 2_048), resolved: text(source.resolved, 4_096), }, kind: oneOf(item.kind, ['skill-only', 'pi-extension', 'mixed']), skillEntries: item.skillEntries.map(entry), extensionEntries: item.extensionEntries.map((value) => text(value, 1_024)), enabled: item.enabled, confirmedExecutableCode: item.confirmedExecutableCode, installedAt: text(item.installedAt, 64), }; } export function parseDevicePackageIndex(value: unknown): DevicePackageIndexV1 { const index = object(value); exact(index, ['schemaVersion', 'generation', 'packages']); if (index.schemaVersion !== 1 || !Number.isSafeInteger(index.generation) || (index.generation as number) < 0 || !Array.isArray(index.packages) || index.packages.length > 200) throw new Error('device package index is invalid'); return { schemaVersion: 1, generation: index.generation as number, packages: index.packages.map(record), }; } function id(value: string): string { const normalized = value.trim(); if (!PACKAGE_ID.test(normalized)) throw new Error('device package id is invalid'); return normalized; } export async function readDevicePackages(): Promise { return parseDevicePackageIndex(await hostApiFetch('/api/coding/device-packages')); } export async function setDevicePackageEnabled( packageId: string, enabled: boolean, ): Promise { return parseDevicePackageIndex(await hostApiFetch( `/api/coding/device-packages/${encodeURIComponent(id(packageId))}`, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ enabled }) }, )); } export async function uninstallDevicePackage(packageId: string): Promise { return parseDevicePackageIndex(await hostApiFetch( `/api/coding/device-packages/${encodeURIComponent(id(packageId))}`, { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: '{}' }, )); } export type { DevicePackageIndexV1, DevicePackageRecordV1 } from '../../shared/device-packages';