@serve.zone/interfaces 32.44.0 → 32.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ import { captureNetwork, networkCounter, networkId, networkRecord, requireNetwork } from '../private/networkcontract.js';
2
+ /**
3
+ * Every reason Cloudly answers instead of designating a service platform-owned or withdrawing the
4
+ * designation. Each travels by name as a `TypedResponseError`, and a refused request changes
5
+ * nothing.
6
+ */
7
+ export const servicePlatformOwnershipRefusals = Object.freeze([
8
+ /** No service of that id belongs to the named organization. */
9
+ 'service-unknown',
10
+ /** The service's deletion has begun; no new authority outlives it. */
11
+ 'service-deleting',
12
+ /**
13
+ * The named organization is not a platform organization (`isPlatformOrganizationId`): an
14
+ * idp.global organization, whose owner keeps the authority over its runtime network policy, or an
15
+ * id that is no canonical identifier.
16
+ */
17
+ 'organization-not-platform',
18
+ /** Another write changed the service's designation first: read it again. */
19
+ 'conflict',
20
+ ]);
21
+ export const servicePlatformOwnershipContract = Object.freeze({
22
+ error: 'Service platform ownership contract rejected.',
23
+ });
24
+ /**
25
+ * Whether an organization id names a platform organization, the only kind whose service an
26
+ * administrator may designate platform-owned.
27
+ *
28
+ * Every organization Cloudly assigns outside idp.global is namespaced with `:` — a legacy service's
29
+ * own `legacy:Service:<id>`, a platform `Organization:<id>` — while idp.global names an
30
+ * organization by a 21-character nanoid in the URL alphabet (`A-Za-z0-9_-`), which never carries
31
+ * `:`. An id is a platform organization's exactly when it is a canonical identifier (an ASCII
32
+ * letter or digit, then up to 199 of letters, digits, `:`, `.`, `_` and `-`) and carries `:`.
33
+ * Everything else is not: an idp.global id, whichever character it starts with, and any value that
34
+ * is not a canonical identifier — empty, whitespace, a non-string. The answer fails closed: an id
35
+ * this rule cannot read never gains administrator authority.
36
+ */
37
+ export const isPlatformOrganizationId = (organizationId) => {
38
+ try {
39
+ return networkId(organizationId).includes(':');
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ };
45
+ /** Detached exact-schema snapshot of a stored or answered designation. */
46
+ export const snapshotServicePlatformOwnership = (value) => {
47
+ try {
48
+ const item = networkRecord(captureNetwork(value), ['organizationId', 'serviceId', 'actorId', 'designatedAt']);
49
+ const organizationId = networkId(item.organizationId);
50
+ requireNetwork(isPlatformOrganizationId(organizationId));
51
+ return {
52
+ organizationId,
53
+ serviceId: networkId(item.serviceId),
54
+ actorId: networkId(item.actorId),
55
+ designatedAt: networkCounter(item.designatedAt),
56
+ };
57
+ }
58
+ catch {
59
+ throw new Error(servicePlatformOwnershipContract.error);
60
+ }
61
+ };
62
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZXBsYXRmb3Jtb3duZXJzaGlwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9zZXJ2aWNlcGxhdGZvcm1vd25lcnNoaXAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGNBQWMsRUFBRSxjQUFjLEVBQUUsU0FBUyxFQUFFLGFBQWEsRUFBRSxjQUFjLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQStCekg7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLGdDQUFnQyxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDNUQsK0RBQStEO0lBQy9ELGlCQUFpQjtJQUNqQixzRUFBc0U7SUFDdEUsa0JBQWtCO0lBQ2xCOzs7O09BSUc7SUFDSCwyQkFBMkI7SUFDM0IsNEVBQTRFO0lBQzVFLFVBQVU7Q0FDRixDQUFDLENBQUM7QUFLWixNQUFNLENBQUMsTUFBTSxnQ0FBZ0MsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQzVELEtBQUssRUFBRSwrQ0FBK0M7Q0FDOUMsQ0FBQyxDQUFDO0FBRVo7Ozs7Ozs7Ozs7OztHQVlHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQUcsQ0FBQyxjQUFzQixFQUFXLEVBQUU7SUFDMUUsSUFBSSxDQUFDO1FBQ0gsT0FBTyxTQUFTLENBQUMsY0FBYyxDQUFDLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2pELENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7QUFDSCxDQUFDLENBQUM7QUFFRiwwRUFBMEU7QUFDMUUsTUFBTSxDQUFDLE1BQU0sZ0NBQWdDLEdBQUcsQ0FBQyxLQUFjLEVBQTZCLEVBQUU7SUFDNUYsSUFBSSxDQUFDO1FBQ0gsTUFBTSxJQUFJLEdBQUcsYUFBYSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLGdCQUFnQixFQUFFLFdBQVcsRUFBRSxTQUFTLEVBQUUsY0FBYyxDQUFDLENBQUMsQ0FBQztRQUM5RyxNQUFNLGNBQWMsR0FBRyxTQUFTLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBQ3RELGNBQWMsQ0FBQyx3QkFBd0IsQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDO1FBQ3pELE9BQU87WUFDTCxjQUFjO1lBQ2QsU0FBUyxFQUFFLFNBQVMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDO1lBQ3BDLE9BQU8sRUFBRSxTQUFTLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQztZQUNoQyxZQUFZLEVBQUUsY0FBYyxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUM7U0FDaEQsQ0FBQztJQUNKLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQzFELENBQUM7QUFDSCxDQUFDLENBQUMifQ==
@@ -73,7 +73,7 @@ export declare const serviceRuntimeContract: Readonly<{
73
73
  readonly maximumAssignmentPageSize: 256;
74
74
  }>;
75
75
  declare const serviceRuntimeWaitingReasons: readonly ["node-network-pending", "conflict", "failure-backoff", "storage-import-pending", "bindings-pending", "legacy-adoption-pending", "objectstorage-retention-pending"];
76
- declare const serviceRuntimeRefusals: readonly ["cluster-runtime-missing", "organization-missing", "image-plan-missing", "platform-unsupported", "secrets-unsupported", "volumes-unsupported", "port-protocol-unsupported", "ports-too-many", "readiness-target-undeclared", "environment-too-large", "no-eligible-node", "replica-limit", "node-authority-unsupported", "network-policy-missing", "network-policy-mismatch", "network-workload-pool-missing", "network-workload-pool-exhausted", "volume-driver-unsupported", "volume-options-unsupported", "volume-shared-unsupported", "volume-placement-unsupported", "volume-initialization-unknown", "volume-ownership-unknown", "objectstorage-endpoint-missing", "objectstorage-binding-invalid", "objectstorage-environment-conflict", "objectstorage-endpoint-unselected", "database-endpoint-missing", "database-endpoint-unselected", "published-port-reserved", "published-port-symmetric-without-egress"];
76
+ declare const serviceRuntimeRefusals: readonly ["cluster-runtime-missing", "organization-missing", "image-plan-missing", "platform-unsupported", "secrets-unsupported", "volumes-unsupported", "port-protocol-unsupported", "ports-too-many", "readiness-target-undeclared", "environment-too-large", "no-eligible-node", "replica-limit", "node-authority-unsupported", "network-policy-missing", "network-policy-mismatch", "network-workload-pool-missing", "network-workload-pool-exhausted", "volume-driver-unsupported", "volume-options-unsupported", "volume-shared-unsupported", "volume-placement-unsupported", "volume-initialization-unknown", "volume-ownership-unknown", "objectstorage-endpoint-missing", "objectstorage-binding-invalid", "objectstorage-environment-conflict", "objectstorage-endpoint-unselected", "database-endpoint-missing", "database-endpoint-unselected", "published-port-reserved", "published-port-symmetric-without-egress", "readiness-network-unsupported", "cluster-relay-missing", "cluster-ingress-environment-conflict"];
77
77
  /**
78
78
  * Why the producer is not converging yet, though nothing needs an administrator:
79
79
  * `node-network-pending` waits for a node's network readiness, `conflict` for a sweep over inputs
@@ -137,6 +137,22 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
137
137
  * which the handoff leases' `runtimeNetworkHandoffSnatPortRange` owns; a document written before
138
138
  * that rule can hold one. `published-port-symmetric-without-egress` is a `symmetric` entry in a
139
139
  * document whose `publicEgress` is false, which a projection endpoint refuses.
140
+ *
141
+ * `readiness-network-unsupported` is a spec whose network is `isolated` and whose readiness probe is
142
+ * `tcp` or `http`: an isolated workload has no address its node can dial, so the probe could never
143
+ * pass and the node refuses the run (`TRuntimeAssignmentFailureCause` `refused` for
144
+ * `unsupported-readiness`). `isServiceRuntimeReadinessReachable` is this rule; a producer answers
145
+ * what it returns. The spec snapshot still reads such a spec, so a stored one stays readable and
146
+ * is answered with this refusal rather than refused as data.
147
+ *
148
+ * The two `cluster-` refusals name why the environment of a cluster's designated ingress service
149
+ * cannot be composed (`composeClusterIngressEnvironment`); a service no designation names is never
150
+ * judged by them. `cluster-relay-missing` is a cluster that states no valid relay
151
+ * (`IClusterRelay`): the ingress registers through its relay's exact origin, so without one it has
152
+ * nothing to start with, and an administrator enables the relay (`enableClusterRelay`).
153
+ * `cluster-ingress-environment-conflict` is an ingress service whose own environment already sets
154
+ * one of `clusterIngressEnvironmentNames`: Cloudly owns those names, and a service value would
155
+ * otherwise silently replace Cloudly's or be replaced by it.
140
156
  */
141
157
  export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
142
158
  interface IServiceRuntimeStatusBase {
@@ -164,6 +180,32 @@ export type IServiceRuntimeStatus = IServiceRuntimeStatusBase & ({
164
180
  * node admitted but has not yet reported on reads as undelivered.
165
181
  */
166
182
  export type TServiceRuntimeAssignmentDelivery = 'undelivered' | 'delivered';
183
+ /**
184
+ * Why one dispatch of an assignment revision to its node ended as it did.
185
+ *
186
+ * The node holds the revision: `accepted`, `replay` or `historical` is what the node answered to
187
+ * the push, and `acknowledged` a revision the node's current session acknowledged before, answered
188
+ * from that acknowledgement without pushing it again.
189
+ *
190
+ * The node does not hold it from this dispatch: `node-busy` while the node already carries its limit
191
+ * of pushes in flight, `node-not-connected` while no live peer holds a session for the node,
192
+ * `session-moved` when the session the dispatch began on is no longer the node's current one,
193
+ * `intent-not-current` when the revision is no longer the one the controller wants on that node,
194
+ * `request-unbound` when the request does not bind to the session it would travel on,
195
+ * `node-refused` when the node (or the relay carrying it) answered the push with a refusal,
196
+ * `push-failed` when the push got no answer in time or its carrier failed, and
197
+ * `acknowledgement-unbound` when the answer names another revision.
198
+ */
199
+ export declare const assignmentDispatchReasons: readonly ["accepted", "replay", "historical", "acknowledged", "node-busy", "node-not-connected", "session-moved", "intent-not-current", "request-unbound", "node-refused", "push-failed", "acknowledgement-unbound"];
200
+ export type TAssignmentDispatchReason = typeof assignmentDispatchReasons[number];
201
+ /** The last dispatch of a slot's current assignment, as the controller recorded it. Value-free. */
202
+ export interface IServiceRuntimeAssignmentDispatch {
203
+ /** Whether the node holds the revision after this dispatch. */
204
+ delivered: boolean;
205
+ reason: TAssignmentDispatchReason;
206
+ /** Unix milliseconds on the controller's clock when this dispatch ended. */
207
+ at: number;
208
+ }
167
209
  /** One replica slot's current assignment, as an administrator reads it. */
168
210
  export interface IServiceRuntimeAssignmentView {
169
211
  serviceId: string;
@@ -180,12 +222,27 @@ export interface IServiceRuntimeAssignmentView {
180
222
  /** Joined stop and remove receipts of this attempt. */
181
223
  terminalReceiptCount: 0 | 1 | 2;
182
224
  delivery: TServiceRuntimeAssignmentDelivery;
225
+ /**
226
+ * The last dispatch of the current revision while the controller still remembers one, null when
227
+ * it remembers none. Absent from a controller that does not report dispatches. Dispatch is the
228
+ * controller's push; `delivery` stays the node's own evidence.
229
+ */
230
+ dispatch?: IServiceRuntimeAssignmentDispatch | null;
183
231
  }
184
232
  /**
185
233
  * Structural snapshot for exact persistence and for a spec write. Whether the cluster, the nodes and
186
234
  * the platform endpoints exist, and whether the service declares the readiness target, is Cloudly's.
187
235
  */
188
236
  export declare const snapshotServiceRuntimeSpec: (value: unknown) => IServiceRuntimeSpec;
237
+ /**
238
+ * Whether a spec's readiness probe can reach its workload: false exactly for an `isolated` network
239
+ * with a `tcp` or `http` probe, which the producer answers as `readiness-network-unsupported`. The
240
+ * spec is read through `snapshotServiceRuntimeSpec`, so a value that is no spec is refused with its
241
+ * rejection rather than judged.
242
+ */
243
+ export declare const isServiceRuntimeReadinessReachable: (value: unknown) => boolean;
244
+ /** Exactly a recorded dispatch of an assignment: a known reason and a positive Unix millisecond time. */
245
+ export declare const isServiceRuntimeAssignmentDispatch: (value: unknown) => value is IServiceRuntimeAssignmentDispatch;
189
246
  /** Structural snapshot for exact persistence: a reason belongs to exactly one state. */
190
247
  export declare const snapshotServiceRuntimeStatus: (value: unknown) => IServiceRuntimeStatus;
191
248
  export {};
@@ -28,7 +28,29 @@ const serviceRuntimeRefusals = [
28
28
  'volume-placement-unsupported', 'volume-initialization-unknown', 'volume-ownership-unknown',
29
29
  'objectstorage-endpoint-missing', 'objectstorage-binding-invalid', 'objectstorage-environment-conflict',
30
30
  'objectstorage-endpoint-unselected', 'database-endpoint-missing', 'database-endpoint-unselected',
31
- 'published-port-reserved', 'published-port-symmetric-without-egress',
31
+ 'published-port-reserved', 'published-port-symmetric-without-egress', 'readiness-network-unsupported',
32
+ 'cluster-relay-missing', 'cluster-ingress-environment-conflict',
33
+ ];
34
+ /**
35
+ * Why one dispatch of an assignment revision to its node ended as it did.
36
+ *
37
+ * The node holds the revision: `accepted`, `replay` or `historical` is what the node answered to
38
+ * the push, and `acknowledged` a revision the node's current session acknowledged before, answered
39
+ * from that acknowledgement without pushing it again.
40
+ *
41
+ * The node does not hold it from this dispatch: `node-busy` while the node already carries its limit
42
+ * of pushes in flight, `node-not-connected` while no live peer holds a session for the node,
43
+ * `session-moved` when the session the dispatch began on is no longer the node's current one,
44
+ * `intent-not-current` when the revision is no longer the one the controller wants on that node,
45
+ * `request-unbound` when the request does not bind to the session it would travel on,
46
+ * `node-refused` when the node (or the relay carrying it) answered the push with a refusal,
47
+ * `push-failed` when the push got no answer in time or its carrier failed, and
48
+ * `acknowledgement-unbound` when the answer names another revision.
49
+ */
50
+ export const assignmentDispatchReasons = [
51
+ 'accepted', 'replay', 'historical', 'acknowledged',
52
+ 'node-busy', 'node-not-connected', 'session-moved', 'intent-not-current', 'request-unbound', 'node-refused',
53
+ 'push-failed', 'acknowledgement-unbound',
32
54
  ];
33
55
  /** Sorted, duplicate-free and read by the vocabulary the ids belong to: a node's, or the canonical one. */
34
56
  const sortedIds = (value, maximum, read) => networkSorted(networkList(value, maximum, read), id => id);
@@ -86,6 +108,27 @@ export const snapshotServiceRuntimeSpec = (value) => producerSafe(() => {
86
108
  placement: placementOf(item.placement), replicasPerNode: item.replicasPerNode, hold: item.hold,
87
109
  profile: profileOf(item.profile), network: networkOf(item.network), revision: networkCounter(item.revision) };
88
110
  });
111
+ /**
112
+ * Whether a spec's readiness probe can reach its workload: false exactly for an `isolated` network
113
+ * with a `tcp` or `http` probe, which the producer answers as `readiness-network-unsupported`. The
114
+ * spec is read through `snapshotServiceRuntimeSpec`, so a value that is no spec is refused with its
115
+ * rejection rather than judged.
116
+ */
117
+ export const isServiceRuntimeReadinessReachable = (value) => {
118
+ const { profile, network } = snapshotServiceRuntimeSpec(value);
119
+ return network.mode === 'attached' || profile.readiness.kind === 'process';
120
+ };
121
+ /** Exactly a recorded dispatch of an assignment: a known reason and a positive Unix millisecond time. */
122
+ export const isServiceRuntimeAssignmentDispatch = (value) => {
123
+ try {
124
+ const item = networkRecord(captureNetwork(value), ['delivered', 'reason', 'at']);
125
+ networkCounter(item.at);
126
+ return typeof item.delivered === 'boolean' && isOneOf(assignmentDispatchReasons, item.reason);
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ };
89
132
  /** Structural snapshot for exact persistence: a reason belongs to exactly one state. */
90
133
  export const snapshotServiceRuntimeStatus = (value) => producerSafe(() => {
91
134
  const item = networkRecord(captureNetwork(value), ['id', 'specRevision', 'state', 'reason', 'changedAt']);
@@ -102,4 +145,4 @@ export const snapshotServiceRuntimeStatus = (value) => producerSafe(() => {
102
145
  requireNetwork(item.state === 'refused' && isOneOf(serviceRuntimeRefusals, item.reason));
103
146
  return { ...base, state: item.state, reason: item.reason };
104
147
  });
105
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZXJ1bnRpbWUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL3NlcnZpY2VydW50aW1lLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUlBLE9BQU8sRUFBRSwrQkFBK0IsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQ3hFLE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsY0FBYyxFQUFFLFNBQVMsRUFBRSxXQUFXLEVBQUUsYUFBYSxFQUFFLGFBQWEsRUFBRSxhQUFhLEVBQzVILGNBQWMsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQ3hELE9BQU8sRUFBRSxZQUFZLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxTQUFTLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNoRyxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsZ0JBQWdCLEVBQUUsa0JBQWtCLEVBQUUsdUJBQXVCLEVBQUUsa0JBQWtCLEVBQ2hILG9CQUFvQixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUF5RDVELE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDbEQsc0JBQXNCLEVBQUUsRUFBRTtJQUMxQiwrRkFBK0Y7SUFDL0YsZUFBZSxFQUFFLEVBQUU7SUFDbkIsb0hBQW9IO0lBQ3BILGtCQUFrQixFQUFFLCtCQUErQixDQUFDLHdCQUF3QjtJQUM1RSx3QkFBd0IsRUFBRSwrQkFBK0IsQ0FBQyx3QkFBd0I7SUFDbEYsMEZBQTBGO0lBQzFGLG1CQUFtQixFQUFFLEdBQUc7SUFDeEIsZ0dBQWdHO0lBQ2hHLHlCQUF5QixFQUFFLEdBQUc7Q0FDdEIsQ0FBQyxDQUFDO0FBRVosTUFBTSw0QkFBNEIsR0FBRztJQUNuQyxzQkFBc0IsRUFBRSxVQUFVLEVBQUUsaUJBQWlCLEVBQUUsd0JBQXdCLEVBQUUsa0JBQWtCO0lBQ25HLHlCQUF5QixFQUFFLGlDQUFpQztDQUNwRCxDQUFDO0FBQ1gsTUFBTSxzQkFBc0IsR0FBRztJ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
148
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZXJ1bnRpbWUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL3NlcnZpY2VydW50aW1lLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUlBLE9BQU8sRUFBRSwrQkFBK0IsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQ3hFLE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsY0FBYyxFQUFFLFNBQVMsRUFBRSxXQUFXLEVBQUUsYUFBYSxFQUFFLGFBQWEsRUFBRSxhQUFhLEVBQzVILGNBQWMsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQ3hELE9BQU8sRUFBRSxZQUFZLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRSxTQUFTLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNoRyxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsZ0JBQWdCLEVBQUUsa0JBQWtCLEVBQUUsdUJBQXVCLEVBQUUsa0JBQWtCLEVBQ2hILG9CQUFvQixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUF5RDVELE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDbEQsc0JBQXNCLEVBQUUsRUFBRTtJQUMxQiwrRkFBK0Y7SUFDL0YsZUFBZSxFQUFFLEVBQUU7SUFDbkIsb0hBQW9IO0lBQ3BILGtCQUFrQixFQUFFLCtCQUErQixDQUFDLHdCQUF3QjtJQUM1RSx3QkFBd0IsRUFBRSwrQkFBK0IsQ0FBQyx3QkFBd0I7SUFDbEYsMEZBQTBGO0lBQzFGLG1CQUFtQixFQUFFLEdBQUc7SUFDeEIsZ0dBQWdHO0lBQ2hHLHlCQUF5QixFQUFFLEdBQUc7Q0FDdEIsQ0FBQyxDQUFDO0FBRVosTUFBTSw0QkFBNEIsR0FBRztJQUNuQyxzQkFBc0IsRUFBRSxVQUFVLEVBQUUsaUJBQWlCLEVBQUUsd0JBQXdCLEVBQUUsa0JBQWtCO0lBQ25HLHlCQUF5QixFQUFFLGlDQUFpQztDQUNwRCxDQUFDO0FBQ1gsTUFBTSxzQkFBc0IsR0FBRztJ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
@@ -4,6 +4,7 @@ import type { IRuntimeNetworkAddressPlan, TRuntimeNetworkAddressPlanWritableData
4
4
  import type { IRuntimeNetworkNodeReadiness, IRuntimeRouterIncarnation } from '../data/networknode.js';
5
5
  import type { IPrivateNetwork, IServicePrivateNetworks, ISetServicePrivateNetworks, TPrivateNetworkMutation } from '../data/privatenetwork.js';
6
6
  import type { IServiceNetworkEgress, IServicePublishedPorts, ISetServiceNetworkEgress, ISetServicePublishedPorts } from '../data/networkprojection.js';
7
+ import type { IServicePlatformOwnership } from '../data/serviceplatformownership.js';
7
8
  /** Current organization picker data. Entries are never cached authorization grants. */
8
9
  export interface IReq_Cloudly_ListOwnedOrganizations extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_ListOwnedOrganizations> {
9
10
  method: 'listOwnedOrganizations';
@@ -97,7 +98,14 @@ export interface IReq_Cloudly_SetServicePrivateNetworks extends plugins.typedreq
97
98
  replayed: boolean;
98
99
  };
99
100
  }
100
- /** null means the service has never had a policy document, neither published ports nor egress. */
101
+ /**
102
+ * null means the service has never had a policy document, neither published ports nor egress.
103
+ *
104
+ * The authority follows the service (`IServicePlatformOwnership`): a platform-owned service is read
105
+ * by a verified administrator, every other service by a federated sign-in holding the named
106
+ * organization's freshly introspected `owner` role. A caller without that authority, or naming no
107
+ * such service, is answered one denial text, whichever it is.
108
+ */
101
109
  export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_GetServicePublishedPorts> {
102
110
  method: 'getServicePublishedPorts';
103
111
  request: {
@@ -119,6 +127,15 @@ export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequ
119
127
  * attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
120
128
  * no public address and no platform endpoint; `network-policy-missing` means the service has no
121
129
  * document at all.
130
+ *
131
+ * The authority follows the service (`IServicePlatformOwnership`): a platform-owned service is
132
+ * written by a verified administrator, every other service by a federated sign-in holding the named
133
+ * organization's freshly introspected `owner` role, and an administrator does not substitute for
134
+ * that owner. A caller without that authority, or naming no such service, is answered one denial
135
+ * text, whichever it is. A designation that changes while the write is staged refuses it as the
136
+ * policy write's own `conflict`, the refusal a stale `expectedRevision` is also answered (read the
137
+ * document and the authority again); it is a service network policy refusal, not one of
138
+ * `servicePlatformOwnershipRefusals`.
122
139
  */
123
140
  export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_SetServicePublishedPorts> {
124
141
  method: 'setServicePublishedPorts';
@@ -183,6 +200,64 @@ export interface IReq_Admin_Cloudly_SetServiceNetworkEgress extends plugins.type
183
200
  replayed: boolean;
184
201
  };
185
202
  }
203
+ /**
204
+ * Designates one service platform-owned (`IServicePlatformOwnership`), so that a verified
205
+ * administrator, and no organization owner, holds the authority over its published ports.
206
+ *
207
+ * Only a verified administrator calls it. Cloudly designates only a service of a platform
208
+ * organization (`isPlatformOrganizationId`: a canonical identifier carrying `:`, such as
209
+ * `legacy:Service:<id>` or `Organization:<id>`) and refuses every other organization id, an
210
+ * idp.global one or one that is no canonical identifier, as `organization-not-platform` before it
211
+ * reads the service. It refuses a service that is not the named organization's (`service-unknown`)
212
+ * and a deleting one (`service-deleting`); refusals are `servicePlatformOwnershipRefusals`. A
213
+ * service already designated answers its stored designation unchanged with `changed: false`.
214
+ */
215
+ export interface IReq_Admin_Cloudly_DesignatePlatformOwnedService extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_DesignatePlatformOwnedService> {
216
+ method: 'designatePlatformOwnedService';
217
+ request: {
218
+ identity: IIdentityCredential;
219
+ organizationId: string;
220
+ serviceId: string;
221
+ };
222
+ response: {
223
+ platformOwnership: IServicePlatformOwnership;
224
+ changed: boolean;
225
+ };
226
+ }
227
+ /**
228
+ * Withdraws a service's platform-owned designation; its published ports stay as they are, and from
229
+ * then on the organization-owner rule governs them. A designated service is always a platform
230
+ * organization's, which has no idp.global owner, so after the withdrawal nobody writes or reads its
231
+ * published ports until an administrator designates it again: the authority fails closed.
232
+ *
233
+ * Only a verified administrator calls it. Refusals are `service-unknown` and `conflict`; a service
234
+ * without a designation answers `changed: false`.
235
+ */
236
+ export interface IReq_Admin_Cloudly_UndesignatePlatformOwnedService extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_UndesignatePlatformOwnedService> {
237
+ method: 'undesignatePlatformOwnedService';
238
+ request: {
239
+ identity: IIdentityCredential;
240
+ organizationId: string;
241
+ serviceId: string;
242
+ };
243
+ response: {
244
+ platformOwnership: null;
245
+ changed: boolean;
246
+ };
247
+ }
248
+ /** The service's platform-owned designation, or null when it has none. Only a verified administrator reads it. */
249
+ export interface IReq_Admin_Cloudly_GetServicePlatformOwnership extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_GetServicePlatformOwnership> {
250
+ method: 'getServicePlatformOwnership';
251
+ /** Refused `service-unknown` when no service of that id belongs to the named organization. */
252
+ request: {
253
+ identity: IIdentityCredential;
254
+ organizationId: string;
255
+ serviceId: string;
256
+ };
257
+ response: {
258
+ platformOwnership: IServicePlatformOwnership | null;
259
+ };
260
+ }
186
261
  export interface IRequest_Any_Cloudly_GetNetworkNodes {
187
262
  method: 'getNetworkNodes';
188
263
  request: {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "32.44.0",
3
+ "version": "32.46.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "devDependencies": {
30
30
  "@git.zone/tsbuild": "^5.1.2",
31
- "@git.zone/tsdoc": "^3.0.1",
31
+ "@git.zone/tsdoc": "^3.0.2",
32
32
  "@git.zone/tsrun": "^3.0.2",
33
- "@git.zone/tstest": "6.3.4",
34
- "@types/node": "26.6.2"
33
+ "@git.zone/tstest": "^7.1.0",
34
+ "@types/node": "26.6.3"
35
35
  },
36
36
  "files": [
37
37
  "ts/**/*",
package/readme.md CHANGED
@@ -515,6 +515,34 @@ list is a valid policy meaning the service publishes nothing. Callers never supp
515
515
  from the accepted policy revision and digest, and node-level host-port placement
516
516
  stays Cloudly's decision.
517
517
 
518
+ Who authors a service's published ports follows the service. A service of an
519
+ idp.global organization is that organization's: only a federated sign-in holding
520
+ its freshly introspected `owner` role reads and writes them, and an
521
+ infrastructure administrator does not substitute for it. A platform-owned
522
+ service (`data.IServicePlatformOwnership`: `organizationId`, `serviceId`, the
523
+ designating administrator's `actorId`, `designatedAt`) belongs to no idp.global
524
+ organization, such as the cluster ingress or a platform call router; only a
525
+ verified administrator reads and writes its published ports, as only an
526
+ administrator writes its egress. An administrator designates such a service
527
+ with `designatePlatformOwnedService`, withdraws it with
528
+ `undesignatePlatformOwnedService` and reads it with `getServicePlatformOwnership`
529
+ (`requests.network.IReq_Admin_Cloudly_DesignatePlatformOwnedService`,
530
+ `IReq_Admin_Cloudly_UndesignatePlatformOwnedService`,
531
+ `IReq_Admin_Cloudly_GetServicePlatformOwnership`). Only a platform
532
+ organization's service is designated: `data.isPlatformOrganizationId` is true
533
+ exactly for a canonical identifier that carries `:` (`legacy:Service:<id>`,
534
+ `Organization:<id>`), and false for an idp.global nanoid, whichever character it
535
+ starts with, and for any value that is not a canonical identifier, such as an
536
+ empty or whitespace id. Cloudly refuses every other id as
537
+ `organization-not-platform`, and further designation refusals are
538
+ `service-unknown`, `service-deleting` and `conflict`
539
+ (`data.servicePlatformOwnershipRefusals`). The designation grants no port, egress
540
+ or placement by itself. Withdrawing it leaves the service's published ports as
541
+ they are with no writer, because a platform organization has no idp.global
542
+ owner, until an administrator designates the service again. A published-port
543
+ write staged while the designation changes is refused as the policy write's
544
+ `conflict`.
545
+
518
546
  A published entry with `hostPortEnd` publishes every port from `hostPort` to
519
547
  `hostPortEnd` inclusive, each to the same port inside the workload, so its
520
548
  `targetPort` equals `hostPort` and a one-port range is written without the member.
@@ -1878,7 +1906,8 @@ evidence has not arrived). `refused` carries a `TServiceRuntimeRefusal`:
1878
1906
  `volume-options-unsupported`, `volume-shared-unsupported`, `volume-placement-unsupported`,
1879
1907
  `volume-initialization-unknown`, `volume-ownership-unknown`, `objectstorage-endpoint-missing`,
1880
1908
  `objectstorage-binding-invalid`, `objectstorage-environment-conflict`,
1881
- `objectstorage-endpoint-unselected`, `database-endpoint-missing` or `database-endpoint-unselected`.
1909
+ `objectstorage-endpoint-unselected`, `database-endpoint-missing`, `database-endpoint-unselected`,
1910
+ `cluster-relay-missing` or `cluster-ingress-environment-conflict`.
1882
1911
  The first four volume
1883
1912
  refusals name what a local storage claim cannot express: a volume driver other than a node-local
1884
1913
  directory, driver or mount options, one volume mounted by more than one replica or service, and a
@@ -1888,7 +1917,12 @@ refusals, `objectstorage-endpoint-missing`, `objectstorage-binding-invalid` and
1888
1917
  `objectstorage-environment-conflict` name why a binding's environment cannot be composed, see
1889
1918
  [Object-Storage Binding Environment](#object-storage-binding-environment), and
1890
1919
  `objectstorage-endpoint-unselected`, like `database-endpoint-unselected`, names an endpoint the
1891
- workload's network does not select (below). `ports-too-many` is a service declaring more target ports than the
1920
+ workload's network does not select (below). The two `cluster-` refusals concern only a cluster's
1921
+ designated ingress service, whose environment `composeClusterIngressEnvironment` composes, see
1922
+ [Cluster Ingress](#cluster-ingress): `cluster-relay-missing` is a cluster that states no valid
1923
+ relay, which an administrator enables with `enableClusterRelay`, and
1924
+ `cluster-ingress-environment-conflict` an ingress service whose own environment already sets one of
1925
+ `clusterIngressEnvironmentNames`. `ports-too-many` is a service declaring more target ports than the
1892
1926
  `runtimeConfigContract.maximumTargetPorts` (64) a config carries, where `port-protocol-unsupported`
1893
1927
  is about one port's protocol; `node-authority-unsupported` is a node whose session resolved fewer
1894
1928
  workload authorities than the spec needs, refused at the admit step rather than handed to a node
@@ -3217,6 +3251,24 @@ already applied and ignores a hint that is not newer, so a lost or repeated hint
3217
3251
  `SERVEZONE_CLUSTER_RELAY_ORIGIN` (the exact `IClusterRelay.origin`) and `SERVEZONE_CLUSTER_NODE_ID`
3218
3252
  (the node the assignment runs on). Cloudly writes them; the ingress reads them.
3219
3253
 
3254
+ `composeClusterIngressEnvironment(serviceEnvironment, { relayOrigin, nodeId })` is how Cloudly
3255
+ writes the two non-secret names for one replica of the designated ingress service: the service's
3256
+ own environment with `SERVEZONE_CLUSTER_RELAY_ORIGIN` and `SERVEZONE_CLUSTER_NODE_ID` added
3257
+ (`IClusterIngressLaunchFacts`; `relayOrigin` is the cluster's `IClusterRelay.origin` or `null`,
3258
+ `nodeId` the node the replica's assignment runs on). The bearer is not added: it reaches the
3259
+ workload through its secret manifest. The answer is a `TClusterIngressEnvironmentComposition`:
3260
+
3261
+ | `status` | `reason` | When |
3262
+ | --- | --- | --- |
3263
+ | `composed` | — | `environment` is the service's own with both names added. |
3264
+ | `refused` | `cluster-relay-missing` | `relayOrigin` is `null` or `validateClusterRelay` refuses it. Answered first. |
3265
+ | `refused` | `cluster-ingress-environment-conflict` | The service's own environment already sets one of the three names; the message names every such key and no value. |
3266
+
3267
+ `refused` carries the contract's `message`, and both reasons are `TClusterIngressEnvironmentRefusal`,
3268
+ members of `TServiceRuntimeRefusal`. A `nodeId` that is not a node id is the caller's error and is
3269
+ thrown. Both values are part of what the replica runs, so a moved relay or another node is another
3270
+ environment. The inputs are not changed.
3271
+
3220
3272
  A cluster born on Pallet has no cutover scope, census or target set, so its ingress is designated by
3221
3273
  `IClusterIngressDesignation { id, generation, previous, clusterId, runtime, ingressServiceId,
3222
3274
  preparedSpecRevision, credential, actorId, acceptedAt, digest }` instead of the fleet-cutover
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.44.0',
6
+ version: '32.46.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -3,6 +3,8 @@ import { isUtcCalendarDay } from '../private/utccalendarday.js';
3
3
  import { validateProtocolOfferOn } from '../protocol.js';
4
4
  import { hasExactKeys, readExactDataRecord } from '../private/dataproperties.js';
5
5
  import { isClusterNodeId } from '../private/nodeidentifier.js';
6
+ import { validateClusterRelay } from './cluster.js';
7
+ import type { TServiceRuntimeRefusal } from './serviceruntime.js';
6
8
 
7
9
  /**
8
10
  * The cluster ingress contracts.
@@ -440,6 +442,74 @@ export const clusterIngressEnvironmentNames = Object.freeze({
440
442
  nodeId: 'SERVEZONE_CLUSTER_NODE_ID',
441
443
  } as const);
442
444
 
445
+ /**
446
+ * The service runtime refusals the ingress environment reports: `cluster-relay-missing`, a cluster
447
+ * that states no valid relay, and `cluster-ingress-environment-conflict`, a service environment
448
+ * that already sets one of `clusterIngressEnvironmentNames`.
449
+ */
450
+ export type TClusterIngressEnvironmentRefusal = Extract<
451
+ TServiceRuntimeRefusal,
452
+ 'cluster-relay-missing' | 'cluster-ingress-environment-conflict'
453
+ >;
454
+
455
+ /** What the designated ingress service runs with, or the one reason it cannot be composed. */
456
+ export type TClusterIngressEnvironmentComposition =
457
+ | { status: 'composed'; environment: Record<string, string> }
458
+ | { status: 'refused'; reason: TClusterIngressEnvironmentRefusal; message: string };
459
+
460
+ /** The facts Cloudly states about one replica of the designated ingress service. */
461
+ export interface IClusterIngressLaunchFacts {
462
+ /** The cluster's `IClusterRelay.origin`, or `null` when the cluster states no relay. */
463
+ relayOrigin: string | null;
464
+ /** The node the replica's assignment runs on. */
465
+ nodeId: string;
466
+ }
467
+
468
+ /**
469
+ * The environment one replica of a cluster's designated ingress service runs with: the service's
470
+ * own values with `relayOrigin` and `nodeId` added under their `clusterIngressEnvironmentNames`.
471
+ *
472
+ * The bearer is not added here: it is a secret and reaches the workload through its secret manifest.
473
+ * Its name is still Cloudly's, so a service environment that sets any of the three names is
474
+ * refused `cluster-ingress-environment-conflict`, naming every such key and none of their values,
475
+ * rather than having one value silently replace another. A relay origin that is `null` or that
476
+ * `validateClusterRelay` refuses is `cluster-relay-missing`, answered before a conflict. A node id
477
+ * that is not a node id is the caller's error, not a refusal: the assignment named it, and it is
478
+ * thrown. The inputs are not changed. Both values are part of what the replica runs, so a relay or
479
+ * node that changes is a new environment.
480
+ */
481
+ export const composeClusterIngressEnvironment = (
482
+ serviceEnvironmentArg: Readonly<Record<string, string>>,
483
+ launchArg: IClusterIngressLaunchFacts,
484
+ ): TClusterIngressEnvironmentComposition => {
485
+ const { relayOrigin, nodeId } = launchArg;
486
+ if (!isClusterNodeId(nodeId)) {
487
+ throw new Error('cluster ingress environment requires a node id');
488
+ }
489
+ if (relayOrigin === null) {
490
+ return { status: 'refused', reason: 'cluster-relay-missing',
491
+ message: 'cluster ingress has no relay origin: the cluster states no relay' };
492
+ }
493
+ const relayErrors = validateClusterRelay({ origin: relayOrigin });
494
+ if (relayErrors.length > 0) {
495
+ return { status: 'refused', reason: 'cluster-relay-missing',
496
+ message: `cluster ingress has no relay origin: ${relayErrors.join('; ')}` };
497
+ }
498
+ const collisions = Object.values(clusterIngressEnvironmentNames)
499
+ .filter((nameArg) => Object.hasOwn(serviceEnvironmentArg, nameArg))
500
+ .sort();
501
+ if (collisions.length > 0) {
502
+ return { status: 'refused', reason: 'cluster-ingress-environment-conflict',
503
+ message: `cluster ingress environment collides with ${collisions.join(', ')} already set on the `
504
+ + 'service' };
505
+ }
506
+ return { status: 'composed', environment: {
507
+ ...serviceEnvironmentArg,
508
+ [clusterIngressEnvironmentNames.relayOrigin]: relayOrigin,
509
+ [clusterIngressEnvironmentNames.nodeId]: nodeId,
510
+ } };
511
+ };
512
+
443
513
  /**
444
514
  * Every reason Cloudly refuses a cluster ingress registration, in the order it judges them.
445
515
  *
package/ts/data/index.ts CHANGED
@@ -76,6 +76,7 @@ export * from './sparklegacyretirement.js';
76
76
  export * from './sparknodereport.js';
77
77
  export * from './service.js';
78
78
  export * from './servicecredential.js';
79
+ export * from './serviceplatformownership.js';
79
80
  export * from './serviceports.js';
80
81
  export * from './serviceruntime.js';
81
82
  export * from './servicevolumeinitialization.js';