Skip to content

Release v1.4.0 — Elastos Node Manager, multi-arch packaging, app-layer compression#25

Merged
SashaMIT merged 18 commits into
mainfrom
feature/elastos-node-manager
Jun 26, 2026
Merged

Release v1.4.0 — Elastos Node Manager, multi-arch packaging, app-layer compression#25
SashaMIT merged 18 commits into
mainfrom
feature/elastos-node-manager

Conversation

@SashaMIT

Copy link
Copy Markdown

Release v1.4.0

Do not merge until sign-off. Merging this ships to every PC2 user on their next launcher update (git reset --hard origin/main). v1.3.0 is already live, so this is the next minor.

Headline features

  • Elastos Node Manager (ENM) — service-type dApp for Council/BPoS node operation, with full operator UI (fleet health, SSE live data, notifications, height series).
  • Service-type app support in pc2-node (AppProcessManager, fail-closed Ed25519 verification for type:service installs).
  • Multi-arch packaging — per-arch distribution.variants (x64/arm64) for apps with native deps; server-side variant resolution; device-compatibility gating via requirements.platform.

Fixes / hardening

  • Same-origin backend proxy for service apps (/api/app-backend/:appName/*) — fixes "ENM backend unavailable".
  • Correct session-DB path injection for spawned service backends — fixes "Authentication required".
  • WalletConnect login resilience — middleware resolves a doubled Authorization header to the live session.

UX / performance

  • dApp Centre: real app icons pre-install + real download progress bar.
  • App-layer gzip with a stream-safe filter (skips SSE / Range / already-encoded).

Verified

  • ENM installed + ran through the multi-arch flow on live arm64 (Jetson) via the private catalog.
  • Compression verified on Jetson: JS/JSON gzipped; SSE, Range (206) and binary untouched.
  • ENM multi-arch CI green on x64 + arm64 (temp branch trigger reverted).

⚠️ Post-merge sequencing (do NOT skip)

ENM only works on this (1.4.0) pc2-node. After this merges and a v1.4.0 release is cut, let users update their nodes before flipping the ENM catalog entry live (staged in .cursor/tasks/ENM-RELEASE-PREP/). Flipping it earlier makes older nodes hit "backend unavailable".

Not included / excluded from commit

Pre-existing local doc WIP (CHANGELOG/README/ROADMAP/community updates) was intentionally left out of the version-bump commit.

Made with Cursor

Elastos DAO and others added 18 commits May 28, 2026 22:08
Adds the ability for pc2-node to run + supervise a backend SERVICE process,
not just serve static web apps. This is the platform capability the Elastos
Node Manager (ENM) needs: ENM ships a long-running Node backend that must be
launched, health-checked, restarted, and torn down by pc2-node.

The feature is GENERIC — nothing here is hardcoded to ENM; "service" is a new
app type any app can declare. ENM is simply the first consumer.

NEW files:
  - src/services/AppProcessManager.ts (483 L) — the process supervisor:
    spawns the service backend, polls its health-check, auto-restarts with
    exponential backoff, quarantines after 3 crashes, and SIGTERMs cleanly
    on shutdown. Crash count is persisted so quarantine survives a restart.
  - scripts/package-app.mjs — CI bundler: packs an app (frontend + backend +
    signed manifest) into an installable .tar.gz. Sibling of the existing
    manual scripts/package-app.ts; this one is non-interactive for CI.

CHANGED files:
  - src/services/AppInstallService.ts — new AppType 'service' with a
    `backend` manifest block (entry / port / healthCheck / teardown) and
    `externalDataDirs` (paths the app writes outside its bundle). Adds
    installFromLocal() for no-IPFS sideload, gated to data/{dev,test}-apps/.
  - src/api/installed-apps.ts — POST /install-local; uninstall now runs the
    app's teardown hook → stops the process → wipes externalDataDirs; and a
    trusted-publisher signature gate (PC2_TRUSTED_SERVICE_PUBLISHERS) because
    a service app runs a privileged backend.
  - src/api/index.ts — constructs AppProcessManager + stores it on app.locals
    so the shutdown hook + boot hydrate can reach it.
  - src/index.ts — shutdown hook SIGTERMs managed service backends BEFORE
    tearing down db/ipfs, so they flush state cleanly.
  - src/storage/migrations.ts (migration 33) + schema.sql — add pid / port /
    started_at / crash_count columns to installed_apps.
  - src/storage/database.ts — setAppRuntime() / clearAppRuntime() to write
    those runtime columns from AppProcessManager.

Also (separable — a PC2-appliance UX choice, not strictly Node-Manager):
  - src/gui/src/UI/UIDesktop.js — disable Puter's consumer "Welcome to your
    Personal Internet Computer" modal (PC2 is a node appliance, not a
    consumer desktop). Flag-gated via PC2_SHOW_WELCOME.

Diff: 10 files, +1388/-17, zero deletions from upstream.
Adds the Elastos Node Manager (ENM): a self-contained service-type PC2
app that installs, runs, and self-heals a full Elastos Council / BPoS
validator node (mainchain + ESC/EID/PG sidechains + 3 oracles + arbiter)
from the PC2 desktop.

Two parts:
- enm-server/ — standalone Express sidecar that supervises the chain
  processes, exposes /api/enm, and runs the health/self-heal engine.
- src/backend/apps/elastos-node-manager/ — the Puter app (the UI the
  operator opens from the desktop).

Plus build + deploy infra:
- .github/workflows/build-enm-bundle.yml — bundles the app for release.
- scripts/deploy-enm.sh — installs/upgrades the bundle on a PC2 node.

DEPENDS ON #18 (pc2-node service-type app support): ENM installs through
the service-app mechanism that PR adds. Merge #18 first.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings generic `type: "service"` app support into the integration
branch: AppProcessManager (spawn/health-poll/crash-backoff/quarantine/
SIGTERM teardown), service manifest block, externalDataDirs, /install-local
sideload, and the PC2_TRUSTED_SERVICE_PUBLISHERS signature gate.

Conflict resolutions:
- migrations.ts: kept main's migrations 33-35; renumbered PR #18's
  "Migration 33" (installed_apps runtime-state columns) to Migration 36
  and appended it; CURRENT_VERSION -> 36.
- database.ts / types.ts: main moved entity interfaces to types.ts
  (Phase 2-A extraction). Dropped PR #18's inline interface block from
  database.ts; added the new InstalledApp runtime fields
  (pid/port/started_at/crash_count) and AppRuntimeState to types.ts,
  imported/re-exported from database.ts.
- schema.sql: auto-merged runtime columns retained.

Excluded from this merge (handled separately per plan):
- src/gui/src/UI/UIDesktop.js welcome-modal change (restored to main).

Backend tsc clean (pre-existing optional native `canvas` module errors
only). Nothing pushed; integration branch only.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds the ENM application on top of PR #18's service-app support:
- enm-server/  : Express sidecar (auth, routes, services, db.js),
  install-enm.sh, Dockerfile, smoke/verify scripts, operator docs.
- src/backend/apps/elastos-node-manager/ : operator UI bundle
  (index.html, components, services, utils; vendored js-sha3.min.js).
- scripts/deploy-enm.sh and .github/workflows/build-enm-bundle.yml.

Purely additive (no existing files modified); clean merge.
Vendored js-sha3.min.js flagged for the security/audit step.
Integration branch only — not pushed, not on main.

Co-authored-by: Cursor <cursoragent@cursor.com>
…talls

Audit finding (High, mitigated): the route-level gateServiceInstall only
checks that distribution.signedBy is a *trusted identity* and that a
signature is *present*. The actual cryptographic verification
(verifyDistributionSignature) was warn-only — its boolean result was
stored as _signatureVerified but never blocked the install. A service
app declaring a trusted signedBy with a forged/mismatched signature and
a substituted CID bundle would still install AND spawn a
pc2-node-privileged backend, defeating signing for the one path that
most needs it.

Fix: in AppInstallService.install(), when manifest.type === 'service',
throw (before any bytes hit disk) unless the signature verifies against
the fetched bundle bytes. Non-service apps keep the v1 warn-only posture.
install-local (service) remains dev/owner-only, sourced from the
server-controlled dev-apps/test-apps allowlist.

Found during ENM integration security audit. Branch only; not pushed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Validates PR #18's privileged service-app machinery in isolation (real
temp SQLite + real spawned stub services; no Linux/ela/IPFS needed):
- migration-36 runtime columns present (pid/port/started_at/crash_count)
- start() spawns, persists the DB runtime row, passes pc2-node env
  conventions (PORT/APP_DATA_DIR/APP_BUNDLE_DIR/cwd)
- health-check flips lastHealthOk
- stop() SIGTERMs cleanly and clears the runtime row
- repeated crashes trip backoff → quarantine (fail-closed); start() on a
  quarantined app rejects; clearQuarantine resets the counter

Run: npx tsx --test tests/unit/app-process-lifecycle.test.ts  (3/3 pass)
Co-authored-by: Cursor <cursoragent@cursor.com>
Lets an app declare a device requirement so the dApp Centre shows
"Not compatible with this device" instead of letting an install fail —
e.g. the Elastos Node Manager runs Linux node binaries, so it's allowed
on Jetson/Raspberry Pi (linux/arm64) and Linux/x64 servers but blocked
on macOS/Windows desktops.

Two-layer gate:
- platform.ts: PlatformRequirement type + getHostPlatformSummary() +
  evaluatePlatformCompatibility() (authoritative, pure).
- system.ts: GET /api/system/host-platform (authenticate) exposes host
  os/arch/memory/jetson facts.
- AppInstallService: requirements.platform shape validation +
  enforcePlatformCompatibility() refusing incompatible installs on both
  the IPFS and install-local paths (defense-in-depth).
- app-center/index.html: fetches host facts, mirrors the evaluator in
  isHostCompatible(), disables Install + shows the reason on the card and
  detail modal, blocks installApp(), and echoes requirements back so the
  server re-checks the same constraints.
- APP_MANIFEST_SPEC.md: documents requirements.platform.

Tests: tests/unit/platform-compat.test.ts (6/6) incl. the Jetson/Pi-vs-Mac
case. Backend tsc clean. Branch only; not pushed.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ariants

ENM bundles the native module better-sqlite3, whose .node binary is
arch-specific, so a single capsule cannot serve both x64 servers and
arm64 Jetson/Pi. Ship one signed capsule per arch and let the install
target pick the one matching its own architecture.

- schema: add AppDistribution.variants ("<os>-<arch>" -> {cid,signature,size}),
  shared signedBy; document in APP_MANIFEST_SPEC.
- server: install() resolves the host's own-arch capsule (resolveHostVariant,
  pure + unit-tested), overrides cid + verification signature, fails closed
  when no variant matches the host arch.
- dApp Centre: carry the full publisher manifest + distribution onto install
  (fixes service apps installing as web apps / dropping the signature) and gate
  hosts with no published variant for their arch.
- CI: build-enm-bundle.yml matrix x64 (ubuntu-latest) + arm64 (ubuntu-24.04-arm),
  each signed with the PC2_ENM_SIGNING_SEED secret; per-arch artifacts.
- tooling: assemble-enm-entry.mjs folds the two per-arch fragments + CIDs into
  one registry entry; ENM_DAPP_RELEASE_PLAYBOOK documents the gated publish.

Branch-only; no live deploy/pin/registry push.

Co-authored-by: Cursor <cursoragent@cursor.com>
…erge)

Co-authored-by: Cursor <cursoragent@cursor.com>
- build-enm-bundle.yml: push trigger back to [main] only (the feature-branch
  entry was a temporary CI-test hook).
- package-app.mjs: point the printed "next steps" at assemble-enm-entry.mjs
  (the real assembly script) instead of the stale sync-from-pc2 path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Service-app backends (ENM/enm-server on a loopback port) were unreachable
when the GUI is accessed through a supernode domain: the relay forwards only
the main PC2 port, so the frontend's direct cross-origin call to host:4180
timed out ("backend unavailable"). And once reachable, auth 401'd because
pc2-node spawned the service with a wrong/relative session-DB path.

- static.ts: add transparent, SSE-safe loopback reverse-proxy
  /api/app-backend/:appName/* -> 127.0.0.1:<port> (running service apps only,
  appName-validated, loopback target).
- ENM api.js: deriveBackendBase() -> same-origin proxy path (SSE inherits it).
- AppProcessManager: inject ABSOLUTE PC2_NODE_DB_PATH/PC2_NODE_CONFIG_PATH
  derived from the live DB handle (spawned services run with cwd=bundleDir,
  so a relative ./data/pc2.db resolved to the wrong dir).
- DatabaseManager.getDbPath() getter.

Verified on a live Jetson via zzz.ela.city: health, REST auth (whoami ->
isOwner), and SSE streaming all green through the proxy.

Co-authored-by: Cursor <cursoragent@cursor.com>
A stale token lingering in one client layer (localStorage after a node
restart) while another sends a fresh one produces `Bearer <stale>,<fresh>`.
The middleware previously took the first value blindly, picking the stale
token and 401/403-ing the user mid-login — the post-restart WalletConnect
"Verifying wallet ownership" hang. Now we try each comma-separated value and
use the first that maps to a live (non-expired) session, falling back to the
first value so single-token behaviour is unchanged. Garbage-only tokens are
still rejected.

Verified on-device: normal=200, doubled(stale,real)=200, stale-only=401.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two installer UX gaps the rendering/plumbing already half-supported:

- Icons: registryAppToCatalog now carries `builtInIcon` from a catalog
  `iconDataUrl`, and mergeApps falls back to it for not-installed apps
  (it was nulling the icon unless installed). Store card, staff-pick and
  the install modal already render `builtInIcon` via <img>, so apps now
  show their real logo BEFORE install instead of a gradient placeholder.

- Progress: install() fetched the bundle with no onProgress, so the bar
  sat at 5% for the whole download then jumped to 55%. Thread onProgress
  through fetchFromIPFS -> pinRemoteCID (already supported) and emit a
  scaled `fetching` % (5..55) with bytesReceived/totalBytes from the
  resolved variant size, giving a real filling bar + MB counter.

Verified on arm64 Jetson: served catalog carries the ENM iconDataUrl and
arm64 variant size; backend rebuilds clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
pc2-node served all responses uncompressed at the app layer. Behind the
supernode nginx this is moot (nginx gzips), but self-hosted nodes served
DIRECTLY on the node port shipped the 3.2MB GUI bundle + JSON raw.

Add `compression` with a conservative filter that NEVER touches responses
where re-encoding corrupts the stream:
  - text/event-stream  → SSE live feeds (install progress, ENM) must flush
  - 206 / Content-Range → partial-content (media seeking, File Explorer
    Range fetches) break when compressed
  - Content-Encoding present → no double-encode
  - images/video/octet-stream → compression.filter mime-db check skips them,
    so binary File Explorer downloads pass through untouched

Verified on arm64 Jetson against the node directly: JS bundle + JSON API
return Content-Encoding: gzip; a Range request returns 206 with no encoding;
binary/SSE unaffected.

Co-authored-by: Cursor <cursoragent@cursor.com>
…efore merge)

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ompression)

Co-authored-by: Cursor <cursoragent@cursor.com>
@SashaMIT SashaMIT merged commit f5b0562 into main Jun 26, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant