@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
sequenceis higher than the previous renewal's, with gaps allowed, or equal with byte-identical content, which is answered asreplaywhatevernowis, 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
issuedAtback 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'sissuedAtto the last window'sexpiresAt. 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:
unchangedwhen the content equals the stored plan; Cloudly writes nothing;refusedwithplan-vpn-immutablewhen either VPN prefix differs,plan-pool-removedwhen any pool is gone or changed its purpose or prefix, andplan-prefix-shrunkwhen the new prefixes, however split, no longer cover every address the old ones did;acceptedwith 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.
placementis{ kind: 'every-node' }or{ kind: 'nodes', nodeIds }, withnodeIdssorted, unique, never empty and at mostmaximumPinnedNodes, the protected authority's egress limit.replicasPerNoderuns from 1 to 64; Cloudly refuses a service desiring more thanmaximumReplicas(64) asreplica-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: nullruns 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 asreadiness-target-undeclared.networkis{ 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.nodeRuntimeRetirementRefusals — node-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.
getServiceSecretBundlesAsFlatObjectand 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-onlyIActiveSecretRecipientMetadata, therequests.secret.IReq_GetSecretIngressRecipientcontract with methodgetSecretIngressRecipient, and fixed-order create/rotate/App Store context builders.ISecretEnvelopeAdmissionBindingunder 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.getSecretVersionPurgePreflightreturns bounded advisory reference pages and complete blocker counts, including indefiniteruntime-assignmentholds until joined terminal removal proof.purgeSecretVersionidentifies one exact version and fences the mutation by secret, version, and target revisions. The non-issuablepurge-pendinglifecycle and revisionedpending/erasing/failed/succeededoperation keep external erasure durable across retries without treating preflight as authorization.IResolvedSecretManifestcontracts, stable Docker resource naming, WorkloadInit map/wrapper helpers, andISealedResolvedSecretMaterialunder@serve.zone/interfaces/runtime.- Two-step Coreflow X25519 recipient enrollment and exact recipient lifecycle validators.
- Mandatory
IImmutableContainerInvocationevidence 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:
getSecretRecipientEnrollmentStatereturns either an exact generation-zero empty state or the complete valid recipient set for the cluster derived from the verified JWT.getCoreflowSecretRuntimeRegistrationExpectationreturns either anavailableexpectation or an explicit unavailable reason. The available expectation binds the live reporter session, active recipient, fresh generation-fenced target authority, and active WorkloadInit approval throughexpectationDigest.- 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.
TSparkSwarmObservationlets 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 designatessparkSwarmNodeContracts.swarmObservation, whose endpoint is/spark/swarm-nodes/swarm-observationand whosemaxRequestBytescovers the largest snapshot this validator accepts; every body on it carries the sender'sprotocoloffer. 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.computeSparkSwarmObservationDigestuses strict canonical JSON and theserve.zone/spark-swarm-observationdomain. A reporter session starts at sequence one. The contract requires a Cloudly consumer to accept only the exact next sequence with a strictly advancingobservedAt, 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.validateSparkSwarmObservationvalidates exact structure and attached snapshots, whilevalidateSparkSwarmObservationRequestandvalidateSparkSwarmObservationResponsevalidate 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) andbuilderId(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, andIWorkloadInitApprovalAuthorityReferencestates only the counterauthorityGenerationbeside the approval digest it points at. coreflowSecretRuntimeRegistrationTagIdis the sole dedicated TypedSocket tag identifier. Its payload is exactlyICoreflowSecretRuntimeRegistration; cluster scope comes from the verified connection identity rather than the tag.ICoreflowSecretRuntimeRegistrationreferences 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.capabilitiesis a sorted, duplicate-free list ofdata.TCoreflowRuntimeCapabilitynames drawn fromdata.coreflowRuntimeCapabilities, judged bydata.isCoreflowRuntimeCapabilityList. A build states what it can do by name; which shape it speaks is the interfaces version both peers exchanged. Every name indata.requiredCoreflowRuntimeCapabilitiesmust be present. That list is the vocabulary minusdata.optionalCoreflowRuntimeCapabilities, which holdscorestore-inventoryalone: 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.validateCoreflowSecretRuntimeRegistrationcompares 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.reportSecretDeploymentStatereports onlyapplying,applied,drifted, orfailedfor 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:
IObjectStorageMigrationPrepareRequestandTObjectStorageMigrationStatusfor the immutable intent and status journal.IObjectStorageMigrationConsumerQuiesceRequestwithIStorageMigrationConsumerQuiesceEvidencefor exact candidate staging and source-workload shutdown.IObjectStorageMigrationConsumerActivationRequestwithIStorageMigrationConsumerActivationEvidencefor durable destination-start acknowledgement.normalizeObjectStorageMigrationStatus,bindObjectStorageMigrationConsumerQuiesceRequest, andbindObjectStorageMigrationConsumerActivationRequestfor strict ingress and current-revision mutation fencing.- The
create*Sha256helpers 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:
IClusterandIClusterNodefor cluster membership and provisioning state, including the optionalICluster.data.relayblock described below.IService,IDeployment,IImage,IRegistryTarget, andIExternalRegistryfor workload delivery.- Service port contracts including
IServiceTargetPort,IServiceDomainRoute, andIServicePublicPortMappingfor 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, routingportRoutes, andIGatewayClientRouteclient-owned route views. Gateway route intent supports optional matchdomains,transport, andremoteIngress, plus explicit routepriorityandmanagedRouteKind. Ownership can combinehostnamewithrouteRefso a normal route and a path-specific managed route for the same hostname reconcile independently. - Value-free
ISecretMetadata,ISecretVersionMetadata, andISecretSetMetadatacontracts for operator views, plus exact-versionIResolvedSecretManifestcontracts 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.listSecretsexposes the dedicatedtargetSecretsRevisionCAS fence; every create, rotate, and lifecycle mutation consumes and returns that aggregate owner fence, whilesetServiceSecretSetAttachmentsreturns the independentsecretConfigurationRevision. 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 inboundsmtpForwardsettings 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 thepushnotificationplatform capability alongside database and object-storage capabilities; this does not turn Web Push into a Corestore resource or volume capability. IUser, JWT-onlyIIdentityCredential, fullIIdentity, and token-related contracts for authentication context.IIdentityextendsIIdentityCredentialwith 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'),
}); // []
Node Uplink Address
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.validateHostedAppRoleDefinitionsdata.validateHostedAppPlatformOidcRegistrationdata.validateHostedAppPlatformOidcClaimsappstore.validateAppStorePlatformOidcCapabilityappstore.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:
- Reserve the exact service, namespace, registry repository/tag, and route intent with
reserveServiceDeployment. - Push the OCI index to the returned exact tag with the authenticated deployer identity.
- Promote the authenticated release evidence with
promoteServiceImageRelease. - Observe exact rollout and runtime-digest evidence with
getServiceDeploymentStatus. - For a greenfield service, expose and verify its public route with
promoteServiceDeploymentRouteonly 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, androtateWebPushVapidKeyuse control-plane identity or gateway API-token authentication.getWebPushServiceStatus,enqueueWebPush,cancelWebPush, andgetWebPushDeliveryStatusrequire 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.adminrequests.appstorerequests.baremetalrequests.baseosrequests.backuprequests.certificaterequests.clusterrequests.configrequests.coremailrequests.coremailAdminrequests.corestorerequests.deploymentrequests.dnsrequests.domainrequests.externalRegistryrequests.gatewayrequests.hostedapprequests.identityrequests.imagerequests.informrequests.logrequests.mailrequests.migrationrequests.networkrequests.noderequests.platformrequests.routingrequests.secretrequests.servicerequests.settingsrequests.statusrequests.taskrequests.versionrequests.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.isolatedRestoreProgressStatuses — prepared, 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.emailfor transactional email, recipient registration, email status, and email stats.platform.smsfor SMS delivery and verification-code delivery.platform.pushnotificationis the deprecated legacy device-token push contract. New browser Web Push integrations userequests.webpush.platform.letterfor physical letter workflows.platform.ai,platform.database,platform.objectstorage,platform.logging,platform.backup, andplatform.sipfor infrastructure and application capabilities.platform.storagefor provider-neutral storage classes, requests, capabilities, and resolved bindings.platform.objectstorageretentionfor value-free immutable-retention intent, capability and sentinel evidence, authority binding, digest helpers, and strict validators.platform.storagemigrationfor fenced object-storage migration intent, status, consumer acknowledgements, canonical digests, and strict normalizers.platform.typesprovider 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.
License and Legal Information
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.