@xyo-network/dapp-kit-node
v1.1.3
Published
Node.js host adapters for headless XL1 dApps
Readme
@xyo-network/dapp-kit-node
Node.js host adapters for @xyo-network/dapp-kit.
createNodeDappHost() is the headless process-owner facade. The caller supplies
a synchronous factory for the early neutral DappLaunch plus an explicit
cli-kit ProcessHost; asynchronous planning, provisioning, recovery, and actor
startup remain inside the launch. The facade retains the owning session and
returns only application ports from whenReady(). It composes cli-kit's
RuntimeSession, graceful interrupt handling, and cleanup watchdog without a
Yargs parser or terminal UI. Its result distinguishes launch, terminal, and
shutdown failures after bounded cleanup. Its liveness, readiness, health,
convergence, and writability probes read only the canonical Dapp Status.
createNodeDappRuntimeLaunch() and createNodeDappRuntimeHost() provide the
trusted plan-to-runtime path. They accept an already-resolved immutable
DappPlan, a NodeDappRuntimeCatalog, explicit incarnation identity, a
recovery function, exact port binder, and canonical status dimensions. The
launch is returned before catalog resolution starts. During boot the composer
verifies the plan and installed metadata, compiles one actor/provider system,
adopts the provisioned owner, runs recovery before actor construction, selects
the plan's sequential or parallel actor-system policy, and binds only the
ports assigned to that host. The port binder receives actors, plan material,
and port IDs but no provider system or locator. The recovery function receives
the one outer provider system because it is the host-owned reconciliation
boundary. A process interrupt is visible to recovery before actors start.
The composer does not load Definition/Configuration documents, dynamically install code, resolve an unbound provider graph, or choose product-specific journals and XL1 adapters. Those remain explicit application assembly inputs.
DappDeploymentStateStore atomically persists deterministic deployment locks
and noncanonical run records. Its default state root is the repository's
.xl1/ directory; callers may supply an explicit state root so xl1-dapp test
can isolate operational records without changing repository-retained dev state.
createNodeApplicationPortServer() attaches the complete application-port
protocol to an existing Node HTTP server at the exact /_xl1/ports WebSocket
path. The caller authenticates each upgrade and supplies one grant plus its
allowlisted operations; the adapter verifies principal and expiry, forbids
binary/non-I-JSON frames, enforces the negotiated size limit, and aborts host
work when the connection closes. Closing an attachment never stops the owning
dApp runtime.
createNodeDappStatusHttpHandler() is a composable HTTP facade for
/_xl1/status, /_xl1/ready, and /_xl1/health. Every response is derived
from one validated canonical DappStatus, uses no-store, and exposes no actor,
provider, locator, or reflective operation inventory. HTTP is therefore a probe
surface, not a second application protocol.
NodeDappRuntimeCatalog is the explicit trusted installed-code catalog for this
composition layer. Actor and provider registrations bind executable
actor-system descriptors to exact versioned plan metadata. resolve() first
rebuilds the supplied plan and checks its planId, then selects one host
partition, its exact actor order, and only the transitive providers required by
that partition. It returns least-scope actor-system registries/configuration and
port IDs. Missing installations, hash drift, capability/dependency or execution
mode drift, duplicate monikers, and unversioned runtime identity ambiguity fail
before any factory runs. It does not dynamically import package names or treat
plan source hints as provenance. Installed dApp execution-mode support is verified
separately from actor-system lifetime: an on-demand actor may remain available
for multiple attached operations without creating autonomous work, while a
declared resident actor cannot be installed as a one-shot lifecycle.
A host artifact must also register exactly one product-owned
NodeDappArtifactHostFactory for every host ID in its admitted manifest. This
factory is the explicit composition boundary for recovery, status dimensions,
port binding, and attachment authority; the generic runner does not infer those
behaviors from actor/provider descriptors. Artifact installation verifies both
the descriptor inventory and host-factory inventory before either may launch.
Factories borrow already-provisioned resource capabilities while their returned
sessions own only the actor/provider runtime they construct.
The initial adapter is FileEffectReceiptJournal, a crash-durable,
append-validated effect receipt journal backed by one atomically replaced JSON
file. A single dApp host must have exclusive write ownership of the journal
path for the lifetime of the host. Multiple processes writing the same path are
not supported.
import { FileEffectReceiptJournal } from '@xyo-network/dapp-kit-node'
const journal = new FileEffectReceiptJournal('/var/lib/example/effects.json')
const admitted = await journal.prepare(preparedEffectRecord)
const signed = await journal.append(admitted.record.receipt, signedReceipt)The containing directory must exist and be writable. A successful append means
the replacement file and its directory entry have both been synced. The
onFault option is deterministic test instrumentation and should not be used
as application control flow. A fresh effect cannot be created with append();
the journal requires prepare() so the initial receipt and private,
hash-verified material sidecar are one atomic durability unit. Ordinary
read()/list() calls never expose that sidecar. readPreparation() is a
privileged effect-runner operation, and compactPreparation() removes only the
sidecar after the receipt proves its safe recovery handoff.
FileDappEventStore is the crash-durable, single-owner Node reference for the
neutral event stream, subscription runner, consumer-outcome, and wake-outbox
ports. It deliberately commits those related records in one canonical JSON
file, so an append and every applicable subscription wake become durable in the
same atomic replacement. A successful mutation has fsynced the temporary file,
renamed it, and fsynced the containing directory.
import { FileDappEventStore } from '@xyo-network/dapp-kit-node'
const events = new FileDappEventStore('/var/lib/example/events.json')
await events.createSubscription(subscriptionDefinition)
await events.append({ draft })The store serializes operations inside one owner, rejects cross-record identity
or fingerprint conflicts, requires the exact durable consumer outcome before a
cursor transition, and re-arms a newer wake generation if lag remains while an
older wake is acknowledged. recover(now) releases expired running leases and
repairs lagging wake state. Multiple processes writing the same path are not
supported.
Repository conformance sends real SIGKILL to a child at six filesystem and
workflow boundaries, then resumes through a fresh process. Deterministic tests
cover the concurrent final-release/append race and expired-lease recovery. The
opt-in pnpm test:api-local suite uses the shared
@xyo-network/xl1-vitest-config apiLocal installer to synthesize and pump one
event from an advancing finalized local XL1 head. These are local Node and
local-chain results, not Cloudflare or hosted qualification.
FileObjectStore is the Node implementation of the neutral byte-object-store
contract. It confines repository-style keys beneath one root, atomically
replaces fsynced files, derives SHA-256 ETags, enforces create-only and
match-ETag preconditions, pages deterministic listings, and hides dapp-kit
ownership/temp files. Local projection and checkpoint resource sessions expose
this object capability while retaining filesystem-root ownership themselves;
product hosts therefore cannot escape or delete the runner's physical root.
The runtime-neutral XL1 transaction planner and reconciler live in
@xyo-network/dapp-kit. createXl1SdkTransactionEffectCapabilities() adapts
the published XL1 transaction phase helpers to those neutral capabilities. The
caller injects its build/block viewers, signer, runner, content resolver,
evidence hashers, clock, and optional datalake. Required-content effects fail
closed when no datalake is supplied; chain-only effects do not acquire one.
The initial XL1 adapter supports one datalake/store copy and initial signing. It does not yet create replacement transactions, classify a missing finalized inclusion as a reorg, or coordinate multiple stores.
UC-01 reference dApp
submitUc01ChainTransfer() is the first application-domain operation built on
the journal and XL1 adapter. It accepts the strict
com.example.dappkit.transfer.command shape, derives one deterministic effect,
embeds that effect ID in a canonical XL1 Transfer, admits the exact pre-sign
material, and executes the chain-only receipt policy. It never resolves or
acquires a datalake. Caller identity and signing capability are separate trusted
authority inputs; they are not fields that a command may claim.
Uc01ChainTransferActor exposes the same operation through
bindUc01ChainTransferPort(). The descriptor declares only
xyo.effect-journal, xyo.xl1.runner, and xyo.xl1.viewer. Its actor-system
lifetime is resident for attached commands, while its dApp execution cadence is
on-demand: start() creates no loop or timer. status() exposes only the safe
receipt and rejects access when its effect identity does not match the caller
authority bound by the host at bindUc01ChainTransferPort(). Product hosts still
own attachment authentication, signer selection, journal path, provider
resolution, clocks, and evidence hashing.
createUc01NodeRuntimeCatalog() is the concrete UC-01 installed-code catalog.
It requires exactly one plan owner for each of xyo.effect-journal,
xyo.xl1.viewer, and xyo.xl1.runner. The journal may be independent while
the two XL1 capabilities may be supplied by one exact multi-capability resource
node; that resource still provisions only one provider instance. Ambiguous,
overlapping, or malformed capability ownership fails closed.
createUc01NodeRuntimeLaunch() and
createUc01NodeRuntimeHost() assemble those providers with the actor, bounded
effect recovery, authenticated authority, and the exact transfers port.
Recovery completes before the actor is constructed, and the port is not bound
until the actor has started. Stop drains the actor before disposing viewer,
runner, and journal in reverse provision order.
The UC-01 runner facade intentionally has no datalake member. Conformance proves
that empty readiness performs no signing, broadcast, journal write, or
autonomous loop, actor start schedules no timer, and the assembled plan installs
no datalake, projection, index, side-channel, or external-resource capability.
This wrapper consumes an already resolved trusted DappPlan; loading
Definition/Configuration documents and authenticating the attachment remain
host responsibilities.
createUc02NodeRuntimeCatalog() extends the same ownership shape to the public
claim profile with five required capabilities: effect journal, XL1 viewer,
XL1 runner, public-lake viewer, and public-lake runner. The plan may express
these as independent nodes or as exact multi-capability XL1 and datalake
resource nodes. Each selected node provisions once, even when it supplies both
reader and runner capabilities. The two lake factories must identify the same
configured store or launch fails before readiness.
Recovery completes required content verification before actor construction;
the claims port binds only after start. After prepared-material compaction,
byId() fetches the required body from the public lake, validates its full Zod
shape, and recomputes its content hash before returning it.
createUc02NodeArtifactHostFactory() adapts four explicitly named, borrowed
deployment resources into that runtime. It validates the XL1, public-lake,
crash-durable journal, and signer capability shapes before constructing any
provider, owns only the gateway sessions it creates, and leaves resource
session teardown to the deployment reconciler. The reference factory exposes
the headless claims port to its owner and creates attachment-scoped
claims.publish and claims.by-id unary operations for network transports.
Those operations validate JSON bodies, preserve host/plan/incarnation fencing,
map failures without leaking internals, and report finalized-through
consistency when the receipt carries finality evidence.
Repository conformance includes both exhaustive deterministic fault injection
and actual process termination. The process suite sends SIGKILL to the
executing process after temporary-file sync, rename, and directory sync, then
uses a fresh process to resume the exported adapter. Fsynced deterministic lake
and broadcast facades retain external observations across the kill, allowing
the recovery path to prove one insert and one broadcast. This is process-level
adapter evidence, not local-XL1 or deployed-network qualification.
The UC-01-specific process cases additionally kill after its atomic prepared
write and after its signed-receipt write. Fresh-process recovery proves one
signing, one broadcast, and byte-for-byte broadcast of the durable signed
transaction. Those facades remain deterministic and offline.
The UC-02-specific process matrix kills the assembled five-provider host after
durable prepared, signed, content-written, content-verified, and broadcast
evidence. A fresh host completes pre-actor recovery and reaches finality with
exactly one signing, one lake insert, and one broadcast. The post-verification
cases compact private prepared material before restart, then resolve the body
from schema- and hash-verified public-lake content. Those fsynced facades remain
deterministic and offline; local-datalake qualification is still separate.
The opt-in pnpm test:local-xl1 repository suite provides a separate evidence
layer. It boots the published @xyo-network/[email protected], executes a
chain-only transfer through this adapter, waits for finality, reads back chain
state, and replays the durable receipt without rebroadcasting. It intentionally
does not acquire a datalake. Because that CLI version predates the SDK's
transaction-to-block RPC, the suite supplies the adapter's documented
BlockViewer injection with a typed finalized-block scan. This proves the
chain-only local contract, not datalake-backed execution, process-failure
recovery against a real chain, multi-store policy, or deployed operation.
