@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.
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
2
2
  import type { IArchivePruneResult, IArchiveRetentionPolicy, IBackupArchiveManifest, IBackupArchiveObject, IBackupRecord, IBackupReplicationResult, IIsolatedRestoreNodeState, IIsolatedRestoreRecord, TBackupSnapshot, TIsolatedRestoreResourceMapping } from '../data/backup.js';
3
3
  import type { IService } from '../data/service.js';
4
4
  import type { IIdentity, IIdentityCredential } from '../data/user.js';
5
+ import type { ICorestoreDatabaseClosureDescriptor } from '../runtime.corestore.relay.js';
5
6
  export interface IReq_Any_Cloudly_CreateServiceBackup extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_CreateServiceBackup> {
6
7
  method: 'createServiceBackup';
7
8
  request: {
@@ -48,6 +49,15 @@ export interface IReq_Any_Cloudly_RestoreServiceBackup extends plugins.typedrequ
48
49
  backup: IBackupRecord;
49
50
  };
50
51
  }
52
+ /**
53
+ * Superseded by `backupClusterService`, which names the node.
54
+ *
55
+ * This method was dispatched at the coreflow that had tagged itself with the
56
+ * node's hostname; a cluster relay authors no tags and serves every node of its
57
+ * cluster, so the node it should act on can no longer be inferred from which
58
+ * connection answered. It is retired together with the tag in the coordinated
59
+ * major.
60
+ */
51
61
  export interface IReq_Cloudly_Coreflow_ExecuteServiceBackup extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Coreflow_ExecuteServiceBackup> {
52
62
  method: 'executeServiceBackup';
53
63
  request: {
@@ -63,6 +73,7 @@ export interface IReq_Cloudly_Coreflow_ExecuteServiceBackup extends plugins.type
63
73
  replication?: IBackupReplicationResult;
64
74
  };
65
75
  }
76
+ /** Superseded by `restoreClusterService`; see `executeServiceBackup`. */
66
77
  export interface IReq_Cloudly_Coreflow_ExecuteServiceRestore extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Coreflow_ExecuteServiceRestore> {
67
78
  method: 'executeServiceRestore';
68
79
  request: {
@@ -90,6 +101,14 @@ export interface IReq_Coreflow_Cloudly_PrepareBackupReplication extends plugins.
90
101
  missingObjects: IBackupArchiveObject[];
91
102
  };
92
103
  }
104
+ /**
105
+ * Retiring: `contentsBase64` carries a whole archive object inside one JSON
106
+ * envelope, which costs a third of the payload in encoding and bounds a backup
107
+ * by the message ceiling rather than by the transport. `backupServiceDatabaseClosure`
108
+ * streams the same bytes as a VirtualStream. Removal is the coordinated major,
109
+ * once every relay and every Cloudly speak the streamed contract; Corestore
110
+ * already documents its repository-wide archive object routes as legacy.
111
+ */
93
112
  export interface IReq_Coreflow_Cloudly_UploadBackupArchiveObject extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Coreflow_Cloudly_UploadBackupArchiveObject> {
94
113
  method: 'uploadBackupArchiveObject';
95
114
  request: {
@@ -123,6 +142,7 @@ export interface IReq_Coreflow_Cloudly_GetBackupArchiveManifest extends plugins.
123
142
  manifest: IBackupArchiveManifest;
124
143
  };
125
144
  }
145
+ /** Retiring with `uploadBackupArchiveObject`; `restoreServiceDatabaseClosure` replaces it. */
126
146
  export interface IReq_Coreflow_Cloudly_DownloadBackupArchiveObject extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Coreflow_Cloudly_DownloadBackupArchiveObject> {
127
147
  method: 'downloadBackupArchiveObject';
128
148
  request: {
@@ -135,6 +155,7 @@ export interface IReq_Coreflow_Cloudly_DownloadBackupArchiveObject extends plugi
135
155
  contentsBase64: string;
136
156
  };
137
157
  }
158
+ /** Superseded by `pruneClusterNodeArchive`; see `executeServiceBackup`. */
138
159
  export interface IReq_Cloudly_Coreflow_PruneNodeArchive extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Coreflow_PruneNodeArchive> {
139
160
  method: 'coreflowPruneNodeArchive';
140
161
  request: {
@@ -146,6 +167,127 @@ export interface IReq_Cloudly_Coreflow_PruneNodeArchive extends plugins.typedreq
146
167
  result?: IArchivePruneResult;
147
168
  };
148
169
  }
