@serve.zone/interfaces

@serve.zone/interfaces is the shared TypeScript contract package for the serve.zone ecosystem. It contains the public data shapes and TypedRequest interfaces used by Cloudly, Coreflow, Spark, Coretraffic, platform clients, SDKs, and external integrations to exchange infrastructure state without duplicating DTOs.

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 @serve.zone/interfaces

Public API

The root export exposes six namespaces:

import { appstore, data, platform, platformservice, protocol, requests } from '@serve.zone/interfaces';
Namespace Purpose
appstore App Store catalog, manifest, service requirement, and upgrade contracts.
data Durable platform object shapes such as clusters, services, deployments, images, domains, DNS entries, secrets, users, status, settings, backups, registries, BaseOS metadata, and task executions.
requests TypedRequest contracts for Cloudly and serve.zone control-plane RPC methods.
protocol The handshake two peers exchange when a session opens: what each side speaks, the oldest peer it accepts, and the named refusal when they cannot serve one session.
platform Current platform-service contracts for email, SMS, push notifications, letters, AI, databases, object storage, logging, backups, and SIP.
platformservice Legacy platform-service namespace kept for older consumers that still depend on the previous layout.

This package intentionally has no service implementation logic. It is a stable vocabulary for services that need to agree on payload shape, method names, and response types.

Identifier Vocabularies

Two vocabularies name things in these contracts, and which one a member uses is part of the contract.

Vocabulary Rule Members
Canonical identifier ^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$ — first character alphanumeric, up to 200 characters organizations, clusters, services, namespaces, sessions, assignments, attempts, authorities, pools, endpoints and every content id derived from them
Node id ^[A-Za-z0-9_-]{1,128}$ — the URL alphabet, a leading - or _ included, up to 128 characters every member that names a node of a cluster

A node may be named -abc or _abc: enrollment, the runtime session registration and the Spark wire admit a node id that begins with a separator, so every member that names the same node admits one too. data.isClusterNodeId(value) is that rule, and it is the rule at every node-bearing member: nodeId on a session binding, a registration, an enrollment, a Spark heartbeat, a relay block, an assignment, a workload lease, a handoff lease, a DNS-lease renewal run, a protection receipt, an egress authority, a cluster VPN node, an ingress registration, a traffic bucket, a Corestore inventory and an isolated-restore node control; id on a router incarnation and on a node retirement; cloudlyNodeId on a cluster runtime target and on a secret runtime target; and placement.nodeIds on a service runtime spec. A service that stores or forwards a node id should judge it with the same reader rather than with the canonical one, which would refuse a node these contracts themselves enrolled and registered.

A replica id is the one composed name: a producer names a replica slot <node id>.<index>, counted from zero. data.isRuntimeReplicaId(value) reads it by decomposition — the part before the last dot in the node vocabulary, the index on its own and never zero-padded — and reads a name that carries no such index, a zero-padded node-a.01 among them, as an opaque slot name in the canonical identifier vocabulary.

WorkloadInit release verifiers should use the dedicated Node-compatible subpath:

import {
  validateWorkloadInitReleaseAttestationStatement,
  validateWorkloadInitReleaseIdentity,
  workloadInitReleaseContract,
} from '@serve.zone/interfaces/runtime/workloadinit';

@serve.zone/interfaces/runtime/workloadinit exposes only the WorkloadInit release identity, attestation, approval, authority, digest, and validator contracts. Its runtime dependency graph is limited to the immutable-image digest validator; unlike @serve.zone/interfaces/runtime, it does not load the general runtime graph, plugins.js, SmartCrypto, TypedRequest, or Corestore runtime modules.

Directional Image Streams

Image transfer contracts use @api.global/typedrequest-interfaces 7.1.0 and the directional VirtualStream protocol used by TypedRequest 8 and TypedSocket 8. Directions in shared DTOs describe the requesting peer:

Contract field Requester endpoint TypedHandler endpoint
requests.image.IRequest_PushImageVersion.request.imageStream TVirtualStream<'send'> TVirtualStream<'receive'>
requests.image.IRequest_PullImageVersion.response.imageStream TVirtualStream<'receive'> TVirtualStream<'send'>

The method names remain pushImageVersion and pullImageVersion. Each stream carries ordered Uint8Array chunks. TypedRequest reverses stream directions for the handler automatically; do not reverse the shared declaration yourself.

Consumers must replace the removed undirected IVirtualStream API with explicit transport-created endpoints. Senders use send() or writable, then close; receivers use receive() or readable, then explicitly accept() after draining EOF and completing application storage or delivery. completion confirms the receiver's accepted receipt, while closed confirms transport cleanup. The upload response's allowed flag permits sending; it does not confirm storage. Receiver rejection, aborts and failed completion must reach the caller. Existing deployment log and shell push-message contracts retain their message shapes.

Human Credential Administration

requests.admin.getHumanCredential and mutateHumanCredential describe human credential inspection and mutation for recently authenticated administrators. Operations rotate a password, revoke password login, or revoke existing sessions. Each successful mutation advances the generation and invalidates older human UI/API and OCI registry sessions. Upstream OIDC identity bindings remain intact.

Send the expected generation and a stable mutation ID. After a lost response, authenticate again and repeat the same request. A replay returns the original metadata without applying the operation again; inspect current metadata separately. Requests containing passwords must never be logged. Responses contain no passwords or verifiers. Cloudly owns validation, atomic persistence, and session enforcement; these shared contracts alone do not implement an endpoint.

Node Credential Administration

requests.node defines getNodeCredential, rotateNodeCredential and revokeNodeCredential for verified platform-infrastructure administrators. getNodeConfig now also requires identity; callers using the previous unauthenticated request shape must update. Its response remains the public IClusterNode, never a persisted backend document.

data.INodeCredentialMetadata separates spark and pallet purposes and exposes lifecycle state, generation, rotation-required status and session epoch, but no bearer, hash, socket peer or controller process identity. Infrastructure credentials do not grant organization or workload permissions.

Mutations require the exact generation and session epoch plus a stable mutation ID. Rotation takes the SHA-256 hash of a node-generated CSPRNG credential that the node has already durably retained with that mutation ID. Lost-response retries repeat the complete request and receive the original metadata, never newly minted plaintext. A replay result is historical and cannot authorize a current session. Treat rotation request hashes as sensitive in transport hooks and logs.

These types do not implement authentication, storage, enrollment or endpoints. Cloudly must verify current administrator authority, validate complete inputs, atomically persist the credential transition and immutable actor-bound receipt, and fence subsequent control effects against current durable credential/session authority. Node identity storage and transport integration must be qualified before enabling rotation or Pallet enrollment.

Spark Host Reporting

data.sparkNodeHeartbeatContract defines the HTTPS POST endpoint /spark/nodes/heartbeat, transport byte limits and protocolRefusalStatus, the status a refused offer is answered with. Its exact request snapshot contains a Spark-purpose bearer, the sender's protocol offer and ISparkNodeReport: fractional host CPU, memory and disk observations, Linux host facts, and the verified bundle version, source commit and manifest digest. It has no Docker or container-count fields. Local runtime readiness remains distinct from unverified workload readiness.

The route reads the bounded body, reads the offer with protocol.readProtocolOffer, negotiates it against its own sparkNode offer and answers a refusal with protocolRefusalStatus and an IProtocolRefusal body — all before the node is authenticated, so a peer of another major is refused by name instead of by a credential verdict it cannot act on. The receipt carries the controller's own offer, so an accepted node judges the session it just reported into.

The Swarm-era Spark runtime keeps its own four routes in data.sparkSwarmNodeContracts, each stating the same three facts as the route above — heartbeat (/spark/swarm-nodes/heartbeat), metricsSample, actionResult and swarmObservation, every one of them an { endpoint, maxRequestBytes, maxResponseBytes } — so one bounded read serves every Spark route. The bounds differ because the bodies do: a heartbeat states the node's whole runtime description, a metrics-sample answer carries every action queued for that node, and one observation carries up to 1024 observed Swarm nodes. That is a second runtime, not an older version of this one: a worker in coreflow-node mode posts those bodies and a pallet node posts this one, and the family retires with coreflow rather than with this release. ISparkSwarmNodeHeartbeatRequest is now owned here, so Cloudly and Spark no longer carry private copies of it, and every Swarm-era body — request and answer alike — carries protocol and is judged in the same order. For fleet cutover, that same observation may carry a bounded local Docker snapshot from the reporting node and, only when control is available, a manager-visible service/task snapshot. The existing node-token, reporter-session, sequence, digest and accepted-receipt rules authenticate both; no separate fleet-report route exists.

Each of the four routes has its validator here, so a server never writes its own member checks: data.validateSparkSwarmNodeHeartbeatRequest, data.validateSparkMetricsSampleRequest, data.validateSparkActionResultRequest and data.validateSparkSwarmObservationRequest. All four answer the same way — one string per refused member, naming the member, and an empty list for an admissible body — so a route table can hold them side by side. They read the envelope first (exact key set, the sender's offer, then the reporter's identity), then the payload: a heartbeat's metrics and full runtime description including each described serve.zone service, a sample's four observations and optional network rates, and an action result whose status is one a node can actually report, never pending. Bodies are judged member by member rather than as canonical bytes, because only the observation is digested and the others carry fractional CPU, memory and rate observations; every level is copied away from the caller's object first, so an accessor is refused instead of run. None of them authenticates a node or acts on the body.

Cloudly must authenticate the bearer against current Spark authority and fence credential generation and session epoch transactionally before recording host liveness. ISparkNodeHeartbeatReceipt contains only server-derived acceptance evidence; the sender must match its node ID and credential generation to its exact active identity. Neither a receipt nor the sender's observation time grants current authority or workload readiness. Persist only the report and receipt in IClusterNode.data.sparkNodeReport, never the bearer. Keep legacy reporting separate. This endpoint does not deliver or acknowledge operator actions.

The snapshot functions reject unknown keys, accessors, malformed identities and nonfinite/out-of-range numbers, then return detached frozen records. Transports own byte limits, timeouts and cancellation. These contracts do not implement a listener, credentials, persistence, freshness or shutdown.

Independent Node Enrollment

requests.node defines getNodeEnrollmentState for current Spark-authenticated adoption preconditions and enrollNode for atomic Spark/Pallet enrollment. These contracts do not implement endpoints, authentication, a database or a daemon.

The enrollment proposal binds the canonical HTTPS Cloudly origin, hostname, enrollment identity, original bootstrap proof hash and an exact ordered pair of independently prepared Spark/Pallet credential hashes and CAS counters. Fresh Jump starts both at zero; existing-node adoption rotates current Spark and requires Pallet authority to be absent. snapshotNodeEnrollment returns a detached, frozen snapshot; computeNodeEnrollmentDigest hashes strict canonical JSON under nodeEnrollmentContract.digestDomain, serve.zone/node-enrollment. Neither helper generates or persists credential material.

Transport proof is excluded from the digest to permit exact lost-response recovery. Bootstrap proof must hash to the original proof hash and is allowed only before a receipt exists. Pending-Spark proof must hash to the prepared Spark hash and is allowed only for an existing exact receipt. verifyNodeEnrollmentProof checks this binding and mode only: Cloudly must derive receipt state itself and fence live bootstrap/credential authority in the same owned transaction. Current Jump codes are 12 random bytes encoded as 16 canonical base64url characters.

bindNodeEnrollmentAcknowledgement checks the complete digest, both owners, exact next generations and unchanged adopted node ID, returning a detached frozen acknowledgement. Authenticate Cloudly before accepting it. The acknowledgement is immutable commit evidence, not ongoing authority or workload readiness. Server replay must still verify both active credential generations and hashes; reporter reconnect alone does not invalidate the credential. Spark and Pallet commit their local activation independently and converge after restart; there is no shared plaintext vault or cross-local-database ACID claim. Never log proof/request bodies or hashes, put them in URLs, or expose them through transport hooks.

requests.pallet defines the node-local preparePalletNodeEnrollment, bindPalletNodeEnrollment and activatePalletNodeEnrollment methods. They belong only on the protected root-owned Unix control socket, never a network router. Preparation requests initial Pallet authority (zero generation and session epoch) and returns only its independently retained credential hash. snapshotPalletNodeEnrollmentPreparation and bindPalletNodeEnrollmentPreparation validate that exact exchange.

Local confirmations explicitly distinguish a durable pending binding from the exact current active Pallet identity. bindPalletNodeEnrollmentBound verifies the whole enrollment digest and its pre-assignment node ID (null for fresh Jump). snapshotPalletNodeEnrollmentActivation captures the exact activation request, rejecting extra wrapper fields as well as malformed nested data. bindPalletNodeEnrollmentActive takes that complete activation request and verifies the full Cloudly acknowledgement, including both owners and the assigned node ID. Both confirmation binders return detached frozen snapshots captured before hashing. These are pure content checks: authenticate the local transport separately, and have Pallet prove its current owned state before issuing a confirmation. A historical receipt is insufficient, and Pallet activation does not imply Spark activation or workload readiness.

palletNodeEnrollmentContract specifies one four-byte unsigned big-endian length-prefixed UTF-8 JSON frame per direction, followed by write-half-close, at most 32768 request bytes and 16384 response bytes excluding the prefix. Implement the listener with half-open response support, strict EOF/trailing-byte rejection, bounded time/concurrency, root-only directory/socket permissions and drained cleanup. The package supplies no listener, permission checks or persistent state. No bearer, generic rotation operation or database access is part of this IPC API.

Node Runtime Routing Binding

requests.node.getNodeRuntimeBinding { nodeId, nodeToken } is a current Spark-node-authenticated read. Cloudly derives data.INodeRuntimeBindingRead from its current credential, node, cluster, durable runtime-controller, relay and cluster-runtime records. The answer contains the exact routing identity { cloudlyOrigin, nodeId, clusterId, controller, runtimeNamespace, relay }, the current { id, phase, generation } runtime reference and the Spark credential generation and session epoch that authenticated the read. snapshotNodeRuntimeBindingRead requires the runtime id to equal the routing cluster, and bindNodeRuntimeBindingRead binds the answer to Spark's exact immutable Cloudly origin, node and credential generation. Neither helper authenticates the server.

requests.pallet.bindPalletNodeRuntimeBinding and readPalletNodeRuntimeBinding belong on the existing root-owned Pallet enrollment control socket. Bind is absent-to-present only: the current active Pallet enrollment must name the same canonical Cloudly origin and node, an exact replay is idempotent and every different origin, node, cluster, controller, namespace or relay conflicts. Read requires the requested origin and node to equal both the active enrollment and the stored binding; an absent binding rejects. Spark applies and reads back the binding before starting runtime-serve. On an offline reboot it may read the exact persisted routing identity to select the relay, but that record grants no workload permission and no right to advance or run a cluster phase. Fresh authenticated runtime-session state remains the only workload authority.

The local operations carry no bearer, signing key, generic configuration field, phase mutation or caller-authored permission. nodeRuntimeBindingContract states the immutable local mutation and routing-only persistence rules.

Service Machine Credentials

requests.admin.getServiceMachineCredential inspects redacted metadata; mutateServiceMachineCredential creates, rotates or revokes authority for one exact existing service and organization. ensure requires absent authority and a null expected generation. Rotation and revocation require the exact current generation. Retain the mutation ID and complete input for retry; a receipt replay returns historical metadata without restoring authority.

IServiceMachineGrant currently grants only platform:session. This is separate from deployment grants, human membership and infrastructure permissions. The backend must authenticate each JWT against the current active credential generation, expiry and exact service ownership. Structural snapshot helpers do not perform these checks. Credential changes, encrypted service SecretSet delivery and the replay receipt must commit in the same authorized transaction. The response contains only the immutable secret/version reference; no bearer or hash is returned. Revocation removes current delivery from the metadata.

Private Networks

requests.network defines organization-scoped network create/update/retire and service attachment operations. Mutations require a retained idempotency ID and an exact expected revision; null means no prior document. Cloudly must authorize each request against live canonical membership and policy, reserve aliases transactionally, reject cross-organization attachments, and retain historical receipts. These contracts do not implement the backend or node enforcement.

data.buildPrivateNetworkFqdn produces <alias>.n-<26 lowercase base32 characters>.o-<26 lowercase base32 characters>.internal. from server-owned immutable DNS keys. Aliases are lowercase ASCII labels unique per network. Display-name edits do not rename DNS. A sole attachment selects its search suffix; several attachments require an explicit default or no search suffix. Fully qualified names remain unambiguous across networks. Network policy permits member-to-member connectivity and explicitly configures or denies external DNS forwarding. Creating an alias does not publish public DNS, ingress or ports.

data.composeServicePrivateNetworkDnsPolicy(membership, networks) derives the service's network suffixes, the single selected search suffix, and the intersection of external forwarding permissions. Supply every attached network exactly once, with active state and matching canonical organization/DNS keys. Missing, extra, inactive or inconsistent network definitions are rejected. Forwarding requires every attachment to permit both the queried public name and the same exact upstream IP/port; a denied or empty intersection returns externalDns: null. The default network changes only short-name search. Nested suffix unions are intersected at DNS label boundaries and canonicalized without dropping permitted branches; the composed list can contain up to 1,024 suffixes across 16 networks. This pure helper returns detached policy metadata. Its caller must authenticate and fence the included membership/network revisions before issuing node authority.

The snapshot helpers validate and detach content; they do not authenticate it, reserve names, prove membership, or report applied state. Packet authority remains until an applied denial or independent fence. DNS snapshots have a separate maximum 15-minute validity, with positive TTLs capped at five seconds and negative TTLs at one second. DNS expiry does not prove packet revocation or permit identity reuse. Retirement and attachment responses describe desired or historical metadata; consumers must separately track outstanding node revocations and readiness.

IRuntimeNetworkProtectedAuthority declares the complete controller-owned IPv4 protection set, disjoint private workload/transit pools, protected resolver and platform endpoints, and every affected egress authority, including offline nodes. Its snapshot and digest helpers bind the exact predecessor and all declaration content. They cannot prove that an operator's inventory is complete. Install the union of old and new protection on every affected egress owner, or independently fence that owner, before exposing a newly allocatable pool. Removing an entry from the declaration is not a revocation acknowledgement. Prefix arrays are sorted lexically and nonoverlapping; pools and endpoints are sorted by ID; egress owners are ordered by canonical JSON of [nodeId, runtimeNamespace].

IRuntimeNetworkProtectionReceipt reports a worker's durable, joined native protection journal separately from projection admission. It binds a complete protected-authority reference, node/controller scope, authenticated reporter, boot identity and exact native journal reference to a monotonic receipt chain. A null nativeBarrier explicitly removes allocation eligibility. A boot or protection change requires new native journal evidence. These portable values do not inspect a kernel or authenticate their own producer.

