@serve.zone/interfaces 31.2.0 → 31.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 +74 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/cluster.d.ts +52 -0
- package/dist_ts/data/cluster.js +68 -1
- package/dist_ts/data/clusteringress.d.ts +137 -0
- package/dist_ts/data/clusteringress.js +302 -0
- package/dist_ts/data/coremail.runtime.js +4 -8
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/networkprotection.d.ts +18 -0
- package/dist_ts/data/networkprotection.js +24 -3
- package/dist_ts/data/privatenetwork.js +3 -5
- package/dist_ts/private/ipv4.d.ts +9 -0
- package/dist_ts/private/ipv4.js +23 -0
- package/dist_ts/private/networkcontract.d.ts +1 -1
- package/dist_ts/private/networkcontract.js +7 -3
- package/dist_ts/private/utccalendarday.d.ts +17 -0
- package/dist_ts/private/utccalendarday.js +27 -0
- package/dist_ts/requests/cluster.d.ts +47 -0
- package/dist_ts/requests/clusteringress.d.ts +120 -0
- package/dist_ts/requests/clusteringress.js +3 -0
- package/dist_ts/requests/index.d.ts +2 -1
- package/dist_ts/requests/index.js +3 -2
- package/package.json +1 -1
- package/readme.md +126 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/cluster.ts +126 -0
- package/ts/data/clusteringress.ts +425 -0
- package/ts/data/coremail.runtime.ts +3 -10
- package/ts/data/index.ts +1 -0
- package/ts/data/networkprotection.ts +34 -2
- package/ts/data/privatenetwork.ts +3 -4
- package/ts/private/ipv4.ts +24 -0
- package/ts/private/networkcontract.ts +8 -2
- package/ts/private/utccalendarday.ts +30 -0
- package/ts/requests/cluster.ts +59 -0
- package/ts/requests/clusteringress.ts +140 -0
- package/ts/requests/index.ts +2 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-09-16 - 31.4.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add the cluster ingress contracts, the first under the rule that a contract carries no version
|
|
8
|
+
suffix and no schema field: the protocol version is the installed package version.
|
|
9
|
+
`IClusterRouteTable { revision, issuedAt, routes, portRoutes }` is the whole table a cluster's
|
|
10
|
+
ingress serves, with `IClusterHttpRoute` (hostname, unicast destinations, the certificate that
|
|
11
|
+
terminates it, optional Basic authentication) and `IClusterPortRoute` (a TCP or UDP listen port
|
|
12
|
+
forwarded untouched). `revision` is the feed's only idempotency: a relay and an ingress apply a
|
|
13
|
+
greater revision and ignore anything else, so a repeated, replayed or crossed push settles the
|
|
14
|
+
same way. `validateClusterRouteTable` returns one named reason per violation, refuses a second
|
|
15
|
+
route for one hostname and a second port route for one transport and port, and never quotes
|
|
16
|
+
certificate material or a password in a reason — a reason travels into logs, and this table
|
|
17
|
+
carries the private key of every hostname the ingress serves.
|
|
18
|
+
- Add the five methods the feed and its counters travel on: `pushClusterRouteTable` (Cloudly →
|
|
19
|
+
relay), `getClusterRouteTable` (relay → Cloudly), `getClusterIngressRouteTable` (ingress →
|
|
20
|
+
relay), `registerClusterIngress` and `getClusterTrafficStatistics` (both one body for two hops:
|
|
21
|
+
ingress → relay → Cloudly, and Cloudly → relay → ingress). An ingress announces itself through a
|
|
22
|
+
method rather than a connection tag, so what a relay knows about its ingress is what a server
|
|
23
|
+
owns. A `null` table means Cloudly has computed none yet — a fact, not an empty table.
|
|
24
|
+
- Authorize the ingress where the authority is. The ingress shares the relay's node listener, so
|
|
25
|
+
anything reaching it could ask to register and a relay cannot tell an ingress from anything
|
|
26
|
+
else: `registerClusterIngress` carries the `bearer` Cloudly minted into the ingress workload's
|
|
27
|
+
assignment environment, the relay forwards it untouched, and Cloudly verifies it and derives the
|
|
28
|
+
cluster from the identity tag on the relay's own session. The bearer is sensitive ephemeral
|
|
29
|
+
input on every hop and is never quoted in a reason. Isolation on the shared listener is by
|
|
30
|
+
authorization, not routing: only the connection whose registration Cloudly accepted is the
|
|
31
|
+
ingress, and every other connection is refused by name.
|
|
32
|
+
`validateClusterIngressRegistration` checks shape only.
|
|
33
|
+
- Add `IClusterTrafficStatistics`: one UTC day of one hostname's traffic on one node — requests,
|
|
34
|
+
bytes in and out, connections and the backend connect, handshake and request failures the proxy
|
|
35
|
+
can observe — paged exactly like CoreMail's mail statistics, down to the `{ statistics,
|
|
36
|
+
nextCursor? }` response and the `fromDayUtc`/`toDayUtc`/`cursor`/`limit` request, with `dayUtc`
|
|
37
|
+
checked against the real calendar through the now-shared UTC calendar-day rule (`2026-13-45` is
|
|
38
|
+
refused rather than stored; CoreMail's own day check consumes the same helper). Day buckets survive a restart, a
|
|
39
|
+
poll across midnight and a Cloudly outage, which a rate does not. HTTP status classes are
|
|
40
|
+
deliberately absent: the proxy exposes no status counters today, and a number the contract
|
|
41
|
+
cannot source would be a guess.
|
|
42
|
+
- Carry the node's own uplink address on the network report it already sends:
|
|
43
|
+
`IReportRuntimeNetworkProtectionRequestV1` gains the optional `uplinkAddress`, validated by
|
|
44
|
+
`runtimeNodeUplinkAddress` as a unicast IPv4 literal that is neither loopback nor link-local.
|
|
45
|
+
A route to that node's published ports uses exactly this address, and only the node can prove
|
|
46
|
+
it. Absence stays absent, so a node that states none is told apart from one that states a value,
|
|
47
|
+
and nothing downstream defaults or infers one. Additive: `networkRecord` now admits declared
|
|
48
|
+
optional keys, and every existing caller passes none.
|
|
49
|
+
|
|
50
|
+
## 2026-09-16 - 31.3.0
|
|
51
|
+
|
|
52
|
+
### Features
|
|
53
|
+
|
|
54
|
+
- Add the cluster relay enable, disable and registration contracts. `enableClusterRelayV1` and
|
|
55
|
+
`disableClusterRelayV1` take only an identity and a cluster id and answer with the published
|
|
56
|
+
`IClusterRelayV1` block or an explicit `relay: null`: Cloudly derives the origin from the
|
|
57
|
+
cluster and its own public hostname and refuses when no DNS zone it manages matches the derived
|
|
58
|
+
name, so no caller can name a relay origin. `registerClusterRelayV1` is what the relay sends
|
|
59
|
+
over its own socket after `registerCloudlyClientSessionV1`, carrying
|
|
60
|
+
`IClusterRelayRegistrationV1 { schemaVersion: 1; relay: { version; nodeId; listenAddress;
|
|
61
|
+
listenPort }; registrationSequence }` and answered with
|
|
62
|
+
`IClusterRelayRegistrationResponseV1 { accepted: true; origin: string | null }` — the origin
|
|
63
|
+
Cloudly published, so the relay can check the certificate it holds against the name its nodes
|
|
64
|
+
are given, and `null` while the cluster carries no relay block.
|
|
65
|
+
`validateClusterRelayRegistrationV1(value, pathArg?)` returns one named reason per violation in
|
|
66
|
+
the package's value-free validator shape, and checks shape only: `listenAddress` must be a
|
|
67
|
+
unicast IPv4 literal, because Cloudly publishes it verbatim as an A record, and a loopback or
|
|
68
|
+
link-local address is refused by its own name because a relay that bound one can never serve
|
|
69
|
+
its cluster; `nodeId` follows the same rule a node's own runtime session registers under;
|
|
70
|
+
`registrationSequence` is a non-negative safe integer, so a relay process's first registration
|
|
71
|
+
at `0` is a registration. Which cluster a relay speaks for, whether its node belongs to that
|
|
72
|
+
cluster, and which of two live peers is the relay all stay Cloudly's decisions, made from the
|
|
73
|
+
verified identity on the socket and its own records. Additive only: the dotted-quad rule the
|
|
74
|
+
private network upstream validator already applied moved into one shared internal predicate
|
|
75
|
+
with unchanged semantics, and nothing was removed or renamed.
|
|
76
|
+
|
|
3
77
|
## 2026-09-16 - 31.2.0
|
|
4
78
|
|
|
5
79
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '31.
|
|
6
|
+
version: '31.4.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
|
@@ -48,6 +48,48 @@ export interface IClusterRelayV1 {
|
|
|
48
48
|
schemaVersion: 1;
|
|
49
49
|
origin: string;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* What a cluster relay tells Cloudly about itself once its socket is authenticated.
|
|
53
|
+
*
|
|
54
|
+
* The relay sends this after `registerCloudlyClientSessionV1`, so the cluster it registers for
|
|
55
|
+
* is the one its verified machine identity names and is never a field here. `relay.nodeId` is
|
|
56
|
+
* the cluster node the relay process runs on, `listenAddress` and `listenPort` are the exact
|
|
57
|
+
* endpoint it bound for the Pallet nodes of its cluster, and `registrationSequence` counts up
|
|
58
|
+
* once per registration within one relay process, so Cloudly can tell a newer registration from
|
|
59
|
+
* a replayed older one while both peers are live.
|
|
60
|
+
*/
|
|
61
|
+
export interface IClusterRelayRegistrationV1 {
|
|
62
|
+
schemaVersion: 1;
|
|
63
|
+
relay: {
|
|
64
|
+
/** The relay build that is registering; a bounded label for operators and logs. */
|
|
65
|
+
version: string;
|
|
66
|
+
nodeId: string;
|
|
67
|
+
/**
|
|
68
|
+
* The address the relay bound, as an IPv4 literal.
|
|
69
|
+
*
|
|
70
|
+
* Cloudly publishes this value verbatim as the A record of the relay's certificate domain
|
|
71
|
+
* name, so the contract admits exactly what that record can carry: a hostname would need a
|
|
72
|
+
* `CNAME` and an IPv6 address an `AAAA` record, and neither is published today. Admitting
|
|
73
|
+
* either is an additive change once Cloudly publishes it.
|
|
74
|
+
*/
|
|
75
|
+
listenAddress: string;
|
|
76
|
+
listenPort: number;
|
|
77
|
+
};
|
|
78
|
+
/** Monotonic within one relay process; it restarts at 0 when the process restarts. */
|
|
79
|
+
registrationSequence: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* What Cloudly tells the relay back.
|
|
83
|
+
*
|
|
84
|
+
* `origin` is the origin Cloudly has published for this cluster, so a relay can check that the
|
|
85
|
+
* certificate it holds and the name its nodes are told to reach are the same one. It is `null`
|
|
86
|
+
* while the cluster carries no relay block, which a relay must treat as "not enabled yet"
|
|
87
|
+
* rather than as an origin it may invent.
|
|
88
|
+
*/
|
|
89
|
+
export interface IClusterRelayRegistrationResponseV1 {
|
|
90
|
+
accepted: true;
|
|
91
|
+
origin: string | null;
|
|
92
|
+
}
|
|
51
93
|
/**
|
|
52
94
|
* Validates one cluster relay block and names every reason it is refused.
|
|
53
95
|
*
|
|
@@ -56,6 +98,16 @@ export interface IClusterRelayV1 {
|
|
|
56
98
|
* registers over.
|
|
57
99
|
*/
|
|
58
100
|
export declare const validateClusterRelayV1: (valueArg: unknown, pathArg?: string) => string[];
|
|
101
|
+
/**
|
|
102
|
+
* Validates one cluster relay registration and names every reason it is refused.
|
|
103
|
+
*
|
|
104
|
+
* Shape only, exactly like every other validator here: which cluster the relay speaks for comes
|
|
105
|
+
* from the verified identity on its socket, whether `relay.nodeId` is a node of that cluster is
|
|
106
|
+
* Cloudly's to decide against its own records, and whether this registration supersedes a live
|
|
107
|
+
* one is Cloudly's comparison of `registrationSequence` between peers. A registration that
|
|
108
|
+
* passes here has claimed nothing.
|
|
109
|
+
*/
|
|
110
|
+
export declare const validateClusterRelayRegistrationV1: (valueArg: unknown, pathArg?: string) => string[];
|
|
59
111
|
/**
|
|
60
112
|
* The name the relay's certificate is issued for, and the A record Cloudly
|
|
61
113
|
* owns for it. Derived from the origin so every consumer derives it
|
package/dist_ts/data/cluster.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import {} from '../data/docker.js';
|
|
3
|
+
import { isPlausibleUnicastIpv4 } from '../private/ipv4.js';
|
|
3
4
|
const maximumClusterRelayOriginLength = 2048;
|
|
5
|
+
const maximumClusterRelayVersionLength = 64;
|
|
4
6
|
const isRecord = (valueArg) => (Boolean(valueArg)
|
|
5
7
|
&& typeof valueArg === 'object'
|
|
6
8
|
&& !Array.isArray(valueArg)
|
|
@@ -56,6 +58,71 @@ export const validateClusterRelayV1 = (valueArg, pathArg = 'cluster relay') => {
|
|
|
56
58
|
errors.push(...validateClusterRelayOrigin(valueArg.origin, `${pathArg} origin`));
|
|
57
59
|
return errors;
|
|
58
60
|
};
|
|
61
|
+
const validateClusterRelayVersion = (valueArg, pathArg) => (typeof valueArg === 'string'
|
|
62
|
+
&& valueArg.length <= maximumClusterRelayVersionLength
|
|
63
|
+
&& /^[0-9A-Za-z][0-9A-Za-z.+_-]*$/.test(valueArg)
|
|
64
|
+
? []
|
|
65
|
+
: [`${pathArg} must be 1 to ${maximumClusterRelayVersionLength} characters of letters, `
|
|
66
|
+
+ 'digits, dot, plus, underscore or hyphen, starting with a letter or digit']);
|
|
67
|
+
/** The same node id rule a node's own runtime session registers under. */
|
|
68
|
+
const validateClusterRelayNodeId = (valueArg, pathArg) => (typeof valueArg === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(valueArg)
|
|
69
|
+
? []
|
|
70
|
+
: [`${pathArg} must be 1 to 128 characters of letters, digits, underscore or hyphen`]);
|
|
71
|
+
const validateClusterRelayListenAddress = (valueArg, pathArg) => {
|
|
72
|
+
if (!isPlausibleUnicastIpv4(valueArg)) {
|
|
73
|
+
return [`${pathArg} must be a unicast IPv4 literal`];
|
|
74
|
+
}
|
|
75
|
+
// A relay that bound one of these can never serve the nodes of its cluster, and publishing
|
|
76
|
+
// it would put an unreachable address into every node's relay origin.
|
|
77
|
+
if (valueArg.startsWith('127.')) {
|
|
78
|
+
return [`${pathArg} must not be a loopback address`];
|
|
79
|
+
}
|
|
80
|
+
if (valueArg.startsWith('169.254.')) {
|
|
81
|
+
return [`${pathArg} must not be a link-local address`];
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
};
|
|
85
|
+
const validateClusterRelayListenPort = (valueArg, pathArg) => (typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 1 && valueArg <= 65535
|
|
86
|
+
? []
|
|
87
|
+
: [`${pathArg} must be an integer from 1 to 65535`]);
|
|
88
|
+
const validateClusterRelayEndpoint = (valueArg, pathArg) => {
|
|
89
|
+
if (!isRecord(valueArg)
|
|
90
|
+
|| !hasExactKeys(valueArg, ['version', 'nodeId', 'listenAddress', 'listenPort'])) {
|
|
91
|
+
return [`${pathArg} must use its exact schema`];
|
|
92
|
+
}
|
|
93
|
+
return [
|
|
94
|
+
...validateClusterRelayVersion(valueArg.version, `${pathArg} version`),
|
|
95
|
+
...validateClusterRelayNodeId(valueArg.nodeId, `${pathArg} nodeId`),
|
|
96
|
+
...validateClusterRelayListenAddress(valueArg.listenAddress, `${pathArg} listenAddress`),
|
|
97
|
+
...validateClusterRelayListenPort(valueArg.listenPort, `${pathArg} listenPort`),
|
|
98
|
+
];
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Validates one cluster relay registration and names every reason it is refused.
|
|
102
|
+
*
|
|
103
|
+
* Shape only, exactly like every other validator here: which cluster the relay speaks for comes
|
|
104
|
+
* from the verified identity on its socket, whether `relay.nodeId` is a node of that cluster is
|
|
105
|
+
* Cloudly's to decide against its own records, and whether this registration supersedes a live
|
|
106
|
+
* one is Cloudly's comparison of `registrationSequence` between peers. A registration that
|
|
107
|
+
* passes here has claimed nothing.
|
|
108
|
+
*/
|
|
109
|
+
export const validateClusterRelayRegistrationV1 = (valueArg, pathArg = 'cluster relay registration') => {
|
|
110
|
+
if (!isRecord(valueArg)
|
|
111
|
+
|| !hasExactKeys(valueArg, ['schemaVersion', 'relay', 'registrationSequence'])) {
|
|
112
|
+
return [`${pathArg} must use its exact schema`];
|
|
113
|
+
}
|
|
114
|
+
const errors = [];
|
|
115
|
+
if (valueArg.schemaVersion !== 1) {
|
|
116
|
+
errors.push(`${pathArg} schemaVersion must be 1`);
|
|
117
|
+
}
|
|
118
|
+
errors.push(...validateClusterRelayEndpoint(valueArg.relay, `${pathArg} relay`));
|
|
119
|
+
if (typeof valueArg.registrationSequence !== 'number'
|
|
120
|
+
|| !Number.isSafeInteger(valueArg.registrationSequence)
|
|
121
|
+
|| valueArg.registrationSequence < 0) {
|
|
122
|
+
errors.push(`${pathArg} registrationSequence must be a non-negative safe integer`);
|
|
123
|
+
}
|
|
124
|
+
return errors;
|
|
125
|
+
};
|
|
59
126
|
/**
|
|
60
127
|
* The name the relay's certificate is issued for, and the A record Cloudly
|
|
61
128
|
* owns for it. Derived from the origin so every consumer derives it
|
|
@@ -69,4 +136,4 @@ export const clusterRelayCertificateDomainName = (relayArg) => {
|
|
|
69
136
|
}
|
|
70
137
|
return new URL(relayArg.origin).hostname;
|
|
71
138
|
};
|
|
72
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
139
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3Rlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvY2x1c3Rlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQUV6QyxPQUFPLEVBQTRCLE1BQU0sbUJBQW1CLENBQUM7QUFFN0QsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUF1RzVELE1BQU0sK0JBQStCLEdBQUcsSUFBSSxDQUFDO0FBQzdDLE1BQU0sZ0NBQWdDLEdBQUcsRUFBRSxDQUFDO0FBRTVDLE1BQU0sUUFBUSxHQUFHLENBQUMsUUFBaUIsRUFBdUMsRUFBRSxDQUFDLENBQzNFLE9BQU8sQ0FBQyxRQUFRLENBQUM7T0FDZCxPQUFPLFFBQVEsS0FBSyxRQUFRO09BQzVCLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUM7T0FDeEIsQ0FBQyxNQUFNLENBQUMsY0FBYyxDQUFDLFFBQVEsQ0FBQyxLQUFLLE1BQU0sQ0FBQyxTQUFTO1dBQ25ELE1BQU0sQ0FBQyxjQUFjLENBQUMsUUFBUSxDQUFDLEtBQUssSUFBSSxDQUFDLENBQy9DLENBQUM7QUFFRixNQUFNLFlBQVksR0FBRyxDQUNuQixRQUFpQyxFQUNqQyxPQUEwQixFQUNqQixFQUFFO0lBQ1gsTUFBTSxVQUFVLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUNoRCxNQUFNLFlBQVksR0FBRyxDQUFDLEdBQUcsT0FBTyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDekMsT0FBTyxVQUFVLENBQUMsTUFBTSxLQUFLLFlBQVksQ0FBQyxNQUFNO1dBQzNDLFVBQVUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxNQUFNLEtBQUssWUFBWSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUM7QUFDakYsQ0FBQyxD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
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cluster ingress contracts.
|
|
3
|
+
*
|
|
4
|
+
* A cluster's ingress terminates TLS for the hostnames its services publish and
|
|
5
|
+
* forwards to the host ports Pallet published for the workloads behind them.
|
|
6
|
+
* Cloudly computes the table — it is the only party that knows every service,
|
|
7
|
+
* every ready endpoint and every certificate — and the relay carries it to the
|
|
8
|
+
* ingress and the ingress's counters back.
|
|
9
|
+
*
|
|
10
|
+
* Names here carry no version suffix and no schema field: the protocol version
|
|
11
|
+
* is the installed `@serve.zone/interfaces` version.
|
|
12
|
+
*/
|
|
13
|
+
export declare const clusterIngressLimits: Readonly<{
|
|
14
|
+
minimumIngressBearerLength: 16;
|
|
15
|
+
maximumIngressBearerLength: 4096;
|
|
16
|
+
maximumHostNameLength: 253;
|
|
17
|
+
maximumRoutes: 4096;
|
|
18
|
+
maximumPortRoutes: 1024;
|
|
19
|
+
maximumDestinationsPerRoute: 64;
|
|
20
|
+
maximumStatisticsPageSize: 1000;
|
|
21
|
+
maximumCursorLength: 512;
|
|
22
|
+
maximumIdentifierLength: 200;
|
|
23
|
+
maximumIngressCapabilities: 32;
|
|
24
|
+
}>;
|
|
25
|
+
/** One backend a route forwards to: an address a node answers on, and its port. */
|
|
26
|
+
export interface IClusterRouteDestination {
|
|
27
|
+
address: string;
|
|
28
|
+
port: number;
|
|
29
|
+
}
|
|
30
|
+
/** Basic authentication for a route, as the proxy has always expressed it. */
|
|
31
|
+
export interface IClusterRouteAuthentication {
|
|
32
|
+
type: 'Basic';
|
|
33
|
+
user: string;
|
|
34
|
+
pass: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One hostname the ingress serves.
|
|
38
|
+
*
|
|
39
|
+
* `certificate` carries the private key the ingress needs to terminate TLS for
|
|
40
|
+
* `hostName`. A route table is therefore secret-bearing end to end: it is never
|
|
41
|
+
* logged, never journaled, never written to disk, and no validation reason
|
|
42
|
+
* derived from it ever quotes it.
|
|
43
|
+
*/
|
|
44
|
+
export interface IClusterHttpRoute {
|
|
45
|
+
hostName: string;
|
|
46
|
+
destinations: IClusterRouteDestination[];
|
|
47
|
+
certificate: {
|
|
48
|
+
publicKey: string;
|
|
49
|
+
privateKey: string;
|
|
50
|
+
};
|
|
51
|
+
authentication?: IClusterRouteAuthentication;
|
|
52
|
+
}
|
|
53
|
+
/** One TCP or UDP port the ingress forwards without terminating anything. */
|
|
54
|
+
export interface IClusterPortRoute {
|
|
55
|
+
name: string;
|
|
56
|
+
listenPort: number;
|
|
57
|
+
transport: 'tcp' | 'udp';
|
|
58
|
+
destinations: IClusterRouteDestination[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Everything a cluster's ingress serves, as one table.
|
|
62
|
+
*
|
|
63
|
+
* `revision` is the only idempotency the feed has: an ingress and a relay apply
|
|
64
|
+
* a table whose revision is greater than the one they hold and ignore anything
|
|
65
|
+
* else, so a repeated push, a replayed push and a push that crossed a newer one
|
|
66
|
+
* all settle the same way.
|
|
67
|
+
*/
|
|
68
|
+
export interface IClusterRouteTable {
|
|
69
|
+
revision: number;
|
|
70
|
+
issuedAt: number;
|
|
71
|
+
routes: IClusterHttpRoute[];
|
|
72
|
+
portRoutes: IClusterPortRoute[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* One day of one hostname's traffic on one node, as the ingress counted it.
|
|
76
|
+
*
|
|
77
|
+
* Day buckets rather than rates: a rate answers "how busy is it now", which the
|
|
78
|
+
* live proxy already answers, while a bucket survives a restart, a poll that
|
|
79
|
+
* straddles midnight and a Cloudly outage.
|
|
80
|
+
*/
|
|
81
|
+
export interface IClusterTrafficStatistics {
|
|
82
|
+
serviceId: string;
|
|
83
|
+
hostName: string;
|
|
84
|
+
nodeId: string;
|
|
85
|
+
/** UTC calendar day, `YYYY-MM-DD`. */
|
|
86
|
+
dayUtc: string;
|
|
87
|
+
requests: number;
|
|
88
|
+
bytesIn: number;
|
|
89
|
+
bytesOut: number;
|
|
90
|
+
connections: number;
|
|
91
|
+
/**
|
|
92
|
+
* What the proxy could observe about its backends. HTTP status classes are
|
|
93
|
+
* deliberately absent: the proxy exposes no status counters today, and a
|
|
94
|
+
* number this contract cannot source would be a guess.
|
|
95
|
+
*/
|
|
96
|
+
backendErrors: {
|
|
97
|
+
connect: number;
|
|
98
|
+
handshake: number;
|
|
99
|
+
request: number;
|
|
100
|
+
};
|
|
101
|
+
updatedAt: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A hostname a certificate can be issued for and a browser can present.
|
|
105
|
+
*
|
|
106
|
+
* Lowercase labels only, because a certificate names the lowercase form and a
|
|
107
|
+
* table that carried both would serve one of them without a certificate.
|
|
108
|
+
*/
|
|
109
|
+
export declare const isClusterIngressHostName: (valueArg: unknown) => valueArg is string;
|
|
110
|
+
/**
|
|
111
|
+
* Validates one route table and names every reason it is refused.
|
|
112
|
+
*
|
|
113
|
+
* Shape only. Whether a hostname belongs to the cluster, whether a certificate
|
|
114
|
+
* matches it, and whether a destination is a node of this cluster are decisions
|
|
115
|
+
* for the party that computed the table; a table that passes here has claimed
|
|
116
|
+
* none of them. No reason ever quotes certificate material.
|
|
117
|
+
*/
|
|
118
|
+
export declare const validateClusterRouteTable: (valueArg: unknown, pathArg?: string) => string[];
|
|
119
|
+
/** Validates one day bucket of ingress counters. */
|
|
120
|
+
export declare const validateClusterTrafficStatistics: (valueArg: unknown, pathArg?: string) => string[];
|
|
121
|
+
/**
|
|
122
|
+
* Validates one page of ingress counters, as a reader receives it.
|
|
123
|
+
*
|
|
124
|
+
* The cursor is opaque to everyone but the ingress that issued it: bounded and
|
|
125
|
+
* printable here, meaningful only there.
|
|
126
|
+
*/
|
|
127
|
+
export declare const validateClusterTrafficStatisticsPage: (valueArg: unknown, pathArg?: string) => string[];
|
|
128
|
+
/**
|
|
129
|
+
* Validates one cluster ingress registration and names every reason it is refused.
|
|
130
|
+
*
|
|
131
|
+
* Shape only, and deliberately so: whether the bearer is the one Cloudly minted
|
|
132
|
+
* for this cluster's ingress is Cloudly's to verify against its own records, and
|
|
133
|
+
* which cluster is speaking comes from the identity tag on the relay's session.
|
|
134
|
+
* A registration that passes here has proven nothing — and no reason it returns
|
|
135
|
+
* ever quotes the bearer, because a reason travels into logs.
|
|
136
|
+
*/
|
|
137
|
+
export declare const validateClusterIngressRegistration: (valueArg: unknown, pathArg?: string) => string[];
|