170
+ /**
171
+ * Backs up one service on one exact node of this cluster.
172
+ *
173
+ * The cluster relay serves every node of its cluster over one session, so the
174
+ * node is a request field: the connection that answers proves which cluster is
175
+ * speaking, never which node is meant. Everything else is what
176
+ * `executeServiceBackup` carried, so a caller moving to this method changes its
177
+ * routing and nothing about its backup.
178
+ */
179
+ export interface IReq_Cloudly_Relay_BackupClusterService extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Relay_BackupClusterService> {
180
+ method: 'backupClusterService';
181
+ request: {
182
+ /** The cluster node whose Corestore holds this service's data. */
183
+ nodeId: string;
184
+ backupId: string;
185
+ service: IService;
186
+ tags?: Record<string, string>;
187
+ replication?: {
188
+ enabled: boolean;
189
+ };
190
+ };
191
+ response: {
192
+ snapshots: TBackupSnapshot[];
193
+ replication?: IBackupReplicationResult;
194
+ };
195
+ }
196
+ /** Restores one service on one exact node of this cluster; see `backupClusterService`. */
197
+ export interface IReq_Cloudly_Relay_RestoreClusterService extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Relay_RestoreClusterService> {
198
+ method: 'restoreClusterService';
199
+ request: {
200
+ /** The cluster node whose Corestore receives the restore. */
201
+ nodeId: string;
202
+ backupId: string;
203
+ service: IService;
204
+ snapshots: TBackupSnapshot[];
205
+ clear?: boolean;
206
+ resourceTypes?: Array<TBackupSnapshot['type']>;
207
+ replication?: {
208
+ enabled: boolean;
209
+ };
210
+ };
211
+ response: {
212
+ restored: TBackupSnapshot[];
213
+ };
214
+ }
215
+ /**
216
+ * Prunes the archive of one exact node of this cluster.
217
+ *
218
+ * `found` stays false when that node has no archive to prune, which is a fact
219
+ * about the node rather than a failure of the relay.
220
+ */
221
+ export interface IReq_Cloudly_Relay_PruneClusterNodeArchive extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Relay_PruneClusterNodeArchive> {
222
+ method: 'pruneClusterNodeArchive';
223
+ request: {
224
+ /** The cluster node whose archive is pruned. */
225
+ nodeId: string;
226
+ retention: IArchiveRetentionPolicy;
227
+ dryRun?: boolean;
228
+ };
229
+ response: {
230
+ found: boolean;
231
+ result?: IArchivePruneResult;
232
+ };
233
+ }
234
+ /**
235
+ * The relay carries one service database closure to Cloudly.
236
+ *
237
+ * Relay-initiated, like every other byte a cluster sends: a cluster is
238
+ * outbound-only, so Cloudly cannot pull from it. The relay exports the closure
239
+ * from its node's Corestore and sends those exact bytes on its own session.
240
+ *
241
+ * `accepted` permits sending; it never confirms storage. The stream's own
242
+ * acceptance receipt, observed through `completion`, is what confirms that
243
+ * Cloudly durably holds the closure — exactly as `pushImageVersion` works.
244
+ *
245
+ * No identity field: the cluster is the one the verified machine identity on
246
+ * the relay's own session names, exactly as `getClusterRouteTable` and
247
+ * `getClusterPlatformDesiredState` work. A request-level identity would be a
248
+ * second authority for the same fact, and the weaker of the two.
249
+ */
250
+ export interface IReq_Relay_Cloudly_BackupServiceDatabaseClosure extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Relay_Cloudly_BackupServiceDatabaseClosure> {
251
+ method: 'backupServiceDatabaseClosure';
252
+ request: {
253
+ backupId: string;
254
+ serviceId: string;
255
+ /** The cluster node this closure was exported from. */
256
+ nodeId: string;
257
+ /** Exact bytes and Corestore's receipt for them; validated before any byte moves. */
258
+ descriptor: ICorestoreDatabaseClosureDescriptor;
259
+ /** Requester sends closure bytes; the TypedHandler receives the opposite endpoint. */
260
+ closure: plugins.typedrequestInterfaces.TVirtualStream<'send'>;
261
+ };
262
+ response: {
263
+ accepted: boolean;
264
+ };
265
+ }
266
+ /**
267
+ * The relay fetches one service database closure back from Cloudly.
268
+ *
269
+ * The descriptor arrives with the stream rather than after it, because
270
+ * Corestore's restore needs the exact byte length and receipt before the first
271
+ * byte is written: the relay opens its Corestore request from this descriptor
272
+ * and only then drains the stream into it.
273
+ *
274
+ * No identity field, for the same reason as `backupServiceDatabaseClosure`: the
275
+ * relay's own session names the cluster.
276
+ */
277
+ export interface IReq_Relay_Cloudly_RestoreServiceDatabaseClosure extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Relay_Cloudly_RestoreServiceDatabaseClosure> {
278
+ method: 'restoreServiceDatabaseClosure';
279
+ request: {
280
+ backupId: string;
281
+ serviceId: string;
282
+ /** The cluster node the closure is restored onto. */
283
+ nodeId: string;
284
+ };
285
+ response: {
286
+ descriptor: ICorestoreDatabaseClosureDescriptor;
287
+ /** Requester receives closure bytes; the TypedHandler returns a sender endpoint. */
288
+ closure: plugins.typedrequestInterfaces.TVirtualStream<'receive'>;
289
+ };
290
+ }
149
291
  /**
150
292
  * Creates a restore in a new non-runnable data-plane scratch namespace.
151
293
  * Cloudly derives clusterId and sourceServiceId from the authoritative backup,
@@ -225,6 +367,18 @@ export interface IReq_Any_Cloudly_GetIsolatedRestoreById extends plugins.typedre
225
367
  export interface IReq_Cloudly_Coreflow_PrepareIsolatedRestore extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Coreflow_PrepareIsolatedRestore> {
226
368
  method: 'coreflowPrepareIsolatedRestore';
227
369
  request: {
370
+ /**
371
+ * The compact RS256 restore grant Cloudly signed for exactly this
372
+ * operation, carried untouched to Corestore.
373
+ *
374
+ * Corestore accepts no other authority on an isolated-restore route: the
375
+ * grant names the operation, expires within minutes, and binds the cluster,
376
+ * the node, the canonical resource mappings and the archive manifest, so a
377
+ * relay can carry it but can neither widen it nor mint one. Sensitive
378
+ * ephemeral input: exclude the complete request from hooks, logs and
379
+ * journals on every hop.
380
+ */
381
+ restoreGrant: string;
228
382
  clusterId: string;
