@serve.zone/interfaces 32.50.0 → 32.52.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.
@@ -22,7 +22,28 @@ export interface ISparkNodeReport {
22
22
  localRuntimeReady: true;
23
23
  workloadReadiness: 'unverified';
24
24
  };
25
+ /**
26
+ * The outcome of Spark's last attempt to apply a desired hostname (`sparkNodeHostContract`), when
27
+ * it made one since it started. Whether the node carries the hostname is `runtime.hostname`.
28
+ */
29
+ hostApply?: ISparkNodeHostApply;
25
30
  }
31
+ /** Whether Spark applied a desired hostname: `applied` once the host carries it. */
32
+ export type TSparkNodeHostApplyOutcome = 'applied' | 'failed';
33
+ /** One attempt to apply the desired hostname of one generation. */
34
+ export interface ISparkNodeHostApply {
35
+ /** The `ISparkNodeDesiredHost.generation` the attempt applied. */
36
+ generation: number;
37
+ /** The hostname it applied. */
38
+ hostname: string;
39
+ outcome: TSparkNodeHostApplyOutcome;
40
+ /** `failed` only, and required there: why, as operator text. */
41
+ reason?: string;
42
+ }
43
+ /** Bounds of `ISparkNodeHostApply.reason`. */
44
+ export declare const sparkNodeHostApplyContract: Readonly<{
45
+ readonly maximumReasonLength: 512;
46
+ }>;
26
47
  /** HTTPS POST body. Never log, persist, or return nodeToken. */
27
48
  export interface ISparkNodeHeartbeatRequest {
28
49
  nodeId: string;
@@ -65,3 +86,42 @@ export declare const sparkNodeHeartbeatContract: Readonly<{
65
86
  export declare const snapshotSparkNodeReport: (valueArg: unknown) => ISparkNodeReport;
66
87
  export declare const snapshotSparkNodeHeartbeatRequest: (valueArg: unknown) => ISparkNodeHeartbeatRequest;
67
88
  export declare const snapshotSparkNodeHeartbeatReceipt: (valueArg: unknown) => ISparkNodeHeartbeatReceipt;
89
+ /** HTTPS POST body asking for the hostname Cloudly wants the node to carry. Never log, persist, or return nodeToken. */
90
+ export interface ISparkNodeHostRequest {
91
+ nodeId: string;
92
+ nodeToken: string;
93
+ /** Read and negotiated before the node is authenticated; see `sparkNodeHostContract`. */
94
+ protocol: IProtocolOffer;
95
+ }
96
+ /**
97
+ * The hostname Cloudly wants the node to carry: `IClusterNode.data.hostnameIntent`, as the node
98
+ * applies it.
99
+ *
100
+ * `hostname: null` with `generation: 0` states that Cloudly holds no intent for the node, and Spark
101
+ * leaves the host's hostname as it is. Applying is idempotent: a host that already carries the
102
+ * hostname is left alone, and every answer states the whole desired value rather than a change.
103
+ */
104
+ export interface ISparkNodeDesiredHost {
105
+ nodeId: string;
106
+ hostname: string | null;
107
+ generation: number;
108
+ /** The controller's offer, so the node judges the answer it acts on. */
109
+ protocol: IProtocolOffer;
110
+ }
111
+ /**
112
+ * The node runtime's desired-host route.
113
+ *
114
+ * Authenticated with the node's Spark credential exactly like `sparkNodeHeartbeatContract`, and
115
+ * judged in the same server order: protocol offer first, against this server's `sparkNode` offer
116
+ * and within `maxRequestBytes`, answered with `protocolRefusalStatus` and an `IProtocolRefusal` body
117
+ * on refusal, before the node is authenticated. Spark reports the outcome of applying the answer as
118
+ * `ISparkNodeReport.hostApply` on its next heartbeat.
119
+ */
120
+ export declare const sparkNodeHostContract: Readonly<{
121
+ readonly endpoint: "/spark/nodes/host";
122
+ readonly maxRequestBytes: 1024;
123
+ readonly maxResponseBytes: 1024;
124
+ readonly protocolRefusalStatus: 422;
125
+ }>;
126
+ export declare const snapshotSparkNodeHostRequest: (valueArg: unknown) => ISparkNodeHostRequest;
127
+ export declare const snapshotSparkNodeDesiredHost: (valueArg: unknown) => ISparkNodeDesiredHost;
@@ -1,7 +1,12 @@
1
1
  import { isReleaseVersion } from './releaseversion.js';
2
- import { readExactDataRecord } from '../private/dataproperties.js';
2
+ import { readDataProperties, readExactDataRecord } from '../private/dataproperties.js';
3
3
  import { isClusterNodeId } from '../private/nodeidentifier.js';
4
4
  import { protocolRefusalHttpStatus, readProtocolOffer } from '../protocol.js';
5
+ import { isClusterNodeHostname } from './clusternodehostname.js';
6
+ /** Bounds of `ISparkNodeHostApply.reason`. */
7
+ export const sparkNodeHostApplyContract = Object.freeze({
8
+ maximumReasonLength: 512,
9
+ });
5
10
  /**
6
11
  * The node runtime's own heartbeat route.
7
12
  *
@@ -48,9 +53,27 @@ const integer = (valueArg, minimumArg) => {
48
53
  requireValue(Number.isSafeInteger(value) && value >= minimumArg);
49
54
  return value;
50
55
  };
56
+ /** One `hostApply`: exactly its keys, a reason exactly when it failed. */
57
+ const snapshotSparkNodeHostApply = (valueArg) => {
58
+ const keys = ['generation', 'hostname', 'outcome'];
59
+ const value = readExactDataRecord(valueArg, keys) ?? record(valueArg, [...keys, 'reason']);
60
+ requireValue(isClusterNodeHostname(value.hostname));
61
+ const generation = integer(value.generation, 1);
62
+ if (value.outcome === 'applied') {
63
+ requireValue(value.reason === undefined);
64
+ return Object.freeze({ generation, hostname: value.hostname, outcome: 'applied' });
65
+ }
66
+ requireValue(value.outcome === 'failed');
67
+ return Object.freeze({ generation, hostname: value.hostname, outcome: 'failed',
68
+ reason: text(value.reason, /^[\x20-\x7e]+$/, sparkNodeHostApplyContract.maximumReasonLength) });
69
+ };
51
70
  /** Exact inert JSON records; detaches and freezes before any consumer awaits. */
52
71
  export const snapshotSparkNodeReport = (valueArg) => {
53
- const value = record(valueArg, ['observedAt', 'metrics', 'runtime']);
72
+ const reportKeys = ['observedAt', 'metrics', 'runtime'];
73
+ const properties = readDataProperties(valueArg);
74
+ requireValue(properties !== null);
75
+ const hasHostApply = Object.hasOwn(properties, 'hostApply');
76
+ const value = record(valueArg, hasHostApply ? [...reportKeys, 'hostApply'] : reportKeys);
54
77
  const metrics = record(value.metrics, ['cpuUsagePercent', 'memoryUsedMB', 'memoryAvailableMB',
55
78
  'diskUsedGB', 'diskAvailableGB']);
56
79
  const runtime = record(value.runtime, ['hostname', 'platform', 'arch', 'osRelease', 'bundle',
@@ -77,6 +100,7 @@ export const snapshotSparkNodeReport = (valueArg) => {
77
100
  }),
78
101
  localRuntimeReady: true, workloadReadiness: 'unverified',
79
102
  }),
103
+ ...(hasHostApply ? { hostApply: snapshotSparkNodeHostApply(value.hostApply) } : {}),
80
104
  });