reportRuntimeNetworkProtection uses the current authenticated physical peer. Its request binder accepts a historical outbox only against receiver-owned reporter history and identifies that use as historical. The response must match the exact sent receipt. bindRuntimeNetworkAllocationProtection requires the current persisted protection receipt and durable session for every declared egress owner, in canonical owner order. Missing, unavailable, stale-protection and superseded-session evidence reject. An offline owner cannot be omitted. The controller must fence all those records together with lease insertion, before reserving any handoff or workload address. No generic independent-fence flag is provided; such a mechanism needs its own verified owner. Protection receipts neither prove workload readiness nor release packet/address/name quarantine.

IRuntimeNetworkHandoffLease binds an immutable node/router incarnation to its protected-authority reference, transit subnet and peers, protocol-qualified source ports, nonzero conntrack zone and 128-bit label. The label is exactly 32 lowercase hex characters. Port ranges are inclusive, sorted by protocol then first port, and disjoint within each protocol. bindRuntimeNetworkHandoffLease verifies both digests, scope and transit-pool containment and returns detached values. It does not authenticate a projection or attest to an actual namespace/interface.

snapshotRuntimeNetworkHandoffLedger verifies at most 256 retained leases for one node/controller epoch, including quarantined allocations. It rejects host handoff-port collisions even across router/runtime namespace changes, and zone or label reuse within one router incarnation. The caller must transactionally supply the complete retained set, serialize allocation and fence controller-epoch changes. These are collision checks, not an IPAM allocator or a native capability. No expiry, empty conntrack dump, process exit or table removal establishes flow drainage or permits handoff identity reuse. Signed complete projections, live interface proofs and the separate router/host apply journal remain responsibilities of Cloudly and Pallet; neither packet admission nor readiness follows from these helpers.

IRuntimeNetworkWorkloadLease is the immutable material referenced by assignment.workload.network. It binds one execution attempt and router incarnation to a workload pool, dedicated unbridged linkSubnet, workload address, exact /32 sourcePrefix, and router-side gateway. The gateway also supplies dnsServer on port 53, because CRI DNS settings cannot encode a port. Canonical private point-to-point subnets through /31 are accepted; the owning CNI implementation must qualify its chosen layout. The source prefix, link subnet, VPN control prefix and host/router transit subnet have different roles. bindRuntimeNetworkWorkloadLeaseToAssignment verifies immutable assignment identity without a circular assignment digest. Membership, aliases, readiness and projection revisions never change this address material.

IRuntimeNetworkProjection contains complete relevant network definitions, service memberships and endpoint leases, including remote ready endpoints and declared services with no replicas. It carries the current protected authority, exact historical allocation authorities, local handoff, a maximum 15-minute DNS window and explicit packet/lease/handoff withdrawals. All arrays use the documented canonical ordering: networks and endpoints by ID, memberships by canonical [organizationId, serviceId], references by canonical complete reference, and withdrawals by canonical complete grant. Projections are bounded to 896 KiB, 128 networks, 256 memberships/endpoints, 16 historical authorities and 4,096 directed grants. These transport bounds do not assert native capacity; the node must preflight the actual SmartVPN, Smartnftables and DNS limits before effectful application.

validateRuntimeNetworkProjection checks complete digest and scope bindings, pool containment, disjoint link subnets, network/organization keys, aliases, resolver authorization and ready-replica uniqueness. It does not authenticate controller inventory, prove observation provenance or authorize allocation reuse. Cloudly must derive readiness from the current authenticated assignment report, retain all affected offline authorities and complete its allocation/barrier transactions before issuance.

An endpoint may publish node ports through the optional IRuntimeNetworkEndpoint.publishedPorts. Each IRuntimeNetworkPublishedPort binds one hostPort (1..65535, unique per node and protocol across the projection) to the workload's targetPort, optionally on an explicit uplink hostIp (never the wildcard or loopback; binding it to the node's uplink is Pallet's enforcement, because the projection carries no uplink fact), and carries the Cloudly authorization reference that permitted it: a published port without that reference cannot exist, and the projection signature covers every byte of it. An endpoint that publishes nothing omits the member entirely and keeps its exact canonical bytes and digest; a present member is never an empty list, so "publishes nothing" has exactly one encoding.

Operators author that authorization through getServicePublishedPorts and setServicePublishedPorts, modelled on the private-network membership pair. IServicePublishedPorts returns the current authorized list with its policy revision, or null when a service never had one. ISetServicePublishedPorts returns the accepted IServicePublishedPorts document with its new revision and a replayed flag, and carries expectedRevision (null for the first document), a retained mutationId for replay, and IRuntimeNetworkPublishedPortRequest entries of exactly protocol, hostPort and targetPort: the request accepts no hostIp (an omitted address means the node uplink) and no ranges. An empty list is a valid policy meaning the service publishes nothing. Callers never supply an authorization reference: Cloudly derives each projection entry's reference from the accepted policy revision and digest, and node-level host-port placement stays Cloudly's decision.

getRuntimeNetworkProjectionPacketGrants derives directed member connectivity and explicit local public/platform egress independently of DNS readiness. composeRuntimeNetworkProjectionDnsViews builds one union view per local workload source address, with every authorized FQDN and complete ready-replica A set. An empty array declares NODATA; absent/unauthorized names remain NXDOMAIN, and IPv4-only service names have AAAA NODATA. The view carries the existing forwarding intersection and zero or one search suffix. Pallet must prove the actual sandbox/veth/source binding, block private/bare-name forwarding, clip TTLs and convert the effective window from getRuntimeNetworkEffectiveDnsWindow (see DNS Lease Renewals) using its qualified boot/time authority. These helpers neither cache queries nor renew a lease.

The DNS composer accepts an optional second argument of exact local workload lease references. Selection happens before view expansion while the complete validated projection still supplies every eligible local or remote target and declared empty name. Omit it for all local views or pass [] for none. Unknown, remote, stale and duplicate references reject. Selection is captured before asynchronous validation and output retains projection order. This lets Pallet compose only its currently attached sources without duplicating membership rules; the references themselves do not prove native attachments or DNS authority.

The optional IRuntimeNetworkProjection.router member carries explicit router-origin egress selections and withdrawals. A projection that omits it retains its exact canonical bytes, digest and signature and grants no router-origin flows. New issuers can select resolverIds and platformEndpointIds from the current protected inventory; an empty selection denies all router egress. DNS forwarding policy, workload egress and inventory presence alone supply no router grant. The whole member is signed with the projection; it is not an unsigned runtime option.

getRuntimeNetworkProjectionRouterPacketGrants validates the complete projection and returns up to 96 detached exact grants. Each binds the current local handoff reference and its router transit source address to a selected destination ID, IPv4 address, protocol and port. Resolver selection explicitly authorizes both TCP and UDP at the declared resolver port. Platform selection authorizes only its declared transport tuple, for example a VPN relay endpoint. Every selected protocol must have a source-port allocation in that handoff. No public wildcard, workload source or alternate source address can be supplied in this selection.

Router withdrawals retain these complete grant bodies, including the old handoff and source address. Removing a selection, changing an inventory endpoint or retiring the handoff must explicitly withdraw the old grants. Unknown denials and denials overlapping current grants reject. Up to 4,096 retained withdrawals carry forward until the caller supplies its exact joined application of the preceding projection, with the same receipt boundary as workload withdrawals. These values do not prove packet drainage, native enforcement or allocation reuse. Consumers must compose both workload and router grants and their separate withdrawals; they must also fence the actual native source, routing and process lifetimes. DNS expiry or readiness changes do not revoke packet authority.

admitRuntimeNetworkProjection checks exact predecessor/replay identity and rejects changed same-generation content, regressed membership/readiness evidence, changed immutable address/DNS keys, and omitted packet grants without explicit withdrawal. Pending withdrawals/tombstones must carry forward until the caller supplies the exact previous projection reference from its own durable joined application receipt. That reference is never taken from an incoming ACK. Keep the accepted history and apply journal until native effects and DNS/allocation quarantine are settled; a complete new snapshot does not erase that history.

IRuntimeNetworkSigningAuthority is separate from human JWT signing. Install its current revision through the existing authenticated physical connection and commit it with a durable CAS fence. Newly enrolled nodes may bootstrap the controller's current key generation; existing trust cannot be reset to do so. Exact successor revisions rotate the Ed25519 public key; publicKey: null revokes it. Rotation/revocation stops acceptance under the previous key, and requires newly signed authority for DNS. It does not deny outstanding packets or permit allocation reuse.

The Node-only @serve.zone/interfaces/runtime export supplies createRuntimeNetworkSigningKey, signRuntimeNetworkProjection and verifyRuntimeNetworkProjection. Signing produces ISignedRuntimeNetworkProjection { authority, projection, signature }, and the signature covers the canonical envelope { domain, authority, projection } under the dedicated serve.zone/runtime-network-projection-signature domain, so it binds the complete projection and the exact signing-authority revision. Verification requires receiver-owned trusted authority and node scope; an envelope contains no key to trust. Private KeyObjects stay process-local; Cloudly persists their exported material only through its encrypted internal secret store. The dedicated requests.runtimesession key/projection push contracts bind the current physical peer. Their response acknowledges durable admission only, never application, readiness, independent fencing or pool activation.

The getRuntimeManagedVpnCredential method contract in requests.runtimesession uses IGetRuntimeManagedVpnCredentialRequest and IGetRuntimeManagedVpnCredentialResponse. Its request carries the exact current session and signed projection reference. The secret response repeats those bindings and supplies the hub tuple the node dials, the transport it dials it over (quic, the one transport a managed hub serves), the managed authority ID, the Noise server public key, the client keypair and the expiry in Unix milliseconds. The response states no TLS name: QUIC is dialled on the address alone. The hub is the node's own cluster's relay (see Cluster VPN Hub And Network). Keep the response process-local; never persist, hash, log or embed it in a signed projection.

snapshotGetRuntimeManagedVpnCredentialRequest and its response counterpart capture closed detached JSON shapes and canonical 32-byte key encodings. bindGetRuntimeManagedVpnCredentialRequest(request, projection, trustedSession) validates the complete projection digest and binds node, controller, namespace, physical-session identity and projection reference. The response binder takes (response, request, projection, trustedSession, now, renewal = null) and additionally requires the hub's exact tuple to be selected in router.platformEndpointIds, carrying the protocol of the credential's transport — udp for quic, as runtimeNetworkAddressPlanContract.hubTransports states. A relay therefore cannot name itself: the endpoint has to be one the node's signed projection already selected. Expiry must be after the caller-qualified time and no later than the referenced projection's effective DNS window (see DNS Lease Renewals); acceptance before that window starts rejects.

These helpers perform content validation. Callers authenticate and fence the current physical peer and projection before and after native work, qualify time independently, and retain ownership through revocation and shutdown. The native Noise implementation owns cryptographic validation and authentication. Credential delivery does not prove a connected VPN, TUN ownership, packet enforcement or workload readiness, and does not replace explicit packet withdrawals.

DNS Lease Renewals

A projection's dns window lasts at most fifteen minutes and is signed content, so a node's private DNS stops when the window ends unless a newer signed statement extends it. Re-issuing the projection would extend it too, but a node applies every new projection as new network state, which restarts its data plane. IRuntimeNetworkDnsLeaseRenewal extends the window and nothing else: it carries the node scope (controller, nodeId, runtimeNamespace), the exact projection reference it renews, a sequence, a fresh dns { issuedAt, notBefore, expiresAt } window and its digest. It names the projection by reference, so it never joins the projection chain and never changes what the node applied. The new shapes carry no schemaVersion and no version suffix; the installed package version is the protocol version.

issuedAt is the start of the window's slot, not the moment of signing: Cloudly signs renewals ahead of time on a grid, so the fifteen-minute rule (maximumDnsLeaseMs, the same limit a projection's window has) is measured from the slot. computeRuntimeNetworkDnsLeaseRenewalDigest hashes every field except digest under its own domain, and validateRuntimeNetworkDnsLeaseRenewal checks structure and digest only; a digest authenticates nothing.

The Node-only @serve.zone/interfaces/runtime export signs and verifies renewals with the same authority and key that sign the controller's projections, under a separate signature domain. signRuntimeNetworkDnsLeaseRenewal(renewal, authority, privateKey) produces ISignedRuntimeNetworkDnsLeaseRenewal { authority, renewal, signature }. verifyRuntimeNetworkDnsLeaseRenewal(signed, trustedAuthority, trustedScope, projectionAuthority) takes the authority and scope from the node's own stored trust, never from the envelope. projectionAuthority is the reference of the authority revision that signed the projection the node admitted, and a renewal verifies only when it was signed under exactly that revision. Rotation and revocation end renewals exactly as they end projections: nothing signed under an earlier revision verifies after, and a renewal signed under a rotated revision does not renew a projection signed under the one before. A renewal never closes a key gap; after rotation, DNS continues only with a newly signed projection.

admitRuntimeNetworkDnsLeaseRenewal(renewal, currentProjection, previousRenewal | null, trustedScope, now) is the pure admission decision after verification. The projection and the previous renewal are the node's own admitted state, and now is its caller-qualified clock, read before the first await like every other input. It admits a renewal only when:

  • the renewal and the projection are in the trusted scope, and the renewal names the current projection's exact reference. Once a successor projection is admitted, every renewal of its predecessor is refused, and a renewal never introduces a projection the node has not admitted;
  • its sequence is higher than the previous renewal's, with gaps allowed, or equal with byte-identical content, which is answered as replay whatever now is, so re-delivery is idempotent. The same sequence with any other content is refused;
  • its window is current: notBefore <= now < expiresAt. Admitting a window that has not opened would displace the coverage the node has now, so a sender delivers a window only once its slot has opened; a renewal refused for arriving early is simply delivered again once it has. An expired window is refused as well;
  • its window does not move issuedAt back and ends later than the previous window. Without a previous renewal, the projection's own window is the baseline. Windows need not touch, so a node whose lease lapsed can still admit the next renewal it is given.

The caller stores the admitted renewal in the same transaction as its trust fence, and deletes it in the transaction that admits a successor projection. Admission is not conversion: the node still turns the window into a deadline with its qualified clock, and must not use a window before its notBefore.

requests.runtimesession.IReq_Controller_Pallet_ApplyRuntimeNetworkDnsLeaseRenewal (applyRuntimeNetworkDnsLeaseRenewal) delivers one renewal as IApplyRuntimeNetworkDnsLeaseRenewalRequest { session, signed }. bindApplyRuntimeNetworkDnsLeaseRenewalRequest(request, trustedSession) requires the node's current physical-session binding and a renewal in that session's scope. It does not care who sent the request: a cluster relay pushes under the binding the node obtained itself. The answer, IApplyRuntimeNetworkDnsLeaseRenewalResponse { status, renewal }, repeats IRuntimeNetworkDnsLeaseRenewalReference { projection, sequence, digest }. bindApplyRuntimeNetworkDnsLeaseRenewalResponse(response, sentRenewal) compares it with the exact renewal that was sent. The ACK asserts durable admission only, not that the node serves DNS.

A cluster is outbound-only, so while its relay cannot reach Cloudly nobody can sign a new window. Cloudly therefore hands the elected relay a run of renewals it signed ahead of time, with requests.cluster.IReq_Cloudly_Relay_HoldRuntimeNetworkDnsLeaseRenewals (holdRuntimeNetworkDnsLeaseRenewals): IHoldRuntimeNetworkDnsLeaseRenewalsRequest { nodeId, projection, renewals } answered by { held }. Each request replaces what the relay held for that node, and an empty run holds nothing. The relay keeps the run in memory only, pushes only the renewal whose window is current and only while it has no Cloudly session (escrowRelease), and holds no key and no trust. snapshotHoldRuntimeNetworkDnsLeaseRenewalsRequest checks the rules a relay can evaluate without the projection itself; the node still measures the first window against its admitted projection, and every window against its clock, when it admits it. The run must have:

  • one node, one projection and one signing authority revision, in one scope;
  • strictly ascending sequences, each window extending the one before;
  • at most maximumEscrowedRenewals (32) renewals;
  • at most maximumEscrowHorizonMs (four hours) from the first window's issuedAt to the last window's expiresAt. On the intended grid of fifteen-minute windows every ten minutes, the horizon allows 23 windows, so it is the limit that binds.

validateHoldRuntimeNetworkDnsLeaseRenewalsRequest adds every renewal's digest check and verifies no signature. bindHoldRuntimeNetworkDnsLeaseRenewalsResponse(response, sentRequest) requires held to equal the number of renewals sent.

A hold is the one call whose outcome the controller cannot read back from its own records, so the refusal is the whole answer. data.holdRuntimeNetworkDnsLeaseRenewalsRefusals is the frozen list of names a relay leads its refusal with — escrow-run-invalid, escrow-node-not-carried, escrow-run-foreign-session, escrow-run-stale, escrow-run-unordered and escrow-cloudly-only — with data.THoldRuntimeNetworkDnsLeaseRenewalsRefusal as their union. The relay writes <name>: <reason> and the controller matches the name, so neither side keeps its own copy of the vocabulary.

The horizon is also the revocation latency: a detached cluster keeps DNS authority until its last held window ends. So a controller must not release an address or a DNS name before the latest expiresAt it has signed for that node. Renewals extend DNS only. They grant no packet authority and cannot introduce new network state, and they do not extend an already-issued VPN credential; a newly issued one may be bound to the renewed window.

getRuntimeNetworkEffectiveDnsWindow(projection, renewal | null) is the one rule for the window a node may use: the admitted renewal's if there is one, else the projection's own. renewal is the bare IRuntimeNetworkDnsLeaseRenewal that admission takes and returns, which is what a node stores; Cloudly passes signed.renewal. It checks digests and that the renewal names exactly that projection, but verifies no signature. Controller and node use it for every limit tied to the window, and bindGetRuntimeManagedVpnCredentialResponse takes the same bare admitted renewal as an optional last argument, so a newly issued managed-VPN credential may run to the end of the renewed window instead of the projection's original one.

import { data } from '@serve.zone/interfaces';
import { signRuntimeNetworkDnsLeaseRenewal, verifyRuntimeNetworkDnsLeaseRenewal } from '@serve.zone/interfaces/runtime';

const renewal: data.IRuntimeNetworkDnsLeaseRenewal = {
  ...scope,
  projection: { id: projection.id, generation: projection.generation, digest: projection.digest },
  sequence: 1,
  dns: { issuedAt: slotStart, notBefore: slotStart, expiresAt: slotStart + 900_000 },
  digest: `sha256:${'0'.repeat(64)}` as data.TSha256Digest, // well-formed placeholder, replaced below
};
renewal.digest = await data.computeRuntimeNetworkDnsLeaseRenewalDigest(renewal);
const signed = await signRuntimeNetworkDnsLeaseRenewal(renewal, signingAuthority, privateKey);

