@serve.zone/interfaces 32.28.0 → 32.30.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 +81 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/cluster.d.ts +1 -1
- package/dist_ts/data/cluster.js +6 -1
- package/dist_ts/data/networkaddressplan.d.ts +27 -3
- package/dist_ts/data/networkaddressplan.js +38 -2
- package/dist_ts/data/networkprojection.d.ts +74 -3
- package/dist_ts/data/networkprojection.js +21 -2
- package/dist_ts/data/secret.d.ts +11 -0
- package/dist_ts/data/secret.js +13 -1
- package/dist_ts/data/serviceruntime.d.ts +19 -3
- package/dist_ts/data/serviceruntime.js +2 -2
- package/dist_ts/data/settings.d.ts +51 -0
- package/dist_ts/data/settings.js +83 -1
- package/dist_ts/platform/corestoreendpointselection.d.ts +37 -0
- package/dist_ts/platform/corestoreendpointselection.js +45 -0
- package/dist_ts/platform/index.d.ts +3 -1
- package/dist_ts/platform/index.js +4 -2
- package/dist_ts/platform/objectstorage.js +5 -1
- package/dist_ts/private/networkcontract.d.ts +20 -0
- package/dist_ts/private/networkcontract.js +31 -9
- package/dist_ts/requests/network.d.ts +62 -3
- package/dist_ts/requests/settings.d.ts +56 -2
- package/dist_ts/requests/settings.js +91 -1
- package/package.json +3 -3
- package/readme.md +151 -12
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/cluster.ts +5 -0
- package/ts/data/networkaddressplan.ts +66 -4
- package/ts/data/networkprojection.ts +96 -4
- package/ts/data/secret.ts +16 -0
- package/ts/data/serviceruntime.ts +19 -3
- package/ts/data/settings.ts +127 -0
- package/ts/platform/corestoreendpointselection.ts +79 -0
- package/ts/platform/index.ts +3 -0
- package/ts/platform/objectstorage.ts +4 -0
- package/ts/private/networkcontract.ts +43 -4
- package/ts/requests/network.ts +58 -3
- package/ts/requests/settings.ts +162 -2
|
@@ -3,7 +3,7 @@ import type { IIdentity, IIdentityCredential } from '../data/user.js';
|
|
|
3
3
|
import type { IRuntimeNetworkAddressPlan, TRuntimeNetworkAddressPlanWritableData } from '../data/networkaddressplan.js';
|
|
4
4
|
import type { IRuntimeNetworkNodeReadiness, IRuntimeRouterIncarnation } from '../data/networknode.js';
|
|
5
5
|
import type { IPrivateNetwork, IServicePrivateNetworks, ISetServicePrivateNetworks, TPrivateNetworkMutation } from '../data/privatenetwork.js';
|
|
6
|
-
import type { IServicePublishedPorts, ISetServicePublishedPorts } from '../data/networkprojection.js';
|
|
6
|
+
import type { IServiceNetworkEgress, IServicePublishedPorts, ISetServiceNetworkEgress, ISetServicePublishedPorts } from '../data/networkprojection.js';
|
|
7
7
|
/** Current organization picker data. Entries are never cached authorization grants. */
|
|
8
8
|
export interface IReq_Cloudly_ListOwnedOrganizations extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_ListOwnedOrganizations> {
|
|
9
9
|
method: 'listOwnedOrganizations';
|
|
@@ -97,7 +97,7 @@ export interface IReq_Cloudly_SetServicePrivateNetworks extends plugins.typedreq
|
|
|
97
97
|
replayed: boolean;
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
-
/** null means the service has never had
|
|
100
|
+
/** null means the service has never had a policy document, neither published ports nor egress. */
|
|
101
101
|
export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_GetServicePublishedPorts> {
|
|
102
102
|
method: 'getServicePublishedPorts';
|
|
103
103
|
request: {
|
|
@@ -109,6 +109,17 @@ export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequ
|
|
|
109
109
|
publishedPorts: IServicePublishedPorts | null;
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Writes the published-port half of one service's policy document, carrying its egress forward.
|
|
114
|
+
*
|
|
115
|
+
* One document per service holds its published ports and its egress under one `revision`. The
|
|
116
|
+
* first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
117
|
+
* with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
118
|
+
* no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
119
|
+
* attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
120
|
+
* no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
121
|
+
* document at all.
|
|
122
|
+
*/
|
|
112
123
|
export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_SetServicePublishedPorts> {
|
|
113
124
|
method: 'setServicePublishedPorts';
|
|
114
125
|
request: {
|
|
@@ -125,6 +136,53 @@ export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequ
|
|
|
125
136
|
replayed: boolean;
|
|
126
137
|
};
|
|
127
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* The egress half of one service's policy document; null means the service has never had a policy
|
|
141
|
+
* document. Only a verified administrator reads it: platform endpoints are platform authority.
|
|
142
|
+
*/
|
|
143
|
+
export interface IReq_Admin_Cloudly_GetServiceNetworkEgress extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_GetServiceNetworkEgress> {
|
|
144
|
+
method: 'getServiceNetworkEgress';
|
|
145
|
+
request: {
|
|
146
|
+
identity: IIdentityCredential;
|
|
147
|
+
organizationId: string;
|
|
148
|
+
serviceId: string;
|
|
149
|
+
};
|
|
150
|
+
response: {
|
|
151
|
+
networkEgress: IServiceNetworkEgress | null;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Writes the egress half of one service's policy document, carrying its published ports forward.
|
|
156
|
+
*
|
|
157
|
+
* Only a verified administrator writes it, as only an administrator writes the runtime spec whose
|
|
158
|
+
* attached network must state the same values. `expectedRevision` is the revision of the one
|
|
159
|
+
* document shared with the published ports; any other stored revision refuses the write, and an id
|
|
160
|
+
* that names no current platform endpoint refuses it. A retry of the exact body under the same
|
|
161
|
+
* `mutationId` answers the stored document with `replayed: true`.
|
|
162
|
+
*
|
|
163
|
+
* One document per service holds its published ports and its egress under one `revision`. The
|
|
164
|
+
* first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
165
|
+
* with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
166
|
+
* no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
167
|
+
* attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
168
|
+
* no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
169
|
+
* document at all.
|
|
170
|
+
*/
|
|
171
|
+
export interface IReq_Admin_Cloudly_SetServiceNetworkEgress extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_SetServiceNetworkEgress> {
|
|
172
|
+
method: 'setServiceNetworkEgress';
|
|
173
|
+
request: {
|
|
174
|
+
identity: IIdentityCredential;
|
|
175
|
+
mutation: ISetServiceNetworkEgress;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* The accepted document including its `revision`; historical on replay. Acceptance is neither an
|
|
179
|
+
* admitted workload nor applied packet state.
|
|
180
|
+
*/
|
|
181
|
+
response: {
|
|
182
|
+
networkEgress: IServiceNetworkEgress;
|
|
183
|
+
replayed: boolean;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
128
186
|
export interface IRequest_Any_Cloudly_GetNetworkNodes {
|
|
129
187
|
method: 'getNetworkNodes';
|
|
130
188
|
request: {};
|
|
@@ -139,7 +197,8 @@ export interface IRequest_Any_Cloudly_GetNetworkNodes {
|
|
|
139
197
|
* validates the plan with `snapshotRuntimeNetworkAddressPlan`, decides the change with
|
|
140
198
|
* `admitRuntimeNetworkAddressPlanChange`, refuses the change while a resolver or platform endpoint
|
|
141
199
|
* it removes or changes is still referenced, and answers with the stored plan: at its new revision,
|
|
142
|
-
* or unchanged.
|
|
200
|
+
* or unchanged. A list stated out of its canonical order (see `IRuntimeNetworkAddressPlan`) is
|
|
201
|
+
* refused `plan-unsorted`, naming the list as the refusal's reference; the plan is never reordered.
|
|
143
202
|
*/
|
|
144
203
|
export interface IReq_Any_Cloudly_SetRuntimeNetworkAddressPlan extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_SetRuntimeNetworkAddressPlan> {
|
|
145
204
|
method: 'setRuntimeNetworkAddressPlan';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
2
|
+
import type { IActiveSecretRecipientMetadata } from '../data/secret.js';
|
|
3
|
+
import type { ICloudlySettings, IExternalGatewayEnrollmentState } from '../data/settings.js';
|
|
4
|
+
import type { IIdentity, IIdentityCredential } from '../data/user.js';
|
|
4
5
|
export interface IRequest_GetSettings extends plugins.typedrequestInterfaces.ITypedRequest {
|
|
5
6
|
method: 'getSettings';
|
|
6
7
|
request: {
|
|
@@ -54,3 +55,56 @@ export interface IRequest_GetSetting extends plugins.typedrequestInterfaces.ITyp
|
|
|
54
55
|
value: ICloudlySettings[keyof ICloudlySettings] | undefined;
|
|
55
56
|
};
|
|
56
57
|
}
|
|
58
|
+
/** A dcrouter admin bootstrap token, sealed to Cloudly's active secret ingress recipient. */
|
|
59
|
+
export interface IExternalGatewayBootstrapTokenInput {
|
|
60
|
+
mode: 'sealed';
|
|
61
|
+
/** Sealed under `data.createGatewayBootstrapEnvelopeContext` for the request's expected target. */
|
|
62
|
+
envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Enrolls Cloudly as its dcrouter gateway client with a dcrouter admin token an administrator
|
|
66
|
+
* hands in once.
|
|
67
|
+
*
|
|
68
|
+
* Cloudly opens the token, checks the target the request names against its own settings, asks
|
|
69
|
+
* dcrouter whether the token is an admin token, and provisions, finalizes and stores its own
|
|
70
|
+
* least-privilege gateway-client credential with it — under the policy its settings and the runtime
|
|
71
|
+
* network address plan state now. The admin token is used for this one call and never stored; the
|
|
72
|
+
* administrator revokes it at dcrouter afterwards. A refusal leaves the credential Cloudly already
|
|
73
|
+
* holds unchanged and is answered with one of `data.externalGatewayBootstrapRefusals`.
|
|
74
|
+
*
|
|
75
|
+
* This is the one way a gateway credential enters Cloudly, for a first enrollment and for the
|
|
76
|
+
* re-bootstrap a changed address plan or gateway setting asks for (`admin-bootstrap-required`).
|
|
77
|
+
*/
|
|
78
|
+
export interface IReq_Any_Cloudly_BootstrapExternalGatewayCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_BootstrapExternalGatewayCredential> {
|
|
79
|
+
method: 'bootstrapExternalGatewayCredential';
|
|
80
|
+
request: {
|
|
81
|
+
identity: IIdentityCredential;
|
|
82
|
+
/** The `dcrouterGatewayUrl` the administrator read from Cloudly's settings. */
|
|
83
|
+
expectedGatewayUrl: string;
|
|
84
|
+
/** The `dcrouterGatewayClientId` the administrator read from Cloudly's settings. */
|
|
85
|
+
expectedGatewayClientId: string;
|
|
86
|
+
bootstrapToken: IExternalGatewayBootstrapTokenInput;
|
|
87
|
+
};
|
|
88
|
+
response: {
|
|
89
|
+
/** The enrollment after the bootstrap: `ready` when the new credential is Cloudly's runtime authority. */
|
|
90
|
+
state: IExternalGatewayEnrollmentState;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Cloudly's gateway-client enrollment state, for an administrator; it never carries a credential. */
|
|
94
|
+
export interface IReq_Any_Cloudly_GetExternalGatewayEnrollmentState extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_GetExternalGatewayEnrollmentState> {
|
|
95
|
+
method: 'getExternalGatewayEnrollmentState';
|
|
96
|
+
request: {
|
|
97
|
+
identity: IIdentityCredential;
|
|
98
|
+
};
|
|
99
|
+
response: {
|
|
100
|
+
state: IExternalGatewayEnrollmentState;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Checks a bootstrap request against its schema and the active ingress recipient, without opening
|
|
105
|
+
* the envelope: the recipient, the context digest of the named target and the ciphertext bound of
|
|
106
|
+
* `data.externalGatewayBootstrapContract.maximumTokenBytes`. Every problem is named.
|
|
107
|
+
*/
|
|
108
|
+
export declare const validateBootstrapExternalGatewayCredentialRequest: (requestArg: unknown, activeIngressRecipientArg: IActiveSecretRecipientMetadata) => Promise<string[]>;
|
|
109
|
+
/** Checks an enrollment state read; every problem is named. */
|
|
110
|
+
export declare const validateGetExternalGatewayEnrollmentStateRequest: (requestArg: unknown) => string[];
|
|
@@ -1,2 +1,92 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
|
|
2
|
+
import * as secretData from '../data/secret.js';
|
|
3
|
+
import * as settingsData from '../data/settings.js';
|
|
4
|
+
import { readDataProperties, hasExactKeys } from '../private/dataproperties.js';
|
|
5
|
+
const hasJwtIdentity = (valueArg) => {
|
|
6
|
+
const identity = readDataProperties(valueArg);
|
|
7
|
+
return identity !== null && typeof identity.jwt === 'string' && identity.jwt.length > 0;
|
|
8
|
+
};
|
|
9
|
+
/** Structural check of the target a bootstrap request names; empty when both are well formed. */
|
|
10
|
+
const validateExpectedGatewayTarget = (urlArg, clientIdArg) => {
|
|
11
|
+
const errors = [];
|
|
12
|
+
let url;
|
|
13
|
+
if (typeof urlArg === 'string'
|
|
14
|
+
&& urlArg.length > 0
|
|
15
|
+
&& urlArg.length <= settingsData.externalGatewayBootstrapContract.maximumGatewayUrlLength) {
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(urlArg);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
url = undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!url
|
|
24
|
+
|| url.protocol !== 'https:'
|
|
25
|
+
|| url.username !== ''
|
|
26
|
+
|| url.password !== ''
|
|
27
|
+
|| url.search !== ''
|
|
28
|
+
|| url.hash !== '') {
|
|
29
|
+
errors.push('bootstrap expectedGatewayUrl must be an HTTPS URL without credentials, query or fragment');
|
|
30
|
+
}
|
|
31
|
+
if (typeof clientIdArg !== 'string'
|
|
32
|
+
|| clientIdArg.length === 0
|
|
33
|
+
|| clientIdArg.length > settingsData.externalGatewayBootstrapContract.maximumGatewayClientIdLength
|
|
34
|
+
|| !/^[\x21-\x7e]+$/.test(clientIdArg)) {
|
|
35
|
+
errors.push('bootstrap expectedGatewayClientId must be a bounded identifier without whitespace');
|
|
36
|
+
}
|
|
37
|
+
return errors;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Checks a bootstrap request against its schema and the active ingress recipient, without opening
|
|
41
|
+
* the envelope: the recipient, the context digest of the named target and the ciphertext bound of
|
|
42
|
+
* `data.externalGatewayBootstrapContract.maximumTokenBytes`. Every problem is named.
|
|
43
|
+
*/
|
|
44
|
+
export const validateBootstrapExternalGatewayCredentialRequest = async (requestArg, activeIngressRecipientArg) => {
|
|
45
|
+
const request = readDataProperties(requestArg);
|
|
46
|
+
if (request === null)
|
|
47
|
+
return ['bootstrap request must be a plain record'];
|
|
48
|
+
const errors = [];
|
|
49
|
+
if (!hasExactKeys(request, ['identity', 'expectedGatewayUrl', 'expectedGatewayClientId', 'bootstrapToken'])) {
|
|
50
|
+
errors.push('bootstrap request contains fields outside its schema');
|
|
51
|
+
}
|
|
52
|
+
if (!hasJwtIdentity(request.identity)) {
|
|
53
|
+
errors.push('bootstrap identity must be a JWT credential');
|
|
54
|
+
}
|
|
55
|
+
const targetErrors = validateExpectedGatewayTarget(request.expectedGatewayUrl, request.expectedGatewayClientId);
|
|
56
|
+
errors.push(...targetErrors);
|
|
57
|
+
const tokenInput = readDataProperties(request.bootstrapToken);
|
|
58
|
+
if (tokenInput === null || tokenInput.mode !== 'sealed') {
|
|
59
|
+
errors.push('bootstrap token must be a sealed input');
|
|
60
|
+
return errors;
|
|
61
|
+
}
|
|
62
|
+
const expectedContext = targetErrors.length === 0
|
|
63
|
+
? secretData.createGatewayBootstrapEnvelopeContext({
|
|
64
|
+
gatewayUrl: request.expectedGatewayUrl,
|
|
65
|
+
gatewayClientId: request.expectedGatewayClientId,
|
|
66
|
+
})
|
|
67
|
+
: new Uint8Array();
|
|
68
|
+
errors.push(...await secretData.validateSecretValueInputForRecipient(tokenInput, activeIngressRecipientArg, expectedContext));
|
|
69
|
+
const envelope = readDataProperties(tokenInput.envelope);
|
|
70
|
+
if (envelope !== null
|
|
71
|
+
&& typeof envelope.ciphertext === 'string'
|
|
72
|
+
&& envelope.ciphertext.length
|
|
73
|
+
> Math.ceil(settingsData.externalGatewayBootstrapContract.maximumTokenBytes * 4 / 3)) {
|
|
74
|
+
errors.push(`bootstrap token must not exceed ${settingsData.externalGatewayBootstrapContract.maximumTokenBytes} bytes`);
|
|
75
|
+
}
|
|
76
|
+
return errors;
|
|
77
|
+
};
|
|
78
|
+
/** Checks an enrollment state read; every problem is named. */
|
|
79
|
+
export const validateGetExternalGatewayEnrollmentStateRequest = (requestArg) => {
|
|
80
|
+
const request = readDataProperties(requestArg);
|
|
81
|
+
if (request === null)
|
|
82
|
+
return ['enrollment state request must be a plain record'];
|
|
83
|
+
const errors = [];
|
|
84
|
+
if (!hasExactKeys(request, ['identity'])) {
|
|
85
|
+
errors.push('enrollment state request contains fields outside its schema');
|
|
86
|
+
}
|
|
87
|
+
if (!hasJwtIdentity(request.identity)) {
|
|
88
|
+
errors.push('enrollment state identity must be a JWT credential');
|
|
89
|
+
}
|
|
90
|
+
return errors;
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2V0dGluZ3MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9yZXF1ZXN0cy9zZXR0aW5ncy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQUN6QyxPQUFPLEtBQUssVUFBVSxNQUFNLG1CQUFtQixDQUFDO0FBQ2hELE9BQU8sS0FBSyxZQUFZLE1BQU0scUJBQXFCLENBQUM7QUFJcEQsT0FBTyxFQUFFLGtCQUFrQixFQUFFLFlBQVksRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBeUhoRixNQUFNLGNBQWMsR0FBRyxDQUFDLFFBQWlCLEVBQVcsRUFBRTtJQUNwRCxNQUFNLFFBQVEsR0FBRyxrQkFBa0IsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUM5QyxPQUFPLFFBQVEsS0FBSyxJQUFJLElBQUksT0FBTyxRQUFRLENBQUMsR0FBRyxLQUFLLFFBQVEsSUFBSSxRQUFRLENBQUMsR0FBRyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7QUFDMUYsQ0FBQyxDQUFDO0FBRUYsaUdBQWlHO0FBQ2pHLE1BQU0sNkJBQTZCLEdBQUcsQ0FBQyxNQUFlLEVBQUUsV0FBb0IsRUFBWSxFQUFFO0lBQ3hGLE1BQU0sTUFBTSxHQUFhLEVBQUUsQ0FBQztJQUM1QixJQUFJLEdBQW9CLENBQUM7SUFDekIsSUFBSSxPQUFPLE1BQU0sS0FBSyxRQUFRO1dBQ3pCLE1BQU0sQ0FBQyxNQUFNLEdBQUcsQ0FBQztXQUNqQixNQUFNLENBQUMsTUFBTSxJQUFJLFlBQVksQ0FBQyxnQ0FBZ0MsQ0FBQyx1QkFBdUIsRUFBRSxDQUFDO1FBQzVGLElBQUksQ0FBQztZQUNILEdBQUcsR0FBRyxJQUFJLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUN4QixDQUFDO1FBQUMsTUFBTSxDQUFDO1lBQ1AsR0FBRyxHQUFHLFNBQVMsQ0FBQztRQUNsQixDQUFDO0lBQ0gsQ0FBQztJQUNELElBQUksQ0FBQyxHQUFHO1dBQ0gs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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/interfaces",
|
|
3
|
-
"version": "32.
|
|
3
|
+
"version": "32.30.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
|
|
6
6
|
"exports": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@git.zone/tsbuild": "^5.0.0",
|
|
31
31
|
"@git.zone/tsdoc": "^2.2.2",
|
|
32
|
-
"@git.zone/tsrun": "^3.0.
|
|
33
|
-
"@git.zone/tstest": "6.3.
|
|
32
|
+
"@git.zone/tsrun": "^3.0.1",
|
|
33
|
+
"@git.zone/tstest": "6.3.2",
|
|
34
34
|
"@types/node": "26.6.2"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
package/readme.md
CHANGED
|
@@ -482,7 +482,7 @@ canonical bytes and digest; a present member is never an empty list, so
|
|
|
482
482
|
Operators author that authorization through `getServicePublishedPorts` and
|
|
483
483
|
`setServicePublishedPorts`, modelled on the private-network membership pair.
|
|
484
484
|
`IServicePublishedPorts` returns the current authorized list with its policy
|
|
485
|
-
`revision`, or null when a service never had
|
|
485
|
+
`revision`, or null when a service never had a policy document. `ISetServicePublishedPorts`
|
|
486
486
|
returns the accepted `IServicePublishedPorts` document with its new
|
|
487
487
|
`revision` and a `replayed` flag, and carries `expectedRevision` (null for the first document), a retained
|
|
488
488
|
`mutationId` for replay, and `IRuntimeNetworkPublishedPortRequest` entries of
|
|
@@ -493,6 +493,39 @@ policy meaning the service publishes nothing. Callers never supply an
|
|
|
493
493
|
from the accepted policy revision and digest, and node-level host-port placement
|
|
494
494
|
stays Cloudly's decision.
|
|
495
495
|
|
|
496
|
+
Every endpoint's `publicEgress` and `platformEndpointIds` come from the same
|
|
497
|
+
service policy document. A verified administrator authors them through
|
|
498
|
+
`getServiceNetworkEgress` and `setServiceNetworkEgress`
|
|
499
|
+
(`requests.network.IReq_Admin_Cloudly_GetServiceNetworkEgress` /
|
|
500
|
+
`IReq_Admin_Cloudly_SetServiceNetworkEgress`): platform endpoints are platform
|
|
501
|
+
authority, and only an administrator writes the runtime spec whose attached
|
|
502
|
+
network has to state the same values. `IServiceNetworkEgress` returns
|
|
503
|
+
`publicEgress` and the selected `platformEndpointIds` (sorted, unique, at most
|
|
504
|
+
`runtimeNetworkAuthorityContract.maximumPlatformEndpoints`) with the document's
|
|
505
|
+
`revision`, or null when a service never had a policy document.
|
|
506
|
+
`ISetServiceNetworkEgress` carries `expectedRevision` (null for the first
|
|
507
|
+
document), a retained `mutationId` for replay and the exact desired values, and
|
|
508
|
+
returns the accepted `IServiceNetworkEgress` with a `replayed` flag; Cloudly
|
|
509
|
+
carries the published ports forward unchanged, and refuses an id that names no
|
|
510
|
+
current platform endpoint. Published ports and egress are two halves of one
|
|
511
|
+
document with one `revision`, so `expectedRevision` of either request is the
|
|
512
|
+
revision the caller read from either answer, and an accepted write of either
|
|
513
|
+
advances it.
|
|
514
|
+
|
|
515
|
+
One document per service holds its published ports and its egress under one `revision`. The
|
|
516
|
+
first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
|
|
517
|
+
with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
|
|
518
|
+
no published ports. From then on both reads return it, never null. It is an explicit policy: an
|
|
519
|
+
attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
|
|
520
|
+
no public address and no platform endpoint; `network-policy-missing` means the service has no
|
|
521
|
+
document at all.
|
|
522
|
+
|
|
523
|
+
`snapshotServiceNetworkEgress` and `snapshotSetServiceNetworkEgress`
|
|
524
|
+
check the shapes. An attached runtime spec is admitted only while it states the
|
|
525
|
+
document's values (`network-policy-mismatch`), and nothing else grants a
|
|
526
|
+
workload egress: a Corestore endpoint on the node's own host is delivered
|
|
527
|
+
locally, but only to a workload that selects it.
|
|
528
|
+
|
|
496
529
|
`getRuntimeNetworkProjectionPacketGrants` derives directed member connectivity
|
|
497
530
|
and explicit local public/platform egress independently of DNS readiness.
|
|
498
531
|
`composeRuntimeNetworkProjectionDnsViews` builds one union view per local
|
|
@@ -829,6 +862,27 @@ because the private-range rule already keeps it at /8 or longer.
|
|
|
829
862
|
`data.runtimeNetworkVpnControlPrefix(value)` is that reader on its own, exported because a
|
|
830
863
|
cluster's VPN network repeats the prefix its hub binds and has to read it exactly as the plan does.
|
|
831
864
|
|
|
865
|
+
Every list of a plan is stated in one canonical order, and a plan is never reordered for its
|
|
866
|
+
writer: `prefixes` strictly ascending by the prefix text, and `pools`, `resolvers` and
|
|
867
|
+
`platformEndpoints` strictly ascending by `id`. Both compare as strings, so the order is
|
|
868
|
+
lexicographic, not numeric — `100.64.0.0/10` comes before `20.0.0.0/8`, and `resolver-10` before
|
|
869
|
+
`resolver-9` — and a value stated twice breaks it as well. A list out of order is the one plan
|
|
870
|
+
refusal the contract names: `snapshotRuntimeNetworkAddressPlan` (and
|
|
871
|
+
`admitRuntimeNetworkAddressPlanChange` through it) throws
|
|
872
|
+
`data.RuntimeNetworkAddressPlanStructureError`, whose `violation` is
|
|
873
|
+
`{ field: 'prefixes' | 'pools' | 'resolvers' | 'platformEndpoints', rule: 'order' }` and whose
|
|
874
|
+
message quotes nothing from the plan. Every other refusal stays the contract's generic rejection.
|
|
875
|
+
|
|
876
|
+
```typescript
|
|
877
|
+
try {
|
|
878
|
+
data.snapshotRuntimeNetworkAddressPlan(plan);
|
|
879
|
+
} catch (error) {
|
|
880
|
+
if (error instanceof data.RuntimeNetworkAddressPlanStructureError) {
|
|
881
|
+
error.violation; // { field: 'prefixes', rule: 'order' }
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
832
886
|
`data.IRuntimeNetworkVpnHub { clusterId, address, quicPort }` is one cluster's hub as its relay
|
|
833
887
|
bound it. `runtimeNetworkVpnHubEndpoints(hub)` is the one derivation of what it contributes:
|
|
834
888
|
`<clusterId>:quic` on `udp`, one endpoint per row of
|
|
@@ -868,7 +922,9 @@ outlive a plan revision, so it answers:
|
|
|
868
922
|
reference check as removing it.
|
|
869
923
|
|
|
870
924
|
`TRuntimeNetworkAddressPlanRefusal` adds `plan-resolver-referenced` and `plan-endpoint-referenced`,
|
|
871
|
-
which only Cloudly can decide against live references,
|
|
925
|
+
which only Cloudly can decide against live references, and `plan-unsorted`, which Cloudly answers
|
|
926
|
+
for a `RuntimeNetworkAddressPlanStructureError` with the list as its reference, so every plan
|
|
927
|
+
refusal has one type.
|
|
872
928
|
`requests.network` carries `setRuntimeNetworkAddressPlan { identity, expectedRevision, plan }`
|
|
873
929
|
(`expectedRevision: null` when no plan exists yet) and `getRuntimeNetworkAddressPlan`, which answers
|
|
874
930
|
`null` until the first plan is set.
|
|
@@ -1431,19 +1487,39 @@ evidence has not arrived). `refused` carries a `TServiceRuntimeRefusal`:
|
|
|
1431
1487
|
`network-workload-pool-missing`, `network-workload-pool-exhausted`, `volume-driver-unsupported`,
|
|
1432
1488
|
`volume-options-unsupported`, `volume-shared-unsupported`, `volume-placement-unsupported`,
|
|
1433
1489
|
`volume-initialization-unknown`, `volume-ownership-unknown`, `objectstorage-endpoint-missing`,
|
|
1434
|
-
`objectstorage-binding-invalid
|
|
1490
|
+
`objectstorage-binding-invalid`, `objectstorage-environment-conflict`,
|
|
1491
|
+
`objectstorage-endpoint-unselected`, `database-endpoint-missing` or `database-endpoint-unselected`.
|
|
1492
|
+
The first four volume
|
|
1435
1493
|
refusals name what a local storage claim cannot express: a volume driver other than a node-local
|
|
1436
1494
|
directory, driver or mount options, one volume mounted by more than one replica or service, and a
|
|
1437
1495
|
placement that does not pin each volume to one node. The other two name what Cloudly will not guess,
|
|
1438
|
-
see [Service Volume Initialization](#service-volume-initialization).
|
|
1439
|
-
refusals
|
|
1440
|
-
|
|
1496
|
+
see [Service Volume Initialization](#service-volume-initialization). Of the four object-storage
|
|
1497
|
+
refusals, `objectstorage-endpoint-missing`, `objectstorage-binding-invalid` and
|
|
1498
|
+
`objectstorage-environment-conflict` name why a binding's environment cannot be composed, see
|
|
1499
|
+
[Object-Storage Binding Environment](#object-storage-binding-environment), and
|
|
1500
|
+
`objectstorage-endpoint-unselected`, like `database-endpoint-unselected`, names an endpoint the
|
|
1501
|
+
workload's network does not select (below). `ports-too-many` is a service declaring more target ports than the
|
|
1441
1502
|
`runtimeConfigContract.maximumTargetPorts` (64) a config carries, where `port-protocol-unsupported`
|
|
1442
1503
|
is about one port's protocol; `node-authority-unsupported` is a node whose session resolved fewer
|
|
1443
1504
|
workload authorities than the spec needs, refused at the admit step rather than handed to a node
|
|
1444
|
-
that would silently park it. An attached network requires an explicit service policy
|
|
1505
|
+
that would silently park it. An attached network requires an explicit service policy
|
|
1506
|
+
(`IServiceNetworkEgress`): its absence
|
|
1445
1507
|
is `network-policy-missing`, and a disagreement with the requested egress or platform endpoints is
|
|
1446
|
-
`network-policy-mismatch`. A
|
|
1508
|
+
`network-policy-mismatch`. A binding endpoint the workload's network does not select would leave its
|
|
1509
|
+
connection dark, so it is refused instead: `database-endpoint-unselected` for an enabled database
|
|
1510
|
+
binding whose node's valid `corestoreDatabase` is not selected, and `objectstorage-endpoint-unselected`
|
|
1511
|
+
for an enabled object-storage binding whose node's valid `corestoreObjectStorage` is not. An endpoint
|
|
1512
|
+
is selected when one of the spec's `platformEndpointIds` names a platform endpoint of the current
|
|
1513
|
+
protected authority with the endpoint's host as its `address`, `tcp` as its `protocol` and the
|
|
1514
|
+
endpoint's port as its `port` — for object storage the origin's port, or 80 for `http:` and 443 for
|
|
1515
|
+
`https:` when it states none. An `isolated` network selects nothing, and platform endpoints are
|
|
1516
|
+
IPv4, so a DNS-name or IPv6 host is never selected. Both are judged only for a valid endpoint: a node
|
|
1517
|
+
stating none, or an invalid one, stays `database-endpoint-missing` or `objectstorage-endpoint-missing`.
|
|
1518
|
+
`platform.findUnselectedCorestoreEndpoints({ database, objectStorage, platformEndpoints, network })`
|
|
1519
|
+
is that rule: given one node's `corestoreDatabase` and `corestoreObjectStorage` (each `null` when no
|
|
1520
|
+
enabled binding of that kind is placed on the node), the current protected authority's
|
|
1521
|
+
`platformEndpoints` and the spec's `network`, it returns the `TCorestoreEndpointSelectionRefusal`s
|
|
1522
|
+
that apply, sorted, or an empty list. A missing workload address pool in the current protected authority is
|
|
1447
1523
|
`network-workload-pool-missing`; existing eligible pools without a free workload subnet are
|
|
1448
1524
|
`network-workload-pool-exhausted`. Neither policy nor pool is silently defaulted. A refusal never
|
|
1449
1525
|
retires running slots.
|
|
@@ -2319,9 +2395,13 @@ with `clusterRelayCertificateDomainName(relay)`, which returns the origin's host
|
|
|
2319
2395
|
Cloudly issues the certificate for and owns the A record for. An origin naming an address literal
|
|
2320
2396
|
therefore has no issuable certificate.
|
|
2321
2397
|
|
|
2322
|
-
`data.clusterRelayUndispatchedRefusals` names the
|
|
2323
|
-
custody — `relay-cluster-not-enabled`, `relay-node-foreign`, `relay-
|
|
2324
|
-
`relay-credential-generation-stale` — with
|
|
2398
|
+
`data.clusterRelayUndispatchedRefusals` names the five registration refusals that end a relay's
|
|
2399
|
+
custody — `relay-cluster-not-enabled`, `relay-node-foreign`, `relay-node-unknown`,
|
|
2400
|
+
`relay-sequence-not-advancing` and `relay-credential-generation-stale` — with
|
|
2401
|
+
`data.TClusterRelayUndispatchedRefusal` as their union. `relay-node-foreign` names a node of another
|
|
2402
|
+
cluster; `relay-node-unknown` names a node Cloudly does not hold at all, because no Spark
|
|
2403
|
+
enrollment has created it yet, so the node a relay's `COREFLOW_NODE_ID` names is enrolled before
|
|
2404
|
+
the relay registers.
|
|
2325
2405
|
Each is Cloudly reading its own records and stating that it does not dispatch to this relay, so a
|
|
2326
2406
|
relay that hears one drops what it holds for its nodes instead of waiting. A refusal that only
|
|
2327
2407
|
says Cloudly could not verify the relay states nothing about dispatch and is deliberately not in
|
|
@@ -2941,7 +3021,62 @@ const provisioning: requests.gateway.IReq_ProvisionGatewayClientCredential['requ
|
|
|
2941
3021
|
};
|
|
2942
3022
|
```
|
|
2943
3023
|
|
|
2944
|
-
`getGatewayClientContext` returns effective live policy. A `gatewayClient` role necessarily includes the credential ID, bound client ID/type, and `policyGeneration`; consumers should reject admin/operator or mismatched contexts rather than falling back to a caller-supplied owner ID. `getGatewayClientMailOverview` provides an owner-scoped domain and recent-message summary. `getGatewayClientMailDomainCount` derives ownership exclusively from the authenticating gateway credential and returns `{ count: number }` for its distinct configured mail domains. Cloudly therefore
|
|
3024
|
+
`getGatewayClientContext` returns effective live policy. A `gatewayClient` role necessarily includes the credential ID, bound client ID/type, and `policyGeneration`; consumers should reject admin/operator or mismatched contexts rather than falling back to a caller-supplied owner ID. `getGatewayClientMailOverview` provides an owner-scoped domain and recent-message summary. `getGatewayClientMailDomainCount` derives ownership exclusively from the authenticating gateway credential and returns `{ count: number }` for its distinct configured mail domains. Cloudly therefore holds only its gateway-client credential, as the system secret `DCROUTER_GATEWAY_API_TOKEN` (see Cloudly Gateway Credential Bootstrap below); neither it nor the former `dcrouterOpsApiToken` is part of `data.ICloudlySettings`.
|
|
3025
|
+
|
|
3026
|
+
### Cloudly Gateway Credential Bootstrap
|
|
3027
|
+
|
|
3028
|
+
Cloudly enrolls itself at dcrouter as one least-privilege gateway client. The only way a gateway
|
|
3029
|
+
credential enters Cloudly is a dcrouter admin token an administrator hands in once, for the first
|
|
3030
|
+
enrollment and for every re-bootstrap a stale policy asks for — a changed gateway setting, or a
|
|
3031
|
+
runtime network address plan whose DNS record allowance moved.
|
|
3032
|
+
|
|
3033
|
+
- `requests.settings.IReq_Any_Cloudly_BootstrapExternalGatewayCredential`
|
|
3034
|
+
(`bootstrapExternalGatewayCredential`): `{ identity, expectedGatewayUrl, expectedGatewayClientId,
|
|
3035
|
+
bootstrapToken: { mode: 'sealed', envelope } }` → `{ state }`. The token is sealed to Cloudly's
|
|
3036
|
+
active secret ingress recipient (`requests.secret.IReq_GetSecretIngressRecipient`) under
|
|
3037
|
+
`data.createGatewayBootstrapEnvelopeContext({ gatewayUrl, gatewayClientId })`, so it opens only
|
|
3038
|
+
for a request naming the same dcrouter URL (HTTPS only, since an admin token is on the wire) and
|
|
3039
|
+
client id, and Cloudly compares both with its own
|
|
3040
|
+
settings before the token leaves it. Cloudly asks dcrouter whether the token is an admin token,
|
|
3041
|
+
provisions, finalizes and stores its own gateway-client credential with it, and answers the
|
|
3042
|
+
enrollment that results. The admin token is used for that one call and never stored; revoke it at
|
|
3043
|
+
dcrouter afterwards. A refusal leaves the held credential unchanged and is one of
|
|
3044
|
+
`data.externalGatewayBootstrapRefusals`: `gateway-unconfigured`, `gateway-target-mismatch`,
|
|
3045
|
+
`bootstrap-token-invalid`, `bootstrap-token-rejected`, `bootstrap-token-not-admin`,
|
|
3046
|
+
`bootstrap-candidate-pending`, `gateway-unreachable` and `bootstrap-provisioning-refused`.
|
|
3047
|
+
- `requests.settings.IReq_Any_Cloudly_GetExternalGatewayEnrollmentState`
|
|
3048
|
+
(`getExternalGatewayEnrollmentState`): `{ identity }` → `{ state }`.
|
|
3049
|
+
|
|
3050
|
+
`data.IExternalGatewayEnrollmentState { status, reason?, lastCheckedAt?, nextRetryAt?,
|
|
3051
|
+
cachedValidationProof, finalizationPending }` never carries a credential. `status` is one of
|
|
3052
|
+
`data.externalGatewayEnrollmentStatuses`: `unconfigured`, `checking`, `ready`, `degraded` (dcrouter
|
|
3053
|
+
unreachable; Cloudly retries) and `admin-bootstrap-required`. Both sides judge values with
|
|
3054
|
+
`requests.settings.validateBootstrapExternalGatewayCredentialRequest(request, recipient)` (schema,
|
|
3055
|
+
recipient, context digest and the ciphertext bound, without opening anything),
|
|
3056
|
+
`requests.settings.validateGetExternalGatewayEnrollmentStateRequest`,
|
|
3057
|
+
`data.validateExternalGatewayEnrollmentState` and `data.isExternalGatewayBootstrapToken(bytes)`,
|
|
3058
|
+
which accepts 1 to 1024 bytes of visible ASCII (`data.externalGatewayBootstrapContract`).
|
|
3059
|
+
|
|
3060
|
+
```typescript
|
|
3061
|
+
import * as smartcrypto from '@push.rocks/smartcrypto';
|
|
3062
|
+
import { data, requests } from '@serve.zone/interfaces';
|
|
3063
|
+
|
|
3064
|
+
const target = { gatewayUrl: 'https://gateway.example.com', gatewayClientId: 'cloudly.example.com' };
|
|
3065
|
+
const request: requests.settings.IReq_Any_Cloudly_BootstrapExternalGatewayCredential['request'] = {
|
|
3066
|
+
identity: { jwt: adminJwt },
|
|
3067
|
+
expectedGatewayUrl: target.gatewayUrl,
|
|
3068
|
+
expectedGatewayClientId: target.gatewayClientId,
|
|
3069
|
+
bootstrapToken: {
|
|
3070
|
+
mode: 'sealed',
|
|
3071
|
+
envelope: await smartcrypto.sealX25519Envelope({
|
|
3072
|
+
plaintext: adminTokenBytes,
|
|
3073
|
+
recipientPublicKey: ingressPublicKey,
|
|
3074
|
+
recipientKeyId: ingressRecipient.recipientKeyId,
|
|
3075
|
+
context: data.createGatewayBootstrapEnvelopeContext(target),
|
|
3076
|
+
}),
|
|
3077
|
+
},
|
|
3078
|
+
};
|
|
3079
|
+
```
|
|
2945
3080
|
|
|
2946
3081
|
### Gateway Client Hostname Records and Certificates
|
|
2947
3082
|
|
|
@@ -3642,6 +3777,10 @@ object-storage endpoint. What they are addressed to is a fact about the node, ow
|
|
|
3642
3777
|
- `TServiceRuntimeRefusal` gains `database-endpoint-missing`: an enabled database binding placed on
|
|
3643
3778
|
a node that states no valid `corestoreDatabase` is neither granted nor published, and its workload
|
|
3644
3779
|
is refused rather than started with a guessed host.
|
|
3780
|
+
- A workload reaches that endpoint only when its attached network selects it as a platform endpoint;
|
|
3781
|
+
an unselected valid endpoint is refused as `database-endpoint-unselected` (see
|
|
3782
|
+
[Service Runtime Specs And Status](#service-runtime-specs-and-status) for the matching rule), as an object-storage endpoint is as
|
|
3783
|
+
`objectstorage-endpoint-unselected`.
|
|
3645
3784
|
|
|
3646
3785
|
### Launcher Environment Delivery
|
|
3647
3786
|
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/cluster.ts
CHANGED
|
@@ -175,6 +175,11 @@ export const clusterRelayUndispatchedRefusals = Object.freeze([
|
|
|
175
175
|
'relay-cluster-not-enabled',
|
|
176
176
|
/** The node the relay names belongs to another cluster than the one its identity names. */
|
|
177
177
|
'relay-node-foreign',
|
|
178
|
+
/**
|
|
179
|
+
* The node the relay names does not exist: no Spark enrollment has created it yet. The relay's
|
|
180
|
+
* `COREFLOW_NODE_ID` must name a node Cloudly already holds, so the node is enrolled first.
|
|
181
|
+
*/
|
|
182
|
+
'relay-node-unknown',
|
|
178
183
|
/** A registration of this relay identity at the same or a higher sequence is already recorded. */
|
|
179
184
|
'relay-sequence-not-advancing',
|
|
180
185
|
/**
|
|
@@ -4,7 +4,15 @@ import { computeRuntimeNetworkProtectedAuthorityDigest, runtimeNetworkAuthorityC
|
|
|
4
4
|
snapshotRuntimeNetworkProtectedAuthority } from './networkauthority.js';
|
|
5
5
|
import { captureNetwork, failNetwork, networkContains, networkCounter, networkEqual, networkId, networkIpv4, networkList,
|
|
6
6
|
networkOverlaps, networkPrefix, networkPrefixBounds, networkPrivatePrefix, networkRecord, networkSafe,
|
|
7
|
-
requireNetwork
|
|
7
|
+
requireNetwork, RuntimeNetworkAddressPlanStructureError, type TRuntimeNetworkAddressPlanOrderedField,
|
|
8
|
+
} from '../private/networkcontract.js';
|
|
9
|
+
|
|
10
|
+
export { RuntimeNetworkAddressPlanStructureError } from '../private/networkcontract.js';
|
|
11
|
+
export type {
|
|
12
|
+
IRuntimeNetworkAddressPlanStructureViolation,
|
|
13
|
+
TRuntimeNetworkAddressPlanOrderedField,
|
|
14
|
+
TRuntimeNetworkAddressPlanStructureRule,
|
|
15
|
+
} from '../private/networkcontract.js';
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* The address space an administrator gives the runtime network, and the one input the protected
|
|
@@ -15,11 +23,25 @@ export interface IRuntimeNetworkAddressPlan {
|
|
|
15
23
|
id: 'address-plan';
|
|
16
24
|
/** Compare-and-swap counter; every accepted change advances it by one. */
|
|
17
25
|
revision: number;
|
|
18
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Every protected prefix: they cover the pools, resolvers, platform endpoints and the VPN.
|
|
28
|
+
*
|
|
29
|
+
* Stated in strictly ascending string order of the prefix text. The order is lexicographic, not
|
|
30
|
+
* numeric: `10.10.0.0/16` comes before `10.2.0.0/16`, and `100.64.0.0/10` before `20.0.0.0/8`.
|
|
31
|
+
* A list out of that order, or one naming a prefix twice, is refused with
|
|
32
|
+
* `RuntimeNetworkAddressPlanStructureError` `{ field: 'prefixes', rule: 'order' }`.
|
|
33
|
+
*/
|
|
19
34
|
prefixes: string[];
|
|
35
|
+
/** Stated in strictly ascending string order of `id`; otherwise `{ field: 'pools', rule: 'order' }`. */
|
|
20
36
|
pools: IRuntimeNetworkProtectedAuthority['pools'];
|
|
37
|
+
/** Stated in strictly ascending string order of `id`; otherwise `{ field: 'resolvers', rule: 'order' }`. */
|
|
21
38
|
resolvers: IRuntimeNetworkProtectedAuthority['resolvers'];
|
|
22
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Without the cluster hubs, which the composed authority adds from the hubs it is given.
|
|
41
|
+
*
|
|
42
|
+
* Stated in strictly ascending string order of `id`; otherwise
|
|
43
|
+
* `{ field: 'platformEndpoints', rule: 'order' }`.
|
|
44
|
+
*/
|
|
23
45
|
platformEndpoints: IRuntimeNetworkProtectedAuthority['platformEndpoints'];
|
|
24
46
|
vpn: {
|
|
25
47
|
/** The private prefix VPN control addresses are allocated from. */
|
|
@@ -63,7 +85,12 @@ export type TRuntimeNetworkAddressPlanChangeRefusal = 'plan-vpn-immutable' | 'pl
|
|
|
63
85
|
* platform endpoint the change removes or changes while a router selection or a service still names it.
|
|
64
86
|
*/
|
|
65
87
|
export type TRuntimeNetworkAddressPlanRefusal =
|
|
66
|
-
| TRuntimeNetworkAddressPlanChangeRefusal | 'plan-resolver-referenced' | 'plan-endpoint-referenced'
|
|
88
|
+
| TRuntimeNetworkAddressPlanChangeRefusal | 'plan-resolver-referenced' | 'plan-endpoint-referenced'
|
|
89
|
+
/**
|
|
90
|
+
* A list of the plan is not in its canonical order; `RuntimeNetworkAddressPlanStructureError`
|
|
91
|
+
* names which one, and a refusal carries that field as its reference.
|
|
92
|
+
*/
|
|
93
|
+
| 'plan-unsorted';
|
|
67
94
|
|
|
68
95
|
export type TRuntimeNetworkAddressPlanChange =
|
|
69
96
|
| { status: 'unchanged' }
|
|
@@ -180,14 +207,49 @@ const requireVpnProtected = (authority: IRuntimeNetworkProtectedAuthority, vpn:
|
|
|
180
207
|
&& !networkOverlaps(vpn.controlPrefix, vpn.hubPrefix));
|
|
181
208
|
};
|
|
182
209
|
|
|
210
|
+
/** The key each ordered list of a plan is sorted by, or `undefined` for an entry no key can be read from. */
|
|
211
|
+
const planOrderKeys: Record<TRuntimeNetworkAddressPlanOrderedField, (entry: unknown) => string | undefined> = {
|
|
212
|
+
prefixes: entry => typeof entry === 'string' ? entry : undefined,
|
|
213
|
+
pools: entry => entryId(entry),
|
|
214
|
+
resolvers: entry => entryId(entry),
|
|
215
|
+
platformEndpoints: entry => entryId(entry),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
function entryId(entry: unknown): string | undefined {
|
|
219
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) return undefined;
|
|
220
|
+
const id = (entry as Record<string, unknown>).id;
|
|
221
|
+
return typeof id === 'string' ? id : undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Refuses a plan list out of its canonical order by name: strictly ascending by the list's key, in
|
|
226
|
+
* JavaScript string order, which is the order the protected authority contract reads it in. A list
|
|
227
|
+
* whose keys cannot all be read is left to the structural check, which refuses it without a name.
|
|
228
|
+
*/
|
|
229
|
+
const requirePlanOrder = (item: Record<string, unknown>): void => {
|
|
230
|
+
for (const field of ['prefixes', 'pools', 'resolvers', 'platformEndpoints'] as const) {
|
|
231
|
+
const list = item[field];
|
|
232
|
+
if (!Array.isArray(list)) continue;
|
|
233
|
+
const keys = list.map(planOrderKeys[field]);
|
|
234
|
+
if (keys.some(key => key === undefined)) continue;
|
|
235
|
+
if (!keys.every((key, index) => index === 0 || (keys[index - 1] as string) < (key as string))) {
|
|
236
|
+
throw new RuntimeNetworkAddressPlanStructureError({ field, rule: 'order' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
183
241
|
/**
|
|
184
242
|
* Structural snapshot for exact persistence and for a plan write. The plan's own sets pass the
|
|
185
243
|
* protected authority's own validator, so no plan can describe an authority that contract refuses,
|
|
186
244
|
* and both VPN prefixes are reserved inside the protected space the plan states.
|
|
245
|
+
*
|
|
246
|
+
* A list out of its canonical order is refused with `RuntimeNetworkAddressPlanStructureError`,
|
|
247
|
+
* which names the list; every other refusal is the contract's generic rejection.
|
|
187
248
|
*/
|
|
188
249
|
export const snapshotRuntimeNetworkAddressPlan = (value: unknown): IRuntimeNetworkAddressPlan => networkSafe(() => {
|
|
189
250
|
const item = networkRecord(captureNetwork(value), ['id', 'revision', 'prefixes', 'pools', 'resolvers', 'platformEndpoints', 'vpn']);
|
|
190
251
|
requireNetwork(item.id === runtimeNetworkAddressPlanContract.id);
|
|
252
|
+
requirePlanOrder(item);
|
|
191
253
|
const own = authorityOf({ prefixes: item.prefixes, pools: item.pools, resolvers: item.resolvers,
|
|
192
254
|
platformEndpoints: item.platformEndpoints }, validationFrame);
|
|
193
255
|
const plan: IRuntimeNetworkAddressPlan = { id: runtimeNetworkAddressPlanContract.id, revision: networkCounter(item.revision),
|