@spottoai/types-package 1.0.2-beta.354 → 1.0.2-beta.356
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 +11 -0
- package/dist/aws/commitments-planning.d.ts +3 -0
- package/dist/aws/commitments-planning.d.ts.map +1 -0
- package/dist/aws/commitments-planning.js +6 -0
- package/dist/aws/commitments-planning.js.map +1 -0
- package/dist/aws/relationships.d.ts +4 -0
- package/dist/aws/relationships.d.ts.map +1 -0
- package/dist/aws/relationships.js +6 -0
- package/dist/aws/relationships.js.map +1 -0
- package/dist/esm/accounts/accounts.js +115 -0
- package/dist/esm/accounts/azureSpSetup.js +1 -0
- package/dist/esm/accounts/azureSpSetupDurable.js +1 -0
- package/dist/esm/accounts/index.js +5 -0
- package/dist/esm/accounts/readPermissions.js +198 -0
- package/dist/esm/accounts/validation.js +1 -0
- package/dist/esm/accounts/writePermissions.js +43 -0
- package/dist/esm/ai/index.js +2 -0
- package/dist/esm/assessments/artifact.js +1 -0
- package/dist/esm/assessments/catalogue.js +12 -0
- package/dist/esm/assessments/common.js +1 -0
- package/dist/esm/assessments/evidence.js +1 -0
- package/dist/esm/assessments/export.js +1 -0
- package/dist/esm/assessments/index.js +10 -0
- package/dist/esm/assessments/queue.js +2 -0
- package/dist/esm/assessments/review.js +1 -0
- package/dist/esm/assessments/run.js +1 -0
- package/dist/esm/assessments/sow.js +1 -0
- package/dist/esm/assessments/status.js +23 -0
- package/dist/esm/aws/commitments-planning.js +1 -0
- package/dist/esm/aws/commitmentsPlanningValidation.js +121 -0
- package/dist/esm/aws/estates.js +21 -0
- package/dist/esm/aws/index.js +18 -0
- package/dist/esm/aws/onboarding.js +1 -0
- package/dist/esm/aws/pluginPublicArtifactBodyValidation.js +217 -0
- package/dist/esm/aws/pluginPublicArtifactEvidence.js +1 -0
- package/dist/esm/aws/pluginPublicArtifactManifestValidation.js +255 -0
- package/dist/esm/aws/pluginPublicArtifactSectionValidation.js +464 -0
- package/dist/esm/aws/pluginPublicArtifactValidation.js +2 -0
- package/dist/esm/aws/pluginPublicArtifactValidationHelpers.js +216 -0
- package/dist/esm/aws/pluginPublicArtifacts.js +109 -0
- package/dist/esm/aws/portalCommitmentsPlanningPublicArtifactValidation.js +509 -0
- package/dist/esm/aws/portalCommitmentsPlanningPublicArtifacts.js +1 -0
- package/dist/esm/aws/portalPublicArtifactCurrentValidation.js +328 -0
- package/dist/esm/aws/portalPublicArtifactEvidenceValidation.js +294 -0
- package/dist/esm/aws/portalPublicArtifactHistoryAiValidation.js +368 -0
- package/dist/esm/aws/portalPublicArtifactHistoryTypes.js +40 -0
- package/dist/esm/aws/portalPublicArtifactNestedEvidence.js +1 -0
- package/dist/esm/aws/portalPublicArtifactNestedEvidenceValidation.js +397 -0
- package/dist/esm/aws/portalPublicArtifactValidation.js +2 -0
- package/dist/esm/aws/portalPublicArtifactValidationCommon.js +139 -0
- package/dist/esm/aws/portalPublicArtifacts.js +73 -0
- package/dist/esm/aws/portalRelationshipPublicArtifactValidation.js +334 -0
- package/dist/esm/aws/portalRelationshipPublicArtifacts.js +59 -0
- package/dist/esm/aws/portalRelationshipResourceNodeTypes.js +1 -0
- package/dist/esm/aws/portalRelationshipResourceNodeValidation.js +368 -0
- package/dist/esm/aws/publicArtifacts.js +13 -0
- package/dist/esm/aws/relationships.js +1 -0
- package/dist/esm/aws/requests.js +19 -0
- package/dist/esm/azure/activityLogs.js +2 -0
- package/dist/esm/azure/advisorScore.js +1 -0
- package/dist/esm/azure/benefits.js +1 -0
- package/dist/esm/azure/billingGeneration.js +1 -0
- package/dist/esm/azure/billingPlots.js +4 -0
- package/dist/esm/azure/budgets.js +1 -0
- package/dist/esm/azure/commitmentsPlanning.js +1 -0
- package/dist/esm/azure/common.js +1 -0
- package/dist/esm/azure/configs.js +1 -0
- package/dist/esm/azure/costComposition.js +1 -0
- package/dist/esm/azure/dataProtection.js +5 -0
- package/dist/esm/azure/governance.js +14 -0
- package/dist/esm/azure/licensing.js +1 -0
- package/dist/esm/azure/metrics.js +1 -0
- package/dist/esm/azure/payloads.js +1 -0
- package/dist/esm/azure/portalArtifacts.js +1 -0
- package/dist/esm/azure/prices.js +1 -0
- package/dist/esm/azure/publicIpAddresses.js +2 -0
- package/dist/esm/azure/recommendationAudit.js +16 -0
- package/dist/esm/azure/recommendationState.js +1 -0
- package/dist/esm/azure/recommendationTracks.js +40 -0
- package/dist/esm/azure/recommendationWorkflow.js +13 -0
- package/dist/esm/azure/recommendations.js +10 -0
- package/dist/esm/azure/regulatoryCompliance.js +6 -0
- package/dist/esm/azure/relationships.js +2 -0
- package/dist/esm/azure/reportingTemplates.js +1 -0
- package/dist/esm/azure/reports.js +12 -0
- package/dist/esm/azure/resourceHealth.js +12 -0
- package/dist/esm/azure/resourceOptimization.js +1 -0
- package/dist/esm/azure/resources.js +1 -0
- package/dist/esm/azure/reviewChecklist.js +1 -0
- package/dist/esm/azure/secureScore.js +1 -0
- package/dist/esm/azure/security.js +1 -0
- package/dist/esm/azure/serviceRetirement.js +1 -0
- package/dist/esm/azure/serviceRetirementState.js +1 -0
- package/dist/esm/azure/serviceRetirementWorkManagement.js +1 -0
- package/dist/esm/azure/storage-inventory.js +1 -0
- package/dist/esm/azure/subscriptions.js +1 -0
- package/dist/esm/azure/views.js +32 -0
- package/dist/esm/azure/workManagement.js +4 -0
- package/dist/esm/common/artifactGeneration.js +1 -0
- package/dist/esm/common/capabilityPassport.js +214 -0
- package/dist/esm/common/index.js +7 -0
- package/dist/esm/common/pagination.js +1 -0
- package/dist/esm/common/provider.js +11 -0
- package/dist/esm/common/providerSyncProgress.js +1 -0
- package/dist/esm/common/relationships.js +1 -0
- package/dist/esm/common/syncProgress.js +1 -0
- package/dist/esm/company/billingAccount.js +1 -0
- package/dist/esm/company/company.js +1 -0
- package/dist/esm/company/companyHierarchy.js +1 -0
- package/dist/esm/company/companyNotes.js +38 -0
- package/dist/esm/company/customProperties.js +1 -0
- package/dist/esm/company/index.js +6 -0
- package/dist/esm/company/notification.js +43 -0
- package/dist/esm/company/survey.js +1 -0
- package/dist/esm/customProperties/customProperties.js +1 -0
- package/dist/esm/customProperties/index.js +1 -0
- package/dist/esm/entries/aws.js +3 -0
- package/dist/esm/entries/commitments-planning.js +3 -0
- package/dist/esm/entries/provider.js +3 -0
- package/dist/esm/entries/recommendations.js +3 -0
- package/dist/esm/entries/relationships.js +3 -0
- package/dist/esm/entries/root.js +3 -0
- package/dist/esm/events/actionGroups.js +1 -0
- package/dist/esm/events/alertDefinitionRun.js +1 -0
- package/dist/esm/events/baseAlert.js +1 -0
- package/dist/esm/events/cost.js +1 -0
- package/dist/esm/events/costAnomalies.js +1 -0
- package/dist/esm/events/costViews.js +1 -0
- package/dist/esm/events/eventStatus.js +1 -0
- package/dist/esm/events/index.js +8 -0
- package/dist/esm/events/quickAlerts.js +2 -0
- package/dist/esm/features-and-permissions/access.js +1 -0
- package/dist/esm/features-and-permissions/index.js +1 -0
- package/dist/esm/feedbacks/feedback.js +1 -0
- package/dist/esm/identity/apikey.js +1 -0
- package/dist/esm/identity/index.js +1 -0
- package/dist/esm/index.js +54 -0
- package/dist/esm/index.vite.js +48 -0
- package/dist/esm/integrations/autotask.js +1 -0
- package/dist/esm/integrations/azureDevOps.js +1 -0
- package/dist/esm/integrations/github.js +1 -0
- package/dist/esm/integrations/halo.js +1 -0
- package/dist/esm/integrations/index.js +5 -0
- package/dist/esm/integrations/integrationTickets.js +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/scheduler/index.js +1 -0
- package/dist/esm/scheduler/scheduler.js +1 -0
- package/dist/esm/tags/index.js +1 -0
- package/dist/esm/tags/tags.js +6 -0
- package/dist/esm/unknown/services.js +1 -0
- package/dist/esm/users/companyUser.js +2 -0
- package/dist/esm/users/index.js +2 -0
- package/dist/esm/users/user.js +1 -0
- package/package.json +23 -6
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { asRecord, assertExactKeys, assertUnique, assertValue, finiteNumber, isoTimestamp, nonNegativeInteger, requiredBoolean, requiredEnum, requiredString, stringArray, } from './portalPublicArtifactValidationCommon.js';
|
|
2
|
+
import { validateRecommendationActionHealth, validateRecommendationSourceEvidence } from './portalPublicArtifactEvidenceValidation.js';
|
|
3
|
+
const ACTION_STATUSES = ['accepted', 'completed', 'failed', 'retryable-failure', 'skipped'];
|
|
4
|
+
const ACTION_TYPES = ['DeleteDBInstance', 'DeleteNatGateway', 'ModifyVolume', 'ModifyStorage', 'Rightsize'];
|
|
5
|
+
const PROVIDER_ACTIONS = [
|
|
6
|
+
'rds-modify-db-cluster-storage-type',
|
|
7
|
+
'autoscaling-update-group-instance-refresh',
|
|
8
|
+
'ec2-modify-volume',
|
|
9
|
+
'ec2-stop-modify-start-instance-type',
|
|
10
|
+
'ecs-register-task-definition-update-service',
|
|
11
|
+
'ec2-delete-nat-gateway',
|
|
12
|
+
'lambda-update-function-configuration',
|
|
13
|
+
'rds-delete-db-instance',
|
|
14
|
+
'rds-modify-db-instance',
|
|
15
|
+
];
|
|
16
|
+
const SKIP_REASONS = [
|
|
17
|
+
'recommendation-state-not-found',
|
|
18
|
+
'source-recommendation-missing',
|
|
19
|
+
'source-recommendation-not-found',
|
|
20
|
+
'published-version-immutable',
|
|
21
|
+
'provider-preflight-conflict',
|
|
22
|
+
'provider-preflight-unsupported',
|
|
23
|
+
'unsupported-recommendation-shape',
|
|
24
|
+
'no-effective-provider-change',
|
|
25
|
+
];
|
|
26
|
+
const RECOMMENDATION_SOURCES = [
|
|
27
|
+
'custom',
|
|
28
|
+
'compute-optimizer',
|
|
29
|
+
'cost-optimization-hub',
|
|
30
|
+
'resilience-hub',
|
|
31
|
+
'security-hub',
|
|
32
|
+
'trusted-advisor',
|
|
33
|
+
'well-architected',
|
|
34
|
+
];
|
|
35
|
+
export function validateResourceBillingEvidence(value, field) {
|
|
36
|
+
const billing = asRecord(value, field);
|
|
37
|
+
assertExactKeys(billing, ['costProvenance', 'matchedExpenseCount', 'totalsByCurrency', 'costViewByCurrency', 'commitmentSpend', 'benefitsCoverage', 'coverageWarning'], field);
|
|
38
|
+
const provenance = asRecord(billing.costProvenance, `${field}.costProvenance`);
|
|
39
|
+
assertExactKeys(provenance, ['costSource', 'costSourceConfidence'], `${field}.costProvenance`);
|
|
40
|
+
assertValue(provenance.costSource, 'billing', `${field}.costProvenance.costSource`);
|
|
41
|
+
assertValue(provenance.costSourceConfidence, 'high', `${field}.costProvenance.costSourceConfidence`);
|
|
42
|
+
nonNegativeInteger(billing.matchedExpenseCount, `${field}.matchedExpenseCount`);
|
|
43
|
+
validateCostTotals(billing.totalsByCurrency, `${field}.totalsByCurrency`);
|
|
44
|
+
validateCostViews(billing.costViewByCurrency, `${field}.costViewByCurrency`);
|
|
45
|
+
validateCommitmentSpend(billing.commitmentSpend, `${field}.commitmentSpend`);
|
|
46
|
+
if (billing.benefitsCoverage !== undefined)
|
|
47
|
+
validateBenefitCoverage(billing.benefitsCoverage, `${field}.benefitsCoverage`);
|
|
48
|
+
if (billing.coverageWarning !== undefined)
|
|
49
|
+
validateBenefitWarning(billing.coverageWarning, `${field}.coverageWarning`);
|
|
50
|
+
}
|
|
51
|
+
export function validateRecommendationActionability(value, field) {
|
|
52
|
+
const actionability = asRecord(value, field);
|
|
53
|
+
assertExactKeys(actionability, ['latestActionStableMaterializationKey', 'latestActionExecution', 'savingsVerification'], field);
|
|
54
|
+
requiredString(actionability.latestActionStableMaterializationKey, `${field}.latestActionStableMaterializationKey`);
|
|
55
|
+
validateLatestAction(actionability.latestActionExecution, `${field}.latestActionExecution`);
|
|
56
|
+
if (actionability.savingsVerification !== undefined)
|
|
57
|
+
validateSavingsVerification(actionability.savingsVerification, `${field}.savingsVerification`);
|
|
58
|
+
}
|
|
59
|
+
export function validateTemplateProvenanceSources(value, field) {
|
|
60
|
+
if (!Array.isArray(value))
|
|
61
|
+
throw new Error(`${field} must be an array.`);
|
|
62
|
+
const identities = [];
|
|
63
|
+
value.forEach((entry, index) => {
|
|
64
|
+
const itemField = `${field}[${index}]`;
|
|
65
|
+
const source = asRecord(entry, itemField);
|
|
66
|
+
assertExactKeys(source, [
|
|
67
|
+
'recommendationCode',
|
|
68
|
+
'templateId',
|
|
69
|
+
'relativePath',
|
|
70
|
+
'source',
|
|
71
|
+
'format',
|
|
72
|
+
'confidencePercentage',
|
|
73
|
+
'confidenceReason',
|
|
74
|
+
'primarySourceUrl',
|
|
75
|
+
'firstPartyValidationSourceCount',
|
|
76
|
+
'communitySourceCount',
|
|
77
|
+
], itemField);
|
|
78
|
+
const recommendationCode = requiredString(source.recommendationCode, `${itemField}.recommendationCode`);
|
|
79
|
+
const relativePath = requiredString(source.relativePath, `${itemField}.relativePath`);
|
|
80
|
+
identities.push(`${recommendationCode}|${relativePath}`);
|
|
81
|
+
requiredString(source.templateId, `${itemField}.templateId`);
|
|
82
|
+
requiredEnum(source.source, ['internal', 'external'], `${itemField}.source`);
|
|
83
|
+
requiredEnum(source.format, ['canonical-v1', 'external-catalog-v1'], `${itemField}.format`);
|
|
84
|
+
if (source.confidencePercentage !== undefined)
|
|
85
|
+
percentage(source.confidencePercentage, `${itemField}.confidencePercentage`);
|
|
86
|
+
if (source.confidenceReason !== undefined)
|
|
87
|
+
requiredString(source.confidenceReason, `${itemField}.confidenceReason`);
|
|
88
|
+
if (source.primarySourceUrl !== undefined)
|
|
89
|
+
httpsUrl(source.primarySourceUrl, `${itemField}.primarySourceUrl`);
|
|
90
|
+
nonNegativeInteger(source.firstPartyValidationSourceCount, `${itemField}.firstPartyValidationSourceCount`);
|
|
91
|
+
nonNegativeInteger(source.communitySourceCount, `${itemField}.communitySourceCount`);
|
|
92
|
+
});
|
|
93
|
+
assertUnique(identities, field);
|
|
94
|
+
}
|
|
95
|
+
export function validateRecommendationPosture(value, field) {
|
|
96
|
+
const posture = asRecord(value, field);
|
|
97
|
+
assertExactKeys(posture, ['sourceCount', 'sources'], field);
|
|
98
|
+
const sourceCount = nonNegativeInteger(posture.sourceCount, `${field}.sourceCount`);
|
|
99
|
+
if (!Array.isArray(posture.sources) || posture.sources.length !== sourceCount)
|
|
100
|
+
throw new Error(`${field}.sources length must equal sourceCount.`);
|
|
101
|
+
const sources = [];
|
|
102
|
+
posture.sources.forEach((entry, index) => {
|
|
103
|
+
const itemField = `${field}.sources[${index}]`;
|
|
104
|
+
const source = asRecord(entry, itemField);
|
|
105
|
+
assertExactKeys(source, ['source', 'sourceEvidence', 'actionHealth'], itemField);
|
|
106
|
+
sources.push(requiredEnum(source.source, RECOMMENDATION_SOURCES, `${itemField}.source`));
|
|
107
|
+
if (source.sourceEvidence !== undefined)
|
|
108
|
+
validateRecommendationSourceEvidence(source.sourceEvidence, `${itemField}.sourceEvidence`);
|
|
109
|
+
if (source.actionHealth !== undefined)
|
|
110
|
+
validateRecommendationActionHealth(source.actionHealth, `${itemField}.actionHealth`);
|
|
111
|
+
});
|
|
112
|
+
assertUnique(sources, `${field}.sources`);
|
|
113
|
+
}
|
|
114
|
+
export function validateSavingsByCurrency(value, field) {
|
|
115
|
+
if (!Array.isArray(value))
|
|
116
|
+
throw new Error(`${field} must be an array.`);
|
|
117
|
+
const currencies = [];
|
|
118
|
+
value.forEach((entry, index) => {
|
|
119
|
+
const itemField = `${field}[${index}]`;
|
|
120
|
+
const item = asRecord(entry, itemField);
|
|
121
|
+
assertExactKeys(item, ['currencyCode', 'recommendationCount', 'totalEstimatedMonthlySavings'], itemField);
|
|
122
|
+
currencies.push(requiredString(item.currencyCode, `${itemField}.currencyCode`));
|
|
123
|
+
nonNegativeInteger(item.recommendationCount, `${itemField}.recommendationCount`);
|
|
124
|
+
finiteNumber(item.totalEstimatedMonthlySavings, `${itemField}.totalEstimatedMonthlySavings`);
|
|
125
|
+
});
|
|
126
|
+
assertUnique(currencies, field);
|
|
127
|
+
}
|
|
128
|
+
function validateLatestAction(value, field) {
|
|
129
|
+
const action = asRecord(value, field);
|
|
130
|
+
const keys = [
|
|
131
|
+
'status',
|
|
132
|
+
'recordedAt',
|
|
133
|
+
'requestId',
|
|
134
|
+
'requestedAt',
|
|
135
|
+
'requestedBy',
|
|
136
|
+
'actionType',
|
|
137
|
+
'providerAction',
|
|
138
|
+
'completionMode',
|
|
139
|
+
'acceptedAt',
|
|
140
|
+
'completedAt',
|
|
141
|
+
'failedAt',
|
|
142
|
+
'providerRequestId',
|
|
143
|
+
'currentResourceSummary',
|
|
144
|
+
'recommendedResourceSummary',
|
|
145
|
+
'sourceTitle',
|
|
146
|
+
'estimatedMonthlySavings',
|
|
147
|
+
'estimatedSavingsPercentage',
|
|
148
|
+
'currencyCode',
|
|
149
|
+
'message',
|
|
150
|
+
'skipReason',
|
|
151
|
+
'implementationChanged',
|
|
152
|
+
'implementationImplemented',
|
|
153
|
+
'implementationPreviouslyImplemented',
|
|
154
|
+
];
|
|
155
|
+
assertExactKeys(action, keys, field);
|
|
156
|
+
requiredEnum(action.status, ACTION_STATUSES, `${field}.status`);
|
|
157
|
+
const recordedAt = isoTimestamp(action.recordedAt, `${field}.recordedAt`);
|
|
158
|
+
const requestedAt = isoTimestamp(action.requestedAt, `${field}.requestedAt`);
|
|
159
|
+
if (Date.parse(requestedAt) > Date.parse(recordedAt))
|
|
160
|
+
throw new Error(`${field}.requestedAt must not exceed recordedAt.`);
|
|
161
|
+
requiredString(action.requestId, `${field}.requestId`);
|
|
162
|
+
assertValue(action.requestedBy, 'manual', `${field}.requestedBy`);
|
|
163
|
+
requiredEnum(action.actionType, ACTION_TYPES, `${field}.actionType`);
|
|
164
|
+
requiredEnum(action.providerAction, PROVIDER_ACTIONS, `${field}.providerAction`);
|
|
165
|
+
assertValue(action.completionMode, 'provider-poll', `${field}.completionMode`);
|
|
166
|
+
['acceptedAt', 'completedAt', 'failedAt'].forEach(key => {
|
|
167
|
+
if (action[key] !== undefined)
|
|
168
|
+
isoTimestamp(action[key], `${field}.${key}`);
|
|
169
|
+
});
|
|
170
|
+
['providerRequestId', 'currentResourceSummary', 'recommendedResourceSummary', 'sourceTitle', 'currencyCode', 'message'].forEach(key => {
|
|
171
|
+
if (action[key] !== undefined)
|
|
172
|
+
requiredString(action[key], `${field}.${key}`);
|
|
173
|
+
});
|
|
174
|
+
['estimatedMonthlySavings', 'estimatedSavingsPercentage'].forEach(key => {
|
|
175
|
+
if (action[key] !== undefined)
|
|
176
|
+
finiteNumber(action[key], `${field}.${key}`);
|
|
177
|
+
});
|
|
178
|
+
if (action.skipReason !== undefined)
|
|
179
|
+
requiredEnum(action.skipReason, SKIP_REASONS, `${field}.skipReason`);
|
|
180
|
+
['implementationChanged', 'implementationImplemented', 'implementationPreviouslyImplemented'].forEach(key => {
|
|
181
|
+
if (action[key] !== undefined)
|
|
182
|
+
requiredBoolean(action[key], `${field}.${key}`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function validateSavingsVerification(value, field) {
|
|
186
|
+
const verification = asRecord(value, field);
|
|
187
|
+
const required = [
|
|
188
|
+
'verificationStatus',
|
|
189
|
+
'artifactType',
|
|
190
|
+
'actionRequestId',
|
|
191
|
+
'actionCompletedAt',
|
|
192
|
+
'postActionEvidenceStatus',
|
|
193
|
+
'postActionEvidenceReason',
|
|
194
|
+
];
|
|
195
|
+
const optional = [
|
|
196
|
+
'postActionArtifactGeneratedAt',
|
|
197
|
+
'postActionRefreshRequestId',
|
|
198
|
+
'postActionPendingRefreshRequestedAt',
|
|
199
|
+
'postActionPendingRefreshUpdatedAt',
|
|
200
|
+
'postActionPendingRefreshSummary',
|
|
201
|
+
'postActionRetainedRefreshAttemptSummary',
|
|
202
|
+
'postActionSourcePresence',
|
|
203
|
+
'actionEstimatedMonthlySavings',
|
|
204
|
+
'actionEstimatedSavingsPercentage',
|
|
205
|
+
'actionCurrencyCode',
|
|
206
|
+
'currentEstimatedMonthlySavings',
|
|
207
|
+
'currentEstimatedSavingsPercentage',
|
|
208
|
+
'currentCurrencyCode',
|
|
209
|
+
'currencyCodesMatch',
|
|
210
|
+
];
|
|
211
|
+
assertExactKeys(verification, [...required, ...optional], field);
|
|
212
|
+
requiredEnum(verification.verificationStatus, [
|
|
213
|
+
'pending-post-action-refresh',
|
|
214
|
+
'recommendation-absent-after-refresh',
|
|
215
|
+
'recommendation-still-present-after-refresh',
|
|
216
|
+
'post-action-evidence-truncated',
|
|
217
|
+
], `${field}.verificationStatus`);
|
|
218
|
+
assertValue(verification.artifactType, 'recommendations', `${field}.artifactType`);
|
|
219
|
+
requiredString(verification.actionRequestId, `${field}.actionRequestId`);
|
|
220
|
+
isoTimestamp(verification.actionCompletedAt, `${field}.actionCompletedAt`);
|
|
221
|
+
requiredEnum(verification.postActionEvidenceStatus, ['qualified', 'refresh-pending', 'missing', 'stale', 'unqualified'], `${field}.postActionEvidenceStatus`);
|
|
222
|
+
requiredEnum(verification.postActionEvidenceReason, [
|
|
223
|
+
'qualified-post-action-artifact',
|
|
224
|
+
'canonical-refresh-row-pending',
|
|
225
|
+
'canonical-refresh-row-missing',
|
|
226
|
+
'artifact-generated-before-action-completion',
|
|
227
|
+
'artifact-refresh-request-mismatch',
|
|
228
|
+
'artifact-refresh-trigger-mismatch',
|
|
229
|
+
'artifact-changed-key-missing',
|
|
230
|
+
'artifact-type-mismatch',
|
|
231
|
+
], `${field}.postActionEvidenceReason`);
|
|
232
|
+
['postActionArtifactGeneratedAt', 'postActionPendingRefreshRequestedAt', 'postActionPendingRefreshUpdatedAt'].forEach(key => {
|
|
233
|
+
if (verification[key] !== undefined)
|
|
234
|
+
isoTimestamp(verification[key], `${field}.${key}`);
|
|
235
|
+
});
|
|
236
|
+
if (verification.postActionRefreshRequestId !== undefined)
|
|
237
|
+
requiredString(verification.postActionRefreshRequestId, `${field}.postActionRefreshRequestId`);
|
|
238
|
+
if (verification.postActionPendingRefreshSummary !== undefined)
|
|
239
|
+
validatePendingRefresh(verification.postActionPendingRefreshSummary, `${field}.postActionPendingRefreshSummary`);
|
|
240
|
+
if (verification.postActionRetainedRefreshAttemptSummary !== undefined)
|
|
241
|
+
validateRefreshAttempts(verification.postActionRetainedRefreshAttemptSummary, `${field}.postActionRetainedRefreshAttemptSummary`);
|
|
242
|
+
if (verification.postActionSourcePresence !== undefined)
|
|
243
|
+
requiredEnum(verification.postActionSourcePresence, ['current', 'missing'], `${field}.postActionSourcePresence`);
|
|
244
|
+
optional
|
|
245
|
+
.filter(key => key.includes('Estimated'))
|
|
246
|
+
.forEach(key => {
|
|
247
|
+
if (verification[key] !== undefined)
|
|
248
|
+
finiteNumber(verification[key], `${field}.${key}`);
|
|
249
|
+
});
|
|
250
|
+
['actionCurrencyCode', 'currentCurrencyCode'].forEach(key => {
|
|
251
|
+
if (verification[key] !== undefined)
|
|
252
|
+
requiredString(verification[key], `${field}.${key}`);
|
|
253
|
+
});
|
|
254
|
+
if (verification.currencyCodesMatch !== undefined)
|
|
255
|
+
requiredBoolean(verification.currencyCodesMatch, `${field}.currencyCodesMatch`);
|
|
256
|
+
}
|
|
257
|
+
function validatePendingRefresh(value, field) {
|
|
258
|
+
const summary = asRecord(value, field);
|
|
259
|
+
assertExactKeys(summary, ['pendingArtifactCount', 'pendingArtifactTypes', 'oldestPendingRefreshUpdatedAt', 'newestPendingRefreshUpdatedAt', 'executionAttemptSummary'], field);
|
|
260
|
+
nonNegativeInteger(summary.pendingArtifactCount, `${field}.pendingArtifactCount`);
|
|
261
|
+
stringArray(summary.pendingArtifactTypes, `${field}.pendingArtifactTypes`).forEach(type => requiredEnum(type, ['recommendations', 'resource-recommendations', 'resource-type-recommendations'], `${field}.pendingArtifactTypes`));
|
|
262
|
+
['oldestPendingRefreshUpdatedAt', 'newestPendingRefreshUpdatedAt'].forEach(key => {
|
|
263
|
+
if (summary[key] !== undefined)
|
|
264
|
+
isoTimestamp(summary[key], `${field}.${key}`);
|
|
265
|
+
});
|
|
266
|
+
if (summary.executionAttemptSummary !== undefined)
|
|
267
|
+
validateRefreshAttempts(summary.executionAttemptSummary, `${field}.executionAttemptSummary`);
|
|
268
|
+
}
|
|
269
|
+
function validateRefreshAttempts(value, field) {
|
|
270
|
+
const summary = asRecord(value, field);
|
|
271
|
+
assertExactKeys(summary, ['attemptedArtifactCount', 'failedAttemptCount', 'supersededAttemptCount', 'attemptReasons', 'oldestAttemptedAt', 'newestAttemptedAt'], field);
|
|
272
|
+
['attemptedArtifactCount', 'failedAttemptCount', 'supersededAttemptCount'].forEach(key => nonNegativeInteger(summary[key], `${field}.${key}`));
|
|
273
|
+
stringArray(summary.attemptReasons, `${field}.attemptReasons`);
|
|
274
|
+
['oldestAttemptedAt', 'newestAttemptedAt'].forEach(key => {
|
|
275
|
+
if (summary[key] !== undefined)
|
|
276
|
+
isoTimestamp(summary[key], `${field}.${key}`);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function validateCommitmentSpend(value, field) {
|
|
280
|
+
const spend = asRecord(value, field);
|
|
281
|
+
assertExactKeys(spend, [
|
|
282
|
+
'expenseCountWithCommitmentEvidence',
|
|
283
|
+
'expenseCountWithoutCommitmentEvidence',
|
|
284
|
+
'totalsByCurrency',
|
|
285
|
+
'costViewByCurrency',
|
|
286
|
+
'groupedByAmortizationSource',
|
|
287
|
+
], field);
|
|
288
|
+
nonNegativeInteger(spend.expenseCountWithCommitmentEvidence, `${field}.expenseCountWithCommitmentEvidence`);
|
|
289
|
+
nonNegativeInteger(spend.expenseCountWithoutCommitmentEvidence, `${field}.expenseCountWithoutCommitmentEvidence`);
|
|
290
|
+
validateCostTotals(spend.totalsByCurrency, `${field}.totalsByCurrency`);
|
|
291
|
+
validateCostViews(spend.costViewByCurrency, `${field}.costViewByCurrency`);
|
|
292
|
+
if (!Array.isArray(spend.groupedByAmortizationSource))
|
|
293
|
+
throw new Error(`${field}.groupedByAmortizationSource must be an array.`);
|
|
294
|
+
const sources = [];
|
|
295
|
+
spend.groupedByAmortizationSource.forEach((entry, index) => {
|
|
296
|
+
const itemField = `${field}.groupedByAmortizationSource[${index}]`;
|
|
297
|
+
const source = asRecord(entry, itemField);
|
|
298
|
+
assertExactKeys(source, ['amortizationSource', 'expenseCount', 'totalsByCurrency', 'costViewByCurrency'], itemField);
|
|
299
|
+
sources.push(requiredEnum(source.amortizationSource, ['reservation-effective-cost', 'savings-plan-effective-cost'], `${itemField}.amortizationSource`));
|
|
300
|
+
nonNegativeInteger(source.expenseCount, `${itemField}.expenseCount`);
|
|
301
|
+
validateCostTotals(source.totalsByCurrency, `${itemField}.totalsByCurrency`);
|
|
302
|
+
validateCostViews(source.costViewByCurrency, `${itemField}.costViewByCurrency`);
|
|
303
|
+
});
|
|
304
|
+
assertUnique(sources, `${field}.groupedByAmortizationSource`);
|
|
305
|
+
}
|
|
306
|
+
function validateBenefitCoverage(value, field) {
|
|
307
|
+
const coverage = asRecord(value, field);
|
|
308
|
+
assertExactKeys(coverage, ['authority', 'status', 'matchedExpenseCount', 'coveredExpenseCount', 'unclassifiedExpenseCount', 'byBenefitType'], field);
|
|
309
|
+
assertValue(coverage.authority, 'persisted-billing-expense-amortization', `${field}.authority`);
|
|
310
|
+
requiredEnum(coverage.status, ['available', 'partial-persisted-evidence', 'missing-persisted-evidence'], `${field}.status`);
|
|
311
|
+
['matchedExpenseCount', 'coveredExpenseCount', 'unclassifiedExpenseCount'].forEach(key => nonNegativeInteger(coverage[key], `${field}.${key}`));
|
|
312
|
+
if (!Array.isArray(coverage.byBenefitType))
|
|
313
|
+
throw new Error(`${field}.byBenefitType must be an array.`);
|
|
314
|
+
const types = [];
|
|
315
|
+
coverage.byBenefitType.forEach((entry, index) => {
|
|
316
|
+
const itemField = `${field}.byBenefitType[${index}]`;
|
|
317
|
+
const benefit = asRecord(entry, itemField);
|
|
318
|
+
assertExactKeys(benefit, ['benefitType', 'amortizationSource', 'expenseCount', 'totalsByCurrency', 'costViewByCurrency'], itemField);
|
|
319
|
+
types.push(requiredEnum(benefit.benefitType, ['reserved-instance', 'savings-plan'], `${itemField}.benefitType`));
|
|
320
|
+
requiredEnum(benefit.amortizationSource, ['reservation-effective-cost', 'savings-plan-effective-cost'], `${itemField}.amortizationSource`);
|
|
321
|
+
nonNegativeInteger(benefit.expenseCount, `${itemField}.expenseCount`);
|
|
322
|
+
validateCostTotals(benefit.totalsByCurrency, `${itemField}.totalsByCurrency`);
|
|
323
|
+
validateCostViews(benefit.costViewByCurrency, `${itemField}.costViewByCurrency`);
|
|
324
|
+
});
|
|
325
|
+
assertUnique(types, `${field}.byBenefitType`);
|
|
326
|
+
}
|
|
327
|
+
function validateBenefitWarning(value, field) {
|
|
328
|
+
const warning = asRecord(value, field);
|
|
329
|
+
assertExactKeys(warning, ['code', 'severity', 'matchedExpenseCount', 'unclassifiedExpenseCount', 'message'], field);
|
|
330
|
+
requiredEnum(warning.code, ['ri-sp-coverage-evidence-missing', 'ri-sp-coverage-evidence-partial'], `${field}.code`);
|
|
331
|
+
assertValue(warning.severity, 'warning', `${field}.severity`);
|
|
332
|
+
nonNegativeInteger(warning.matchedExpenseCount, `${field}.matchedExpenseCount`);
|
|
333
|
+
nonNegativeInteger(warning.unclassifiedExpenseCount, `${field}.unclassifiedExpenseCount`);
|
|
334
|
+
requiredString(warning.message, `${field}.message`);
|
|
335
|
+
}
|
|
336
|
+
function validateCostTotals(value, field) {
|
|
337
|
+
if (!Array.isArray(value))
|
|
338
|
+
throw new Error(`${field} must be an array.`);
|
|
339
|
+
const currencies = [];
|
|
340
|
+
value.forEach((entry, index) => {
|
|
341
|
+
const itemField = `${field}[${index}]`;
|
|
342
|
+
const total = asRecord(entry, itemField);
|
|
343
|
+
assertExactKeys(total, ['currency', 'expenseCount', 'baseCostAmount', 'amortizedCostAmount', 'amortizedExpenseCount'], itemField);
|
|
344
|
+
currencies.push(requiredString(total.currency, `${itemField}.currency`));
|
|
345
|
+
nonNegativeInteger(total.expenseCount, `${itemField}.expenseCount`);
|
|
346
|
+
finiteNumber(total.baseCostAmount, `${itemField}.baseCostAmount`);
|
|
347
|
+
if (total.amortizedCostAmount !== undefined)
|
|
348
|
+
finiteNumber(total.amortizedCostAmount, `${itemField}.amortizedCostAmount`);
|
|
349
|
+
if (total.amortizedExpenseCount !== undefined)
|
|
350
|
+
nonNegativeInteger(total.amortizedExpenseCount, `${itemField}.amortizedExpenseCount`);
|
|
351
|
+
});
|
|
352
|
+
assertUnique(currencies, field);
|
|
353
|
+
}
|
|
354
|
+
function validateCostViews(value, field) {
|
|
355
|
+
if (!Array.isArray(value))
|
|
356
|
+
throw new Error(`${field} must be an array.`);
|
|
357
|
+
const currencies = [];
|
|
358
|
+
value.forEach((entry, index) => {
|
|
359
|
+
const itemField = `${field}[${index}]`;
|
|
360
|
+
const view = asRecord(entry, itemField);
|
|
361
|
+
assertExactKeys(view, [
|
|
362
|
+
'currency',
|
|
363
|
+
'actualCostAmount',
|
|
364
|
+
'amortizedCostAmount',
|
|
365
|
+
'amortizedExpenseCount',
|
|
366
|
+
'amortizationCoverageState',
|
|
367
|
+
'actualVsAmortizedDeltaAmount',
|
|
368
|
+
'commitmentCoverageState',
|
|
369
|
+
'showAmortizedCosts',
|
|
370
|
+
], itemField);
|
|
371
|
+
currencies.push(requiredString(view.currency, `${itemField}.currency`));
|
|
372
|
+
finiteNumber(view.actualCostAmount, `${itemField}.actualCostAmount`);
|
|
373
|
+
['amortizedCostAmount', 'actualVsAmortizedDeltaAmount'].forEach(key => {
|
|
374
|
+
if (view[key] !== undefined)
|
|
375
|
+
finiteNumber(view[key], `${itemField}.${key}`);
|
|
376
|
+
});
|
|
377
|
+
if (view.amortizedExpenseCount !== undefined)
|
|
378
|
+
nonNegativeInteger(view.amortizedExpenseCount, `${itemField}.amortizedExpenseCount`);
|
|
379
|
+
if (view.amortizationCoverageState !== undefined)
|
|
380
|
+
requiredEnum(view.amortizationCoverageState, ['complete', 'partial', 'none', 'unavailable'], `${itemField}.amortizationCoverageState`);
|
|
381
|
+
requiredEnum(view.commitmentCoverageState, ['discounted', 'no-amortized-cost', 'no-delta', 'amortized-exceeds-base', 'partial-amortized-cost', 'amortization-coverage-unavailable'], `${itemField}.commitmentCoverageState`);
|
|
382
|
+
requiredBoolean(view.showAmortizedCosts, `${itemField}.showAmortizedCosts`);
|
|
383
|
+
});
|
|
384
|
+
assertUnique(currencies, field);
|
|
385
|
+
}
|
|
386
|
+
function percentage(value, field) {
|
|
387
|
+
const result = finiteNumber(value, field);
|
|
388
|
+
if (result < 0 || result > 100)
|
|
389
|
+
throw new Error(`${field} must be between 0 and 100.`);
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
function httpsUrl(value, field) {
|
|
393
|
+
const result = requiredString(value, field);
|
|
394
|
+
if (!result.startsWith('https://'))
|
|
395
|
+
throw new Error(`${field} must be an HTTPS URL.`);
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { validateAwsPortalAccountSummaryDetailArtifact, validateAwsPortalResourceCollectionDetailArtifact, } from './portalPublicArtifactCurrentValidation.js';
|
|
2
|
+
export { validateAwsPortalAccountSummaryAiCostSummaryArtifact, validateAwsPortalAccountSummaryAiCostSummarySiblingBinding, validateAwsPortalAccountSummaryHistoryArtifact, validateAwsPortalResourceCollectionHistoryArtifact, validateAwsPortalRetainedHistoryBodyBinding, validateAwsPortalRetainedHistoryBodyReference, } from './portalPublicArtifactHistoryAiValidation.js';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { AWS_PORTAL_PUBLIC_ARTIFACT_SCHEMA_VERSION, } from './portalPublicArtifacts.js';
|
|
2
|
+
import { AWS_PUBLIC_ARTIFACT_SCHEMA_VERSION } from './publicArtifacts.js';
|
|
3
|
+
import { asRecord, assertAccount, assertExactKeys, assertPublicJson, assertValue, isoTimestamp, requiredEnum, requiredString, stringArray, validateBilling, validateMetricWindow, } from './pluginPublicArtifactValidationHelpers.js';
|
|
4
|
+
export { asRecord, assertExactKeys, assertPublicJson, assertValue, isoTimestamp, requiredEnum, requiredString, stringArray };
|
|
5
|
+
export function validatePortalEnvelope(artifact, artifactType, allowed) {
|
|
6
|
+
assertExactKeys(artifact, allowed, 'artifact');
|
|
7
|
+
assertValue(artifact.schemaVersion, AWS_PUBLIC_ARTIFACT_SCHEMA_VERSION, 'artifact.schemaVersion');
|
|
8
|
+
assertValue(artifact.portalSchemaVersion, AWS_PORTAL_PUBLIC_ARTIFACT_SCHEMA_VERSION, 'artifact.portalSchemaVersion');
|
|
9
|
+
assertValue(artifact.provider, 'aws', 'artifact.provider');
|
|
10
|
+
const accountId = requiredString(artifact.accountId, 'artifact.accountId');
|
|
11
|
+
assertAccount(accountId, accountId, 'artifact.accountId');
|
|
12
|
+
assertValue(artifact.artifactType, artifactType, 'artifact.artifactType');
|
|
13
|
+
const generation = validateGeneration(artifact.artifactGeneration, 'artifact.artifactGeneration');
|
|
14
|
+
const bodyGeneratedAt = isoTimestamp(artifact.generatedAt, 'artifact.generatedAt');
|
|
15
|
+
if (Date.parse(bodyGeneratedAt) > Date.parse(generation.generatedAt))
|
|
16
|
+
throw new Error('artifact.generatedAt must not be newer than the Portal generation.');
|
|
17
|
+
return { accountId, runId: generation.runId, portalGeneratedAt: generation.generatedAt, bodyGeneratedAt };
|
|
18
|
+
}
|
|
19
|
+
export function validateGeneration(value, field) {
|
|
20
|
+
const generation = asRecord(value, field);
|
|
21
|
+
assertExactKeys(generation, ['runId', 'generatedAt'], field);
|
|
22
|
+
return {
|
|
23
|
+
runId: requiredPublicIdentifier(generation.runId, `${field}.runId`),
|
|
24
|
+
generatedAt: isoTimestamp(generation.generatedAt, `${field}.generatedAt`),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function validateResourceScope(value, accountId, field) {
|
|
28
|
+
const scope = asRecord(value, field);
|
|
29
|
+
assertExactKeys(scope, ['provider', 'accountId', 'billing', 'resourceRegions', 'metricTimeWindow', 'tagRuleScope'], field);
|
|
30
|
+
assertValue(scope.provider, 'aws', `${field}.provider`);
|
|
31
|
+
assertAccount(scope.accountId, accountId, `${field}.accountId`);
|
|
32
|
+
validateBilling(scope.billing, `${field}.billing`);
|
|
33
|
+
regionArray(scope.resourceRegions, `${field}.resourceRegions`);
|
|
34
|
+
validateMetricWindow(scope.metricTimeWindow, `${field}.metricTimeWindow`);
|
|
35
|
+
if (scope.tagRuleScope !== undefined) {
|
|
36
|
+
const tagRuleScope = asRecord(scope.tagRuleScope, `${field}.tagRuleScope`);
|
|
37
|
+
assertExactKeys(tagRuleScope, ['companyId'], `${field}.tagRuleScope`);
|
|
38
|
+
requiredString(tagRuleScope.companyId, `${field}.tagRuleScope.companyId`);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
export function validateAccountScope(value, accountId, field) {
|
|
43
|
+
const scope = asRecord(value, field);
|
|
44
|
+
assertExactKeys(scope, ['provider', 'accountId', 'billing', 'comparisonBillingPeriod', 'resourceRegions', 'metricTimeWindow'], field);
|
|
45
|
+
assertValue(scope.provider, 'aws', `${field}.provider`);
|
|
46
|
+
assertAccount(scope.accountId, accountId, `${field}.accountId`);
|
|
47
|
+
validateBilling(scope.billing, `${field}.billing`);
|
|
48
|
+
if (scope.comparisonBillingPeriod !== undefined)
|
|
49
|
+
validatePeriod(scope.comparisonBillingPeriod, `${field}.comparisonBillingPeriod`);
|
|
50
|
+
regionArray(scope.resourceRegions, `${field}.resourceRegions`);
|
|
51
|
+
validateMetricWindow(scope.metricTimeWindow, `${field}.metricTimeWindow`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
export function validateHistoryAccountScope(value, accountId, field) {
|
|
55
|
+
const scope = asRecord(value, field);
|
|
56
|
+
assertExactKeys(scope, ['provider', 'accountId', 'billing', 'resourceRegions'], field);
|
|
57
|
+
assertValue(scope.provider, 'aws', `${field}.provider`);
|
|
58
|
+
assertAccount(scope.accountId, accountId, `${field}.accountId`);
|
|
59
|
+
validateBilling(scope.billing, `${field}.billing`);
|
|
60
|
+
regionArray(scope.resourceRegions, `${field}.resourceRegions`);
|
|
61
|
+
}
|
|
62
|
+
export function validatePeriod(value, field) {
|
|
63
|
+
const period = asRecord(value, field);
|
|
64
|
+
assertExactKeys(period, ['start', 'end'], field);
|
|
65
|
+
dateValue(period.start, `${field}.start`);
|
|
66
|
+
dateValue(period.end, `${field}.end`);
|
|
67
|
+
if (String(period.start) > String(period.end))
|
|
68
|
+
throw new Error(`${field}.start must not exceed end.`);
|
|
69
|
+
}
|
|
70
|
+
export function validateFreshness(value, field, timestampField) {
|
|
71
|
+
const freshness = asRecord(value, field);
|
|
72
|
+
assertExactKeys(freshness, [timestampField, timestampField === 'lastSuccessfulImportAt' ? 'hasSuccessfulImport' : 'hasSuccessfulRefresh'], field);
|
|
73
|
+
if (freshness[timestampField] !== undefined)
|
|
74
|
+
isoTimestamp(freshness[timestampField], `${field}.${timestampField}`);
|
|
75
|
+
const booleanField = timestampField === 'lastSuccessfulImportAt' ? 'hasSuccessfulImport' : 'hasSuccessfulRefresh';
|
|
76
|
+
requiredBoolean(freshness[booleanField], `${field}.${booleanField}`);
|
|
77
|
+
}
|
|
78
|
+
export function requiredBoolean(value, field) {
|
|
79
|
+
if (typeof value !== 'boolean')
|
|
80
|
+
throw new Error(`${field} must be a boolean.`);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
export function nonNegativeInteger(value, field) {
|
|
84
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0)
|
|
85
|
+
throw new Error(`${field} must be a non-negative safe integer.`);
|
|
86
|
+
return Number(value);
|
|
87
|
+
}
|
|
88
|
+
export function finiteNumber(value, field) {
|
|
89
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
90
|
+
throw new Error(`${field} must be a finite number.`);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
export function validateJsonObject(value, field) {
|
|
94
|
+
asRecord(value, field);
|
|
95
|
+
assertPublicJson(value, field);
|
|
96
|
+
}
|
|
97
|
+
export function validateJsonArray(value, field) {
|
|
98
|
+
if (!Array.isArray(value))
|
|
99
|
+
throw new Error(`${field} must be an array.`);
|
|
100
|
+
assertPublicJson(value, field);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
export function assertRegionsBelong(regions, allowedRegions, field) {
|
|
104
|
+
const nested = stringArray(regions, field);
|
|
105
|
+
nested.forEach(region => {
|
|
106
|
+
if (!allowedRegions.includes(region))
|
|
107
|
+
throw new Error(`${field} contains Region outside artifact scope: ${region}.`);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function assertUnique(values, field) {
|
|
111
|
+
if (new Set(values).size !== values.length)
|
|
112
|
+
throw new Error(`${field} must not contain duplicates.`);
|
|
113
|
+
}
|
|
114
|
+
export function regionArray(value, field) {
|
|
115
|
+
const regions = stringArray(value, field);
|
|
116
|
+
if (regions.length === 0)
|
|
117
|
+
throw new Error(`${field} must not be empty.`);
|
|
118
|
+
assertUnique(regions, field);
|
|
119
|
+
regions.forEach(region => {
|
|
120
|
+
if (region !== 'global' && !/^[a-z]{2}(?:-gov)?-[a-z0-9-]+-\d+$/.test(region))
|
|
121
|
+
throw new Error(`${field} contains a non-canonical AWS Region: ${region}.`);
|
|
122
|
+
});
|
|
123
|
+
return regions;
|
|
124
|
+
}
|
|
125
|
+
export function requiredPublicIdentifier(value, field) {
|
|
126
|
+
const identifier = requiredString(value, field);
|
|
127
|
+
if (identifier.includes('/') || identifier.includes('\\') || identifier.includes('://') || identifier.includes('..'))
|
|
128
|
+
throw new Error(`${field} must not contain a physical path or URI.`);
|
|
129
|
+
return identifier;
|
|
130
|
+
}
|
|
131
|
+
export function dateValue(value, field) {
|
|
132
|
+
const date = requiredString(value, field);
|
|
133
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || Number.isNaN(Date.parse(`${date}T00:00:00.000Z`)))
|
|
134
|
+
throw new Error(`${field} must be a calendar date.`);
|
|
135
|
+
const normalized = new Date(`${date}T00:00:00.000Z`).toISOString().slice(0, 10);
|
|
136
|
+
if (normalized !== date)
|
|
137
|
+
throw new Error(`${field} must be a valid calendar date.`);
|
|
138
|
+
return date;
|
|
139
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { sha256AwsPluginIdentity } from './pluginPublicArtifacts.js';
|
|
2
|
+
export const AWS_PORTAL_PUBLIC_ARTIFACT_SCHEMA_VERSION = 1;
|
|
3
|
+
export const AWS_PORTAL_RESOURCE_COLLECTION_LOGICAL_NAME = 'resources.json.gz';
|
|
4
|
+
export const AWS_PORTAL_ACCOUNT_SUMMARY_LOGICAL_NAME = 'account-summary.json.gz';
|
|
5
|
+
export const AWS_PORTAL_ACCOUNT_SUMMARY_AI_COST_SUMMARY_LOGICAL_NAME = 'account-summary-ai-cost-summary.json.gz';
|
|
6
|
+
export function buildAwsPortalResourceCollectionHistoryLogicalName(identitySha256) {
|
|
7
|
+
assertSha256(identitySha256, 'identitySha256');
|
|
8
|
+
return `resources-history--${identitySha256}.json.gz`;
|
|
9
|
+
}
|
|
10
|
+
export function buildAwsPortalAccountSummaryHistoryLogicalName(identitySha256) {
|
|
11
|
+
assertSha256(identitySha256, 'identitySha256');
|
|
12
|
+
return `account-summary-history--${identitySha256}.json.gz`;
|
|
13
|
+
}
|
|
14
|
+
export function buildAwsPortalResourceCollectionHistoryIdentityKey(scope, generatedAt) {
|
|
15
|
+
return [
|
|
16
|
+
'resource-collection-history',
|
|
17
|
+
scope.provider,
|
|
18
|
+
scope.accountId,
|
|
19
|
+
scope.billing.source,
|
|
20
|
+
scope.billing.source === 'cur' ? `report:${scope.billing.reportName}` : `export:${scope.billing.exportName}`,
|
|
21
|
+
scope.billing.billingPeriod.start,
|
|
22
|
+
scope.billing.billingPeriod.end,
|
|
23
|
+
scope.resourceRegions.join(','),
|
|
24
|
+
scope.metricTimeWindow.start,
|
|
25
|
+
scope.metricTimeWindow.end,
|
|
26
|
+
...(scope.tagRuleScope ? [`tag-rules:company:${scope.tagRuleScope.companyId}`] : []),
|
|
27
|
+
generatedAt,
|
|
28
|
+
].join('|');
|
|
29
|
+
}
|
|
30
|
+
export function buildAwsPortalResourceCollectionHistoryLogicalNameForScope(scope, generatedAt) {
|
|
31
|
+
return buildAwsPortalResourceCollectionHistoryLogicalName(sha256Text(buildAwsPortalResourceCollectionHistoryIdentityKey(scope, generatedAt)));
|
|
32
|
+
}
|
|
33
|
+
export function buildAwsPortalAccountSummaryHistoryIdentityKey(scope) {
|
|
34
|
+
return [
|
|
35
|
+
'account-summary-history',
|
|
36
|
+
scope.provider,
|
|
37
|
+
scope.accountId,
|
|
38
|
+
scope.billing.source,
|
|
39
|
+
scope.billing.source === 'cur' ? `report:${scope.billing.reportName}` : `export:${scope.billing.exportName}`,
|
|
40
|
+
scope.resourceRegions.join(','),
|
|
41
|
+
scope.billing.billingPeriod.start,
|
|
42
|
+
scope.billing.billingPeriod.end,
|
|
43
|
+
].join('|');
|
|
44
|
+
}
|
|
45
|
+
export function buildAwsPortalAccountSummaryHistoryLogicalNameForScope(scope) {
|
|
46
|
+
return buildAwsPortalAccountSummaryHistoryLogicalName(sha256Text(buildAwsPortalAccountSummaryHistoryIdentityKey(scope)));
|
|
47
|
+
}
|
|
48
|
+
export function buildAwsPortalAccountSummaryTargetKey(scope) {
|
|
49
|
+
return [
|
|
50
|
+
'account-summary',
|
|
51
|
+
scope.provider,
|
|
52
|
+
scope.accountId,
|
|
53
|
+
scope.billing.source,
|
|
54
|
+
scope.billing.source === 'cur' ? `report:${scope.billing.reportName}` : `export:${scope.billing.exportName}`,
|
|
55
|
+
scope.billing.billingPeriod.start,
|
|
56
|
+
scope.billing.billingPeriod.end,
|
|
57
|
+
scope.resourceRegions.join(','),
|
|
58
|
+
scope.metricTimeWindow.start,
|
|
59
|
+
scope.metricTimeWindow.end,
|
|
60
|
+
...(scope.comparisonBillingPeriod ? [`comparison:${scope.comparisonBillingPeriod.start}:${scope.comparisonBillingPeriod.end}`] : []),
|
|
61
|
+
].join('|');
|
|
62
|
+
}
|
|
63
|
+
export function buildAwsPortalAccountSummaryAiCostSummaryTargetKey(scope, audience) {
|
|
64
|
+
const sourceTargetKey = buildAwsPortalAccountSummaryTargetKey(scope);
|
|
65
|
+
return `account-summary-ai-cost-summary|${sourceTargetKey.slice('account-summary|'.length)}|audience:${audience}`;
|
|
66
|
+
}
|
|
67
|
+
function assertSha256(value, field) {
|
|
68
|
+
if (!/^[a-f0-9]{64}$/.test(value))
|
|
69
|
+
throw new Error(`${field} must be a lowercase hexadecimal SHA-256.`);
|
|
70
|
+
}
|
|
71
|
+
function sha256Text(value) {
|
|
72
|
+
return sha256AwsPluginIdentity(value);
|
|
73
|
+
}
|