// On the node: stored trust, then the node's own admitted state and qualified clock.
const verified = await verifyRuntimeNetworkDnsLeaseRenewal(
  signed, storedAuthority, nodeScope, admittedProjectionAuthority);
const { renewal: admittedRenewal } = await data.admitRuntimeNetworkDnsLeaseRenewal(
  verified.renewal, admittedProjection, previousRenewal, nodeScope, qualifiedNow);
const window = await data.getRuntimeNetworkEffectiveDnsWindow(admittedProjection, admittedRenewal);

Runtime Network Address Plan

data.IRuntimeNetworkAddressPlan is the address space an administrator gives the runtime network, and the single input Cloudly composes the protected authority from: { id: 'address-plan', revision, prefixes, pools, resolvers, platformEndpoints, vpn: { controlPrefix, hubPrefix } }. Pools, resolvers and platform endpoints use the protected authority's own element shapes. controlPrefix is the private prefix VPN control addresses are allocated from, and hubPrefix is the protected prefix every cluster's VPN hub binds inside. The hub endpoints themselves are no administrator's input: each cluster's relay reports what it bound, and the composition turns that into platform endpoints, so platformEndpoints never lists one.

snapshotRuntimeNetworkAddressPlan refuses any plan that could describe an authority the authority contract refuses: it runs snapshotRuntimeNetworkProtectedAuthority over the plan's own sets, so overlapping pools, more than 16 resolvers and a resolver or endpoint outside prefixes or inside a pool are all refused by that one validator. On top it requires both VPN prefixes to sit inside prefixes, to overlap no pool, to hold no resolver or platform endpoint, and never to overlap each other — control addresses live inside the tunnel, hub endpoints on the underlay. controlPrefix is additionally a private prefix no longer than /30 (runtimeNetworkAddressPlanContract.maximumControlPrefixLength); no minimum length is stated, because the private-range rule already keeps it at /8 or longer. data.runtimeNetworkVpnControlPrefix(value) is that reader on its own, exported because a cluster's VPN network repeats the prefix its hub binds and has to read it exactly as the plan does.

data.IRuntimeNetworkVpnHub { clusterId, address, quicPort } is one cluster's hub as its relay bound it. runtimeNetworkVpnHubEndpoints(hub) is the one derivation of what it contributes: <clusterId>:quic on udp, one endpoint per row of runtimeNetworkAddressPlanContract.hubTransports. That table has one row, because a managed hub terminates TLS on its QUIC listener alone and no other transport of it can be dialled over a protected connection. Every reader calls the derivation — the composition that seals the authority and the router selection that names which endpoint a node may dial — so two readers can never derive two ids for one hub. composeRuntimeNetworkProtectedAuthority(plan, frame, hubs) is the composition itself: the producer supplies the frame (TRuntimeNetworkProtectedAuthorityFrame: authority id, generation, controller, previous reference and egress authorities) and the hubs of the clusters that registered one, and receives the complete authority with its digest, so the validator and the protection producer can never compose differently. Each hub's address must sit inside vpn.hubPrefix and one cluster is named once; an empty hubs composes the authority of a runtime network whose clusters have no hub yet. One hub costs one of the authority's 64 platform endpoints, so 64 clusters fit one authority.

admitRuntimeNetworkAddressPlanChange(previous | null, next) is the pure decision for a plan write at the following revision; any other revision is a contract violation. Leases and the managed VPN outlive a plan revision, so it answers:

  • unchanged when the content equals the stored plan; Cloudly writes nothing;
  • refused with plan-vpn-immutable when either VPN prefix differs, plan-pool-removed when any pool is gone or changed its purpose or prefix, and plan-prefix-shrunk when the new prefixes, however split, no longer cover every address the old ones did;
  • accepted with the ids of the resolvers and platform endpoints the change removes or changes. An entry that keeps its id but differs in any field counts, so moving it goes through the same reference check as removing it.

TRuntimeNetworkAddressPlanRefusal adds plan-resolver-referenced and plan-endpoint-referenced, which only Cloudly can decide against live references, so every plan refusal has one type. requests.network carries setRuntimeNetworkAddressPlan { identity, expectedRevision, plan } (expectedRevision: null when no plan exists yet) and getRuntimeNetworkAddressPlan, which answers null until the first plan is set.

Node Network Readiness

requests.network.getRuntimeNetworkNodeReadiness { identity, nodeId } answers data.IRuntimeNetworkNodeReadiness: ready: true with nothing missing, or ready: false with at least one TRuntimeNetworkNodeReadinessGap. The gaps follow the node producer's order: the node joins the protected authority's egress (egress-pending), every egress owner reports current protection (protection-receipts-pending, naming those owners, at least one), a handoff is reserved (handoff-pending), the router selection carries it (selection-pending), the node's acknowledged projection carries it (projection-pending), and the node is a managed VPN member (vpn-member-pending).

data.IRuntimeRouterIncarnation { id, incarnation } is a node's router incarnation, from which its handoff and router incarnation ids derive; snapshotRuntimeRouterIncarnation checks the record. reincarnateRuntimeNetworkRouter { identity, nodeId, expectedIncarnation } advances it, so the node producer reserves the router a fresh handoff, and answers with the new incarnation.

Node-Bound Runtime Assignments

The data namespace exports IRuntimeAssignment, canonical digest helpers, evaluateRuntimeAssignment, and assignment observation validators/evaluators for Cloudly/Onebox controllers and Pallet nodes. These are shared contracts; they do not install a runtime, schedule containers or expose an RPC endpoint.

Each assignment binds a controller incarnation, organization, stable node and replica IDs, one immutable execution attempt, image/configuration and opaque network/storage/secret references. Group and site IDs are nullable topology provenance. Those workload references are also the authority vocabulary: data.runtimeWorkloadAuthorities is ['network', 'secrets', 'storage'], and data.requiredWorkloadAuthorities(workload) names the ones this workload cannot run without — filtered out of that vocabulary and sorted, so the answer is duplicate-free and compares byte for byte against the resolvedAuthorities a node stated when it registered. data.isResolvedWorkloadAuthorities(value) is the same rule read the other way, for a receiver checking a list it was sent. Canonical is strictly increasing in the code-unit order of the names themselves — a rule a peer in another language implements from the vocabulary alone, rather than from the order this package happens to declare it in — and both directions compare through that one rule, so neither can drift from the other.

workload.registryHost is the registry that published the image, which is the publication identity the image release is bound to, and it is never rewritten. workload.pullEndpoint is the optional host[:port] the node fetches the bytes from instead — a cluster whose relay forwards the registry states the relay's own origin, so a node pulls inside its cluster and nothing cluster-side dials the control plane. data.buildRuntimeAssignmentImageReference(workload) is the one place that choice is made: pullEndpoint ?? registryHost, digest-pinned either way, so the endpoint decides reachability and never content, and a controller and a node cannot disagree about the reference. Absence stays absence: an assignment that states no endpoint is different content, and so a different digest, from one that does.

All fields except the digest itself participate in a domain-separated SHA-256 digest. Digests provide integrity and compare-and-swap identity; authentication must separately establish the trusted controller/node scope.

An attempt starts at generation one with run, then advances through stop-preserve and remove-runtime-preserve-storage using exact predecessor generation/digest references. Exact retries return replay; conflicts, stale revisions, gaps, changed execution specifications and resurrection are rejected. Consumers must atomically persist the evaluator's detached snapshot before effects, retain removal tombstones and storage, and serialize ownership of each replica. Omission causes no change. A new attempt requires separate admission proving any older exclusive writer stopped or fenced. The supported offline policy is continue-node-bound; no heartbeat timeout or lease expiry proves a writer absent.

Observations bind the exact assignment, authenticated reporter session, durable per-assignment sequence and digest. Reconnect does not reset the counter. Observation history must reference the same assignment revision or its exact predecessor; receivers process each disposition's observations in order. Session authorization, clock/freshness policy and durable receipt persistence belong to the receiving service. Readiness requires matching running image evidence and run intent; assignment acceptance alone is never readiness. A valid observation can describe drift (such as a removed runtime while intent remains run), and does not authorize storage relocation or prove fencing.

Resolved Runtime Configuration

data.IRuntimeConfig seals the public environment, working directory, resource and identity policies, target ports, readiness timing and log limits together with the immutable image invocation. computeRuntimeConfigDigest hashes that exact content; bindRuntimeConfigToAssignment checks the config reference, service, organization, image and selected platform against one assignment. Secret material, registry credentials and host paths remain outside the configuration.

Both CPU and identity policies are required. cpuMillis: null explicitly means no CPU quota; a positive number sets a quota. runAsUser and runAsGroup must either both be numeric overrides or both be null. The null pair selects the pinned platform image's OCI user and group resolution against its immutable root filesystem. A consumer must not guess a group for a UID-only or named image user. Version 29 widens these numeric TypeScript fields to include those explicit null policies; consumers must handle them before accepting configurations from v29. Existing numeric configurations retain their exact meaning and digest.

Authenticated Runtime Sessions

requests.runtimesession defines registerPalletRuntimeSession, applyRuntimeAssignment, reportRuntimeAssignmentObservation, and getRuntimeRegistryCredential. Pallet initiates WSS through its configured Cloudly HTTPS origin, or, on a relayed cluster, through the relay origin its cluster publishes in ICluster.data.relay while Cloudly remains the credential authority. The receiver derives the physical peer, process instance and registration idempotency; callers cannot supply those authorities. A serialized IRuntimeSessionBinding provides content to compare with authenticated peer state and does not itself authenticate a connection. Its clusterId is issued from the controller-owned node record during registration; it is never selected by Pallet, and a current-session check must fence the node to that same cluster. Registry credentials are ephemeral, bounded and authorized against an exact current run assignment.

After reconnect or credential rotation, the current authenticated session may submit an unchanged historical observation from its durable outbox. Server-indexed history must prove the same node, runtime namespace and durable controller, an older session epoch and an eligible credential generation. A historical-recovery receipt can advance the durable counter and acknowledge the exact body; it cannot restore active readiness, routes or liveness. Those require a fresh observation from the current session. The contract helpers validate and classify content; authentication and atomic receipt persistence remain server-owned.

A registration is identified by its node, its credential generation and nodeInstanceNonce: a canonical lowercase UUID that Pallet generates once per process start and sends on every registration. From 32.0.0 the field is required, so a controller no longer has a nonce-less case to decide. One physical socket may carry the registrations of many nodes — a cluster relay forwards a whole cluster — so the transport alone can no longer tell a node restart from a replay. The nonce is not a secret, is not authentication, and grants nothing on its own: the bearer still authenticates every registration.

The registration also states resolvedAuthorities: which of data.runtimeWorkloadAuthorities (network, secrets, storage) this node build can resolve, sorted and duplicate-free so a replay compares byte for byte. It is a capability of the registering build, never a permission: a controller refuses an assignment whose data.requiredWorkloadAuthorities(workload) the list does not cover, instead of a node accepting work it cannot carry out. It is not part of the registration identity — a changed build is a new process and therefore a new nonce — so a replay that states different authorities is a conflict rather than an update.

Every registration carries protocol, and the controller judges the body in the one server order protocol.readProtocolOffer states: the bounded body, the offer, protocol.negotiateProtocol against this session kind's own offer, and only then the exact-key validator, the bearer and any mutation. For a node that order is what keeps a refused registration from touching its packet access. registerPalletRuntimeSession answers { session, protocol }, and data.bindRegisterPalletRuntimeSessionResponse binds that answer to the request the node sent and negotiates the two offers from the node's side.

A changed nonce, or a changed credential generation, is a new registration: the controller mints a new binding and a new session epoch and fences the node's previous session. An unchanged nonce with an unchanged generation is a replay and must return the exact historical binding. Bearer rotation therefore needs no transport reconnect: a node that rotates behind a relay registers again with its unchanged nonce and its new generation on the same live socket, and the other nodes that socket carries are untouched.

registerCloudlyClientSession separately authenticates a current human or machine JWT on a physical TypedSocket so the server can own its identity tag; it carries the same offer, judged in the same order before the JWT, and answers with the server's own offer. Both registration methods reject HTTP transport. Sensitive requests and registry responses must be excluded from hooks, logs, diagnostics and durable journals.

Assignment-Bound Runtime Secrets

@serve.zone/interfaces/runtime exports the Pallet assignment-secret contract. runtime.createRuntimeAssignmentSecretAuthority takes an already validated, immutable IResolvedSecretManifest and, for launcher-environment deliveries, the pinned WorkloadInit approval reference and platform artifact. It computes a value-free descriptor digest before Cloudly computes the generation-one run assignment digest. workload.secrets contains that descriptor's { id, generation, digest } reference. The manifest digest, descriptor digest and assignment digest form an ordered chain; none depends on a later digest. bindRuntimeAssignmentSecretAuthority checks that chain against the full manifest, run assignment and pinned runtime config, including image and invocation digest, organization, service, platform and required launcher mode. The producer must separately prove that the manifest and WorkloadInit approval are currently accepted by their owning stores.

The Pallet-session methods getRuntimeSecretRecipientState, beginRuntimeSecretRecipientEnrollment and completeRuntimeSecretRecipientEnrollment enroll one X25519 public recipient generation for the authenticated node. The challenge is sealed with SmartCrypto and binds the exact current session, node, controller epoch, key, generation and expiry. Cloudly must derive the node from the current bearer-authenticated node session and separately verify ownership of its physical peer or cluster relay, retain one challenge hash with its expiry, consume it once, advance recipient generation by durable CAS, retire the prior recipient and reject a revoked node or stale session. Pallet holds the corresponding private key only in process memory. After a daemon restart it must generate a new pair and re-enroll before requesting fresh material; no old envelope or private key is persisted for replay. Existing running mounts require separate native ownership recovery.

getRuntimeAssignmentSecretMaterial names the exact current generation-one run reference, its sealed secret authority reference and the active recipient key and generation. Its response carries the full value-free descriptor and manifest, exact sorted version coverage, an approved immutable WorkloadInit authority when launcher delivery requires it, and X25519 envelopes. Each envelope context binds the final assignment digest, node, organization, cluster, service, descriptor digest, image/invocation/secret rollouts, secret version and recipient generation. bindGetRuntimeAssignmentSecretMaterialRequest validates the request against server-owned current assignment/session and active recipient inputs. bindGetRuntimeAssignmentSecretMaterialResponse additionally validates the response against the pinned config and cluster bound into that authenticated session; neither helper decrypts. A serialized IRuntimeSessionBinding does not authenticate a peer. Cloudly still owns node-session authentication and peer or relay ownership, current slot/service authority, recipient CAS, accepted-version retention and issuance policy. Pallet owns recipient private-key custody, native memory mounts and exact terminal cleanup. Both request and response bodies are sensitive and must be excluded from hooks, logs, diagnostics and durable runtime configuration.

ISecretVersionRetentionReference uses runtime-assignment with the exact assignment ID as resourceId when a Pallet run still owns a secret version. This reference has no expiresAt: plan acceptance windows do not end a running assignment's hold. Cloudly releases it only after joining the exact generation-three removal receipt, which Pallet may emit only after its owned runtime and secret mounts are removed. Retention does not itself authorize fresh material issuance to a stale session or recipient.

Joined Runtime Terminal Receipts

data.IRuntimeAssignmentTerminalReceipt and requests.runtimesession.reportRuntimeAssignmentTerminalReceipt carry a separate receipt for the exact Pallet attempt's joined terminal execution. Sequence 1 binds the generation 2 stop assignment; sequence 2 binds generation 3 removal and the exact stop receipt digest. Both retain the original generation 1 run reference. The full immutable assignment chain and both receipt bodies remain durable after removal, permitting an exact earlier stop receipt to be acknowledged again.

Only a durably completed native operation can issue a terminal receipt. Native stop joins container and sandbox stop; native remove requires an already stopped sandbox and joins container and sandbox removal. The assignment owner separately requires the exact completed stop before admitting removal. Terminal evidence contains precise CRI identifiers, the kernel boot UUID, and stopped/absent state; this native path does not collect IP addresses. Pending, unknown and host-fenced journal entries never qualify. A proven boot change can permit a new joined terminal operation but cannot skip the stop/remove chain.

The synchronous snapshot helpers validate bounded exact data only. Digest, assignment-chain and receipt-chain helpers do not prove native execution or authenticate a reporter. bindRuntimeReporterSession and the terminal report binder require the current physical session and immutable server-indexed reporter history from their trusted owners. Reconnect recovery preserves every original receipt field and returns historical-recovery; it never restores active readiness, routes or liveness. The response binder rejects rewritten receipt bodies.

Cloudly must transact new acceptance against the exact global incumbent, stored assignment chain and current peer, credential and node authority. A first receipt after the incumbent changes is denied; only an already persisted exact receipt can be acknowledged again under current authentication. These receipts settle only the named Pallet attempt. They do not authorize storage relocation, prove whole-host or Docker/Coreflow absence, grant another controller or organization authority, or replace independently qualified readiness and routing evidence.

Cluster Runtime Phase

data.IClusterRuntime { id, phase, generation, changedAt, changedBy } records which runtime owns a cluster's workloads: TClusterRuntimePhase is swarm, draining, switching or pallet. It is stored beside the cluster document, because a cluster update merges caller data and no caller may move a phase with it; generation is its compare-and-swap counter. snapshotClusterRuntime checks the record. Which phase may follow which belongs to the cutover, whose requests ship with it.

requests.cluster.getClusterRuntime { identity, clusterId } answers { runtime, blockers }. Each TClusterRuntimeBlocker names what it is about: cluster-wide blockers (relay-not-enabled, relay-not-registered, address-plan-missing, vpn-unconfigured) name nothing, node blockers (node-without-pallet-credential, node-arch-unsupported) name the nodeId, and service blockers (service-multi-cluster-scope, service-authority-unsupported, platform-binding-present) name the serviceId.

Fleet Runtime Cutover Authority

data.IFleetCutoverCensus is Cloudly's accepted, Spark-authenticated Docker census. It carries only immutable Docker service/spec identity, exact mode and replica count, hashed constraint identity, the ownership labels used by the cutover, exact tasks and value-free mounts. A manager reports only manager-visible /nodes, /services and /tasks state. Every node separately reports its own complete local physical-container list through the existing authenticated sparkSwarmNodeContracts.swarmObservation route; a manager never attests remote Docker-daemon contents. The local list is fenced by exact sorted container-ID set digests read before and after all bounded inspect calls, so a container appearing or disappearing during inspection refuses the snapshot. The accepted census references two consecutive manager observations with identical Swarm and inventory digests and one current accepted local observation for every target node. Every reference preserves the authenticating node, credential generation, reporter session, sequence, observation time and fixed freshUntil; assembling a later census cannot renew an old observation. It never transports environment, arbitrary labels, mount paths, volume names, registry credentials or secret values.

