@serve.zone/interfaces 11.2.0 → 11.4.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,6 +2,7 @@ export * from './cloudlyconfig.js';
2
2
  export * from './cluster.js';
3
3
  export * from './config.js';
4
4
  export * from './deployment.js';
5
+ export * from './deploymentpreflight.js';
5
6
  export * from './migration.js';
6
7
  export * from './dns.js';
7
8
  export * from './docker.js';
@@ -11,6 +12,7 @@ export * from './externalregistry.js';
11
12
  export * from './gateway.js';
12
13
  export * from './hostedapp.js';
13
14
  export * from './image.js';
15
+ export * from './immutableimage.js';
14
16
  export * from './isolatedrestore.js';
15
17
  export * from './isolatedrestore.golden.js';
16
18
  export * from './mail.js';
@@ -29,4 +31,4 @@ export * from './taskexecution.js';
29
31
  export * from './traffic.js';
30
32
  export * from './user.js';
31
33
  export * from './version.js';
32
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsVUFBVSxDQUFDO0FBQ3pCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxjQUFjLENBQUMifQ==
34
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLG9CQUFvQixDQUFDO0FBQ25DLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsY0FBYyxDQUFDIn0=
@@ -1,4 +1,5 @@
1
1
  export type TRegistryProtocol = 'oci';
