@serve.zone/interfaces 11.3.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-07-19 - 12.0.0
4
+
5
+ ### Fixes
6
+
7
+ - make greenfield deployment preflight evidence fail closed (deployment)
8
+ - Distinguish the client intent digest from Cloudly's server-computed configuration digest.
9
+ - Report dirty source, registry host, complete replica counts, explicit proposed storage resources, and unknown namespace safety.
10
+ - Define an admin-only, idempotent machine-user grant normalization request without returning token material.
11
+
12
+ ## 2026-07-19 - 11.4.0
13
+
14
+ ### Features
15
+
16
+ - define least-privilege greenfield deployment preflight contracts (deployment)
17
+ - Add server-persisted, service-scoped machine capabilities without broad cluster authority.
18
+ - Define sanitized GO/NO-GO inventory for OCI indexes, runtime digests, Corestore bindings, and namespace collisions.
19
+ - Bind release evidence and future durable operations to source, version, image, configuration, and namespace digests.
20
+
21
+ ### Maintenance
22
+
23
+ - replace the broken pnpm 11.13.0 executable pin with pnpm 11.15.0.
24
+
3
25
  ## 2026-07-18 - 11.3.0
4
26
 
5
27
  ### Features
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '11.3.0',
6
+ version: '12.0.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=
@@ -0,0 +1,160 @@
1
+ import type { TOciManifestMediaType, TSha256Digest } from './immutableimage.js';
2
+ import type { TServiceDeploymentCapability } from './user.js';
3
+ export type TDeploymentPreflightDecision = 'GO' | 'NO-GO';
4
+ export type TDeploymentPreflightBlockerCode = 'INVALID_REQUEST' | 'AUTHORIZATION_MISSING' | 'SOURCE_REVISION_INVALID' | 'SOURCE_WORKTREE_DIRTY' | 'VERSION_NOT_RESERVED' | 'VERSION_COLLISION' | 'NAMESPACE_NOT_RESERVED' | 'NAMESPACE_COLLISION' | 'NAMESPACE_NOT_PROVEN_BLANK' | 'CONFIGURATION_DIGEST_INVALID' | 'INTENT_DIGEST_INVALID' | 'CURRENT_REFERENCE_MUTABLE' | 'CURRENT_RUNTIME_DIGEST_MISSING' | 'OCI_INDEX_DIGEST_REQUIRED' | 'OCI_INDEX_NOT_FOUND' | 'OCI_INDEX_MEDIA_TYPE_REQUIRED' | 'OCI_PLATFORM_MISSING' | 'OCI_PLATFORM_MANIFEST_MISSING' | 'IMMUTABLE_RELEASE_TAG_REQUIRED' | 'IMMUTABLE_RELEASE_TAG_NOT_RESERVED' | 'RELEASE_EVIDENCE_MISSING' | 'RELEASE_EVIDENCE_INVALID' | 'RELEASE_NOT_AUTHORIZED' | 'GREENFIELD_EXECUTION_UNAVAILABLE' | 'ROUTE_PROMOTION_FENCE_UNAVAILABLE' | 'POST_DEPLOYMENT_VERIFICATION_UNAVAILABLE' | 'WOULD_REPLACE_EXISTING_STATE' | 'CORESTORE_INVENTORY_UNAVAILABLE' | 'RUNTIME_ATTESTATION_MISMATCH';
5
+ export interface IDeploymentPreflightBlocker {
6
+ code: TDeploymentPreflightBlockerCode;
7
+ message: string;
8
+ }
9
+ export interface IDeploymentPreflightAuthorizationCheck {
10
+ capability: TServiceDeploymentCapability;
11
+ granted: boolean;
12
+ }
13
+ export interface IOciPlatformDescriptorSummary {
14
+ os: string;
15
+ architecture: string;
16
+ digest?: TSha256Digest;
17
+ mediaType?: string;
18
+ runnable: boolean;
19
+ manifestPresent: boolean;
20
+ }
21
+ export interface IDeploymentPreflightRuntimeTask {
22
+ deploymentId: string;
23
+ nodeName?: string;
24
+ status: string;
25
+ healthStatus?: string;
26
+ imageReference?: string;
27
+ reportedDigest?: TSha256Digest;
28
+ verificationStatus?: 'not-required' | 'pending' | 'verified' | 'missing' | 'mismatched';
29
+ }
30
+ export interface IDeploymentPreflightPlatformBinding {
31
+ capability: 'database' | 'objectstorage';
32
+ desiredState: 'enabled' | 'disabled';
33
+ status: 'requested' | 'provisioning' | 'ready' | 'degraded' | 'failed' | 'disabled';
34
+ }
35
+ export interface IDeploymentPreflightCorestoreResource {
36
+ capability: 'database' | 'objectstorage';
37
+ provider: string;
38
+ resourceName: string;
39
+ }
40
+ export interface IDeploymentPreflightProposedCorestoreResource {
41
+ capability: 'database' | 'objectstorage';
42
+ resourceName: string;
43
+ }
44
+ export interface IDeploymentReleaseEvidenceReference {
45
+ kind: 'provenance' | 'sbom' | 'signature' | 'vulnerability-policy' | 'release-authorization';
46
+ statementDigest: TSha256Digest;
47
+ subjectDigest: TSha256Digest;
48
+ verifier: string;
49
+ verifiedAt: number;
50
+ }
51
+ /** Evidence is descriptive input until a trusted Cloudly verifier validates it. */
52
+ export interface IDeploymentReleaseEvidence {
53
+ sourceRevision: string;
54
+ version: string;
55
+ configurationDigest: TSha256Digest;
56
+ ociIndexDigest: TSha256Digest;
57
+ releaseAuthorized: boolean;
58
+ attestations: IDeploymentReleaseEvidenceReference[];
59
+ }
60
+ export interface IDeploymentPreflightRequestData {
61
+ requestId: string;
62
+ serviceId: string;
63
+ sourceRevision: string;
64
+ sourceDirty: boolean;
65
+ version: string;
66
+ intentDigest: string;
67
+ proposedNamespace: string;
68
+ proposedCorestoreResources: IDeploymentPreflightProposedCorestoreResource[];
69
+ ociIndexDigest?: string;
70
+ releaseTag?: string;
71
+ releaseEvidence?: IDeploymentReleaseEvidence;
72
+ greenfield: true;
73
+ }
74
+ export interface IDeploymentPreflightReport {
75
+ schemaVersion: 2;
76
+ decision: TDeploymentPreflightDecision;
77
+ mutationPerformed: false;
78
+ generatedAt: number;
79
+ service: {
80
+ serviceId: string;
81
+ serviceName: string;
82
+ workspaceIdentity: string;
83
+ registryHost: string;
84
+ registryRepository: string;
85
+ deployOnPush: boolean;
86
+ };
87
+ source: {
88
+ revision: string;
89
+ dirty: boolean;
90
+ version: string;
91
+ intentDigest: TSha256Digest;
92
+ configurationDigest: TSha256Digest;
93
+ };
94
+ currentImage: {
95
+ tag?: string;
96
+ digest?: TSha256Digest;
97
+ imageReference?: string;
98
+ mutableReference: boolean;
99
+ mediaType?: TOciManifestMediaType;
100
+ platforms: IOciPlatformDescriptorSummary[];
101
+ };
102
+ requestedImage: {
103
+ digest?: TSha256Digest;
104
+ releaseTag?: string;
105
+ tagReserved: boolean;
106
+ mediaType?: TOciManifestMediaType;
107
+ platforms: IOciPlatformDescriptorSummary[];
108
+ };
109
+ runtimeTasks: IDeploymentPreflightRuntimeTask[];
110
+ runtimeAttestation: {
111
+ requiredReplicaCount: number;
112
+ observedReplicaCount: number;
113
+ healthyReplicaCount: number;
114
+ verifiedReplicaCount: number;
115
+ consistentDigest: boolean;
116
+ };
117
+ platformBindings: IDeploymentPreflightPlatformBinding[];
118
+ corestoreResources: IDeploymentPreflightCorestoreResource[];
119
+ proposed: {
120
+ namespace: string;
121
+ corestoreResources: IDeploymentPreflightProposedCorestoreResource[];
122
+ namespaceReserved: boolean;
123
+ inventoryComplete: boolean;
124
+ namespaceBlank: boolean | 'unknown';
125
+ versionReserved: boolean;
126
+ wouldReplaceExistingState: boolean | 'unknown';
127
+ };
128
+ authorizationChecks: IDeploymentPreflightAuthorizationCheck[];
129
+ compatibilityBlockers: IDeploymentPreflightBlocker[];
130
+ }
131
+ export interface IGreenfieldDeploymentFence {
132
+ sourceServiceId: string;
133
+ sourceRevision: string;
134
+ version: string;
135
+ ociIndexDigest: TSha256Digest;
136
+ configurationDigest: TSha256Digest;
137
+ targetNamespace: string;
138
+ }
139
+ export interface IGreenfieldDeploymentOperation {
140
+ id: string;
141
+ data: {
142
+ idempotencyKey: string;
143
+ requestDigest: TSha256Digest;
144
+ fence: IGreenfieldDeploymentFence;
145
+ candidateServiceId?: string;
146
+ state: 'reserved' | 'provisioning' | 'deploying' | 'verifying' | 'ready-for-promotion' | 'promoting' | 'promoted' | 'rolling-back' | 'rolled-back' | 'failed';
147
+ routeGeneration?: number;
148
+ createdAt: number;
149
+ updatedAt: number;
150
+ failureCode?: string;
151
+ };
152
+ }
153
+ export declare const validateDeploymentPreflightRequest: (requestArg: unknown) => IDeploymentPreflightBlocker[];
154
+ export declare const validateDeploymentReleaseEvidence: (argsArg: {
155
+ evidence: unknown;
156
+ sourceRevision: string;
157
+ version: string;
158
+ configurationDigest: TSha256Digest;
159
+ ociIndexDigest: TSha256Digest;
160
+ }) => IDeploymentPreflightBlocker[];
@@ -0,0 +1,119 @@
1
+ import { normalizeSha256Digest } from './immutableimage.js';
2
+ const fullGitRevisionRegex = /^[a-f0-9]{40}$/;
3
+ const namespaceRegex = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
4
+ 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-]+)*)?$/;
5
+ const requiredEvidenceKinds = new Set([
6
+ 'provenance',
7
+ 'sbom',
8
+ 'signature',
9
+ 'vulnerability-policy',
10
+ 'release-authorization',
11
+ ]);
12
+ const isRecord = (valueArg) => Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg);
13
+ export const validateDeploymentPreflightRequest = (requestArg) => {
14
+ const blockers = [];
15
+ if (!isRecord(requestArg)) {
16
+ return [{ code: 'INVALID_REQUEST', message: 'Deployment preflight request must be an object.' }];
17
+ }
18
+ if (typeof requestArg.requestId !== 'string' || !requestArg.requestId.trim() || requestArg.requestId.length > 200) {
19
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded request identifier is required.' });
20
+ }
21
+ if (typeof requestArg.serviceId !== 'string' || !requestArg.serviceId.trim() || requestArg.serviceId.length > 200) {
22
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded service identifier is required.' });
23
+ }
24
+ if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) {
25
+ blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' });
26
+ }
27
+ if (typeof requestArg.sourceDirty !== 'boolean') {
28
+ blockers.push({ code: 'INVALID_REQUEST', message: 'Source worktree state must be reported.' });
29
+ }
30
+ else if (requestArg.sourceDirty) {
31
+ blockers.push({ code: 'SOURCE_WORKTREE_DIRTY', message: 'The source worktree contains uncommitted changes.' });
32
+ }
33
+ if (typeof requestArg.version !== 'string'
34
+ || requestArg.version.length > 100
35
+ || !semanticVersionRegex.test(requestArg.version)) {
36
+ blockers.push({ code: 'VERSION_NOT_RESERVED', message: 'A canonical semantic release version is required.' });
37
+ }
38
+ if (typeof requestArg.proposedNamespace !== 'string' || !namespaceRegex.test(requestArg.proposedNamespace)) {
39
+ blockers.push({ code: 'NAMESPACE_NOT_RESERVED', message: 'The proposed namespace is missing or malformed.' });
40
+ }
41
+ if (typeof requestArg.intentDigest !== 'string' || !normalizeSha256Digest(requestArg.intentDigest)) {
42
+ blockers.push({ code: 'INTENT_DIGEST_INVALID', message: 'Deployment intent digest must be a canonical sha256 digest.' });
43
+ }
44
+ const proposedResources = Array.isArray(requestArg.proposedCorestoreResources)
45
+ ? requestArg.proposedCorestoreResources
46
+ : [];
47
+ if (!Array.isArray(requestArg.proposedCorestoreResources)
48
+ || proposedResources.length > 8
49
+ || proposedResources.some((resourceArg) => (!isRecord(resourceArg)
50
+ || (resourceArg.capability !== 'database' && resourceArg.capability !== 'objectstorage')
51
+ || typeof resourceArg.resourceName !== 'string'
52
+ || !namespaceRegex.test(resourceArg.resourceName)))) {
53
+ blockers.push({ code: 'INVALID_REQUEST', message: 'Proposed Corestore resources are malformed.' });
54
+ }
55
+ const proposedResourceKeys = proposedResources
56
+ .filter(isRecord)
57
+ .map((resourceArg) => `${String(resourceArg.capability)}:${String(resourceArg.resourceName)}`);
58
+ if (new Set(proposedResourceKeys).size !== proposedResourceKeys.length) {
59
+ blockers.push({ code: 'INVALID_REQUEST', message: 'Proposed Corestore resources must be unique.' });
60
+ }
61
+ if (requestArg.ociIndexDigest !== undefined
62
+ && (typeof requestArg.ociIndexDigest !== 'string' || !normalizeSha256Digest(requestArg.ociIndexDigest))) {
63
+ blockers.push({ code: 'OCI_INDEX_DIGEST_REQUIRED', message: 'OCI index digest must be a canonical sha256 digest.' });
64
+ }
65
+ if (requestArg.greenfield !== true) {
66
+ blockers.push({ code: 'WOULD_REPLACE_EXISTING_STATE', message: 'Only greenfield deployment preflight is supported.' });
67
+ }
68
+ return blockers;
69
+ };
70
+ export const validateDeploymentReleaseEvidence = (argsArg) => {
71
+ const blockers = [];
72
+ if (!isRecord(argsArg.evidence)) {
73
+ return [{ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence must be an object.' }];
74
+ }
75
+ const evidence = argsArg.evidence;
76
+ if (evidence.releaseAuthorized !== true) {
77
+ blockers.push({ code: 'RELEASE_NOT_AUTHORIZED', message: 'Release authorization evidence is explicitly false.' });
78
+ }
79
+ if (evidence.sourceRevision !== argsArg.sourceRevision
80
+ || evidence.version !== argsArg.version
81
+ || evidence.configurationDigest !== argsArg.configurationDigest
82
+ || evidence.ociIndexDigest !== argsArg.ociIndexDigest) {
83
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence is not bound to the requested deployment fence.' });
84
+ }
85
+ const seenKinds = new Set();
86
+ const attestations = Array.isArray(evidence.attestations) ? evidence.attestations : [];
87
+ if (!Array.isArray(evidence.attestations)) {
88
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestations must be an array.' });
89
+ }
90
+ for (const attestation of attestations) {
91
+ if (!isRecord(attestation)) {
92
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation must be an object.' });
93
+ continue;
94
+ }
95
+ const kind = attestation.kind;
96
+ if (typeof kind !== 'string' || !requiredEvidenceKinds.has(kind)) {
97
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation kind is invalid.' });
98
+ continue;
99
+ }
100
+ seenKinds.add(kind);
101
+ if (attestation.subjectDigest !== argsArg.ociIndexDigest
102
+ || typeof attestation.statementDigest !== 'string'
103
+ || !normalizeSha256Digest(attestation.statementDigest)
104
+ || typeof attestation.verifier !== 'string'
105
+ || !attestation.verifier.trim()
106
+ || typeof attestation.verifiedAt !== 'number'
107
+ || !Number.isSafeInteger(attestation.verifiedAt)
108
+ || attestation.verifiedAt < 1) {
109
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: `Invalid ${kind} evidence.` });
110
+ }
111
+ }
112
+ for (const requiredKind of requiredEvidenceKinds) {
113
+ if (!seenKinds.has(requiredKind)) {
114
+ blockers.push({ code: 'RELEASE_EVIDENCE_MISSING', message: `Missing ${requiredKind} evidence.` });
115
+ }
116
+ }
117
+ return blockers;
118
+ };
119
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwbG95bWVudHByZWZsaWdodC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvZGVwbG95bWVudHByZWZsaWdodC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFJQSxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQStNNUQsTUFBTSxvQkFBb0IsR0FBRyxnQkFBZ0IsQ0FBQztBQUM5QyxNQUFNLGNBQWMsR0FBRyx3Q0FBd0MsQ0FBQztBQUNoRSxNQUFNLG9CQUFvQixHQUFHLDRNQUE0TSxDQUFDO0FBQzFPLE1BQU0scUJBQXFCLEdBQUcsSUFBSSxHQUFHLENBQThDO0lBQ2pGLFlBQVk7SUFDWixNQUFNO0lBQ04sV0FBVztJQUNYLHNCQUFzQjtJQUN0Qix1QkFBdUI7Q0FDeEIsQ0FBQyxDQUFDO0FBRUgsTUFBTSxRQUFRLEdBQUcsQ0FBQyxRQUFpQixFQUF1QyxFQUFFLENBQzFFLE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSSxPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0FBRWhGLE1BQU0sQ0FBQyxNQUFNLGtDQUFrQyxHQUFHLENBQ2hELFVBQW1CLEVBQ1ksRUFBRTtJQUNqQyxNQUFNLFFBQVEsR0FBa0MsRUFBRSxDQUFDO0lBQ25ELElBQUksQ0FBQyxRQUFRLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQztRQUMxQixPQUFPLENBQUMsRUFBRSxJQUFJLEVBQUUsaUJBQWlCLEVBQUUsT0FBTyxFQUFFLGlEQUFpRCxFQUFFLENBQUMsQ0FBQztJQUNuRyxDQUFDO0lBQ0QsSUFBSSxPQUFPLFVBQVUsQ0FBQyxTQUFTLEtBQUssUUFBUSxJQUFJLENBQUMsVUFBVSxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsSUFBSSxVQUFVLENBQUMsU0FBUyxDQUFDLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQztRQUNsSCxRQUFRLENBQUMsSUFB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
@@ -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';
@@ -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';
@@ -30,4 +31,4 @@ export * from './taskexecution.js';
30
31
  export * from './traffic.js';
