2026-08-10 16:48:17 +00:00
2026-08-10 16:48:17 +00:00
2026-05-05 12:03:45 +00:00
2026-08-10 16:48:17 +00:00
2026-05-05 12:03:45 +00:00

@smarthome.exchange/hub

🧠 Local smarthome.exchange runtime for device tools, scoped agents, automations, approvals, audit receipts, integrations, and typed hub handlers.

The hub is the composition root. It wires together registries, approval queues, audit logs, automation execution, MCP descriptors, integration discovery, and TypedRequest handlers around the shared contracts from @smarthome.exchange/interfaces.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm add @smarthome.exchange/hub

Basic Usage

import { ShxHub } from '@smarthome.exchange/hub';

const hub = new ShxHub({
  home: {
    id: 'home:studio',
    name: 'Studio',
    timezone: 'Europe/Berlin',
  },
});

await hub.start();

const snapshot = hub.getSnapshot();
console.log(snapshot.devices.map((deviceArg) => deviceArg.id));

await hub.stop();

ShxHub registers typed handlers on hub.typedrouter. Host applications can bind that router to the transport they want to expose.

For a local standalone HTTP host, use ShxHubHost. It starts ShxHub, creates an @api.global/typedserver TypedServer bound to 127.0.0.1:8080 by default, assigns hub.typedrouter to one instance-local HTTP/WebSocket surface, exposes typed calls at /typedrequest, and serves the bundled console from dist_serve when it is available.

import { ShxHubHost } from '@smarthome.exchange/hub';

const host = new ShxHubHost({ host: '127.0.0.1', port: 8080 });

await host.start();
console.log(`Hub available at ${host.origin}`);

await host.stop();

host is the listener interface, not a public service hostname. Set it explicitly to 0.0.0.0 or :: only when the surrounding network policy requires a wider bind. For wildcard listeners, origin remains a usable loopback URL and its hostname is always admitted by the owned surface. Add exact reverse-proxy or private service authorities through surfaceHostnames; they supplement rather than replace the origin hostname. Unknown authorities receive 404. Direct TypedServer TLS options are reflected as https only when both key and certificate are configured.

ShxHubHost instances are single-use. Calling stop() before start() permanently stops that host, and a start attempted while stop is in progress is rejected. After stop(), create a new host instance instead of restarting the stopped instance. The same ShxHub may be passed to sequential host instances, but only one host may own that hub's lifecycle at a time; hosts with distinct hubs can run concurrently. Stop calls TypedServer cleanup immediately even when host start is still pending. A stop attempt rejects after resourceCleanupTimeoutMs, defaulting to 15 seconds. While cleanup is pending, later stops await the same exact promise. If cleanup rejects, that attempt is cleared so a later stop can retry, while the TypedServer and per-hub ownership remain retained until cleanup succeeds. TypedServer 10.1 bounds its own component cleanup while ensuring a stalled component does not prevent listener cleanup. Caller-supplied typedServerOptions.surfaces are rejected because ShxHubHost owns its surface configuration.

The package CLI uses the same host:

export SHX_REQUIRED_INTEGRATIONS=your_domain
export SHX_INTEGRATIONS_JSON='[{"domain":"your_domain","config":{}}]'
export SHX_LISTEN_HOST='127.0.0.1'
# Add exact private proxy authorities only when they differ from the listen origin.
export SHX_SURFACE_HOSTNAMES='hub.private.local'
pnpm exec shx-hub

Installed packages expose shx-hub for the host and shx-hub-accept-homeassistant for read-only live acceptance. From this repository, pnpm start is an alias for the packaged host command. SHX_LISTEN_HOST defaults to 127.0.0.1; set it to 0.0.0.0 or :: only when the container or network policy requires a wider bind. SHX_SURFACE_HOSTNAMES supplies comma-separated additional exact authorities. ShxHubHost.port, PORT, and --port=<port> accept only decimal integers from 1 through 65535; an explicit malformed, zero, fractional, or out-of-range value stops startup.

SHX_INTEGRATIONS_JSON must be a JSON array of { "domain": string, "config": unknown } entries. The CLI starts each configured runtime through setupIntegration(...) and exits if parsing, setup, or cleanup fails. The CLI disables the seeded demo home and devices.

