PSA/ee/appliance/host-service/install-code.mjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

106 lines
4.5 KiB
JavaScript

/**
* Install-code redemption for appliance setup.
*
* At first-boot setup the operator enters a one-time INSTALL CODE (from their
* registration email). The host-service redeems it against the alga-license
* service `/register` endpoint, which returns the registry-minted tenant id the
* appliance adopts (INITIAL_TENANT_ID) plus the edition and — for paid tiers —
* the first license JWT + per-appliance credential + check-in URL. Those flow
* into the appliance-initial-tenant and appliance-license-seed Secrets, so the
* appliance comes up already bound to its registry tenant.
*
* Pure/host-only: no kube or DB access, so it's unit-testable with a mock fetch.
*/
import crypto from 'node:crypto';
import fs from 'node:fs';
/**
* Stable per-appliance id for /register (appliances are keyed by it, so check-in
* later reuses the same one). Prefer the host machine-id; fall back to a hash of
* the app hostname.
*/
export function deriveApplianceId(appHostname, machineIdPath = '/etc/machine-id') {
try {
const mid = fs.readFileSync(machineIdPath, 'utf8').trim();
if (mid) return `appliance-${mid.replace(/[^a-f0-9]/gi, '').slice(0, 16)}`;
} catch {
// no machine-id (non-Linux / restricted) — fall through to the hostname hash
}
const h = crypto.createHash('sha256').update(String(appHostname || 'appliance')).digest('hex').slice(0, 16);
return `appliance-${h}`;
}
const FRIENDLY_ERRORS = {
invalid_claim_code: 'Invalid install code. Check the code from your registration email and try again.',
expired_claim_code: 'Install code has expired. Request a fresh one from the portal (re-issue).',
consumed_claim_code: 'Install code has already been used. Request a fresh one from the portal (re-issue).',
};
/**
* Redeem an install code against alga-license `/register`.
* @returns {Promise<{tenantId,edition,companyName,contactEmail,licenseToken,applianceCredential,checkInUrl,applianceId}>}
* @throws {Error} with a setup-friendly message on a bad/expired/used code or an
* unreachable service (surfaced to the setup UI; install is blocked, never
* silently falls back to a self-generated tenant).
*/
export async function redeemInstallCode({ serviceUrl, installCode, applianceId, fetchImpl }) {
const doFetch = fetchImpl || globalThis.fetch;
if (!serviceUrl) {
throw new Error('License service URL is not configured (ALGA_LICENSE_SERVICE_URL); cannot redeem the install code.');
}
const url = `${String(serviceUrl).replace(/\/$/, '')}/register`;
const code = String(installCode || '').trim().toUpperCase();
let res;
try {
res = await doFetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ claim_code: code, appliance_id: applianceId }),
});
} catch (err) {
throw new Error(`Could not reach the license service at ${url} to redeem the install code: ${err instanceof Error ? err.message : String(err)}`);
}
if (!res.ok) {
let body = {};
try { body = await res.json(); } catch { /* non-JSON error body */ }
const err = new Error(FRIENDLY_ERRORS[body.code] || body.error || `Install-code redemption failed (HTTP ${res.status}).`);
// An invalid/expired/used code is operator-correctable: setup keeps the form
// open so they can re-enter a fresh (re-issued) code, and stops auto-retrying
// a code that will never change. (Network/reach errors are transient — they
// stay retry-safe and are NOT flagged here.)
if (FRIENDLY_ERRORS[body.code]) err.correctable = true;
throw err;
}
const data = await res.json();
return {
tenantId: data.tenant_id || '',
edition: data.edition || 'essentials',
companyName: data.company_name || '',
contactEmail: data.contact_email || '',
licenseToken: data.first_jwt || null,
applianceCredential: data.appliance_credential || null,
checkInUrl: data.check_in_url || null,
applianceId,
};
}
/**
* Map a redeem result to the appliance-license-seed Secret literals consumed by
* the bootstrap (appliance-bootstrap.sh). The appliance always runs the EE image:
* - essentials → EE, no token, no auto-trial (INSTALL_EDITION suppresses it)
* - pro/premium → EE, licensed via the minted token (+ connected refresh)
*/
export function licenseSeedFromRedeem(redeem) {
return {
EDITION_CHOICE: 'ee',
INSTALL_EDITION: redeem.edition || 'essentials',
LICENSE_TOKEN: redeem.licenseToken || '',
APPLIANCE_ID: redeem.applianceId || '',
APPLIANCE_CREDENTIAL: redeem.applianceCredential || '',
CHECK_IN_URL: redeem.checkInUrl || '',
};
}