31
32
  export * from './user.js';
32
33
  export * from './version.js';
33
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsVUFBVSxDQUFDO0FBQ3pCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGNBQWMsQ0FBQyJ9
34
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLG9CQUFvQixDQUFDO0FBQ25DLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsY0FBYyxDQUFDIn0=
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  import * as plugins from '../plugins.js';
2
- import type { IIdentity } from '../data/user.js';
2
+ import type { IIdentity, TServiceDeploymentCapability } from '../data/user.js';
3
3
  export interface IReq_Admin_LoginWithUsernameAndPassword extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_LoginWithUsernameAndPassword> {
4
4
  method: 'adminLoginWithUsernameAndPassword';
5
5
  request: {
@@ -10,6 +10,21 @@ export interface IReq_Admin_LoginWithUsernameAndPassword extends plugins.typedre
10
10
  identity: IIdentity;
11
11
  };
12
12
  }
13
+ export interface IReq_Admin_ConfigureServiceDeploymentMachineUser extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_ConfigureServiceDeploymentMachineUser> {
14
+ method: 'configureServiceDeploymentMachineUser';
15
+ request: {
16
+ identity: IIdentity;
17
+ userId: string;
18
+ serviceId: string;
19
+ capabilities: TServiceDeploymentCapability[];
20
+ };
21
+ response: {
22
+ userId: string;
23
+ serviceId: string;
24
+ capabilities: TServiceDeploymentCapability[];
25
+ tokenRolesNormalized: true;
26
+ };
27
+ }
13
28
  export interface ICoreflowBaseServiceReconciliationResult {
14
29
  status: 'fulfilled' | 'rejected';
15
30
  nodeNames: string[];
@@ -4,6 +4,16 @@ import type { TServiceWritableData } from '../data/service.js';
4
4
  import type { IRegistryTarget } from '../data/registry.js';
5
5
  import type { IIdentity } from '../data/user.js';
6
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
+ }
7
17
  export interface IRequest_Any_Cloudly_GetServiceById extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceById> {
8
18
  method: 'getServiceById';
9
19
  request: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "11.3.0",
3
+ "version": "12.0.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.3.0',
6
+ version: '12.0.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -0,0 +1,345 @@
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
+ | 'SOURCE_WORKTREE_DIRTY'
15
+ | 'VERSION_NOT_RESERVED'
16
+ | 'VERSION_COLLISION'
17
+ | 'NAMESPACE_NOT_RESERVED'
18
+ | 'NAMESPACE_COLLISION'
19
+ | 'NAMESPACE_NOT_PROVEN_BLANK'
20
+ | 'CONFIGURATION_DIGEST_INVALID'
21
+ | 'INTENT_DIGEST_INVALID'
22
+ | 'CURRENT_REFERENCE_MUTABLE'
23
+ | 'CURRENT_RUNTIME_DIGEST_MISSING'
24
+ | 'OCI_INDEX_DIGEST_REQUIRED'
25
+ | 'OCI_INDEX_NOT_FOUND'
26
+ | 'OCI_INDEX_MEDIA_TYPE_REQUIRED'
27
+ | 'OCI_PLATFORM_MISSING'
28
+ | 'OCI_PLATFORM_MANIFEST_MISSING'
29
+ | 'IMMUTABLE_RELEASE_TAG_REQUIRED'
30
+ | 'IMMUTABLE_RELEASE_TAG_NOT_RESERVED'
31
+ | 'RELEASE_EVIDENCE_MISSING'
32
+ | 'RELEASE_EVIDENCE_INVALID'
33
+ | 'RELEASE_NOT_AUTHORIZED'
34
+ | 'GREENFIELD_EXECUTION_UNAVAILABLE'
35
+ | 'ROUTE_PROMOTION_FENCE_UNAVAILABLE'
36
+ | 'POST_DEPLOYMENT_VERIFICATION_UNAVAILABLE'
37
+ | 'WOULD_REPLACE_EXISTING_STATE'
38
+ | 'CORESTORE_INVENTORY_UNAVAILABLE'
39
+ | 'RUNTIME_ATTESTATION_MISMATCH';
40
+
41
+ export interface IDeploymentPreflightBlocker {
42
+ code: TDeploymentPreflightBlockerCode;
43
+ message: string;
44
+ }
45
+
46
+ export interface IDeploymentPreflightAuthorizationCheck {
47
+ capability: TServiceDeploymentCapability;
48
+ granted: boolean;
49
+ }
50
+
51
+ export interface IOciPlatformDescriptorSummary {
52
+ os: string;
53
+ architecture: string;
54
+ digest?: TSha256Digest;
55
+ mediaType?: string;
56
+ runnable: boolean;
57
+ manifestPresent: boolean;
58
+ }
59
+
60
+ export interface IDeploymentPreflightRuntimeTask {
61
+ deploymentId: string;
62
+ nodeName?: string;
63
+ status: string;
64
+ healthStatus?: string;
65
+ imageReference?: string;
66
+ reportedDigest?: TSha256Digest;
67
+ verificationStatus?: 'not-required' | 'pending' | 'verified' | 'missing' | 'mismatched';
68
+ }
69
+
70
+ export interface IDeploymentPreflightPlatformBinding {
71
+ capability: 'database' | 'objectstorage';
72
+ desiredState: 'enabled' | 'disabled';
73
+ status: 'requested' | 'provisioning' | 'ready' | 'degraded' | 'failed' | 'disabled';
74
+ }
75
+
76
+ export interface IDeploymentPreflightCorestoreResource {
77
+ capability: 'database' | 'objectstorage';
78
+ provider: string;
79
+ resourceName: string;
80
+ }
81
+
82
+ export interface IDeploymentPreflightProposedCorestoreResource {
83
+ capability: 'database' | 'objectstorage';
84
+ resourceName: string;
85
+ }
86
+
87
+ export interface IDeploymentReleaseEvidenceReference {
88
+ kind: 'provenance' | 'sbom' | 'signature' | 'vulnerability-policy' | 'release-authorization';
89
+ statementDigest: TSha256Digest;
90
+ subjectDigest: TSha256Digest;
91
+ verifier: string;
92
+ verifiedAt: number;
93
+ }
94
+
95
+ /** Evidence is descriptive input until a trusted Cloudly verifier validates it. */
96
+ export interface IDeploymentReleaseEvidence {
97
+ sourceRevision: string;
98
+ version: string;
99
+ configurationDigest: TSha256Digest;
100
+ ociIndexDigest: TSha256Digest;
101
+ releaseAuthorized: boolean;
102
+ attestations: IDeploymentReleaseEvidenceReference[];
103
+ }
104
+
105
+ export interface IDeploymentPreflightRequestData {
106
+ requestId: string;
107
+ serviceId: string;
108
+ sourceRevision: string;
109
+ sourceDirty: boolean;
110
+ version: string;
111
+ intentDigest: string;
112
+ proposedNamespace: string;
113
+ proposedCorestoreResources: IDeploymentPreflightProposedCorestoreResource[];
114
+ ociIndexDigest?: string;
115
+ releaseTag?: string;
116
+ releaseEvidence?: IDeploymentReleaseEvidence;
117
+ greenfield: true;
118
+ }
119
+
120
+ export interface IDeploymentPreflightReport {
121
+ schemaVersion: 2;
122
+ decision: TDeploymentPreflightDecision;
123
+ mutationPerformed: false;
124
+ generatedAt: number;
125
+ service: {
126
+ serviceId: string;
127
+ serviceName: string;
128
+ workspaceIdentity: string;
129
+ registryHost: string;
130
+ registryRepository: string;
131
+ deployOnPush: boolean;
132
+ };
133
+ source: {
134
+ revision: string;
135
+ dirty: boolean;
136
+ version: string;
137
+ intentDigest: TSha256Digest;
138
+ configurationDigest: TSha256Digest;
139
+ };
140
+ currentImage: {
141
+ tag?: string;
142
+ digest?: TSha256Digest;
143
+ imageReference?: string;
144
+ mutableReference: boolean;
145
+ mediaType?: TOciManifestMediaType;
146
+ platforms: IOciPlatformDescriptorSummary[];
147
+ };
148
+ requestedImage: {
149
+ digest?: TSha256Digest;
150
+ releaseTag?: string;
151
+ tagReserved: boolean;
152
+ mediaType?: TOciManifestMediaType;
153
+ platforms: IOciPlatformDescriptorSummary[];
154
+ };
155
+ runtimeTasks: IDeploymentPreflightRuntimeTask[];
156
+ runtimeAttestation: {
157
+ requiredReplicaCount: number;
158
+ observedReplicaCount: number;
159
+ healthyReplicaCount: number;
160
+ verifiedReplicaCount: number;
161
+ consistentDigest: boolean;
162
+ };
163
+ platformBindings: IDeploymentPreflightPlatformBinding[];
164
+ corestoreResources: IDeploymentPreflightCorestoreResource[];
165
+ proposed: {
166
+ namespace: string;
167
+ corestoreResources: IDeploymentPreflightProposedCorestoreResource[];
168
+ namespaceReserved: boolean;
169
+ inventoryComplete: boolean;
170
+ namespaceBlank: boolean | 'unknown';
171
+ versionReserved: boolean;
172
+ wouldReplaceExistingState: boolean | 'unknown';
173
+ };
174
+ authorizationChecks: IDeploymentPreflightAuthorizationCheck[];
175
+ compatibilityBlockers: IDeploymentPreflightBlocker[];
176
+ }
177
+
178
+ export interface IGreenfieldDeploymentFence {
179
+ sourceServiceId: string;
180
+ sourceRevision: string;
181
+ version: string;
182
+ ociIndexDigest: TSha256Digest;
183
+ configurationDigest: TSha256Digest;
184
+ targetNamespace: string;
185
+ }
186
+
187
+ export interface IGreenfieldDeploymentOperation {
188
+ id: string;
189
+ data: {
190
+ idempotencyKey: string;
191
+ requestDigest: TSha256Digest;
192
+ fence: IGreenfieldDeploymentFence;
193
+ candidateServiceId?: string;
194
+ state:
195
+ | 'reserved'
196
+ | 'provisioning'
197
+ | 'deploying'
198
+ | 'verifying'
199
+ | 'ready-for-promotion'
200
+ | 'promoting'
201
+ | 'promoted'
202
+ | 'rolling-back'
203
+ | 'rolled-back'
204
+ | 'failed';
205
+ routeGeneration?: number;
206
+ createdAt: number;
207
+ updatedAt: number;
208
+ failureCode?: string;
209
+ };
210
+ }
211
+
212
+ const fullGitRevisionRegex = /^[a-f0-9]{40}$/;
213
+ const namespaceRegex = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
214
+ 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-]+)*)?$/;
215
+ const requiredEvidenceKinds = new Set<IDeploymentReleaseEvidenceReference['kind']>([
216
+ 'provenance',
217
+ 'sbom',
218
+ 'signature',
219
+ 'vulnerability-policy',
220
+ 'release-authorization',
221
+ ]);
222
+
223
+ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> =>
224
+ Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg);
225
+
226
+ export const validateDeploymentPreflightRequest = (
227
+ requestArg: unknown,
228
+ ): IDeploymentPreflightBlocker[] => {
229
+ const blockers: IDeploymentPreflightBlocker[] = [];
230
+ if (!isRecord(requestArg)) {
231
+ return [{ code: 'INVALID_REQUEST', message: 'Deployment preflight request must be an object.' }];
232
+ }
233
+ if (typeof requestArg.requestId !== 'string' || !requestArg.requestId.trim() || requestArg.requestId.length > 200) {
234
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded request identifier is required.' });
235
+ }
236
+ if (typeof requestArg.serviceId !== 'string' || !requestArg.serviceId.trim() || requestArg.serviceId.length > 200) {
237
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded service identifier is required.' });
238
+ }
239
+ if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) {
240
+ blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' });
241
+ }
242
+ if (typeof requestArg.sourceDirty !== 'boolean') {
243
+ blockers.push({ code: 'INVALID_REQUEST', message: 'Source worktree state must be reported.' });
244
+ } else if (requestArg.sourceDirty) {
245
+ blockers.push({ code: 'SOURCE_WORKTREE_DIRTY', message: 'The source worktree contains uncommitted changes.' });
246
+ }
247
+ if (typeof requestArg.version !== 'string'
248
+ || requestArg.version.length > 100
249
+ || !semanticVersionRegex.test(requestArg.version)) {
250
+ blockers.push({ code: 'VERSION_NOT_RESERVED', message: 'A canonical semantic release version is required.' });
251
+ }
252
+ if (typeof requestArg.proposedNamespace !== 'string' || !namespaceRegex.test(requestArg.proposedNamespace)) {
253
+ blockers.push({ code: 'NAMESPACE_NOT_RESERVED', message: 'The proposed namespace is missing or malformed.' });
254
+ }
255
+ if (typeof requestArg.intentDigest !== 'string' || !normalizeSha256Digest(requestArg.intentDigest)) {
256
+ blockers.push({ code: 'INTENT_DIGEST_INVALID', message: 'Deployment intent digest must be a canonical sha256 digest.' });
257
+ }
258
+ const proposedResources = Array.isArray(requestArg.proposedCorestoreResources)
259
+ ? requestArg.proposedCorestoreResources
260
+ : [];
261
+ if (!Array.isArray(requestArg.proposedCorestoreResources)
262
+ || proposedResources.length > 8
263
+ || proposedResources.some((resourceArg) => (
264
+ !isRecord(resourceArg)
265
+ || (resourceArg.capability !== 'database' && resourceArg.capability !== 'objectstorage')
266
+ || typeof resourceArg.resourceName !== 'string'
267
+ || !namespaceRegex.test(resourceArg.resourceName)
268
+ ))) {
269
+ blockers.push({ code: 'INVALID_REQUEST', message: 'Proposed Corestore resources are malformed.' });
270
+ }
271
+ const proposedResourceKeys = proposedResources
272
+ .filter(isRecord)
273
+ .map((resourceArg) => `${String(resourceArg.capability)}:${String(resourceArg.resourceName)}`);
274
+ if (new Set(proposedResourceKeys).size !== proposedResourceKeys.length) {
275
+ blockers.push({ code: 'INVALID_REQUEST', message: 'Proposed Corestore resources must be unique.' });
276
+ }
277
+ if (requestArg.ociIndexDigest !== undefined
278
+ && (typeof requestArg.ociIndexDigest !== 'string' || !normalizeSha256Digest(requestArg.ociIndexDigest))) {
279
+ blockers.push({ code: 'OCI_INDEX_DIGEST_REQUIRED', message: 'OCI index digest must be a canonical sha256 digest.' });
280
+ }
281
+ if (requestArg.greenfield !== true) {
282
+ blockers.push({ code: 'WOULD_REPLACE_EXISTING_STATE', message: 'Only greenfield deployment preflight is supported.' });
283
+ }
284
+ return blockers;
285
+ };
286
+
287
+ export const validateDeploymentReleaseEvidence = (argsArg: {
288
+ evidence: unknown;
289
+ sourceRevision: string;
290
+ version: string;
291
+ configurationDigest: TSha256Digest;
292
+ ociIndexDigest: TSha256Digest;
293
+ }): IDeploymentPreflightBlocker[] => {
294
+ const blockers: IDeploymentPreflightBlocker[] = [];
295
+ if (!isRecord(argsArg.evidence)) {
296
+ return [{ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence must be an object.' }];
297
+ }
298
+ const evidence = argsArg.evidence;
299
+ if (evidence.releaseAuthorized !== true) {
300
+ blockers.push({ code: 'RELEASE_NOT_AUTHORIZED', message: 'Release authorization evidence is explicitly false.' });
301
+ }
302
+ if (
303
+ evidence.sourceRevision !== argsArg.sourceRevision
304
+ || evidence.version !== argsArg.version
305
+ || evidence.configurationDigest !== argsArg.configurationDigest
306
+ || evidence.ociIndexDigest !== argsArg.ociIndexDigest
307
+ ) {
308
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence is not bound to the requested deployment fence.' });
309
+ }
310
+ const seenKinds = new Set<IDeploymentReleaseEvidenceReference['kind']>();
311
+ const attestations = Array.isArray(evidence.attestations) ? evidence.attestations : [];
312
+ if (!Array.isArray(evidence.attestations)) {
313
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestations must be an array.' });
314
+ }
315
+ for (const attestation of attestations) {
316
+ if (!isRecord(attestation)) {
317
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation must be an object.' });
318
+ continue;
319
+ }
320
+ const kind = attestation.kind;
321
+ if (typeof kind !== 'string' || !requiredEvidenceKinds.has(kind as IDeploymentReleaseEvidenceReference['kind'])) {
322
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation kind is invalid.' });
323
+ continue;
324
+ }
325
+ seenKinds.add(kind as IDeploymentReleaseEvidenceReference['kind']);
326
+ if (
327
+ attestation.subjectDigest !== argsArg.ociIndexDigest
328
+ || typeof attestation.statementDigest !== 'string'
329
+ || !normalizeSha256Digest(attestation.statementDigest)
330
+ || typeof attestation.verifier !== 'string'
331
+ || !attestation.verifier.trim()
332
+ || typeof attestation.verifiedAt !== 'number'
333
+ || !Number.isSafeInteger(attestation.verifiedAt)
334
+ || attestation.verifiedAt < 1
335
+ ) {
336
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: `Invalid ${kind} evidence.` });
337
+ }
338
+ }
339
+ for (const requiredKind of requiredEvidenceKinds) {
340
+ if (!seenKinds.has(requiredKind)) {
341
+ blockers.push({ code: 'RELEASE_EVIDENCE_MISSING', message: `Missing ${requiredKind} evidence.` });
342
+ }
343
+ }
344
+ return blockers;
345
+ };
package/ts/data/index.ts CHANGED
@@ -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';
package/ts/data/user.ts CHANGED
@@ -4,6 +4,22 @@ export interface IToken {
4
4
  assignedRoles: string[];
5
5
  }
