@serve.zone/interfaces 32.16.0 → 32.17.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.
@@ -0,0 +1,206 @@
1
+ import { isReleaseVersion } from '../private/releaseversion.js';
2
+ import { captureNetwork, networkCounter, networkDigest, networkEqual, networkHash, networkId, networkNodeId, networkRecord, networkReference, networkRevision, requireNetwork, } from '../private/networkcontract.js';
3
+ /** The two runtimes a released Spark could be put into by its `mode` configuration key. */
4
+ export const sparkLegacyRuntimeModes = ['cloudly', 'coreflow-node'];
5
+ /**
6
+ * Which Docker services a released mode owns, sorted, so a refusal names a mode instead of
7
+ * restating a set. A record over the mode union: a mode cannot be added without naming its
8
+ * services, and a reader can never meet a mode whose services it does not know.
9
+ */
10
+ export const sparkLegacyRuntimeModeServices = Object.freeze({
11
+ 'cloudly': Object.freeze(['cloudly']),
12
+ 'coreflow-node': Object.freeze(['coreflow', 'corestore', 'coretraffic']),
13
+ });
14
+ export const sparkLegacyRetirementContract = Object.freeze({
15
+ modeRefusalDigestDomain: 'serve.zone/spark-legacy-mode-refusal',
16
+ configurationRetirementDigestDomain: 'serve.zone/spark-legacy-configuration-retirement',
17
+ });
18
+ /** Every reason Cloudly answers instead of advancing a node's legacy retirement state. */
19
+ export const sparkLegacyRetirementRefusals = Object.freeze([
20
+ /** The observed owner generation is below the accepted one. */
21
+ 'legacy-retirement-generation-stale',
22
+ /** The observed owner generation equals the accepted one and states something else. */
23
+ 'legacy-retirement-generation-conflict',
24
+ /** A node imported one source once; another source digest describes another host. */
25
+ 'legacy-retirement-source-changed',
26
+ /** A removed source cannot become a retained one. */
27
+ 'legacy-retirement-disposition-reversed',
28
+ /** The stated bundle is below the release whose refusal this server accepts as real. */
29
+ 'legacy-retirement-bundle-below-minimum',
30
+ ]);
31
+ const fail = () => { throw new Error('Spark legacy retirement contract rejected.'); };
32
+ const safe = (operationArg) => {
33
+ try {
34
+ return operationArg();
35
+ }
36
+ catch {
37
+ return fail();
38
+ }
39
+ };
40
+ const sha256Hex = (valueArg) => {
41
+ requireNetwork(typeof valueArg === 'string' && /^[a-f0-9]{64}$/.test(valueArg));
42
+ return valueArg;
43
+ };
44
+ const sourceCommit = (valueArg) => {
45
+ requireNetwork(typeof valueArg === 'string' && /^[a-f0-9]{40}$/.test(valueArg));
46
+ return valueArg;
47
+ };
48
+ const releaseVersion = (valueArg) => {
49
+ requireNetwork(isReleaseVersion(valueArg));
50
+ return valueArg;
51
+ };
52
+ const legacyRuntimeMode = (valueArg) => {
53
+ requireNetwork(sparkLegacyRuntimeModes.includes(valueArg));
54
+ return valueArg;
55
+ };
56
+ const configurationDisposition = (valueArg) => {
57
+ requireNetwork(valueArg === 'retained-inert' || valueArg === 'removed');
58
+ return valueArg;
59
+ };
60
+ /** Structural copy only; whether the tree it describes is installed is the node's to observe. */
61
+ export const snapshotSparkNodeBundleIdentity = (valueArg) => safe(() => {
62
+ const value = networkRecord(captureNetwork(valueArg), [
63
+ 'version', 'sourceCommit', 'manifestSha256', 'unitName', 'unitDigest',
64
+ ]);
65
+ return {
66
+ version: releaseVersion(value.version),
67
+ sourceCommit: sourceCommit(value.sourceCommit),
68
+ manifestSha256: sha256Hex(value.manifestSha256),
69
+ unitName: networkId(value.unitName),
70
+ unitDigest: networkDigest(value.unitDigest),
71
+ };
72
+ });
73
+ export const snapshotSparkLegacyConfigurationState = (valueArg) => safe(() => {
74
+ const value = networkRecord(captureNetwork(valueArg), [
75
+ 'ownerGeneration', 'sourceSha256', 'disposition',
76
+ ]);
77
+ return {
78
+ ownerGeneration: networkCounter(value.ownerGeneration),
79
+ sourceSha256: sha256Hex(value.sourceSha256),
80
+ disposition: configurationDisposition(value.disposition),
81
+ };
82
+ });
83
+ export const snapshotSparkLegacyRuntimeRetirement = (valueArg) => safe(() => {
84
+ const value = networkRecord(captureNetwork(valueArg), [
85
+ 'refusedMode', 'bundle', 'configuration',
86
+ ]);
87
+ return {
88
+ refusedMode: legacyRuntimeMode(value.refusedMode),
89
+ bundle: snapshotSparkNodeBundleIdentity(value.bundle),
90
+ configuration: snapshotSparkLegacyConfigurationState(value.configuration),
91
+ };
92
+ });
93
+ /**
94
+ * Synchronous because the member carries no digest: the observation that transports it is
95
+ * digested as a whole, so a second domain here would only be a second thing to keep in step.
96
+ */
97
+ export const validateSparkLegacyRuntimeRetirement = (valueArg) => {
98
+ try {
99
+ snapshotSparkLegacyRuntimeRetirement(valueArg);
100
+ return [];
101
+ }
102
+ catch {
103
+ return ['Spark legacy runtime retirement must use its exact canonical schema'];
104
+ }
105
+ };
106
+ /** The revision rule every accepted receipt of this contract obeys, read once for both of them. */
107
+ const retirementReceiptBase = (valueArg, ownKeysArg) => {
108
+ const value = networkRecord(captureNetwork(valueArg), [
109
+ 'id', 'generation', 'previous', 'clusterId', 'nodeId', 'observation', 'reporterSessionId',
110
+ 'observationSequence', 'acceptedAt', 'digest',
111
+ ...ownKeysArg,
112
+ ]);
113
+ const { id, generation, previous } = networkRevision(value);
114
+ return {
115
+ base: {
116
+ id,
117
+ generation,
118
+ previous,
119
+ clusterId: networkId(value.clusterId),
120
+ nodeId: networkNodeId(value.nodeId),
121
+ observation: networkReference(value.observation),
122
+ reporterSessionId: networkId(value.reporterSessionId),
123
+ observationSequence: networkCounter(value.observationSequence),
124
+ acceptedAt: networkCounter(value.acceptedAt),
125
+ digest: networkDigest(value.digest),
126
+ },
127
+ record: value,
128
+ };
129
+ };
130
+ export const snapshotSparkLegacyModeRefusal = (valueArg) => safe(() => {
131
+ const { base, record } = retirementReceiptBase(valueArg, ['refusedMode', 'bundle']);
132
+ return {
133
+ ...base,
134
+ refusedMode: legacyRuntimeMode(record.refusedMode),
135
+ bundle: snapshotSparkNodeBundleIdentity(record.bundle),
136
+ };
137
+ });
138
+ export const computeSparkLegacyModeRefusalDigest = async (valueArg) => networkHash(sparkLegacyRetirementContract.modeRefusalDigestDomain, snapshotSparkLegacyModeRefusal(valueArg));
139
+ export const validateSparkLegacyModeRefusal = async (valueArg) => {
140
+ try {
141
+ const value = snapshotSparkLegacyModeRefusal(valueArg);
142
+ return value.digest === await computeSparkLegacyModeRefusalDigest(value)
143
+ ? [] : ['Spark legacy mode refusal digest does not match'];
144
+ }
145
+ catch {
146
+ return ['Spark legacy mode refusal must use its exact canonical schema'];
147
+ }
148
+ };
149
+ export const snapshotSparkLegacyConfigurationRetirement = (valueArg) => safe(() => {
150
+ const { base, record } = retirementReceiptBase(valueArg, ['configuration']);
151
+ return { ...base, configuration: snapshotSparkLegacyConfigurationState(record.configuration) };
152
+ });
153
+ export const computeSparkLegacyConfigurationRetirementDigest = async (valueArg) => networkHash(sparkLegacyRetirementContract.configurationRetirementDigestDomain, snapshotSparkLegacyConfigurationRetirement(valueArg));
154
+ export const validateSparkLegacyConfigurationRetirement = async (valueArg) => {
155
+ try {
156
+ const value = snapshotSparkLegacyConfigurationRetirement(valueArg);
157
+ return value.digest === await computeSparkLegacyConfigurationRetirementDigest(value)
158
+ ? [] : ['Spark legacy configuration retirement digest does not match'];
159
+ }
160
+ catch {
161
+ return ['Spark legacy configuration retirement must use its exact canonical schema'];
162
+ }
163
+ };
164
+ /**
165
+ * Whether one observed configuration state is a legal successor of the accepted one.
166
+ *
167
+ * `null` is a node's first accepted state. A replay states the accepted generation again and is
168
+ * therefore not an advance: it renews nothing, which is what keeps `acceptedAt` and the named
169
+ * observation from aging forward on a node that has stopped changing.
170
+ */
171
+ export const advancesSparkLegacyConfigurationState = (currentArg, nextArg) => {
172
+ const next = snapshotSparkLegacyConfigurationState(nextArg);
173
+ if (currentArg === null)
174
+ return true;
175
+ if (next.sourceSha256 !== currentArg.sourceSha256)
176
+ return false;
177
+ if (next.ownerGeneration <= currentArg.ownerGeneration)
178
+ return false;
179
+ return !(currentArg.disposition === 'removed' && next.disposition !== 'removed');
180
+ };
181
+ /**
182
+ * Joins the two accepted receipts a Coreflow retirement cites to the one observation they were
183
+ * derived from.
184
+ *
185
+ * Content join only. Whether each receipt is its node's current revision, and whether the
186
+ * observation it names is still fresh, remain Cloudly's to decide against its own records.
187
+ */
188
+ export const bindSparkLegacyRetirement = async (modeRefusalArg, configurationRetirementArg) => {
189
+ try {
190
+ const modeRefusal = snapshotSparkLegacyModeRefusal(modeRefusalArg);
191
+ const configurationRetirement = snapshotSparkLegacyConfigurationRetirement(configurationRetirementArg);
192
+ if ((await validateSparkLegacyModeRefusal(modeRefusal)).length
193
+ || (await validateSparkLegacyConfigurationRetirement(configurationRetirement)).length)
194
+ fail();
195
+ requireNetwork(modeRefusal.clusterId === configurationRetirement.clusterId
196
+ && modeRefusal.nodeId === configurationRetirement.nodeId
197
+ && modeRefusal.reporterSessionId === configurationRetirement.reporterSessionId
198
+ && modeRefusal.observationSequence === configurationRetirement.observationSequence
199
+ && networkEqual(modeRefusal.observation, configurationRetirement.observation));
200
+ return { modeRefusal, configurationRetirement };
201
+ }
202
+ catch {
203
+ return fail();
204
+ }
205
+ };
206
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3BhcmtsZWdhY3lyZXRpcmVtZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9zcGFya2xlZ2FjeXJldGlyZW1lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUEsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDaEUsT0FBTyxFQUNMLGNBQWMsRUFDZCxjQUFjLEVBQ2QsYUFBYSxFQUNiLFlBQVksRUFDWixXQUFXLEVBQ1gsU0FBUyxFQUNULGFBQWEsRUFDYixhQUFhLEVBQ2IsZ0JBQWdCLEVBQ2hCLGVBQWUsRUFDZixjQUFjLEdBQ2YsTUFBTSwrQkFBK0IsQ0FBQztBQUV2QywyRkFBMkY7QUFDM0YsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUcsQ0FBQyxTQUFTLEVBQUUsZUFBZSxDQUFVLENBQUM7QUFJN0U7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLDhCQUE4QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDMUQsU0FBUyxFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUNyQyxlQUFlLEVBQUUsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLFVBQVUsRUFBRSxXQUFXLEVBQUUsYUFBYSxDQUFDLENBQUM7Q0FDWixDQUFDLENBQUM7QUF5RmhFLE1BQU0sQ0FBQyxNQUFNLDZCQUE2QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDekQsdUJBQXVCLEVBQUUsc0NBQXNDO0lBQy9ELG1DQUFtQyxFQUFFLGtEQUFrRDtDQUMvRSxDQUFDLENBQUM7QUFFWiwwRkFBMEY7QUFDMUYsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN6RCwrREFBK0Q7SUFDL0Qsb0NBQW9DO0lBQ3BDLHVGQUF1RjtJQUN2Rix1Q0FBdUM7SUFDdkMscUZBQXFGO0lBQ3JGLGtDQUFrQztJQUNsQyxxREFBcUQ7SUFDckQs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
@@ -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 relayCredentialInterfaces from '../data/clusterrelaycredential.js';
3
4
  import type * as clusterVpnInterfaces from '../data/clustervpn.js';
