@serve.zone/interfaces 31.1.0 → 31.2.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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-16 - 31.2.0
4
+
5
+ ### Features
6
+
7
+ - Add the optional `ICluster.data.relay` block, `IClusterRelayV1 { schemaVersion: 1; origin: string }`:
8
+ how cluster-side components reach this cluster's relay, absent on a cluster whose nodes still
9
+ target Cloudly directly, so its presence is what marks a cluster as relayed. `origin` is the
10
+ exact value a Pallet node stores as its `relayOrigin` — `https:`, an explicit port allowed, no
11
+ path, query, fragment, userinfo or trailing slash, so `new URL(origin).origin === origin`; a
12
+ default `:443` and a cased host are therefore not canonical, because `URL` drops the one and
13
+ lowercases the other. `validateClusterRelayV1(value, pathArg?)` returns one named reason per
14
+ violation and an empty array for a valid block, following the package's value-free validator
15
+ shape. The certificate subject is deliberately not a second field: every consumer derives it
16
+ from the origin with `clusterRelayCertificateDomainName(relay)`, which returns the origin's
17
+ hostname — the name Cloudly issues the certificate for and owns the A record for — so two
18
+ fields can never disagree. An origin naming an address literal consequently has no issuable
19
+ certificate. Additive only: a cluster document without `relay` is unchanged, and nothing was
20
+ removed or renamed.
21
+
3
22
  ## 2026-09-16 - 31.1.0
4
23
 
5
24
  ### Features
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '31.1.0',
6
+ version: '31.2.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=
@@ -28,5 +28,38 @@ export interface ICluster {
28
28
  serverSecret: string;
29
29
  };
30
30
  sshKeys: plugins.tsclass.network.ISshKey[];
31
+ /**
32
+ * How cluster-side components reach this cluster's relay. Absent on a
33
+ * cluster whose nodes still target Cloudly directly.
34
+ */
35
+ relay?: IClusterRelayV1;
31
36
  };
32
37
  }
