what the mobile app needs is now buildable against something real: GET /apps, GET /key, GET /install/:app/:platform/:version, POST /installs, plus device and build verbs. all under /api/kiosk, all behind a platform session. the install URL is built per request and never stored, because it carries the CALLING user's key - two people asking for the same build get different links, which is the point: the artefact route is public and the key is what authorises it. deliberately NOT here: the four requests that arrive with no session. installd fetching the manifest and the ipa, the device posting its udid, safari downloading a mobileconfig. a plugin cannot serve an unauthenticated route today, so those wait for the platform primitive and will land in api/public.ts - a separate file so the two surfaces cannot be confused by whoever reads either one. so /install returns a correct URL pointing at a route that does not exist yet. that is the shape the app should build against. layout is a horizontal root with the app list first, and that is load-bearing: on a phone WorkspaceRenderer collapses horizontal groups to children[0] and PluginScreen passes no mobilePanelId, so first is the only panel a phone shows. the install page is the one page that must work in mobile safari. PUBLIC_URL is read from the environment rather than officer-url.mjs, which exports only loopback URLs - ios is not on loopback, and it needs https with a cert it already trusts or it declines silently.
59 lines
3.2 KiB
TypeScript
59 lines
3.2 KiB
TypeScript
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/<app>/<version>/`. 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' };
|
|
}
|