81
105
  };
82
106
  export const snapshotSparkNodeHeartbeatRequest = (valueArg) => {
@@ -100,4 +124,43 @@ export const snapshotSparkNodeHeartbeatReceipt = (valueArg) => {
100
124
  protocol: readProtocolOffer(value),
101
125
  });
102
126
  };
103
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3Bhcmtub2RlcmVwb3J0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9zcGFya25vZGVyZXBvcnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDdkQsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDbkUsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQy9ELE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxpQkFBaUIsRUFBdUIsTUFBTSxnQkFBZ0IsQ0FBQztBQWlEbkc7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFNLENBQUMsTUFBTSwwQkFBMEIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3RELFFBQVEsRUFBRSx3QkFBd0I7SUFDbEMsZUFBZSxFQUFFLElBQUk7SUFDckIsZ0JBQWdCLEVBQUUsSUFBSTtJQUN0QixxQkFBcUIsRUFBRSx5QkFBeUI7Q0FDeEMsQ0FBQyxDQUFDO0FBRVosTUFBTSxJQUFJLEdBQUcsR0FBVSxFQUFFLEdBQUcsTUFBTSxJQUFJLEtBQUssQ0FBQyx5Q0FBeUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQzFGLFNBQVMsWUFBWSxDQUFDLFFBQWlCO0lBQ3JDLElBQUksQ0FBQyxRQUFRO1FBQUUsSUFBSSxFQUFFLENBQUM7QUFDeEIsQ0FBQztBQUNELHNHQUFzRztBQUN0RyxNQUFNLE1BQU0sR0FBRyxDQUFDLFFBQWlCLEVBQUUsT0FBMEIsRUFBMkIsRUFBRTtJQUN4RixNQUFNLE1BQU0sR0FBRyxtQkFBbUIsQ0FBQyxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDdEQsWUFBWSxDQUFDLE1BQU0sS0FBSyxJQUFJLENBQUMsQ0FBQztJQUM5QixPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDLENBQUM7QUFDRixNQUFNLElBQUksR0FBRyxDQUFDLFFBQWlCLEVBQUUsVUFBa0IsRUFBRSxZQUFvQixFQUFVLEVBQUU7SUFDbkYsWUFBWSxDQUFDLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxRQUFRLENBQUMsTUFBTSxJQUFJLFlBQVk7V0FDdkUsUUFBUSxDQUFDLEtBQUssQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxLQUFLLFFBQVEsQ0FBQyxDQUFDO0lBQ25ELE9BQU8sUUFBUSxDQUFDO0FBQ2xCLENBQUMsQ0FBQztBQUNGLGtHQUFrRztBQUNsRyxNQUFNLE1BQU0sR0FBRyxDQUFDLFFBQWlCLEVBQVUsRUFBRSxDQUFDLENBQzVDLGVBQWUsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FDOUMsQ0FBQztBQUNGLDBGQUEwRjtBQUMxRiwwRkFBMEY7QUFDMUYsTUFBTSxjQUFjLEdBQUcsQ0FBQyxRQUFpQixFQUFVLEVBQUUsQ0FBQyxDQUNwRCxnQkFBZ0IsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FDL0MsQ0FBQztBQUNGLE1BQU0sTUFBTSxHQUFHLENBQUMsUUFBaUIsRUFBRSxNQUFNLEdBQUcsTUFBTSxDQUFDLGdCQUFnQixFQUFVLEVBQUU7SUFDN0UsWUFBWSxDQUFDLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxNQUFNLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQztXQUNqRSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksUUFBUSxJQUFJLENBQUMsSUFBSSxRQUFRLElBQUksTUFBTSxDQUFDLENBQUM7SUFDdEUsT0FBTyxRQUFRLENBQUM7QUFDbEIsQ0FBQyxDQUFDO0FBQ0YsTUFBTSxPQUFPLEdBQUcsQ0FBQyxRQUFpQixFQUFFLFVBQWtCLEVBQVUsRUFBRTtJQUNoRSxNQUFNLEtBQUssR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDL0IsWUFBWSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxJQUFJLFVBQVUsQ0FBQyxDQUFDO0lBQ2pFLE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQyxDQUFDO0FBRUYsaUZBQWlGO0FBQ2pGLE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsUUFBaUIsRUFBb0IsRUFBRTtJQUM3RSxNQUFNLEtBQUssR0FBRyxNQUFNLENBQUMsUUFBUSxFQUFFLENBQUMsWUFBWSxFQUFFLFNBQVMsRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUFDO0lBQ3JFLE1BQU0sT0FBTyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsaUJBQWlCLEVBQUUsY0FBYyxFQUFFLG1CQUFtQjtRQUMzRixZQUFZLEVBQUUsaUJBQWlCLENBQUMsQ0FBQyxDQUFDO0lBQ3BDLE1BQU0sT0FBTyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsVUFBVSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsV0FBVyxFQUFFLFFBQVE7UUFDMUYsbUJBQW1CLEVBQUUsbUJBQW1CLENBQUMsQ0FBQyxDQUFDO0lBQzdDLFlBQVksQ0FBQyxPQUFPLENBQUMsUUFBUSxLQUFLLE9BQU8sSUFBSSxDQUFDLFFBQVEsRUFBRSxTQUFTLENBQUMsQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLElBQWMsQ0FBQztXQUM5RixPQUFPLENBQUMsaUJBQWlCLEtBQUssSUFBSSxJQUFJLE9BQU8sQ0FBQyxpQkFBaUIsS0FBSyxZQUFZLENBQUMsQ0FBQztJQUN2RixNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsRUFBRSxlQUFlLEVBQUUsR0FBRyxDQUFDLENBQUM7SUFDOUQsWUFBWSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyx3Q0FBd0MsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3pHLE1BQU0sTUFBTSxHQUFHLE1BQU0sQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLENBQUMsU0FBUyxFQUFFLGNBQWMsRUFBRSxnQkFBZ0IsQ0FBQyxDQUFDLENBQUM7SUFDckYsT0FBTyxNQUFNLENBQUMsTUFBTSxDQUFDO1FBQ25CLFVBQVUsRUFBRSxPQUFPLENBQUMsS0FBSyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBQUM7UUFDeEMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUM7WUFDckIsZUFBZSxFQUFFLE1BQU0sQ0FBQyxPQUFPLENBQUMsZUFBZSxFQUFFLEdBQUcsQ0FBQztZQUNyRCxZQUFZLEVBQUUsTUFBTSxDQUFDLE9BQU8sQ0FBQyxZQUFZLENBQUMsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLENBQUMsT0FBTyxDQUFDLGlCQUFpQixDQUFDO1lBQ2hHLFVBQVUsRUFBRSxNQUFNLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxFQUFFLGVBQWUsRUFBRSxNQUFNLENBQUMsT0FBTyxDQUFDLGVBQWUsQ0FBQztTQUN6RixDQUFDO1FBQ0YsT0FBTyxFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUM7WUFDckIsUUFBUSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLE9BQU8sQ0FBQyxJQUE0QjtZQUN2RSxTQUFTLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLEVBQUUsZ0JBQWdCLEVBQUUsR0FBRyxDQUFDO1lBQ3pELE1BQU0sRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDO2dCQUNwQixPQUFPLEVBQUUsY0FBYyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUM7Z0JBQ3ZDLFlBQVksRUFBRSxJQUFJLENBQUMsTUFBTSxDQUFDLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxFQUFFLENBQUM7Z0JBQzdELGNBQWMsRUFBRSxJQUFJLENBQUMsTUFBTSxDQUFDLGNBQWMsRUFBRSxnQkFBZ0IsRUFBRSxFQUFFLENBQUM7YUFDbEUsQ0FBQztZQUNGLGlCQUFpQixFQUFFLElBQUksRUFBRSxpQkFBaUIsRUFBRSxZQUFZO1NBQ3pELENBQUM7S0FDSCxDQUFDLENBQUM7QUFDTCxDQUFDLENBQUM7QUFFRixNQUFNLENBQUMsTUFBTSxpQ0FBaUMsR0FBRyxDQUFDLFFBQWlCLEVBQThCLEVBQUU7SUFDakcsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLFFBQVEsRUFBRSxDQUFDLFFBQVEsRUFBRSxXQUFXLEVBQUUsVUFBVSxFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUM7SUFDOUUsT0FBTyxNQUFNLENBQUMsTUFBTSxDQUFDO1FBQ25CLE1BQU0sRUFBRSxNQUFNLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQztRQUM1QixtRkFBbUY7UUFDbkYsU0FBUyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUFFLDJCQUEyQixFQUFFLEVBQUUsQ0FBQztRQUNqRSxRQUFRLEVBQUUsaUJBQWlCLENBQUMsS0FBSyxDQUFDO1FBQ2xDLE1BQU0sRUFBRSx1QkFBdUIsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDO0tBQzlDLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLGlDQUFpQyxHQUFHLENBQUMsUUFBaUIsRUFBOEIsRUFBRTtJQUNqRyxNQUFNLEtBQUssR0FBRyxNQUFNLENBQUMsUUFBUSxFQUFFLENBQUMsVUFBVSxFQUFFLFFBQVEsRUFBRSxzQkFBc0I7UUFDMUUsY0FBYyxFQUFFLFlBQVksRUFBRSxVQUFVLENBQUMsQ0FBQyxDQUFDO0lBQzdDLFlBQVksQ0FBQyxLQUFLLENBQUMsUUFBUSxLQUFLLElBQUksQ0FBQyxDQUFDO0lBQ3RDLE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQztRQUNuQixRQUFRLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQztRQUM1QyxvQkFBb0IsRUFBRSxPQUFPLENBQUMsS0FBSyxDQUFDLG9CQUFvQixFQUFFLENBQUMsQ0FBQztRQUM1RCxZQUFZLEVBQUUsT0FBTyxDQUFDLEtBQUssQ0FBQyxZQUFZLEVBQUUsQ0FBQyxDQUFDLEVBQUUsVUFBVSxFQUFFLE9BQU8sQ0FBQyxLQUFLLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQztRQUN0RixRQUFRLEVBQUUsaUJBQWlCLENBQUMsS0FBSyxDQUFDO0tBQ25DLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyJ9
127
+ /**
128
+ * The node runtime's desired-host route.
129
+ *
130
+ * Authenticated with the node's Spark credential exactly like `sparkNodeHeartbeatContract`, and
131
+ * judged in the same server order: protocol offer first, against this server's `sparkNode` offer
132
+ * and within `maxRequestBytes`, answered with `protocolRefusalStatus` and an `IProtocolRefusal` body
133
+ * on refusal, before the node is authenticated. Spark reports the outcome of applying the answer as
134
+ * `ISparkNodeReport.hostApply` on its next heartbeat.
135
+ */
136
+ export const sparkNodeHostContract = Object.freeze({
137
+ endpoint: '/spark/nodes/host',
138
+ maxRequestBytes: 1024,
139
+ maxResponseBytes: 1024,
140
+ protocolRefusalStatus: protocolRefusalHttpStatus,
141
+ });
142
+ export const snapshotSparkNodeHostRequest = (valueArg) => {
143
+ const value = record(valueArg, ['nodeId', 'nodeToken', 'protocol']);
144
+ return Object.freeze({
145
+ nodeId: nodeId(value.nodeId),
146
+ nodeToken: text(value.nodeToken, /^secureToken[a-f0-9]{64}$/, 75),
147
+ protocol: readProtocolOffer(value),
148
+ });
149
+ };
150
+ export const snapshotSparkNodeDesiredHost = (valueArg) => {
151
+ const value = record(valueArg, ['nodeId', 'hostname', 'generation', 'protocol']);
152
+ const hostname = value.hostname;
153
+ if (hostname === null) {
154
+ requireValue(value.generation === 0);
155
+ }
156
+ else {
157
+ requireValue(isClusterNodeHostname(hostname));
158
+ }
159
+ return Object.freeze({
160
+ nodeId: nodeId(value.nodeId),
161
+ hostname,
162
+ generation: hostname === null ? 0 : integer(value.generation, 1),
163
+ protocol: readProtocolOffer(value),
164
+ });
165
+ };
166
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3Bhcmtub2RlcmVwb3J0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9zcGFya25vZGVyZXBvcnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDdkQsT0FBTyxFQUFFLGtCQUFrQixFQUFFLG1CQUFtQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDdkYsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQy9ELE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxpQkFBaUIsRUFBdUIsTUFBTSxnQkFBZ0IsQ0FBQztBQUNuRyxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQTBDakUsOENBQThDO0FBQzlDLE1BQU0sQ0FBQyxNQUFNLDBCQUEwQixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDdEQsbUJBQW1CLEVBQUUsR0FBRztDQUNoQixDQUFDLENBQUM7QUE0Qlo7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFNLENBQUMsTUFBTSwwQkFBMEIsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ3RELFFBQVEsRUFBRSx3QkFBd0I7SUFDbEMsZUFBZSxFQUFFLElBQUk7SUFDckIsZ0JBQWdCLEVBQUUsSUFBSTtJQUN0QixxQkFBcUIsRUFBRSx5QkFBeUI7Q0FDeEMsQ0FBQyxDQUFDO0FBRVosTUFBTSxJQUFJLEdBQUcsR0FBVSxFQUFFLEdBQUcsTUFBTSxJQUFJLEtBQUssQ0FBQyx5Q0FBeUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQzFGLFNBQVMsWUFBWSxDQUFDLFFBQWlCO0lBQ3JDLElBQUksQ0FBQyxRQUFRO1FBQUUsSUFBSSxFQUFFLENBQUM7QUFDeEIsQ0FBQztBQUNELHNHQUFzRztBQUN0RyxNQUFNLE1BQU0sR0FBRyxDQUFDLFFBQWlCLEVBQUUsT0FBMEIsRUFBMkIsRUFBRTtJQUN4RixNQUFN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
@@ -110,6 +110,13 @@ export interface IReq_Any_Cloudly_CreateNodeJumpCommand extends plugins.typedreq
110
110
  role?: IClusterNode['data']['role'];