6
6
 
7
+ export type TServiceDeploymentCapability =
8
+ | 'deployment:preflight'
9
+ | 'deployment:create-greenfield'
10
+ | 'deployment:promote-route'
11
+ | 'deployment:rollback-route';
12
+
13
+ /**
14
+ * Server-persisted authority for one logical service. These grants are never
15
+ * accepted from the identity payload supplied by a caller; Cloudly reloads
16
+ * the owning user from the verified JWT before evaluating them.
17
+ */
18
+ export interface IServiceDeploymentGrant {
19
+ serviceId: string;
20
+ capabilities: TServiceDeploymentCapability[];
21
+ }
22
+
7
23
  export interface IIdentityCredential {
8
24
  jwt: string;
9
25
  }
@@ -28,5 +44,6 @@ export interface IUser {
28
44
  username?: string;
29
45
  password?: string;
30
46
  tokens?: IToken[];
47
+ serviceDeploymentGrants?: IServiceDeploymentGrant[];
31
48
  }
32
49
  }
@@ -1,5 +1,5 @@
1
1
  import * as plugins from '../plugins.js';
2
- import type { IIdentity } from '../data/user.js';
2
+ import type { IIdentity, TServiceDeploymentCapability } from '../data/user.js';
3
3
 
4
4
  export interface IReq_Admin_LoginWithUsernameAndPassword extends plugins.typedrequestInterfaces.implementsTR<
