@claudian-collab/protocol 3.2.1 → 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/CollabCloudBinding.mjs +7 -0
- package/dist/esm/CollabControlOperationCodecs.mjs +2 -0
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +640 -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
|
@@ -2,9 +2,11 @@ import { COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES, COLLAB_CLOUD_TO_LAN_TRAN
|
|
|
2
2
|
import { COLLAB_MAIN_REF, COLLAB_MEMBER_REF_PREFIX, } from './CollabConstants.mjs';
|
|
3
3
|
import { collabControlOperationCodec, } from './CollabControlOperationCodecs.mjs';
|
|
4
4
|
import { CollabError } from './CollabError.mjs';
|
|
5
|
-
import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS,
|
|
5
|
+
import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS, COLLAB_PROJECT_COORDINATION_FORMAT_VERSION, decodeCollabProjectCheckpointCoordinationNdjson, decodeCollabProjectCheckpointManifest, validateCollabProjectCheckpointConsistency, } from './CollabProjectCheckpoint.mjs';
|
|
6
|
+
import { COLLAB_PROJECT_MEMBERSHIP_LIMITS, } from './CollabProjectMembership.mjs';
|
|
6
7
|
import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
|
|
7
|
-
export const
|
|
8
|
+
export const COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE = 'pre-production-replaceable';
|
|
9
|
+
export const COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION = 3;
|
|
8
10
|
const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
9
11
|
'lifecycle-journal',
|
|
10
12
|
'authority-transfer-recovery',
|
|
@@ -15,11 +17,28 @@ const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
|
15
17
|
'terminal-principal',
|
|
16
18
|
'terminal-responder-replay',
|
|
17
19
|
'leave-former-principal-replay',
|
|
20
|
+
'project-invitation',
|
|
21
|
+
'protected-invitation-envelope',
|
|
22
|
+
'transferred-membership-claim-override',
|
|
23
|
+
'protected-claim-override-envelope',
|
|
24
|
+
'manager-responsibility-offer',
|
|
25
|
+
'project-membership-recovery',
|
|
26
|
+
'secret-replay-tombstone',
|
|
18
27
|
]);
|
|
19
28
|
export const COLLAB_PROJECT_BACKUP_RECORD_KINDS = Object.freeze([
|
|
20
|
-
...
|
|
29
|
+
...COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS,
|
|
30
|
+
'cloud-event',
|
|
31
|
+
'cloud-event-cursor',
|
|
32
|
+
'idempotency-result',
|
|
33
|
+
'principal-binding',
|
|
34
|
+
'repository-placement',
|
|
21
35
|
...BACKUP_CONTINUITY_RECORD_KINDS,
|
|
22
|
-
|
|
36
|
+
'terminal-responder',
|
|
37
|
+
'protected-claim-envelope',
|
|
38
|
+
'tombstone',
|
|
39
|
+
'schema-catalog',
|
|
40
|
+
'server-compatibility',
|
|
41
|
+
'authority-volume-pair',
|
|
23
42
|
]);
|
|
24
43
|
const CONTINUITY_KIND_SET = new Set(BACKUP_CONTINUITY_RECORD_KINDS);
|
|
25
44
|
const BACKUP_KIND_SET = new Set(COLLAB_PROJECT_BACKUP_RECORD_KINDS);
|
|
@@ -49,7 +68,9 @@ const DELETE_ACTIVE_PHASE_SET = new Set([
|
|
|
49
68
|
'tombstoned',
|
|
50
69
|
]);
|
|
51
70
|
const PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET = new Set([
|
|
71
|
+
'createProjectInvitation',
|
|
52
72
|
'getTransferredMembershipClaim',
|
|
73
|
+
'reissueTransferredMembershipClaim',
|
|
53
74
|
'rotateTransferredMembershipClaims',
|
|
54
75
|
]);
|
|
55
76
|
function invalidPayload(field) {
|
|
@@ -107,6 +128,9 @@ function timestampValue(value, field) {
|
|
|
107
128
|
function timestamp(source, field) {
|
|
108
129
|
return timestampValue(source[field], field);
|
|
109
130
|
}
|
|
131
|
+
function hasExactDuration(start, end, durationMs) {
|
|
132
|
+
return Date.parse(end) === Date.parse(start) + durationMs;
|
|
133
|
+
}
|
|
110
134
|
function nullableTimestamp(source, field) {
|
|
111
135
|
return source[field] === null ? null : timestampValue(source[field], field);
|
|
112
136
|
}
|
|
@@ -457,7 +481,15 @@ function lifecycleJournalRecord(source, recordId, revision) {
|
|
|
457
481
|
if (recordId !== operationId)
|
|
458
482
|
throw invalidPayload('recordId');
|
|
459
483
|
const operationKind = literal(value, 'operationKind', [
|
|
460
|
-
'authority-transfer',
|
|
484
|
+
'authority-transfer',
|
|
485
|
+
'backup',
|
|
486
|
+
'create-project',
|
|
487
|
+
'delete',
|
|
488
|
+
'export',
|
|
489
|
+
'join-project',
|
|
490
|
+
'leave',
|
|
491
|
+
'remove-member',
|
|
492
|
+
'retire',
|
|
461
493
|
]);
|
|
462
494
|
const direction = value.direction === null
|
|
463
495
|
? null
|
|
@@ -828,6 +860,350 @@ function leaveFormerPrincipalReplayRecord(source, recordId, revision) {
|
|
|
828
860
|
},
|
|
829
861
|
};
|
|
830
862
|
}
|
|
863
|
+
function projectInvitationRecord(source, recordId, revision) {
|
|
864
|
+
const value = exactRecord(source.value, 'value', [
|
|
865
|
+
'createdAt',
|
|
866
|
+
'expiresAt',
|
|
867
|
+
'idempotencyKey',
|
|
868
|
+
'invitationId',
|
|
869
|
+
'issuedByMemberId',
|
|
870
|
+
'projectId',
|
|
871
|
+
'requestFingerprint',
|
|
872
|
+
'revision',
|
|
873
|
+
'secretReplayExpiresAt',
|
|
874
|
+
'secretSha256',
|
|
875
|
+
'state',
|
|
876
|
+
'terminalAt',
|
|
877
|
+
]);
|
|
878
|
+
const invitationId = token(value, 'invitationId');
|
|
879
|
+
if (recordId !== invitationId)
|
|
880
|
+
throw invalidPayload('recordId');
|
|
881
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
882
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
883
|
+
const secretReplayExpiresAt = timestamp(value, 'secretReplayExpiresAt');
|
|
884
|
+
const state = literal(value, 'state', [
|
|
885
|
+
'active', 'redeeming', 'redeemed', 'revoked', 'expired',
|
|
886
|
+
]);
|
|
887
|
+
const terminalAt = nullableTimestamp(value, 'terminalAt');
|
|
888
|
+
const terminal = state === 'redeemed' || state === 'revoked' || state === 'expired';
|
|
889
|
+
if (!hasExactDuration(createdAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.invitationTtlMs)
|
|
890
|
+
|| !hasExactDuration(createdAt, secretReplayExpiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs)
|
|
891
|
+
|| terminal !== (terminalAt !== null)
|
|
892
|
+
|| (terminalAt !== null && Date.parse(terminalAt) < Date.parse(createdAt)))
|
|
893
|
+
throw invalidPayload('state');
|
|
894
|
+
return {
|
|
895
|
+
kind: 'project-invitation',
|
|
896
|
+
recordId,
|
|
897
|
+
revision,
|
|
898
|
+
value: {
|
|
899
|
+
createdAt,
|
|
900
|
+
expiresAt,
|
|
901
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
902
|
+
invitationId,
|
|
903
|
+
issuedByMemberId: token(value, 'issuedByMemberId', isCollabMemberId),
|
|
904
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
905
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
906
|
+
revision: positiveInteger(value, 'revision'),
|
|
907
|
+
secretReplayExpiresAt,
|
|
908
|
+
secretSha256: sha256(value, 'secretSha256'),
|
|
909
|
+
state,
|
|
910
|
+
terminalAt,
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function protectedSecretEnvelopeFields(value) {
|
|
915
|
+
const ciphertext = boundedString(value, 'ciphertext', 4096);
|
|
916
|
+
const nonce = boundedString(value, 'nonce', 128);
|
|
917
|
+
if (!BASE64URL_PATTERN.test(ciphertext) || !BASE64URL_PATTERN.test(nonce)) {
|
|
918
|
+
throw invalidPayload('ciphertext');
|
|
919
|
+
}
|
|
920
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
921
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
922
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt))
|
|
923
|
+
throw invalidPayload('expiresAt');
|
|
924
|
+
return {
|
|
925
|
+
associatedDataSha256: sha256(value, 'associatedDataSha256'),
|
|
926
|
+
ciphertext,
|
|
927
|
+
createdAt,
|
|
928
|
+
expiresAt,
|
|
929
|
+
keyId: token(value, 'keyId'),
|
|
930
|
+
nonce,
|
|
931
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function protectedInvitationEnvelopeRecord(source, recordId, revision) {
|
|
935
|
+
const value = exactRecord(source.value, 'value', [
|
|
936
|
+
'associatedDataSha256',
|
|
937
|
+
'ciphertext',
|
|
938
|
+
'createdAt',
|
|
939
|
+
'expiresAt',
|
|
940
|
+
'invitationId',
|
|
941
|
+
'keyId',
|
|
942
|
+
'nonce',
|
|
943
|
+
'projectId',
|
|
944
|
+
]);
|
|
945
|
+
const invitationId = token(value, 'invitationId');
|
|
946
|
+
if (recordId !== invitationId)
|
|
947
|
+
throw invalidPayload('recordId');
|
|
948
|
+
const envelope = protectedSecretEnvelopeFields(value);
|
|
949
|
+
return {
|
|
950
|
+
kind: 'protected-invitation-envelope',
|
|
951
|
+
recordId,
|
|
952
|
+
revision,
|
|
953
|
+
value: {
|
|
954
|
+
associatedDataSha256: envelope.associatedDataSha256,
|
|
955
|
+
ciphertext: envelope.ciphertext,
|
|
956
|
+
createdAt: envelope.createdAt,
|
|
957
|
+
expiresAt: envelope.expiresAt,
|
|
958
|
+
invitationId,
|
|
959
|
+
keyId: envelope.keyId,
|
|
960
|
+
nonce: envelope.nonce,
|
|
961
|
+
projectId: envelope.projectId,
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function transferredMembershipClaimOverrideRecord(source, recordId, revision) {
|
|
966
|
+
const value = exactRecord(source.value, 'value', [
|
|
967
|
+
'claimGeneration',
|
|
968
|
+
'claimSha256',
|
|
969
|
+
'createdAt',
|
|
970
|
+
'expiresAt',
|
|
971
|
+
'idempotencyKey',
|
|
972
|
+
'managerMemberId',
|
|
973
|
+
'memberId',
|
|
974
|
+
'projectId',
|
|
975
|
+
'redemptionReceiptId',
|
|
976
|
+
'requestFingerprint',
|
|
977
|
+
'secretReplayExpiresAt',
|
|
978
|
+
'state',
|
|
979
|
+
'supersededClaimSha256',
|
|
980
|
+
'targetPrincipalId',
|
|
981
|
+
'transferId',
|
|
982
|
+
'updatedAt',
|
|
983
|
+
]);
|
|
984
|
+
const transferId = token(value, 'transferId');
|
|
985
|
+
const memberId = token(value, 'memberId', isCollabMemberId);
|
|
986
|
+
const claimGeneration = positiveInteger(value, 'claimGeneration');
|
|
987
|
+
if (recordId !== `${transferId}:${memberId}:${claimGeneration}`) {
|
|
988
|
+
throw invalidPayload('recordId');
|
|
989
|
+
}
|
|
990
|
+
const state = literal(value, 'state', [
|
|
991
|
+
'active', 'redeemed', 'revoked', 'superseded', 'expired',
|
|
992
|
+
]);
|
|
993
|
+
const targetPrincipalId = nullableToken(value, 'targetPrincipalId', candidate => (typeof candidate === 'string' && PRINCIPAL_PATTERN.test(candidate)));
|
|
994
|
+
const redemptionReceiptId = nullableToken(value, 'redemptionReceiptId');
|
|
995
|
+
if ((state === 'redeemed') !== (targetPrincipalId !== null && redemptionReceiptId !== null)
|
|
996
|
+
|| (state !== 'redeemed' && (targetPrincipalId !== null || redemptionReceiptId !== null)))
|
|
997
|
+
throw invalidPayload('state');
|
|
998
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
999
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
1000
|
+
const secretReplayExpiresAt = timestamp(value, 'secretReplayExpiresAt');
|
|
1001
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
1002
|
+
if (!hasExactDuration(createdAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.transferredClaimTtlMs)
|
|
1003
|
+
|| !hasExactDuration(createdAt, secretReplayExpiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs)
|
|
1004
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt))
|
|
1005
|
+
throw invalidPayload('updatedAt');
|
|
1006
|
+
return {
|
|
1007
|
+
kind: 'transferred-membership-claim-override',
|
|
1008
|
+
recordId,
|
|
1009
|
+
revision,
|
|
1010
|
+
value: {
|
|
1011
|
+
claimGeneration,
|
|
1012
|
+
claimSha256: sha256(value, 'claimSha256'),
|
|
1013
|
+
createdAt,
|
|
1014
|
+
expiresAt,
|
|
1015
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
1016
|
+
managerMemberId: token(value, 'managerMemberId', isCollabMemberId),
|
|
1017
|
+
memberId,
|
|
1018
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1019
|
+
redemptionReceiptId,
|
|
1020
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1021
|
+
secretReplayExpiresAt,
|
|
1022
|
+
state,
|
|
1023
|
+
supersededClaimSha256: sha256(value, 'supersededClaimSha256'),
|
|
1024
|
+
targetPrincipalId,
|
|
1025
|
+
transferId,
|
|
1026
|
+
updatedAt,
|
|
1027
|
+
},
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function protectedClaimOverrideEnvelopeRecord(source, recordId, revision) {
|
|
1031
|
+
const value = exactRecord(source.value, 'value', [
|
|
1032
|
+
'associatedDataSha256',
|
|
1033
|
+
'ciphertext',
|
|
1034
|
+
'claimGeneration',
|
|
1035
|
+
'createdAt',
|
|
1036
|
+
'expiresAt',
|
|
1037
|
+
'keyId',
|
|
1038
|
+
'memberId',
|
|
1039
|
+
'nonce',
|
|
1040
|
+
'projectId',
|
|
1041
|
+
'transferId',
|
|
1042
|
+
]);
|
|
1043
|
+
const transferId = token(value, 'transferId');
|
|
1044
|
+
const memberId = token(value, 'memberId', isCollabMemberId);
|
|
1045
|
+
const claimGeneration = positiveInteger(value, 'claimGeneration');
|
|
1046
|
+
if (recordId !== `${transferId}:${memberId}:${claimGeneration}`) {
|
|
1047
|
+
throw invalidPayload('recordId');
|
|
1048
|
+
}
|
|
1049
|
+
const envelope = protectedSecretEnvelopeFields(value);
|
|
1050
|
+
return {
|
|
1051
|
+
kind: 'protected-claim-override-envelope',
|
|
1052
|
+
recordId,
|
|
1053
|
+
revision,
|
|
1054
|
+
value: {
|
|
1055
|
+
associatedDataSha256: envelope.associatedDataSha256,
|
|
1056
|
+
ciphertext: envelope.ciphertext,
|
|
1057
|
+
claimGeneration,
|
|
1058
|
+
createdAt: envelope.createdAt,
|
|
1059
|
+
expiresAt: envelope.expiresAt,
|
|
1060
|
+
keyId: envelope.keyId,
|
|
1061
|
+
memberId,
|
|
1062
|
+
nonce: envelope.nonce,
|
|
1063
|
+
projectId: envelope.projectId,
|
|
1064
|
+
transferId,
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function managerResponsibilityOfferRecord(source, recordId, revision) {
|
|
1069
|
+
const value = exactRecord(source.value, 'value', [
|
|
1070
|
+
'acknowledgedAt',
|
|
1071
|
+
'expiresAt',
|
|
1072
|
+
'idempotencyKey',
|
|
1073
|
+
'managerSetGenerationAtOffer',
|
|
1074
|
+
'offeredAt',
|
|
1075
|
+
'offerId',
|
|
1076
|
+
'projectId',
|
|
1077
|
+
'purpose',
|
|
1078
|
+
'requestFingerprint',
|
|
1079
|
+
'revision',
|
|
1080
|
+
'sourceManagerMemberId',
|
|
1081
|
+
'state',
|
|
1082
|
+
'targetMemberId',
|
|
1083
|
+
'targetMembershipRevisionAtOffer',
|
|
1084
|
+
'terminalAt',
|
|
1085
|
+
]);
|
|
1086
|
+
const offerId = token(value, 'offerId');
|
|
1087
|
+
if (recordId !== offerId)
|
|
1088
|
+
throw invalidPayload('recordId');
|
|
1089
|
+
const state = literal(value, 'state', [
|
|
1090
|
+
'offered', 'acknowledged', 'declined', 'cancelled', 'consumed', 'expired',
|
|
1091
|
+
]);
|
|
1092
|
+
const offeredAt = timestamp(value, 'offeredAt');
|
|
1093
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
1094
|
+
const acknowledgedAt = nullableTimestamp(value, 'acknowledgedAt');
|
|
1095
|
+
const terminalAt = nullableTimestamp(value, 'terminalAt');
|
|
1096
|
+
const terminal = state === 'declined'
|
|
1097
|
+
|| state === 'cancelled'
|
|
1098
|
+
|| state === 'consumed'
|
|
1099
|
+
|| state === 'expired';
|
|
1100
|
+
if (!hasExactDuration(offeredAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.managerResponsibilityOfferTtlMs)
|
|
1101
|
+
|| (state === 'offered' && acknowledgedAt !== null)
|
|
1102
|
+
|| (state === 'acknowledged' && acknowledgedAt === null)
|
|
1103
|
+
|| terminal !== (terminalAt !== null))
|
|
1104
|
+
throw invalidPayload('state');
|
|
1105
|
+
return {
|
|
1106
|
+
kind: 'manager-responsibility-offer',
|
|
1107
|
+
recordId,
|
|
1108
|
+
revision,
|
|
1109
|
+
value: {
|
|
1110
|
+
acknowledgedAt,
|
|
1111
|
+
expiresAt,
|
|
1112
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
1113
|
+
managerSetGenerationAtOffer: positiveInteger(value, 'managerSetGenerationAtOffer'),
|
|
1114
|
+
offeredAt,
|
|
1115
|
+
offerId,
|
|
1116
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1117
|
+
purpose: literal(value, 'purpose', ['manager-promotion', 'manager-leave']),
|
|
1118
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1119
|
+
revision: positiveInteger(value, 'revision'),
|
|
1120
|
+
sourceManagerMemberId: token(value, 'sourceManagerMemberId', isCollabMemberId),
|
|
1121
|
+
state,
|
|
1122
|
+
targetMemberId: token(value, 'targetMemberId', isCollabMemberId),
|
|
1123
|
+
targetMembershipRevisionAtOffer: positiveInteger(value, 'targetMembershipRevisionAtOffer'),
|
|
1124
|
+
terminalAt,
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function membershipRecoveryRecord(source, recordId, revision) {
|
|
1129
|
+
const value = exactRecord(source.value, 'value', [
|
|
1130
|
+
'expectedMainOid',
|
|
1131
|
+
'expectedPersonalRefOid',
|
|
1132
|
+
'invitationId',
|
|
1133
|
+
'memberId',
|
|
1134
|
+
'operationId',
|
|
1135
|
+
'operationKind',
|
|
1136
|
+
'principalSha256',
|
|
1137
|
+
'projectId',
|
|
1138
|
+
'publicationMarkerSha256',
|
|
1139
|
+
'repositoryPlanSha256',
|
|
1140
|
+
'requestFingerprint',
|
|
1141
|
+
]);
|
|
1142
|
+
const operationId = token(value, 'operationId');
|
|
1143
|
+
if (recordId !== operationId)
|
|
1144
|
+
throw invalidPayload('recordId');
|
|
1145
|
+
const operationKind = literal(value, 'operationKind', [
|
|
1146
|
+
'create-project', 'join-project', 'remove-member',
|
|
1147
|
+
]);
|
|
1148
|
+
const invitationId = nullableToken(value, 'invitationId');
|
|
1149
|
+
const principalSha256 = nullableSha256(value, 'principalSha256');
|
|
1150
|
+
const publicationMarkerSha256 = nullableSha256(value, 'publicationMarkerSha256');
|
|
1151
|
+
const repositoryPlanSha256 = nullableSha256(value, 'repositoryPlanSha256');
|
|
1152
|
+
if ((operationKind === 'join-project') !== (invitationId !== null)
|
|
1153
|
+
|| (operationKind === 'remove-member') === (principalSha256 !== null)
|
|
1154
|
+
|| (operationKind === 'create-project') !== (repositoryPlanSha256 !== null)
|
|
1155
|
+
|| (operationKind !== 'create-project' && publicationMarkerSha256 !== null))
|
|
1156
|
+
throw invalidPayload('operationKind');
|
|
1157
|
+
return {
|
|
1158
|
+
kind: 'project-membership-recovery',
|
|
1159
|
+
recordId,
|
|
1160
|
+
revision,
|
|
1161
|
+
value: {
|
|
1162
|
+
expectedMainOid: token(value, 'expectedMainOid', isCollabGitOid),
|
|
1163
|
+
expectedPersonalRefOid: token(value, 'expectedPersonalRefOid', isCollabGitOid),
|
|
1164
|
+
invitationId,
|
|
1165
|
+
memberId: token(value, 'memberId', isCollabMemberId),
|
|
1166
|
+
operationId,
|
|
1167
|
+
operationKind,
|
|
1168
|
+
principalSha256,
|
|
1169
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1170
|
+
publicationMarkerSha256,
|
|
1171
|
+
repositoryPlanSha256,
|
|
1172
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1173
|
+
},
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function secretReplayTombstoneRecord(source, recordId, revision) {
|
|
1177
|
+
const value = exactRecord(source.value, 'value', [
|
|
1178
|
+
'actorMemberId',
|
|
1179
|
+
'expiredAt',
|
|
1180
|
+
'idempotencyKey',
|
|
1181
|
+
'operation',
|
|
1182
|
+
'projectId',
|
|
1183
|
+
'requestFingerprint',
|
|
1184
|
+
]);
|
|
1185
|
+
const actorMemberId = token(value, 'actorMemberId', isCollabMemberId);
|
|
1186
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1187
|
+
const operation = literal(value, 'operation', [
|
|
1188
|
+
'createProjectInvitation', 'reissueTransferredMembershipClaim',
|
|
1189
|
+
]);
|
|
1190
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1191
|
+
throw invalidPayload('recordId');
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
kind: 'secret-replay-tombstone',
|
|
1195
|
+
recordId,
|
|
1196
|
+
revision,
|
|
1197
|
+
value: {
|
|
1198
|
+
actorMemberId,
|
|
1199
|
+
expiredAt: timestamp(value, 'expiredAt'),
|
|
1200
|
+
idempotencyKey,
|
|
1201
|
+
operation,
|
|
1202
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1203
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1204
|
+
},
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
831
1207
|
function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
832
1208
|
switch (kind) {
|
|
833
1209
|
case 'lifecycle-journal': return lifecycleJournalRecord(source, recordId, revision);
|
|
@@ -845,6 +1221,19 @@ function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
|
845
1221
|
return terminalResponderReplayRecord(source, recordId, revision);
|
|
846
1222
|
case 'leave-former-principal-replay':
|
|
847
1223
|
return leaveFormerPrincipalReplayRecord(source, recordId, revision);
|
|
1224
|
+
case 'project-invitation': return projectInvitationRecord(source, recordId, revision);
|
|
1225
|
+
case 'protected-invitation-envelope':
|
|
1226
|
+
return protectedInvitationEnvelopeRecord(source, recordId, revision);
|
|
1227
|
+
case 'transferred-membership-claim-override':
|
|
1228
|
+
return transferredMembershipClaimOverrideRecord(source, recordId, revision);
|
|
1229
|
+
case 'protected-claim-override-envelope':
|
|
1230
|
+
return protectedClaimOverrideEnvelopeRecord(source, recordId, revision);
|
|
1231
|
+
case 'manager-responsibility-offer':
|
|
1232
|
+
return managerResponsibilityOfferRecord(source, recordId, revision);
|
|
1233
|
+
case 'project-membership-recovery':
|
|
1234
|
+
return membershipRecoveryRecord(source, recordId, revision);
|
|
1235
|
+
case 'secret-replay-tombstone':
|
|
1236
|
+
return secretReplayTombstoneRecord(source, recordId, revision);
|
|
848
1237
|
}
|
|
849
1238
|
}
|
|
850
1239
|
function kindOrder(kind) {
|
|
@@ -901,6 +1290,33 @@ function lifecycleJournalHasInvalidSemantics(item) {
|
|
|
901
1290
|
}
|
|
902
1291
|
if (hasCheckpoint)
|
|
903
1292
|
return true;
|
|
1293
|
+
if (value.operationKind === 'create-project'
|
|
1294
|
+
|| value.operationKind === 'join-project'
|
|
1295
|
+
|| value.operationKind === 'remove-member') {
|
|
1296
|
+
const phases = value.operationKind === 'create-project'
|
|
1297
|
+
? new Set([
|
|
1298
|
+
'prepared',
|
|
1299
|
+
'repository-publication-intent',
|
|
1300
|
+
'repository-published',
|
|
1301
|
+
'activated',
|
|
1302
|
+
])
|
|
1303
|
+
: value.operationKind === 'join-project'
|
|
1304
|
+
? new Set(['prepared', 'membership-pending', 'personal-ref-created', 'membership-active'])
|
|
1305
|
+
: new Set(['prepared', 'membership-revoked', 'personal-ref-removed']);
|
|
1306
|
+
const statePhaseValid = (value.state === 'active' && phases.has(value.phase))
|
|
1307
|
+
|| (value.state === 'recovery-required'
|
|
1308
|
+
&& value.phase === 'recovery-required'
|
|
1309
|
+
&& value.recoveryFromPhase !== null
|
|
1310
|
+
&& phases.has(value.recoveryFromPhase))
|
|
1311
|
+
|| (value.state === 'completed' && value.phase === 'completed');
|
|
1312
|
+
const resultRequired = value.state === 'completed'
|
|
1313
|
+
|| (value.operationKind === 'create-project' && effectivePhase === 'activated')
|
|
1314
|
+
|| (value.operationKind === 'join-project' && effectivePhase === 'membership-active')
|
|
1315
|
+
|| (value.operationKind === 'remove-member'
|
|
1316
|
+
&& (effectivePhase === 'membership-revoked'
|
|
1317
|
+
|| effectivePhase === 'personal-ref-removed'));
|
|
1318
|
+
return !statePhaseValid || hasResult !== resultRequired;
|
|
1319
|
+
}
|
|
904
1320
|
if (value.operationKind === 'leave') {
|
|
905
1321
|
const statePhaseValid = (value.state === 'active' && (value.phase === 'prepared'
|
|
906
1322
|
|| value.phase === 'membership-left'
|
|
@@ -969,13 +1385,41 @@ function validateContinuity(records) {
|
|
|
969
1385
|
const leaveReplays = new Map(records
|
|
970
1386
|
.filter((item) => (item.kind === 'leave-former-principal-replay'))
|
|
971
1387
|
.map(item => [item.value.operationId, item]));
|
|
1388
|
+
const invitations = new Map(records
|
|
1389
|
+
.filter((item) => (item.kind === 'project-invitation'))
|
|
1390
|
+
.map(item => [item.value.invitationId, item]));
|
|
1391
|
+
const invitationEnvelopes = new Map(records
|
|
1392
|
+
.filter((item) => (item.kind === 'protected-invitation-envelope'))
|
|
1393
|
+
.map(item => [item.value.invitationId, item]));
|
|
1394
|
+
const claimOverrides = records.filter((item) => (item.kind === 'transferred-membership-claim-override'));
|
|
1395
|
+
const claimOverrideEnvelopes = new Map(records
|
|
1396
|
+
.filter((item) => (item.kind === 'protected-claim-override-envelope'))
|
|
1397
|
+
.map(item => [
|
|
1398
|
+
`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`,
|
|
1399
|
+
item,
|
|
1400
|
+
]));
|
|
1401
|
+
const managerOffers = records.filter((item) => (item.kind === 'manager-responsibility-offer'));
|
|
1402
|
+
const membershipRecoveries = new Map(records
|
|
1403
|
+
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1404
|
+
.map(item => [item.value.operationId, item]));
|
|
1405
|
+
const secretReplayTombstones = records.filter((item) => (item.kind === 'secret-replay-tombstone'));
|
|
972
1406
|
const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
|
|
973
1407
|
if (nonterminalLifecycles.length > 1
|
|
974
1408
|
|| principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
|
|
975
1409
|
|| new Set(principalBindingRecords.map(item => item.value.principalId)).size
|
|
976
1410
|
!== principalBindingRecords.length
|
|
977
1411
|
|| new Set(claimRecords.map(item => (`${item.value.transferId}:${item.value.claimSha256}`))).size !== claimRecords.length
|
|
978
|
-
|| new Set(redemptionReceiptRecords.map(item => (`${item.value.receipt.transferId}:${item.value.receipt.receiptId}`))).size !== redemptionReceiptRecords.length
|
|
1412
|
+
|| new Set(redemptionReceiptRecords.map(item => (`${item.value.receipt.transferId}:${item.value.receipt.receiptId}`))).size !== redemptionReceiptRecords.length
|
|
1413
|
+
|| invitations.size !== records.filter(item => item.kind === 'project-invitation').length
|
|
1414
|
+
|| invitationEnvelopes.size
|
|
1415
|
+
!== records.filter(item => item.kind === 'protected-invitation-envelope').length
|
|
1416
|
+
|| new Set([...invitations.values()].map(item => (`${item.value.issuedByMemberId}:${item.value.idempotencyKey}`))).size !== invitations.size
|
|
1417
|
+
|| new Set(claimOverrides.map(item => (`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`))).size !== claimOverrides.length
|
|
1418
|
+
|| new Set(claimOverrides.map(item => (`${item.value.managerMemberId}:${item.value.idempotencyKey}`))).size !== claimOverrides.length
|
|
1419
|
+
|| claimOverrideEnvelopes.size
|
|
1420
|
+
!== records.filter(item => item.kind === 'protected-claim-override-envelope').length
|
|
1421
|
+
|| membershipRecoveries.size
|
|
1422
|
+
!== records.filter(item => item.kind === 'project-membership-recovery').length)
|
|
979
1423
|
throw invalidPayload('records');
|
|
980
1424
|
for (const lifecycle of lifecycles.values()) {
|
|
981
1425
|
if (lifecycleJournalHasInvalidSemantics(lifecycle)
|
|
@@ -986,6 +1430,11 @@ function validateContinuity(records) {
|
|
|
986
1430
|
if (lifecycle.value.operationKind === 'authority-transfer'
|
|
987
1431
|
&& !recoveries.has(lifecycle.value.operationId))
|
|
988
1432
|
throw invalidPayload('records');
|
|
1433
|
+
if ((lifecycle.value.operationKind === 'create-project'
|
|
1434
|
+
|| lifecycle.value.operationKind === 'join-project'
|
|
1435
|
+
|| lifecycle.value.operationKind === 'remove-member')
|
|
1436
|
+
&& !membershipRecoveries.has(lifecycle.value.operationId))
|
|
1437
|
+
throw invalidPayload('records');
|
|
989
1438
|
if (lifecycle.value.operationKind === 'leave') {
|
|
990
1439
|
const actorMemberId = lifecycle.value.actorMemberId;
|
|
991
1440
|
const actor = actorMemberId === null ? undefined : memberRecords.get(actorMemberId);
|
|
@@ -1162,6 +1611,13 @@ function validateContinuity(records) {
|
|
|
1162
1611
|
const lifecycle = lifecycles.get(claim.value.transferId);
|
|
1163
1612
|
const recovery = recoveries.get(claim.value.transferId);
|
|
1164
1613
|
const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
|
|
1614
|
+
const redeemedOverride = redemptionReceipt === undefined
|
|
1615
|
+
? undefined
|
|
1616
|
+
: claimOverrides.find(item => (item.value.transferId === claim.value.transferId
|
|
1617
|
+
&& item.value.memberId === claim.value.memberId
|
|
1618
|
+
&& item.value.state === 'redeemed'
|
|
1619
|
+
&& item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
|
|
1620
|
+
&& item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
|
|
1165
1621
|
if (memberRecords.get(claim.value.memberId)?.value.status !== 'active'
|
|
1166
1622
|
|| recovery === undefined
|
|
1167
1623
|
|| recovery.value.sourceAuthority.kind !== 'lan'
|
|
@@ -1171,7 +1627,11 @@ function validateContinuity(records) {
|
|
|
1171
1627
|
|| claim.value.expiresAt !== recovery.value.expiresAt
|
|
1172
1628
|
|| (claim.value.targetPrincipalId !== null
|
|
1173
1629
|
&& principalBindings.get(claim.value.memberId) !== claim.value.targetPrincipalId)
|
|
1174
|
-
|| (claim.value.state === 'redeemed'
|
|
1630
|
+
|| (claim.value.state === 'redeemed' && redemptionReceipt === undefined)
|
|
1631
|
+
|| (redemptionReceipt !== undefined
|
|
1632
|
+
&& claim.value.state !== 'redeemed'
|
|
1633
|
+
&& redeemedOverride === undefined)
|
|
1634
|
+
|| (claim.value.state === 'redeemed' && redeemedOverride !== undefined)) {
|
|
1175
1635
|
throw invalidPayload('records');
|
|
1176
1636
|
}
|
|
1177
1637
|
}
|
|
@@ -1187,6 +1647,141 @@ function validateContinuity(records) {
|
|
|
1187
1647
|
!== responderReplays.has(terminal.value.operationId))
|
|
1188
1648
|
throw invalidPayload('records');
|
|
1189
1649
|
}
|
|
1650
|
+
for (const recovery of membershipRecoveries.values()) {
|
|
1651
|
+
const lifecycle = lifecycles.get(recovery.value.operationId);
|
|
1652
|
+
const invitation = recovery.value.invitationId === null
|
|
1653
|
+
? undefined
|
|
1654
|
+
: invitations.get(recovery.value.invitationId);
|
|
1655
|
+
const member = memberRecords.get(recovery.value.memberId);
|
|
1656
|
+
if (lifecycle === undefined
|
|
1657
|
+
|| lifecycle.value.operationKind !== recovery.value.operationKind
|
|
1658
|
+
|| lifecycle.value.requestFingerprint !== recovery.value.requestFingerprint
|
|
1659
|
+
|| lifecycle.value.state !== 'completed'
|
|
1660
|
+
|| member === undefined
|
|
1661
|
+
|| (recovery.value.operationKind === 'join-project' && invitation === undefined)
|
|
1662
|
+
|| (recovery.value.operationKind !== 'join-project' && invitation !== undefined)
|
|
1663
|
+
|| (recovery.value.operationKind === 'create-project'
|
|
1664
|
+
&& lifecycle.value.actorMemberId !== null)
|
|
1665
|
+
|| (recovery.value.operationKind === 'join-project'
|
|
1666
|
+
&& lifecycle.value.actorMemberId !== null)
|
|
1667
|
+
|| (recovery.value.operationKind === 'remove-member'
|
|
1668
|
+
&& lifecycle.value.actorMemberId === null)
|
|
1669
|
+
|| (recovery.value.operationKind === 'join-project'
|
|
1670
|
+
&& invitation?.value.state !== 'redeemed')
|
|
1671
|
+
|| (recovery.value.operationKind === 'remove-member' && (member.value.status !== 'revoked'
|
|
1672
|
+
|| principalBindings.has(recovery.value.memberId))))
|
|
1673
|
+
throw invalidPayload('records');
|
|
1674
|
+
}
|
|
1675
|
+
for (const invitation of invitations.values()) {
|
|
1676
|
+
const envelope = invitationEnvelopes.get(invitation.value.invitationId);
|
|
1677
|
+
const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'createProjectInvitation'
|
|
1678
|
+
&& item.value.actorMemberId === invitation.value.issuedByMemberId
|
|
1679
|
+
&& item.value.idempotencyKey === invitation.value.idempotencyKey
|
|
1680
|
+
&& item.value.requestFingerprint === invitation.value.requestFingerprint));
|
|
1681
|
+
if (!members.has(invitation.value.issuedByMemberId)
|
|
1682
|
+
|| (envelope === undefined) === (tombstone === undefined)
|
|
1683
|
+
|| (tombstone !== undefined
|
|
1684
|
+
&& tombstone.value.expiredAt !== invitation.value.secretReplayExpiresAt)
|
|
1685
|
+
|| (envelope !== undefined && (envelope.value.projectId !== invitation.value.projectId
|
|
1686
|
+
|| envelope.value.createdAt !== invitation.value.createdAt
|
|
1687
|
+
|| envelope.value.expiresAt !== invitation.value.secretReplayExpiresAt)))
|
|
1688
|
+
throw invalidPayload('records');
|
|
1689
|
+
}
|
|
1690
|
+
for (const envelope of invitationEnvelopes.values()) {
|
|
1691
|
+
if (!invitations.has(envelope.value.invitationId))
|
|
1692
|
+
throw invalidPayload('records');
|
|
1693
|
+
}
|
|
1694
|
+
const overridesByMember = new Map();
|
|
1695
|
+
for (const override of claimOverrides) {
|
|
1696
|
+
const identity = `${override.value.transferId}:${override.value.memberId}`;
|
|
1697
|
+
const values = overridesByMember.get(identity) ?? [];
|
|
1698
|
+
values.push(override);
|
|
1699
|
+
overridesByMember.set(identity, values);
|
|
1700
|
+
const envelope = claimOverrideEnvelopes.get(`${identity}:${override.value.claimGeneration}`);
|
|
1701
|
+
const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'reissueTransferredMembershipClaim'
|
|
1702
|
+
&& item.value.actorMemberId === override.value.managerMemberId
|
|
1703
|
+
&& item.value.idempotencyKey === override.value.idempotencyKey
|
|
1704
|
+
&& item.value.requestFingerprint === override.value.requestFingerprint));
|
|
1705
|
+
if (!members.has(override.value.memberId)
|
|
1706
|
+
|| !members.has(override.value.managerMemberId)
|
|
1707
|
+
|| (envelope === undefined) === (tombstone === undefined)
|
|
1708
|
+
|| (tombstone !== undefined
|
|
1709
|
+
&& tombstone.value.expiredAt !== override.value.secretReplayExpiresAt)
|
|
1710
|
+
|| (envelope !== undefined && (envelope.value.projectId !== override.value.projectId
|
|
1711
|
+
|| envelope.value.createdAt !== override.value.createdAt
|
|
1712
|
+
|| envelope.value.expiresAt !== override.value.secretReplayExpiresAt)))
|
|
1713
|
+
throw invalidPayload('records');
|
|
1714
|
+
}
|
|
1715
|
+
for (const overrides of overridesByMember.values()) {
|
|
1716
|
+
overrides.sort((left, right) => left.value.claimGeneration - right.value.claimGeneration);
|
|
1717
|
+
const highest = overrides.at(-1);
|
|
1718
|
+
const first = overrides[0];
|
|
1719
|
+
const identity = first === undefined
|
|
1720
|
+
? ''
|
|
1721
|
+
: `${first.value.transferId}:${first.value.memberId}`;
|
|
1722
|
+
const sourceClaim = claims.get(identity);
|
|
1723
|
+
const recovery = first === undefined ? undefined : recoveries.get(first.value.transferId);
|
|
1724
|
+
const member = first === undefined ? undefined : memberRecords.get(first.value.memberId);
|
|
1725
|
+
const principalBinding = first === undefined
|
|
1726
|
+
? undefined
|
|
1727
|
+
: principalBindings.get(first.value.memberId);
|
|
1728
|
+
const redemptionReceipt = redemptionReceipts.get(identity);
|
|
1729
|
+
const redeemedOverrides = overrides.filter(item => item.value.state === 'redeemed');
|
|
1730
|
+
if (first === undefined
|
|
1731
|
+
|| sourceClaim === undefined
|
|
1732
|
+
|| recovery?.value.sourceAuthority.kind !== 'lan'
|
|
1733
|
+
|| member?.value.status !== 'active'
|
|
1734
|
+
|| sourceClaim.value.state === 'redeemed'
|
|
1735
|
+
|| overrides.some((item, index) => item.value.claimGeneration !== index + 1)
|
|
1736
|
+
|| overrides.some((item, index) => item.value.supersededClaimSha256 !== (index === 0
|
|
1737
|
+
? sourceClaim.value.claimSha256
|
|
1738
|
+
: overrides[index - 1].value.claimSha256))
|
|
1739
|
+
|| new Set(overrides.map(item => item.value.claimSha256)).size !== overrides.length
|
|
1740
|
+
|| overrides.filter(item => item.value.state === 'active').length > 1
|
|
1741
|
+
|| overrides.some(item => item.value.state === 'active' && item !== highest)
|
|
1742
|
+
|| redeemedOverrides.length > 1
|
|
1743
|
+
|| redeemedOverrides.some(item => item !== highest)
|
|
1744
|
+
|| highest?.value.state === 'superseded'
|
|
1745
|
+
|| (highest?.value.state === 'active' && (principalBinding !== undefined
|
|
1746
|
+
|| redemptionReceipt !== undefined))
|
|
1747
|
+
|| (highest?.value.state === 'redeemed' && (principalBinding !== highest.value.targetPrincipalId
|
|
1748
|
+
|| redemptionReceipt === undefined
|
|
1749
|
+
|| redemptionReceipt.value.receipt.transferId !== highest.value.transferId
|
|
1750
|
+
|| redemptionReceipt.value.receipt.memberId !== highest.value.memberId
|
|
1751
|
+
|| redemptionReceipt.value.receipt.claimSha256 !== highest.value.claimSha256
|
|
1752
|
+
|| redemptionReceipt.value.receipt.receiptId !== highest.value.redemptionReceiptId
|
|
1753
|
+
|| redemptionReceipt.value.receipt.checkpointSha256
|
|
1754
|
+
!== sourceClaim.value.checkpointSha256))
|
|
1755
|
+
|| (highest?.value.state !== 'redeemed' && redemptionReceipt !== undefined))
|
|
1756
|
+
throw invalidPayload('records');
|
|
1757
|
+
}
|
|
1758
|
+
for (const envelope of claimOverrideEnvelopes.values()) {
|
|
1759
|
+
if (!claimOverrides.some(item => (item.value.transferId === envelope.value.transferId
|
|
1760
|
+
&& item.value.memberId === envelope.value.memberId
|
|
1761
|
+
&& item.value.claimGeneration === envelope.value.claimGeneration)))
|
|
1762
|
+
throw invalidPayload('records');
|
|
1763
|
+
}
|
|
1764
|
+
const currentOffers = managerOffers.filter(item => (item.value.state === 'offered' || item.value.state === 'acknowledged'));
|
|
1765
|
+
if (managerOffers.some(item => (item.value.sourceManagerMemberId === item.value.targetMemberId
|
|
1766
|
+
|| !members.has(item.value.sourceManagerMemberId)
|
|
1767
|
+
|| !members.has(item.value.targetMemberId)))
|
|
1768
|
+
|| new Set(currentOffers.map(item => item.value.sourceManagerMemberId)).size
|
|
1769
|
+
!== currentOffers.length
|
|
1770
|
+
|| new Set(currentOffers.map(item => item.value.targetMemberId)).size
|
|
1771
|
+
!== currentOffers.length)
|
|
1772
|
+
throw invalidPayload('records');
|
|
1773
|
+
for (const tombstone of secretReplayTombstones) {
|
|
1774
|
+
const liveSecretRecord = tombstone.value.operation === 'createProjectInvitation'
|
|
1775
|
+
? [...invitations.values()].some(item => (item.value.issuedByMemberId === tombstone.value.actorMemberId
|
|
1776
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1777
|
+
&& invitationEnvelopes.has(item.value.invitationId)))
|
|
1778
|
+
: claimOverrides.some(item => (item.value.managerMemberId === tombstone.value.actorMemberId
|
|
1779
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1780
|
+
&& claimOverrideEnvelopes.has(`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`)));
|
|
1781
|
+
if (!members.has(tombstone.value.actorMemberId) || liveSecretRecord) {
|
|
1782
|
+
throw invalidPayload('records');
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1190
1785
|
for (const item of records) {
|
|
1191
1786
|
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1192
1787
|
throw invalidPayload('records');
|
|
@@ -1238,6 +1833,19 @@ function validateContinuity(records) {
|
|
|
1238
1833
|
const lifecycle = lifecycles.get(receipt.transferId);
|
|
1239
1834
|
const terminalPrincipal = terminalPrincipals.get(identity);
|
|
1240
1835
|
const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
|
|
1836
|
+
const redeemedOverride = claimOverrides.find(item => (item.value.transferId === receipt.transferId
|
|
1837
|
+
&& item.value.memberId === receipt.memberId
|
|
1838
|
+
&& item.value.state === 'redeemed'
|
|
1839
|
+
&& item.value.claimSha256 === receipt.claimSha256
|
|
1840
|
+
&& item.value.redemptionReceiptId === receipt.receiptId));
|
|
1841
|
+
const matchesSourceClaim = claim?.value.state === 'redeemed'
|
|
1842
|
+
&& claim.value.claimSha256 === receipt.claimSha256
|
|
1843
|
+
&& claim.value.checkpointSha256 === receipt.checkpointSha256
|
|
1844
|
+
&& claim.value.operationIntentId === receipt.operationIntentId
|
|
1845
|
+
&& claim.value.redemptionReceiptId === receipt.receiptId;
|
|
1846
|
+
const matchesOverride = claim !== undefined
|
|
1847
|
+
&& redeemedOverride !== undefined
|
|
1848
|
+
&& claim.value.checkpointSha256 === receipt.checkpointSha256;
|
|
1241
1849
|
if (recovery === undefined
|
|
1242
1850
|
|| lifecycle === undefined
|
|
1243
1851
|
|| memberRecords.get(receipt.memberId)?.value.status !== 'active'
|
|
@@ -1245,11 +1853,7 @@ function validateContinuity(records) {
|
|
|
1245
1853
|
|| !keys.has(`${receipt.transferId}:${receipt.receiptKeyId}`)
|
|
1246
1854
|
|| Date.parse(receipt.redeemedAt) > Date.parse(recovery.value.expiresAt)
|
|
1247
1855
|
|| (lanToCloud && (item.value.acknowledgedAt !== null
|
|
1248
|
-
||
|
|
1249
|
-
|| claim.value.claimSha256 !== receipt.claimSha256
|
|
1250
|
-
|| claim.value.checkpointSha256 !== receipt.checkpointSha256
|
|
1251
|
-
|| claim.value.operationIntentId !== receipt.operationIntentId
|
|
1252
|
-
|| claim.value.redemptionReceiptId !== receipt.receiptId))
|
|
1856
|
+
|| (!matchesSourceClaim && !matchesOverride)))
|
|
1253
1857
|
|| (!lanToCloud && (item.value.acknowledgedAt === null
|
|
1254
1858
|
|| receipt.memberId === recovery.value.targetHostMemberId
|
|
1255
1859
|
|| receipt.checkpointSha256 !== lifecycle.value.checkpointSha256
|
|
@@ -1444,6 +2048,30 @@ export function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
|
|
|
1444
2048
|
export function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
1445
2049
|
const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
|
|
1446
2050
|
const decodedRecords = decodeCollabProjectBackupCheckpointCoordinationNdjson(records.map(item => JSON.stringify(item)).join('\n') + '\n');
|
|
2051
|
+
const invitationEnvelopeIds = new Set(decodedRecords
|
|
2052
|
+
.filter(item => item.kind === 'protected-invitation-envelope')
|
|
2053
|
+
.map(item => item.recordId));
|
|
2054
|
+
const claimOverrideEnvelopeIds = new Set(decodedRecords
|
|
2055
|
+
.filter(item => item.kind === 'protected-claim-override-envelope')
|
|
2056
|
+
.map(item => item.recordId));
|
|
2057
|
+
const captureTime = Date.parse(decodedManifest.createdAt);
|
|
2058
|
+
if (decodedRecords.some(item => {
|
|
2059
|
+
if (item.kind === 'secret-replay-tombstone') {
|
|
2060
|
+
return Date.parse(item.value.expiredAt) > captureTime;
|
|
2061
|
+
}
|
|
2062
|
+
if (item.kind === 'project-invitation') {
|
|
2063
|
+
return captureTime < Date.parse(item.value.createdAt)
|
|
2064
|
+
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
2065
|
+
!== invitationEnvelopeIds.has(item.recordId);
|
|
2066
|
+
}
|
|
2067
|
+
if (item.kind === 'transferred-membership-claim-override') {
|
|
2068
|
+
return captureTime < Date.parse(item.value.createdAt)
|
|
2069
|
+
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
2070
|
+
!== claimOverrideEnvelopeIds.has(item.recordId);
|
|
2071
|
+
}
|
|
2072
|
+
return false;
|
|
2073
|
+
}))
|
|
2074
|
+
throw invalidPayload('records');
|
|
1447
2075
|
if (decodedRecords.some(item => {
|
|
1448
2076
|
switch (item.kind) {
|
|
1449
2077
|
case 'lifecycle-journal': return item.value.operationId === decodedManifest.operationId;
|