@serve.zone/interfaces 32.2.0 → 32.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +69 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/cluster.d.ts +30 -0
- package/dist_ts/data/cluster.js +46 -2
- package/dist_ts/data/clusternode.d.ts +21 -0
- package/dist_ts/data/clusternode.js +345 -11
- package/dist_ts/data/clustervpn.d.ts +135 -0
- package/dist_ts/data/clustervpn.js +147 -0
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/networkaddressplan.d.ts +54 -11
- package/dist_ts/data/networkaddressplan.js +67 -23
- package/dist_ts/data/networkcredential.d.ts +13 -4
- package/dist_ts/data/networkcredential.js +41 -23
- package/dist_ts/data/runtimeassignment.d.ts +23 -0
- package/dist_ts/data/runtimeassignment.js +42 -7
- package/dist_ts/platform/storage.d.ts +19 -0
- package/dist_ts/platform/storage.js +23 -3
- package/dist_ts/private/networkcontract.d.ts +6 -0
- package/dist_ts/private/networkcontract.js +12 -1
- package/dist_ts/requests/cluster.d.ts +27 -0
- package/dist_ts/requests/cluster.js +1 -1
- package/package.json +1 -1
- package/readme.md +139 -21
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/cluster.ts +76 -1
- package/ts/data/clusternode.ts +457 -10
- package/ts/data/clustervpn.ts +248 -0
- package/ts/data/index.ts +1 -0
- package/ts/data/networkaddressplan.ts +103 -27
- package/ts/data/networkcredential.ts +52 -24
- package/ts/data/runtimeassignment.ts +58 -5
- package/ts/platform/storage.ts +27 -5
- package/ts/private/networkcontract.ts +11 -0
- package/ts/requests/cluster.ts +35 -0
|
@@ -55,7 +55,20 @@ export interface IRuntimeAssignment extends IRuntimeAssignmentScope {
|
|
|
55
55
|
/** No automatic relocation or expiry-based claim of a stopped writer. */
|
|
56
56
|
offlinePolicy: 'continue-node-bound';
|
|
57
57
|
workload: {
|
|
58
|
+
/**
|
|
59
|
+
* The registry that published this image, which is the publication identity the image release
|
|
60
|
+
* is bound to. It is never rewritten: only `pullEndpoint` decides where the bytes are fetched.
|
|
61
|
+
*/
|
|
58
62
|
registryHost: string;
|
|
63
|
+
/**
|
|
64
|
+
* Where this node fetches the image instead of `registryHost`, as `host[:port]`.
|
|
65
|
+
*
|
|
66
|
+
* A cluster whose relay forwards the registry states the relay's own origin here, so a node
|
|
67
|
+
* pulls inside its cluster and nothing cluster-side dials the control plane. Absent when the
|
|
68
|
+
* node pulls from the publishing registry itself. The reference stays digest-pinned either
|
|
69
|
+
* way, so the endpoint decides reachability and never content.
|
|
70
|
+
*/
|
|
71
|
+
pullEndpoint?: string;
|
|
59
72
|
repository: string;
|
|
60
73
|
imageDigest: TSha256Digest;
|
|
61
74
|
platform: TImmutableContainerPlatform;
|
|
@@ -99,7 +112,7 @@ const workloadAuthorityDemands: Readonly<Record<
|
|
|
99
112
|
export const requiredWorkloadAuthorities = (
|
|
100
113
|
workloadArg: IRuntimeAssignment['workload'],
|
|
101
114
|
): TRuntimeWorkloadAuthority[] => {
|
|
102
|
-
if (!isExactRecord(workloadArg, workloadKeys) || !Array.isArray(workloadArg.storage)) {
|
|
115
|
+
if (!isExactRecord(workloadArg, workloadKeys, optionalWorkloadKeys) || !Array.isArray(workloadArg.storage)) {
|
|
103
116
|
fail(workloadSchemaMessage);
|
|
104
117
|
}
|
|
105
118
|
return runtimeWorkloadAuthorities
|
|
@@ -107,6 +120,30 @@ export const requiredWorkloadAuthorities = (
|
|
|
107
120
|
.sort(compareWireTokens);
|
|
108
121
|
};
|
|
109
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The digest-pinned image reference a node pulls this workload by.
|
|
125
|
+
*
|
|
126
|
+
* One function decides where the bytes come from: the stated `pullEndpoint`, else the publishing
|
|
127
|
+
* `registryHost`. The digest is the same either way, so a node that pulls through its cluster's
|
|
128
|
+
* relay fetches exactly the content the publishing registry named, and a controller and a node can
|
|
129
|
+
* never disagree about the reference. The workload is read as a wire value, so one whose members
|
|
130
|
+
* this contract refuses is refused here rather than by a member read.
|
|
131
|
+
*/
|
|
132
|
+
export const buildRuntimeAssignmentImageReference = (
|
|
133
|
+
workloadArg: IRuntimeAssignment['workload'],
|
|
134
|
+
): string => {
|
|
135
|
+
if (!isExactRecord(workloadArg, workloadKeys, optionalWorkloadKeys)) {
|
|
136
|
+
fail(workloadSchemaMessage);
|
|
137
|
+
}
|
|
138
|
+
const endpoint = Object.hasOwn(workloadArg, 'pullEndpoint')
|
|
139
|
+
? workloadArg.pullEndpoint : workloadArg.registryHost;
|
|
140
|
+
if (typeof endpoint !== 'string' || typeof workloadArg.repository !== 'string'
|
|
141
|
+
|| !isDigest(workloadArg.imageDigest)) {
|
|
142
|
+
fail(workloadSchemaMessage);
|
|
143
|
+
}
|
|
144
|
+
return buildDigestPinnedImageReference(endpoint, workloadArg.repository, workloadArg.imageDigest);
|
|
145
|
+
};
|
|
146
|
+
|
|
110
147
|
/** True when a value is exactly a canonical authority list drawn from the authority vocabulary. */
|
|
111
148
|
export const isResolvedWorkloadAuthorities = (
|
|
112
149
|
valueArg: unknown,
|
|
@@ -152,18 +189,24 @@ const isPositiveInteger = (valueArg: unknown): valueArg is number =>
|
|
|
152
189
|
const isDigest = (valueArg: unknown): valueArg is TSha256Digest =>
|
|
153
190
|
typeof valueArg === 'string' && isSha256Digest(valueArg);
|
|
154
191
|
|
|
155
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* Reject accessors before reading any fields; do not run input-owned code.
|
|
194
|
+
*
|
|
195
|
+
* Absence stays absence: a stated optional key is read, an omitted one is not defaulted, so a
|
|
196
|
+
* controller that stated nothing is distinguishable from one that stated a value.
|
|
197
|
+
*/
|
|
156
198
|
const isExactRecord = (
|
|
157
199
|
valueArg: unknown,
|
|
158
200
|
keysArg: readonly string[],
|
|
201
|
+
optionalKeysArg: readonly string[] = [],
|
|
159
202
|
): valueArg is Record<string, unknown> => {
|
|
160
203
|
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
|
|
161
204
|
const prototype = Object.getPrototypeOf(valueArg);
|
|
162
205
|
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
163
206
|
const keys = Reflect.ownKeys(valueArg);
|
|
164
|
-
return
|
|
207
|
+
return keysArg.every((keyArg) => keys.includes(keyArg)) && keys.every((keyArg) => {
|
|
165
208
|
const descriptor = Object.getOwnPropertyDescriptor(valueArg, keyArg);
|
|
166
|
-
return typeof keyArg === 'string' && keysArg.includes(keyArg)
|
|
209
|
+
return typeof keyArg === 'string' && (keysArg.includes(keyArg) || optionalKeysArg.includes(keyArg))
|
|
167
210
|
&& descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
168
211
|
});
|
|
169
212
|
};
|
|
@@ -183,6 +226,8 @@ const isAssignmentReference = (valueArg: unknown): valueArg is IRuntimeAssignmen
|
|
|
183
226
|
/** The workload's exact members, read by the authority vocabulary and by the snapshot alike. */
|
|
184
227
|
const workloadKeys = ['registryHost', 'repository', 'imageDigest', 'platform',
|
|
185
228
|
'config', 'network', 'secrets', 'storage'] as const;
|
|
229
|
+
/** Stated only by a controller that sends the node somewhere else for the image bytes. */
|
|
230
|
+
const optionalWorkloadKeys = ['pullEndpoint'] as const;
|
|
186
231
|
const workloadSchemaMessage = 'runtime assignment workload must use exact immutable references';
|
|
187
232
|
|
|
188
233
|
const assignmentKeys = [
|
|
@@ -213,8 +258,10 @@ const assertAssignment = (valueArg: unknown): IRuntimeAssignment => {
|
|
|
213
258
|
fail('runtime assignment must bind its exact preceding generation');
|
|
214
259
|
}
|
|
215
260
|
const workload = valueArg.workload;
|
|
216
|
-
if (!isExactRecord(workload, workloadKeys)
|
|
261
|
+
if (!isExactRecord(workload, workloadKeys, optionalWorkloadKeys)
|
|
217
262
|
|| typeof workload.registryHost !== 'string' || workload.registryHost.length > 253
|
|
263
|
+
|| (Object.hasOwn(workload, 'pullEndpoint')
|
|
264
|
+
&& (typeof workload.pullEndpoint !== 'string' || workload.pullEndpoint.length > 253))
|
|
218
265
|
|| typeof workload.repository !== 'string' || workload.repository.length > 255
|
|
219
266
|
|| !isDigest(workload.imageDigest)
|
|
220
267
|
|| !['linux/amd64', 'linux/arm64'].includes(workload.platform as string)
|
|
@@ -227,6 +274,12 @@ const assertAssignment = (valueArg: unknown): IRuntimeAssignment => {
|
|
|
227
274
|
}
|
|
228
275
|
buildDigestPinnedImageReference(workload.registryHost as string,
|
|
229
276
|
workload.repository as string, workload.imageDigest as TSha256Digest);
|
|
277
|
+
// The pull endpoint is an OCI registry host like the publishing one, so the same reference the
|
|
278
|
+
// node builds proves it: an endpoint no reference can name is refused where it is written.
|
|
279
|
+
if (Object.hasOwn(workload, 'pullEndpoint')) {
|
|
280
|
+
buildDigestPinnedImageReference(workload.pullEndpoint as string,
|
|
281
|
+
workload.repository as string, workload.imageDigest as TSha256Digest);
|
|
282
|
+
}
|
|
230
283
|
let previousId = '';
|
|
231
284
|
for (let index = 0; index < (workload.storage as unknown[]).length; index++) {
|
|
232
285
|
const descriptor = Object.getOwnPropertyDescriptor(workload.storage, String(index));
|
package/ts/platform/storage.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TAppStoreStorageRequest } from '../appstore/types.js';
|
|
1
2
|
import type { TSecretManagementScope } from '../data/secret.js';
|
|
2
3
|
import {
|
|
3
4
|
canonicalizeStrictJson,
|
|
@@ -253,7 +254,31 @@ const fail = (reasonArg: string): never => {
|
|
|
253
254
|
throw new StorageContractError(reasonArg);
|
|
254
255
|
};
|
|
255
256
|
|
|
256
|
-
const { normalizeStorageRequest } =
|
|
257
|
+
const { normalizeStorageRequest: readStorageRequest } =
|
|
258
|
+
createStorageContractReaders(fail);
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The one judgement of what a storage request is: the request read against its exact schema and
|
|
262
|
+
* returned in its contract shape — required members present, optional members carried only when
|
|
263
|
+
* the request declares them, and every value within the bounds an adapter can fulfil.
|
|
264
|
+
*
|
|
265
|
+
* A control plane that admits requests, a fulfillment adapter that provisions them, and an App
|
|
266
|
+
* Store front end that assembles them all have to agree on the same acceptance rules; a second
|
|
267
|
+
* copy of them anywhere is a second answer to which requests exist, and the digest one side issues
|
|
268
|
+
* would name a request the other never accepted. `canonicalizeStorageRequest` and
|
|
269
|
+
* `createStorageRequestSha256` are derived from this function rather than the other way round, so
|
|
270
|
+
* a consumer that needs the judged request itself does not have to re-parse its own canonical
|
|
271
|
+
* bytes.
|
|
272
|
+
*
|
|
273
|
+
* The returned request is a fresh value; the input is never read again after normalization and
|
|
274
|
+
* never mutated. Anything that is not a storage request is refused with `StorageContractError`,
|
|
275
|
+
* whose message names the failing field so a consumer can map it to its own status vocabulary.
|
|
276
|
+
*/
|
|
277
|
+
export const normalizeStorageRequest = (
|
|
278
|
+
valueArg: unknown,
|
|
279
|
+
): TAppStoreStorageRequest => {
|
|
280
|
+
return readStorageRequest(valueArg, 'storage request');
|
|
281
|
+
};
|
|
257
282
|
|
|
258
283
|
/**
|
|
259
284
|
* The exact bytes a storage request's digest is taken over: the request normalized to its contract
|
|
@@ -264,10 +289,7 @@ const { normalizeStorageRequest } = createStorageContractReaders(fail);
|
|
|
264
289
|
* unjudged body would name a request no adapter could fulfil.
|
|
265
290
|
*/
|
|
266
291
|
export const canonicalizeStorageRequest = (valueArg: unknown): string => {
|
|
267
|
-
return canonicalizeStrictJson(
|
|
268
|
-
normalizeStorageRequest(valueArg, 'storage request'),
|
|
269
|
-
fail,
|
|
270
|
-
);
|
|
292
|
+
return canonicalizeStrictJson(normalizeStorageRequest(valueArg), fail);
|
|
271
293
|
};
|
|
272
294
|
|
|
273
295
|
/**
|
|
@@ -89,6 +89,17 @@ export const networkDigest = (value: unknown): TSha256Digest => {
|
|
|
89
89
|
export const networkSignature = (value: unknown): string => {
|
|
90
90
|
requireNetwork(typeof value === 'string' && /^[a-f0-9]{128}$/.test(value)); return value;
|
|
91
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* One encoding for every Noise key this contract carries — the managed VPN credential's, a relay's
|
|
94
|
+
* hub advertisement and a cluster's VPN network: canonical 32-byte base64, never the all-zero key.
|
|
95
|
+
* Key agreement and key-pair validation belong to the native Noise implementation, not to a reader.
|
|
96
|
+
*/
|
|
97
|
+
export const networkNoiseKey = (value: unknown): string => {
|
|
98
|
+
requireNetwork(typeof value === 'string' && /^[A-Za-z0-9+/]{43}=$/.test(value));
|
|
99
|
+
const decoded = atob(value as string);
|
|
100
|
+
requireNetwork(decoded.length === 32 && btoa(decoded) === value && [...decoded].some(byte => byte.charCodeAt(0) !== 0));
|
|
101
|
+
return value as string;
|
|
102
|
+
};
|
|
92
103
|
export const networkController = (value: unknown): IRuntimeController => {
|
|
93
104
|
const item = networkRecord(value, ['kind', 'id', 'epoch']);
|
|
94
105
|
requireNetwork(item.kind === 'cloudly' || item.kind === 'onebox');
|
package/ts/requests/cluster.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as userInterfaces from '../data/user.js';
|
|
2
2
|
import * as clusterInterfaces from '../data/cluster.js';
|
|
3
|
+
import type * as clusterVpnInterfaces from '../data/clustervpn.js';
|
|
3
4
|
import type { IClusterRuntime, TClusterRuntimeBlocker } from '../data/clusterruntime.js';
|
|
4
5
|
import type { IServiceRuntimeAssignmentView } from '../data/serviceruntime.js';
|
|
5
6
|
import type * as leaseInterfaces from '../data/networklease.js';
|
|
@@ -168,6 +169,40 @@ export interface IReq_Cloudly_Relay_HoldRuntimeNetworkDnsLeaseRenewals extends p
|
|
|
168
169
|
response: leaseInterfaces.IHoldRuntimeNetworkDnsLeaseRenewalsResponse;
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Cloudly pushes one cluster's complete managed VPN to that cluster's relay.
|
|
174
|
+
*
|
|
175
|
+
* The relay is its cluster's own hub: it runs the managed VPN server every node of the cluster
|
|
176
|
+
* dials, so this is the whole authority that hub serves, never a patch. It rides the relay's
|
|
177
|
+
* already registered session, which is why no cluster is named — the socket names it. The relay
|
|
178
|
+
* answers with what its hub applied, and refuses a network for another authority or a revision it
|
|
179
|
+
* has passed with one of `data.applyClusterVpnNetworkRefusals`.
|
|
180
|
+
*/
|
|
181
|
+
export interface IReq_Cloudly_Relay_ApplyClusterVpnNetwork extends plugins.typedrequestInterfaces.implementsTR<
|
|
182
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
183
|
+
IReq_Cloudly_Relay_ApplyClusterVpnNetwork
|
|
184
|
+
> {
|
|
185
|
+
method: 'applyClusterVpnNetwork';
|
|
186
|
+
request: clusterVpnInterfaces.IApplyClusterVpnNetworkRequest;
|
|
187
|
+
response: clusterVpnInterfaces.IApplyClusterVpnNetworkResponse;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The relay asks Cloudly for its cluster's current managed VPN.
|
|
192
|
+
*
|
|
193
|
+
* A hub daemon starts empty and its lifetime is the relay's own, invisible to Cloudly, so a relay
|
|
194
|
+
* that just bound its hub pulls the current network instead of waiting for the next push. The
|
|
195
|
+
* answer is `null` while the cluster has none.
|
|
196
|
+
*/
|
|
197
|
+
export interface IReq_Relay_Cloudly_GetClusterVpnNetwork extends plugins.typedrequestInterfaces.implementsTR<
|
|
198
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
199
|
+
IReq_Relay_Cloudly_GetClusterVpnNetwork
|
|
200
|
+
> {
|
|
201
|
+
method: 'getClusterVpnNetwork';
|
|
202
|
+
request: clusterVpnInterfaces.TGetClusterVpnNetworkRequest;
|
|
203
|
+
response: clusterVpnInterfaces.IGetClusterVpnNetworkResponse;
|
|
204
|
+
}
|
|
205
|
+
|
|
171
206
|
/**
|
|
172
207
|
* deletes a cluster
|
|
173
208
|
*/
|