@serve.zone/interfaces 13.0.1 → 15.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.
@@ -2,13 +2,15 @@ import * as plugins from '../plugins.js';
2
2
  import type { IService } from '../data/service.js';
3
3
  import type { TServiceWritableData } from '../data/service.js';
4
4
  import type { IRegistryTarget } from '../data/registry.js';
5
- import type { IIdentity } from '../data/user.js';
5
+ import type { IIdentity, IIdentityCredential } from '../data/user.js';
6
6
  import type { IImageRelease } from '../data/immutableimage.js';
7
7
  import type { IDeploymentPreflightReport, IDeploymentPreflightRequestData } from '../data/deploymentpreflight.js';
8
+ import type { IDeploymentReservationInput, IServiceDeploymentOperation, IServiceDeploymentStatus } from '../data/deploymentoperation.js';
9
+ import type { TSha256Digest } from '../data/immutableimage.js';
8
10
  export interface IRequest_Any_Cloudly_GetDeploymentPreflight extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetDeploymentPreflight> {
9
11
  method: 'getDeploymentPreflight';
10
12
  request: IDeploymentPreflightRequestData & {
11
- identity: IIdentity;
13
+ identity: IIdentityCredential;
12
14
  };
13
15
  response: {
14
16
  report: IDeploymentPreflightReport;
@@ -47,8 +49,9 @@ export interface IRequest_Any_Cloudly_GetServiceRegistryTarget extends plugins.t
47
49
  export interface IRequest_Any_Cloudly_GetServiceImageReleases extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceImageReleases> {
48
50
  method: 'getServiceImageReleases';
49
51
  request: {
50
- identity: IIdentity;
52
+ identity: IIdentityCredential;
51
53
  serviceId: string;
54
+ operationId?: string;
52
55
  };
53
56
  response: {
54
57
  releases: IImageRelease[];
@@ -57,9 +60,14 @@ export interface IRequest_Any_Cloudly_GetServiceImageReleases extends plugins.ty
57
60
  export interface IRequest_Any_Cloudly_PromoteServiceImageRelease extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_PromoteServiceImageRelease> {
58
61
  method: 'promoteServiceImageRelease';
59
62
  request: {
60
- identity: IIdentity;
63
+ identity: IIdentityCredential;
64
+ operationId: string;
61
65
  serviceId: string;
62
66
  releaseId: string;
67
+ expectedRepository: string;
68
+ expectedTag: string;
69
+ expectedDigest: TSha256Digest;
70
+ expectedOperationRevision: number;
63
71
  idempotencyKey: string;
64
72
  expectedRolloutGeneration: number;
65
73
  expectedCurrentRolloutId?: string;
@@ -68,20 +76,87 @@ export interface IRequest_Any_Cloudly_PromoteServiceImageRelease extends plugins
68
76
  };
69
77
  response: {
70
78
  service: IService;
79
+ operation: IServiceDeploymentOperation;
71
80
  };
72
81
  }
73
82
  export interface IRequest_Any_Cloudly_RollbackServiceImageRelease extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_RollbackServiceImageRelease> {
74
83
  method: 'rollbackServiceImageRelease';
75
84
  request: {
76
- identity: IIdentity;
85
+ identity: IIdentityCredential;
86
+ operationId: string;
77
87
  serviceId: string;
78
88
  releaseId: string;
89
+ expectedRepository: string;
90
+ expectedTag: string;
91
+ expectedDigest: TSha256Digest;
92
+ expectedOperationRevision: number;
79
93
  idempotencyKey: string;
80
94
  expectedCurrentRolloutId: string;
81
95
  expectedRolloutGeneration: number;
82
96
  };
83
97
  response: {
84
98
  service: IService;
99
+ operation: IServiceDeploymentOperation;
100
+ };
101
+ }
102
+ export interface IRequest_Any_Cloudly_ReserveServiceDeployment extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_ReserveServiceDeployment> {
103
+ method: 'reserveServiceDeployment';
104
+ request: {
105
+ identity: IIdentityCredential;
106
+ } & IDeploymentReservationInput;
107
+ response: {
108
+ operation: IServiceDeploymentOperation;
109
+ registryTarget: IRegistryTarget;
110
+ };
111
+ }
112
+ export interface IRequest_Any_Cloudly_GetServiceDeploymentStatus extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceDeploymentStatus> {
113
+ method: 'getServiceDeploymentStatus';
114
+ request: {
115
+ identity: IIdentityCredential;
116
+ serviceId: string;
117
+ operationId: string;
118
+ };
119
+ response: {
120
+ status: IServiceDeploymentStatus;
121
+ };
122
+ }
123
+ export interface IRequest_Any_Cloudly_PromoteServiceDeploymentRoute extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_PromoteServiceDeploymentRoute> {
124
+ method: 'promoteServiceDeploymentRoute';
125
+ request: {
126
+ identity: IIdentityCredential;
127
+ serviceId: string;
128
+ operationId: string;
129
+ expectedOperationRevision: number;
130
+ expectedRolloutId: string;
131
+ expectedRolloutGeneration: number;
132
+ expectedDigest: TSha256Digest;
133
+ };
134
+ response: {
135
+ status: IServiceDeploymentStatus;
136
+ };
137
+ }
138
+ export interface IRequest_Any_Cloudly_RetryServiceDeployment extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_RetryServiceDeployment> {
139
+ method: 'retryServiceDeployment';
140
+ request: {
141
+ identity: IIdentityCredential;
142
+ serviceId: string;
143
+ operationId: string;
144
+ expectedOperationRevision: number;
145
+ };
146
+ response: {
147
+ operation: IServiceDeploymentOperation;
148
+ };
149
+ }
150
+ export interface IRequest_Any_Cloudly_CleanupServiceDeployment extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_CleanupServiceDeployment> {
151
+ method: 'cleanupServiceDeployment';
152
+ request: {
153
+ identity: IIdentityCredential;
154
+ serviceId: string;
155
+ operationId: string;
156
+ expectedOperationRevision: number;
157
+ };
158
+ response: {
159
+ operation: IServiceDeploymentOperation;
85
160
  };
86
161
  }
87
162
  export interface IRequest_Any_Cloudly_CreateService extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_CreateService> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "13.0.1",
3
+ "version": "15.0.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
package/readme.md CHANGED
@@ -140,6 +140,20 @@ interface IExampleRequest {
140
140
 
141
141
  `requests.config.IRequest_Any_Cloudly_GetClusterConfig` accepts `data.IIdentityCredential`, which contains only the JWT needed for server-side identity resolution. Username/password login and machine-token exchange responses continue to return the full `data.IIdentity`.
142
142
 
143
+ ### Immutable Deployment Contracts
144
+
145
+ Cloudly deployment authority is expressed as an exact `data.IServiceDeploymentGrant`. A `service` grant applies to one owned existing service. An `organization-service-slot` grant reserves authority for one exact future service ID in an organization; it is not an organization-wide wildcard. `configureServiceDeploymentMachineUser` accepts this discriminated `grant` object instead of separate service and capability fields.
146
+
147
+ The immutable deployment workflow is:
148
+
149
+ 1. Reserve the exact service, namespace, registry repository/tag, and route intent with `reserveServiceDeployment`.
150
+ 2. Push the OCI index to the returned exact tag with the authenticated deployer identity.
151
+ 3. Promote the authenticated release evidence with `promoteServiceImageRelease`.
152
+ 4. Observe exact rollout and runtime-digest evidence with `getServiceDeploymentStatus`.
153
+ 5. For a greenfield service, expose and verify its public route with `promoteServiceDeploymentRoute` only after the immutable rollout succeeds.
154
+
155
+ `data.IServiceDeploymentOperation` is the durable revisioned compare-and-set fence for this workflow. Image promotion requires Cloudly-created trusted evidence that binds the operation, actor, repository, exact tag, root digest, and OCI index media type. The service request group also exposes deployment preflight, exact-digest rollback, retry, and cleanup contracts.
156
+
143
157
  Gateway request contracts include `getGatewayClientRoutes` (`requests.gateway.IReq_GetGatewayClientRoutes`) for listing owned `IGatewayClientRoute[]` route views, and `syncGatewayClientRoute` for idempotently syncing or deleting hostname-owned, routeRef-owned, and combined hostname-plus-routeRef routes. A client can label canonical intent with `managedRouteKind: 'letsencrypt-http01-forward'` and set a higher `priority` for a path-specific HTTP-01 route while retaining a separate normal route for the same hostname. Mail request contracts include `syncMailAddressBinding`, `deleteMailAddressBinding`, `rotateMailCredential`, and `getMailDeliveryStatus`. `IReq_GetMailDeliveryStatus` looks up a delivery spool item by `spoolItemId`, returns `data.IMailDeliveryStatus`, and accepts `IMailSubmissionRequestAuth` so service-mail credentials can query their own accepted, queued, deferred, delivered, or failed status. `TMailAddressBindingSync.outboundEnabled` explicitly controls whether a gateway should maintain a managed outbound SMTP credential for an address binding. Binding credential metadata is public; `rotateMailCredential` returns the new secret only in its one-time `IMailCredentialOneTimeSecret` response.
144
158
 
145
159
  Request groups are exported by product area:
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '13.0.1',
6
+ version: '15.0.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -0,0 +1,441 @@
1
+ import type { IDeploymentPreflightProposedConfiguration, IDeploymentPreflightRuntimeTask } from './deploymentpreflight.js';
2
+ import type { IImageRolloutStatus, IImmutableImageDeploymentPlan, TSha256Digest } from './immutableimage.js';
3
+ import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
4
+ import type { IRegistryTarget } from './registry.js';
5
+ import type { TServiceTargetPortRef } from './serviceports.js';
6
+
7
+ export type TDeploymentOperationMode = 'existing-service' | 'greenfield';
8
+ export type TDeploymentOperationPhase = 'reserve' | 'promote' | 'route';
9
+
10
+ interface IDeploymentReservationInputBase {
11
+ organizationId: string;
12
+ serviceId: string;
13
+ idempotencyKey: string;
14
+ sourceRevision: string;
15
+ version: string;
16
+ intentDigest: TSha256Digest;
17
+ proposedConfiguration: IDeploymentPreflightProposedConfiguration;
18
+ releaseTag: string;
19
+ routes: IDeploymentRouteRequest[];
20
+ }
21
+
22
+ export interface IExistingServiceDeploymentReservationInput
23
+ extends IDeploymentReservationInputBase {
24
+ mode: 'existing-service';
25
+ expectedRolloutGeneration: number;
26
+ expectedCurrentRolloutId?: string;
27
+ }
28
+
29
+ export interface IGreenfieldServiceDeploymentReservationInput
30
+ extends IDeploymentReservationInputBase {
31
+ mode: 'greenfield';
32
+ expectedRolloutGeneration?: never;
33
+ expectedCurrentRolloutId?: never;
34
+ }
35
+
36
+ export type IDeploymentReservationInput =
37
+ | IExistingServiceDeploymentReservationInput
38
+ | IGreenfieldServiceDeploymentReservationInput;
39
+
40
+ export type TDeploymentOperationState =
41
+ | 'reserving'
42
+ | 'reserved'
43
+ | 'provisioning'
44
+ | 'awaiting-image'
45
+ | 'image-recorded'
46
+ | 'promoting-image'
47
+ | 'rolling-out'
48
+ | 'ready-for-route'
49
+ | 'promoting-route'
50
+ | 'verifying-route'
51
+ | 'succeeded'
52
+ | 'rolling-back'
53
+ | 'rolled-back'
54
+ | 'failed'
55
+ | 'cleaning'
56
+ | 'cleaned';
57
+
58
+ export interface IDeploymentRouteRequest {
59
+ hostname: string;
60
+ targetPort: Extract<TServiceTargetPortRef, number>;
61
+ /** Required exact one-label hostname used to verify a wildcard route. */
62
+ verificationHostname?: string;
63
+ readinessPath?: string;
64
+ expectedStatusCodes?: number[];
65
+ }
66
+
67
+ export interface IDeploymentRouteClaimTarget {
68
+ hostname: string;
69
+ bucketHostname: string;
70
+ wildcard: boolean;
71
+ }
72
+
73
+ const canonicalHostnameRegex = /^(?:\*\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
74
+
75
+ export const canonicalizeDeploymentHostname = (hostnameArg: unknown): string | undefined => {
76
+ if (typeof hostnameArg !== 'string') return undefined;
77
+ const hostname = hostnameArg.trim().toLowerCase().replace(/\.$/, '');
78
+ return hostname.length <= 253 && canonicalHostnameRegex.test(hostname)
79
+ ? hostname
80
+ : undefined;
81
+ };
82
+
83
+ /**
84
+ * Wildcards match exactly one label. Exact siblings share a bucket document
85
+ * but may coexist; a wildcard claim excludes every exact name in its bucket.
86
+ */
87
+ export const getDeploymentRouteClaimTarget = (
88
+ hostnameArg: string,
89
+ ): IDeploymentRouteClaimTarget | undefined => {
90
+ const hostname = canonicalizeDeploymentHostname(hostnameArg);
91
+ if (!hostname) return undefined;
92
+ const wildcard = hostname.startsWith('*.');
93
+ const labels = (wildcard ? hostname.slice(2) : hostname).split('.');
94
+ const bucketHostname = wildcard ? labels.join('.') : labels.slice(1).join('.');
95
+ if (!bucketHostname || (wildcard && labels.length < 2)) return undefined;
96
+ return { hostname, bucketHostname, wildcard };
97
+ };
98
+
99
+ export const getDeploymentRouteVerificationHostname = (
100
+ routeArg: IDeploymentRouteRequest,
101
+ ): string | undefined => {
102
+ const target = getDeploymentRouteClaimTarget(routeArg.hostname);
103
+ if (!target) return undefined;
104
+ if (!target.wildcard) {
105
+ return routeArg.verificationHostname === undefined
106
+ || routeArg.verificationHostname === target.hostname
107
+ ? target.hostname
108
+ : undefined;
109
+ }
110
+ if (!routeArg.verificationHostname) return undefined;
111
+ const verificationTarget = getDeploymentRouteClaimTarget(routeArg.verificationHostname);
112
+ return verificationTarget
113
+ && !verificationTarget.wildcard
114
+ && verificationTarget.bucketHostname === target.bucketHostname
115
+ ? verificationTarget.hostname
116
+ : undefined;
117
+ };
118
+
119
+ export const normalizeDeploymentReadinessPath = (pathArg?: unknown): string | undefined => {
120
+ if (pathArg !== undefined && typeof pathArg !== 'string') return undefined;
121
+ const path = pathArg || '/';
122
+ return /^\/(?!\/)[A-Za-z0-9._~!$&'()*+,;=:@%\/-]{0,1023}$/.test(path)
123
+ ? path
124
+ : undefined;
125
+ };
126
+
127
+ const deploymentIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
128
+ const fullGitRevisionRegex = /^[a-f0-9]{40}$/;
129
+ const semanticVersionRegex = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
130
+
131
+ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => (
132
+ Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg)
133
+ );
134
+
135
+ const validateStringArray = (
136
+ valueArg: unknown,
137
+ maxItemsArg: number,
138
+ validatorArg: (valueArg: string) => boolean,
139
+ ): valueArg is string[] => Array.isArray(valueArg)
140
+ && valueArg.length <= maxItemsArg
141
+ && valueArg.every((itemArg) => typeof itemArg === 'string'
142
+ && itemArg.length > 0
143
+ && itemArg.length <= 253
144
+ && validatorArg(itemArg))
145
+ && new Set(valueArg).size === valueArg.length;
146
+
147
+ export const validateDeploymentProposedConfiguration = (
148
+ configurationArg: unknown,
149
+ ): string[] => {
150
+ if (!isRecord(configurationArg)) return ['proposedConfiguration must be an object'];
151
+ const errors: string[] = [];
152
+ if (configurationArg.schemaVersion !== 1) errors.push('schemaVersion must be 1');
153
+ if (typeof configurationArg.declarationComplete !== 'boolean') {
154
+ errors.push('declarationComplete must be boolean');
155
+ }
156
+ if (typeof configurationArg.projectName !== 'string'
157
+ || !configurationArg.projectName.trim()
158
+ || configurationArg.projectName.length > 200) {
159
+ errors.push('projectName must be bounded and non-empty');
160
+ }
161
+ if (!validateStringArray(configurationArg.targetPlatforms, 8, (platformArg) => (
162
+ /^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(platformArg)
163
+ ))) {
164
+ errors.push('targetPlatforms must be unique canonical platform identifiers');
165
+ }
166
+ if (!validateStringArray(configurationArg.publicDomains, 32, (domainArg) => (
167
+ canonicalizeDeploymentHostname(domainArg) === domainArg
168
+ ))) {
169
+ errors.push('publicDomains must be unique canonical exact or wildcard hostnames');
170
+ }
171
+ if (!validateStringArray(configurationArg.environmentVariableNames, 256, (nameArg) => (
172
+ nameArg.length <= 253 && /^[A-Z_][A-Z0-9_]*$/.test(nameArg)
173
+ ))) {
174
+ errors.push('environmentVariableNames must be unique canonical names');
175
+ }
176
+ if (!validateStringArray(configurationArg.secretReferenceIds, 64, (idArg) => (
177
+ idArg.length <= 253 && /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(idArg)
178
+ ))) {
179
+ errors.push('secretReferenceIds must be unique canonical identifiers');
180
+ }
181
+ if (!Array.isArray(configurationArg.containerPorts)
182
+ || configurationArg.containerPorts.length > 64
183
+ || configurationArg.containerPorts.some((portArg) => !Number.isSafeInteger(portArg)
184
+ || portArg < 1 || portArg > 65535)
185
+ || new Set(configurationArg.containerPorts).size !== configurationArg.containerPorts.length) {
186
+ errors.push('containerPorts must be unique valid ports');
187
+ }
188
+ if (!Array.isArray(configurationArg.volumeMounts)
189
+ || configurationArg.volumeMounts.length > 32
190
+ || configurationArg.volumeMounts.some((mountArg) => !isRecord(mountArg)
191
+ || typeof mountArg.mountPath !== 'string'
192
+ || !/^\/[A-Za-z0-9._/-]{0,254}$/.test(mountArg.mountPath)
193
+ || (mountArg.storageClass !== 'corestore' && mountArg.storageClass !== 'ephemeral')
194
+ || (mountArg.capability !== undefined
195
+ && mountArg.capability !== 'database'
196
+ && mountArg.capability !== 'objectstorage'))) {
197
+ errors.push('volumeMounts must be canonical supported declarations');
198
+ }
199
+ if (!Array.isArray(configurationArg.requiredCapabilities)
200
+ || configurationArg.requiredCapabilities.length > 2
201
+ || configurationArg.requiredCapabilities.some((capabilityArg) => (
202
+ capabilityArg !== 'database' && capabilityArg !== 'objectstorage'
203
+ ))
204
+ || new Set(configurationArg.requiredCapabilities).size
205
+ !== configurationArg.requiredCapabilities.length) {
206
+ errors.push('requiredCapabilities must be a unique supported capability set');
207
+ }
208
+ return errors;
209
+ };
210
+
211
+ export const validateDeploymentReservationInput = (inputArg: unknown): string[] => {
212
+ if (!isRecord(inputArg)) return ['reservation input must be an object'];
213
+ const errors: string[] = [];
214
+ if (inputArg.mode !== 'existing-service' && inputArg.mode !== 'greenfield') {
215
+ errors.push('mode must identify an existing-service or greenfield deployment');
216
+ }
217
+ for (const field of ['organizationId', 'serviceId', 'idempotencyKey'] as const) {
218
+ if (typeof inputArg[field] !== 'string'
219
+ || !deploymentIdentifierRegex.test(inputArg[field] as string)) {
220
+ errors.push(`${field} must be a bounded canonical identifier`);
221
+ }
222
+ }
223
+ if (typeof inputArg.sourceRevision !== 'string'
224
+ || !fullGitRevisionRegex.test(inputArg.sourceRevision)) {
225
+ errors.push('sourceRevision must be a full lowercase git revision');
226
+ }
227
+ if (typeof inputArg.version !== 'string' || !semanticVersionRegex.test(inputArg.version)) {
228
+ errors.push('version must be a canonical semantic version');
229
+ }
230
+ if (!normalizeSha256Digest(inputArg.intentDigest)) {
231
+ errors.push('intentDigest must be a canonical sha256 digest');
232
+ }
233
+ if (!normalizeImmutableReleaseTag(inputArg.releaseTag)) {
234
+ errors.push('releaseTag must be a canonical non-latest OCI tag');
235
+ }
236
+ const configuration = isRecord(inputArg.proposedConfiguration)
237
+ ? inputArg.proposedConfiguration
238
+ : undefined;
239
+ errors.push(...validateDeploymentProposedConfiguration(inputArg.proposedConfiguration));
240
+ const containerPorts = configuration && Array.isArray(configuration.containerPorts)
241
+ ? configuration.containerPorts
242
+ : undefined;
243
+ const publicDomains = configuration && Array.isArray(configuration.publicDomains)
244
+ ? configuration.publicDomains.filter((domainArg): domainArg is string => (
245
+ typeof domainArg === 'string'
246
+ ))
247
+ : undefined;
248
+ if (configuration?.declarationComplete !== true) {
249
+ errors.push('proposedConfiguration must declare itself complete');
250
+ }
251
+ const routes = Array.isArray(inputArg.routes) ? inputArg.routes : undefined;
252
+ const routeHostnames = routes?.map((routeArg) => (
253
+ isRecord(routeArg) && typeof routeArg.hostname === 'string' ? routeArg.hostname : undefined
254
+ ));
255
+ if (!routes
256
+ || routes.length > 32
257
+ || !routeHostnames
258
+ || new Set(routeHostnames).size !== routeHostnames.length
259
+ || routes.some((routeArg) => {
260
+ if (!isRecord(routeArg)
261
+ || typeof routeArg.hostname !== 'string'
262
+ || canonicalizeDeploymentHostname(routeArg.hostname) !== routeArg.hostname
263
+ || !Number.isSafeInteger(routeArg.targetPort)
264
+ || (routeArg.targetPort as number) < 1
265
+ || (routeArg.targetPort as number) > 65535
266
+ || !containerPorts?.includes(routeArg.targetPort)) {
267
+ return true;
268
+ }
269
+ const typedRoute = routeArg as unknown as IDeploymentRouteRequest;
270
+ return getDeploymentRouteVerificationHostname(typedRoute) === undefined
271
+ || (routeArg.readinessPath !== undefined
272
+ && (typeof routeArg.readinessPath !== 'string'
273
+ || normalizeDeploymentReadinessPath(routeArg.readinessPath) !== routeArg.readinessPath))
274
+ || (routeArg.expectedStatusCodes !== undefined
275
+ && (!Array.isArray(routeArg.expectedStatusCodes)
276
+ || routeArg.expectedStatusCodes.length < 1
277
+ || routeArg.expectedStatusCodes.length > 16
278
+ || new Set(routeArg.expectedStatusCodes).size !== routeArg.expectedStatusCodes.length
279
+ || routeArg.expectedStatusCodes.some((statusArg) => !Number.isSafeInteger(statusArg)
280
+ || statusArg < 200 || statusArg > 299)));
281
+ })) {
282
+ errors.push('routes must be unique canonical declarations over declared numeric ports');
283
+ }
284
+ if (inputArg.mode === 'greenfield') {
285
+ if (inputArg.expectedRolloutGeneration !== undefined
286
+ || inputArg.expectedCurrentRolloutId !== undefined) {
287
+ errors.push('greenfield reservation must not carry an existing rollout fence');
288
+ }
289
+ if (publicDomains && routeHostnames) {
290
+ const declared = [...new Set(publicDomains)].sort();
291
+ const routed = [...new Set(routeHostnames.filter((valueArg): valueArg is string => (
292
+ typeof valueArg === 'string'
293
+ )))].sort();
294
+ if (JSON.stringify(declared) !== JSON.stringify(routed)) {
295
+ errors.push('greenfield routes must exactly match proposed publicDomains');
296
+ }
297
+ }
298
+ }
299
+ if (inputArg.mode === 'existing-service') {
300
+ if (!Number.isSafeInteger(inputArg.expectedRolloutGeneration)
301
+ || (inputArg.expectedRolloutGeneration as number) < 0) {
302
+ errors.push('existing-service reservation requires an explicit rollout generation fence');
303
+ }
304
+ if (inputArg.expectedCurrentRolloutId !== undefined
305
+ && (typeof inputArg.expectedCurrentRolloutId !== 'string'
306
+ || !deploymentIdentifierRegex.test(inputArg.expectedCurrentRolloutId))) {
307
+ errors.push('expectedCurrentRolloutId must be a bounded canonical identifier');
308
+ }
309
+ }
310
+ return errors;
311
+ };
312
+
313
+ interface IDeploymentRouteVerificationEvidenceBase {
314
+ hostname: string;
315
+ url: string;
316
+ checkedAt: number;
317
+ resolvedAddresses: string[];
318
+ rolloutId: string;
319
+ rolloutGeneration: number;
320
+ expectedDigest: TSha256Digest;
321
+ }
322
+
323
+ export interface IDeploymentRouteVerificationSuccessEvidence
324
+ extends IDeploymentRouteVerificationEvidenceBase {
325
+ outcome: 'succeeded';
326
+ tls: {
327
+ authorized: true;
328
+ servername: string;
329
+ subjectAlternativeNames: string[];
330
+ validFrom?: string;
331
+ validTo?: string;
332
+ };
333
+ http: {
334
+ statusCode: number;
335
+ redirected: false;
336
+ };
337
+ readiness: {
338
+ path: string;
339
+ expectedStatusCodes: number[];
340
+ ready: true;
341
+ };
342
+ }
343
+
344
+ export interface IDeploymentRouteVerificationFailureEvidence
345
+ extends IDeploymentRouteVerificationEvidenceBase {
346
+ outcome: 'failed';
347
+ stage: 'dns' | 'address-policy' | 'connect' | 'tls' | 'http' | 'readiness';
348
+ code: string;
349
+ message: string;
350
+ tls?: IDeploymentRouteVerificationSuccessEvidence['tls'];
351
+ http?: IDeploymentRouteVerificationSuccessEvidence['http'];
352
+ readiness?: IDeploymentRouteVerificationSuccessEvidence['readiness'];
353
+ }
354
+
355
+ export type IDeploymentRouteVerificationEvidence =
356
+ | IDeploymentRouteVerificationSuccessEvidence
357
+ | IDeploymentRouteVerificationFailureEvidence;
358
+
359
+ export interface IDeploymentOperationFailure {
360
+ code: string;
361
+ message: string;
362
+ retryable: boolean;
363
+ failedAt: number;
364
+ }
365
+
366
+ /**
367
+ * Durable deployment fence. All state transitions compare-and-set both id and
368
+ * revision. Claim keys are server-generated and never accepted as authority.
369
+ */
370
+ export interface IServiceDeploymentOperation {
371
+ id: string;
372
+ revision: number;
373
+ data: {
374
+ mode: TDeploymentOperationMode;
375
+ organizationId: string;
376
+ serviceId: string;
377
+ actorUserId: string;
378
+ idempotencyKey: string;
379
+ requestDigest: TSha256Digest;
380
+ sourceRevision: string;
381
+ version: string;
382
+ intentDigest: TSha256Digest;
383
+ configurationDigest: TSha256Digest;
384
+ proposedConfiguration: IDeploymentPreflightProposedConfiguration;
385
+ namespace: string;
386
+ registryTarget: IRegistryTarget;
387
+ releaseTag: string;
388
+ routes: IDeploymentRouteRequest[];
389
+ claimKeys: string[];
390
+ state: TDeploymentOperationState;
391
+ releaseId?: string;
392
+ registryRootDigest?: TSha256Digest;
393
+ rolloutId?: string;
394
+ rolloutGeneration?: number;
395
+ routeGeneration?: number;
396
+ routeVerification?: IDeploymentRouteVerificationEvidence[];
397
+ failure?: IDeploymentOperationFailure;
398
+ createdAt: number;
399
+ updatedAt: number;
400
+ };
401
+ }
402
+
403
+ export interface IServiceDeploymentStatus {
404
+ operation: IServiceDeploymentOperation;
405
+ imageDeployment?: IImmutableImageDeploymentPlan;
406
+ imageRolloutStatus?: IImageRolloutStatus;
407
+ runtimeTasks: IDeploymentRuntimeTaskEvidence[];
408
+ routeVerification: IDeploymentRouteVerificationEvidence[];
409
+ immutableRolloutSucceeded: boolean;
410
+ runtimeDigestVerified: boolean;
411
+ routeVerified: boolean;
412
+ succeeded: boolean;
413
+ }
414
+
415
+ interface IDeploymentRuntimeTaskEvidenceBase extends Omit<
416
+ IDeploymentPreflightRuntimeTask,
417
+ 'verificationStatus' | 'observedDigest' | 'reportedDigest'
418
+ > {
419
+ rolloutId: string;
420
+ rolloutGeneration: number;
421
+ expectedDigest: TSha256Digest;
422
+ observedAt: number;
423
+ }
424
+
425
+ export interface IVerifiedDeploymentRuntimeTaskEvidence
426
+ extends IDeploymentRuntimeTaskEvidenceBase {
427
+ verificationStatus: 'verified';
428
+ observedDigest: TSha256Digest;
429
+ reportedDigest: TSha256Digest;
430
+ }
431
+
432
+ export interface IUnverifiedDeploymentRuntimeTaskEvidence
433
+ extends IDeploymentRuntimeTaskEvidenceBase {
434
+ verificationStatus: 'missing' | 'mismatched';
435
+ observedDigest?: TSha256Digest;
436
+ reportedDigest?: TSha256Digest;
437
+ }
438
+
439
+ export type IDeploymentRuntimeTaskEvidence =
440
+ | IVerifiedDeploymentRuntimeTaskEvidence
441
+ | IUnverifiedDeploymentRuntimeTaskEvidence;