@spottoai/types-package 1.0.2-beta.360 → 1.0.2-beta.361
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 +28 -0
- package/dist/azure/billingArtifactCanonicalization.d.ts +3 -1
- package/dist/azure/billingArtifactCanonicalization.d.ts.map +1 -1
- package/dist/azure/billingArtifactCanonicalization.js +123 -43
- package/dist/azure/billingArtifactCanonicalization.js.map +1 -1
- package/dist/azure/billingArtifactGeneration.d.ts +53 -1
- package/dist/azure/billingArtifactGeneration.d.ts.map +1 -1
- package/dist/azure/billingArtifactGeneration.js +190 -25
- package/dist/azure/billingArtifactGeneration.js.map +1 -1
- package/dist/azure/billingArtifactLimits.d.ts +19 -0
- package/dist/azure/billingArtifactLimits.d.ts.map +1 -0
- package/dist/azure/billingArtifactLimits.js +22 -0
- package/dist/azure/billingArtifactLimits.js.map +1 -0
- package/dist/azure/billingPlots.d.ts +26 -3
- package/dist/azure/billingPlots.d.ts.map +1 -1
- package/dist/azure/billingPlots.js +257 -10
- package/dist/azure/billingPlots.js.map +1 -1
- package/dist/azure/views.d.ts.map +1 -1
- package/dist/azure/views.js +15 -3
- package/dist/azure/views.js.map +1 -1
- package/dist/common/artifactControlData.d.ts +16 -2
- package/dist/common/artifactControlData.d.ts.map +1 -1
- package/dist/common/artifactControlData.js +129 -23
- package/dist/common/artifactControlData.js.map +1 -1
- package/dist/esm/azure/billingArtifactCanonicalization.js +121 -42
- package/dist/esm/azure/billingArtifactGeneration.js +186 -25
- package/dist/esm/azure/billingArtifactLimits.js +18 -0
- package/dist/esm/azure/billingPlots.js +253 -10
- package/dist/esm/azure/views.js +16 -4
- package/dist/esm/common/artifactControlData.js +127 -22
- package/dist/esm/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import { isArtifactOwnershipBinding, isEnforceableArtifactOwnershipBinding, } from '../common/artifactEvidence.js';
|
|
2
2
|
import { isArtifactRevisionVector, isStrictLogicalArtifactReference } from '../common/artifactEvidenceValidation.js';
|
|
3
|
-
import { allowedArtifactIdentityField, allowedArtifactReferenceField, containsForbiddenArtifactControlData, } from '../common/artifactControlData.js';
|
|
3
|
+
import { ARTIFACT_CONTROL_DATA_MAX_VISITED_NODES, allowedArtifactIdentityField, allowedArtifactReferenceField, allowedArtifactTraversalField, containsForbiddenArtifactControlData, } from '../common/artifactControlData.js';
|
|
4
4
|
import { isBillingCompletedArtifactPublicationDecision } from './billingArtifactEvidence.js';
|
|
5
|
+
import { BILLING_ARTIFACT_OBJECT_LIMITS_V1 } from './billingArtifactLimits.js';
|
|
6
|
+
/** Stable diagnostic-only suffix for the latest successfully enqueued observe input. */
|
|
7
|
+
export const BILLING_ANALYZER_INPUT_OBSERVATION_POINTER_RELATIVE_PATH = 'history/billing/analyzer-inputs/latest-enqueued.json';
|
|
5
8
|
const BILLING_BASES = new Set(['actual', 'amortized']);
|
|
6
9
|
const COVERAGE_VERDICTS = new Set(['complete', 'partial', 'none', 'unknown']);
|
|
7
10
|
const DATE_BASES = new Set(['utc', 'billing-calendar', 'company-local']);
|
|
8
11
|
const CONTENT_ENCODINGS = new Set(['identity', 'gzip']);
|
|
12
|
+
const OBSERVATION_COMPARISONS = new Set([
|
|
13
|
+
'authority-absent',
|
|
14
|
+
'newer',
|
|
15
|
+
'equal',
|
|
16
|
+
'older',
|
|
17
|
+
'incomparable',
|
|
18
|
+
'newer-ownership',
|
|
19
|
+
'older-ownership',
|
|
20
|
+
'unenforceable',
|
|
21
|
+
]);
|
|
22
|
+
const OBSERVATION_PROJECTED_OUTCOMES = new Map([
|
|
23
|
+
['authority-absent', 'would-promote'],
|
|
24
|
+
['newer', 'would-promote'],
|
|
25
|
+
['newer-ownership', 'would-promote'],
|
|
26
|
+
['equal', 'would-be-idempotent'],
|
|
27
|
+
['older', 'would-be-superseded'],
|
|
28
|
+
['older-ownership', 'would-be-superseded'],
|
|
29
|
+
['incomparable', 'would-quarantine'],
|
|
30
|
+
['unenforceable', 'not-enforceable'],
|
|
31
|
+
]);
|
|
9
32
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
10
33
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
34
|
const isNonEmptyString = (value) => typeof value === 'string' && value.trim() === value && value.length > 0;
|
|
@@ -25,7 +48,23 @@ const isCanonicalIsoTimestamp = (value) => {
|
|
|
25
48
|
};
|
|
26
49
|
const hasControlCharacters = (value) => Array.from(value).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
27
50
|
const allowedDescriptorPaths = (descriptors) => Array.isArray(descriptors) ? descriptors.flatMap(descriptor => allowedArtifactReferenceField(descriptor, 'path')) : [];
|
|
51
|
+
const containsForbiddenBillingArtifactControlData = (value, allowedReferenceFields = []) => containsForbiddenArtifactControlData(value, allowedReferenceFields, {
|
|
52
|
+
requireAllowedFieldTraversalContext: true,
|
|
53
|
+
});
|
|
28
54
|
const isPathSegment = (value) => isNonEmptyString(value) && !/[\\/?#%]/.test(value) && !hasControlCharacters(value) && value !== '.' && value !== '..';
|
|
55
|
+
/** Builds the diagnostic-only latest-enqueued observation path for one safe subscription segment. */
|
|
56
|
+
export const buildBillingAnalyzerInputObservationPointerPath = (subscriptionId) => {
|
|
57
|
+
if (!isPathSegment(subscriptionId))
|
|
58
|
+
throw new TypeError('subscriptionId must be a safe path segment');
|
|
59
|
+
return `subscriptions/${subscriptionId}/${BILLING_ANALYZER_INPUT_OBSERVATION_POINTER_RELATIVE_PATH}`;
|
|
60
|
+
};
|
|
61
|
+
/** Validates the exact diagnostic-only latest-enqueued observation logical path. */
|
|
62
|
+
export const isBillingAnalyzerInputObservationPointerPath = (value) => {
|
|
63
|
+
if (!isStrictLogicalArtifactReference(value))
|
|
64
|
+
return false;
|
|
65
|
+
const match = /^subscriptions\/([^/]+)\/history\/billing\/analyzer-inputs\/latest-enqueued\.json$/.exec(value);
|
|
66
|
+
return match !== null && isPathSegment(match[1]);
|
|
67
|
+
};
|
|
29
68
|
const inputGenerationPrefix = (subscriptionId, generationId) => `subscriptions/${subscriptionId}/history/billing/analyzer-inputs/generations/${generationId}/`;
|
|
30
69
|
const inputManifestPath = (subscriptionId, generationId) => `${inputGenerationPrefix(subscriptionId, generationId)}manifest.json`;
|
|
31
70
|
const outputGenerationPrefix = (subscriptionId, generationId) => `subscriptions/${subscriptionId}/billing/generations/${generationId}/`;
|
|
@@ -45,6 +84,9 @@ const hasMatchingIdentity = (subscriptionId, generationId, ownership, revision,
|
|
|
45
84
|
return !enforceable || isEnforceableArtifactOwnershipBinding(ownership);
|
|
46
85
|
};
|
|
47
86
|
const isSha256 = (value) => typeof value === 'string' && SHA256_PATTERN.test(value);
|
|
87
|
+
const hasDiagnosticObservationDiscriminant = (value) => value.authority === 'diagnostic-only' ||
|
|
88
|
+
(value.publicationMode === 'observe' &&
|
|
89
|
+
(value.documentType === 'billing-analyzer-input-observation-pointer' || value.documentType === 'billing-analysis-promotion-observation'));
|
|
48
90
|
const isRequestedPeriod = (value) => {
|
|
49
91
|
if (!isRecord(value) ||
|
|
50
92
|
!isCanonicalIsoTimestamp(value.fromInclusive) ||
|
|
@@ -65,6 +107,7 @@ const isInputObjectDescriptor = (value, subscriptionId, generationId) => {
|
|
|
65
107
|
isNonEmptyString(value.etag) &&
|
|
66
108
|
isSha256(value.sha256) &&
|
|
67
109
|
isPositiveInteger(value.byteCount) &&
|
|
110
|
+
value.byteCount <= BILLING_ARTIFACT_OBJECT_LIMITS_V1.inputObjectStoredBytes &&
|
|
68
111
|
isNonNegativeInteger(value.rowCount) &&
|
|
69
112
|
isStringIn(value.basis, BILLING_BASES) &&
|
|
70
113
|
(value.currencyCode === undefined || isNonEmptyString(value.currencyCode)) &&
|
|
@@ -74,31 +117,76 @@ const isOutputArtifactDescriptor = (value, subscriptionId, generationId) => {
|
|
|
74
117
|
if (!isRecord(value))
|
|
75
118
|
return false;
|
|
76
119
|
const prefix = outputGenerationPrefix(subscriptionId, generationId);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
value.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
120
|
+
if (!isGenerationPath(value.path, prefix, outputManifestPath(subscriptionId, generationId)) ||
|
|
121
|
+
!isPathSegment(value.name) ||
|
|
122
|
+
!((value.name === 'metadata.json' &&
|
|
123
|
+
value.path === `${prefix}metadata.json` &&
|
|
124
|
+
isNonNegativeInteger(value.byteLength) &&
|
|
125
|
+
value.byteLength <= BILLING_ARTIFACT_OBJECT_LIMITS_V1.metadataStoredBytes) ||
|
|
126
|
+
(value.name !== 'metadata.json' &&
|
|
127
|
+
value.path === `${prefix}plots/${value.name}` &&
|
|
128
|
+
isNonNegativeInteger(value.byteLength) &&
|
|
129
|
+
value.byteLength <= BILLING_ARTIFACT_OBJECT_LIMITS_V1.plotStoredBytes))) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return value.mediaType === 'application/json' && isStringIn(value.contentEncoding, CONTENT_ENCODINGS) && isSha256(value.sha256);
|
|
83
133
|
};
|
|
84
134
|
const isJsonMetadata = (value) => {
|
|
85
135
|
if (!isRecord(value))
|
|
86
136
|
return false;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
137
|
+
const pending = [{ kind: 'visit', value }];
|
|
138
|
+
const activeContainers = new WeakSet();
|
|
139
|
+
const completedContainers = new WeakSet();
|
|
140
|
+
let visitedNodeCount = 0;
|
|
141
|
+
while (pending.length > 0) {
|
|
142
|
+
const candidate = pending.pop();
|
|
143
|
+
if (candidate.kind === 'leave') {
|
|
144
|
+
activeContainers.delete(candidate.container);
|
|
145
|
+
completedContainers.add(candidate.container);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (candidate.value === null || typeof candidate.value === 'string' || typeof candidate.value === 'boolean' || isFiniteNumber(candidate.value)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(candidate.value)) {
|
|
152
|
+
visitedNodeCount += 1;
|
|
153
|
+
if (visitedNodeCount > ARTIFACT_CONTROL_DATA_MAX_VISITED_NODES)
|
|
154
|
+
return false;
|
|
155
|
+
if (activeContainers.has(candidate.value))
|
|
156
|
+
return false;
|
|
157
|
+
if (completedContainers.has(candidate.value))
|
|
158
|
+
continue;
|
|
159
|
+
activeContainers.add(candidate.value);
|
|
160
|
+
pending.push({ kind: 'leave', container: candidate.value });
|
|
161
|
+
for (let index = candidate.value.length - 1; index >= 0; index -= 1) {
|
|
162
|
+
pending.push({ kind: 'visit', value: candidate.value[index] });
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!isRecord(candidate.value) || Object.getPrototypeOf(candidate.value) !== Object.prototype)
|
|
167
|
+
return false;
|
|
168
|
+
visitedNodeCount += 1;
|
|
169
|
+
if (visitedNodeCount > ARTIFACT_CONTROL_DATA_MAX_VISITED_NODES)
|
|
170
|
+
return false;
|
|
171
|
+
if (activeContainers.has(candidate.value))
|
|
172
|
+
return false;
|
|
173
|
+
if (completedContainers.has(candidate.value))
|
|
174
|
+
continue;
|
|
175
|
+
activeContainers.add(candidate.value);
|
|
176
|
+
pending.push({ kind: 'leave', container: candidate.value });
|
|
177
|
+
for (const child of Object.values(candidate.value))
|
|
178
|
+
pending.push({ kind: 'visit', value: child });
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
97
181
|
};
|
|
98
182
|
/** Validates one immutable billing analyzer input manifest without performing I/O. */
|
|
99
183
|
export const isBillingAnalyzerInputManifestV2 = (value) => {
|
|
100
184
|
if (!isRecord(value) ||
|
|
101
|
-
|
|
185
|
+
containsForbiddenBillingArtifactControlData(value, [
|
|
186
|
+
...allowedArtifactIdentityField(value, 'publicationKey'),
|
|
187
|
+
...allowedArtifactTraversalField(value, 'inputs'),
|
|
188
|
+
...allowedDescriptorPaths(value.inputs),
|
|
189
|
+
])) {
|
|
102
190
|
return false;
|
|
103
191
|
}
|
|
104
192
|
if (value.schemaVersion !== 2 || value.status !== 'completed')
|
|
@@ -117,7 +205,7 @@ export const isBillingAnalyzerInputManifestV2 = (value) => {
|
|
|
117
205
|
const periodKeys = value.requestedPeriods.map(period => `${period.fromInclusive}\n${period.throughExclusive}\n${period.dateBasis}\n${period.timeZone ?? ''}\n${period.basis}`);
|
|
118
206
|
if (!hasUniqueValues(periodKeys))
|
|
119
207
|
return false;
|
|
120
|
-
if (!Array.isArray(value.inputs) || value.inputs.length === 0)
|
|
208
|
+
if (!Array.isArray(value.inputs) || value.inputs.length === 0 || value.inputs.length > BILLING_ARTIFACT_OBJECT_LIMITS_V1.maxInputObjects)
|
|
121
209
|
return false;
|
|
122
210
|
if (!value.inputs.every(input => isInputObjectDescriptor(input, value.subscriptionId, value.generationId)))
|
|
123
211
|
return false;
|
|
@@ -125,7 +213,9 @@ export const isBillingAnalyzerInputManifestV2 = (value) => {
|
|
|
125
213
|
};
|
|
126
214
|
/** Validates the enforceable current pointer for one published analyzer input generation. */
|
|
127
215
|
export const isBillingAnalyzerInputCurrentPointerV1 = (value) => {
|
|
128
|
-
if (!isRecord(value) ||
|
|
216
|
+
if (!isRecord(value) ||
|
|
217
|
+
hasDiagnosticObservationDiscriminant(value) ||
|
|
218
|
+
containsForbiddenBillingArtifactControlData(value, allowedArtifactReferenceField(value, 'manifestPath')))
|
|
129
219
|
return false;
|
|
130
220
|
if (value.schemaVersion !== 1 || value.status !== 'completed')
|
|
131
221
|
return false;
|
|
@@ -138,7 +228,7 @@ export const isBillingAnalyzerInputCurrentPointerV1 = (value) => {
|
|
|
138
228
|
};
|
|
139
229
|
/** Validates the V2 queue envelope and its immutable input-manifest binding. */
|
|
140
230
|
export const isBillingAnalyzerRequestV2 = (value) => {
|
|
141
|
-
if (!isRecord(value) ||
|
|
231
|
+
if (!isRecord(value) || containsForbiddenBillingArtifactControlData(value, allowedArtifactReferenceField(value, 'inputManifestPath')))
|
|
142
232
|
return false;
|
|
143
233
|
if (value.schemaVersion !== 2 || (value.publicationMode !== 'observe' && value.publicationMode !== 'enforce'))
|
|
144
234
|
return false;
|
|
@@ -157,11 +247,32 @@ export const isBillingAnalyzerRequestV2 = (value) => {
|
|
|
157
247
|
return false;
|
|
158
248
|
return value.displayMetadata === undefined || isJsonMetadata(value.displayMetadata);
|
|
159
249
|
};
|
|
250
|
+
/** Validates a diagnostic-only latest-enqueued pointer; it is never customer authority. */
|
|
251
|
+
export const isBillingAnalyzerInputObservationPointerV1 = (value) => {
|
|
252
|
+
if (!isRecord(value) || containsForbiddenBillingArtifactControlData(value, allowedArtifactReferenceField(value, 'inputManifestPath')))
|
|
253
|
+
return false;
|
|
254
|
+
if (value.schemaVersion !== 1 ||
|
|
255
|
+
value.documentType !== 'billing-analyzer-input-observation-pointer' ||
|
|
256
|
+
value.authority !== 'diagnostic-only' ||
|
|
257
|
+
value.publicationMode !== 'observe' ||
|
|
258
|
+
value.inputState !== 'enqueued') {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (!hasMatchingIdentity(value.subscriptionId, value.generationId, value.ownership, value.revision, false))
|
|
262
|
+
return false;
|
|
263
|
+
return (isStrictLogicalArtifactReference(value.inputManifestPath) &&
|
|
264
|
+
value.inputManifestPath === inputManifestPath(value.subscriptionId, value.generationId) &&
|
|
265
|
+
isSha256(value.inputManifestDigest) &&
|
|
266
|
+
isSha256(value.messageId) &&
|
|
267
|
+
isNonEmptyString(value.correlationId) &&
|
|
268
|
+
isCanonicalIsoTimestamp(value.enqueuedAt));
|
|
269
|
+
};
|
|
160
270
|
/** Validates an immutable analyzer output manifest and its exact input binding. */
|
|
161
271
|
export const isBillingAnalyzerOutputManifestV2 = (value) => {
|
|
162
272
|
if (!isRecord(value) ||
|
|
163
|
-
|
|
273
|
+
containsForbiddenBillingArtifactControlData(value, [
|
|
164
274
|
...allowedArtifactReferenceField(value, 'inputManifestPath'),
|
|
275
|
+
...allowedArtifactTraversalField(value, 'artifacts'),
|
|
165
276
|
...allowedDescriptorPaths(value.artifacts),
|
|
166
277
|
])) {
|
|
167
278
|
return false;
|
|
@@ -195,16 +306,16 @@ export const isBillingAnalyzerOutputManifestV2 = (value) => {
|
|
|
195
306
|
publicationDecisionReferencesDigest(value.publicationDecision, new Set([...outputDerivedDigests].filter(digest => digest !== value.inputManifestDigest)))) {
|
|
196
307
|
return false;
|
|
197
308
|
}
|
|
198
|
-
if (
|
|
309
|
+
if (value.artifacts.filter(artifact => artifact.name === 'metadata.json').length !== 1)
|
|
199
310
|
return false;
|
|
200
|
-
}
|
|
201
311
|
return (isBillingCompletedArtifactPublicationDecision(value.publicationDecision, value.generationId, value.inputManifestDigest) &&
|
|
202
312
|
isCanonicalIsoTimestamp(value.completedAt));
|
|
203
313
|
};
|
|
204
314
|
/** Validates the sole promoted authority pointer for completed billing analysis. */
|
|
205
315
|
export const isBillingAnalysisCurrentPointerV1 = (value) => {
|
|
206
316
|
if (!isRecord(value) ||
|
|
207
|
-
|
|
317
|
+
hasDiagnosticObservationDiscriminant(value) ||
|
|
318
|
+
containsForbiddenBillingArtifactControlData(value, [
|
|
208
319
|
...allowedArtifactReferenceField(value, 'inputManifestPath'),
|
|
209
320
|
...allowedArtifactReferenceField(value, 'outputManifestPath'),
|
|
210
321
|
])) {
|
|
@@ -227,3 +338,53 @@ export const isBillingAnalysisCurrentPointerV1 = (value) => {
|
|
|
227
338
|
return (isBillingCompletedArtifactPublicationDecision(value.publicationDecision, value.generationId, value.inputManifestDigest) &&
|
|
228
339
|
isCanonicalIsoTimestamp(value.completedAt));
|
|
229
340
|
};
|
|
341
|
+
/** Validates an immutable diagnostic-only promotion evaluation. */
|
|
342
|
+
export const isBillingAnalysisPromotionObservationV1 = (value) => {
|
|
343
|
+
if (!isRecord(value) ||
|
|
344
|
+
containsForbiddenBillingArtifactControlData(value, [
|
|
345
|
+
...allowedArtifactReferenceField(value, 'inputManifestPath'),
|
|
346
|
+
...allowedArtifactReferenceField(value, 'outputManifestPath'),
|
|
347
|
+
])) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
if (value.schemaVersion !== 1 ||
|
|
351
|
+
value.documentType !== 'billing-analysis-promotion-observation' ||
|
|
352
|
+
value.authority !== 'diagnostic-only' ||
|
|
353
|
+
value.publicationMode !== 'observe' ||
|
|
354
|
+
value.processingState !== 'succeeded') {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
if (!hasMatchingIdentity(value.subscriptionId, value.generationId, value.ownership, value.revision, false))
|
|
358
|
+
return false;
|
|
359
|
+
if (!isStrictLogicalArtifactReference(value.inputManifestPath) ||
|
|
360
|
+
value.inputManifestPath !== inputManifestPath(value.subscriptionId, value.generationId) ||
|
|
361
|
+
!isStrictLogicalArtifactReference(value.outputManifestPath) ||
|
|
362
|
+
value.outputManifestPath !== outputManifestPath(value.subscriptionId, value.generationId)) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
if (!isSha256(value.inputManifestDigest) ||
|
|
366
|
+
!isSha256(value.outputManifestDigest) ||
|
|
367
|
+
!isSha256(value.observationDigest) ||
|
|
368
|
+
!isSha256(value.messageId) ||
|
|
369
|
+
!isNonEmptyString(value.correlationId) ||
|
|
370
|
+
!isCanonicalIsoTimestamp(value.observedAt) ||
|
|
371
|
+
!isRecord(value.evaluation) ||
|
|
372
|
+
!isStringIn(value.evaluation.comparison, OBSERVATION_COMPARISONS)) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
const hasOwnershipEpoch = value.ownership.ownershipEpochRevision !== undefined;
|
|
376
|
+
const outputDigestRelation = value.evaluation.outputDigestRelation;
|
|
377
|
+
if (outputDigestRelation !== undefined && outputDigestRelation !== 'same' && outputDigestRelation !== 'different')
|
|
378
|
+
return false;
|
|
379
|
+
if (!hasOwnershipEpoch) {
|
|
380
|
+
return (outputDigestRelation === undefined && value.evaluation.comparison === 'unenforceable' && value.evaluation.projectedOutcome === 'not-enforceable');
|
|
381
|
+
}
|
|
382
|
+
if (value.evaluation.comparison === 'equal') {
|
|
383
|
+
return outputDigestRelation === 'different'
|
|
384
|
+
? value.evaluation.projectedOutcome === 'would-quarantine'
|
|
385
|
+
: value.evaluation.projectedOutcome === 'would-be-idempotent';
|
|
386
|
+
}
|
|
387
|
+
return (outputDigestRelation === undefined &&
|
|
388
|
+
value.evaluation.comparison !== 'unenforceable' &&
|
|
389
|
+
OBSERVATION_PROJECTED_OUTCOMES.get(value.evaluation.comparison) === value.evaluation.projectedOutcome);
|
|
390
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V1 billing artifact object limits in bytes.
|
|
3
|
+
*
|
|
4
|
+
* `StoredBytes` limits apply to the exact persisted object before content
|
|
5
|
+
* decoding. `DecodedBytes` limits apply after decoding or decompression.
|
|
6
|
+
*/
|
|
7
|
+
export const BILLING_ARTIFACT_OBJECT_LIMITS_V1 = Object.freeze({
|
|
8
|
+
pointerStoredBytes: 64 * 1024,
|
|
9
|
+
observationStoredBytes: 64 * 1024,
|
|
10
|
+
manifestStoredBytes: 1024 * 1024,
|
|
11
|
+
metadataStoredBytes: 4 * 1024 * 1024,
|
|
12
|
+
metadataDecodedBytes: 16 * 1024 * 1024,
|
|
13
|
+
plotStoredBytes: 32 * 1024 * 1024,
|
|
14
|
+
plotDecodedBytes: 128 * 1024 * 1024,
|
|
15
|
+
inputObjectStoredBytes: 32 * 1024 * 1024,
|
|
16
|
+
inputObjectDecodedBytes: 128 * 1024 * 1024,
|
|
17
|
+
maxInputObjects: 12,
|
|
18
|
+
});
|
|
@@ -1,11 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Billing cost analysis types for Azure cost visualization.
|
|
3
3
|
*/
|
|
4
|
-
import { isArtifactOwnershipBinding
|
|
4
|
+
import { isArtifactOwnershipBinding } from '../common/artifactEvidence.js';
|
|
5
5
|
import { containsForbiddenArtifactControlData } from '../common/artifactControlData.js';
|
|
6
6
|
import { isArtifactRevisionVector } from '../common/artifactEvidenceValidation.js';
|
|
7
7
|
import { isBillingCompletedArtifactPublicationDecision, isBillingPartialArtifactPublicationDecision, } from './billingArtifactEvidence.js';
|
|
8
|
-
const
|
|
8
|
+
const LEGACY_FALLBACK_FORBIDDEN_OWN_FIELDS = [
|
|
9
|
+
'schemaVersion',
|
|
10
|
+
'ownership',
|
|
11
|
+
'revision',
|
|
12
|
+
'authority',
|
|
13
|
+
'documentType',
|
|
14
|
+
'publicationMode',
|
|
15
|
+
'status',
|
|
16
|
+
'generationId',
|
|
17
|
+
'inputManifestDigest',
|
|
18
|
+
'outputBindingDigest',
|
|
19
|
+
'outputManifestDigest',
|
|
20
|
+
'manifestDigest',
|
|
21
|
+
'observationDigest',
|
|
22
|
+
'sha256',
|
|
23
|
+
'artifactEvidence',
|
|
24
|
+
'publicationDecision',
|
|
25
|
+
'artifacts',
|
|
26
|
+
'manifestPath',
|
|
27
|
+
'inputManifestPath',
|
|
28
|
+
'outputManifestPath',
|
|
29
|
+
'byteLength',
|
|
30
|
+
'byteCount',
|
|
31
|
+
'rowCount',
|
|
32
|
+
'contentEncoding',
|
|
33
|
+
'mediaType',
|
|
34
|
+
'etag',
|
|
35
|
+
'versionId',
|
|
36
|
+
'publicationKey',
|
|
37
|
+
'coveragePlanDigest',
|
|
38
|
+
'messageId',
|
|
39
|
+
'eventId',
|
|
40
|
+
'correlationId',
|
|
41
|
+
'idempotencyKey',
|
|
42
|
+
'inputState',
|
|
43
|
+
'processingState',
|
|
44
|
+
'evaluation',
|
|
45
|
+
'dependencies',
|
|
46
|
+
'claims',
|
|
47
|
+
'issues',
|
|
48
|
+
'completedAt',
|
|
49
|
+
'observedAt',
|
|
50
|
+
'enqueuedAt',
|
|
51
|
+
];
|
|
52
|
+
const BILLING_FORBIDDEN_CONTROL_FIELDS = new Set([
|
|
53
|
+
...LEGACY_FALLBACK_FORBIDDEN_OWN_FIELDS,
|
|
54
|
+
'artifactState',
|
|
55
|
+
'artifactSource',
|
|
56
|
+
'digest',
|
|
57
|
+
'fingerprint',
|
|
58
|
+
'checksum',
|
|
59
|
+
'hash',
|
|
60
|
+
'subscriptionId',
|
|
61
|
+
'provider',
|
|
62
|
+
'tenantId',
|
|
63
|
+
'companyId',
|
|
64
|
+
'cloudAccountId',
|
|
65
|
+
'accountId',
|
|
66
|
+
'ownershipEpochRevision',
|
|
67
|
+
'sourceRevision',
|
|
68
|
+
'policyRevision',
|
|
69
|
+
'processing',
|
|
70
|
+
'evidence',
|
|
71
|
+
'publication',
|
|
72
|
+
'required',
|
|
73
|
+
'support',
|
|
74
|
+
'applicability',
|
|
75
|
+
'attempt',
|
|
76
|
+
'coverage',
|
|
77
|
+
'emptyEvidence',
|
|
78
|
+
'freshness',
|
|
79
|
+
'reasonCode',
|
|
80
|
+
'acceptedRowCount',
|
|
81
|
+
'emptyProofRef',
|
|
82
|
+
'claimId',
|
|
83
|
+
'sectionPaths',
|
|
84
|
+
'requiredDependencies',
|
|
85
|
+
'code',
|
|
86
|
+
'blocking',
|
|
87
|
+
'dependency',
|
|
88
|
+
].map(field => field.toLowerCase()));
|
|
89
|
+
const BILLING_DOCUMENT_STATES = new Set(['current', 'stale', 'partial', 'complete-empty']);
|
|
90
|
+
const BILLING_VERIFIED_READ_STATES = new Set(['current', 'stale', 'complete-empty']);
|
|
9
91
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
10
92
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
93
|
const isNonEmptyString = (value) => typeof value === 'string' && value.trim() === value && value.length > 0;
|
|
@@ -15,9 +97,145 @@ const isNullableFiniteNumber = (value) => value === null || isFiniteNumber(value
|
|
|
15
97
|
const isNonNegativeInteger = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
16
98
|
const isPositiveInteger = (value) => Number.isSafeInteger(value) && Number(value) > 0;
|
|
17
99
|
const isStringArray = (value) => Array.isArray(value) && value.every(isNonEmptyString);
|
|
100
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
18
101
|
const hasControlCharacters = (value) => Array.from(value).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
|
|
19
102
|
const isPathSegment = (value) => isNonEmptyString(value) && !/[\\/?#%]/.test(value) && !hasControlCharacters(value) && value !== '.' && value !== '..';
|
|
20
103
|
const publicationDecisionReferencesDigest = (value, digest) => isRecord(value) && Array.isArray(value.dependencies) && value.dependencies.some(dependency => isRecord(dependency) && dependency.digest === digest);
|
|
104
|
+
const allowedBillingCostAnalysisFields = (value, validationBranch) => {
|
|
105
|
+
const fields = [];
|
|
106
|
+
const allowText = (object, ...keys) => {
|
|
107
|
+
if (!isRecord(object))
|
|
108
|
+
return;
|
|
109
|
+
for (const key of keys)
|
|
110
|
+
fields.push({ object, key, allowUriScheme: true, allowDigestLike: true, allowControlField: true });
|
|
111
|
+
};
|
|
112
|
+
const allowTextArray = (object, key) => {
|
|
113
|
+
if (isRecord(object)) {
|
|
114
|
+
fields.push({ object, key, allowUriSchemeInStringArray: true, allowDigestLike: true, allowControlField: true });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const allowControl = (object, ...keys) => {
|
|
118
|
+
if (!isRecord(object))
|
|
119
|
+
return;
|
|
120
|
+
for (const key of keys)
|
|
121
|
+
fields.push({ object, key, allowControlField: true });
|
|
122
|
+
};
|
|
123
|
+
const allowDigest = (object, key) => {
|
|
124
|
+
if (isRecord(object))
|
|
125
|
+
fields.push({ object, key, allowDigestLike: true, allowControlField: true });
|
|
126
|
+
};
|
|
127
|
+
const allowChildren = (object, ...keys) => {
|
|
128
|
+
if (!isRecord(object))
|
|
129
|
+
return;
|
|
130
|
+
for (const key of keys)
|
|
131
|
+
fields.push({ object, key, allowChildArtifactFields: true });
|
|
132
|
+
};
|
|
133
|
+
allowText(value, 'subscriptionId', 'billingGenerationId', 'currencyCode', 'currencySymbol', 'forecastMethod');
|
|
134
|
+
allowControl(value, 'schemaVersion', 'ownership', 'revision', 'artifactEvidence');
|
|
135
|
+
allowChildren(value, 'ownership', 'revision', 'artifactEvidence', 'chartData', 'anomalies');
|
|
136
|
+
if (validationBranch !== 'business-v1')
|
|
137
|
+
allowControl(value, 'artifactState');
|
|
138
|
+
if (validationBranch === 'legacy-fallback')
|
|
139
|
+
allowControl(value, 'artifactSource');
|
|
140
|
+
allowDigest(value, 'inputManifestDigest');
|
|
141
|
+
allowDigest(value, 'outputBindingDigest');
|
|
142
|
+
if (isRecord(value.ownership)) {
|
|
143
|
+
allowControl(value.ownership, 'provider', 'ownershipEpochRevision');
|
|
144
|
+
allowText(value.ownership, 'tenantId', 'companyId', 'cloudAccountId', 'accountId');
|
|
145
|
+
}
|
|
146
|
+
if (isRecord(value.revision))
|
|
147
|
+
allowControl(value.revision, 'ownershipEpochRevision', 'sourceRevision', 'policyRevision');
|
|
148
|
+
if (isRecord(value.artifactEvidence)) {
|
|
149
|
+
const evidence = value.artifactEvidence;
|
|
150
|
+
allowControl(evidence, 'processing', 'evidence', 'publication', 'dependencies', 'claims', 'issues');
|
|
151
|
+
allowChildren(evidence, 'dependencies', 'claims', 'issues');
|
|
152
|
+
if (Array.isArray(evidence.dependencies)) {
|
|
153
|
+
for (const dependency of evidence.dependencies) {
|
|
154
|
+
allowControl(dependency, 'required', 'support', 'applicability', 'attempt', 'coverage', 'emptyEvidence', 'freshness', 'evidence', 'publication', 'sourceRevision', 'policyRevision', 'acceptedRowCount', 'emptyProofRef');
|
|
155
|
+
allowText(dependency, 'name', 'reasonCode', 'generationId');
|
|
156
|
+
allowDigest(dependency, 'digest');
|
|
157
|
+
if (isRecord(dependency) && isRecord(dependency.observedRange)) {
|
|
158
|
+
allowChildren(dependency, 'observedRange');
|
|
159
|
+
allowText(dependency.observedRange, 'timeZone');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (Array.isArray(evidence.claims)) {
|
|
164
|
+
for (const claim of evidence.claims) {
|
|
165
|
+
allowControl(claim, 'evidence', 'publication', 'issues');
|
|
166
|
+
allowText(claim, 'claimId');
|
|
167
|
+
allowTextArray(claim, 'sectionPaths');
|
|
168
|
+
allowTextArray(claim, 'requiredDependencies');
|
|
169
|
+
if (!isRecord(claim) || !Array.isArray(claim.issues))
|
|
170
|
+
continue;
|
|
171
|
+
allowChildren(claim, 'issues');
|
|
172
|
+
for (const issue of claim.issues) {
|
|
173
|
+
allowControl(issue, 'blocking');
|
|
174
|
+
allowText(issue, 'code', 'dependency');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(evidence.issues)) {
|
|
179
|
+
for (const issue of evidence.issues) {
|
|
180
|
+
allowControl(issue, 'blocking');
|
|
181
|
+
allowText(issue, 'code', 'dependency');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (isRecord(value.chartData)) {
|
|
186
|
+
allowControl(value.chartData, 'schemaVersion');
|
|
187
|
+
allowText(value.chartData, 'source');
|
|
188
|
+
allowChildren(value.chartData, 'dataWindow', 'views', 'detectors');
|
|
189
|
+
if (isRecord(value.chartData.views)) {
|
|
190
|
+
for (const [viewKey, view] of Object.entries(value.chartData.views)) {
|
|
191
|
+
allowChildren(value.chartData.views, viewKey);
|
|
192
|
+
allowText(view, 'aggregation', 'forecastMethod');
|
|
193
|
+
if (!isRecord(view))
|
|
194
|
+
continue;
|
|
195
|
+
allowChildren(view, 'trend', 'points', 'actualPoints', 'forecastPoints', 'fittedPoints');
|
|
196
|
+
allowText(view.trend, 'method');
|
|
197
|
+
for (const pointsKey of ['points', 'actualPoints', 'forecastPoints', 'fittedPoints']) {
|
|
198
|
+
const points = view[pointsKey];
|
|
199
|
+
if (!Array.isArray(points))
|
|
200
|
+
continue;
|
|
201
|
+
for (const point of points) {
|
|
202
|
+
allowText(point, 'date', 'month');
|
|
203
|
+
allowTextArray(point, 'anomalyMethods');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (isRecord(value.chartData.detectors) && Array.isArray(value.chartData.detectors.methods)) {
|
|
209
|
+
allowChildren(value.chartData.detectors, 'methods');
|
|
210
|
+
for (const method of value.chartData.detectors.methods)
|
|
211
|
+
allowText(method, 'name', 'status', 'error');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!Array.isArray(value.anomalies))
|
|
215
|
+
return fields;
|
|
216
|
+
for (const anomaly of value.anomalies) {
|
|
217
|
+
allowText(anomaly, 'summary', 'confidence');
|
|
218
|
+
allowTextArray(anomaly, 'notes');
|
|
219
|
+
if (!isRecord(anomaly) || !Array.isArray(anomaly.drivers))
|
|
220
|
+
continue;
|
|
221
|
+
allowChildren(anomaly, 'impact', 'drivers');
|
|
222
|
+
for (const driver of anomaly.drivers) {
|
|
223
|
+
allowText(driver, 'type', 'name', 'summary');
|
|
224
|
+
if (!isRecord(driver) || !Array.isArray(driver.resources))
|
|
225
|
+
continue;
|
|
226
|
+
allowChildren(driver, 'resources');
|
|
227
|
+
for (const resource of driver.resources)
|
|
228
|
+
allowText(resource, 'name', 'resourceScope', 'summary');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return fields;
|
|
232
|
+
};
|
|
233
|
+
const containsForbiddenBillingCostAnalysisControlData = (value, validationBranch) => containsForbiddenArtifactControlData(value, allowedBillingCostAnalysisFields(value, validationBranch), {
|
|
234
|
+
rejectDigestLikeValues: true,
|
|
235
|
+
requireSafeAzureResourceIds: true,
|
|
236
|
+
forbiddenControlFields: BILLING_FORBIDDEN_CONTROL_FIELDS,
|
|
237
|
+
requireAllowedFieldTraversalContext: true,
|
|
238
|
+
});
|
|
21
239
|
const isTrend = (value) => isRecord(value) && isNonEmptyString(value.method) && isFiniteNumber(value.slope) && isFiniteNumber(value.intercept);
|
|
22
240
|
const isDailyPoint = (value) => isRecord(value) &&
|
|
23
241
|
isNonEmptyString(value.date) &&
|
|
@@ -139,7 +357,11 @@ const hasValidMetadataEvidenceState = (state, evidence, billingGenerationId, inp
|
|
|
139
357
|
return false;
|
|
140
358
|
if (state !== 'complete-empty')
|
|
141
359
|
return true;
|
|
360
|
+
if (!Array.isArray(anomalies))
|
|
361
|
+
return false;
|
|
142
362
|
const billingHistory = evidence.dependencies.find(dependency => dependency.name === 'billing-history');
|
|
363
|
+
if (!isRecord(chartData))
|
|
364
|
+
return false;
|
|
143
365
|
const dataWindow = chartData.dataWindow;
|
|
144
366
|
const views = chartData.views;
|
|
145
367
|
return (billingHistory?.emptyEvidence === 'complete-empty' &&
|
|
@@ -151,11 +373,27 @@ const hasValidMetadataEvidenceState = (state, evidence, billingGenerationId, inp
|
|
|
151
373
|
Object.values(views).every(view => view === undefined) &&
|
|
152
374
|
anomalies.length === 0);
|
|
153
375
|
};
|
|
376
|
+
const hasValidBillingCostAnalysisBusinessFields = (value) => {
|
|
377
|
+
if (!isPathSegment(value.subscriptionId) || !isPathSegment(value.billingGenerationId))
|
|
378
|
+
return false;
|
|
379
|
+
if (!isChartData(value.chartData) || !Array.isArray(value.anomalies) || !value.anomalies.every(isAnomaly))
|
|
380
|
+
return false;
|
|
381
|
+
if (!isNonEmptyString(value.currencyCode) || !isNonEmptyString(value.currencySymbol))
|
|
382
|
+
return false;
|
|
383
|
+
if (value.forecastMethod !== undefined && !isNonEmptyString(value.forecastMethod))
|
|
384
|
+
return false;
|
|
385
|
+
return [value.forecastMonthTotal, value.forecastRemaining, value.forecastPeriodEnd].every(isOptionalFiniteNumber);
|
|
386
|
+
};
|
|
387
|
+
const isBillingCostAnalysisBusinessPayloadForBranch = (value, validationBranch) => !containsForbiddenBillingCostAnalysisControlData(value, validationBranch) &&
|
|
388
|
+
!LEGACY_FALLBACK_FORBIDDEN_OWN_FIELDS.some(field => hasOwn(value, field)) &&
|
|
389
|
+
hasValidBillingCostAnalysisBusinessFields(value);
|
|
390
|
+
/** Dependency-free validator for the complete legacy V1 business payload. */
|
|
391
|
+
export const isBillingCostAnalysisBusinessPayloadV1 = (value) => isRecord(value) && isBillingCostAnalysisBusinessPayloadForBranch(value, 'business-v1');
|
|
154
392
|
/** Dependency-free validator for customer-readable V2 billing metadata. */
|
|
155
393
|
export const isBillingCostAnalysisMetadataV2 = (value) => {
|
|
156
|
-
if (!isRecord(value) ||
|
|
394
|
+
if (!isRecord(value) || containsForbiddenBillingCostAnalysisControlData(value, 'metadata-v2') || value.schemaVersion !== 2)
|
|
157
395
|
return false;
|
|
158
|
-
if (
|
|
396
|
+
if (hasOwn(value, 'outputManifestDigest'))
|
|
159
397
|
return false;
|
|
160
398
|
if (!isPathSegment(value.subscriptionId) || !isPathSegment(value.billingGenerationId))
|
|
161
399
|
return false;
|
|
@@ -174,14 +412,19 @@ export const isBillingCostAnalysisMetadataV2 = (value) => {
|
|
|
174
412
|
publicationDecisionReferencesDigest(value.artifactEvidence, value.outputBindingDigest)) {
|
|
175
413
|
return false;
|
|
176
414
|
}
|
|
177
|
-
if (!
|
|
415
|
+
if (!hasValidBillingCostAnalysisBusinessFields(value))
|
|
178
416
|
return false;
|
|
179
417
|
if (!hasValidMetadataEvidenceState(value.artifactState, value.artifactEvidence, value.billingGenerationId, value.inputManifestDigest, value.chartData, value.anomalies)) {
|
|
180
418
|
return false;
|
|
181
419
|
}
|
|
182
|
-
|
|
183
|
-
return false;
|
|
184
|
-
if (value.forecastMethod !== undefined && !isNonEmptyString(value.forecastMethod))
|
|
185
|
-
return false;
|
|
186
|
-
return [value.forecastMonthTotal, value.forecastRemaining, value.forecastPeriodEnd].every(isOptionalFiniteNumber);
|
|
420
|
+
return true;
|
|
187
421
|
};
|
|
422
|
+
/** Dependency-free validator for an explicit legacy-transition fallback response. */
|
|
423
|
+
export const isBillingCostAnalysisLegacyFallbackResponse = (value) => isRecord(value) &&
|
|
424
|
+
value.artifactState === 'fallback' &&
|
|
425
|
+
value.artifactSource === 'legacy-transition' &&
|
|
426
|
+
isBillingCostAnalysisBusinessPayloadForBranch(value, 'legacy-fallback');
|
|
427
|
+
/** Dependency-free validator for an evidence-verified endpoint response. */
|
|
428
|
+
export const isBillingCostAnalysisVerifiedReadResponse = (value) => isBillingCostAnalysisMetadataV2(value) && BILLING_VERIFIED_READ_STATES.has(value.artifactState);
|
|
429
|
+
/** Dependency-free validator for the successful billing read-response union. */
|
|
430
|
+
export const isBillingCostAnalysisReadResponse = (value) => isBillingCostAnalysisVerifiedReadResponse(value) || isBillingCostAnalysisLegacyFallbackResponse(value);
|