@serve.zone/interfaces 31.9.0 → 32.0.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.
Files changed (204) hide show
  1. package/.smartconfig.json +1 -0
  2. package/changelog.md +467 -0
  3. package/dist_ts/00_commitinfo_data.js +3 -3
  4. package/dist_ts/appstore/types.d.ts +4 -6
  5. package/dist_ts/appstore/types.js +5 -5
  6. package/dist_ts/data/backup.d.ts +3 -26
  7. package/dist_ts/data/cluster.d.ts +36 -11
  8. package/dist_ts/data/cluster.js +33 -22
  9. package/dist_ts/data/clusteringress.d.ts +8 -0
  10. package/dist_ts/data/clusteringress.js +13 -8
  11. package/dist_ts/data/clusternode.d.ts +121 -74
  12. package/dist_ts/data/clusternode.js +109 -132
  13. package/dist_ts/data/coremail.d.ts +3 -7
  14. package/dist_ts/data/coremail.js +2 -2
  15. package/dist_ts/data/coremail.runtime.d.ts +0 -1
  16. package/dist_ts/data/coremail.runtime.js +5 -22
  17. package/dist_ts/data/deploymentadoption.d.ts +0 -2
  18. package/dist_ts/data/deploymentadoption.js +40 -6
  19. package/dist_ts/data/deploymentoperation.js +16 -3
  20. package/dist_ts/data/deploymentpreflight.d.ts +0 -2
  21. package/dist_ts/data/deploymentpreflight.js +1 -1
  22. package/dist_ts/data/immutableimage.d.ts +38 -21
  23. package/dist_ts/data/immutableimage.js +45 -5
  24. package/dist_ts/data/index.d.ts +1 -1
  25. package/dist_ts/data/isolatedrestore.d.ts +139 -21
  26. package/dist_ts/data/isolatedrestore.golden.d.ts +0 -1
  27. package/dist_ts/data/isolatedrestore.golden.js +6 -9
  28. package/dist_ts/data/isolatedrestore.js +176 -40
  29. package/dist_ts/data/mail.d.ts +1 -1
  30. package/dist_ts/data/networkaddressplan.d.ts +8 -8
  31. package/dist_ts/data/networkaddressplan.js +4 -4
  32. package/dist_ts/data/networkauthority.d.ts +20 -22
  33. package/dist_ts/data/networkauthority.js +26 -28
  34. package/dist_ts/data/networkcredential.d.ts +10 -10
  35. package/dist_ts/data/networkcredential.js +16 -16
  36. package/dist_ts/data/networkdelivery.d.ts +18 -18
  37. package/dist_ts/data/networkdelivery.js +16 -16
  38. package/dist_ts/data/networklease.d.ts +18 -7
  39. package/dist_ts/data/networklease.js +29 -7
  40. package/dist_ts/data/networknode.d.ts +2 -2
  41. package/dist_ts/data/networkprojection.d.ts +59 -60
  42. package/dist_ts/data/networkprojection.js +53 -57
  43. package/dist_ts/data/networkprotection.d.ts +27 -28
  44. package/dist_ts/data/networkprotection.js +36 -36
  45. package/dist_ts/data/networkrouter.d.ts +11 -11
  46. package/dist_ts/data/networkrouter.js +1 -1
  47. package/dist_ts/data/networksigning.d.ts +16 -18
  48. package/dist_ts/data/networksigning.js +20 -21
  49. package/dist_ts/data/nodecredential.d.ts +4 -6
  50. package/dist_ts/data/nodeenrollment.d.ts +35 -40
  51. package/dist_ts/data/nodeenrollment.js +57 -72
  52. package/dist_ts/data/privatenetwork.d.ts +21 -24
  53. package/dist_ts/data/privatenetwork.js +39 -54
  54. package/dist_ts/data/runtimeassignment.d.ts +50 -34
  55. package/dist_ts/data/runtimeassignment.js +79 -45
  56. package/dist_ts/data/runtimeconfig.d.ts +21 -22
  57. package/dist_ts/data/runtimeconfig.js +24 -24
  58. package/dist_ts/data/runtimesession.d.ts +100 -62
  59. package/dist_ts/data/runtimesession.js +98 -79
  60. package/dist_ts/data/runtimeterminalreceipt.d.ts +28 -29
  61. package/dist_ts/data/runtimeterminalreceipt.js +42 -42
  62. package/dist_ts/data/secret.d.ts +18 -25
  63. package/dist_ts/data/secret.js +12 -52
  64. package/dist_ts/data/servicecredential.d.ts +14 -16
  65. package/dist_ts/data/servicecredential.js +17 -28
  66. package/dist_ts/data/serviceruntime.d.ts +7 -7
  67. package/dist_ts/data/serviceruntime.js +4 -4
  68. package/dist_ts/data/sparknodereport.d.ts +27 -14
  69. package/dist_ts/data/sparknodereport.js +35 -28
  70. package/dist_ts/data/webpush.d.ts +0 -1
  71. package/dist_ts/index.d.ts +2 -1
  72. package/dist_ts/index.js +3 -2
  73. package/dist_ts/platform/objectstorageretention.d.ts +21 -24
  74. package/dist_ts/platform/objectstorageretention.js +6 -18
  75. package/dist_ts/platform/storage.d.ts +1 -3
  76. package/dist_ts/platform/storagemigration.d.ts +0 -7
  77. package/dist_ts/platform/storagemigration.golden.d.ts +0 -1
  78. package/dist_ts/platform/storagemigration.golden.js +8 -12
  79. package/dist_ts/platform/storagemigration.js +6 -51
  80. package/dist_ts/platform/types.d.ts +2 -2
  81. package/dist_ts/private/canonicaljson.d.ts +9 -1
  82. package/dist_ts/private/canonicaljson.js +14 -2
  83. package/dist_ts/private/dataproperties.d.ts +62 -0
  84. package/dist_ts/private/dataproperties.js +117 -0
  85. package/dist_ts/private/networkcontract.d.ts +10 -10
  86. package/dist_ts/private/networkcontract.js +1 -1
  87. package/dist_ts/private/releaseversion.d.ts +37 -0
  88. package/dist_ts/private/releaseversion.js +48 -0
  89. package/dist_ts/private/runtimepolicy.d.ts +2 -2
  90. package/dist_ts/private/runtimepolicy.js +1 -1
  91. package/dist_ts/private/tokenlist.d.ts +9 -0
  92. package/dist_ts/private/tokenlist.js +23 -0
  93. package/dist_ts/protocol.d.ts +137 -0
  94. package/dist_ts/protocol.js +255 -0
  95. package/dist_ts/requests/admin.d.ts +8 -8
  96. package/dist_ts/requests/backup.d.ts +102 -112
  97. package/dist_ts/requests/cluster.d.ts +10 -10
  98. package/dist_ts/requests/clusteringress.d.ts +9 -0
  99. package/dist_ts/requests/clusteringress.js +1 -1
  100. package/dist_ts/requests/config.d.ts +3 -3
  101. package/dist_ts/requests/corestore.d.ts +0 -3
  102. package/dist_ts/requests/corestore.js +5 -9
  103. package/dist_ts/requests/network.d.ts +30 -30
  104. package/dist_ts/requests/node.d.ts +15 -28
  105. package/dist_ts/requests/pallet.d.ts +13 -13
  106. package/dist_ts/requests/runtimesession.d.ts +53 -41
  107. package/dist_ts/requests/secret.d.ts +4 -5
  108. package/dist_ts/requests/secret.js +1 -5
  109. package/dist_ts/runtime.corestore.d.ts +56 -74
  110. package/dist_ts/runtime.corestore.databaseexportpackage.d.ts +30 -36
  111. package/dist_ts/runtime.corestore.databaseexportpackage.golden.d.ts +11 -12
  112. package/dist_ts/runtime.corestore.databaseexportpackage.golden.js +54 -71
  113. package/dist_ts/runtime.corestore.databaseexportpackage.js +9 -25
  114. package/dist_ts/runtime.corestore.js +35 -135
  115. package/dist_ts/runtime.corestore.relay.js +2 -7
  116. package/dist_ts/runtime.d.ts +38 -48
  117. package/dist_ts/runtime.js +20 -77
  118. package/dist_ts/runtime.networkprojection.d.ts +4 -4
  119. package/dist_ts/runtime.networkprojection.js +18 -18
  120. package/dist_ts/runtime.workloadinit.d.ts +23 -24
  121. package/dist_ts/runtime.workloadinit.js +6 -13
  122. package/package.json +2 -2
  123. package/readme.md +421 -270
  124. package/ts/00_commitinfo_data.ts +3 -3
  125. package/ts/appstore/types.ts +4 -6
  126. package/ts/data/backup.ts +3 -38
  127. package/ts/data/cluster.ts +51 -34
  128. package/ts/data/clusteringress.ts +12 -12
  129. package/ts/data/clusternode.ts +204 -232
  130. package/ts/data/coremail.runtime.ts +4 -21
  131. package/ts/data/coremail.ts +3 -7
  132. package/ts/data/deploymentadoption.ts +46 -4
  133. package/ts/data/deploymentoperation.ts +16 -1
  134. package/ts/data/deploymentpreflight.ts +0 -2
  135. package/ts/data/immutableimage.ts +71 -24
  136. package/ts/data/index.ts +1 -1
  137. package/ts/data/isolatedrestore.golden.ts +5 -9
  138. package/ts/data/isolatedrestore.ts +339 -54
  139. package/ts/data/mail.ts +1 -1
  140. package/ts/data/networkaddressplan.ts +13 -13
  141. package/ts/data/networkauthority.ts +42 -46
  142. package/ts/data/networkcredential.ts +24 -24
  143. package/ts/data/networkdelivery.ts +27 -27
  144. package/ts/data/networklease.ts +40 -13
  145. package/ts/data/networknode.ts +2 -2
  146. package/ts/data/networkprojection.ts +119 -124
  147. package/ts/data/networkprotection.ts +54 -55
  148. package/ts/data/networkrouter.ts +14 -14
  149. package/ts/data/networksigning.ts +29 -32
  150. package/ts/data/nodecredential.ts +4 -6
  151. package/ts/data/nodeenrollment.ts +96 -114
  152. package/ts/data/privatenetwork.ts +58 -74
  153. package/ts/data/runtimeassignment.ts +143 -94
  154. package/ts/data/runtimeconfig.ts +45 -46
  155. package/ts/data/runtimesession.ts +185 -138
  156. package/ts/data/runtimeterminalreceipt.ts +72 -73
  157. package/ts/data/secret.ts +28 -76
  158. package/ts/data/servicecredential.ts +33 -43
  159. package/ts/data/serviceruntime.ts +11 -11
  160. package/ts/data/sparknodereport.ts +48 -36
  161. package/ts/data/webpush.ts +0 -1
  162. package/ts/index.ts +2 -0
  163. package/ts/platform/objectstorageretention.ts +36 -55
  164. package/ts/platform/storage.ts +1 -3
  165. package/ts/platform/storagemigration.golden.ts +7 -11
  166. package/ts/platform/storagemigration.ts +5 -55
  167. package/ts/platform/types.ts +2 -2
  168. package/ts/private/canonicaljson.ts +13 -1
  169. package/ts/private/dataproperties.ts +139 -0
  170. package/ts/private/networkcontract.ts +9 -9
  171. package/ts/private/releaseversion.ts +70 -0
  172. package/ts/private/runtimepolicy.ts +2 -2
  173. package/ts/private/tokenlist.ts +30 -0
  174. package/ts/protocol.ts +364 -0
  175. package/ts/requests/admin.ts +11 -11
  176. package/ts/requests/backup.ts +112 -117
  177. package/ts/requests/cluster.ts +13 -13
  178. package/ts/requests/clusteringress.ts +9 -0
  179. package/ts/requests/config.ts +3 -3
  180. package/ts/requests/corestore.ts +4 -16
  181. package/ts/requests/network.ts +40 -40
  182. package/ts/requests/node.ts +21 -38
  183. package/ts/requests/pallet.ts +17 -17
  184. package/ts/requests/runtimesession.ts +63 -51
  185. package/ts/requests/secret.ts +5 -10
  186. package/ts/runtime.corestore.databaseexportpackage.golden.ts +77 -95
  187. package/ts/runtime.corestore.databaseexportpackage.ts +51 -73
  188. package/ts/runtime.corestore.relay.ts +1 -10
  189. package/ts/runtime.corestore.ts +118 -268
  190. package/ts/runtime.networkprojection.ts +30 -30
  191. package/ts/runtime.ts +83 -158
  192. package/ts/runtime.workloadinit.ts +36 -48
  193. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.d.ts +0 -902
  194. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.d.ts +0 -35
  195. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.golden.js +0 -315
  196. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.js +0 -3315
  197. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.d.ts +0 -291
  198. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.d.ts +0 -30
  199. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.js +0 -408
  200. package/dist_ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.js +0 -1126
  201. package/ts/runtime.cloudlylegacydeploymentsettlement.golden.ts +0 -421
  202. package/ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.golden.ts +0 -510
  203. package/ts/runtime.cloudlylegacydeploymentsettlement.offlineexport.ts +0 -2470
  204. package/ts/runtime.cloudlylegacydeploymentsettlement.ts +0 -6630
@@ -1,10 +1,12 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import type { TSha256Digest, TImmutableContainerPlatform } from './immutableimage.js';
3
- import type { ISparkNodeReportRecordV2 } from './sparknodereport.js';
3
+ import type { ISparkNodeReportRecord } from './sparknodereport.js';
4
+ import { protocolRefusalHttpStatus, validateProtocolOfferOn, type IProtocolOffer } from '../protocol.js';
4
5
  import {
5
6
  canonicalizeStrictJson,
6
7
  createCanonicalJsonSha256Hex,
7
8
  } from '../private/canonicaljson.js';
9
+ import { hasExactKeys } from '../private/dataproperties.js';
8
10
 
9
11
  export interface IClusterNodeMetrics {
10
12
  cpuUsagePercent: number;
@@ -69,10 +71,85 @@ export interface IServeZoneServiceRuntimeInfo {
69
71
  error?: string;
70
72
  }
71
73
 
72
- /** Spark -> Cloudly HTTP body for POST /spark/v1/nodes/metrics-sample */
74
+ /**
75
+ * The Swarm-era Spark runtime's own routes.
76
+ *
77
+ * A separate runtime, not an older version of the node runtime: a worker in `coreflow-node` mode
78
+ * posts these four bodies, while a pallet node posts `sparkNodeHeartbeatContract.endpoint`. They
79
+ * retire together with coreflow, not with this release.
80
+ *
81
+ * Each route states its endpoint beside the bytes it reads and writes, in the same three members
82
+ * as `sparkNodeHeartbeatContract`, so one bounded-read helper serves every Spark route. The four
83
+ * bounds differ because the bodies do: a bound is the largest body its own shape can carry here,
84
+ * rounded up, and nothing larger is read before it is refused. Every route is judged in the one
85
+ * server order `protocol.readProtocolOffer` states, against this server's `sparkSwarmNode` offer;
86
+ * none of them carries a typed error, so a refusal is answered with `protocolRefusalStatus` and an
87
+ * `IProtocolRefusal` body before the node is authenticated and before the body is acted on. Every
88
+ * answer states this server's own offer, exactly as the request states the reporter's, so each
89
+ * side judges the other from the bodies they exchanged.
90
+ */
91
+ export const sparkSwarmNodeContracts = Object.freeze({
92
+ /** The full runtime description on every beat; each described service costs some 600 bytes. */
93
+ heartbeat: Object.freeze({
94
+ endpoint: '/spark/swarm-nodes/heartbeat',
95
+ maxRequestBytes: 32_768,
96
+ maxResponseBytes: 2048,
97
+ } as const),
98
+ /** One sample up, every action the server holds for this node back down. */
99
+ metricsSample: Object.freeze({
100
+ endpoint: '/spark/swarm-nodes/metrics-sample',
101
+ maxRequestBytes: 4096,
102
+ maxResponseBytes: 32_768,
103
+ } as const),
104
+ /** `resultText` is the body here, and a server keeps at most 4000 characters of it. */
105
+ actionResult: Object.freeze({
106
+ endpoint: '/spark/swarm-nodes/action-result',
107
+ maxRequestBytes: 8192,
108
+ maxResponseBytes: 2048,
109
+ } as const),
110
+ /**
111
+ * By far the largest of the four: a manager snapshot carries up to the 1024 observed nodes
112
+ * `validateSparkSwarmObservationRequest` admits, each with two identifiers of up to 200
113
+ * characters, which is close to 600 KiB of body this route must still be able to read.
114
+ */
115
+ swarmObservation: Object.freeze({
116
+ endpoint: '/spark/swarm-nodes/swarm-observation',
117
+ maxRequestBytes: 786_432,
118
+ maxResponseBytes: 2048,
119
+ } as const),
120
+ protocolRefusalStatus: protocolRefusalHttpStatus,
121
+ } as const);
122
+
123
+ /**
124
+ * Spark -> Cloudly body for `sparkSwarmNodeContracts.heartbeat.endpoint`.
125
+ *
126
+ * The host presentation of a Swarm-era worker: its metrics and its runtime description, both
127
+ * stated in full on every beat. This is the one owner of the shape — Cloudly and Spark each
128
+ * carried a private copy until 32.0.0, which is how the two drifted apart.
129
+ */
130
+ export interface ISparkSwarmNodeHeartbeatRequest {
131
+ nodeId: string;
132
+ nodeToken: string;
133
+ /** Read and negotiated before the node is authenticated; see `sparkSwarmNodeContracts`. */
134
+ protocol: IProtocolOffer;
135
+ metrics: IClusterNodeMetrics;
136
+ runtimeInfo: ISparkNodeRuntimeInfo;
137
+ }
138
+
139
+ /** What Cloudly answers a Swarm-era heartbeat with; `message` explains a refusal to an operator. */
140
+ export interface ISparkSwarmNodeHeartbeatResponse {
141
+ accepted: boolean;
142
+ message?: string;
143
+ /** The answering server's offer; see `sparkSwarmNodeContracts`. */
144
+ protocol: IProtocolOffer;
145
+ }
146
+
147
+ /** Spark -> Cloudly body for `sparkSwarmNodeContracts.metricsSample.endpoint`. */
73
148
  export interface ISparkMetricsSampleRequest {
74
149
  nodeId: string;
75
150
  nodeToken: string;
151
+ /** Read and negotiated before the node is authenticated; see `sparkSwarmNodeContracts`. */
152
+ protocol: IProtocolOffer;
76
153
  sample: INodeMetricsSample;
77
154
  }
78
155
 
@@ -81,12 +158,16 @@ export interface ISparkMetricsSampleResponse {
81
158
  message?: string;
82
159
  /** Actions queued for this node; spark executes them and posts results. */
83
160
  pendingActions?: INodeAction[];
161
+ /** The answering server's offer; see `sparkSwarmNodeContracts`. */
162
+ protocol: IProtocolOffer;
84
163
  }
85
164
 
86
- /** Spark -> Cloudly HTTP body for POST /spark/v1/nodes/action-result */
165
+ /** Spark -> Cloudly body for `sparkSwarmNodeContracts.actionResult.endpoint`. */
87
166
  export interface ISparkActionResultRequest {
88
167
  nodeId: string;
89
168
  nodeToken: string;
169
+ /** Read and negotiated before the node is authenticated; see `sparkSwarmNodeContracts`. */
170
+ protocol: IProtocolOffer;
90
171
  actionId: string;
91
172
  status: Extract<TNodeActionStatus, 'running' | 'succeeded' | 'failed'>;
92
173
  resultText?: string;
@@ -95,9 +176,11 @@ export interface ISparkActionResultRequest {
95
176
  export interface ISparkActionResultResponse {
96
177
  accepted: boolean;
97
178
  message?: string;
179
+ /** The answering server's offer; see `sparkSwarmNodeContracts`. */
180
+ protocol: IProtocolOffer;
98
181
  }
99
182
 
100
- export interface ISwarmManagerObservedNodeV1 {
183
+ export interface ISwarmManagerObservedNode {
101
184
  swarmNodeId: string;
102
185
  nodeName: string;
103
186
  platform: TImmutableContainerPlatform;
@@ -110,66 +193,41 @@ export interface ISwarmManagerObservedNodeV1 {
110
193
  };
111
194
  }
112
195
 
113
- export interface ISwarmManagerSnapshotV1 {
114
- snapshotVersion: 1;
196
+ export interface ISwarmManagerSnapshot {
115
197
  snapshotDigest: TSha256Digest;
116
198
  nodeCount: number;
117
199
  managerCount: number;
118
- nodes: ISwarmManagerObservedNodeV1[];
200
+ nodes: ISwarmManagerObservedNode[];
119
201
  }
120
202
 
121
- interface ISparkSwarmObservationBaseV1 {
122
- schemaVersion: 1;
203
+ interface ISparkSwarmObservationBase {
123
204
  reporterSessionId: string;
124
205
  observationSequence: number;
125
206
  observedAt: number;
126
207
  }
127
208
 
128
- export type TSparkSwarmObservationV1 =
129
- | (ISparkSwarmObservationBaseV1 & {
209
+ export type TSparkSwarmObservation =
210
+ | (ISparkSwarmObservationBase & {
130
211
  state: 'unknown';
131
212
  reason: 'docker-unavailable' | 'query-failed' | 'not-initialized';
132
213
  })
133
- | (ISparkSwarmObservationBaseV1 & {
214
+ | (ISparkSwarmObservationBase & {
134
215
  state: 'not-member';
135
216
  })
136
- | (ISparkSwarmObservationBaseV1 & {
137
- state: 'member';
138
- swarmClusterId: string;
139
- localSwarmNodeId: string;
140
- controlAvailable: boolean;
141
- managerSnapshot?: ISwarmManagerSnapshotV1;
142
- });
143
-
144
- interface ISparkSwarmObservationBaseV2 {
145
- schemaVersion: 2;
146
- reporterSessionId: string;
147
- observationSequence: number;
148
- observedAt: number;
149
- }
150
-
151
- export type TSparkSwarmObservationV2 =
152
- | (ISparkSwarmObservationBaseV2 & {
153
- state: 'unknown';
154
- reason: 'docker-unavailable' | 'query-failed' | 'not-initialized';
155
- })
156
- | (ISparkSwarmObservationBaseV2 & {
157
- state: 'not-member';
158
- })
159
- | (ISparkSwarmObservationBaseV2 & {
217
+ | (ISparkSwarmObservationBase & {
160
218
  state: 'member';
161
219
  localSwarmNodeId: string;
162
220
  controlAvailable: false;
163
221
  })
164
- | (ISparkSwarmObservationBaseV2 & {
222
+ | (ISparkSwarmObservationBase & {
165
223
  state: 'member';
166
224
  swarmClusterId: string;
167
225
  localSwarmNodeId: string;
168
226
  controlAvailable: true;
169
- managerSnapshot?: ISwarmManagerSnapshotV1;
227
+ managerSnapshot?: ISwarmManagerSnapshot;
170
228
  });
171
229
 
172
- export type TSparkSwarmObservationV2RejectionCode =
230
+ export type TSparkSwarmObservationRejectionCode =
173
231
  | 'sequence-conflict'
174
232
  | 'sequence-stale'
175
233
  | 'sequence-gap'
@@ -180,9 +238,8 @@ export type TSparkSwarmObservationV2RejectionCode =
180
238
  | 'observation-time-not-advancing'
181
239
  | 'acceptance-race';
182
240
 
183
- export const sparkSwarmObservationV2Contract = Object.freeze({
184
- endpoint: '/spark/v2/nodes/swarm-observation',
185
- digestDomain: 'serve.zone/spark-swarm-observation-v2',
241
+ export const sparkSwarmObservationContract = Object.freeze({
242
+ digestDomain: 'serve.zone/spark-swarm-observation',
186
243
  maximumPastAgeMs: 300_000,
187
244
  maximumFutureSkewMs: 30_000,
188
245
  retainedRetiredSessionIds: 32,
@@ -196,18 +253,19 @@ export const sparkSwarmObservationV2Contract = Object.freeze({
196
253
  'observation-from-future': false,
197
254
  'observation-time-not-advancing': false,
198
255
  'acceptance-race': true,
199
- } satisfies Record<TSparkSwarmObservationV2RejectionCode, boolean>),
256
+ } satisfies Record<TSparkSwarmObservationRejectionCode, boolean>),
200
257
  } as const);
201
258
 
202
259
  /** Cloudly derives node and cluster scope from this authenticated Spark request. */
203
- export interface ISparkSwarmObservationV2Request {
260
+ export interface ISparkSwarmObservationRequest {
204
261
  nodeId: string;
205
262
  nodeToken: string;
206
- observation: TSparkSwarmObservationV2;
263
+ /** Read and negotiated before the node is authenticated; see `sparkSwarmNodeContracts`. */
264
+ protocol: IProtocolOffer;
265
+ observation: TSparkSwarmObservation;
207
266
  }
208
267
 
209
- export interface ISparkSwarmObservationV2AcceptedReceipt {
210
- schemaVersion: 2;
268
+ export interface ISparkSwarmObservationAcceptedReceipt {
211
269
  state: 'accepted';
212
270
  nodeId: string;
213
271
  reporterSessionId: string;
@@ -216,18 +274,21 @@ export interface ISparkSwarmObservationV2AcceptedReceipt {
216
274
  acceptedAt: number;
217
275
  /** Runtime target generation observed immediately after this acceptance. */
218
276
  targetGenerationAtAcceptance: number;
277
+ /** The answering server's offer; see `sparkSwarmNodeContracts`. */
278
+ protocol: IProtocolOffer;
219
279
  }
220
280
 
221
- export interface ISparkSwarmObservationV2RejectedReceipt {
222
- schemaVersion: 2;
281
+ export interface ISparkSwarmObservationRejectedReceipt {
223
282
  state: 'rejected';
224
283
  nodeId: string;
225
284
  reporterSessionId: string;
226
285
  observationSequence: number;
227
286
  observationDigest: TSha256Digest;
228
287
  rejectedAt: number;
229
- code: TSparkSwarmObservationV2RejectionCode;
288
+ code: TSparkSwarmObservationRejectionCode;
230
289
  retryable: boolean;
290
+ /** The answering server's offer; see `sparkSwarmNodeContracts`. */
291
+ protocol: IProtocolOffer;
231
292
  }
232
293
 
233
294
  /**
@@ -235,25 +296,11 @@ export interface ISparkSwarmObservationV2RejectedReceipt {
235
296
  * authentication. Malformed and authentication failures use generic HTTP
236
297
  * errors so they cannot expose credential or persisted replay state.
237
298
  */
238
- export type TSparkSwarmObservationV2Response =
239
- | ISparkSwarmObservationV2AcceptedReceipt
240
- | ISparkSwarmObservationV2RejectedReceipt;
241
-
242
- /** Cloudly derives node and cluster scope from this authenticated Spark request. */
243
- export interface ISparkSwarmObservationRequest {
244
- nodeId: string;
245
- nodeToken: string;
246
- observation: TSparkSwarmObservationV1;
247
- }
299
+ export type TSparkSwarmObservationResponse =
300
+ | ISparkSwarmObservationAcceptedReceipt
301
+ | ISparkSwarmObservationRejectedReceipt;
248
302
 
249
- export interface ISparkSwarmObservationResponse {
250
- accepted: boolean;
251
- acceptedSequence?: number;
252
- targetGeneration?: number;
253
- message?: string;
254
- }
255
-
256
- export interface IClusterRuntimeTargetV1 {
303
+ export interface IClusterRuntimeTarget {
257
304
  cloudlyNodeId: string;
258
305
  swarmClusterId: string;
259
306
  swarmNodeId: string;
@@ -261,8 +308,7 @@ export interface IClusterRuntimeTargetV1 {
261
308
  platform: TImmutableContainerPlatform;
262
309
  }
263
310
 
264
- export interface IClusterRuntimeTargetSetReadyV1 {
265
- schemaVersion: 1;
311
+ export interface IClusterRuntimeTargetSetReady {
266
312
  state: 'ready';
267
313
  cloudlyClusterId: string;
268
314
  swarmClusterId: string;
@@ -270,11 +316,10 @@ export interface IClusterRuntimeTargetSetReadyV1 {
270
316
  targetSetDigest: TSha256Digest;
271
317
  acceptedAt: number;
272
318
  freshUntil: number;
273
- targets: IClusterRuntimeTargetV1[];
319
+ targets: IClusterRuntimeTarget[];
274
320
  }
275
321
 
276
- export interface IClusterRuntimeTargetSetUnavailableV1 {
277
- schemaVersion: 1;
322
+ export interface IClusterRuntimeTargetSetUnavailable {
278
323
  state: 'unavailable';
279
324
  cloudlyClusterId: string;
280
325
  generation: number;
@@ -287,9 +332,9 @@ export interface IClusterRuntimeTargetSetUnavailableV1 {
287
332
  | 'no-schedulable-targets';
288
333
  }
289
334
 
290
- export type TClusterRuntimeTargetSetV1 =
291
- | IClusterRuntimeTargetSetReadyV1
292
- | IClusterRuntimeTargetSetUnavailableV1;
335
+ export type TClusterRuntimeTargetSet =
336
+ | IClusterRuntimeTargetSetReady
337
+ | IClusterRuntimeTargetSetUnavailable;
293
338
 
294
339
  const runtimeIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
295
340
  const runtimeDigestRegex = /^sha256:[a-f0-9]{64}$/;
@@ -303,11 +348,6 @@ const isRuntimeRecord = (valueArg: unknown): valueArg is Record<string, unknown>
303
348
  Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg)
304
349
  );
305
350
 
306
- const hasExactRuntimeKeys = (
307
- valueArg: Record<string, unknown>,
308
- keysArg: string[],
309
- ): boolean => JSON.stringify(Object.keys(valueArg).sort()) === JSON.stringify([...keysArg].sort());
310
-
311
351
  const isRuntimeIdentifier = (valueArg: unknown): valueArg is string => (
312
352
  typeof valueArg === 'string' && runtimeIdentifierRegex.test(valueArg)
313
353
  );
@@ -326,32 +366,32 @@ const computeSha256 = async (inputArg: string): Promise<TSha256Digest> => {
326
366
  .join('')}` as TSha256Digest;
327
367
  };
328
368
 
329
- const failSparkSwarmObservationV2Canonicalization = (reasonArg: string): never => {
330
- throw new Error(`Spark Swarm observation v2 cannot be canonicalized: ${reasonArg}`);
369
+ const failSparkSwarmObservationCanonicalization = (reasonArg: string): never => {
370
+ throw new Error(`Spark Swarm observation cannot be canonicalized: ${reasonArg}`);
331
371
  };
332
372
 
333
- export const createSparkSwarmObservationV2DigestInput = (
334
- observationArg: TSparkSwarmObservationV2,
373
+ export const createSparkSwarmObservationDigestInput = (
374
+ observationArg: TSparkSwarmObservation,
335
375
  ): string => canonicalizeStrictJson(
336
376
  {
337
- domain: sparkSwarmObservationV2Contract.digestDomain,
377
+ domain: sparkSwarmObservationContract.digestDomain,
338
378
  observation: observationArg,
339
379
  },
340
- failSparkSwarmObservationV2Canonicalization,
341
- 'Spark Swarm observation v2 digest input',
380
+ failSparkSwarmObservationCanonicalization,
381
+ 'Spark Swarm observation digest input',
342
382
  );
343
383
 
344
- export const computeSparkSwarmObservationV2Digest = async (
345
- observationArg: TSparkSwarmObservationV2,
384
+ export const computeSparkSwarmObservationDigest = async (
385
+ observationArg: TSparkSwarmObservation,
346
386
  ): Promise<TSha256Digest> => (
347
387
  `sha256:${await createCanonicalJsonSha256Hex(
348
- createSparkSwarmObservationV2DigestInput(observationArg),
349
- failSparkSwarmObservationV2Canonicalization,
388
+ createSparkSwarmObservationDigestInput(observationArg),
389
+ failSparkSwarmObservationCanonicalization,
350
390
  )}` as TSha256Digest
351
391
  );
352
392
 
353
393
  const canonicalSwarmNode = (
354
- nodeArg: ISwarmManagerObservedNodeV1,
394
+ nodeArg: ISwarmManagerObservedNode,
355
395
  ): Record<string, unknown> => ({
356
396
  swarmNodeId: nodeArg.swarmNodeId,
357
397
  nodeName: nodeArg.nodeName,
@@ -369,12 +409,10 @@ const canonicalSwarmNode = (
369
409
 
370
410
  export const createSwarmManagerSnapshotDigestInput = (
371
411
  swarmClusterIdArg: string,
372
- snapshotArg: ISwarmManagerSnapshotV1,
412
+ snapshotArg: ISwarmManagerSnapshot,
373
413
  ): string => JSON.stringify({
374
- schemaVersion: 1,
375
414
  purpose: 'serve.zone/swarm-manager-snapshot',
376
415
  swarmClusterId: swarmClusterIdArg,
377
- snapshotVersion: snapshotArg.snapshotVersion,
378
416
  nodeCount: snapshotArg.nodeCount,
379
417
  managerCount: snapshotArg.managerCount,
380
418
  nodes: snapshotArg.nodes.map(canonicalSwarmNode),
@@ -382,7 +420,7 @@ export const createSwarmManagerSnapshotDigestInput = (
382
420
 
383
421
  export const computeSwarmManagerSnapshotDigest = async (
384
422
  swarmClusterIdArg: string,
385
- snapshotArg: ISwarmManagerSnapshotV1,
423
+ snapshotArg: ISwarmManagerSnapshot,
386
424
  ): Promise<TSha256Digest> => computeSha256(
387
425
  createSwarmManagerSnapshotDigestInput(swarmClusterIdArg, snapshotArg),
388
426
  );
@@ -393,8 +431,7 @@ const validateSwarmManagerSnapshot = async (
393
431
  snapshotArg: unknown,
394
432
  ): Promise<string[]> => {
395
433
  if (!isRuntimeRecord(snapshotArg)
396
- || !hasExactRuntimeKeys(snapshotArg, [
397
- 'snapshotVersion',
434
+ || !hasExactKeys(snapshotArg, [
398
435
  'snapshotDigest',
399
436
  'nodeCount',
400
437
  'managerCount',
@@ -403,9 +440,6 @@ const validateSwarmManagerSnapshot = async (
403
440
  return ['Spark Swarm manager snapshot must use its exact schema'];
404
441
  }
405
442
  const errors: string[] = [];
406
- if (snapshotArg.snapshotVersion !== 1) {
407
- errors.push('Spark Swarm manager snapshotVersion must be 1');
408
- }
409
443
  if (typeof snapshotArg.snapshotDigest !== 'string'
410
444
  || !runtimeDigestRegex.test(snapshotArg.snapshotDigest)) {
411
445
  errors.push('Spark Swarm manager snapshot digest must be canonical');
@@ -440,7 +474,7 @@ const validateSwarmManagerSnapshot = async (
440
474
  'state',
441
475
  ...(isManager ? ['managerStatus'] : []),
442
476
  ];
443
- if (!hasExactRuntimeKeys(nodeArg, expectedKeys)) {
477
+ if (!hasExactKeys(nodeArg, expectedKeys)) {
444
478
  errors.push(`Spark Swarm manager snapshot nodes[${index}] must use its exact role schema`);
445
479
  }
446
480
  if (!isRuntimeIdentifier(nodeArg.swarmNodeId) || !isRuntimeIdentifier(nodeArg.nodeName)) {
@@ -457,7 +491,7 @@ const validateSwarmManagerSnapshot = async (
457
491
  if (isManager) {
458
492
  managerCount++;
459
493
  if (!isRuntimeRecord(nodeArg.managerStatus)
460
- || !hasExactRuntimeKeys(nodeArg.managerStatus, ['leader', 'reachability'])
494
+ || !hasExactKeys(nodeArg.managerStatus, ['leader', 'reachability'])
461
495
  || typeof nodeArg.managerStatus.leader !== 'boolean'
462
496
  || !['unknown', 'unreachable', 'reachable']
463
497
  .includes(nodeArg.managerStatus.reachability as string)) {
@@ -504,7 +538,7 @@ const validateSwarmManagerSnapshot = async (
504
538
  if (errors.length === 0
505
539
  && snapshotArg.snapshotDigest !== await computeSwarmManagerSnapshotDigest(
506
540
  swarmClusterIdArg,
507
- snapshotArg as unknown as ISwarmManagerSnapshotV1,
541
+ snapshotArg as unknown as ISwarmManagerSnapshot,
508
542
  )) {
509
543
  errors.push('Spark Swarm manager snapshot digest does not match');
510
544
  }
@@ -518,73 +552,6 @@ export const validateSparkSwarmObservation = async (
518
552
  if (!isRuntimeRecord(observationArg)) return ['Spark Swarm observation must be an object'];
519
553
  const observation = observationArg as Record<string, unknown>;
520
554
  const commonKeys = [
521
- 'schemaVersion',
522
- 'reporterSessionId',
523
- 'observationSequence',
524
- 'observedAt',
525
- 'state',
526
- ];
527
- const expectedKeys = observation.state === 'unknown'
528
- ? [...commonKeys, 'reason']
529
- : observation.state === 'not-member'
530
- ? commonKeys
531
- : observation.state === 'member'
532
- ? [
533
- ...commonKeys,
534
- 'swarmClusterId',
535
- 'localSwarmNodeId',
536
- 'controlAvailable',
537
- ...(observation.managerSnapshot === undefined ? [] : ['managerSnapshot']),
538
- ]
539
- : [];
540
- if (expectedKeys.length === 0 || !hasExactRuntimeKeys(observation, expectedKeys)) {
541
- return ['Spark Swarm observation must use its exact state schema'];
542
- }
543
- const errors: string[] = [];
544
- if (observation.schemaVersion !== 1) errors.push('Spark Swarm observation schemaVersion must be 1');
545
- if (!isRuntimeIdentifier(observation.reporterSessionId)
546
- || !isPositiveSafeInteger(observation.observationSequence)
547
- || !isPositiveSafeInteger(observation.observedAt)) {
548
- errors.push('Spark Swarm observation identity, sequence, and timestamp must be canonical');
549
- }
550
- if (observation.state === 'unknown'
551
- && !['docker-unavailable', 'query-failed', 'not-initialized']
552
- .includes(observation.reason as string)) {
553
- errors.push('Spark Swarm observation unknown reason must be canonical');
554
- }
555
- if (observation.state === 'member') {
556
- if (!isRuntimeIdentifier(observation.swarmClusterId)
557
- || !isRuntimeIdentifier(observation.localSwarmNodeId)
558
- || typeof observation.controlAvailable !== 'boolean') {
559
- errors.push('Spark Swarm member observation must have canonical local authority');
560
- }
561
- if (observation.managerSnapshot !== undefined) {
562
- if (observation.controlAvailable !== true) {
563
- errors.push('Spark Swarm manager snapshot requires controlAvailable');
564
- } else if (isRuntimeIdentifier(observation.swarmClusterId)
565
- && isRuntimeIdentifier(observation.localSwarmNodeId)) {
566
- errors.push(...await validateSwarmManagerSnapshot(
567
- observation.swarmClusterId,
568
- observation.localSwarmNodeId,
569
- observation.managerSnapshot,
570
- ));
571
- }
572
- }
573
- }
574
- return errors;
575
- } catch {
576
- return ['Spark Swarm observation must be safely inspectable'];
577
- }
578
- };
579
-
580
- export const validateSparkSwarmObservationV2 = async (
581
- observationArg: unknown,
582
- ): Promise<string[]> => {
583
- try {
584
- if (!isRuntimeRecord(observationArg)) return ['Spark Swarm observation v2 must be an object'];
585
- const observation = observationArg as Record<string, unknown>;
586
- const commonKeys = [
587
- 'schemaVersion',
588
555
  'reporterSessionId',
589
556
  'observationSequence',
590
557
  'observedAt',
@@ -605,31 +572,28 @@ export const validateSparkSwarmObservationV2 = async (
605
572
  ...(observation.managerSnapshot === undefined ? [] : ['managerSnapshot']),
606
573
  ]
607
574
  : [];
608
- if (expectedKeys.length === 0 || !hasExactRuntimeKeys(observation, expectedKeys)) {
609
- return ['Spark Swarm observation v2 must use its exact state schema'];
575
+ if (expectedKeys.length === 0 || !hasExactKeys(observation, expectedKeys)) {
576
+ return ['Spark Swarm observation must use its exact state schema'];
610
577
  }
611
578
  const errors: string[] = [];
612
- if (observation.schemaVersion !== 2) {
613
- errors.push('Spark Swarm observation v2 schemaVersion must be 2');
614
- }
615
579
  if (!isRuntimeIdentifier(observation.reporterSessionId)
616
580
  || !isPositiveSafeInteger(observation.observationSequence)
617
581
  || !isPositiveSafeInteger(observation.observedAt)) {
618
- errors.push('Spark Swarm observation v2 identity, sequence, and timestamp must be canonical');
582
+ errors.push('Spark Swarm observation identity, sequence, and timestamp must be canonical');
619
583
  }
620
584
  if (observation.state === 'unknown'
621
585
  && !['docker-unavailable', 'query-failed', 'not-initialized']
622
586
  .includes(observation.reason as string)) {
623
- errors.push('Spark Swarm observation v2 unknown reason must be canonical');
587
+ errors.push('Spark Swarm observation unknown reason must be canonical');
624
588
  }
625
589
  if (observation.state === 'member') {
626
590
  if (!isRuntimeIdentifier(observation.localSwarmNodeId)
627
591
  || typeof observation.controlAvailable !== 'boolean') {
628
- errors.push('Spark Swarm member observation v2 must have canonical local authority');
592
+ errors.push('Spark Swarm member observation must have canonical local authority');
629
593
  }
630
594
  if (observation.controlAvailable === true) {
631
595
  if (!isRuntimeIdentifier(observation.swarmClusterId)) {
632
- errors.push('Spark Swarm manager observation v2 must have canonical cluster authority');
596
+ errors.push('Spark Swarm manager observation must have canonical cluster authority');
633
597
  } else if (observation.managerSnapshot !== undefined
634
598
  && isRuntimeIdentifier(observation.localSwarmNodeId)) {
635
599
  errors.push(...await validateSwarmManagerSnapshot(
@@ -642,59 +606,62 @@ export const validateSparkSwarmObservationV2 = async (
642
606
  }
643
607
  return errors;
644
608
  } catch {
645
- return ['Spark Swarm observation v2 must be safely inspectable'];
609
+ return ['Spark Swarm observation must be safely inspectable'];
646
610
  }
647
611
  };
648
612
 
649
- export const validateSparkSwarmObservationV2Request = async (
613
+ export const validateSparkSwarmObservationRequest = async (
650
614
  requestArg: unknown,
651
615
  ): Promise<string[]> => {
652
616
  try {
653
617
  if (!isRuntimeRecord(requestArg)
654
- || !hasExactRuntimeKeys(requestArg, ['nodeId', 'nodeToken', 'observation'])) {
655
- return ['Spark Swarm observation v2 request must use its exact schema'];
618
+ || !hasExactKeys(requestArg, ['nodeId', 'nodeToken', 'protocol', 'observation'])) {
619
+ return ['Spark Swarm observation request must use its exact schema'];
656
620
  }
657
621
  try {
658
622
  canonicalizeStrictJson(
659
623
  requestArg,
660
- failSparkSwarmObservationV2Canonicalization,
661
- 'Spark Swarm observation v2 request',
624
+ failSparkSwarmObservationCanonicalization,
625
+ 'Spark Swarm observation request',
662
626
  );
663
627
  } catch {
664
- return ['Spark Swarm observation v2 request must contain strict canonical JSON data'];
628
+ return ['Spark Swarm observation request must contain strict canonical JSON data'];
665
629
  }
666
- const errors = await validateSparkSwarmObservationV2(requestArg.observation);
630
+ const errors = validateProtocolOfferOn(
631
+ requestArg,
632
+ 'Spark Swarm observation request protocol',
633
+ );
634
+ errors.push(...await validateSparkSwarmObservation(requestArg.observation));
667
635
  if (!isRuntimeIdentifier(requestArg.nodeId)) {
668
- errors.push('Spark Swarm observation v2 request nodeId must be canonical');
636
+ errors.push('Spark Swarm observation request nodeId must be canonical');
669
637
  }
670
638
  if (typeof requestArg.nodeToken !== 'string'
671
639
  || requestArg.nodeToken.length < 1
672
640
  || requestArg.nodeToken.length > 512
673
641
  || requestArg.nodeToken.trim() !== requestArg.nodeToken) {
674
- errors.push('Spark Swarm observation v2 request nodeToken must be a bounded opaque token');
642
+ errors.push('Spark Swarm observation request nodeToken must be a bounded opaque token');
675
643
  }
676
644
  return errors;
677
645
  } catch {
678
- return ['Spark Swarm observation v2 request must be safely inspectable'];
646
+ return ['Spark Swarm observation request must be safely inspectable'];
679
647
  }
680
648
  };
681
649
 
682
- export const validateSparkSwarmObservationV2Response = async (
650
+ export const validateSparkSwarmObservationResponse = async (
683
651
  responseArg: unknown,
684
- expectedRequestArg: ISparkSwarmObservationV2Request,
652
+ expectedRequestArg: ISparkSwarmObservationRequest,
685
653
  ): Promise<string[]> => {
686
654
  try {
687
- const requestErrors = await validateSparkSwarmObservationV2Request(
655
+ const requestErrors = await validateSparkSwarmObservationRequest(
688
656
  expectedRequestArg,
689
657
  );
690
658
  if (requestErrors.length > 0) {
691
- return ['expected Spark Swarm observation v2 request must be valid'];
659
+ return ['expected Spark Swarm observation request must be valid'];
692
660
  }
693
661
  if (!isRuntimeRecord(responseArg)) {
694
- return ['Spark Swarm observation v2 response must be an object'];
662
+ return ['Spark Swarm observation response must be an object'];
695
663
  }
696
664
  const acceptedKeys = [
697
- 'schemaVersion',
698
665
  'state',
699
666
  'nodeId',
700
667
  'reporterSessionId',
@@ -702,9 +669,9 @@ export const validateSparkSwarmObservationV2Response = async (
702
669
  'observationDigest',
703
670
  'acceptedAt',
704
671
  'targetGenerationAtAcceptance',
672
+ 'protocol',
705
673
  ];
706
674
  const rejectedKeys = [
707
- 'schemaVersion',
708
675
  'state',
709
676
  'nodeId',
710
677
  'reporterSessionId',
@@ -713,61 +680,71 @@ export const validateSparkSwarmObservationV2Response = async (
713
680
  'rejectedAt',
714
681
  'code',
715
682
  'retryable',
683
+ 'protocol',
716
684
  ];
717
685
  const expectedKeys = responseArg.state === 'accepted'
718
686
  ? acceptedKeys
719
687
  : responseArg.state === 'rejected'
720
688
  ? rejectedKeys
721
689
  : [];
722
- if (expectedKeys.length === 0 || !hasExactRuntimeKeys(responseArg, expectedKeys)) {
723
- return ['Spark Swarm observation v2 response must use its exact state schema'];
690
+ if (expectedKeys.length === 0 || !hasExactKeys(responseArg, expectedKeys)) {
691
+ return ['Spark Swarm observation response must use its exact state schema'];
724
692
  }
725
693
  try {
726
694
  canonicalizeStrictJson(
727
695
  responseArg,
728
- failSparkSwarmObservationV2Canonicalization,
729
- 'Spark Swarm observation v2 response',
696
+ failSparkSwarmObservationCanonicalization,
697
+ 'Spark Swarm observation response',
730
698
  );
731
699
  } catch {
732
- return ['Spark Swarm observation v2 response must contain strict canonical JSON data'];
700
+ return ['Spark Swarm observation response must contain strict canonical JSON data'];
733
701
  }
734
- const errors: string[] = [];
735
- const observationDigest = await computeSparkSwarmObservationV2Digest(
702
+ const errors = validateProtocolOfferOn(
703
+ responseArg,
704
+ 'Spark Swarm observation response protocol',
705
+ );
706
+ const observationDigest = await computeSparkSwarmObservationDigest(
736
707
  expectedRequestArg.observation,
737
708
  );
738
- if (responseArg.schemaVersion !== 2
739
- || responseArg.nodeId !== expectedRequestArg.nodeId
709
+ if (responseArg.nodeId !== expectedRequestArg.nodeId
740
710
  || responseArg.reporterSessionId !== expectedRequestArg.observation.reporterSessionId
741
711
  || responseArg.observationSequence !== expectedRequestArg.observation.observationSequence
742
712
  || responseArg.observationDigest !== observationDigest) {
743
- errors.push('Spark Swarm observation v2 response must bind the expected request');
713
+ errors.push('Spark Swarm observation response must bind the expected request');
744
714
  }
745
715
  if (responseArg.state === 'accepted') {
746
716
  if (!isPositiveSafeInteger(responseArg.acceptedAt)
747
717
  || !Number.isSafeInteger(responseArg.targetGenerationAtAcceptance)
748
718
  || (responseArg.targetGenerationAtAcceptance as number) < 0) {
749
- errors.push('Spark Swarm observation v2 accepted receipt fields must be canonical');
719
+ errors.push('Spark Swarm observation accepted receipt fields must be canonical');
750
720
  }
751
721
  } else {
752
- const retryability = sparkSwarmObservationV2Contract.rejectionRetryability;
722
+ const retryability = sparkSwarmObservationContract.rejectionRetryability;
753
723
  if (!isPositiveSafeInteger(responseArg.rejectedAt)
754
724
  || typeof responseArg.code !== 'string'
755
725
  || !Object.hasOwn(retryability, responseArg.code)
756
726
  || responseArg.retryable !== retryability[
757
- responseArg.code as TSparkSwarmObservationV2RejectionCode
727
+ responseArg.code as TSparkSwarmObservationRejectionCode
758
728
  ]) {
759
- errors.push('Spark Swarm observation v2 rejected receipt fields must be canonical');
729
+ errors.push('Spark Swarm observation rejected receipt fields must be canonical');
760
730
  }
761
731
  }
762
732
  return errors;
763
733
  } catch {
764
- return ['Spark Swarm observation v2 response must be safely inspectable'];
734
+ return ['Spark Swarm observation response must be safely inspectable'];
765
735
  }
766
736
  };
767
737
 
738
+ /**
739
+ * The exact five members of a target that the set digest is taken over.
740
+ *
741
+ * A projection, not the canonical-JSON rule: it drops anything else a caller's target carries, so
742
+ * the digest cannot move with a member no peer agreed on. Canonicalizing the target instead would
743
+ * hash whatever it happens to hold.
744
+ */
768
745
  const canonicalRuntimeTarget = (
769
- targetArg: IClusterRuntimeTargetV1,
770
- ): IClusterRuntimeTargetV1 => ({
746
+ targetArg: IClusterRuntimeTarget,
747
+ ): IClusterRuntimeTarget => ({
771
748
  cloudlyNodeId: targetArg.cloudlyNodeId,
772
749
  swarmClusterId: targetArg.swarmClusterId,
773
750
  swarmNodeId: targetArg.swarmNodeId,
@@ -776,9 +753,8 @@ const canonicalRuntimeTarget = (
776
753
  });
777
754
 
778
755
  export const createClusterRuntimeTargetSetDigestInput = (
779
- targetSetArg: IClusterRuntimeTargetSetReadyV1,
756
+ targetSetArg: IClusterRuntimeTargetSetReady,
780
757
  ): string => JSON.stringify({
781
- schemaVersion: targetSetArg.schemaVersion,
782
758
  purpose: 'serve.zone/cluster-runtime-target-set',
783
759
  cloudlyClusterId: targetSetArg.cloudlyClusterId,
784
760
  swarmClusterId: targetSetArg.swarmClusterId,
@@ -787,7 +763,7 @@ export const createClusterRuntimeTargetSetDigestInput = (
787
763
  });
788
764
 
789
765
  export const computeClusterRuntimeTargetSetDigest = async (
790
- targetSetArg: IClusterRuntimeTargetSetReadyV1,
766
+ targetSetArg: IClusterRuntimeTargetSetReady,
791
767
  ): Promise<TSha256Digest> => computeSha256(createClusterRuntimeTargetSetDigestInput(targetSetArg));
792
768
 
793
769
  export const validateClusterRuntimeTargetSet = async (
@@ -796,8 +772,7 @@ export const validateClusterRuntimeTargetSet = async (
796
772
  try {
797
773
  if (!isRuntimeRecord(targetSetArg)) return ['cluster runtime target set must be an object'];
798
774
  if (targetSetArg.state === 'unavailable') {
799
- if (!hasExactRuntimeKeys(targetSetArg, [
800
- 'schemaVersion',
775
+ if (!hasExactKeys(targetSetArg, [
801
776
  'state',
802
777
  'cloudlyClusterId',
803
778
  'generation',
@@ -807,8 +782,7 @@ export const validateClusterRuntimeTargetSet = async (
807
782
  return ['unavailable cluster runtime target set must use its exact schema'];
808
783
  }
809
784
  const errors: string[] = [];
810
- if (targetSetArg.schemaVersion !== 1
811
- || !isRuntimeIdentifier(targetSetArg.cloudlyClusterId)
785
+ if (!isRuntimeIdentifier(targetSetArg.cloudlyClusterId)
812
786
  || !Number.isSafeInteger(targetSetArg.generation)
813
787
  || (targetSetArg.generation as number) < 0
814
788
  || !isPositiveSafeInteger(targetSetArg.changedAt)
@@ -827,8 +801,7 @@ export const validateClusterRuntimeTargetSet = async (
827
801
  return errors;
828
802
  }
829
803
  if (targetSetArg.state !== 'ready'
830
- || !hasExactRuntimeKeys(targetSetArg, [
831
- 'schemaVersion',
804
+ || !hasExactKeys(targetSetArg, [
832
805
  'state',
833
806
  'cloudlyClusterId',
834
807
  'swarmClusterId',
@@ -841,8 +814,7 @@ export const validateClusterRuntimeTargetSet = async (
841
814
  return ['ready cluster runtime target set must use its exact schema'];
842
815
  }
843
816
  const errors: string[] = [];
844
- if (targetSetArg.schemaVersion !== 1
845
- || !isRuntimeIdentifier(targetSetArg.cloudlyClusterId)
817
+ if (!isRuntimeIdentifier(targetSetArg.cloudlyClusterId)
846
818
  || !isRuntimeIdentifier(targetSetArg.swarmClusterId)
847
819
  || !isPositiveSafeInteger(targetSetArg.generation)
848
820
  || !isPositiveSafeInteger(targetSetArg.acceptedAt)
@@ -864,7 +836,7 @@ export const validateClusterRuntimeTargetSet = async (
864
836
  let previousSortKey: string | undefined;
865
837
  for (const [index, targetArg] of targetSetArg.targets.entries()) {
866
838
  if (!isRuntimeRecord(targetArg)
867
- || !hasExactRuntimeKeys(targetArg, [
839
+ || !hasExactKeys(targetArg, [
868
840
  'cloudlyNodeId',
869
841
  'swarmClusterId',
870
842
  'swarmNodeId',
@@ -899,7 +871,7 @@ export const validateClusterRuntimeTargetSet = async (
899
871
  }
900
872
  if (errors.length === 0
901
873
  && targetSetArg.targetSetDigest !== await computeClusterRuntimeTargetSetDigest(
902
- targetSetArg as unknown as IClusterRuntimeTargetSetReadyV1,
874
+ targetSetArg as unknown as IClusterRuntimeTargetSetReady,
903
875
  )) {
904
876
  errors.push('cluster runtime target set digest does not match');
905
877
  }
@@ -910,9 +882,9 @@ export const validateClusterRuntimeTargetSet = async (
910
882
  };
911
883
 
912
884
  export const isClusterRuntimeTargetSetFresh = (
913
- targetSetArg: TClusterRuntimeTargetSetV1,
885
+ targetSetArg: TClusterRuntimeTargetSet,
914
886
  nowArg: number,
915
- ): targetSetArg is IClusterRuntimeTargetSetReadyV1 => (
887
+ ): targetSetArg is IClusterRuntimeTargetSetReady => (
916
888
  targetSetArg.state === 'ready'
917
889
  && Number.isSafeInteger(nowArg)
918
890
  && nowArg >= targetSetArg.acceptedAt
@@ -994,7 +966,7 @@ export interface IClusterNode {
994
966
  sparkRuntimeInfo?: ISparkNodeRuntimeInfo;
995
967
 
996
968
  /** Separately accepted Pallet-era host report; never grants workload readiness. */
997
- sparkNodeReport?: ISparkNodeReportRecordV2;
969
+ sparkNodeReport?: ISparkNodeReportRecord;
998
970
 
999
971
  /**
1000
972
  * Docker swarm node ID if part of swarm