5
5
  plugins.typedrequestInterfaces.ITypedRequest,
@@ -15,6 +15,26 @@ export interface IReq_Admin_LoginWithUsernameAndPassword extends plugins.typedre
15
15
  }
16
16
  }
17
17
 
18
+ export interface IReq_Admin_ConfigureServiceDeploymentMachineUser
19
+ extends plugins.typedrequestInterfaces.implementsTR<
20
+ plugins.typedrequestInterfaces.ITypedRequest,
21
+ IReq_Admin_ConfigureServiceDeploymentMachineUser
22
+ > {
23
+ method: 'configureServiceDeploymentMachineUser';
24
+ request: {
25
+ identity: IIdentity;
26
+ userId: string;
27
+ serviceId: string;
28
+ capabilities: TServiceDeploymentCapability[];
29
+ };
30
+ response: {
31
+ userId: string;
32
+ serviceId: string;
33
+ capabilities: TServiceDeploymentCapability[];
34
+ tokenRolesNormalized: true;
35
+ };
36
+ }
37
+
18
38
  export interface ICoreflowBaseServiceReconciliationResult {
19
39
  status: 'fulfilled' | 'rejected';
20
40
  nodeNames: string[];
@@ -5,6 +5,24 @@ import type { IRegistryTarget } from '../data/registry.js';
5
5
  import type { IIdentity } from '../data/user.js';
6
6
  import type { IServiceRessources } from '../data/docker.js';
7
7
  import type { IImageRelease } from '../data/immutableimage.js';
8
+ import type {
9
+ IDeploymentPreflightReport,
10
+ IDeploymentPreflightRequestData,
11
+ } from '../data/deploymentpreflight.js';
12
+
13
+ export interface IRequest_Any_Cloudly_GetDeploymentPreflight
14
+ extends plugins.typedrequestInterfaces.implementsTR<
15
+ plugins.typedrequestInterfaces.ITypedRequest,
16
+ IRequest_Any_Cloudly_GetDeploymentPreflight
17
+ > {
18
+ method: 'getDeploymentPreflight';
19
+ request: IDeploymentPreflightRequestData & {
20
+ identity: IIdentity;
21
+ };
22
+ response: {
23
+ report: IDeploymentPreflightReport;
24
+ };
25
+ }
8
26
 
9
27
  export interface IRequest_Any_Cloudly_GetServiceById
10
28
  extends plugins.typedrequestInterfaces.implementsTR<