@serve.zone/interfaces 11.3.0 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-07-19 - 11.4.0
4
+
5
+ ### Features
6
+
7
+ - define least-privilege greenfield deployment preflight contracts (deployment)
8
+ - Add server-persisted, service-scoped machine capabilities without broad cluster authority.
9
+ - Define sanitized GO/NO-GO inventory for OCI indexes, runtime digests, Corestore bindings, and namespace collisions.
10
+ - Bind release evidence and future durable operations to source, version, image, configuration, and namespace digests.
11
+
12
+ ### Maintenance
13
+
14
+ - replace the broken pnpm 11.13.0 executable pin with pnpm 11.15.0.
15
+
3
16
  ## 2026-07-18 - 11.3.0
4
17
 
5
18
  ### 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: '11.4.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,142 @@
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' | 'VERSION_NOT_RESERVED' | 'VERSION_COLLISION' | 'NAMESPACE_NOT_RESERVED' | 'NAMESPACE_COLLISION' | 'NAMESPACE_NOT_PROVEN_BLANK' | 'CONFIGURATION_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 IDeploymentReleaseEvidenceReference {
41
+ kind: 'provenance' | 'sbom' | 'signature' | 'vulnerability-policy' | 'release-authorization';
42
+ statementDigest: TSha256Digest;
43
+ subjectDigest: TSha256Digest;
44
+ verifier: string;
45
+ verifiedAt: number;
46
+ }
47
+ /** Evidence is descriptive input until a trusted Cloudly verifier validates it. */
48
+ export interface IDeploymentReleaseEvidence {
49
+ sourceRevision: string;
50
+ version: string;
51
+ configurationDigest: TSha256Digest;
52
+ ociIndexDigest: TSha256Digest;
53
+ releaseAuthorized: boolean;
54
+ attestations: IDeploymentReleaseEvidenceReference[];
55
+ }
56
+ export interface IDeploymentPreflightRequestData {
57
+ requestId: string;
58
+ serviceId: string;
59
+ sourceRevision: string;
60
+ version: string;
61
+ configurationDigest: string;
62
+ proposedNamespace: string;
63
+ ociIndexDigest?: string;
64
+ releaseTag?: string;
65
+ releaseEvidence?: IDeploymentReleaseEvidence;
66
+ greenfield: true;
67
+ }
68
+ export interface IDeploymentPreflightReport {
69
+ schemaVersion: 1;
70
+ decision: TDeploymentPreflightDecision;
71
+ mutationPerformed: false;
72
+ generatedAt: number;
73
+ service: {
74
+ serviceId: string;
75
+ serviceName: string;
76
+ workspaceIdentity: string;
77
+ registryRepository: string;
78
+ deployOnPush: boolean;
79
+ };
80
+ source: {
81
+ revision: string;
82
+ version: string;
83
+ configurationDigest?: TSha256Digest;
84
+ };
85
+ currentImage: {
86
+ tag?: string;
87
+ digest?: TSha256Digest;
88
+ imageReference?: string;
89
+ mutableReference: boolean;
90
+ mediaType?: TOciManifestMediaType;
91
+ platforms: IOciPlatformDescriptorSummary[];
92
+ };
93
+ requestedImage: {
94
+ digest?: TSha256Digest;
95
+ releaseTag?: string;
96
+ tagReserved: boolean;
97
+ mediaType?: TOciManifestMediaType;
98
+ platforms: IOciPlatformDescriptorSummary[];
99
+ };
100
+ runtimeTasks: IDeploymentPreflightRuntimeTask[];
101
+ platformBindings: IDeploymentPreflightPlatformBinding[];
102
+ corestoreResources: IDeploymentPreflightCorestoreResource[];
103
+ proposed: {
104
+ namespace: string;
105
+ namespaceReserved: boolean;
106
+ namespaceBlank: boolean;
107
+ versionReserved: boolean;
108
+ wouldReplaceExistingState: boolean;
109
+ };
110
+ authorizationChecks: IDeploymentPreflightAuthorizationCheck[];
111
+ compatibilityBlockers: IDeploymentPreflightBlocker[];
112
+ }
113
+ export interface IGreenfieldDeploymentFence {
114
+ sourceServiceId: string;
115
+ sourceRevision: string;
116
+ version: string;
117
+ ociIndexDigest: TSha256Digest;
118
+ configurationDigest: TSha256Digest;
119
+ targetNamespace: string;
120
+ }
121
+ export interface IGreenfieldDeploymentOperation {
122
+ id: string;
123
+ data: {
124
+ idempotencyKey: string;
125
+ requestDigest: TSha256Digest;
126
+ fence: IGreenfieldDeploymentFence;
127
+ candidateServiceId?: string;
128
+ state: 'reserved' | 'provisioning' | 'deploying' | 'verifying' | 'ready-for-promotion' | 'promoting' | 'promoted' | 'rolling-back' | 'rolled-back' | 'failed';
129
+ routeGeneration?: number;
130
+ createdAt: number;
131
+ updatedAt: number;
132
+ failureCode?: string;
133
+ };
134
+ }
135
+ export declare const validateDeploymentPreflightRequest: (requestArg: unknown) => IDeploymentPreflightBlocker[];
136
+ export declare const validateDeploymentReleaseEvidence: (argsArg: {
137
+ evidence: unknown;
138
+ sourceRevision: string;
139
+ version: string;
140
+ configurationDigest: TSha256Digest;
141
+ ociIndexDigest: TSha256Digest;
142
+ }) => IDeploymentPreflightBlocker[];
@@ -0,0 +1,93 @@
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 requiredEvidenceKinds = new Set([
5
+ 'provenance',
6
+ 'sbom',
7
+ 'signature',
8
+ 'vulnerability-policy',
9
+ 'release-authorization',
10
+ ]);
11
+ const isRecord = (valueArg) => Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg);
12
+ export const validateDeploymentPreflightRequest = (requestArg) => {
13
+ const blockers = [];
14
+ if (!isRecord(requestArg)) {
15
+ return [{ code: 'INVALID_REQUEST', message: 'Deployment preflight request must be an object.' }];
16
+ }
17
+ if (typeof requestArg.requestId !== 'string' || !requestArg.requestId.trim() || requestArg.requestId.length > 200) {
18
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded request identifier is required.' });
19
+ }
20
+ if (typeof requestArg.serviceId !== 'string' || !requestArg.serviceId.trim() || requestArg.serviceId.length > 200) {
21
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded service identifier is required.' });
22
+ }
23
+ if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) {
24
+ blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' });
25
+ }
26
+ if (typeof requestArg.version !== 'string' || !requestArg.version.trim() || requestArg.version.length > 100) {
27
+ blockers.push({ code: 'VERSION_NOT_RESERVED', message: 'A bounded release version is required.' });
28
+ }
29
+ if (typeof requestArg.proposedNamespace !== 'string' || !namespaceRegex.test(requestArg.proposedNamespace)) {
30
+ blockers.push({ code: 'NAMESPACE_NOT_RESERVED', message: 'The proposed namespace is missing or malformed.' });
31
+ }
32
+ if (typeof requestArg.configurationDigest !== 'string' || !normalizeSha256Digest(requestArg.configurationDigest)) {
33
+ blockers.push({ code: 'CONFIGURATION_DIGEST_INVALID', message: 'Configuration digest must be a canonical sha256 digest.' });
34
+ }
35
+ if (requestArg.ociIndexDigest !== undefined
36
+ && (typeof requestArg.ociIndexDigest !== 'string' || !normalizeSha256Digest(requestArg.ociIndexDigest))) {
37
+ blockers.push({ code: 'OCI_INDEX_DIGEST_REQUIRED', message: 'OCI index digest must be a canonical sha256 digest.' });
38
+ }
39
+ if (requestArg.greenfield !== true) {
40
+ blockers.push({ code: 'WOULD_REPLACE_EXISTING_STATE', message: 'Only greenfield deployment preflight is supported.' });
41
+ }
42
+ return blockers;
43
+ };
44
+ export const validateDeploymentReleaseEvidence = (argsArg) => {
45
+ const blockers = [];
46
+ if (!isRecord(argsArg.evidence)) {
47
+ return [{ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence must be an object.' }];
48
+ }
49
+ const evidence = argsArg.evidence;
50
+ if (evidence.releaseAuthorized !== true) {
51
+ blockers.push({ code: 'RELEASE_NOT_AUTHORIZED', message: 'Release authorization evidence is explicitly false.' });
52
+ }
53
+ if (evidence.sourceRevision !== argsArg.sourceRevision
54
+ || evidence.version !== argsArg.version
55
+ || evidence.configurationDigest !== argsArg.configurationDigest
56
+ || evidence.ociIndexDigest !== argsArg.ociIndexDigest) {
57
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence is not bound to the requested deployment fence.' });
58
+ }
59
+ const seenKinds = new Set();
60
+ const attestations = Array.isArray(evidence.attestations) ? evidence.attestations : [];
61
+ if (!Array.isArray(evidence.attestations)) {
62
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestations must be an array.' });
63
+ }
64
+ for (const attestation of attestations) {
65
+ if (!isRecord(attestation)) {
66
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation must be an object.' });
67
+ continue;
68
+ }
69
+ const kind = attestation.kind;
70
+ if (typeof kind !== 'string' || !requiredEvidenceKinds.has(kind)) {
71
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation kind is invalid.' });
72
+ continue;
73
+ }
74
+ seenKinds.add(kind);
75
+ if (attestation.subjectDigest !== argsArg.ociIndexDigest
76
+ || typeof attestation.statementDigest !== 'string'
77
+ || !normalizeSha256Digest(attestation.statementDigest)
78
+ || typeof attestation.verifier !== 'string'
79
+ || !attestation.verifier.trim()
80
+ || typeof attestation.verifiedAt !== 'number'
81
+ || !Number.isSafeInteger(attestation.verifiedAt)
82
+ || attestation.verifiedAt < 1) {
83
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: `Invalid ${kind} evidence.` });
84
+ }
85
+ }
86
+ for (const requiredKind of requiredEvidenceKinds) {
87
+ if (!seenKinds.has(requiredKind)) {
88
+ blockers.push({ code: 'RELEASE_EVIDENCE_MISSING', message: `Missing ${requiredKind} evidence.` });
89
+ }
90
+ }
91
+ return blockers;
92
+ };
93
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwbG95bWVudHByZWZsaWdodC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvZGVwbG95bWVudHByZWZsaWdodC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFJQSxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQTBMNUQsTUFBTSxvQkFBb0IsR0FBRyxnQkFBZ0IsQ0FBQztBQUM5QyxNQUFNLGNBQWMsR0FBRyx3Q0FBd0MsQ0FBQztBQUNoRSxNQUFNLHFCQUFxQixHQUFHLElBQUksR0FBRyxDQUE4QztJQUNqRixZQUFZO0lBQ1osTUFBTTtJQUNOLFdBQVc7SUFDWCxzQkFBc0I7SUFDdEIsdUJBQXVCO0NBQ3hCLENBQUMsQ0FBQztBQUVILE1BQU0sUUFBUSxHQUFHLENBQUMsUUFBaUIsRUFBdUMsRUFBRSxDQUMxRSxPQUFPLENBQUMsUUFBUSxDQUFDLElBQUksT0FBTyxRQUFRLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUVoRixNQUFNLENBQUMsTUFBTSxrQ0FBa0MsR0FBRyxDQUNoRCxVQUFtQixFQUNZLEVBQUU7SUFDakMsTUFBTSxRQUFRLEdBQWtDLEVBQUUsQ0FBQztJQUNuRCxJQUFJLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7UUFDMUIsT0FBTyxDQUFDLEVBQUUsSUFBSSxFQUFFLGlCQUFpQixFQUFFLE9BQU8sRUFBRSxpREFBaUQsRUFBRSxDQUFDLENBQUM7SUFDbkcsQ0FBQztJQUNELElBQUksT0FBTyxVQUFVLENBQUMsU0FBUyxLQUFLLFFBQVEsSUFBSSxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLElBQUksVUFBVSxDQUFDLFNBQVMsQ0FBQyxNQUFNLEdBQUcsR0FBRyxFQUFFLENBQUM7UUFDbEgsUUFBUSxDQUFDLElBQUksQ0FBQyxFQUFFLElBQUksRUFBRSxpQkFBaUIsRUFBRSxPQUFPLEVBQUUsMkNBQTJDLEVBQUUsQ0FBQyxDQUFDO0lBQ25HLENBQUM7SUFDRCxJQUFJLE9BQU8sVUFBVSxDQUFDLFNBQVMsS0FBSyxRQUFRLElBQUksQ0FBQyxVQUFVLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxJQUFJLFVBQVUsQ0FBQyxTQUFTLENBQUMsTUFBTSxHQUFHLEdBQUcsRUFBRSxDQUFDO1FBQ2xILFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsaUJBQWlCLEVBQUUsT0FBTyxFQUFFLDJDQUEyQyxFQUFFLENBQUMsQ0FBQztJQUNuRyxDQUFDO0lBQ0QsSUFBSSxPQUFPLFVBQVUsQ0FBQyxjQUFjLEtBQUssUUFBUSxJQUFJLENBQUMsb0JBQW9CLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxjQUFjLENBQUMsRUFBRSxDQUFDO1FBQzNHLFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUseUJBQXlCLEVBQUUsT0FBTyxFQUFFLDJEQUEyRCxFQUFFLENBQUMsQ0FBQztJQUMzSCxDQUFDO0lBQ0QsSUFBSSxPQUFPLFVBQVUsQ0FBQyxPQUFPLEtBQUssUUFBUSxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsSUFBSSxVQUFVLENBQUMsT0FBTyxDQUFDLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQztRQUM1RyxRQUFRLENBQUMsSUFBSSxDQUFDLEVBQUUsSUFBSSxFQUFFLHNCQUFzQixFQUFFLE9BQU8sRUFBRSx3Q0FBd0MsRUFBRSxDQUFDLENBQUM7SUFDckcsQ0FBQztJQUNELElBQUksT0FBTyxVQUFVLENBQUMsaUJBQWlCLEtBQUssUUFBUSxJQUFJLENBQUMsY0FBYyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsaUJBQWlCLENBQUMsRUFBRSxDQUFDO1FBQzNHLFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsd0JBQXdCLEVBQUUsT0FBTyxFQUFFLGlEQUFpRCxFQUFFLENBQUMsQ0FBQztJQUNoSCxDQUFDO0lBQ0QsSUFBSSxPQUFPLFVBQVUsQ0FBQyxtQkFBbUIsS0FBSyxRQUFRLElBQUksQ0FBQyxxQkFBcUIsQ0FBQyxVQUFVLENBQUMsbUJBQW1CLENBQUMsRUFBRSxDQUFDO1FBQ2pILFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsOEJBQThCLEVBQUUsT0FBTyxFQUFFLHlEQUF5RCxFQUFFLENBQUMsQ0FBQztJQUM5SCxDQUFDO0lBQ0QsSUFBSSxVQUFVLENBQUMsY0FBYyxLQUFLLFNBQVM7V0FDdEMsQ0FBQyxPQUFPLFVBQVUsQ0FBQyxjQUFjLEtBQUssUUFBUSxJQUFJLENBQUMscUJBQXFCLENBQUMsVUFBVSxDQUFDLGNBQWMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUMxRyxRQUFRLENBQUMsSUFBSSxDQUFDLEVBQUUsSUFBSSxFQUFFLDJCQUEyQixFQUFFLE9BQU8sRUFBRSxxREFBcUQsRUFBRSxDQUFDLENBQUM7SUFDdkgsQ0FBQztJQUNELElBQUksVUFBVSxDQUFDLFVBQVUsS0FBSyxJQUFJLEVBQUUsQ0FBQztRQUNuQyxRQUFRLENBQUMsSUFBSSxDQUFDLEVBQUUsSUFBSSxFQUFFLDhCQUE4QixFQUFFLE9BQU8sRUFBRSxvREFBb0QsRUFBRSxDQUFDLENBQUM7SUFDekgsQ0FBQztJQUNELE9BQU8sUUFBUSxDQUFDO0FBQ2xCLENBQUMsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLGlDQUFpQyxHQUFHLENBQUMsT0FNakQsRUFBaUMsRUFBRTtJQUNsQyxNQUFNLFFBQVEsR0FBa0MsRUFBRSxDQUFDO0lBQ25ELElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7UUFDaEMsT0FBTyxDQUFDLEVBQUUsSUFBSSxFQUFFLDBCQUEwQixFQUFFLE9BQU8sRUFBRSxxQ0FBcUMsRUFBRSxDQUFDLENBQUM7SUFDaEcsQ0FBQztJQUNELE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFRLENBQUM7SUFDbEMsSUFBSSxRQUFRLENBQUMsaUJBQWlCLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDeEMsUUFBUSxDQUFDLElBQUksQ0FBQyxFQUFFLElBQUksRUFBRSx3QkFBd0IsRUFBRSxPQUFPLEVBQUUscURBQXFELEVBQUUsQ0FBQyxDQUFDO0lBQ3BILENBQUM7SUFDRCxJQUNFLFFBQVEsQ0FBQyxjQUFjLEtBQUssT0FBTyxDQUFDLGNBQWM7V0FDL0MsUUFBUSxDQUFDLE9BQU8sS0FBSyxPQUFPLENBQUMsT0FBTztXQUNwQyxRQUFRLENBQUMsbUJBQW1CLEtBQUssT0FBTyxDQUFDLG1CQUFtQjtXQUM1RCxRQUFRLENBQUMsY0FBYyxLQUFLLE9BQU8sQ0FBQyxjQUFjLEVBQ3JELENBQUM7UUFDRCxRQUFRLENBQUMsSUFBSSxDQUFDLEVBQUUsSUFBSSxFQUFFLDBCQUEwQixFQUFFLE9BQU8sRUFBRSxrRUFBa0UsRUFBRSxDQUFDLENBQUM7SUFDbkksQ0FBQztJQUNELE1BQU0sU0FBUyxHQUFHLElBQUksR0FBRyxFQUErQyxDQUFDO0lBQ3pFLE1BQU0sWUFBWSxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7SUFDdkYsSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxFQUFFLENBQUM7UUFDMUMsUUFBUSxDQUFDLElBQUksQ0FBQyxFQUFFLElBQUksRUFBRSwwQkFBMEIsRUFBRSxPQUFPLEVBQUUsd0NBQXdDLEVBQUUsQ0FBQyxDQUFDO0lBQ3pHLENBQUM7SUFDRCxLQUFLLE1BQU0sV0FBVyxJQUFJLFlBQVksRUFBRSxDQUFDO1FBQ3ZDLElBQUksQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQztZQUMzQixRQUFRLENBQUMsSUFBSSxDQUFDLEVBQUUsSUFBSSxFQUFFLDBCQUEwQixFQUFFLE9BQU8sRUFBRSx3Q0FBd0MsRUFBRSxDQUFDLENBQUM7WUFDdkcsU0FBUztRQUNYLENBQUM7UUFDRCxNQUFNLElBQUksR0FBRyxXQUFXLENBQUMsSUFBSSxDQUFDO1FBQzlCLElBQUksT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLENBQUMscUJBQXFCLENBQUMsR0FBRyxDQUFDLElBQW1ELENBQUMsRUFBRSxDQUFDO1lBQ2hILFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsMEJBQTBCLEVBQUUsT0FBTyxFQUFFLHNDQUFzQyxFQUFFLENBQUMsQ0FBQztZQUNyRyxTQUFTO1FBQ1gsQ0FBQztRQUNELFNBQVMsQ0FBQyxHQUFHLENBQUMsSUFBbUQsQ0FBQyxDQUFDO1FBQ25FLElBQ0UsV0FBVyxDQUFDLGFBQWEsS0FBSyxPQUFPLENBQUMsY0FBYztlQUNqRCxPQUFPLFdBQVcsQ0FBQyxlQUFlLEtBQUssUUFBUTtlQUMvQyxDQUFDLHFCQUFxQixDQUFDLFdBQVcsQ0FBQyxlQUFlLENBQUM7ZUFDbkQsT0FBTyxXQUFXLENBQUMsUUFBUSxLQUFLLFFBQVE7ZUFDeEMsQ0FBQyxXQUFXLENBQUMsUUFBUSxDQUFDLElBQUksRUFBRTtlQUM1QixPQUFPLFdBQVcsQ0FBQyxVQUFVLEtBQUssUUFBUTtlQUMxQyxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsV0FBVyxDQUFDLFVBQVUsQ0FBQztlQUM3QyxXQUFXLENBQUMsVUFBVSxHQUFHLENBQUMsRUFDN0IsQ0FBQztZQUNELFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsMEJBQTBCLEVBQUUsT0FBTyxFQUFFLFdBQVcsSUFBSSxZQUFZLEVBQUUsQ0FBQyxDQUFDO1FBQzVGLENBQUM7SUFDSCxDQUFDO0lBQ0QsS0FBSyxNQUFNLFlBQVksSUFBSSxxQkFBcUIsRUFBRSxDQUFDO1FBQ2pELElBQUksQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLFlBQVksQ0FBQyxFQUFFLENBQUM7WUFDakMsUUFBUSxDQUFDLElBQUksQ0FBQyxFQUFFLElBQUksRUFBRSwwQkFBMEIsRUFBRSxPQUFPLEVBQUUsV0FBVyxZQUFZLFlBQVksRUFBRSxDQUFDLENBQUM7UUFDcEcsQ0FBQztJQUNILENBQUM7SUFDRCxPQUFPLFFBQVEsQ0FBQztBQUNsQixDQUFDLENBQUMifQ==
@@ -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
  }