38
+ /**
39
+ * How cluster-side components reach this cluster's relay.
40
+ *
41
+ * `origin` is the exact value a Pallet node stores as its `relayOrigin`: an
42
+ * `https:` origin, an explicit port allowed, with no path, no query, no
43
+ * fragment, no userinfo and no trailing slash, so that
44
+ * `new URL(origin).origin === origin`. A default `:443` is therefore written
45
+ * implicitly, because `URL` drops it from the origin it computes.
46
+ */
47
+ export interface IClusterRelayV1 {
48
+ schemaVersion: 1;
49
+ origin: string;
50
+ }
51
+ /**
52
+ * Validates one cluster relay block and names every reason it is refused.
53
+ *
54
+ * A relay whose origin cannot be trusted verbatim must never reach a node's
55
+ * identity store: it is the transport authority the node's runtime session
56
+ * registers over.
57
+ */
58
+ export declare const validateClusterRelayV1: (valueArg: unknown, pathArg?: string) => string[];
59
+ /**
60
+ * The name the relay's certificate is issued for, and the A record Cloudly
61
+ * owns for it. Derived from the origin so every consumer derives it
62
+ * identically; an origin that names an address literal instead of a domain
63
+ * therefore has no issuable certificate.
64
+ */
65
+ export declare const clusterRelayCertificateDomainName: (relayArg: IClusterRelayV1) => string;
@@ -1,3 +1,72 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import {} from '../data/docker.js';
3
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3Rlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvY2x1c3Rlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQUV6QyxPQUFPLEVBQTRCLE1BQU0sbUJBQW1CLENBQUMifQ==
3
+ const maximumClusterRelayOriginLength = 2048;
4
+ const isRecord = (valueArg) => (Boolean(valueArg)
5
+ && typeof valueArg === 'object'
6
+ && !Array.isArray(valueArg)
7
+ && (Object.getPrototypeOf(valueArg) === Object.prototype
8
+ || Object.getPrototypeOf(valueArg) === null));
9
+ const hasExactKeys = (valueArg, keysArg) => {
10
+ const actualKeys = Object.keys(valueArg).sort();
11
+ const expectedKeys = [...keysArg].sort();
12
+ return actualKeys.length === expectedKeys.length
13
+ && actualKeys.every((keyArg, indexArg) => keyArg === expectedKeys[indexArg]);
14
+ };
15
+ const validateClusterRelayOrigin = (valueArg, pathArg) => {
16
+ if (typeof valueArg !== 'string'
17
+ || valueArg.length === 0
18
+ || valueArg.length > maximumClusterRelayOriginLength) {
19
+ return [`${pathArg} must be a string of 1 to ${maximumClusterRelayOriginLength} characters`];
20
+ }
21
+ let parsedOrigin;
22
+ try {
23
+ parsedOrigin = new URL(valueArg);
24
+ }
25
+ catch {
26
+ return [`${pathArg} must be a parsable URL`];
27
+ }
28
+ const errors = [];
29
+ if (parsedOrigin.protocol !== 'https:') {
30
+ errors.push(`${pathArg} must use the https scheme`);
31
+ }
32
+ if (parsedOrigin.username || parsedOrigin.password) {
33
+ errors.push(`${pathArg} must not carry userinfo`);
34
+ }
35
+ if (parsedOrigin.origin !== valueArg) {
36
+ errors.push(`${pathArg} must be the canonical origin ${parsedOrigin.origin}, `
37
+ + 'without a path, query, fragment, trailing slash or default port');
38
+ }
39
+ return errors;
40
+ };
41
+ /**
42
+ * Validates one cluster relay block and names every reason it is refused.
43
+ *
44
+ * A relay whose origin cannot be trusted verbatim must never reach a node's
45
+ * identity store: it is the transport authority the node's runtime session
46
+ * registers over.
47
+ */
48
+ export const validateClusterRelayV1 = (valueArg, pathArg = 'cluster relay') => {
49
+ if (!isRecord(valueArg) || !hasExactKeys(valueArg, ['schemaVersion', 'origin'])) {
50
+ return [`${pathArg} must use its exact schema`];
51
+ }
52
+ const errors = [];
53
+ if (valueArg.schemaVersion !== 1) {
54
+ errors.push(`${pathArg} schemaVersion must be 1`);
55
+ }
56
+ errors.push(...validateClusterRelayOrigin(valueArg.origin, `${pathArg} origin`));
57
+ return errors;
58
+ };
59
+ /**
60
+ * The name the relay's certificate is issued for, and the A record Cloudly
61
+ * owns for it. Derived from the origin so every consumer derives it
62
+ * identically; an origin that names an address literal instead of a domain
63
+ * therefore has no issuable certificate.
64
+ */
65
+ export const clusterRelayCertificateDomainName = (relayArg) => {
66
+ const errors = validateClusterRelayV1(relayArg);
67
+ if (errors.length > 0) {
68
+ throw new Error(errors[0]);
69
+ }
70
+ return new URL(relayArg.origin).hostname;
71
+ };
72
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3Rlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvY2x1c3Rlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQUV6QyxPQUFPLEVBQTRCLE1BQU0sbUJBQW1CLENBQUM7QUE0RDdELE1BQU0sK0JBQStCLEdBQUcsSUFBSSxDQUFDO0FBRTdDLE1BQU0sUUFBUSxHQUFHLENBQUMsUUFBaUIsRUFBdUMsRUFBRSxDQUFDLENBQzNFLE9BQU8sQ0FBQyxRQUFRLENBQUM7T0FDZCxPQUFPLFFBQVEsS0FBSyxRQUFRO09BQzVCLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUM7T0FDeEIsQ0FBQyxNQUFNLENBQUMsY0FBYyxDQUFDLFFBQVEsQ0FBQyxLQUFLLE1BQU0sQ0FBQyxTQUFTO1dBQ25ELE1BQU0sQ0FBQyxjQUFjLENBQUMsUUFBUSxDQUFDLEtBQUssSUFBSSxDQUFDLENBQy9DLENBQUM7QUFFRixNQUFNLFlBQVksR0FBRyxDQUNuQixRQUFpQyxFQUNqQyxPQUEwQixFQUNqQixFQUFFO0lBQ1gsTUFBTSxVQUFVLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUNoRCxNQUFNLFlBQVksR0FBRyxDQUFDLEdBQUcsT0FBTyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7SUFDekMsT0FBTyxVQUFVLENBQUMsTUFBTSxLQUFLLFlBQVksQ0FBQyxNQUFNO1dBQzNDLFVBQVUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxNQUFNLEtBQUssWUFBWSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUM7QUFDakYsQ0FBQyxDQUFDO0FBRUYsTUFBTSwwQkFBMEIsR0FBRyxDQUFDLFFBQWlCLEVBQUUsT0FBZSxFQUFZLEVBQUU7SUFDbEYsSUFBSSxPQUFPLFFBQVEs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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "31.1.0",
3
+ "version": "31.2.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
package/readme.md CHANGED
@@ -523,7 +523,9 @@ Existing numeric configurations retain their exact meaning and digest.
523
523
  `requests.runtimesession` defines `registerPalletRuntimeSessionV1`,
