omnyhub 1.6.0
omnyhub: ^1.6.0 copied to clipboard
A reusable, protocol-agnostic HUB framework in pure Dart. Host multiple services on one port, route by host/path/header/protocol/auth, act as a reverse proxy over HTTP/HTTPS/WS/WSS with automatic Let' [...]
1.6.0 #
The browser release: a hub can now be called from a web app on another origin, and push a live event stream to it.
Additive and backward-compatible. Every new parameter defaults to today's behaviour, and a hub that configures neither feature emits byte-identical responses — verified by running the full suite unmodified against this release.
Added #
-
CORS.
cors()— aMiddlewarethat answers a preflightOPTIONSitself with a204, short-circuiting the pipeline so it never reaches routing (where it would come back as the router's405or the hub's404, neither carrying CORS headers), and stampsAccess-Control-Allow-Originonto every other response. Origins come from an exact allow-list, a predicate, orallowAnyOrigin.authorizationandx-omny-principalare allowed by default, because that is what the omny APIs send.Vary: Originis merged into anyvarythe handler already set, and a request with noOriginheader — any non-browser client — passes through untouched.final hub = OmnyHub( transports: [HttpTransport.http(port: 8080)], outerMiddleware: [cors(allowedOrigins: ['https://app.example.com'])], ); -
OmnyHub.outerMiddlewareanduseOuter. Middleware composed outside the hub's error mapping and global authentication, so it sees every response the client will actually receive — including the oneserrorMapperrenders from a thrown exception — and every request before the authenticator can reject it.CORS needs both. Mounted in the ordinary
middlewarelayer it can never stamp the401the authenticator throws, the404routing throws, or a500, since those become responses only above it; the browser would then get an opaque network error instead of the real status. And a preflight, which by specification carries no credentials, would be rejected by a strict authenticator before CORS ever saw it. Defaults to empty, in which case the pipeline is exactly what it was. -
Server-Sent Events.
sseResponse(Stream<SseEvent>),SseEvent(data/event/id/retry, plusSseEvent.json),encodeSseEvents, and the lower-levelHubResponse.eventStream. Events are flushed as they are produced; a: pingcomment every 15s (configurable) keeps the connection alive and is what eventually surfaces a client that vanished;onCancelthen fires so per-client resources are released.hub.registerService(HandlerService( name: 'events', mount: '/events', handler: (request) async => sseResponse(bus.stream.map(SseEvent.json)), )); -
HubResponse.bufferOutput. Whether the transport may buffer the body —trueby default, unchanged. It exists becausedart:ioholds written bytes until an 8 KiB buffer fills or the response closes: a live stream closes never and its events are tiny, so without this flag a Server-Sent Event would sit in the buffer and never reach the browser.HubResponse.eventStreamsets itfalse, andHttpTransporttranslates it into shelf'sshelf.io.buffer_outputcontext key.HubResponse.streamaccepts it too, for any long-lived push stream. -
HubResponse.withHeaders. A copy with headers merged over the originals, carrying the status code, the unread body andbufferOutputacross — the seam middleware needs,headersbeing unmodifiable andread()once-only. Reading the original afterwards throws, exactly as a secondread()would.
1.5.1 #
Changed #
- Dependency bump.
multi_domain_secure_server: ^1.0.17(from^1.0.16) — improves error handling and logging in_acceptandextractSNIHostname, the path every TLS handshake takes before aSecurityContextis chosen, so an SNI read that fails now reports why instead of failing silently.meta: ^1.19.0(from^1.16.0).
No API changes; the suite passes unmodified against these versions.
1.5.0 #
Added #
-
LetsEncryptTls.renewBefore. How much validity a certificate must have left to be kept; below it,maybeRenewrenews. Defaults to 5 days — the previous, hard-codedshelf_letsencryptbehaviour — so nothing changes unless you set it.5 days is a thin margin: a certificate is renewed at most one
OmnyHub.tlsRenewalInterval(12h by default) after dropping below the threshold, and a failed renewal then has very little room to retry before the certificate actually expires. Let's Encrypt's own advice is to renew with roughly a third of the lifetime left (30 of 90 days). Applications that were previously enforcing their own margin — refusing to serve a near-expiry certificate, and reissuing — can now express it here instead:LetsEncryptTls.onDemand( allowDomain: ..., cacheDir: '/etc/letsencrypt/live', renewBefore: const Duration(days: 15), );A certificate that is still valid keeps being served while it renews in the background; only an expired one is withheld (
isHandledDomainCertificatealready rejects an expired, corrupt or unloadable certificate before anySecurityContextis built, so an expired certificate is never served).
1.4.0 #
On-demand TLS release — the seams an application needs to decide its own way
whether a host may be certified, rather than reimplementing the SNI cache and
issuance around LetsEncryptTls. Driven by adopting omnyhub in MenuIci's
sites_server, whose front door had grown a parallel copy of both.
Additive and backward-compatible: DomainPolicy is widened, so existing
synchronous policies still satisfy it, and autoIssue defaults to the 1.3.0
behaviour. Verified by running the full suite unmodified against this release.
Added #
- Asynchronous domain policy.
DomainPolicyis nowFutureOr<bool> Function(String host)(wasbool Function(String host)), so on-demand issuance can be gated by a real lookup — a database query, a tenant API call — instead of only what is knowable synchronously. Previously an application whose "may this host be certified?" answer lived behind I/O had to keep a hand-rolled cache beside the hub and pre-warm it, which is exactly the duplicationallowDomainexists to remove. Existing sync policies are unaffected:boolis a subtype ofFutureOr<bool>. LetsEncryptTls.autoIssue. Whenfalse, only certificates already cached incacheDirare served and the CA is never contacted — neither on a handshake miss nor from the renewal loop. For a deployment whose certificates are provisioned out-of-band (certbot, a secrets mount, a sibling process), which previously could not be expressed through this provider at all.
Changed #
LetsEncryptTls.isAllowedreturnsFuture<bool>. It awaits the policy. The synchronous SNI path (TlsProvider.contextFor, which cannot await) no longer calls it:contextFornow schedulesobtain()in the background, andobtain()is the single place the policy is evaluated. The only breaking change, and only for code callingisAlloweddirectly.- A rejected host is no longer re-checked on every handshake. With a sync
policy the pre-check in
contextForwas free; with an async one it could be an HTTP call per TLS handshake. Rejections are now remembered in a bounded cache (capped, so an SNI flood of random hostnames cannot grow it without limit). obtain()stays de-duplicated under an async policy. It registers its in-flight future before the first suspension, so concurrent handshakes for the same host still share one provisioning attempt now that the allow-check can itself suspend.
1.3.0 #
Control-plane release — the seams an application needs to host its own node
protocol on NodeGateway/NodeRuntime rather than reimplementing the registry,
heartbeat watchdog and RPC correlation around them. Driven by adopting omnyhub in
OmnyServer, whose hub/agent had grown a parallel copy of all three.
Additive and backward-compatible: every new field is omitted from the wire when
empty, every new hook defaults to null, and every new option defaults to the
1.2.0 behaviour. Verified by running the omnydrive and omnyshell suites
unmodified against this release.
Added #
- Heartbeat telemetry.
Heartbeat.payloadcarries application data on the beat (a metrics snapshot, a queue depth), produced byNodeConfig.heartbeatPayloadand consumed byNodeGateway.onHeartbeat. Saves a second periodic message for telemetry a node already reports on the same cadence. A payload builder that throws or stalls never costs the node its liveness — the beat goes out empty. - One-way push (
NodeNotify). The fire-and-forget counterpart ofNodeRequest: sameaction+payload, no correlation id, no reply. Sent withNodeRuntime.notify/NodeGateway.notify, received viaNodeGateway.onNotify/NodeConfig.onRequest. Previously a node could only push by callingrequestand discarding a response it did not want. - Connection lifecycle hooks.
NodeGateway.onConnect/onDisconnect/onTimeoutobserve a control connection opening and closing, so an application can audit, persist or publish on every transition.onConnectfires for sockets that never register — which the registry's events cannot see at all — andonDisconnecthands over the wholeRegisteredNode, not just a descriptor. - Node retention.
NodeGateway.retainNodeskeeps a disconnected or timed-out node in the registry, marked offline, instead of dropping the record. For a hub that is the system of record for a known fleet: an offline node stays queryable byNodeRegistry.byIdwith its last-known descriptor and re-registers into the same slot. Offline nodes are excluded fromdiscovereither way. Backed by a newNodeRegistry.markOfflineandNodeEventKind.disconnected. - Per-node application state.
RegisteredNode.state, a mutable bag the application owns. The registry constructsRegisteredNodeitself, so subclassing it to add fields never worked; the pre-existingactiveSessionsfield was the one hardcoded concession to this. NodeEvent.nodeexposes the affectedRegisteredNode— its connection, principal and state — so a subscriber no longer has to re-look-up the registry to act on an event.- Injectable node transport.
NodeConfig.connectreplaces the built-in WebSocket dial, for a transport omnyhub does not ship and for driving aNodeRuntimeagainst a loopback connection in tests.NodeConfig.pingIntervalexposes WebSocket-level keepalive, which the runtime previously never passed. NodeConfig.descriptorBuilderrebuilds the advertised descriptor on every connection attempt, replacing the static one assembled at construction. A node that changes while it is running — a GPU driver lands, a runtime is installed, a label is retagged — now advertises the change on its next (re)registration instead of being stuck with what it knew at startup.- Terminal-failure policy.
NodeConfig.isTerminalends the runtime instead of retrying, with the cause left inNodeRuntime.terminalError. Reconnecting cannot fix a revoked key or a refused enrolment — it just hammers the hub forever, which is what a node did before this. AppException, a publicHubExceptionan application raises with its owncodeandstatusCode.HubExceptionissealed, so an application could not slot its own failures into the hierarchy, and everything that maps errors to the wire keys offHubException— so an application error became an opaque 500.WsCloseCodes.forException, the singleHubException→ close-code mapping, replacing a private copy duplicated inOmnyHubandNodeGateway. It now maps 502/503/504 tobadGateway; previously every status outside 401/403/404 fell through tounauthorized, so an unavailable node was reported to the peer as an auth failure.TypedConnection.onDecodeErrorobserves frames the codec rejects. Dropping them is deliberate — one bad frame must not tear the connection down — but it was also invisible, hiding version skew and codec bugs.LoggerBase, a mixin derivingdebug/info/warn/errorfromlog, so aLoggeradapter implements two methods instead of six.
Fixed #
- Frames sent during registration were silently dropped.
NodeGatewayranonRegisterwithout awaiting it before dispatching further frames, so anything arriving while an async handler was in flight raced an unset registration and was discarded (Heartbeat,NodeUpdate) or refused asNot registered(NodeRequest). A node may pipeline afterregisterwithout waiting for its ack, and anyonRegisterdoing real work (vetting a CSR, writing to a repo) widened the window. Such frames are now queued and replayed in order once registration settles, and discarded if it is rejected. - A stray binary frame could take down the gateway.
MessageCodec.decodeUTF-8-decoded aBinaryMessageoutside its guard, so arbitrary bytes escaped as a rawFormatException— pastNodeGateway'sHubException-only catch — and surfaced as an uncaught async error. Decode failures are now always aProtocolException, the peer gets aNodeErrorMessage, and the connection keeps serving.
Changed #
MessageCodec's documentation claimed third parties could register new message types. They cannot:NodeControlMessageissealed, so a decoder can only ever return a built-in, andregistermerely remaps a wire string onto one. Application protocols ride onNodeRequest/NodeResponseandNodeNotify, which is now what the docs say.
1.2.0 #
Control-plane release — the node protocol grows the pieces an application needs
to run a real HUB/Node infrastructure on it (enrolment, node-initiated calls,
domain-specific discovery) instead of only the "worker node" shape. Fully
backward-compatible (additive): every new field is omitted from the wire when
empty and every new hook defaults to null, so existing peers and callers are
unaffected.
Added #
- Bidirectional RPC.
request/responsenow flow in both directions. A node calls the hub withNodeRuntime.request(action, payload:), answered by the hub's newNodeGateway.onRequesthandler — which receives the callingRegisteredNode, so the hub knows who is asking and can authorize on itsprincipal. Registration is a precondition: a request from a connection that has not registered is rejected (error: "Not registered") without reaching the handler. The existing hub→node direction (NodeGateway.request) is unchanged. Previously the only node-initiated message wasquery, so an application needed a second channel back to the hub. - Enrolment.
NodeRegisterandNodeRegisteredcarry aMap<String, dynamic> payload, andNodeGateway.onRegistervets registrations — returning the ack payload, or throwing aHubExceptionto reject (the hub replieserror, closes, and never registers the node). Node-side:NodeConfig.registerPayload/NodeConfig.onRegisteredandNodeRuntime.registration. This is the seam for CA-style enrolment: submit a CSR, get a signed certificate back. - Node-side in-band handshake.
NodeConfig.onHandshakeruns on the raw connection before registering — the counterpart of the hub's existingConnectionAuthenticator, so a node can now answer a challenge/response or key-agreement exchange. Unconsumed frames are replayed to the control protocol. - Application-defined discovery.
NodeDescriptor.attributes(Map<String, dynamic>, may nest — unlike the flat string-onlylabelsandmetadata),NodeQuery.filter, and theNodeMatcherport, so an application owns query semantics the hub cannot know about (version ranges, nested service catalogues).NodeRegistry.discovertakes awherepredicate. NodeUpdate(t: "update") — a node revises its advertised descriptor without re-registering (NodeRuntime.updateDescriptor,NodeRegistry.updateDescriptor,NodeEventKind.updated).Json.optObjectfor reading free-form JSON object fields.
Changed #
NodeGateway.codecandNodeRuntime.codecare now typedConnectionCodec<NodeControlMessage>rather than the concreteMessageCodec, so an application can supply its own wire format (e.g. binary) without forking either endpoint.MessageCodec.standard()remains the default; source- compatible for existing callers.
Fixed #
- In-flight RPCs no longer hang when a connection drops.
NodeGatewayandNodeRuntimenow fail their pending calls withNodeUnavailableExceptionon disconnect, goodbye and heartbeat timeout, instead of leaving callers to wait out their own timeout.NodeRuntime's pending discovery queries were leaked on disconnect and are now cleared too.
1.1.0 #
Synergy release — shared primitives that let protocol-oriented apps (like OmnyShell) ride on omnyhub's transport without a reverse-adapter, and make TLS renewal seamless. Fully backward-compatible (additive).
Added #
ConnectionCodec<T>+TypedConnection<T>— a first-class "codec over a raw duplex connection" primitive. A protocol supplies aConnectionCodec<AppFrame>(mapping its frames toMessages) and exchanges decoded values over any omnyhubConnection; undecodable inbound frames are dropped. omnyhub's nodeMessageCodecis now aConnectionCodec<NodeControlMessage>, and the node runtime consumes aTypedConnection.
Changed #
- Gap-free TLS rebind.
HttpTransport.rebind()now binds a freshsharedlistener on the same port with the renewed certificate and drains the old one gracefully (force: false) — live connections survive certificate renewal instead of being dropped. Benefits automatic Let's Encrypt renewal. ReloadableFileTlsdetects changes by byte content (not mtime+size), so same-size rotations are caught and a partial write that fails to parse keeps the previous certificate.
1.0.0 #
First stable release. OmnyHub is a reusable, protocol-agnostic HUB framework for building distributed HUB/Node infrastructures over HTTP/HTTPS/WS/WSS behind one architecture and API.
Features #
- Multi-service hosting. Host many
Services on oneOmnyHubinstance, exposed through the same server, port and protocol (/api/*,/drive/*,/metrics/*, …). Services register and unregister dynamically. - Protocol-agnostic transport. HTTP, HTTPS, WS and WSS on a single
shelflistener behind aTransportport; aConnection/Messageabstraction for the WebSocket control plane. All protocol-specific code is isolated from business logic. - Advanced routing. Match on host, domain and subdomain (exact,
*.wildcard, or regexp viaHostPatternRule), path prefix, path parameters (RouterService/PathPattern, or an existingshelf_routerviaShelfService), protocol, headers, method and authentication state. Compose rules with&/|/~, use a predicate, or plug in a customRouter. - Reverse proxy & gateway.
ProxyServicestreams HTTP requests/responses, injectsX-Forwarded-*, strips hop-by-hop headers, and forwards WebSocket upgrades — to local or remote upstreams. Host- and path-based gateways and hybrid (local + proxied) deployments. - Automatic TLS.
StaticTls,ReloadableFileTls(hot-reload cert files), andLetsEncryptTls(ACME HTTP-01) with provisioning, renewal and hot-reload — including dynamic, on-demand multi-domain issuance via SNI (a domain policy and per-host, optionally async, contact-email resolver), so new subdomains are provisioned as they are first used. - Layered authentication & authorization. A global
AuthCoordinatordeciding authenticate / bypass / delegate / block (pre-check), per-service authenticators and authorizers, and an in-bandConnectionAuthenticatorfor WebSocket handshakes. Built-in Bearer/Basic/composite authenticators and role-based/predicate authorizers, all fail-closed. - Node infrastructure. A generic control protocol + extensible
MessageCodec,NodeGateway,NodeRegistry, capability/label discovery,HeartbeatMonitor, and a node-sideNodeRuntime/OmnyNodewith registration, heartbeats, RPC, peer discovery and reconnection with backoff. - Demo CLI (
bin/omnyhub.dart) that launches a config-driven reverse-proxy/gateway, runnable examples, anddoc/protocol.md+doc/security.md. - Tested. Unit, integration and end-to-end tests over real servers and sockets — no mocking library.
The 0.x entries below record the incremental pre-release development.
0.3.0 #
Added #
- Path-parameter routing.
PathPattern(named<param>+ wildcard tail<name|.*>) andRouterService(intra-service method dispatch exposing captured params, 405/404).ShelfServiceadapts an existingshelf.Handler/shelf_router.Routerverbatim. - Layered authentication framework. A global
AuthCoordinatorreturning a sealedAuthDecision(Authenticated/Anonymousbypass /Delegateto the service's authenticator /Blockedpre-check), per-serviceauthenticator/authorizeronregisterService/route, andTooManyRequestsException(429). Fully backward-compatible (DefaultAuthCoordinator). - In-band connection authentication.
ConnectionAuthenticator+ aHandshakeConnectionbuffered wrapper so a WebSocket handshake can authenticate and then hand the live connection to the service (single-subscription safe). - Host/domain regexp routing.
HostPatternRule(RegExp, {part})matching the host, domain or subdomain, combinable with aPathRulevia&. ReloadableFileTls— aTlsProviderthat hot-reloads certificate/key files when they change on disk (cert-manager/certbot friendly).- Pipeline helpers —
mapErrorsmiddleware (map app exceptions to responses) andsuccessEnvelope/errorEnvelope({success, data}/{success, error}). NodeRegistryextras —RegisteredNode.activeSessions/connectionId,NodeRegistry.byConnectionId/updateActiveSessions.- Examples:
path_params_example.dart,layered_auth_example.dart.
0.2.0 #
Added #
- Dynamic / on-demand Let's Encrypt domains.
LetsEncryptTlsnow accepts anallowDomainpolicy (viaLetsEncryptTls.onDemand(...)or the main constructor) to provision certificates for any allowed host on demand, served via SNI from a live per-host cache — sofoo.example.com,bar.example.com, … work without listing each domain in code. Newobtain(host),isAllowed(host)andisOnDemandAPIs. The ACME contact email may be a fixedonDemandEmailor resolved per host with an asynconDemandEmailResolver(e.g. a per-tenant lookup). - SNI transport binding.
HttpTransportserves multiple certificates on one TLS listener via SNI when its TLS provider implements the newSniTlsProvidercapability; a certificate obtained on demand is served on the next handshake with no rebind. Clients must send SNI (all modern browsers do). HandlerService.handlesWebSocketgetter and expanded WebSocket docs.- New
example/lets_encrypt_example.dart(fixed and--on-demandmodes).
Changed #
LetsEncryptTlsrequires seeddomainsand/or anallowDomainpolicy;allowDomainrequires anonDemandEmail.
0.1.0 #
- Initial release of OmnyHub — a reusable, protocol-agnostic HUB framework.
Added #
- Multi-service hosting.
OmnyHubbinds one or more transports and hosts manyServices on the same port, with dynamic registration/removal. - Transports.
HttpTransportserves HTTP/HTTPS/WS/WSS on oneshelflistener behind aTransportport;Connection/Messageabstract the WebSocket control channel. - Advanced routing.
RouteContext+ composableRouteRules (path, host, domain, subdomain, header, protocol, method, auth-state,and/or/not, predicate) selected by a pluggableRouter(defaultRuleRouter). - Authentication & authorization. Bearer/Basic/composite authenticators and role-based/predicate authorizers, wired fail-closed into the request pipeline.
- Reverse proxy.
ProxyServicestreams HTTP requests/responses, injectsX-Forwarded-*, strips hop-by-hop headers, and forwards WebSocket upgrades to local or remote upstreams; host- and path-based gateways and hybrid modes. - Automatic TLS.
TlsProviderwithStaticTlsandLetsEncryptTls(ACME HTTP-01 viashelf_letsencrypt) — provisioning, challenge auto-mount and hot-reload on renewal. - Node infrastructure. A generic control protocol +
MessageCodec,NodeGateway,NodeRegistry, discovery,HeartbeatMonitor, and a node-sideNodeRuntime/OmnyNodewith registration, heartbeats, RPC, peer discovery and reconnection with backoff. - Demo CLI.
bin/omnyhub.dartlaunches a config-driven reverse-proxy/gateway. - Unit, integration and end-to-end tests over real servers and sockets.