111
111
  nodeType?: IClusterNode['data']['nodeType'];
112
112
  ttlMs?: number;
113
+ /**
114
+ * The new node's name (`data.isClusterNodeName`). The setup command names the host
115
+ * `cloudly-worker-<nodeName>` (`data.deriveDefaultClusterNodeHostname`) before it enrolls, so
116
+ * the node enrolls under its default hostname. Absent, the node keeps the hostname it has and
117
+ * its name is proposed from that (`data.proposeClusterNodeName`).
118
+ */
119
+ nodeName?: string;
113
120
  };
114
121
  response: {
115
122
  jumpCode: string;
@@ -283,6 +290,58 @@ export interface IReq_Any_Cloudly_SetClusterNodeCorestoreDatabaseEndpoint extend
283
290
  node: IClusterNode;
284
291
  };
285
292
  }
293
+ /**
294
+ * An administrator renames a node: the static hostname Cloudly wants it to carry
295
+ * (`IClusterNode.data.hostnameIntent.hostname`).
296
+ *
297
+ * The node's name stays as it is: the new intent keeps the name of the node's current intent. A
298
+ * node without an intent takes the name `data.proposeClusterNodeName(hostname)` (`foo` names the
299
+ * node `foo`, `cloudly-worker-grasberg` names it `grasberg`), and is refused `name-invalid` when
300
+ * that is `null`.
301
+ *
302
+ * Cloudly records the intent and answers at once; Spark applies the intent of the latest
303
+ * generation on the host and the node has converged when its report states the new hostname. A new
304
+ * intent may replace one the node has not converged to, whether it is still applying, failed or
305
+ * never reached the node. `expectedGeneration` is the generation of the node's current intent, `0`
306
+ * when it has none, and serializes concurrent changes. Every refusal is one of
307
+ * `data.clusterNodeHostnameRefusals` and changes nothing.
308
+ */
309
+ export interface IReq_Any_Cloudly_SetClusterNodeHostname extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_SetClusterNodeHostname> {
310
+ method: 'setClusterNodeHostname';
311
+ request: {
312
+ identity: IIdentity;
313
+ nodeId: string;
314
+ expectedGeneration: number;
315
+ /** One lowercase RFC 1123 label (`data.isClusterNodeHostname`). */
316
+ hostname: string;
317
+ };
318
+ response: {
319
+ /** The node as it now stands, carrying the new intent. */
320
+ node: IClusterNode;
321
+ };
322
+ }
323
+ /**
324
+ * An administrator resets a node to the default scheme: hostname `cloudly-worker-<name>`
325
+ * (`data.deriveDefaultClusterNodeHostname`).
326
+ *
327
+ * `name` replaces the node's name when stated; absent, the name of the node's current intent is
328
+ * used, and a node without an intent is refused `name-invalid`. Recorded, applied and replaceable
329
+ * exactly like `setClusterNodeHostname`, with the same refusals.
330
+ */
331
+ export interface IReq_Any_Cloudly_ResetClusterNodeHostname extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_ResetClusterNodeHostname> {
332
+ method: 'resetClusterNodeHostname';
333
+ request: {
334
+ identity: IIdentity;
335
+ nodeId: string;
336
+ expectedGeneration: number;
337
+ /** The node's name (`data.isClusterNodeName`). */
338
+ name?: string;
339
+ };
340
+ response: {
341
+ /** The node as it now stands, carrying the new intent. */
342
+ node: IClusterNode;
343
+ };
344
+ }
286
345
  export interface IReq_Any_Cloudly_CreateNodeAction extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_CreateNodeAction> {
287
346
  method: 'createNodeAction';
288
347
  request: {
@@ -9,7 +9,7 @@ import type { IImageRelease } from '../data/immutableimage.js';
9
9
  import type { IDeploymentPreflightReport, IDeploymentPreflightRequestData } from '../data/deploymentpreflight.js';
10
10
  import type { IDeploymentReservationInput, IServiceDeploymentOperation, IServiceDeploymentStatus } from '../data/deploymentoperation.js';
11
11
  import type { TSha256Digest } from '../data/immutableimage.js';
12
- import type { ILegacyServiceRuntimeAttestation } from '../data/deploymentlegacyimport.js';
12
+ import type { ILegacyServiceHostObservation, ILegacyServiceRuntimeAttestation } from '../data/deploymentlegacyimport.js';
13
13
  import type { IProtocolOffer } from '../protocol.js';
14
14
  import type { IRuntimeAssignmentLogAttemptPage, IRuntimeAssignmentLogPage } from '../data/runtimeassignmentlogs.js';
15
15
  import type { IServiceRuntimeAssignmentView, IServiceRuntimeSpec, IServiceRuntimeStatus, TServiceRuntimeSpecWritableData } from '../data/serviceruntime.js';
@@ -224,10 +224,13 @@ export interface IRequest_Any_Cloudly_CleanupServiceDeployment extends plugins.t
224
224
  };
225
225
  }
