@agentplat/mesh-protocol 0.3.0-alpha.2 → 0.3.0-alpha.4
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/dist/contracts.d.ts +117 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +542 -15
- package/dist/validation.js.map +1 -1
- package/fixtures/v0/README.md +5 -0
- package/fixtures/v0/evidence-attest.json +27 -0
- package/fixtures/v0/evidence-challenge.json +26 -0
- package/fixtures/v0/evidence-claim.json +27 -0
- package/fixtures/v0/evidence-retract.json +26 -0
- package/fixtures/v0/trust-observation.json +34 -0
- package/package.json +4 -1
package/dist/validation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalizeJsonValue } from './canonical-json.js';
|
|
2
|
+
import { normalizeMeshEvidenceAttestationV1, normalizeMeshEvidenceChallengeV1, normalizeMeshEvidenceClaimV1, normalizeMeshEvidenceRetractionV1, normalizeMeshTrustObservationV1, } from '@agentplat/trust/mesh-records';
|
|
2
3
|
import { DEFAULT_MESH_PROTOCOL_LIMITS, MESH_AUDIENCE_TOPICS, MESH_MESSAGE_TYPES, MESH_PROTOCOL, MESH_SIGNATURE_ALGORITHM, MESH_WIRE_VERSION, } from './contracts.js';
|
|
3
4
|
import { parseStrictJsonDocument, } from './strict-json.js';
|
|
4
5
|
const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:@-]*$/;
|
|
@@ -42,6 +43,13 @@ const maximumCapacityReservationUnits = 1_000_000;
|
|
|
42
43
|
const maximumWorkBidWindowMs = 60 * 60 * 1_000;
|
|
43
44
|
const maximumWorkDeadlineMs = 30 * 24 * 60 * 60 * 1_000;
|
|
44
45
|
const maximumWorkExecutionLifetimeMs = 5 * 60 * 1_000;
|
|
46
|
+
const maximumEvidenceLifetimeMs = 5 * 60 * 1_000;
|
|
47
|
+
const maximumEvidenceReferences = 32;
|
|
48
|
+
const maximumEvidenceReferenceBytes = 4_096;
|
|
49
|
+
const maximumEvidenceContentBytes = 4_096;
|
|
50
|
+
const maximumEvidenceIdentifierBytes = 256;
|
|
51
|
+
const maximumEvidenceReasonCodes = 32;
|
|
52
|
+
const trustDigestPattern = /^[0-9a-f]{64}$/;
|
|
45
53
|
const knownMessageTypes = new Set(MESH_MESSAGE_TYPES);
|
|
46
54
|
const knownTopics = new Set(MESH_AUDIENCE_TOPICS);
|
|
47
55
|
const utf8Encoder = new TextEncoder();
|
|
@@ -288,6 +296,7 @@ function validateEnvelope(input, limits) {
|
|
|
288
296
|
fail('type_payload_mismatch', '$["payload"]["type"]');
|
|
289
297
|
}
|
|
290
298
|
validateMessageSpecificEnvelope(type, sender, audience, objectiveId, causationId, payload, sentAt, expiresAt);
|
|
299
|
+
validateEvidenceTrustBinding(tenantId, meshId, objectiveId, sender, causationId, payload);
|
|
291
300
|
return {
|
|
292
301
|
protocol: MESH_PROTOCOL,
|
|
293
302
|
wireVersion: MESH_WIRE_VERSION,
|
|
@@ -573,8 +582,367 @@ function validatePayload(type, input, limits) {
|
|
|
573
582
|
if (type === 'lease.certificate') {
|
|
574
583
|
return validateLeaseCertificate(input, limits);
|
|
575
584
|
}
|
|
585
|
+
if (type === 'evidence.claim') {
|
|
586
|
+
return validateEvidenceClaim(input, limits);
|
|
587
|
+
}
|
|
588
|
+
if (type === 'evidence.attest') {
|
|
589
|
+
return validateEvidenceAttestation(input, limits);
|
|
590
|
+
}
|
|
591
|
+
if (type === 'evidence.challenge') {
|
|
592
|
+
return validateEvidenceChallenge(input, limits);
|
|
593
|
+
}
|
|
594
|
+
if (type === 'evidence.retract') {
|
|
595
|
+
return validateEvidenceRetraction(input, limits);
|
|
596
|
+
}
|
|
597
|
+
if (type === 'trust.observation') {
|
|
598
|
+
return validateTrustObservation(input, limits);
|
|
599
|
+
}
|
|
576
600
|
return fail('unsupported_message_type', '$["type"]');
|
|
577
601
|
}
|
|
602
|
+
function validateEvidenceClaim(input, limits) {
|
|
603
|
+
const payload = assertClosedRecord(input, [
|
|
604
|
+
'assertionDigest',
|
|
605
|
+
'basisReferences',
|
|
606
|
+
'claimId',
|
|
607
|
+
'content',
|
|
608
|
+
'criterionId',
|
|
609
|
+
'observedAt',
|
|
610
|
+
'outcome',
|
|
611
|
+
'scope',
|
|
612
|
+
'subject',
|
|
613
|
+
'type',
|
|
614
|
+
], [], '$["payload"]', 'invalid_payload');
|
|
615
|
+
const content = validateEvidenceContent(payload.content, '$["payload"]["content"]', limits);
|
|
616
|
+
return {
|
|
617
|
+
type: assertPayloadType(payload.type, 'evidence.claim'),
|
|
618
|
+
claimId: assertTrustRecordId(payload.claimId, 'claim', '$["payload"]["claimId"]'),
|
|
619
|
+
subject: validateEvidenceSubject(payload.subject, '$["payload"]["subject"]', limits),
|
|
620
|
+
scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
|
|
621
|
+
criterionId: assertBoundedString(payload.criterionId, '$["payload"]["criterionId"]', maximumDomainStringBytes),
|
|
622
|
+
outcome: assertEnum(payload.outcome, ['satisfied', 'violated', 'inconclusive'], '$["payload"]["outcome"]'),
|
|
623
|
+
assertionDigest: assertTrustDigest(payload.assertionDigest, '$["payload"]["assertionDigest"]'),
|
|
624
|
+
content,
|
|
625
|
+
basisReferences: validateEvidenceReferences(payload.basisReferences, '$["payload"]["basisReferences"]', limits, { allowControlRecords: false, minimum: 0 }),
|
|
626
|
+
observedAt: validateOptionalEvidenceTimestamp(payload.observedAt, '$["payload"]["observedAt"]'),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function validateEvidenceAttestation(input, limits) {
|
|
630
|
+
const payload = assertClosedRecord(input, [
|
|
631
|
+
'attestationId',
|
|
632
|
+
'basisReferences',
|
|
633
|
+
'claimDigest',
|
|
634
|
+
'claimId',
|
|
635
|
+
'confidenceBasisPoints',
|
|
636
|
+
'disposition',
|
|
637
|
+
'observedAt',
|
|
638
|
+
'scope',
|
|
639
|
+
'type',
|
|
640
|
+
], [], '$["payload"]', 'invalid_payload');
|
|
641
|
+
return {
|
|
642
|
+
type: assertPayloadType(payload.type, 'evidence.attest'),
|
|
643
|
+
attestationId: assertTrustRecordId(payload.attestationId, 'attestation', '$["payload"]["attestationId"]'),
|
|
644
|
+
scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
|
|
645
|
+
claimId: assertTrustRecordId(payload.claimId, 'claim', '$["payload"]["claimId"]'),
|
|
646
|
+
claimDigest: assertTrustDigest(payload.claimDigest, '$["payload"]["claimDigest"]'),
|
|
647
|
+
disposition: assertEnum(payload.disposition, ['support', 'contradict', 'inconclusive'], '$["payload"]["disposition"]'),
|
|
648
|
+
confidenceBasisPoints: assertBoundedSafeInteger(payload.confidenceBasisPoints, '$["payload"]["confidenceBasisPoints"]', 0, 10_000),
|
|
649
|
+
basisReferences: validateEvidenceReferences(payload.basisReferences, '$["payload"]["basisReferences"]', limits, { allowControlRecords: false, minimum: 0 }),
|
|
650
|
+
observedAt: validateOptionalEvidenceTimestamp(payload.observedAt, '$["payload"]["observedAt"]'),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function validateEvidenceChallenge(input, limits) {
|
|
654
|
+
const payload = assertClosedRecord(input, [
|
|
655
|
+
'basisReferences',
|
|
656
|
+
'challengeId',
|
|
657
|
+
'observedAt',
|
|
658
|
+
'reasonCode',
|
|
659
|
+
'scope',
|
|
660
|
+
'targetDigest',
|
|
661
|
+
'targetId',
|
|
662
|
+
'targetKind',
|
|
663
|
+
'type',
|
|
664
|
+
], [], '$["payload"]', 'invalid_payload');
|
|
665
|
+
const targetKind = assertEnum(payload.targetKind, ['claim', 'attestation'], '$["payload"]["targetKind"]');
|
|
666
|
+
return {
|
|
667
|
+
type: assertPayloadType(payload.type, 'evidence.challenge'),
|
|
668
|
+
challengeId: assertTrustRecordId(payload.challengeId, 'challenge', '$["payload"]["challengeId"]'),
|
|
669
|
+
scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
|
|
670
|
+
targetKind,
|
|
671
|
+
targetId: assertTrustRecordId(payload.targetId, targetKind, '$["payload"]["targetId"]'),
|
|
672
|
+
targetDigest: assertTrustDigest(payload.targetDigest, '$["payload"]["targetDigest"]'),
|
|
673
|
+
reasonCode: assertBoundedString(payload.reasonCode, '$["payload"]["reasonCode"]', maximumDomainStringBytes),
|
|
674
|
+
basisReferences: validateEvidenceReferences(payload.basisReferences, '$["payload"]["basisReferences"]', limits, { allowControlRecords: false, minimum: 1 }),
|
|
675
|
+
observedAt: validateOptionalEvidenceTimestamp(payload.observedAt, '$["payload"]["observedAt"]'),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function validateEvidenceRetraction(input, limits) {
|
|
679
|
+
const payload = assertClosedRecord(input, [
|
|
680
|
+
'observedAt',
|
|
681
|
+
'reasonCode',
|
|
682
|
+
'retractionId',
|
|
683
|
+
'scope',
|
|
684
|
+
'targetDigest',
|
|
685
|
+
'targetId',
|
|
686
|
+
'targetKind',
|
|
687
|
+
'type',
|
|
688
|
+
], [], '$["payload"]', 'invalid_payload');
|
|
689
|
+
const targetKind = assertEnum(payload.targetKind, ['claim', 'attestation'], '$["payload"]["targetKind"]');
|
|
690
|
+
return {
|
|
691
|
+
type: assertPayloadType(payload.type, 'evidence.retract'),
|
|
692
|
+
retractionId: assertTrustRecordId(payload.retractionId, 'retraction', '$["payload"]["retractionId"]'),
|
|
693
|
+
scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
|
|
694
|
+
targetKind,
|
|
695
|
+
targetId: assertTrustRecordId(payload.targetId, targetKind, '$["payload"]["targetId"]'),
|
|
696
|
+
targetDigest: assertTrustDigest(payload.targetDigest, '$["payload"]["targetDigest"]'),
|
|
697
|
+
reasonCode: assertBoundedString(payload.reasonCode, '$["payload"]["reasonCode"]', maximumDomainStringBytes),
|
|
698
|
+
observedAt: validateOptionalEvidenceTimestamp(payload.observedAt, '$["payload"]["observedAt"]'),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function validateTrustObservation(input, limits) {
|
|
702
|
+
const payload = assertClosedRecord(input, [
|
|
703
|
+
'dimensionId',
|
|
704
|
+
'disposition',
|
|
705
|
+
'evidenceIds',
|
|
706
|
+
'fusionDecisionDigest',
|
|
707
|
+
'observationId',
|
|
708
|
+
'observedAt',
|
|
709
|
+
'policyDigest',
|
|
710
|
+
'policyId',
|
|
711
|
+
'policyVersion',
|
|
712
|
+
'profileDigest',
|
|
713
|
+
'reasonCodes',
|
|
714
|
+
'scope',
|
|
715
|
+
'scoreBand',
|
|
716
|
+
'subject',
|
|
717
|
+
'type',
|
|
718
|
+
'uncertaintyBand',
|
|
719
|
+
'validUntil',
|
|
720
|
+
], [], '$["payload"]', 'invalid_payload');
|
|
721
|
+
const observedAt = assertRfc3339PayloadTimestamp(payload.observedAt, '$["payload"]["observedAt"]');
|
|
722
|
+
const validUntil = assertRfc3339PayloadTimestamp(payload.validUntil, '$["payload"]["validUntil"]');
|
|
723
|
+
if (validUntil.instant <= observedAt.instant ||
|
|
724
|
+
validUntil.instant - observedAt.instant > 24n * 60n * 60n * 1000000000n) {
|
|
725
|
+
fail('invalid_payload', '$["payload"]["validUntil"]');
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
type: assertPayloadType(payload.type, 'trust.observation'),
|
|
729
|
+
observationId: assertTrustRecordId(payload.observationId, 'observation', '$["payload"]["observationId"]'),
|
|
730
|
+
subject: validateEvidenceSubject(payload.subject, '$["payload"]["subject"]', limits),
|
|
731
|
+
scope: validateEvidenceScope(payload.scope, '$["payload"]["scope"]', limits),
|
|
732
|
+
policyId: assertBoundedString(payload.policyId, '$["payload"]["policyId"]', maximumDomainStringBytes),
|
|
733
|
+
policyVersion: assertPositiveSafeInteger(payload.policyVersion, '$["payload"]["policyVersion"]', 'invalid_payload'),
|
|
734
|
+
policyDigest: assertTrustDigest(payload.policyDigest, '$["payload"]["policyDigest"]'),
|
|
735
|
+
profileDigest: assertTrustDigest(payload.profileDigest, '$["payload"]["profileDigest"]'),
|
|
736
|
+
fusionDecisionDigest: assertTrustDigest(payload.fusionDecisionDigest, '$["payload"]["fusionDecisionDigest"]'),
|
|
737
|
+
dimensionId: assertBoundedString(payload.dimensionId, '$["payload"]["dimensionId"]', maximumDomainStringBytes),
|
|
738
|
+
scoreBand: assertEnum(payload.scoreBand, ['unknown', 'low', 'medium', 'high'], '$["payload"]["scoreBand"]'),
|
|
739
|
+
uncertaintyBand: assertEnum(payload.uncertaintyBand, ['low', 'medium', 'high'], '$["payload"]["uncertaintyBand"]'),
|
|
740
|
+
disposition: assertEnum(payload.disposition, ['eligible', 'restricted', 'quarantined', 'unavailable'], '$["payload"]["disposition"]'),
|
|
741
|
+
evidenceIds: validateTrustRecordIdArray(payload.evidenceIds, '$["payload"]["evidenceIds"]'),
|
|
742
|
+
observedAt: observedAt.text,
|
|
743
|
+
validUntil: validUntil.text,
|
|
744
|
+
reasonCodes: validateBoundedStringArray(payload.reasonCodes, '$["payload"]["reasonCodes"]', maximumEvidenceReasonCodes, maximumDomainStringBytes, undefined, true),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function validateEvidenceSubject(input, path, limits) {
|
|
748
|
+
const subject = assertRecord(input, path, 'invalid_payload');
|
|
749
|
+
if (subject.kind === 'peer') {
|
|
750
|
+
assertExactKeys(subject, ['kind', 'peerId'], path, 'invalid_payload');
|
|
751
|
+
return {
|
|
752
|
+
kind: 'peer',
|
|
753
|
+
peerId: assertIdentifier(subject.peerId, `${path}["peerId"]`, limits),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (subject.kind === 'peer_capability') {
|
|
757
|
+
assertExactKeys(subject, [
|
|
758
|
+
'capabilityKey',
|
|
759
|
+
'capabilityRevision',
|
|
760
|
+
'capabilityVersion',
|
|
761
|
+
'kind',
|
|
762
|
+
'peerId',
|
|
763
|
+
], path, 'invalid_payload');
|
|
764
|
+
return {
|
|
765
|
+
kind: 'peer_capability',
|
|
766
|
+
peerId: assertIdentifier(subject.peerId, `${path}["peerId"]`, limits),
|
|
767
|
+
capabilityKey: assertBoundedString(subject.capabilityKey, `${path}["capabilityKey"]`, maximumDomainStringBytes),
|
|
768
|
+
capabilityVersion: assertBoundedString(subject.capabilityVersion, `${path}["capabilityVersion"]`, maximumVersionBytes),
|
|
769
|
+
capabilityRevision: assertPositiveSafeInteger(subject.capabilityRevision, `${path}["capabilityRevision"]`, 'invalid_payload'),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
return fail('invalid_payload', `${path}["kind"]`);
|
|
773
|
+
}
|
|
774
|
+
function validateEvidenceScope(input, path, limits) {
|
|
775
|
+
const scope = assertRecord(input, path, 'invalid_payload');
|
|
776
|
+
if (scope.kind === 'mesh') {
|
|
777
|
+
assertExactKeys(scope, ['kind'], path, 'invalid_payload');
|
|
778
|
+
return { kind: 'mesh' };
|
|
779
|
+
}
|
|
780
|
+
if (scope.kind === 'objective') {
|
|
781
|
+
assertExactKeys(scope, ['kind', 'objectiveRevision'], path, 'invalid_payload');
|
|
782
|
+
return {
|
|
783
|
+
kind: 'objective',
|
|
784
|
+
objectiveRevision: assertPositiveSafeInteger(scope.objectiveRevision, `${path}["objectiveRevision"]`, 'invalid_payload'),
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (scope.kind === 'work') {
|
|
788
|
+
assertExactKeys(scope, [
|
|
789
|
+
'assignmentAuthorityId',
|
|
790
|
+
'assignmentEpoch',
|
|
791
|
+
'fencingToken',
|
|
792
|
+
'kind',
|
|
793
|
+
'objectiveRevision',
|
|
794
|
+
'workItemId',
|
|
795
|
+
'workItemRevision',
|
|
796
|
+
], path, 'invalid_payload');
|
|
797
|
+
const assignmentAuthorityId = assertIdentifier(scope.assignmentAuthorityId, `${path}["assignmentAuthorityId"]`, limits);
|
|
798
|
+
const fencingToken = assertIdentifier(scope.fencingToken, `${path}["fencingToken"]`, limits);
|
|
799
|
+
return {
|
|
800
|
+
kind: 'work',
|
|
801
|
+
objectiveRevision: assertPositiveSafeInteger(scope.objectiveRevision, `${path}["objectiveRevision"]`, 'invalid_payload'),
|
|
802
|
+
workItemId: assertIdentifier(scope.workItemId, `${path}["workItemId"]`, limits),
|
|
803
|
+
workItemRevision: assertPositiveSafeInteger(scope.workItemRevision, `${path}["workItemRevision"]`, 'invalid_payload'),
|
|
804
|
+
assignmentEpoch: assertPositiveSafeInteger(scope.assignmentEpoch, `${path}["assignmentEpoch"]`, 'invalid_payload'),
|
|
805
|
+
assignmentAuthorityId,
|
|
806
|
+
fencingToken,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
return fail('invalid_payload', `${path}["kind"]`);
|
|
810
|
+
}
|
|
811
|
+
function validateEvidenceReferences(input, path, limits, { allowControlRecords, minimum, }) {
|
|
812
|
+
if (!Array.isArray(input) ||
|
|
813
|
+
input.length < minimum ||
|
|
814
|
+
input.length > maximumEvidenceReferences) {
|
|
815
|
+
fail('invalid_payload', path);
|
|
816
|
+
}
|
|
817
|
+
const references = input.map((item, index) => validateEvidenceReference(item, `${path}[${index}]`, limits, allowControlRecords));
|
|
818
|
+
let previous = '';
|
|
819
|
+
for (const reference of references) {
|
|
820
|
+
const order = JSON.stringify([
|
|
821
|
+
reference.kind,
|
|
822
|
+
reference.referenceType,
|
|
823
|
+
reference.referenceId,
|
|
824
|
+
reference.referenceDigest,
|
|
825
|
+
]);
|
|
826
|
+
if (order <= previous)
|
|
827
|
+
fail('invalid_payload', path);
|
|
828
|
+
previous = order;
|
|
829
|
+
}
|
|
830
|
+
return Object.freeze(references);
|
|
831
|
+
}
|
|
832
|
+
function validateEvidenceReference(input, path, limits, allowControlRecords) {
|
|
833
|
+
const reference = assertClosedRecord(input, [
|
|
834
|
+
'kind',
|
|
835
|
+
'referenceDigest',
|
|
836
|
+
'referenceId',
|
|
837
|
+
'referenceType',
|
|
838
|
+
'schemaVersion',
|
|
839
|
+
], [], path, 'invalid_payload');
|
|
840
|
+
if (reference.schemaVersion !== 1)
|
|
841
|
+
fail('invalid_payload', `${path}["schemaVersion"]`);
|
|
842
|
+
const kind = assertEnum(reference.kind, ['evidence', 'mesh_record', 'control_record', 'external'], `${path}["kind"]`);
|
|
843
|
+
if (!allowControlRecords && kind === 'control_record') {
|
|
844
|
+
fail('invalid_payload', `${path}["kind"]`);
|
|
845
|
+
}
|
|
846
|
+
const referenceDigest = assertEvidenceReferenceDigest(kind, reference.referenceDigest, `${path}["referenceDigest"]`);
|
|
847
|
+
return {
|
|
848
|
+
schemaVersion: 1,
|
|
849
|
+
kind,
|
|
850
|
+
referenceType: assertBoundedString(reference.referenceType, `${path}["referenceType"]`, maximumEvidenceReferenceBytes),
|
|
851
|
+
referenceId: assertBoundedString(reference.referenceId, `${path}["referenceId"]`, maximumEvidenceReferenceBytes),
|
|
852
|
+
referenceDigest,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function validateEvidenceContent(input, path, limits) {
|
|
856
|
+
if (input === null)
|
|
857
|
+
return null;
|
|
858
|
+
const content = assertRecord(input, path, 'invalid_payload');
|
|
859
|
+
if (content.kind === 'inline_summary') {
|
|
860
|
+
assertExactKeys(content, ['contentDigest', 'encodedBytes', 'kind', 'mediaType', 'summary'], path, 'invalid_payload');
|
|
861
|
+
const summary = assertBoundedString(content.summary, `${path}["summary"]`, maximumEvidenceContentBytes);
|
|
862
|
+
const encodedBytes = assertBoundedSafeInteger(content.encodedBytes, `${path}["encodedBytes"]`, 0, maximumEvidenceContentBytes);
|
|
863
|
+
if (utf8Encoder.encode(summary).byteLength !== encodedBytes) {
|
|
864
|
+
fail('invalid_payload', `${path}["encodedBytes"]`);
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
kind: 'inline_summary',
|
|
868
|
+
mediaType: assertBoundedString(content.mediaType, `${path}["mediaType"]`, maximumMediaTypeBytes),
|
|
869
|
+
summary,
|
|
870
|
+
contentDigest: assertTrustDigest(content.contentDigest, `${path}["contentDigest"]`),
|
|
871
|
+
encodedBytes,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
if (content.kind === 'reference') {
|
|
875
|
+
assertExactKeys(content, ['contentDigest', 'encodedBytes', 'kind', 'mediaType', 'reference'], path, 'invalid_payload');
|
|
876
|
+
const reference = validateEvidenceReference(content.reference, `${path}["reference"]`, limits, false);
|
|
877
|
+
if (reference.kind !== 'mesh_record' && reference.kind !== 'external') {
|
|
878
|
+
fail('invalid_payload', `${path}["reference"]["kind"]`);
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
kind: 'reference',
|
|
882
|
+
mediaType: assertBoundedString(content.mediaType, `${path}["mediaType"]`, maximumMediaTypeBytes),
|
|
883
|
+
reference,
|
|
884
|
+
contentDigest: assertTrustDigest(content.contentDigest, `${path}["contentDigest"]`),
|
|
885
|
+
encodedBytes: assertBoundedSafeInteger(content.encodedBytes, `${path}["encodedBytes"]`, 0, maximumEvidenceContentBytes),
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
return fail('invalid_payload', `${path}["kind"]`);
|
|
889
|
+
}
|
|
890
|
+
function validateTrustRecordIdArray(input, path) {
|
|
891
|
+
if (!Array.isArray(input) || input.length > maximumEvidenceReferences) {
|
|
892
|
+
fail('invalid_payload', path);
|
|
893
|
+
}
|
|
894
|
+
const ids = input.map((value, index) => assertBoundedString(value, `${path}[${index}]`, maximumEvidenceIdentifierBytes));
|
|
895
|
+
let previous = '';
|
|
896
|
+
for (const id of ids) {
|
|
897
|
+
if (id <= previous)
|
|
898
|
+
fail('invalid_payload', path);
|
|
899
|
+
previous = id;
|
|
900
|
+
}
|
|
901
|
+
return Object.freeze(ids);
|
|
902
|
+
}
|
|
903
|
+
function validateOptionalEvidenceTimestamp(input, path) {
|
|
904
|
+
if (input === null)
|
|
905
|
+
return null;
|
|
906
|
+
return assertRfc3339PayloadTimestamp(input, path).text;
|
|
907
|
+
}
|
|
908
|
+
function assertTrustRecordId(input, prefix, path) {
|
|
909
|
+
const value = assertBoundedString(input, path, maximumEvidenceIdentifierBytes);
|
|
910
|
+
if (!new RegExp(`^${prefix}:[0-9a-f]{64}$`).test(value)) {
|
|
911
|
+
fail('invalid_payload', path);
|
|
912
|
+
}
|
|
913
|
+
return value;
|
|
914
|
+
}
|
|
915
|
+
function assertTrustDigest(input, path) {
|
|
916
|
+
const value = assertString(input, path, 'invalid_payload');
|
|
917
|
+
if (!trustDigestPattern.test(value))
|
|
918
|
+
fail('invalid_payload', path);
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
function assertEvidenceReferenceDigest(kind, input, path) {
|
|
922
|
+
if (kind === 'mesh_record')
|
|
923
|
+
return assertPayloadHash(input, path);
|
|
924
|
+
if (kind === 'control_record') {
|
|
925
|
+
const value = assertString(input, path, 'invalid_payload');
|
|
926
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(value))
|
|
927
|
+
fail('invalid_payload', path);
|
|
928
|
+
return value;
|
|
929
|
+
}
|
|
930
|
+
return assertTrustDigest(input, path);
|
|
931
|
+
}
|
|
932
|
+
function assertBoundedSafeInteger(input, path, minimum, maximum) {
|
|
933
|
+
if (!Number.isSafeInteger(input) ||
|
|
934
|
+
input < minimum ||
|
|
935
|
+
input > maximum) {
|
|
936
|
+
fail('invalid_payload', path);
|
|
937
|
+
}
|
|
938
|
+
return input;
|
|
939
|
+
}
|
|
940
|
+
function assertEnum(input, values, path) {
|
|
941
|
+
if (typeof input !== 'string' || !values.includes(input)) {
|
|
942
|
+
fail('invalid_payload', path);
|
|
943
|
+
}
|
|
944
|
+
return input;
|
|
945
|
+
}
|
|
578
946
|
function validateWorkOffer(input, limits) {
|
|
579
947
|
const payload = assertClosedRecord(input, [
|
|
580
948
|
'bidDeadline',
|
|
@@ -1578,6 +1946,19 @@ function validateMessageSpecificEnvelope(type, sender, audience, objectiveId, ca
|
|
|
1578
1946
|
fail('invalid_audience', '$["audience"]["peerId"]');
|
|
1579
1947
|
}
|
|
1580
1948
|
}
|
|
1949
|
+
else if (type === 'evidence.claim' ||
|
|
1950
|
+
type === 'evidence.attest' ||
|
|
1951
|
+
type === 'evidence.challenge' ||
|
|
1952
|
+
type === 'evidence.retract') {
|
|
1953
|
+
if (audience.kind === 'mesh' && audience.topic !== 'evidence') {
|
|
1954
|
+
fail('invalid_audience', '$["audience"]["topic"]');
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
else if (type === 'trust.observation') {
|
|
1958
|
+
if (audience.kind !== 'peer') {
|
|
1959
|
+
fail('invalid_audience', '$["audience"]');
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1581
1962
|
else if (type === 'work.award' ||
|
|
1582
1963
|
type === 'work.accept' ||
|
|
1583
1964
|
type === 'work.decline' ||
|
|
@@ -1658,6 +2039,30 @@ function validateMessageSpecificEnvelope(type, sender, audience, objectiveId, ca
|
|
|
1658
2039
|
causationId === undefined) {
|
|
1659
2040
|
fail('invalid_payload', '$["causationId"]');
|
|
1660
2041
|
}
|
|
2042
|
+
if (type === 'evidence.claim' ||
|
|
2043
|
+
type === 'evidence.attest' ||
|
|
2044
|
+
type === 'evidence.challenge' ||
|
|
2045
|
+
type === 'evidence.retract') {
|
|
2046
|
+
const evidencePayload = payload;
|
|
2047
|
+
if (evidencePayload.scope.kind === 'mesh') {
|
|
2048
|
+
if (objectiveId !== undefined) {
|
|
2049
|
+
fail('invalid_payload', '$["objectiveId"]');
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
else if (objectiveId === undefined) {
|
|
2053
|
+
fail('invalid_payload', '$["objectiveId"]');
|
|
2054
|
+
}
|
|
2055
|
+
const references = evidencePayload.type === 'evidence.claim' ||
|
|
2056
|
+
evidencePayload.type === 'evidence.attest' ||
|
|
2057
|
+
evidencePayload.type === 'evidence.challenge'
|
|
2058
|
+
? evidencePayload.basisReferences
|
|
2059
|
+
: [];
|
|
2060
|
+
if ((evidencePayload.scope.kind === 'work' ||
|
|
2061
|
+
references.some((reference) => reference.kind === 'mesh_record')) &&
|
|
2062
|
+
causationId === undefined) {
|
|
2063
|
+
fail('invalid_payload', '$["causationId"]');
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
1661
2066
|
if (type === 'objective.announce' ||
|
|
1662
2067
|
type === 'objective.revise' ||
|
|
1663
2068
|
type === 'objective.cancel' ||
|
|
@@ -1705,6 +2110,117 @@ function validateMessageSpecificEnvelope(type, sender, audience, objectiveId, ca
|
|
|
1705
2110
|
validateWorkExecutionTimes(payload, sentAt, expiresAt);
|
|
1706
2111
|
}
|
|
1707
2112
|
}
|
|
2113
|
+
/**
|
|
2114
|
+
* The shared Trust normalizer owns every content-bound Evidence digest. Mesh
|
|
2115
|
+
* only supplies the signed envelope material, then compares the projected ID
|
|
2116
|
+
* (and Claim assertion digest) with the closed wire payload.
|
|
2117
|
+
*/
|
|
2118
|
+
function validateEvidenceTrustBinding(tenantId, meshId, objectiveId, sender, causationId, payload) {
|
|
2119
|
+
if (payload.type !== 'evidence.claim' &&
|
|
2120
|
+
payload.type !== 'evidence.attest' &&
|
|
2121
|
+
payload.type !== 'evidence.challenge' &&
|
|
2122
|
+
payload.type !== 'evidence.retract' &&
|
|
2123
|
+
payload.type !== 'trust.observation') {
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const envelope = {
|
|
2127
|
+
schemaVersion: 1,
|
|
2128
|
+
tenantId,
|
|
2129
|
+
meshId,
|
|
2130
|
+
objectiveId: objectiveId ?? null,
|
|
2131
|
+
senderPeerId: sender.peerId,
|
|
2132
|
+
causationId: causationId ?? null,
|
|
2133
|
+
};
|
|
2134
|
+
try {
|
|
2135
|
+
if (payload.type === 'evidence.claim') {
|
|
2136
|
+
const normalized = normalizeMeshEvidenceClaimV1(envelope, {
|
|
2137
|
+
subject: payload.subject,
|
|
2138
|
+
scope: payload.scope,
|
|
2139
|
+
criterionId: payload.criterionId,
|
|
2140
|
+
outcome: payload.outcome,
|
|
2141
|
+
content: payload.content,
|
|
2142
|
+
basisReferences: payload.basisReferences,
|
|
2143
|
+
observedAt: payload.observedAt,
|
|
2144
|
+
});
|
|
2145
|
+
if (normalized.claimId !== payload.claimId) {
|
|
2146
|
+
fail('invalid_payload', '$["payload"]["claimId"]');
|
|
2147
|
+
}
|
|
2148
|
+
if (normalized.assertionDigest !== payload.assertionDigest) {
|
|
2149
|
+
fail('invalid_payload', '$["payload"]["assertionDigest"]');
|
|
2150
|
+
}
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
if (payload.type === 'evidence.attest') {
|
|
2154
|
+
const normalized = normalizeMeshEvidenceAttestationV1(envelope, {
|
|
2155
|
+
scope: payload.scope,
|
|
2156
|
+
claimId: payload.claimId,
|
|
2157
|
+
claimDigest: payload.claimDigest,
|
|
2158
|
+
disposition: payload.disposition,
|
|
2159
|
+
confidenceBasisPoints: payload.confidenceBasisPoints,
|
|
2160
|
+
basisReferences: payload.basisReferences,
|
|
2161
|
+
observedAt: payload.observedAt,
|
|
2162
|
+
});
|
|
2163
|
+
if (normalized.attestationId !== payload.attestationId) {
|
|
2164
|
+
fail('invalid_payload', '$["payload"]["attestationId"]');
|
|
2165
|
+
}
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
if (payload.type === 'evidence.challenge') {
|
|
2169
|
+
const normalized = normalizeMeshEvidenceChallengeV1(envelope, {
|
|
2170
|
+
scope: payload.scope,
|
|
2171
|
+
targetKind: payload.targetKind,
|
|
2172
|
+
targetId: payload.targetId,
|
|
2173
|
+
targetDigest: payload.targetDigest,
|
|
2174
|
+
reasonCode: payload.reasonCode,
|
|
2175
|
+
basisReferences: payload.basisReferences,
|
|
2176
|
+
observedAt: payload.observedAt,
|
|
2177
|
+
});
|
|
2178
|
+
if (normalized.challengeId !== payload.challengeId) {
|
|
2179
|
+
fail('invalid_payload', '$["payload"]["challengeId"]');
|
|
2180
|
+
}
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (payload.type === 'trust.observation') {
|
|
2184
|
+
const normalized = normalizeMeshTrustObservationV1(envelope, {
|
|
2185
|
+
subject: payload.subject,
|
|
2186
|
+
scope: payload.scope,
|
|
2187
|
+
policyId: payload.policyId,
|
|
2188
|
+
policyVersion: payload.policyVersion,
|
|
2189
|
+
policyDigest: payload.policyDigest,
|
|
2190
|
+
profileDigest: payload.profileDigest,
|
|
2191
|
+
fusionDecisionDigest: payload.fusionDecisionDigest,
|
|
2192
|
+
dimensionId: payload.dimensionId,
|
|
2193
|
+
scoreBand: payload.scoreBand,
|
|
2194
|
+
uncertaintyBand: payload.uncertaintyBand,
|
|
2195
|
+
disposition: payload.disposition,
|
|
2196
|
+
evidenceIds: payload.evidenceIds,
|
|
2197
|
+
observedAt: payload.observedAt,
|
|
2198
|
+
validUntil: payload.validUntil,
|
|
2199
|
+
reasonCodes: payload.reasonCodes,
|
|
2200
|
+
});
|
|
2201
|
+
if (normalized.observationId !== payload.observationId) {
|
|
2202
|
+
fail('invalid_payload', '$["payload"]["observationId"]');
|
|
2203
|
+
}
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
const normalized = normalizeMeshEvidenceRetractionV1(envelope, {
|
|
2207
|
+
scope: payload.scope,
|
|
2208
|
+
targetKind: payload.targetKind,
|
|
2209
|
+
targetId: payload.targetId,
|
|
2210
|
+
targetDigest: payload.targetDigest,
|
|
2211
|
+
reasonCode: payload.reasonCode,
|
|
2212
|
+
observedAt: payload.observedAt,
|
|
2213
|
+
});
|
|
2214
|
+
if (normalized.retractionId !== payload.retractionId) {
|
|
2215
|
+
fail('invalid_payload', '$["payload"]["retractionId"]');
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
catch (error) {
|
|
2219
|
+
if (error instanceof ValidationFailure)
|
|
2220
|
+
throw error;
|
|
2221
|
+
fail('invalid_payload', '$["payload"]');
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
1708
2224
|
function validateWorkOfferTimes(payload, sentAt, expiresAt) {
|
|
1709
2225
|
const bidDeadline = parseRfc3339(payload.bidDeadline, '$["payload"]["bidDeadline"]');
|
|
1710
2226
|
const workDeadline = parseRfc3339(payload.workDeadline, '$["payload"]["workDeadline"]');
|
|
@@ -1973,20 +2489,26 @@ function validateLifetime(type, sentAt, expiresAt, limits) {
|
|
|
1973
2489
|
}
|
|
1974
2490
|
const messageMaximum = type === 'peer.ping' || type === 'peer.ping_ack'
|
|
1975
2491
|
? 30_000
|
|
1976
|
-
: type === '
|
|
1977
|
-
type === '
|
|
1978
|
-
type === '
|
|
1979
|
-
type === '
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2492
|
+
: type === 'evidence.claim' ||
|
|
2493
|
+
type === 'evidence.attest' ||
|
|
2494
|
+
type === 'evidence.challenge' ||
|
|
2495
|
+
type === 'evidence.retract' ||
|
|
2496
|
+
type === 'trust.observation'
|
|
2497
|
+
? maximumEvidenceLifetimeMs
|
|
2498
|
+
: type === 'peer.goodbye' ||
|
|
2499
|
+
type === 'lease.takeover_proposal' ||
|
|
2500
|
+
type === 'lease.vote' ||
|
|
2501
|
+
type === 'lease.certificate'
|
|
2502
|
+
? 60_000
|
|
2503
|
+
: type === 'objective.announce' || type === 'objective.revise'
|
|
2504
|
+
? 5 * 60_000
|
|
2505
|
+
: type === 'work.progress' ||
|
|
2506
|
+
type === 'work.checkpoint' ||
|
|
2507
|
+
type === 'work.result'
|
|
2508
|
+
? maximumWorkExecutionLifetimeMs
|
|
2509
|
+
: type === 'lease.renew'
|
|
2510
|
+
? 30_000
|
|
2511
|
+
: 120_000;
|
|
1990
2512
|
const maximum = Math.min(limits.maximumLifetimeMs, messageMaximum);
|
|
1991
2513
|
if (expiresAt - sentAt > BigInt(maximum) * 1000000n) {
|
|
1992
2514
|
fail('invalid_lifetime', '$["expiresAt"]');
|
|
@@ -2174,7 +2696,12 @@ function isImplementedMessageType(value) {
|
|
|
2174
2696
|
value === 'lease.renew' ||
|
|
2175
2697
|
value === 'lease.takeover_proposal' ||
|
|
2176
2698
|
value === 'lease.vote' ||
|
|
2177
|
-
value === 'lease.certificate'
|
|
2699
|
+
value === 'lease.certificate' ||
|
|
2700
|
+
value === 'evidence.claim' ||
|
|
2701
|
+
value === 'evidence.attest' ||
|
|
2702
|
+
value === 'evidence.challenge' ||
|
|
2703
|
+
value === 'evidence.retract' ||
|
|
2704
|
+
value === 'trust.observation');
|
|
2178
2705
|
}
|
|
2179
2706
|
function canonicalizeMeshJsonInternal(input, options) {
|
|
2180
2707
|
const limits = resolveLimits(options.limits);
|