@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 +19 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/cluster.d.ts +33 -0
- package/dist_ts/data/cluster.js +70 -1
- package/package.json +1 -1
- package/readme.md +35 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/cluster.ts +104 -0
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.
|
|
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;
|
package/dist_ts/data/cluster.js
CHANGED
|
@@ -1,3 +1,72 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import {} from '../data/docker.js';
|
|
3
|
-
|
|
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
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
|
|
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
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/cluster.ts
CHANGED
|
@@ -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
|
+
};
|