Mount identity has three explicit canonical domains. serve.zone/fleet-cutover-mount-source-identity hashes { type, source }, where source is the exact UTF-8 Mount.Source returned by Docker, including an empty string. serve.zone/fleet-cutover-mount-target-identity hashes { target }, where target is the exact UTF-8 Mount.Destination. serve.zone/fleet-cutover-mount-identity then hashes { type, classification, sourceIdentityDigest, targetIdentityDigest, readOnly }. All three use strict canonical JSON and no case, slash, path, volume-name or alias normalization. Only the digests and classification cross the wire. unknown type/classification remains visible and prevents Cloudly accepting a complete scope; missing, unmanaged or conflicting service/container ownership also prevents completion rather than omitting the container. Structural validation does not establish freshness, authentication, manager consensus, ownership or Docker truth; Cloudly establishes those policy facts while dereferencing the accepted observations.

data.IFleetCutoverScope is the server-owned authoritative service set. It starts incomplete; every complete entry binds an exact Docker service id to the current service authority and an accepted classification authority. unknown cannot appear in a complete structural snapshot. An authenticated administrator may submit IFleetCutoverScopeClassificationRequest against the exact current census, including for an unlabeled legacy Coretraffic or Corestore service. Cloudly still compares the declaration with current service, platform-service, credential, storage and census authority before accepting it. A classification is an operator decision, never a client proof boolean or a service-name inference.

data.TFleetCutoverPhaseAdvance permits only swarm -> draining -> switching -> pallet and carries opaque references to owner-accepted records. requests.fleetcutover.advanceFleetCutoverPhase dereferences and joins them inside Cloudly's phase CAS. Leaving draining requires the accepted fresh census/quiescence decision, the complete current scope and target set, every runtime spec held, and the accepted set of root-local volume fences. Entering pallet still requires every spec held. switching grants session and network preparation only; it runs no service. Structural snapshots enforce the closed forward shape but do not claim the referenced records exist or are current.

Before the first swarm -> draining CAS, Spark starts the replacement Coreflow32 relay as the exact Docker service/spec/image, delivers only its new generation-bearing credential and obtains the live accepted relay registration. Cloudly then revokes the old credential and accepts the current Spark bundle/unit/configuration and old-mode refusal evidence. This is a pre-switch Docker owner; it is never represented as a Pallet assignment. Downstream service holds and storage fences therefore depend on a live relay without requiring assignment authority that cannot exist yet.

The storage bootstrap has two different authorities. IFleetCutoverVolumeHandoffIntent is Cloudly's accepted pre-switch statement of exact service, volume, source-lineage and storage-request identity. It also binds the exact persistent mountIdentityDigest and accepted source-node observation. It is not a ready IRuntimeStorageClaim. Pallet persists a root-local TFleetCutoverVolumeFence in held state while the cluster is draining. That state carries no claim, session, assignment, attachment, reservation, confirmation or execution authority, and the storage owner must block all those operations for the volume. After the cluster reaches pallet, Corestore performs its one-shot initial lineage/ReadWriteOnce CAS for the same mount identity under that held native fence and returns IFleetCutoverCorestoreAdoptionReceipt, which for the first time binds the real ready IRuntimeStorageClaim reference. Only that exact accepted receipt can consume the Pallet fence. This separation prevents a pre-switch fence from fabricating provider readiness merely to satisfy cutover ordering.

IFleetCutoverCoreflowRetirementEvidence binds the real pre-switch Docker Coreflow32 relay by exact service ID, container ID, spec version/spec digest and image digest to accepted manager and local node observations. It requires no Pallet assignment. Cloudly dereferences those observations to require a live exact container without Docker/cgroup mounts, then joins its generation-bearing relay credential and accepted live registration to the current Spark credential, bundle, unit and accepted observation. It separately binds invalidation of the legacy credential, refusal of the old callable mode, and Spark's configuration CAS. The legacy source is identified by SHA-256 and may be retained-inert or removed; deletion and secure erasure are not cutover requirements. Its exact owner generation is retained with that disposition. Retained recovery evidence is valid only while the released owner cannot consume it into live mode and the old credential remains revoked.

The post-pallet order is represented without a second route or publication model. First Cloudly releases only the new ingress spec under an accepted empty published-port policy. It then accepts the existing cluster-ingress registration and route-table revision, exact Spark evidence that the old ingress is absent, the current source and target publication records and owning port-transfer receipt, and a readback of the current route revision as IFleetCutoverIngressAuthority. Only an application release may cite that authority. The legacy Docker Coretraffic instance therefore remains through switching; the new ingress runs without public ports before exact port ownership moves, and applications remain held until the new authoritative route is read back.

The owner methods live under requests.fleetcutover: administrator scope classification and forward phase CAS, root-local Pallet volume-fence hold/read/ consume, one-shot Corestore adoption, ingress-authority read, and ingress/application spec release. Spark transports local and manager evidence through its existing authenticated Swarm-observation route, and Cloudly assembles census and Coreflow-retirement records from accepted owner state; there is no caller-supplied census or retirement proof endpoint. Accepted evidence references are opaque { id, generation, digest } records. They contain no secret bytes and gain meaning only when the named server owner dereferences them under current authorization and policy.

Service Runtime Specs And Status

data.IServiceRuntimeSpec is what Cloudly runs for one service on a Pallet cluster: { id, organizationId, clusterId, placement, replicasPerNode, hold, profile, network, revision }. It is stored complete and beside the service document, so a hold written by a cutover never races an operator's service edit.

  • placement is { kind: 'every-node' } or { kind: 'nodes', nodeIds }, with nodeIds sorted, unique, never empty and at most maximumPinnedNodes, the protected authority's egress limit.
  • replicasPerNode runs from 1 to 64; Cloudly refuses a service desiring more than maximumReplicas (64) as replica-limit.
  • profile { cpuMillis, memoryBytes, readiness, logs, readonlyRootfs, runAs } is the runtime config's execution policy ahead of any image: the same readiness and log shapes, and the same CPU, memory (at least 16 MiB), identity, log, readiness timing and HTTP request rules, which both validators share. runAs: null runs the image's own OCI user. Only the service's target ports decide whether a readiness probe's target exists: the runtime config requires the service to declare it, and the producer refuses a service that does not as readiness-target-undeclared.
  • network is { mode: 'isolated' } or { mode: 'attached', publicEgress, platformEndpointIds } with sorted unique endpoint ids.

snapshotServiceRuntimeSpec checks all of that; whether the cluster, the nodes and the endpoints exist is Cloudly's. One canonical service may have one runtime target on each of several clusters. Every public target keeps the same IServiceRuntimeSpec: id is the exact service id and clusterId names that target. Cloudly's private persistence joins those two values; no composite public service id is introduced.

requests.service.setServiceRuntimeSpec { identity, serviceId, expectedRevision, spec } writes TServiceRuntimeSpecWritableData, and Cloudly copies the organization from the service. Its compare-and-swap is for (serviceId, spec.clusterId): null means that exact target is absent, and each target advances its own revision. The request bytes and public spec shape are unchanged.

getServiceRuntimeSpecForCluster { identity, serviceId, clusterId } reads one exact target. getServiceRuntimeSpecs { identity, serviceId, cursor, limit } reads targets in cluster-id order, with an opaque cursor and a limit from 1 through serviceRuntimeContract.maximumSpecPageSize. Both reads answer each spec with its producer status, which is null until the producer first decides the spec. A status never comes without its spec.

The original getServiceRuntimeSpec { identity, serviceId } remains a scalar compatibility read: zero targets answer { spec: null, status: null }, and one target answers it. More than one target is refused with IServiceRuntimeSpecAmbiguityErrorData { code: 'service-runtime-spec-ambiguous', retryable: false } in TypedResponseError.errorData; Cloudly never selects a target implicitly.

removeServiceRuntimeSpecForCluster { identity, serviceId, clusterId, expectedRevision } removes one exact target only after Cloudly transactionally derives and fences all authority and safety facts: the revision still matches, the target is held, every target slot is fully settled and released, and the cluster's current phase still belongs to the spec producer. Callers provide no phase, settlement, release or authority booleans or evidence.

data.IServiceRuntimeStatus { id, specRevision, state, reason, changedAt } is the producer's verdict on one spec revision, and each reason belongs to exactly one state. converged and converging carry reason: null. waiting carries a TServiceRuntimeWaitingReason: node-network-pending, conflict or failure-backoff. refused carries a TServiceRuntimeRefusal: cluster-runtime-missing, organization-missing, image-plan-missing, platform-unsupported, secrets-unsupported, volumes-unsupported, port-protocol-unsupported, ports-too-many, readiness-target-undeclared, environment-too-large, no-eligible-node, replica-limit, node-authority-unsupported, network-policy-missing, network-policy-mismatch, network-workload-pool-missing or network-workload-pool-exhausted. ports-too-many is a service declaring more target ports than the runtimeConfigContract.maximumTargetPorts (64) a config carries, where port-protocol-unsupported is about one port's protocol; node-authority-unsupported is a node whose session resolved fewer workload authorities than the spec needs, refused at the admit step rather than handed to a node that would silently park it. An attached network requires an explicit service policy: its absence is network-policy-missing, and a disagreement with the requested egress or platform endpoints is network-policy-mismatch. A missing workload address pool in the current protected authority is network-workload-pool-missing; existing eligible pools without a free workload subnet are network-workload-pool-exhausted. Neither policy nor pool is silently defaulted. A refusal never retires running slots. snapshotServiceRuntimeStatus enforces the pairing.

Runtime Assignment Views

data.IServiceRuntimeAssignmentView is one replica slot's current assignment as an administrator reads it: serviceId, replicaId, nodeId, the current assignment reference, its disposition, the latest accepted observation { phase, ready, observedAt, sequence } (which may cite an earlier revision), terminalReceiptCount (0 to 2) and delivery. delivered means the node's own evidence, an observation or a terminal receipt, cites the current revision; until then it is undelivered.

requests.service.getServiceRuntimeAssignments { identity, serviceId } answers every slot of one service. requests.cluster.getClusterRuntimeAssignments { identity, clusterId, cursor, limit } pages through every slot on a cluster's nodes; limit runs from 1 to serviceRuntimeContract.maximumAssignmentPageSize (256), and Cloudly refuses any other. requests.service.pushServiceRuntimeChanged { serviceId } tells admin UIs that a service's spec, status or assignments changed; it carries no state, so a late push never overwrites a newer read.

Node Retirement

data.INodeRuntimeRetirement { id, clusterId, state, actorId, since } records that one node leaves its cluster's Pallet runtime. TNodeRuntimeRetirementState is retiring while the producer still stops and removes the node's slots, the compiler quarantines its handoff and both node credentials are revoked, and retired once that has settled. It is stored beside the node document, because a node update merges caller data and no caller may retire a node with it. The producers read the row's presence rather than its state: a node that has one is no longer an egress member and desires no slots, so the row is written before anything is taken away and stays as long as the node document. snapshotNodeRuntimeRetirement checks the record.

requests.node.retireClusterNode { identity, nodeId, expectedGeneration } answers { retirement }. expectedGeneration is the cluster runtime generation the caller read with getClusterRuntime, so a cutover that moved the phase meanwhile refuses the write. Every refusal is one of data.nodeRuntimeRetirementRefusalsnode-missing, cluster-runtime-not-pallet, node-already-retired or conflict — and takes nothing away.

Corestore Database Export Packaging

The Node-only @serve.zone/interfaces/runtime export provides metadata-only Corestore database-export package contracts. A package request carries exact input length and SHA-256, approved source-backup and complete-output-receipt digests, a path-safe attempt ID, and an explicit absent or exact allocation expectation. It never carries the export bytes or Base64 content. Immutable completion binds the normal Corestore database backup receipt and exact closure identity; delivery wrappers may report exact replay without changing completion. Release is permitted only against the caller's exact fsynced closure and receipt identities. API-token authentication remains transport authority, and these Corestore contracts do not claim to verify Cloudly authority-B HMACs.

Exact input identities include one terminating LF and are limited to 96 MiB plus that LF. Package control metadata is limited to 16 KiB and keeps the existing 256 MiB Corestore closure limit.

One media type and one backup format are stated here for every process that handles a Corestore database backup: runtime.corestoreDatabaseBackupMediaType (application/vnd.serve-zone.corestore-database-backup) is the type an archive is served and stored under, and runtime.corestoreDatabaseBackupFormat (corestore.database.backup) is the format a receipt states. Corestore's control server, its export packager, Onebox and the cluster relay import them instead of writing the bytes again, so the side that writes a transfer and the side that reads it cannot disagree about its type.

Service creation carries canonical ownership separately from caller-writable service data. requests.service.IRequest_Any_Cloudly_CreateService requires a top-level organizationId, while data.TServiceWritableData excludes the server-managed ownership field. Consumers should use data.validateOrganizationId() to validate the identifier shape and verify the request's ownership authorization separately before persistence.

Protocol handshake

The protocol version is the installed @serve.zone/interfaces release. There is no protocol number beside it, no per-shape version suffix and no schemaVersion field: one release of this package is exactly one contract, so naming the release states everything about what a peer sends and what it can read.

Two peers exchange an offer when a session opens and refuse an incompatibility by name:

import { protocol } from '@serve.zone/interfaces';
import { TypedResponseError } from '@api.global/typedrequest';

// One offer per session kind a deployable serves, built once at start-up. A deployable that
// depends on nothing newer than its own major accepts that whole major.
const [installedMajor] = protocol.protocolVersion.split('.');
const ownOffer = protocol.createProtocolOffer(`${installedMajor}.0.0`);

// Server, on every carrier: the bounded body, then the offer, then everything else — the
// exact-key validator, the bearer and any mutation.
const peerOffer = protocol.readProtocolOffer(requestBody);
const negotiation = protocol.negotiateProtocol(ownOffer, peerOffer);
if (!negotiation.compatible) {
  throw new TypedResponseError(
    protocol.describeProtocolRefusal(negotiation.refusal),
    negotiation.refusal,
  );
}

// Client, on the payload of a failed hop: `TypedResponseError.errorData` on a TypedRequest, the
// parsed body on the Spark heartbeat's refusal status. Everything else is not a refusal.
const refusal = protocol.readProtocolRefusal(errorData);
if (refusal) {
  logger.warn(protocol.describeProtocolRefusal(refusal));
  // A refusal stands until an operator upgrades one of the two sides, so the client waits.
  setTimeout(offerAgain, protocol.refusedOfferRetryIntervalMs);
}
Export Purpose
protocolVersion The installed release, taken from the package's own commit info.
IProtocolOffer { interfacesVersion; minimumPeerVersion } — what a side speaks and the oldest peer it accepts.
createProtocolOffer(minimumPeerVersion) This build's offer. A minimum above the installed release, or in another major, throws here — at start-up, not at the first registration.
validateProtocolOffer(value, path?) Every reason an offer is refused, as an array of messages; empty for a valid offer.
validateProtocolOfferOn(body, path?) The same reasons for the protocol key of a body, read as a data property. The naming twin of readProtocolOffer, for validators that list reasons instead of throwing.
readProtocolOffer(body) The offer on a body, read from the protocol key alone and returned detached.
negotiateProtocol(local, remote) { compatible: true } or { compatible: false; refusal }. Pure; throws when either side's offer names something that is not a release version.
IProtocolRefusal { code: 'protocol-incompatible'; reason; refusing; refused }, carried as TypedResponseError.errorData or as a plain HTTP JSON body.
TProtocolIncompatibility 'major-mismatch', 'peer-below-minimum', 'self-below-peer-minimum'.
readProtocolRefusal(value) The refusal inside an error payload (TypedResponseError.errorData) or a response body, or null when the value is not one.
describeProtocolRefusal(refusal) One log line naming both sides, opening with the frozen protocol-incompatible code.
refusedOfferRetryIntervalMs 300_000 — the shared cadence at which a refused client offers again.

negotiateProtocol checks in a fixed order: different majors are major-mismatch even when a minimum would also fail, then a remote below the local minimum is peer-below-minimum, then a local below the remote minimum is self-below-peer-minimum. The last two are mirror images of one another — one holding on this side means the other holds on the peer — and they can never hold at once, which would require each version to be below the other. An offer that does not name two release versions is a local defect, not an incompatibility between two peers: negotiateProtocol throws on it, and a peer-sent body is read with readProtocolOffer first, which refuses it by name. Both offers and refusals come back frozen and detached from the values they were read from.

Every carrier reads the offer once, as data. A protocol key, or a member of the offer under it, that is an accessor rather than a plain value is refused as "must use its exact schema" and is never invoked — by readProtocolOffer, by validateProtocolOfferOn, by validateProtocolOffer and by readProtocolRefusal alike. No snapshot, validator or client on any carrier runs code a peer attached to a body, and none of them can observe one value while a session is served by another. This is stated here and nowhere else: the families that carry an offer go through these functions rather than restating the rule.

A carrier that negotiates puts the offer in a top-level protocol key on both its request and its response, and a server reads it before it validates the rest of the body, authenticates or changes any state. Validators in this package are exact-key, so a body from a later major is unreadable by an older build; reading the offer first is what turns that into a named refusal instead of a generic contract rejection, and it keeps a refused peer's credentials, sessions and packet access untouched.

Versions are MAJOR.MINOR.PATCH and nothing else — no prerelease, no build metadata, no v prefix, no leading zeros — and are compared per numeric component, so 32.10.0 is above 32.9.0. minimumPeerVersion is in the same major as interfacesVersion and never above it. Each deployable owns its minimums, one per session kind it serves, and raises one only in the commit that starts depending on a later interfaces minor, for that session kind alone.

Secrets v24 Pre-Cutover Contract

Version 24 retains the value-free v23 architecture while replacing the remaining pre-cutover lifecycle and runtime authority gaps. Secret values enter Cloudly only as strict SmartCrypto X25519 envelopes or an explicit bounded server-generation request. Coreflow receives a full digest-verified manifest plus one sealed envelope per pinned SecretVersion through the Node-only runtime export.

This is a contract release before consumer cutover. It does not claim that clean-v2 migration, backup verification, historical secret erasure, or storage cleanup has completed. Those operations remain separately gated and must be proven by their owning services before destructive cleanup.

Breaking removals include:

  • The SecretGroup and SecretBundle data/request modules and request namespaces.
  • getServiceSecretBundlesAsFlatObject and every service/preflight field tied to bundled flattening or aggregate runtime files.
  • Plaintext resolved runtime values, generic platform config/credential maps, credential-bearing Cloudly settings, and serialized object-storage credential references.
  • Hosted-app control-token identities and credential-bearing bootstrap actions.

