Release v1.4.0 — Elastos Node Manager, multi-arch packaging, app-layer compression#25
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v1.4.0
Headline features
AppProcessManager, fail-closed Ed25519 verification fortype:serviceinstalls).distribution.variants(x64/arm64) for apps with native deps; server-side variant resolution; device-compatibility gating viarequirements.platform.Fixes / hardening
/api/app-backend/:appName/*) — fixes "ENM backend unavailable".Authorizationheader to the live session.UX / performance
Verified
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