@serve.zone/interfaces 31.3.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 CHANGED
@@ -1,5 +1,52 @@
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
+
3
50
  ## 2026-09-16 - 31.3.0
4
51
 
5
52
  ### Features
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '31.3.0',
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=
@@ -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[];
@@ -0,0 +1,302 @@
1
+ import { isPlausibleUnicastIpv4 } from '../private/ipv4.js';
2
+ import { isUtcCalendarDay } from '../private/utccalendarday.js';
3
+ /**
4
+ * The cluster ingress contracts.
5
+ *
6
+ * A cluster's ingress terminates TLS for the hostnames its services publish and
7
+ * forwards to the host ports Pallet published for the workloads behind them.
8
+ * Cloudly computes the table — it is the only party that knows every service,
9
+ * every ready endpoint and every certificate — and the relay carries it to the
10
+ * ingress and the ingress's counters back.
11
+ *
12
+ * Names here carry no version suffix and no schema field: the protocol version
13
+ * is the installed `@serve.zone/interfaces` version.
14
+ */
15
+ export const clusterIngressLimits = Object.freeze({
16
+ minimumIngressBearerLength: 16,
17
+ maximumIngressBearerLength: 4096,
18
+ maximumHostNameLength: 253,
19
+ maximumRoutes: 4096,
20
+ maximumPortRoutes: 1024,
21
+ maximumDestinationsPerRoute: 64,
22
+ maximumStatisticsPageSize: 1000,
23
+ maximumCursorLength: 512,
24
+ maximumIdentifierLength: 200,
25
+ maximumIngressCapabilities: 32,
26
+ });
27
+ const isRecord = (valueArg) => (Boolean(valueArg)
28
+ && typeof valueArg === 'object'
29
+ && !Array.isArray(valueArg)
30
+ && (Object.getPrototypeOf(valueArg) === Object.prototype
31
+ || Object.getPrototypeOf(valueArg) === null));
32
+ const hasExactKeys = (valueArg, keysArg, optionalKeysArg = []) => {
33
+ const actualKeys = Object.keys(valueArg);
34
+ return keysArg.every((keyArg) => actualKeys.includes(keyArg))
35
+ && actualKeys.every((keyArg) => keysArg.includes(keyArg) || optionalKeysArg.includes(keyArg));
36
+ };
37
+ const isCounter = (valueArg) => (Number.isSafeInteger(valueArg) && valueArg >= 0);
38
+ const isPort = (valueArg) => (Number.isSafeInteger(valueArg) && valueArg >= 1 && valueArg <= 65_535);
39
+ const hostLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
40
+ // Built from the limit rather than beside it, so the two can never disagree.
41
+ const identifierPattern = new RegExp(`^[A-Za-z0-9][A-Za-z0-9:._-]{0,${clusterIngressLimits.maximumIdentifierLength - 1}}$`);
42
+ /**
43
+ * A hostname a certificate can be issued for and a browser can present.
44
+ *
45
+ * Lowercase labels only, because a certificate names the lowercase form and a
46
+ * table that carried both would serve one of them without a certificate.
47
+ */
48
+ export const isClusterIngressHostName = (valueArg) => {
49
+ if (typeof valueArg !== 'string'
50
+ || valueArg.length === 0
51
+ || valueArg.length > clusterIngressLimits.maximumHostNameLength) {
52
+ return false;
53
+ }
54
+ const labels = valueArg.split('.');
55
+ return labels.length >= 2 && labels.every((labelArg) => hostLabelPattern.test(labelArg));
56
+ };
57
+ const validateDestinations = (valueArg, pathArg) => {
58
+ if (!Array.isArray(valueArg)
59
+ || valueArg.length === 0
60
+ || valueArg.length > clusterIngressLimits.maximumDestinationsPerRoute) {
61
+ return [`${pathArg} must list 1 to `
62
+ + `${clusterIngressLimits.maximumDestinationsPerRoute} destinations`];
63
+ }
64
+ const errors = [];
65
+ for (const [index, destination] of valueArg.entries()) {
66
+ const destinationPath = `${pathArg}[${index}]`;
67
+ if (!isRecord(destination) || !hasExactKeys(destination, ['address', 'port'])) {
68
+ errors.push(`${destinationPath} must use its exact schema`);
69
+ continue;
70
+ }
71
+ if (!isPlausibleUnicastIpv4(destination.address)) {
72
+ errors.push(`${destinationPath} address must be a unicast IPv4 literal`);
73
+ }
74
+ if (!isPort(destination.port)) {
75
+ errors.push(`${destinationPath} port must be an integer from 1 to 65535`);
76
+ }
77
+ }
78
+ return errors;
79
+ };
80
+ const validateCertificate = (valueArg, pathArg) => {
81
+ // Presence and shape only. The material itself is never described in a reason:
82
+ // a validation message travels into logs, and this one would carry a key.
83
+ if (!isRecord(valueArg) || !hasExactKeys(valueArg, ['publicKey', 'privateKey'])) {
84
+ return [`${pathArg} must use its exact schema`];
85
+ }
86
+ const errors = [];
87
+ if (typeof valueArg.publicKey !== 'string' || valueArg.publicKey.length === 0) {
88
+ errors.push(`${pathArg} publicKey must be a non-empty PEM string`);
89
+ }
90
+ if (typeof valueArg.privateKey !== 'string' || valueArg.privateKey.length === 0) {
91
+ errors.push(`${pathArg} privateKey must be a non-empty PEM string`);
92
+ }
93
+ return errors;
94
+ };
95
+ const validateAuthentication = (valueArg, pathArg) => {
96
+ if (!isRecord(valueArg) || !hasExactKeys(valueArg, ['type', 'user', 'pass'])) {
97
+ return [`${pathArg} must use its exact schema`];
98
+ }
99
+ return valueArg.type === 'Basic'
100
+ && typeof valueArg.user === 'string' && valueArg.user.length > 0
101
+ && typeof valueArg.pass === 'string' && valueArg.pass.length > 0
102
+ ? []
103
+ : [`${pathArg} must be Basic authentication with a user and a pass`];
104
+ };
105
+ const validateHttpRoute = (valueArg, pathArg) => {
106
+ if (!isRecord(valueArg)
107
+ || !hasExactKeys(valueArg, ['hostName', 'destinations', 'certificate'], ['authentication'])) {
108
+ return [`${pathArg} must use its exact schema`];
109
+ }
110
+ const errors = [];
111
+ if (!isClusterIngressHostName(valueArg.hostName)) {
112
+ errors.push(`${pathArg} hostName must be a lowercase dotted hostname a certificate can name`);
113
+ }
114
+ errors.push(...validateDestinations(valueArg.destinations, `${pathArg} destinations`));
115
+ errors.push(...validateCertificate(valueArg.certificate, `${pathArg} certificate`));
116
+ if (valueArg.authentication !== undefined) {
117
+ errors.push(...validateAuthentication(valueArg.authentication, `${pathArg} authentication`));
118
+ }
119
+ return errors;
120
+ };
121
+ const validatePortRoute = (valueArg, pathArg) => {
122
+ if (!isRecord(valueArg)
123
+ || !hasExactKeys(valueArg, ['name', 'listenPort', 'transport', 'destinations'])) {
124
+ return [`${pathArg} must use its exact schema`];
125
+ }
126
+ const errors = [];
127
+ if (typeof valueArg.name !== 'string' || !identifierPattern.test(valueArg.name)) {
128
+ errors.push(`${pathArg} name must be a bounded identifier`);
129
+ }
130
+ if (!isPort(valueArg.listenPort)) {
131
+ errors.push(`${pathArg} listenPort must be an integer from 1 to 65535`);
132
+ }
133
+ if (valueArg.transport !== 'tcp' && valueArg.transport !== 'udp') {
134
+ errors.push(`${pathArg} transport must be tcp or udp`);
135
+ }
136
+ errors.push(...validateDestinations(valueArg.destinations, `${pathArg} destinations`));
137
+ return errors;
138
+ };
139
+ /**
140
+ * Validates one route table and names every reason it is refused.
141
+ *
142
+ * Shape only. Whether a hostname belongs to the cluster, whether a certificate
143
+ * matches it, and whether a destination is a node of this cluster are decisions
144
+ * for the party that computed the table; a table that passes here has claimed
145
+ * none of them. No reason ever quotes certificate material.
146
+ */
147
+ export const validateClusterRouteTable = (valueArg, pathArg = 'cluster route table') => {
148
+ if (!isRecord(valueArg)
149
+ || !hasExactKeys(valueArg, ['revision', 'issuedAt', 'routes', 'portRoutes'])) {
150
+ return [`${pathArg} must use its exact schema`];
151
+ }
152
+ const errors = [];
153
+ if (!isCounter(valueArg.revision)) {
154
+ errors.push(`${pathArg} revision must be a non-negative safe integer`);
155
+ }
156
+ if (!Number.isSafeInteger(valueArg.issuedAt) || valueArg.issuedAt <= 0) {
157
+ errors.push(`${pathArg} issuedAt must be a positive epoch millisecond timestamp`);
158
+ }
159
+ if (!Array.isArray(valueArg.routes)
160
+ || valueArg.routes.length > clusterIngressLimits.maximumRoutes) {
161
+ errors.push(`${pathArg} routes must list at most ${clusterIngressLimits.maximumRoutes} routes`);
162
+ }
163
+ else {
164
+ const seenHostNames = new Set();
165
+ for (const [index, route] of valueArg.routes.entries()) {
166
+ errors.push(...validateHttpRoute(route, `${pathArg} routes[${index}]`));
167
+ const hostName = isRecord(route) ? route.hostName : undefined;
168
+ if (typeof hostName === 'string') {
169
+ if (seenHostNames.has(hostName)) {
170
+ errors.push(`${pathArg} routes[${index}] repeats hostName ${hostName}`);
171
+ }
172
+ seenHostNames.add(hostName);
173
+ }
174
+ }
175
+ }
176
+ if (!Array.isArray(valueArg.portRoutes)
177
+ || valueArg.portRoutes.length > clusterIngressLimits.maximumPortRoutes) {
178
+ errors.push(`${pathArg} portRoutes must list at most `
179
+ + `${clusterIngressLimits.maximumPortRoutes} port routes`);
180
+ }
181
+ else {
182
+ const seenPorts = new Set();
183
+ for (const [index, portRoute] of valueArg.portRoutes.entries()) {
184
+ errors.push(...validatePortRoute(portRoute, `${pathArg} portRoutes[${index}]`));
185
+ if (!isRecord(portRoute)) {
186
+ continue;
187
+ }
188
+ const key = `${String(portRoute.transport)}:${String(portRoute.listenPort)}`;
189
+ if (seenPorts.has(key)) {
190
+ errors.push(`${pathArg} portRoutes[${index}] repeats ${key}`);
191
+ }
192
+ seenPorts.add(key);
193
+ }
194
+ }
195
+ return errors;
196
+ };
197
+ /** Validates one day bucket of ingress counters. */
198
+ export const validateClusterTrafficStatistics = (valueArg, pathArg = 'cluster traffic statistics') => {
199
+ if (!isRecord(valueArg) || !hasExactKeys(valueArg, [
200
+ 'serviceId', 'hostName', 'nodeId', 'dayUtc', 'requests', 'bytesIn', 'bytesOut',
201
+ 'connections', 'backendErrors', 'updatedAt',
202
+ ])) {
203
+ return [`${pathArg} must use its exact schema`];
204
+ }
205
+ const errors = [];
206
+ for (const key of ['serviceId', 'nodeId']) {
207
+ const value = valueArg[key];
208
+ if (typeof value !== 'string' || !identifierPattern.test(value)) {
209
+ errors.push(`${pathArg} ${key} must be a bounded identifier`);
210
+ }
211
+ }
212
+ if (!isClusterIngressHostName(valueArg.hostName)) {
213
+ errors.push(`${pathArg} hostName must be a lowercase dotted hostname`);
214
+ }
215
+ if (!isUtcCalendarDay(valueArg.dayUtc)) {
216
+ errors.push(`${pathArg} dayUtc must be a real YYYY-MM-DD UTC calendar day`);
217
+ }
218
+ for (const key of ['requests', 'bytesIn', 'bytesOut', 'connections']) {
219
+ if (!isCounter(valueArg[key])) {
220
+ errors.push(`${pathArg} ${key} must be a non-negative safe integer`);
221
+ }
222
+ }
223
+ const backendErrors = valueArg.backendErrors;
224
+ if (!isRecord(backendErrors)
225
+ || !hasExactKeys(backendErrors, ['connect', 'handshake', 'request'])
226
+ || !isCounter(backendErrors.connect)
227
+ || !isCounter(backendErrors.handshake)
228
+ || !isCounter(backendErrors.request)) {
229
+ errors.push(`${pathArg} backendErrors must count connect, handshake and request failures`);
230
+ }
231
+ if (!Number.isSafeInteger(valueArg.updatedAt) || valueArg.updatedAt <= 0) {
232
+ errors.push(`${pathArg} updatedAt must be a positive epoch millisecond timestamp`);
233
+ }
234
+ return errors;
235
+ };
236
+ /**
237
+ * Validates one page of ingress counters, as a reader receives it.
238
+ *
239
+ * The cursor is opaque to everyone but the ingress that issued it: bounded and
240
+ * printable here, meaningful only there.
241
+ */
242
+ export const validateClusterTrafficStatisticsPage = (valueArg, pathArg = 'cluster traffic statistics page') => {
243
+ if (!isRecord(valueArg) || !hasExactKeys(valueArg, ['statistics'], ['nextCursor'])) {
244
+ return [`${pathArg} must use its exact schema`];
245
+ }
246
+ const errors = [];
247
+ if (!Array.isArray(valueArg.statistics)
248
+ || valueArg.statistics.length > clusterIngressLimits.maximumStatisticsPageSize) {
249
+ errors.push(`${pathArg} statistics must list at most `
250
+ + `${clusterIngressLimits.maximumStatisticsPageSize} entries`);
251
+ }
252
+ else {
253
+ for (const [index, item] of valueArg.statistics.entries()) {
254
+ errors.push(...validateClusterTrafficStatistics(item, `${pathArg} statistics[${index}]`));
255
+ }
256
+ }
257
+ if (valueArg.nextCursor !== undefined
258
+ && (typeof valueArg.nextCursor !== 'string'
259
+ || valueArg.nextCursor.length === 0
260
+ || valueArg.nextCursor.length > clusterIngressLimits.maximumCursorLength)) {
261
+ errors.push(`${pathArg} nextCursor must be 1 to `
262
+ + `${clusterIngressLimits.maximumCursorLength} characters when present`);
263
+ }
264
+ return errors;
265
+ };
266
+ /**
267
+ * Validates one cluster ingress registration and names every reason it is refused.
268
+ *
269
+ * Shape only, and deliberately so: whether the bearer is the one Cloudly minted
270
+ * for this cluster's ingress is Cloudly's to verify against its own records, and
271
+ * which cluster is speaking comes from the identity tag on the relay's session.
272
+ * A registration that passes here has proven nothing — and no reason it returns
273
+ * ever quotes the bearer, because a reason travels into logs.
274
+ */
275
+ export const validateClusterIngressRegistration = (valueArg, pathArg = 'cluster ingress registration') => {
276
+ if (!isRecord(valueArg)
277
+ || !hasExactKeys(valueArg, ['bearer', 'version', 'nodeId'], ['capabilities'])) {
278
+ return [`${pathArg} must use its exact schema`];
279
+ }
280
+ const errors = [];
281
+ if (typeof valueArg.bearer !== 'string'
282
+ || valueArg.bearer.length < clusterIngressLimits.minimumIngressBearerLength
283
+ || valueArg.bearer.length > clusterIngressLimits.maximumIngressBearerLength) {
284
+ errors.push(`${pathArg} bearer must be ${clusterIngressLimits.minimumIngressBearerLength} to `
285
+ + `${clusterIngressLimits.maximumIngressBearerLength} characters`);
286
+ }
287
+ if (typeof valueArg.version !== 'string' || !identifierPattern.test(valueArg.version)) {
288
+ errors.push(`${pathArg} version must be a bounded identifier`);
289
+ }
290
+ if (typeof valueArg.nodeId !== 'string' || !identifierPattern.test(valueArg.nodeId)) {
291
+ errors.push(`${pathArg} nodeId must be a bounded identifier`);
292
+ }
293
+ if (valueArg.capabilities !== undefined
294
+ && (!Array.isArray(valueArg.capabilities)
295
+ || valueArg.capabilities.length > clusterIngressLimits.maximumIngressCapabilities
296
+ || !valueArg.capabilities.every((capabilityArg) => (typeof capabilityArg === 'string' && identifierPattern.test(capabilityArg))))) {
297
+ errors.push(`${pathArg} capabilities must be at most `
298
+ + `${clusterIngressLimits.maximumIngressCapabilities} bounded identifiers when present`);
299
+ }
300
+ return errors;
301
+ };
302
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3RlcmluZ3Jlc3MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2NsdXN0ZXJpbmdyZXNzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzVELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBRWhFOzs7Ozs7Ozs7OztHQVdHO0FBRUgsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUNoRCwwQkFBMEIsRUFBRSxFQUFFO0lBQzlCLDBCQUEwQixFQUFFLElBQUk7SUFDaEMscUJBQXFCLEVBQUUsR0FBRztJQUMxQixhQUFhLEVBQUUsSUFBSTtJQUNuQixpQkFBaUIsRUFBRSxJQUFJO0lBQ3ZCLDJCQUEyQixFQUFFLEVBQUU7SUFDL0IseUJBQXlCLEVBQUUsSUFBSTtJQUMvQixtQkFBbUIsRUFBRSxHQUFHO0lBQ3hCLHVCQUF1QixFQUFFLEdBQUc7SUFDNUIsMEJBQTBCLEVBQUUsRUFBRTtDQUMvQixDQUFDLENBQUM7QUFzRkgsTUFBTSxRQUFRLEdBQUcsQ0FBQyxRQUFpQixFQUF1QyxFQUFFLENBQUMsQ0FDM0UsT0FBTyxDQUFDLFFBQVEsQ0FBQztPQUNkLE9BQU8sUUFBUSxLQUFLLFFBQVE7T0FDNUIsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQztPQUN4QixDQUFDLE1BQU0sQ0FBQyxjQUFjLENBQUMsUUFBUSxDQUFDLEtBQUssTUFBTSxDQUFDLFNBQVM7V0FDbkQsTUFBTSxDQUFDLGNBQWMsQ0FBQyxRQUFRLENBQUMsS0FBSyxJQUFJLENBQUMsQ0FDL0MsQ0FBQztBQUVGLE1BQU0sWUFBWSxHQUFHLENBQ25CLFFBQWlDLEVBQ2pDLE9BQTBCLEVBQzFCLGtCQUFxQyxFQUFFLEVBQzlCLEVBQUU7SUFDWCxNQUFNLFVBQVUsR0FBRyxNQUFNLENBQUMsSUFB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