@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
|
@@ -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,29 @@ 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
|
+
'membership-idempotency-tombstone',
|
|
26
|
+
'project-membership-recovery',
|
|
27
|
+
'secret-replay-tombstone',
|
|
18
28
|
]);
|
|
19
29
|
export const COLLAB_PROJECT_BACKUP_RECORD_KINDS = Object.freeze([
|
|
20
|
-
...
|
|
30
|
+
...COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS,
|
|
31
|
+
'cloud-event',
|
|
32
|
+
'cloud-event-cursor',
|
|
33
|
+
'idempotency-result',
|
|
34
|
+
'principal-binding',
|
|
35
|
+
'repository-placement',
|
|
21
36
|
...BACKUP_CONTINUITY_RECORD_KINDS,
|
|
22
|
-
|
|
37
|
+
'terminal-responder',
|
|
38
|
+
'protected-claim-envelope',
|
|
39
|
+
'tombstone',
|
|
40
|
+
'schema-catalog',
|
|
41
|
+
'server-compatibility',
|
|
42
|
+
'authority-volume-pair',
|
|
23
43
|
]);
|
|
24
44
|
const CONTINUITY_KIND_SET = new Set(BACKUP_CONTINUITY_RECORD_KINDS);
|
|
25
45
|
const BACKUP_KIND_SET = new Set(COLLAB_PROJECT_BACKUP_RECORD_KINDS);
|
|
@@ -49,7 +69,9 @@ const DELETE_ACTIVE_PHASE_SET = new Set([
|
|
|
49
69
|
'tombstoned',
|
|
50
70
|
]);
|
|
51
71
|
const PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET = new Set([
|
|
72
|
+
'createProjectInvitation',
|
|
52
73
|
'getTransferredMembershipClaim',
|
|
74
|
+
'reissueTransferredMembershipClaim',
|
|
53
75
|
'rotateTransferredMembershipClaims',
|
|
54
76
|
]);
|
|
55
77
|
function invalidPayload(field) {
|
|
@@ -107,6 +129,9 @@ function timestampValue(value, field) {
|
|
|
107
129
|
function timestamp(source, field) {
|
|
108
130
|
return timestampValue(source[field], field);
|
|
109
131
|
}
|
|
132
|
+
function hasExactDuration(start, end, durationMs) {
|
|
133
|
+
return Date.parse(end) === Date.parse(start) + durationMs;
|
|
134
|
+
}
|
|
110
135
|
function nullableTimestamp(source, field) {
|
|
111
136
|
return source[field] === null ? null : timestampValue(source[field], field);
|
|
112
137
|
}
|
|
@@ -457,7 +482,15 @@ function lifecycleJournalRecord(source, recordId, revision) {
|
|
|
457
482
|
if (recordId !== operationId)
|
|
458
483
|
throw invalidPayload('recordId');
|
|
459
484
|
const operationKind = literal(value, 'operationKind', [
|
|
460
|
-
'authority-transfer',
|
|
485
|
+
'authority-transfer',
|
|
486
|
+
'backup',
|
|
487
|
+
'create-project',
|
|
488
|
+
'delete',
|
|
489
|
+
'export',
|
|
490
|
+
'join-project',
|
|
491
|
+
'leave',
|
|
492
|
+
'remove-member',
|
|
493
|
+
'retire',
|
|
461
494
|
]);
|
|
462
495
|
const direction = value.direction === null
|
|
463
496
|
? null
|
|
@@ -828,6 +861,385 @@ function leaveFormerPrincipalReplayRecord(source, recordId, revision) {
|
|
|
828
861
|
},
|
|
829
862
|
};
|
|
830
863
|
}
|
|
864
|
+
function projectInvitationRecord(source, recordId, revision) {
|
|
865
|
+
const value = exactRecord(source.value, 'value', [
|
|
866
|
+
'createdAt',
|
|
867
|
+
'expiresAt',
|
|
868
|
+
'idempotencyKey',
|
|
869
|
+
'invitationId',
|
|
870
|
+
'issuedByMemberId',
|
|
871
|
+
'projectId',
|
|
872
|
+
'requestFingerprint',
|
|
873
|
+
'revision',
|
|
874
|
+
'secretReplayExpiresAt',
|
|
875
|
+
'secretSha256',
|
|
876
|
+
'state',
|
|
877
|
+
'terminalAt',
|
|
878
|
+
]);
|
|
879
|
+
const invitationId = token(value, 'invitationId');
|
|
880
|
+
if (recordId !== invitationId)
|
|
881
|
+
throw invalidPayload('recordId');
|
|
882
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
883
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
884
|
+
const secretReplayExpiresAt = timestamp(value, 'secretReplayExpiresAt');
|
|
885
|
+
const state = literal(value, 'state', [
|
|
886
|
+
'active', 'redeeming', 'redeemed', 'revoked', 'expired',
|
|
887
|
+
]);
|
|
888
|
+
const terminalAt = nullableTimestamp(value, 'terminalAt');
|
|
889
|
+
const terminal = state === 'redeemed' || state === 'revoked' || state === 'expired';
|
|
890
|
+
if (!hasExactDuration(createdAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.invitationTtlMs)
|
|
891
|
+
|| !hasExactDuration(createdAt, secretReplayExpiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs)
|
|
892
|
+
|| terminal !== (terminalAt !== null)
|
|
893
|
+
|| (terminalAt !== null && Date.parse(terminalAt) < Date.parse(createdAt)))
|
|
894
|
+
throw invalidPayload('state');
|
|
895
|
+
return {
|
|
896
|
+
kind: 'project-invitation',
|
|
897
|
+
recordId,
|
|
898
|
+
revision,
|
|
899
|
+
value: {
|
|
900
|
+
createdAt,
|
|
901
|
+
expiresAt,
|
|
902
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
903
|
+
invitationId,
|
|
904
|
+
issuedByMemberId: token(value, 'issuedByMemberId', isCollabMemberId),
|
|
905
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
906
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
907
|
+
revision: positiveInteger(value, 'revision'),
|
|
908
|
+
secretReplayExpiresAt,
|
|
909
|
+
secretSha256: sha256(value, 'secretSha256'),
|
|
910
|
+
state,
|
|
911
|
+
terminalAt,
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function protectedSecretEnvelopeFields(value) {
|
|
916
|
+
const ciphertext = boundedString(value, 'ciphertext', 4096);
|
|
917
|
+
const nonce = boundedString(value, 'nonce', 128);
|
|
918
|
+
if (!BASE64URL_PATTERN.test(ciphertext) || !BASE64URL_PATTERN.test(nonce)) {
|
|
919
|
+
throw invalidPayload('ciphertext');
|
|
920
|
+
}
|
|
921
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
922
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
923
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt))
|
|
924
|
+
throw invalidPayload('expiresAt');
|
|
925
|
+
return {
|
|
926
|
+
associatedDataSha256: sha256(value, 'associatedDataSha256'),
|
|
927
|
+
ciphertext,
|
|
928
|
+
createdAt,
|
|
929
|
+
expiresAt,
|
|
930
|
+
keyId: token(value, 'keyId'),
|
|
931
|
+
nonce,
|
|
932
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function protectedInvitationEnvelopeRecord(source, recordId, revision) {
|
|
936
|
+
const value = exactRecord(source.value, 'value', [
|
|
937
|
+
'associatedDataSha256',
|
|
938
|
+
'ciphertext',
|
|
939
|
+
'createdAt',
|
|
940
|
+
'expiresAt',
|
|
941
|
+
'invitationId',
|
|
942
|
+
'keyId',
|
|
943
|
+
'nonce',
|
|
944
|
+
'projectId',
|
|
945
|
+
]);
|
|
946
|
+
const invitationId = token(value, 'invitationId');
|
|
947
|
+
if (recordId !== invitationId)
|
|
948
|
+
throw invalidPayload('recordId');
|
|
949
|
+
const envelope = protectedSecretEnvelopeFields(value);
|
|
950
|
+
return {
|
|
951
|
+
kind: 'protected-invitation-envelope',
|
|
952
|
+
recordId,
|
|
953
|
+
revision,
|
|
954
|
+
value: {
|
|
955
|
+
associatedDataSha256: envelope.associatedDataSha256,
|
|
956
|
+
ciphertext: envelope.ciphertext,
|
|
957
|
+
createdAt: envelope.createdAt,
|
|
958
|
+
expiresAt: envelope.expiresAt,
|
|
959
|
+
invitationId,
|
|
960
|
+
keyId: envelope.keyId,
|
|
961
|
+
nonce: envelope.nonce,
|
|
962
|
+
projectId: envelope.projectId,
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function transferredMembershipClaimOverrideRecord(source, recordId, revision) {
|
|
967
|
+
const value = exactRecord(source.value, 'value', [
|
|
968
|
+
'claimGeneration',
|
|
969
|
+
'claimSha256',
|
|
970
|
+
'createdAt',
|
|
971
|
+
'expiresAt',
|
|
972
|
+
'idempotencyKey',
|
|
973
|
+
'managerMemberId',
|
|
974
|
+
'memberId',
|
|
975
|
+
'projectId',
|
|
976
|
+
'redemptionReceiptId',
|
|
977
|
+
'requestFingerprint',
|
|
978
|
+
'secretReplayExpiresAt',
|
|
979
|
+
'state',
|
|
980
|
+
'supersededClaimSha256',
|
|
981
|
+
'targetPrincipalId',
|
|
982
|
+
'transferId',
|
|
983
|
+
'updatedAt',
|
|
984
|
+
]);
|
|
985
|
+
const transferId = token(value, 'transferId');
|
|
986
|
+
const memberId = token(value, 'memberId', isCollabMemberId);
|
|
987
|
+
const claimGeneration = positiveInteger(value, 'claimGeneration');
|
|
988
|
+
if (recordId !== `${transferId}:${memberId}:${claimGeneration}`) {
|
|
989
|
+
throw invalidPayload('recordId');
|
|
990
|
+
}
|
|
991
|
+
const state = literal(value, 'state', [
|
|
992
|
+
'active', 'redeemed', 'revoked', 'superseded', 'expired',
|
|
993
|
+
]);
|
|
994
|
+
const targetPrincipalId = nullableToken(value, 'targetPrincipalId', candidate => (typeof candidate === 'string' && PRINCIPAL_PATTERN.test(candidate)));
|
|
995
|
+
const redemptionReceiptId = nullableToken(value, 'redemptionReceiptId');
|
|
996
|
+
if ((state === 'redeemed') !== (targetPrincipalId !== null && redemptionReceiptId !== null)
|
|
997
|
+
|| (state !== 'redeemed' && (targetPrincipalId !== null || redemptionReceiptId !== null)))
|
|
998
|
+
throw invalidPayload('state');
|
|
999
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
1000
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
1001
|
+
const secretReplayExpiresAt = timestamp(value, 'secretReplayExpiresAt');
|
|
1002
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
1003
|
+
if (!hasExactDuration(createdAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.transferredClaimTtlMs)
|
|
1004
|
+
|| !hasExactDuration(createdAt, secretReplayExpiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs)
|
|
1005
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt))
|
|
1006
|
+
throw invalidPayload('updatedAt');
|
|
1007
|
+
return {
|
|
1008
|
+
kind: 'transferred-membership-claim-override',
|
|
1009
|
+
recordId,
|
|
1010
|
+
revision,
|
|
1011
|
+
value: {
|
|
1012
|
+
claimGeneration,
|
|
1013
|
+
claimSha256: sha256(value, 'claimSha256'),
|
|
1014
|
+
createdAt,
|
|
1015
|
+
expiresAt,
|
|
1016
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
1017
|
+
managerMemberId: token(value, 'managerMemberId', isCollabMemberId),
|
|
1018
|
+
memberId,
|
|
1019
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1020
|
+
redemptionReceiptId,
|
|
1021
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1022
|
+
secretReplayExpiresAt,
|
|
1023
|
+
state,
|
|
1024
|
+
supersededClaimSha256: sha256(value, 'supersededClaimSha256'),
|
|
1025
|
+
targetPrincipalId,
|
|
1026
|
+
transferId,
|
|
1027
|
+
updatedAt,
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function protectedClaimOverrideEnvelopeRecord(source, recordId, revision) {
|
|
1032
|
+
const value = exactRecord(source.value, 'value', [
|
|
1033
|
+
'associatedDataSha256',
|
|
1034
|
+
'ciphertext',
|
|
1035
|
+
'claimGeneration',
|
|
1036
|
+
'createdAt',
|
|
1037
|
+
'expiresAt',
|
|
1038
|
+
'keyId',
|
|
1039
|
+
'memberId',
|
|
1040
|
+
'nonce',
|
|
1041
|
+
'projectId',
|
|
1042
|
+
'transferId',
|
|
1043
|
+
]);
|
|
1044
|
+
const transferId = token(value, 'transferId');
|
|
1045
|
+
const memberId = token(value, 'memberId', isCollabMemberId);
|
|
1046
|
+
const claimGeneration = positiveInteger(value, 'claimGeneration');
|
|
1047
|
+
if (recordId !== `${transferId}:${memberId}:${claimGeneration}`) {
|
|
1048
|
+
throw invalidPayload('recordId');
|
|
1049
|
+
}
|
|
1050
|
+
const envelope = protectedSecretEnvelopeFields(value);
|
|
1051
|
+
return {
|
|
1052
|
+
kind: 'protected-claim-override-envelope',
|
|
1053
|
+
recordId,
|
|
1054
|
+
revision,
|
|
1055
|
+
value: {
|
|
1056
|
+
associatedDataSha256: envelope.associatedDataSha256,
|
|
1057
|
+
ciphertext: envelope.ciphertext,
|
|
1058
|
+
claimGeneration,
|
|
1059
|
+
createdAt: envelope.createdAt,
|
|
1060
|
+
expiresAt: envelope.expiresAt,
|
|
1061
|
+
keyId: envelope.keyId,
|
|
1062
|
+
memberId,
|
|
1063
|
+
nonce: envelope.nonce,
|
|
1064
|
+
projectId: envelope.projectId,
|
|
1065
|
+
transferId,
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
function managerResponsibilityOfferRecord(source, recordId, revision) {
|
|
1070
|
+
const value = exactRecord(source.value, 'value', [
|
|
1071
|
+
'acknowledgedAt',
|
|
1072
|
+
'expiresAt',
|
|
1073
|
+
'idempotencyKey',
|
|
1074
|
+
'managerSetGenerationAtOffer',
|
|
1075
|
+
'offeredAt',
|
|
1076
|
+
'offerId',
|
|
1077
|
+
'projectId',
|
|
1078
|
+
'purpose',
|
|
1079
|
+
'requestFingerprint',
|
|
1080
|
+
'revision',
|
|
1081
|
+
'sourceManagerMemberId',
|
|
1082
|
+
'state',
|
|
1083
|
+
'targetMemberId',
|
|
1084
|
+
'targetMembershipRevisionAtOffer',
|
|
1085
|
+
'terminalAt',
|
|
1086
|
+
]);
|
|
1087
|
+
const offerId = token(value, 'offerId');
|
|
1088
|
+
if (recordId !== offerId)
|
|
1089
|
+
throw invalidPayload('recordId');
|
|
1090
|
+
const state = literal(value, 'state', [
|
|
1091
|
+
'offered', 'acknowledged', 'declined', 'cancelled', 'consumed', 'expired',
|
|
1092
|
+
]);
|
|
1093
|
+
const offeredAt = timestamp(value, 'offeredAt');
|
|
1094
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
1095
|
+
const acknowledgedAt = nullableTimestamp(value, 'acknowledgedAt');
|
|
1096
|
+
const terminalAt = nullableTimestamp(value, 'terminalAt');
|
|
1097
|
+
const terminal = state === 'declined'
|
|
1098
|
+
|| state === 'cancelled'
|
|
1099
|
+
|| state === 'consumed'
|
|
1100
|
+
|| state === 'expired';
|
|
1101
|
+
if (!hasExactDuration(offeredAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.managerResponsibilityOfferTtlMs)
|
|
1102
|
+
|| (state === 'offered' && acknowledgedAt !== null)
|
|
1103
|
+
|| (state === 'acknowledged' && acknowledgedAt === null)
|
|
1104
|
+
|| terminal !== (terminalAt !== null))
|
|
1105
|
+
throw invalidPayload('state');
|
|
1106
|
+
return {
|
|
1107
|
+
kind: 'manager-responsibility-offer',
|
|
1108
|
+
recordId,
|
|
1109
|
+
revision,
|
|
1110
|
+
value: {
|
|
1111
|
+
acknowledgedAt,
|
|
1112
|
+
expiresAt,
|
|
1113
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
1114
|
+
managerSetGenerationAtOffer: positiveInteger(value, 'managerSetGenerationAtOffer'),
|
|
1115
|
+
offeredAt,
|
|
1116
|
+
offerId,
|
|
1117
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1118
|
+
purpose: literal(value, 'purpose', ['manager-promotion', 'manager-leave']),
|
|
1119
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1120
|
+
revision: positiveInteger(value, 'revision'),
|
|
1121
|
+
sourceManagerMemberId: token(value, 'sourceManagerMemberId', isCollabMemberId),
|
|
1122
|
+
state,
|
|
1123
|
+
targetMemberId: token(value, 'targetMemberId', isCollabMemberId),
|
|
1124
|
+
targetMembershipRevisionAtOffer: positiveInteger(value, 'targetMembershipRevisionAtOffer'),
|
|
1125
|
+
terminalAt,
|
|
1126
|
+
},
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function membershipRecoveryRecord(source, recordId, revision) {
|
|
1130
|
+
const value = exactRecord(source.value, 'value', [
|
|
1131
|
+
'expectedMainOid',
|
|
1132
|
+
'expectedPersonalRefOid',
|
|
1133
|
+
'invitationId',
|
|
1134
|
+
'memberId',
|
|
1135
|
+
'operationId',
|
|
1136
|
+
'operationKind',
|
|
1137
|
+
'principalSha256',
|
|
1138
|
+
'projectId',
|
|
1139
|
+
'publicationMarkerSha256',
|
|
1140
|
+
'repositoryPlanSha256',
|
|
1141
|
+
'requestFingerprint',
|
|
1142
|
+
]);
|
|
1143
|
+
const operationId = token(value, 'operationId');
|
|
1144
|
+
if (recordId !== operationId)
|
|
1145
|
+
throw invalidPayload('recordId');
|
|
1146
|
+
const operationKind = literal(value, 'operationKind', [
|
|
1147
|
+
'create-project', 'join-project', 'remove-member',
|
|
1148
|
+
]);
|
|
1149
|
+
const invitationId = nullableToken(value, 'invitationId');
|
|
1150
|
+
const principalSha256 = nullableSha256(value, 'principalSha256');
|
|
1151
|
+
const publicationMarkerSha256 = nullableSha256(value, 'publicationMarkerSha256');
|
|
1152
|
+
const repositoryPlanSha256 = nullableSha256(value, 'repositoryPlanSha256');
|
|
1153
|
+
if ((operationKind === 'join-project') !== (invitationId !== null)
|
|
1154
|
+
|| (operationKind === 'remove-member') === (principalSha256 !== null)
|
|
1155
|
+
|| (operationKind === 'create-project') !== (repositoryPlanSha256 !== null)
|
|
1156
|
+
|| (operationKind !== 'create-project' && publicationMarkerSha256 !== null))
|
|
1157
|
+
throw invalidPayload('operationKind');
|
|
1158
|
+
return {
|
|
1159
|
+
kind: 'project-membership-recovery',
|
|
1160
|
+
recordId,
|
|
1161
|
+
revision,
|
|
1162
|
+
value: {
|
|
1163
|
+
expectedMainOid: token(value, 'expectedMainOid', isCollabGitOid),
|
|
1164
|
+
expectedPersonalRefOid: token(value, 'expectedPersonalRefOid', isCollabGitOid),
|
|
1165
|
+
invitationId,
|
|
1166
|
+
memberId: token(value, 'memberId', isCollabMemberId),
|
|
1167
|
+
operationId,
|
|
1168
|
+
operationKind,
|
|
1169
|
+
principalSha256,
|
|
1170
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1171
|
+
publicationMarkerSha256,
|
|
1172
|
+
repositoryPlanSha256,
|
|
1173
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1174
|
+
},
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
function membershipIdempotencyTombstoneRecord(source, recordId, revision) {
|
|
1178
|
+
const value = exactRecord(source.value, 'value', [
|
|
1179
|
+
'actorMemberId',
|
|
1180
|
+
'compactedAt',
|
|
1181
|
+
'idempotencyKey',
|
|
1182
|
+
'operation',
|
|
1183
|
+
'projectId',
|
|
1184
|
+
'requestFingerprint',
|
|
1185
|
+
]);
|
|
1186
|
+
const actorMemberId = token(value, 'actorMemberId', isCollabMemberId);
|
|
1187
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1188
|
+
const operation = literal(value, 'operation', [
|
|
1189
|
+
'createManagerResponsibilityOffer',
|
|
1190
|
+
'acknowledgeManagerResponsibility',
|
|
1191
|
+
'declineManagerResponsibility',
|
|
1192
|
+
'cancelManagerResponsibilityOffer',
|
|
1193
|
+
'promoteManager',
|
|
1194
|
+
]);
|
|
1195
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1196
|
+
throw invalidPayload('recordId');
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
kind: 'membership-idempotency-tombstone',
|
|
1200
|
+
recordId,
|
|
1201
|
+
revision,
|
|
1202
|
+
value: {
|
|
1203
|
+
actorMemberId,
|
|
1204
|
+
compactedAt: timestamp(value, 'compactedAt'),
|
|
1205
|
+
idempotencyKey,
|
|
1206
|
+
operation,
|
|
1207
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1208
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1209
|
+
},
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function secretReplayTombstoneRecord(source, recordId, revision) {
|
|
1213
|
+
const value = exactRecord(source.value, 'value', [
|
|
1214
|
+
'actorMemberId',
|
|
1215
|
+
'expiredAt',
|
|
1216
|
+
'idempotencyKey',
|
|
1217
|
+
'operation',
|
|
1218
|
+
'projectId',
|
|
1219
|
+
'requestFingerprint',
|
|
1220
|
+
]);
|
|
1221
|
+
const actorMemberId = token(value, 'actorMemberId', isCollabMemberId);
|
|
1222
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1223
|
+
const operation = literal(value, 'operation', [
|
|
1224
|
+
'createProjectInvitation', 'reissueTransferredMembershipClaim',
|
|
1225
|
+
]);
|
|
1226
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1227
|
+
throw invalidPayload('recordId');
|
|
1228
|
+
}
|
|
1229
|
+
return {
|
|
1230
|
+
kind: 'secret-replay-tombstone',
|
|
1231
|
+
recordId,
|
|
1232
|
+
revision,
|
|
1233
|
+
value: {
|
|
1234
|
+
actorMemberId,
|
|
1235
|
+
expiredAt: timestamp(value, 'expiredAt'),
|
|
1236
|
+
idempotencyKey,
|
|
1237
|
+
operation,
|
|
1238
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1239
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1240
|
+
},
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
831
1243
|
function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
832
1244
|
switch (kind) {
|
|
833
1245
|
case 'lifecycle-journal': return lifecycleJournalRecord(source, recordId, revision);
|
|
@@ -845,6 +1257,21 @@ function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
|
845
1257
|
return terminalResponderReplayRecord(source, recordId, revision);
|
|
846
1258
|
case 'leave-former-principal-replay':
|
|
847
1259
|
return leaveFormerPrincipalReplayRecord(source, recordId, revision);
|
|
1260
|
+
case 'project-invitation': return projectInvitationRecord(source, recordId, revision);
|
|
1261
|
+
case 'protected-invitation-envelope':
|
|
1262
|
+
return protectedInvitationEnvelopeRecord(source, recordId, revision);
|
|
1263
|
+
case 'transferred-membership-claim-override':
|
|
1264
|
+
return transferredMembershipClaimOverrideRecord(source, recordId, revision);
|
|
1265
|
+
case 'protected-claim-override-envelope':
|
|
1266
|
+
return protectedClaimOverrideEnvelopeRecord(source, recordId, revision);
|
|
1267
|
+
case 'manager-responsibility-offer':
|
|
1268
|
+
return managerResponsibilityOfferRecord(source, recordId, revision);
|
|
1269
|
+
case 'membership-idempotency-tombstone':
|
|
1270
|
+
return membershipIdempotencyTombstoneRecord(source, recordId, revision);
|
|
1271
|
+
case 'project-membership-recovery':
|
|
1272
|
+
return membershipRecoveryRecord(source, recordId, revision);
|
|
1273
|
+
case 'secret-replay-tombstone':
|
|
1274
|
+
return secretReplayTombstoneRecord(source, recordId, revision);
|
|
848
1275
|
}
|
|
849
1276
|
}
|
|
850
1277
|
function kindOrder(kind) {
|
|
@@ -901,6 +1328,33 @@ function lifecycleJournalHasInvalidSemantics(item) {
|
|
|
901
1328
|
}
|
|
902
1329
|
if (hasCheckpoint)
|
|
903
1330
|
return true;
|
|
1331
|
+
if (value.operationKind === 'create-project'
|
|
1332
|
+
|| value.operationKind === 'join-project'
|
|
1333
|
+
|| value.operationKind === 'remove-member') {
|
|
1334
|
+
const phases = value.operationKind === 'create-project'
|
|
1335
|
+
? new Set([
|
|
1336
|
+
'prepared',
|
|
1337
|
+
'repository-publication-intent',
|
|
1338
|
+
'repository-published',
|
|
1339
|
+
'activated',
|
|
1340
|
+
])
|
|
1341
|
+
: value.operationKind === 'join-project'
|
|
1342
|
+
? new Set(['prepared', 'membership-pending', 'personal-ref-created', 'membership-active'])
|
|
1343
|
+
: new Set(['prepared', 'membership-revoked', 'personal-ref-removed']);
|
|
1344
|
+
const statePhaseValid = (value.state === 'active' && phases.has(value.phase))
|
|
1345
|
+
|| (value.state === 'recovery-required'
|
|
1346
|
+
&& value.phase === 'recovery-required'
|
|
1347
|
+
&& value.recoveryFromPhase !== null
|
|
1348
|
+
&& phases.has(value.recoveryFromPhase))
|
|
1349
|
+
|| (value.state === 'completed' && value.phase === 'completed');
|
|
1350
|
+
const resultRequired = value.state === 'completed'
|
|
1351
|
+
|| (value.operationKind === 'create-project' && effectivePhase === 'activated')
|
|
1352
|
+
|| (value.operationKind === 'join-project' && effectivePhase === 'membership-active')
|
|
1353
|
+
|| (value.operationKind === 'remove-member'
|
|
1354
|
+
&& (effectivePhase === 'membership-revoked'
|
|
1355
|
+
|| effectivePhase === 'personal-ref-removed'));
|
|
1356
|
+
return !statePhaseValid || hasResult !== resultRequired;
|
|
1357
|
+
}
|
|
904
1358
|
if (value.operationKind === 'leave') {
|
|
905
1359
|
const statePhaseValid = (value.state === 'active' && (value.phase === 'prepared'
|
|
906
1360
|
|| value.phase === 'membership-left'
|
|
@@ -969,13 +1423,42 @@ function validateContinuity(records) {
|
|
|
969
1423
|
const leaveReplays = new Map(records
|
|
970
1424
|
.filter((item) => (item.kind === 'leave-former-principal-replay'))
|
|
971
1425
|
.map(item => [item.value.operationId, item]));
|
|
1426
|
+
const invitations = new Map(records
|
|
1427
|
+
.filter((item) => (item.kind === 'project-invitation'))
|
|
1428
|
+
.map(item => [item.value.invitationId, item]));
|
|
1429
|
+
const invitationEnvelopes = new Map(records
|
|
1430
|
+
.filter((item) => (item.kind === 'protected-invitation-envelope'))
|
|
1431
|
+
.map(item => [item.value.invitationId, item]));
|
|
1432
|
+
const claimOverrides = records.filter((item) => (item.kind === 'transferred-membership-claim-override'));
|
|
1433
|
+
const claimOverrideEnvelopes = new Map(records
|
|
1434
|
+
.filter((item) => (item.kind === 'protected-claim-override-envelope'))
|
|
1435
|
+
.map(item => [
|
|
1436
|
+
`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`,
|
|
1437
|
+
item,
|
|
1438
|
+
]));
|
|
1439
|
+
const managerOffers = records.filter((item) => (item.kind === 'manager-responsibility-offer'));
|
|
1440
|
+
const membershipIdempotencyTombstones = records.filter((item) => (item.kind === 'membership-idempotency-tombstone'));
|
|
1441
|
+
const membershipRecoveries = new Map(records
|
|
1442
|
+
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1443
|
+
.map(item => [item.value.operationId, item]));
|
|
1444
|
+
const secretReplayTombstones = records.filter((item) => (item.kind === 'secret-replay-tombstone'));
|
|
972
1445
|
const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
|
|
973
1446
|
if (nonterminalLifecycles.length > 1
|
|
974
1447
|
|| principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
|
|
975
1448
|
|| new Set(principalBindingRecords.map(item => item.value.principalId)).size
|
|
976
1449
|
!== principalBindingRecords.length
|
|
977
1450
|
|| 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
|
|
1451
|
+
|| new Set(redemptionReceiptRecords.map(item => (`${item.value.receipt.transferId}:${item.value.receipt.receiptId}`))).size !== redemptionReceiptRecords.length
|
|
1452
|
+
|| invitations.size !== records.filter(item => item.kind === 'project-invitation').length
|
|
1453
|
+
|| invitationEnvelopes.size
|
|
1454
|
+
!== records.filter(item => item.kind === 'protected-invitation-envelope').length
|
|
1455
|
+
|| new Set([...invitations.values()].map(item => (`${item.value.issuedByMemberId}:${item.value.idempotencyKey}`))).size !== invitations.size
|
|
1456
|
+
|| new Set(claimOverrides.map(item => (`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`))).size !== claimOverrides.length
|
|
1457
|
+
|| new Set(claimOverrides.map(item => (`${item.value.managerMemberId}:${item.value.idempotencyKey}`))).size !== claimOverrides.length
|
|
1458
|
+
|| claimOverrideEnvelopes.size
|
|
1459
|
+
!== records.filter(item => item.kind === 'protected-claim-override-envelope').length
|
|
1460
|
+
|| membershipRecoveries.size
|
|
1461
|
+
!== records.filter(item => item.kind === 'project-membership-recovery').length)
|
|
979
1462
|
throw invalidPayload('records');
|
|
980
1463
|
for (const lifecycle of lifecycles.values()) {
|
|
981
1464
|
if (lifecycleJournalHasInvalidSemantics(lifecycle)
|
|
@@ -986,6 +1469,11 @@ function validateContinuity(records) {
|
|
|
986
1469
|
if (lifecycle.value.operationKind === 'authority-transfer'
|
|
987
1470
|
&& !recoveries.has(lifecycle.value.operationId))
|
|
988
1471
|
throw invalidPayload('records');
|
|
1472
|
+
if ((lifecycle.value.operationKind === 'create-project'
|
|
1473
|
+
|| lifecycle.value.operationKind === 'join-project'
|
|
1474
|
+
|| lifecycle.value.operationKind === 'remove-member')
|
|
1475
|
+
&& !membershipRecoveries.has(lifecycle.value.operationId))
|
|
1476
|
+
throw invalidPayload('records');
|
|
989
1477
|
if (lifecycle.value.operationKind === 'leave') {
|
|
990
1478
|
const actorMemberId = lifecycle.value.actorMemberId;
|
|
991
1479
|
const actor = actorMemberId === null ? undefined : memberRecords.get(actorMemberId);
|
|
@@ -1162,6 +1650,13 @@ function validateContinuity(records) {
|
|
|
1162
1650
|
const lifecycle = lifecycles.get(claim.value.transferId);
|
|
1163
1651
|
const recovery = recoveries.get(claim.value.transferId);
|
|
1164
1652
|
const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
|
|
1653
|
+
const redeemedOverride = redemptionReceipt === undefined
|
|
1654
|
+
? undefined
|
|
1655
|
+
: claimOverrides.find(item => (item.value.transferId === claim.value.transferId
|
|
1656
|
+
&& item.value.memberId === claim.value.memberId
|
|
1657
|
+
&& item.value.state === 'redeemed'
|
|
1658
|
+
&& item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
|
|
1659
|
+
&& item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
|
|
1165
1660
|
if (memberRecords.get(claim.value.memberId)?.value.status !== 'active'
|
|
1166
1661
|
|| recovery === undefined
|
|
1167
1662
|
|| recovery.value.sourceAuthority.kind !== 'lan'
|
|
@@ -1171,7 +1666,11 @@ function validateContinuity(records) {
|
|
|
1171
1666
|
|| claim.value.expiresAt !== recovery.value.expiresAt
|
|
1172
1667
|
|| (claim.value.targetPrincipalId !== null
|
|
1173
1668
|
&& principalBindings.get(claim.value.memberId) !== claim.value.targetPrincipalId)
|
|
1174
|
-
|| (claim.value.state === 'redeemed'
|
|
1669
|
+
|| (claim.value.state === 'redeemed' && redemptionReceipt === undefined)
|
|
1670
|
+
|| (redemptionReceipt !== undefined
|
|
1671
|
+
&& claim.value.state !== 'redeemed'
|
|
1672
|
+
&& redeemedOverride === undefined)
|
|
1673
|
+
|| (claim.value.state === 'redeemed' && redeemedOverride !== undefined)) {
|
|
1175
1674
|
throw invalidPayload('records');
|
|
1176
1675
|
}
|
|
1177
1676
|
}
|
|
@@ -1187,6 +1686,153 @@ function validateContinuity(records) {
|
|
|
1187
1686
|
!== responderReplays.has(terminal.value.operationId))
|
|
1188
1687
|
throw invalidPayload('records');
|
|
1189
1688
|
}
|
|
1689
|
+
for (const recovery of membershipRecoveries.values()) {
|
|
1690
|
+
const lifecycle = lifecycles.get(recovery.value.operationId);
|
|
1691
|
+
const invitation = recovery.value.invitationId === null
|
|
1692
|
+
? undefined
|
|
1693
|
+
: invitations.get(recovery.value.invitationId);
|
|
1694
|
+
const member = memberRecords.get(recovery.value.memberId);
|
|
1695
|
+
if (lifecycle === undefined
|
|
1696
|
+
|| lifecycle.value.operationKind !== recovery.value.operationKind
|
|
1697
|
+
|| lifecycle.value.requestFingerprint !== recovery.value.requestFingerprint
|
|
1698
|
+
|| lifecycle.value.state !== 'completed'
|
|
1699
|
+
|| member === undefined
|
|
1700
|
+
|| (recovery.value.operationKind === 'join-project' && invitation === undefined)
|
|
1701
|
+
|| (recovery.value.operationKind !== 'join-project' && invitation !== undefined)
|
|
1702
|
+
|| (recovery.value.operationKind === 'create-project'
|
|
1703
|
+
&& lifecycle.value.actorMemberId !== null)
|
|
1704
|
+
|| (recovery.value.operationKind === 'join-project'
|
|
1705
|
+
&& lifecycle.value.actorMemberId !== null)
|
|
1706
|
+
|| (recovery.value.operationKind === 'remove-member'
|
|
1707
|
+
&& lifecycle.value.actorMemberId === null)
|
|
1708
|
+
|| (recovery.value.operationKind === 'join-project'
|
|
1709
|
+
&& invitation?.value.state !== 'redeemed')
|
|
1710
|
+
|| (recovery.value.operationKind === 'remove-member' && (member.value.status !== 'revoked'
|
|
1711
|
+
|| principalBindings.has(recovery.value.memberId))))
|
|
1712
|
+
throw invalidPayload('records');
|
|
1713
|
+
}
|
|
1714
|
+
for (const invitation of invitations.values()) {
|
|
1715
|
+
const envelope = invitationEnvelopes.get(invitation.value.invitationId);
|
|
1716
|
+
const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'createProjectInvitation'
|
|
1717
|
+
&& item.value.actorMemberId === invitation.value.issuedByMemberId
|
|
1718
|
+
&& item.value.idempotencyKey === invitation.value.idempotencyKey
|
|
1719
|
+
&& item.value.requestFingerprint === invitation.value.requestFingerprint));
|
|
1720
|
+
if (!members.has(invitation.value.issuedByMemberId)
|
|
1721
|
+
|| (envelope === undefined) === (tombstone === undefined)
|
|
1722
|
+
|| (tombstone !== undefined
|
|
1723
|
+
&& tombstone.value.expiredAt !== invitation.value.secretReplayExpiresAt)
|
|
1724
|
+
|| (envelope !== undefined && (envelope.value.projectId !== invitation.value.projectId
|
|
1725
|
+
|| envelope.value.createdAt !== invitation.value.createdAt
|
|
1726
|
+
|| envelope.value.expiresAt !== invitation.value.secretReplayExpiresAt)))
|
|
1727
|
+
throw invalidPayload('records');
|
|
1728
|
+
}
|
|
1729
|
+
for (const envelope of invitationEnvelopes.values()) {
|
|
1730
|
+
if (!invitations.has(envelope.value.invitationId))
|
|
1731
|
+
throw invalidPayload('records');
|
|
1732
|
+
}
|
|
1733
|
+
const overridesByMember = new Map();
|
|
1734
|
+
for (const override of claimOverrides) {
|
|
1735
|
+
const identity = `${override.value.transferId}:${override.value.memberId}`;
|
|
1736
|
+
const values = overridesByMember.get(identity) ?? [];
|
|
1737
|
+
values.push(override);
|
|
1738
|
+
overridesByMember.set(identity, values);
|
|
1739
|
+
const envelope = claimOverrideEnvelopes.get(`${identity}:${override.value.claimGeneration}`);
|
|
1740
|
+
const tombstone = secretReplayTombstones.find(item => (item.value.operation === 'reissueTransferredMembershipClaim'
|
|
1741
|
+
&& item.value.actorMemberId === override.value.managerMemberId
|
|
1742
|
+
&& item.value.idempotencyKey === override.value.idempotencyKey
|
|
1743
|
+
&& item.value.requestFingerprint === override.value.requestFingerprint));
|
|
1744
|
+
if (!members.has(override.value.memberId)
|
|
1745
|
+
|| !members.has(override.value.managerMemberId)
|
|
1746
|
+
|| (envelope === undefined) === (tombstone === undefined)
|
|
1747
|
+
|| (tombstone !== undefined
|
|
1748
|
+
&& tombstone.value.expiredAt !== override.value.secretReplayExpiresAt)
|
|
1749
|
+
|| (envelope !== undefined && (envelope.value.projectId !== override.value.projectId
|
|
1750
|
+
|| envelope.value.createdAt !== override.value.createdAt
|
|
1751
|
+
|| envelope.value.expiresAt !== override.value.secretReplayExpiresAt)))
|
|
1752
|
+
throw invalidPayload('records');
|
|
1753
|
+
}
|
|
1754
|
+
for (const overrides of overridesByMember.values()) {
|
|
1755
|
+
overrides.sort((left, right) => left.value.claimGeneration - right.value.claimGeneration);
|
|
1756
|
+
const highest = overrides.at(-1);
|
|
1757
|
+
const first = overrides[0];
|
|
1758
|
+
const identity = first === undefined
|
|
1759
|
+
? ''
|
|
1760
|
+
: `${first.value.transferId}:${first.value.memberId}`;
|
|
1761
|
+
const sourceClaim = claims.get(identity);
|
|
1762
|
+
const recovery = first === undefined ? undefined : recoveries.get(first.value.transferId);
|
|
1763
|
+
const member = first === undefined ? undefined : memberRecords.get(first.value.memberId);
|
|
1764
|
+
const principalBinding = first === undefined
|
|
1765
|
+
? undefined
|
|
1766
|
+
: principalBindings.get(first.value.memberId);
|
|
1767
|
+
const redemptionReceipt = redemptionReceipts.get(identity);
|
|
1768
|
+
const redeemedOverrides = overrides.filter(item => item.value.state === 'redeemed');
|
|
1769
|
+
if (first === undefined
|
|
1770
|
+
|| sourceClaim === undefined
|
|
1771
|
+
|| recovery?.value.sourceAuthority.kind !== 'lan'
|
|
1772
|
+
|| member?.value.status !== 'active'
|
|
1773
|
+
|| sourceClaim.value.state === 'redeemed'
|
|
1774
|
+
|| overrides.some((item, index) => item.value.claimGeneration !== index + 1)
|
|
1775
|
+
|| overrides.some((item, index) => item.value.supersededClaimSha256 !== (index === 0
|
|
1776
|
+
? sourceClaim.value.claimSha256
|
|
1777
|
+
: overrides[index - 1].value.claimSha256))
|
|
1778
|
+
|| new Set(overrides.map(item => item.value.claimSha256)).size !== overrides.length
|
|
1779
|
+
|| overrides.filter(item => item.value.state === 'active').length > 1
|
|
1780
|
+
|| overrides.some(item => item.value.state === 'active' && item !== highest)
|
|
1781
|
+
|| redeemedOverrides.length > 1
|
|
1782
|
+
|| redeemedOverrides.some(item => item !== highest)
|
|
1783
|
+
|| highest?.value.state === 'superseded'
|
|
1784
|
+
|| (highest?.value.state === 'active' && (principalBinding !== undefined
|
|
1785
|
+
|| redemptionReceipt !== undefined))
|
|
1786
|
+
|| (highest?.value.state === 'redeemed' && (principalBinding !== highest.value.targetPrincipalId
|
|
1787
|
+
|| redemptionReceipt === undefined
|
|
1788
|
+
|| redemptionReceipt.value.receipt.transferId !== highest.value.transferId
|
|
1789
|
+
|| redemptionReceipt.value.receipt.memberId !== highest.value.memberId
|
|
1790
|
+
|| redemptionReceipt.value.receipt.claimSha256 !== highest.value.claimSha256
|
|
1791
|
+
|| redemptionReceipt.value.receipt.receiptId !== highest.value.redemptionReceiptId
|
|
1792
|
+
|| redemptionReceipt.value.receipt.checkpointSha256
|
|
1793
|
+
!== sourceClaim.value.checkpointSha256))
|
|
1794
|
+
|| (highest?.value.state !== 'redeemed' && redemptionReceipt !== undefined))
|
|
1795
|
+
throw invalidPayload('records');
|
|
1796
|
+
}
|
|
1797
|
+
for (const envelope of claimOverrideEnvelopes.values()) {
|
|
1798
|
+
if (!claimOverrides.some(item => (item.value.transferId === envelope.value.transferId
|
|
1799
|
+
&& item.value.memberId === envelope.value.memberId
|
|
1800
|
+
&& item.value.claimGeneration === envelope.value.claimGeneration)))
|
|
1801
|
+
throw invalidPayload('records');
|
|
1802
|
+
}
|
|
1803
|
+
const currentOffers = managerOffers.filter(item => (item.value.state === 'offered' || item.value.state === 'acknowledged'));
|
|
1804
|
+
if (managerOffers.some(item => (item.value.sourceManagerMemberId === item.value.targetMemberId
|
|
1805
|
+
|| !members.has(item.value.sourceManagerMemberId)
|
|
1806
|
+
|| !members.has(item.value.targetMemberId)))
|
|
1807
|
+
|| new Set(currentOffers.map(item => item.value.sourceManagerMemberId)).size
|
|
1808
|
+
!== currentOffers.length
|
|
1809
|
+
|| new Set(currentOffers.map(item => item.value.targetMemberId)).size
|
|
1810
|
+
!== currentOffers.length)
|
|
1811
|
+
throw invalidPayload('records');
|
|
1812
|
+
for (const tombstone of secretReplayTombstones) {
|
|
1813
|
+
const liveSecretRecord = tombstone.value.operation === 'createProjectInvitation'
|
|
1814
|
+
? [...invitations.values()].some(item => (item.value.issuedByMemberId === tombstone.value.actorMemberId
|
|
1815
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1816
|
+
&& invitationEnvelopes.has(item.value.invitationId)))
|
|
1817
|
+
: claimOverrides.some(item => (item.value.managerMemberId === tombstone.value.actorMemberId
|
|
1818
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1819
|
+
&& claimOverrideEnvelopes.has(`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`)));
|
|
1820
|
+
if (!members.has(tombstone.value.actorMemberId) || liveSecretRecord) {
|
|
1821
|
+
throw invalidPayload('records');
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
for (const tombstone of membershipIdempotencyTombstones) {
|
|
1825
|
+
const exactResult = records.some(item => (item.kind === 'idempotency-result'
|
|
1826
|
+
&& item.value.memberId === tombstone.value.actorMemberId
|
|
1827
|
+
&& item.value.operation === tombstone.value.operation
|
|
1828
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1829
|
+
const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
|
|
1830
|
+
&& managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
|
|
1831
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1832
|
+
if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
|
|
1833
|
+
throw invalidPayload('records');
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1190
1836
|
for (const item of records) {
|
|
1191
1837
|
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1192
1838
|
throw invalidPayload('records');
|
|
@@ -1238,6 +1884,19 @@ function validateContinuity(records) {
|
|
|
1238
1884
|
const lifecycle = lifecycles.get(receipt.transferId);
|
|
1239
1885
|
const terminalPrincipal = terminalPrincipals.get(identity);
|
|
1240
1886
|
const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
|
|
1887
|
+
const redeemedOverride = claimOverrides.find(item => (item.value.transferId === receipt.transferId
|
|
1888
|
+
&& item.value.memberId === receipt.memberId
|
|
1889
|
+
&& item.value.state === 'redeemed'
|
|
1890
|
+
&& item.value.claimSha256 === receipt.claimSha256
|
|
1891
|
+
&& item.value.redemptionReceiptId === receipt.receiptId));
|
|
1892
|
+
const matchesSourceClaim = claim?.value.state === 'redeemed'
|
|
1893
|
+
&& claim.value.claimSha256 === receipt.claimSha256
|
|
1894
|
+
&& claim.value.checkpointSha256 === receipt.checkpointSha256
|
|
1895
|
+
&& claim.value.operationIntentId === receipt.operationIntentId
|
|
1896
|
+
&& claim.value.redemptionReceiptId === receipt.receiptId;
|
|
1897
|
+
const matchesOverride = claim !== undefined
|
|
1898
|
+
&& redeemedOverride !== undefined
|
|
1899
|
+
&& claim.value.checkpointSha256 === receipt.checkpointSha256;
|
|
1241
1900
|
if (recovery === undefined
|
|
1242
1901
|
|| lifecycle === undefined
|
|
1243
1902
|
|| memberRecords.get(receipt.memberId)?.value.status !== 'active'
|
|
@@ -1245,11 +1904,7 @@ function validateContinuity(records) {
|
|
|
1245
1904
|
|| !keys.has(`${receipt.transferId}:${receipt.receiptKeyId}`)
|
|
1246
1905
|
|| Date.parse(receipt.redeemedAt) > Date.parse(recovery.value.expiresAt)
|
|
1247
1906
|
|| (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))
|
|
1907
|
+
|| (!matchesSourceClaim && !matchesOverride)))
|
|
1253
1908
|
|| (!lanToCloud && (item.value.acknowledgedAt === null
|
|
1254
1909
|
|| receipt.memberId === recovery.value.targetHostMemberId
|
|
1255
1910
|
|| receipt.checkpointSha256 !== lifecycle.value.checkpointSha256
|
|
@@ -1444,6 +2099,33 @@ export function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
|
|
|
1444
2099
|
export function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
1445
2100
|
const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
|
|
1446
2101
|
const decodedRecords = decodeCollabProjectBackupCheckpointCoordinationNdjson(records.map(item => JSON.stringify(item)).join('\n') + '\n');
|
|
2102
|
+
const invitationEnvelopeIds = new Set(decodedRecords
|
|
2103
|
+
.filter(item => item.kind === 'protected-invitation-envelope')
|
|
2104
|
+
.map(item => item.recordId));
|
|
2105
|
+
const claimOverrideEnvelopeIds = new Set(decodedRecords
|
|
2106
|
+
.filter(item => item.kind === 'protected-claim-override-envelope')
|
|
2107
|
+
.map(item => item.recordId));
|
|
2108
|
+
const captureTime = Date.parse(decodedManifest.createdAt);
|
|
2109
|
+
if (decodedRecords.some(item => {
|
|
2110
|
+
if (item.kind === 'secret-replay-tombstone') {
|
|
2111
|
+
return Date.parse(item.value.expiredAt) > captureTime;
|
|
2112
|
+
}
|
|
2113
|
+
if (item.kind === 'membership-idempotency-tombstone') {
|
|
2114
|
+
return Date.parse(item.value.compactedAt) > captureTime;
|
|
2115
|
+
}
|
|
2116
|
+
if (item.kind === 'project-invitation') {
|
|
2117
|
+
return captureTime < Date.parse(item.value.createdAt)
|
|
2118
|
+
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
2119
|
+
!== invitationEnvelopeIds.has(item.recordId);
|
|
2120
|
+
}
|
|
2121
|
+
if (item.kind === 'transferred-membership-claim-override') {
|
|
2122
|
+
return captureTime < Date.parse(item.value.createdAt)
|
|
2123
|
+
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
2124
|
+
!== claimOverrideEnvelopeIds.has(item.recordId);
|
|
2125
|
+
}
|
|
2126
|
+
return false;
|
|
2127
|
+
}))
|
|
2128
|
+
throw invalidPayload('records');
|
|
1447
2129
|
if (decodedRecords.some(item => {
|
|
1448
2130
|
switch (item.kind) {
|
|
1449
2131
|
case 'lifecycle-journal': return item.value.operationId === decodedManifest.operationId;
|