The current secret contract includes:

  • TSecretValueInput, active-only IActiveSecretRecipientMetadata, the requests.secret.IReq_GetSecretIngressRecipient contract with method getSecretIngressRecipient, and fixed-order create/rotate/App Store context builders.
  • ISecretEnvelopeAdmissionBinding under the Node-only runtime export binds an exact envelope and request context to one recipient generation. The binding is reproducible and does not itself prove admission. Retiring-key retries require a trusted Cloudly mutation receipt created atomically while that recipient was active; caller-supplied admission or issuance timestamps are not part of the contract.
  • getSecretVersionPurgePreflight returns bounded advisory reference pages and complete blocker counts, including indefinite runtime-assignment holds until joined terminal removal proof. purgeSecretVersion identifies one exact version and fences the mutation by secret, version, and target revisions. The non-issuable purge-pending lifecycle and revisioned pending/erasing/failed/succeeded operation keep external erasure durable across retries without treating preflight as authorization.
  • IResolvedSecretManifest contracts, stable Docker resource naming, WorkloadInit map/wrapper helpers, and ISealedResolvedSecretMaterial under @serve.zone/interfaces/runtime.
  • Two-step Coreflow X25519 recipient enrollment and exact recipient lifecycle validators.
  • Mandatory IImmutableContainerInvocation evidence on immutable image deployment plans.

Secrets v24 Runtime Registration And Reporting

