@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.
Files changed (36) hide show
  1. package/README.md +28 -0
  2. package/dist/azure/billingArtifactCanonicalization.d.ts +3 -1
  3. package/dist/azure/billingArtifactCanonicalization.d.ts.map +1 -1
  4. package/dist/azure/billingArtifactCanonicalization.js +123 -43
  5. package/dist/azure/billingArtifactCanonicalization.js.map +1 -1
  6. package/dist/azure/billingArtifactGeneration.d.ts +53 -1
  7. package/dist/azure/billingArtifactGeneration.d.ts.map +1 -1
  8. package/dist/azure/billingArtifactGeneration.js +190 -25
  9. package/dist/azure/billingArtifactGeneration.js.map +1 -1
  10. package/dist/azure/billingArtifactLimits.d.ts +19 -0
  11. package/dist/azure/billingArtifactLimits.d.ts.map +1 -0
  12. package/dist/azure/billingArtifactLimits.js +22 -0
  13. package/dist/azure/billingArtifactLimits.js.map +1 -0
  14. package/dist/azure/billingPlots.d.ts +26 -3
  15. package/dist/azure/billingPlots.d.ts.map +1 -1
  16. package/dist/azure/billingPlots.js +257 -10
  17. package/dist/azure/billingPlots.js.map +1 -1
  18. package/dist/azure/views.d.ts.map +1 -1
  19. package/dist/azure/views.js +15 -3
  20. package/dist/azure/views.js.map +1 -1
  21. package/dist/common/artifactControlData.d.ts +16 -2
  22. package/dist/common/artifactControlData.d.ts.map +1 -1
  23. package/dist/common/artifactControlData.js +129 -23
  24. package/dist/common/artifactControlData.js.map +1 -1
  25. package/dist/esm/azure/billingArtifactCanonicalization.js +121 -42
  26. package/dist/esm/azure/billingArtifactGeneration.js +186 -25
  27. package/dist/esm/azure/billingArtifactLimits.js +18 -0
  28. package/dist/esm/azure/billingPlots.js +253 -10
  29. package/dist/esm/azure/views.js +16 -4
  30. package/dist/esm/common/artifactControlData.js +127 -22
  31. package/dist/esm/index.js +1 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -0
  35. package/dist/index.js.map +1 -1
  36. 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
- return (isGenerationPath(value.path, prefix, outputManifestPath(subscriptionId, generationId)) &&
78
- isNonEmptyString(value.name) &&
79
- value.mediaType === 'application/json' &&
80
- isStringIn(value.contentEncoding, CONTENT_ENCODINGS) &&
81
- isNonNegativeInteger(value.byteLength) &&
82
- isSha256(value.sha256));
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 visit = (candidate) => {
88
- if (candidate === null || typeof candidate === 'string' || typeof candidate === 'boolean')
89
- return true;
90
- if (isFiniteNumber(candidate))
91
- return true;
92
- if (Array.isArray(candidate))
93
- return candidate.every(visit);
94
- return isRecord(candidate) && Object.getPrototypeOf(candidate) === Object.prototype && Object.values(candidate).every(visit);
95
- };
96
- return visit(value);
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
- containsForbiddenArtifactControlData(value, [...allowedArtifactIdentityField(value, 'publicationKey'), ...allowedDescriptorPaths(value.inputs)])) {
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) || containsForbiddenArtifactControlData(value, allowedArtifactReferenceField(value, 'manifestPath')))
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) || containsForbiddenArtifactControlData(value, allowedArtifactReferenceField(value, 'inputManifestPath')))
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
- containsForbiddenArtifactControlData(value, [
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 (!value.artifacts.some(artifact => artifact.path === `${outputGenerationPrefix(value.subscriptionId, value.generationId)}metadata.json`)) {
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
- containsForbiddenArtifactControlData(value, [
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, } from '../common/artifactEvidence.js';
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 BILLING_DOCUMENT_STATES = new Set(['current', 'stale', 'partial', 'fallback', 'complete-empty']);
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) || containsForbiddenArtifactControlData(value) || value.schemaVersion !== 2)
394
+ if (!isRecord(value) || containsForbiddenBillingCostAnalysisControlData(value, 'metadata-v2') || value.schemaVersion !== 2)
157
395
  return false;
158
- if (Object.prototype.hasOwnProperty.call(value, 'outputManifestDigest'))
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 (!isChartData(value.chartData) || !Array.isArray(value.anomalies) || !value.anomalies.every(isAnomaly))
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
- 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);
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);