@serve.zone/interfaces 32.29.0 → 32.30.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.
@@ -33,7 +33,8 @@ export interface IServiceRuntimeProfile {
33
33
 
34
34
  /**
35
35
  * `isolated` gives the workload no network authority. `attached` joins the runtime network with
36
- * public egress on or off and the platform endpoints it may reach, by id: sorted and unique.
36
+ * public egress on or off and the platform endpoints it may reach, by id: sorted and unique. The
37
+ * service's `IServiceNetworkEgress` must state the same values, or the workload is not admitted.
37
38
  */
38
39
  export type TServiceRuntimeNetwork =
39
40
  | { mode: 'isolated' }
@@ -89,7 +90,7 @@ const serviceRuntimeRefusals = [
89
90
  'volume-driver-unsupported', 'volume-options-unsupported', 'volume-shared-unsupported',
90
91
  'volume-placement-unsupported', 'volume-initialization-unknown', 'volume-ownership-unknown',
91
92
  'objectstorage-endpoint-missing', 'objectstorage-binding-invalid', 'objectstorage-environment-conflict',
92
- 'database-endpoint-missing',
93
+ 'objectstorage-endpoint-unselected', 'database-endpoint-missing', 'database-endpoint-unselected',
93
94
  ] as const;
94
95
 
95
96
  /**
@@ -114,7 +115,8 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
114
115
  * `port-protocol-unsupported` is about one port's protocol — and `node-authority-unsupported` is a
115
116
  * node whose session resolved fewer workload authorities than this spec needs, which the admit step
116
117
  * refuses rather than handing the node work it would silently park. An attached network needs an
117
- * explicit matching policy and a current workload address pool; neither is defaulted.
118
+ * explicit matching policy (`IServiceNetworkEgress`, written by `setServiceNetworkEgress`) and a
119
+ * current workload address pool; neither is defaulted.
118
120
  *
119
121
  * The volume refusals name what a local storage claim cannot express: `volume-driver-unsupported`
120
122
  * a volume of a driver other than a node-local directory, `volume-options-unsupported` driver or
@@ -133,6 +135,20 @@ export type TServiceRuntimeWaitingReason = typeof serviceRuntimeWaitingReasons[n
133
135
  * `database-endpoint-missing` is an enabled database binding placed on a node that states no valid
134
136
  * `corestoreDatabase`: its credential cannot be addressed to any host a workload reaches, so it is
135
137
  * neither granted nor published, and the workload is not started with a guessed one.
138
+ *
139
+ * The two `-unselected` refusals name a binding endpoint the workload's network cannot reach, so a
140
+ * workload is refused rather than started against a connection that stays dark. A workload reaches
141
+ * a platform endpoint only when its spec's attached network selects it (`platformEndpointIds`), and
142
+ * an `isolated` network selects none. `database-endpoint-unselected` is an enabled database binding
143
+ * whose node's valid `corestoreDatabase` is not a selected endpoint: none of the selected
144
+ * `IRuntimeNetworkProtectedAuthority.platformEndpoints` has the endpoint's `host` as its `address`,
145
+ * `tcp` as its `protocol` and the endpoint's `port` as its `port`. `objectstorage-endpoint-unselected`
146
+ * is the same for an enabled object-storage binding's node's valid `corestoreObjectStorage`, whose
147
+ * `endpoint` origin states the host and the port (the scheme's default, 80 for `http:` and 443 for
148
+ * `https:`, when it states none). Platform endpoints are IPv4, so an endpoint whose host is a DNS name
149
+ * or an IPv6 literal is never selected. Each is judged only for an endpoint that is valid: a node
150
+ * stating none, or an invalid one, stays `database-endpoint-missing` or `objectstorage-endpoint-missing`.
151
+ * `platform.findUnselectedCorestoreEndpoints` is this rule; a producer answers what it returns.
136
152
  */
137
153
  export type TServiceRuntimeRefusal = typeof serviceRuntimeRefusals[number];
138
154
 
@@ -0,0 +1,79 @@
1
+ import type { IRuntimeNetworkProtectedAuthority } from '../data/networkauthority.js';
2
+ import type { TServiceRuntimeNetwork, TServiceRuntimeRefusal } from '../data/serviceruntime.js';
3
+ import { isNetworkIpv4 } from '../private/networkcontract.js';
4
+ import { validatePlatformDatabaseEndpoint, type IPlatformDatabaseEndpoint } from './database.js';
5
+ import { validatePlatformObjectStorageEndpoint, type IPlatformObjectStorageEndpoint } from './objectstorage.js';
6
+
7
+ /** The service runtime refusals a Corestore endpoint the workload's network does not select raises. */
8
+ export type TCorestoreEndpointSelectionRefusal = Extract<TServiceRuntimeRefusal,
9
+ 'database-endpoint-unselected' | 'objectstorage-endpoint-unselected'>;
10
+
11
+ /** What `findUnselectedCorestoreEndpoints` judges for one node a service's bindings are placed on. */
12
+ export interface ICorestoreEndpointSelection {
13
+ /**
14
+ * The node's `corestoreDatabase` when an enabled database binding of the service is placed on it,
15
+ * otherwise `null`. A node stating none is `database-endpoint-missing`, never judged here.
16
+ */
17
+ database: IPlatformDatabaseEndpoint | null;
18
+ /**
19
+ * The node's `corestoreObjectStorage` when an enabled object-storage binding of the service is
20
+ * placed on it, otherwise `null`. A node stating none is `objectstorage-endpoint-missing`.
21
+ */
22
+ objectStorage: IPlatformObjectStorageEndpoint | null;
23
+ /** The current protected authority's platform endpoints, which the selected ids resolve against. */
24
+ platformEndpoints: IRuntimeNetworkProtectedAuthority['platformEndpoints'];
25
+ /** The runtime spec's network: `attached` selects its `platformEndpointIds`, `isolated` selects none. */
26
+ network: TServiceRuntimeNetwork;
27
+ }
28
+
29
+ type TPlatformEndpoint = IRuntimeNetworkProtectedAuthority['platformEndpoints'][number];
30
+
31
+ const selectedPlatformEndpoints = (selectionArg: ICorestoreEndpointSelection): TPlatformEndpoint[] => {
32
+ if (selectionArg.network.mode !== 'attached') return [];
33
+ const selectedIds = new Set(selectionArg.network.platformEndpointIds);
34
+ return selectionArg.platformEndpoints.filter((endpointArg) => selectedIds.has(endpointArg.id));
35
+ };
36
+
37
+ /** Platform endpoints are IPv4, so only a canonical IPv4 host can be one; a DNS name or IPv6 literal never is. */
38
+ const selectsTcp = (endpointsArg: TPlatformEndpoint[], hostArg: string, portArg: number): boolean => (
39
+ isNetworkIpv4(hostArg) && endpointsArg.some((endpointArg) => endpointArg.address === hostArg
40
+ && endpointArg.protocol === 'tcp' && endpointArg.port === portArg)
41
+ );
42
+
43
+ /** The origin's port, or the scheme's default (80 for `http:`, 443 for `https:`) when it states none. */
44
+ const objectStorageOriginPort = (originArg: URL): number => (
45
+ originArg.port === '' ? (originArg.protocol === 'https:' ? 443 : 80) : Number(originArg.port)
46
+ );
47
+
48
+ /**
49
+ * Which of a node's Corestore endpoints a service's workload would dial without its network
50
+ * selecting them, as the service runtime refusals that name them; empty when none.
51
+ *
52
+ * An endpoint is selected when one of the spec's selected platform endpoints has the endpoint's host
53
+ * as its `address`, `tcp` as its `protocol` and the endpoint's port as its `port`: the database
54
+ * endpoint's `host` and `port`, and the object-storage endpoint origin's host and port, or 80 for
55
+ * `http:` and 443 for `https:` when the origin states none. An `isolated` network selects nothing, a
56
+ * selected id the protected authority does not hold selects nothing, and a DNS-name or IPv6 host is
57
+ * never selected. Only a valid endpoint is judged: one `validatePlatformDatabaseEndpoint` or
58
+ * `validatePlatformObjectStorageEndpoint` refuses is the `-missing` refusal's, not this one's. The
59
+ * answer is sorted, so it does not depend on the order of the inputs.
60
+ */
61
+ export const findUnselectedCorestoreEndpoints = (
62
+ selectionArg: ICorestoreEndpointSelection,
63
+ ): TCorestoreEndpointSelectionRefusal[] => {
64
+ const selected = selectedPlatformEndpoints(selectionArg);
65
+ const refusals: TCorestoreEndpointSelectionRefusal[] = [];
66
+ const database = selectionArg.database;
67
+ if (database !== null && validatePlatformDatabaseEndpoint(database).length === 0
68
+ && !selectsTcp(selected, database.host, database.port)) {
69
+ refusals.push('database-endpoint-unselected');
70
+ }
71
+ const objectStorage = selectionArg.objectStorage;
72
+ if (objectStorage !== null && validatePlatformObjectStorageEndpoint(objectStorage).length === 0) {
73
+ const origin = new URL(objectStorage.endpoint);
74
+ if (!selectsTcp(selected, origin.hostname, objectStorageOriginPort(origin))) {
75
+ refusals.push('objectstorage-endpoint-unselected');
76
+ }
77
+ }
78
+ return refusals;
79
+ };
@@ -1,5 +1,6 @@
1
1
  import * as ai from './ai.js';
2
2
  import * as backup from './backup.js';
3
+ import * as corestoreendpointselection from './corestoreendpointselection.js';
3
4
  import * as database from './database.js';
4
5
  import * as email from './email.js';
5
6
  import * as letter from './letter.js';
@@ -17,6 +18,7 @@ import * as types from './types.js';
17
18
  export {
18
19
  ai,
19
20
  backup,
21
+ corestoreendpointselection,
20
22
  database,
21
23
  email,
22
24
  letter,
@@ -32,6 +34,7 @@ export {
32
34
  types,
33
35
  };
34
36
 
37
+ export * from './corestoreendpointselection.js';
35
38
  export * from './database.js';
36
39
  export * from './storage.js';
37
40
  export * from './objectstorage.js';
@@ -102,6 +102,10 @@ export const validatePlatformObjectStorageEndpoint = (
102
102
  errors.push(`${pathArg} endpoint must be a canonical origin without a path, query, `
103
103
  + 'fragment, trailing slash or default port');
104
104
  }
105
+ // A URL parser refuses a port above 65535 but reads port 0 as stated; no workload dials it.
106
+ if (parsedEndpoint.port !== '' && Number(parsedEndpoint.port) < 1) {
107
+ errors.push(`${pathArg} endpoint port must be from 1 to 65535`);
108
+ }
105
109
  }
106
110
  }