4
5
  import type { IClusterRuntime, TClusterRuntimeBlocker } from '../data/clusterruntime.js';
5
6
  import type { IServiceRuntimeAssignmentView } from '../data/serviceruntime.js';
@@ -116,6 +117,59 @@ export interface IReq_Relay_Cloudly_RegisterClusterRelay extends plugins.typedre
116
117
  request: clusterInterfaces.IRegisterClusterRelayRequest;
117
118
  response: clusterInterfaces.IRegisterClusterRelayResponse;
118
119
  }
120
+ /**
121
+ * Mints a cluster's relay credential, or rotates it at its exact current generation.
122
+ *
123
+ * Only the verified credential is read from the caller: a credential mutation trusts no identity
124
+ * member a caller states about itself. The bearer is never in the answer; it is delivered as the
125
+ * encrypted secret version `credential.delivery` names. The first mint also records the shared
126
+ * authority the cluster still carries, so a replacement never exists without a named predecessor.
127
+ * Cloudly refuses with one of `data.clusterRelayCredentialRefusals`.
128
+ */
129
+ export interface IReq_Admin_Cloudly_MutateClusterRelayCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_MutateClusterRelayCredential> {
130
+ method: 'mutateClusterRelayCredential';
131
+ request: {
132
+ identity: userInterfaces.IIdentityCredential;
133
+ mutation: relayCredentialInterfaces.TClusterRelayCredentialMutation;
134
+ };
135
+ response: relayCredentialInterfaces.IClusterRelayCredentialMutationResult;
136
+ }
137
+ /**
138
+ * One cluster's relay authority chain as Cloudly records it: the relay credential, the shared
139
+ * authority it replaces and the current accepted registration, each `null` while no record exists.
140
+ *
141
+ * Liveness is not here. It is a fact of a socket, which `getClusterRelayRegistrations` answers and
142
+ * which Cloudly checks for itself before it accepts a retirement.
143
+ */
144
+ export interface IReq_Any_Cloudly_GetClusterRelayCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_GetClusterRelayCredential> {
145
+ method: 'getClusterRelayCredential';
146
+ request: {
147
+ identity: userInterfaces.IIdentityCredential;
148
+ clusterId: string;
149
+ };
150
+ response: {
151
+ credential: relayCredentialInterfaces.IClusterRelayCredential | null;
152
+ legacy: relayCredentialInterfaces.IClusterLegacyRelayCredential | null;
153
+ registration: clusterInterfaces.IClusterRelayAcceptedRegistration | null;
154
+ };
155
+ }
156
+ /**
157
+ * Revokes a cluster's shared pre-relay authority, which is the last step of replacing it.
158
+ *
159
+ * Its own request rather than a mutation of the relay credential, because it is the one step that
160
+ * removes authority: Cloudly accepts it only while the named replacement is the current relay
161
+ * credential, the named registration is current and was made under that replacement, and a
162
+ * verified socket carries that registration. Every other answer is one of
163
+ * `data.clusterRelayCredentialRefusals`.
164
+ */
165
+ export interface IReq_Admin_Cloudly_RetireLegacyClusterRelayCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_RetireLegacyClusterRelayCredential> {
166
+ method: 'retireLegacyClusterRelayCredential';
167
+ request: {
168
+ identity: userInterfaces.IIdentityCredential;
169
+ retirement: relayCredentialInterfaces.IClusterLegacyRelayCredentialRetirementRequest;
170
+ };
171
+ response: relayCredentialInterfaces.IClusterLegacyRelayCredentialRetirementResult;
172
+ }
119
173
  /**
120
174
  * Cloudly hands the cluster relay the DNS lease renewals it has signed ahead of time for one node.
121
175
  *
@@ -1,4 +1,4 @@
1
1
  import * as userInterfaces from '../data/user.js';
2
2
  import * as clusterInterfaces from '../data/cluster.js';
3
3
  import * as plugins from '../plugins.js';
4
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3Rlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3JlcXVlc3RzL2NsdXN0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLGNBQWMsTUFBTSxpQkFBaUIsQ0FBQztBQUNsRCxPQUFPLEtBQUssaUJBQWlCLE1BQU0sb0JBQW9CLENBQUM7QUFLeEQsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUMifQ==
4
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2x1c3Rlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3JlcXVlc3RzL2NsdXN0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLGNBQWMsTUFBTSxpQkFBaUIsQ0FBQztBQUNsRCxPQUFPLEtBQUssaUJBQWlCLE1BQU0sb0JBQW9CLENBQUM7QUFNeEQsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "32.16.0",
3
+ "version": "32.17.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
package/readme.md CHANGED
@@ -1198,6 +1198,10 @@ source is identified by SHA-256 and may be `retained-inert` or `removed`; deleti
1198
1198
  erasure are not cutover requirements. Its exact owner generation is retained with that disposition.
1199
1199
  Retained recovery evidence is valid only while the released
1200
1200
  owner cannot consume it into live mode and the old credential remains revoked.
1201
+ `relay.credential`, `relay.acceptedRegistration`, `legacy.credential` and `legacy.invalidation`
1202
+ reference the records of [Cluster Relay Credential](#cluster-relay-credential). The old-mode
1203
+ refusal, the configuration CAS and the Spark bundle and unit identity are Spark-owned and come
1204
+ from [Spark Legacy Runtime Retirement](#spark-legacy-runtime-retirement).
1201
1205
 
1202
1206
  Before any post-pallet assignment exists, an administrator calls
1203
1207
  `requests.fleetcutover.prepareFleetCutoverIngress` with the exact server-created service ID,
@@ -2027,12 +2031,13 @@ with `clusterRelayCertificateDomainName(relay)`, which returns the origin's host
2027
2031
  Cloudly issues the certificate for and owns the A record for. An origin naming an address literal
2028
2032
  therefore has no issuable certificate.
2029
2033
 
2030
- `data.clusterRelayUndispatchedRefusals` names the three registration refusals that end a relay's
2031
- custody — `relay-cluster-not-enabled`, `relay-node-foreign` and `relay-sequence-not-advancing` —
2032
- with `data.TClusterRelayUndispatchedRefusal` as their union. Each is Cloudly reading its own
2033
- records and stating that it does not dispatch to this relay, so a relay that hears one drops what
2034
- it holds for its nodes instead of waiting. A refusal that only says Cloudly could not verify the
2035
- relay states nothing about dispatch and is deliberately not in the list.
2034
+ `data.clusterRelayUndispatchedRefusals` names the four registration refusals that end a relay's
2035
+ custody — `relay-cluster-not-enabled`, `relay-node-foreign`, `relay-sequence-not-advancing` and
2036
+ `relay-credential-generation-stale` — with `data.TClusterRelayUndispatchedRefusal` as their union.
2037
+ Each is Cloudly reading its own records and stating that it does not dispatch to this relay, so a
2038
+ relay that hears one drops what it holds for its nodes instead of waiting. A refusal that only
2039
+ says Cloudly could not verify the relay states nothing about dispatch and is deliberately not in
2040
+ the list.
2036
2041
 
2037
2042
  ```typescript
2038
2043
  const relay: data.IClusterRelay = {
@@ -2109,6 +2114,133 @@ const registrationReasons = data.validateRegisterClusterRelayRequest(registratio
2109
2114
  How the relay reaches each node's Corestore is part of the same document: see
2110
2115
  [Corestore Through The Relay](#corestore-through-the-relay).
2111
2116
 
2117
+ ### Cluster Relay Credential
2118
+
2119
+ A cluster's relay authenticates with its own generation-bearing credential instead of the shared
2120
+ cluster authority every cluster-side component used before it. `IClusterRelayCredential` is public
2121
+ metadata: the cluster, a `generation` that advances by one per accepted mint or rotation, the
2122
+ machine `userId` it authenticates as, and `delivery`, the `ISecretVersionReference` of the
2123
+ immutable encrypted version the bearer was delivered as. No record, receipt, request or response
2124
+ of this contract carries a bearer, a hash or key material, and every reader is exact-keyed, so a
2125
+ peer that adds a secret-looking member is refused rather than copied.
2126
+
2127
+ The credential authenticates the cluster's whole control-plane session, exactly what the shared
2128
+ authority authenticated, so a relay changes one credential and keeps every call it makes. The gain
2129
+ is a generation Cloudly fences each socket against and an authority it can rotate and retire on
2130
+ its own; the scope is not narrower. One cluster has one relay and therefore one credential record,
2131
+ and it never expires by itself: rotation is its lifecycle.
2132
+
2133
+ `mutateClusterRelayCredential { identity, mutation }` takes `TClusterRelayCredentialMutation`.
2134
+ `ensure` requires `expectedGeneration: null` and an absent record; `rotate` requires the exact
2135
+ current generation, never a minimum. The caller retains the complete request and its `mutationId`
2136
+ across retries: the identical body answers with the original result and `replayed: true` without
2137
+ renewing delivery, generation or authority, and a changed body fails closed. The first mint also
2138
+ records the shared authority the cluster still carries as `IClusterLegacyRelayCredential`, so a
2139
+ replacement never exists without a named predecessor. Rotation has no overlap: the previous
2140
+ generation stops authenticating at commit, and a relay still holding it hears
2141
+ `relay-credential-generation-stale`, drops what it holds for its nodes, reads the delivered version
2142
+ and reconnects. There is no revocation of the relay credential, because a rotation already covers
2143
+ a compromised one without a state in which the cluster has no relay authority at all.
2144
+
2145
+ An accepted `registerClusterRelay` answers with two references beside the origin and the offer:
2146
+ `credential`, the generation Cloudly authenticated the socket as, and `acceptedRegistration`, the
2147
+ record that acceptance wrote. The relay states no generation of its own — the registration request
2148
+ is unchanged — because a relay naming its own generation would be asserting its own rank.
2149
+ `IClusterRelayAcceptedRegistration` is that record: the registration facts the served read shape
2150
+ also carries, its own `id`, a `generation` that advances once per acceptance, the `credential`
2151
+ reference and a `digest`. Liveness is not part of it. `IClusterRelayRegistration` keeps exactly its
2152
+ published members, the facts plus `live`.
2153
+
2154
+ `retireLegacyClusterRelayCredential { identity, retirement }` revokes the shared authority, and is
2155
+ the last step of replacing it. `IClusterLegacyRelayCredentialRetirementRequest` carries
2156
+ compare-and-swap inputs only: the exact `expectedLegacyGeneration`, the `replacement` credential
2157
+ reference and the `acceptedRegistration` reference the caller observed. Cloudly dereferences both
2158
+ against its own current records, requires the registration to have been made under that
2159
+ replacement, and requires a verified live socket carrying it; a caller can state no liveness or
2160
+ proof of its own. Acceptance writes the one-shot `IClusterLegacyRelayCredentialInvalidation`
2161
+ (`generation: 1`) and advances the legacy record to `state: 'revoked'` naming it. The invalidation
2162
+ names the legacy record at the generation before the revocation and the record names the
2163
+ invalidation after it, so neither digest covers the other's.
2164
+ `data.clusterRelayCredentialRefusals` names every other answer, with
2165
+ `data.TClusterRelayCredentialRefusal` as their union. `getClusterRelayCredential { identity,
2166
+ clusterId }` reads the chain: the credential, the legacy record and the current accepted
2167
+ registration, each `null` while no record exists.
2168
+
2169
+ Each digest-bearing record has a `snapshot…`, `compute…Digest` and `validate…` function under its
2170
+ own digest domain in `data.clusterRelayCredentialContract` and `data.clusterRelayContract`.
2171
+ `bindClusterLegacyRelayCredentialInvalidation(invalidation, legacyCredential,
2172
+ acceptedRegistration)` joins an invalidation to the legacy record it revoked — the active record
2173
+ it names or that record's revoked successor — and to a registration whose `credential` equals the
2174
+ invalidation's `replacement`. It is a content check; dereferencing each record as current and
2175
+ requiring the live socket remain Cloudly's.
2176
+
2177
+ All three requests read only the verified `IIdentityCredential`, never an identity member a caller
2178
+ states about itself.
2179
+
2180
+ ### Spark Legacy Runtime Retirement
2181
+
2182
+ The Spark-owned half of `IFleetCutoverCoreflowRetirementEvidence` — the refused old mode, the
2183
+ configuration compare-and-swap, and the bundle and unit identity — is transported on the Swarm
2184
+ observation a node already posts. There is no route, request or endpoint for it. That is forced
2185
+ rather than preferred: the evidence requires `relay.nodeObservation` and `spark.observation` to be
2186
+ the identical accepted reference, so these facts must reach Cloudly on the very observation that
2187
+ carries the node's Docker census.
2188
+
2189
+ `data.ISparkLegacyRuntimeRetirement` is the optional observation member, available in every
2190
+ observation state because it describes the host and not its Docker daemon. It is
2191
+ `{ refusedMode, bundle, configuration }` and nothing else. It names no node: Cloudly binds it to
2192
+ the reporter it already authenticated, so there is no node identity on it to disagree with. It
2193
+ carries no digest of its own either — `computeSparkSwarmObservationDigest` canonicalizes the whole
2194
+ observation, so a second domain here would only be a second thing to keep in step.
2195
+
2196
+ `refusedMode` is one of `data.sparkLegacyRuntimeModes`, the two runtimes a released Spark could be
2197
+ put into by its `mode` configuration key. `data.sparkLegacyRuntimeModeServices` is the record over
2198
+ that union naming the Docker services each mode owns, so a refusal states a mode and a reader
2199
+ derives the set. `data.ISparkNodeBundleIdentity { version, sourceCommit, manifestSha256, unitName,
2200
+ unitDigest }` is read back from the installed tree and the installed unit file, never from the
2201
+ running process's own build constants: a build can state its own version, it cannot state that the
2202
+ host will start that same code again. `data.ISparkLegacyConfigurationState { ownerGeneration,
2203
+ sourceSha256, disposition }` names the released key-value source by digest alone. No decoded member
2204
+ of that file is representable in any shape here, because it carries the node's released bearer and
2205
+ its jump code; `sourceSha256` covers the whole file, whose smallest unguessable part is a 256-bit
2206
+ token. `disposition` is `retained-inert` or `removed`; erasure is not a cutover requirement.
2207
+
2208
+ Cloudly accepts an observation first and then advances two of its own records, which is what the
2209
+ evidence's two references point at. `data.ISparkLegacyModeRefusal` and
2210
+ `data.ISparkLegacyConfigurationRetirement` are revisions, not one-shot receipts: a node restates
2211
+ both on every observation, a bundle upgrade changes what the boot path is, and a disposition
2212
+ changes once. Both carry the accepted observation they were derived from, its reporter session and
2213
+ its sequence, and both obey the revision rule every record of this package obeys — a first
2214
+ generation has no predecessor and a later one names exactly the generation before it. The
2215
+ configuration record carries two counters on purpose: its own `generation` is contiguous because it
2216
+ is this record's revision, while `configuration.ownerGeneration` is the node's own and may skip,
2217
+ since an observation that never arrived must not look like a skipped revision.
2218
+
2219
+ `advancesSparkLegacyConfigurationState(current, next)` is the compare-and-swap rule, `null` for a
2220
+ node's first accepted state: the same `sourceSha256`, a strictly higher `ownerGeneration`, and no
2221
+ return from `removed` to `retained-inert`. Restating the accepted generation is a replay and
2222
+ therefore not an advance, which is what keeps `acceptedAt` and the named observation from aging
2223
+ forward on a node that has stopped changing. `data.sparkLegacyRetirementRefusals` names every other
2224
+ answer, with `data.TSparkLegacyRetirementRefusal` as their union.
2225
+ `bindSparkLegacyRetirement(modeRefusal, configurationRetirement)` joins the two receipts a
2226
+ retirement cites, requiring the same cluster, node, reporter session, sequence and accepted
2227
+ observation. It is a content check; whether each receipt is its node's current revision remains
2228
+ Cloudly's.
2229
+
2230
+ An omitted member is not a refusal and advances nothing, so the existence of these records is not
2231
+ what makes a retirement honest — the freshness of the observation they name is. Assembly must
2232
+ require that observation to still be inside `fleetCutoverContract.observationFreshnessMs`, exactly
2233
+ as `IFleetCutoverSparkObservationReference` already does, or a node that stopped refusing would go
2234
+ on looking retired.
2235
+
2236
+ Cloudly reads this member before Spark writes it. Every observation validator in this package is
2237
+ exact-key, so a server built against an earlier minor refuses the whole observation rather than
2238
+ ignoring a member it does not know — the peers of a session kind are upgraded before the side that
2239
+ starts depending on the later minor, and the `minimumPeerVersion` of that session kind is raised in
2240
+ the same commit that starts depending on it. A Spark that begins stating this member therefore
2241
+ raises its `sparkSwarmNode` minimum alone, which requires one minimum per session kind rather than
2242
+ one shared across a deployable's offers.
2243
+
2112
2244
  ### Cluster VPN Hub And Network
2113
2245
 
2114
2246
  A cluster's relay is also that cluster's managed VPN hub: every node of the cluster dials it, and
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.16.0',
6
+ version: '32.17.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }