@serve.zone/interfaces 31.5.0 → 31.7.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.
@@ -13,6 +13,7 @@ import type {
13
13
  } from '../data/backup.js';
14
14
  import type { IService } from '../data/service.js';
15
15
  import type { IIdentity, IIdentityCredential } from '../data/user.js';
16
+ import type { ICorestoreDatabaseClosureDescriptor } from '../runtime.corestore.relay.js';
16
17
 
17
18
  export interface IReq_Any_Cloudly_CreateServiceBackup
18
19
  extends plugins.typedrequestInterfaces.implementsTR<
@@ -80,6 +81,15 @@ extends plugins.typedrequestInterfaces.implementsTR<
80
81
  };
81
82
  }
82
83
 
84
+ /**
85
+ * Superseded by `backupClusterService`, which names the node.
86
+ *
87
+ * This method was dispatched at the coreflow that had tagged itself with the
88
+ * node's hostname; a cluster relay authors no tags and serves every node of its
89
+ * cluster, so the node it should act on can no longer be inferred from which
90
+ * connection answered. It is retired together with the tag in the coordinated
91
+ * major.
92
+ */
83
93
  export interface IReq_Cloudly_Coreflow_ExecuteServiceBackup
84
94
  extends plugins.typedrequestInterfaces.implementsTR<
85
95
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -100,6 +110,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
100
110
  };
101
111
  }
102
112
 
113
+ /** Superseded by `restoreClusterService`; see `executeServiceBackup`. */
103
114
  export interface IReq_Cloudly_Coreflow_ExecuteServiceRestore
104
115
  extends plugins.typedrequestInterfaces.implementsTR<
105
116
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -137,6 +148,14 @@ extends plugins.typedrequestInterfaces.implementsTR<
137
148
  };
138
149
  }
139
150
 
151
+ /**
152
+ * Retiring: `contentsBase64` carries a whole archive object inside one JSON
153
+ * envelope, which costs a third of the payload in encoding and bounds a backup
154
+ * by the message ceiling rather than by the transport. `backupServiceDatabaseClosure`
155
+ * streams the same bytes as a VirtualStream. Removal is the coordinated major,
156
+ * once every relay and every Cloudly speak the streamed contract; Corestore
157
+ * already documents its repository-wide archive object routes as legacy.
158
+ */
140
159
  export interface IReq_Coreflow_Cloudly_UploadBackupArchiveObject
141
160
  extends plugins.typedrequestInterfaces.implementsTR<
142
161
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -185,6 +204,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
185
204
  };
186
205
  }
187
206
 
207
+ /** Retiring with `uploadBackupArchiveObject`; `restoreServiceDatabaseClosure` replaces it. */
188
208
  export interface IReq_Coreflow_Cloudly_DownloadBackupArchiveObject
189
209
  extends plugins.typedrequestInterfaces.implementsTR<
190
210
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -202,6 +222,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
202
222
  };
203
223
  }
204
224
 
225
+ /** Superseded by `pruneClusterNodeArchive`; see `executeServiceBackup`. */
205
226
  export interface IReq_Cloudly_Coreflow_PruneNodeArchive
206
227
  extends plugins.typedrequestInterfaces.implementsTR<
207
228
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -218,6 +239,152 @@ extends plugins.typedrequestInterfaces.implementsTR<
218
239
  };
219
240
  }
220
241
 
242
+ /**
243
+ * Backs up one service on one exact node of this cluster.
244
+ *
245
+ * The cluster relay serves every node of its cluster over one session, so the
246
+ * node is a request field: the connection that answers proves which cluster is
247
+ * speaking, never which node is meant. Everything else is what
248
+ * `executeServiceBackup` carried, so a caller moving to this method changes its
249
+ * routing and nothing about its backup.
250
+ */
251
+ export interface IReq_Cloudly_Relay_BackupClusterService
252
+ extends plugins.typedrequestInterfaces.implementsTR<
253
+ plugins.typedrequestInterfaces.ITypedRequest,
254
+ IReq_Cloudly_Relay_BackupClusterService
255
+ > {
256
+ method: 'backupClusterService';
257
+ request: {
258
+ /** The cluster node whose Corestore holds this service's data. */
259
+ nodeId: string;
260
+ backupId: string;
261
+ service: IService;
262
+ tags?: Record<string, string>;
263
+ replication?: {
264
+ enabled: boolean;
265
+ };
266
+ };
267
+ response: {
268
+ snapshots: TBackupSnapshot[];
269
+ replication?: IBackupReplicationResult;
270
+ };
271
+ }
272
+
273
+ /** Restores one service on one exact node of this cluster; see `backupClusterService`. */
274
+ export interface IReq_Cloudly_Relay_RestoreClusterService
275
+ extends plugins.typedrequestInterfaces.implementsTR<
276
+ plugins.typedrequestInterfaces.ITypedRequest,
277
+ IReq_Cloudly_Relay_RestoreClusterService
278
+ > {
279
+ method: 'restoreClusterService';
280
+ request: {
281
+ /** The cluster node whose Corestore receives the restore. */
282
+ nodeId: string;
283
+ backupId: string;
284
+ service: IService;
285
+ snapshots: TBackupSnapshot[];
286
+ clear?: boolean;
287
+ resourceTypes?: Array<TBackupSnapshot['type']>;
288
+ replication?: {
289
+ enabled: boolean;
290
+ };
291
+ };
292
+ response: {
293
+ restored: TBackupSnapshot[];
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Prunes the archive of one exact node of this cluster.
299
+ *
300
+ * `found` stays false when that node has no archive to prune, which is a fact
301
+ * about the node rather than a failure of the relay.
302
+ */
303
+ export interface IReq_Cloudly_Relay_PruneClusterNodeArchive
304
+ extends plugins.typedrequestInterfaces.implementsTR<
305
+ plugins.typedrequestInterfaces.ITypedRequest,
306
+ IReq_Cloudly_Relay_PruneClusterNodeArchive
307
+ > {
308
+ method: 'pruneClusterNodeArchive';
309
+ request: {
310
+ /** The cluster node whose archive is pruned. */
311
+ nodeId: string;
312
+ retention: IArchiveRetentionPolicy;
313
+ dryRun?: boolean;
314
+ };
315
+ response: {
316
+ found: boolean;
317
+ result?: IArchivePruneResult;
318
+ };
319
+ }
320
+
321
+ /**
322
+ * The relay carries one service database closure to Cloudly.
323
+ *
324
+ * Relay-initiated, like every other byte a cluster sends: a cluster is
325
+ * outbound-only, so Cloudly cannot pull from it. The relay exports the closure
326
+ * from its node's Corestore and sends those exact bytes on its own session.
327
+ *
328
+ * `accepted` permits sending; it never confirms storage. The stream's own
329
+ * acceptance receipt, observed through `completion`, is what confirms that
330
+ * Cloudly durably holds the closure — exactly as `pushImageVersion` works.
331
+ *
332
+ * No identity field: the cluster is the one the verified machine identity on
333
+ * the relay's own session names, exactly as `getClusterRouteTable` and
334
+ * `getClusterPlatformDesiredState` work. A request-level identity would be a
335
+ * second authority for the same fact, and the weaker of the two.
336
+ */
337
+ export interface IReq_Relay_Cloudly_BackupServiceDatabaseClosure
338
+ extends plugins.typedrequestInterfaces.implementsTR<
339
+ plugins.typedrequestInterfaces.ITypedRequest,
340
+ IReq_Relay_Cloudly_BackupServiceDatabaseClosure
341
+ > {
342
+ method: 'backupServiceDatabaseClosure';
343
+ request: {
344
+ backupId: string;
345
+ serviceId: string;
346
+ /** The cluster node this closure was exported from. */
347
+ nodeId: string;
348
+ /** Exact bytes and Corestore's receipt for them; validated before any byte moves. */
349
+ descriptor: ICorestoreDatabaseClosureDescriptor;
350
+ /** Requester sends closure bytes; the TypedHandler receives the opposite endpoint. */
351
+ closure: plugins.typedrequestInterfaces.TVirtualStream<'send'>;
352
+ };
353
+ response: {
354
+ accepted: boolean;
355
+ };
356
+ }
357
+
358
+ /**
359
+ * The relay fetches one service database closure back from Cloudly.
360
+ *
361
+ * The descriptor arrives with the stream rather than after it, because
362
+ * Corestore's restore needs the exact byte length and receipt before the first
363
+ * byte is written: the relay opens its Corestore request from this descriptor
364
+ * and only then drains the stream into it.
365
+ *
366
+ * No identity field, for the same reason as `backupServiceDatabaseClosure`: the
367
+ * relay's own session names the cluster.
368
+ */
369
+ export interface IReq_Relay_Cloudly_RestoreServiceDatabaseClosure
370
+ extends plugins.typedrequestInterfaces.implementsTR<
371
+ plugins.typedrequestInterfaces.ITypedRequest,
372
+ IReq_Relay_Cloudly_RestoreServiceDatabaseClosure
373
+ > {
374
+ method: 'restoreServiceDatabaseClosure';
375
+ request: {
376
+ backupId: string;
377
+ serviceId: string;
378
+ /** The cluster node the closure is restored onto. */
379
+ nodeId: string;
380
+ };
381
+ response: {
382
+ descriptor: ICorestoreDatabaseClosureDescriptor;
383
+ /** Requester receives closure bytes; the TypedHandler returns a sender endpoint. */
384
+ closure: plugins.typedrequestInterfaces.TVirtualStream<'receive'>;
385
+ };
386
+ }
387
+
221
388
  /**
222
389
  * Creates a restore in a new non-runnable data-plane scratch namespace.
223
390
  * Cloudly derives clusterId and sourceServiceId from the authoritative backup,
@@ -316,6 +483,18 @@ extends plugins.typedrequestInterfaces.implementsTR<
316
483
  > {
317
484
  method: 'coreflowPrepareIsolatedRestore';
318
485
  request: {
486
+ /**
487
+ * The compact RS256 restore grant Cloudly signed for exactly this
488
+ * operation, carried untouched to Corestore.
489
+ *
490
+ * Corestore accepts no other authority on an isolated-restore route: the
491
+ * grant names the operation, expires within minutes, and binds the cluster,
492
+ * the node, the canonical resource mappings and the archive manifest, so a
493
+ * relay can carry it but can neither widen it nor mint one. Sensitive
494
+ * ephemeral input: exclude the complete request from hooks, logs and
495
+ * journals on every hop.
496
+ */
497
+ restoreGrant: string;
319
498
  clusterId: string;
320
499
  targetNodeName: string;
321
500
  restoreId: string;
@@ -332,6 +511,44 @@ extends plugins.typedrequestInterfaces.implementsTR<
332
511
  };
333
512
  }
