@spottoai/types-package 1.0.2-beta.356 → 1.0.2-beta.358
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/README.md +26 -0
- package/dist/accounts/azureSpSetup.d.ts +2 -4
- package/dist/accounts/azureSpSetup.d.ts.map +1 -1
- package/dist/azure/billingArtifactCanonicalization.d.ts +28 -0
- package/dist/azure/billingArtifactCanonicalization.d.ts.map +1 -0
- package/dist/azure/billingArtifactCanonicalization.js +291 -0
- package/dist/azure/billingArtifactCanonicalization.js.map +1 -0
- package/dist/azure/billingArtifactEvidence.d.ts +39 -0
- package/dist/azure/billingArtifactEvidence.d.ts.map +1 -0
- package/dist/azure/billingArtifactEvidence.js +29 -0
- package/dist/azure/billingArtifactEvidence.js.map +1 -0
- package/dist/azure/billingArtifactGeneration.d.ts +114 -0
- package/dist/azure/billingArtifactGeneration.d.ts.map +1 -0
- package/dist/azure/billingArtifactGeneration.js +238 -0
- package/dist/azure/billingArtifactGeneration.js.map +1 -0
- package/dist/azure/billingPlots.d.ts +22 -0
- package/dist/azure/billingPlots.d.ts.map +1 -1
- package/dist/azure/billingPlots.js +186 -0
- package/dist/azure/billingPlots.js.map +1 -1
- package/dist/azure/views.d.ts +62 -1
- package/dist/azure/views.d.ts.map +1 -1
- package/dist/azure/views.js +152 -1
- package/dist/azure/views.js.map +1 -1
- package/dist/common/artifactControlData.d.ts +12 -0
- package/dist/common/artifactControlData.d.ts.map +1 -0
- package/dist/common/artifactControlData.js +102 -0
- package/dist/common/artifactControlData.js.map +1 -0
- package/dist/common/artifactEvidence.d.ts +144 -0
- package/dist/common/artifactEvidence.d.ts.map +1 -0
- package/dist/common/artifactEvidence.js +181 -0
- package/dist/common/artifactEvidence.js.map +1 -0
- package/dist/common/artifactEvidenceValidation.d.ts +16 -0
- package/dist/common/artifactEvidenceValidation.d.ts.map +1 -0
- package/dist/common/artifactEvidenceValidation.js +31 -0
- package/dist/common/artifactEvidenceValidation.js.map +1 -0
- package/dist/common/artifactGeneration.d.ts +4 -2
- package/dist/common/artifactGeneration.d.ts.map +1 -1
- package/dist/common/artifactGeneration.js.map +1 -1
- package/dist/common/capabilityPassport.d.ts +6 -11
- package/dist/common/capabilityPassport.d.ts.map +1 -1
- package/dist/common/capabilityPassport.js +2 -10
- package/dist/common/capabilityPassport.js.map +1 -1
- package/dist/esm/azure/billingArtifactCanonicalization.js +281 -0
- package/dist/esm/azure/billingArtifactEvidence.js +23 -0
- package/dist/esm/azure/billingArtifactGeneration.js +229 -0
- package/dist/esm/azure/billingPlots.js +184 -1
- package/dist/esm/azure/views.js +149 -0
- package/dist/esm/common/artifactControlData.js +95 -0
- package/dist/esm/common/artifactEvidence.js +173 -0
- package/dist/esm/common/artifactEvidenceValidation.js +24 -0
- package/dist/esm/common/capabilityPassport.js +2 -10
- package/dist/esm/index.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
|
@@ -1,4 +1,187 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Billing cost analysis types for Azure cost visualization.
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
import { isArtifactOwnershipBinding, } from '../common/artifactEvidence.js';
|
|
5
|
+
import { containsForbiddenArtifactControlData } from '../common/artifactControlData.js';
|
|
6
|
+
import { isArtifactRevisionVector } from '../common/artifactEvidenceValidation.js';
|
|
7
|
+
import { isBillingCompletedArtifactPublicationDecision, isBillingPartialArtifactPublicationDecision, } from './billingArtifactEvidence.js';
|
|
8
|
+
const BILLING_DOCUMENT_STATES = new Set(['current', 'stale', 'partial', 'fallback', 'complete-empty']);
|
|
9
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
10
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
const isNonEmptyString = (value) => typeof value === 'string' && value.trim() === value && value.length > 0;
|
|
12
|
+
const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
13
|
+
const isOptionalFiniteNumber = (value) => value === undefined || isFiniteNumber(value);
|
|
14
|
+
const isNullableFiniteNumber = (value) => value === null || isFiniteNumber(value);
|
|
15
|
+
const isNonNegativeInteger = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
16
|
+
const isPositiveInteger = (value) => Number.isSafeInteger(value) && Number(value) > 0;
|
|
17
|
+
const isStringArray = (value) => Array.isArray(value) && value.every(isNonEmptyString);
|
|
18
|
+
const hasControlCharacters = (value) => Array.from(value).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
19
|
+
const isPathSegment = (value) => isNonEmptyString(value) && !/[\\/?#%]/.test(value) && !hasControlCharacters(value) && value !== '.' && value !== '..';
|
|
20
|
+
const publicationDecisionReferencesDigest = (value, digest) => isRecord(value) && Array.isArray(value.dependencies) && value.dependencies.some(dependency => isRecord(dependency) && dependency.digest === digest);
|
|
21
|
+
const isTrend = (value) => isRecord(value) && isNonEmptyString(value.method) && isFiniteNumber(value.slope) && isFiniteNumber(value.intercept);
|
|
22
|
+
const isDailyPoint = (value) => isRecord(value) &&
|
|
23
|
+
isNonEmptyString(value.date) &&
|
|
24
|
+
isFiniteNumber(value.timestamp) &&
|
|
25
|
+
isFiniteNumber(value.cost) &&
|
|
26
|
+
typeof value.isAnomaly === 'boolean' &&
|
|
27
|
+
isNonNegativeInteger(value.anomalyVotes) &&
|
|
28
|
+
isOptionalFiniteNumber(value.trendCost) &&
|
|
29
|
+
(value.anomalyMethods === undefined || isStringArray(value.anomalyMethods));
|
|
30
|
+
const isForecastPoint = (value) => isRecord(value) &&
|
|
31
|
+
isNonEmptyString(value.date) &&
|
|
32
|
+
isFiniteNumber(value.timestamp) &&
|
|
33
|
+
isFiniteNumber(value.cost) &&
|
|
34
|
+
isOptionalFiniteNumber(value.trendCost);
|
|
35
|
+
const isMonthlyPoint = (value) => isRecord(value) &&
|
|
36
|
+
/^\d{4}-\d{2}$/.test(String(value.month)) &&
|
|
37
|
+
isFiniteNumber(value.startDate) &&
|
|
38
|
+
isFiniteNumber(value.endDate) &&
|
|
39
|
+
isFiniteNumber(value.cost) &&
|
|
40
|
+
isFiniteNumber(value.averageDailyCost) &&
|
|
41
|
+
isNonNegativeInteger(value.anomalyCount) &&
|
|
42
|
+
isOptionalFiniteNumber(value.trendCost) &&
|
|
43
|
+
(value.anomalyDates === undefined || (Array.isArray(value.anomalyDates) && value.anomalyDates.every(isFiniteNumber)));
|
|
44
|
+
const hasCommonChartViewFields = (value) => isFiniteNumber(value.startDate) &&
|
|
45
|
+
isFiniteNumber(value.endDate) &&
|
|
46
|
+
isFiniteNumber(value.averageDailyCost) &&
|
|
47
|
+
isFiniteNumber(value.totalCost) &&
|
|
48
|
+
(value.trend === undefined || isTrend(value.trend));
|
|
49
|
+
const isChartView = (value) => {
|
|
50
|
+
if (!isRecord(value) || (value.trend !== undefined && !isTrend(value.trend)))
|
|
51
|
+
return false;
|
|
52
|
+
if (value.forecastMethod !== undefined) {
|
|
53
|
+
return (value.aggregation === 'daily' &&
|
|
54
|
+
isNonEmptyString(value.forecastMethod) &&
|
|
55
|
+
[value.startDate, value.endDate, value.actualTotalCost, value.forecastRemaining, value.forecastMonthTotal].every(isFiniteNumber) &&
|
|
56
|
+
Array.isArray(value.actualPoints) &&
|
|
57
|
+
value.actualPoints.every(isDailyPoint) &&
|
|
58
|
+
Array.isArray(value.forecastPoints) &&
|
|
59
|
+
value.forecastPoints.every(isForecastPoint) &&
|
|
60
|
+
Array.isArray(value.fittedPoints) &&
|
|
61
|
+
value.fittedPoints.every(isForecastPoint));
|
|
62
|
+
}
|
|
63
|
+
if (!hasCommonChartViewFields(value) || !Array.isArray(value.points))
|
|
64
|
+
return false;
|
|
65
|
+
if (value.aggregation === 'daily')
|
|
66
|
+
return value.points.every(isDailyPoint);
|
|
67
|
+
if (value.aggregation === 'monthly')
|
|
68
|
+
return value.points.every(isMonthlyPoint);
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
const isChartData = (value) => {
|
|
72
|
+
if (!isRecord(value) || !isPositiveInteger(value.schemaVersion) || !isNonEmptyString(value.source))
|
|
73
|
+
return false;
|
|
74
|
+
if (!isRecord(value.dataWindow) ||
|
|
75
|
+
!isFiniteNumber(value.dataWindow.startDate) ||
|
|
76
|
+
!isFiniteNumber(value.dataWindow.endDate) ||
|
|
77
|
+
!isNonNegativeInteger(value.dataWindow.pointCount)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (!isRecord(value.views) || !Object.values(value.views).every(view => view === undefined || isChartView(view)))
|
|
81
|
+
return false;
|
|
82
|
+
if (!isRecord(value.detectors) || !isFiniteNumber(value.detectors.threshold) || !Array.isArray(value.detectors.methods))
|
|
83
|
+
return false;
|
|
84
|
+
return value.detectors.methods.every(method => isRecord(method) &&
|
|
85
|
+
isNonEmptyString(method.name) &&
|
|
86
|
+
(method.status === undefined || isNonEmptyString(method.status)) &&
|
|
87
|
+
(method.error === undefined || method.error === null || typeof method.error === 'string') &&
|
|
88
|
+
Array.isArray(method.triggeredDates) &&
|
|
89
|
+
method.triggeredDates.every(isFiniteNumber));
|
|
90
|
+
};
|
|
91
|
+
const isDriverResource = (value) => isRecord(value) &&
|
|
92
|
+
isNonEmptyString(value.name) &&
|
|
93
|
+
(value.resourceScope === undefined || isNonEmptyString(value.resourceScope)) &&
|
|
94
|
+
(value.resourceId === undefined || isNonEmptyString(value.resourceId)) &&
|
|
95
|
+
(value.isSubscriptionLevel === undefined || typeof value.isSubscriptionLevel === 'boolean') &&
|
|
96
|
+
isFiniteNumber(value.cost) &&
|
|
97
|
+
isNullableFiniteNumber(value.baseline) &&
|
|
98
|
+
isFiniteNumber(value.delta) &&
|
|
99
|
+
isNullableFiniteNumber(value.percentChange) &&
|
|
100
|
+
typeof value.isNew === 'boolean' &&
|
|
101
|
+
isNonEmptyString(value.summary);
|
|
102
|
+
const isAnomaly = (value) => {
|
|
103
|
+
if (!isRecord(value) || !isFiniteNumber(value.date) || !isNonEmptyString(value.summary) || !isNonEmptyString(value.confidence))
|
|
104
|
+
return false;
|
|
105
|
+
if (!isStringArray(value.notes) || !isRecord(value.impact) || !Array.isArray(value.drivers))
|
|
106
|
+
return false;
|
|
107
|
+
const impact = value.impact;
|
|
108
|
+
const impactRequired = ['cost', 'delta', 'monthToDateCost'];
|
|
109
|
+
const impactNullable = [
|
|
110
|
+
'baseline7Day',
|
|
111
|
+
'baseline30Day',
|
|
112
|
+
'percentChange',
|
|
113
|
+
'previousDayCost',
|
|
114
|
+
'previousDayDelta',
|
|
115
|
+
'monthToDateBaseline',
|
|
116
|
+
'monthToDateDelta',
|
|
117
|
+
'monthToDatePercentChange',
|
|
118
|
+
];
|
|
119
|
+
if (!impactRequired.every(field => isFiniteNumber(impact[field])) || !impactNullable.every(field => isNullableFiniteNumber(impact[field]))) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return value.drivers.every(driver => isRecord(driver) &&
|
|
123
|
+
isNonEmptyString(driver.type) &&
|
|
124
|
+
isNonEmptyString(driver.name) &&
|
|
125
|
+
isNonEmptyString(driver.summary) &&
|
|
126
|
+
isFiniteNumber(driver.cost) &&
|
|
127
|
+
isFiniteNumber(driver.delta) &&
|
|
128
|
+
isNullableFiniteNumber(driver.baseline) &&
|
|
129
|
+
isNullableFiniteNumber(driver.percentChange) &&
|
|
130
|
+
isFiniteNumber(driver.shareOfImpactPercent) &&
|
|
131
|
+
typeof driver.isNew === 'boolean' &&
|
|
132
|
+
Array.isArray(driver.resources) &&
|
|
133
|
+
driver.resources.every(isDriverResource));
|
|
134
|
+
};
|
|
135
|
+
const hasValidMetadataEvidenceState = (state, evidence, billingGenerationId, inputManifestDigest, chartData, anomalies) => {
|
|
136
|
+
if (state === 'partial')
|
|
137
|
+
return isBillingPartialArtifactPublicationDecision(evidence, billingGenerationId, inputManifestDigest);
|
|
138
|
+
if (!isBillingCompletedArtifactPublicationDecision(evidence, billingGenerationId, inputManifestDigest))
|
|
139
|
+
return false;
|
|
140
|
+
if (state !== 'complete-empty')
|
|
141
|
+
return true;
|
|
142
|
+
const billingHistory = evidence.dependencies.find(dependency => dependency.name === 'billing-history');
|
|
143
|
+
const dataWindow = chartData.dataWindow;
|
|
144
|
+
const views = chartData.views;
|
|
145
|
+
return (billingHistory?.emptyEvidence === 'complete-empty' &&
|
|
146
|
+
billingHistory.acceptedRowCount === 0 &&
|
|
147
|
+
isNonEmptyString(billingHistory.emptyProofRef) &&
|
|
148
|
+
isRecord(dataWindow) &&
|
|
149
|
+
dataWindow.pointCount === 0 &&
|
|
150
|
+
isRecord(views) &&
|
|
151
|
+
Object.values(views).every(view => view === undefined) &&
|
|
152
|
+
anomalies.length === 0);
|
|
153
|
+
};
|
|
154
|
+
/** Dependency-free validator for customer-readable V2 billing metadata. */
|
|
155
|
+
export const isBillingCostAnalysisMetadataV2 = (value) => {
|
|
156
|
+
if (!isRecord(value) || containsForbiddenArtifactControlData(value) || value.schemaVersion !== 2)
|
|
157
|
+
return false;
|
|
158
|
+
if (Object.prototype.hasOwnProperty.call(value, 'outputManifestDigest'))
|
|
159
|
+
return false;
|
|
160
|
+
if (!isPathSegment(value.subscriptionId) || !isPathSegment(value.billingGenerationId))
|
|
161
|
+
return false;
|
|
162
|
+
if (!isArtifactOwnershipBinding(value.ownership) || value.ownership.provider !== 'azure' || value.ownership.accountId !== value.subscriptionId)
|
|
163
|
+
return false;
|
|
164
|
+
if (!isArtifactRevisionVector(value.revision) || value.ownership.ownershipEpochRevision !== value.revision.ownershipEpochRevision) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (typeof value.artifactState !== 'string' || !BILLING_DOCUMENT_STATES.has(value.artifactState))
|
|
168
|
+
return false;
|
|
169
|
+
if (typeof value.inputManifestDigest !== 'string' || !SHA256_PATTERN.test(value.inputManifestDigest))
|
|
170
|
+
return false;
|
|
171
|
+
if (typeof value.outputBindingDigest !== 'string' || !SHA256_PATTERN.test(value.outputBindingDigest))
|
|
172
|
+
return false;
|
|
173
|
+
if (value.outputBindingDigest === value.inputManifestDigest ||
|
|
174
|
+
publicationDecisionReferencesDigest(value.artifactEvidence, value.outputBindingDigest)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (!isChartData(value.chartData) || !Array.isArray(value.anomalies) || !value.anomalies.every(isAnomaly))
|
|
178
|
+
return false;
|
|
179
|
+
if (!hasValidMetadataEvidenceState(value.artifactState, value.artifactEvidence, value.billingGenerationId, value.inputManifestDigest, value.chartData, value.anomalies)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (!isNonEmptyString(value.currencyCode) || !isNonEmptyString(value.currencySymbol))
|
|
183
|
+
return false;
|
|
184
|
+
if (value.forecastMethod !== undefined && !isNonEmptyString(value.forecastMethod))
|
|
185
|
+
return false;
|
|
186
|
+
return [value.forecastMonthTotal, value.forecastRemaining, value.forecastPeriodEnd].every(isOptionalFiniteNumber);
|
|
187
|
+
};
|
package/dist/esm/azure/views.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { allowedArtifactReferenceField, containsForbiddenArtifactControlData, } from '../common/artifactControlData.js';
|
|
2
|
+
import { isArtifactOwnershipBinding, isArtifactPublicationDecision, isEnforceableArtifactOwnershipBinding, } from '../common/artifactEvidence.js';
|
|
3
|
+
import { isArtifactRevisionVector, isStrictLogicalArtifactReference } from '../common/artifactEvidenceValidation.js';
|
|
1
4
|
const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
2
5
|
const isCanonicalIsoTimestamp = (value) => {
|
|
3
6
|
if (!isNonEmptyString(value))
|
|
@@ -30,3 +33,149 @@ export const isCompletedAzureViewSetV1 = (value) => {
|
|
|
30
33
|
return false;
|
|
31
34
|
return isNonEmptyString(value.economics.generationId) && isNonEmptyString(value.economics.fingerprint);
|
|
32
35
|
};
|
|
36
|
+
const VIEW_CONTENT_ENCODINGS = new Set(['identity', 'gzip']);
|
|
37
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
38
|
+
const hasControlCharacters = (value) => Array.from(value).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
39
|
+
const isStrictNonEmptyString = (value) => typeof value === 'string' && value.trim() === value && value.length > 0 && !hasControlCharacters(value);
|
|
40
|
+
const isPositiveSafeInteger = (value) => Number.isSafeInteger(value) && Number(value) > 0;
|
|
41
|
+
const isNonNegativeSafeInteger = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
42
|
+
const isSha256 = (value) => typeof value === 'string' && SHA256_PATTERN.test(value);
|
|
43
|
+
const allowedViewArtifactPaths = (artifacts) => Array.isArray(artifacts) ? artifacts.flatMap(artifact => allowedArtifactReferenceField(artifact, 'path')) : [];
|
|
44
|
+
const isSafePathSegment = (value) => isStrictNonEmptyString(value) && !/[\\/?#%]/.test(value) && value !== '.' && value !== '..';
|
|
45
|
+
const isStrictCanonicalIsoTimestamp = (value) => {
|
|
46
|
+
if (!isStrictNonEmptyString(value))
|
|
47
|
+
return false;
|
|
48
|
+
const timestamp = Date.parse(value);
|
|
49
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
|
|
50
|
+
};
|
|
51
|
+
const isEnforceableAzureOwnershipBinding = (value) => isEnforceableArtifactOwnershipBinding(value) && value.provider === 'azure';
|
|
52
|
+
const hasMatchingViewOwnership = (subscriptionId, ownership, revision, enforceable) => {
|
|
53
|
+
if (!isSafePathSegment(subscriptionId) || !isArtifactRevisionVector(revision))
|
|
54
|
+
return false;
|
|
55
|
+
if (!isArtifactOwnershipBinding(ownership))
|
|
56
|
+
return false;
|
|
57
|
+
if (enforceable && !isEnforceableArtifactOwnershipBinding(ownership))
|
|
58
|
+
return false;
|
|
59
|
+
if (ownership.provider !== 'azure' || ownership.accountId !== subscriptionId)
|
|
60
|
+
return false;
|
|
61
|
+
return ownership.ownershipEpochRevision === revision.ownershipEpochRevision;
|
|
62
|
+
};
|
|
63
|
+
const isViewArtifactDescriptor = (value, runId) => isRecord(value) &&
|
|
64
|
+
isStrictLogicalArtifactReference(value.path) &&
|
|
65
|
+
value.path.startsWith(`runs/${runId}/`) &&
|
|
66
|
+
value.path.length > `runs/${runId}/`.length &&
|
|
67
|
+
isStrictNonEmptyString(value.name) &&
|
|
68
|
+
value.mediaType === 'application/json' &&
|
|
69
|
+
typeof value.contentEncoding === 'string' &&
|
|
70
|
+
VIEW_CONTENT_ENCODINGS.has(value.contentEncoding) &&
|
|
71
|
+
isNonNegativeSafeInteger(value.byteLength) &&
|
|
72
|
+
isSha256(value.sha256);
|
|
73
|
+
const hasRequiredViewDependencies = (decision) => {
|
|
74
|
+
const dependencies = new Map(decision.dependencies.map(dependency => [dependency.name, dependency]));
|
|
75
|
+
const billing = dependencies.get('billing');
|
|
76
|
+
const economics = dependencies.get('economics');
|
|
77
|
+
if (!billing || !economics)
|
|
78
|
+
return false;
|
|
79
|
+
for (const dependency of [billing, economics]) {
|
|
80
|
+
if (dependency.publication === 'completed' && (!isStrictNonEmptyString(dependency.generationId) || !isSha256(dependency.digest))) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const completedDependencies = new Set(decision.dependencies.filter(dependency => dependency.publication === 'completed').map(dependency => dependency.name));
|
|
85
|
+
return decision.claims.every(claim => claim.publication !== 'completed' || claim.requiredDependencies.every(dependency => completedDependencies.has(dependency)));
|
|
86
|
+
};
|
|
87
|
+
/** Validates an evidence-aware completed portal or plugin generation manifest. */
|
|
88
|
+
export const isCompletedViewManifestV3 = (value) => {
|
|
89
|
+
if (!isRecord(value) || containsForbiddenArtifactControlData(value, allowedViewArtifactPaths(value.artifacts)))
|
|
90
|
+
return false;
|
|
91
|
+
if (value.schemaVersion !== 3 || value.status !== 'completed')
|
|
92
|
+
return false;
|
|
93
|
+
if (!isSafePathSegment(value.runId) || !hasMatchingViewOwnership(value.subscriptionId, value.ownership, value.revision, false))
|
|
94
|
+
return false;
|
|
95
|
+
if (!isRecord(value.artifactGeneration) ||
|
|
96
|
+
value.artifactGeneration.runId !== value.runId ||
|
|
97
|
+
!isStrictCanonicalIsoTimestamp(value.artifactGeneration.generatedAt)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(value.artifacts) || value.artifacts.length === 0)
|
|
101
|
+
return false;
|
|
102
|
+
if (!value.artifacts.every(artifact => isViewArtifactDescriptor(artifact, value.runId)))
|
|
103
|
+
return false;
|
|
104
|
+
if (new Set(value.artifacts.map(artifact => artifact.path)).size !== value.artifacts.length ||
|
|
105
|
+
new Set(value.artifacts.map(artifact => artifact.name)).size !== value.artifacts.length) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (!isPositiveSafeInteger(value.requestedArtifactCount) ||
|
|
109
|
+
value.requestedArtifactCount !== value.artifacts.length ||
|
|
110
|
+
!isNonNegativeSafeInteger(value.requestedResourceCount) ||
|
|
111
|
+
value.failedArtifactCount !== 0 ||
|
|
112
|
+
value.failedResourceCount !== 0 ||
|
|
113
|
+
!isSha256(value.compositeDependencyDigest) ||
|
|
114
|
+
!isStrictCanonicalIsoTimestamp(value.completedAt)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return isArtifactPublicationDecision(value.publicationDecision) && hasRequiredViewDependencies(value.publicationDecision);
|
|
118
|
+
};
|
|
119
|
+
const hasSameOwnership = (left, right) => left.provider === right.provider &&
|
|
120
|
+
left.tenantId === right.tenantId &&
|
|
121
|
+
left.companyId === right.companyId &&
|
|
122
|
+
left.cloudAccountId === right.cloudAccountId &&
|
|
123
|
+
left.accountId === right.accountId &&
|
|
124
|
+
left.ownershipEpochRevision === right.ownershipEpochRevision;
|
|
125
|
+
const hasSameRevision = (left, right) => left.ownershipEpochRevision === right.ownershipEpochRevision &&
|
|
126
|
+
left.sourceRevision === right.sourceRevision &&
|
|
127
|
+
left.policyRevision === right.policyRevision;
|
|
128
|
+
const isViewSetV2SurfaceReference = (value, surface, subscriptionId, ownership, revision, compositeDependencyDigest) => {
|
|
129
|
+
if (!isRecord(value) || !isSafePathSegment(value.runId))
|
|
130
|
+
return false;
|
|
131
|
+
const expectedManifestName = surface === 'portal' ? 'completed-view-manifest.json' : 'completed-plugin-generation.json';
|
|
132
|
+
if (value.manifestPath !== `runs/${value.runId}/${expectedManifestName}` || !isStrictLogicalArtifactReference(value.manifestPath))
|
|
133
|
+
return false;
|
|
134
|
+
if (!isEnforceableAzureOwnershipBinding(value.ownership) || !isArtifactRevisionVector(value.revision))
|
|
135
|
+
return false;
|
|
136
|
+
if (!hasMatchingViewOwnership(subscriptionId, value.ownership, value.revision, true))
|
|
137
|
+
return false;
|
|
138
|
+
if (!hasSameOwnership(ownership, value.ownership) || !hasSameRevision(revision, value.revision))
|
|
139
|
+
return false;
|
|
140
|
+
return (isSha256(value.manifestDigest) &&
|
|
141
|
+
value.compositeDependencyDigest === compositeDependencyDigest &&
|
|
142
|
+
isStrictCanonicalIsoTimestamp(value.completedAt));
|
|
143
|
+
};
|
|
144
|
+
const hasMatchingSurfaceDependency = (decision, name, surface) => {
|
|
145
|
+
const dependency = decision.dependencies.find(candidate => candidate.name === name);
|
|
146
|
+
return (dependency !== undefined &&
|
|
147
|
+
dependency.required &&
|
|
148
|
+
dependency.publication === 'completed' &&
|
|
149
|
+
dependency.generationId === surface.runId &&
|
|
150
|
+
dependency.digest === surface.manifestDigest);
|
|
151
|
+
};
|
|
152
|
+
/** Validates the promoted pointer for an evidence-enforced portal/plugin view pair. */
|
|
153
|
+
export const isCompletedAzureViewSetV2 = (value) => {
|
|
154
|
+
const allowedReferences = isRecord(value)
|
|
155
|
+
? [...allowedArtifactReferenceField(value.portal, 'manifestPath'), ...allowedArtifactReferenceField(value.plugin, 'manifestPath')]
|
|
156
|
+
: [];
|
|
157
|
+
if (!isRecord(value) || containsForbiddenArtifactControlData(value, allowedReferences))
|
|
158
|
+
return false;
|
|
159
|
+
if (value.schemaVersion !== 2 || value.status !== 'completed')
|
|
160
|
+
return false;
|
|
161
|
+
if (!isSafePathSegment(value.subscriptionId) ||
|
|
162
|
+
!isStrictNonEmptyString(value.publicationId) ||
|
|
163
|
+
!isEnforceableAzureOwnershipBinding(value.ownership) ||
|
|
164
|
+
!isArtifactRevisionVector(value.revision) ||
|
|
165
|
+
!hasMatchingViewOwnership(value.subscriptionId, value.ownership, value.revision, true)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (!isSha256(value.compositeDependencyDigest) || !isStrictCanonicalIsoTimestamp(value.completedAt))
|
|
169
|
+
return false;
|
|
170
|
+
if (!isViewSetV2SurfaceReference(value.portal, 'portal', value.subscriptionId, value.ownership, value.revision, value.compositeDependencyDigest) ||
|
|
171
|
+
!isViewSetV2SurfaceReference(value.plugin, 'plugin', value.subscriptionId, value.ownership, value.revision, value.compositeDependencyDigest)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const laterSurfaceCompletedAt = Date.parse(value.portal.completedAt) >= Date.parse(value.plugin.completedAt) ? value.portal.completedAt : value.plugin.completedAt;
|
|
175
|
+
if (value.completedAt !== laterSurfaceCompletedAt)
|
|
176
|
+
return false;
|
|
177
|
+
return (isArtifactPublicationDecision(value.publicationDecision) &&
|
|
178
|
+
value.publicationDecision.publication === 'completed' &&
|
|
179
|
+
hasMatchingSurfaceDependency(value.publicationDecision, 'portal', value.portal) &&
|
|
180
|
+
hasMatchingSurfaceDependency(value.publicationDecision, 'plugin', value.plugin));
|
|
181
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const FORBIDDEN_SENSITIVE_FIELDS = new Set([
|
|
2
|
+
'accountkey',
|
|
3
|
+
'accesskey',
|
|
4
|
+
'accesstoken',
|
|
5
|
+
'apikey',
|
|
6
|
+
'authorization',
|
|
7
|
+
'clientsecret',
|
|
8
|
+
'connectionstring',
|
|
9
|
+
'credential',
|
|
10
|
+
'credentials',
|
|
11
|
+
'password',
|
|
12
|
+
'refreshtoken',
|
|
13
|
+
'sastoken',
|
|
14
|
+
'secret',
|
|
15
|
+
'sharedaccesssignature',
|
|
16
|
+
'token',
|
|
17
|
+
]);
|
|
18
|
+
const PHYSICAL_REFERENCE_FIELDS = new Set([
|
|
19
|
+
'artifactpath',
|
|
20
|
+
'blobpath',
|
|
21
|
+
'bloburi',
|
|
22
|
+
'bloburl',
|
|
23
|
+
'containerpath',
|
|
24
|
+
'containeruri',
|
|
25
|
+
'containerurl',
|
|
26
|
+
'filepath',
|
|
27
|
+
'filesystempath',
|
|
28
|
+
'inputmanifestpath',
|
|
29
|
+
'manifestpath',
|
|
30
|
+
'outputmanifestpath',
|
|
31
|
+
'path',
|
|
32
|
+
'physicalpath',
|
|
33
|
+
'physicaluri',
|
|
34
|
+
'physicalurl',
|
|
35
|
+
'sourcepath',
|
|
36
|
+
'sourceuri',
|
|
37
|
+
'sourceurl',
|
|
38
|
+
'storagepath',
|
|
39
|
+
'storageuri',
|
|
40
|
+
'storageurl',
|
|
41
|
+
'uri',
|
|
42
|
+
'url',
|
|
43
|
+
]);
|
|
44
|
+
const PERCENT_ENCODED_BYTE_PATTERN = /%[0-9A-Fa-f]{2}/;
|
|
45
|
+
const URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
46
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
|
+
const hasControlCharacters = (value) => Array.from(value).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
48
|
+
const normalizeFieldName = (key) => key.replace(/[-_\s]/g, '').toLowerCase();
|
|
49
|
+
const hasDotTraversal = (value) => value.split(/[\\/]/).some(segment => segment === '.' || segment === '..');
|
|
50
|
+
const isForbiddenReferenceValue = (value, allowUriScheme = false) => hasControlCharacters(value) ||
|
|
51
|
+
PERCENT_ENCODED_BYTE_PATTERN.test(value) ||
|
|
52
|
+
(!allowUriScheme && URI_SCHEME_PATTERN.test(value)) ||
|
|
53
|
+
value.startsWith('/') ||
|
|
54
|
+
value.startsWith('\\\\') ||
|
|
55
|
+
/^[A-Za-z]:[\\/]/.test(value) ||
|
|
56
|
+
hasDotTraversal(value);
|
|
57
|
+
const isSafeAzureResourceId = (key, value) => {
|
|
58
|
+
if (key !== 'resourceId' || typeof value !== 'string' || hasControlCharacters(value) || value.includes('\\'))
|
|
59
|
+
return false;
|
|
60
|
+
const segments = value.split('/');
|
|
61
|
+
return (segments.length >= 3 &&
|
|
62
|
+
segments[0] === '' &&
|
|
63
|
+
segments[1].toLowerCase() === 'subscriptions' &&
|
|
64
|
+
segments
|
|
65
|
+
.slice(2)
|
|
66
|
+
.every(segment => segment.trim() === segment && segment.length > 0 && !/[?#%]/.test(segment) && segment !== '.' && segment !== '..'));
|
|
67
|
+
};
|
|
68
|
+
/** Marks a structurally validated schema field whose logical reference name is allowed. */
|
|
69
|
+
export const allowedArtifactReferenceField = (object, key) => isRecord(object) ? [{ object, key }] : [];
|
|
70
|
+
/** Marks a structurally validated identity field whose value is allowed by its owning schema. */
|
|
71
|
+
export const allowedArtifactIdentityField = (object, key) => isRecord(object) ? [{ object, key, allowUriScheme: true }] : [];
|
|
72
|
+
/** Rejects exact normalized sensitive fields and physical-reference control data recursively. */
|
|
73
|
+
export const containsForbiddenArtifactControlData = (value, allowedReferenceFields = []) => {
|
|
74
|
+
if (typeof value === 'string')
|
|
75
|
+
return isForbiddenReferenceValue(value);
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return value.some(child => containsForbiddenArtifactControlData(child, allowedReferenceFields));
|
|
78
|
+
if (!isRecord(value))
|
|
79
|
+
return false;
|
|
80
|
+
return Object.entries(value).some(([key, child]) => {
|
|
81
|
+
if (hasControlCharacters(key))
|
|
82
|
+
return true;
|
|
83
|
+
const normalizedKey = normalizeFieldName(key);
|
|
84
|
+
if (FORBIDDEN_SENSITIVE_FIELDS.has(normalizedKey))
|
|
85
|
+
return true;
|
|
86
|
+
const allowedField = allowedReferenceFields.find(field => field.object === value && field.key === key);
|
|
87
|
+
if (allowedField?.allowUriScheme && typeof child === 'string')
|
|
88
|
+
return isForbiddenReferenceValue(child, true);
|
|
89
|
+
if (PHYSICAL_REFERENCE_FIELDS.has(normalizedKey) && !allowedField)
|
|
90
|
+
return true;
|
|
91
|
+
if (isSafeAzureResourceId(key, child))
|
|
92
|
+
return false;
|
|
93
|
+
return containsForbiddenArtifactControlData(child, allowedReferenceFields);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { hasValidOptionalArtifactRevisionComponents, isStrictLogicalArtifactReference } from './artifactEvidenceValidation.js';
|
|
2
|
+
const SUPPORT_VERDICTS = new Set(['supported', 'unsupported', 'unknown']);
|
|
3
|
+
const APPLICABILITY_VERDICTS = new Set(['applicable', 'not-applicable', 'unknown']);
|
|
4
|
+
const ATTEMPT_OUTCOMES = new Set([
|
|
5
|
+
'not-attempted',
|
|
6
|
+
'queued',
|
|
7
|
+
'running',
|
|
8
|
+
'succeeded',
|
|
9
|
+
'failed',
|
|
10
|
+
'cancelled',
|
|
11
|
+
'timed-out',
|
|
12
|
+
'dead-lettered',
|
|
13
|
+
'superseded',
|
|
14
|
+
]);
|
|
15
|
+
const COVERAGE_VERDICTS = new Set(['complete', 'partial', 'none', 'unknown']);
|
|
16
|
+
const EMPTY_EVIDENCE_VERDICTS = new Set(['populated', 'complete-empty', 'not-observed', 'unknown']);
|
|
17
|
+
const FRESHNESS_VERDICTS = new Set(['current', 'stale', 'expired', 'unknown']);
|
|
18
|
+
const EVIDENCE_VERDICTS = new Set(['complete', 'partial', 'insufficient', 'conflicting']);
|
|
19
|
+
const PROCESSING_LIFECYCLES = new Set(['queued', 'running', 'succeeded', 'failed', 'cancelled', 'timed-out', 'dead-lettered', 'superseded']);
|
|
20
|
+
const PUBLICATION_VERDICTS = new Set(['completed', 'partial', 'suppressed', 'superseded', 'quarantined']);
|
|
21
|
+
const PROVIDERS = new Set(['azure', 'aws']);
|
|
22
|
+
const DATE_BASES = new Set(['utc', 'billing-calendar', 'company-local']);
|
|
23
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
24
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
26
|
+
const isNonNegativeInteger = (value) => Number.isInteger(value) && value >= 0;
|
|
27
|
+
const isPositiveInteger = (value) => Number.isInteger(value) && value > 0;
|
|
28
|
+
const isStringIn = (value, values) => typeof value === 'string' && values.has(value);
|
|
29
|
+
const isCanonicalIsoTimestamp = (value) => {
|
|
30
|
+
if (!isNonEmptyString(value))
|
|
31
|
+
return false;
|
|
32
|
+
const timestamp = Date.parse(value);
|
|
33
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
|
|
34
|
+
};
|
|
35
|
+
const isArtifactDecisionIssue = (value) => isRecord(value) &&
|
|
36
|
+
isNonEmptyString(value.code) &&
|
|
37
|
+
typeof value.blocking === 'boolean' &&
|
|
38
|
+
(value.dependency === undefined || isNonEmptyString(value.dependency));
|
|
39
|
+
const isObservedRange = (value) => {
|
|
40
|
+
if (!isRecord(value) ||
|
|
41
|
+
!isCanonicalIsoTimestamp(value.fromInclusive) ||
|
|
42
|
+
!isCanonicalIsoTimestamp(value.throughExclusive) ||
|
|
43
|
+
Date.parse(value.throughExclusive) <= Date.parse(value.fromInclusive) ||
|
|
44
|
+
!isStringIn(value.dateBasis, DATE_BASES)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return value.timeZone === undefined || isNonEmptyString(value.timeZone);
|
|
48
|
+
};
|
|
49
|
+
const isArtifactDependencyDescriptor = (value) => {
|
|
50
|
+
if (!isRecord(value) ||
|
|
51
|
+
!isNonEmptyString(value.name) ||
|
|
52
|
+
typeof value.required !== 'boolean' ||
|
|
53
|
+
!isStringIn(value.support, SUPPORT_VERDICTS) ||
|
|
54
|
+
!isStringIn(value.applicability, APPLICABILITY_VERDICTS) ||
|
|
55
|
+
!isStringIn(value.attempt, ATTEMPT_OUTCOMES) ||
|
|
56
|
+
!isStringIn(value.coverage, COVERAGE_VERDICTS) ||
|
|
57
|
+
!isStringIn(value.emptyEvidence, EMPTY_EVIDENCE_VERDICTS) ||
|
|
58
|
+
!isStringIn(value.freshness, FRESHNESS_VERDICTS) ||
|
|
59
|
+
!isStringIn(value.evidence, EVIDENCE_VERDICTS) ||
|
|
60
|
+
!isStringIn(value.publication, PUBLICATION_VERDICTS)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (value.generationId !== undefined && !isNonEmptyString(value.generationId))
|
|
64
|
+
return false;
|
|
65
|
+
if (value.digest !== undefined && (typeof value.digest !== 'string' || !SHA256_PATTERN.test(value.digest)))
|
|
66
|
+
return false;
|
|
67
|
+
if (!hasValidOptionalArtifactRevisionComponents(value))
|
|
68
|
+
return false;
|
|
69
|
+
if (value.observedRange !== undefined && !isObservedRange(value.observedRange))
|
|
70
|
+
return false;
|
|
71
|
+
if (value.completeThrough !== undefined && !isCanonicalIsoTimestamp(value.completeThrough))
|
|
72
|
+
return false;
|
|
73
|
+
if (value.reasonCode !== undefined && !isNonEmptyString(value.reasonCode))
|
|
74
|
+
return false;
|
|
75
|
+
if (value.acceptedRowCount !== undefined && !isNonNegativeInteger(value.acceptedRowCount))
|
|
76
|
+
return false;
|
|
77
|
+
if (value.emptyEvidence === 'complete-empty') {
|
|
78
|
+
if (value.attempt !== 'succeeded' ||
|
|
79
|
+
value.coverage !== 'complete' ||
|
|
80
|
+
value.acceptedRowCount !== 0 ||
|
|
81
|
+
!isStrictLogicalArtifactReference(value.emptyProofRef)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (value.emptyProofRef !== undefined) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (value.publication === 'completed') {
|
|
89
|
+
return (value.support === 'supported' &&
|
|
90
|
+
value.applicability === 'applicable' &&
|
|
91
|
+
value.attempt === 'succeeded' &&
|
|
92
|
+
value.coverage === 'complete' &&
|
|
93
|
+
(value.emptyEvidence === 'populated' || value.emptyEvidence === 'complete-empty') &&
|
|
94
|
+
value.evidence === 'complete');
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
const isArtifactClaimDependencyDecision = (value) => {
|
|
99
|
+
if (!isRecord(value) ||
|
|
100
|
+
!isNonEmptyString(value.claimId) ||
|
|
101
|
+
!Array.isArray(value.sectionPaths) ||
|
|
102
|
+
!value.sectionPaths.every(isNonEmptyString) ||
|
|
103
|
+
!Array.isArray(value.requiredDependencies) ||
|
|
104
|
+
!value.requiredDependencies.every(isNonEmptyString) ||
|
|
105
|
+
!isStringIn(value.evidence, EVIDENCE_VERDICTS) ||
|
|
106
|
+
!isStringIn(value.publication, PUBLICATION_VERDICTS) ||
|
|
107
|
+
!Array.isArray(value.issues) ||
|
|
108
|
+
!value.issues.every(isArtifactDecisionIssue)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return value.publication !== 'completed' || (value.evidence === 'complete' && !value.issues.some(issue => issue.blocking));
|
|
112
|
+
};
|
|
113
|
+
const hasUniqueValues = (values) => new Set(values).size === values.length;
|
|
114
|
+
/** Dependency-free runtime rejection boundary for provider-neutral publication decisions. */
|
|
115
|
+
export const isArtifactPublicationDecision = (value) => {
|
|
116
|
+
if (!isRecord(value) ||
|
|
117
|
+
!isStringIn(value.processing, PROCESSING_LIFECYCLES) ||
|
|
118
|
+
!isStringIn(value.evidence, EVIDENCE_VERDICTS) ||
|
|
119
|
+
!isStringIn(value.publication, PUBLICATION_VERDICTS) ||
|
|
120
|
+
!Array.isArray(value.dependencies) ||
|
|
121
|
+
!value.dependencies.every(isArtifactDependencyDescriptor) ||
|
|
122
|
+
!Array.isArray(value.claims) ||
|
|
123
|
+
!value.claims.every(isArtifactClaimDependencyDecision) ||
|
|
124
|
+
!Array.isArray(value.issues) ||
|
|
125
|
+
!value.issues.every(isArtifactDecisionIssue)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const dependencyNames = value.dependencies.map(dependency => dependency.name);
|
|
129
|
+
const claimIds = value.claims.map(claim => claim.claimId);
|
|
130
|
+
if (!hasUniqueValues(dependencyNames) || !hasUniqueValues(claimIds))
|
|
131
|
+
return false;
|
|
132
|
+
if (!value.claims.every(claim => hasUniqueValues(claim.requiredDependencies)))
|
|
133
|
+
return false;
|
|
134
|
+
if (value.publication !== 'completed')
|
|
135
|
+
return true;
|
|
136
|
+
const completedDependencies = new Set(value.dependencies.filter(dependency => dependency.publication === 'completed').map(dependency => dependency.name));
|
|
137
|
+
return (value.processing === 'succeeded' &&
|
|
138
|
+
(value.evidence === 'complete' || value.evidence === 'partial') &&
|
|
139
|
+
value.dependencies.every(dependency => !dependency.required || dependency.publication === 'completed') &&
|
|
140
|
+
value.claims.every(claim => claim.publication === 'completed' && claim.requiredDependencies.every(dependency => completedDependencies.has(dependency))) &&
|
|
141
|
+
!value.issues.some(issue => issue.blocking));
|
|
142
|
+
};
|
|
143
|
+
/** Checks the provider-neutral ownership shape used by observe- and enforce-mode artifacts. */
|
|
144
|
+
export const isArtifactOwnershipBinding = (value) => isRecord(value) &&
|
|
145
|
+
isStringIn(value.provider, PROVIDERS) &&
|
|
146
|
+
isNonEmptyString(value.tenantId) &&
|
|
147
|
+
isNonEmptyString(value.companyId) &&
|
|
148
|
+
isNonEmptyString(value.cloudAccountId) &&
|
|
149
|
+
isNonEmptyString(value.accountId) &&
|
|
150
|
+
(value.ownershipEpochRevision === undefined || isPositiveInteger(value.ownershipEpochRevision));
|
|
151
|
+
/** Checks that an ownership binding is valid for enforce-mode publication. */
|
|
152
|
+
export const isEnforceableArtifactOwnershipBinding = (value) => isArtifactOwnershipBinding(value) && value.ownershipEpochRevision !== undefined;
|
|
153
|
+
/**
|
|
154
|
+
* Compares ownership first, then source and policy revisions component-wise.
|
|
155
|
+
* Callers must validate both vectors before comparison; this function deliberately compares the supplied values without adding a second validation policy.
|
|
156
|
+
*/
|
|
157
|
+
export const compareArtifactRevisionVector = (left, right) => {
|
|
158
|
+
if (left.ownershipEpochRevision === undefined || right.ownershipEpochRevision === undefined)
|
|
159
|
+
return 'unenforceable';
|
|
160
|
+
if (left.ownershipEpochRevision > right.ownershipEpochRevision)
|
|
161
|
+
return 'newer-ownership';
|
|
162
|
+
if (left.ownershipEpochRevision < right.ownershipEpochRevision)
|
|
163
|
+
return 'older-ownership';
|
|
164
|
+
const sourceComparison = Math.sign(left.sourceRevision - right.sourceRevision);
|
|
165
|
+
const policyComparison = Math.sign(left.policyRevision - right.policyRevision);
|
|
166
|
+
if (sourceComparison === 0 && policyComparison === 0)
|
|
167
|
+
return 'equal';
|
|
168
|
+
if (sourceComparison >= 0 && policyComparison >= 0)
|
|
169
|
+
return 'newer';
|
|
170
|
+
if (sourceComparison <= 0 && policyComparison <= 0)
|
|
171
|
+
return 'older';
|
|
172
|
+
return 'incomparable';
|
|
173
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const hasControlCharacters = (value) => Array.from(value).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
2
|
+
/** Validates a generation-relative logical artifact reference without accepting physical-path encodings. */
|
|
3
|
+
export const isStrictLogicalArtifactReference = (value) => {
|
|
4
|
+
if (typeof value !== 'string' || value.trim() !== value || value.length === 0 || hasControlCharacters(value))
|
|
5
|
+
return false;
|
|
6
|
+
if (value.startsWith('/') || value.includes('\\') || value.includes('%') || value.includes('?') || value.includes('#'))
|
|
7
|
+
return false;
|
|
8
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || /^[A-Za-z]:[\\/]/.test(value))
|
|
9
|
+
return false;
|
|
10
|
+
const segments = value.split('/');
|
|
11
|
+
return segments.length >= 2 && segments.every(segment => segment.length > 0 && segment !== '.' && segment !== '..');
|
|
12
|
+
};
|
|
13
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
const isPositiveSafeInteger = (value) => Number.isSafeInteger(value) && Number(value) > 0;
|
|
15
|
+
/** Validates the positive source/policy revision vector shared by evidence producers and consumers. */
|
|
16
|
+
export const isArtifactRevisionVector = (value) => isRecord(value) &&
|
|
17
|
+
isPositiveSafeInteger(value.sourceRevision) &&
|
|
18
|
+
isPositiveSafeInteger(value.policyRevision) &&
|
|
19
|
+
(value.ownershipEpochRevision === undefined || isPositiveSafeInteger(value.ownershipEpochRevision));
|
|
20
|
+
/** Validates optional dependency revision components using the shared revision-vector rules. */
|
|
21
|
+
export const hasValidOptionalArtifactRevisionComponents = (value) => isArtifactRevisionVector({
|
|
22
|
+
sourceRevision: value.sourceRevision === undefined ? 1 : value.sourceRevision,
|
|
23
|
+
policyRevision: value.policyRevision === undefined ? 1 : value.policyRevision,
|
|
24
|
+
});
|