@@ -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": "11.4.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '11.3.0',
6
+ version: '11.4.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
@@ -0,0 +1,297 @@
1
+ import type {
2
+ TOciManifestMediaType,
3
+ TSha256Digest,
4
+ } from './immutableimage.js';
5
+ import { normalizeSha256Digest } from './immutableimage.js';
6
+ import type { TServiceDeploymentCapability } from './user.js';
7
+
8
+ export type TDeploymentPreflightDecision = 'GO' | 'NO-GO';
9
+
10
+ export type TDeploymentPreflightBlockerCode =
11
+ | 'INVALID_REQUEST'
12
+ | 'AUTHORIZATION_MISSING'
13
+ | 'SOURCE_REVISION_INVALID'
14
+ | 'VERSION_NOT_RESERVED'
15
+ | 'VERSION_COLLISION'
16
+ | 'NAMESPACE_NOT_RESERVED'
17
+ | 'NAMESPACE_COLLISION'
18
+ | 'NAMESPACE_NOT_PROVEN_BLANK'
19
+ | 'CONFIGURATION_DIGEST_INVALID'
20
+ | 'CURRENT_REFERENCE_MUTABLE'
21
+ | 'CURRENT_RUNTIME_DIGEST_MISSING'
22
+ | 'OCI_INDEX_DIGEST_REQUIRED'
23
+ | 'OCI_INDEX_NOT_FOUND'
24
+ | 'OCI_INDEX_MEDIA_TYPE_REQUIRED'
25
+ | 'OCI_PLATFORM_MISSING'
26
+ | 'OCI_PLATFORM_MANIFEST_MISSING'
27
+ | 'IMMUTABLE_RELEASE_TAG_REQUIRED'
28
+ | 'IMMUTABLE_RELEASE_TAG_NOT_RESERVED'
29
+ | 'RELEASE_EVIDENCE_MISSING'
30
+ | 'RELEASE_EVIDENCE_INVALID'
31
+ | 'RELEASE_NOT_AUTHORIZED'
32
+ | 'GREENFIELD_EXECUTION_UNAVAILABLE'
33
+ | 'ROUTE_PROMOTION_FENCE_UNAVAILABLE'
34
+ | 'POST_DEPLOYMENT_VERIFICATION_UNAVAILABLE'
35
+ | 'WOULD_REPLACE_EXISTING_STATE'
36
+ | 'CORESTORE_INVENTORY_UNAVAILABLE'
37
+ | 'RUNTIME_ATTESTATION_MISMATCH';
38
+
39
+ export interface IDeploymentPreflightBlocker {
40
+ code: TDeploymentPreflightBlockerCode;
41
+ message: string;
42
+ }
43
+
44
+ export interface IDeploymentPreflightAuthorizationCheck {
45
+ capability: TServiceDeploymentCapability;
46
+ granted: boolean;
47
+ }
48
+
49
+ export interface IOciPlatformDescriptorSummary {
50
+ os: string;
51
+ architecture: string;
52
+ digest?: TSha256Digest;
53
+ mediaType?: string;
54
+ runnable: boolean;
55
+ manifestPresent: boolean;
56
+ }
57
+
58
+ export interface IDeploymentPreflightRuntimeTask {
59
+ deploymentId: string;
60
+ nodeName?: string;
61
+ status: string;
62
+ healthStatus?: string;
63
+ imageReference?: string;
64
+ reportedDigest?: TSha256Digest;
65
+ verificationStatus?: 'not-required' | 'pending' | 'verified' | 'missing' | 'mismatched';
66
+ }
67
+
68
+ export interface IDeploymentPreflightPlatformBinding {
69
+ capability: 'database' | 'objectstorage';
70
+ desiredState: 'enabled' | 'disabled';
71
+ status: 'requested' | 'provisioning' | 'ready' | 'degraded' | 'failed' | 'disabled';
72
+ }
73
+
74
+ export interface IDeploymentPreflightCorestoreResource {
75
+ capability: 'database' | 'objectstorage';
76
+ provider: string;
77
+ resourceName: string;
78
+ }
79
+
80
+ export interface IDeploymentReleaseEvidenceReference {
81
+ kind: 'provenance' | 'sbom' | 'signature' | 'vulnerability-policy' | 'release-authorization';
82
+ statementDigest: TSha256Digest;
83
+ subjectDigest: TSha256Digest;
84
+ verifier: string;
85
+ verifiedAt: number;
86
+ }
87
+
88
+ /** Evidence is descriptive input until a trusted Cloudly verifier validates it. */
89
+ export interface IDeploymentReleaseEvidence {
90
+ sourceRevision: string;
91
+ version: string;
92
+ configurationDigest: TSha256Digest;
93
+ ociIndexDigest: TSha256Digest;
94
+ releaseAuthorized: boolean;
95
+ attestations: IDeploymentReleaseEvidenceReference[];
96
+ }
97
+
98
+ export interface IDeploymentPreflightRequestData {
99
+ requestId: string;
100
+ serviceId: string;
101
+ sourceRevision: string;
102
+ version: string;
103
+ configurationDigest: string;
104
+ proposedNamespace: string;
105
+ ociIndexDigest?: string;
106
+ releaseTag?: string;
107
+ releaseEvidence?: IDeploymentReleaseEvidence;
108
+ greenfield: true;
109
+ }
110
+
111
+ export interface IDeploymentPreflightReport {
112
+ schemaVersion: 1;
113
+ decision: TDeploymentPreflightDecision;
114
+ mutationPerformed: false;
115
+ generatedAt: number;
116
+ service: {
117
+ serviceId: string;
118
+ serviceName: string;
119
+ workspaceIdentity: string;
120
+ registryRepository: string;
121
+ deployOnPush: boolean;
122
+ };
123
+ source: {
124
+ revision: string;
125
+ version: string;
126
+ configurationDigest?: TSha256Digest;
127
+ };
128
+ currentImage: {
129
+ tag?: string;
130
+ digest?: TSha256Digest;
131
+ imageReference?: string;
132
+ mutableReference: boolean;
133
+ mediaType?: TOciManifestMediaType;
134
+ platforms: IOciPlatformDescriptorSummary[];
135
+ };
136
+ requestedImage: {
137
+ digest?: TSha256Digest;
138
+ releaseTag?: string;
139
+ tagReserved: boolean;
140
+ mediaType?: TOciManifestMediaType;
141
+ platforms: IOciPlatformDescriptorSummary[];
142
+ };
143
+ runtimeTasks: IDeploymentPreflightRuntimeTask[];
144
+ platformBindings: IDeploymentPreflightPlatformBinding[];
145
+ corestoreResources: IDeploymentPreflightCorestoreResource[];
146
+ proposed: {
147
+ namespace: string;
148
+ namespaceReserved: boolean;
149
+ namespaceBlank: boolean;
150
+ versionReserved: boolean;
151
+ wouldReplaceExistingState: boolean;
152
+ };
153
+ authorizationChecks: IDeploymentPreflightAuthorizationCheck[];
154
+ compatibilityBlockers: IDeploymentPreflightBlocker[];
155
+ }
156
+
157
+ export interface IGreenfieldDeploymentFence {
158
+ sourceServiceId: string;
159
+ sourceRevision: string;
160
+ version: string;
161
+ ociIndexDigest: TSha256Digest;
162
+ configurationDigest: TSha256Digest;
163
+ targetNamespace: string;
164
+ }
165
+
166
+ export interface IGreenfieldDeploymentOperation {
167
+ id: string;
168
+ data: {
169
+ idempotencyKey: string;
170
+ requestDigest: TSha256Digest;
171
+ fence: IGreenfieldDeploymentFence;
172
+ candidateServiceId?: string;
173
+ state:
174
+ | 'reserved'
175
+ | 'provisioning'
176
+ | 'deploying'
177
+ | 'verifying'
178
+ | 'ready-for-promotion'
179
+ | 'promoting'
180
+ | 'promoted'
181
+ | 'rolling-back'
182
+ | 'rolled-back'
183
+ | 'failed';
184
+ routeGeneration?: number;
185
+ createdAt: number;
186
+ updatedAt: number;
187
+ failureCode?: string;
188
+ };
189
+ }
190
+
191
+ const fullGitRevisionRegex = /^[a-f0-9]{40}$/;
192
+ const namespaceRegex = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
193
+ const requiredEvidenceKinds = new Set<IDeploymentReleaseEvidenceReference['kind']>([
194
+ 'provenance',
195
+ 'sbom',
196
+ 'signature',
197
+ 'vulnerability-policy',
198
+ 'release-authorization',
199
+ ]);
200
+
201
+ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> =>
202
+ Boolean(valueArg) && typeof valueArg === 'object' && !Array.isArray(valueArg);
203
+
204
+ export const validateDeploymentPreflightRequest = (
205
+ requestArg: unknown,
206
+ ): IDeploymentPreflightBlocker[] => {
207
+ const blockers: IDeploymentPreflightBlocker[] = [];
208
+ if (!isRecord(requestArg)) {
209
+ return [{ code: 'INVALID_REQUEST', message: 'Deployment preflight request must be an object.' }];
210
+ }
211
+ if (typeof requestArg.requestId !== 'string' || !requestArg.requestId.trim() || requestArg.requestId.length > 200) {
212
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded request identifier is required.' });
213
+ }
214
+ if (typeof requestArg.serviceId !== 'string' || !requestArg.serviceId.trim() || requestArg.serviceId.length > 200) {
215
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded service identifier is required.' });
216
+ }
217
+ if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) {
218
+ blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' });
219
+ }
220
+ if (typeof requestArg.version !== 'string' || !requestArg.version.trim() || requestArg.version.length > 100) {
221
+ blockers.push({ code: 'VERSION_NOT_RESERVED', message: 'A bounded release version is required.' });
222
+ }
223
+ if (typeof requestArg.proposedNamespace !== 'string' || !namespaceRegex.test(requestArg.proposedNamespace)) {
224
+ blockers.push({ code: 'NAMESPACE_NOT_RESERVED', message: 'The proposed namespace is missing or malformed.' });
225
+ }
226
+ if (typeof requestArg.configurationDigest !== 'string' || !normalizeSha256Digest(requestArg.configurationDigest)) {
227
+ blockers.push({ code: 'CONFIGURATION_DIGEST_INVALID', message: 'Configuration digest must be a canonical sha256 digest.' });
228
+ }
229
+ if (requestArg.ociIndexDigest !== undefined
230
+ && (typeof requestArg.ociIndexDigest !== 'string' || !normalizeSha256Digest(requestArg.ociIndexDigest))) {
231
+ blockers.push({ code: 'OCI_INDEX_DIGEST_REQUIRED', message: 'OCI index digest must be a canonical sha256 digest.' });
232
+ }
233
+ if (requestArg.greenfield !== true) {
234
+ blockers.push({ code: 'WOULD_REPLACE_EXISTING_STATE', message: 'Only greenfield deployment preflight is supported.' });
235
+ }
236
+ return blockers;
237
+ };
238
+
239
+ export const validateDeploymentReleaseEvidence = (argsArg: {
240
+ evidence: unknown;
241
+ sourceRevision: string;
242
+ version: string;
243
+ configurationDigest: TSha256Digest;
244
+ ociIndexDigest: TSha256Digest;
245
+ }): IDeploymentPreflightBlocker[] => {
246
+ const blockers: IDeploymentPreflightBlocker[] = [];
247
+ if (!isRecord(argsArg.evidence)) {
248
+ return [{ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence must be an object.' }];
249
+ }
250
+ const evidence = argsArg.evidence;
251
+ if (evidence.releaseAuthorized !== true) {
252
+ blockers.push({ code: 'RELEASE_NOT_AUTHORIZED', message: 'Release authorization evidence is explicitly false.' });
253
+ }
254
+ if (
255
+ evidence.sourceRevision !== argsArg.sourceRevision
256
+ || evidence.version !== argsArg.version
257
+ || evidence.configurationDigest !== argsArg.configurationDigest
258
+ || evidence.ociIndexDigest !== argsArg.ociIndexDigest
259
+ ) {
260
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release evidence is not bound to the requested deployment fence.' });
261
+ }
262
+ const seenKinds = new Set<IDeploymentReleaseEvidenceReference['kind']>();
263
+ const attestations = Array.isArray(evidence.attestations) ? evidence.attestations : [];
264
+ if (!Array.isArray(evidence.attestations)) {
265
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestations must be an array.' });
266
+ }
267
+ for (const attestation of attestations) {
268
+ if (!isRecord(attestation)) {
269
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation must be an object.' });
270
+ continue;
271
+ }
272
+ const kind = attestation.kind;
273
+ if (typeof kind !== 'string' || !requiredEvidenceKinds.has(kind as IDeploymentReleaseEvidenceReference['kind'])) {
274
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: 'Release attestation kind is invalid.' });
275
+ continue;
276
+ }
277
+ seenKinds.add(kind as IDeploymentReleaseEvidenceReference['kind']);
278
+ if (
279
+ attestation.subjectDigest !== argsArg.ociIndexDigest
280
+ || typeof attestation.statementDigest !== 'string'
281
+ || !normalizeSha256Digest(attestation.statementDigest)
282
+ || typeof attestation.verifier !== 'string'
283
+ || !attestation.verifier.trim()
284
+ || typeof attestation.verifiedAt !== 'number'
285
+ || !Number.isSafeInteger(attestation.verifiedAt)
286
+ || attestation.verifiedAt < 1
287
+ ) {
288
+ blockers.push({ code: 'RELEASE_EVIDENCE_INVALID', message: `Invalid ${kind} evidence.` });
289
+ }
290
+ }
291
+ for (const requiredKind of requiredEvidenceKinds) {
292
+ if (!seenKinds.has(requiredKind)) {
293
+ blockers.push({ code: 'RELEASE_EVIDENCE_MISSING', message: `Missing ${requiredKind} evidence.` });
294
+ }
295
+ }
296
+ return blockers;
297
+ };
package/ts/data/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
  }
@@ -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<