334
513
 
514
+ /**
515
+ * Cloudly stages one archive object of an isolated restore on the relay's node.
516
+ *
517
+ * Cloudly holds the source backup, so these bytes travel Cloudly → relay and
518
+ * the requester is the sender. One object per call, as the archive manifest
519
+ * lists it: Corestore writes staged objects under a `write-object` grant, one
520
+ * bounded chunk at a time, and the relay is what turns this stream into those
521
+ * chunks. The object descriptor is the manifest entry itself — path, exact size
522
+ * and digest — so the relay can prove the complete object before it counts as
523
+ * written, and so a replayed stage settles on the same bytes.
524
+ */
525
+ export interface IReq_Cloudly_Relay_StageIsolatedRestoreArchive
526
+ extends plugins.typedrequestInterfaces.implementsTR<
527
+ plugins.typedrequestInterfaces.ITypedRequest,
528
+ IReq_Cloudly_Relay_StageIsolatedRestoreArchive
529
+ > {
530
+ method: 'stageIsolatedRestoreArchive';
531
+ request: {
532
+ /**
533
+ * The compact RS256 restore grant for the `write-object` operation, carried
534
+ * untouched to Corestore. Sensitive ephemeral input, like every other
535
+ * grant on these routes.
536
+ */
537
+ restoreGrant: string;
538
+ clusterId: string;
539
+ targetNodeName: string;
540
+ restoreId: string;
541
+ expectedRevision: number;
542
+ /** The manifest entry these bytes complete: `path`, `size`, `sha256`. */
543
+ object: IBackupArchiveObject;
544
+ /** Requester sends the object bytes; the TypedHandler receives them. */
545
+ archive: plugins.typedrequestInterfaces.TVirtualStream<'send'>;
546
+ };
547
+ response: {
548
+ state: IIsolatedRestoreNodeState;
549
+ };
550
+ }
551
+
335
552
  /** Read-only status for one exact cluster/node/restore routing key. */
336
553
  export interface IReq_Cloudly_Coreflow_GetIsolatedRestoreStatus
337
554
  extends plugins.typedrequestInterfaces.implementsTR<
