@serve.zone/interfaces 7.28.0 → 8.0.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.
package/changelog.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-07-04 - 8.0.0
4
+
5
+ ### Breaking Changes
6
+
7
+ - introduce canonical service port routing contracts (service-ports)
8
+ - Add shared service target port, domain route, public port mapping, and normalization helper contracts.
9
+ - Update service and App Store interfaces to support targetPorts, domain target references, and publicPortMappings.
10
+ - Extend gateway and Coretraffic routing contracts with transport-aware port routes, routeRef ownership, and remoteIngress intent.
11
+ - Breaking change: legacy SSH domain routing is no longer represented through domain protocol values; public TCP/UDP exposure now uses publicPortMappings.
12
+
3
13
  ## 2026-06-24 - 7.28.0
4
14
 
5
15
  ### Features
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '7.28.0',
6
+ version: '8.0.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  };
9
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
9
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
@@ -1,3 +1,4 @@
1
+ import type { IServiceDomainRoute, IServicePublicPortMapping, IServiceTargetPort } from '../data/serviceports.js';
1
2
  export type TAppStorePlatformRequirement = 'mongodb' | 's3' | 'clickhouse' | 'valkey' | 'mariadb';
2
3
  export type TAppStoreSourceType = 'inline' | 'repoManifest' | 'dockerImage';
3
4
  export type TAppStoreTrackingMode = 'tag' | 'digest';
@@ -98,7 +99,12 @@ export interface IAppStoreAppMeta {
98
99
  }