The Node-only @serve.zone/interfaces/runtime export adds the live contract that Cloudly must validate before publishing secret-bearing desired state to a Coreflow connection:

  • getSecretRecipientEnrollmentState returns either an exact generation-zero empty state or the complete valid recipient set for the cluster derived from the verified JWT.
  • getCoreflowSecretRuntimeRegistrationExpectation returns either an available expectation or an explicit unavailable reason. The available expectation binds the live reporter session, active recipient, fresh generation-fenced target authority, and active WorkloadInit approval through expectationDigest.
  • Spark sends authenticated, sequenced local Swarm membership observations and, on managers, complete manager snapshots. Cloudly derives scope from the Spark credential, reconciles manager consensus privately, and publishes one fresh single-Swarm target authority or a targetless unavailable state. Structural contract validation does not itself establish manager consensus.
  • TSparkSwarmObservation lets workers report only their local Swarm node ID, because Docker does not expose the Swarm cluster ID to workers. Cloudly may associate that node with a cluster only through authenticated node scope and accepted manager consensus. The transport contract designates sparkSwarmNodeContracts.swarmObservation, whose endpoint is /spark/swarm-nodes/swarm-observation and whose maxRequestBytes covers the largest snapshot this validator accepts; every body on it carries the sender's protocol offer. A conforming Spark sender must retain one exact request until it receives a request-bound acceptance receipt. A conforming Cloudly consumer must authenticate the node before inspecting replay state and derive cluster scope from that persisted identity.
  • computeSparkSwarmObservationDigest uses strict canonical JSON and the serve.zone/spark-swarm-observation domain. A reporter session starts at sequence one. The contract requires a Cloudly consumer to accept only the exact next sequence with a strictly advancing observedAt, while an exact same-sequence/same-digest retry returns the byte-equivalent persisted receipt before age checks. It must reject conflicting, stale, skipped, retired-session, old, future, or non-advancing reports. A reused session ID may be treated as new only after it leaves the bounded retired window, and its sequence-one observation must still advance the permanent timestamp high-water mark. The consumer must persist acceptance state and its receipt in one atomic boundary.
  • validateSparkSwarmObservation validates exact structure and attached snapshots, while validateSparkSwarmObservationRequest and validateSparkSwarmObservationResponse validate transport and request binding. None of these functions authenticates a node, persists replay state, establishes manager consensus, or creates runtime authority.
  • WorkloadInit approval binds a clean stable release identity, version-tagged OCI index, exact amd64 and arm64 platform/executable digests, policy generation, and a Cloudly summary of detached Cosign DSSE/SLSA verification. Public shape and digest validators do not verify the signature, public-key trust root, or private Cloudly policy. The provenance identifiers a verifier matches are workloadInitReleaseContract.buildType (https://serve.zone/buildtypes/workloadinit-gitzone-tsdocker) and builderId (https://serve.zone/builders/workloadinit-manual-release); the in-toto statement type, the SLSA predicate type, the Cosign bundle format and the OCI index media type keep the exact bytes their own specifications define. An approval carries no shape version, and IWorkloadInitApprovalAuthorityReference states only the counter authorityGeneration beside the approval digest it points at.
  • coreflowSecretRuntimeRegistrationTagId is the sole dedicated TypedSocket tag identifier. Its payload is exactly ICoreflowSecretRuntimeRegistration; cluster scope comes from the verified connection identity rather than the tag.
  • ICoreflowSecretRuntimeRegistration references the exact expectation, target generation/digest, and WorkloadInit authority. It must cover every Cloudly-node/Swarm-cluster/Swarm-node identity and approved per-platform manifest and installed-executable digest without self-asserting placement or approval.
  • capabilities is a sorted, duplicate-free list of data.TCoreflowRuntimeCapability names drawn from data.coreflowRuntimeCapabilities, judged by data.isCoreflowRuntimeCapabilityList. A build states what it can do by name; which shape it speaks is the interfaces version both peers exchanged. Every name in data.requiredCoreflowRuntimeCapabilities must be present. That list is the vocabulary minus data.optionalCoreflowRuntimeCapabilities, which holds corestore-inventory alone: it is the one capability a build may lack, because it depends on the node's Corestore deployment rather than on the contracts the build was compiled against.
  • validateCoreflowSecretRuntimeRegistration compares the registration with a trusted expectation built by Cloudly, including the live reporter session. Missing, extra, duplicate, reordered, or mismatched node evidence fails closed. Consumers must discard the registration on transport disconnect, tag removal or replacement, or any live session, placement, artifact, or recipient expectation change before publishing more secret-bearing state.
  • reportSecretDeploymentState reports only applying, applied, drifted, or failed for the manifest and plan revision selected by Cloudly. The validator receives the trusted cluster ID after JWT verification and the trusted live reporter session. Wire-provided manifest scope is checked against, and never replaces, that trusted authority.

The package validates report shape, digest, trusted cluster, and trusted live session; it does not persist replay state or mutate deployment plans. Deployment report consumers must persist an atomic receipt keyed by the verified cluster, reporter session, service, and positive sequence. The report digest uses fixed-order JSON and excludes only the JWT identity and reportDigest. The consumer accepts the exact next sequence once, returns the prior response for a same-digest replay, rejects a different-digest replay, and ensures a plan revision CAS failure consumes neither the sequence nor a receipt. Timestamps are informational and never replace live session, placement, artifact, recipient, sequence, or plan-revision fences.

createWorkloadInitEnvironmentMap now rejects manifests without any launcher-environment delivery. Consumers must bypass WorkloadInit for file-only manifests.

Portable Storage Contracts

App Store templates can declare logical, template-local storageClasses and stable named storageRequests. The same manifest is fulfilled by Onebox or Cloudly without exposing a physical provider:

const storageConfig: appstore.IAppStoreVersionConfig = {
  image: 'example/database:1.0.0',
  port: 5432,
  storageClasses: {
    databaseFast: {
      kind: 'filesystem',
      purpose: 'database',
      required: {
        performanceTier: 'highIops',
        durability: 'persistent',
        hardQuota: true,
        snapshots: 'native',
        encryptedInTransit: true,
      },
    },
    backupCapacity: {
      kind: 'objectStorage',
      purpose: 'backup',
      required: {
        performanceTier: 'capacity',
        durability: 'persistent',
        hardQuota: true,
        encryptedInTransit: true,
      },
    },
  },
  storageRequests: [
    {
      id: 'database-data',
      kind: 'filesystem',
      storageClass: 'databaseFast',
      mountPath: '/var/lib/example',
      accessMode: 'ReadWriteOnce',
      capacity: { request: '20GiB', limit: '40GiB' },
      reclaimPolicy: 'retain',
      protection: { backup: 'required', snapshots: 'native' },
    },
    {
      id: 'backup-archive',
      kind: 'objectStorage',
      storageClass: 'backupCapacity',
      accessMode: 'readWrite',
      capacity: { request: '100GiB', limit: '1TiB' },
      reclaimPolicy: 'retain',
      delivery: {
        type: 'file',
        targetPath: '/run/secrets/backup-archive.json',
        format: 'servezone-object-storage',
        uid: 1000,
        gid: 1000,
        mode: 0o400,
      },
      protection: { versioning: 'required', retentionDays: 30 },
    },
  ],
  requiresFeatures: [
    appstore.appStoreStorageFeatureIds.bindings,
    appstore.appStoreStorageFeatureIds.filesystem,
    appstore.appStoreStorageFeatureIds.objectStorage,
    appstore.appStoreStorageFeatureIds.objectStorageFile,
  ],
};

Capacity quantities are positive integers followed by KiB, MiB, GiB, or TiB. Storage request IDs survive upgrades and restores. Logical class keys express requirements and preferences only; Onebox and Cloudly map them to operator policy independently.

An app may declare multiple objectStorage requests. Each request resolves to its own endpoint, bucket, and value-free credential management scope. Launcher environment delivery uses an explicit key map and file delivery uses one managed JSON Secret at a unique target path, so two bindings cannot share credential destinations accidentally.

platform.storage contains separate capability advertisements and resolved binding/status contracts. Resolved object-storage bindings expose connection metadata plus a service-owned credential management scope and delivery policy, never credential references or values. Filesystem bindings expose the container mount and access mode, never a host path.

A binding's requestDigest is stated by one function, so the control plane that issues it and the consumer that re-derives it compute the same value:

const requestDigest = await platform.createStorageRequestSha256(storageRequest);

It normalizes the request to its contract shape, canonicalizes it and returns bare lowercase hex over the request alone — no prefix, and nothing wrapped around the request. A body that is not a storage request is refused with platform.StorageContractError instead of hashed, and platform.canonicalizeStorageRequest returns the exact bytes the digest is taken over. platform.storageRequestCanonicalDigestGoldenVectors states both for a filesystem request and for both object-storage deliveries.

Which requests exist at all is stated by the same contract:

const storageRequest = platform.normalizeStorageRequest(declaredRequest);

platform.normalizeStorageRequest reads a body against the exact request schema and returns the request in its contract shape — required members present, optional members carried only when declared, canonical environment keys, no empty protection block, no explicitly undefined member, no -0 or unsafe integer, and plain JSON data members only. It is the value the other two functions are derived from: canonicalizing and digesting a request normalize it first, so a consumer that admits a request through this function and a control plane that digests it cannot accept different sets of requests. The input is never mutated and the returned request is a fresh object. A body that is not a storage request is refused with platform.StorageContractError, whose message names the failing member so a consumer can map the refusal to its own status vocabulary instead of restating the rules.

Portable manifests and resolved bindings intentionally have no fields for Synology, NFS, Kerberos, Corestore, Kubernetes, Docker drivers, servers, exports, mount options, provider credential values, or local fallback paths. Runtimes must reject unknown manifest fields and unsupported required feature IDs before provisioning. Legacy volumes and platformRequirements.s3 remain deprecated inputs for strict resolver normalization only. platformRequirements.mail declares that the runtime provisions a CoreMail binding for the instance and injects the platform-owned MAIL_COREMAIL_*, MAIL_FROM and SMTP_* identities; a template never declares mail credentials or sender addresses itself.

Runtime filesystem storage claims

data.IRuntimeStorageClaim is the portable, Cloudly-issued authority for one ready, retained ReadWriteOnce filesystem binding on one cluster node. It names the organization, service, cluster, node, runtime namespace, controller epoch, binding ID/generation, bare canonical storage-request digest, opaque resource reference, policy class/revision, and canonical container mount path. The binding must report a persistent, single-node capability and matching observed generation. The claim carries no host path, physical inode, Docker option, credential, execution attempt, or assignment digest.

data.snapshotRuntimeStorageClaim makes a detached exact-schema snapshot; data.computeRuntimeStorageClaimDigest hashes its full content except digest under the serve.zone/runtime-storage-claim domain; and data.validateRuntimeStorageClaim verifies the stated digest. Cloudly seals the claim's { id, generation, digest } in IRuntimeAssignment.workload.storage. At admission, data.bindRuntimeStorageClaimToAssignment(claim, assignment, expectedClusterId) checks that reference and the exact controller, organization, service, node, namespace and cluster. The cluster ID must come from the authenticated cluster scope, because an assignment does not contain one. These functions prove content consistency only: the caller must authenticate Cloudly, check current provider readiness and policy, durably admit the assignment, and obtain a fenced native attachment from the storage owner before a CRI mount. An absent claim or an unverified opaque resource reference never authorizes a mount.

platform.storagemigration defines the provider-neutral cutover contract for a named object-storage binding. Corestore atomically owns and fences the source binding after validating a distinct, unfenced active-object snapshot. Onebox only receives a held candidate, stages that exact candidate, stops the matching workload generation, and attests the quiesced state. The candidate cannot start until destinationBindingStartAuthorized is true. Corestore continues returning consumerAction: 'startDestination' until Onebox submits the mutation-fenced consumer activation request and the acknowledgement becomes durable evidence. Candidate-issued abort tombstones authorize only startSource and never retain the staged candidate binding.

Every migration DTO and status has an exact, versioned runtime normalizer. Unknown fields, provider or pool identifiers, unbounded strings, unsafe integers, stale mutation revisions, identity drift, and lifecycle-inconsistent fields are rejected. Migration-created digests use strict canonical JSON and a bare lowercase 64-hex SHA-256 value; portable golden vectors cover the source snapshot, target request, prepare intent, and candidate binding. Pre-cutover failures may retry or abort; after the durable commit point, recovery can only retry or roll forward. Physical pool IDs, mount details, provider receipts, and publication capabilities remain private.

Primary exports include:

  • IObjectStorageMigrationPrepareRequest and TObjectStorageMigrationStatus for the immutable intent and status journal.
  • IObjectStorageMigrationConsumerQuiesceRequest with IStorageMigrationConsumerQuiesceEvidence for exact candidate staging and source-workload shutdown.
  • IObjectStorageMigrationConsumerActivationRequest with IStorageMigrationConsumerActivationEvidence for durable destination-start acknowledgement.
  • normalizeObjectStorageMigrationStatus, bindObjectStorageMigrationConsumerQuiesceRequest, and bindObjectStorageMigrationConsumerActivationRequest for strict ingress and current-revision mutation fencing.
  • The create*Sha256 helpers for source snapshots, prepare intent, target requests, candidate/active bindings, and persisted staging or activation evidence.

The phase and consumer-action progression is exact:

Phase consumerAction Destination start authorized
preparing, transferring wait No
awaitingConsumerQuiesce stageCandidateAndStop No
finalizing, committing wait No
readyToStart startDestination Yes
cleanupPending, complete none Yes; durable activation evidence is required
aborting wait No
aborted startSource No; only the active source binding may restart

Normalize every status before acting, and bind consumer mutations to that exact status revision:

const status =
  await platform.storagemigration.normalizeObjectStorageMigrationStatus(
    untrustedStatusPayload,
  );

if (status.phase === 'awaitingConsumerQuiesce') {
  const request =
    await platform.storagemigration.bindObjectStorageMigrationConsumerQuiesceRequest(
      untrustedQuiescePayload,
      status,
    );
  await submitQuiesceAcknowledgement(request);
}

if (status.phase === 'readyToStart') {
  if (
    !status.destinationBindingStartAuthorized ||
    status.consumerAction !== 'startDestination'
  ) {
    throw new Error('destination binding is not authorized to start');
  }
  await startWorkload(status.activeBinding);
  const request =
    await platform.storagemigration.bindObjectStorageMigrationConsumerActivationRequest(
      untrustedActivationPayload,
      status,
    );
  await submitActivationAcknowledgement(request);
}

if (status.phase === 'cleanupPending') {
  // Cleanup is reachable only after this durable acknowledgement was accepted.
  const durableActivation = status.consumerActivationEvidence;
}

if (status.phase === 'aborted') {
  if (
    status.destinationBindingStartAuthorized ||
    status.consumerAction !== 'startSource'
  ) {
    throw new Error('invalid aborted migration status');
  }
  await startWorkload(status.activeBinding);
}

Here startWorkload, submitQuiesceAcknowledgement, and submitActivationAcknowledgement are consumer-owned operations, not package exports. Canonical digests are produced from normalized payloads:

const snapshotSha256 =
  await platform.storagemigration.createUnfencedObjectStorageBindingControlSnapshotSha256(
    snapshotDigestPayload,
  );
const migrationSha256 =
  await platform.storagemigration.createObjectStorageMigrationSha256(
    prepareRequest,
  );
const candidateSha256 =
  await platform.storagemigration.createObjectStorageMigrationBindingSha256(
    candidateBinding,
  );
const activationRecordSha256 =
  await platform.storagemigration.createObjectStorageMigrationPersistedActivationSha256(
    activationDigestPayload,
  );

Data Contracts

Use data when you need object shapes that are persisted, exchanged between services, or exposed through the Cloudly API.

import { data } from '@serve.zone/interfaces';

const service: data.IService = {
  id: 'service-api',
  data: {
    name: 'api',
    description: 'Public API service',
    imageId: 'image-api',
    imageVersion: '1.0.0',
    environment: {
      NODE_ENV: 'production',
    },
    serviceCategory: 'workload',
    deploymentStrategy: 'limited-replicas',
    scaleFactor: 2,
    balancingStrategy: 'round-robin',
    targetPorts: [
      {
        name: 'web',
        port: 3000,
        protocol: 'http',
        default: true,
      },
      {
        name: 'ssh',
        port: 2222,
        protocol: 'ssh',
      },
    ],
    ports: {
      web: 3000, // legacy compatibility shorthand during migration
    },
    domains: [
      {
        name: 'api',
        protocol: 'https',
        targetPort: 'web',
      },
    ],
    publicPortMappings: [
      {
        name: 'ssh-public',
        publicPort: 2222,
        targetPort: 'ssh',
        protocol: 'tcp',
        exclusive: true,
      },
    ],
    deploymentIds: [],
  },
};

Common data contracts include:

  • ICluster and IClusterNode for cluster membership and provisioning state, including the optional ICluster.data.relay block described below.
  • IService, IDeployment, IImage, IRegistryTarget, and IExternalRegistry for workload delivery.
  • Service port contracts including IServiceTargetPort, IServiceDomainRoute, and IServicePublicPortMapping for canonical backend targets, domain target references, and edge/Coretraffic TCP/UDP public exposure.
  • IDomain, IDnsEntry, and traffic contracts for routing and DNS management.
  • Traffic and gateway route contracts including ICoretrafficPortRouteConfig, routing portRoutes, and IGatewayClientRoute client-owned route views. Gateway route intent supports optional match domains, transport, and remoteIngress, plus explicit route priority and managedRouteKind. Ownership can combine hostname with routeRef so a normal route and a path-specific managed route for the same hostname reconcile independently.
  • Value-free ISecretMetadata, ISecretVersionMetadata, and ISecretSetMetadata contracts for operator views, plus exact-version IResolvedSecretManifest contracts for cluster delivery. Manifest helpers bind immutable image rollout and invocation evidence, enforce canonical digests and globally unique launcher/file targets, and track per-cluster desired/applied/previous-accepted rollout state. Platform-provider and system owners are valid metadata owners but are rejected from workload manifests. listSecrets exposes the dedicated targetSecretsRevision CAS fence; every create, rotate, and lifecycle mutation consumes and returns that aggregate owner fence, while setServiceSecretSetAttachments returns the independent secretConfigurationRevision. Generic service writes own neither revision. Purge is a separate exact-version mutation with its own version revision and durable operation; it is not a logical-secret lifecycle action.
  • Mail gateway contracts for domain authorities, address bindings, WorkApp bindings, managed SMTP/API credentials, spool items, delivery journals, and inbound/outbound message payloads.
  • Service-level mail configuration through IService.data.mail, including per-address inbound smtpForward settings and outbound credential metadata. Cloudly settings include dcrouter gateway, SMTP submission, and inbound forward-target keys for reconciling those bindings.
  • Web Push contracts for environment-specific service bindings, public credential state, public VAPID key rotation metadata, privacy-minimal notification signals, and redacted delivery state. Subscription endpoints, browser key material, provider ciphertext, VAPID private keys, and credential secrets are intentionally absent from public binding and status DTOs.
  • Service-level Web Push declaration through IService.data.webPush. Immutable deployment declarations can require the pushnotification platform capability alongside database and object-storage capabilities; this does not turn Web Push into a Corestore resource or volume capability.
  • IUser, JWT-only IIdentityCredential, full IIdentity, and token-related contracts for authentication context. IIdentity extends IIdentityCredential with server-issued user metadata.
  • ICloudlyConfig, ICloudlySettings, status, server, bare-metal, BaseOS, backup, and task execution interfaces for control-plane state.

Cluster Relay Block

ICluster.data.relay is the optional IClusterRelay { origin: string } that states how cluster-side components reach this cluster's relay. It is absent on a cluster whose nodes still target Cloudly directly, so its presence is what marks a cluster as relayed.

origin is the exact value a Pallet node stores as its relayOrigin — the origin its runtime session registers over, as described in Authenticated Runtime Sessions. It must be an https: origin with no path, query, fragment, userinfo or trailing slash, so that new URL(origin).origin === origin; an explicit port is allowed, and because URL drops a default port and lowercases the host, https://relay.example:443 and https://Relay.example are not canonical origins. validateClusterRelay(value, path?) returns one named reason per violation and an empty array for a valid block.

The relay's certificate subject is not a second field: every consumer derives it from the origin with clusterRelayCertificateDomainName(relay), which returns the origin's hostname — the name Cloudly issues the certificate for and owns the A record for. An origin naming an address literal therefore has no issuable certificate.

data.clusterRelayUndispatchedRefusals names the three registration refusals that end a relay's custody — relay-cluster-not-enabled, relay-node-foreign and relay-sequence-not-advancing — with data.TClusterRelayUndispatchedRefusal as their union. Each is Cloudly reading its own records and stating that it does not dispatch to this relay, so a relay that hears one drops what it holds for its nodes instead of waiting. A refusal that only says Cloudly could not verify the relay states nothing about dispatch and is deliberately not in the list.

const relay: data.IClusterRelay = {
  origin: 'https://relay.cluster-a.serve.zone:8443',
};

const reasons = data.validateClusterRelay(relay); // []
const certificateDomainName = data.clusterRelayCertificateDomainName(relay);
// 'relay.cluster-a.serve.zone'

Enabling a relay is one operator flip per cluster. enableClusterRelay { identity, clusterId } answers with the block Cloudly published, and disableClusterRelay removes it and answers with an explicit relay: null. The caller names only the cluster: Cloudly derives the origin itself from that cluster and its own public hostname, and refuses the request when no DNS zone it manages matches the derived name, so a relay name that nothing can publish is never written. Both requests push the cluster configuration to the connected relay afterwards, so a relay learns the change without reconnecting.

Once its socket is authenticated with registerCloudlyClientSession, the relay sends registerClusterRelay carrying IRegisterClusterRelayRequest: the relay build, the cluster node it runs on, the exact endpoint it bound for its cluster's Pallet nodes, its protocol offer, and a registrationSequence that counts up once per registration within one relay process and restarts at 0 when that process restarts. validateRegisterClusterRelayRequest(value, path?) returns one named reason per violation, like every validator here, and checks shape only; it validates the offer through the one grammar protocol owns. Cloudly reads and negotiates that offer before it takes the verified cluster connection or decides the sequence, so a refused registration consumes no sequence, and the accepted answer carries Cloudly's own offer beside the origin. relay.version stays the relay build — a different fact from protocol.interfacesVersion, the contract it speaks. listenAddress is an IPv4 literal because Cloudly publishes it verbatim as the A record of the relay's certificate domain name — a hostname would need a CNAME and an IPv6 address an AAAA record, so admitting either is an additive change once Cloudly publishes it. A loopback or link-local address is refused by its own name, because a relay that bound one can never serve the nodes of its cluster.

Authority stays with Cloudly, and none of it is a request field. The cluster is the one the verified machine identity on that socket names; whether relay.nodeId is a node of that cluster is decided against Cloudly's own records; and one cluster has one relay. A registration for a cluster whose relay nobody enabled is refused by name, because there is no origin to register into, so an accepted registration always answers with the origin Cloudly published: that is how a relay checks that the certificate it holds and the name its nodes are told to reach are the same one.

registrationSequence restarts with the relay process, so it orders registrations only within one live peer: there it must strictly increase, a refused registration does not consume it (the same body may be re-sent once the cause is gone), and a retry that repeats a sequence Cloudly already accepted is refused. Across peers it says nothing, because a restarted relay counts from zero again; there Cloudly's own acceptance time orders the registrations, and a registration arriving on a live verified socket supersedes the stored one.

An administrator reads the result with getClusterRelayRegistrations { identity, clusterIds? }, answered with IClusterRelayRegistration per cluster: the cluster, the node, the listen address and port, the relay version, the sequence that relay counted, Cloudly's acceptedAt — and live, whether a verified socket carries that relay at this instant. live is a liveness fact and grants nothing: a registration that is perfectly current reads live: false while its socket is gone, and a cluster whose relay never registered is absent from the answer rather than present with an empty one. validateClusterRelayRegistration(value, path?) returns one named reason per violation, as every validator here does.

const registration: data.IRegisterClusterRelayRequest = {
  protocol: protocol.createProtocolOffer('32.0.0'),
  relay: {
    version: '4.0.0',
    nodeId: 'node-a',
    listenAddress: '10.0.4.7',
    listenPort: 8443,
  },
  registrationSequence: 0,
};

const registrationReasons = data.validateRegisterClusterRelayRequest(registration); // []

How the relay reaches each node's Corestore is part of the same document: see Corestore Through The Relay.

Cluster VPN Hub And Network

A cluster's relay is also that cluster's managed VPN hub: every node of the cluster dials it, and an inter-node workload packet is relayed inside the cluster instead of crossing the control plane. Cloudly keeps every authority — it compiles the network and issues each node's credential — and carries no packet.

A relay that bound a hub says so in its registration, in relay.vpnHub: data.IClusterRelayVpnHub { publicKey, address, quicPort }. It reports what it bound, exactly like listenAddress and listenPort: address is an IPv4 literal inside the address plan's vpn.hubPrefix and quicPort is the port it bound for the one transport a managed hub serves. publicKey is the hub's Noise public key in the one canonical 32-byte base64 encoding every Noise key of this contract is read with — this advertisement, the managed VPN credential's keys and a cluster network's nodes all pass the same reader. A relay names no endpoint id of its own: Cloudly composes the endpoints with runtimeNetworkVpnHubEndpoints (see Runtime Network Address Plan). The member is optional — a relay that serves no hub registers exactly as before, and Cloudly selects no hub for that cluster, stating relay-vpn-hub-absent. validateClusterRelayVpnHub(value, path?) returns one named reason per violation.

data.IClusterVpnNetwork { authorityId, controlPrefix, revision, nodes, grants } is the whole managed VPN of one cluster. It is never a patch: a node or a grant the network does not carry is withdrawn, and membership is presence, so there is no enabled flag. controlPrefix is the subnet the cluster's hub serves its control plane on — the address plan's vpn.controlPrefix, read by the same runtimeNetworkVpnControlPrefix reader. A hub fixes that subnet when it binds and no other carrier hands a relay the address plan, so the network it is pushed states it; the plan contract never lets it change, which is why a network that moves the prefix under the same authority is refused as vpn-network-authority-foreign rather than by a name of its own. Each node states { nodeId, publicKey, controlAddress, expiresAt, workloadPrefixes }, where expiresAt is that node's credential expiry in Unix milliseconds and each prefix is { cidr, policyDomain }. Each grant is one directed { sourceDomain, destinationDomain } pair. clusterVpnControlPolicyDomain(address) and clusterVpnWorkloadPolicyDomain(leaseDigest) state the domain names once for both sides.

snapshotClusterVpnNetwork requires sorted, unique members, prefixes and grants, requires every control address to lie inside controlPrefix and every workload prefix to stay clear of it — the two rules the hub enforces against the subnet it bound — and requires every grant to join two workload domains the network itself carries, on two different nodes. That is what a per-cluster hub means: a grant whose other end sits on another cluster's hub cannot be expressed at all, which is why Cloudly refuses it while it compiles, by name, with grant-cross-cluster-unsupported from data.clusterVpnNetworkSelectionRefusals. clusterVpnNetworkContract bounds a network at 256 nodes, 128 workload prefixes per node, 4096 grants and 917 504 canonical bytes, and authorityId at clusterVpnNetworkContract.maximumAuthorityIdBytes — 128 UTF-8 bytes, the bound the hub daemon itself holds for the authority it binds. Bytes rather than code points, because that is what the daemon counts. The same bound is read wherever the id is admitted, snapshotClusterVpnNetwork and the managed VPN credential's authorityId alike, so an id no hub could take over is refused where the network is compiled instead of arriving back as a hub reporting state: 'failed'.

applyClusterVpnNetwork { network } is Cloudly's push over the relay's already registered session; the relay answers { appliedRevision, state }, where state is idle, applying or failed and idle means the pushed revision is the applied one — which is what bindApplyClusterVpnNetworkResponse(response, sentRequest) checks. admitClusterVpnNetwork(held | null, next) is the pure decision the relay makes against what its hub holds: accepted for a higher revision or an empty hub, replay for a repeat, and refused with vpn-network-revision-stale or vpn-network-authority-foreign from data.applyClusterVpnNetworkRefusals. getClusterVpnNetwork is the pull the other way, with no request body — the relay's socket names the cluster — answering { network } or { network: null } while the cluster has none. A hub daemon starts empty and its lifetime is the relay's own, so a relay that just bound one pulls instead of waiting for the next push.

Cluster Ingress

A cluster's ingress terminates TLS for the hostnames its services publish and forwards to the host ports Pallet published for the workloads behind them. Cloudly computes what it serves — it is the only party that knows every service, every ready endpoint, the node address each published port answers on and the certificate each hostname needs — and the cluster relay carries that table to the ingress and the ingress's counters back. These contracts carry no version suffix and no schema field: the protocol version is the installed @serve.zone/interfaces version.

IClusterRouteTable { revision, issuedAt, routes, portRoutes } is the whole table, never a delta. revision is its only idempotency: a relay and an ingress apply a table whose revision is greater than the one they hold and ignore anything else, so a repeated push, a replayed push and a push that crossed a newer one all settle the same way. IClusterHttpRoute names one hostname, its destinations ({ address, port }, unicast IPv4 and a real port), the certificate that terminates it and optional Basic authentication; IClusterPortRoute forwards a TCP or UDP listen port without terminating anything. validateClusterRouteTable(value, path?) returns one named reason per violation, refuses two routes for one hostname and two port routes for one transport and port, and — because a validation reason travels into logs — never quotes certificate material or a password in a reason.

The table is secret-bearing end to end: it carries the private key of every hostname the ingress serves. Exclude it from hooks, logs, journals and disk on every hop, exactly as a runtime session credential is excluded.

Method Direction Body
pushClusterRouteTable Cloudly → relay { table }{ appliedRevision }
getClusterRouteTable relay → Cloudly {}{ table | null }
registerClusterIngress ingress → relay → Cloudly { bearer, version, nodeId, capabilities?, protocol }{ accepted: true, revision | null, protocol }
getClusterIngressRouteTable ingress → relay { knownRevision | null }{ table | null }
getClusterTrafficStatistics Cloudly → relay → ingress { fromDayUtc, toDayUtc, cursor?, limit }{ statistics, nextCursor? }

null for a table means Cloudly has computed none yet — a fact, not an empty table, and an ingress must not serve it as one. An ingress announces itself with a method, not a connection tag: the relay learns what connected from what it registered through, which a server owns, rather than from a label a client authored.

Who may be the ingress is Cloudly's decision, not the relay's. The ingress shares the relay's node listener, so anything that reaches it could ask to register, and a relay cannot tell an ingress from anything else. registerClusterIngress therefore carries the bearer Cloudly minted into the ingress workload's assignment environment and travels both hops with one body: the ingress sends it to its relay, the relay forwards it untouched, and Cloudly verifies the bearer and derives the cluster from the identity tag on the relay's own session. The bearer is sensitive ephemeral input — excluded from hooks, logs, diagnostics and journals on every hop, and never quoted in a validation reason.

Isolation on that shared listener is therefore by authorization, not by routing: only the connection whose registration Cloudly accepted is the ingress, getClusterIngressRouteTable and getClusterTrafficStatistics are answered for that connection alone, and any other connection — including a Pallet node's — is refused by name. validateClusterIngressRegistration(value, path?) checks shape only: whether the bearer is the one Cloudly minted is Cloudly's to verify. It reads the registration's protocol offer through the one grammar protocol owns, and both hops judge the body in the one server order protocol.readProtocolOffer states — the offer is negotiated before this shape check and before the bearer — so a relay refuses a peer it cannot serve without asking Cloudly, and an accepted registration carries the accepting side's own offer back.

IClusterTrafficStatistics is one UTC day of one hostname's traffic on one node — requests, bytesIn, bytesOut, connections and the backend failures the proxy can observe (connect, handshake, request). Day buckets rather than rates: a rate answers "how busy is it now", which the live proxy already answers, while a bucket survives a restart, a poll that straddles midnight and a Cloudly outage. HTTP status classes are deliberately absent — the proxy exposes no status counters today, and a number this contract cannot source would be a guess. Readers page it exactly like CoreMail's mail statistics — the same fromDayUtc, toDayUtc, cursor and limit, answered as { statistics, nextCursor? } — re-reading yesterday and today on every pass because a day only stops changing once it has closed. dayUtc must name a day that exists: the shared UTC calendar-day rule round-trips through Date.UTC, so 2026-13-45 is refused rather than stored.

const reasons = data.validateClusterRouteTable(table); // []
const pageReasons = data.validateClusterTrafficStatisticsPage({ statistics: [] }); // []
const ingressReasons = data.validateClusterIngressRegistration({
  bearer: ingressBearerFromAssignmentEnvironment,
  version: '3.0.0',
  nodeId: 'node-a',
  protocol: protocol.createProtocolOffer('32.0.0'),
}); // []

A route to a node's published host ports needs the address that node answers on, and only the node can prove it: it holds the uplink observation the address belongs to. It therefore states it on the network report it already sends — IReportRuntimeNetworkProtectionRequest gains the optional uplinkAddress, a unicast IPv4 literal that is neither loopback nor link-local (runtimeNodeUplinkAddress). Absence stays absent: a node that states no address is told apart from one that states a value, and nothing downstream defaults or infers one. Cloudly stores what the node stated and resolves it into the route table; the relay never substitutes an address of its own.

Shared service port helpers are exported from data so Cloudly, App Store resolution, Coreflow, Coretraffic, Onebox, and dcrouter agree on the same normalization rules:

const normalizedPorts = data.normalizeServicePortConfig(service.data);
const defaultTarget = data.resolveDefaultServiceTargetPort(normalizedPorts.targetPorts);
const webTarget = data.resolveServiceTargetPort(normalizedPorts.targetPorts, 'web');

if (webTarget && data.isHttpServiceTargetProtocol(webTarget.protocol)) {
  // safe to use as a domain route target
}

normalizeServicePortData() returns service data with canonical targetPorts, domain targetPort refs, and publicPortMappings while removing legacy domain port fields from normalized writes.

Hosted-App Authorization and Platform OIDC

An App Store version can declare that it supports platform-managed OpenID Connect. This is a capability declaration only: Onebox and Cloudly keep OIDC disabled until an administrator explicitly enables it for that exact app instance, and they can disable it again without changing the template.

import { appstore } from '@serve.zone/interfaces';

const config: appstore.IAppStoreVersionConfig = {
  image: 'registry.example.com/example/app:1.0.0',
  platformOidc: {
    redirectPath: '/auth/oidc/callback',
    roles: [
      { id: 'admin', label: 'Administrator' },
      { id: 'user', label: 'User' },
    ],
    environmentVariables: {
      issuerUrl: 'SERVEZONE_PLATFORM_OIDC_ISSUER',
      clientId: 'SERVEZONE_PLATFORM_OIDC_CLIENT_ID',
      clientSecret: 'SERVEZONE_PLATFORM_OIDC_CLIENT_SECRET',
      redirectUri: 'SERVEZONE_PLATFORM_OIDC_REDIRECT_URI',
      audience: 'SERVEZONE_PLATFORM_OIDC_AUDIENCE',
    },
    clientAuthenticationMethod: 'client_secret_basic',
  },
};

redirectPath is a canonical callback path on the app's HTTPS origin. Registration validation also requires that canonical app origin explicitly and rejects cross-origin, normalized, query-bearing, fragment-bearing, or duplicate callback URLs. The five environment values are environment-key names, not credentials embedded in the manifest. A host injects the generated client secret through launcher environment delivery and injects the other registration values only while OIDC is enabled.

data.IHostedAppRoleAssignment binds a stable user subject to an immutable appInstanceId. That same app instance is the OIDC client_id, the ID-token aud, and the servezone_app_instance_id claim. Tokens include only the assigned roles for that audience; preferred_username is display metadata and must not be treated as identity authority.

requests.hostedapp exports the shared authorization RPCs used by a host dashboard. getHostedAppAccessConfiguration returns human user summaries, role assignments, and hosted-app summaries. setHostedAppRoleAssignment returns the assignment or null when it is removed, while setHostedAppPlatformOidc returns the current registration state. getHostedAppOidcAuthorization returns the app and role summary for a pending request; completeHostedAppOidcAuthorization and cancelHostedAppOidcAuthorization return the redirect URL. All six requests require a full data.IIdentity.

Validate untrusted manifests, registrations, and claims with:

  • data.validateHostedAppRoleDefinitions
  • data.validateHostedAppPlatformOidcRegistration
  • data.validateHostedAppPlatformOidcClaims
  • appstore.validateAppStorePlatformOidcCapability
  • appstore.validateAppStoreVersionPlatformOidc

Hosted lifecycle RPCs accept only IIdentityCredential. After JWT verification, handlers validate IHostedAppMachineClaims and derive the exact app instance and service from servezone_app_instance_id and servezone_service_id; callers cannot submit those selectors. Bootstrap actions are either a canonical same-origin setupRoute path or a nonsecret message. Control tokens, usernames, passwords, and token URLs are not part of the lifecycle contract. Each server action carries a CAS revision; completion requires its exact ID, current revision, and ready status so a delayed request cannot complete a replacement action.

TypedRequest Contracts

Use requests when registering handlers with @api.global/typedrequest or when creating typed requests through a TypedSocket client.

import { requests } from '@serve.zone/interfaces';

type GetClustersRequest = requests.cluster.IReq_Any_Cloudly_GetClusters;

const methodName: GetClustersRequest['method'] = 'getClusters';

Each request interface follows the same pattern:

interface IExampleRequest {
  method: 'methodName';
  request: Record<string, unknown>;
  response: Record<string, unknown>;
}

requests.config.IRequest_Any_Cloudly_GetClusterConfig accepts data.IIdentityCredential, which contains only the JWT needed for server-side identity resolution. Username/password login and machine-token exchange responses continue to return the full data.IIdentity.

Immutable Deployment Contracts

Cloudly deployment authority is expressed as an exact data.IServiceDeploymentGrant. A service grant applies to one owned existing service. An organization-service-slot grant reserves authority for one exact future service ID in an organization; it is not an organization-wide wildcard. configureServiceDeploymentMachineUser accepts this discriminated grant object instead of separate service and capability fields.

The immutable deployment workflow is:

  1. Reserve the exact service, namespace, registry repository/tag, and route intent with reserveServiceDeployment.
  2. Push the OCI index to the returned exact tag with the authenticated deployer identity.
  3. Promote the authenticated release evidence with promoteServiceImageRelease.
  4. Observe exact rollout and runtime-digest evidence with getServiceDeploymentStatus.
  5. For a greenfield service, expose and verify its public route with promoteServiceDeploymentRoute only after the immutable rollout succeeds.

data.IDeploymentRouteRequest.proxied carries provider-specific DNS proxy intent as an optional boolean. New route declarations should set it explicitly; omission remains valid for persisted historical operations and legacy callers.

data.IServiceDeploymentOperation is the durable revisioned compare-and-set fence for this workflow. Image promotion requires Cloudly-created trusted evidence that binds the operation, actor, repository, exact tag, root digest, and OCI index media type. The service request group also exposes deployment preflight, exact-digest rollback, retry, and cleanup contracts.

Two of these carriers accept an optional protocol offer (protocol.IProtocolOffer, see Protocol handshake): getDeploymentPreflight, through data.IDeploymentPreflightRequestData.protocol, and getServiceById. Those are the read-only requests that open a deployment and an adoption, and they are the only deployment carriers that take one. The mutating carriers stay byte-exact deliberately: Cloudly derives reserveServiceDeployment's requestDigest from the whole request body, so an offer inside one of them would bind a durable operation's identity to whichever @serve.zone/interfaces release the deployer had installed, and an in-flight deployment would stop being resumable across a deployer upgrade. Stating the offer on the opening read names developer-machine skew before anything is reserved and leaves nothing durable behind.

The member is optional — a peer of this release states nothing by offering and an older deployer sends none, so absence is a valid request, not a refusal. A stated offer is exact-key like every other offer in this package: validateDeploymentPreflightRequest judges it with protocol.validateProtocolOfferOn, the naming twin of the protocol.readProtocolOffer a server negotiates from, and answers a malformed or accessor-backed offer with a single INVALID_REQUEST blocker naming the offer and its reasons. Whether two peers can serve one session is never a preflight blocker: that is protocol.negotiateProtocol's decision, answered as an IProtocolRefusal.

Gateway request contracts include getGatewayClientRoutes (requests.gateway.IReq_GetGatewayClientRoutes) for listing owned IGatewayClientRoute[] route views, and syncGatewayClientRoute for idempotently syncing or deleting hostname-owned, routeRef-owned, and combined hostname-plus-routeRef routes. A client can label canonical intent with managedRouteKind: 'letsencrypt-http01-forward' and set a higher priority for a path-specific HTTP-01 route while retaining a separate normal route for the same hostname. Mail request contracts include syncMailAddressBinding, deleteMailAddressBinding, rotateMailCredential, and getMailDeliveryStatus. IReq_GetMailDeliveryStatus looks up a delivery spool item by spoolItemId, returns data.IMailDeliveryStatus, and accepts IMailSubmissionRequestAuth so service-mail credentials can query their own accepted, queued, deferred, delivered, or failed status. Typed outbound messages may set replyTo to one bare ASCII mailbox address; arbitrary Reply-To values do not belong in the custom header bag. Invalid values and typed-field/custom-header conflicts return stable TMailSubmissionErrorCode values. TMailAddressBindingSync.outboundEnabled explicitly controls whether a gateway should maintain a managed outbound SMTP credential for an address binding. Binding credential metadata is public; rotateMailCredential returns the new secret only in its one-time IMailCredentialOneTimeSecret response.

Web Push Contracts

New Web Push integrations use requests.webpush. Control-plane methods and application delivery methods deliberately use different, non-overlapping authentication types:

  • listWebPushBindings, syncWebPushBinding, deleteWebPushBinding, rotateWebPushCredential, and rotateWebPushVapidKey use control-plane identity or gateway API-token authentication.
  • getWebPushServiceStatus, enqueueWebPush, cancelWebPush, and getWebPushDeliveryStatus require a Web Push application credential. The gateway derives the owner exclusively from that credential; application requests cannot submit owner identity.

syncWebPushBinding may return the initial application credential secret once, and rotateWebPushCredential may return its replacement once. Binding and status DTOs contain only public credential and VAPID metadata. enqueueWebPush requires a credential-scoped idempotency key, an opaque application subscription ID, the browser Push API subscription, the VAPID key ID used for that browser subscription, and a privacy-minimal notificationAvailable signal.

import { requests } from '@serve.zone/interfaces';

type EnqueueWebPush = requests.webpush.IReq_EnqueueWebPush;
type WebPushStatus = requests.webpush.IReq_GetWebPushDeliveryStatus;

A delivery state of pushServiceAccepted means only that the remote push service accepted the encrypted request. It does not prove browser receipt, notification display, or user interaction.

Gateway Client Lifecycle and DNS

syncGatewayClientRoute accepts an optional dnsMode. Omission means skip for older clients. observe reports DNS without changing it. reconcile makes the gateway authoritative for the exact route hostname: it claims or replaces manual A, AAAA, and CNAME records, including already-correct manual values. Its optional dns result contains a closed status, retryability, the desired A/AAAA target, overwritten-record evidence, checkedAt, and authoritativeVerifiedAt once the provider or authoritative server confirms the state. Consumers can carry that evidence while retrying public propagation instead of treating an immediate recursive lookup miss as permanent.

Use requests.gateway.IReq_ProvisionGatewayClientCredential to replace an admin/bootstrap token with a first-class gateway-client credential. The admin-authenticated request idempotently upserts a data.IGatewayClient, durably creates a new bound credential, returns its raw value once, and then revokes older credentials bound to that client. It never revokes the bootstrap/admin credential. A successful response is discriminated with success: true and always includes the action, durable client, one-time credential, and revocation count.

import { requests } from '@serve.zone/interfaces';

const provisioning: requests.gateway.IReq_ProvisionGatewayClientCredential['request'] = {
  apiToken: 'admin-bootstrap-token',
  provisioning: {
    id: 'cloudly-main',
    type: 'cloudly',
    name: 'Cloudly main',
    hostnamePatterns: ['*'],
    allowedRouteTargets: [
      {
        host: 'coretraffic.internal',
        ports: [],
        allowAnyPort: true,
      },
    ],
    capabilities: {
      readDomains: true,
      readDnsRecords: true,
      readRoutes: true,
      syncRoutes: true,
      syncDnsRecords: true,
      readMail: true,
      manageMail: true,
      readCertificates: true,
      requestCertificates: true,
    },
  },
};

getGatewayClientContext returns effective live policy. A gatewayClient role necessarily includes the credential ID, bound client ID/type, and policyGeneration; consumers should reject admin/operator or mismatched contexts rather than falling back to a caller-supplied owner ID. getGatewayClientMailOverview provides an owner-scoped domain and recent-message summary. getGatewayClientMailDomainCount derives ownership exclusively from the authenticating gateway credential and returns { count: number } for its distinct configured mail domains. Cloudly therefore uses only dcrouterGatewayApiToken; the former dcrouterOpsApiToken setting is not part of data.ICloudlySettings.

CoreMail contracts

requests.coremail is the shared contract boundary for authenticated workload sessions, Coreflow reconciliation, and the CoreMail-to-dcrouter gateway session. Only the three authentication handshakes carry reusable peer credentials. Subsequent transfer operations may carry a scoped, short-lived, one-time bearer capability, while every subsequent workload request derives tenant, service, binding, capabilities, allowed senders, and the composite credential ID/version identity from the server-owned TypedSocket peer. Successful workload authentication also returns the effective binding state and the exact allowedOperations derived from data.coreMailWorkloadOperationPolicy. Disabled bindings never authenticate; draining bindings permit outbound status plus inbound list/fetch/ack only.

Gateway recipient resolution uses four strict outcomes. accept, defer, and reject are authoritative only for recipients owned by an active binding. unhandled means the CoreMail peer does not own that recipient, so the gateway may continue its next configured resolver. Consumers must apply data.normalizeCoreMailRecipientResolutions() against the exact requested recipient set before acting on a peer response.

CoreMail binds every issued routing handle to a digest of the exact normalized envelope and source sent at resolve time. A following coreMailGatewayPrepareInboundHandoff must resend byte-identical values, including the complete rcptTo set in the same order. Both peers use data.normalizeCoreMailGatewayMessage and data.normalizeCoreMailConnectionInfo so the digested bytes come from one shared implementation. Outbound status is monotonic per transportMessageId, and a permanently unknown transportMessageId is answered with a terminal failed status rather than an error, because CoreMail retries errors indefinitely.

Large content never travels inside TypedRequest JSON. Outbound body parts and attachments use prepare/upload/complete operations with short-lived one-time HTTP transfer grants. Inbound delivery uses bounded delivery listing followed by prepare/fetch/complete and an explicit acknowledgement after the workload has processed the exact byte count and SHA-256 digest. data.coreMailLimits defines the 64 KiB control-frame boundary, bounded structured content, the 30 MiB serialized MIME ceiling, a 56 KiB inbound page budget, bounded opaque cursors, transfer deadlines, and five-minute grant lifetime.

Coreflow applies data.ICoreMailDesiredState with a config-epoch compare-and-set fence. It stages the digest-fenced JSON snapshot through a bounded one-time HTTP upload, then applies it by reconciliation ID, so a large binding set never bypasses the 64 KiB control-frame boundary. Desired bindings contain password verifiers and secret references only, never plaintext workload or dcrouter credentials. Use data.normalizeCoreMailDesiredState, data.canonicalizeCoreMailDesiredState, and data.createCoreMailDesiredStateDigest at every producer and consumer boundary. Use data.verifyCoreMailDesiredStateDigest before applying a staged snapshot, data.normalizeCoreMailCredentialVerifier before accepting verifier metadata, data.normalizeCoreMailControlBootstrap for startup authority, and data.normalizeCoreMailGatewayPeerDesiredState for dcrouter peer state. The normalizers reject unknown fields, noncanonical mailboxes, ambiguous active recipient ownership, malformed SHA-256 values, and credential-lifecycle inconsistencies. CoreMail credential verifiers use the argon2id format and the exact policy exported as data.coreMailCredentialVerifierPolicy. The PHC parameter block is read by name, so any encoder's parameter order is accepted as long as m, t and p each appear once at the policy cost; the normalized verificationHash is always re-emitted as $argon2id$v=19$m=…,t=…,p=…$<salt>$<hash>, the canonical order the desired-state digest is computed over.

data.ICoreMailDesiredState.smtp is the optional CoreMail SMTP submission listener (MSA). It carries the listener port, the advertised EHLO hostname, and the runtime secret keys holding certificate and private-key PEM text; plaintext PEM material never appears in desired state. SMTP AUTH uses the binding identity directly: the username is the bindingId and the password is any current or retiring credential secret of that binding. The listener stays unavailable until the referenced PEM material resolves. Omitting smtp keeps the exact canonical bytes and digest of an API-only CoreMail. Accepted submissions report their entry point through the optional ICoreMailSubmission.source, either api or smtp.

data.ICoreMailServiceMailStatistics reports per-service outbound and inbound counters for one UTC calendar day. Control sessions read them through coreMailGetServiceMailStatistics using an inclusive day range, an optional service filter, and an opaque cursor. Apply data.normalizeCoreMailServiceMailStatistics before using reported counters.

data.ICoreMailControlBootstrap is installed before ordinary reconciliation. It contains the CoreMail service identity and verifier metadata only. The matching plaintext control credential is delivered exclusively to Coreflow through resolved runtime secrets. data.coreMailRuntimeKeys publishes the canonical environment keys for the verifier-only bootstrap payload and the separate control and gateway secret values; no consumer may derive or embed plaintext material in desired state.

The CoreMail contracts keep durable authority at the stable tenant, service, and binding identity while revisions, config epochs, and composite credentialId/version values fence sessions and new actions. Credential versions are authority-wide monotonic and unique even when rotation changes the credential ID. active bindings accept new mail, draining bindings permit existing status and inbound fetch/ack work without accepting new mail, and disabled bindings reject authentication. Cloudly retains a draining binding until pending inbound delivery reaches zero as reported by ICoreMailBindingReconciliationStatus.pendingInboundCount.

data.ICoreMailGatewayPeerDesiredState gives dcrouter the authoritative HTTPS CoreMail transfer origin associated with an authenticated CoreMail service. The same origin is carried in CoreMail desired state and returned by workload and gateway authentication. It must never be inferred from a socket, Host header, TypedSocket tag, or unrestricted peer input. CoreMail compares both authoritative views before handing over a path-only transfer grant. Cloudly and Onebox provision that peer state through listCoreMailGatewayPeers, syncCoreMailGatewayPeer, and deleteCoreMailGatewayPeer on the gateway-client mail surface, and the producer is the single owner of peer.transferOrigin. A mail inbound target may point at a CoreMail service using type: 'coreMail' with coreMail.coreMailServiceId, mirrored by the coreMail member of IServiceMailInboundConfig.targetType.

Inbound delivery pagination uses an opaque CoreMail-owned cursor, is bounded by data.coreMailLimits.inboundPageSize, and returns nextCursor only when another page may exist. Consumers must not construct or parse cursor contents. Cursor signing keys are value-free runtime references with one current and bounded retiring versions; plaintext remains in resolved runtime secrets.

Quota windows are fixed UTC minute/day buckets. A new outbound quota unit is consumed only by the first durable insertion of an idempotency identity, while replays consume none. Pending inbound includes every state except acknowledged. These semantics are exported as data.coreMailQuotaPolicy. Every binding carries finite messagesPerMinute, messagesPerDay, and maxPendingInbound values; omitted or unlimited quotas are not valid desired state. data.coreMailRetentionPolicy retains terminal outbound, acknowledged inbound, and idempotency receipts for 30 days and expired capabilities for 24 hours. Pending inbound is never age-purged.

HTTP transfers use canonical /transfers/<uuid> paths and one-time Bearer tokens. data.coreMailTransferTokenPolicy requires canonical 256-bit base64url token material, while issuedAt and expiresAt prove the exact five-minute lifetime. PUT succeeds with 204 and GET with 200. Content length and type must match the grant; digest integrity is bound by grant metadata and repeated in the completion RPC rather than an optional HTTP digest header. Apply the exported strict normalizers for outbound message descriptors, envelopes, method-specific upload/download grants, submissions, gateway outbound statuses, inbound deliveries, desired state, bootstrap state, and gateway-peer state at their corresponding untrusted request and response boundaries. Normalizer failures throw data.CoreMailContractError; its readonly code defaults to INVALID_REQUEST, while an otherwise valid outbound part that exceeds its kind-specific byte budget reports PAYLOAD_LIMIT_EXCEEDED.

Control and gateway credential rotation is ordered: provision the candidate plaintext through resolved runtime secrets, publish and activate the matching argon2id verifier, roll or reconnect every affected replica, observe per-task authentication and readiness, then mark the previous verifier retiring with acceptUntil. Remove the previous verifier and secret only after its acceptance window has elapsed and no session uses that composite credential identity. Reconciliation status carries the exact CoreMail task, rollout generation, and image digest so Coreflow can correlate every response with its authoritative current task roster. Apply data.normalizeCoreMailReconciliationStatus before using pending-inbound or composite active-session counts for drain and rotation decisions.

CoreMail control-plane settings and operator RPCs

data.ICloudlySettings carries the CoreMail control plane: coreMailEnabled, coreMailServiceId, the canonical coreMailControlEndpointUrl (https://host/socket), the path-free coreMailTransferOrigin, dcrouter's coreMailGatewayEndpointUrl (wss://…), the composite coreMailControlCredentialId/coreMailControlCredentialVersion, the submission listener's coreMailSmtpHost, coreMailSmtpPort and coreMailSmtpTlsMode, and the coreMailDefault* binding quota defaults. No credential or PEM value is part of settings: data.cloudlySystemSecretKeys publishes coreMailControlCredentialSecret, coreMailGatewayCredentialSecret, coreMailSmtpTlsCertificatePem, and coreMailSmtpTlsPrivateKeyPem as the stable keys of the removed values. The submission listener's PEM material is operator-supplied in v1; automatic issuance is a later workstream.

Secrets that Cloudly manages for CoreMail use the coremail management source and a coremail:<identifier> management scope, validated by the existing data.validateSecretManagementScope and secret-metadata validators.

IServiceMailConfig.coreMail publishes the service's CoreMail binding as public metadata only: binding and composite credential identity, the argon2id verification hash, the binding state, and any retiring predecessor with its acceptUntil. Plaintext never appears there, and the field is excluded from data.TServiceWritableData, so only the control plane writes it while the rest of mail stays caller-writable. IServiceMailConfig.allowedSenders makes the outbound sender allowlist explicit instead of inferring it from per-address outbound.enabled.

requests.coremailAdmin carries the identity-authenticated operator surface: getCoreMailServiceMailStatistics for per-service daily counters, getCoreMailControlStatus for the applied config epoch, desired-state digest and last reconciliation status, and listServiceMailTargets for the mail target inventory (data.IServiceMailTargetSummary with data.IServiceMailTargetAddressSummary rows).

Request groups are exported by product area:

  • requests.admin
  • requests.appstore
  • requests.baremetal
  • requests.baseos
  • requests.backup
  • requests.certificate
  • requests.cluster
  • requests.config
  • requests.coremail
  • requests.coremailAdmin
  • requests.corestore
  • requests.deployment
  • requests.dns
  • requests.domain
  • requests.externalRegistry
  • requests.gateway
  • requests.hostedapp
  • requests.identity
  • requests.image
  • requests.inform
  • requests.log
  • requests.mail
  • requests.migration
  • requests.network
  • requests.node
  • requests.platform
  • requests.routing
  • requests.secret
  • requests.service
  • requests.settings
  • requests.status
  • requests.task
  • requests.version
  • requests.webpush

Secret material response contracts are not exported from the universal browser-facing entrypoint.

The root entrypoint does use @push.rocks/smartcrypto to parse and validate strict ingress envelopes. Browser consumers that import the root contract can therefore include SmartCrypto and its browser-compatible crypto dependencies in their bundle. This package never opens private keys; sealed runtime material contracts remain isolated to the Node-only /runtime subpath.

Node runtimes import the isolated subpath:

import {
  verifySealedResolvedSecretMaterial,
} from '@serve.zone/interfaces/runtime';
import type {
  IReq_GetResolvedSecretMaterial,
  ISecretMaterialExpectation,
  ISealedResolvedSecretMaterial,
} from '@serve.zone/interfaces/runtime';

async function acceptMaterial(
  material: ISealedResolvedSecretMaterial,
  expectation: ISecretMaterialExpectation,
) {
  if (!await verifySealedResolvedSecretMaterial(material, expectation)) {
    throw new Error('secret material does not match its immutable manifest');
  }
}

Runtime material is shaped as { manifest, entries }, where each entry contains only secretVersionId and a strict X25519 envelope. The helper verifies the canonical manifest digest, envelope context digests, active recipient key, sorted exact one-to-one version coverage, every request fence, and the trusted local organization/cluster expectation. It never decrypts. Organization and cluster are derived from the verified cluster JWT and cannot be selected by request fields.

Corestore Runtime Credentials

The Node-only /runtime export defines getCorestoreControlCredentialMaterial for recipient-bound sealed retrieval and publishCorestoreCredentialMaterial for ingress-sealed database or object storage credential publication. isCorestoreControlToken() validates the canonical token boundary, while createCorestoreControlCredentialPlaintextBytes() emits its exact JSON plaintext directly from validated UTF-8 bytes without first constructing a JavaScript token string. Publication grants carry the service, binding, reconciliation generation, binding-request digest, target Secrets revision, and bounded validity window. TCorestoreCredentialBindingRequest with validateCorestoreCredentialBindingRequest(), createCorestoreCredentialBindingRequestDigestInput(), and computeCorestoreCredentialBindingRequestSha256() produces that digest from the exact value-free provider, scope, environment, bucket, and optional retention request. Every object-storage request binds the exact durable bucketName. Object-storage plaintext keeps the logical accessKeyId and secretAccessKey fields while createCorestoreCredentialSecretValues() expands them to the six canonical Corestore S3_* and AWS_* Secret aliases. Receipts bind that exact key coverage, created SecretVersion references, the admitted envelope/context digest, and the trusted organization and cluster scope. Cluster configuration DTOs add optional sorted corestoreCredentialPublicationGrants; existing consumers may omit them. When validateCorestoreObjectStorageCredentialMaterial() receives a trusted retention expectation, matching retention evidence is mandatory; missing or different evidence fails validation.

The runtime export also defines exact Corestore database backup receipts, allocation references, restore requests, and restore responses. Use normalizeCorestoreDatabaseAllocationReference(), encodeCorestoreDatabaseAllocationReference(), and computeCorestoreDatabaseAllocationReferenceSha256() for the allocation boundary; the receipt, restore-request, and restore-response APIs follow the same exact normalize, encode, and SHA-256 naming. They enforce bounded canonical JSON without performing a backup or restore. A restore request may carry expectedDatabaseAllocation so the restore implementation can fence the exact scratch allocation, and may carry the caller-owned restoreAttemptId to select a new fenced idempotency attempt. The attempt ID uses the database-backup identifier grammar and 256-character maximum. Omitting the snapshot databaseAllocation field together with request-level expectedDatabaseAllocation and restoreAttemptId preserves legacy request bytes and digests exactly. corestoreDatabaseBackupRuntimeLimits publishes the distinct 96 MiB snapshot payload, one-byte-larger framed snapshot-original, 128 MiB verified closure plaintext, and 256 MiB closure boundaries used by Corestore.

Corestore Through The Relay

A serve.zone cluster is outbound-only and Corestore is node-local, so Cloudly never reaches a Corestore itself: every Cloudly-owned operation against one travels through the cluster relay, and the relay is told which node it means. These contracts carry no version suffix and no schema field: the protocol version is the installed @serve.zone/interfaces version.

The node is a field, never a connection. One relay serves every node of its cluster, so the connection that answers proves the cluster and nothing more. backupClusterService, restoreClusterService, pruneClusterNodeArchive and getClusterCorestoreInventory each name nodeId, and they supersede executeServiceBackup, executeServiceRestore, coreflowPruneNodeArchive and coreflowGetCorestoreInventory, which were dispatched at whichever coreflow had tagged itself with a node hostname. The superseded four stay declared until the coordinated major so a mixed fleet keeps compiling; nothing new should use them.

IClusterCorestoreInventory { nodeId, checkedAt, reachable, errorCode?, services } is what one node's Corestore holds, and validateClusterCorestoreInventory(value, path?) returns one named reason per violation. A reachable inventory with an empty services list is the proof that a namespace is blank — the answer a greenfield cluster gives. An unreachable node proves nothing, so reachable: false is refused without an errorCode and refused with any service, and a reachable answer is refused with one: the two states can never be read as each other.

Method Direction Body
backupClusterService Cloudly → relay { nodeId, backupId, service, tags?, replication? }{ snapshots, replication? }
restoreClusterService Cloudly → relay { nodeId, backupId, service, snapshots, clear?, resourceTypes?, replication? }{ restored }
pruneClusterNodeArchive Cloudly → relay { nodeId, retention, dryRun? }{ found, result? }
getClusterCorestoreInventory Cloudly → relay { nodeId }{ inventory }
getClusterPlatformDesiredState relay → Cloudly {}{ capabilities, providerConfigs, bindings }
backupServiceDatabaseClosure relay → Cloudly { backupId, serviceId, nodeId, descriptor, closure }{ accepted }
restoreServiceDatabaseClosure relay → Cloudly { backupId, serviceId, nodeId }{ descriptor, closure }
prepareIsolatedRestoreOnNode Cloudly → relay { nodeId, control }{ progress }
stageIsolatedRestoreArchive Cloudly → relay { nodeId, control }{ progress }
getIsolatedRestoreProgressOnNode Cloudly → relay { nodeId, control }{ progress }
executeIsolatedRestoreOnNode Cloudly → relay { nodeId, control }{ progress }
cleanupIsolatedRestoreOnNode Cloudly → relay { nodeId, control }{ progress }

Placement is a field too. getClusterPlatformDesiredState answers with IClusterPlatformBindingPlacement { nodeId, binding } rather than a bare binding list: a database binding without a node is a database nobody can place once one relay serves the whole cluster. Cloudly derives the placement from where the service is assigned; the relay never guesses it.

Closures stream; they are never base64. A database backup larger than the portable handoff envelope moves as a Corestore closure, and the bytes travel as a directional VirtualStream on the relay's own session, exactly like pushImageVersion and pullImageVersion. Neither method carries an identity: the cluster is the one the verified machine identity on that session names, and a request-level identity would be a second, weaker authority for the same fact. The relay is the requesting peer in both directions, because only outbound requests exist: backupServiceDatabaseClosure sends (TVirtualStream<'send'> in its request) and restoreServiceDatabaseClosure receives (TVirtualStream<'receive'> in its response). accepted permits sending and never confirms storage; the stream's acceptance receipt does. The base64 uploadBackupArchiveObject and downloadBackupArchiveObject contracts are retiring with this path — Corestore already documents its repository-wide archive object routes as legacy — and are removed in the coordinated major.

ICorestoreDatabaseClosureDescriptor { size, sha256, receipt } from the Node-only /runtime export travels in the parent request, and validateCorestoreDatabaseClosureDescriptor(value, path?) refuses it by name. size is mandatory, and it is why the descriptor exists: Corestore's database backup restore requires one exact Content-Length, identity transfer encoding and no content encoding, and refuses a body whose byte length differs from its receipt, so the receiver must open its Corestore request before the first byte arrives. A stream that only learned its length at EOF could never be restored. size and sha256 are cross-checked against receipt.closure.archiveBytes and receipt.closure.sha256, so a descriptor can never describe a closure its own receipt contradicts.

Isolated restore carries its grant. Corestore accepts exactly one authority on /isolated-restore/*: a compact RS256 grant that names one operation, expires within minutes and binds the cluster, the node name, the canonical resource mappings and the archive manifest. The five methods above are therefore one shape — { nodeId, control }{ progress } — where control is the body Corestore itself reads (data.IIsolatedRestoreControlPrepareRequest, …WriteRequest, …StatusRequest, …ExecuteRequest, …CleanupRequest) with the grant inside it. The relay forwards the control untouched and can neither widen it nor mint one; it adds no identity and no protocol offer either, because the cluster is the one the verified machine identity on the session registerCloudlyClientSession opened names, and the protocol offer was exchanged by that handshake and again by registerClusterRelay's own protocol member. Every one of them is sensitive ephemeral input: the grant is a bearer credential, declared once on data.IIsolatedRestoreControlAuthority, so exclude the complete request from hooks, logs, diagnostics and durable journals on every hop.

The order an operator's restore runs in. Cloudly calls createIsolatedRestore with the node it dispatches to, then, on that node: prepareIsolatedRestoreOnNode with expectedRevision: 0, stageIsolatedRestoreArchive once per bounded chunk of every manifest object, executeIsolatedRestoreOnNode, and cleanupIsolatedRestoreOnNode when the rehearsal is over; getIsolatedRestoreProgressOnNode reads the same state without changing it. Staging is bounded by data.isolatedRestoreContractLimits: at most 512 KiB decoded per chunk, 4,096 chunks per object and 64 MiB per object, each chunk authenticated by its own chunkSha256 against the immutable object descriptor. Every mutation carries the revision the node last stated as its expectedRevision, so a replay settles instead of racing.

The node id routes, the node name is authorized. createIsolatedRestore takes targetNodeId — the cluster document's node id, which is what a relay routes on — while the signed grant keeps targetNodeName, which is what the operator authorized and what Corestore verifies. Cloudly is the only party holding both, so IIsolatedRestoreRecord states both and every control call is dispatched by the id. data.bindIsolatedRestoreProgressToNode(progress, { nodeId, restoreId }) is where an answer is tied back: the control body names neither the node nor the restore, so without it a relay could answer with another node's — or another restore's — perfectly canonical progress.

What a node can state, in Corestore's own words. data.IIsolatedRestoreProgress is Corestore's staging summary plus the node the control was carried to: { nodeId, restoreId, stagingArchiveId, status, revision, lastFence, expectedObjects, receivedObjects, completedMappings, totalMappings, preparedAt, updatedAt, executedAt?, stagedObject? }, with status one of data.isolatedRestoreProgressStatusesprepared, executing, failed, executed, cleaned. stagedObject is present only on a staged chunk and says where the next chunk continues; it is named apart from the object receipt Corestore's own write answer carries, which is a different shape and is not projected. data.normalizeIsolatedRestoreProgress(value) reads it exactly, refusing an unknown status, a count larger than the plan it belongs to, a revision or lastFence below 1 — a committed state carries both — an executing or executed answer that does not hold the whole archive, and an executed answer that left a mapping behind or does not say when it finished. A node states nothing of the control plane's record — no source backup, no requester, no verification — so Cloudly composes its record from this and its own knowledge, exactly as it does with the Corestore inventory. A relay that cannot reach the node answers one of data.isolatedRestoreNodeControlRefusals instead: restore-node-not-carried, restore-node-unreachable, or restore-node-refused followed by Corestore's own reason.

IIsolatedRestoreDatabaseMapping.target.databaseAllocation carries Corestore's safe allocation reference, present exactly when the target database is allocation-managed. It lives inside the mapping rather than beside it because the canonical mapping digest is what a grant binds: an allocation reference outside it would be authority nobody signed. This is what an allocated isolated restore was missing — Corestore refuses one whose exact reference its authority never covered. The reference itself is declared once, as data.ICorestoreDatabaseAllocationReference, and re-exported from /runtime so both trees name one structure.

The endpoint comes from the cluster document. IClusterNode.data.corestore { endpoint } is where one node's Corestore control API answers, owned by Cloudly and read by the relay, stored in Cloudly's database like every other field of the cluster document — never an alias, a file or a relay environment variable. validateClusterNodeCorestore(value, path?) admits a canonical http: or https: origin with no userinfo, path, query, fragment or trailing slash. http: is admitted because Corestore terminates no TLS today and the hop is cluster-private by construction; https: is admitted so putting TLS in front of Corestore needs no contract change. Absent means the node runs no Corestore the relay may reach, which an inventory reports as errorCode: 'CORESTORE_ENDPOINT_UNKNOWN' rather than guessing a hostname.

An administrator sets it with setClusterNodeCorestoreEndpoint { identity; nodeId; endpoint }, answered by Cloudly with the node as it now stands. Cloudly validates the endpoint with validateClusterNodeCorestore before it writes, takes the node's own real-write fence inside the writing transaction so a concurrent deletion conflicts instead of leaving an endpoint behind for a node that is gone, and pushes the cluster configuration afterwards so every connected relay learns the change without reconnecting — the same push that the cluster relay block uses when an operator enables or disables a relay. endpoint: null clears it, which states the same fact as never having set one. There is no node-wide update request: one field with one validator cannot drift into a generic node mutation whose per-field authority nobody defined.

The endpoint says where, never with what. The bearer control token that authorizes every call is obtained from Cloudly through getCorestoreControlCredentialMaterial, sealed to the X25519 recipient the relay enrolled with beginSecretRecipientEnrollment and completeSecretRecipientEnrollment — see Corestore Runtime Credentials. No credential travels in the cluster document, and none is read from the relay's environment.

Launcher Environment Delivery

Launcher environment delivery uses stable szsv-<base32-sha256> Docker resource names and /run/serve.zone/secrets/<resource> source paths. The nonsecret map is written to /run/serve.zone/workloadinit-map.json with mode 0444. Runtime assets under /opt/serve.zone/runtime-assets are read-only, the wrapper itself lives at /opt/serve.zone/runtime-assets/workloadinit/workloadinit, and it executes workloadinit run --map /run/serve.zone/workloadinit-map.json -- <argv...> without a shell, symlink, or aggregate value file. A resource name is the lowercase base32 of a SHA-256 over serve.zone/docker-secret-resource, a NUL and the SecretVersion id, so every launcher secret of a service is republished under a new Docker resource name when a cluster moves to 32.0.0. Resources and maps remain retained while any desired, applied, or previous-accepted manifest references them and are deleted only after Docker confirms rollout or service removal.

Platform Contracts

Use platform for current platform-service capabilities and application-facing platform RPCs.

import { platform } from '@serve.zone/interfaces';

type SendEmailRequest = platform.email.IReq_SendEmail;
type PlatformBinding = platform.IPlatformBinding;

const sendEmailMethod: SendEmailRequest['method'] = 'sendEmail';

Available platform modules:

  • platform.email for transactional email, recipient registration, email status, and email stats.
  • platform.sms for SMS delivery and verification-code delivery.
  • platform.pushnotification is the deprecated legacy device-token push contract. New browser Web Push integrations use requests.webpush.
  • platform.letter for physical letter workflows.
  • platform.ai, platform.database, platform.objectstorage, platform.logging, platform.backup, and platform.sip for infrastructure and application capabilities.
  • platform.storage for provider-neutral storage classes, requests, capabilities, and resolved bindings.
  • platform.objectstorageretention for value-free immutable-retention intent, capability and sentinel evidence, authority binding, digest helpers, and strict validators.
  • platform.storagemigration for fenced object-storage migration intent, status, consumer acknowledgements, canonical digests, and strict normalizers.
  • platform.types provider and binding metadata is value-free. Provider-specific operational config stays adapter-internal, while public DTOs expose typed endpoints and optional credential management scopes only.

Optional IPlatformBinding.objectstorageBucketName is the durable value-free Cloudly authority for an exact Corestore bucket. It is required whenever a caller needs to establish trusted bucket authority for retention validation. Cluster runtimes report the exact provider-returned bucket through the optional requests.platform.IReq_Any_Cloudly_UpdatePlatformBindingStatus.request.objectstorageBucketName field when updating binding status. validatePlatformObjectStorageBucketName() applies the canonical ObjectStorage S3 bucket boundary without normalizing input, and platformObjectStorageBucketNameLimits publishes its 3-byte minimum and 63-byte maximum. IPlatformBinding.objectstorageRetention carries compliance-mode intent and optional provider evidence bound to the exact service, binding, reconciliation generation, request digest, and authoritative bucket. Use validatePlatformBindingObjectStorageRetention() for the full binding boundary or the narrower intent/evidence validators when the trusted authority context is already available; none of these contracts contains credential material.

Legacy Platformservice Contracts

platformservice is retained for older integrations that still consume the previous namespace layout.

import { platformservice } from '@serve.zone/interfaces';

type LegacySendEmailRequest = platformservice.mta.IRequest_SendEmail;

New code should prefer platform unless it must remain compatible with an active legacy consumer.

Contract Ownership

Only ecosystem-wide public contracts belong in this package. Cloudly-internal implementation details, service-private DTOs, and temporary migration helpers should stay in their owning service until they become real shared contracts.

Good candidates for this package:

  • Types persisted or exchanged across multiple serve.zone services.
  • TypedRequest contracts used by more than one project.
  • SDK-facing interfaces that external consumers should be able to rely on.

Poor candidates for this package:

  • Private implementation details of one service.
  • Runtime helpers or convenience wrappers, unless they are shared contract normalization or validation helpers used by multiple packages.
  • Compatibility aliases without an active consumer.

Development

pnpm install
pnpm run build
pnpm test
pnpm run buildDocs

The package is authored as ESM TypeScript and built with tsbuild tsfolders.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license.md 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
Shared TypeScript data and RPC contracts for the serve.zone ecosystem.
Readme
14 MiB
Languages
TypeScript 100%