229
383
  targetNodeName: string;
230
384
  restoreId: string;
@@ -240,10 +394,55 @@ export interface IReq_Cloudly_Coreflow_PrepareIsolatedRestore extends plugins.ty
240
394
  state: IIsolatedRestoreNodeState;
241
395
  };
242
396
  }
397
+ /**
398
+ * Cloudly stages one archive object of an isolated restore on the relay's node.
399
+ *
400
+ * Cloudly holds the source backup, so these bytes travel Cloudly → relay and
401
+ * the requester is the sender. One object per call, as the archive manifest
402
+ * lists it: Corestore writes staged objects under a `write-object` grant, one
403
+ * bounded chunk at a time, and the relay is what turns this stream into those
404
+ * chunks. The object descriptor is the manifest entry itself — path, exact size
405
+ * and digest — so the relay can prove the complete object before it counts as
406
+ * written, and so a replayed stage settles on the same bytes.
407
+ */
408
+ export interface IReq_Cloudly_Relay_StageIsolatedRestoreArchive extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Relay_StageIsolatedRestoreArchive> {
409
+ method: 'stageIsolatedRestoreArchive';
410
+ request: {
411
+ /**
412
+ * The compact RS256 restore grant for the `write-object` operation, carried
413
+ * untouched to Corestore. Sensitive ephemeral input, like every other
414
+ * grant on these routes.
415
+ */
416
+ restoreGrant: string;
417
+ clusterId: string;
418
+ targetNodeName: string;
419
+ restoreId: string;
420
+ expectedRevision: number;
421
+ /** The manifest entry these bytes complete: `path`, `size`, `sha256`. */
422
+ object: IBackupArchiveObject;
423
+ /** Requester sends the object bytes; the TypedHandler receives them. */
424
+ archive: plugins.typedrequestInterfaces.TVirtualStream<'send'>;
425
+ };
426
+ response: {
427
+ state: IIsolatedRestoreNodeState;
428
+ };
429
+ }
243
430
  /** Read-only status for one exact cluster/node/restore routing key. */
