feat(fpagent): public installer and private-release proxy

Serve /fpagent/install.sh (also curl|bash) and proxy the private
fingerprint-agent releases through /fpagent/releases/* using
GITEA_RELEASE_INSTALL_KEY, so a field laptop needs no forge credentials.
The installer picks the offline bundle for the host codename when present,
verifies the published sha256, then hands off to the bundle's own installer.
This commit is contained in:
2026-10-02 09:53:35 +01:00
parent 1ddf61b59f
commit b3b75ffac0
3 changed files with 343 additions and 1 deletions
+80 -1
View File
@@ -20,6 +20,11 @@ export const PORTAL_URL = 'https://portal.yrka.app'; // web-auth + openfga/verif
export const PB_URL = 'https://pb.itcass.net'; // PocketBase session refresh
export const A2LOFT_BASE = 'https://apis.a2loft.net'; // staff lookup + fingerprint save
export const AGENT_URL = 'http://127.0.0.1:9000'; // fpagent Socket.IO
export const GITEA_URL = 'https://sources.yrka.app'; // fpagent release forge (private)
export const FPAGENT_REPO = 'YOUreka/fingerprint-agent';
// The Gitea token used to read the private fpagent releases. The operator's
// laptop never sees it: /fpagent/* proxies the bundles through this server.
const GITEA_RELEASE_KEY = process.env.GITEA_RELEASE_INSTALL_KEY || '';
export const DOMAINS = [
{ code: 'uc', name: 'UNICAL' },
{ code: 'mtr', name: 'Metropolitan Hotel' },
@@ -272,6 +277,10 @@ export function createApp(overrides = {}) {
agentUrl: AGENT_URL,
domains: DOMAINS,
ttlMs: DOMAIN_TTL_MS,
giteaUrl: GITEA_URL,
fpagentRepo: FPAGENT_REPO,
giteaToken: GITEA_RELEASE_KEY,
giteaFetch: fetch,
fetch: fetch,
now: Date.now,
...overrides,
@@ -314,12 +323,82 @@ export function createApp(overrides = {}) {
setHeaders: (res, filePath) => {
res.setHeader(
'Cache-Control',
filePath.endsWith('.html') ? 'no-store' : 'public, max-age=31536000, immutable'
filePath.endsWith('.html') || filePath.endsWith('.sh')
? 'no-store'
: 'public, max-age=31536000, immutable'
);
},
})
); // assets only; '/' is session-aware
// ── fpagent installer: public proxy for the private release bundles ───────
// The install script (public/fpagent/install.sh) runs on a field laptop with
// no Gitea credentials, so this server reads the private release with the
// token and streams the bundle back. The token and forge host never leave
// here. Registered before the session middleware: the installer is public.
const giteaBase = () => cfg.giteaUrl.replace(/\/+$/, '');
const giteaHeaders = () => (cfg.giteaToken ? { authorization: `token ${cfg.giteaToken}` } : {});
const SAFE_ASSET = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
const installerNotConfigured = (res) =>
res.status(503).json({
ok: false,
message: 'The Fingerprint Agent installer is not configured on this deployment.',
});
app.get('/fpagent/releases/latest', async (req, res) => {
if (!cfg.giteaToken) return installerNotConfigured(res);
try {
const r = await cfg.giteaFetch(`${giteaBase()}/api/v1/repos/${cfg.fpagentRepo}/releases/latest`, {
headers: giteaHeaders(),
});
if (!r.ok) {
return res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
}
const release = await r.json();
const tag = String(release?.tag_name || '');
const assets = (Array.isArray(release?.assets) ? release.assets : [])
.map((a) => String(a?.name || ''))
.filter((name) => SAFE_ASSET.test(name))
.map((name) => ({
name,
url: `/fpagent/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`,
}));
if (!tag || assets.length === 0) {
return res
.status(502)
.json({ ok: false, message: 'The Fingerprint Agent release has no downloadable bundles.' });
}
res.set('Cache-Control', 'no-store').json({ ok: true, tag, assets });
} catch {
res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
}
});
app.get('/fpagent/releases/download/:tag/:name', async (req, res) => {
if (!cfg.giteaToken) return installerNotConfigured(res);
const { tag, name } = req.params;
if (!SAFE_ASSET.test(tag) || !SAFE_ASSET.test(name)) {
return res.status(400).json({ ok: false, message: 'Unknown release asset.' });
}
try {
const r = await cfg.giteaFetch(
`${giteaBase()}/${cfg.fpagentRepo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`,
{ headers: giteaHeaders() }
);
if (!r.ok) {
return res
.status(r.status === 404 ? 404 : 502)
.json({ ok: false, message: 'Release asset not available.' });
}
res
.set('Content-Type', r.headers.get('content-type') || 'application/octet-stream')
.set('Cache-Control', 'no-store')
.send(Buffer.from(await r.arrayBuffer()));
} catch {
res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
}
});
// ── session middleware: recover / refresh the YOUreka Auth session ────────
app.use(async (req, res, next) => {
req.session = null;