@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
package/ts/requests/settings.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import * as secretData from '../data/secret.js';
|
|
3
|
+
import * as settingsData from '../data/settings.js';
|
|
4
|
+
import type { IActiveSecretRecipientMetadata } from '../data/secret.js';
|
|
5
|
+
import type { ICloudlySettings, IExternalGatewayEnrollmentState } from '../data/settings.js';
|
|
6
|
+
import type { IIdentity, IIdentityCredential } from '../data/user.js';
|
|
7
|
+
import { readDataProperties, hasExactKeys } from '../private/dataproperties.js';
|
|
4
8
|
|
|
5
9
|
// Get Settings
|
|
6
10
|
export interface IRequest_GetSettings extends plugins.typedrequestInterfaces.ITypedRequest {
|
|
@@ -64,3 +68,159 @@ export interface IRequest_GetSetting extends plugins.typedrequestInterfaces.ITyp
|
|
|
64
68
|
value: ICloudlySettings[keyof ICloudlySettings] | undefined;
|
|
65
69
|
};
|
|
66
70
|
}
|
|
71
|
+
|
|
72
|
+
/** A dcrouter admin bootstrap token, sealed to Cloudly's active secret ingress recipient. */
|
|
73
|
+
export interface IExternalGatewayBootstrapTokenInput {
|
|
74
|
+
mode: 'sealed';
|
|
75
|
+
/** Sealed under `data.createGatewayBootstrapEnvelopeContext` for the request's expected target. */
|
|
76
|
+
envelope: plugins.smartcrypto.IX25519EnvelopeV1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enrolls Cloudly as its dcrouter gateway client with a dcrouter admin token an administrator
|
|
81
|
+
* hands in once.
|
|
82
|
+
*
|
|
83
|
+
* Cloudly opens the token, checks the target the request names against its own settings, asks
|
|
84
|
+
* dcrouter whether the token is an admin token, and provisions, finalizes and stores its own
|
|
85
|
+
* least-privilege gateway-client credential with it — under the policy its settings and the runtime
|
|
86
|
+
* network address plan state now. The admin token is used for this one call and never stored; the
|
|
87
|
+
* administrator revokes it at dcrouter afterwards. A refusal leaves the credential Cloudly already
|
|
88
|
+
* holds unchanged and is answered with one of `data.externalGatewayBootstrapRefusals`.
|
|
89
|
+
*
|
|
90
|
+
* This is the one way a gateway credential enters Cloudly, for a first enrollment and for the
|
|
91
|
+
* re-bootstrap a changed address plan or gateway setting asks for (`admin-bootstrap-required`).
|
|
92
|
+
*/
|
|
93
|
+
export interface IReq_Any_Cloudly_BootstrapExternalGatewayCredential
|
|
94
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
95
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
96
|
+
IReq_Any_Cloudly_BootstrapExternalGatewayCredential
|
|
97
|
+
> {
|
|
98
|
+
method: 'bootstrapExternalGatewayCredential';
|
|
99
|
+
request: {
|
|
100
|
+
identity: IIdentityCredential;
|
|
101
|
+
/** The `dcrouterGatewayUrl` the administrator read from Cloudly's settings. */
|
|
102
|
+
expectedGatewayUrl: string;
|
|
103
|
+
/** The `dcrouterGatewayClientId` the administrator read from Cloudly's settings. */
|
|
104
|
+
expectedGatewayClientId: string;
|
|
105
|
+
bootstrapToken: IExternalGatewayBootstrapTokenInput;
|
|
106
|
+
};
|
|
107
|
+
response: {
|
|
108
|
+
/** The enrollment after the bootstrap: `ready` when the new credential is Cloudly's runtime authority. */
|
|
109
|
+
state: IExternalGatewayEnrollmentState;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Cloudly's gateway-client enrollment state, for an administrator; it never carries a credential. */
|
|
114
|
+
export interface IReq_Any_Cloudly_GetExternalGatewayEnrollmentState
|
|
115
|
+
extends plugins.typedrequestInterfaces.implementsTR<
|
|
116
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
117
|
+
IReq_Any_Cloudly_GetExternalGatewayEnrollmentState
|
|
118
|
+
> {
|
|
119
|
+
method: 'getExternalGatewayEnrollmentState';
|
|
120
|
+
request: {
|
|
121
|
+
identity: IIdentityCredential;
|
|
122
|
+
};
|
|
123
|
+
response: {
|
|
124
|
+
state: IExternalGatewayEnrollmentState;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const hasJwtIdentity = (valueArg: unknown): boolean => {
|
|
129
|
+
const identity = readDataProperties(valueArg);
|
|
130
|
+
return identity !== null && typeof identity.jwt === 'string' && identity.jwt.length > 0;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Structural check of the target a bootstrap request names; empty when both are well formed. */
|
|
134
|
+
const validateExpectedGatewayTarget = (urlArg: unknown, clientIdArg: unknown): string[] => {
|
|
135
|
+
const errors: string[] = [];
|
|
136
|
+
let url: URL | undefined;
|
|
137
|
+
if (typeof urlArg === 'string'
|
|
138
|
+
&& urlArg.length > 0
|
|
139
|
+
&& urlArg.length <= settingsData.externalGatewayBootstrapContract.maximumGatewayUrlLength) {
|
|
140
|
+
try {
|
|
141
|
+
url = new URL(urlArg);
|
|
142
|
+
} catch {
|
|
143
|
+
url = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!url
|
|
147
|
+
|| url.protocol !== 'https:'
|
|
148
|
+
|| url.username !== ''
|
|
149
|
+
|| url.password !== ''
|
|
150
|
+
|| url.search !== ''
|
|
151
|
+
|| url.hash !== '') {
|
|
152
|
+
errors.push('bootstrap expectedGatewayUrl must be an HTTPS URL without credentials, query or fragment');
|
|
153
|
+
}
|
|
154
|
+
if (typeof clientIdArg !== 'string'
|
|
155
|
+
|| clientIdArg.length === 0
|
|
156
|
+
|| clientIdArg.length > settingsData.externalGatewayBootstrapContract.maximumGatewayClientIdLength
|
|
157
|
+
|| !/^[\x21-\x7e]+$/.test(clientIdArg)) {
|
|
158
|
+
errors.push('bootstrap expectedGatewayClientId must be a bounded identifier without whitespace');
|
|
159
|
+
}
|
|
160
|
+
return errors;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Checks a bootstrap request against its schema and the active ingress recipient, without opening
|
|
165
|
+
* the envelope: the recipient, the context digest of the named target and the ciphertext bound of
|
|
166
|
+
* `data.externalGatewayBootstrapContract.maximumTokenBytes`. Every problem is named.
|
|
167
|
+
*/
|
|
168
|
+
export const validateBootstrapExternalGatewayCredentialRequest = async (
|
|
169
|
+
requestArg: unknown,
|
|
170
|
+
activeIngressRecipientArg: IActiveSecretRecipientMetadata,
|
|
171
|
+
): Promise<string[]> => {
|
|
172
|
+
const request = readDataProperties(requestArg);
|
|
173
|
+
if (request === null) return ['bootstrap request must be a plain record'];
|
|
174
|
+
const errors: string[] = [];
|
|
175
|
+
if (!hasExactKeys(request, ['identity', 'expectedGatewayUrl', 'expectedGatewayClientId', 'bootstrapToken'])) {
|
|
176
|
+
errors.push('bootstrap request contains fields outside its schema');
|
|
177
|
+
}
|
|
178
|
+
if (!hasJwtIdentity(request.identity)) {
|
|
179
|
+
errors.push('bootstrap identity must be a JWT credential');
|
|
180
|
+
}
|
|
181
|
+
const targetErrors = validateExpectedGatewayTarget(
|
|
182
|
+
request.expectedGatewayUrl,
|
|
183
|
+
request.expectedGatewayClientId,
|
|
184
|
+
);
|
|
185
|
+
errors.push(...targetErrors);
|
|
186
|
+
const tokenInput = readDataProperties(request.bootstrapToken);
|
|
187
|
+
if (tokenInput === null || tokenInput.mode !== 'sealed') {
|
|
188
|
+
errors.push('bootstrap token must be a sealed input');
|
|
189
|
+
return errors;
|
|
190
|
+
}
|
|
191
|
+
const expectedContext = targetErrors.length === 0
|
|
192
|
+
? secretData.createGatewayBootstrapEnvelopeContext({
|
|
193
|
+
gatewayUrl: request.expectedGatewayUrl as string,
|
|
194
|
+
gatewayClientId: request.expectedGatewayClientId as string,
|
|
195
|
+
})
|
|
196
|
+
: new Uint8Array();
|
|
197
|
+
errors.push(...await secretData.validateSecretValueInputForRecipient(
|
|
198
|
+
tokenInput,
|
|
199
|
+
activeIngressRecipientArg,
|
|
200
|
+
expectedContext,
|
|
201
|
+
));
|
|
202
|
+
const envelope = readDataProperties(tokenInput.envelope);
|
|
203
|
+
if (envelope !== null
|
|
204
|
+
&& typeof envelope.ciphertext === 'string'
|
|
205
|
+
&& envelope.ciphertext.length
|
|
206
|
+
> Math.ceil(settingsData.externalGatewayBootstrapContract.maximumTokenBytes * 4 / 3)) {
|
|
207
|
+
errors.push(
|
|
208
|
+
`bootstrap token must not exceed ${settingsData.externalGatewayBootstrapContract.maximumTokenBytes} bytes`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return errors;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Checks an enrollment state read; every problem is named. */
|
|
215
|
+
export const validateGetExternalGatewayEnrollmentStateRequest = (requestArg: unknown): string[] => {
|
|
216
|
+
const request = readDataProperties(requestArg);
|
|
217
|
+
if (request === null) return ['enrollment state request must be a plain record'];
|
|
218
|
+
const errors: string[] = [];
|
|
219
|
+
if (!hasExactKeys(request, ['identity'])) {
|
|
220
|
+
errors.push('enrollment state request contains fields outside its schema');
|
|
221
|
+
}
|
|
222
|
+
if (!hasJwtIdentity(request.identity)) {
|
|
223
|
+
errors.push('enrollment state identity must be a JWT credential');
|
|
224
|
+
}
|
|
225
|
+
return errors;
|
|
226
|
+
};
|