226
226
  /**
227
- * Captures durable evidence that a legacy `deployOnPush` service runs exactly the registry root its
228
- * tag names: every required replica observed healthy on that one digest, with the root's platform
229
- * manifests read from Cloudly's own registry. Administrators only, and only while the legacy
230
- * runtime still runs — incomplete evidence is refused, never stored.
227
+ * Captures durable evidence that a legacy `deployOnPush` service runs exactly one registry root of
228
+ * its own repository, from an administrator's observation of its Swarm service and containers on
229
+ * the hosts that run them (`hostObservation`). Cloudly judges the observation itself
230
+ * (`data.judgeLegacyServiceHostObservation`): every desired replica running, healthy or without a
231
+ * healthcheck, on one root that is whole in Cloudly's own registry, its platform manifests read
232
+ * from there. The source tag need not still name that root. Administrators only, and only while
233
+ * the legacy runtime still runs — incomplete evidence is refused, never stored.
231
234
  *
232
235
  * `expectedRuntimeDigest`, when stated, is an assertion the observed digest must equal. Capturing
233
236
  * again for an unchanged digest returns the attestation already stored.
@@ -238,6 +241,7 @@ export interface IRequest_Any_Cloudly_CaptureLegacyServiceRuntimeAttestation ext
238
241
  identity: IIdentityCredential;
239
242
  legacyServiceId: string;
240
243
  expectedRuntimeDigest?: TSha256Digest;
244
+ hostObservation: ILegacyServiceHostObservation;
241
245
  };
242
246
  response: {
243
247
  attestation: ILegacyServiceRuntimeAttestation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "32.50.0",
3
+ "version": "32.52.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
@@ -30,7 +30,7 @@
30
30
  "@git.zone/tsbuild": "^5.1.2",
31
31
  "@git.zone/tsdoc": "^3.0.2",
32
32
  "@git.zone/tsrun": "^3.0.2",
33
- "@git.zone/tstest": "^7.1.0",
33
+ "@git.zone/tstest": "^7.2.0",
34
34
  "@types/node": "26.6.3"
35
35
  },
36
36
  "files": [
package/readme.md CHANGED
@@ -203,6 +203,19 @@ current authority or workload readiness. Persist only the report and receipt in
203
203
  `IClusterNode.data.sparkNodeReport`, never the bearer. Keep legacy reporting
204
204
  separate. This endpoint does not deliver or acknowledge operator actions.
205
205
 
206
+ `data.sparkNodeHostContract` defines the node's desired-host route,
207
+ `/spark/nodes/host`, with 1024-byte request and response bounds, authenticated and
208
+ judged in the same order as the heartbeat. `ISparkNodeHostRequest` carries the
209
+ Spark bearer and the sender's offer; `ISparkNodeDesiredHost` answers with the whole
210
+ desired value of `IClusterNode.data.hostnameIntent` — the hostname and its
211
+ generation — never a change, and `hostname: null` with generation `0` states that
212
+ Cloudly holds no intent, so Spark leaves the host's hostname as it is. Spark applies
213
+ the latest generation and states the outcome of its last attempt as the optional
214
+ `ISparkNodeReport.hostApply` (`applied`, or `failed` with a bounded reason) on its
215
+ next heartbeat; the node has converged when `runtime.hostname` states the intent's
216
+ hostname. A Spark that sends `hostApply` raises its `minimumPeerVersion` so it
217
+ reaches only a Cloudly that reads it.
218
+
206
219
  The snapshot functions reject unknown keys, accessors, malformed identities and
207
220
  nonfinite/out-of-range numbers, then return detached frozen records. Transports
208
221
  own byte limits, timeouts and cancellation. These contracts do not implement a
@@ -2921,6 +2934,11 @@ Common data contracts include:
2921
2934
 
2922
2935
  - `ICluster` and `IClusterNode` for cluster membership and provisioning state, including the
2923
2936
  optional `ICluster.data.relay` block described below.
2937
+ - `clusternodehostname` for the static hostname Cloudly wants a node to carry:
2938
+ `IClusterNodeHostnameIntent` on `IClusterNode.data.hostnameIntent`, the default scheme
2939
+ `cloudly-worker-<name>` (`clusterNodeHostnameContract`, `deriveDefaultClusterNodeHostname`,
2940
+ `proposeClusterNodeName`) and the named `clusterNodeHostnameRefusals` of
2941
+ `setClusterNodeHostname` and `resetClusterNodeHostname`.
2924
2942
  - `IService`, `IDeployment`, `IImage`, `IRegistryTarget`, and `IExternalRegistry` for workload delivery.
2925
2943
  - Service port contracts including `IServiceTargetPort`, `IServiceDomainRoute`, and `IServicePublicPortMapping` for canonical backend targets, domain target references, and edge/Coretraffic TCP/UDP public exposure.
2926
2944
  - `IDomain`, `IDnsEntry`, and traffic contracts for routing and DNS management.
@@ -3640,7 +3658,7 @@ The immutable deployment workflow is:
3640
3658
 
3641
3659
  A legacy `deployOnPush` service without deployable source moves to a greenfield successor by a legacy import, which replaces step 2:
3642
3660
 
3643
- - While the legacy replicas still run — before its runtime is frozen — an administrator captures the evidence with `captureLegacyServiceRuntimeAttestation` (`requests.service.IRequest_Any_Cloudly_CaptureLegacyServiceRuntimeAttestation`). The stored `data.ILegacyServiceRuntimeAttestation` records every required replica healthy on one digest, the registry root the legacy tag names, and that root's platform manifests; `data.validateLegacyServiceRuntimeAttestation` states its rules. Incomplete evidence is refused, never stored, and there is no substitute once the runtime is stopped. `getLegacyServiceRuntimeAttestations` lists what Cloudly holds for a legacy service; both requests are for administrators only.
3661
+ - While the legacy replicas still run — before its runtime is frozen — an administrator reads the legacy Swarm service and every container of its tasks on the hosts that run them, and submits that as `hostObservation` (`data.ILegacyServiceHostObservation`: Swarm service name, spec image and mode with its desired replicas; per replica the node hostname, Swarm task id, container id, the engine's image store, the container's image id, the image's `RepoDigests`, the container state and its health, `none` when no healthcheck applies) to `captureLegacyServiceRuntimeAttestation` (`requests.service.IRequest_Any_Cloudly_CaptureLegacyServiceRuntimeAttestation`). `data.validateLegacyServiceHostObservation` judges its shape, `data.selectLegacyServiceHostObservationRootDigest` names the one root every replica runs from the service's own repository, and `data.judgeLegacyServiceHostObservation` judges it against that root as Cloudly reads it from its own registry: exactly the desired replicas of a replicated service, its spec image naming the repository by the source tag or the root digest, every replica `running` and `healthy` or `none`, each container's image id the root digest on the containerd image store or one of the root's platform image configuration digests on the classic store. The source tag need not still name the root. The stored `data.ILegacyServiceRuntimeAttestation` records that root, its platform manifests, the replicas, the evidence source `administrator-host-observation` and the observation itself; `data.validateLegacyServiceRuntimeAttestation` states its rules. Incomplete evidence is refused, never stored, and there is no substitute once the runtime is stopped. `getLegacyServiceRuntimeAttestations` lists what Cloudly holds for a legacy service; both requests are for administrators only.
3644
3662
  - The greenfield reservation, and every preflight of it, names `legacyImport` (`data.ILegacyImageImportReference`: legacy service, attestation, asserted source root digest) instead of `sourceRevision`, `sourceDirty` and release evidence. Refusals are the blockers `LEGACY_IMPORT_ATTESTATION_MISSING`, `LEGACY_IMPORT_SOURCE_MISMATCH` and `LEGACY_IMPORT_PLATFORM_MISMATCH`.
3645
3663
  - Instead of the registry push, the deployer calls `importServiceDeploymentLegacyImage`: Cloudly imports the attested image from its own registry into the reserved tag — an index root as it is, a single platform manifest under a one-descriptor index of the manifest's own family — and records release evidence of source `cloudly-legacy-import` naming the legacy source. Steps 3 to 5 follow unchanged; `selectTrustedImageReleaseEvidence` accepts that evidence only with `evidenceSource: 'cloudly-legacy-import'`.
3646
3664
  - The reservation and the import need the capability `deployment:import-legacy`, which only an organization-service-slot grant can carry.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.50.0',
6
+ version: '32.52.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { isSha256Digest, type TSha256Digest, type TImmutableContainerPlatform } from './immutableimage.js';
3
3
  import type { ISparkNodeReportRecord } from './sparknodereport.js';
4
+ import type { IClusterNodeHostnameIntent } from './clusternodehostname.js';
4
5
  import type { IPlatformObjectStorageEndpoint } from '../platform/objectstorage.js';
5
6
  import type { IPlatformDatabaseEndpoint } from '../platform/database.js';
6
7
  import {
@@ -1765,6 +1766,13 @@ export interface IClusterNode {
1765
1766
 
1766
1767
  /** Separately accepted Pallet-era host report; never grants workload readiness. */
