import { apiRequest, type ApiOutcome } from './client' /* The nine endpoints of the `apps` family (upstream/master:www/js/apps.js). Shapes verified with curl against a real instance (v15.x) on 2026-08-25, not deduced from the code: 1. `updateVersion`, `updateUrl` and `updateAvailable` are OPTIONAL on an installed app. The server only writes them if the app appears in the store catalog AND there is a version there compatible with this server (WebServiceAppsApi.cs:60-99). An app installed from your own zip never brings them. On top of that, `apps/list` queries the catalog with a 5 s timeout and, if it fails, returns the list WITHOUT those three fields on any app. 2. `installedApp` / `updatedApp` (what install, update, downloadAndInstall and downloadAndUpdate return) never bring them either: that `WriteAppAsJson` is called without the catalog. Upstream redraws the row with that response, so after installing, the "Store Update" button disappears until the next reload of the list. Here the list is reloaded and that is that. 3. `config/get` can return `config: null`, not only a string. It happens as soon as someone saves an empty config: `config/set` with an empty string stores it as null (WebServiceAppsApi.cs:494-496). 4. `apps/uninstall` of an app that does not exist answers `ok`, not an error. */ export interface DnsAppDetail { classPath: string description: string isAppRecordRequestHandler: boolean recordDataTemplate: string | null isRequestController: boolean isAuthoritativeRequestHandler: boolean isRequestBlockingHandler: boolean isQueryLogger: boolean isQueryLogs: boolean isPostProcessor: boolean } export interface InstalledApp { name: string description: string | null version: string updateVersion?: string updateUrl?: string updateAvailable?: boolean dnsApps: DnsAppDetail[] } export interface StoreApp { name: string description: string version: string url: string size: string installed: boolean /** Only when `installed` is true. */ installedVersion?: string /** Only when `installed` is true. */ updateAvailable?: boolean } /* The capability labels of each of the app's classes, in the exact order of `getAppRowHtml` (apps.js:80-104). If there is none, upstream puts "Generic". */ export function dnsAppLabels(d: DnsAppDetail): string[] { const labels: string[] = [] if (d.isAppRecordRequestHandler) labels.push('APP Record') if (d.isRequestController) labels.push('Access Control') if (d.isAuthoritativeRequestHandler) labels.push('Authoritative') if (d.isRequestBlockingHandler) labels.push('Blocking') if (d.isQueryLogger) labels.push('Query Logger') if (d.isQueryLogs) labels.push('Query Logs') if (d.isPostProcessor) labels.push('Post Processor') return labels.length > 0 ? labels : ['Generic'] } export function listApps( token: string | null, node = '', ): Promise> { return apiRequest('apps/list', { token, body: { node } }) } export function listStoreApps( token: string | null, ): Promise> { return apiRequest('apps/listStoreApps', { token }) } export function downloadAndInstall( token: string | null, name: string, url: string, ): Promise> { return apiRequest('apps/downloadAndInstall', { token, body: { name, url } }) } export function downloadAndUpdate( token: string | null, name: string, url: string, ): Promise> { return apiRequest('apps/downloadAndUpdate', { token, body: { name, url } }) } export function uninstallApp(token: string | null, name: string): Promise { return apiRequest('apps/uninstall', { token, body: { name } }) } /* `node` is the name of the cluster's PRIMARY node: upstream always reads from it so as not to show a config that has not propagated yet (apps.js:460). Without a cluster it sends the empty string and the server ignores it (DnsWebService.cs:2367-2370). The primary's name comes from `sessionData.info.clusterNodes`, which this console does not expose yet. */ export function getAppConfig( token: string | null, name: string, node = '', ): Promise> { return apiRequest('apps/config/get', { token, body: { name, node } }) } /* POST, because a config can be long and does not fit in a query. The server reads both parameters with `QueryOrForm`, so both go in the body. An empty config is stored as `null`: that is the server's doing, not ours. */ export function setAppConfig( token: string | null, name: string, config: string, ): Promise { return apiRequest('apps/config/set', { token, method: 'POST', body: { name, config } }) } /* --- Zip upload: pending multipart support in the client --- `apps/install` and `apps/update` are the ONLY TWO calls of this family that are not an ordinary request: they are POST `multipart/form-data` with the zip (apps.js:348 and apps.js:392). The server requires `HasFormContentType` and takes `Form.Files[0]` (WebServiceAppsApi.cs:350-357 and 401-408), so: - the field NAME of the file does not matter (verified with curl: uploading the zip as `anyNameAtAll` installs just the same). `fileAppZip` is sent here only out of fidelity to upstream; - `name` may go in the query, and that is what upstream does; - they are POST-only: by GET the server answers 404 (DnsWebService.cs:2183-2184). `apiRequest` only knows how to send `x-www-form-urlencoded` and is not touched from here. `buildUpload` leaves the request built for the day the client accepts a `FormData`; that day `installApp`/`updateApp` become one line each. */ export interface UploadRequest { path: string form: FormData } export function buildUpload(path: string, name: string, file: File): UploadRequest { const form = new FormData() form.append('fileAppZip', file) return { path: `${path}?${new URLSearchParams({ name }).toString()}`, form } } export function installApp( token: string | null, name: string, file: File, ): Promise> { const { path, form } = buildUpload('apps/install', name, file) return apiRequest(path, { token, form }) } export function updateApp( token: string | null, name: string, file: File, ): Promise> { const { path, form } = buildUpload('apps/update', name, file) return apiRequest(path, { token, form }) } /* The hub's matrix of apps over servers (`hub/apps/matrix`, hub/apps-matrix.ts). Only on a hub. A config is never in it: just a hash per server and the comparison. */ export interface MatrixCell { installed: boolean version?: string updateAvailable?: boolean updateVersion?: string configStatus: 'none' | 'read' | 'unreadable' configHash?: string configError?: string } export interface MatrixApp { name: string description: string servers: Record configs: 'same' | 'different' | 'single' | 'unknown' diffCount?: number versions: 'same' | 'different' | 'single' } export interface AppsMatrixData { servers: { id: string; ok: boolean; error?: string }[] apps: MatrixApp[] } export function getAppsMatrix(token: string | null): Promise> { return apiRequest('hub/apps/matrix', { token }) }