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

20 KiB
Raw Permalink Blame History

Scratchpad — Appliance registration → download → install flow

  • Plan slug: appliance-registration-install-flow
  • Created: 2026-06-05
  • Source spec: docs/superpowers/specs/2026-06-05-appliance-registration-install-flow-design.md

What This Is

Working memory for the register → download → install (+ re-issue) flow. The spine is a direction inversion: today the appliance generates its own tenant UUID and sends it up at /register; the new flow mints tenant_id upstream at registration, carries it down via a one-time install code, and the appliance adopts it via a new INITIAL_TENANT_ID.

Decisions

  • (2026-06-05) Reuse /register for the friendly install code (decision 1) — one redeem path, reuse the claim-code machinery.
  • (2026-06-05) Extend claim_codes (decision 2): entitlement_id → nullable, add tenant_id FK → tenant_registry. One code type for essentials (no entitlement) and paid. Chosen over a separate install_tokens table.
  • (2026-06-05) Presigned for all downloads (decision 3) — registration-gated, not public; ISO isn't a secret, the code is the gate.
  • (2026-06-05) Re-issue in scope now (decision 4) — portal action resolves an existing registry tenant and mints a fresh code for the same tenant_id.
  • (2026-06-05) Test weighting is smoke-first (user directive): small automated set for the riskiest seams, everything else validated live on the VM. This deliberately inverts the software-planner default (tests longer than features) — tests.json is shorter and smoke-weighted on purpose.
  • (2026-06-05) The short code is the identity carrier for BOTH tiers (not just paid). Essentials installs get a short code too and never type a raw tenant_id; the code's tenant_id is set for everyone, entitlement_id is null for essentials. One-liner framing: "a short code always resolves a tenant; for paid it additionally mints a bound license."
  • (2026-06-05) §16 open questions all resolved: (1) alga-license presigns with object-store creds via env vars; the presigned URL is emailed to the user after registration/purchase (alongside the code) — not a public link. (2) Paid ordering: registry row at form submit, entitlement + code on checkout.session.completed. (3) Essentials edition from tenant_registry.edition (no edition on the code). (4) Reuse generateClaimCode() for install codes. Remaining impl detail (not a fork): which object-store key holds the current generic ISO + how the release process publishes it (F041).