2
+ import type { TOciManifestMediaType, TSha256Digest } from './immutableimage.js';
2
3
  export interface IRegistryTarget {
3
4
  protocol: TRegistryProtocol;
4
5
  registryHost: string;
@@ -9,6 +10,8 @@ export interface IRegistryTarget {
9
10
  imageId?: string;
10
11
  }
11
12
  export interface IRegistryPushEvent {
13
+ eventId?: string;
14
+ releaseId?: string;
12
15
  protocol: TRegistryProtocol;
13
16
  registryHost: string;
14
17
  repository: string;
@@ -16,8 +19,27 @@ export interface IRegistryPushEvent {
16
19
  digest: string;
17
20
  imageUrl: string;
18
21
  pushedAt: number;
22
+ observedAt?: number;
19
23
  serviceId?: string;
20
24
  imageId?: string;
21
25
  actorUserId?: string;
22
26
  manifestMediaType?: string;
23
27
  }
28
+ /** Fully verified registry observation used by immutable release processing. */
29
+ export interface IImmutableRegistryPushEvent {
30
+ deploymentPolicy: 'immutable-digest';
31
+ eventId: string;
32
+ releaseId: string;
33
+ protocol: TRegistryProtocol;
34
+ registryHost: string;
35
+ repository: string;
36
+ tag: string;
37
+ registryRootDigest: TSha256Digest;
38
+ digestPinnedImageReference: string;
39
+ imageUrl: string;
40
+ observedAt: number;
41
+ serviceId: string;
42
+ imageId: string;
43
+ manifestMediaType: TOciManifestMediaType;
44
+ actorUserId?: string;
45
+ }
@@ -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 { IImageRolloutStatus, IImmutableImageDeploymentPlan } from './immutableimage.js';
6
7
  import type { IServiceDomainRoute, IServicePublicPortMapping, IServiceTargetPort } from './serviceports.js';
7
8
  export interface IServiceVolume {
8
9
  /** Stable Docker volume name. If omitted, Coreflow derives one from service id and mount path. */
@@ -60,6 +61,12 @@ export interface IService {
60
61
  imageVersion: string;
61
62
  registryTarget?: IRegistryTarget;
62
63
  deployOnPush?: boolean;
64
+ /** Fail closed unless Cloudly supplies and Coreflow verifies a digest-pinned rollout. */
65
+ immutableImageRequired?: boolean;
66
+ /** Server-managed immutable desired state. Use the promotion/rollback APIs to change it. */
67
+ imageDeployment?: IImmutableImageDeploymentPlan;
68
+ /** Server-managed aggregate of task-derived runtime evidence. */
69
+ imageRolloutStatus?: IImageRolloutStatus;
63
70
  /**
64
71
  * When true the service tolerates overlapping deployments during
65
72
  * updates (rolling). Default false: the previous deployment is stopped
@@ -139,3 +146,5 @@ export interface IService {
139
146
  deploymentIds: string[];
140
147
  };
141
148
  }
149
+ /** Caller-writable service data. Immutable policy and rollout fields are server-managed. */
150
+ export type TServiceWritableData = Omit<IService['data'], 'immutableImageRequired' | 'imageDeployment' | 'imageRolloutStatus'>;
@@ -3,6 +3,16 @@ export interface IToken {
3
3
  expiresAt: number;
4
4
  assignedRoles: string[];
5
5
  }
6
+ export type TServiceDeploymentCapability = 'deployment:preflight' | 'deployment:create-greenfield' | 'deployment:promote-route' | 'deployment:rollback-route';
7
+ /**
8
+ * Server-persisted authority for one logical service. These grants are never
9
+ * accepted from the identity payload supplied by a caller; Cloudly reloads
10
+ * the owning user from the verified JWT before evaluating them.
11
+ */
12
+ export interface IServiceDeploymentGrant {
13
+ serviceId: string;
14
+ capabilities: TServiceDeploymentCapability[];
15
+ }
6
16
  export interface IIdentityCredential {
7
17
  jwt: string;
8
18
  }
@@ -25,5 +35,6 @@ export interface IUser {
25
35
  username?: string;
26
36
  password?: string;
27
37
  tokens?: IToken[];
38
+ serviceDeploymentGrants?: IServiceDeploymentGrant[];
28
39
  };
29
40
  }
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
2
2
  import type { IDeployment } from '../data/deployment.js';
3
3
  import type { IService } from '../data/service.js';
4
4
  import type { IIdentity } from '../data/user.js';
5
+ import type { IImmutableRolloutReportEnvelope } from '../data/immutableimage.js';
5
6
  export interface IDeploymentWorkspaceFileEntry {
6
7
  type: 'file' | 'directory';
7
8
  name: string;
@@ -311,6 +312,8 @@ export interface IReq_Coreflow_Cloudly_ReportDeploymentSnapshots extends plugins
311
312
  * When absent, the snapshot replaces all persisted deployments of the service.
312
313
  */
313
314
  reporterNodeNames?: string[];
315
+ /** Mandatory when the referenced service requires immutable deployment. */
316
+ immutableReport?: IImmutableRolloutReportEnvelope;
314
317
  reportedAt: number;
315
318
  };
316
319
  response: Record<string, never>;
@@ -1,7 +1,19 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import type { IService } from '../data/service.js';
3
+ import type { TServiceWritableData } from '../data/service.js';
3
4
  import type { IRegistryTarget } from '../data/registry.js';
4
5
  import type { IIdentity } from '../data/user.js';
6
+ import type { IImageRelease } from '../data/immutableimage.js';
7
+ import type { IDeploymentPreflightReport, IDeploymentPreflightRequestData } from '../data/deploymentpreflight.js';
8
+ export interface IRequest_Any_Cloudly_GetDeploymentPreflight extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetDeploymentPreflight> {
9
+ method: 'getDeploymentPreflight';
10
+ request: IDeploymentPreflightRequestData & {
11
+ identity: IIdentity;
12
+ };
13
+ response: {
14
+ report: IDeploymentPreflightReport;
15
+ };
16
+ }
5
17
  export interface IRequest_Any_Cloudly_GetServiceById extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceById> {
6
18
  method: 'getServiceById';
7
19
  request: {
@@ -32,11 +44,51 @@ export interface IRequest_Any_Cloudly_GetServiceRegistryTarget extends plugins.t
32
44
  registryTarget: IRegistryTarget;
33
45
  };
34
46
  }
47
+ export interface IRequest_Any_Cloudly_GetServiceImageReleases extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceImageReleases> {
48
+ method: 'getServiceImageReleases';
49
+ request: {
50
+ identity: IIdentity;
51
+ serviceId: string;
52
+ };
53
+ response: {
54
+ releases: IImageRelease[];
55
+ };
56
+ }
57
+ export interface IRequest_Any_Cloudly_PromoteServiceImageRelease extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_PromoteServiceImageRelease> {
58
+ method: 'promoteServiceImageRelease';
59
+ request: {
60
+ identity: IIdentity;
61
+ serviceId: string;
62
+ releaseId: string;
63
+ idempotencyKey: string;
64
+ expectedRolloutGeneration: number;
65
+ expectedCurrentRolloutId?: string;
66
+ /** Explicitly opts the service into fail-closed immutable deployment. */
67
+ enableImmutableImage: true;
68
+ };
69
+ response: {
70
+ service: IService;
71
+ };
72
+ }
73
+ export interface IRequest_Any_Cloudly_RollbackServiceImageRelease extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_RollbackServiceImageRelease> {
74
+ method: 'rollbackServiceImageRelease';
75
+ request: {
76
+ identity: IIdentity;
77
+ serviceId: string;
78
+ releaseId: string;
79
+ idempotencyKey: string;
80
+ expectedCurrentRolloutId: string;
81
+ expectedRolloutGeneration: number;
82
+ };
83
+ response: {
84
+ service: IService;
85
+ };
86
+ }
35
87
  export interface IRequest_Any_Cloudly_CreateService extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_CreateService> {
36
88
  method: 'createService';
37
89
  request: {
38
90
  identity: IIdentity;
39
- serviceData: IService['data'];
91
+ serviceData: TServiceWritableData;
40
92
  };
41
93
  response: {
42
94
  service: IService;
@@ -47,7 +99,7 @@ export interface IRequest_Any_Cloudly_UpdateService extends plugins.typedrequest
47
99
  request: {
48
100
  identity: IIdentity;
49
101
  serviceId: string;
50
- serviceData: IService['data'];
102
+ serviceData: TServiceWritableData;
51
103
  };
52
104
  response: {
53
105
  service: IService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "11.2.0",
3
+ "version": "11.4.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '11.2.0',
6
+ version: '11.4.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -1,4 +1,5 @@
1
1
  import * as plugins from '../plugins.js';
2
+ import type { IRuntimeImageEvidence } from './immutableimage.js';
2
3
 
3
4
  /**
4
5
  * a deployment happens when a service is deployed
@@ -39,6 +40,13 @@ export interface IDeployment {
39
40
 
40
41
  /** Docker desired state for this task/deployment. */
41
42
  desiredState?: string;
43
+
44
+ /** Immutable rollout generation represented by this task, when applicable. */
45
+ rolloutId?: string;
46
+ rolloutGeneration?: number;
47
+
48
+ /** Evidence derived from Docker runtime state rather than service intent metadata. */
49
+ runtimeImageEvidence?: IRuntimeImageEvidence;
42
50
 
43
51
  /**
44
52
  * Image used for this deployment
@@ -0,0 +1,297 @@
1
+ import type {
2
+ TOciManifestMediaType,
3
+ TSha256Digest,
4
+ } from './immutableimage.js';
5
+ import { normalizeSha256Digest } from './immutableimage.js';
6
+ import type { TServiceDeploymentCapability } from './user.js';
7
+
8
+ export type TDeploymentPreflightDecision = 'GO' | 'NO-GO';
9
+
10
+ export type TDeploymentPreflightBlockerCode =
11
+ | 'INVALID_REQUEST'
12
+ | 'AUTHORIZATION_MISSING'
13
+ | 'SOURCE_REVISION_INVALID'
14
+ | 'VERSION_NOT_RESERVED'
15
+ | 'VERSION_COLLISION'
16
+ | 'NAMESPACE_NOT_RESERVED'
17
+ | 'NAMESPACE_COLLISION'
18
+ | 'NAMESPACE_NOT_PROVEN_BLANK'
19
+ | 'CONFIGURATION_DIGEST_INVALID'
20
+ | 'CURRENT_REFERENCE_MUTABLE'
21
+ | 'CURRENT_RUNTIME_DIGEST_MISSING'
22
+ | 'OCI_INDEX_DIGEST_REQUIRED'
23
+ | 'OCI_INDEX_NOT_FOUND'
24
+ | 'OCI_INDEX_MEDIA_TYPE_REQUIRED'
25
+ | 'OCI_PLATFORM_MISSING'
26
+ | 'OCI_PLATFORM_MANIFEST_MISSING'
27
+ | 'IMMUTABLE_RELEASE_TAG_REQUIRED'
28
+ | 'IMMUTABLE_RELEASE_TAG_NOT_RESERVED'
29
+ | 'RELEASE_EVIDENCE_MISSING'
30
+ | 'RELEASE_EVIDENCE_INVALID'
31
+ | 'RELEASE_NOT_AUTHORIZED'
32
+ | 'GREENFIELD_EXECUTION_UNAVAILABLE'
33
+ | 'ROUTE_PROMOTION_FENCE_UNAVAILABLE'
34
+ | 'POST_DEPLOYMENT_VERIFICATION_UNAVAILABLE'
35
+ | 'WOULD_REPLACE_EXISTING_STATE'
36
+ | 'CORESTORE_INVENTORY_UNAVAILABLE'
37
+ | 'RUNTIME_ATTESTATION_MISMATCH';
38
+
39
+ export interface IDeploymentPreflightBlocker {
40
+ code: TDeploymentPreflightBlockerCode;
41
+ message: string;
42
+ }
43
+
44
+ export interface IDeploymentPreflightAuthorizationCheck {
45
+ capability: TServiceDeploymentCapability;
46
+ granted: boolean;
47
+ }
48
+
49
+ export interface IOciPlatformDescriptorSummary {
50
+ os: string;
51
+ architecture: string;
52
+ digest?: TSha256Digest;
53
+ mediaType?: string;
54
+ runnable: boolean;
55
+ manifestPresent: boolean;
56
+ }
57
+
58
+ export interface IDeploymentPreflightRuntimeTask {
59
+ deploymentId: string;
60
+ nodeName?: string;
61
+ status: string;
62
+ healthStatus?: string;
63
+ imageReference?: string;
64
+ reportedDigest?: TSha256Digest;
65
+ verificationStatus?: 'not-required' | 'pending' | 'verified' | 'missing' | 'mismatched';
66
+ }
67
+
68
+ export interface IDeploymentPreflightPlatformBinding {
69
+ capability: 'database' | 'objectstorage';
70
+ desiredState: 'enabled' | 'disabled';
71
+ status: 'requested' | 'provisioning' | 'ready' | 'degraded' | 'failed' | 'disabled';
72
+ }
73
+
74
+ export interface IDeploymentPreflightCorestoreResource {
75
+ capability: 'database' | 'objectstorage';
76
+ provider: string;
77
+ resourceName: string;
78
+ }
79
+
80
+ export interface IDeploymentReleaseEvidenceReference {
81
+ kind: 'provenance' | 'sbom' | 'signature' | 'vulnerability-policy' | 'release-authorization';
82
+ statementDigest: TSha256Digest;
83
+ subjectDigest: TSha256Digest;
84
+ verifier: string;
85
+ verifiedAt: number;
86
+ }
87
+
88
+ /** Evidence is descriptive input until a trusted Cloudly verifier validates it. */
89
+ export interface IDeploymentReleaseEvidence {
90
+ sourceRevision: string;
91
+ version: string;
92
+ configurationDigest: TSha256Digest;
93
+ ociIndexDigest: TSha256Digest;
94
+ releaseAuthorized: boolean;
95
+ attestations: IDeploymentReleaseEvidenceReference[];
96
+ }
97
+
98
+ export interface IDeploymentPreflightRequestData {
99
+ requestId: string;
100
+ serviceId: string;
101
+ sourceRevision: string;
102
+ version: string;
103
+ configurationDigest: string;
104
+ proposedNamespace: string;
105
+ ociIndexDigest?: string;
106
+ releaseTag?: string;
107
+ releaseEvidence?: IDeploymentReleaseEvidence;
108
+ greenfield: true;
109
+ }
110
+
111
+ export interface IDeploymentPreflightReport {
112
+ schemaVersion: 1;
113
+ decision: TDeploymentPreflightDecision;
114
+ mutationPerformed: false;
115
+ generatedAt: number;
116
+ service: {
117
+ serviceId: string;
118
+ serviceName: string;
119
+ workspaceIdentity: string;
120
+ registryRepository: string;
121
+ deployOnPush: boolean;
122
+ };
123
+ source: {
124
+ revision: string;
125
+ version: string;
126
+ configurationDigest?: TSha256Digest;
127
+ };
128
+ currentImage: {
129
+ tag?: string;
130
+ digest?: TSha256Digest;
131
+ imageReference?: string;
132
+ mutableReference: boolean;
133
+ mediaType?: TOciManifestMediaType;
134
+ platforms: IOciPlatformDescriptorSummary[];
135
+ };
136
+ requestedImage: {
137
+ digest?: TSha256Digest;
138
+ releaseTag?: string;
139
+ tagReserved: boolean;
140
+ mediaType?: TOciManifestMediaType;
141
+ platforms: IOciPlatformDescriptorSummary[];
142
+ };
143
+ runtimeTasks: IDeploymentPreflightRuntimeTask[];
144
+ platformBindings: IDeploymentPreflightPlatformBinding[];
145
+ corestoreResources: IDeploymentPreflightCorestoreResource[];
146
+ proposed: {
147
+ namespace: string;
148
+ namespaceReserved: boolean;
149
+ namespaceBlank: boolean;
150
+ versionReserved: boolean;
151
+ wouldReplaceExistingState: boolean;
152
+ };
153
+ authorizationChecks: IDeploymentPreflightAuthorizationCheck[];
154
+ compatibilityBlockers: IDeploymentPreflightBlocker[];
155
+ }
156
+
157
+ export interface IGreenfieldDeploymentFence {
158
+ sourceServiceId: string;
159
+ sourceRevision: string;
160
+ version: string;
161
+ ociIndexDigest: TSha256Digest;
162
+ configurationDigest: TSha256Digest;
163
+ targetNamespace: string;
164
+ }
165
+
166
+ export interface IGreenfieldDeploymentOperation {
167
+ id: string;
168
+ data: {
169
+ idempotencyKey: string;
170
+ requestDigest: TSha256Digest;
171
+ fence: IGreenfieldDeploymentFence;
172
+ candidateServiceId?: string;
173
+ state:
174
+ | 'reserved'
175
+ | 'provisioning'
176
+ | 'deploying'
177
+ | 'verifying'
178
+ | 'ready-for-promotion'
179
+ | 'promoting'
180
+ | 'promoted'
181
+ | 'rolling-back'
182
+ | 'rolled-back'
183
+ | 'failed';
184
+ routeGeneration?: number;
185
+ createdAt: number;
186
+ updatedAt: number;
187
+ failureCode?: string;
188
+ };
189
+ }
190
+
191
+ const fullGitRevisionRegex = /^[a-f0-9]{40}$/;
192
+ const namespaceRegex = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
193
+ const requiredEvidenceKinds = new Set<IDeploymentReleaseEvidenceReference['kind']>([
194
+ 'provenance',
195
+ 'sbom',
196
+ 'signature',
197
+ 'vulnerability-policy',
198
+ 'release-authorization',
199
+ ]);
200
+
201
+ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> =>
202
+ Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg);
203
+
204
+ export const validateDeploymentPreflightRequest = (
205
+ requestArg: unknown,
206
+ ): IDeploymentPreflightBlocker[] => {
207
+ const blockers: IDeploymentPreflightBlocker[] = [];
208
+ if (!isRecord(requestArg)) {
209
+ return [{ code: 'INVALID_REQUEST', message: 'Deployment preflight request must be an object.' }];
210
+ }
211
+ if (typeof requestArg.requestId !== 'string' || !requestArg.requestId.trim() || requestArg.requestId.length > 200) {
212
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded request identifier is required.' });
213
+ }
214
+ if (typeof requestArg.serviceId !== 'string' || !requestArg.serviceId.trim() || requestArg.serviceId.length > 200) {
215
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded service identifier is required.' });
216
+ }
217
+ if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) {
218
+ blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' });
219
+ }
220
+ if (typeof requestArg.version !== 'string' || !requestArg.version.trim() || requestArg.version.length > 100) {
221
+ blockers.push({ code: 'VERSION_NOT_RESERVED', message: 'A bounded release version is required.' });
222
+ }
223
+ if (typeof requestArg.proposedNamespace !== 'string' || !namespaceRegex.test(requestArg.proposedNamespace)) {
224
+ blockers.push({ code: 'NAMESPACE_NOT_RESERVED', message: 'The proposed namespace is missing or malformed.' });
225
+ }
226
+ if (typeof requestArg.configurationDigest !== 'string' || !normalizeSha256Digest(requestArg.configurationDigest)) {
227
+ blockers.push({ code: 'CONFIGURATION_DIGEST_INVALID', message: 'Configuration digest must be a canonical sha256 digest.' });
228
+ }
229
+ if (requestArg.ociIndexDigest !== undefined
230
+ && (typeof requestArg.ociIndexDigest !== 'string' || !normalizeSha256Digest(requestArg.ociIndexDigest))) {
231
+ blockers.push({ code: 'OCI_INDEX_DIGEST_REQUIRED', message: 'OCI index digest must be a canonical sha256 digest.' });
232
+ }
233
+ if (requestArg.greenfield !== true) {
234
+ blockers.push({ code: 'WOULD_REPLACE_EXISTING_STATE', message: 'Only greenfield deployment preflight is supported.' });
235
+ }
236
+ return blockers;
237
+ };
238
+
239
+ export const validateDeploymentReleaseEvidence = (argsArg: {
240
+ evidence: unknown;
241
+ sourceRevision: string;
242
+ version: string;
243
+ configurationDigest: TSha256Digest;
244
+ ociIndexDigest: TSha256Digest;
245
+ }): IDeploymentPreflightBlocker[] => {
246
+ const blockers: IDeploymentPreflightBlocker[] = [];
247
+ if (!isRecord(argsArg.evidence)) {
248
+ return [{ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence must be an object.' }];
249
+ }
250
+ const evidence = argsArg.evidence;
251
+ if (evidence.releaseAuthorized !== true) {
252
+ blockers.push({ code: 'RELEASE_NOT_AUTHORIZED', message: 'Release authorization evidence is explicitly false.' });
253
+ }
254
+ if (
255
+ evidence.sourceRevision !== argsArg.sourceRevision
256
+ || evidence.version !== argsArg.version
257
+ || evidence.configurationDigest !== argsArg.configurationDigest
258
+ || evidence.ociIndexDigest !== argsArg.ociIndexDigest
259
+ ) {
260
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence is not bound to the requested deployment fence.' });
261
+ }
262
+ const seenKinds = new Set<IDeploymentReleaseEvidenceReference['kind']>();
263
+ const attestations = Array.isArray(evidence.attestations) ? evidence.attestations : [];
264
+ if (!Array.isArray(evidence.attestations)) {
265
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestations must be an array.' });
266
+ }
267
+ for (const attestation of attestations) {
268
+ if (!isRecord(attestation)) {
269
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation must be an object.' });
270
+ continue;
271
+ }
272
+ const kind = attestation.kind;
273
+ if (typeof kind !== 'string' || !requiredEvidenceKinds.has(kind as IDeploymentReleaseEvidenceReference['kind'])) {
274
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation kind is invalid.' });
275
+ continue;
276
+ }
277
+ seenKinds.add(kind as IDeploymentReleaseEvidenceReference['kind']);
278
+ if (
279
+ attestation.subjectDigest !== argsArg.ociIndexDigest
280
+ || typeof attestation.statementDigest !== 'string'
281
+ || !normalizeSha256Digest(attestation.statementDigest)
282
+ || typeof attestation.verifier !== 'string'
283
+ || !attestation.verifier.trim()
284
+ || typeof attestation.verifiedAt !== 'number'
285
+ || !Number.isSafeInteger(attestation.verifiedAt)
286
+ || attestation.verifiedAt < 1
287
+ ) {
288
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: `Invalid ${kind} evidence.` });
289
+ }
290
+ }
291
+ for (const requiredKind of requiredEvidenceKinds) {
292
+ if (!seenKinds.has(requiredKind)) {
293
+ blockers.push({ code: 'RELEASE_EVIDENCE_MISSING', message: `Missing ${requiredKind} evidence.` });
294
+ }
295
+ }
296
+ return blockers;
297
+ };
package/ts/data/image.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as plugins from '../plugins.js';
2
- import type { IRegistryPushEvent } from './registry.js';
2
+ import type { IImmutableRegistryPushEvent, IRegistryPushEvent } from './registry.js';
3
3
 
4
4
  export interface IImage {
5
5
  id: string;
@@ -15,6 +15,8 @@ export interface IImage {
15
15
  versions: Array<{
16
16
  versionString: string;
17
17
  digest?: string;
18
+ releaseId?: string;
19
+ manifestMediaType?: string;
18
20
  registryRepository?: string;
19
21
  registryTag?: string;
20
22
  source?: 'upload' | 'registry';
@@ -22,6 +24,6 @@ export interface IImage {
22
24
  size: number;
23
25
  createdAt: number;
24
26
  }>;
25
- lastPushEvent?: IRegistryPushEvent;
27
+ lastPushEvent?: IRegistryPushEvent | IImmutableRegistryPushEvent;
26
28
  };
27
29
  }