107
111
  if (typeof valueArg.region !== 'string'
@@ -209,10 +209,14 @@ export const networkHash = async (domain: string, value: { digest: TSha256Digest
209
209
  const { digest: omitted, ...payload } = value;
210
210
  return `sha256:${await createCanonicalJsonSha256Hex(networkCanonical({ domain, payload }), failNetwork)}`;
211
211
  };
212
- export const networkIpv4 = (value: unknown): string => {
213
- requireNetwork(typeof value === 'string' && value.length <= 15);
212
+ /** A canonical dotted-quad IPv4 literal: four decimal octets, no leading zeros. */
213
+ export const isNetworkIpv4 = (value: unknown): value is string => {
214
+ if (typeof value !== 'string' || value.length > 15) return false;
214
215
  const parts = value.split('.');
215
- requireNetwork(parts.length === 4 && parts.every(part => /^(?:0|[1-9][0-9]{0,2})$/.test(part) && Number(part) <= 255));
216
+ return parts.length === 4 && parts.every(part => /^(?:0|[1-9][0-9]{0,2})$/.test(part) && Number(part) <= 255);
217
+ };
218
+ export const networkIpv4 = (value: unknown): string => {
219
+ requireNetwork(isNetworkIpv4(value));
216
220
  return value;
217
221
  };
218
222
  export const networkIpv4Number = (value: string): number => value.split('.').reduce((sum, part) => sum * 256 + Number(part), 0);
@@ -6,7 +6,9 @@ import type {
6
6
  IPrivateNetwork, IServicePrivateNetworks, ISetServicePrivateNetworks,
7
7
  TPrivateNetworkMutation,
8
8
  } from '../data/privatenetwork.js';
9
- import type { IServicePublishedPorts, ISetServicePublishedPorts } from '../data/networkprojection.js';
9
+ import type {
10
+ IServiceNetworkEgress, IServicePublishedPorts, ISetServiceNetworkEgress, ISetServicePublishedPorts,
11
+ } from '../data/networkprojection.js';
10
12
 
11
13
  /** Current organization picker data. Entries are never cached authorization grants. */
12
14
  export interface IReq_Cloudly_ListOwnedOrganizations extends plugins.typedrequestInterfaces.implementsTR<
@@ -71,7 +73,7 @@ export interface IReq_Cloudly_SetServicePrivateNetworks extends plugins.typedreq
71
73
  response: { membership: IServicePrivateNetworks; replayed: boolean };
72
74
  }
73
75
 
74
- /** null means the service has never had an authorized published-port policy. */
76
+ /** null means the service has never had a policy document, neither published ports nor egress. */
75
77
  export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<
76
78
  plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_GetServicePublishedPorts
77
79
  > {
@@ -80,6 +82,17 @@ export interface IReq_Cloudly_GetServicePublishedPorts extends plugins.typedrequ
80
82
  response: { publishedPorts: IServicePublishedPorts | null };
81
83
  }
82
84
 
85
+ /**
86
+ * Writes the published-port half of one service's policy document, carrying its egress forward.
87
+ *
88
+ * One document per service holds its published ports and its egress under one `revision`. The
89
+ * first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
90
+ * with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
91
+ * no published ports. From then on both reads return it, never null. It is an explicit policy: an
92
+ * attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
93
+ * no public address and no platform endpoint; `network-policy-missing` means the service has no
94
+ * document at all.
95
+ */
83
96
  export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequestInterfaces.implementsTR<
84
97
  plugins.typedrequestInterfaces.ITypedRequest, IReq_Cloudly_SetServicePublishedPorts
85
98
  > {
@@ -93,6 +106,47 @@ export interface IReq_Cloudly_SetServicePublishedPorts extends plugins.typedrequ
93
106
  response: { publishedPorts: IServicePublishedPorts; replayed: boolean };
94
107
  }
95
108
 
109
+ /**
110
+ * The egress half of one service's policy document; null means the service has never had a policy
111
+ * document. Only a verified administrator reads it: platform endpoints are platform authority.
112
+ */
113
+ export interface IReq_Admin_Cloudly_GetServiceNetworkEgress extends plugins.typedrequestInterfaces.implementsTR<
114
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_GetServiceNetworkEgress
115
+ > {
116
+ method: 'getServiceNetworkEgress';
117
+ request: { identity: IIdentityCredential; organizationId: string; serviceId: string };
118
+ response: { networkEgress: IServiceNetworkEgress | null };
119
+ }
120
+
121
+ /**
122
+ * Writes the egress half of one service's policy document, carrying its published ports forward.
123
+ *
124
+ * Only a verified administrator writes it, as only an administrator writes the runtime spec whose
125
+ * attached network must state the same values. `expectedRevision` is the revision of the one
126
+ * document shared with the published ports; any other stored revision refuses the write, and an id
127
+ * that names no current platform endpoint refuses it. A retry of the exact body under the same
128
+ * `mutationId` answers the stored document with `replayed: true`.
129
+ *
130
+ * One document per service holds its published ports and its egress under one `revision`. The
131
+ * first accepted write of either (`expectedRevision: null`) creates it: `setServicePublishedPorts`
132
+ * with egress `publicEgress: false` and `platformEndpointIds: []`, `setServiceNetworkEgress` with
133
+ * no published ports. From then on both reads return it, never null. It is an explicit policy: an
134
+ * attached spec stating `publicEgress: false` and `platformEndpointIds: []` matches it and reaches
135
+ * no public address and no platform endpoint; `network-policy-missing` means the service has no
136
+ * document at all.
137
+ */
138
+ export interface IReq_Admin_Cloudly_SetServiceNetworkEgress extends plugins.typedrequestInterfaces.implementsTR<
139
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_Cloudly_SetServiceNetworkEgress
140
+ > {
141
+ method: 'setServiceNetworkEgress';
142
+ request: { identity: IIdentityCredential; mutation: ISetServiceNetworkEgress };
143
+ /**
144
+ * The accepted document including its `revision`; historical on replay. Acceptance is neither an
145
+ * admitted workload nor applied packet state.
146
+ */
147
+ response: { networkEgress: IServiceNetworkEgress; replayed: boolean };
148
+ }
149
+
96
150
  export interface IRequest_Any_Cloudly_GetNetworkNodes {
97
151
  method: 'getNetworkNodes';
98
152
  request: {};