@claudian-collab/protocol 3.2.1 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -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 +102 -4
- package/dist/CollabProjectBackupCheckpoint.js +694 -12
- package/dist/CollabProjectMembership.d.ts +318 -0
- package/dist/CollabProjectMembership.js +714 -0
- package/dist/CollabProtocol.d.ts +2 -1
- package/dist/esm/CollabCloudBinding.mjs +7 -0
- package/dist/esm/CollabControlOperationCodecs.mjs +2 -0
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +694 -12
- package/dist/esm/CollabProjectMembership.mjs +709 -0
- package/dist/esm/index.mjs +2 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +10 -3
- package/package.json +1 -1
|
@@ -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,29 @@ 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
|
+
'membership-idempotency-tombstone',
|
|
36
|
+
'project-membership-recovery',
|
|
37
|
+
'secret-replay-tombstone',
|
|
28
38
|
]);
|
|
29
39
|
exports.COLLAB_PROJECT_BACKUP_RECORD_KINDS = Object.freeze([
|
|
30
|
-
...CollabProjectCheckpoint_1.
|
|
40
|
+
...CollabProjectCheckpoint_1.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS,
|
|
41
|
+
'cloud-event',
|
|
42
|
+
'cloud-event-cursor',
|
|
43
|
+
'idempotency-result',
|
|
44
|
+
'principal-binding',
|
|
45
|
+
'repository-placement',
|
|
31
46
|
...BACKUP_CONTINUITY_RECORD_KINDS,
|
|
32
|
-
|
|
47
|
+
'terminal-responder',
|
|
48
|
+
'protected-claim-envelope',
|
|
49
|
+
'tombstone',
|
|
50
|
+
'schema-catalog',
|
|
51
|
+
'server-compatibility',
|
|
52
|
+
'authority-volume-pair',
|
|
33
53
|
]);
|
|
34
54
|
const CONTINUITY_KIND_SET = new Set(BACKUP_CONTINUITY_RECORD_KINDS);
|
|
35
55
|
const BACKUP_KIND_SET = new Set(exports.COLLAB_PROJECT_BACKUP_RECORD_KINDS);
|
|
@@ -59,7 +79,9 @@ const DELETE_ACTIVE_PHASE_SET = new Set([
|
|
|
59
79
|
'tombstoned',
|
|
60
80
|
]);
|
|
61
81
|
const PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET = new Set([
|
|
82
|
+
'createProjectInvitation',
|
|
62
83
|
'getTransferredMembershipClaim',
|
|
84
|
+
'reissueTransferredMembershipClaim',
|
|
63
85
|
'rotateTransferredMembershipClaims',
|
|
64
86
|
]);
|
|
65
87
|
function invalidPayload(field) {
|
|
@@ -117,6 +139,9 @@ function timestampValue(value, field) {
|
|
|
117
139
|
function timestamp(source, field) {
|
|
118
140
|
return timestampValue(source[field], field);
|
|
119
141
|
}
|
|
142
|
+
function hasExactDuration(start, end, durationMs) {
|
|
143
|
+
return Date.parse(end) === Date.parse(start) + durationMs;
|
|
144
|
+
}
|
|
120
145
|
function nullableTimestamp(source, field) {
|
|
121
146
|
return source[field] === null ? null : timestampValue(source[field], field);
|
|
122
147
|
}
|
|
@@ -467,7 +492,15 @@ function lifecycleJournalRecord(source, recordId, revision) {
|
|
|
467
492
|
if (recordId !== operationId)
|
|
468
493
|
throw invalidPayload('recordId');
|
|
469
494
|
const operationKind = literal(value, 'operationKind', [
|
|
470
|
-
'authority-transfer',
|
|
495
|
+
'authority-transfer',
|
|
496
|
+
'backup',
|
|
497
|
+
'create-project',
|
|
498
|
+
'delete',
|
|
499
|
+
'export',
|
|
500
|
+
'join-project',
|
|
501
|
+
'leave',
|
|
502
|
+
'remove-member',
|
|
503
|
+
'retire',
|
|
471
504
|
]);
|
|
472
505
|
const direction = value.direction === null
|
|
473
506
|
? null
|
|
@@ -838,6 +871,385 @@ function leaveFormerPrincipalReplayRecord(source, recordId, revision) {
|
|
|
838
871
|
},
|
|
839
872
|
};
|
|
840
873
|
}
|
|
874
|
+
function projectInvitationRecord(source, recordId, revision) {
|
|
875
|
+
const value = exactRecord(source.value, 'value', [
|
|
876
|
+
'createdAt',
|
|
877
|
+
'expiresAt',
|
|
878
|
+
'idempotencyKey',
|
|
879
|
+
'invitationId',
|
|
880
|
+
'issuedByMemberId',
|
|
881
|
+
'projectId',
|
|
882
|
+
'requestFingerprint',
|
|
883
|
+
'revision',
|
|
884
|
+
'secretReplayExpiresAt',
|
|
885
|
+
'secretSha256',
|
|
886
|
+
'state',
|
|
887
|
+
'terminalAt',
|
|
888
|
+
]);
|
|
889
|
+
const invitationId = token(value, 'invitationId');
|
|
890
|
+
if (recordId !== invitationId)
|
|
891
|
+
throw invalidPayload('recordId');
|
|
892
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
893
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
894
|
+
const secretReplayExpiresAt = timestamp(value, 'secretReplayExpiresAt');
|
|
895
|
+
const state = literal(value, 'state', [
|
|
896
|
+
'active', 'redeeming', 'redeemed', 'revoked', 'expired',
|
|
897
|
+
]);
|
|
898
|
+
const terminalAt = nullableTimestamp(value, 'terminalAt');
|
|
899
|
+
const terminal = state === 'redeemed' || state === 'revoked' || state === 'expired';
|
|
900
|
+
if (!hasExactDuration(createdAt, expiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.invitationTtlMs)
|
|
901
|
+
|| !hasExactDuration(createdAt, secretReplayExpiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs)
|
|
902
|
+
|| terminal !== (terminalAt !== null)
|
|
903
|
+
|| (terminalAt !== null && Date.parse(terminalAt) < Date.parse(createdAt)))
|
|
904
|
+
throw invalidPayload('state');
|
|
905
|
+
return {
|
|
906
|
+
kind: 'project-invitation',
|
|
907
|
+
recordId,
|
|
908
|
+
revision,
|
|
909
|
+
value: {
|
|
910
|
+
createdAt,
|
|
911
|
+
expiresAt,
|
|
912
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
913
|
+
invitationId,
|
|
914
|
+
issuedByMemberId: token(value, 'issuedByMemberId', CollabValidation_1.isCollabMemberId),
|
|
915
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
916
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
917
|
+
revision: positiveInteger(value, 'revision'),
|
|
918
|
+
secretReplayExpiresAt,
|
|
919
|
+
secretSha256: sha256(value, 'secretSha256'),
|
|
920
|
+
state,
|
|
921
|
+
terminalAt,
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function protectedSecretEnvelopeFields(value) {
|
|
926
|
+
const ciphertext = boundedString(value, 'ciphertext', 4096);
|
|
927
|
+
const nonce = boundedString(value, 'nonce', 128);
|
|
928
|
+
if (!BASE64URL_PATTERN.test(ciphertext) || !BASE64URL_PATTERN.test(nonce)) {
|
|
929
|
+
throw invalidPayload('ciphertext');
|
|
930
|
+
}
|
|
931
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
932
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
933
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt))
|
|
934
|
+
throw invalidPayload('expiresAt');
|
|
935
|
+
return {
|
|
936
|
+
associatedDataSha256: sha256(value, 'associatedDataSha256'),
|
|
937
|
+
ciphertext,
|
|
938
|
+
createdAt,
|
|
939
|
+
expiresAt,
|
|
940
|
+
keyId: token(value, 'keyId'),
|
|
941
|
+
nonce,
|
|
942
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function protectedInvitationEnvelopeRecord(source, recordId, revision) {
|
|
946
|
+
const value = exactRecord(source.value, 'value', [
|
|
947
|
+
'associatedDataSha256',
|
|
948
|
+
'ciphertext',
|
|
949
|
+
'createdAt',
|
|
950
|
+
'expiresAt',
|
|
951
|
+
'invitationId',
|
|
952
|
+
'keyId',
|
|
953
|
+
'nonce',
|
|
954
|
+
'projectId',
|
|
955
|
+
]);
|
|
956
|
+
const invitationId = token(value, 'invitationId');
|
|
957
|
+
if (recordId !== invitationId)
|
|
958
|
+
throw invalidPayload('recordId');
|
|
959
|
+
const envelope = protectedSecretEnvelopeFields(value);
|
|
960
|
+
return {
|
|
961
|
+
kind: 'protected-invitation-envelope',
|
|
962
|
+
recordId,
|
|
963
|
+
revision,
|
|
964
|
+
value: {
|
|
965
|
+
associatedDataSha256: envelope.associatedDataSha256,
|
|
966
|
+
ciphertext: envelope.ciphertext,
|
|
967
|
+
createdAt: envelope.createdAt,
|
|
968
|
+
expiresAt: envelope.expiresAt,
|
|
969
|
+
invitationId,
|
|
970
|
+
keyId: envelope.keyId,
|
|
971
|
+
nonce: envelope.nonce,
|
|
972
|
+
projectId: envelope.projectId,
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function transferredMembershipClaimOverrideRecord(source, recordId, revision) {
|
|
977
|
+
const value = exactRecord(source.value, 'value', [
|
|
978
|
+
'claimGeneration',
|
|
979
|
+
'claimSha256',
|
|
980
|
+
'createdAt',
|
|
981
|
+
'expiresAt',
|
|
982
|
+
'idempotencyKey',
|
|
983
|
+
'managerMemberId',
|
|
984
|
+
'memberId',
|
|
985
|
+
'projectId',
|
|
986
|
+
'redemptionReceiptId',
|
|
987
|
+
'requestFingerprint',
|
|
988
|
+
'secretReplayExpiresAt',
|
|
989
|
+
'state',
|
|
990
|
+
'supersededClaimSha256',
|
|
991
|
+
'targetPrincipalId',
|
|
992
|
+
'transferId',
|
|
993
|
+
'updatedAt',
|
|
994
|
+
]);
|
|
995
|
+
const transferId = token(value, 'transferId');
|
|
996
|
+
const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
|
|
997
|
+
const claimGeneration = positiveInteger(value, 'claimGeneration');
|
|
998
|
+
if (recordId !== `${transferId}:${memberId}:${claimGeneration}`) {
|
|
999
|
+
throw invalidPayload('recordId');
|
|
1000
|
+
}
|
|
1001
|
+
const state = literal(value, 'state', [
|
|
1002
|
+
'active', 'redeemed', 'revoked', 'superseded', 'expired',
|
|
1003
|
+
]);
|
|
1004
|
+
const targetPrincipalId = nullableToken(value, 'targetPrincipalId', candidate => (typeof candidate === 'string' && PRINCIPAL_PATTERN.test(candidate)));
|
|
1005
|
+
const redemptionReceiptId = nullableToken(value, 'redemptionReceiptId');
|
|
1006
|
+
if ((state === 'redeemed') !== (targetPrincipalId !== null && redemptionReceiptId !== null)
|
|
1007
|
+
|| (state !== 'redeemed' && (targetPrincipalId !== null || redemptionReceiptId !== null)))
|
|
1008
|
+
throw invalidPayload('state');
|
|
1009
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
1010
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
1011
|
+
const secretReplayExpiresAt = timestamp(value, 'secretReplayExpiresAt');
|
|
1012
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
1013
|
+
if (!hasExactDuration(createdAt, expiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.transferredClaimTtlMs)
|
|
1014
|
+
|| !hasExactDuration(createdAt, secretReplayExpiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs)
|
|
1015
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt))
|
|
1016
|
+
throw invalidPayload('updatedAt');
|
|
1017
|
+
return {
|
|
1018
|
+
kind: 'transferred-membership-claim-override',
|
|
1019
|
+
recordId,
|
|
1020
|
+
revision,
|
|
1021
|
+
value: {
|
|
1022
|
+
claimGeneration,
|
|
1023
|
+
claimSha256: sha256(value, 'claimSha256'),
|
|
1024
|
+
createdAt,
|
|
1025
|
+
expiresAt,
|
|
1026
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
1027
|
+
managerMemberId: token(value, 'managerMemberId', CollabValidation_1.isCollabMemberId),
|
|
1028
|
+
memberId,
|
|
1029
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1030
|
+
redemptionReceiptId,
|
|
1031
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1032
|
+
secretReplayExpiresAt,
|
|
1033
|
+
state,
|
|
1034
|
+
supersededClaimSha256: sha256(value, 'supersededClaimSha256'),
|
|
1035
|
+
targetPrincipalId,
|
|
1036
|
+
transferId,
|
|
1037
|
+
updatedAt,
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function protectedClaimOverrideEnvelopeRecord(source, recordId, revision) {
|
|
1042
|
+
const value = exactRecord(source.value, 'value', [
|
|
1043
|
+
'associatedDataSha256',
|
|
1044
|
+
'ciphertext',
|
|
1045
|
+
'claimGeneration',
|
|
1046
|
+
'createdAt',
|
|
1047
|
+
'expiresAt',
|
|
1048
|
+
'keyId',
|
|
1049
|
+
'memberId',
|
|
1050
|
+
'nonce',
|
|
1051
|
+
'projectId',
|
|
1052
|
+
'transferId',
|
|
1053
|
+
]);
|
|
1054
|
+
const transferId = token(value, 'transferId');
|
|
1055
|
+
const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
|
|
1056
|
+
const claimGeneration = positiveInteger(value, 'claimGeneration');
|
|
1057
|
+
if (recordId !== `${transferId}:${memberId}:${claimGeneration}`) {
|
|
1058
|
+
throw invalidPayload('recordId');
|
|
1059
|
+
}
|
|
1060
|
+
const envelope = protectedSecretEnvelopeFields(value);
|
|
1061
|
+
return {
|
|
1062
|
+
kind: 'protected-claim-override-envelope',
|
|
1063
|
+
recordId,
|
|
1064
|
+
revision,
|
|
1065
|
+
value: {
|
|
1066
|
+
associatedDataSha256: envelope.associatedDataSha256,
|
|
1067
|
+
ciphertext: envelope.ciphertext,
|
|
1068
|
+
claimGeneration,
|
|
1069
|
+
createdAt: envelope.createdAt,
|
|
1070
|
+
expiresAt: envelope.expiresAt,
|
|
1071
|
+
keyId: envelope.keyId,
|
|
1072
|
+
memberId,
|
|
1073
|
+
nonce: envelope.nonce,
|
|
1074
|
+
projectId: envelope.projectId,
|
|
1075
|
+
transferId,
|
|
1076
|
+
},
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function managerResponsibilityOfferRecord(source, recordId, revision) {
|
|
1080
|
+
const value = exactRecord(source.value, 'value', [
|
|
1081
|
+
'acknowledgedAt',
|
|
1082
|
+
'expiresAt',
|
|
1083
|
+
'idempotencyKey',
|
|
1084
|
+
'managerSetGenerationAtOffer',
|
|
1085
|
+
'offeredAt',
|
|
1086
|
+
'offerId',
|
|
1087
|
+
'projectId',
|
|
1088
|
+
'purpose',
|
|
1089
|
+
'requestFingerprint',
|
|
1090
|
+
'revision',
|
|
1091
|
+
'sourceManagerMemberId',
|
|
1092
|
+
'state',
|
|
1093
|
+
'targetMemberId',
|
|
1094
|
+
'targetMembershipRevisionAtOffer',
|
|
1095
|
+
'terminalAt',
|
|
1096
|
+
]);
|
|
1097
|
+
const offerId = token(value, 'offerId');
|
|
1098
|
+
if (recordId !== offerId)
|
|
1099
|
+
throw invalidPayload('recordId');
|
|
1100
|
+
const state = literal(value, 'state', [
|
|
1101
|
+
'offered', 'acknowledged', 'declined', 'cancelled', 'consumed', 'expired',
|
|
1102
|
+
]);
|
|
1103
|
+
const offeredAt = timestamp(value, 'offeredAt');
|
|
1104
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
1105
|
+
const acknowledgedAt = nullableTimestamp(value, 'acknowledgedAt');
|
|
1106
|
+
const terminalAt = nullableTimestamp(value, 'terminalAt');
|
|
1107
|
+
const terminal = state === 'declined'
|
|
1108
|
+
|| state === 'cancelled'
|
|
1109
|
+
|| state === 'consumed'
|
|
1110
|
+
|| state === 'expired';
|
|
1111
|
+
if (!hasExactDuration(offeredAt, expiresAt, CollabProjectMembership_1.COLLAB_PROJECT_MEMBERSHIP_LIMITS.managerResponsibilityOfferTtlMs)
|
|
1112
|
+
|| (state === 'offered' && acknowledgedAt !== null)
|
|
1113
|
+
|| (state === 'acknowledged' && acknowledgedAt === null)
|
|
1114
|
+
|| terminal !== (terminalAt !== null))
|
|
1115
|
+
throw invalidPayload('state');
|
|
1116
|
+
return {
|
|
1117
|
+
kind: 'manager-responsibility-offer',
|
|
1118
|
+
recordId,
|
|
1119
|
+
revision,
|
|
1120
|
+
value: {
|
|
1121
|
+
acknowledgedAt,
|
|
1122
|
+
expiresAt,
|
|
1123
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
1124
|
+
managerSetGenerationAtOffer: positiveInteger(value, 'managerSetGenerationAtOffer'),
|
|
1125
|
+
offeredAt,
|
|
1126
|
+
offerId,
|
|
1127
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1128
|
+
purpose: literal(value, 'purpose', ['manager-promotion', 'manager-leave']),
|
|
1129
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1130
|
+
revision: positiveInteger(value, 'revision'),
|
|
1131
|
+
sourceManagerMemberId: token(value, 'sourceManagerMemberId', CollabValidation_1.isCollabMemberId),
|
|
1132
|
+
state,
|
|
1133
|
+
targetMemberId: token(value, 'targetMemberId', CollabValidation_1.isCollabMemberId),
|
|
1134
|
+
targetMembershipRevisionAtOffer: positiveInteger(value, 'targetMembershipRevisionAtOffer'),
|
|
1135
|
+
terminalAt,
|
|
1136
|
+
},
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function membershipRecoveryRecord(source, recordId, revision) {
|
|
1140
|
+
const value = exactRecord(source.value, 'value', [
|
|
1141
|
+
'expectedMainOid',
|
|
1142
|
+
'expectedPersonalRefOid',
|
|
1143
|
+
'invitationId',
|
|
1144
|
+
'memberId',
|
|
1145
|
+
'operationId',
|
|
1146
|
+
'operationKind',
|
|
1147
|
+
'principalSha256',
|
|
1148
|
+
'projectId',
|
|
1149
|
+
'publicationMarkerSha256',
|
|
1150
|
+
'repositoryPlanSha256',
|
|
1151
|
+
'requestFingerprint',
|
|
1152
|
+
]);
|
|
1153
|
+
const operationId = token(value, 'operationId');
|
|
1154
|
+
if (recordId !== operationId)
|
|
1155
|
+
throw invalidPayload('recordId');
|
|
1156
|
+
const operationKind = literal(value, 'operationKind', [
|
|
1157
|
+
'create-project', 'join-project', 'remove-member',
|
|
1158
|
+
]);
|
|
1159
|
+
const invitationId = nullableToken(value, 'invitationId');
|
|
1160
|
+
const principalSha256 = nullableSha256(value, 'principalSha256');
|
|
1161
|
+
const publicationMarkerSha256 = nullableSha256(value, 'publicationMarkerSha256');
|
|
1162
|
+
const repositoryPlanSha256 = nullableSha256(value, 'repositoryPlanSha256');
|
|
1163
|
+
if ((operationKind === 'join-project') !== (invitationId !== null)
|
|
1164
|
+
|| (operationKind === 'remove-member') === (principalSha256 !== null)
|
|
1165
|
+
|| (operationKind === 'create-project') !== (repositoryPlanSha256 !== null)
|
|
1166
|
+
|| (operationKind !== 'create-project' && publicationMarkerSha256 !== null))
|
|
1167
|
+
throw invalidPayload('operationKind');
|
|
1168
|
+
return {
|
|
1169
|
+
kind: 'project-membership-recovery',
|
|
1170
|
+
recordId,
|
|
1171
|
+
revision,
|
|
1172
|
+
value: {
|
|
1173
|
+
expectedMainOid: token(value, 'expectedMainOid', CollabValidation_1.isCollabGitOid),
|
|
1174
|
+
expectedPersonalRefOid: token(value, 'expectedPersonalRefOid', CollabValidation_1.isCollabGitOid),
|
|
1175
|
+
invitationId,
|
|
1176
|
+
memberId: token(value, 'memberId', CollabValidation_1.isCollabMemberId),
|
|
1177
|
+
operationId,
|
|
1178
|
+
operationKind,
|
|
1179
|
+
principalSha256,
|
|
1180
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1181
|
+
publicationMarkerSha256,
|
|
1182
|
+
repositoryPlanSha256,
|
|
1183
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function membershipIdempotencyTombstoneRecord(source, recordId, revision) {
|
|
1188
|
+
const value = exactRecord(source.value, 'value', [
|
|
1189
|
+
'actorMemberId',
|
|
1190
|
+
'compactedAt',
|
|
1191
|
+
'idempotencyKey',
|
|
1192
|
+
'operation',
|
|
1193
|
+
'projectId',
|
|
1194
|
+
'requestFingerprint',
|
|
1195
|
+
]);
|
|
1196
|
+
const actorMemberId = token(value, 'actorMemberId', CollabValidation_1.isCollabMemberId);
|
|
1197
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1198
|
+
const operation = literal(value, 'operation', [
|
|
1199
|
+
'createManagerResponsibilityOffer',
|
|
1200
|
+
'acknowledgeManagerResponsibility',
|
|
1201
|
+
'declineManagerResponsibility',
|
|
1202
|
+
'cancelManagerResponsibilityOffer',
|
|
1203
|
+
'promoteManager',
|
|
1204
|
+
]);
|
|
1205
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1206
|
+
throw invalidPayload('recordId');
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
kind: 'membership-idempotency-tombstone',
|
|
1210
|
+
recordId,
|
|
1211
|
+
revision,
|
|
1212
|
+
value: {
|
|
1213
|
+
actorMemberId,
|
|
1214
|
+
compactedAt: timestamp(value, 'compactedAt'),
|
|
1215
|
+
idempotencyKey,
|
|
1216
|
+
operation,
|
|
1217
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1218
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1219
|
+
},
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
function secretReplayTombstoneRecord(source, recordId, revision) {
|
|
1223
|
+
const value = exactRecord(source.value, 'value', [
|
|
1224
|
+
'actorMemberId',
|
|
1225
|
+
'expiredAt',
|
|
1226
|
+
'idempotencyKey',
|
|
1227
|
+
'operation',
|
|
1228
|
+
'projectId',
|
|
1229
|
+
'requestFingerprint',
|
|
1230
|
+
]);
|
|
1231
|
+
const actorMemberId = token(value, 'actorMemberId', CollabValidation_1.isCollabMemberId);
|
|
1232
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1233
|
+
const operation = literal(value, 'operation', [
|
|
1234
|
+
'createProjectInvitation', 'reissueTransferredMembershipClaim',
|
|
1235
|
+
]);
|
|
1236
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1237
|
+
throw invalidPayload('recordId');
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
kind: 'secret-replay-tombstone',
|
|
1241
|
+
recordId,
|
|
1242
|
+
revision,
|
|
1243
|
+
value: {
|
|
1244
|
+
actorMemberId,
|
|
1245
|
+
expiredAt: timestamp(value, 'expiredAt'),
|
|
1246
|
+
idempotencyKey,
|
|
1247
|
+
operation,
|
|
1248
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1249
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1250
|
+
},
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
841
1253
|
function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
842
1254
|
switch (kind) {
|
|
843
1255
|
case 'lifecycle-journal': return lifecycleJournalRecord(source, recordId, revision);
|
|
@@ -855,6 +1267,21 @@ function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
|
855
1267
|
return terminalResponderReplayRecord(source, recordId, revision);
|
|
856
1268
|
case 'leave-former-principal-replay':
|
|
857
1269
|
return leaveFormerPrincipalReplayRecord(source, recordId, revision);
|
|
1270
|
+
case 'project-invitation': return projectInvitationRecord(source, recordId, revision);
|
|
1271
|
+
case 'protected-invitation-envelope':
|
|
1272
|
+
return protectedInvitationEnvelopeRecord(source, recordId, revision);
|
|
1273
|
+
case 'transferred-membership-claim-override':
|
|
1274
|
+
return transferredMembershipClaimOverrideRecord(source, recordId, revision);
|
|
1275
|
+
case 'protected-claim-override-envelope':
|
|
1276
|
+
return protectedClaimOverrideEnvelopeRecord(source, recordId, revision);
|
|
1277
|
+
case 'manager-responsibility-offer':
|
|
1278
|
+
return managerResponsibilityOfferRecord(source, recordId, revision);
|
|
1279
|
+
case 'membership-idempotency-tombstone':
|
|
1280
|
+
return membershipIdempotencyTombstoneRecord(source, recordId, revision);
|
|
1281
|
+
case 'project-membership-recovery':
|
|
1282
|
+
return membershipRecoveryRecord(source, recordId, revision);
|
|
1283
|
+
case 'secret-replay-tombstone':
|
|
1284
|
+
return secretReplayTombstoneRecord(source, recordId, revision);
|
|
858
1285
|
}
|
|
859
1286
|
}
|
|
860
1287
|
function kindOrder(kind) {
|
|
@@ -911,6 +1338,33 @@ function lifecycleJournalHasInvalidSemantics(item) {
|
|
|
911
1338
|
}
|
|
912
1339
|
if (hasCheckpoint)
|
|
913
1340
|
return true;
|
|
1341
|
+
if (value.operationKind === 'create-project'
|
|
1342
|
+
|| value.operationKind === 'join-project'
|
|
1343
|
+
|| value.operationKind === 'remove-member') {
|
|
1344
|
+
const phases = value.operationKind === 'create-project'
|
|
1345
|
+
? new Set([
|
|
1346
|
+
'prepared',
|
|
1347
|
+
'repository-publication-intent',
|
|
1348
|
+
'repository-published',
|
|
1349
|
+
'activated',
|
|
1350
|
+
])
|
|
1351
|
+
: value.operationKind === 'join-project'
|
|
1352
|
+
? new Set(['prepared', 'membership-pending', 'personal-ref-created', 'membership-active'])
|
|
1353
|
+
: new Set(['prepared', 'membership-revoked', 'personal-ref-removed']);
|
|
1354
|
+
const statePhaseValid = (value.state === 'active' && phases.has(value.phase))
|
|
1355
|
+
|| (value.state === 'recovery-required'
|
|
1356
|
+
&& value.phase === 'recovery-required'
|
|
1357
|
+
&& value.recoveryFromPhase !== null
|
|
1358
|
+
&& phases.has(value.recoveryFromPhase))
|
|
1359
|
+
|| (value.state === 'completed' && value.phase === 'completed');
|
|
1360
|
+
const resultRequired = value.state === 'completed'
|
|
1361
|
+
|| (value.operationKind === 'create-project' && effectivePhase === 'activated')
|
|
1362
|
+
|| (value.operationKind === 'join-project' && effectivePhase === 'membership-active')
|
|
1363
|
+
|| (value.operationKind === 'remove-member'
|
|
1364
|
+
&& (effectivePhase === 'membership-revoked'
|
|
1365
|
+
|| effectivePhase === 'personal-ref-removed'));
|
|
1366
|
+
return !statePhaseValid || hasResult !== resultRequired;
|
|
1367
|
+
}
|
|
914
1368
|
if (value.operationKind === 'leave') {
|
|
915
1369
|
const statePhaseValid = (value.state === 'active' && (value.phase === 'prepared'
|
|
916
1370
|
|| value.phase === 'membership-left'
|
|
@@ -979,13 +1433,42 @@ function validateContinuity(records) {
|
|
|
979
1433
|
const leaveReplays = new Map(records
|
|
980
1434
|
.filter((item) => (item.kind === 'leave-former-principal-replay'))
|
|
981
1435
|
.map(item => [item.value.operationId, item]));
|
|
1436
|
+
const invitations = new Map(records
|
|
1437
|
+
.filter((item) => (item.kind === 'project-invitation'))
|
|
1438
|
+
.map(item => [item.value.invitationId, item]));
|
|
1439
|
+
const invitationEnvelopes = new Map(records
|
|
1440
|
+
.filter((item) => (item.kind === 'protected-invitation-envelope'))
|
|
1441
|
+
.map(item => [item.value.invitationId, item]));
|
|
1442
|
+
const claimOverrides = records.filter((item) => (item.kind === 'transferred-membership-claim-override'));
|
|
1443
|
+
const claimOverrideEnvelopes = new Map(records
|
|
1444
|
+
.filter((item) => (item.kind === 'protected-claim-override-envelope'))
|
|
1445
|
+
.map(item => [
|
|
1446
|
+
`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`,
|
|
1447
|
+
item,
|
|
1448
|
+
]));
|
|
1449
|
+
const managerOffers = records.filter((item) => (item.kind === 'manager-responsibility-offer'));
|
|
1450
|
+
const membershipIdempotencyTombstones = records.filter((item) => (item.kind === 'membership-idempotency-tombstone'));
|
|
1451
|
+
const membershipRecoveries = new Map(records
|
|
1452
|
+
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1453
|
+
.map(item => [item.value.operationId, item]));
|
|
1454
|
+
const secretReplayTombstones = records.filter((item) => (item.kind === 'secret-replay-tombstone'));
|
|
982
1455
|
const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
|
|
983
1456
|
if (nonterminalLifecycles.length > 1
|
|
984
1457
|
|| principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
|
|
985
1458
|
|| new Set(principalBindingRecords.map(item => item.value.principalId)).size
|
|
986
1459
|
!== principalBindingRecords.length
|
|
987
1460
|
|| 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
|
|
1461
|
+
|| new Set(redemptionReceiptRecords.map(item => (`${item.value.receipt.transferId}:${item.value.receipt.receiptId}`))).size !== redemptionReceiptRecords.length
|
|
1462
|
+
|| invitations.size !== records.filter(item => item.kind === 'project-invitation').length
|
|
1463
|
+
|| invitationEnvelopes.size
|
|
1464
|
+
!== records.filter(item => item.kind === 'protected-invitation-envelope').length
|
|
1465
|
+
|| new Set([...invitations.values()].map(item => (`${item.value.issuedByMemberId}:${item.value.idempotencyKey}`))).size !== invitations.size
|
|
1466
|
+
|| new Set(claimOverrides.map(item => (`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`))).size !== claimOverrides.length
|
|
1467
|
+
|| new Set(claimOverrides.map(item => (`${item.value.managerMemberId}:${item.value.idempotencyKey}`))).size !== claimOverrides.length
|
|
1468
|
+
|| claimOverrideEnvelopes.size
|
|
1469
|
+
!== records.filter(item => item.kind === 'protected-claim-override-envelope').length
|
|
1470
|
+
|| membershipRecoveries.size
|
|
1471
|
+
!== records.filter(item => item.kind === 'project-membership-recovery').length)
|
|
989
1472
|
throw invalidPayload('records');
|
|
990
1473
|
for (const lifecycle of lifecycles.values()) {
|
|
991
1474
|
if (lifecycleJournalHasInvalidSemantics(lifecycle)
|
|
@@ -996,6 +1479,11 @@ function validateContinuity(records) {
|
|
|
996
1479
|
if (lifecycle.value.operationKind === 'authority-transfer'
|
|
997
1480
|
&& !recoveries.has(lifecycle.value.operationId))
|
|
998
1481
|
throw invalidPayload('records');
|
|
1482
|
+
if ((lifecycle.value.operationKind === 'create-project'
|
|
1483
|
+
|| lifecycle.value.operationKind === 'join-project'
|
|
1484
|
+
|| lifecycle.value.operationKind === 'remove-member')
|
|
1485
|
+
&& !membershipRecoveries.has(lifecycle.value.operationId))
|
|
1486
|
+
throw invalidPayload('records');
|
|
999
1487
|
if (lifecycle.value.operationKind === 'leave') {
|
|
1000
1488
|
const actorMemberId = lifecycle.value.actorMemberId;
|
|
1001
1489
|
const actor = actorMemberId === null ? undefined : memberRecords.get(actorMemberId);
|
|
@@ -1172,6 +1660,13 @@ function validateContinuity(records) {
|
|
|
1172
1660
|
const lifecycle = lifecycles.get(claim.value.transferId);
|
|
1173
1661
|
const recovery = recoveries.get(claim.value.transferId);
|
|
1174
1662
|
const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
|
|
1663
|
+
const redeemedOverride = redemptionReceipt === undefined
|
|
1664
|
+
? undefined
|
|
1665
|
+
: claimOverrides.find(item => (item.value.transferId === claim.value.transferId
|
|
1666
|
+
&& item.value.memberId === claim.value.memberId
|
|
1667
|
+
&& item.value.state === 'redeemed'
|
|
1668
|
+
&& item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
|
|
1669
|
+
&& item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
|
|
1175
1670
|
if (memberRecords.get(claim.value.memberId)?.value.status !== 'active'
|
|
1176
1671
|
|| recovery === undefined
|
|
1177
1672
|
|| recovery.value.sourceAuthority.kind !== 'lan'
|
|
@@ -1181,7 +1676,11 @@ function validateContinuity(records) {
|
|
|
1181
1676
|
|| claim.value.expiresAt !== recovery.value.expiresAt
|
|
1182
1677
|
|| (claim.value.targetPrincipalId !== null
|
|
1183
1678
|
&& principalBindings.get(claim.value.memberId) !== claim.value.targetPrincipalId)
|
|
1184
|
-
|| (claim.value.state === 'redeemed'
|
|
1679
|
+
|| (claim.value.state === 'redeemed' && redemptionReceipt === undefined)
|
|
1680
|
+
|| (redemptionReceipt !== undefined
|
|
1681
|
+
&& claim.value.state !== 'redeemed'
|
|
1682
|
+
&& redeemedOverride === undefined)
|
|
1683
|
+
|| (claim.value.state === 'redeemed' && redeemedOverride !== undefined)) {
|
|
1185
1684
|
throw invalidPayload('records');
|
|
1186
1685
|
}
|
|
1187
1686
|
}
|
|
@@ -1197,6 +1696,153 @@ function validateContinuity(records) {
|
|
|
1197
1696
|
!== responderReplays.has(terminal.value.operationId))
|
|
1198
1697
|
throw invalidPayload('records');
|
|
1199
1698
|
}
|
|
1699
|
+
for (const recovery of membershipRecoveries.values()) {
|
|
1700
|
+
const lifecycle = lifecycles.get(recovery.value.operationId);
|
|
1701
|
+
const invitation = recovery.value.invitationId === null
|
|
1702
|
+
? undefined
|
|
1703
|
+
: invitations.get(recovery.value.invitationId);
|
|
1704
|
+
const member = memberRecords.get(recovery.value.memberId);
|
|
1705
|
+
if (lifecycle === undefined
|
|
1706
|
+
|| lifecycle.value.operationKind !== recovery.value.operationKind
|
|
1707
|
+
|| lifecycle.value.requestFingerprint !== recovery.value.requestFingerprint
|
|
1708
|
+
|| lifecycle.value.state !== 'completed'
|
|
1709
|
+
|| member === undefined
|
|
1710
|
+
|| (recovery.value.operationKind === 'join-project' && invitation === undefined)
|
|
1711
|
+
|| (recovery.value.operationKind !== 'join-project' && invitation !== undefined)
|
|
1712
|
+
|| (recovery.value.operationKind === 'create-project'
|
|
1713
|
+
&& lifecycle.value.actorMemberId !== null)
|
|
1714
|
+
|| (recovery.value.operationKind === 'join-project'
|
|
1715
|
+
&& lifecycle.value.actorMemberId !== null)
|
|
1716
|
+
|| (recovery.value.operationKind === 'remove-member'
|
|
1717
|
+
&& lifecycle.value.actorMemberId === null)
|
|
1718
|
+
|| (recovery.value.operationKind === 'join-project'
|
|
1719
|
+
&& invitation?.value.state !== 'redeemed')
|
|
1720
|
+
|| (recovery.value.operationKind === 'remove-member' && (member.value.status !== 'revoked'
|
|
1721
|
+
|| principalBindings.has(recovery.value.memberId))))
|
|
1722
|
+
throw invalidPayload('records');
|
|
1723
|
+
}
|
|
1724
|
+
for (const invitation of invitations.values()) {
|
|
1725
|
+
const envelope = invitationEnvelopes.get(invitation.value.invitationId);
|
|
1726
|
+
const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'createProjectInvitation'
|
|
1727
|
+
&& item.value.actorMemberId === invitation.value.issuedByMemberId
|
|
1728
|
+
&& item.value.idempotencyKey === invitation.value.idempotencyKey
|
|
1729
|
+
&& item.value.requestFingerprint === invitation.value.requestFingerprint));
|
|
1730
|
+
if (!members.has(invitation.value.issuedByMemberId)
|
|
1731
|
+
|| (envelope === undefined) === (tombstone === undefined)
|
|
1732
|
+
|| (tombstone !== undefined
|
|
1733
|
+
&& tombstone.value.expiredAt !== invitation.value.secretReplayExpiresAt)
|
|
1734
|
+
|| (envelope !== undefined && (envelope.value.projectId !== invitation.value.projectId
|
|
1735
|
+
|| envelope.value.createdAt !== invitation.value.createdAt
|
|
1736
|
+
|| envelope.value.expiresAt !== invitation.value.secretReplayExpiresAt)))
|
|
1737
|
+
throw invalidPayload('records');
|
|
1738
|
+
}
|
|
1739
|
+
for (const envelope of invitationEnvelopes.values()) {
|
|
1740
|
+
if (!invitations.has(envelope.value.invitationId))
|
|
1741
|
+
throw invalidPayload('records');
|
|
1742
|
+
}
|
|
1743
|
+
const overridesByMember = new Map();
|
|
1744
|
+
for (const override of claimOverrides) {
|
|
1745
|
+
const identity = `${override.value.transferId}:${override.value.memberId}`;
|
|
1746
|
+
const values = overridesByMember.get(identity) ?? [];
|
|
1747
|
+
values.push(override);
|
|
1748
|
+
overridesByMember.set(identity, values);
|
|
1749
|
+
const envelope = claimOverrideEnvelopes.get(`${identity}:${override.value.claimGeneration}`);
|
|
1750
|
+
const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'reissueTransferredMembershipClaim'
|
|
1751
|
+
&& item.value.actorMemberId === override.value.managerMemberId
|
|
1752
|
+
&& item.value.idempotencyKey === override.value.idempotencyKey
|
|
1753
|
+
&& item.value.requestFingerprint === override.value.requestFingerprint));
|
|
1754
|
+
if (!members.has(override.value.memberId)
|
|
1755
|
+
|| !members.has(override.value.managerMemberId)
|
|
1756
|
+
|| (envelope === undefined) === (tombstone === undefined)
|
|
1757
|
+
|| (tombstone !== undefined
|
|
1758
|
+
&& tombstone.value.expiredAt !== override.value.secretReplayExpiresAt)
|
|
1759
|
+
|| (envelope !== undefined && (envelope.value.projectId !== override.value.projectId
|
|
1760
|
+
|| envelope.value.createdAt !== override.value.createdAt
|
|
1761
|
+
|| envelope.value.expiresAt !== override.value.secretReplayExpiresAt)))
|
|
1762
|
+
throw invalidPayload('records');
|
|
1763
|
+
}
|
|
1764
|
+
for (const overrides of overridesByMember.values()) {
|
|
1765
|
+
overrides.sort((left, right) => left.value.claimGeneration - right.value.claimGeneration);
|
|
1766
|
+
const highest = overrides.at(-1);
|
|
1767
|
+
const first = overrides[0];
|
|
1768
|
+
const identity = first === undefined
|
|
1769
|
+
? ''
|
|
1770
|
+
: `${first.value.transferId}:${first.value.memberId}`;
|
|
1771
|
+
const sourceClaim = claims.get(identity);
|
|
1772
|
+
const recovery = first === undefined ? undefined : recoveries.get(first.value.transferId);
|
|
1773
|
+
const member = first === undefined ? undefined : memberRecords.get(first.value.memberId);
|
|
1774
|
+
const principalBinding = first === undefined
|
|
1775
|
+
? undefined
|
|
1776
|
+
: principalBindings.get(first.value.memberId);
|
|
1777
|
+
const redemptionReceipt = redemptionReceipts.get(identity);
|
|
1778
|
+
const redeemedOverrides = overrides.filter(item => item.value.state === 'redeemed');
|
|
1779
|
+
if (first === undefined
|
|
1780
|
+
|| sourceClaim === undefined
|
|
1781
|
+
|| recovery?.value.sourceAuthority.kind !== 'lan'
|
|
1782
|
+
|| member?.value.status !== 'active'
|
|
1783
|
+
|| sourceClaim.value.state === 'redeemed'
|
|
1784
|
+
|| overrides.some((item, index) => item.value.claimGeneration !== index + 1)
|
|
1785
|
+
|| overrides.some((item, index) => item.value.supersededClaimSha256 !== (index === 0
|
|
1786
|
+
? sourceClaim.value.claimSha256
|
|
1787
|
+
: overrides[index - 1].value.claimSha256))
|
|
1788
|
+
|| new Set(overrides.map(item => item.value.claimSha256)).size !== overrides.length
|
|
1789
|
+
|| overrides.filter(item => item.value.state === 'active').length > 1
|
|
1790
|
+
|| overrides.some(item => item.value.state === 'active' && item !== highest)
|
|
1791
|
+
|| redeemedOverrides.length > 1
|
|
1792
|
+
|| redeemedOverrides.some(item => item !== highest)
|
|
1793
|
+
|| highest?.value.state === 'superseded'
|
|
1794
|
+
|| (highest?.value.state === 'active' && (principalBinding !== undefined
|
|
1795
|
+
|| redemptionReceipt !== undefined))
|
|
1796
|
+
|| (highest?.value.state === 'redeemed' && (principalBinding !== highest.value.targetPrincipalId
|
|
1797
|
+
|| redemptionReceipt === undefined
|
|
1798
|
+
|| redemptionReceipt.value.receipt.transferId !== highest.value.transferId
|
|
1799
|
+
|| redemptionReceipt.value.receipt.memberId !== highest.value.memberId
|
|
1800
|
+
|| redemptionReceipt.value.receipt.claimSha256 !== highest.value.claimSha256
|
|
1801
|
+
|| redemptionReceipt.value.receipt.receiptId !== highest.value.redemptionReceiptId
|
|
1802
|
+
|| redemptionReceipt.value.receipt.checkpointSha256
|
|
1803
|
+
!== sourceClaim.value.checkpointSha256))
|
|
1804
|
+
|| (highest?.value.state !== 'redeemed' && redemptionReceipt !== undefined))
|
|
1805
|
+
throw invalidPayload('records');
|
|
1806
|
+
}
|
|
1807
|
+
for (const envelope of claimOverrideEnvelopes.values()) {
|
|
1808
|
+
if (!claimOverrides.some(item => (item.value.transferId === envelope.value.transferId
|
|
1809
|
+
&& item.value.memberId === envelope.value.memberId
|
|
1810
|
+
&& item.value.claimGeneration === envelope.value.claimGeneration)))
|
|
1811
|
+
throw invalidPayload('records');
|
|
1812
|
+
}
|
|
1813
|
+
const currentOffers = managerOffers.filter(item => (item.value.state === 'offered' || item.value.state === 'acknowledged'));
|
|
1814
|
+
if (managerOffers.some(item => (item.value.sourceManagerMemberId === item.value.targetMemberId
|
|
1815
|
+
|| !members.has(item.value.sourceManagerMemberId)
|
|
1816
|
+
|| !members.has(item.value.targetMemberId)))
|
|
1817
|
+
|| new Set(currentOffers.map(item => item.value.sourceManagerMemberId)).size
|
|
1818
|
+
!== currentOffers.length
|
|
1819
|
+
|| new Set(currentOffers.map(item => item.value.targetMemberId)).size
|
|
1820
|
+
!== currentOffers.length)
|
|
1821
|
+
throw invalidPayload('records');
|
|
1822
|
+
for (const tombstone of secretReplayTombstones) {
|
|
1823
|
+
const liveSecretRecord = tombstone.value.operation === 'createProjectInvitation'
|
|
1824
|
+
? [...invitations.values()].some(item => (item.value.issuedByMemberId === tombstone.value.actorMemberId
|
|
1825
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1826
|
+
&& invitationEnvelopes.has(item.value.invitationId)))
|
|
1827
|
+
: claimOverrides.some(item => (item.value.managerMemberId === tombstone.value.actorMemberId
|
|
1828
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1829
|
+
&& claimOverrideEnvelopes.has(`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`)));
|
|
1830
|
+
if (!members.has(tombstone.value.actorMemberId) || liveSecretRecord) {
|
|
1831
|
+
throw invalidPayload('records');
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
for (const tombstone of membershipIdempotencyTombstones) {
|
|
1835
|
+
const exactResult = records.some(item => (item.kind === 'idempotency-result'
|
|
1836
|
+
&& item.value.memberId === tombstone.value.actorMemberId
|
|
1837
|
+
&& item.value.operation === tombstone.value.operation
|
|
1838
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1839
|
+
const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
|
|
1840
|
+
&& managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
|
|
1841
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1842
|
+
if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
|
|
1843
|
+
throw invalidPayload('records');
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1200
1846
|
for (const item of records) {
|
|
1201
1847
|
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1202
1848
|
throw invalidPayload('records');
|
|
@@ -1248,6 +1894,19 @@ function validateContinuity(records) {
|
|
|
1248
1894
|
const lifecycle = lifecycles.get(receipt.transferId);
|
|
1249
1895
|
const terminalPrincipal = terminalPrincipals.get(identity);
|
|
1250
1896
|
const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
|
|
1897
|
+
const redeemedOverride = claimOverrides.find(item => (item.value.transferId === receipt.transferId
|
|
1898
|
+
&& item.value.memberId === receipt.memberId
|
|
1899
|
+
&& item.value.state === 'redeemed'
|
|
1900
|
+
&& item.value.claimSha256 === receipt.claimSha256
|
|
1901
|
+
&& item.value.redemptionReceiptId === receipt.receiptId));
|
|
1902
|
+
const matchesSourceClaim = claim?.value.state === 'redeemed'
|
|
1903
|
+
&& claim.value.claimSha256 === receipt.claimSha256
|
|
1904
|
+
&& claim.value.checkpointSha256 === receipt.checkpointSha256
|
|
1905
|
+
&& claim.value.operationIntentId === receipt.operationIntentId
|
|
1906
|
+
&& claim.value.redemptionReceiptId === receipt.receiptId;
|
|
1907
|
+
const matchesOverride = claim !== undefined
|
|
1908
|
+
&& redeemedOverride !== undefined
|
|
1909
|
+
&& claim.value.checkpointSha256 === receipt.checkpointSha256;
|
|
1251
1910
|
if (recovery === undefined
|
|
1252
1911
|
|| lifecycle === undefined
|
|
1253
1912
|
|| memberRecords.get(receipt.memberId)?.value.status !== 'active'
|
|
@@ -1255,11 +1914,7 @@ function validateContinuity(records) {
|
|
|
1255
1914
|
|| !keys.has(`${receipt.transferId}:${receipt.receiptKeyId}`)
|
|
1256
1915
|
|| Date.parse(receipt.redeemedAt) > Date.parse(recovery.value.expiresAt)
|
|
1257
1916
|
|| (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))
|
|
1917
|
+
|| (!matchesSourceClaim && !matchesOverride)))
|
|
1263
1918
|
|| (!lanToCloud && (item.value.acknowledgedAt === null
|
|
1264
1919
|
|| receipt.memberId === recovery.value.targetHostMemberId
|
|
1265
1920
|
|| receipt.checkpointSha256 !== lifecycle.value.checkpointSha256
|
|
@@ -1454,6 +2109,33 @@ function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
|
|
|
1454
2109
|
function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
1455
2110
|
const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
|
|
1456
2111
|
const decodedRecords = decodeCollabProjectBackupCheckpointCoordinationNdjson(records.map(item => JSON.stringify(item)).join('\n') + '\n');
|
|
2112
|
+
const invitationEnvelopeIds = new Set(decodedRecords
|
|
2113
|
+
.filter(item => item.kind === 'protected-invitation-envelope')
|
|
2114
|
+
.map(item => item.recordId));
|
|
2115
|
+
const claimOverrideEnvelopeIds = new Set(decodedRecords
|
|
2116
|
+
.filter(item => item.kind === 'protected-claim-override-envelope')
|
|
2117
|
+
.map(item => item.recordId));
|
|
2118
|
+
const captureTime = Date.parse(decodedManifest.createdAt);
|
|
2119
|
+
if (decodedRecords.some(item => {
|
|
2120
|
+
if (item.kind === 'secret-replay-tombstone') {
|
|
2121
|
+
return Date.parse(item.value.expiredAt) > captureTime;
|
|
2122
|
+
}
|
|
2123
|
+
if (item.kind === 'membership-idempotency-tombstone') {
|
|
2124
|
+
return Date.parse(item.value.compactedAt) > captureTime;
|
|
2125
|
+
}
|
|
2126
|
+
if (item.kind === 'project-invitation') {
|
|
2127
|
+
return captureTime < Date.parse(item.value.createdAt)
|
|
2128
|
+
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
2129
|
+
!== invitationEnvelopeIds.has(item.recordId);
|
|
2130
|
+
}
|
|
2131
|
+
if (item.kind === 'transferred-membership-claim-override') {
|
|
2132
|
+
return captureTime < Date.parse(item.value.createdAt)
|
|
2133
|
+
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
2134
|
+
!== claimOverrideEnvelopeIds.has(item.recordId);
|
|
2135
|
+
}
|
|
2136
|
+
return false;
|
|
2137
|
+
}))
|
|
2138
|
+
throw invalidPayload('records');
|
|
1457
2139
|
if (decodedRecords.some(item => {
|
|
1458
2140
|
switch (item.kind) {
|
|
1459
2141
|
case 'lifecycle-journal': return item.value.operationId === decodedManifest.operationId;
|