@serve.zone/interfaces 32.2.0 → 32.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +69 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/cluster.d.ts +30 -0
- package/dist_ts/data/cluster.js +46 -2
- package/dist_ts/data/clusternode.d.ts +21 -0
- package/dist_ts/data/clusternode.js +345 -11
- package/dist_ts/data/clustervpn.d.ts +135 -0
- package/dist_ts/data/clustervpn.js +147 -0
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/networkaddressplan.d.ts +54 -11
- package/dist_ts/data/networkaddressplan.js +67 -23
- package/dist_ts/data/networkcredential.d.ts +13 -4
- package/dist_ts/data/networkcredential.js +41 -23
- package/dist_ts/data/runtimeassignment.d.ts +23 -0
- package/dist_ts/data/runtimeassignment.js +42 -7
- package/dist_ts/platform/storage.d.ts +19 -0
- package/dist_ts/platform/storage.js +23 -3
- package/dist_ts/private/networkcontract.d.ts +6 -0
- package/dist_ts/private/networkcontract.js +12 -1
- package/dist_ts/requests/cluster.d.ts +27 -0
- package/dist_ts/requests/cluster.js +1 -1
- package/package.json +1 -1
- package/readme.md +139 -21
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/cluster.ts +76 -1
- package/ts/data/clusternode.ts +457 -10
- package/ts/data/clustervpn.ts +248 -0
- package/ts/data/index.ts +1 -0
- package/ts/data/networkaddressplan.ts +103 -27
- package/ts/data/networkcredential.ts +52 -24
- package/ts/data/runtimeassignment.ts +58 -5
- package/ts/platform/storage.ts +27 -5
- package/ts/private/networkcontract.ts +11 -0
- package/ts/requests/cluster.ts +35 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as userInterfaces from '../data/user.js';
|
|
2
2
|
import * as clusterInterfaces from '../data/cluster.js';
|
|
3
3
|
import * as plugins from '../plugins.js';
|
|
4
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
4
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3Rlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3JlcXVlc3RzL2NsdXN0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLGNBQWMsTUFBTSxpQkFBaUIsQ0FBQztBQUNsRCxPQUFPLEtBQUssaUJBQWlCLE1BQU0sb0JBQW9CLENBQUM7QUFLeEQsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUMifQ==
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -146,6 +146,22 @@ rather than with this release. `ISparkSwarmNodeHeartbeatRequest` is now owned he
|
|
|
146
146
|
so Cloudly and Spark no longer carry private copies of it, and every Swarm-era body
|
|
147
147
|
— request and answer alike — carries `protocol` and is judged in the same order.
|
|
148
148
|
|
|
149
|
+
Each of the four routes has its validator here, so a server never writes its own
|
|
150
|
+
member checks: `data.validateSparkSwarmNodeHeartbeatRequest`,
|
|
151
|
+
`data.validateSparkMetricsSampleRequest`, `data.validateSparkActionResultRequest`
|
|
152
|
+
and `data.validateSparkSwarmObservationRequest`. All four answer the same way —
|
|
153
|
+
one string per refused member, naming the member, and an empty list for an
|
|
154
|
+
admissible body — so a route table can hold them side by side. They read the
|
|
155
|
+
envelope first (exact key set, the sender's offer, then the reporter's identity),
|
|
156
|
+
then the payload: a heartbeat's metrics and full runtime description including
|
|
157
|
+
each described serve.zone service, a sample's four observations and optional
|
|
158
|
+
network rates, and an action result whose status is one a node can actually
|
|
159
|
+
report, never `pending`. Bodies are judged member by member rather than as
|
|
160
|
+
canonical bytes, because only the observation is digested and the others carry
|
|
161
|
+
fractional CPU, memory and rate observations; every level is copied away from the
|
|
162
|
+
caller's object first, so an accessor is refused instead of run. None of them
|
|
163
|
+
authenticates a node or acts on the body.
|
|
164
|
+
|
|
149
165
|
Cloudly must authenticate the bearer against current Spark authority and fence
|
|
150
166
|
credential generation and session epoch transactionally before recording host
|
|
151
167
|
liveness. `ISparkNodeHeartbeatReceipt` contains only server-derived acceptance
|
|
@@ -474,17 +490,25 @@ The `getRuntimeManagedVpnCredential` method contract in `requests.runtimesession
|
|
|
474
490
|
uses `IGetRuntimeManagedVpnCredentialRequest` and
|
|
475
491
|
`IGetRuntimeManagedVpnCredentialResponse`. Its request carries the exact current
|
|
476
492
|
session and signed projection reference. The secret response repeats those
|
|
477
|
-
bindings and supplies
|
|
478
|
-
|
|
479
|
-
|
|
493
|
+
bindings and supplies the `hub` tuple the node dials, the `transport` it dials it
|
|
494
|
+
over (`quic` or `websocket`), the managed authority ID, the Noise server public
|
|
495
|
+
key, the client keypair and the expiry in Unix milliseconds. `serverName` is present
|
|
496
|
+
exactly when the transport is `websocket` — the TLS name that hub is dialled under,
|
|
497
|
+
which the node cross-checks against the relay origin it stores — and absent for
|
|
498
|
+
`quic`, which is dialled on the address alone. The hub is the node's own cluster's
|
|
499
|
+
relay (see [Cluster VPN Hub And Network](#cluster-vpn-hub-and-network)). Keep the
|
|
500
|
+
response process-local; never persist, hash, log or embed it in a signed projection.
|
|
480
501
|
|
|
481
502
|
`snapshotGetRuntimeManagedVpnCredentialRequest` and its response counterpart
|
|
482
503
|
capture closed detached JSON shapes and canonical 32-byte key encodings.
|
|
483
504
|
`bindGetRuntimeManagedVpnCredentialRequest(request, projection, trustedSession)`
|
|
484
505
|
validates the complete projection digest and binds node, controller, namespace,
|
|
485
506
|
physical-session identity and projection reference. The response binder takes
|
|
486
|
-
`(response, request, projection, trustedSession, now, renewal = null)` and additionally requires
|
|
487
|
-
|
|
507
|
+
`(response, request, projection, trustedSession, now, renewal = null)` and additionally requires the
|
|
508
|
+
hub's exact tuple to be selected in `router.platformEndpointIds`, carrying the protocol of the
|
|
509
|
+
credential's transport — `udp` for `quic` and `tcp` for `websocket`, as
|
|
510
|
+
`runtimeNetworkAddressPlanContract.hubTransports` states. A relay therefore cannot name itself: the
|
|
511
|
+
endpoint has to be one the node's **signed** projection already selected.
|
|
488
512
|
Expiry must be after the caller-qualified time and no later than the referenced
|
|
489
513
|
projection's effective DNS window (see [DNS Lease Renewals](#dns-lease-renewals));
|
|
490
514
|
acceptance before that window starts rejects.
|
|
@@ -634,30 +658,45 @@ const window = await data.getRuntimeNetworkEffectiveDnsWindow(admittedProjection
|
|
|
634
658
|
|
|
635
659
|
`data.IRuntimeNetworkAddressPlan` is the address space an administrator gives the runtime network,
|
|
636
660
|
and the single input Cloudly composes the protected authority from:
|
|
637
|
-
`{ id: 'address-plan', revision, prefixes, pools, resolvers, platformEndpoints, vpn: { controlPrefix,
|
|
638
|
-
Pools, resolvers and platform endpoints use the protected authority's own element shapes
|
|
639
|
-
|
|
640
|
-
|
|
661
|
+
`{ id: 'address-plan', revision, prefixes, pools, resolvers, platformEndpoints, vpn: { controlPrefix, hubPrefix } }`.
|
|
662
|
+
Pools, resolvers and platform endpoints use the protected authority's own element shapes.
|
|
663
|
+
`controlPrefix` is the private prefix VPN control addresses are allocated from, and `hubPrefix` is
|
|
664
|
+
the protected prefix every cluster's VPN hub binds inside. The hub endpoints themselves are no
|
|
665
|
+
administrator's input: each cluster's relay reports what it bound, and the composition turns that
|
|
666
|
+
into platform endpoints, so `platformEndpoints` never lists one.
|
|
641
667
|
|
|
642
668
|
`snapshotRuntimeNetworkAddressPlan` refuses any plan that could describe an authority the authority
|
|
643
|
-
contract refuses
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
length is stated
|
|
650
|
-
|
|
669
|
+
contract refuses: it runs `snapshotRuntimeNetworkProtectedAuthority` over the plan's own sets, so
|
|
670
|
+
overlapping pools, more than 16 resolvers and a resolver or endpoint outside `prefixes` or inside a
|
|
671
|
+
pool are all refused by that one validator. On top it requires both VPN prefixes to sit inside
|
|
672
|
+
`prefixes`, to overlap no pool, to hold no resolver or platform endpoint, and never to overlap each
|
|
673
|
+
other — control addresses live inside the tunnel, hub endpoints on the underlay. `controlPrefix` is
|
|
674
|
+
additionally a private prefix no longer than /30
|
|
675
|
+
(`runtimeNetworkAddressPlanContract.maximumControlPrefixLength`); no minimum length is stated,
|
|
676
|
+
because the private-range rule already keeps it at /8 or longer.
|
|
677
|
+
|
|
678
|
+
`data.IRuntimeNetworkVpnHub { clusterId, address, quicPort, websocketPort }` is one cluster's hub as
|
|
679
|
+
its relay bound it, with `null` for a transport it did not bind and at least one of the two bound.
|
|
680
|
+
`runtimeNetworkVpnHubEndpoints(hub)` is the one derivation of what it contributes:
|
|
681
|
+
`<clusterId>:quic` on `udp` and `<clusterId>:ws` on `tcp`, in id order, as
|
|
682
|
+
`runtimeNetworkAddressPlanContract.hubTransports` states. Every reader calls it — the composition
|
|
683
|
+
that seals the authority and the router selection that names which endpoint a node may dial — so
|
|
684
|
+
two readers can never derive two ids for one hub.
|
|
685
|
+
`composeRuntimeNetworkProtectedAuthority(plan, frame, hubs)` is the composition itself: the producer
|
|
651
686
|
supplies the frame (`TRuntimeNetworkProtectedAuthorityFrame`: authority id, generation, controller,
|
|
652
|
-
previous reference and egress authorities) and
|
|
653
|
-
the validator and the protection producer can
|
|
687
|
+
previous reference and egress authorities) and the hubs of the clusters that registered one, and
|
|
688
|
+
receives the complete authority with its digest, so the validator and the protection producer can
|
|
689
|
+
never compose differently. Each hub's address must sit inside `vpn.hubPrefix` and one cluster is
|
|
690
|
+
named once; an empty `hubs` composes the authority of a runtime network whose clusters have no hub
|
|
691
|
+
yet. One hub costs up to two of the authority's 64 platform endpoints, so 32 clusters fit one
|
|
692
|
+
authority.
|
|
654
693
|
|
|
655
694
|
`admitRuntimeNetworkAddressPlanChange(previous | null, next)` is the pure decision for a plan write at
|
|
656
695
|
the following revision; any other revision is a contract violation. Leases and the managed VPN
|
|
657
696
|
outlive a plan revision, so it answers:
|
|
658
697
|
|
|
659
698
|
- `unchanged` when the content equals the stored plan; Cloudly writes nothing;
|
|
660
|
-
- `refused` with `plan-vpn-immutable` when
|
|
699
|
+
- `refused` with `plan-vpn-immutable` when either VPN prefix differs, `plan-pool-removed` when any pool is gone
|
|
661
700
|
or changed its purpose or prefix, and `plan-prefix-shrunk` when the new prefixes, however split,
|
|
662
701
|
no longer cover every address the old ones did;
|
|
663
702
|
- `accepted` with the ids of the resolvers and platform endpoints the change removes or changes. An
|
|
@@ -706,7 +745,19 @@ rule read the other way, for a receiver checking a list it was sent. Canonical i
|
|
|
706
745
|
strictly increasing in the code-unit order of the names themselves — a rule a peer
|
|
707
746
|
in another language implements from the vocabulary alone, rather than from the
|
|
708
747
|
order this package happens to declare it in — and both directions compare through
|
|
709
|
-
that one rule, so neither can drift from the other.
|
|
748
|
+
that one rule, so neither can drift from the other.
|
|
749
|
+
|
|
750
|
+
`workload.registryHost` is the registry that published the image, which is the publication identity
|
|
751
|
+
the image release is bound to, and it is never rewritten. `workload.pullEndpoint` is the optional
|
|
752
|
+
`host[:port]` the node fetches the bytes from instead — a cluster whose relay forwards the registry
|
|
753
|
+
states the relay's own origin, so a node pulls inside its cluster and nothing cluster-side dials the
|
|
754
|
+
control plane. `data.buildRuntimeAssignmentImageReference(workload)` is the one place that choice is
|
|
755
|
+
made: `pullEndpoint ?? registryHost`, digest-pinned either way, so the endpoint decides reachability
|
|
756
|
+
and never content, and a controller and a node cannot disagree about the reference. Absence stays
|
|
757
|
+
absence: an assignment that states no endpoint is different content, and so a different digest, from
|
|
758
|
+
one that does.
|
|
759
|
+
|
|
760
|
+
All fields except the digest itself participate in a domain-separated
|
|
710
761
|
SHA-256 digest. Digests provide integrity and compare-and-swap identity;
|
|
711
762
|
authentication must separately establish the trusted controller/node scope.
|
|
712
763
|
|
|
@@ -1308,6 +1359,25 @@ around the request. A body that is not a storage request is refused with
|
|
|
1308
1359
|
taken over. `platform.storageRequestCanonicalDigestGoldenVectors` states both
|
|
1309
1360
|
for a filesystem request and for both object-storage deliveries.
|
|
1310
1361
|
|
|
1362
|
+
Which requests exist at all is stated by the same contract:
|
|
1363
|
+
|
|
1364
|
+
```typescript
|
|
1365
|
+
const storageRequest = platform.normalizeStorageRequest(declaredRequest);
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
`platform.normalizeStorageRequest` reads a body against the exact request schema
|
|
1369
|
+
and returns the request in its contract shape — required members present,
|
|
1370
|
+
optional members carried only when declared, canonical environment keys, no
|
|
1371
|
+
empty `protection` block, no explicitly undefined member, no `-0` or unsafe
|
|
1372
|
+
integer, and plain JSON data members only. It is the value the other two
|
|
1373
|
+
functions are derived from: canonicalizing and digesting a request normalize it
|
|
1374
|
+
first, so a consumer that admits a request through this function and a control
|
|
1375
|
+
plane that digests it cannot accept different sets of requests. The input is
|
|
1376
|
+
never mutated and the returned request is a fresh object. A body that is not a
|
|
1377
|
+
storage request is refused with `platform.StorageContractError`, whose message
|
|
1378
|
+
names the failing member so a consumer can map the refusal to its own status
|
|
1379
|
+
vocabulary instead of restating the rules.
|
|
1380
|
+
|
|
1311
1381
|
Portable manifests and resolved bindings intentionally have no fields for
|
|
1312
1382
|
Synology, NFS, Kerberos, Corestore, Kubernetes, Docker drivers, servers,
|
|
1313
1383
|
exports, mount options, provider credential values, or local fallback paths. Runtimes
|
|
@@ -1631,6 +1701,54 @@ const registrationReasons = data.validateRegisterClusterRelayRequest(registratio
|
|
|
1631
1701
|
How the relay reaches each node's Corestore is part of the same document: see
|
|
1632
1702
|
[Corestore Through The Relay](#corestore-through-the-relay).
|
|
1633
1703
|
|
|
1704
|
+
### Cluster VPN Hub And Network
|
|
1705
|
+
|
|
1706
|
+
A cluster's relay is also that cluster's managed VPN hub: every node of the cluster dials it, and
|
|
1707
|
+
an inter-node workload packet is relayed inside the cluster instead of crossing the control plane.
|
|
1708
|
+
Cloudly keeps every authority — it compiles the network and issues each node's credential — and
|
|
1709
|
+
carries no packet.
|
|
1710
|
+
|
|
1711
|
+
A relay that bound a hub says so in its registration, in `relay.vpnHub`:
|
|
1712
|
+
`data.IClusterRelayVpnHub { publicKey, address, quicPort, websocketPort }`. It reports what it
|
|
1713
|
+
bound, exactly like `listenAddress` and `listenPort`: `address` is an IPv4 literal inside the
|
|
1714
|
+
address plan's `vpn.hubPrefix`, a transport it did not bind is `null`, and at least one of the two
|
|
1715
|
+
is bound. `publicKey` is the hub's Noise public key in the one canonical 32-byte base64 encoding
|
|
1716
|
+
every Noise key of this contract is read with — this advertisement, the managed VPN credential's
|
|
1717
|
+
keys and a cluster network's nodes all pass the same reader. A relay names no endpoint id and no
|
|
1718
|
+
TLS name of its own: Cloudly composes the endpoints with `runtimeNetworkVpnHubEndpoints` (see
|
|
1719
|
+
[Runtime Network Address Plan](#runtime-network-address-plan)) and states the TLS name from the
|
|
1720
|
+
origin it published for that relay. The member is optional — a relay that serves no hub registers
|
|
1721
|
+
exactly as before, and Cloudly selects no hub for that cluster, stating `relay-vpn-hub-absent`.
|
|
1722
|
+
`validateClusterRelayVpnHub(value, path?)` returns one named reason per violation.
|
|
1723
|
+
|
|
1724
|
+
`data.IClusterVpnNetwork { authorityId, revision, nodes, grants }` is the whole managed VPN of one
|
|
1725
|
+
cluster. It is never a patch: a node or a grant the network does not carry is withdrawn, and
|
|
1726
|
+
membership is presence, so there is no enabled flag. Each node states `{ nodeId, publicKey,
|
|
1727
|
+
controlAddress, expiresAt, workloadPrefixes }`, where `expiresAt` is that node's credential expiry
|
|
1728
|
+
in Unix milliseconds and each prefix is `{ cidr, policyDomain }`. Each grant is one directed
|
|
1729
|
+
`{ sourceDomain, destinationDomain }` pair. `clusterVpnControlPolicyDomain(address)` and
|
|
1730
|
+
`clusterVpnWorkloadPolicyDomain(leaseDigest)` state the domain names once for both sides.
|
|
1731
|
+
|
|
1732
|
+
`snapshotClusterVpnNetwork` requires sorted, unique members, prefixes and grants, and requires
|
|
1733
|
+
every grant to join two workload domains the network itself carries, on two different nodes. That
|
|
1734
|
+
is what a per-cluster hub means: a grant whose other end sits on another cluster's hub cannot be
|
|
1735
|
+
expressed at all, which is why Cloudly refuses it while it compiles, by name, with
|
|
1736
|
+
`grant-cross-cluster-unsupported` from `data.clusterVpnNetworkSelectionRefusals`.
|
|
1737
|
+
`clusterVpnNetworkContract` bounds a network at 256 nodes, 128 workload prefixes per node, 4096
|
|
1738
|
+
grants and 917 504 canonical bytes.
|
|
1739
|
+
|
|
1740
|
+
`applyClusterVpnNetwork { network }` is Cloudly's push over the relay's already registered session;
|
|
1741
|
+
the relay answers `{ appliedRevision, state }`, where `state` is `idle`, `applying` or `failed` and
|
|
1742
|
+
`idle` means the pushed revision is the applied one — which is what
|
|
1743
|
+
`bindApplyClusterVpnNetworkResponse(response, sentRequest)` checks. `admitClusterVpnNetwork(held |
|
|
1744
|
+
null, next)` is the pure decision the relay makes against what its hub holds: `accepted` for a
|
|
1745
|
+
higher revision or an empty hub, `replay` for a repeat, and `refused` with
|
|
1746
|
+
`vpn-network-revision-stale` or `vpn-network-authority-foreign` from
|
|
1747
|
+
`data.applyClusterVpnNetworkRefusals`. `getClusterVpnNetwork` is the pull the other way, with no
|
|
1748
|
+
request body — the relay's socket names the cluster — answering `{ network }` or `{ network: null }`
|
|
1749
|
+
while the cluster has none. A hub daemon starts empty and its lifetime is the relay's own, so a
|
|
1750
|
+
relay that just bound one pulls instead of waiting for the next push.
|
|
1751
|
+
|
|
1634
1752
|
### Cluster Ingress
|
|
1635
1753
|
|
|
1636
1754
|
A cluster's ingress terminates TLS for the hostnames its services publish and forwards to the
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/cluster.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { IClusterNode } from './clusternode.js';
|
|
|
5
5
|
import { isPlausibleUnicastIpv4 } from '../private/ipv4.js';
|
|
6
6
|
import { validateProtocolOfferOn, type IProtocolOffer } from '../protocol.js';
|
|
7
7
|
import { hasExactKeys } from '../private/dataproperties.js';
|
|
8
|
+
import { networkNoiseKey } from '../private/networkcontract.js';
|
|
8
9
|
|
|
9
10
|
export interface ICluster {
|
|
10
11
|
id: string;
|
|
@@ -62,6 +63,25 @@ export interface IClusterRelay {
|
|
|
62
63
|
origin: string;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The managed VPN hub a cluster relay bound for its own cluster.
|
|
68
|
+
*
|
|
69
|
+
* The relay is its cluster's packet relay: every node of the cluster dials this hub and no
|
|
70
|
+
* inter-node packet leaves the cluster. It reports what it bound, exactly like `listenAddress` and
|
|
71
|
+
* `listenPort`, and Cloudly composes the platform endpoints from it — a relay names no endpoint id
|
|
72
|
+
* and no TLS name of its own. `publicKey` is the hub's Noise public key in the one canonical
|
|
73
|
+
* 32-byte base64 encoding every Noise key of this contract is read with; it is public material and
|
|
74
|
+
* authenticates nothing by itself. A hub that bound only one transport reports the other port as
|
|
75
|
+
* `null`.
|
|
76
|
+
*/
|
|
77
|
+
export interface IClusterRelayVpnHub {
|
|
78
|
+
publicKey: string;
|
|
79
|
+
/** The address the hub bound, as an IPv4 literal inside the address plan's `vpn.hubPrefix`. */
|
|
80
|
+
address: string;
|
|
81
|
+
quicPort: number | null;
|
|
82
|
+
websocketPort: number | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
/**
|
|
66
86
|
* What a cluster relay tells Cloudly about itself once its socket is authenticated.
|
|
67
87
|
*
|
|
@@ -88,6 +108,11 @@ export interface IRegisterClusterRelayRequest {
|
|
|
88
108
|
*/
|
|
89
109
|
listenAddress: string;
|
|
90
110
|
listenPort: number;
|
|
111
|
+
/**
|
|
112
|
+
* Absent on a relay that serves no managed VPN hub, which registers exactly as it did before.
|
|
113
|
+
* Cloudly selects no hub for such a cluster and states `relay-vpn-hub-absent` instead.
|
|
114
|
+
*/
|
|
115
|
+
vpnHub?: IClusterRelayVpnHub;
|
|
91
116
|
};
|
|
92
117
|
/** Monotonic within one relay process; it restarts at 0 when the process restarts. */
|
|
93
118
|
registrationSequence: number;
|
|
@@ -234,9 +259,56 @@ const validateClusterRelayNonNegativeInteger = (valueArg: unknown, pathArg: stri
|
|
|
234
259
|
: [`${pathArg} must be a non-negative safe integer`]
|
|
235
260
|
);
|
|
236
261
|
|
|
262
|
+
/**
|
|
263
|
+
* The one canonical 32-byte base64 reading every Noise key of this contract passes, stated as a
|
|
264
|
+
* refusal this validator's callers can collect: a hub key is the same key material the managed VPN
|
|
265
|
+
* credential carries, so it is never read by a second rule that could drift from it.
|
|
266
|
+
*/
|
|
267
|
+
const validateClusterRelayVpnKey = (valueArg: unknown, pathArg: string): string[] => {
|
|
268
|
+
try {
|
|
269
|
+
networkNoiseKey(valueArg);
|
|
270
|
+
return [];
|
|
271
|
+
} catch {
|
|
272
|
+
return [`${pathArg} must be a canonical 32-byte base64 public key`];
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/** A port the hub bound, or `null` for a transport it did not bind. */
|
|
277
|
+
const validateClusterRelayHubPort = (valueArg: unknown, pathArg: string): string[] => (
|
|
278
|
+
valueArg === null || validateClusterRelayListenPort(valueArg, pathArg).length === 0
|
|
279
|
+
? []
|
|
280
|
+
: [`${pathArg} must be null or an integer from 1 to 65535`]
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Validates one hub advertisement and names every reason it is refused.
|
|
285
|
+
*
|
|
286
|
+
* Shape only: whether the address sits inside the address plan's `vpn.hubPrefix` is Cloudly's to
|
|
287
|
+
* decide against the plan it stores, and the endpoints themselves are composed there.
|
|
288
|
+
*/
|
|
289
|
+
export const validateClusterRelayVpnHub = (
|
|
290
|
+
valueArg: unknown,
|
|
291
|
+
pathArg = 'cluster relay vpn hub',
|
|
292
|
+
): string[] => {
|
|
293
|
+
if (!isRecord(valueArg)
|
|
294
|
+
|| !hasExactKeys(valueArg, ['publicKey', 'address', 'quicPort', 'websocketPort'])) {
|
|
295
|
+
return [`${pathArg} must use its exact schema`];
|
|
296
|
+
}
|
|
297
|
+
const errors = [
|
|
298
|
+
...validateClusterRelayVpnKey(valueArg.publicKey, `${pathArg} publicKey`),
|
|
299
|
+
...validateClusterRelayListenAddress(valueArg.address, `${pathArg} address`),
|
|
300
|
+
...validateClusterRelayHubPort(valueArg.quicPort, `${pathArg} quicPort`),
|
|
301
|
+
...validateClusterRelayHubPort(valueArg.websocketPort, `${pathArg} websocketPort`),
|
|
302
|
+
];
|
|
303
|
+
if (valueArg.quicPort === null && valueArg.websocketPort === null) {
|
|
304
|
+
errors.push(`${pathArg} must bind at least one of quicPort and websocketPort`);
|
|
305
|
+
}
|
|
306
|
+
return errors;
|
|
307
|
+
};
|
|
308
|
+
|
|
237
309
|
const validateClusterRelayEndpoint = (valueArg: unknown, pathArg: string): string[] => {
|
|
238
310
|
if (!isRecord(valueArg)
|
|
239
|
-
|| !hasExactKeys(valueArg, ['version', 'nodeId', 'listenAddress', 'listenPort'])) {
|
|
311
|
+
|| !hasExactKeys(valueArg, ['version', 'nodeId', 'listenAddress', 'listenPort'], ['vpnHub'])) {
|
|
240
312
|
return [`${pathArg} must use its exact schema`];
|
|
241
313
|
}
|
|
242
314
|
return [
|
|
@@ -244,6 +316,9 @@ const validateClusterRelayEndpoint = (valueArg: unknown, pathArg: string): strin
|
|
|
244
316
|
...validateClusterRelayIdentifier(valueArg.nodeId, `${pathArg} nodeId`),
|
|
245
317
|
...validateClusterRelayListenAddress(valueArg.listenAddress, `${pathArg} listenAddress`),
|
|
246
318
|
...validateClusterRelayListenPort(valueArg.listenPort, `${pathArg} listenPort`),
|
|
319
|
+
...(Object.keys(valueArg).includes('vpnHub')
|
|
320
|
+
? validateClusterRelayVpnHub(valueArg.vpnHub, `${pathArg} vpnHub`)
|
|
321
|
+
: []),
|
|
247
322
|
];
|
|
248
323
|
};
|
|
249
324
|
|