@claudian-collab/protocol 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/dist/CollabAuthorityTransfer.d.ts +1 -1
- package/dist/CollabCloudBinding.d.ts +1 -1
- package/dist/CollabCloudBinding.js +7 -0
- package/dist/CollabControlOperationCodecs.d.ts +18 -0
- package/dist/CollabControlOperationCodecs.js +2 -0
- package/dist/CollabProjectBackupCheckpoint.d.ts +93 -4
- package/dist/CollabProjectBackupCheckpoint.js +640 -12
- package/dist/CollabProjectMembership.d.ts +318 -0
- package/dist/CollabProjectMembership.js +714 -0
- package/dist/CollabProtocol.d.ts +2 -1
- package/dist/esm/CollabAuthorityTransfer.mjs +884 -0
- package/dist/esm/CollabCloudBinding.mjs +559 -0
- package/dist/esm/CollabCloudProjectEvent.mjs +150 -0
- package/dist/esm/CollabCloudProjectSnapshot.mjs +277 -0
- package/dist/esm/CollabConstants.mjs +31 -0
- package/dist/esm/CollabControlOperationCodecs.mjs +106 -0
- package/dist/esm/CollabError.mjs +175 -0
- package/dist/esm/CollabMarkdownProse.mjs +54 -0
- package/dist/esm/CollabMemberMentionParser.mjs +48 -0
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +2101 -0
- package/dist/esm/CollabProjectCheckpoint.mjs +1326 -0
- package/dist/esm/CollabProjectMembership.mjs +709 -0
- package/dist/esm/CollabProjectRetirement.mjs +119 -0
- package/dist/esm/CollabProtocol.mjs +64 -0
- package/dist/esm/CollabRequestTicketRequestCodecs.mjs +263 -0
- package/dist/esm/CollabRequestTicketResponseCodecs.mjs +353 -0
- package/dist/esm/CollabTicketReferenceParser.mjs +52 -0
- package/dist/esm/CollabValidation.mjs +19 -0
- package/dist/esm/DevelopmentBootstrap.mjs +537 -0
- package/dist/esm/index.mjs +17 -0
- package/dist/esm/types.mjs +8 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +10 -3
- package/package.json +5 -2
|
@@ -0,0 +1,2101 @@
|
|
|
1
|
+
import { COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES, COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES, COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES, decodeCollabAuthorityRelinquishmentProof, decodeCollabAuthorityTransferLifecycleFence, decodeCollabTransferredMembershipClaimCustodyReceipt, decodeCollabTransferredMembershipRedemptionReceipt, } from './CollabAuthorityTransfer.mjs';
|
|
2
|
+
import { COLLAB_MAIN_REF, COLLAB_MEMBER_REF_PREFIX, } from './CollabConstants.mjs';
|
|
3
|
+
import { collabControlOperationCodec, } from './CollabControlOperationCodecs.mjs';
|
|
4
|
+
import { CollabError } from './CollabError.mjs';
|
|
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';
|
|
7
|
+
import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
|
|
8
|
+
export const COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE = 'pre-production-replaceable';
|
|
9
|
+
export const COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION = 3;
|
|
10
|
+
const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
11
|
+
'lifecycle-journal',
|
|
12
|
+
'authority-transfer-recovery',
|
|
13
|
+
'transferred-membership-claim',
|
|
14
|
+
'transfer-receipt-key',
|
|
15
|
+
'transfer-claim-batch-receipt',
|
|
16
|
+
'transfer-redemption-receipt',
|
|
17
|
+
'terminal-principal',
|
|
18
|
+
'terminal-responder-replay',
|
|
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',
|
|
27
|
+
]);
|
|
28
|
+
export const COLLAB_PROJECT_BACKUP_RECORD_KINDS = Object.freeze([
|
|
29
|
+
...COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS,
|
|
30
|
+
'cloud-event',
|
|
31
|
+
'cloud-event-cursor',
|
|
32
|
+
'idempotency-result',
|
|
33
|
+
'principal-binding',
|
|
34
|
+
'repository-placement',
|
|
35
|
+
...BACKUP_CONTINUITY_RECORD_KINDS,
|
|
36
|
+
'terminal-responder',
|
|
37
|
+
'protected-claim-envelope',
|
|
38
|
+
'tombstone',
|
|
39
|
+
'schema-catalog',
|
|
40
|
+
'server-compatibility',
|
|
41
|
+
'authority-volume-pair',
|
|
42
|
+
]);
|
|
43
|
+
const CONTINUITY_KIND_SET = new Set(BACKUP_CONTINUITY_RECORD_KINDS);
|
|
44
|
+
const BACKUP_KIND_SET = new Set(COLLAB_PROJECT_BACKUP_RECORD_KINDS);
|
|
45
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
46
|
+
const PRINCIPAL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
47
|
+
const PHASE_PATTERN = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
48
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
49
|
+
const STORAGE_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]{0,127}$/u;
|
|
50
|
+
const STORAGE_NODE_PATTERN = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
|
|
51
|
+
const CANCELLATION_PHASE_INDEX = new Map(COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES.map((phase, index) => [phase, index]));
|
|
52
|
+
const BACKUP_EXPORT_ACTIVE_PHASE_SET = new Set([
|
|
53
|
+
'prepared',
|
|
54
|
+
'coordination-captured',
|
|
55
|
+
'repository-captured',
|
|
56
|
+
'checkpoint-verified',
|
|
57
|
+
'artifact-published',
|
|
58
|
+
'cancel-intent',
|
|
59
|
+
]);
|
|
60
|
+
const BACKUP_EXPORT_CHECKPOINT_REQUIRED_PHASE_SET = new Set([
|
|
61
|
+
'checkpoint-verified', 'artifact-published', 'completed',
|
|
62
|
+
]);
|
|
63
|
+
const DELETE_ACTIVE_PHASE_SET = new Set([
|
|
64
|
+
'traffic-denied',
|
|
65
|
+
'repository-delete-intent',
|
|
66
|
+
'repository-removed',
|
|
67
|
+
'coordination-removed',
|
|
68
|
+
'tombstoned',
|
|
69
|
+
]);
|
|
70
|
+
const PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET = new Set([
|
|
71
|
+
'createProjectInvitation',
|
|
72
|
+
'getTransferredMembershipClaim',
|
|
73
|
+
'reissueTransferredMembershipClaim',
|
|
74
|
+
'rotateTransferredMembershipClaims',
|
|
75
|
+
]);
|
|
76
|
+
function invalidPayload(field) {
|
|
77
|
+
return new CollabError({ code: 'protocol-payload-invalid', safeContext: { field } });
|
|
78
|
+
}
|
|
79
|
+
function record(value, field) {
|
|
80
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
81
|
+
throw invalidPayload(field);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function exactRecord(value, field, keys) {
|
|
86
|
+
const source = record(value, field);
|
|
87
|
+
const expected = new Set(keys);
|
|
88
|
+
if (!keys.every(key => Object.hasOwn(source, key))
|
|
89
|
+
|| Object.keys(source).some(key => !expected.has(key)))
|
|
90
|
+
throw invalidPayload(field);
|
|
91
|
+
return source;
|
|
92
|
+
}
|
|
93
|
+
function boundedString(source, field, maximumBytes, allowEmpty = false) {
|
|
94
|
+
const value = source[field];
|
|
95
|
+
if (typeof value !== 'string'
|
|
96
|
+
|| (!allowEmpty && value.length === 0)
|
|
97
|
+
|| !hasUtf8ByteLengthAtMost(value, maximumBytes))
|
|
98
|
+
throw invalidPayload(field);
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
function token(source, field, validate = isCollabOpaqueId) {
|
|
102
|
+
const value = source[field];
|
|
103
|
+
if (typeof value !== 'string' || !validate(value))
|
|
104
|
+
throw invalidPayload(field);
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
function nullableToken(source, field, validate = isCollabOpaqueId) {
|
|
108
|
+
return source[field] === null ? null : token(source, field, validate);
|
|
109
|
+
}
|
|
110
|
+
function positiveInteger(source, field) {
|
|
111
|
+
const value = source[field];
|
|
112
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
|
|
113
|
+
throw invalidPayload(field);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function nullablePositiveInteger(source, field) {
|
|
118
|
+
return source[field] === null ? null : positiveInteger(source, field);
|
|
119
|
+
}
|
|
120
|
+
function timestampValue(value, field) {
|
|
121
|
+
if (typeof value !== 'string'
|
|
122
|
+
|| value.length > 64
|
|
123
|
+
|| Number.isNaN(Date.parse(value))
|
|
124
|
+
|| new Date(value).toISOString() !== value)
|
|
125
|
+
throw invalidPayload(field);
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
function timestamp(source, field) {
|
|
129
|
+
return timestampValue(source[field], field);
|
|
130
|
+
}
|
|
131
|
+
function hasExactDuration(start, end, durationMs) {
|
|
132
|
+
return Date.parse(end) === Date.parse(start) + durationMs;
|
|
133
|
+
}
|
|
134
|
+
function nullableTimestamp(source, field) {
|
|
135
|
+
return source[field] === null ? null : timestampValue(source[field], field);
|
|
136
|
+
}
|
|
137
|
+
function sha256(source, field) {
|
|
138
|
+
const value = source[field];
|
|
139
|
+
if (typeof value !== 'string' || !SHA256_PATTERN.test(value))
|
|
140
|
+
throw invalidPayload(field);
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
function nullableSha256(source, field) {
|
|
144
|
+
return source[field] === null ? null : sha256(source, field);
|
|
145
|
+
}
|
|
146
|
+
function literal(source, field, values) {
|
|
147
|
+
const value = source[field];
|
|
148
|
+
if (typeof value !== 'string' || !values.includes(value))
|
|
149
|
+
throw invalidPayload(field);
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
function authority(value, field) {
|
|
153
|
+
const source = exactRecord(value, field, ['generation', 'kind']);
|
|
154
|
+
return {
|
|
155
|
+
generation: positiveInteger(source, 'generation'),
|
|
156
|
+
kind: literal(source, 'kind', ['cloud', 'lan']),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function canonicalBase64urlPublicKey(source, field) {
|
|
160
|
+
const value = boundedString(source, field, 128);
|
|
161
|
+
if (!BASE64URL_PATTERN.test(value))
|
|
162
|
+
throw invalidPayload(field);
|
|
163
|
+
const remainder = value.length % 4;
|
|
164
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
165
|
+
const finalIndex = alphabet.indexOf(value[value.length - 1]);
|
|
166
|
+
const decodedBytes = Math.floor(value.length * 6 / 8);
|
|
167
|
+
if (remainder === 1
|
|
168
|
+
|| (remainder === 2 && (finalIndex & 15) !== 0)
|
|
169
|
+
|| (remainder === 3 && (finalIndex & 3) !== 0)
|
|
170
|
+
|| decodedBytes !== 32)
|
|
171
|
+
throw invalidPayload(field);
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
function proofValue(source, field) {
|
|
175
|
+
const value = boundedString(source, field, 4096);
|
|
176
|
+
if (!BASE64URL_PATTERN.test(value))
|
|
177
|
+
throw invalidPayload(field);
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
function absoluteTargetUrl(source) {
|
|
181
|
+
const targetUrl = boundedString(source, 'targetUrl', 2048);
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = new URL(targetUrl);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
throw invalidPayload('targetUrl');
|
|
188
|
+
}
|
|
189
|
+
if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
190
|
+
|| parsed.username.length > 0
|
|
191
|
+
|| parsed.password.length > 0
|
|
192
|
+
|| targetUrl.includes('?')
|
|
193
|
+
|| targetUrl.includes('#'))
|
|
194
|
+
throw invalidPayload('targetUrl');
|
|
195
|
+
return targetUrl;
|
|
196
|
+
}
|
|
197
|
+
function principal(source, field) {
|
|
198
|
+
return token(source, field, value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
|
|
199
|
+
}
|
|
200
|
+
function sanitizeBackupBasePrincipalRecord(item) {
|
|
201
|
+
const source = record(item, 'record');
|
|
202
|
+
const value = record(source.value, 'value');
|
|
203
|
+
if (source.kind === 'principal-binding') {
|
|
204
|
+
return {
|
|
205
|
+
...source,
|
|
206
|
+
value: { ...value, principalId: `principal_${String(value.memberId)}` },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (source.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
|
|
210
|
+
return {
|
|
211
|
+
...source,
|
|
212
|
+
value: {
|
|
213
|
+
...value,
|
|
214
|
+
acknowledgements: value.acknowledgements.map(item => ({
|
|
215
|
+
...record(item, 'acknowledgement'),
|
|
216
|
+
principalId: `principal_${String(record(item, 'acknowledgement').memberId)}`,
|
|
217
|
+
})),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return item;
|
|
222
|
+
}
|
|
223
|
+
function restoreBackupBasePrincipals(source, base) {
|
|
224
|
+
if (base.kind === 'principal-binding') {
|
|
225
|
+
const value = exactRecord(source.value, 'value', [
|
|
226
|
+
'boundAt', 'memberId', 'principalId', 'projectId',
|
|
227
|
+
]);
|
|
228
|
+
return {
|
|
229
|
+
...base,
|
|
230
|
+
value: { ...base.value, principalId: principal(value, 'principalId') },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (base.kind === 'terminal-responder') {
|
|
234
|
+
const value = exactRecord(source.value, 'value', [
|
|
235
|
+
'acknowledgements',
|
|
236
|
+
'eligibleMemberIds',
|
|
237
|
+
'expiresAt',
|
|
238
|
+
'operation',
|
|
239
|
+
'operationId',
|
|
240
|
+
'projectId',
|
|
241
|
+
'responseJson',
|
|
242
|
+
]);
|
|
243
|
+
const sourceAcknowledgements = value.acknowledgements;
|
|
244
|
+
if (!Array.isArray(sourceAcknowledgements)
|
|
245
|
+
|| sourceAcknowledgements.length !== base.value.acknowledgements.length)
|
|
246
|
+
throw invalidPayload('acknowledgements');
|
|
247
|
+
const acknowledgements = base.value.acknowledgements.map((item, index) => {
|
|
248
|
+
const original = exactRecord(sourceAcknowledgements[index], 'acknowledgement', [
|
|
249
|
+
'acknowledgedAt', 'memberId', 'principalId',
|
|
250
|
+
]);
|
|
251
|
+
return { ...item, principalId: principal(original, 'principalId') };
|
|
252
|
+
});
|
|
253
|
+
if (new Set(acknowledgements.map(item => item.principalId)).size !== acknowledgements.length) {
|
|
254
|
+
throw invalidPayload('acknowledgements');
|
|
255
|
+
}
|
|
256
|
+
return { ...base, value: { ...base.value, acknowledgements } };
|
|
257
|
+
}
|
|
258
|
+
return base;
|
|
259
|
+
}
|
|
260
|
+
function repositoryPublicationRef(value) {
|
|
261
|
+
const source = exactRecord(value, 'ref', ['name', 'oid']);
|
|
262
|
+
const name = boundedString(source, 'name', 512);
|
|
263
|
+
const memberId = name.startsWith(COLLAB_MEMBER_REF_PREFIX)
|
|
264
|
+
? name.slice(COLLAB_MEMBER_REF_PREFIX.length)
|
|
265
|
+
: undefined;
|
|
266
|
+
if (name !== COLLAB_MAIN_REF
|
|
267
|
+
&& (memberId === undefined || !isCollabMemberId(memberId))) {
|
|
268
|
+
throw invalidPayload('name');
|
|
269
|
+
}
|
|
270
|
+
return { name, oid: token(source, 'oid', isCollabGitOid) };
|
|
271
|
+
}
|
|
272
|
+
function inactiveRepositoryPublication(value) {
|
|
273
|
+
if (value === null)
|
|
274
|
+
return null;
|
|
275
|
+
const source = exactRecord(value, 'inactivePublication', [
|
|
276
|
+
'artifactKey',
|
|
277
|
+
'bundleByteCount',
|
|
278
|
+
'bundleSha256',
|
|
279
|
+
'objectFormat',
|
|
280
|
+
'operationId',
|
|
281
|
+
'placementGeneration',
|
|
282
|
+
'projectId',
|
|
283
|
+
'publicationMarkerSha256',
|
|
284
|
+
'refs',
|
|
285
|
+
'repositoryStorageKey',
|
|
286
|
+
'status',
|
|
287
|
+
'storageNodeId',
|
|
288
|
+
'validationMarkerSha256',
|
|
289
|
+
]);
|
|
290
|
+
if (!Array.isArray(source.refs) || source.refs.length < 2) {
|
|
291
|
+
throw invalidPayload('refs');
|
|
292
|
+
}
|
|
293
|
+
const refs = source.refs.map(repositoryPublicationRef);
|
|
294
|
+
const objectFormat = literal(source, 'objectFormat', ['sha1', 'sha256']);
|
|
295
|
+
const objectLength = objectFormat === 'sha1' ? 40 : 64;
|
|
296
|
+
const bundleByteCount = positiveInteger(source, 'bundleByteCount');
|
|
297
|
+
const repositoryStorageKey = boundedString(source, 'repositoryStorageKey', 128);
|
|
298
|
+
const storageNodeId = boundedString(source, 'storageNodeId', 64);
|
|
299
|
+
if (refs[0].name !== COLLAB_MAIN_REF
|
|
300
|
+
|| refs.slice(1).some(item => !item.name.startsWith(COLLAB_MEMBER_REF_PREFIX))
|
|
301
|
+
|| refs.some(item => item.oid.length !== objectLength)
|
|
302
|
+
|| refs.some((item, index) => index > 0
|
|
303
|
+
&& refs[index - 1].name.localeCompare(item.name, 'en-US') >= 0)
|
|
304
|
+
|| bundleByteCount > COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxRepositoryBundleBytes
|
|
305
|
+
|| !STORAGE_KEY_PATTERN.test(repositoryStorageKey)
|
|
306
|
+
|| !STORAGE_NODE_PATTERN.test(storageNodeId))
|
|
307
|
+
throw invalidPayload('refs');
|
|
308
|
+
const artifactKey = boundedString(source, 'artifactKey', 64);
|
|
309
|
+
const placementGeneration = positiveInteger(source, 'placementGeneration');
|
|
310
|
+
if (!SHA256_PATTERN.test(artifactKey) || placementGeneration !== 1) {
|
|
311
|
+
throw invalidPayload('inactivePublication');
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
artifactKey,
|
|
315
|
+
bundleByteCount,
|
|
316
|
+
bundleSha256: sha256(source, 'bundleSha256'),
|
|
317
|
+
objectFormat,
|
|
318
|
+
operationId: token(source, 'operationId'),
|
|
319
|
+
placementGeneration,
|
|
320
|
+
projectId: token(source, 'projectId', isCollabProjectId),
|
|
321
|
+
publicationMarkerSha256: sha256(source, 'publicationMarkerSha256'),
|
|
322
|
+
refs: Object.freeze(refs),
|
|
323
|
+
repositoryStorageKey,
|
|
324
|
+
status: literal(source, 'status', ['inactive']),
|
|
325
|
+
storageNodeId,
|
|
326
|
+
validationMarkerSha256: sha256(source, 'validationMarkerSha256'),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function lanToCloudSourceEvidence(value) {
|
|
330
|
+
if (value === null)
|
|
331
|
+
return null;
|
|
332
|
+
const source = exactRecord(value, 'sourceEvidence', [
|
|
333
|
+
'checkpointManifestSha256',
|
|
334
|
+
'principalId',
|
|
335
|
+
'proof',
|
|
336
|
+
'receiptKeyId',
|
|
337
|
+
'receiptPublicKey',
|
|
338
|
+
'schemaVersion',
|
|
339
|
+
]);
|
|
340
|
+
if (source.schemaVersion !== 1)
|
|
341
|
+
throw invalidPayload('schemaVersion');
|
|
342
|
+
return {
|
|
343
|
+
checkpointManifestSha256: sha256(source, 'checkpointManifestSha256'),
|
|
344
|
+
principalId: principal(source, 'principalId'),
|
|
345
|
+
proof: proofValue(source, 'proof'),
|
|
346
|
+
receiptKeyId: token(source, 'receiptKeyId'),
|
|
347
|
+
receiptPublicKey: canonicalBase64urlPublicKey(source, 'receiptPublicKey'),
|
|
348
|
+
schemaVersion: 1,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function cloudToLanTargetEvidence(value) {
|
|
352
|
+
if (value === null)
|
|
353
|
+
return null;
|
|
354
|
+
const source = exactRecord(value, 'targetEvidence', [
|
|
355
|
+
'acceptanceIntentId',
|
|
356
|
+
'principalId',
|
|
357
|
+
'proof',
|
|
358
|
+
'receiptKeyId',
|
|
359
|
+
'receiptPublicKey',
|
|
360
|
+
'schemaVersion',
|
|
361
|
+
]);
|
|
362
|
+
if (source.schemaVersion !== 1)
|
|
363
|
+
throw invalidPayload('schemaVersion');
|
|
364
|
+
return {
|
|
365
|
+
acceptanceIntentId: token(source, 'acceptanceIntentId'),
|
|
366
|
+
principalId: principal(source, 'principalId'),
|
|
367
|
+
proof: proofValue(source, 'proof'),
|
|
368
|
+
receiptKeyId: token(source, 'receiptKeyId'),
|
|
369
|
+
receiptPublicKey: canonicalBase64urlPublicKey(source, 'receiptPublicKey'),
|
|
370
|
+
schemaVersion: 1,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function operation(source, field) {
|
|
374
|
+
const value = source[field];
|
|
375
|
+
if (typeof value !== 'string')
|
|
376
|
+
throw invalidPayload(field);
|
|
377
|
+
try {
|
|
378
|
+
collabControlOperationCodec(value);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
throw invalidPayload(field);
|
|
382
|
+
}
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
function recordEnvelope(value) {
|
|
386
|
+
const source = exactRecord(value, 'record', ['kind', 'recordId', 'revision', 'value']);
|
|
387
|
+
const kind = source.kind;
|
|
388
|
+
if (typeof kind !== 'string' || !BACKUP_KIND_SET.has(kind))
|
|
389
|
+
throw invalidPayload('kind');
|
|
390
|
+
return {
|
|
391
|
+
kind: kind,
|
|
392
|
+
recordId: boundedString(source, 'recordId', 512),
|
|
393
|
+
revision: positiveInteger(source, 'revision'),
|
|
394
|
+
source,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
export function collabProjectBackupIdempotencyRecordId(input) {
|
|
398
|
+
if (!isCollabProjectId(input.projectId)
|
|
399
|
+
|| !isCollabMemberId(input.memberId)
|
|
400
|
+
|| !isCollabOpaqueId(input.idempotencyKey))
|
|
401
|
+
throw invalidPayload('idempotencyIdentity');
|
|
402
|
+
operation({ operation: input.operation }, 'operation');
|
|
403
|
+
return `${input.projectId}:${input.memberId}:${input.operation}:${input.idempotencyKey}`;
|
|
404
|
+
}
|
|
405
|
+
function idempotencyResultRecord(source, recordId, revision) {
|
|
406
|
+
const value = exactRecord(source.value, 'value', [
|
|
407
|
+
'createdAt',
|
|
408
|
+
'idempotencyKey',
|
|
409
|
+
'memberId',
|
|
410
|
+
'operation',
|
|
411
|
+
'projectId',
|
|
412
|
+
'requestFingerprint',
|
|
413
|
+
'responseJson',
|
|
414
|
+
]);
|
|
415
|
+
const decodedOperation = operation(value, 'operation');
|
|
416
|
+
if (PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET.has(decodedOperation)) {
|
|
417
|
+
throw invalidPayload('responseJson');
|
|
418
|
+
}
|
|
419
|
+
const projectId = token(value, 'projectId', isCollabProjectId);
|
|
420
|
+
const memberId = token(value, 'memberId', isCollabMemberId);
|
|
421
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
422
|
+
let responseJson;
|
|
423
|
+
try {
|
|
424
|
+
responseJson = boundedString(value, 'responseJson', 512 * 1024, true);
|
|
425
|
+
const decoded = collabControlOperationCodec(decodedOperation).decodeResponse(JSON.parse(responseJson));
|
|
426
|
+
if (JSON.stringify(decoded) !== responseJson
|
|
427
|
+
|| (typeof decoded === 'object'
|
|
428
|
+
&& decoded !== null
|
|
429
|
+
&& 'projectId' in decoded
|
|
430
|
+
&& decoded.projectId !== projectId))
|
|
431
|
+
throw invalidPayload('responseJson');
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
throw invalidPayload('responseJson');
|
|
435
|
+
}
|
|
436
|
+
if (recordId !== collabProjectBackupIdempotencyRecordId({
|
|
437
|
+
idempotencyKey,
|
|
438
|
+
memberId,
|
|
439
|
+
operation: decodedOperation,
|
|
440
|
+
projectId,
|
|
441
|
+
}))
|
|
442
|
+
throw invalidPayload('recordId');
|
|
443
|
+
return {
|
|
444
|
+
kind: 'idempotency-result',
|
|
445
|
+
recordId,
|
|
446
|
+
revision,
|
|
447
|
+
value: {
|
|
448
|
+
createdAt: timestamp(value, 'createdAt'),
|
|
449
|
+
idempotencyKey,
|
|
450
|
+
memberId,
|
|
451
|
+
operation: decodedOperation,
|
|
452
|
+
projectId,
|
|
453
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
454
|
+
responseJson,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function lifecycleJournalRecord(source, recordId, revision) {
|
|
459
|
+
const value = exactRecord(source.value, 'value', [
|
|
460
|
+
'actorMemberId',
|
|
461
|
+
'batchRevision',
|
|
462
|
+
'batchSha256',
|
|
463
|
+
'checkpointSha256',
|
|
464
|
+
'createdAt',
|
|
465
|
+
'direction',
|
|
466
|
+
'expectedAuthorityGeneration',
|
|
467
|
+
'expectedPersonalRefOid',
|
|
468
|
+
'idempotencyKey',
|
|
469
|
+
'operationId',
|
|
470
|
+
'operationKind',
|
|
471
|
+
'phase',
|
|
472
|
+
'projectId',
|
|
473
|
+
'recoveryFromPhase',
|
|
474
|
+
'requestFingerprint',
|
|
475
|
+
'resultSha256',
|
|
476
|
+
'scheduledAt',
|
|
477
|
+
'state',
|
|
478
|
+
'updatedAt',
|
|
479
|
+
]);
|
|
480
|
+
const operationId = token(value, 'operationId');
|
|
481
|
+
if (recordId !== operationId)
|
|
482
|
+
throw invalidPayload('recordId');
|
|
483
|
+
const operationKind = literal(value, 'operationKind', [
|
|
484
|
+
'authority-transfer',
|
|
485
|
+
'backup',
|
|
486
|
+
'create-project',
|
|
487
|
+
'delete',
|
|
488
|
+
'export',
|
|
489
|
+
'join-project',
|
|
490
|
+
'leave',
|
|
491
|
+
'remove-member',
|
|
492
|
+
'retire',
|
|
493
|
+
]);
|
|
494
|
+
const direction = value.direction === null
|
|
495
|
+
? null
|
|
496
|
+
: literal(value, 'direction', ['cloud-to-lan', 'lan-to-cloud']);
|
|
497
|
+
if ((operationKind === 'authority-transfer') !== (direction !== null)) {
|
|
498
|
+
throw invalidPayload('direction');
|
|
499
|
+
}
|
|
500
|
+
const expectedPersonalRefOid = value.expectedPersonalRefOid === null
|
|
501
|
+
? null
|
|
502
|
+
: token(value, 'expectedPersonalRefOid', isCollabGitOid);
|
|
503
|
+
if ((operationKind === 'leave') !== (expectedPersonalRefOid !== null)) {
|
|
504
|
+
throw invalidPayload('expectedPersonalRefOid');
|
|
505
|
+
}
|
|
506
|
+
const batchRevision = nullablePositiveInteger(value, 'batchRevision');
|
|
507
|
+
const batchSha256 = nullableSha256(value, 'batchSha256');
|
|
508
|
+
const checkpointSha256 = nullableSha256(value, 'checkpointSha256');
|
|
509
|
+
if ((batchRevision === null) !== (batchSha256 === null)
|
|
510
|
+
|| (batchRevision !== null && checkpointSha256 === null))
|
|
511
|
+
throw invalidPayload('batchRevision');
|
|
512
|
+
const state = literal(value, 'state', [
|
|
513
|
+
'active', 'cancelled', 'completed', 'recovery-required',
|
|
514
|
+
]);
|
|
515
|
+
const recoveryFromPhase = value.recoveryFromPhase === null
|
|
516
|
+
? null
|
|
517
|
+
: boundedString(value, 'recoveryFromPhase', 64);
|
|
518
|
+
if ((state === 'recovery-required') !== (recoveryFromPhase !== null)
|
|
519
|
+
|| (recoveryFromPhase !== null && !PHASE_PATTERN.test(recoveryFromPhase)))
|
|
520
|
+
throw invalidPayload('recoveryFromPhase');
|
|
521
|
+
const phase = boundedString(value, 'phase', 64);
|
|
522
|
+
if (!PHASE_PATTERN.test(phase))
|
|
523
|
+
throw invalidPayload('phase');
|
|
524
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
525
|
+
const scheduledAt = timestamp(value, 'scheduledAt');
|
|
526
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
527
|
+
if (Date.parse(scheduledAt) < Date.parse(createdAt)
|
|
528
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt))
|
|
529
|
+
throw invalidPayload('updatedAt');
|
|
530
|
+
return {
|
|
531
|
+
kind: 'lifecycle-journal',
|
|
532
|
+
recordId,
|
|
533
|
+
revision,
|
|
534
|
+
value: {
|
|
535
|
+
actorMemberId: nullableToken(value, 'actorMemberId', isCollabMemberId),
|
|
536
|
+
batchRevision,
|
|
537
|
+
batchSha256,
|
|
538
|
+
checkpointSha256,
|
|
539
|
+
createdAt,
|
|
540
|
+
direction,
|
|
541
|
+
expectedAuthorityGeneration: positiveInteger(value, 'expectedAuthorityGeneration'),
|
|
542
|
+
expectedPersonalRefOid,
|
|
543
|
+
idempotencyKey: token(value, 'idempotencyKey'),
|
|
544
|
+
operationId,
|
|
545
|
+
operationKind,
|
|
546
|
+
phase,
|
|
547
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
548
|
+
recoveryFromPhase,
|
|
549
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
550
|
+
resultSha256: nullableSha256(value, 'resultSha256'),
|
|
551
|
+
scheduledAt,
|
|
552
|
+
state,
|
|
553
|
+
updatedAt,
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function nullableProof(source, field) {
|
|
558
|
+
return source[field] === null ? null : proofValue(source, field);
|
|
559
|
+
}
|
|
560
|
+
function authorityTransferRecoveryRecord(source, recordId, revision) {
|
|
561
|
+
const value = exactRecord(source.value, 'value', [
|
|
562
|
+
'cancellationRequestSha256',
|
|
563
|
+
'createdAt',
|
|
564
|
+
'expiresAt',
|
|
565
|
+
'inactivePublication',
|
|
566
|
+
'projectId',
|
|
567
|
+
'relinquishmentProof',
|
|
568
|
+
'sourceAuthority',
|
|
569
|
+
'sourceHostMemberId',
|
|
570
|
+
'sourceEvidence',
|
|
571
|
+
'sourceReopenSha256',
|
|
572
|
+
'stageSha256',
|
|
573
|
+
'targetActivationProof',
|
|
574
|
+
'targetActivationRequestSha256',
|
|
575
|
+
'targetAuthority',
|
|
576
|
+
'targetHostMemberId',
|
|
577
|
+
'targetEvidence',
|
|
578
|
+
'targetUrl',
|
|
579
|
+
'transferId',
|
|
580
|
+
'updatedAt',
|
|
581
|
+
]);
|
|
582
|
+
const transferId = token(value, 'transferId');
|
|
583
|
+
if (recordId !== transferId)
|
|
584
|
+
throw invalidPayload('recordId');
|
|
585
|
+
const projectId = token(value, 'projectId', isCollabProjectId);
|
|
586
|
+
const sourceAuthority = authority(value.sourceAuthority, 'sourceAuthority');
|
|
587
|
+
const targetAuthority = authority(value.targetAuthority, 'targetAuthority');
|
|
588
|
+
const sourceHostMemberId = nullableToken(value, 'sourceHostMemberId', isCollabMemberId);
|
|
589
|
+
const targetHostMemberId = nullableToken(value, 'targetHostMemberId', isCollabMemberId);
|
|
590
|
+
const inactivePublication = inactiveRepositoryPublication(value.inactivePublication);
|
|
591
|
+
const sourceEvidence = lanToCloudSourceEvidence(value.sourceEvidence);
|
|
592
|
+
const targetEvidence = cloudToLanTargetEvidence(value.targetEvidence);
|
|
593
|
+
if (sourceAuthority.kind === targetAuthority.kind
|
|
594
|
+
|| targetAuthority.generation !== sourceAuthority.generation + 1
|
|
595
|
+
|| (sourceAuthority.kind === 'lan'
|
|
596
|
+
? sourceHostMemberId === null || targetHostMemberId !== null
|
|
597
|
+
: sourceHostMemberId !== null || targetHostMemberId === null)
|
|
598
|
+
|| (sourceAuthority.kind === 'lan'
|
|
599
|
+
? targetEvidence !== null
|
|
600
|
+
: sourceEvidence !== null || inactivePublication !== null))
|
|
601
|
+
throw invalidPayload('sourceAuthority');
|
|
602
|
+
if (inactivePublication !== null && (inactivePublication.projectId !== projectId
|
|
603
|
+
|| inactivePublication.operationId !== transferId))
|
|
604
|
+
throw invalidPayload('inactivePublication');
|
|
605
|
+
const relinquishmentProof = value.relinquishmentProof === null
|
|
606
|
+
? null
|
|
607
|
+
: decodeCollabAuthorityRelinquishmentProof(value.relinquishmentProof);
|
|
608
|
+
if (relinquishmentProof !== null && (relinquishmentProof.projectId !== projectId
|
|
609
|
+
|| relinquishmentProof.transferId !== transferId
|
|
610
|
+
|| relinquishmentProof.sourceAuthority.kind !== sourceAuthority.kind
|
|
611
|
+
|| relinquishmentProof.sourceAuthority.generation !== sourceAuthority.generation
|
|
612
|
+
|| relinquishmentProof.sourceHostMemberId !== sourceHostMemberId
|
|
613
|
+
|| relinquishmentProof.targetAuthority.kind !== targetAuthority.kind
|
|
614
|
+
|| relinquishmentProof.targetAuthority.generation !== targetAuthority.generation))
|
|
615
|
+
throw invalidPayload('relinquishmentProof');
|
|
616
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
617
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
618
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
619
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt)
|
|
620
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt))
|
|
621
|
+
throw invalidPayload('updatedAt');
|
|
622
|
+
return {
|
|
623
|
+
kind: 'authority-transfer-recovery',
|
|
624
|
+
recordId,
|
|
625
|
+
revision,
|
|
626
|
+
value: {
|
|
627
|
+
cancellationRequestSha256: nullableSha256(value, 'cancellationRequestSha256'),
|
|
628
|
+
createdAt,
|
|
629
|
+
expiresAt,
|
|
630
|
+
inactivePublication,
|
|
631
|
+
projectId,
|
|
632
|
+
relinquishmentProof,
|
|
633
|
+
sourceAuthority,
|
|
634
|
+
sourceHostMemberId,
|
|
635
|
+
sourceEvidence,
|
|
636
|
+
sourceReopenSha256: nullableSha256(value, 'sourceReopenSha256'),
|
|
637
|
+
stageSha256: nullableSha256(value, 'stageSha256'),
|
|
638
|
+
targetActivationProof: nullableProof(value, 'targetActivationProof'),
|
|
639
|
+
targetActivationRequestSha256: nullableSha256(value, 'targetActivationRequestSha256'),
|
|
640
|
+
targetAuthority,
|
|
641
|
+
targetHostMemberId,
|
|
642
|
+
targetEvidence,
|
|
643
|
+
targetUrl: absoluteTargetUrl(value),
|
|
644
|
+
transferId,
|
|
645
|
+
updatedAt,
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function transferredMembershipClaimRecord(source, recordId, revision) {
|
|
650
|
+
const value = exactRecord(source.value, 'value', [
|
|
651
|
+
'batchRevision',
|
|
652
|
+
'checkpointSha256',
|
|
653
|
+
'claimSha256',
|
|
654
|
+
'createdAt',
|
|
655
|
+
'expiresAt',
|
|
656
|
+
'memberId',
|
|
657
|
+
'operationIntentId',
|
|
658
|
+
'projectId',
|
|
659
|
+
'redemptionReceiptId',
|
|
660
|
+
'state',
|
|
661
|
+
'targetPrincipalId',
|
|
662
|
+
'transferId',
|
|
663
|
+
'updatedAt',
|
|
664
|
+
]);
|
|
665
|
+
const transferId = token(value, 'transferId');
|
|
666
|
+
const memberId = token(value, 'memberId', isCollabMemberId);
|
|
667
|
+
if (recordId !== `${transferId}:${memberId}`)
|
|
668
|
+
throw invalidPayload('recordId');
|
|
669
|
+
const state = literal(value, 'state', ['redeemed', 'revoked', 'unclaimed']);
|
|
670
|
+
const targetPrincipalId = nullableToken(value, 'targetPrincipalId', value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
|
|
671
|
+
const operationIntentId = nullableToken(value, 'operationIntentId');
|
|
672
|
+
const redemptionReceiptId = nullableToken(value, 'redemptionReceiptId');
|
|
673
|
+
if ((state !== 'redeemed'
|
|
674
|
+
&& (targetPrincipalId !== null
|
|
675
|
+
|| operationIntentId !== null
|
|
676
|
+
|| redemptionReceiptId !== null))
|
|
677
|
+
|| (state === 'redeemed'
|
|
678
|
+
&& (targetPrincipalId === null
|
|
679
|
+
|| operationIntentId === null
|
|
680
|
+
|| redemptionReceiptId === null)))
|
|
681
|
+
throw invalidPayload('state');
|
|
682
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
683
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
684
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
685
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt)
|
|
686
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt))
|
|
687
|
+
throw invalidPayload('updatedAt');
|
|
688
|
+
return {
|
|
689
|
+
kind: 'transferred-membership-claim',
|
|
690
|
+
recordId,
|
|
691
|
+
revision,
|
|
692
|
+
value: {
|
|
693
|
+
batchRevision: positiveInteger(value, 'batchRevision'),
|
|
694
|
+
checkpointSha256: sha256(value, 'checkpointSha256'),
|
|
695
|
+
claimSha256: sha256(value, 'claimSha256'),
|
|
696
|
+
createdAt,
|
|
697
|
+
expiresAt,
|
|
698
|
+
memberId,
|
|
699
|
+
operationIntentId,
|
|
700
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
701
|
+
redemptionReceiptId,
|
|
702
|
+
state,
|
|
703
|
+
targetPrincipalId,
|
|
704
|
+
transferId,
|
|
705
|
+
updatedAt,
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function transferReceiptKeyRecord(source, recordId, revision) {
|
|
710
|
+
const value = exactRecord(source.value, 'value', [
|
|
711
|
+
'createdAt',
|
|
712
|
+
'projectId',
|
|
713
|
+
'receiptKeyId',
|
|
714
|
+
'receiptPublicKey',
|
|
715
|
+
'receiptPublicKeyEncoding',
|
|
716
|
+
'signatureAlgorithm',
|
|
717
|
+
'transferId',
|
|
718
|
+
]);
|
|
719
|
+
const transferId = token(value, 'transferId');
|
|
720
|
+
const receiptKeyId = token(value, 'receiptKeyId');
|
|
721
|
+
if (recordId !== `${transferId}:${receiptKeyId}`)
|
|
722
|
+
throw invalidPayload('recordId');
|
|
723
|
+
return {
|
|
724
|
+
kind: 'transfer-receipt-key',
|
|
725
|
+
recordId,
|
|
726
|
+
revision,
|
|
727
|
+
value: {
|
|
728
|
+
createdAt: timestamp(value, 'createdAt'),
|
|
729
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
730
|
+
receiptKeyId,
|
|
731
|
+
receiptPublicKey: canonicalBase64urlPublicKey(value, 'receiptPublicKey'),
|
|
732
|
+
receiptPublicKeyEncoding: literal(value, 'receiptPublicKeyEncoding', ['base64url-raw']),
|
|
733
|
+
signatureAlgorithm: literal(value, 'signatureAlgorithm', ['ed25519']),
|
|
734
|
+
transferId,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function transferClaimBatchReceiptRecord(source, recordId, revision) {
|
|
739
|
+
const value = exactRecord(source.value, 'value', ['receipt']);
|
|
740
|
+
const receipt = decodeCollabTransferredMembershipClaimCustodyReceipt(value.receipt);
|
|
741
|
+
if (recordId !== receipt.transferId)
|
|
742
|
+
throw invalidPayload('recordId');
|
|
743
|
+
return { kind: 'transfer-claim-batch-receipt', recordId, revision, value: { receipt } };
|
|
744
|
+
}
|
|
745
|
+
function transferRedemptionReceiptRecord(source, recordId, revision) {
|
|
746
|
+
const value = exactRecord(source.value, 'value', [
|
|
747
|
+
'acknowledgedAt',
|
|
748
|
+
'projectId',
|
|
749
|
+
'receipt',
|
|
750
|
+
]);
|
|
751
|
+
const receipt = decodeCollabTransferredMembershipRedemptionReceipt(value.receipt);
|
|
752
|
+
const projectId = token(value, 'projectId', isCollabProjectId);
|
|
753
|
+
if (projectId !== receipt.projectId
|
|
754
|
+
|| recordId !== `${receipt.transferId}:${receipt.memberId}`)
|
|
755
|
+
throw invalidPayload('recordId');
|
|
756
|
+
const acknowledgedAt = nullableTimestamp(value, 'acknowledgedAt');
|
|
757
|
+
if (acknowledgedAt !== null
|
|
758
|
+
&& Date.parse(acknowledgedAt) < Date.parse(receipt.redeemedAt))
|
|
759
|
+
throw invalidPayload('acknowledgedAt');
|
|
760
|
+
return {
|
|
761
|
+
kind: 'transfer-redemption-receipt',
|
|
762
|
+
recordId,
|
|
763
|
+
revision,
|
|
764
|
+
value: { acknowledgedAt, projectId, receipt },
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function terminalPrincipalRecord(source, recordId, revision) {
|
|
768
|
+
const value = exactRecord(source.value, 'value', [
|
|
769
|
+
'acknowledgedAt',
|
|
770
|
+
'memberId',
|
|
771
|
+
'operationId',
|
|
772
|
+
'operationKind',
|
|
773
|
+
'principalId',
|
|
774
|
+
'projectId',
|
|
775
|
+
]);
|
|
776
|
+
const operationId = token(value, 'operationId');
|
|
777
|
+
const memberId = token(value, 'memberId', isCollabMemberId);
|
|
778
|
+
if (recordId !== `${operationId}:${memberId}`)
|
|
779
|
+
throw invalidPayload('recordId');
|
|
780
|
+
return {
|
|
781
|
+
kind: 'terminal-principal',
|
|
782
|
+
recordId,
|
|
783
|
+
revision,
|
|
784
|
+
value: {
|
|
785
|
+
acknowledgedAt: nullableTimestamp(value, 'acknowledgedAt'),
|
|
786
|
+
memberId,
|
|
787
|
+
operationId,
|
|
788
|
+
operationKind: literal(value, 'operationKind', ['authority-transfer', 'retire']),
|
|
789
|
+
principalId: principal(value, 'principalId'),
|
|
790
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
791
|
+
},
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function terminalResponderReplayRecord(source, recordId, revision) {
|
|
795
|
+
const value = exactRecord(source.value, 'value', [
|
|
796
|
+
'memberId', 'operationId', 'projectId', 'requestSha256',
|
|
797
|
+
]);
|
|
798
|
+
const operationId = token(value, 'operationId');
|
|
799
|
+
if (recordId !== operationId)
|
|
800
|
+
throw invalidPayload('recordId');
|
|
801
|
+
return {
|
|
802
|
+
kind: 'terminal-responder-replay',
|
|
803
|
+
recordId,
|
|
804
|
+
revision,
|
|
805
|
+
value: {
|
|
806
|
+
memberId: token(value, 'memberId', isCollabMemberId),
|
|
807
|
+
operationId,
|
|
808
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
809
|
+
requestSha256: sha256(value, 'requestSha256'),
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function leaveFormerPrincipalReplayRecord(source, recordId, revision) {
|
|
814
|
+
const value = exactRecord(source.value, 'value', [
|
|
815
|
+
'completedAt',
|
|
816
|
+
'createdAt',
|
|
817
|
+
'expectedPersonalRefOid',
|
|
818
|
+
'expiresAt',
|
|
819
|
+
'intentId',
|
|
820
|
+
'memberId',
|
|
821
|
+
'operationId',
|
|
822
|
+
'principalSha256',
|
|
823
|
+
'projectId',
|
|
824
|
+
'requestFingerprint',
|
|
825
|
+
'resultSha256',
|
|
826
|
+
'state',
|
|
827
|
+
]);
|
|
828
|
+
const operationId = token(value, 'operationId');
|
|
829
|
+
if (recordId !== operationId)
|
|
830
|
+
throw invalidPayload('recordId');
|
|
831
|
+
const state = literal(value, 'state', ['completed', 'recovering']);
|
|
832
|
+
const completedAt = nullableTimestamp(value, 'completedAt');
|
|
833
|
+
const resultSha256 = nullableSha256(value, 'resultSha256');
|
|
834
|
+
if ((state === 'completed' && (completedAt === null || resultSha256 === null))
|
|
835
|
+
|| (state === 'recovering' && (completedAt !== null || resultSha256 !== null))) {
|
|
836
|
+
throw invalidPayload('state');
|
|
837
|
+
}
|
|
838
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
839
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
840
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt)
|
|
841
|
+
|| (completedAt !== null && Date.parse(completedAt) < Date.parse(createdAt)))
|
|
842
|
+
throw invalidPayload('completedAt');
|
|
843
|
+
return {
|
|
844
|
+
kind: 'leave-former-principal-replay',
|
|
845
|
+
recordId,
|
|
846
|
+
revision,
|
|
847
|
+
value: {
|
|
848
|
+
completedAt,
|
|
849
|
+
createdAt,
|
|
850
|
+
expectedPersonalRefOid: token(value, 'expectedPersonalRefOid', isCollabGitOid),
|
|
851
|
+
expiresAt,
|
|
852
|
+
intentId: token(value, 'intentId'),
|
|
853
|
+
memberId: token(value, 'memberId', isCollabMemberId),
|
|
854
|
+
operationId,
|
|
855
|
+
principalSha256: sha256(value, 'principalSha256'),
|
|
856
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
857
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
858
|
+
resultSha256,
|
|
859
|
+
state,
|
|
860
|
+
},
|
|
861
|
+
};
|
|
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
|
+
}
|
|
1207
|
+
function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
1208
|
+
switch (kind) {
|
|
1209
|
+
case 'lifecycle-journal': return lifecycleJournalRecord(source, recordId, revision);
|
|
1210
|
+
case 'authority-transfer-recovery':
|
|
1211
|
+
return authorityTransferRecoveryRecord(source, recordId, revision);
|
|
1212
|
+
case 'transferred-membership-claim':
|
|
1213
|
+
return transferredMembershipClaimRecord(source, recordId, revision);
|
|
1214
|
+
case 'transfer-receipt-key': return transferReceiptKeyRecord(source, recordId, revision);
|
|
1215
|
+
case 'transfer-claim-batch-receipt':
|
|
1216
|
+
return transferClaimBatchReceiptRecord(source, recordId, revision);
|
|
1217
|
+
case 'transfer-redemption-receipt':
|
|
1218
|
+
return transferRedemptionReceiptRecord(source, recordId, revision);
|
|
1219
|
+
case 'terminal-principal': return terminalPrincipalRecord(source, recordId, revision);
|
|
1220
|
+
case 'terminal-responder-replay':
|
|
1221
|
+
return terminalResponderReplayRecord(source, recordId, revision);
|
|
1222
|
+
case 'leave-former-principal-replay':
|
|
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);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function kindOrder(kind) {
|
|
1240
|
+
return COLLAB_PROJECT_BACKUP_RECORD_KINDS.indexOf(kind);
|
|
1241
|
+
}
|
|
1242
|
+
function compareRecords(left, right) {
|
|
1243
|
+
const difference = kindOrder(left.kind) - kindOrder(right.kind);
|
|
1244
|
+
return difference === 0
|
|
1245
|
+
? left.recordId.localeCompare(right.recordId, 'en-US')
|
|
1246
|
+
: difference;
|
|
1247
|
+
}
|
|
1248
|
+
function recordProjectId(item) {
|
|
1249
|
+
if (item.kind === 'cloud-event')
|
|
1250
|
+
return item.value.event.projectId;
|
|
1251
|
+
if (item.kind === 'protected-claim-envelope')
|
|
1252
|
+
return item.value.associatedData.projectId;
|
|
1253
|
+
if (item.kind === 'transfer-claim-batch-receipt')
|
|
1254
|
+
return item.value.receipt.projectId;
|
|
1255
|
+
return item.value.projectId;
|
|
1256
|
+
}
|
|
1257
|
+
function lifecycleJournalHasInvalidSemantics(item) {
|
|
1258
|
+
const value = item.value;
|
|
1259
|
+
const effectivePhase = value.state === 'recovery-required'
|
|
1260
|
+
? value.recoveryFromPhase
|
|
1261
|
+
: value.phase;
|
|
1262
|
+
if (effectivePhase === null)
|
|
1263
|
+
return true;
|
|
1264
|
+
const hasResult = value.resultSha256 !== null;
|
|
1265
|
+
const hasCheckpoint = value.checkpointSha256 !== null;
|
|
1266
|
+
const hasTransferBatch = value.batchRevision !== null || value.batchSha256 !== null;
|
|
1267
|
+
if (value.operationKind === 'authority-transfer') {
|
|
1268
|
+
return value.state === 'recovery-required'
|
|
1269
|
+
|| hasResult !== (value.state === 'completed');
|
|
1270
|
+
}
|
|
1271
|
+
if (hasTransferBatch)
|
|
1272
|
+
return true;
|
|
1273
|
+
if (value.operationKind === 'backup' || value.operationKind === 'export') {
|
|
1274
|
+
const statePhaseInvalid = value.state === 'active'
|
|
1275
|
+
? !BACKUP_EXPORT_ACTIVE_PHASE_SET.has(value.phase)
|
|
1276
|
+
: value.state === 'recovery-required'
|
|
1277
|
+
? value.phase !== value.recoveryFromPhase
|
|
1278
|
+
|| !BACKUP_EXPORT_ACTIVE_PHASE_SET.has(effectivePhase)
|
|
1279
|
+
: value.state === 'cancelled'
|
|
1280
|
+
? value.phase !== 'cancelled'
|
|
1281
|
+
: value.phase !== 'completed';
|
|
1282
|
+
const checkpointRequired = BACKUP_EXPORT_CHECKPOINT_REQUIRED_PHASE_SET.has(effectivePhase);
|
|
1283
|
+
const checkpointOptional = effectivePhase === 'cancel-intent'
|
|
1284
|
+
|| effectivePhase === 'cancelled';
|
|
1285
|
+
const resultRequired = effectivePhase === 'artifact-published'
|
|
1286
|
+
|| value.state === 'completed';
|
|
1287
|
+
return statePhaseInvalid
|
|
1288
|
+
|| (!checkpointOptional && hasCheckpoint !== checkpointRequired)
|
|
1289
|
+
|| hasResult !== resultRequired;
|
|
1290
|
+
}
|
|
1291
|
+
if (hasCheckpoint)
|
|
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
|
+
}
|
|
1320
|
+
if (value.operationKind === 'leave') {
|
|
1321
|
+
const statePhaseValid = (value.state === 'active' && (value.phase === 'prepared'
|
|
1322
|
+
|| value.phase === 'membership-left'
|
|
1323
|
+
|| value.phase === 'personal-ref-removed')) || (value.state === 'recovery-required'
|
|
1324
|
+
&& value.phase === 'recovery-required'
|
|
1325
|
+
&& value.recoveryFromPhase === 'membership-left') || (value.state === 'cancelled'
|
|
1326
|
+
&& value.phase === 'manager-succession-required') || (value.state === 'completed' && value.phase === 'completed');
|
|
1327
|
+
return !statePhaseValid || hasResult !== (value.state === 'completed');
|
|
1328
|
+
}
|
|
1329
|
+
if (value.operationKind === 'retire') {
|
|
1330
|
+
return value.state !== 'completed' || value.phase !== 'completed' || !hasResult;
|
|
1331
|
+
}
|
|
1332
|
+
const deleteStatePhaseValid = (value.state === 'active'
|
|
1333
|
+
&& DELETE_ACTIVE_PHASE_SET.has(value.phase))
|
|
1334
|
+
|| (value.state === 'completed' && value.phase === 'completed');
|
|
1335
|
+
return !deleteStatePhaseValid || hasResult !== (value.state === 'completed');
|
|
1336
|
+
}
|
|
1337
|
+
function validateContinuity(records) {
|
|
1338
|
+
if (records.length === 0
|
|
1339
|
+
|| records[0].kind !== 'project'
|
|
1340
|
+
|| records.some((item, index) => index > 0 && compareRecords(records[index - 1], item) >= 0))
|
|
1341
|
+
throw invalidPayload('records');
|
|
1342
|
+
const projectId = records[0].value.projectId;
|
|
1343
|
+
const projectAuthorityGeneration = records[0].value.authorityGeneration;
|
|
1344
|
+
if (records.some(item => recordProjectId(item) !== projectId))
|
|
1345
|
+
throw invalidPayload('records');
|
|
1346
|
+
const memberRecords = new Map(records
|
|
1347
|
+
.filter(item => item.kind === 'member')
|
|
1348
|
+
.map(item => [item.value.memberId, item]));
|
|
1349
|
+
const members = new Set(memberRecords.keys());
|
|
1350
|
+
const principalBindingRecords = records.filter(item => item.kind === 'principal-binding');
|
|
1351
|
+
const principalBindings = new Map(principalBindingRecords
|
|
1352
|
+
.map(item => [item.value.memberId, item.value.principalId]));
|
|
1353
|
+
const lifecycles = new Map(records
|
|
1354
|
+
.filter((item) => (item.kind === 'lifecycle-journal'))
|
|
1355
|
+
.map(item => [item.value.operationId, item]));
|
|
1356
|
+
const recoveries = new Map(records
|
|
1357
|
+
.filter((item) => (item.kind === 'authority-transfer-recovery'))
|
|
1358
|
+
.map(item => [item.value.transferId, item]));
|
|
1359
|
+
const keys = new Map(records
|
|
1360
|
+
.filter((item) => (item.kind === 'transfer-receipt-key'))
|
|
1361
|
+
.map(item => [`${item.value.transferId}:${item.value.receiptKeyId}`, item]));
|
|
1362
|
+
const claimRecords = records
|
|
1363
|
+
.filter((item) => (item.kind === 'transferred-membership-claim'));
|
|
1364
|
+
const claims = new Map(claimRecords
|
|
1365
|
+
.map(item => [`${item.value.transferId}:${item.value.memberId}`, item]));
|
|
1366
|
+
const redemptionReceiptRecords = records
|
|
1367
|
+
.filter((item) => (item.kind === 'transfer-redemption-receipt'));
|
|
1368
|
+
const redemptionReceipts = new Map(redemptionReceiptRecords
|
|
1369
|
+
.map(item => [`${item.value.receipt.transferId}:${item.value.receipt.memberId}`, item]));
|
|
1370
|
+
const protectedEnvelopes = new Map(records
|
|
1371
|
+
.filter(item => item.kind === 'protected-claim-envelope')
|
|
1372
|
+
.map(item => [`${item.value.transferId}:${item.value.memberId}`, item]));
|
|
1373
|
+
const batchReceipts = new Map(records
|
|
1374
|
+
.filter((item) => (item.kind === 'transfer-claim-batch-receipt'))
|
|
1375
|
+
.map(item => [item.value.receipt.transferId, item]));
|
|
1376
|
+
const terminalResponders = new Map(records
|
|
1377
|
+
.filter((item) => (item.kind === 'terminal-responder'))
|
|
1378
|
+
.map(item => [item.value.operationId, item]));
|
|
1379
|
+
const terminalPrincipals = new Map(records
|
|
1380
|
+
.filter((item) => (item.kind === 'terminal-principal'))
|
|
1381
|
+
.map(item => [`${item.value.operationId}:${item.value.memberId}`, item]));
|
|
1382
|
+
const responderReplays = new Map(records
|
|
1383
|
+
.filter((item) => (item.kind === 'terminal-responder-replay'))
|
|
1384
|
+
.map(item => [item.value.operationId, item]));
|
|
1385
|
+
const leaveReplays = new Map(records
|
|
1386
|
+
.filter((item) => (item.kind === 'leave-former-principal-replay'))
|
|
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'));
|
|
1406
|
+
const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
|
|
1407
|
+
if (nonterminalLifecycles.length > 1
|
|
1408
|
+
|| principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
|
|
1409
|
+
|| new Set(principalBindingRecords.map(item => item.value.principalId)).size
|
|
1410
|
+
!== principalBindingRecords.length
|
|
1411
|
+
|| new Set(claimRecords.map(item => (`${item.value.transferId}:${item.value.claimSha256}`))).size !== claimRecords.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)
|
|
1423
|
+
throw invalidPayload('records');
|
|
1424
|
+
for (const lifecycle of lifecycles.values()) {
|
|
1425
|
+
if (lifecycleJournalHasInvalidSemantics(lifecycle)
|
|
1426
|
+
|| (lifecycle.value.actorMemberId !== null
|
|
1427
|
+
&& !members.has(lifecycle.value.actorMemberId))) {
|
|
1428
|
+
throw invalidPayload('records');
|
|
1429
|
+
}
|
|
1430
|
+
if (lifecycle.value.operationKind === 'authority-transfer'
|
|
1431
|
+
&& !recoveries.has(lifecycle.value.operationId))
|
|
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');
|
|
1438
|
+
if (lifecycle.value.operationKind === 'leave') {
|
|
1439
|
+
const actorMemberId = lifecycle.value.actorMemberId;
|
|
1440
|
+
const actor = actorMemberId === null ? undefined : memberRecords.get(actorMemberId);
|
|
1441
|
+
const replay = leaveReplays.get(lifecycle.value.operationId);
|
|
1442
|
+
const effectivePhase = lifecycle.value.state === 'recovery-required'
|
|
1443
|
+
? lifecycle.value.recoveryFromPhase
|
|
1444
|
+
: lifecycle.value.phase;
|
|
1445
|
+
const afterMembershipSettlement = effectivePhase === 'membership-left'
|
|
1446
|
+
|| effectivePhase === 'personal-ref-removed'
|
|
1447
|
+
|| effectivePhase === 'completed';
|
|
1448
|
+
if (actor === undefined
|
|
1449
|
+
|| (lifecycle.value.state === 'completed') !== (lifecycle.value.phase === 'completed')
|
|
1450
|
+
|| (afterMembershipSettlement && (actor.value.status !== 'left'
|
|
1451
|
+
|| principalBindings.has(actor.value.memberId)
|
|
1452
|
+
|| replay === undefined))
|
|
1453
|
+
|| (!afterMembershipSettlement && (actor.value.status !== 'active'
|
|
1454
|
+
|| !principalBindings.has(actor.value.memberId)
|
|
1455
|
+
|| replay !== undefined)))
|
|
1456
|
+
throw invalidPayload('records');
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
for (const recovery of recoveries.values()) {
|
|
1460
|
+
const lifecycle = lifecycles.get(recovery.value.transferId);
|
|
1461
|
+
if (lifecycle === undefined)
|
|
1462
|
+
throw invalidPayload('records');
|
|
1463
|
+
const proof = recovery.value.relinquishmentProof;
|
|
1464
|
+
const batchReceipt = batchReceipts.get(recovery.value.transferId)?.value.receipt;
|
|
1465
|
+
const evidence = recovery.value.sourceEvidence ?? recovery.value.targetEvidence;
|
|
1466
|
+
const receiptKey = evidence === null
|
|
1467
|
+
? undefined
|
|
1468
|
+
: keys.get(`${recovery.value.transferId}:${evidence.receiptKeyId}`);
|
|
1469
|
+
const effectivePhase = lifecycle.value.state === 'recovery-required'
|
|
1470
|
+
? lifecycle.value.recoveryFromPhase
|
|
1471
|
+
: lifecycle.value.phase;
|
|
1472
|
+
let relinquishmentRequired;
|
|
1473
|
+
try {
|
|
1474
|
+
if (lifecycle.value.direction !== null && effectivePhase !== null) {
|
|
1475
|
+
relinquishmentRequired = decodeCollabAuthorityTransferLifecycleFence({
|
|
1476
|
+
batchRevision: lifecycle.value.batchRevision,
|
|
1477
|
+
batchSha256: lifecycle.value.batchSha256,
|
|
1478
|
+
checkpointSha256: lifecycle.value.checkpointSha256,
|
|
1479
|
+
direction: lifecycle.value.direction,
|
|
1480
|
+
phase: effectivePhase,
|
|
1481
|
+
}).relinquishmentRequired;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
catch {
|
|
1485
|
+
throw invalidPayload('records');
|
|
1486
|
+
}
|
|
1487
|
+
const direction = lifecycle.value.direction;
|
|
1488
|
+
const cancellationIndex = effectivePhase === null
|
|
1489
|
+
? undefined
|
|
1490
|
+
: CANCELLATION_PHASE_INDEX.get(effectivePhase);
|
|
1491
|
+
const isCancellation = cancellationIndex !== undefined;
|
|
1492
|
+
const cleanupCompleted = isCancellation && cancellationIndex >= 2;
|
|
1493
|
+
const normalPhaseIndex = direction === 'cloud-to-lan'
|
|
1494
|
+
? COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.indexOf(effectivePhase)
|
|
1495
|
+
: COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES.indexOf(effectivePhase);
|
|
1496
|
+
const stageRequired = !isCancellation && normalPhaseIndex >= 3;
|
|
1497
|
+
const targetEvidenceRequired = direction === 'cloud-to-lan'
|
|
1498
|
+
&& !isCancellation
|
|
1499
|
+
&& normalPhaseIndex >= 1;
|
|
1500
|
+
const activationEvidenceRequired = direction === 'cloud-to-lan'
|
|
1501
|
+
&& !isCancellation
|
|
1502
|
+
&& normalPhaseIndex >= 6;
|
|
1503
|
+
const publicationRequired = direction === 'lan-to-cloud'
|
|
1504
|
+
&& !isCancellation
|
|
1505
|
+
&& normalPhaseIndex >= 5;
|
|
1506
|
+
const publicationForbidden = direction === 'cloud-to-lan'
|
|
1507
|
+
|| (!isCancellation && normalPhaseIndex < 4);
|
|
1508
|
+
const sourceReopenRequired = isCancellation && (direction === 'cloud-to-lan'
|
|
1509
|
+
? cancellationIndex >= 1
|
|
1510
|
+
: cancellationIndex >= 3);
|
|
1511
|
+
const recoveryEvidenceInvalid = direction === null
|
|
1512
|
+
|| (direction === 'lan-to-cloud' && recovery.value.sourceEvidence === null)
|
|
1513
|
+
|| (targetEvidenceRequired && recovery.value.targetEvidence === null)
|
|
1514
|
+
|| (!isCancellation && (stageRequired !== (recovery.value.stageSha256 !== null)))
|
|
1515
|
+
|| (direction === 'lan-to-cloud'
|
|
1516
|
+
&& recovery.value.stageSha256 !== null
|
|
1517
|
+
&& recovery.value.stageSha256 !== lifecycle.value.checkpointSha256)
|
|
1518
|
+
|| activationEvidenceRequired !== (recovery.value.targetActivationProof !== null
|
|
1519
|
+
&& recovery.value.targetActivationRequestSha256 !== null)
|
|
1520
|
+
|| ((recovery.value.targetActivationProof === null)
|
|
1521
|
+
!== (recovery.value.targetActivationRequestSha256 === null))
|
|
1522
|
+
|| (publicationRequired && recovery.value.inactivePublication === null)
|
|
1523
|
+
|| (publicationForbidden && recovery.value.inactivePublication !== null)
|
|
1524
|
+
|| isCancellation !== (recovery.value.cancellationRequestSha256 !== null)
|
|
1525
|
+
|| sourceReopenRequired !== (recovery.value.sourceReopenSha256 !== null);
|
|
1526
|
+
const expectedProjectAuthorityGeneration = relinquishmentRequired === true
|
|
1527
|
+
? recovery.value.targetAuthority.generation
|
|
1528
|
+
: recovery.value.sourceAuthority.generation;
|
|
1529
|
+
if (recoveryEvidenceInvalid)
|
|
1530
|
+
throw invalidPayload('records');
|
|
1531
|
+
if (cleanupCompleted && ((direction === 'cloud-to-lan' && [...protectedEnvelopes.values()].some(item => (item.value.transferId === recovery.value.transferId)))
|
|
1532
|
+
|| (direction === 'lan-to-cloud' && claimRecords.some(item => (item.value.transferId === recovery.value.transferId)))))
|
|
1533
|
+
throw invalidPayload('records');
|
|
1534
|
+
if (projectAuthorityGeneration !== expectedProjectAuthorityGeneration) {
|
|
1535
|
+
throw invalidPayload('records');
|
|
1536
|
+
}
|
|
1537
|
+
if (!isCancellation) {
|
|
1538
|
+
const hostMemberId = direction === 'lan-to-cloud'
|
|
1539
|
+
? recovery.value.sourceHostMemberId
|
|
1540
|
+
: recovery.value.targetHostMemberId;
|
|
1541
|
+
const eligibleMemberIds = [...memberRecords.values()]
|
|
1542
|
+
.filter(item => item.value.status === 'active' && item.value.memberId !== hostMemberId)
|
|
1543
|
+
.map(item => item.value.memberId);
|
|
1544
|
+
const memberCustodyRequired = normalPhaseIndex >= 3;
|
|
1545
|
+
const batchReceiptRequired = direction === 'cloud-to-lan'
|
|
1546
|
+
? normalPhaseIndex >= 3
|
|
1547
|
+
: normalPhaseIndex >= 4;
|
|
1548
|
+
if ((batchReceipt !== undefined) !== batchReceiptRequired) {
|
|
1549
|
+
throw invalidPayload('records');
|
|
1550
|
+
}
|
|
1551
|
+
for (const memberId of eligibleMemberIds) {
|
|
1552
|
+
const identity = `${recovery.value.transferId}:${memberId}`;
|
|
1553
|
+
const claimPresent = claims.has(identity);
|
|
1554
|
+
const envelopePresent = protectedEnvelopes.has(identity);
|
|
1555
|
+
const redemptionPresent = redemptionReceipts.has(identity);
|
|
1556
|
+
const exactMemberCustody = direction === 'lan-to-cloud'
|
|
1557
|
+
? claimPresent && !envelopePresent
|
|
1558
|
+
: !claimPresent && envelopePresent !== redemptionPresent;
|
|
1559
|
+
if (memberCustodyRequired !== exactMemberCustody) {
|
|
1560
|
+
throw invalidPayload('records');
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (recovery.value.targetEvidence !== null && (recovery.value.targetHostMemberId === null
|
|
1565
|
+
|| principalBindings.get(recovery.value.targetHostMemberId)
|
|
1566
|
+
!== recovery.value.targetEvidence.principalId))
|
|
1567
|
+
throw invalidPayload('records');
|
|
1568
|
+
if (lifecycle.value.operationKind !== 'authority-transfer'
|
|
1569
|
+
|| relinquishmentRequired === undefined
|
|
1570
|
+
|| relinquishmentRequired !== (proof !== null)
|
|
1571
|
+
|| (lifecycle.value.state === 'cancelled') !== (lifecycle.value.phase === 'cancelled')
|
|
1572
|
+
|| (lifecycle.value.state === 'completed') !== (lifecycle.value.phase === 'completed')
|
|
1573
|
+
|| (lifecycle.value.state === 'active'
|
|
1574
|
+
&& (lifecycle.value.phase === 'cancelled' || lifecycle.value.phase === 'completed'))
|
|
1575
|
+
|| lifecycle.value.direction !== (recovery.value.sourceAuthority.kind === 'cloud'
|
|
1576
|
+
? 'cloud-to-lan'
|
|
1577
|
+
: 'lan-to-cloud')
|
|
1578
|
+
|| lifecycle.value.expectedAuthorityGeneration !== recovery.value.sourceAuthority.generation
|
|
1579
|
+
|| lifecycle.value.scheduledAt !== recovery.value.expiresAt
|
|
1580
|
+
|| (recovery.value.sourceHostMemberId !== null
|
|
1581
|
+
&& !members.has(recovery.value.sourceHostMemberId))
|
|
1582
|
+
|| (recovery.value.targetHostMemberId !== null
|
|
1583
|
+
&& !members.has(recovery.value.targetHostMemberId))
|
|
1584
|
+
|| (proof !== null && (proof.batchRevision !== lifecycle.value.batchRevision
|
|
1585
|
+
|| proof.batchSha256 !== lifecycle.value.batchSha256
|
|
1586
|
+
|| proof.checkpointSha256 !== lifecycle.value.checkpointSha256
|
|
1587
|
+
|| (batchReceipt !== undefined && (proof.batchRevision !== batchReceipt.batchRevision
|
|
1588
|
+
|| proof.batchSha256 !== batchReceipt.batchSha256
|
|
1589
|
+
|| proof.checkpointSha256 !== batchReceipt.checkpointSha256))))
|
|
1590
|
+
|| (recovery.value.sourceEvidence !== null
|
|
1591
|
+
&& (recovery.value.sourceEvidence.checkpointManifestSha256
|
|
1592
|
+
!== lifecycle.value.checkpointSha256
|
|
1593
|
+
|| recovery.value.sourceHostMemberId === null
|
|
1594
|
+
|| principalBindings.get(recovery.value.sourceHostMemberId)
|
|
1595
|
+
!== recovery.value.sourceEvidence.principalId))
|
|
1596
|
+
|| (evidence !== null && (receiptKey === undefined
|
|
1597
|
+
|| receiptKey.value.receiptPublicKey !== evidence.receiptPublicKey)))
|
|
1598
|
+
throw invalidPayload('records');
|
|
1599
|
+
}
|
|
1600
|
+
for (const key of keys.values()) {
|
|
1601
|
+
const recovery = recoveries.get(key.value.transferId);
|
|
1602
|
+
const evidence = recovery?.value.sourceEvidence ?? recovery?.value.targetEvidence;
|
|
1603
|
+
if (recovery === undefined
|
|
1604
|
+
|| evidence === null
|
|
1605
|
+
|| evidence === undefined
|
|
1606
|
+
|| evidence.receiptKeyId !== key.value.receiptKeyId
|
|
1607
|
+
|| evidence.receiptPublicKey !== key.value.receiptPublicKey)
|
|
1608
|
+
throw invalidPayload('records');
|
|
1609
|
+
}
|
|
1610
|
+
for (const claim of claims.values()) {
|
|
1611
|
+
const lifecycle = lifecycles.get(claim.value.transferId);
|
|
1612
|
+
const recovery = recoveries.get(claim.value.transferId);
|
|
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));
|
|
1621
|
+
if (memberRecords.get(claim.value.memberId)?.value.status !== 'active'
|
|
1622
|
+
|| recovery === undefined
|
|
1623
|
+
|| recovery.value.sourceAuthority.kind !== 'lan'
|
|
1624
|
+
|| claim.value.memberId === recovery.value.sourceHostMemberId
|
|
1625
|
+
|| lifecycle?.value.batchRevision !== claim.value.batchRevision
|
|
1626
|
+
|| lifecycle.value.checkpointSha256 !== claim.value.checkpointSha256
|
|
1627
|
+
|| claim.value.expiresAt !== recovery.value.expiresAt
|
|
1628
|
+
|| (claim.value.targetPrincipalId !== null
|
|
1629
|
+
&& principalBindings.get(claim.value.memberId) !== claim.value.targetPrincipalId)
|
|
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)) {
|
|
1635
|
+
throw invalidPayload('records');
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
for (const terminal of terminalResponders.values()) {
|
|
1639
|
+
const operationKind = terminal.value.operation === 'getProjectAuthorityTransfer'
|
|
1640
|
+
? 'authority-transfer'
|
|
1641
|
+
: 'retire';
|
|
1642
|
+
const principals = terminal.value.eligibleMemberIds.map(memberId => (terminalPrincipals.get(`${terminal.value.operationId}:${memberId}`)));
|
|
1643
|
+
if (principals.some(item => item === undefined)
|
|
1644
|
+
|| principals.some(item => item?.value.operationKind !== operationKind)
|
|
1645
|
+
|| new Set(principals.map(item => item?.value.principalId)).size !== principals.length
|
|
1646
|
+
|| (operationKind === 'authority-transfer')
|
|
1647
|
+
!== responderReplays.has(terminal.value.operationId))
|
|
1648
|
+
throw invalidPayload('records');
|
|
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
|
+
}
|
|
1785
|
+
for (const item of records) {
|
|
1786
|
+
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1787
|
+
throw invalidPayload('records');
|
|
1788
|
+
}
|
|
1789
|
+
if (item.kind === 'protected-claim-envelope') {
|
|
1790
|
+
const identity = `${item.value.transferId}:${item.value.memberId}`;
|
|
1791
|
+
const recovery = recoveries.get(item.value.transferId);
|
|
1792
|
+
const lifecycle = lifecycles.get(item.value.transferId);
|
|
1793
|
+
if (item.recordId !== identity
|
|
1794
|
+
|| recovery === undefined
|
|
1795
|
+
|| lifecycle === undefined
|
|
1796
|
+
|| memberRecords.get(item.value.memberId)?.value.status !== 'active'
|
|
1797
|
+
|| item.value.memberId === recovery.value.targetHostMemberId
|
|
1798
|
+
|| item.value.associatedData.authorityGeneration
|
|
1799
|
+
!== recovery.value.sourceAuthority.generation
|
|
1800
|
+
|| recovery.value.sourceAuthority.kind !== 'cloud'
|
|
1801
|
+
|| item.value.associatedData.checkpointSha256 !== lifecycle.value.checkpointSha256
|
|
1802
|
+
|| item.value.expiresAt !== recovery.value.expiresAt
|
|
1803
|
+
|| !keys.has(`${item.value.transferId}:${item.value.receiptKeyId}`)
|
|
1804
|
+
|| claims.has(identity)
|
|
1805
|
+
|| redemptionReceipts.has(identity))
|
|
1806
|
+
throw invalidPayload('records');
|
|
1807
|
+
}
|
|
1808
|
+
if (item.kind === 'transfer-claim-batch-receipt') {
|
|
1809
|
+
const lifecycle = lifecycles.get(item.value.receipt.transferId);
|
|
1810
|
+
const recovery = recoveries.get(item.value.receipt.transferId);
|
|
1811
|
+
if (lifecycle === undefined
|
|
1812
|
+
|| recovery === undefined
|
|
1813
|
+
|| lifecycle.value.batchRevision !== item.value.receipt.batchRevision
|
|
1814
|
+
|| lifecycle.value.batchSha256 !== item.value.receipt.batchSha256
|
|
1815
|
+
|| lifecycle.value.checkpointSha256 !== item.value.receipt.checkpointSha256
|
|
1816
|
+
|| item.value.receipt.custodyAuthority.kind
|
|
1817
|
+
!== recovery.value.sourceAuthority.kind
|
|
1818
|
+
|| item.value.receipt.custodyAuthority.generation
|
|
1819
|
+
!== recovery.value.sourceAuthority.generation
|
|
1820
|
+
|| item.value.receipt.targetAuthorityGeneration
|
|
1821
|
+
!== recovery.value.targetAuthority.generation
|
|
1822
|
+
|| item.value.receipt.submittedByMemberId !== (recovery.value.sourceAuthority.kind === 'lan'
|
|
1823
|
+
? recovery.value.sourceHostMemberId
|
|
1824
|
+
: recovery.value.targetHostMemberId)
|
|
1825
|
+
|| !members.has(item.value.receipt.submittedByMemberId))
|
|
1826
|
+
throw invalidPayload('records');
|
|
1827
|
+
}
|
|
1828
|
+
if (item.kind === 'transfer-redemption-receipt') {
|
|
1829
|
+
const receipt = item.value.receipt;
|
|
1830
|
+
const identity = `${receipt.transferId}:${receipt.memberId}`;
|
|
1831
|
+
const claim = claims.get(identity);
|
|
1832
|
+
const recovery = recoveries.get(receipt.transferId);
|
|
1833
|
+
const lifecycle = lifecycles.get(receipt.transferId);
|
|
1834
|
+
const terminalPrincipal = terminalPrincipals.get(identity);
|
|
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;
|
|
1849
|
+
if (recovery === undefined
|
|
1850
|
+
|| lifecycle === undefined
|
|
1851
|
+
|| memberRecords.get(receipt.memberId)?.value.status !== 'active'
|
|
1852
|
+
|| receipt.targetAuthorityGeneration !== recovery.value.targetAuthority.generation
|
|
1853
|
+
|| !keys.has(`${receipt.transferId}:${receipt.receiptKeyId}`)
|
|
1854
|
+
|| Date.parse(receipt.redeemedAt) > Date.parse(recovery.value.expiresAt)
|
|
1855
|
+
|| (lanToCloud && (item.value.acknowledgedAt !== null
|
|
1856
|
+
|| (!matchesSourceClaim && !matchesOverride)))
|
|
1857
|
+
|| (!lanToCloud && (item.value.acknowledgedAt === null
|
|
1858
|
+
|| receipt.memberId === recovery.value.targetHostMemberId
|
|
1859
|
+
|| receipt.checkpointSha256 !== lifecycle.value.checkpointSha256
|
|
1860
|
+
|| claim !== undefined
|
|
1861
|
+
|| protectedEnvelopes.has(identity)
|
|
1862
|
+
|| terminalPrincipal?.value.acknowledgedAt !== item.value.acknowledgedAt)))
|
|
1863
|
+
throw invalidPayload('records');
|
|
1864
|
+
}
|
|
1865
|
+
if (item.kind === 'terminal-principal') {
|
|
1866
|
+
const terminal = terminalResponders.get(item.value.operationId);
|
|
1867
|
+
const acknowledgement = terminal?.value.acknowledgements.find(value => (value.memberId === item.value.memberId));
|
|
1868
|
+
const expectedOperationKind = terminal?.value.operation === 'getProjectAuthorityTransfer'
|
|
1869
|
+
? 'authority-transfer'
|
|
1870
|
+
: terminal?.value.operation === 'retireProject'
|
|
1871
|
+
? 'retire'
|
|
1872
|
+
: undefined;
|
|
1873
|
+
if (!members.has(item.value.memberId)
|
|
1874
|
+
|| terminal === undefined
|
|
1875
|
+
|| expectedOperationKind !== item.value.operationKind
|
|
1876
|
+
|| !terminal.value.eligibleMemberIds.includes(item.value.memberId)
|
|
1877
|
+
|| (item.value.acknowledgedAt === null) !== (acknowledgement === undefined)
|
|
1878
|
+
|| (acknowledgement !== undefined && (acknowledgement.acknowledgedAt !== item.value.acknowledgedAt
|
|
1879
|
+
|| acknowledgement.principalId !== item.value.principalId))
|
|
1880
|
+
|| (item.value.acknowledgedAt !== null
|
|
1881
|
+
&& Date.parse(item.value.acknowledgedAt) > Date.parse(terminal.value.expiresAt)))
|
|
1882
|
+
throw invalidPayload('records');
|
|
1883
|
+
}
|
|
1884
|
+
if (item.kind === 'terminal-responder-replay') {
|
|
1885
|
+
const lifecycle = lifecycles.get(item.value.operationId);
|
|
1886
|
+
const terminal = terminalResponders.get(item.value.operationId);
|
|
1887
|
+
const recovery = recoveries.get(item.value.operationId);
|
|
1888
|
+
let response;
|
|
1889
|
+
try {
|
|
1890
|
+
response = terminal?.value.operation === 'getProjectAuthorityTransfer'
|
|
1891
|
+
? collabControlOperationCodec('getProjectAuthorityTransfer').decodeResponse(JSON.parse(terminal.value.responseJson))
|
|
1892
|
+
: undefined;
|
|
1893
|
+
}
|
|
1894
|
+
catch {
|
|
1895
|
+
throw invalidPayload('records');
|
|
1896
|
+
}
|
|
1897
|
+
if (!members.has(item.value.memberId)
|
|
1898
|
+
|| lifecycle?.value.operationKind !== 'authority-transfer'
|
|
1899
|
+
|| lifecycle.value.direction !== 'cloud-to-lan'
|
|
1900
|
+
|| lifecycle.value.state !== 'completed'
|
|
1901
|
+
|| terminal === undefined
|
|
1902
|
+
|| recovery === undefined
|
|
1903
|
+
|| item.value.memberId !== recovery.value.targetHostMemberId
|
|
1904
|
+
|| item.value.requestSha256 !== recovery.value.targetActivationRequestSha256
|
|
1905
|
+
|| !terminal.value.eligibleMemberIds.includes(item.value.memberId)
|
|
1906
|
+
|| response === undefined
|
|
1907
|
+
|| response.batchRevision !== lifecycle.value.batchRevision
|
|
1908
|
+
|| response.batchSha256 !== lifecycle.value.batchSha256
|
|
1909
|
+
|| response.checkpointSha256 !== lifecycle.value.checkpointSha256
|
|
1910
|
+
|| response.direction !== lifecycle.value.direction
|
|
1911
|
+
|| response.expiresAt !== recovery.value.expiresAt
|
|
1912
|
+
|| response.targetUrl !== recovery.value.targetUrl
|
|
1913
|
+
|| JSON.stringify(response.relinquishmentProof)
|
|
1914
|
+
!== JSON.stringify(recovery.value.relinquishmentProof)
|
|
1915
|
+
|| response.sourceAuthority.kind !== recovery.value.sourceAuthority.kind
|
|
1916
|
+
|| response.sourceAuthority.generation !== recovery.value.sourceAuthority.generation
|
|
1917
|
+
|| response.targetAuthority.kind !== recovery.value.targetAuthority.kind
|
|
1918
|
+
|| response.targetAuthority.generation !== recovery.value.targetAuthority.generation)
|
|
1919
|
+
throw invalidPayload('records');
|
|
1920
|
+
}
|
|
1921
|
+
if (item.kind === 'leave-former-principal-replay') {
|
|
1922
|
+
const lifecycle = lifecycles.get(item.value.operationId);
|
|
1923
|
+
if (!members.has(item.value.memberId)
|
|
1924
|
+
|| lifecycle?.value.operationKind !== 'leave'
|
|
1925
|
+
|| lifecycle.value.actorMemberId !== item.value.memberId
|
|
1926
|
+
|| lifecycle.value.expectedPersonalRefOid !== item.value.expectedPersonalRefOid
|
|
1927
|
+
|| lifecycle.value.idempotencyKey !== item.value.intentId
|
|
1928
|
+
|| lifecycle.value.requestFingerprint !== item.value.requestFingerprint
|
|
1929
|
+
|| lifecycle.value.resultSha256 !== item.value.resultSha256
|
|
1930
|
+
|| (item.value.state === 'completed' && (lifecycle.value.state !== 'completed'
|
|
1931
|
+
|| item.value.completedAt !== lifecycle.value.updatedAt))
|
|
1932
|
+
|| (item.value.state === 'recovering' && (lifecycle.value.state !== 'active'
|
|
1933
|
+
&& lifecycle.value.state !== 'recovery-required')))
|
|
1934
|
+
throw invalidPayload('records');
|
|
1935
|
+
}
|
|
1936
|
+
if (item.kind === 'tombstone') {
|
|
1937
|
+
const transferTerminal = [...terminalResponders.values()].find(value => (value.value.operation === 'getProjectAuthorityTransfer'));
|
|
1938
|
+
const lifecycle = transferTerminal === undefined
|
|
1939
|
+
? undefined
|
|
1940
|
+
: lifecycles.get(transferTerminal.value.operationId);
|
|
1941
|
+
const recovery = transferTerminal === undefined
|
|
1942
|
+
? undefined
|
|
1943
|
+
: recoveries.get(transferTerminal.value.operationId);
|
|
1944
|
+
if (transferTerminal !== undefined && (lifecycle === undefined
|
|
1945
|
+
|| recovery === undefined
|
|
1946
|
+
|| item.value.authorityGeneration !== recovery.value.targetAuthority.generation
|
|
1947
|
+
|| item.value.retiredAt !== lifecycle.value.updatedAt))
|
|
1948
|
+
throw invalidPayload('records');
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
function manifestWithFormatOne(value) {
|
|
1953
|
+
const source = record(value, 'manifest');
|
|
1954
|
+
if (source.profile !== 'backup'
|
|
1955
|
+
|| source.coordinationFormatVersion !== COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION)
|
|
1956
|
+
throw invalidPayload('manifest');
|
|
1957
|
+
return decodeCollabProjectCheckpointManifest({
|
|
1958
|
+
...source,
|
|
1959
|
+
coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
export function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
1963
|
+
const decoded = manifestWithFormatOne(value);
|
|
1964
|
+
return {
|
|
1965
|
+
artifacts: decoded.artifacts,
|
|
1966
|
+
coordinationFormatVersion: COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION,
|
|
1967
|
+
createdAt: decoded.createdAt,
|
|
1968
|
+
expectedMainOid: decoded.expectedMainOid,
|
|
1969
|
+
gitObjectFormat: decoded.gitObjectFormat,
|
|
1970
|
+
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
1971
|
+
manifestSha256: decoded.manifestSha256,
|
|
1972
|
+
operationId: decoded.operationId,
|
|
1973
|
+
profile: 'backup',
|
|
1974
|
+
projectId: decoded.projectId,
|
|
1975
|
+
protocolVersion: decoded.protocolVersion,
|
|
1976
|
+
refs: decoded.refs,
|
|
1977
|
+
sourceAuthority: decoded.sourceAuthority,
|
|
1978
|
+
targetAuthority: decoded.targetAuthority,
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
export function encodeCollabProjectBackupCheckpointManifestCanonicalJson(manifest) {
|
|
1982
|
+
return JSON.stringify(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
1983
|
+
}
|
|
1984
|
+
export function encodeCollabProjectBackupCheckpointManifestDigestInput(manifest) {
|
|
1985
|
+
const decoded = decodeCollabProjectBackupCheckpointManifest(manifest);
|
|
1986
|
+
return JSON.stringify({
|
|
1987
|
+
artifacts: decoded.artifacts,
|
|
1988
|
+
coordinationFormatVersion: decoded.coordinationFormatVersion,
|
|
1989
|
+
createdAt: decoded.createdAt,
|
|
1990
|
+
expectedMainOid: decoded.expectedMainOid,
|
|
1991
|
+
gitObjectFormat: decoded.gitObjectFormat,
|
|
1992
|
+
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
1993
|
+
operationId: decoded.operationId,
|
|
1994
|
+
profile: decoded.profile,
|
|
1995
|
+
projectId: decoded.projectId,
|
|
1996
|
+
protocolVersion: decoded.protocolVersion,
|
|
1997
|
+
refs: decoded.refs,
|
|
1998
|
+
sourceAuthority: decoded.sourceAuthority,
|
|
1999
|
+
targetAuthority: decoded.targetAuthority,
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
2003
|
+
if (typeof value !== 'string'
|
|
2004
|
+
|| !value.endsWith('\n')
|
|
2005
|
+
|| !hasUtf8ByteLengthAtMost(value, COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxCoordinationBytes))
|
|
2006
|
+
throw invalidPayload('coordination');
|
|
2007
|
+
const lines = value.slice(0, -1).split('\n');
|
|
2008
|
+
if (lines.some(line => line.length === 0))
|
|
2009
|
+
throw invalidPayload('coordination');
|
|
2010
|
+
const parsed = lines.map((line) => {
|
|
2011
|
+
let item;
|
|
2012
|
+
try {
|
|
2013
|
+
item = JSON.parse(line);
|
|
2014
|
+
}
|
|
2015
|
+
catch {
|
|
2016
|
+
throw invalidPayload('coordination');
|
|
2017
|
+
}
|
|
2018
|
+
if (JSON.stringify(item) !== line)
|
|
2019
|
+
throw invalidPayload('coordination');
|
|
2020
|
+
return { envelope: recordEnvelope(item), item };
|
|
2021
|
+
});
|
|
2022
|
+
const baseItems = parsed.filter(({ envelope }) => (!CONTINUITY_KIND_SET.has(envelope.kind) && envelope.kind !== 'idempotency-result'));
|
|
2023
|
+
const baseDecoded = decodeCollabProjectCheckpointCoordinationNdjson(baseItems.map(({ item }) => JSON.stringify(sanitizeBackupBasePrincipalRecord(item))).join('\n') + '\n', 'backup');
|
|
2024
|
+
const baseByIdentity = new Map(baseDecoded.map(item => [`${item.kind}\0${item.recordId}`, item]));
|
|
2025
|
+
const decoded = parsed.map(({ envelope }) => {
|
|
2026
|
+
if (envelope.kind === 'idempotency-result') {
|
|
2027
|
+
return idempotencyResultRecord(envelope.source, envelope.recordId, envelope.revision);
|
|
2028
|
+
}
|
|
2029
|
+
if (CONTINUITY_KIND_SET.has(envelope.kind)) {
|
|
2030
|
+
return decodeContinuityRecord(envelope.kind, envelope.source, envelope.recordId, envelope.revision);
|
|
2031
|
+
}
|
|
2032
|
+
const base = baseByIdentity.get(`${envelope.kind}\0${envelope.recordId}`);
|
|
2033
|
+
if (base === undefined)
|
|
2034
|
+
throw invalidPayload('records');
|
|
2035
|
+
return restoreBackupBasePrincipals(envelope.source, base);
|
|
2036
|
+
});
|
|
2037
|
+
if (decoded.some((item, index) => JSON.stringify(item) !== lines[index])) {
|
|
2038
|
+
throw invalidPayload('coordination');
|
|
2039
|
+
}
|
|
2040
|
+
validateContinuity(decoded);
|
|
2041
|
+
return Object.freeze(decoded);
|
|
2042
|
+
}
|
|
2043
|
+
export function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
|
|
2044
|
+
const value = records.map(item => JSON.stringify(item)).join('\n') + '\n';
|
|
2045
|
+
return decodeCollabProjectBackupCheckpointCoordinationNdjson(value)
|
|
2046
|
+
.map(item => JSON.stringify(item)).join('\n') + '\n';
|
|
2047
|
+
}
|
|
2048
|
+
export function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
2049
|
+
const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
|
|
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');
|
|
2075
|
+
if (decodedRecords.some(item => {
|
|
2076
|
+
switch (item.kind) {
|
|
2077
|
+
case 'lifecycle-journal': return item.value.operationId === decodedManifest.operationId;
|
|
2078
|
+
case 'authority-transfer-recovery':
|
|
2079
|
+
case 'transferred-membership-claim':
|
|
2080
|
+
case 'transfer-receipt-key': return item.value.transferId === decodedManifest.operationId;
|
|
2081
|
+
case 'transfer-claim-batch-receipt':
|
|
2082
|
+
return item.value.receipt.transferId === decodedManifest.operationId;
|
|
2083
|
+
case 'transfer-redemption-receipt':
|
|
2084
|
+
return item.value.receipt.transferId === decodedManifest.operationId;
|
|
2085
|
+
case 'terminal-principal':
|
|
2086
|
+
case 'terminal-responder-replay':
|
|
2087
|
+
case 'leave-former-principal-replay':
|
|
2088
|
+
case 'terminal-responder': return item.value.operationId === decodedManifest.operationId;
|
|
2089
|
+
case 'protected-claim-envelope':
|
|
2090
|
+
return item.value.transferId === decodedManifest.operationId;
|
|
2091
|
+
default: return false;
|
|
2092
|
+
}
|
|
2093
|
+
}))
|
|
2094
|
+
throw invalidPayload('records');
|
|
2095
|
+
const baseRecords = decodedRecords.filter(item => (!CONTINUITY_KIND_SET.has(item.kind) && item.kind !== 'idempotency-result'));
|
|
2096
|
+
validateCollabProjectCheckpointConsistency(decodeCollabProjectCheckpointManifest({
|
|
2097
|
+
...decodedManifest,
|
|
2098
|
+
coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
2099
|
+
}), baseRecords);
|
|
2100
|
+
return records;
|
|
2101
|
+
}
|