@claudian-collab/protocol 3.2.0 → 3.3.0
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 +8 -6
- package/dist/CollabAuthorityTransfer.d.ts +1 -1
- package/dist/CollabCloudBinding.d.ts +1 -1
- package/dist/CollabCloudBinding.js +7 -0
- package/dist/CollabControlOperationCodecs.d.ts +18 -0
- package/dist/CollabControlOperationCodecs.js +2 -0
- package/dist/CollabProjectBackupCheckpoint.d.ts +93 -4
- package/dist/CollabProjectBackupCheckpoint.js +640 -12
- package/dist/CollabProjectMembership.d.ts +318 -0
- package/dist/CollabProjectMembership.js +714 -0
- package/dist/CollabProtocol.d.ts +2 -1
- package/dist/esm/CollabAuthorityTransfer.mjs +884 -0
- package/dist/esm/CollabCloudBinding.mjs +559 -0
- package/dist/esm/CollabCloudProjectEvent.mjs +150 -0
- package/dist/esm/CollabCloudProjectSnapshot.mjs +277 -0
- package/dist/esm/CollabConstants.mjs +31 -0
- package/dist/esm/CollabControlOperationCodecs.mjs +106 -0
- package/dist/esm/CollabError.mjs +175 -0
- package/dist/esm/CollabMarkdownProse.mjs +54 -0
- package/dist/esm/CollabMemberMentionParser.mjs +48 -0
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +2101 -0
- package/dist/esm/CollabProjectCheckpoint.mjs +1326 -0
- package/dist/esm/CollabProjectMembership.mjs +709 -0
- package/dist/esm/CollabProjectRetirement.mjs +119 -0
- package/dist/esm/CollabProtocol.mjs +64 -0
- package/dist/esm/CollabRequestTicketRequestCodecs.mjs +263 -0
- package/dist/esm/CollabRequestTicketResponseCodecs.mjs +353 -0
- package/dist/esm/CollabTicketReferenceParser.mjs +52 -0
- package/dist/esm/CollabValidation.mjs +19 -0
- package/dist/esm/DevelopmentBootstrap.mjs +537 -0
- package/dist/esm/index.mjs +17 -0
- package/dist/esm/types.mjs +8 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +10 -3
- package/package.json +5 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.COLLAB_PROJECT_BACKUP_RECORD_KINDS = exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION = void 0;
|
|
3
|
+
exports.COLLAB_PROJECT_BACKUP_RECORD_KINDS = exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION = exports.COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE = void 0;
|
|
4
4
|
exports.collabProjectBackupIdempotencyRecordId = collabProjectBackupIdempotencyRecordId;
|
|
5
5
|
exports.decodeCollabProjectBackupCheckpointManifest = decodeCollabProjectBackupCheckpointManifest;
|
|
6
6
|
exports.encodeCollabProjectBackupCheckpointManifestCanonicalJson = encodeCollabProjectBackupCheckpointManifestCanonicalJson;
|
|
@@ -13,8 +13,10 @@ const CollabConstants_1 = require("./CollabConstants");
|
|
|
13
13
|
const CollabControlOperationCodecs_1 = require("./CollabControlOperationCodecs");
|
|
14
14
|
const CollabError_1 = require("./CollabError");
|
|
15
15
|
const CollabProjectCheckpoint_1 = require("./CollabProjectCheckpoint");
|
|
16
|
+
const CollabProjectMembership_1 = require("./CollabProjectMembership");
|
|
16
17
|
const CollabValidation_1 = require("./CollabValidation");
|
|
17
|
-
exports.
|
|
18
|
+
exports.COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE = 'pre-production-replaceable';
|
|
19
|
+
exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION = 3;
|
|
18
20
|
const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
19
21
|
'lifecycle-journal',
|
|
20
22
|
'authority-transfer-recovery',
|
|
@@ -25,11 +27,28 @@ const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
|
25
27
|
'terminal-principal',
|
|
26
28
|
'terminal-responder-replay',
|
|
27
29
|
'leave-former-principal-replay',
|
|
30
|
+
'project-invitation',
|
|
31
|
+
'protected-invitation-envelope',
|
|
32
|
+
'transferred-membership-claim-override',
|
|
33
|
+
'protected-claim-override-envelope',
|
|
34
|
+
'manager-responsibility-offer',
|
|
35
|
+
'project-membership-recovery',
|
|
36
|
+
'secret-replay-tombstone',
|
|
28
37
|
]);
|
|
29
38
|
exports.COLLAB_PROJECT_BACKUP_RECORD_KINDS = Object.freeze([
|
|
30
|
-
...CollabProjectCheckpoint_1.
|
|
39
|
+
...CollabProjectCheckpoint_1.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS,
|
|
40
|
+
'cloud-event',
|
|
41
|
+
'cloud-event-cursor',
|
|
42
|
+
'idempotency-result',
|
|
43
|
+
'principal-binding',
|
|
44
|
+
'repository-placement',
|
|
31
45
|
...BACKUP_CONTINUITY_RECORD_KINDS,
|
|
32
|
-
|
|
46
|
+
'terminal-responder',
|
|
47
|
+
'protected-claim-envelope',
|
|
48
|
+
'tombstone',
|
|
49
|
+
'schema-catalog',
|
|
50
|
+
'server-compatibility',
|
|
51
|
+
'authority-volume-pair',
|
|
33
52
|
]);
|
|
34
53
|
const CONTINUITY_KIND_SET = new Set(BACKUP_CONTINUITY_RECORD_KINDS);
|
|
35
54
|
const BACKUP_KIND_SET = new Set(exports.COLLAB_PROJECT_BACKUP_RECORD_KINDS);
|
|
@@ -59,7 +78,9 @@ const DELETE_ACTIVE_PHASE_SET = new Set([
|
|
|
59
78
|
'tombstoned',
|
|
60
79
|
]);
|
|
61
80
|
const PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET = new Set([
|
|
81
|
+
'createProjectInvitation',
|
|
62
82
|
'getTransferredMembershipClaim',
|
|
83
|
+
'reissueTransferredMembershipClaim',
|
|
63
84
|
'rotateTransferredMembershipClaims',
|
|
64
85
|
]);
|
|
65
86
|
function invalidPayload(field) {
|
|
@@ -117,6 +138,9 @@ function timestampValue(value, field) {
|
|
|
117
138
|
function timestamp(source, field) {
|
|
118
139
|
return timestampValue(source[field], field);
|
|
119
140
|
}
|
|
141
|
+
function hasExactDuration(start, end, durationMs) {
|
|
142
|
+
return Date.parse(end) === Date.parse(start) + durationMs;
|
|
143
|
+
}
|
|
120
144
|
function nullableTimestamp(source, field) {
|
|
121
145
|
return source[field] === null ? null : timestampValue(source[field], field);
|
|
122
146
|
}
|
|
@@ -467,7 +491,15 @@ function lifecycleJournalRecord(source, recordId, revision) {
|
|
|
467
491
|
if (recordId !== operationId)
|
|
468
492
|
throw invalidPayload('recordId');
|
|
469
493
|
const operationKind = literal(value, 'operationKind', [
|
|
470
|
-
'authority-transfer',
|
|
494
|
+
'authority-transfer',
|
|
495
|
+
'backup',
|
|
496
|
+
'create-project',
|
|
497
|
+
'delete',
|
|
498
|
+
'export',
|
|
499
|
+
'join-project',
|
|
500
|
+
'leave',
|
|
501
|
+
'remove-member',
|
|
502
|
+
'retire',
|
|
471
503
|
]);
|
|
472
504
|
const direction = value.direction === null
|
|
473
505
|
? null
|
|
@@ -838,6 +870,350 @@ function leaveFormerPrincipalReplayRecord(source, recordId, revision) {
|
|
|
838
870
|
},
|
|
839
871
|
};
|
|
840
872
|
}
|
|
873
|
+
function projectInvitationRecord(source, recordId, revision) {
|
|
874
|
+
const value = exactRecord(source.value, 'value', [
|
|
875
|
+
'createdAt',
|
|
876
|
+
'expiresAt',
|
|
877
|
+
'idempotencyKey',
|
|
878
|
+
'invitationId',
|
|
879
|
+
'issuedByMemberId',
|
|
880
|
+
'projectId',
|
|
881
|
+
'requestFingerprint',
|
|
882
|
+
'revision',
|
|
883
|
+
'secretReplayExpiresAt',
|
|
884
|
+
'secretSha256',
|
|
885
|
+
'state',
|
|
886
|
+
'terminalAt',
|
|
887
|
+
]);
|
|
888
|
+
const invitationId = token(value, 'invitationId');
|
|
889
|
+
if (recordId !== invitationId)
|
|
890
|
+
throw invalidPayload('recordId');
|
|
891
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
892
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
893
|
+
const secretReplayExpiresAt = timestamp(value, 'secretReplayExpiresAt');
|
|
894
|
+
const state = literal(value, 'state', [
|
|
895
|
+
'active', 'redeeming', 'redeemed', 'revoked', 'expired',
|
|
896
|
+
]);
|
|
897
|
+
const terminalAt = nullableTimestamp(value, 'terminalAt');
|
|
898
|
+
const terminal = state === 'redeemed' || state === 'revoked' || state === 'expired';
|
|
899
|
+
if (!hasExactDuration(createdAt, expiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.invitationTtlMs)
|
|
900
|
+
|| !hasExactDuration(createdAt, secretReplayExpiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs)
|
|
901
|
+
|| terminal !== (terminalAt !== null)
|
|
902
|
+
|| (terminalAt !== null && Date.parse(terminalAt) < Date.parse(createdAt)))
|
|
903
|
+
throw invalidPayload('state');
|
|
904
|
+
return {
|
|
905
|
+
kind: 'project-invitation',
|
|
906
|
+
recordId,
|
|
907
|
+
revision,
|
|
908
|
+
value: {
|
|
909
|
+
createdAt,
|
|
910
|
+
expiresAt,
|
|
911
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
912
|
+
invitationId,
|
|
913
|
+
issuedByMemberId: token(value, 'issuedByMemberId', CollabValidation_1.isCollabMemberId),
|
|
914
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
915
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
916
|
+
revision: positiveInteger(value, 'revision'),
|
|
917
|
+
secretReplayExpiresAt,
|
|
918
|
+
secretSha256: sha256(value, 'secretSha256'),
|
|
919
|
+
state,
|
|
920
|
+
terminalAt,
|
|
921
|
+
},
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function protectedSecretEnvelopeFields(value) {
|
|
925
|
+
const ciphertext = boundedString(value, 'ciphertext', 4096);
|
|
926
|
+
const nonce = boundedString(value, 'nonce', 128);
|
|
927
|
+
if (!BASE64URL_PATTERN.test(ciphertext) || !BASE64URL_PATTERN.test(nonce)) {
|
|
928
|
+
throw invalidPayload('ciphertext');
|
|
929
|
+
}
|
|
930
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
931
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
932
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt))
|
|
933
|
+
throw invalidPayload('expiresAt');
|
|
934
|
+
return {
|
|
935
|
+
associatedDataSha256: sha256(value, 'associatedDataSha256'),
|
|
936
|
+
ciphertext,
|
|
937
|
+
createdAt,
|
|
938
|
+
expiresAt,
|
|
939
|
+
keyId: token(value, 'keyId'),
|
|
940
|
+
nonce,
|
|
941
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function protectedInvitationEnvelopeRecord(source, recordId, revision) {
|
|
945
|
+
const value = exactRecord(source.value, 'value', [
|
|
946
|
+
'associatedDataSha256',
|
|
947
|
+
'ciphertext',
|
|
948
|
+
'createdAt',
|
|
949
|
+
'expiresAt',
|
|
950
|
+
'invitationId',
|
|
951
|
+
'keyId',
|
|
952
|
+
'nonce',
|
|
953
|
+
'projectId',
|
|
954
|
+
]);
|
|
955
|
+
const invitationId = token(value, 'invitationId');
|
|
956
|
+
if (recordId !== invitationId)
|
|
957
|
+
throw invalidPayload('recordId');
|
|
958
|
+
const envelope = protectedSecretEnvelopeFields(value);
|
|
959
|
+
return {
|
|
960
|
+
kind: 'protected-invitation-envelope',
|
|
961
|
+
recordId,
|
|
962
|
+
revision,
|
|
963
|
+
value: {
|
|
964
|
+
associatedDataSha256: envelope.associatedDataSha256,
|
|
965
|
+
ciphertext: envelope.ciphertext,
|
|
966
|
+
createdAt: envelope.createdAt,
|
|
967
|
+
expiresAt: envelope.expiresAt,
|
|
968
|
+
invitationId,
|
|
969
|
+
keyId: envelope.keyId,
|
|
970
|
+
nonce: envelope.nonce,
|
|
971
|
+
projectId: envelope.projectId,
|
|
972
|
+
},
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function transferredMembershipClaimOverrideRecord(source, recordId, revision) {
|
|
976
|
+
const value = exactRecord(source.value, 'value', [
|
|
977
|
+
'claimGeneration',
|
|
978
|
+
'claimSha256',
|
|
979
|
+
'createdAt',
|
|
980
|
+
'expiresAt',
|
|
981
|
+
'idempotencyKey',
|
|
982
|
+
'managerMemberId',
|
|
983
|
+
'memberId',
|
|
984
|
+
'projectId',
|
|
985
|
+
'redemptionReceiptId',
|
|
986
|
+
'requestFingerprint',
|
|
987
|
+
'secretReplayExpiresAt',
|
|
988
|
+
'state',
|
|
989
|
+
'supersededClaimSha256',
|
|
990
|
+
'targetPrincipalId',
|
|
991
|
+
'transferId',
|
|
992
|
+
'updatedAt',
|
|
993
|
+
]);
|
|
994
|
+
const transferId = token(value, 'transferId');
|
|
995
|
+
const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
|
|
996
|
+
const claimGeneration = positiveInteger(value, 'claimGeneration');
|
|
997
|
+
if (recordId !== `${transferId}:${memberId}:${claimGeneration}`) {
|
|
998
|
+
throw invalidPayload('recordId');
|
|
999
|
+
}
|
|
1000
|
+
const state = literal(value, 'state', [
|
|
1001
|
+
'active', 'redeemed', 'revoked', 'superseded', 'expired',
|
|
1002
|
+
]);
|
|
1003
|
+
const targetPrincipalId = nullableToken(value, 'targetPrincipalId', candidate => (typeof candidate === 'string' && PRINCIPAL_PATTERN.test(candidate)));
|
|
1004
|
+
const redemptionReceiptId = nullableToken(value, 'redemptionReceiptId');
|
|
1005
|
+
if ((state === 'redeemed') !== (targetPrincipalId !== null && redemptionReceiptId !== null)
|
|
1006
|
+
|| (state !== 'redeemed' && (targetPrincipalId !== null || redemptionReceiptId !== null)))
|
|
1007
|
+
throw invalidPayload('state');
|
|
1008
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
1009
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
1010
|
+
const secretReplayExpiresAt = timestamp(value, 'secretReplayExpiresAt');
|
|
1011
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
1012
|
+
if (!hasExactDuration(createdAt, expiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.transferredClaimTtlMs)
|
|
1013
|
+
|| !hasExactDuration(createdAt, secretReplayExpiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs)
|
|
1014
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt))
|
|
1015
|
+
throw invalidPayload('updatedAt');
|
|
1016
|
+
return {
|
|
1017
|
+
kind: 'transferred-membership-claim-override',
|
|
1018
|
+
recordId,
|
|
1019
|
+
revision,
|
|
1020
|
+
value: {
|
|
1021
|
+
claimGeneration,
|
|
1022
|
+
claimSha256: sha256(value, 'claimSha256'),
|
|
1023
|
+
createdAt,
|
|
1024
|
+
expiresAt,
|
|
1025
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
1026
|
+
managerMemberId: token(value, 'managerMemberId', CollabValidation_1.isCollabMemberId),
|
|
1027
|
+
memberId,
|
|
1028
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1029
|
+
redemptionReceiptId,
|
|
1030
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1031
|
+
secretReplayExpiresAt,
|
|
1032
|
+
state,
|
|
1033
|
+
supersededClaimSha256: sha256(value, 'supersededClaimSha256'),
|
|
1034
|
+
targetPrincipalId,
|
|
1035
|
+
transferId,
|
|
1036
|
+
updatedAt,
|
|
1037
|
+
},
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function protectedClaimOverrideEnvelopeRecord(source, recordId, revision) {
|
|
1041
|
+
const value = exactRecord(source.value, 'value', [
|
|
1042
|
+
'associatedDataSha256',
|
|
1043
|
+
'ciphertext',
|
|
1044
|
+
'claimGeneration',
|
|
1045
|
+
'createdAt',
|
|
1046
|
+
'expiresAt',
|
|
1047
|
+
'keyId',
|
|
1048
|
+
'memberId',
|
|
1049
|
+
'nonce',
|
|
1050
|
+
'projectId',
|
|
1051
|
+
'transferId',
|
|
1052
|
+
]);
|
|
1053
|
+
const transferId = token(value, 'transferId');
|
|
1054
|
+
const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
|
|
1055
|
+
const claimGeneration = positiveInteger(value, 'claimGeneration');
|
|
1056
|
+
if (recordId !== `${transferId}:${memberId}:${claimGeneration}`) {
|
|
1057
|
+
throw invalidPayload('recordId');
|
|
1058
|
+
}
|
|
1059
|
+
const envelope = protectedSecretEnvelopeFields(value);
|
|
1060
|
+
return {
|
|
1061
|
+
kind: 'protected-claim-override-envelope',
|
|
1062
|
+
recordId,
|
|
1063
|
+
revision,
|
|
1064
|
+
value: {
|
|
1065
|
+
associatedDataSha256: envelope.associatedDataSha256,
|
|
1066
|
+
ciphertext: envelope.ciphertext,
|
|
1067
|
+
claimGeneration,
|
|
1068
|
+
createdAt: envelope.createdAt,
|
|
1069
|
+
expiresAt: envelope.expiresAt,
|
|
1070
|
+
keyId: envelope.keyId,
|
|
1071
|
+
memberId,
|
|
1072
|
+
nonce: envelope.nonce,
|
|
1073
|
+
projectId: envelope.projectId,
|
|
1074
|
+
transferId,
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function managerResponsibilityOfferRecord(source, recordId, revision) {
|
|
1079
|
+
const value = exactRecord(source.value, 'value', [
|
|
1080
|
+
'acknowledgedAt',
|
|
1081
|
+
'expiresAt',
|
|
1082
|
+
'idempotencyKey',
|
|
1083
|
+
'managerSetGenerationAtOffer',
|
|
1084
|
+
'offeredAt',
|
|
1085
|
+
'offerId',
|
|
1086
|
+
'projectId',
|
|
1087
|
+
'purpose',
|
|
1088
|
+
'requestFingerprint',
|
|
1089
|
+
'revision',
|
|
1090
|
+
'sourceManagerMemberId',
|
|
1091
|
+
'state',
|
|
1092
|
+
'targetMemberId',
|
|
1093
|
+
'targetMembershipRevisionAtOffer',
|
|
1094
|
+
'terminalAt',
|
|
1095
|
+
]);
|
|
1096
|
+
const offerId = token(value, 'offerId');
|
|
1097
|
+
if (recordId !== offerId)
|
|
1098
|
+
throw invalidPayload('recordId');
|
|
1099
|
+
const state = literal(value, 'state', [
|
|
1100
|
+
'offered', 'acknowledged', 'declined', 'cancelled', 'consumed', 'expired',
|
|
1101
|
+
]);
|
|
1102
|
+
const offeredAt = timestamp(value, 'offeredAt');
|
|
1103
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
1104
|
+
const acknowledgedAt = nullableTimestamp(value, 'acknowledgedAt');
|
|
1105
|
+
const terminalAt = nullableTimestamp(value, 'terminalAt');
|
|
1106
|
+
const terminal = state === 'declined'
|
|
1107
|
+
|| state === 'cancelled'
|
|
1108
|
+
|| state === 'consumed'
|
|
1109
|
+
|| state === 'expired';
|
|
1110
|
+
if (!hasExactDuration(offeredAt, expiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.managerResponsibilityOfferTtlMs)
|
|
1111
|
+
|| (state === 'offered' && acknowledgedAt !== null)
|
|
1112
|
+
|| (state === 'acknowledged' && acknowledgedAt === null)
|
|
1113
|
+
|| terminal !== (terminalAt !== null))
|
|
1114
|
+
throw invalidPayload('state');
|
|
1115
|
+
return {
|
|
1116
|
+
kind: 'manager-responsibility-offer',
|
|
1117
|
+
recordId,
|
|
1118
|
+
revision,
|
|
1119
|
+
value: {
|
|
1120
|
+
acknowledgedAt,
|
|
1121
|
+
expiresAt,
|
|
1122
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
1123
|
+
managerSetGenerationAtOffer: positiveInteger(value, 'managerSetGenerationAtOffer'),
|
|
1124
|
+
offeredAt,
|
|
1125
|
+
offerId,
|
|
1126
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1127
|
+
purpose: literal(value, 'purpose', ['manager-promotion', 'manager-leave']),
|
|
1128
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1129
|
+
revision: positiveInteger(value, 'revision'),
|
|
1130
|
+
sourceManagerMemberId: token(value, 'sourceManagerMemberId', CollabValidation_1.isCollabMemberId),
|
|
1131
|
+
state,
|
|
1132
|
+
targetMemberId: token(value, 'targetMemberId', CollabValidation_1.isCollabMemberId),
|
|
1133
|
+
targetMembershipRevisionAtOffer: positiveInteger(value, 'targetMembershipRevisionAtOffer'),
|
|
1134
|
+
terminalAt,
|
|
1135
|
+
},
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function membershipRecoveryRecord(source, recordId, revision) {
|
|
1139
|
+
const value = exactRecord(source.value, 'value', [
|
|
1140
|
+
'expectedMainOid',
|
|
1141
|
+
'expectedPersonalRefOid',
|
|
1142
|
+
'invitationId',
|
|
1143
|
+
'memberId',
|
|
1144
|
+
'operationId',
|
|
1145
|
+
'operationKind',
|
|
1146
|
+
'principalSha256',
|
|
1147
|
+
'projectId',
|
|
1148
|
+
'publicationMarkerSha256',
|
|
1149
|
+
'repositoryPlanSha256',
|
|
1150
|
+
'requestFingerprint',
|
|
1151
|
+
]);
|
|
1152
|
+
const operationId = token(value, 'operationId');
|
|
1153
|
+
if (recordId !== operationId)
|
|
1154
|
+
throw invalidPayload('recordId');
|
|
1155
|
+
const operationKind = literal(value, 'operationKind', [
|
|
1156
|
+
'create-project', 'join-project', 'remove-member',
|
|
1157
|
+
]);
|
|
1158
|
+
const invitationId = nullableToken(value, 'invitationId');
|
|
1159
|
+
const principalSha256 = nullableSha256(value, 'principalSha256');
|
|
1160
|
+
const publicationMarkerSha256 = nullableSha256(value, 'publicationMarkerSha256');
|
|
1161
|
+
const repositoryPlanSha256 = nullableSha256(value, 'repositoryPlanSha256');
|
|
1162
|
+
if ((operationKind === 'join-project') !== (invitationId !== null)
|
|
1163
|
+
|| (operationKind === 'remove-member') === (principalSha256 !== null)
|
|
1164
|
+
|| (operationKind === 'create-project') !== (repositoryPlanSha256 !== null)
|
|
1165
|
+
|| (operationKind !== 'create-project' && publicationMarkerSha256 !== null))
|
|
1166
|
+
throw invalidPayload('operationKind');
|
|
1167
|
+
return {
|
|
1168
|
+
kind: 'project-membership-recovery',
|
|
1169
|
+
recordId,
|
|
1170
|
+
revision,
|
|
1171
|
+
value: {
|
|
1172
|
+
expectedMainOid: token(value, 'expectedMainOid', CollabValidation_1.isCollabGitOid),
|
|
1173
|
+
expectedPersonalRefOid: token(value, 'expectedPersonalRefOid', CollabValidation_1.isCollabGitOid),
|
|
1174
|
+
invitationId,
|
|
1175
|
+
memberId: token(value, 'memberId', CollabValidation_1.isCollabMemberId),
|
|
1176
|
+
operationId,
|
|
1177
|
+
operationKind,
|
|
1178
|
+
principalSha256,
|
|
1179
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1180
|
+
publicationMarkerSha256,
|
|
1181
|
+
repositoryPlanSha256,
|
|
1182
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
function secretReplayTombstoneRecord(source, recordId, revision) {
|
|
1187
|
+
const value = exactRecord(source.value, 'value', [
|
|
1188
|
+
'actorMemberId',
|
|
1189
|
+
'expiredAt',
|
|
1190
|
+
'idempotencyKey',
|
|
1191
|
+
'operation',
|
|
1192
|
+
'projectId',
|
|
1193
|
+
'requestFingerprint',
|
|
1194
|
+
]);
|
|
1195
|
+
const actorMemberId = token(value, 'actorMemberId', CollabValidation_1.isCollabMemberId);
|
|
1196
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1197
|
+
const operation = literal(value, 'operation', [
|
|
1198
|
+
'createProjectInvitation', 'reissueTransferredMembershipClaim',
|
|
1199
|
+
]);
|
|
1200
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1201
|
+
throw invalidPayload('recordId');
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
kind: 'secret-replay-tombstone',
|
|
1205
|
+
recordId,
|
|
1206
|
+
revision,
|
|
1207
|
+
value: {
|
|
1208
|
+
actorMemberId,
|
|
1209
|
+
expiredAt: timestamp(value, 'expiredAt'),
|
|
1210
|
+
idempotencyKey,
|
|
1211
|
+
operation,
|
|
1212
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1213
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
841
1217
|
function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
842
1218
|
switch (kind) {
|
|
843
1219
|
case 'lifecycle-journal': return lifecycleJournalRecord(source, recordId, revision);
|
|
@@ -855,6 +1231,19 @@ function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
|
855
1231
|
return terminalResponderReplayRecord(source, recordId, revision);
|
|
856
1232
|
case 'leave-former-principal-replay':
|
|
857
1233
|
return leaveFormerPrincipalReplayRecord(source, recordId, revision);
|
|
1234
|
+
case 'project-invitation': return projectInvitationRecord(source, recordId, revision);
|
|
1235
|
+
case 'protected-invitation-envelope':
|
|
1236
|
+
return protectedInvitationEnvelopeRecord(source, recordId, revision);
|
|
1237
|
+
case 'transferred-membership-claim-override':
|
|
1238
|
+
return transferredMembershipClaimOverrideRecord(source, recordId, revision);
|
|
1239
|
+
case 'protected-claim-override-envelope':
|
|
1240
|
+
return protectedClaimOverrideEnvelopeRecord(source, recordId, revision);
|
|
1241
|
+
case 'manager-responsibility-offer':
|
|
1242
|
+
return managerResponsibilityOfferRecord(source, recordId, revision);
|
|
1243
|
+
case 'project-membership-recovery':
|
|
1244
|
+
return membershipRecoveryRecord(source, recordId, revision);
|
|
1245
|
+
case 'secret-replay-tombstone':
|
|
1246
|
+
return secretReplayTombstoneRecord(source, recordId, revision);
|
|
858
1247
|
}
|
|
859
1248
|
}
|
|
860
1249
|
function kindOrder(kind) {
|
|
@@ -911,6 +1300,33 @@ function lifecycleJournalHasInvalidSemantics(item) {
|
|
|
911
1300
|
}
|
|
912
1301
|
if (hasCheckpoint)
|
|
913
1302
|
return true;
|
|
1303
|
+
if (value.operationKind === 'create-project'
|
|
1304
|
+
|| value.operationKind === 'join-project'
|
|
1305
|
+
|| value.operationKind === 'remove-member') {
|
|
1306
|
+
const phases = value.operationKind === 'create-project'
|
|
1307
|
+
? new Set([
|
|
1308
|
+
'prepared',
|
|
1309
|
+
'repository-publication-intent',
|
|
1310
|
+
'repository-published',
|
|
1311
|
+
'activated',
|
|
1312
|
+
])
|
|
1313
|
+
: value.operationKind === 'join-project'
|
|
1314
|
+
? new Set(['prepared', 'membership-pending', 'personal-ref-created', 'membership-active'])
|
|
1315
|
+
: new Set(['prepared', 'membership-revoked', 'personal-ref-removed']);
|
|
1316
|
+
const statePhaseValid = (value.state === 'active' && phases.has(value.phase))
|
|
1317
|
+
|| (value.state === 'recovery-required'
|
|
1318
|
+
&& value.phase === 'recovery-required'
|
|
1319
|
+
&& value.recoveryFromPhase !== null
|
|
1320
|
+
&& phases.has(value.recoveryFromPhase))
|
|
1321
|
+
|| (value.state === 'completed' && value.phase === 'completed');
|
|
1322
|
+
const resultRequired = value.state === 'completed'
|
|
1323
|
+
|| (value.operationKind === 'create-project' && effectivePhase === 'activated')
|
|
1324
|
+
|| (value.operationKind === 'join-project' && effectivePhase === 'membership-active')
|
|
1325
|
+
|| (value.operationKind === 'remove-member'
|
|
1326
|
+
&& (effectivePhase === 'membership-revoked'
|
|
1327
|
+
|| effectivePhase === 'personal-ref-removed'));
|
|
1328
|
+
return !statePhaseValid || hasResult !== resultRequired;
|
|
1329
|
+
}
|
|
914
1330
|
if (value.operationKind === 'leave') {
|
|
915
1331
|
const statePhaseValid = (value.state === 'active' && (value.phase === 'prepared'
|
|
916
1332
|
|| value.phase === 'membership-left'
|
|
@@ -979,13 +1395,41 @@ function validateContinuity(records) {
|
|
|
979
1395
|
const leaveReplays = new Map(records
|
|
980
1396
|
.filter((item) => (item.kind === 'leave-former-principal-replay'))
|
|
981
1397
|
.map(item => [item.value.operationId, item]));
|
|
1398
|
+
const invitations = new Map(records
|
|
1399
|
+
.filter((item) => (item.kind === 'project-invitation'))
|
|
1400
|
+
.map(item => [item.value.invitationId, item]));
|
|
1401
|
+
const invitationEnvelopes = new Map(records
|
|
1402
|
+
.filter((item) => (item.kind === 'protected-invitation-envelope'))
|
|
1403
|
+
.map(item => [item.value.invitationId, item]));
|
|
1404
|
+
const claimOverrides = records.filter((item) => (item.kind === 'transferred-membership-claim-override'));
|
|
1405
|
+
const claimOverrideEnvelopes = new Map(records
|
|
1406
|
+
.filter((item) => (item.kind === 'protected-claim-override-envelope'))
|
|
1407
|
+
.map(item => [
|
|
1408
|
+
`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`,
|
|
1409
|
+
item,
|
|
1410
|
+
]));
|
|
1411
|
+
const managerOffers = records.filter((item) => (item.kind === 'manager-responsibility-offer'));
|
|
1412
|
+
const membershipRecoveries = new Map(records
|
|
1413
|
+
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1414
|
+
.map(item => [item.value.operationId, item]));
|
|
1415
|
+
const secretReplayTombstones = records.filter((item) => (item.kind === 'secret-replay-tombstone'));
|
|
982
1416
|
const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
|
|
983
1417
|
if (nonterminalLifecycles.length > 1
|
|
984
1418
|
|| principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
|
|
985
1419
|
|| new Set(principalBindingRecords.map(item => item.value.principalId)).size
|
|
986
1420
|
!== principalBindingRecords.length
|
|
987
1421
|
|| new Set(claimRecords.map(item => (`${item.value.transferId}:${item.value.claimSha256}`))).size !== claimRecords.length
|
|
988
|
-
|| new Set(redemptionReceiptRecords.map(item => (`${item.value.receipt.transferId}:${item.value.receipt.receiptId}`))).size !== redemptionReceiptRecords.length
|
|
1422
|
+
|| new Set(redemptionReceiptRecords.map(item => (`${item.value.receipt.transferId}:${item.value.receipt.receiptId}`))).size !== redemptionReceiptRecords.length
|
|
1423
|
+
|| invitations.size !== records.filter(item => item.kind === 'project-invitation').length
|
|
1424
|
+
|| invitationEnvelopes.size
|
|
1425
|
+
!== records.filter(item => item.kind === 'protected-invitation-envelope').length
|
|
1426
|
+
|| new Set([...invitations.values()].map(item => (`${item.value.issuedByMemberId}:${item.value.idempotencyKey}`))).size !== invitations.size
|
|
1427
|
+
|| new Set(claimOverrides.map(item => (`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`))).size !== claimOverrides.length
|
|
1428
|
+
|| new Set(claimOverrides.map(item => (`${item.value.managerMemberId}:${item.value.idempotencyKey}`))).size !== claimOverrides.length
|
|
1429
|
+
|| claimOverrideEnvelopes.size
|
|
1430
|
+
!== records.filter(item => item.kind === 'protected-claim-override-envelope').length
|
|
1431
|
+
|| membershipRecoveries.size
|
|
1432
|
+
!== records.filter(item => item.kind === 'project-membership-recovery').length)
|
|
989
1433
|
throw invalidPayload('records');
|
|
990
1434
|
for (const lifecycle of lifecycles.values()) {
|
|
991
1435
|
if (lifecycleJournalHasInvalidSemantics(lifecycle)
|
|
@@ -996,6 +1440,11 @@ function validateContinuity(records) {
|
|
|
996
1440
|
if (lifecycle.value.operationKind === 'authority-transfer'
|
|
997
1441
|
&& !recoveries.has(lifecycle.value.operationId))
|
|
998
1442
|
throw invalidPayload('records');
|
|
1443
|
+
if ((lifecycle.value.operationKind === 'create-project'
|
|
1444
|
+
|| lifecycle.value.operationKind === 'join-project'
|
|
1445
|
+
|| lifecycle.value.operationKind === 'remove-member')
|
|
1446
|
+
&& !membershipRecoveries.has(lifecycle.value.operationId))
|
|
1447
|
+
throw invalidPayload('records');
|
|
999
1448
|
if (lifecycle.value.operationKind === 'leave') {
|
|
1000
1449
|
const actorMemberId = lifecycle.value.actorMemberId;
|
|
1001
1450
|
const actor = actorMemberId === null ? undefined : memberRecords.get(actorMemberId);
|
|
@@ -1172,6 +1621,13 @@ function validateContinuity(records) {
|
|
|
1172
1621
|
const lifecycle = lifecycles.get(claim.value.transferId);
|
|
1173
1622
|
const recovery = recoveries.get(claim.value.transferId);
|
|
1174
1623
|
const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
|
|
1624
|
+
const redeemedOverride = redemptionReceipt === undefined
|
|
1625
|
+
? undefined
|
|
1626
|
+
: claimOverrides.find(item => (item.value.transferId === claim.value.transferId
|
|
1627
|
+
&& item.value.memberId === claim.value.memberId
|
|
1628
|
+
&& item.value.state === 'redeemed'
|
|
1629
|
+
&& item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
|
|
1630
|
+
&& item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
|
|
1175
1631
|
if (memberRecords.get(claim.value.memberId)?.value.status !== 'active'
|
|
1176
1632
|
|| recovery === undefined
|
|
1177
1633
|
|| recovery.value.sourceAuthority.kind !== 'lan'
|
|
@@ -1181,7 +1637,11 @@ function validateContinuity(records) {
|
|
|
1181
1637
|
|| claim.value.expiresAt !== recovery.value.expiresAt
|
|
1182
1638
|
|| (claim.value.targetPrincipalId !== null
|
|
1183
1639
|
&& principalBindings.get(claim.value.memberId) !== claim.value.targetPrincipalId)
|
|
1184
|
-
|| (claim.value.state === 'redeemed'
|
|
1640
|
+
|| (claim.value.state === 'redeemed' && redemptionReceipt === undefined)
|
|
1641
|
+
|| (redemptionReceipt !== undefined
|
|
1642
|
+
&& claim.value.state !== 'redeemed'
|
|
1643
|
+
&& redeemedOverride === undefined)
|
|
1644
|
+
|| (claim.value.state === 'redeemed' && redeemedOverride !== undefined)) {
|
|
1185
1645
|
throw invalidPayload('records');
|
|
1186
1646
|
}
|
|
1187
1647
|
}
|
|
@@ -1197,6 +1657,141 @@ function validateContinuity(records) {
|
|
|
1197
1657
|
!== responderReplays.has(terminal.value.operationId))
|
|
1198
1658
|
throw invalidPayload('records');
|
|
1199
1659
|
}
|
|
1660
|
+
for (const recovery of membershipRecoveries.values()) {
|
|
1661
|
+
const lifecycle = lifecycles.get(recovery.value.operationId);
|
|
1662
|
+
const invitation = recovery.value.invitationId === null
|
|
1663
|
+
? undefined
|
|
1664
|
+
: invitations.get(recovery.value.invitationId);
|
|
1665
|
+
const member = memberRecords.get(recovery.value.memberId);
|
|
1666
|
+
if (lifecycle === undefined
|
|
1667
|
+
|| lifecycle.value.operationKind !== recovery.value.operationKind
|
|
1668
|
+
|| lifecycle.value.requestFingerprint !== recovery.value.requestFingerprint
|
|
1669
|
+
|| lifecycle.value.state !== 'completed'
|
|
1670
|
+
|| member === undefined
|
|
1671
|
+
|| (recovery.value.operationKind === 'join-project' && invitation === undefined)
|
|
1672
|
+
|| (recovery.value.operationKind !== 'join-project' && invitation !== undefined)
|
|
1673
|
+
|| (recovery.value.operationKind === 'create-project'
|
|
1674
|
+
&& lifecycle.value.actorMemberId !== null)
|
|
1675
|
+
|| (recovery.value.operationKind === 'join-project'
|
|
1676
|
+
&& lifecycle.value.actorMemberId !== null)
|
|
1677
|
+
|| (recovery.value.operationKind === 'remove-member'
|
|
1678
|
+
&& lifecycle.value.actorMemberId === null)
|
|
1679
|
+
|| (recovery.value.operationKind === 'join-project'
|
|
1680
|
+
&& invitation?.value.state !== 'redeemed')
|
|
1681
|
+
|| (recovery.value.operationKind === 'remove-member' && (member.value.status !== 'revoked'
|
|
1682
|
+
|| principalBindings.has(recovery.value.memberId))))
|
|
1683
|
+
throw invalidPayload('records');
|
|
1684
|
+
}
|
|
1685
|
+
for (const invitation of invitations.values()) {
|
|
1686
|
+
const envelope = invitationEnvelopes.get(invitation.value.invitationId);
|
|
1687
|
+
const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'createProjectInvitation'
|
|
1688
|
+
&& item.value.actorMemberId === invitation.value.issuedByMemberId
|
|
1689
|
+
&& item.value.idempotencyKey === invitation.value.idempotencyKey
|
|
1690
|
+
&& item.value.requestFingerprint === invitation.value.requestFingerprint));
|
|
1691
|
+
if (!members.has(invitation.value.issuedByMemberId)
|
|
1692
|
+
|| (envelope === undefined) === (tombstone === undefined)
|
|
1693
|
+
|| (tombstone !== undefined
|
|
1694
|
+
&& tombstone.value.expiredAt !== invitation.value.secretReplayExpiresAt)
|
|
1695
|
+
|| (envelope !== undefined && (envelope.value.projectId !== invitation.value.projectId
|
|
1696
|
+
|| envelope.value.createdAt !== invitation.value.createdAt
|
|
1697
|
+
|| envelope.value.expiresAt !== invitation.value.secretReplayExpiresAt)))
|
|
1698
|
+
throw invalidPayload('records');
|
|
1699
|
+
}
|
|
1700
|
+
for (const envelope of invitationEnvelopes.values()) {
|
|
1701
|
+
if (!invitations.has(envelope.value.invitationId))
|
|
1702
|
+
throw invalidPayload('records');
|
|
1703
|
+
}
|
|
1704
|
+
const overridesByMember = new Map();
|
|
1705
|
+
for (const override of claimOverrides) {
|
|
1706
|
+
const identity = `${override.value.transferId}:${override.value.memberId}`;
|
|
1707
|
+
const values = overridesByMember.get(identity) ?? [];
|
|
1708
|
+
values.push(override);
|
|
1709
|
+
overridesByMember.set(identity, values);
|
|
1710
|
+
const envelope = claimOverrideEnvelopes.get(`${identity}:${override.value.claimGeneration}`);
|
|
1711
|
+
const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'reissueTransferredMembershipClaim'
|
|
1712
|
+
&& item.value.actorMemberId === override.value.managerMemberId
|
|
1713
|
+
&& item.value.idempotencyKey === override.value.idempotencyKey
|
|
1714
|
+
&& item.value.requestFingerprint === override.value.requestFingerprint));
|
|
1715
|
+
if (!members.has(override.value.memberId)
|
|
1716
|
+
|| !members.has(override.value.managerMemberId)
|
|
1717
|
+
|| (envelope === undefined) === (tombstone === undefined)
|
|
1718
|
+
|| (tombstone !== undefined
|
|
1719
|
+
&& tombstone.value.expiredAt !== override.value.secretReplayExpiresAt)
|
|
1720
|
+
|| (envelope !== undefined && (envelope.value.projectId !== override.value.projectId
|
|
1721
|
+
|| envelope.value.createdAt !== override.value.createdAt
|
|
1722
|
+
|| envelope.value.expiresAt !== override.value.secretReplayExpiresAt)))
|
|
1723
|
+
throw invalidPayload('records');
|
|
1724
|
+
}
|
|
1725
|
+
for (const overrides of overridesByMember.values()) {
|
|
1726
|
+
overrides.sort((left, right) => left.value.claimGeneration - right.value.claimGeneration);
|
|
1727
|
+
const highest = overrides.at(-1);
|
|
1728
|
+
const first = overrides[0];
|
|
1729
|
+
const identity = first === undefined
|
|
1730
|
+
? ''
|
|
1731
|
+
: `${first.value.transferId}:${first.value.memberId}`;
|
|
1732
|
+
const sourceClaim = claims.get(identity);
|
|
1733
|
+
const recovery = first === undefined ? undefined : recoveries.get(first.value.transferId);
|
|
1734
|
+
const member = first === undefined ? undefined : memberRecords.get(first.value.memberId);
|
|
1735
|
+
const principalBinding = first === undefined
|
|
1736
|
+
? undefined
|
|
1737
|
+
: principalBindings.get(first.value.memberId);
|
|
1738
|
+
const redemptionReceipt = redemptionReceipts.get(identity);
|
|
1739
|
+
const redeemedOverrides = overrides.filter(item => item.value.state === 'redeemed');
|
|
1740
|
+
if (first === undefined
|
|
1741
|
+
|| sourceClaim === undefined
|
|
1742
|
+
|| recovery?.value.sourceAuthority.kind !== 'lan'
|
|
1743
|
+
|| member?.value.status !== 'active'
|
|
1744
|
+
|| sourceClaim.value.state === 'redeemed'
|
|
1745
|
+
|| overrides.some((item, index) => item.value.claimGeneration !== index + 1)
|
|
1746
|
+
|| overrides.some((item, index) => item.value.supersededClaimSha256 !== (index === 0
|
|
1747
|
+
? sourceClaim.value.claimSha256
|
|
1748
|
+
: overrides[index - 1].value.claimSha256))
|
|
1749
|
+
|| new Set(overrides.map(item => item.value.claimSha256)).size !== overrides.length
|
|
1750
|
+
|| overrides.filter(item => item.value.state === 'active').length > 1
|
|
1751
|
+
|| overrides.some(item => item.value.state === 'active' && item !== highest)
|
|
1752
|
+
|| redeemedOverrides.length > 1
|
|
1753
|
+
|| redeemedOverrides.some(item => item !== highest)
|
|
1754
|
+
|| highest?.value.state === 'superseded'
|
|
1755
|
+
|| (highest?.value.state === 'active' && (principalBinding !== undefined
|
|
1756
|
+
|| redemptionReceipt !== undefined))
|
|
1757
|
+
|| (highest?.value.state === 'redeemed' && (principalBinding !== highest.value.targetPrincipalId
|
|
1758
|
+
|| redemptionReceipt === undefined
|
|
1759
|
+
|| redemptionReceipt.value.receipt.transferId !== highest.value.transferId
|
|
1760
|
+
|| redemptionReceipt.value.receipt.memberId !== highest.value.memberId
|
|
1761
|
+
|| redemptionReceipt.value.receipt.claimSha256 !== highest.value.claimSha256
|
|
1762
|
+
|| redemptionReceipt.value.receipt.receiptId !== highest.value.redemptionReceiptId
|
|
1763
|
+
|| redemptionReceipt.value.receipt.checkpointSha256
|
|
1764
|
+
!== sourceClaim.value.checkpointSha256))
|
|
1765
|
+
|| (highest?.value.state !== 'redeemed' && redemptionReceipt !== undefined))
|
|
1766
|
+
throw invalidPayload('records');
|
|
1767
|
+
}
|
|
1768
|
+
for (const envelope of claimOverrideEnvelopes.values()) {
|
|
1769
|
+
if (!claimOverrides.some(item => (item.value.transferId === envelope.value.transferId
|
|
1770
|
+
&& item.value.memberId === envelope.value.memberId
|
|
1771
|
+
&& item.value.claimGeneration === envelope.value.claimGeneration)))
|
|
1772
|
+
throw invalidPayload('records');
|
|
1773
|
+
}
|
|
1774
|
+
const currentOffers = managerOffers.filter(item => (item.value.state === 'offered' || item.value.state === 'acknowledged'));
|
|
1775
|
+
if (managerOffers.some(item => (item.value.sourceManagerMemberId === item.value.targetMemberId
|
|
1776
|
+
|| !members.has(item.value.sourceManagerMemberId)
|
|
1777
|
+
|| !members.has(item.value.targetMemberId)))
|
|
1778
|
+
|| new Set(currentOffers.map(item => item.value.sourceManagerMemberId)).size
|
|
1779
|
+
!== currentOffers.length
|
|
1780
|
+
|| new Set(currentOffers.map(item => item.value.targetMemberId)).size
|
|
1781
|
+
!== currentOffers.length)
|
|
1782
|
+
throw invalidPayload('records');
|
|
1783
|
+
for (const tombstone of secretReplayTombstones) {
|
|
1784
|
+
const liveSecretRecord = tombstone.value.operation === 'createProjectInvitation'
|
|
1785
|
+
? [...invitations.values()].some(item => (item.value.issuedByMemberId === tombstone.value.actorMemberId
|
|
1786
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1787
|
+
&& invitationEnvelopes.has(item.value.invitationId)))
|
|
1788
|
+
: claimOverrides.some(item => (item.value.managerMemberId === tombstone.value.actorMemberId
|
|
1789
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1790
|
+
&& claimOverrideEnvelopes.has(`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`)));
|
|
1791
|
+
if (!members.has(tombstone.value.actorMemberId) || liveSecretRecord) {
|
|
1792
|
+
throw invalidPayload('records');
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1200
1795
|
for (const item of records) {
|
|
1201
1796
|
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1202
1797
|
throw invalidPayload('records');
|
|
@@ -1248,6 +1843,19 @@ function validateContinuity(records) {
|
|
|
1248
1843
|
const lifecycle = lifecycles.get(receipt.transferId);
|
|
1249
1844
|
const terminalPrincipal = terminalPrincipals.get(identity);
|
|
1250
1845
|
const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
|
|
1846
|
+
const redeemedOverride = claimOverrides.find(item => (item.value.transferId === receipt.transferId
|
|
1847
|
+
&& item.value.memberId === receipt.memberId
|
|
1848
|
+
&& item.value.state === 'redeemed'
|
|
1849
|
+
&& item.value.claimSha256 === receipt.claimSha256
|
|
1850
|
+
&& item.value.redemptionReceiptId === receipt.receiptId));
|
|
1851
|
+
const matchesSourceClaim = claim?.value.state === 'redeemed'
|
|
1852
|
+
&& claim.value.claimSha256 === receipt.claimSha256
|
|
1853
|
+
&& claim.value.checkpointSha256 === receipt.checkpointSha256
|
|
1854
|
+
&& claim.value.operationIntentId === receipt.operationIntentId
|
|
1855
|
+
&& claim.value.redemptionReceiptId === receipt.receiptId;
|
|
1856
|
+
const matchesOverride = claim !== undefined
|
|
1857
|
+
&& redeemedOverride !== undefined
|
|
1858
|
+
&& claim.value.checkpointSha256 === receipt.checkpointSha256;
|
|
1251
1859
|
if (recovery === undefined
|
|
1252
1860
|
|| lifecycle === undefined
|
|
1253
1861
|
|| memberRecords.get(receipt.memberId)?.value.status !== 'active'
|
|
@@ -1255,11 +1863,7 @@ function validateContinuity(records) {
|
|
|
1255
1863
|
|| !keys.has(`${receipt.transferId}:${receipt.receiptKeyId}`)
|
|
1256
1864
|
|| Date.parse(receipt.redeemedAt) > Date.parse(recovery.value.expiresAt)
|
|
1257
1865
|
|| (lanToCloud && (item.value.acknowledgedAt !== null
|
|
1258
|
-
||
|
|
1259
|
-
|| claim.value.claimSha256 !== receipt.claimSha256
|
|
1260
|
-
|| claim.value.checkpointSha256 !== receipt.checkpointSha256
|
|
1261
|
-
|| claim.value.operationIntentId !== receipt.operationIntentId
|
|
1262
|
-
|| claim.value.redemptionReceiptId !== receipt.receiptId))
|
|
1866
|
+
|| (!matchesSourceClaim && !matchesOverride)))
|
|
1263
1867
|
|| (!lanToCloud && (item.value.acknowledgedAt === null
|
|
1264
1868
|
|| receipt.memberId === recovery.value.targetHostMemberId
|
|
1265
1869
|
|| receipt.checkpointSha256 !== lifecycle.value.checkpointSha256
|
|
@@ -1454,6 +2058,30 @@ function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
|
|
|
1454
2058
|
function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
1455
2059
|
const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
|
|
1456
2060
|
const decodedRecords = decodeCollabProjectBackupCheckpointCoordinationNdjson(records.map(item => JSON.stringify(item)).join('\n') + '\n');
|
|
2061
|
+
const invitationEnvelopeIds = new Set(decodedRecords
|
|
2062
|
+
.filter(item => item.kind === 'protected-invitation-envelope')
|
|
2063
|
+
.map(item => item.recordId));
|
|
2064
|
+
const claimOverrideEnvelopeIds = new Set(decodedRecords
|
|
2065
|
+
.filter(item => item.kind === 'protected-claim-override-envelope')
|
|
2066
|
+
.map(item => item.recordId));
|
|
2067
|
+
const captureTime = Date.parse(decodedManifest.createdAt);
|
|
2068
|
+
if (decodedRecords.some(item => {
|
|
2069
|
+
if (item.kind === 'secret-replay-tombstone') {
|
|
2070
|
+
return Date.parse(item.value.expiredAt) > captureTime;
|
|
2071
|
+
}
|
|
2072
|
+
if (item.kind === 'project-invitation') {
|
|
2073
|
+
return captureTime < Date.parse(item.value.createdAt)
|
|
2074
|
+
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
2075
|
+
!== invitationEnvelopeIds.has(item.recordId);
|
|
2076
|
+
}
|
|
2077
|
+
if (item.kind === 'transferred-membership-claim-override') {
|
|
2078
|
+
return captureTime < Date.parse(item.value.createdAt)
|
|
2079
|
+
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
2080
|
+
!== claimOverrideEnvelopeIds.has(item.recordId);
|
|
2081
|
+
}
|
|
2082
|
+
return false;
|
|
2083
|
+
}))
|
|
2084
|
+
throw invalidPayload('records');
|
|
1457
2085
|
if (decodedRecords.some(item => {
|
|
1458
2086
|
switch (item.kind) {
|
|
1459
2087
|
case 'lifecycle-journal': return item.value.operationId === decodedManifest.operationId;
|