import { join } from 'node:path'; import { OFFICER_ROOT } from '@@/data-path'; import type { KioskBuild } from './db/types'; /* Where the artefacts live, and why not in this directory. `$OFFICER_ROOT/data/kiosk///`. NOT under `plugins/kiosk/`, and that is not a preference: a plugin directory is a git clone that an update replaces, so artefacts kept there would vanish on upgrade with nothing logging it. `DATA_PATH` proper is keyed by user email and these are not per-user — a build is one file that many people install. Builds arrive by rsync from the Mac, which already works and keeps a credential it already has. The plugin records metadata about a file already on disk rather than accepting an upload; the platform's shared upload route caps at 5MB and no `.ipa` will ever satisfy that. */ export const KIOSK_ROOT = join(OFFICER_ROOT, 'data', 'kiosk'); /** Absolute path to a build's artefact. `artefactPath` is stored relative, because the root moves. */ export const artefactPath = (build: KioskBuild): string => join(KIOSK_ROOT, build.artefactPath); /** * What to hand the OS to start an install. * * iOS cannot be given a `.ipa` directly — it takes an `itms-services://` URL naming a **manifest**, which * the OS fetches itself and which in turn names the binary. Both of those fetches arrive with no session, * which is why the key is in the query string rather than a header: `installd` is not our code and will not * set one. * * Android needs none of this. An `.apk` installs from a plain URL. * * `PUBLIC_URL` rather than a literal: the manifest is the one artefact that embeds a hostname, and the * standalone service learned this the hard way — its publisher and its server disagreed about the default, * which was invisible until a restart pointed every Install button at a host that no longer served this. */ /** * The externally reachable base for this server. * * Read from the environment rather than from `officer-url.mjs`, which exports only loopback URLs — those * are for one process talking to another on the same box, and iOS is neither. * * **iOS requires HTTPS with a certificate it already trusts** for both the manifest and the binary; it * fetches them itself and declines silently when it does not like the certificate, which looks exactly * like a broken build. So a `PUBLIC_URL` that is http, an IP, or a name only the tailnet resolves will * produce installs that fail with nothing useful said. */ const publicBase = (): string => { const url = process.env.PUBLIC_URL?.replace(/\/$/, ''); if (!url) throw new Error('PUBLIC_URL is not set — Kiosk cannot build an install URL without it'); return url; }; export function installUrl(build: KioskBuild, key: string): { url: string; kind: 'itms' | 'apk' } { const base = `${publicBase()}/api/kiosk/artefact/${encodeURIComponent(build.appName)}/${build.platform}/${encodeURIComponent(build.version)}`; if (build.platform === 'android') return { url: `${base}/binary?k=${encodeURIComponent(key)}`, kind: 'apk' }; const manifest = `${base}/manifest.plist?k=${encodeURIComponent(key)}`; return { url: `itms-services://?action=download-manifest&url=${encodeURIComponent(manifest)}`, kind: 'itms' }; }