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:
+80
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user