@serve.zone/interfaces 32.4.0 → 32.5.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 +42 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/cluster.js +3 -2
- package/dist_ts/data/clusteringress.js +9 -8
- package/dist_ts/data/clusternode.d.ts +6 -1
- package/dist_ts/data/clusternode.js +32 -5
- package/dist_ts/data/clustervpn.js +3 -3
- package/dist_ts/data/isolatedrestore.js +12 -4
- package/dist_ts/data/networkauthority.js +9 -3
- package/dist_ts/data/networklease.js +3 -3
- package/dist_ts/data/networknode.js +3 -3
- package/dist_ts/data/networkprojection.js +8 -3
- package/dist_ts/data/nodeenrollment.js +4 -2
- package/dist_ts/data/noderetirement.js +3 -3
- package/dist_ts/data/runtimeassignment.d.ts +12 -0
- package/dist_ts/data/runtimeassignment.js +29 -4
- package/dist_ts/data/runtimesession.js +3 -2
- package/dist_ts/data/serviceruntime.js +6 -5
- package/dist_ts/data/sparknodereport.js +6 -3
- package/dist_ts/private/networkcontract.d.ts +2 -0
- package/dist_ts/private/networkcontract.js +8 -2
- package/dist_ts/private/nodeidentifier.d.ts +22 -0
- package/dist_ts/private/nodeidentifier.js +23 -0
- package/dist_ts/requests/corestore.js +4 -4
- package/dist_ts/runtime.js +7 -4
- package/package.json +1 -1
- package/readme.md +28 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/cluster.ts +2 -1
- package/ts/data/clusteringress.ts +8 -7
- package/ts/data/clusternode.ts +32 -4
- package/ts/data/clustervpn.ts +2 -2
- package/ts/data/isolatedrestore.ts +12 -3
- package/ts/data/networkauthority.ts +8 -2
- package/ts/data/networklease.ts +2 -2
- package/ts/data/networknode.ts +2 -2
- package/ts/data/networkprojection.ts +6 -2
- package/ts/data/nodeenrollment.ts +4 -2
- package/ts/data/noderetirement.ts +2 -2
- package/ts/data/runtimeassignment.ts +26 -3
- package/ts/data/runtimesession.ts +3 -2
- package/ts/data/serviceruntime.ts +6 -5
- package/ts/data/sparknodereport.ts +7 -2
- package/ts/private/networkcontract.ts +6 -1
- package/ts/private/nodeidentifier.ts +25 -0
- package/ts/requests/corestore.ts +3 -3
- package/ts/runtime.ts +6 -3
|
@@ -3,6 +3,7 @@ import { buildDigestPinnedImageReference, isSha256Digest } from './immutableimag
|
|
|
3
3
|
import {
|
|
4
4
|
canonicalizeStrictJson, compareWireTokens, createCanonicalJsonSha256Hex,
|
|
5
5
|
} from '../private/canonicaljson.js';
|
|
6
|
+
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
6
7
|
import { isCanonicalTokenList } from '../private/tokenlist.js';
|
|
7
8
|
|
|
8
9
|
export interface IRuntimeController {
|
|
@@ -184,6 +185,27 @@ export const runtimeAssignmentContract = Object.freeze({
|
|
|
184
185
|
const fail: (messageArg: string) => never = (messageArg) => { throw new Error(messageArg); };
|
|
185
186
|
const isIdentifier = (valueArg: unknown): valueArg is string =>
|
|
186
187
|
typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg);
|
|
188
|
+
/** The index of a replica slot: counted from zero by its producer, never zero-padded. */
|
|
189
|
+
const isReplicaIndex = (valueArg: string): boolean => /^(?:0|[1-9][0-9]{0,8})$/.test(valueArg);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* True when a value is exactly a replica id.
|
|
193
|
+
*
|
|
194
|
+
* A producer names a replica slot after the node that runs it — `<node id>.<index>`, counted from
|
|
195
|
+
* zero — so a replica id that carries an index is read by decomposition: the part before the last
|
|
196
|
+
* dot in the node vocabulary, the index on its own. Reading the composed form as one canonical
|
|
197
|
+
* identifier would refuse every replica of a node whose id begins with `-` or `_`, a node these
|
|
198
|
+
* contracts themselves enrol and register. A name that carries no such index — a zero-padded index
|
|
199
|
+
* among them — is an opaque slot name and is read whole in the canonical identifier vocabulary, as
|
|
200
|
+
* it always was.
|
|
201
|
+
*/
|
|
202
|
+
export const isRuntimeReplicaId = (valueArg: unknown): valueArg is string => {
|
|
203
|
+
if (typeof valueArg !== 'string') return false;
|
|
204
|
+
const separator = valueArg.lastIndexOf('.');
|
|
205
|
+
return separator > 0 && isReplicaIndex(valueArg.slice(separator + 1))
|
|
206
|
+
? isClusterNodeId(valueArg.slice(0, separator))
|
|
207
|
+
: isIdentifier(valueArg);
|
|
208
|
+
};
|
|
187
209
|
const isPositiveInteger = (valueArg: unknown): valueArg is number =>
|
|
188
210
|
typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg > 0;
|
|
189
211
|
const isDigest = (valueArg: unknown): valueArg is TSha256Digest =>
|
|
@@ -244,10 +266,11 @@ const assertAssignment = (valueArg: unknown): IRuntimeAssignment => {
|
|
|
244
266
|
|| !['run', 'stop-preserve', 'remove-runtime-preserve-storage'].includes(
|
|
245
267
|
valueArg.disposition as string,
|
|
246
268
|
)) fail('runtime assignment must use its exact schema');
|
|
247
|
-
for (const key of ['organizationId', '
|
|
248
|
-
'serviceId', 'replicaId', 'attemptId']) {
|
|
269
|
+
for (const key of ['organizationId', 'runtimeNamespace', 'assignmentId', 'serviceId', 'attemptId']) {
|
|
249
270
|
if (!isIdentifier(valueArg[key])) fail(`runtime assignment ${key} must be a canonical identifier`);
|
|
250
271
|
}
|
|
272
|
+
if (!isClusterNodeId(valueArg.nodeId)) fail('runtime assignment nodeId must be a node id');
|
|
273
|
+
if (!isRuntimeReplicaId(valueArg.replicaId)) fail('runtime assignment replicaId must be a replica id');
|
|
251
274
|
for (const key of ['workerGroupId', 'siteId']) {
|
|
252
275
|
if (valueArg[key] !== null && !isIdentifier(valueArg[key])) fail('invalid runtime topology ID');
|
|
253
276
|
}
|
|
@@ -357,7 +380,7 @@ export const evaluateRuntimeAssignment = async (
|
|
|
357
380
|
acceptedArg = acceptedArg === null ? null : snapshotRuntimeAssignment(acceptedArg);
|
|
358
381
|
if (!isExactRecord(trustedScopeArg, ['controller', 'organizationId', 'nodeId', 'runtimeNamespace'])
|
|
359
382
|
|| !isController(trustedScopeArg.controller)
|
|
360
|
-
|| !isIdentifier(trustedScopeArg.organizationId) || !
|
|
383
|
+
|| !isIdentifier(trustedScopeArg.organizationId) || !isClusterNodeId(trustedScopeArg.nodeId)
|
|
361
384
|
|| !isIdentifier(trustedScopeArg.runtimeNamespace)) return { status: 'reject', reason: 'invalid' };
|
|
362
385
|
trustedScopeArg = JSON.parse(canonical(trustedScopeArg)) as IRuntimeAssignmentScope;
|
|
363
386
|
} catch {
|
|
@@ -12,6 +12,7 @@ import { negotiateProtocol, readProtocolOffer, type IProtocolOffer } from '../pr
|
|
|
12
12
|
import type { IRuntimeConfig } from './runtimeconfig.js';
|
|
13
13
|
import { bindRuntimeConfigToAssignment, snapshotRuntimeConfig } from './runtimeconfig.js';
|
|
14
14
|
import { canonicalizeStrictJson } from '../private/canonicaljson.js';
|
|
15
|
+
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Server-issued public binding for the current authenticated physical connection.
|
|
@@ -178,8 +179,8 @@ const identifier = (valueArg: unknown): string => {
|
|
|
178
179
|
return valueArg;
|
|
179
180
|
};
|
|
180
181
|
const nodeId = (valueArg: unknown): string => {
|
|
181
|
-
requireValue(
|
|
182
|
-
return valueArg;
|
|
182
|
+
requireValue(isClusterNodeId(valueArg));
|
|
183
|
+
return valueArg as string;
|
|
183
184
|
};
|
|
184
185
|
/** One node process, in the same canonical form as the native boot UUID. */
|
|
185
186
|
const nodeInstanceNonce = (valueArg: unknown): string => {
|
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
} from './runtimeassignment.js';
|
|
4
4
|
import type { IRuntimeConfigLogPolicy, TRuntimeConfigReadiness } from './runtimeconfig.js';
|
|
5
5
|
import { runtimeNetworkAuthorityContract } from './networkauthority.js';
|
|
6
|
-
import { captureNetwork, networkCanonical, networkCounter, networkId, networkList, networkRecord, networkSorted,
|
|
6
|
+
import { captureNetwork, networkCanonical, networkCounter, networkId, networkList, networkNodeId, networkRecord, networkSorted,
|
|
7
7
|
requireNetwork } from '../private/networkcontract.js';
|
|
8
8
|
import { failProducer, isOneOf, producerSafe, variantOf } from '../private/producercontract.js';
|
|
9
9
|
import { assertRuntimeReadiness, isBoundedInteger, isRuntimeCpuMillis, isRuntimeIdentityNumber, isRuntimeLogPolicy,
|
|
@@ -138,8 +138,9 @@ export interface IServiceRuntimeAssignmentView {
|
|
|
138
138
|
delivery: TServiceRuntimeAssignmentDelivery;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
/** Sorted, duplicate-free and read by the vocabulary the ids belong to: a node's, or the canonical one. */
|
|
142
|
+
const sortedIds = (value: unknown, maximum: number, read: (item: unknown) => string): string[] =>
|
|
143
|
+
networkSorted(networkList(value, maximum, read), id => id);
|
|
143
144
|
|
|
144
145
|
const placementOf = (value: unknown): TServiceRuntimePlacement => {
|
|
145
146
|
const kind = variantOf(value, 'kind');
|
|
@@ -148,7 +149,7 @@ const placementOf = (value: unknown): TServiceRuntimePlacement => {
|
|
|
148
149
|
return { kind };
|
|
149
150
|
}
|
|
150
151
|
requireNetwork(kind === 'nodes');
|
|
151
|
-
const nodeIds = sortedIds(networkRecord(value, ['kind', 'nodeIds']).nodeIds, serviceRuntimeContract.maximumPinnedNodes);
|
|
152
|
+
const nodeIds = sortedIds(networkRecord(value, ['kind', 'nodeIds']).nodeIds, serviceRuntimeContract.maximumPinnedNodes, networkNodeId);
|
|
152
153
|
requireNetwork(nodeIds.length > 0);
|
|
153
154
|
return { kind, nodeIds };
|
|
154
155
|
};
|
|
@@ -182,7 +183,7 @@ const networkOf = (value: unknown): TServiceRuntimeNetwork => {
|
|
|
182
183
|
const item = networkRecord(value, ['mode', 'publicEgress', 'platformEndpointIds']);
|
|
183
184
|
requireNetwork(typeof item.publicEgress === 'boolean');
|
|
184
185
|
return { mode, publicEgress: item.publicEgress,
|
|
185
|
-
platformEndpointIds: sortedIds(item.platformEndpointIds, serviceRuntimeContract.maximumPlatformEndpoints) };
|
|
186
|
+
platformEndpointIds: sortedIds(item.platformEndpointIds, serviceRuntimeContract.maximumPlatformEndpoints, networkId) };
|
|
186
187
|
};
|
|
187
188
|
|
|
188
189
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isReleaseVersion } from '../private/releaseversion.js';
|
|
2
2
|
import { readExactDataRecord } from '../private/dataproperties.js';
|
|
3
|
+
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
3
4
|
import { protocolRefusalHttpStatus, readProtocolOffer, type IProtocolOffer } from '../protocol.js';
|
|
4
5
|
|
|
5
6
|
/** Host observations only. Container and workload readiness belong to Pallet. */
|
|
@@ -80,6 +81,10 @@ const text = (valueArg: unknown, patternArg: RegExp, maxLengthArg: number): stri
|
|
|
80
81
|
&& valueArg.match(patternArg)?.[0] === valueArg);
|
|
81
82
|
return valueArg;
|
|
82
83
|
};
|
|
84
|
+
// The shared vocabulary, not a copy of it: the id on this wire is the id the node enrolled under.
|
|
85
|
+
const nodeId = (valueArg: unknown): string => (
|
|
86
|
+
isClusterNodeId(valueArg) ? valueArg : fail()
|
|
87
|
+
);
|
|
83
88
|
// The shared grammar, not a copy of it: a stored bundle version has to be one the release
|
|
84
89
|
// comparison can order, and a second rule here would be a second answer to that question.
|
|
85
90
|
const releaseVersion = (valueArg: unknown): string => (
|
|
@@ -131,7 +136,7 @@ export const snapshotSparkNodeReport = (valueArg: unknown): ISparkNodeReport =>
|
|
|
131
136
|
export const snapshotSparkNodeHeartbeatRequest = (valueArg: unknown): ISparkNodeHeartbeatRequest => {
|
|
132
137
|
const value = record(valueArg, ['nodeId', 'nodeToken', 'protocol', 'report']);
|
|
133
138
|
return Object.freeze({
|
|
134
|
-
nodeId:
|
|
139
|
+
nodeId: nodeId(value.nodeId),
|
|
135
140
|
// Both freshly enrolled and explicitly adopted credentials use this bearer format.
|
|
136
141
|
nodeToken: text(value.nodeToken, /^secureToken[a-f0-9]{64}$/, 75),
|
|
137
142
|
protocol: readProtocolOffer(value),
|
|
@@ -144,7 +149,7 @@ export const snapshotSparkNodeHeartbeatReceipt = (valueArg: unknown): ISparkNode
|
|
|
144
149
|
'sessionEpoch', 'acceptedAt', 'protocol']);
|
|
145
150
|
requireValue(value.accepted === true);
|
|
146
151
|
return Object.freeze({
|
|
147
|
-
accepted: true, nodeId:
|
|
152
|
+
accepted: true, nodeId: nodeId(value.nodeId),
|
|
148
153
|
credentialGeneration: integer(value.credentialGeneration, 1),
|
|
149
154
|
sessionEpoch: integer(value.sessionEpoch, 0), acceptedAt: integer(value.acceptedAt, 1),
|
|
150
155
|
protocol: readProtocolOffer(value),
|
|
@@ -3,6 +3,7 @@ import type { IRuntimeAuthorityReference, IRuntimeController } from '../data/run
|
|
|
3
3
|
import type { IRuntimeNetworkDnsLeaseRenewal } from '../data/networklease.js';
|
|
4
4
|
import type { IRuntimeNetworkDnsWindow, IRuntimeNetworkNodeScope, IRuntimeNetworkProjection } from '../data/networkprojection.js';
|
|
5
5
|
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from './canonicaljson.js';
|
|
6
|
+
import { isClusterNodeId } from './nodeidentifier.js';
|
|
6
7
|
|
|
7
8
|
export const failNetwork = (): never => { throw new Error('Runtime network projection contract rejected.'); };
|
|
8
9
|
export function requireNetwork(value: unknown): asserts value { if (!value) failNetwork(); }
|
|
@@ -79,6 +80,10 @@ export const networkSorted = <T>(values: T[], key: (item: T) => string): T[] =>
|
|
|
79
80
|
export const networkId = (value: unknown): string => {
|
|
80
81
|
requireNetwork(typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(value)); return value;
|
|
81
82
|
};
|
|
83
|
+
/** A node id is read in the node vocabulary here as everywhere else, never in the canonical one. */
|
|
84
|
+
export const networkNodeId = (value: unknown): string => {
|
|
85
|
+
requireNetwork(isClusterNodeId(value)); return value as string;
|
|
86
|
+
};
|
|
82
87
|
export const networkCounter = (value: unknown, maximum = Number.MAX_SAFE_INTEGER - 1): number => {
|
|
83
88
|
requireNetwork(typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= maximum); return value;
|
|
84
89
|
};
|
|
@@ -106,7 +111,7 @@ export const networkController = (value: unknown): IRuntimeController => {
|
|
|
106
111
|
return { kind: item.kind, id: networkId(item.id), epoch: networkId(item.epoch) };
|
|
107
112
|
};
|
|
108
113
|
export const networkScope = (item: Record<string, unknown>): IRuntimeNetworkNodeScope => ({
|
|
109
|
-
controller: networkController(item.controller), nodeId:
|
|
114
|
+
controller: networkController(item.controller), nodeId: networkNodeId(item.nodeId), runtimeNamespace: networkId(item.runtimeNamespace),
|
|
110
115
|
});
|
|
111
116
|
export const networkSameScope = (left: IRuntimeNetworkNodeScope, right: IRuntimeNetworkNodeScope): boolean =>
|
|
112
117
|
networkEqual(left.controller, right.controller) && left.nodeId === right.nodeId && left.runtimeNamespace === right.runtimeNamespace;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one vocabulary a node of a cluster is named in.
|
|
3
|
+
*
|
|
4
|
+
* It lives beside the canonical JSON and data-property helpers rather than in a contract module
|
|
5
|
+
* because every family reads it — the node's enrollment, its runtime session, the Spark wire, its
|
|
6
|
+
* assignments, the private network contract and the relay block — and a second spelling of the rule
|
|
7
|
+
* is a second answer to which nodes exist.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* True when a value is exactly a node id: the URL alphabet, a leading `-` or `_` included,
|
|
12
|
+
* one to 128 characters.
|
|
13
|
+
*
|
|
14
|
+
* These contracts disagreed with themselves about what a node may be called: enrollment, the
|
|
15
|
+
* session registration and the Spark wire admit a separator-led node id, which makes this the
|
|
16
|
+
* vocabulary a stored node id is written in, while the assignment and the network members read
|
|
17
|
+
* that same id by the canonical identifier vocabulary — whose first character must be
|
|
18
|
+
* alphanumeric — and refuse it, and with it the assignment, the runtime network and, because one
|
|
19
|
+
* refused egress owner fails the whole pass, its entire cluster's protected authority. A
|
|
20
|
+
* controller now keeps the first character alphanumeric where it mints, so the ids this one
|
|
21
|
+
* vocabulary serves are the ones already stored under the members that admit them.
|
|
22
|
+
*/
|
|
23
|
+
export const isClusterNodeId = (valueArg: unknown): valueArg is string => (
|
|
24
|
+
typeof valueArg === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(valueArg)
|
|
25
|
+
);
|
package/ts/requests/corestore.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import type { IIdentity } from '../data/user.js';
|
|
3
3
|
import { hasExactKeys } from '../private/dataproperties.js';
|
|
4
|
+
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* A provisioned corestore resource (NoSQLDB database or ObjectStorage bucket)
|
|
@@ -189,9 +190,8 @@ export const validateClusterCorestoreInventory = (
|
|
|
189
190
|
return [`${pathArg} must use its exact schema`];
|
|
190
191
|
}
|
|
191
192
|
const errors: string[] = [];
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
errors.push(`${pathArg} nodeId must be a bounded identifier`);
|
|
193
|
+
if (!isClusterNodeId(valueArg.nodeId)) {
|
|
194
|
+
errors.push(`${pathArg} nodeId must be a node id`);
|
|
195
195
|
}
|
|
196
196
|
if (!Number.isSafeInteger(valueArg.checkedAt) || (valueArg.checkedAt as number) < 0) {
|
|
197
197
|
errors.push(`${pathArg} checkedAt must be a non-negative safe integer`);
|
package/ts/runtime.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from './data/clusternode.js';
|
|
30
30
|
import { isCoreflowRuntimeCapabilityList, isSha256Digest } from './data/immutableimage.js';
|
|
31
31
|
import { hasExactKeys } from './private/dataproperties.js';
|
|
32
|
+
import { isClusterNodeId } from './private/nodeidentifier.js';
|
|
32
33
|
import type {
|
|
33
34
|
IWorkloadInitActiveApprovalAuthority,
|
|
34
35
|
IWorkloadInitApprovalAuthorityReference,
|
|
@@ -759,8 +760,10 @@ const validateRuntimeTargets = (
|
|
|
759
760
|
errors.push(`${pathArg}[${index}] must use its exact schema`);
|
|
760
761
|
continue;
|
|
761
762
|
}
|
|
762
|
-
if (!
|
|
763
|
-
|
|
763
|
+
if (!isClusterNodeId(targetArg.cloudlyNodeId)) {
|
|
764
|
+
errors.push(`${pathArg}[${index}].cloudlyNodeId must be a node id`);
|
|
765
|
+
}
|
|
766
|
+
if (!isRuntimeIdentifier(targetArg.swarmClusterId)
|
|
764
767
|
|| !isRuntimeIdentifier(targetArg.swarmNodeId)
|
|
765
768
|
|| !isRuntimeIdentifier(targetArg.nodeName)) {
|
|
766
769
|
errors.push(`${pathArg}[${index}] node identity must be canonical`);
|
|
@@ -768,7 +771,7 @@ const validateRuntimeTargets = (
|
|
|
768
771
|
if (!runtimePlatforms.has(targetArg.platform as TImmutableContainerPlatform)) {
|
|
769
772
|
errors.push(`${pathArg}[${index}].platform must be supported`);
|
|
770
773
|
}
|
|
771
|
-
if (
|
|
774
|
+
if (isClusterNodeId(targetArg.cloudlyNodeId)
|
|
772
775
|
&& isRuntimeIdentifier(targetArg.swarmClusterId)
|
|
773
776
|
&& isRuntimeIdentifier(targetArg.swarmNodeId)
|
|
774
777
|
&& isRuntimeIdentifier(targetArg.nodeName)
|