1767
1768
  sparkNodeReport?: ISparkNodeReportRecord;
1769
+
1770
+ /**
1771
+ * The static hostname Cloudly wants this node to carry, which Spark applies on the host. The
1772
+ * node has converged when `sparkNodeReport` states the same hostname. Absent until enrollment
1773
+ * or a migration records one; Spark then leaves the host's hostname as it is.
1774
+ */
1775
+ hostnameIntent?: IClusterNodeHostnameIntent;
1768
1776
 
1769
1777
  /**
1770
1778
  * Docker swarm node ID if part of swarm
@@ -0,0 +1,153 @@
1
+ import { readExactDataRecord } from '../private/dataproperties.js';
2
+
3
+ /**
4
+ * The static hostname a cluster node runs under, and the scheme Cloudly names a node by default.
5
+ *
6
+ * Cloudly owns the hostname a node should carry (`IClusterNode.data.hostnameIntent`); Spark applies
7
+ * it on the host and reports the hostname it observes, so a node has converged when its report
8
+ * states the intent's hostname. The default scheme is `cloudly-worker-<name>`: `name` is the
9
+ * operator's short name for the node (`ottilie`, `grasberg`), kept beside the hostname so the node
10
+ * can always be reset to the default however it was renamed.
11
+ *
12
+ * A hostname here is one RFC 1123 label, lowercase: the node's short static hostname, which is
13
+ * what enrollment already requires of a host. A fully qualified name is DNS, not the host's name.
14
+ */
15
+ export const clusterNodeHostnameContract = Object.freeze({
16
+ /** The default hostname is this prefix followed by the node's name. */
17
+ defaultPrefix: 'cloudly-worker-',
18
+ /** One DNS label. */
19
+ maximumHostnameLength: 63,
20
+ /** What is left of one label after `defaultPrefix`. */
21
+ maximumNameLength: 48,
22
+ /** `requestedBy` is an identity's user id. */
23
+ maximumRequestedByLength: 256,
24
+ error: 'Cluster node hostname contract rejected.',
25
+ } as const);
26
+
27
+ const hostnameLabel = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
28
+
29
+ /** True for one lowercase RFC 1123 label of 1 to 63 characters: a node's static hostname. */
30
+ export const isClusterNodeHostname = (valueArg: unknown): valueArg is string => (
31
+ typeof valueArg === 'string'
32
+ && valueArg.length <= clusterNodeHostnameContract.maximumHostnameLength
33
+ && hostnameLabel.test(valueArg)
34
+ );
35
+
36
+ /** True for a node name the default scheme can carry: a label of at most 48 characters. */
37
+ export const isClusterNodeName = (valueArg: unknown): valueArg is string => (
38
+ isClusterNodeHostname(valueArg) && valueArg.length <= clusterNodeHostnameContract.maximumNameLength
39
+ );
40
+
41
+ /** `cloudly-worker-<name>`; throws for a name the scheme cannot carry. */
42
+ export const deriveDefaultClusterNodeHostname = (nameArg: string): string => {
43
+ if (!isClusterNodeName(nameArg)) {
44
+ throw new Error(clusterNodeHostnameContract.error);
45
+ }
46
+ return `${clusterNodeHostnameContract.defaultPrefix}${nameArg}`;
47
+ };
48
+
49
+ /**
50
+ * The node name an observed hostname suggests, or `null` when it suggests none.
51
+ *
52
+ * It reads the first label of the hostname, so a report that states a dotted name proposes its
53
+ * short name. A label already in the default scheme proposes what follows the prefix
54
+ * (`cloudly-worker-ottilie` → `ottilie`); any other valid label proposes itself unchanged
55
+ * (`cloudlyworkergrasberg` stays `cloudlyworkergrasberg`). It never guesses a name out of a
56
+ * label in another scheme: an operator states that name when resetting the node to the default.
57
+ */
58
+ export const proposeClusterNodeName = (hostnameArg: string): string | null => {
59
+ const label = hostnameArg.split('.')[0];
60
+ if (!isClusterNodeHostname(label)) {
61
+ return null;
62
+ }
63
+ const prefix = clusterNodeHostnameContract.defaultPrefix;
64
+ if (label.startsWith(prefix) && isClusterNodeName(label.slice(prefix.length))) {
65
+ return label.slice(prefix.length);
66
+ }
67
+ return isClusterNodeName(label) ? label : null;
68
+ };
69
+
70
+ /** The hostname Cloudly wants a node to carry; `IClusterNode.data.hostnameIntent`. */
71
+ export interface IClusterNodeHostnameIntent {
72
+ /** The node's name: the input of the default scheme, and what a reset to the default uses. */
73
+ name: string;
74
+ /** The static hostname the node should carry; the default derived from `name` or any other label. */
75
+ hostname: string;
76
+ /** Counts accepted intents from 1; a change names the generation it replaces. */
77
+ generation: number;
78
+ /** Unix milliseconds on Cloudly's clock when this intent was accepted. */
79
+ requestedAt: number;
80
+ /** The administrator who asked for it; absent for the intent enrollment or a migration recorded. */
81
+ requestedBy?: string;
82
+ }
83
+
84
+ /** True when the intent's hostname is the default its name derives. */
85
+ export const isDefaultClusterNodeHostname = (intentArg: IClusterNodeHostnameIntent): boolean => (
86
+ isClusterNodeName(intentArg.name) && intentArg.hostname === deriveDefaultClusterNodeHostname(intentArg.name)
87
+ );
88
+
89
+ const fail = (): never => { throw new Error(clusterNodeHostnameContract.error); };
90
+ function requireValue(valueArg: unknown): asserts valueArg {
91
+ if (!valueArg) fail();
92
+ }
93
+ const counter = (valueArg: unknown, minimumArg: number): number => {
94
+ requireValue(typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= minimumArg);
95
+ return valueArg;
96
+ };
97
+
98
+ /** Exact, detached and frozen; refuses every key and value this contract does not state. */
99
+ export const snapshotClusterNodeHostnameIntent = (valueArg: unknown): IClusterNodeHostnameIntent => {
100
+ const keys = ['name', 'hostname', 'generation', 'requestedAt'];
101
+ const value = readExactDataRecord(valueArg, keys) ?? readExactDataRecord(valueArg, [...keys, 'requestedBy']);
102
+ requireValue(value !== null);
103
+ requireValue(isClusterNodeName(value.name) && isClusterNodeHostname(value.hostname));
104
+ const intent: IClusterNodeHostnameIntent = {
105
+ name: value.name, hostname: value.hostname,
106
+ generation: counter(value.generation, 1), requestedAt: counter(value.requestedAt, 1),
107
+ };
108
+ if (Object.hasOwn(value, 'requestedBy')) {
109
+ const requestedBy = value.requestedBy;
110
+ requireValue(typeof requestedBy === 'string' && requestedBy.length > 0
111
+ && requestedBy.length <= clusterNodeHostnameContract.maximumRequestedByLength
112
+ && /^[\x21-\x7e]+$/.test(requestedBy));
113
+ intent.requestedBy = requestedBy;
114
+ }
115
+ return Object.freeze(intent);
116
+ };
117
+
118
+ /**
119
+ * Every reason Cloudly answers instead of renaming a node or resetting it to the default. Each
120
+ * travels by name as a `TypedResponseError`, and a refused request changes nothing.
121
+ */
122
+ export const clusterNodeHostnameRefusals = Object.freeze([
123
+ /** No node of that id exists. */
124
+ 'node-missing',
125
+ /** The requested hostname is not one lowercase RFC 1123 label (`isClusterNodeHostname`). */
126
+ 'hostname-invalid',
127
+ /**
128
+ * The node would carry no name the default scheme can carry (`isClusterNodeName`). A rename keeps
129
+ * the name of the node's intent; a node without an intent takes
130
+ * `proposeClusterNodeName(<requested hostname>)`, and is refused when that is `null`. A reset
131
+ * names no valid name, or names none while the node has no intent whose name it could reuse.
132
+ */
133
+ 'name-invalid',
134
+ /** Another node of the cluster carries that hostname, as reported or as its intent. */
135
+ 'hostname-in-use',
136
+ /** The node's intent already is that hostname under that name. */
137
+ 'hostname-unchanged',
138
+ /**
139
+ * Cloudly holds state naming the node by its current hostname that it cannot move to the new
140
+ * hostname with the change, such as a rollout in flight that targets the node.
141
+ */
142
+ 'node-hostname-referenced',
143
+ /** The node does not run the Pallet-era node runtime, the only one that applies a hostname. */
144
+ 'node-runtime-unsupported',
145
+ /**
146
+ * `expectedGeneration` is not the node's current intent generation: read the node again. This
147
+ * fence alone serializes changes; an intent the node has not converged to may always be replaced.
148
+ */
149
+ 'conflict',
150
+ ] as const);
151
+
152
+ /** One of the reasons Cloudly answers instead of a hostname change. */
153
+ export type TClusterNodeHostnameRefusalCode = typeof clusterNodeHostnameRefusals[number];