For the managed first-instance path, setting any of HOME_ASSISTANT_URL, HOME_ASSISTANT_TOKEN, SHX_HOME_ASSISTANT_ACCEPTANCE_ENTITIES_JSON, SHX_HOME_ASSISTANT_SETUP_RETRY, SHX_HOME_ASSISTANT_INITIAL_SNAPSHOT_TIMEOUT_MS, or SHX_INTEGRATION_SETUP_TIMEOUT_MS activates strict Home Assistant bootstrap. The URL, token, and exact five-role entity JSON must then all be valid. The CLI adds homeassistant to the required domains, derives the exact entity allowlist, requires entity-registry provenance, applies the validated setup timeout, and redacts the token from startup errors. Do not also declare homeassistant in SHX_INTEGRATIONS_JSON; normalized duplicate domains are rejected before the host is constructed.

Readiness fails closed until at least one required integration domain is configured. SHX_REQUIRED_INTEGRATIONS supplies comma-separated generic domains, while managed Home Assistant bootstrap adds homeassistant automatically. GET /livez and GET /live report process liveness; GET /healthz and GET /health return 503 until every required integration has completed a healthy authoritative device synchronization. A bootstrap entry only works when that domain has a real runtime; descriptor-only integrations fail setup. These transport endpoints do not authenticate callers.

The served console polls getHomeSnapshot through @smarthome.exchange/api, assigns the complete response envelope to <shx-console-shell>.snapshotResponse, and submits approval decisions through submitApproval before refreshing. The hub marks these transport-proven responses with canonical availability: 'live'.

Runtime Pieces

Export Responsibility
ShxHub Composition root for the local runtime.
ShxHubHost Local standalone TypedServer host for ShxHub.
DeviceRegistry Lists, looks up, and reconciles integration-owned device snapshots.
AgentRegistry Holds reference agents and runtime statuses.
ToolRegistry Lists tools and executes plans according to tool/agent modes.
ApprovalQueue Creates, lists, approves, and rejects approval requests.
AuditLog Stores a bounded, process-local window of audit receipts.
AutomationRunner Owns an SDK automation context wired to the tool registry.
McpDescriptor Exposes registered tools as MCP-style descriptors.

Typed Operations

registerTypedHandlers() installs handlers for:

Operation Purpose
listDevices Return normalized devices from the device registry.
listAgents Return reference agents and runtime statuses.
listTools Return registered device and agent tools.
getHomeSnapshot Return canonical live availability with home, devices, agents, approvals, dashboards, receipts, and timestamp.
executeToolPlan Execute a plan, queue approval, suggest, fail, or return an indeterminate result per tool call.
listApprovals Return approval requests filtered by status.
submitApproval Approve or reject a pending request with its ownerId, committing the decision only after its audit receipt is appended.

AuditLog retains the newest auditReceiptLimit receipts in memory, defaulting to 1,000, and discards the oldest receipt when that bound is exceeded. ApprovalQueue retains at most approvalQueueLimit approvals, also defaulting to 1,000. It evicts only the oldest decided approvals; pending approvals are never evicted, and a full pending queue returns a failed tool result for new approval-requiring calls. Evicted decided IDs are no longer queryable. Restarting the process loses both windows. Durable approval and audit persistence and export are required before production deployment.

Integration Flow

const integrationHub = new ShxHub({
  requiredIntegrationDomains: ['homeassistant'],
  seedDemoState: false,
});
await integrationHub.start();

try {
  const runtime = await integrationHub.setupIntegration('homeassistant', {
    baseUrl: 'http://homeassistant.local:8123',
    accessToken: process.env.HOME_ASSISTANT_TOKEN!,
    entityIds: [
      'light.living_room',
      'sensor.sun2000_power',
      'switch.fs20_lamp',
      'binary_sensor.zha_front_door',
      'vacuum.roborock',
    ],
    requireEntityIds: true,
    requireEntityRegistry: true,
  });

  console.log(await runtime.devices());
} finally {
  await integrationHub.stop();
}

