@serve.zone/interfaces 11.1.1 → 11.3.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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-07-18 - 11.3.0
4
+
5
+ ### Features
6
+
7
+ - define fail-closed immutable image deployment and runtime evidence contracts (deployment)
8
+ - Separate observed registry releases, accepted digest rollouts, and exact-digest rollback plans.
9
+ - Require task-derived Docker runtime evidence and complete healthy replica reports before rollout success.
10
+ - Add idempotent promotion, compare-and-swap rollback, capability, ordering, and mismatch contracts.
11
+
12
+ ## 2026-07-15 - 11.2.0
13
+
14
+ ### Features
15
+
16
+ - Support priority and managed-kind metadata on gateway routes.
17
+ - Allow combined `hostname` plus `routeRef` ownership so multiple managed routes for one hostname reconcile independently.
18
+ - Define the canonical `letsencrypt-http01-forward` managed route kind.
19
+
20
+ ### Maintenance
21
+
22
+ - Update the release toolchain to pnpm 11.13.0 and Node.js types 26.1.1.
23
+
3
24
  ## 2026-07-13 - 11.1.1
4
25
 
5
26
  ### Fixes
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '11.1.1',
6
+ version: '11.3.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
@@ -1,3 +1,4 @@
1
+ import type { IRuntimeImageEvidence } from './immutableimage.js';
1
2
  /**
2
3
  * a deployment happens when a service is deployed
3
4
  * tracks the status of a deployment
@@ -28,6 +29,11 @@ export interface IDeployment {
28
29
  slot?: number;
29
30
  /** Docker desired state for this task/deployment. */
30
31
  desiredState?: string;
32
+ /** Immutable rollout generation represented by this task, when applicable. */
33
+ rolloutId?: string;
34
+ rolloutGeneration?: number;
35
+ /** Evidence derived from Docker runtime state rather than service intent metadata. */
36
+ runtimeImageEvidence?: IRuntimeImageEvidence;
31
37
  /**
32
38
  * Image used for this deployment
33
39
  */
@@ -3,6 +3,10 @@ import type { TDnsRecordType } from './dns.js';
3
3
  * Type of platform acting as a gateway client against a gateway (dcrouter).
4
4
  */
5
5
  export type TGatewayClientType = 'onebox' | 'cloudly' | 'custom';
6
+ /**
7
+ * Canonical managed route purposes understood by gateways.
8
+ */
9
+ export type TManagedRouteKind = 'letsencrypt-http01-forward';
6
10
  /**
7
11
  * Identity attached to gateway requests. Gateways run their own user/auth
8
12
  * domain (e.g. dcrouter operator accounts), so role/type stay open strings;
@@ -175,7 +179,11 @@ export interface IGatewayClientOwnership {
175
179
  appId: string;
176
180
  /** Hostname ownership for domain routes. Preserved for existing route keys. */
177
181
  hostname?: string;
178
- /** Stable non-domain route key, for example public-port mapping exposure refs. */
182
+ /**
183
+ * Stable route key. Without hostname it identifies a non-domain route such as
184
+ * a public-port mapping. Together with hostname it distinguishes multiple
185
+ * managed routes for the same hostname.
186
+ */
179
187
  routeRef?: string;
180
188
  }
