Files
kiosk/artefacts.ts
T
Claude Opus 5 b729aa3a67 kiosk: the authenticated API and two panels
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.
2026-08-18 03:07:56 +00:00

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' };
}