The first production instance uses this single Home Assistant aggregation runtime for Hue, Huawei SUN2000 telemetry, FS20 through CUL/FHEM-MQTT, SkyConnect/ZHA entities, and Roborock. Keep the long-lived token in deployment secret configuration rather than source. The native hue adapter remains a simulated/injected development adapter and is not the live first-instance path.

Integrations come from @smarthome.exchange/integrations. Successful setup returns a runtime and reconciles the integration's full normalized device snapshot into the hub device registry. Events schedule a debounced authoritative snapshot refresh; absent devices are removed. Setup admission retains at most integrationSetupQueueLimit configurations, defaulting to 32, and rejects another setup for a domain that is already active or queued. Setup rollback keeps its owned state registered until pending refresh and write calls have settled through bounded lifecycle cleanup, and unresolved calls remain tracked so retries cannot orphan them. Failed runtime destruction remains retained by the integration manager and is retried through a bounded hub wait before any new same-domain runtime is created. Writable readable features are confirmed by polling authoritative state, while write-only features report accepted delivery without inventing state. Once a connector accepts a physical write, refresh failure, runtime replacement, cancellation, or non-convergence is reported as indeterminate, never failed. Integration setup has a separate 30-second default deadline, while Home Assistant uses one 15-second deadline for subscription acknowledgement, the first snapshot, and optional registry verification after connection retries. The 10-second steady-state operation deadline remains separate. All hub timer options reject non-integer, non-finite, negative, and Node-overflow values instead of allowing collapsed timers. Write admission and readiness both consult current runtime health, so a Home Assistant disconnect fails closed without waiting for the debounced registry refresh.

Live Home Assistant Acceptance

The packaged acceptance command connects through the real hub path without calling services or actuating devices. It requires exact representative entity IDs, fail-closed readiness, online normalized devices, role capabilities, state evidence for every required readable feature, and Home Assistant entity-registry provenance: Hue must report platform hue, SUN2000 huawei_solar, FS20 through FHEM-MQTT mqtt, SkyConnect devices zha, and Roborock roborock.

export HOME_ASSISTANT_URL='http://homeassistant.local:8123'
# Supply HOME_ASSISTANT_TOKEN through a local or deployment secret, not source control.
export SHX_HOME_ASSISTANT_ACCEPTANCE_ENTITIES_JSON='{
  "hue":["light.living_room"],
  "huaweiSun2000":["sensor.sun2000_power"],
  "fs20":["switch.fs20_lamp"],
  "skyConnect":["binary_sensor.zha_front_door"],
  "roborock":["vacuum.roborock"]
}'
pnpm exec shx-hub-accept-homeassistant

From this repository, pnpm accept:homeassistant is an alias for the packaged acceptance command. Each role must be present and non-empty; entity IDs must be canonical and unique across roles. Optional SHX_HOME_ASSISTANT_SETUP_RETRY, SHX_HOME_ASSISTANT_INITIAL_SNAPSHOT_TIMEOUT_MS, and SHX_INTEGRATION_SETUP_TIMEOUT_MS values are bounded integers, and the outer setup budget must cover the snapshot and retry budgets. Output is structured JSON and token values are redacted from failures. Physical write acceptance remains a separate, explicitly approved operation.

Seeded Demo State

By default, embedded ShxHub instances seed a Birch Lane demo home and devices for development. Pass seedDemoState: false for a neutral home with no demo devices; the package CLI does this automatically. Override home with the real home definition.

Console Bundle

The web entry imports @smarthome.exchange/catalog and renders <shx-console-shell>. It passes the full typed snapshot response to the catalog so canonical availability is preserved across the API and UI boundary.

Cloudly Release Evidence

Set TSDEPLOY_RELEASE_AUTHORIZED=true before running pnpm release:evidence. The command consumes the exact source revision, version, configuration digest, OCI index digest, and output path supplied through the five required TSDEPLOY_* environment variables. It writes digest-bound descriptive evidence with mode 0600; Cloudly registry validation remains authoritative.

Scripts

pnpm test
pnpm build
pnpm watch
pnpm start
pnpm accept:homeassistant
pnpm release:evidence
pnpm buildDocs

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the LICENSE file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme
214 KiB
Languages
TypeScript 97%
JavaScript 2.8%
HTML 0.2%