Discoveries / Constraints

  • (2026-06-05) claim_codes baseline is entitlement_id NOT NULL (alga-license/migrations/03_claim_codes.cjs). Needs a new migration to relax it and add tenant_id.
  • (2026-06-05) alga-license accessors that already exist (src/db/db.ts): createRegistryTenant, getRegistryTenant, getRegistryTenantByEmail, insertClaimCode, getClaimCode, consumeClaimCode, revokeClaimCodesForEntitlement, upsertAppliance (already takes tenantId), setEntitlementLicenseSub, getEntitlementById. Signing helpers exist: signLicense (takes aud), generateClaimCode, generateApplianceCredential, generateLicenseId.
  • (2026-06-05) New alga-license work: the migration, insertClaimCode + getClaimCode/ClaimCodeRow for tenant_id/nullable entitlement, a new revokeClaimCodesForTenant (essentials reissue has no entitlement to key on), a setRegistryTenantInstalled, the /register essentials+response changes, the /register-tenant and /install-codes/reissue routes, and a MinIO presign helper.
  • (2026-06-05) /register today (src/routes/register.ts): requires an entitlement (getEntitlementById → 500 if none), binds aud from the request body tenant_id, returns { appliance_credential, first_jwt, check_in_url }. The new flow sources tenant_id from the code row and returns it (+ edition) to the appliance.
  • (2026-06-05) /check-in (src/routes/checkIn.ts) already preserves appliance.tenant_id as aud across re-signs — no change needed.
  • (2026-06-05) The appliance tenant UUID is born in ee/server/src/lib/testing/tenant-creation.ts createTenant (~line 80): it inserts into tenants without a tenant value, so the DB default (gen_random_uuid()) generates it and it's returned via .returning('tenant'). That insert is the exact INITIAL_TENANT_ID seam.
  • (2026-06-05) connectAppliance (server/src/lib/actions/licenseManagementActions.ts, ~line 125) already POSTs { claim_code, appliance_id } to /register and seeds license_state with first_jwt/appliance_credential/check_in_url — but AFTER the tenant exists. The install-time consumer generalizes this to run before create-tenant and adopt the returned tenant_id.
  • (2026-06-05) Licval WIP already in the working tree (uncommitted, NOT part of this plan's deltas — treat as precondition F001): create-tenant.ts + tenant-creation.ts thread INITIAL_ADMIN_PASSWORD/args.password through to createAdminUser (uses input.password ?? generateSecurePassword()), initialize tenant_settings (onboarding pending), and honor DB_HOST/DB_PORT/DB_USER_ADMIN. INITIAL_TENANT_ID lands in the same two files. Do not revert this WIP.

Build-step 1 — alga-license schema + /register (DONE on branch feat/registration-install-flow)

  • (2026-06-05) Two refinements discovered while implementing (both fed back into the PRD/features):
    • claim_codes.tenant_id is NULLABLE, not NOT NULL. Migration alters entitlement_id NULLABLE via knex.raw('ALTER TABLE … DROP NOT NULL') (knex .alter() would try to rebuild the FK) and adds tenant_id uuid nullable FK + index (migrations/05_claim_codes_registry.cjs). /register resolves tenant = row.tenant_id ?? body.tenant_id — the body path is the clean legacy fallback, which only works because tenant_id is nullable.
    • Essentials /register creates NO appliance row. appliances.entitlement_id is NOT NULL (FK), and essentials has nothing to refresh (no license → no check-in), so the essentials branch returns { tenant_id, edition } and does not call upsertAppliance or issue a credential. (Original F012 said "still upsertAppliance" — corrected.)
  • (2026-06-05) createRegistryTenant already accepted a pinned tenantId and returns the row — /register-tenant (build step 3) is mostly wiring.
  • (2026-06-05) RegisterResponse now returns tenant_id + edition + company_name/contact_email; appliance_credential/first_jwt/check_in_url are optional (paid only). The existing alga-psa connectAppliance destructures the three paid fields and ignores the rest, so paid back-compat holds.
  • (2026-06-05) Validated: tsc --noEmit clean; migration applies + rolls back + re-applies on a throwaway postgres:16 (5433); jest 28/28 incl. 3 new seam tests (claim_codes tenant_id round-trip, revokeClaimCodesForTenant, setRegistryTenantInstalled). The repo's signing.test.ts IS the gated-on-DB_HOST pg integration suite — extend that block, don't add a parallel harness.
  • (2026-06-05) Test weighting note: /register HTTP behavior (T003/T004) is smoke, not Fastify route tests — validated in the build-step-4 live loop, per the light-automated directive.

Build-step 2 — INITIAL_TENANT_ID seam (code in working tree, alga-psa; UNCOMMITTED w/ licval WIP)

  • (2026-06-05) Implemented F050/F051/F052/F054/F055: createTenant (ee/server/src/lib/testing/tenant-creation.ts) takes optional tenantId → sets tenantInsert.tenant when present (else DB gen_random_uuid() default, unchanged); idempotency guard returns the existing tenant (+ its client) if the id already exists so a re-run doesn't error/duplicate. Threaded through TenantCreationInputcreateTenantComplete. create-tenant.ts reads INITIAL_TENANT_ID (env or --tenantId) and passes it down.
  • (2026-06-05) Three tenant-creation.ts copies — only ee/server/src/lib/testing/tenant-creation.ts is the live path (what server/scripts/create-tenant.ts imports). packages/ee/src/lib/testing/… is the CE stub (throws). No parallel edit needed.
  • (2026-06-05) No caller breakage: the change is additive-optional; tenant-test-factory.ts (the only other createTenantComplete consumer) is unaffected. Other createTenant( matches are unrelated functions (temporal activities, server test-utils).
  • (2026-06-05) Not committed — these two files also carry the user's licval WIP (password seam). Left uncommitted in the working tree to avoid committing in-flight work; appliance changes (steps 2+4) commit together when the user is ready. F053 (bootstrap passes the redeemed id) + full typecheck land in step 4.
  • (2026-06-05) T007/T008 reclassified AUTO→SMOKE: exercising createTenant's id adoption needs the full alga-psa tenants/clients schema (heavy), so it's validated in the live install loop, not a unit DB.

Build-step 3 — /register-tenant + /install-codes/reissue + presign (DONE, alga-license branch)

  • (2026-06-05) Dependency-free SigV4 presigner (src/storage/presign.ts) — the service had no AWS SDK and is intentionally lean (fastify/knex/pg only), so the S3/MinIO presigned-GET URL is hand-rolled with node:crypto (path-style, UNSIGNED-PAYLOAD, query-auth). now is injectable for deterministic tests.
  • (2026-06-05) Object-store config is env-driven and OPTIONAL: OBJECT_STORE_ENDPOINT/REGION/BUCKET/ACCESS_KEY/SECRET_KEY + APPLIANCE_ISO_KEY (default appliance/current/alga-appliance.iso). getPresignConfigFromEnv() returns null when unset so existing deploys (and /sign /register /check-in) boot without it; only register-tenant/reissue need it, and they emit download_url:'' if it's missing. TTL = claimCodeTtlSeconds (link expiry aligned to the code).
  • (2026-06-05) New routes: /register-tenant (service-authed) creates the registry row, (paid) upserts the entitlement bound to the new tenant, mints the install code carrying tenant_id (+ entitlement), presigns. /install-codes/reissue resolves by tenant_id|email, revokeClaimCodesForTenant, re-attaches the active entitlement via new getActiveEntitlementByTenant, mints a fresh code + link.
  • (2026-06-05) Validated: tsc clean; jest 31/31 (added presign structural test ×2 + getActiveEntitlementByTenant). Route HTTP behavior (T010/T018) is smoke.
  • (2026-06-05) DEPLOY FOLLOW-UP (relates to F041): the alga-license Deployment (service k8s/deployment.yaml + nm-kube-config) must get the OBJECT_STORE_* env + APPLIANCE_ISO_KEY, and the appliance release process must publish the current ISO to that key. Not wired here (ops); register-tenant returns an empty download_url until it is.

Build-step 4 — appliance setup integration (MAPPED; needs coordination, NOT yet built)

  • (2026-06-05) The redeem belongs in the host-service setup-engine.mjs (has the form inputs + network egress; mints the in-cluster secrets), not the in-app connectAppliance (which runs post-tenant). Seam: after normalizeInitialTenant, if an install code is present, POST it to ALGA_LICENSE_SERVICE_URL/register → capture tenant_id + edition (+ paid first_jwt/appliance_credential/ check_in_url), then:
    • add INITIAL_TENANT_ID to the appliance-initial-tenant Secret (initialTenantSecretYaml, ~line 429) → create-tenant adopts it (build-step 2);
    • feed the appliance-license-seed Secret (licenseSeedCmd, ~line 1014: EDITION_CHOICE + optional LICENSE_TOKEN).
  • (2026-06-05) THREE reconciliation decisions block a clean edit (all inside the user's ACTIVE licval license-setup rework):
    1. UX: the setup UI (status-ui/app/setup/page.tsx) already has editionChoice (ee/ce) + a pasted licenseKey. Does the install code become the primary path (overriding those), an added third option, or replace them?
    2. Taxonomy: registry edition = essentials|pro|premium vs appliance editionChoice = ee|ce + license tier pro|premium. Need a mapping (essentials→ce? pro/premium→ee+token?).
    3. Connected seed: appliance_credential/check_in_url are written today only by in-app connectAppliance, not the seed Secret — install-time connected licensing needs new seed plumbing in packages/licensing/src/lib/license-state.ts.
  • (2026-06-05) setup-engine.mjs + status-ui/ are NOT in the uncommitted WIP (safe to edit git-wise) but are conceptually the licval subsystem. F053/F060F068 pend on the three decisions above.

Build-step 4 — appliance setup integration (BUILT in working tree, alga-psa)

  • (2026-06-05) Redeem lives in the host-service. New install-code.mjs (redeemInstallCode / deriveApplianceId / licenseSeedFromRedeem) is pure + unit-tested (7 tests, mock fetch). setup-engine.mjs validateSetupInputs takes installCode; applyRuntimeValuesAndReleaseSelection redeems it (injectable via options.redeemInstallCode for tests) BEFORE building the two Secrets, threading tenant_idinitialTenantSecretYaml(initialTenant, tenantId) (INITIAL_TENANT_ID line) and the license fields → the appliance-license-seed literals (licenseSeedFromRedeem). Redeem failure → preflightFailure blocks the install (F067/F068). 2 new workflow tests; 8/8 pass.
  • (2026-06-05) appliance-bootstrap-configmap.yaml: create-tenant gets INITIAL_TENANT_ID="${INITIAL_TENANT_ID:-}" (create-tenant.ts reads it; empty => DB-generated). License-seed SQL extended: a sql_value() helper + new columns appliance_id/check_in_url/appliance_credential (connected refresh), and the trial is suppressed when INSTALL_EDITION=essentials (essentials resolves to the essentials tier, not an auto premium trial).
  • (2026-06-05) status-ui setup/page.tsx: an Install code field (primary, recommended) above the manual Edition/license fallback; included in the POST. Typechecks clean.
  • (2026-06-05) DEFAULTS I PICKED — confirm during smoke (you authorized sensible defaults + flag):
    1. Taxonomy: the appliance always runs EE (EDITION_CHOICE=ee); essentials = EE-unlicensed (no token, no auto-trial), pro/premium = EE + minted token. The ce choice stays only for a manual community install (no install code).
    2. Connected seed: appliance_id/check_in_url/appliance_credential are seeded straight into license_state by the bootstrap SQL (vs. the in-app connectAppliance plaintext write) so daily check-in works from first boot.
  • (2026-06-05) Two caveats: (a) the install code is single-use and consumed at apply time — a failed install past that point needs a re-issued code (the reissue endpoint exists; consider persisting the redeem to skip re-redeem on retry). (b) ee/appliance/ubuntu-iso/overlay/.../host-service/ is a build-synced COPY — the ISO build must pick up the new install-code.mjs + setup-engine edits (overlay sync, not a manual edit).

Build-step 5 — nm-store (separate repo /home/robert/nm-store; outward-facing)

  • (2026-05-31/06-06) nm-store is local. Its existing order flow is hosted-only (checkout → Temporal OrderInstallWorkflow → hosted tenant; no appliance/ deploymentType concept). Appliance registration is a new outward-facing product surface there.
  • (2026-06-06) BUILT (branch feat/appliance-registration-client, commit 6ab0a427): src/lib/algaLicenseClient.ts — typed registerTenant() + reissueInstallCode() calling /register-tenant + /install-codes/reissue (Bearer ALGA_LICENSE_SERVICE_SECRET, ALGA_LICENSE_SERVICE_URL). This is the transport F071/F075 build on. Logic validated via a tsx harness (the repo's vitest can't run here — missing strip-literal in node_modules under Node 24; the .test.ts is correct and runs once that dep gap is fixed).
  • (2026-06-06) BUILT — commerce surface (decisions: SEPARATE route + essentials+paid), branch feat/appliance-registration-client commit ee53d0b0, tsc clean:
    • /order/appliance registration form (client): essentials → direct register → show code; paid (pro/premium) → embedded Stripe checkout. /order/appliance/reissue.
    • lib/appliance/applianceRegistration.ts: registerApplianceAndEmail, reissueApplianceAndEmail, sendInstallCodeEmail (Resend, reuses the newsletter pattern), and idempotent provisionApplianceFromCheckoutSession — nm-store has no DB, so it writes the minted tenant_id/install_code back onto the Stripe subscription metadata and reuses them on thank-you refresh (so a refresh never re-mints).
    • lib/appliance/applianceCheckout.ts: embedded subscription checkout stamping deploymentType=appliance + edition/contact into metadata, dedicated return_url=/order/appliance/thank-you. Hosted OrderForm/checkout/thank-you untouched (the separate-route choice).
    • Checkout-complete is handled by the thank-you page (Stripe return_url), not a webhook — so the appliance thank-you provisions on render.
    • Email is best-effort (Resend): logs + returns emailSent:false if RESEND_API_KEY unset; the code is always shown on-page too.
  • (2026-06-06) nm-store env to set for PAID: STRIPE_PRICE_ID_APPLIANCE_{PRO,PREMIUM}_{MONTHLY,YEARLY} (+ optional ..._USER_MONTHLY/YEARLY per-user), ALGA_LICENSE_SERVICE_URL, ALGA_LICENSE_SERVICE_SECRET, RESEND_API_KEY/APPLIANCE_FROM_EMAIL. Essentials needs no Stripe price. Tests can't run here (repo vitest strip-literal gap + server-only blocks tsx) — validated by tsc + the tsx client harness + smoke.
  • (2026-06-06) F076 operator doc written: ee/appliance/docs/registration-install-flow.md.
  • (2026-06-06) F041 remains ops: publish the current generic ISO to the object-store key the presigner targets (APPLIANCE_ISO_KEY), and wire the OBJECT_STORE_* env onto the alga-license Deployment (service k8s/deployment.yaml
    • nm-kube-config).

Commands / Runbooks

  • (2026-06-05) alga-license migration/db tests: run against a throwaway docker run postgres:16 (snap docker can't reach /tmp; docker run is fine — prior runs used port 5433). The least-priv app role can't TRUNCATE (by design) — tests DELETE. DB-layer tests are gated on DB_HOST (skip without a DB).
  • (2026-06-05) Build gotcha: the shell has NODE_ENV=production, so npm install omits devDeps (breaks tsc/@types) — use npm install --include=dev.
  • (2026-06-05) alga-license validation one-liner (throwaway pg): docker run -d --name algalic-pg -e POSTGRES_PASSWORD=test -e POSTGRES_DB=alga_license -p 5433:5432 postgres:16 then DB_HOST=localhost DB_PORT=5433 DB_NAME=alga_license DB_USER_ADMIN=postgres DB_PASSWORD_ADMIN=test npm run migrate and DB_HOST=localhost DB_PORT=5433 DB_NAME=alga_license DB_USER_APP=postgres DB_PASSWORD_APP=test npm test. The signLicense tests need the alga-psa fixture key at packages/licensing/src/lib/__test-fixtures__/v1-test.private.pem (present).
  • (2026-06-05) Appliance VM smoke: the full register→download→install loop is validated live on the libvirt appliance VM (see the appliance teardown/reinstall
    • VM ISO-test memories for driving setup via the browser / virsh).
  • Design spec: docs/superpowers/specs/2026-06-05-appliance-registration-install-flow-design.md
  • Registry foundation (already shipped): alga-license PR #3/#4, nm-kube-config PR #50/#51.
  • alga-license routes: src/routes/{register,claimCodes,checkIn,sign,revoke}.ts; db: src/db/db.ts; migrations: migrations/0{1..4}_*.cjs; types: src/api-types.ts.
  • Appliance: ee/server/src/lib/testing/tenant-creation.ts, server/scripts/create-tenant.ts, server/src/lib/actions/licenseManagementActions.ts.

Open Questions

  • (2026-06-05) All four §16 design questions resolved — see the Decisions section. No open design forks remain. Only impl detail left: the object-store key for the current generic ISO + the release-process publish step (F041).