99
100
  export interface IAppStoreVersionConfig {
100
101
  image: string;
101
- port: number;
102
+ /** Legacy shorthand for targetPorts.web. New templates should prefer targetPorts. */
103
+ port?: number;
104
+ targetPorts?: IServiceTargetPort[];
105
+ domains?: IServiceDomainRoute[];
106
+ /** Edge/coretraffic public TCP/UDP exposure, distinct from Docker publishedPorts. */
107
+ publicPortMappings?: IServicePublicPortMapping[];
102
108
  envVars?: IAppStoreEnvVar[];
103
109
  volumes?: TAppStoreVolumeSpec[];
104
110
  publishedPorts?: IAppStorePublishedPort[];
@@ -155,8 +161,10 @@ export interface IAppStoreInstallRequest {
155
161
  version?: string;
156
162
  serviceName: string;
157
163
  domain?: string;
164
+ /** Legacy install-time override for the web target port. */
158
165
  port?: number;
159
166
  publishedPorts?: IAppStorePublishedPort[];
167
+ publicPortMappings?: IServicePublicPortMapping[];
160
168
  envVars?: Record<string, string>;
161
169
  }
162
170
  export interface IUpgradeableAppStoreService {
@@ -173,7 +173,10 @@ export interface IGatewayClientOwnership {
173
173
  gatewayClientId?: string;
174
174
  /** App identifier within the owning gateway client platform. */
175
175
  appId: string;
176
- hostname: string;
176
+ /** Hostname ownership for domain routes. Preserved for existing route keys. */
177
+ hostname?: string;
178
+ /** Stable non-domain route key, for example public-port mapping exposure refs. */
179
+ routeRef?: string;
177
180
  }
178
181
  /**
179
182
  * A single forward target of a gateway route.
@@ -187,7 +190,8 @@ export interface IGatewayRouteTarget {
187
190
  */
188
191
  export interface IGatewayRouteMatch {
189
192
  ports: number[];
190
- domains: string[];
193
+ domains?: string[];
194
+ transport?: 'tcp' | 'udp' | 'all';
191
195
  path?: string;
192
196
  }
193
197
  /**
@@ -220,6 +224,10 @@ export interface IGatewayRouteConfig {
220
224
  name: string;
221
225
  match: IGatewayRouteMatch;
222
226
  action: IGatewayRouteAction;
227
+ remoteIngress?: {
228
+ enabled: boolean;
229
+ edgeFilter?: string[];
230
+ };
223
231
  }
224
232
  /**
225
233
  * Result of an idempotent gateway route sync.
@@ -21,6 +21,7 @@ export * from './backup.js';
21
21
  export * from './clusternode.js';
22
22
  export * from './settings.js';
23
23
  export * from './service.js';
24
+ export * from './serviceports.js';
24
25
  export * from './status.js';
25
26
  export * from './taskexecution.js';
26
27
  export * from './traffic.js';
@@ -21,9 +21,10 @@ export * from './backup.js';
21
21
  export * from './clusternode.js';
22
22
  export * from './settings.js';
23
23
  export * from './service.js';
24
+ export * from './serviceports.js';
24
25
  export * from './status.js';
25
26
  export * from './taskexecution.js';
26
27
  export * from './traffic.js';
27
28
  export * from './user.js';
28
29
  export * from './version.js';
29
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsVUFBVSxDQUFDO0FBQ3pCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxjQUFjLENBQUMifQ==
30
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsVUFBVSxDQUFDO0FBQ3pCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGNBQWMsQ0FBQyJ9
@@ -3,6 +3,7 @@ import type { IRegistryTarget } from './registry.js';
3
3
  import type { IAppStorePublishedPort } from '../appstore/index.js';
4
4
  import type { IHostedAppLifecycleState } from './hostedapp.js';
5
5
  import type { IServiceMailConfig } from './mail.js';
6
+ import type { IServiceDomainRoute, IServicePublicPortMapping, IServiceTargetPort } from './serviceports.js';
6
7
  export interface IServiceVolume {
7
8
  /** Stable Docker volume name. If omitted, Coreflow derives one from service id and mount path. */
8
9
  name?: string;
@@ -109,8 +110,21 @@ export interface IService {
109
110
  antiAffinity?: boolean;
110
111
  scaleFactor: number;
111
112
  balancingStrategy: 'round-robin' | 'least-connections';
113
+ /**
114
+ * Canonical backend/container ports for this service. New domain routes
115
+ * and public mappings reference these targets by name or exact port.
116
+ */
117
+ targetPorts?: IServiceTargetPort[];
112
118
  ports: {
119
+ /**
120
+ * Legacy main backend port. New writes should declare targetPorts and
121
+ * keep this only as compatibility shorthand during migration.
122
+ */
113
123
  web: number;
124
+ /**
125
+ * Legacy implicit domain-to-port routes. Runtime code must not treat
126
+ * this as a permissive routing source for new services.
127
+ */
114
128
  custom?: {
115
129
  [domain: string]: string;
116
130
  };
@@ -118,31 +132,10 @@ export interface IService {
118
132
  mail?: IServiceMailConfig;
119
133
  volumes?: IServiceVolume[];
120
134
  publishedPorts?: IAppStorePublishedPort[];
135
+ /** Edge/coretraffic public TCP/UDP exposure, distinct from Docker Swarm publishedPorts. */
136
+ publicPortMappings?: IServicePublicPortMapping[];
121
137
  resources?: IServiceRessources;
122
- domains: {
123
- /**
124
- * Optional domain ID to specify which domain to use
125
- * If not specified, will use the default domain or require manual configuration
126
- */
127
- domainId?: string;
128
- /**
129
- * The subdomain name (e.g., 'api', 'www', '@' for root)
130
- */
131
- name: string;
132
- /**
133
- * The port to expose (defaults to ports.web if not specified)
134
- */
135
- port?: number;
136
- /**
137
- * The protocol for this domain entry
138
- */
139
- protocol?: 'http' | 'https' | 'ssh';
140
- /**
141
- * Gateway source profile for this domain's route (e.g. 'standard'
142
- * for internal-only access). Defaults to the gateway's public profile.
143
- */
144
- sourceProfileRef?: string;
145
- }[];
138
+ domains: IServiceDomainRoute[];
146
139
  deploymentIds: string[];
147
140
  };
148
141
  }
@@ -0,0 +1,76 @@
1
+ export type TServiceTargetProtocol = 'http' | 'https' | 'ssh' | 'tcp' | 'udp' | `custom:${string}`;
2
+ export type TServiceTargetPortRef = string | number;
3
+ export interface IServiceTargetPort {
4
+ name: string;
5
+ port: number;
6
+ portEnd?: number;
7
+ protocol: TServiceTargetProtocol;
8
+ description?: string;
9
+ default?: boolean;
10
+ }
11
+ export interface IServiceDomainRoute {
12
+ domainId?: string;
13
+ name: string;
14
+ targetPort?: TServiceTargetPortRef;
15
+ /**
16
+ * Legacy backend target port. New writes must use targetPort.
17
+ */
18
+ port?: number;
19
+ protocol?: 'http' | 'https';
20
+ sourceProfileRef?: string;
21
+ }
22
+ export interface IServicePublicPortMapping {
23
+ name: string;
24
+ publicPort: number;
25
+ publicPortEnd?: number;
26
+ targetPort: TServiceTargetPortRef;
27
+ protocol: 'tcp' | 'udp';
28
+ sourceProfileRef?: string;
29
+ exclusive: true;
30
+ }
31
+ export interface IResolvedServiceDomainRoute extends IServiceDomainRoute {
32
+ targetPort: TServiceTargetPortRef;
33
+ target: IServiceTargetPort;
34
+ }
35
+ export interface IResolvedServicePublicPortMapping extends IServicePublicPortMapping {
36
+ target: IServiceTargetPort;
37
+ targetPortStart: number;
38
+ targetPortEnd: number;
39
+ publicPortEnd: number;
40
+ }
41
+ export interface IServicePortSourceData {
42
+ name?: string;
43
+ ports?: {
44
+ web?: number;
45
+ custom?: Record<string, string>;
46
+ };
47
+ targetPorts?: IServiceTargetPort[];
48
+ domains?: IServiceDomainRoute[];
49
+ publicPortMappings?: IServicePublicPortMapping[];
50
+ }
51
+ export interface INormalizedServicePortConfig {
52
+ targetPorts: IServiceTargetPort[];
53
+ defaultTargetPort?: IServiceTargetPort;
54
+ domains: IResolvedServiceDomainRoute[];
55
+ publicPortMappings: IResolvedServicePublicPortMapping[];
56
+ }
57
+ export interface IServicePortNormalizationOptions {
58
+ /**
59
+ * Accept legacy domains[].port only when it resolves to a declared target.
60
+ * Defaults to true for staged migration reads.
61
+ */
62
+ allowLegacyDomainPort?: boolean;
63
+ /**
64
+ * Domain routes are HTTP(S) virtual-host routes. Defaults to true.
65
+ */
66
+ requireHttpDomainTargets?: boolean;
67
+ }
68
+ export declare function normalizeServicePortConfig(serviceDataArg: IServicePortSourceData, optionsArg?: IServicePortNormalizationOptions): INormalizedServicePortConfig;
69
+ export declare function normalizeServicePortData<T extends IServicePortSourceData>(serviceDataArg: T, optionsArg?: IServicePortNormalizationOptions): T & {
70
+ targetPorts: IServiceTargetPort[];
71
+ domains?: IServiceDomainRoute[];
72
+ publicPortMappings?: IServicePublicPortMapping[];
73
+ };
74
+ export declare function resolveServiceTargetPort(targetPortsArg: IServiceTargetPort[], refArg: TServiceTargetPortRef): IServiceTargetPort | undefined;
75
+ export declare function resolveDefaultServiceTargetPort(targetPortsArg: IServiceTargetPort[]): IServiceTargetPort | undefined;
76
+ export declare function isHttpServiceTargetProtocol(protocolArg: TServiceTargetProtocol): boolean;
@@ -0,0 +1,210 @@
1
+ export function normalizeServicePortConfig(serviceDataArg, optionsArg = {}) {
2
+ const options = {
3
+ allowLegacyDomainPort: optionsArg.allowLegacyDomainPort ?? true,
4
+ requireHttpDomainTargets: optionsArg.requireHttpDomainTargets ?? true,
5
+ };
6
+ const serviceName = serviceDataArg.name || 'service';
7
+ const targetPorts = normalizeServiceTargetPorts(serviceDataArg, serviceName);
8
+ const defaultTargetPort = resolveDefaultServiceTargetPort(targetPorts);
9
+ const domains = (serviceDataArg.domains || []).map((domainArg) => {
10
+ const target = resolveServiceDomainTarget(domainArg, targetPorts, defaultTargetPort, serviceName, options);
11
+ if (options.requireHttpDomainTargets && !isHttpServiceTargetProtocol(target.protocol)) {
12
+ throw new Error(`Domain ${domainArg.name} on ${serviceName} references non-HTTP target '${target.name}' (${target.protocol})`);
13
+ }
14
+ if (target.portEnd && target.portEnd !== target.port) {
15
+ throw new Error(`Domain ${domainArg.name} on ${serviceName} references ranged target '${target.name}'`);
16
+ }
17
+ const normalizedDomain = {
18
+ ...domainArg,
19
+ targetPort: domainArg.targetPort ?? target.name,
20
+ target,
21
+ };
22
+ delete normalizedDomain.port;
23
+ return normalizedDomain;
24
+ });
25
+ const publicPortMappings = normalizeServicePublicPortMappings(serviceDataArg.publicPortMappings || [], targetPorts, serviceName);
26
+ return {
27
+ targetPorts,
28
+ defaultTargetPort,
29
+ domains,
30
+ publicPortMappings,
31
+ };
32
+ }
33
+ export function normalizeServicePortData(serviceDataArg, optionsArg = {}) {
34
+ const normalized = normalizeServicePortConfig(serviceDataArg, optionsArg);
35
+ return {
36
+ ...serviceDataArg,
37
+ targetPorts: normalized.targetPorts,
38
+ domains: normalized.domains.map(({ target, ...domainArg }) => domainArg),
39
+ publicPortMappings: normalized.publicPortMappings.map(({ target, targetPortStart, targetPortEnd, ...mappingArg }) => mappingArg),
40
+ };
41
+ }
42
+ export function resolveServiceTargetPort(targetPortsArg, refArg) {
43
+ if (typeof refArg === 'string') {
44
+ const normalizedRef = refArg.trim().toLowerCase();
45
+ return targetPortsArg.find((targetArg) => targetArg.name.trim().toLowerCase() === normalizedRef);
46
+ }
47
+ return targetPortsArg.find((targetArg) => targetArg.port === refArg && !targetArg.portEnd);
48
+ }
49
+ export function resolveDefaultServiceTargetPort(targetPortsArg) {
50
+ const explicitDefaults = targetPortsArg.filter((targetArg) => targetArg.default);
51
+ if (explicitDefaults.length > 1) {
52
+ throw new Error(`Multiple default target ports declared: ${explicitDefaults.map((targetArg) => targetArg.name).join(', ')}`);
53
+ }
54
+ return explicitDefaults[0]
55
+ || targetPortsArg.find((targetArg) => targetArg.name === 'web')
56
+ || targetPortsArg.find((targetArg) => isHttpServiceTargetProtocol(targetArg.protocol))
57
+ || targetPortsArg[0];
58
+ }
59
+ export function isHttpServiceTargetProtocol(protocolArg) {
60
+ return protocolArg === 'http' || protocolArg === 'https';
61
+ }
62
+ function normalizeServiceTargetPorts(serviceDataArg, serviceNameArg) {
63
+ const declaredTargets = [...(serviceDataArg.targetPorts || [])];
64
+ const legacyWebPort = Number(serviceDataArg.ports?.web);
65
+ if (declaredTargets.length === 0 && Number.isInteger(legacyWebPort) && legacyWebPort > 0) {
66
+ declaredTargets.push({
67
+ name: 'web',
68
+ port: legacyWebPort,
69
+ protocol: 'http',
70
+ default: true,
71
+ });
72
+ }
73
+ if (declaredTargets.length === 0) {
74
+ return [];
75
+ }
76
+ const seenNames = new Set();
77
+ const seenTuples = new Set();
78
+ const normalizedTargets = declaredTargets.map((targetArg) => {
79
+ const name = targetArg.name?.trim();
80
+ if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/.test(name)) {
81
+ throw new Error(`Invalid target port name '${targetArg.name}' on ${serviceNameArg}`);
82
+ }
83
+ const normalizedName = name.toLowerCase();
84
+ if (seenNames.has(normalizedName)) {
85
+ throw new Error(`Duplicate target port name '${name}' on ${serviceNameArg}`);
86
+ }
87
+ seenNames.add(normalizedName);
88
+ const port = assertValidServicePort(targetArg.port, `target port ${name} on ${serviceNameArg}`);
89
+ const portEnd = targetArg.portEnd === undefined
90
+ ? undefined
91
+ : assertValidServicePort(targetArg.portEnd, `target port end ${name} on ${serviceNameArg}`);
92
+ if (portEnd !== undefined && portEnd < port) {
93
+ throw new Error(`target port range '${name}' on ${serviceNameArg} must be ascending`);
94
+ }
95
+ const protocol = targetArg.protocol || 'tcp';
96
+ const tuple = `${protocol}:${port}:${portEnd || port}`;
97
+ if (seenTuples.has(tuple)) {
98
+ throw new Error(`Duplicate target port tuple ${tuple} on ${serviceNameArg}`);
99
+ }
100
+ seenTuples.add(tuple);
101
+ return {
102
+ ...targetArg,
103
+ name,
104
+ port,
105
+ portEnd,
106
+ protocol,
107
+ };
108
+ });
109
+ if (!normalizedTargets.some((targetArg) => targetArg.default)) {
110
+ const webTarget = normalizedTargets.find((targetArg) => targetArg.name === 'web');
111
+ if (webTarget) {
112
+ webTarget.default = true;
113
+ }
114
+ }
115
+ return normalizedTargets;
116
+ }
117
+ function resolveServiceDomainTarget(domainArg, targetPortsArg, defaultTargetPortArg, serviceNameArg, optionsArg) {
118
+ const domainName = domainArg.name || '(unnamed domain)';
119
+ if (!targetPortsArg.length) {
120
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} requires at least one declared target port`);
121
+ }
122
+ if (domainArg.targetPort !== undefined) {
123
+ const target = resolveServiceTargetPort(targetPortsArg, domainArg.targetPort);
124
+ if (!target) {
125
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} references undeclared targetPort '${domainArg.targetPort}'`);
126
+ }
127
+ return target;
128
+ }
129
+ if (domainArg.port !== undefined) {
130
+ if (!optionsArg.allowLegacyDomainPort) {
131
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} uses legacy port; use targetPort instead`);
132
+ }
133
+ const legacyPort = Number(domainArg.port);
134
+ const target = resolveServiceTargetPort(targetPortsArg, legacyPort);
135
+ if (!target) {
136
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} references undeclared legacy port ${domainArg.port}`);
137
+ }
138
+ return target;
139
+ }
140
+ if (!defaultTargetPortArg) {
141
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} has no targetPort and no default target port exists`);
142
+ }
143
+ return defaultTargetPortArg;
144
+ }
145
+ function normalizeServicePublicPortMappings(mappingsArg, targetPortsArg, serviceNameArg) {
146
+ const seenNames = new Set();
147
+ const seenPublicPorts = new Set();
148
+ return mappingsArg.map((mappingArg) => {
149
+ const name = mappingArg.name?.trim();
150
+ if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/.test(name)) {
151
+ throw new Error(`Invalid public port mapping name '${mappingArg.name}' on ${serviceNameArg}`);
152
+ }
153
+ const normalizedName = name.toLowerCase();
154
+ if (seenNames.has(normalizedName)) {
155
+ throw new Error(`Duplicate public port mapping '${name}' on ${serviceNameArg}`);
156
+ }
157
+ seenNames.add(normalizedName);
158
+ if (mappingArg.exclusive !== true) {
159
+ throw new Error(`Public port mapping '${name}' on ${serviceNameArg} must be exclusive`);
160
+ }
161
+ if (!['tcp', 'udp'].includes(mappingArg.protocol)) {
162
+ throw new Error(`Public port mapping '${name}' on ${serviceNameArg} has unsupported protocol '${mappingArg.protocol}'`);
163
+ }
164
+ const target = resolveServiceTargetPort(targetPortsArg, mappingArg.targetPort);
165
+ if (!target) {
166
+ throw new Error(`Public port mapping '${name}' on ${serviceNameArg} references undeclared targetPort '${mappingArg.targetPort}'`);
167
+ }
168
+ const publicPort = assertValidServicePort(mappingArg.publicPort, `public port ${name} on ${serviceNameArg}`);
169
+ const targetStart = target.port;
170
+ const targetEnd = target.portEnd || target.port;
171
+ const publicPortEnd = mappingArg.publicPortEnd === undefined
172
+ ? publicPort + (targetEnd - targetStart)
173
+ : assertValidServicePort(mappingArg.publicPortEnd, `public port end ${name} on ${serviceNameArg}`);
174
+ if (publicPortEnd < publicPort) {
175
+ throw new Error(`Public port mapping '${name}' on ${serviceNameArg} must have an ascending public range`);
176
+ }
177
+ if ((publicPortEnd - publicPort) !== (targetEnd - targetStart)) {
178
+ throw new Error(`Public and target port ranges must have the same length for mapping '${name}' on ${serviceNameArg}`);
179
+ }
180
+ for (let offset = 0; offset <= publicPortEnd - publicPort; offset++) {
181
+ const key = `${mappingArg.protocol}:${publicPort + offset}`;
182
+ if (seenPublicPorts.has(key)) {
183
+ throw new Error(`Duplicate public port ${publicPort + offset}/${mappingArg.protocol} on ${serviceNameArg}`);
184
+ }
185
+ seenPublicPorts.add(key);
186
+ }
187
+ return {
188
+ ...mappingArg,
189
+ name,
190
+ publicPort,
191
+ publicPortEnd,
192
+ protocol: mappingArg.protocol,
193
+ target,
194
+ targetPortStart: targetStart,
195
+ targetPortEnd: targetEnd,
196
+ };
197
+ }).sort((a, b) => {
198
+ return a.protocol.localeCompare(b.protocol)
199
+ || a.publicPort - b.publicPort
200
+ || a.name.localeCompare(b.name);
201
+ });
202
+ }
203
+ function assertValidServicePort(portArg, labelArg) {
204
+ const port = Number(portArg);
205
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
206
+ throw new Error(`Invalid ${labelArg}: ${portArg}. Expected an integer between 1 and 65535.`);
207
+ }
208
+ return port;
209
+ }
210
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZXBvcnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9zZXJ2aWNlcG9ydHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBbUZBLE1BQU0sVUFBVSwwQkFBMEIsQ0FDeEMsY0FBc0MsRUFDdEMsYUFBK0MsRUFBRTtJQUVqRCxNQUFNLE9BQU8sR0FBK0M7UUFDMUQscUJBQXFCLEVBQUUsVUFBVSxDQUFDLHFCQUFxQixJQUFJLElBQUk7UUFDL0Qsd0JBQXdCLEVBQUUsVUFBVSxDQUFDLHdCQUF3QixJQUFJLElBQUk7S0FDdEUsQ0FBQztJQUNGLE1BQU0sV0FBVyxHQUFHLGNBQWMsQ0FBQyxJQUFJLElBQUksU0FBUyxDQUFDO0lBQ3JELE1BQU0sV0FBVyxHQUFHLDJCQUEyQixDQUFDLGNBQWMsRUFBRSxXQUFXLENBQUMsQ0FBQztJQUM3RSxNQUFNLGlCQUFpQixHQUFHLCtCQUErQixDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBQ3ZFLE1BQU0sT0FBTyxHQUFHLENBQUMsY0FBYyxDQUFDLE9BQU8sSUFBSSxFQUFFLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxTQUFTLEVBQUUsRUFBRTtRQUMvRCxNQUFNLE1BQU0sR0FBRywwQkFBMEIsQ0FBQyxTQUFTLEVBQUUsV0FBVyxFQUFFLGlCQUFpQixFQUFFLFdBQVcsRUFBRSxPQUFPLENBQUMsQ0FBQztRQUMzRyxJQUFJLE9BQU8sQ0FBQyx3QkFBd0IsSUFBSSxDQUFDLDJCQUEyQixDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO1lBQ3RGLE1BQU0sSUFBSSxLQUFLLENBQUMsVUFBVSxTQUFTLENBQUMsSUFBSSxPQUFPLFdBQVcsZ0NBQWdDLE1BQU0sQ0FBQyxJQUFJLE1BQU0sTUFBTSxDQUFDLFFBQVEsR0FBRyxDQUFDLENBQUM7UUFDakksQ0FBQztRQUNELElBQUksTUFBTSxDQUFDLE9BQU8sSUFBSSxNQUFNLENBQUMsT0FBTyxLQUFLLE1BQU0sQ0FBQyxJ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
@@ -1,4 +1,11 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  interface IReverseProxyConfig extends plugins.tsclass.network.IReverseProxyConfig {
3
3
  }
4
+ export interface ICoretrafficPortRouteConfig {
5
+ name: string;
6
+ listenPort: number;
7
+ transport: 'tcp' | 'udp';
8
+ destinationIps: string[];
9
+ destinationPort: number;
10
+ }
4
11
  export { type IReverseProxyConfig };
@@ -1,3 +1,3 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  export {};
3
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhZmZpYy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvdHJhZmZpYy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQUl6QyxPQUFPLEVBQTRCLENBQUMifQ==
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhZmZpYy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvdHJhZmZpYy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGVBQWUsQ0FBQztBQVl6QyxPQUFPLEVBQTRCLENBQUMifQ==
@@ -1,8 +1,9 @@
1
- import { type IReverseProxyConfig } from '../data/traffic.js';
1
+ import { type ICoretrafficPortRouteConfig, type IReverseProxyConfig } from '../data/traffic.js';
2
2
  export interface IRequest_Coreflow_Coretraffic_RoutingUpdate {
3
3
  method: 'updateRouting';
4
4
  request: {
5
5
  reverseConfigs: IReverseProxyConfig[];
6
+ portRoutes?: ICoretrafficPortRouteConfig[];
6
7
  };
7
8
  response: {
8
9
  status: 'ok' | 'error';
@@ -19,6 +20,7 @@ export interface IRequest_Coretraffic_Coreflow_GetCurrentRouting {
19
20
  request: Record<string, never>;
20
21
  response: {
21
22
  reverseConfigs: IReverseProxyConfig[];
23
+ portRoutes?: ICoretrafficPortRouteConfig[];
22
24
  };
23
25
  }
24
26
  /**
@@ -1,2 +1,2 @@
1
1
  import {} from '../data/traffic.js';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGluZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3JlcXVlc3RzL3JvdXRpbmcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUE0QixNQUFNLG9CQUFvQixDQUFDIn0=
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGluZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3JlcXVlc3RzL3JvdXRpbmcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUE4RCxNQUFNLG9CQUFvQixDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "7.28.0",
3
+ "version": "8.0.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
@@ -17,7 +17,7 @@
17
17
  "devDependencies": {
18
18
  "@git.zone/tsbuild": "^4.4.2",
19
19
  "@git.zone/tsdoc": "^2.1.1",
20
- "@git.zone/tsrun": "^2.0.4",
20
+ "@git.zone/tsrun": "^2.0.5",
21
21
  "@git.zone/tstest": "^3.6.6",
22
22
  "@types/node": "^25.9.2"
23
23
  },
package/readme.md CHANGED
@@ -52,13 +52,36 @@ const service: data.IService = {
52
52
  deploymentStrategy: 'limited-replicas',
53
53
  scaleFactor: 2,
54
54
  balancingStrategy: 'round-robin',
55
+ targetPorts: [
56
+ {
57
+ name: 'web',
58
+ port: 3000,
59
+ protocol: 'http',
60
+ default: true,
61
+ },
62
+ {
63
+ name: 'ssh',
64
+ port: 2222,
65
+ protocol: 'ssh',
66
+ },
67
+ ],
55
68
  ports: {
56
- web: 3000,
69
+ web: 3000, // legacy compatibility shorthand during migration
57
70
  },
58
71
  domains: [
59
72
  {
60
73
  name: 'api',
61
74
  protocol: 'https',
75
+ targetPort: 'web',
76
+ },
77
+ ],
78
+ publicPortMappings: [
79
+ {
80
+ name: 'ssh-public',
81
+ publicPort: 2222,
82
+ targetPort: 'ssh',
83
+ protocol: 'tcp',
84
+ exclusive: true,
62
85
  },
63
86
  ],
64
87
  deploymentIds: [],
@@ -70,13 +93,29 @@ Common data contracts include:
70
93
 
71
94
  - `ICluster` and `IClusterNode` for cluster membership and provisioning state.
72
95
  - `IService`, `IDeployment`, `IImage`, `IRegistryTarget`, and `IExternalRegistry` for workload delivery.
96
+ - Service port contracts including `IServiceTargetPort`, `IServiceDomainRoute`, and `IServicePublicPortMapping` for canonical backend targets, domain target references, and edge/Coretraffic TCP/UDP public exposure.
73
97
  - `IDomain`, `IDnsEntry`, and traffic contracts for routing and DNS management.
98
+ - Traffic and gateway route contracts including `ICoretrafficPortRouteConfig`, routing `portRoutes`, gateway route `routeRef` ownership, optional match `domains`, `transport`, and `remoteIngress` route intent.
74
99
  - `ISecretBundle` and `ISecretGroup` for secret ownership and shared secret groups.
75
100
  - Mail gateway contracts for domain authorities, address bindings, WorkApp bindings, managed SMTP/API credentials, spool items, delivery journals, and inbound/outbound message payloads.
76
101
  - Service-level mail configuration through `IService.data.mail`, including per-address inbound `smtpForward` settings and outbound credential metadata. Cloudly settings include dcrouter gateway, SMTP submission, and inbound forward-target keys for reconciling those bindings.
77
102
  - `IUser`, `IIdentity`, and token-related contracts for authentication context.
78
103
  - `ICloudlyConfig`, `ICloudlySettings`, status, server, bare-metal, BaseOS, backup, and task execution interfaces for control-plane state.
79
104
 
105
+ Shared service port helpers are exported from `data` so Cloudly, App Store resolution, Coreflow, Coretraffic, Onebox, and dcrouter agree on the same normalization rules:
106
+
107
+ ```typescript
108
+ const normalizedPorts = data.normalizeServicePortConfig(service.data);
109
+ const defaultTarget = data.resolveDefaultServiceTargetPort(normalizedPorts.targetPorts);
110
+ const webTarget = data.resolveServiceTargetPort(normalizedPorts.targetPorts, 'web');
111
+
112
+ if (webTarget && data.isHttpServiceTargetProtocol(webTarget.protocol)) {
113
+ // safe to use as a domain route target
114
+ }
115
+ ```
116
+
117
+ `normalizeServicePortData()` returns service data with canonical `targetPorts`, domain `targetPort` refs, and `publicPortMappings` while removing legacy domain `port` fields from normalized writes.
118
+
80
119
  ## TypedRequest Contracts
81
120
 
82
121
  Use `requests` when registering handlers with `@api.global/typedrequest` or when creating typed requests through a TypedSocket client.
@@ -111,16 +150,19 @@ Request groups are exported by product area:
111
150
  - `requests.certificate`
112
151
  - `requests.cluster`
113
152
  - `requests.config`
153
+ - `requests.corestore`
114
154
  - `requests.deployment`
115
155
  - `requests.dns`
116
156
  - `requests.domain`
117
157
  - `requests.externalRegistry`
158
+ - `requests.gateway`
118
159
  - `requests.hostedapp`
119
160
  - `requests.identity`
120
161
  - `requests.image`
121
162
  - `requests.inform`
122
163
  - `requests.log`
123
164
  - `requests.mail`
165
+ - `requests.migration`
124
166
  - `requests.network`
125
167
  - `requests.node`
126
168
  - `requests.platform`
@@ -181,7 +223,7 @@ Good candidates for this package:
181
223
  Poor candidates for this package:
182
224
 
183
225
  - Private implementation details of one service.
184
- - Runtime helpers or convenience wrappers.
226
+ - Runtime helpers or convenience wrappers, unless they are shared contract normalization or validation helpers used by multiple packages.
185
227
  - Compatibility aliases without an active consumer.
186
228
 
187
229
  ## Development
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '7.28.0',
6
+ version: '8.0.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -1,3 +1,9 @@
1
+ import type {
2
+ IServiceDomainRoute,
3
+ IServicePublicPortMapping,
4
+ IServiceTargetPort,
5
+ } from '../data/serviceports.js';
6
+
1
7
  export type TAppStorePlatformRequirement = 'mongodb' | 's3' | 'clickhouse' | 'valkey' | 'mariadb';
2
8
  export type TAppStoreSourceType = 'inline' | 'repoManifest' | 'dockerImage';
3
9
  export type TAppStoreTrackingMode = 'tag' | 'digest';
@@ -114,7 +120,12 @@ export interface IAppStoreAppMeta {
114
120
 
115
121
  export interface IAppStoreVersionConfig {
116
122
  image: string;
117
- port: number;
123
+ /** Legacy shorthand for targetPorts.web. New templates should prefer targetPorts. */
124
+ port?: number;
125
+ targetPorts?: IServiceTargetPort[];
126
+ domains?: IServiceDomainRoute[];
127
+ /** Edge/coretraffic public TCP/UDP exposure, distinct from Docker publishedPorts. */
128
+ publicPortMappings?: IServicePublicPortMapping[];
118
129
  envVars?: IAppStoreEnvVar[];
119
130
  volumes?: TAppStoreVolumeSpec[];
120
131
  publishedPorts?: IAppStorePublishedPort[];
@@ -175,8 +186,10 @@ export interface IAppStoreInstallRequest {
175
186
  version?: string;
176
187
  serviceName: string;
177
188
  domain?: string;
189
+ /** Legacy install-time override for the web target port. */
178
190
  port?: number;
179
191
  publishedPorts?: IAppStorePublishedPort[];
192
+ publicPortMappings?: IServicePublicPortMapping[];
180
193
  envVars?: Record<string, string>;
181
194
  }
182
195
 
@@ -185,7 +185,10 @@ export interface IGatewayClientOwnership {
185
185
  gatewayClientId?: string;
186
186
  /** App identifier within the owning gateway client platform. */
187
187
  appId: string;
188
- hostname: string;
188
+ /** Hostname ownership for domain routes. Preserved for existing route keys. */
189
+ hostname?: string;
190
+ /** Stable non-domain route key, for example public-port mapping exposure refs. */
191
+ routeRef?: string;
189
192
  }
190
193
 
191
194
  /**
@@ -201,7 +204,8 @@ export interface IGatewayRouteTarget {
201
204
  */
202
205
  export interface IGatewayRouteMatch {
203
206
  ports: number[];
204
- domains: string[];
207
+ domains?: string[];
208
+ transport?: 'tcp' | 'udp' | 'all';
205
209
  path?: string;
206
210
  }
207
211
 
@@ -237,6 +241,10 @@ export interface IGatewayRouteConfig {
237
241
  name: string;
238
242
  match: IGatewayRouteMatch;
239
243
  action: IGatewayRouteAction;
244
+ remoteIngress?: {
245
+ enabled: boolean;
246
+ edgeFilter?: string[];
247
+ };
240
248
  }
241
249
 
242
250
  /**
package/ts/data/index.ts CHANGED
@@ -21,6 +21,7 @@ export * from './backup.js';
21
21
  export * from './clusternode.js';
22
22
  export * from './settings.js';
23
23
  export * from './service.js';
24
+ export * from './serviceports.js';
24
25
  export * from './status.js';
25
26
  export * from './taskexecution.js';
26
27
  export * from './traffic.js';
@@ -3,6 +3,11 @@ import type { IRegistryTarget } from './registry.js';
3
3
  import type { IAppStorePublishedPort } from '../appstore/index.js';
4
4
  import type { IHostedAppLifecycleState } from './hostedapp.js';
5
5
  import type { IServiceMailConfig } from './mail.js';
6
+ import type {
7
+ IServiceDomainRoute,
8
+ IServicePublicPortMapping,
9
+ IServiceTargetPort,
10
+ } from './serviceports.js';
6
11
 
7
12
  export interface IServiceVolume {
8
13
  /** Stable Docker volume name. If omitted, Coreflow derives one from service id and mount path. */
@@ -119,38 +124,30 @@ export interface IService {
119
124
 
120
125
  scaleFactor: number;
121
126
  balancingStrategy: 'round-robin' | 'least-connections';
127
+ /**
128
+ * Canonical backend/container ports for this service. New domain routes
129
+ * and public mappings reference these targets by name or exact port.
130
+ */
131
+ targetPorts?: IServiceTargetPort[];
122
132
  ports: {
133
+ /**
134
+ * Legacy main backend port. New writes should declare targetPorts and
135
+ * keep this only as compatibility shorthand during migration.
136
+ */
123
137
  web: number;
138
+ /**
139
+ * Legacy implicit domain-to-port routes. Runtime code must not treat
140
+ * this as a permissive routing source for new services.
141
+ */
124
142
  custom?: { [domain: string]: string };
125
143
  };
126
144
  mail?: IServiceMailConfig;
127
145
  volumes?: IServiceVolume[];
128
146
  publishedPorts?: IAppStorePublishedPort[];
147
+ /** Edge/coretraffic public TCP/UDP exposure, distinct from Docker Swarm publishedPorts. */
148
+ publicPortMappings?: IServicePublicPortMapping[];
129
149
  resources?: IServiceRessources;
130
- domains: {
131
- /**
132
- * Optional domain ID to specify which domain to use
133
- * If not specified, will use the default domain or require manual configuration
134
- */
135
- domainId?: string;
136
- /**
137
- * The subdomain name (e.g., 'api', 'www', '@' for root)
138
- */
139
- name: string;
140
- /**
141
- * The port to expose (defaults to ports.web if not specified)
142
- */
143
- port?: number;
144
- /**
145
- * The protocol for this domain entry
146
- */
147
- protocol?: 'http' | 'https' | 'ssh';
148
- /**
149
- * Gateway source profile for this domain's route (e.g. 'standard'
150
- * for internal-only access). Defaults to the gateway's public profile.
151
- */
152
- sourceProfileRef?: string;
153
- }[];
150
+ domains: IServiceDomainRoute[];
154
151
  deploymentIds: string[];
155
152
  };
156
153
  }
@@ -0,0 +1,338 @@
1
+ export type TServiceTargetProtocol =
2
+ | 'http'
3
+ | 'https'
4
+ | 'ssh'
5
+ | 'tcp'
6
+ | 'udp'
7
+ | `custom:${string}`;
8
+
9
+ export type TServiceTargetPortRef = string | number;
10
+
11
+ export interface IServiceTargetPort {
12
+ name: string;
13
+ port: number;
14
+ portEnd?: number;
15
+ protocol: TServiceTargetProtocol;
16
+ description?: string;
17
+ default?: boolean;
18
+ }
19
+
20
+ export interface IServiceDomainRoute {
21
+ domainId?: string;
22
+ name: string;
23
+ targetPort?: TServiceTargetPortRef;
24
+ /**
25
+ * Legacy backend target port. New writes must use targetPort.
26
+ */
27
+ port?: number;
28
+ protocol?: 'http' | 'https';
29
+ sourceProfileRef?: string;
30
+ }
31
+
32
+ export interface IServicePublicPortMapping {
33
+ name: string;
34
+ publicPort: number;
35
+ publicPortEnd?: number;
36
+ targetPort: TServiceTargetPortRef;
37
+ protocol: 'tcp' | 'udp';
38
+ sourceProfileRef?: string;
39
+ exclusive: true;
40
+ }
41
+
42
+ export interface IResolvedServiceDomainRoute extends IServiceDomainRoute {
43
+ targetPort: TServiceTargetPortRef;
44
+ target: IServiceTargetPort;
45
+ }
46
+
47
+ export interface IResolvedServicePublicPortMapping extends IServicePublicPortMapping {
48
+ target: IServiceTargetPort;
49
+ targetPortStart: number;
50
+ targetPortEnd: number;
51
+ publicPortEnd: number;
52
+ }
53
+
54
+ export interface IServicePortSourceData {
55
+ name?: string;
56
+ ports?: {
57
+ web?: number;
58
+ custom?: Record<string, string>;
59
+ };
60
+ targetPorts?: IServiceTargetPort[];
61
+ domains?: IServiceDomainRoute[];
62
+ publicPortMappings?: IServicePublicPortMapping[];
63
+ }
64
+
65
+ export interface INormalizedServicePortConfig {
66
+ targetPorts: IServiceTargetPort[];
67
+ defaultTargetPort?: IServiceTargetPort;
68
+ domains: IResolvedServiceDomainRoute[];
69
+ publicPortMappings: IResolvedServicePublicPortMapping[];
70
+ }
71
+
72
+ export interface IServicePortNormalizationOptions {
73
+ /**
74
+ * Accept legacy domains[].port only when it resolves to a declared target.
75
+ * Defaults to true for staged migration reads.
76
+ */
77
+ allowLegacyDomainPort?: boolean;
78
+ /**
79
+ * Domain routes are HTTP(S) virtual-host routes. Defaults to true.
80
+ */
81
+ requireHttpDomainTargets?: boolean;
82
+ }
83
+
84
+ export function normalizeServicePortConfig(
85
+ serviceDataArg: IServicePortSourceData,
86
+ optionsArg: IServicePortNormalizationOptions = {},
87
+ ): INormalizedServicePortConfig {
88
+ const options: Required<IServicePortNormalizationOptions> = {
89
+ allowLegacyDomainPort: optionsArg.allowLegacyDomainPort ?? true,
90
+ requireHttpDomainTargets: optionsArg.requireHttpDomainTargets ?? true,
91
+ };
92
+ const serviceName = serviceDataArg.name || 'service';
93
+ const targetPorts = normalizeServiceTargetPorts(serviceDataArg, serviceName);
94
+ const defaultTargetPort = resolveDefaultServiceTargetPort(targetPorts);
95
+ const domains = (serviceDataArg.domains || []).map((domainArg) => {
96
+ const target = resolveServiceDomainTarget(domainArg, targetPorts, defaultTargetPort, serviceName, options);
97
+ if (options.requireHttpDomainTargets && !isHttpServiceTargetProtocol(target.protocol)) {
98
+ throw new Error(`Domain ${domainArg.name} on ${serviceName} references non-HTTP target '${target.name}' (${target.protocol})`);
99
+ }
100
+ if (target.portEnd && target.portEnd !== target.port) {
101
+ throw new Error(`Domain ${domainArg.name} on ${serviceName} references ranged target '${target.name}'`);
102
+ }
103
+ const normalizedDomain: IResolvedServiceDomainRoute = {
104
+ ...domainArg,
105
+ targetPort: domainArg.targetPort ?? target.name,
106
+ target,
107
+ };
108
+ delete normalizedDomain.port;
109
+ return normalizedDomain;
110
+ });
111
+ const publicPortMappings = normalizeServicePublicPortMappings(
112
+ serviceDataArg.publicPortMappings || [],
113
+ targetPorts,
114
+ serviceName,
115
+ );
116
+ return {
117
+ targetPorts,
118
+ defaultTargetPort,
119
+ domains,
120
+ publicPortMappings,
121
+ };
122
+ }
123
+
124
+ export function normalizeServicePortData<T extends IServicePortSourceData>(
125
+ serviceDataArg: T,
126
+ optionsArg: IServicePortNormalizationOptions = {},
127
+ ): T & {
128
+ targetPorts: IServiceTargetPort[];
129
+ domains?: IServiceDomainRoute[];
130
+ publicPortMappings?: IServicePublicPortMapping[];
131
+ } {
132
+ const normalized = normalizeServicePortConfig(serviceDataArg, optionsArg);
133
+ return {
134
+ ...serviceDataArg,
135
+ targetPorts: normalized.targetPorts,
136
+ domains: normalized.domains.map(({ target, ...domainArg }) => domainArg),
137
+ publicPortMappings: normalized.publicPortMappings.map(({ target, targetPortStart, targetPortEnd, ...mappingArg }) => mappingArg),
138
+ };
139
+ }
140
+
141
+ export function resolveServiceTargetPort(
142
+ targetPortsArg: IServiceTargetPort[],
143
+ refArg: TServiceTargetPortRef,
144
+ ): IServiceTargetPort | undefined {
145
+ if (typeof refArg === 'string') {
146
+ const normalizedRef = refArg.trim().toLowerCase();
147
+ return targetPortsArg.find((targetArg) => targetArg.name.trim().toLowerCase() === normalizedRef);
148
+ }
149
+ return targetPortsArg.find((targetArg) => targetArg.port === refArg && !targetArg.portEnd);
150
+ }
151
+
152
+ export function resolveDefaultServiceTargetPort(
153
+ targetPortsArg: IServiceTargetPort[],
154
+ ): IServiceTargetPort | undefined {
155
+ const explicitDefaults = targetPortsArg.filter((targetArg) => targetArg.default);
156
+ if (explicitDefaults.length > 1) {
157
+ throw new Error(`Multiple default target ports declared: ${explicitDefaults.map((targetArg) => targetArg.name).join(', ')}`);
158
+ }
159
+ return explicitDefaults[0]
160
+ || targetPortsArg.find((targetArg) => targetArg.name === 'web')
161
+ || targetPortsArg.find((targetArg) => isHttpServiceTargetProtocol(targetArg.protocol))
162
+ || targetPortsArg[0];
163
+ }
164
+
165
+ export function isHttpServiceTargetProtocol(protocolArg: TServiceTargetProtocol): boolean {
166
+ return protocolArg === 'http' || protocolArg === 'https';
167
+ }
168
+
169
+ function normalizeServiceTargetPorts(
170
+ serviceDataArg: IServicePortSourceData,
171
+ serviceNameArg: string,
172
+ ): IServiceTargetPort[] {
173
+ const declaredTargets = [...(serviceDataArg.targetPorts || [])];
174
+ const legacyWebPort = Number(serviceDataArg.ports?.web);
175
+ if (declaredTargets.length === 0 && Number.isInteger(legacyWebPort) && legacyWebPort > 0) {
176
+ declaredTargets.push({
177
+ name: 'web',
178
+ port: legacyWebPort,
179
+ protocol: 'http',
180
+ default: true,
181
+ });
182
+ }
183
+ if (declaredTargets.length === 0) {
184
+ return [];
185
+ }
186
+
187
+ const seenNames = new Set<string>();
188
+ const seenTuples = new Set<string>();
189
+ const normalizedTargets = declaredTargets.map((targetArg) => {
190
+ const name = targetArg.name?.trim();
191
+ if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/.test(name)) {
192
+ throw new Error(`Invalid target port name '${targetArg.name}' on ${serviceNameArg}`);
193
+ }
194
+ const normalizedName = name.toLowerCase();
195
+ if (seenNames.has(normalizedName)) {
196
+ throw new Error(`Duplicate target port name '${name}' on ${serviceNameArg}`);
197
+ }
198
+ seenNames.add(normalizedName);
199
+ const port = assertValidServicePort(targetArg.port, `target port ${name} on ${serviceNameArg}`);
200
+ const portEnd = targetArg.portEnd === undefined
201
+ ? undefined
202
+ : assertValidServicePort(targetArg.portEnd, `target port end ${name} on ${serviceNameArg}`);
203
+ if (portEnd !== undefined && portEnd < port) {
204
+ throw new Error(`target port range '${name}' on ${serviceNameArg} must be ascending`);
205
+ }
206
+ const protocol = targetArg.protocol || 'tcp';
207
+ const tuple = `${protocol}:${port}:${portEnd || port}`;
208
+ if (seenTuples.has(tuple)) {
209
+ throw new Error(`Duplicate target port tuple ${tuple} on ${serviceNameArg}`);
210
+ }
211
+ seenTuples.add(tuple);
212
+ return {
213
+ ...targetArg,
214
+ name,
215
+ port,
216
+ portEnd,
217
+ protocol,
218
+ };
219
+ });
220
+
221
+ if (!normalizedTargets.some((targetArg) => targetArg.default)) {
222
+ const webTarget = normalizedTargets.find((targetArg) => targetArg.name === 'web');
223
+ if (webTarget) {
224
+ webTarget.default = true;
225
+ }
226
+ }
227
+
228
+ return normalizedTargets;
229
+ }
230
+
231
+ function resolveServiceDomainTarget(
232
+ domainArg: IServiceDomainRoute,
233
+ targetPortsArg: IServiceTargetPort[],
234
+ defaultTargetPortArg: IServiceTargetPort | undefined,
235
+ serviceNameArg: string,
236
+ optionsArg: Required<IServicePortNormalizationOptions>,
237
+ ): IServiceTargetPort {
238
+ const domainName = domainArg.name || '(unnamed domain)';
239
+ if (!targetPortsArg.length) {
240
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} requires at least one declared target port`);
241
+ }
242
+
243
+ if (domainArg.targetPort !== undefined) {
244
+ const target = resolveServiceTargetPort(targetPortsArg, domainArg.targetPort);
245
+ if (!target) {
246
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} references undeclared targetPort '${domainArg.targetPort}'`);
247
+ }
248
+ return target;
249
+ }
250
+
251
+ if (domainArg.port !== undefined) {
252
+ if (!optionsArg.allowLegacyDomainPort) {
253
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} uses legacy port; use targetPort instead`);
254
+ }
255
+ const legacyPort = Number(domainArg.port);
256
+ const target = resolveServiceTargetPort(targetPortsArg, legacyPort);
257
+ if (!target) {
258
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} references undeclared legacy port ${domainArg.port}`);
259
+ }
260
+ return target;
261
+ }
262
+
263
+ if (!defaultTargetPortArg) {
264
+ throw new Error(`Domain ${domainName} on ${serviceNameArg} has no targetPort and no default target port exists`);
265
+ }
266
+ return defaultTargetPortArg;
267
+ }
268
+
269
+ function normalizeServicePublicPortMappings(
270
+ mappingsArg: IServicePublicPortMapping[],
271
+ targetPortsArg: IServiceTargetPort[],
272
+ serviceNameArg: string,
273
+ ): IResolvedServicePublicPortMapping[] {
274
+ const seenNames = new Set<string>();
275
+ const seenPublicPorts = new Set<string>();
276
+ return mappingsArg.map((mappingArg) => {
277
+ const name = mappingArg.name?.trim();
278
+ if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$/.test(name)) {
279
+ throw new Error(`Invalid public port mapping name '${mappingArg.name}' on ${serviceNameArg}`);
280
+ }
281
+ const normalizedName = name.toLowerCase();
282
+ if (seenNames.has(normalizedName)) {
283
+ throw new Error(`Duplicate public port mapping '${name}' on ${serviceNameArg}`);
284
+ }
285
+ seenNames.add(normalizedName);
286
+ if (mappingArg.exclusive !== true) {
287
+ throw new Error(`Public port mapping '${name}' on ${serviceNameArg} must be exclusive`);
288
+ }
289
+ if (!['tcp', 'udp'].includes(mappingArg.protocol)) {
290
+ throw new Error(`Public port mapping '${name}' on ${serviceNameArg} has unsupported protocol '${mappingArg.protocol}'`);
291
+ }
292
+ const target = resolveServiceTargetPort(targetPortsArg, mappingArg.targetPort);
293
+ if (!target) {
294
+ throw new Error(`Public port mapping '${name}' on ${serviceNameArg} references undeclared targetPort '${mappingArg.targetPort}'`);
295
+ }
296
+ const publicPort = assertValidServicePort(mappingArg.publicPort, `public port ${name} on ${serviceNameArg}`);
297
+ const targetStart = target.port;
298
+ const targetEnd = target.portEnd || target.port;
299
+ const publicPortEnd = mappingArg.publicPortEnd === undefined
300
+ ? publicPort + (targetEnd - targetStart)
301
+ : assertValidServicePort(mappingArg.publicPortEnd, `public port end ${name} on ${serviceNameArg}`);
302
+ if (publicPortEnd < publicPort) {
303
+ throw new Error(`Public port mapping '${name}' on ${serviceNameArg} must have an ascending public range`);
304
+ }
305
+ if ((publicPortEnd - publicPort) !== (targetEnd - targetStart)) {
306
+ throw new Error(`Public and target port ranges must have the same length for mapping '${name}' on ${serviceNameArg}`);
307
+ }
308
+ for (let offset = 0; offset <= publicPortEnd - publicPort; offset++) {
309
+ const key = `${mappingArg.protocol}:${publicPort + offset}`;
310
+ if (seenPublicPorts.has(key)) {
311
+ throw new Error(`Duplicate public port ${publicPort + offset}/${mappingArg.protocol} on ${serviceNameArg}`);
312
+ }
313
+ seenPublicPorts.add(key);
314
+ }
315
+ return {
316
+ ...mappingArg,
317
+ name,
318
+ publicPort,
319
+ publicPortEnd,
320
+ protocol: mappingArg.protocol,
321
+ target,
322
+ targetPortStart: targetStart,
323
+ targetPortEnd: targetEnd,
324
+ };
325
+ }).sort((a, b) => {
326
+ return a.protocol.localeCompare(b.protocol)
327
+ || a.publicPort - b.publicPort
328
+ || a.name.localeCompare(b.name);
329
+ });
330
+ }
331
+
332
+ function assertValidServicePort(portArg: number, labelArg: string): number {
333
+ const port = Number(portArg);
334
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
335
+ throw new Error(`Invalid ${labelArg}: ${portArg}. Expected an integer between 1 and 65535.`);
336
+ }
337
+ return port;
338
+ }
@@ -2,4 +2,12 @@ import * as plugins from '../plugins.js';
2
2
 
3
3
  interface IReverseProxyConfig extends plugins.tsclass.network.IReverseProxyConfig {}
4
4
 
5
+ export interface ICoretrafficPortRouteConfig {
6
+ name: string;
7
+ listenPort: number;
8
+ transport: 'tcp' | 'udp';
9
+ destinationIps: string[];
10
+ destinationPort: number;
11
+ }
12
+
5
13
  export { type IReverseProxyConfig };
@@ -1,9 +1,10 @@
1
- import { type IReverseProxyConfig } from '../data/traffic.js';
1
+ import { type ICoretrafficPortRouteConfig, type IReverseProxyConfig } from '../data/traffic.js';
2
2
 
3
3
  export interface IRequest_Coreflow_Coretraffic_RoutingUpdate {
4
4
  method: 'updateRouting';
5
5
  request: {
6
6
  reverseConfigs: IReverseProxyConfig[];
7
+ portRoutes?: ICoretrafficPortRouteConfig[];
7
8
  };
8
9
  response: {
9
10
  status: 'ok' | 'error';
@@ -21,6 +22,7 @@ export interface IRequest_Coretraffic_Coreflow_GetCurrentRouting {
21
22
  request: Record<string, never>;
22
23
  response: {
23
24
  reverseConfigs: IReverseProxyConfig[];
25
+ portRoutes?: ICoretrafficPortRouteConfig[];
24
26
  };
25
27
  }
26
28