@@ -340,6 +557,18 @@ extends plugins.typedrequestInterfaces.implementsTR<
340
557
  > {
341
558
  method: 'coreflowGetIsolatedRestoreStatus';
342
559
  request: {
560
+ /**
561
+ * The compact RS256 restore grant Cloudly signed for exactly this
562
+ * operation, carried untouched to Corestore.
563
+ *
564
+ * Corestore accepts no other authority on an isolated-restore route: the
565
+ * grant names the operation, expires within minutes, and binds the cluster,
566
+ * the node, the canonical resource mappings and the archive manifest, so a
567
+ * relay can carry it but can neither widen it nor mint one. Sensitive
568
+ * ephemeral input: exclude the complete request from hooks, logs and
569
+ * journals on every hop.
570
+ */
571
+ restoreGrant: string;
343
572
  clusterId: string;
344
573
  targetNodeName: string;
345
574
  restoreId: string;
@@ -357,6 +586,18 @@ extends plugins.typedrequestInterfaces.implementsTR<
357
586
  > {
358
587
  method: 'coreflowExecuteIsolatedRestore';
359
588
  request: {
589
+ /**
590
+ * The compact RS256 restore grant Cloudly signed for exactly this
591
+ * operation, carried untouched to Corestore.
592
+ *
593
+ * Corestore accepts no other authority on an isolated-restore route: the
594
+ * grant names the operation, expires within minutes, and binds the cluster,
595
+ * the node, the canonical resource mappings and the archive manifest, so a
596
+ * relay can carry it but can neither widen it nor mint one. Sensitive
597
+ * ephemeral input: exclude the complete request from hooks, logs and
598
+ * journals on every hop.
599
+ */
600
+ restoreGrant: string;
360
601
  clusterId: string;
361
602
  targetNodeName: string;
362
603
  restoreId: string;
@@ -378,6 +619,18 @@ extends plugins.typedrequestInterfaces.implementsTR<
378
619
  > {
379
620
  method: 'coreflowCleanupIsolatedRestore';
380
621
  request: {
622
+ /**
623
+ * The compact RS256 restore grant Cloudly signed for exactly this
624
+ * operation, carried untouched to Corestore.
625
+ *
626
+ * Corestore accepts no other authority on an isolated-restore route: the
627
+ * grant names the operation, expires within minutes, and binds the cluster,
628
+ * the node, the canonical resource mappings and the archive manifest, so a
629
+ * relay can carry it but can neither widen it nor mint one. Sensitive
630
+ * ephemeral input: exclude the complete request from hooks, logs and
631
+ * journals on every hop.
632
+ */
633
+ restoreGrant: string;
381
634
  clusterId: string;
382
635
  targetNodeName: string;
383
636
  restoreId: string;
@@ -22,6 +22,10 @@ export interface ICorestoreServiceResources {
22
22
  /**
23
23
  * Corestore inventory of one node (each coreflow node runs its own corestore;
24
24
  * the data is node-local, which is what makes node placement matter).
25
+ *
26
+ * Superseded by `IClusterCorestoreInventory`, which names the node by its
27
+ * cluster-document id instead of by the Swarm hostnames a connection tagged
28
+ * itself with.
25
29
  */
26
30
  export interface ICorestoreNodeInventory {
27
31
  /** Swarm node hostnames of the reporting coreflow. */
@@ -83,6 +87,156 @@ export interface ICoreflowInventoryRegistration {
83
87
  registeredAt: number;
84
88
  }
85
89
 
90
+ /**
91
+ * What one node's Corestore holds, as the cluster relay read it.
92
+ *
93
+ * Node identity is the cluster document's node id: one relay serves every node
94
+ * of its cluster, so which node an inventory describes is stated, never derived
95
+ * from which connection answered. An unreachable node names why — `errorCode`
96
+ * is required whenever `reachable` is false and refused whenever it is true —
97
+ * because a node whose Corestore cannot be reached is a fact the control plane
98
+ * needs, and a silent empty inventory would read as "nothing provisioned",
99
+ * which is the opposite.
100
+ */
101
+ export interface IClusterCorestoreInventory {
102
+ nodeId: string;
103
+ checkedAt: number;
104
+ reachable: boolean;
105
+ errorCode?: 'CORESTORE_ENDPOINT_UNKNOWN' | 'CORESTORE_UNREACHABLE';
106
+ services: ICorestoreServiceResources[];
107
+ }
108
+
109
+ const clusterCorestoreInventoryIdentifierPattern = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
110
+
111
+ const isClusterCorestoreRecord = (
112
+ valueArg: unknown,
113
+ ): valueArg is Record<string, unknown> => (
114
+ Boolean(valueArg)
115
+ && typeof valueArg === 'object'
116
+ && !Array.isArray(valueArg)
117
+ && (Object.getPrototypeOf(valueArg) === Object.prototype
118
+ || Object.getPrototypeOf(valueArg) === null)
119
+ );
120
+
121
+ const hasClusterCorestoreKeys = (
122
+ valueArg: Record<string, unknown>,
123
+ keysArg: readonly string[],
124
+ optionalKeysArg: readonly string[] = [],
125
+ ): boolean => {
126
+ const actualKeys = Object.keys(valueArg);
127
+ return keysArg.every((keyArg) => actualKeys.includes(keyArg))
128
+ && actualKeys.every((keyArg) => keysArg.includes(keyArg) || optionalKeysArg.includes(keyArg));
129
+ };
130
+
131
+ const validateClusterCorestoreServiceResources = (
132
+ valueArg: unknown,
133
+ pathArg: string,
134
+ ): string[] => {
135
+ if (!isClusterCorestoreRecord(valueArg)
136
+ || !hasClusterCorestoreKeys(
137
+ valueArg,
138
+ ['serviceId', 'resources', 'updatedAt'],
139
+ ['serviceName'],
140
+ )) {
141
+ return [`${pathArg} must use its exact schema`];
142
+ }
143
+ const errors: string[] = [];
144
+ if (typeof valueArg.serviceId !== 'string'
145
+ || !clusterCorestoreInventoryIdentifierPattern.test(valueArg.serviceId)) {
146
+ errors.push(`${pathArg} serviceId must be a bounded identifier`);
147
+ }
148
+ if (!Number.isSafeInteger(valueArg.updatedAt) || (valueArg.updatedAt as number) < 0) {
149
+ errors.push(`${pathArg} updatedAt must be a non-negative safe integer`);
150
+ }
151
+ if (!Array.isArray(valueArg.resources)) {
152
+ errors.push(`${pathArg} resources must be an array`);
153
+ return errors;
154
+ }
155
+ for (const [index, resource] of valueArg.resources.entries()) {
156
+ const resourcePath = `${pathArg} resources[${index}]`;
157
+ if (!isClusterCorestoreRecord(resource)
158
+ || !hasClusterCorestoreKeys(
159
+ resource,
160
+ ['capability', 'provider', 'resourceName', 'createdAt'],
161
+ )) {
162
+ errors.push(`${resourcePath} must use its exact schema`);
163
+ continue;
164
+ }
165
+ if (resource.capability !== 'database' && resource.capability !== 'objectstorage') {
166
+ errors.push(`${resourcePath} capability must be database or objectstorage`);
167
+ }
168
+ if (typeof resource.provider !== 'string' || resource.provider.length === 0) {
169
+ errors.push(`${resourcePath} provider must be a non-empty string`);
170
+ }
171
+ if (typeof resource.resourceName !== 'string' || resource.resourceName.length === 0) {
172
+ errors.push(`${resourcePath} resourceName must be a non-empty string`);
173
+ }
174
+ if (!Number.isSafeInteger(resource.createdAt) || (resource.createdAt as number) < 0) {
175
+ errors.push(`${resourcePath} createdAt must be a non-negative safe integer`);
176
+ }
177
+ }
178
+ return errors;
179
+ };
180
+
181
+ /**
182
+ * Validates one cluster Corestore inventory and names every reason it is
183
+ * refused.
184
+ *
185
+ * Shape only. Whether that node belongs to the cluster, and whether a resource
186
+ * should exist, are decisions for the control plane that asked; an inventory
187
+ * that passes here has claimed neither. A reachable inventory that names an
188
+ * error, and an unreachable one that lists resources, are both refused: an
189
+ * answer has to be one of the two things it can be.
190
+ */
191
+ export const validateClusterCorestoreInventory = (
192
+ valueArg: unknown,
193
+ pathArg = 'cluster corestore inventory',
194
+ ): string[] => {
195
+ if (!isClusterCorestoreRecord(valueArg)
196
+ || !hasClusterCorestoreKeys(
197
+ valueArg,
198
+ ['nodeId', 'checkedAt', 'reachable', 'services'],
199
+ ['errorCode'],
200
+ )) {
201
+ return [`${pathArg} must use its exact schema`];
202
+ }
203
+ const errors: string[] = [];
204
+ if (typeof valueArg.nodeId !== 'string'
205
+ || !clusterCorestoreInventoryIdentifierPattern.test(valueArg.nodeId)) {
206
+ errors.push(`${pathArg} nodeId must be a bounded identifier`);
207
+ }
208
+ if (!Number.isSafeInteger(valueArg.checkedAt) || (valueArg.checkedAt as number) < 0) {
209
+ errors.push(`${pathArg} checkedAt must be a non-negative safe integer`);
210
+ }
211
+ if (typeof valueArg.reachable !== 'boolean') {
212
+ errors.push(`${pathArg} reachable must be a boolean`);
213
+ }
214
+ if (valueArg.errorCode !== undefined
215
+ && valueArg.errorCode !== 'CORESTORE_ENDPOINT_UNKNOWN'
216
+ && valueArg.errorCode !== 'CORESTORE_UNREACHABLE') {
217
+ errors.push(`${pathArg} errorCode must name why the node could not be read`);
218
+ }
219
+ if (valueArg.reachable === true && valueArg.errorCode !== undefined) {
220
+ errors.push(`${pathArg} must not carry an errorCode while it reports the node reachable`);
221
+ }
222
+ if (valueArg.reachable === false && valueArg.errorCode === undefined) {
223
+ // An unreachable node with no reason reads like a node holding nothing,
224
+ // which is the one confusion this shape exists to prevent.
225
+ errors.push(`${pathArg} must name why the node could not be read while it reports it unreachable`);
226
+ }
227
+ if (!Array.isArray(valueArg.services)) {
228
+ errors.push(`${pathArg} services must be an array`);
229
+ return errors;
230
+ }
231
+ if (valueArg.reachable === false && valueArg.services.length > 0) {
232
+ errors.push(`${pathArg} must list no services while it reports the node unreachable`);
233
+ }
234
+ for (const [index, service] of valueArg.services.entries()) {
235
+ errors.push(...validateClusterCorestoreServiceResources(service, `${pathArg} services[${index}]`));
236
+ }
237
+ return errors;
238
+ };
239
+
86
240
  export interface IReq_Any_Cloudly_GetCorestoreInventory
87
241
  extends plugins.typedrequestInterfaces.implementsTR<
88
242
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -95,6 +249,35 @@ extends plugins.typedrequestInterfaces.implementsTR<
95
249
  response: ICorestoreInventoryReport;
96
250
  }
97
251
 
252
+ /**
253
+ * Cloudly asks the relay what one exact node of its cluster holds.
254
+ *
255
+ * The node is a request field because one relay serves them all: the connection
256
+ * that answers proves the cluster, never the node. A greenfield cluster answers
257
+ * with an empty `services` list and `reachable: true`, which is the proof that
258
+ * a namespace is blank — distinct from `reachable: false`, which proves
259
+ * nothing at all.
260
+ *
261
+ * Named apart from `getCorestoreInventory` on purpose: that method is Cloudly's
262
+ * own operator-facing read and lives on the other end of this socket. One name
263
+ * on both ends would leave direction as the only thing telling two different
264
+ * bodies apart.
265
+ */
266
+ export interface IReq_Cloudly_Relay_GetClusterCorestoreInventory
267
+ extends plugins.typedrequestInterfaces.implementsTR<
268
+ plugins.typedrequestInterfaces.ITypedRequest,
269
+ IReq_Cloudly_Relay_GetClusterCorestoreInventory
270
+ > {
271
+ method: 'getClusterCorestoreInventory';
272
+ request: {
273
+ nodeId: string;
274
+ };
275
+ response: {
276
+ inventory: IClusterCorestoreInventory;
277
+ };
278
+ }
279
+
280
+ /** Superseded by `getClusterCorestoreInventory`, which names the node. */
98
281
  export interface IReq_Cloudly_Coreflow_GetCorestoreInventory
99
282
  extends plugins.typedrequestInterfaces.implementsTR<
100
283
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -203,6 +203,41 @@ export interface IReq_Any_Cloudly_DeleteNodeById extends plugins.typedrequestInt
203
203
  };
204
204
  }
205
205
 
206
+ /**
207
+ * An administrator states where one node's Corestore control API answers.
208
+ *
209
+ * Corestore is node-local, so one relay serving a whole cluster must be told per node where to
210
+ * dial; the cluster-local alias every component used while Docker Swarm resolved it is exactly
211
+ * what stops resolving. The endpoint is control-plane state Cloudly owns and the relay reads,
212
+ * so an administrator sets it here rather than a relay discovering it.
213
+ *
214
+ * Cloudly validates the endpoint with `validateClusterNodeCorestore` before it writes, takes the
215
+ * node's own real-write fence inside the writing transaction so a concurrent deletion conflicts
216
+ * instead of leaving an endpoint behind for a node that is gone, and pushes the cluster
217
+ * configuration afterwards so every connected relay learns the change without reconnecting.
218
+ *
219
+ * `endpoint: null` clears it, which is the same fact as never having set one: the node runs no
220
+ * Corestore the relay may reach, and an inventory for it answers
221
+ * `errorCode: 'CORESTORE_ENDPOINT_UNKNOWN'` rather than guessing a hostname. The endpoint says
222
+ * where, never with what — no credential travels in this request or in the node document.
223
+ */
224
+ export interface IReq_Any_Cloudly_SetClusterNodeCorestoreEndpoint extends plugins.typedrequestInterfaces.implementsTR<
225
+ plugins.typedrequestInterfaces.ITypedRequest,
226
+ IReq_Any_Cloudly_SetClusterNodeCorestoreEndpoint
227
+ > {
228
+ method: 'setClusterNodeCorestoreEndpoint';
229
+ request: {
230
+ identity: IIdentity;
231
+ nodeId: string;
232
+ /** A canonical `http:` or `https:` origin, or `null` to state that this node has none. */
233
+ endpoint: string | null;
234
+ };
235
+ response: {
236
+ /** The node as it now stands, so a caller needs no second read to see what it set. */
237
+ node: IClusterNode;
238
+ };
239
+ }
240
+
206
241
  export interface IRequest_Spark_Cloudly_SendHeartbeat extends plugins.typedrequestInterfaces.implementsTR<
207
242
  plugins.typedrequestInterfaces.ITypedRequest,
208
243
  IRequest_Spark_Cloudly_SendHeartbeat
@@ -23,6 +23,42 @@ extends plugins.typedrequestInterfaces.implementsTR<
23
23
  };
24
24
  }
25
25
 
26
+ /**
27
+ * One platform binding and the node its Corestore resources live on.
28
+ *
29
+ * Corestore is node-local, so a binding without a node is a database nobody can
30
+ * place: while every cluster ran one coreflow per node, "this node" was whoever
31
+ * answered, and a cluster relay serving every node has no such implicit answer.
32
+ * The placement is Cloudly's, derived from where the service is assigned.
33
+ */
34
+ export interface IClusterPlatformBindingPlacement {
35
+ nodeId: string;
36
+ binding: IPlatformBinding;
37
+ }
38
+
39
+ /**
40
+ * The desired platform state for the relay's own cluster, node by node.
41
+ *
42
+ * Its session decides which cluster it speaks for, so the request carries no
43
+ * cluster of its own. It replaces `getPlatformDesiredState` for a cluster
44
+ * relay: the same capabilities and provider configs, and bindings that state
45
+ * where they belong instead of leaving it to be inferred.
46
+ */
47
+ export interface IReq_Relay_Cloudly_GetClusterPlatformDesiredState
48
+ extends plugins.typedrequestInterfaces.implementsTR<
49
+ plugins.typedrequestInterfaces.ITypedRequest,
50
+ IReq_Relay_Cloudly_GetClusterPlatformDesiredState
51
+ > {
52
+ method: 'getClusterPlatformDesiredState';
53
+ request: Record<string, never>;
54
+ response: {
55
+ capabilities: IPlatformCapability[];
56
+ providerConfigs: IPlatformProviderConfig[];
57
+ /** Sorted by node, then by binding id, so two reads compare directly. */
58
+ bindings: IClusterPlatformBindingPlacement[];
59
+ };
60
+ }
61
+
26
62
  export interface IReq_Any_Cloudly_GetPlatformCapabilities
27
63
  extends plugins.typedrequestInterfaces.implementsTR<
28
64
  plugins.typedrequestInterfaces.ITypedRequest,