524
524
  `applyRuntimeAssignmentV1`, `reportRuntimeAssignmentObservationV1`, and
525
525
  `getRuntimeRegistryCredentialV1`. Pallet initiates WSS through its configured
526
- Cloudly HTTPS origin. The receiver derives the physical peer, process instance
526
+ Cloudly HTTPS origin, or, on a relayed cluster, through the relay origin its
527
+ cluster publishes in [`ICluster.data.relay`](#cluster-relay-block) while Cloudly
528
+ remains the credential authority. The receiver derives the physical peer, process instance
527
529
  and registration idempotency; callers cannot supply those authorities. A serialized
528
530
  `IRuntimeSessionBindingV1` provides content to compare with authenticated peer
529
531
  state and does not itself authenticate a connection. Registry credentials are
@@ -1054,7 +1056,8 @@ const service: data.IService = {
1054
1056
 
1055
1057
  Common data contracts include:
1056
1058
 
1057
- - `ICluster` and `IClusterNode` for cluster membership and provisioning state.
1059
+ - `ICluster` and `IClusterNode` for cluster membership and provisioning state, including the
1060
+ optional `ICluster.data.relay` block described below.
1058
1061
  - `IService`, `IDeployment`, `IImage`, `IRegistryTarget`, and `IExternalRegistry` for workload delivery.
1059
1062
  - Service port contracts including `IServiceTargetPort`, `IServiceDomainRoute`, and `IServicePublicPortMapping` for canonical backend targets, domain target references, and edge/Coretraffic TCP/UDP public exposure.
1060
1063
  - `IDomain`, `IDnsEntry`, and traffic contracts for routing and DNS management.
@@ -1080,6 +1083,36 @@ Common data contracts include:
1080
1083
  - `IUser`, JWT-only `IIdentityCredential`, full `IIdentity`, and token-related contracts for authentication context. `IIdentity` extends `IIdentityCredential` with server-issued user metadata.
1081
1084
  - `ICloudlyConfig`, `ICloudlySettings`, status, server, bare-metal, BaseOS, backup, and task execution interfaces for control-plane state.
1082
1085
 
1086
+ ### Cluster Relay Block
1087
+
1088
+ `ICluster.data.relay` is the optional `IClusterRelayV1 { schemaVersion: 1; origin: string }`
1089
+ that states how cluster-side components reach this cluster's relay. It is absent on a cluster
1090
+ whose nodes still target Cloudly directly, so its presence is what marks a cluster as relayed.
1091
+
1092
+ `origin` is the exact value a Pallet node stores as its `relayOrigin` — the origin its runtime
1093
+ session registers over, as described in [Authenticated Runtime Sessions](#authenticated-runtime-sessions).
1094
+ It must be an `https:` origin with no path, query, fragment, userinfo or trailing slash, so that
1095
+ `new URL(origin).origin === origin`; an explicit port is allowed, and because `URL` drops a
1096
+ default port and lowercases the host, `https://relay.example:443` and `https://Relay.example`
1097
+ are not canonical origins. `validateClusterRelayV1(value, path?)` returns one named reason per
1098
+ violation and an empty array for a valid block.
1099
+
1100
+ The relay's certificate subject is not a second field: every consumer derives it from the origin
1101
+ with `clusterRelayCertificateDomainName(relay)`, which returns the origin's hostname — the name
1102
+ Cloudly issues the certificate for and owns the A record for. An origin naming an address literal
1103
+ therefore has no issuable certificate.
1104
+
1105
+ ```typescript
1106
+ const relay: data.IClusterRelayV1 = {
1107
+ schemaVersion: 1,
1108
+ origin: 'https://relay.cluster-a.serve.zone:8443',
1109
+ };
1110
+
1111
+ const reasons = data.validateClusterRelayV1(relay); // []
1112
+ const certificateDomainName = data.clusterRelayCertificateDomainName(relay);
1113
+ // 'relay.cluster-a.serve.zone'
1114
+ ```
1115
+
1083
1116
  Shared service port helpers are exported from `data` so Cloudly, App Store resolution, Coreflow, Coretraffic, Onebox, and dcrouter agree on the same normalization rules:
1084
1117
 
1085
1118
  ```typescript
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '31.1.0',
6
+ version: '31.2.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -37,5 +37,109 @@ export interface ICluster {
37
37
  };
38
38
 
39
39
  sshKeys: plugins.tsclass.network.ISshKey[];
40
+
41
+ /**
42
+ * How cluster-side components reach this cluster's relay. Absent on a
43
+ * cluster whose nodes still target Cloudly directly.
44
+ */
45
+ relay?: IClusterRelayV1;
40
46
  };
41
47
  }
48
+
49
+ /**
50
+ * How cluster-side components reach this cluster's relay.
51
+ *
52
+ * `origin` is the exact value a Pallet node stores as its `relayOrigin`: an
53
+ * `https:` origin, an explicit port allowed, with no path, no query, no
54
+ * fragment, no userinfo and no trailing slash, so that
55
+ * `new URL(origin).origin === origin`. A default `:443` is therefore written
56
+ * implicitly, because `URL` drops it from the origin it computes.
57
+ */
58
+ export interface IClusterRelayV1 {
59
+ schemaVersion: 1;
60
+ origin: string;
61
+ }
62
+
63
+ const maximumClusterRelayOriginLength = 2048;
64
+
65
+ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => (
66
+ Boolean(valueArg)
67
+ && typeof valueArg === 'object'
68
+ && !Array.isArray(valueArg)
69
+ && (Object.getPrototypeOf(valueArg) === Object.prototype
70
+ || Object.getPrototypeOf(valueArg) === null)
71
+ );
72
+
73
+ const hasExactKeys = (
74
+ valueArg: Record<string, unknown>,
75
+ keysArg: readonly string[],
76
+ ): boolean => {
77
+ const actualKeys = Object.keys(valueArg).sort();
78
+ const expectedKeys = [...keysArg].sort();
79
+ return actualKeys.length === expectedKeys.length
80
+ && actualKeys.every((keyArg, indexArg) => keyArg === expectedKeys[indexArg]);
81
+ };
82
+
83
+ const validateClusterRelayOrigin = (valueArg: unknown, pathArg: string): string[] => {
84
+ if (typeof valueArg !== 'string'
85
+ || valueArg.length === 0
86
+ || valueArg.length > maximumClusterRelayOriginLength) {
87
+ return [`${pathArg} must be a string of 1 to ${maximumClusterRelayOriginLength} characters`];
88
+ }
89
+ let parsedOrigin: URL;
90
+ try {
91
+ parsedOrigin = new URL(valueArg);
92
+ } catch {
93
+ return [`${pathArg} must be a parsable URL`];
94
+ }
95
+ const errors: string[] = [];
96
+ if (parsedOrigin.protocol !== 'https:') {
97
+ errors.push(`${pathArg} must use the https scheme`);
98
+ }
99
+ if (parsedOrigin.username || parsedOrigin.password) {
100
+ errors.push(`${pathArg} must not carry userinfo`);
101
+ }
102
+ if (parsedOrigin.origin !== valueArg) {
103
+ errors.push(
104
+ `${pathArg} must be the canonical origin ${parsedOrigin.origin}, `
105
+ + 'without a path, query, fragment, trailing slash or default port',
106
+ );
107
+ }
108
+ return errors;
109
+ };
110
+
111
+ /**
112
+ * Validates one cluster relay block and names every reason it is refused.
113
+ *
114
+ * A relay whose origin cannot be trusted verbatim must never reach a node's
115
+ * identity store: it is the transport authority the node's runtime session
116
+ * registers over.
117
+ */
118
+ export const validateClusterRelayV1 = (
119
+ valueArg: unknown,
120
+ pathArg = 'cluster relay',
121
+ ): string[] => {
122
+ if (!isRecord(valueArg) || !hasExactKeys(valueArg, ['schemaVersion', 'origin'])) {
123
+ return [`${pathArg} must use its exact schema`];
124
+ }
125
+ const errors: string[] = [];
126
+ if (valueArg.schemaVersion !== 1) {
127
+ errors.push(`${pathArg} schemaVersion must be 1`);
128
+ }
129
+ errors.push(...validateClusterRelayOrigin(valueArg.origin, `${pathArg} origin`));
130
+ return errors;
131
+ };
132
+
133
+ /**
134
+ * The name the relay's certificate is issued for, and the A record Cloudly
135
+ * owns for it. Derived from the origin so every consumer derives it
136
+ * identically; an origin that names an address literal instead of a domain
137
+ * therefore has no issuable certificate.
138
+ */
139
+ export const clusterRelayCertificateDomainName = (relayArg: IClusterRelayV1): string => {
140
+ const errors = validateClusterRelayV1(relayArg);
141
+ if (errors.length > 0) {
142
+ throw new Error(errors[0]);
143
+ }
144
+ return new URL(relayArg.origin).hostname;
145
+ };