244
431
  export interface IReq_Cloudly_Coreflow_GetIsolatedRestoreStatus extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Coreflow_GetIsolatedRestoreStatus> {
245
432
  method: 'coreflowGetIsolatedRestoreStatus';
246
433
  request: {
434
+ /**
435
+ * The compact RS256 restore grant Cloudly signed for exactly this
436
+ * operation, carried untouched to Corestore.
437
+ *
438
+ * Corestore accepts no other authority on an isolated-restore route: the
439
+ * grant names the operation, expires within minutes, and binds the cluster,
440
+ * the node, the canonical resource mappings and the archive manifest, so a
441
+ * relay can carry it but can neither widen it nor mint one. Sensitive
442
+ * ephemeral input: exclude the complete request from hooks, logs and
443
+ * journals on every hop.
444
+ */
445
+ restoreGrant: string;
247
446
  clusterId: string;
248
447
  targetNodeName: string;
249
448
  restoreId: string;
@@ -256,6 +455,18 @@ export interface IReq_Cloudly_Coreflow_GetIsolatedRestoreStatus extends plugins.
256
455
  export interface IReq_Cloudly_Coreflow_ExecuteIsolatedRestore extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Coreflow_ExecuteIsolatedRestore> {
257
456
  method: 'coreflowExecuteIsolatedRestore';
258
457
  request: {
458
+ /**
459
+ * The compact RS256 restore grant Cloudly signed for exactly this
460
+ * operation, carried untouched to Corestore.
461
+ *
462
+ * Corestore accepts no other authority on an isolated-restore route: the
463
+ * grant names the operation, expires within minutes, and binds the cluster,
464
+ * the node, the canonical resource mappings and the archive manifest, so a
465
+ * relay can carry it but can neither widen it nor mint one. Sensitive
466
+ * ephemeral input: exclude the complete request from hooks, logs and
467
+ * journals on every hop.
468
+ */
469
+ restoreGrant: string;
259
470
  clusterId: string;
260
471
  targetNodeName: string;
261
472
  restoreId: string;
@@ -272,6 +483,18 @@ export interface IReq_Cloudly_Coreflow_ExecuteIsolatedRestore extends plugins.ty
272
483
  export interface IReq_Cloudly_Coreflow_CleanupIsolatedRestore extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Coreflow_CleanupIsolatedRestore> {
273
484
  method: 'coreflowCleanupIsolatedRestore';
274
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;
275
498
  clusterId: string;
276
499
  targetNodeName: string;
277
500
  restoreId: string;
@@ -19,6 +19,10 @@ export interface ICorestoreServiceResources {
19
19
  /**
20
20
  * Corestore inventory of one node (each coreflow node runs its own corestore;
21
21
  * the data is node-local, which is what makes node placement matter).
22
+ *
23
+ * Superseded by `IClusterCorestoreInventory`, which names the node by its
24
+ * cluster-document id instead of by the Swarm hostnames a connection tagged
25
+ * itself with.
22
26
  */
23
27
  export interface ICorestoreNodeInventory {
24
28
  /** Swarm node hostnames of the reporting coreflow. */
@@ -63,6 +67,35 @@ export interface ICoreflowInventoryRegistration {
63
67
  reporterSessionId: string;
64
68
  registeredAt: number;
65
69
  }
70
+ /**
71
+ * What one node's Corestore holds, as the cluster relay read it.
72
+ *
73
+ * Node identity is the cluster document's node id: one relay serves every node
74
+ * of its cluster, so which node an inventory describes is stated, never derived
75
+ * from which connection answered. An unreachable node names why — `errorCode`
76
+ * is required whenever `reachable` is false and refused whenever it is true —
77
+ * because a node whose Corestore cannot be reached is a fact the control plane
78
+ * needs, and a silent empty inventory would read as "nothing provisioned",
79
+ * which is the opposite.
80
+ */
81
+ export interface IClusterCorestoreInventory {
82
+ nodeId: string;
83
+ checkedAt: number;
84
+ reachable: boolean;
85
+ errorCode?: 'CORESTORE_ENDPOINT_UNKNOWN' | 'CORESTORE_UNREACHABLE';
86
+ services: ICorestoreServiceResources[];
87
+ }
88
+ /**
89
+ * Validates one cluster Corestore inventory and names every reason it is
90
+ * refused.
91
+ *
92
+ * Shape only. Whether that node belongs to the cluster, and whether a resource
93
+ * should exist, are decisions for the control plane that asked; an inventory
94
+ * that passes here has claimed neither. A reachable inventory that names an
95
+ * error, and an unreachable one that lists resources, are both refused: an
96
+ * answer has to be one of the two things it can be.
97
+ */
98
+ export declare const validateClusterCorestoreInventory: (valueArg: unknown, pathArg?: string) => string[];
66
99
  export interface IReq_Any_Cloudly_GetCorestoreInventory extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_GetCorestoreInventory> {
67
100
  method: 'getCorestoreInventory';
68
101
  request: {
@@ -70,6 +103,30 @@ export interface IReq_Any_Cloudly_GetCorestoreInventory extends plugins.typedreq
70
103
  };
71
104
  response: ICorestoreInventoryReport;
72
105
  }
106
+ /**
107
+ * Cloudly asks the relay what one exact node of its cluster holds.
108
+ *
109
+ * The node is a request field because one relay serves them all: the connection
110
+ * that answers proves the cluster, never the node. A greenfield cluster answers
111
+ * with an empty `services` list and `reachable: true`, which is the proof that
112
+ * a namespace is blank — distinct from `reachable: false`, which proves
113
+ * nothing at all.
114
+ *
115
+ * Named apart from `getCorestoreInventory` on purpose: that method is Cloudly's
116
+ * own operator-facing read and lives on the other end of this socket. One name
117
+ * on both ends would leave direction as the only thing telling two different
118
+ * bodies apart.
119
+ */
120
+ export interface IReq_Cloudly_Relay_GetClusterCorestoreInventory extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Relay_GetClusterCorestoreInventory> {
121
+ method: 'getClusterCorestoreInventory';
122
+ request: {
123
+ nodeId: string;
124
+ };
125
+ response: {
126
+ inventory: IClusterCorestoreInventory;
127
+ };
128
+ }
129
+ /** Superseded by `getClusterCorestoreInventory`, which names the node. */
73
130
  export interface IReq_Cloudly_Coreflow_GetCorestoreInventory extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_Coreflow_GetCorestoreInventory> {
74
131
  method: 'coreflowGetCorestoreInventory';
75
132
  request: Record<string, never>;
@@ -1,2 +1,103 @@
1
1
  import * as plugins from '../plugins.js';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29yZXN0b3JlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvcmVxdWVzdHMvY29yZXN0b3JlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sZUFBZSxDQUFDIn0=
2
+ const clusterCorestoreInventoryIdentifierPattern = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
3
+ const isClusterCorestoreRecord = (valueArg) => (Boolean(valueArg)
4
+ && typeof valueArg === 'object'
5
+ && !Array.isArray(valueArg)
6
+ && (Object.getPrototypeOf(valueArg) === Object.prototype
7
+ || Object.getPrototypeOf(valueArg) === null));
8
+ const hasClusterCorestoreKeys = (valueArg, keysArg, optionalKeysArg = []) => {
9
+ const actualKeys = Object.keys(valueArg);
10
+ return keysArg.every((keyArg) => actualKeys.includes(keyArg))
11
+ && actualKeys.every((keyArg) => keysArg.includes(keyArg) || optionalKeysArg.includes(keyArg));
12
+ };
13
+ const validateClusterCorestoreServiceResources = (valueArg, pathArg) => {
14
+ if (!isClusterCorestoreRecord(valueArg)
15
+ || !hasClusterCorestoreKeys(valueArg, ['serviceId', 'resources', 'updatedAt'], ['serviceName'])) {
16
+ return [`${pathArg} must use its exact schema`];
17
+ }
18
+ const errors = [];
19
+ if (typeof valueArg.serviceId !== 'string'
20
+ || !clusterCorestoreInventoryIdentifierPattern.test(valueArg.serviceId)) {
21
+ errors.push(`${pathArg} serviceId must be a bounded identifier`);
22
+ }
23
+ if (!Number.isSafeInteger(valueArg.updatedAt) || valueArg.updatedAt < 0) {
24
+ errors.push(`${pathArg} updatedAt must be a non-negative safe integer`);
25
+ }
26
+ if (!Array.isArray(valueArg.resources)) {
27
+ errors.push(`${pathArg} resources must be an array`);
28
+ return errors;
29
+ }
30
+ for (const [index, resource] of valueArg.resources.entries()) {
31
+ const resourcePath = `${pathArg} resources[${index}]`;
32
+ if (!isClusterCorestoreRecord(resource)
33
+ || !hasClusterCorestoreKeys(resource, ['capability', 'provider', 'resourceName', 'createdAt'])) {
34
+ errors.push(`${resourcePath} must use its exact schema`);
35
+ continue;
36
+ }
37
+ if (resource.capability !== 'database' && resource.capability !== 'objectstorage') {
38
+ errors.push(`${resourcePath} capability must be database or objectstorage`);
39
+ }
40
+ if (typeof resource.provider !== 'string' || resource.provider.length === 0) {
41
+ errors.push(`${resourcePath} provider must be a non-empty string`);
42
+ }
43
+ if (typeof resource.resourceName !== 'string' || resource.resourceName.length === 0) {
44
+ errors.push(`${resourcePath} resourceName must be a non-empty string`);
45
+ }
46
+ if (!Number.isSafeInteger(resource.createdAt) || resource.createdAt < 0) {
47
+ errors.push(`${resourcePath} createdAt must be a non-negative safe integer`);
48
+ }
49
+ }
50
+ return errors;
51
+ };
52
+ /**
53
+ * Validates one cluster Corestore inventory and names every reason it is
54
+ * refused.
55
+ *
56
+ * Shape only. Whether that node belongs to the cluster, and whether a resource
57
+ * should exist, are decisions for the control plane that asked; an inventory
58
+ * that passes here has claimed neither. A reachable inventory that names an
59
+ * error, and an unreachable one that lists resources, are both refused: an
60
+ * answer has to be one of the two things it can be.
61
+ */
62
+ export const validateClusterCorestoreInventory = (valueArg, pathArg = 'cluster corestore inventory') => {
63
+ if (!isClusterCorestoreRecord(valueArg)
64
+ || !hasClusterCorestoreKeys(valueArg, ['nodeId', 'checkedAt', 'reachable', 'services'], ['errorCode'])) {
65
+ return [`${pathArg} must use its exact schema`];
66
+ }
67
+ const errors = [];
68
+ if (typeof valueArg.nodeId !== 'string'
69
+ || !clusterCorestoreInventoryIdentifierPattern.test(valueArg.nodeId)) {
70
+ errors.push(`${pathArg} nodeId must be a bounded identifier`);
71
+ }
72
+ if (!Number.isSafeInteger(valueArg.checkedAt) || valueArg.checkedAt < 0) {
73
+ errors.push(`${pathArg} checkedAt must be a non-negative safe integer`);
74
+ }
75
+ if (typeof valueArg.reachable !== 'boolean') {
76
+ errors.push(`${pathArg} reachable must be a boolean`);
77
+ }
78
+ if (valueArg.errorCode !== undefined
79
+ && valueArg.errorCode !== 'CORESTORE_ENDPOINT_UNKNOWN'
80
+ && valueArg.errorCode !== 'CORESTORE_UNREACHABLE') {
81
+ errors.push(`${pathArg} errorCode must name why the node could not be read`);
82
+ }
83
+ if (valueArg.reachable === true && valueArg.errorCode !== undefined) {
84
+ errors.push(`${pathArg} must not carry an errorCode while it reports the node reachable`);
85
+ }
86
+ if (valueArg.reachable === false && valueArg.errorCode === undefined) {
87
+ // An unreachable node with no reason reads like a node holding nothing,
88
+ // which is the one confusion this shape exists to prevent.
89
+ errors.push(`${pathArg} must name why the node could not be read while it reports it unreachable`);
90
+ }
91
+ if (!Array.isArray(valueArg.services)) {
92
+ errors.push(`${pathArg} services must be an array`);
93
+ return errors;
94
+ }
95
+ if (valueArg.reachable === false && valueArg.services.length > 0) {
96
+ errors.push(`${pathArg} must list no services while it reports the node unreachable`);
97
+ }
98
+ for (const [index, service] of valueArg.services.entries()) {
99
+ errors.push(...validateClusterCorestoreServiceResources(service, `${pathArg} services[${index}]`));
100
+ }
101
+ return errors;
102
+ };
103
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29yZXN0b3JlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvcmVxdWVzdHMvY29yZXN0b3JlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sZUFBZSxDQUFDO0FBNEd6QyxNQUFNLDBDQUEwQyxHQUFHLHFDQUFxQyxDQUFDO0FBRXpGLE1BQU0sd0JBQXdCLEdBQUcsQ0FDL0IsUUFBaUIsRUFDb0IsRUFBRSxDQUFDLENBQ3hDLE9BQU8sQ0FBQyxRQUFRLENBQUM7T0FDZCxPQUFPLFFBQVEsS0FBSyxRQUFRO09BQzVCLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUM7T0FDeEIsQ0FBQyxNQUFNLENBQUMsY0FBYyxDQUFDLFFBQVEsQ0FBQyxLQUFLLE1BQU0sQ0FBQyxTQUFTO1dBQ25ELE1BQU0sQ0FBQyxjQUFjLENBQUMsUUFBUSxDQUFDLEtBQUssSUFBSSxDQUFDLENBQy9DLENBQUM7QUFFRixNQUFNLHVCQUF1QixHQUFHLENBQzlCLFFBQWlDLEVBQ2pDLE9BQTBCLEVBQzFCLGtCQUFxQyxFQUFFLEVBQzlCLEVBQUU7SUFDWCxNQUFNLFVBQVUsR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ3pDLE9BQU8sT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsQ0FBQztXQUN4RCxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLE1BQU0sQ0FBQyxJQUFJLGVBQWUsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQztBQUNsRyxDQUFDLENBQUM7QUFFRixNQUFNLHdDQUF3QyxHQUFHLENBQy9DLFFBQWlCLEVBQ2pCLE9BQWUsRUFDTCxFQUFFO0lBQ1osSUFBSSxDQUFDLHdCQUF3QixDQUFDLFFBQVEsQ0FBQztXQUNsQyxDQUFDLHVCQUF1QixDQUN6QixRQUFRLEVBQ1IsQ0FBQyxXQUFXLEVBQUUsV0FBVyxFQUFFLFdBQVcsQ0FBQyxFQUN2QyxDQUFDLGFBQWEsQ0FBQyxDQUNoQixFQUFFLENBQUM7UUFDSixPQUFPLENBQUMsR0FBRyxPQUFPLDRCQUE0QixDQUFDLENBQUM7SUFDbEQsQ0FBQztJQUNELE1BQU0sTUFBTSxHQUFhLEVBQUUsQ0FBQztJQUM1QixJQUFJLE9BQU8sUUFBUSxDQUFDLFNBQVMsS0FBSyxRQUFRO1dBQ3JDLENBQUMsMENBQTBDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDO1FBQzFFLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLHlDQUF5QyxDQUFDLENBQUM7SUFDbkUsQ0FBQztJQUNELElBQUksQ0FBQyxNQUFNLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsSUFBSyxRQUFRLENBQUMsU0FBb0IsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUNwRixNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxnREFBZ0QsQ0FBQyxDQUFDO0lBQzFFLENBQUM7SUFDRCxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztRQUN2QyxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyw2QkFBNkIsQ0FBQyxDQUFDO1FBQ3JELE9BQU8sTUFBTSxDQUFDO0lBQ2hCLENBQUM7SUFDRCxLQUFLLE1BQU0sQ0FBQyxLQUFLLEVBQUUsUUFBUSxDQUFDLElBQUksUUFBUSxDQUFDLFNBQVMsQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDO1FBQzdELE1BQU0sWUFBWSxHQUFHLEdBQUcsT0FBTyxjQUFjLEtBQUssR0FBRyxDQUFDO1FBQ3RELElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxRQUFRLENBQUM7ZUFDbEMsQ0FBQyx1QkFBdUIsQ0FDekIsUUFBUSxFQUNSLENBQUMsWUFBWSxFQUFFLFVBQVUsRUFBRSxjQUFjLEVBQUUsV0FBVyxDQUFDLENBQ3hELEVBQUUsQ0FBQztZQUNKLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxZQUFZLDRCQUE0QixDQUFDLENBQUM7WUFDekQsU0FBUztRQUNYLENBQUM7UUFDRCxJQUFJLFFBQVEsQ0FBQyxVQUFVLEtBQUssVUFBVSxJQUFJLFFBQVEsQ0FBQyxVQUFVLEtBQUssZUFBZSxFQUFFLENBQUM7WUFDbEYsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLFlBQVksK0NBQStDLENBQUMsQ0FBQztRQUM5RSxDQUFDO1FBQ0QsSUFBSSxPQUFPLFFBQVEsQ0FBQyxRQUFRLEtBQUssUUFBUSxJQUFJLFFBQVEsQ0FBQyxRQUFRLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1lBQzVFLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxZQUFZLHNDQUFzQyxDQUFDLENBQUM7UUFDckUsQ0FBQztRQUNELElBQUksT0FBTyxRQUFRLENBQUMsWUFBWSxLQUFLLFFBQVEsSUFBSSxRQUFRLENBQUMsWUFBWSxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztZQUNwRixNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsWUFBWSwwQ0FBMEMsQ0FBQyxDQUFDO1FBQ3pFLENBQUM7UUFDRCxJQUFJLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLElBQUssUUFBUSxDQUFDLFNBQW9CLEdBQUcsQ0FBQyxFQUFFLENBQUM7WUFDcEYsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLFlBQVksZ0RBQWdELENBQUMsQ0FBQztRQUMvRSxDQUFDO0lBQ0gsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUMsQ0FBQztBQUVGOzs7Ozs7Ozs7R0FTRztBQUNILE1BQU0sQ0FBQyxNQUFNLGlDQUFpQyxHQUFHLENBQy9DLFFBQWlCLEVBQ2pCLE9BQU8sR0FBRyw2QkFBNkIsRUFDN0IsRUFBRTtJQUNaLElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxRQUFRLENBQUM7V0FDbEMsQ0FBQyx1QkFBdUIsQ0FDekIsUUFBUSxFQUNSLENBQUMsUUFBUSxFQUFFLFdBQVcsRUFBRSxXQUFXLEVBQUUsVUFBVSxDQUFDLEVBQ2hELENBQUMsV0FBVyxDQUFDLENBQ2QsRUFBRSxDQUFDO1FBQ0osT0FBTyxDQUFDLEdBQUcsT0FBTyw0QkFBNEIsQ0FBQyxDQUFDO0lBQ2xELENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBYSxFQUFFLENBQUM7SUFDNUIsSUFBSSxPQUFPLFFBQVEsQ0FBQyxNQUFNLEtBQUssUUFBUTtXQUNsQyxDQUFDLDBDQUEwQyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUN2RSxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxzQ0FBc0MsQ0FBQyxDQUFDO0lBQ2hFLENBQUM7SUFDRCxJQUFJLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLElBQUssUUFBUSxDQUFDLFNBQW9CLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDcEYsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sZ0RBQWdELENBQUMsQ0FBQztJQUMxRSxDQUFDO0lBQ0QsSUFBSSxPQUFPLFFBQVEsQ0FBQyxTQUFTLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDNUMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sOEJBQThCLENBQUMsQ0FBQztJQUN4RCxDQUFDO0lBQ0QsSUFBSSxRQUFRLENBQUMsU0FBUyxLQUFLLFNBQVM7V0FDL0IsUUFBUSxDQUFDLFNBQVMsS0FBSyw0QkFBNEI7V0FDbkQsUUFBUSxDQUFDLFNBQVMsS0FBSyx1QkFBdUIsRUFBRSxDQUFDO1FBQ3BELE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLHFEQUFxRCxDQUFDLENBQUM7SUFDL0UsQ0FBQztJQUNELElBQUksUUFBUSxDQUFDLFNBQVMsS0FBSyxJQUFJLElBQUksUUFBUSxDQUFDLFNBQVMsS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUNwRSxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxrRUFBa0UsQ0FBQyxDQUFDO0lBQzVGLENBQUM7SUFDRCxJQUFJLFFBQVEsQ0FBQyxTQUFTLEtBQUssS0FBSyxJQUFJLFFBQVEsQ0FBQyxTQUFTLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDckUsd0VBQXdFO1FBQ3hFLDJEQUEyRDtRQUMzRCxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTywyRUFBMkUsQ0FBQyxDQUFDO0lBQ3JHLENBQUM7SUFDRCxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQztRQUN0QyxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyw0QkFBNEIsQ0FBQyxDQUFDO1FBQ3BELE9BQU8sTUFBTSxDQUFDO0lBQ2hCLENBQUM7SUFDRCxJQUFJLFFBQVEsQ0FBQyxTQUFTLEtBQUssS0FBSyxJQUFJLFFBQVEsQ0FBQyxRQUFRLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ2pFLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLDhEQUE4RCxDQUFDLENBQUM7SUFDeEYsQ0FBQztJQUNELEtBQUssTUFBTSxDQUFDLEtBQUssRUFBRSxPQUFPLENBQUMsSUFBSSxRQUFRLENBQUMsUUFBUSxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUM7UUFDM0QsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLHdDQUF3QyxDQUFDLE9BQU8sRUFBRSxHQUFHLE9BQU8sYUFBYSxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDckcsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUMsQ0FBQyJ9
@@ -150,6 +150,37 @@ export interface IReq_Any_Cloudly_DeleteNodeById extends plugins.typedrequestInt
150
150
  ok: boolean;
151
151
  };
152
152
  }
153
+ /**
154
+ * An administrator states where one node's Corestore control API answers.
155
+ *
156
+ * Corestore is node-local, so one relay serving a whole cluster must be told per node where to
157
+ * dial; the cluster-local alias every component used while Docker Swarm resolved it is exactly
158
+ * what stops resolving. The endpoint is control-plane state Cloudly owns and the relay reads,
159
+ * so an administrator sets it here rather than a relay discovering it.
160
+ *
161
+ * Cloudly validates the endpoint with `validateClusterNodeCorestore` before it writes, takes the
162
+ * node's own real-write fence inside the writing transaction so a concurrent deletion conflicts
163
+ * instead of leaving an endpoint behind for a node that is gone, and pushes the cluster
164
+ * configuration afterwards so every connected relay learns the change without reconnecting.
165
+ *
166
+ * `endpoint: null` clears it, which is the same fact as never having set one: the node runs no
167
+ * Corestore the relay may reach, and an inventory for it answers
168
+ * `errorCode: 'CORESTORE_ENDPOINT_UNKNOWN'` rather than guessing a hostname. The endpoint says
169
+ * where, never with what — no credential travels in this request or in the node document.
170
+ */
171
+ export interface IReq_Any_Cloudly_SetClusterNodeCorestoreEndpoint extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_SetClusterNodeCorestoreEndpoint> {
172
+ method: 'setClusterNodeCorestoreEndpoint';
173
+ request: {
174
+ identity: IIdentity;
175
+ nodeId: string;
176
+ /** A canonical `http:` or `https:` origin, or `null` to state that this node has none. */
177
+ endpoint: string | null;
178
+ };
179
+ response: {
180
+ /** The node as it now stands, so a caller needs no second read to see what it set. */
181
+ node: IClusterNode;
182
+ };
183
+ }
153
184
  export interface IRequest_Spark_Cloudly_SendHeartbeat extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Spark_Cloudly_SendHeartbeat> {
154
185
  method: 'sparkNodeHeartbeat';
155
186
  request: {
@@ -12,6 +12,36 @@ export interface IReq_Any_Cloudly_GetPlatformDesiredState extends plugins.typedr
12
12
  bindings: IPlatformBinding[];
13
13
  };
14
14
  }
15
+ /**
16
+ * One platform binding and the node its Corestore resources live on.
17
+ *
18
+ * Corestore is node-local, so a binding without a node is a database nobody can
19
+ * place: while every cluster ran one coreflow per node, "this node" was whoever
20
+ * answered, and a cluster relay serving every node has no such implicit answer.
21
+ * The placement is Cloudly's, derived from where the service is assigned.
22
+ */
23
+ export interface IClusterPlatformBindingPlacement {
24
+ nodeId: string;
25
+ binding: IPlatformBinding;
26
+ }
27
+ /**
28
+ * The desired platform state for the relay's own cluster, node by node.
29
+ *
30
+ * Its session decides which cluster it speaks for, so the request carries no
31
+ * cluster of its own. It replaces `getPlatformDesiredState` for a cluster
32
+ * relay: the same capabilities and provider configs, and bindings that state
33
+ * where they belong instead of leaving it to be inferred.
34
+ */
35
+ export interface IReq_Relay_Cloudly_GetClusterPlatformDesiredState extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Relay_Cloudly_GetClusterPlatformDesiredState> {
36
+ method: 'getClusterPlatformDesiredState';
37
+ request: Record<string, never>;
38
+ response: {
39
+ capabilities: IPlatformCapability[];
40
+ providerConfigs: IPlatformProviderConfig[];
41
+ /** Sorted by node, then by binding id, so two reads compare directly. */
42
+ bindings: IClusterPlatformBindingPlacement[];
43
+ };
44
+ }
15
45
  export interface IReq_Any_Cloudly_GetPlatformCapabilities extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_GetPlatformCapabilities> {
16
46
  method: 'getPlatformCapabilities';
17
47
  request: {
@@ -1,5 +1,5 @@
1
1
  import * as plugins from './plugins.js';
2
- import type { IActiveSecretRecipientMetadata, IIdentityCredential, ISecretVersionReference } from './data/index.js';
2
+ import type { IActiveSecretRecipientMetadata, ICorestoreDatabaseAllocationReference, IIdentityCredential, ISecretVersionReference } from './data/index.js';
3
3
  import type { ISecretEnvelopeAdmissionBindingV1 } from './runtime.js';
4
4
  import type { IObjectStorageRetentionIntentV1, IObjectStorageRetentionEvidenceExpectationV1, IObjectStorageRetentionEvidenceV1 } from './platform/objectstorageretention.js';
5
5
  export type TCorestoreCredentialCapability = 'database' | 'objectstorage';
@@ -257,14 +257,12 @@ export declare const corestoreDatabaseBackupRuntimeLimits: Readonly<{
257
257
  maximumClosureChunks: 1000000;
258
258
  maximumServiceNameBytes: 1024;
259
259
  }>;
260
- export interface ICorestoreDatabaseAllocationReference {
261
- purpose: 'scratch-rehearsal';
262
- rehearsalId: string;
263
- allocationId: string;
264
- generation: number;
265
- bindingSha256: string;
266
- receiptSha256: string;
267
- }
260
+ /**
261
+ * Declared in the data tree, because an isolated restore's canonical resource
262
+ * mappings bind the exact same reference and that tree carries no runtime
263
+ * import. Re-exported here so this module's readers keep one import site.
264
+ */
265
+ export type { ICorestoreDatabaseAllocationReference };
268
266
  export interface ICorestoreDatabaseBackupSnapshotTags {
269
267
  corestore: 'resource';
270
268
  serviceId: string;
@@ -336,4 +334,3 @@ export declare const computeCorestoreDatabaseAllocationReferenceSha256: (valueAr
336
334
  export declare const computeCorestoreDatabaseBackupReceiptSha256: (valueArg: unknown) => Promise<string>;
337
335
  export declare const computeCorestoreDatabaseBackupRestoreRequestSha256: (valueArg: unknown) => Promise<string>;
338
336
  export declare const computeCorestoreDatabaseBackupRestoreResponseSha256: (valueArg: unknown) => Promise<string>;
339
- export {};