181
189
  /**
@@ -224,6 +232,10 @@ export interface IGatewayRouteConfig {
224
232
  name: string;
225
233
  match: IGatewayRouteMatch;
226
234
  action: IGatewayRouteAction;
235
+ /** Higher values are matched first by the gateway. */
236
+ priority?: number;
237
+ /** Declares a canonical gateway-managed route purpose. */
238
+ managedRouteKind?: TManagedRouteKind;
227
239
  remoteIngress?: {
228
240
  enabled: boolean;
229
241
  edgeFilter?: string[];
@@ -1,4 +1,4 @@
1
- import type { IRegistryPushEvent } from './registry.js';
1
+ import type { IImmutableRegistryPushEvent, IRegistryPushEvent } from './registry.js';
2
2
  export interface IImage {
3
3
  id: string;
4
4
  data: {
@@ -13,6 +13,8 @@ export interface IImage {
13
13
  versions: Array<{
14
14
  versionString: string;
15
15
  digest?: string;
16
+ releaseId?: string;
17
+ manifestMediaType?: string;
16
18
  registryRepository?: string;
17
19
  registryTag?: string;
18
20
  source?: 'upload' | 'registry';
@@ -20,6 +22,6 @@ export interface IImage {
20
22
  size: number;
21
23
  createdAt: number;
22
24
  }>;
23
- lastPushEvent?: IRegistryPushEvent;
25
+ lastPushEvent?: IRegistryPushEvent | IImmutableRegistryPushEvent;
24
26
  };
25
27
  }
@@ -0,0 +1,145 @@
1
+ /** Canonical lower-case OCI sha256 digest. */
2
+ export type TSha256Digest = `sha256:${string}`;
3
+ export type TImageDeploymentPolicy = 'legacy-tag' | 'immutable-digest';
4
+ export type TOciManifestMediaType = 'application/vnd.oci.image.index.v1+json' | 'application/vnd.oci.image.manifest.v1+json' | 'application/vnd.docker.distribution.manifest.list.v2+json' | 'application/vnd.docker.distribution.manifest.v2+json';
5
+ export type TImmutableImageRolloutMode = 'promotion' | 'automatic' | 'rollback';
6
+ export type TImmutableImageMismatchReason = 'immutable-plan-missing' | 'requested-digest-missing' | 'requested-digest-malformed' | 'registry-digest-inconsistent' | 'runtime-capability-missing' | 'pull-failed' | 'pulled-digest-missing' | 'pulled-digest-mismatch' | 'service-image-not-digest-pinned' | 'service-image-digest-mismatch' | 'task-image-not-digest-pinned' | 'task-image-digest-mismatch' | 'container-inspection-unavailable' | 'container-image-id-missing' | 'container-repodigest-missing' | 'container-repodigest-mismatch' | 'runtime-attestation-missing' | 'required-replica-missing' | 'replica-unhealthy' | 'replica-failed' | 'stale-rollout-report' | 'rollback-target-not-accepted';
7
+ export type TRuntimeImageVerificationStatus = 'not-required' | 'pending' | 'verified' | 'missing' | 'mismatched';
8
+ export type TImmutableImageRolloutStatus = 'pending' | 'deploying' | 'verifying' | 'succeeded' | 'failed' | 'mismatched' | 'rolling-back' | 'rolled-back';
9
+ export interface ICoreflowRuntimeCapabilities {
10
+ immutableImageDeploymentVersion: 1;
11
+ }
12
+ /**
13
+ * A registry manifest observed by Cloudly. Observation alone does not make the
14
+ * release deployable: acceptedAt is set only by an explicit promotion or a
15
+ * configured automatic promotion.
16
+ */
17
+ export interface IImageRelease {
18
+ id: string;
19
+ data: {
20
+ serviceId: string;
21
+ imageId: string;
22
+ registryHost: string;
23
+ repository: string;
24
+ /** Digest of the registry root descriptor, preserving an index/list when submitted. */
25
+ registryRootDigest: TSha256Digest;
26
+ digestPinnedImageReference: string;
27
+ manifestMediaType: TOciManifestMediaType;
28
+ observedTags: string[];
29
+ firstObservedAt: number;
30
+ lastObservedAt: number;
31
+ observationCount: number;
32
+ acceptedAt?: number;
33
+ lastPromotedAt?: number;
34
+ promotionCount?: number;
35
+ };
36
+ }
37
+ export interface IImmutableImageTargetScope {
38
+ targetClusterIds: string[];
39
+ targetNodeNames: string[];
40
+ replicasPerNode: number;
41
+ requiredReplicaCount: number;
42
+ }
43
+ /** Immutable desired state issued by Cloudly and consumed by Coreflow. */
44
+ export interface IImmutableImageDeploymentPlan {
45
+ policy: 'immutable-digest';
46
+ rolloutId: string;
47
+ rolloutGeneration: number;
48
+ releaseId: string;
49
+ registryHost: string;
50
+ repository: string;
51
+ requestedDigest: TSha256Digest;
52
+ digestPinnedImageReference: string;
53
+ manifestMediaType: TOciManifestMediaType;
54
+ createdAt: number;
55
+ mode: TImmutableImageRolloutMode;
56
+ rollbackOfRolloutId?: string;
57
+ targetScope: IImmutableImageTargetScope;
58
+ }
59
+ /** Runtime evidence collected from Docker service, task, container and image state. */
60
+ interface IRuntimeImageEvidenceBase {
61
+ rolloutId?: string;
62
+ rolloutGeneration?: number;
63
+ expectedDigest?: TSha256Digest;
64
+ serviceImageReference?: string;
65
+ taskImageReference?: string;
66
+ taskReportedDigest?: TSha256Digest;
67
+ containerImageId?: string;
68
+ containerConfigImageReference?: string;
69
+ resolvedDigest?: TSha256Digest;
70
+ platformManifestDigest?: TSha256Digest;
71
+ repoDigests?: string[];
72
+ evidenceSource?: 'task-spec' | 'local-container-inspect';
73
+ observedAt: number;
74
+ }
75
+ export interface IVerifiedRuntimeImageEvidence extends IRuntimeImageEvidenceBase {
76
+ verificationStatus: 'verified';
77
+ rolloutId: string;
78
+ rolloutGeneration: number;
79
+ expectedDigest: TSha256Digest;
80
+ serviceImageReference: string;
81
+ taskImageReference: string;
82
+ taskReportedDigest: TSha256Digest;
83
+ containerImageId: string;
84
+ containerConfigImageReference: string;
85
+ resolvedDigest: TSha256Digest;
86
+ repoDigests: string[];
87
+ evidenceSource: 'local-container-inspect';
88
+ mismatchReason?: never;
89
+ }
90
+ export interface IUnverifiedRuntimeImageEvidence extends IRuntimeImageEvidenceBase {
91
+ verificationStatus: 'missing' | 'mismatched';
92
+ mismatchReason: TImmutableImageMismatchReason;
93
+ }
94
+ export interface IPendingRuntimeImageEvidence extends IRuntimeImageEvidenceBase {
95
+ verificationStatus: 'pending' | 'not-required';
96
+ mismatchReason?: never;
97
+ }
98
+ export type IRuntimeImageEvidence = IVerifiedRuntimeImageEvidence | IUnverifiedRuntimeImageEvidence | IPendingRuntimeImageEvidence;
99
+ /** Mandatory security envelope when reporting an immutable rollout. */
100
+ export interface IImmutableRolloutReportEnvelope {
101
+ rolloutId: string;
102
+ rolloutGeneration: number;
103
+ reporterSessionId: string;
104
+ reportSequence: number;
105
+ reporterCapabilities: ICoreflowRuntimeCapabilities;
106
+ reporterNodeIds: string[];
107
+ reporterNodeNames: string[];
108
+ }
109
+ export interface IImageRolloutReporterStatus {
110
+ reporterId: string;
111
+ reporterSessionId: string;
112
+ clusterId: string;
113
+ reporterNodeNames: string[];
114
+ rolloutId: string;
115
+ rolloutGeneration: number;
116
+ reportSequence: number;
117
+ reportedAt: number;
118
+ requiredReplicaCount: number;
119
+ observedReplicaCount: number;
120
+ healthyReplicaCount: number;
121
+ verifiedReplicaCount: number;
122
+ failedReplicaCount: number;
123
+ mismatchReason?: TImmutableImageMismatchReason;
124
+ }
125
+ export interface IImageRolloutStatus {
126
+ rolloutId: string;
127
+ rolloutGeneration: number;
128
+ expectedDigest: TSha256Digest;
129
+ status: TImmutableImageRolloutStatus;
130
+ requiredReplicaCount: number;
131
+ observedReplicaCount: number;
132
+ healthyReplicaCount: number;
133
+ verifiedReplicaCount: number;
134
+ failedReplicaCount: number;
135
+ mismatchReason?: TImmutableImageMismatchReason;
136
+ updatedAt: number;
137
+ reporters: IImageRolloutReporterStatus[];
138
+ }
139
+ export declare const normalizeSha256Digest: (digest: string) => TSha256Digest | undefined;
140
+ export declare const isSha256Digest: (digest: string) => digest is TSha256Digest;
141
+ export declare const buildDigestPinnedImageReference: (registryHost: string, repository: string, digest: TSha256Digest) => string;
142
+ export declare const getDigestFromPinnedImageReference: (imageReference: string) => TSha256Digest | undefined;
143
+ export declare const validateImmutableImageDeploymentPlan: (planArg: IImmutableImageDeploymentPlan) => string[];
144
+ export declare const validateImageRolloutStatus: (statusArg: IImageRolloutStatus) => string[];
145
+ export {};
@@ -0,0 +1,146 @@
1
+ const sha256DigestRegex = /^sha256:[a-f0-9]{64}$/;
2
+ export const normalizeSha256Digest = (digest) => {
3
+ const normalizedDigest = digest.trim().toLowerCase();
4
+ return sha256DigestRegex.test(normalizedDigest)
5
+ ? normalizedDigest
6
+ : undefined;
7
+ };
8
+ export const isSha256Digest = (digest) => sha256DigestRegex.test(digest);
9
+ const registryHostRegex = /^(?:localhost|[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(?::[1-9][0-9]{0,4})?$/;
10
+ const repositoryRegex = /^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
11
+ export const buildDigestPinnedImageReference = (registryHost, repository, digest) => {
12
+ if (!registryHostRegex.test(registryHost) || !repositoryRegex.test(repository)) {
13
+ throw new Error('registry host and repository must be canonical OCI names');
14
+ }
15
+ if (!isSha256Digest(digest)) {
16
+ throw new Error('digest must be a canonical sha256 digest');
17
+ }
18
+ return `${registryHost}/${repository}@${digest}`;
19
+ };
20
+ export const getDigestFromPinnedImageReference = (imageReference) => {
21
+ const separatorIndex = imageReference.lastIndexOf('@');
22
+ if (separatorIndex < 0) {
23
+ return undefined;
24
+ }
25
+ return normalizeSha256Digest(imageReference.slice(separatorIndex + 1));
26
+ };
27
+ export const validateImmutableImageDeploymentPlan = (planArg) => {
28
+ const errors = [];
29
+ if (!Number.isSafeInteger(planArg.rolloutGeneration) || planArg.rolloutGeneration < 1) {
30
+ errors.push('rolloutGeneration must be a positive safe integer');
31
+ }
32
+ if (!Number.isSafeInteger(planArg.createdAt) || planArg.createdAt < 1) {
33
+ errors.push('createdAt must be a positive safe integer');
34
+ }
35
+ let expectedReference;
36
+ try {
37
+ expectedReference = buildDigestPinnedImageReference(planArg.registryHost, planArg.repository, planArg.requestedDigest);
38
+ }
39
+ catch (error) {
40
+ errors.push(error.message);
41
+ }
42
+ if (expectedReference && planArg.digestPinnedImageReference !== expectedReference) {
43
+ errors.push('digestPinnedImageReference does not match registry, repository and digest');
44
+ }
45
+ const scope = planArg.targetScope;
46
+ if (!Number.isSafeInteger(scope.replicasPerNode) || scope.replicasPerNode < 1) {
47
+ errors.push('replicasPerNode must be a positive safe integer');
48
+ }
49
+ if (scope.targetClusterIds.length < 1 || scope.targetNodeNames.length < 1) {
50
+ errors.push('immutable rollout target scope must contain clusters and nodes');
51
+ }
52
+ if (new Set(scope.targetClusterIds).size !== scope.targetClusterIds.length
53
+ || new Set(scope.targetNodeNames).size !== scope.targetNodeNames.length) {
54
+ errors.push('immutable rollout target scope must not contain duplicates');
55
+ }
56
+ if (scope.requiredReplicaCount !== scope.targetNodeNames.length * scope.replicasPerNode) {
57
+ errors.push('requiredReplicaCount does not match target nodes and replicasPerNode');
58
+ }
59
+ if (planArg.mode === 'rollback' && !planArg.rollbackOfRolloutId) {
60
+ errors.push('rollback rollout must identify the rollout it replaces');
61
+ }
62
+ if (planArg.mode !== 'rollback' && planArg.rollbackOfRolloutId) {
63
+ errors.push('only rollback rollouts may set rollbackOfRolloutId');
64
+ }
65
+ return errors;
66
+ };
67
+ export const validateImageRolloutStatus = (statusArg) => {
68
+ const errors = [];
69
+ const counts = [
70
+ statusArg.requiredReplicaCount,
71
+ statusArg.observedReplicaCount,
72
+ statusArg.healthyReplicaCount,
73
+ statusArg.verifiedReplicaCount,
74
+ statusArg.failedReplicaCount,
75
+ ];
76
+ if (counts.some((countArg) => !Number.isSafeInteger(countArg) || countArg < 0)) {
77
+ errors.push('rollout replica counts must be non-negative safe integers');
78
+ }
79
+ const reporterIds = statusArg.reporters.map((reporterArg) => reporterArg.reporterId);
80
+ const reporterSessionIds = statusArg.reporters.map((reporterArg) => reporterArg.reporterSessionId);
81
+ const reporterNodeNames = statusArg.reporters.flatMap((reporterArg) => reporterArg.reporterNodeNames);
82
+ if (new Set(reporterIds).size !== reporterIds.length
83
+ || new Set(reporterSessionIds).size !== reporterSessionIds.length) {
84
+ errors.push('rollout status must contain one latest report per reporter and session');
85
+ }
86
+ for (const reporter of statusArg.reporters) {
87
+ const reporterCounts = [
88
+ reporter.requiredReplicaCount,
89
+ reporter.observedReplicaCount,
90
+ reporter.healthyReplicaCount,
91
+ reporter.verifiedReplicaCount,
92
+ reporter.failedReplicaCount,
93
+ ];
94
+ if (reporterCounts.some((countArg) => !Number.isSafeInteger(countArg) || countArg < 0)) {
95
+ errors.push('reporter replica counts must be non-negative safe integers');
96
+ }
97
+ if (reporter.rolloutId !== statusArg.rolloutId
98
+ || reporter.rolloutGeneration !== statusArg.rolloutGeneration) {
99
+ errors.push('reporter rollout identity does not match aggregate rollout identity');
100
+ }
101
+ if (reporter.observedReplicaCount > reporter.requiredReplicaCount
102
+ || reporter.healthyReplicaCount > reporter.observedReplicaCount
103
+ || reporter.verifiedReplicaCount > reporter.observedReplicaCount) {
104
+ errors.push('reporter replica counts are contradictory');
105
+ }
106
+ }
107
+ if (statusArg.status === 'succeeded' || statusArg.status === 'rolled-back') {
108
+ if (statusArg.requiredReplicaCount < 1
109
+ || statusArg.observedReplicaCount !== statusArg.requiredReplicaCount
110
+ || statusArg.healthyReplicaCount !== statusArg.requiredReplicaCount
111
+ || statusArg.verifiedReplicaCount !== statusArg.requiredReplicaCount
112
+ || statusArg.failedReplicaCount !== 0) {
113
+ errors.push('successful rollout status requires every replica to be observed, healthy and verified');
114
+ }
115
+ if (statusArg.mismatchReason || statusArg.reporters.length < 1) {
116
+ errors.push('successful rollout status requires reporters without mismatch reasons');
117
+ }
118
+ if (new Set(reporterNodeNames).size !== reporterNodeNames.length) {
119
+ errors.push('successful rollout status must not count a node in multiple reporters');
120
+ }
121
+ if (statusArg.reporters.some((reporterArg) => (reporterArg.requiredReplicaCount < 1
122
+ || reporterArg.observedReplicaCount !== reporterArg.requiredReplicaCount
123
+ || reporterArg.healthyReplicaCount !== reporterArg.requiredReplicaCount
124
+ || reporterArg.verifiedReplicaCount !== reporterArg.requiredReplicaCount
125
+ || reporterArg.failedReplicaCount !== 0
126
+ || Boolean(reporterArg.mismatchReason)))) {
127
+ errors.push('successful rollout requires every reporter replica to be healthy and verified');
128
+ }
129
+ const reporterTotals = statusArg.reporters.reduce((totalsArg, reporterArg) => ({
130
+ required: totalsArg.required + reporterArg.requiredReplicaCount,
131
+ observed: totalsArg.observed + reporterArg.observedReplicaCount,
132
+ healthy: totalsArg.healthy + reporterArg.healthyReplicaCount,
133
+ verified: totalsArg.verified + reporterArg.verifiedReplicaCount,
134
+ failed: totalsArg.failed + reporterArg.failedReplicaCount,
135
+ }), { required: 0, observed: 0, healthy: 0, verified: 0, failed: 0 });
136
+ if (reporterTotals.required !== statusArg.requiredReplicaCount
137
+ || reporterTotals.observed !== statusArg.observedReplicaCount
138
+ || reporterTotals.healthy !== statusArg.healthyReplicaCount
139
+ || reporterTotals.verified !== statusArg.verifiedReplicaCount
140
+ || reporterTotals.failed !== statusArg.failedReplicaCount) {
141
+ errors.push('successful rollout aggregate must exactly match successful reporter evidence');
142
+ }
143
+ }
144
+ return errors;
145
+ };
146
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1tdXRhYmxlaW1hZ2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2ltbXV0YWJsZWltYWdlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXVNQSxNQUFNLGlCQUFpQixHQUFHLHVCQUF1QixDQUFDO0FBRWxELE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUFHLENBQUMsTUFBYyxFQUE2QixFQUFFO0lBQ2pGLE1BQU0sZ0JBQWdCLEdBQUcsTUFBTSxDQUFDLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxDQUFDO0lBQ3JELE9BQU8saUJBQWlCLENBQUMsSUFBSSxDQUFDLGdCQUFnQixDQUFDO1FBQzdDLENBQUMsQ0FBRSxnQkFBa0M7UUFDckMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztBQUNoQixDQUFDLENBQUM7QUFFRixNQUFNLENBQUMsTUFBTSxjQUFjLEdBQUcsQ0FBQyxNQUFjLEVBQTJCLEVBQUUsQ0FDeEUsaUJBQWlCLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0FBRWpDLE1BQU0saUJBQWlCLEdBQUcsdUVBQXVFLENBQUM7QUFDbEcsTUFBTSxlQUFlLEdBQUcsbUVBQW1FLENBQUM7QUFFNUYsTUFBTSxDQUFDLE1BQU0sK0JBQStCLEdBQUcsQ0FDN0MsWUFBb0IsRUFDcEIsVUFBa0IsRUFDbEIsTUFBcUIsRUFDYixFQUFFO0lBQ1YsSUFBSSxDQUFDLGlCQUFpQixDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLGVBQWUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQztRQUMvRSxNQUFNLElBQUksS0FBSyxDQUFDLDBEQUEwRCxDQUFDLENBQUM7SUFDOUUsQ0FBQztJQUNELElBQUksQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUM1QixNQUFNLElBQUksS0FBSyxDQUFDLDBDQUEwQyxDQUFDLENBQUM7SUFDOUQsQ0FBQztJQUNELE9BQU8sR0FBRyxZQUFZLElB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
@@ -11,6 +11,7 @@ export * from './externalregistry.js';
11
11
  export * from './gateway.js';
12
12
  export * from './hostedapp.js';
13
13
  export * from './image.js';
14
+ export * from './immutableimage.js';
14
15
  export * from './isolatedrestore.js';
15
16
  export * from './isolatedrestore.golden.js';
16
17
  export * from './mail.js';
@@ -11,6 +11,7 @@ export * from './externalregistry.js';
11
11
  export * from './gateway.js';
12
12
  export * from './hostedapp.js';
13
13
  export * from './image.js';
14
+ export * from './immutableimage.js';
14
15
  export * from './isolatedrestore.js';
15
16
  export * from './isolatedrestore.golden.js';
16
17
  export * from './mail.js';
@@ -29,4 +30,4 @@ export * from './taskexecution.js';
29
30
  export * from './traffic.js';
30
31
  export * from './user.js';
31
32
  export * from './version.js';
32
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsVUFBVSxDQUFDO0FBQ3pCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxjQUFjLENBQUMifQ==
33
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsVUFBVSxDQUFDO0FBQ3pCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGNBQWMsQ0FBQyJ9
@@ -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'>;
@@ -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,9 @@
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';
5
7
  export interface IRequest_Any_Cloudly_GetServiceById extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceById> {
6
8
  method: 'getServiceById';
7
9
  request: {
@@ -32,11 +34,51 @@ export interface IRequest_Any_Cloudly_GetServiceRegistryTarget extends plugins.t
32
34
  registryTarget: IRegistryTarget;
33
35
  };
34
36
  }
37
+ export interface IRequest_Any_Cloudly_GetServiceImageReleases extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceImageReleases> {
38
+ method: 'getServiceImageReleases';
39
+ request: {
40
+ identity: IIdentity;
41
+ serviceId: string;
42
+ };
43
+ response: {
44
+ releases: IImageRelease[];
45
+ };
46
+ }
47
+ export interface IRequest_Any_Cloudly_PromoteServiceImageRelease extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_PromoteServiceImageRelease> {
48
+ method: 'promoteServiceImageRelease';
49
+ request: {
50
+ identity: IIdentity;
51
+ serviceId: string;
52
+ releaseId: string;
53
+ idempotencyKey: string;
54
+ expectedRolloutGeneration: number;
55
+ expectedCurrentRolloutId?: string;
56
+ /** Explicitly opts the service into fail-closed immutable deployment. */
57
+ enableImmutableImage: true;
58
+ };
59
+ response: {
60
+ service: IService;
61
+ };
62
+ }
63
+ export interface IRequest_Any_Cloudly_RollbackServiceImageRelease extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_RollbackServiceImageRelease> {
64
+ method: 'rollbackServiceImageRelease';
65
+ request: {
66
+ identity: IIdentity;
67
+ serviceId: string;
68
+ releaseId: string;
69
+ idempotencyKey: string;
70
+ expectedCurrentRolloutId: string;
71
+ expectedRolloutGeneration: number;
72
+ };
73
+ response: {
74
+ service: IService;
75
+ };
76
+ }
35
77
  export interface IRequest_Any_Cloudly_CreateService extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_CreateService> {
36
78
  method: 'createService';
37
79
  request: {
38
80
  identity: IIdentity;
39
- serviceData: IService['data'];
81
+ serviceData: TServiceWritableData;
40
82
  };
41
83
  response: {
42
84
  service: IService;
@@ -47,7 +89,7 @@ export interface IRequest_Any_Cloudly_UpdateService extends plugins.typedrequest
47
89
  request: {
48
90
  identity: IIdentity;
49
91
  serviceId: string;
50
- serviceData: IService['data'];
92
+ serviceData: TServiceWritableData;
51
93
  };
52
94
  response: {
53
95
  service: IService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "11.1.1",
3
+ "version": "11.3.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
@@ -19,7 +19,7 @@
19
19
  "@git.zone/tsdoc": "^2.1.1",
20
20
  "@git.zone/tsrun": "^2.0.5",
21
21
  "@git.zone/tstest": "^3.6.6",
22
- "@types/node": "^26.1.0"
22
+ "@types/node": "^26.1.1"
23
23
  },
24
24
  "files": [
25
25
  "ts/**/*",
package/readme.md CHANGED
@@ -95,7 +95,7 @@ Common data contracts include:
95
95
  - `IService`, `IDeployment`, `IImage`, `IRegistryTarget`, and `IExternalRegistry` for workload delivery.
96
96
  - Service port contracts including `IServiceTargetPort`, `IServiceDomainRoute`, and `IServicePublicPortMapping` for canonical backend targets, domain target references, and edge/Coretraffic TCP/UDP public exposure.
97
97
  - `IDomain`, `IDnsEntry`, and traffic contracts for routing and DNS management.
98
- - Traffic and gateway route contracts including `ICoretrafficPortRouteConfig`, routing `portRoutes`, `IGatewayClientRoute` client-owned route views, gateway route `routeRef` ownership, optional match `domains`, `transport`, and `remoteIngress` route intent.
98
+ - Traffic and gateway route contracts including `ICoretrafficPortRouteConfig`, routing `portRoutes`, and `IGatewayClientRoute` client-owned route views. Gateway route intent supports optional match `domains`, `transport`, and `remoteIngress`, plus explicit route `priority` and `managedRouteKind`. Ownership can combine `hostname` with `routeRef` so a normal route and a path-specific managed route for the same hostname reconcile independently.
99
99
  - `ISecretBundle` and `ISecretGroup` for secret ownership and shared secret groups.
100
100
  - Mail gateway contracts for domain authorities, address bindings, WorkApp bindings, managed SMTP/API credentials, spool items, delivery journals, and inbound/outbound message payloads.
101
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.
@@ -140,7 +140,7 @@ 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
- Gateway request contracts include `getGatewayClientRoutes` (`requests.gateway.IReq_GetGatewayClientRoutes`) for listing owned `IGatewayClientRoute[]` route views, and `syncGatewayClientRoute` for idempotently syncing or deleting hostname-owned and routeRef-owned routes. 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.
143
+ 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
144
 
145
145
  Request groups are exported by product area:
146
146
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '11.1.1',
6
+ version: '11.3.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
@@ -5,6 +5,11 @@ import type { TDnsRecordType } from './dns.js';
5
5
  */
6
6
  export type TGatewayClientType = 'onebox' | 'cloudly' | 'custom';
7
7
 
8
+ /**
9
+ * Canonical managed route purposes understood by gateways.
10
+ */
11
+ export type TManagedRouteKind = 'letsencrypt-http01-forward';
12
+
8
13
  /**
9
14
  * Identity attached to gateway requests. Gateways run their own user/auth
10
15
  * domain (e.g. dcrouter operator accounts), so role/type stay open strings;
@@ -187,7 +192,11 @@ export interface IGatewayClientOwnership {
187
192
  appId: string;
188
193
  /** Hostname ownership for domain routes. Preserved for existing route keys. */
189
194
  hostname?: string;
190
- /** Stable non-domain route key, for example public-port mapping exposure refs. */
195
+ /**
196
+ * Stable route key. Without hostname it identifies a non-domain route such as
197
+ * a public-port mapping. Together with hostname it distinguishes multiple
198
+ * managed routes for the same hostname.
199
+ */
191
200
  routeRef?: string;
192
201
  }
193
202
 
@@ -241,6 +250,10 @@ export interface IGatewayRouteConfig {
241
250
  name: string;
242
251
  match: IGatewayRouteMatch;
243
252
  action: IGatewayRouteAction;
253
+ /** Higher values are matched first by the gateway. */
254
+ priority?: number;
255
+ /** Declares a canonical gateway-managed route purpose. */
256
+ managedRouteKind?: TManagedRouteKind;
244
257
  remoteIngress?: {
245
258
  enabled: boolean;
246
259
  edgeFilter?: string[];
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
  }
@@ -0,0 +1,373 @@
1
+ /** Canonical lower-case OCI sha256 digest. */
2
+ export type TSha256Digest = `sha256:${string}`;
3
+
4
+ export type TImageDeploymentPolicy = 'legacy-tag' | 'immutable-digest';
5
+
6
+ export type TOciManifestMediaType =
7
+ | 'application/vnd.oci.image.index.v1+json'
8
+ | 'application/vnd.oci.image.manifest.v1+json'
9
+ | 'application/vnd.docker.distribution.manifest.list.v2+json'
10
+ | 'application/vnd.docker.distribution.manifest.v2+json';
11
+
12
+ export type TImmutableImageRolloutMode = 'promotion' | 'automatic' | 'rollback';
13
+
14
+ export type TImmutableImageMismatchReason =
15
+ | 'immutable-plan-missing'
16
+ | 'requested-digest-missing'
17
+ | 'requested-digest-malformed'
18
+ | 'registry-digest-inconsistent'
19
+ | 'runtime-capability-missing'
20
+ | 'pull-failed'
21
+ | 'pulled-digest-missing'
22
+ | 'pulled-digest-mismatch'
23
+ | 'service-image-not-digest-pinned'
24
+ | 'service-image-digest-mismatch'
25
+ | 'task-image-not-digest-pinned'
26
+ | 'task-image-digest-mismatch'
27
+ | 'container-inspection-unavailable'
28
+ | 'container-image-id-missing'
29
+ | 'container-repodigest-missing'
30
+ | 'container-repodigest-mismatch'
31
+ | 'runtime-attestation-missing'
32
+ | 'required-replica-missing'
33
+ | 'replica-unhealthy'
34
+ | 'replica-failed'
35
+ | 'stale-rollout-report'
36
+ | 'rollback-target-not-accepted';
37
+
38
+ export type TRuntimeImageVerificationStatus =
39
+ | 'not-required'
40
+ | 'pending'
41
+ | 'verified'
42
+ | 'missing'
43
+ | 'mismatched';
44
+
45
+ export type TImmutableImageRolloutStatus =
46
+ | 'pending'
47
+ | 'deploying'
48
+ | 'verifying'
49
+ | 'succeeded'
50
+ | 'failed'
51
+ | 'mismatched'
52
+ | 'rolling-back'
53
+ | 'rolled-back';
54
+
55
+ export interface ICoreflowRuntimeCapabilities {
56
+ immutableImageDeploymentVersion: 1;
57
+ }
58
+
59
+ /**
60
+ * A registry manifest observed by Cloudly. Observation alone does not make the
61
+ * release deployable: acceptedAt is set only by an explicit promotion or a
62
+ * configured automatic promotion.
63
+ */
64
+ export interface IImageRelease {
65
+ id: string;
66
+ data: {
67
+ serviceId: string;
68
+ imageId: string;
69
+ registryHost: string;
70
+ repository: string;
71
+ /** Digest of the registry root descriptor, preserving an index/list when submitted. */
72
+ registryRootDigest: TSha256Digest;
73
+ digestPinnedImageReference: string;
74
+ manifestMediaType: TOciManifestMediaType;
75
+ observedTags: string[];
76
+ firstObservedAt: number;
77
+ lastObservedAt: number;
78
+ observationCount: number;
79
+ acceptedAt?: number;
80
+ lastPromotedAt?: number;
81
+ promotionCount?: number;
82
+ };
83
+ }
84
+
85
+ export interface IImmutableImageTargetScope {
86
+ targetClusterIds: string[];
87
+ targetNodeNames: string[];
88
+ replicasPerNode: number;
89
+ requiredReplicaCount: number;
90
+ }
91
+
92
+ /** Immutable desired state issued by Cloudly and consumed by Coreflow. */
93
+ export interface IImmutableImageDeploymentPlan {
94
+ policy: 'immutable-digest';
95
+ rolloutId: string;
96
+ rolloutGeneration: number;
97
+ releaseId: string;
98
+ registryHost: string;
99
+ repository: string;
100
+ requestedDigest: TSha256Digest;
101
+ digestPinnedImageReference: string;
102
+ manifestMediaType: TOciManifestMediaType;
103
+ createdAt: number;
104
+ mode: TImmutableImageRolloutMode;
105
+ rollbackOfRolloutId?: string;
106
+ targetScope: IImmutableImageTargetScope;
107
+ }
108
+
109
+ /** Runtime evidence collected from Docker service, task, container and image state. */
110
+ interface IRuntimeImageEvidenceBase {
111
+ rolloutId?: string;
112
+ rolloutGeneration?: number;
113
+ expectedDigest?: TSha256Digest;
114
+ serviceImageReference?: string;
115
+ taskImageReference?: string;
116
+ taskReportedDigest?: TSha256Digest;
117
+ containerImageId?: string;
118
+ containerConfigImageReference?: string;
119
+ resolvedDigest?: TSha256Digest;
120
+ platformManifestDigest?: TSha256Digest;
121
+ repoDigests?: string[];
122
+ evidenceSource?: 'task-spec' | 'local-container-inspect';
123
+ observedAt: number;
124
+ }
125
+
126
+ export interface IVerifiedRuntimeImageEvidence extends IRuntimeImageEvidenceBase {
127
+ verificationStatus: 'verified';
128
+ rolloutId: string;
129
+ rolloutGeneration: number;
130
+ expectedDigest: TSha256Digest;
131
+ serviceImageReference: string;
132
+ taskImageReference: string;
133
+ taskReportedDigest: TSha256Digest;
134
+ containerImageId: string;
135
+ containerConfigImageReference: string;
136
+ resolvedDigest: TSha256Digest;
137
+ repoDigests: string[];
138
+ evidenceSource: 'local-container-inspect';
139
+ mismatchReason?: never;
140
+ }
141
+
142
+ export interface IUnverifiedRuntimeImageEvidence extends IRuntimeImageEvidenceBase {
143
+ verificationStatus: 'missing' | 'mismatched';
144
+ mismatchReason: TImmutableImageMismatchReason;
145
+ }
146
+
147
+ export interface IPendingRuntimeImageEvidence extends IRuntimeImageEvidenceBase {
148
+ verificationStatus: 'pending' | 'not-required';
149
+ mismatchReason?: never;
150
+ }
151
+
152
+ export type IRuntimeImageEvidence =
153
+ | IVerifiedRuntimeImageEvidence
154
+ | IUnverifiedRuntimeImageEvidence
155
+ | IPendingRuntimeImageEvidence;
156
+
157
+ /** Mandatory security envelope when reporting an immutable rollout. */
158
+ export interface IImmutableRolloutReportEnvelope {
159
+ rolloutId: string;
160
+ rolloutGeneration: number;
161
+ reporterSessionId: string;
162
+ reportSequence: number;
163
+ reporterCapabilities: ICoreflowRuntimeCapabilities;
164
+ reporterNodeIds: string[];
165
+ reporterNodeNames: string[];
166
+ }
167
+
168
+ export interface IImageRolloutReporterStatus {
169
+ reporterId: string;
170
+ reporterSessionId: string;
171
+ clusterId: string;
172
+ reporterNodeNames: string[];
173
+ rolloutId: string;
174
+ rolloutGeneration: number;
175
+ reportSequence: number;
176
+ reportedAt: number;
177
+ requiredReplicaCount: number;
178
+ observedReplicaCount: number;
179
+ healthyReplicaCount: number;
180
+ verifiedReplicaCount: number;
181
+ failedReplicaCount: number;
182
+ mismatchReason?: TImmutableImageMismatchReason;
183
+ }
184
+
185
+ export interface IImageRolloutStatus {
186
+ rolloutId: string;
187
+ rolloutGeneration: number;
188
+ expectedDigest: TSha256Digest;
189
+ status: TImmutableImageRolloutStatus;
190
+ requiredReplicaCount: number;
191
+ observedReplicaCount: number;
192
+ healthyReplicaCount: number;
193
+ verifiedReplicaCount: number;
194
+ failedReplicaCount: number;
195
+ mismatchReason?: TImmutableImageMismatchReason;
196
+ updatedAt: number;
197
+ reporters: IImageRolloutReporterStatus[];
198
+ }
199
+
200
+ const sha256DigestRegex = /^sha256:[a-f0-9]{64}$/;
201
+
202
+ export const normalizeSha256Digest = (digest: string): TSha256Digest | undefined => {
203
+ const normalizedDigest = digest.trim().toLowerCase();
204
+ return sha256DigestRegex.test(normalizedDigest)
205
+ ? (normalizedDigest as TSha256Digest)
206
+ : undefined;
207
+ };
208
+
209
+ export const isSha256Digest = (digest: string): digest is TSha256Digest =>
210
+ sha256DigestRegex.test(digest);
211
+
212
+ const registryHostRegex = /^(?:localhost|[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(?::[1-9][0-9]{0,4})?$/;
213
+ const repositoryRegex = /^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
214
+
215
+ export const buildDigestPinnedImageReference = (
216
+ registryHost: string,
217
+ repository: string,
218
+ digest: TSha256Digest
219
+ ): string => {
220
+ if (!registryHostRegex.test(registryHost) || !repositoryRegex.test(repository)) {
221
+ throw new Error('registry host and repository must be canonical OCI names');
222
+ }
223
+ if (!isSha256Digest(digest)) {
224
+ throw new Error('digest must be a canonical sha256 digest');
225
+ }
226
+ return `${registryHost}/${repository}@${digest}`;
227
+ };
228
+
229
+ export const getDigestFromPinnedImageReference = (
230
+ imageReference: string
231
+ ): TSha256Digest | undefined => {
232
+ const separatorIndex = imageReference.lastIndexOf('@');
233
+ if (separatorIndex < 0) {
234
+ return undefined;
235
+ }
236
+ return normalizeSha256Digest(imageReference.slice(separatorIndex + 1));
237
+ };
238
+
239
+ export const validateImmutableImageDeploymentPlan = (
240
+ planArg: IImmutableImageDeploymentPlan
241
+ ): string[] => {
242
+ const errors: string[] = [];
243
+ if (!Number.isSafeInteger(planArg.rolloutGeneration) || planArg.rolloutGeneration < 1) {
244
+ errors.push('rolloutGeneration must be a positive safe integer');
245
+ }
246
+ if (!Number.isSafeInteger(planArg.createdAt) || planArg.createdAt < 1) {
247
+ errors.push('createdAt must be a positive safe integer');
248
+ }
249
+ let expectedReference: string | undefined;
250
+ try {
251
+ expectedReference = buildDigestPinnedImageReference(
252
+ planArg.registryHost,
253
+ planArg.repository,
254
+ planArg.requestedDigest
255
+ );
256
+ } catch (error) {
257
+ errors.push((error as Error).message);
258
+ }
259
+ if (expectedReference && planArg.digestPinnedImageReference !== expectedReference) {
260
+ errors.push('digestPinnedImageReference does not match registry, repository and digest');
261
+ }
262
+ const scope = planArg.targetScope;
263
+ if (!Number.isSafeInteger(scope.replicasPerNode) || scope.replicasPerNode < 1) {
264
+ errors.push('replicasPerNode must be a positive safe integer');
265
+ }
266
+ if (scope.targetClusterIds.length < 1 || scope.targetNodeNames.length < 1) {
267
+ errors.push('immutable rollout target scope must contain clusters and nodes');
268
+ }
269
+ if (new Set(scope.targetClusterIds).size !== scope.targetClusterIds.length
270
+ || new Set(scope.targetNodeNames).size !== scope.targetNodeNames.length) {
271
+ errors.push('immutable rollout target scope must not contain duplicates');
272
+ }
273
+ if (scope.requiredReplicaCount !== scope.targetNodeNames.length * scope.replicasPerNode) {
274
+ errors.push('requiredReplicaCount does not match target nodes and replicasPerNode');
275
+ }
276
+ if (planArg.mode === 'rollback' && !planArg.rollbackOfRolloutId) {
277
+ errors.push('rollback rollout must identify the rollout it replaces');
278
+ }
279
+ if (planArg.mode !== 'rollback' && planArg.rollbackOfRolloutId) {
280
+ errors.push('only rollback rollouts may set rollbackOfRolloutId');
281
+ }
282
+ return errors;
283
+ };
284
+
285
+ export const validateImageRolloutStatus = (statusArg: IImageRolloutStatus): string[] => {
286
+ const errors: string[] = [];
287
+ const counts = [
288
+ statusArg.requiredReplicaCount,
289
+ statusArg.observedReplicaCount,
290
+ statusArg.healthyReplicaCount,
291
+ statusArg.verifiedReplicaCount,
292
+ statusArg.failedReplicaCount,
293
+ ];
294
+ if (counts.some((countArg) => !Number.isSafeInteger(countArg) || countArg < 0)) {
295
+ errors.push('rollout replica counts must be non-negative safe integers');
296
+ }
297
+ const reporterIds = statusArg.reporters.map((reporterArg) => reporterArg.reporterId);
298
+ const reporterSessionIds = statusArg.reporters.map((reporterArg) => reporterArg.reporterSessionId);
299
+ const reporterNodeNames = statusArg.reporters.flatMap(
300
+ (reporterArg) => reporterArg.reporterNodeNames
301
+ );
302
+ if (new Set(reporterIds).size !== reporterIds.length
303
+ || new Set(reporterSessionIds).size !== reporterSessionIds.length) {
304
+ errors.push('rollout status must contain one latest report per reporter and session');
305
+ }
306
+ for (const reporter of statusArg.reporters) {
307
+ const reporterCounts = [
308
+ reporter.requiredReplicaCount,
309
+ reporter.observedReplicaCount,
310
+ reporter.healthyReplicaCount,
311
+ reporter.verifiedReplicaCount,
312
+ reporter.failedReplicaCount,
313
+ ];
314
+ if (reporterCounts.some((countArg) => !Number.isSafeInteger(countArg) || countArg < 0)) {
315
+ errors.push('reporter replica counts must be non-negative safe integers');
316
+ }
317
+ if (reporter.rolloutId !== statusArg.rolloutId
318
+ || reporter.rolloutGeneration !== statusArg.rolloutGeneration) {
319
+ errors.push('reporter rollout identity does not match aggregate rollout identity');
320
+ }
321
+ if (
322
+ reporter.observedReplicaCount > reporter.requiredReplicaCount
323
+ || reporter.healthyReplicaCount > reporter.observedReplicaCount
324
+ || reporter.verifiedReplicaCount > reporter.observedReplicaCount
325
+ ) {
326
+ errors.push('reporter replica counts are contradictory');
327
+ }
328
+ }
329
+ if (statusArg.status === 'succeeded' || statusArg.status === 'rolled-back') {
330
+ if (
331
+ statusArg.requiredReplicaCount < 1
332
+ || statusArg.observedReplicaCount !== statusArg.requiredReplicaCount
333
+ || statusArg.healthyReplicaCount !== statusArg.requiredReplicaCount
334
+ || statusArg.verifiedReplicaCount !== statusArg.requiredReplicaCount
335
+ || statusArg.failedReplicaCount !== 0
336
+ ) {
337
+ errors.push('successful rollout status requires every replica to be observed, healthy and verified');
338
+ }
339
+ if (statusArg.mismatchReason || statusArg.reporters.length < 1) {
340
+ errors.push('successful rollout status requires reporters without mismatch reasons');
341
+ }
342
+ if (new Set(reporterNodeNames).size !== reporterNodeNames.length) {
343
+ errors.push('successful rollout status must not count a node in multiple reporters');
344
+ }
345
+ if (statusArg.reporters.some((reporterArg) => (
346
+ reporterArg.requiredReplicaCount < 1
347
+ || reporterArg.observedReplicaCount !== reporterArg.requiredReplicaCount
348
+ || reporterArg.healthyReplicaCount !== reporterArg.requiredReplicaCount
349
+ || reporterArg.verifiedReplicaCount !== reporterArg.requiredReplicaCount
350
+ || reporterArg.failedReplicaCount !== 0
351
+ || Boolean(reporterArg.mismatchReason)
352
+ ))) {
353
+ errors.push('successful rollout requires every reporter replica to be healthy and verified');
354
+ }
355
+ const reporterTotals = statusArg.reporters.reduce((totalsArg, reporterArg) => ({
356
+ required: totalsArg.required + reporterArg.requiredReplicaCount,
357
+ observed: totalsArg.observed + reporterArg.observedReplicaCount,
358
+ healthy: totalsArg.healthy + reporterArg.healthyReplicaCount,
359
+ verified: totalsArg.verified + reporterArg.verifiedReplicaCount,
360
+ failed: totalsArg.failed + reporterArg.failedReplicaCount,
361
+ }), { required: 0, observed: 0, healthy: 0, verified: 0, failed: 0 });
362
+ if (
363
+ reporterTotals.required !== statusArg.requiredReplicaCount
364
+ || reporterTotals.observed !== statusArg.observedReplicaCount
365
+ || reporterTotals.healthy !== statusArg.healthyReplicaCount
366
+ || reporterTotals.verified !== statusArg.verifiedReplicaCount
367
+ || reporterTotals.failed !== statusArg.failedReplicaCount
368
+ ) {
369
+ errors.push('successful rollout aggregate must exactly match successful reporter evidence');
370
+ }
371
+ }
372
+ return errors;
373
+ };
package/ts/data/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from './externalregistry.js';
11
11
  export * from './gateway.js';
12
12
  export * from './hostedapp.js';
13
13
  export * from './image.js';
14
+ export * from './immutableimage.js';
14
15
  export * from './isolatedrestore.js';
15
16
  export * from './isolatedrestore.golden.js';
16
17
  export * from './mail.js';
@@ -1,5 +1,10 @@
1
1
  export type TRegistryProtocol = 'oci';
2
2
 
3
+ import type {
4
+ TOciManifestMediaType,
5
+ TSha256Digest,
6
+ } from './immutableimage.js';
7
+
3
8
  export interface IRegistryTarget {
4
9
  protocol: TRegistryProtocol;
5
10
  registryHost: string;
@@ -11,6 +16,8 @@ export interface IRegistryTarget {
11
16
  }
12
17
 
13
18
  export interface IRegistryPushEvent {
19
+ eventId?: string;
20
+ releaseId?: string;
14
21
  protocol: TRegistryProtocol;
15
22
  registryHost: string;
16
23
  repository: string;
@@ -18,8 +25,28 @@ export interface IRegistryPushEvent {
18
25
  digest: string;
19
26
  imageUrl: string;
20
27
  pushedAt: number;
28
+ observedAt?: number;
21
29
  serviceId?: string;
22
30
  imageId?: string;
23
31
  actorUserId?: string;
24
32
  manifestMediaType?: string;
25
33
  }
34
+
35
+ /** Fully verified registry observation used by immutable release processing. */
36
+ export interface IImmutableRegistryPushEvent {
37
+ deploymentPolicy: 'immutable-digest';
38
+ eventId: string;
39
+ releaseId: string;
40
+ protocol: TRegistryProtocol;
41
+ registryHost: string;
42
+ repository: string;
43
+ tag: string;
44
+ registryRootDigest: TSha256Digest;
45
+ digestPinnedImageReference: string;
46
+ imageUrl: string;
47
+ observedAt: number;
48
+ serviceId: string;
49
+ imageId: string;
50
+ manifestMediaType: TOciManifestMediaType;
51
+ actorUserId?: string;
52
+ }
@@ -3,6 +3,10 @@ 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
+ IImageRolloutStatus,
8
+ IImmutableImageDeploymentPlan,
9
+ } from './immutableimage.js';
6
10
  import type {
7
11
  IServiceDomainRoute,
8
12
  IServicePublicPortMapping,
@@ -70,6 +74,12 @@ export interface IService {
70
74
  imageVersion: string;
71
75
  registryTarget?: IRegistryTarget;
72
76
  deployOnPush?: boolean;
77
+ /** Fail closed unless Cloudly supplies and Coreflow verifies a digest-pinned rollout. */
78
+ immutableImageRequired?: boolean;
79
+ /** Server-managed immutable desired state. Use the promotion/rollback APIs to change it. */
80
+ imageDeployment?: IImmutableImageDeploymentPlan;
81
+ /** Server-managed aggregate of task-derived runtime evidence. */
82
+ imageRolloutStatus?: IImageRolloutStatus;
73
83
 
74
84
  /**
75
85
  * When true the service tolerates overlapping deployments during
@@ -151,3 +161,9 @@ export interface IService {
151
161
  deploymentIds: string[];
152
162
  };
153
163
  }
164
+
165
+ /** Caller-writable service data. Immutable policy and rollout fields are server-managed. */
166
+ export type TServiceWritableData = Omit<
167
+ IService['data'],
168
+ 'immutableImageRequired' | 'imageDeployment' | 'imageRolloutStatus'
169
+ >;
@@ -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
 
6
7
  export interface IDeploymentWorkspaceFileEntry {
7
8
  type: 'file' | 'directory';
@@ -452,6 +453,8 @@ extends plugins.typedrequestInterfaces.implementsTR<
452
453
  * When absent, the snapshot replaces all persisted deployments of the service.
453
454
  */
454
455
  reporterNodeNames?: string[];
456
+ /** Mandatory when the referenced service requires immutable deployment. */
457
+ immutableReport?: IImmutableRolloutReportEnvelope;
455
458
  reportedAt: number;
456
459
  };
457
460
  response: Record<string, never>;
@@ -1,8 +1,10 @@
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';
5
6
  import type { IServiceRessources } from '../data/docker.js';
7
+ import type { IImageRelease } from '../data/immutableimage.js';
6
8
 
7
9
  export interface IRequest_Any_Cloudly_GetServiceById
8
10
  extends plugins.typedrequestInterfaces.implementsTR<
@@ -49,6 +51,61 @@ extends plugins.typedrequestInterfaces.implementsTR<
49
51
  };
50
52
  }
51
53
 
54
+ export interface IRequest_Any_Cloudly_GetServiceImageReleases
55
+ extends plugins.typedrequestInterfaces.implementsTR<
56
+ plugins.typedrequestInterfaces.ITypedRequest,
57
+ IRequest_Any_Cloudly_GetServiceImageReleases
58
+ > {
59
+ method: 'getServiceImageReleases';
60
+ request: {
61
+ identity: IIdentity;
62
+ serviceId: string;
63
+ };
64
+ response: {
65
+ releases: IImageRelease[];
66
+ };
67
+ }
68
+
69
+ export interface IRequest_Any_Cloudly_PromoteServiceImageRelease
70
+ extends plugins.typedrequestInterfaces.implementsTR<
71
+ plugins.typedrequestInterfaces.ITypedRequest,
72
+ IRequest_Any_Cloudly_PromoteServiceImageRelease
73
+ > {
74
+ method: 'promoteServiceImageRelease';
75
+ request: {
76
+ identity: IIdentity;
77
+ serviceId: string;
78
+ releaseId: string;
79
+ idempotencyKey: string;
80
+ expectedRolloutGeneration: number;
81
+ expectedCurrentRolloutId?: string;
82
+ /** Explicitly opts the service into fail-closed immutable deployment. */
83
+ enableImmutableImage: true;
84
+ };
85
+ response: {
86
+ service: IService;
87
+ };
88
+ }
89
+
90
+ export interface IRequest_Any_Cloudly_RollbackServiceImageRelease
91
+ extends plugins.typedrequestInterfaces.implementsTR<
92
+ plugins.typedrequestInterfaces.ITypedRequest,
93
+ IRequest_Any_Cloudly_RollbackServiceImageRelease
94
+ > {
95
+ method: 'rollbackServiceImageRelease';
96
+ request: {
97
+ identity: IIdentity;
98
+ serviceId: string;
99
+ releaseId: string;
100
+ idempotencyKey: string;
101
+ expectedCurrentRolloutId: string;
102
+ expectedRolloutGeneration: number;
103
+ };
104
+ response: {
105
+ service: IService;
106
+ };
107
+ }
108
+
52
109
  export interface IRequest_Any_Cloudly_CreateService
53
110
  extends plugins.typedrequestInterfaces.implementsTR<
54
111
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -57,7 +114,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
57
114
  method: 'createService';
58
115
  request: {
59
116
  identity: IIdentity;
60
- serviceData: IService['data'];
117
+ serviceData: TServiceWritableData;
61
118
  };
62
119
  response: {
63
120
  service: IService;
@@ -73,7 +130,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
73
130
  request: {
74
131
  identity: IIdentity;
75
132
  serviceId: string;
76
- serviceData: IService['data'];
133
+ serviceData: TServiceWritableData;
77
134
  };
78
135
  response: {
79
136
  service: IService;