@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.
Files changed (35) hide show
  1. package/README.md +8 -6
  2. package/dist/CollabAuthorityTransfer.d.ts +1 -1
  3. package/dist/CollabCloudBinding.d.ts +1 -1
  4. package/dist/CollabCloudBinding.js +7 -0
  5. package/dist/CollabControlOperationCodecs.d.ts +18 -0
  6. package/dist/CollabControlOperationCodecs.js +2 -0
  7. package/dist/CollabProjectBackupCheckpoint.d.ts +93 -4
  8. package/dist/CollabProjectBackupCheckpoint.js +640 -12
  9. package/dist/CollabProjectMembership.d.ts +318 -0
  10. package/dist/CollabProjectMembership.js +714 -0
  11. package/dist/CollabProtocol.d.ts +2 -1
  12. package/dist/esm/CollabAuthorityTransfer.mjs +884 -0
  13. package/dist/esm/CollabCloudBinding.mjs +559 -0
  14. package/dist/esm/CollabCloudProjectEvent.mjs +150 -0
  15. package/dist/esm/CollabCloudProjectSnapshot.mjs +277 -0
  16. package/dist/esm/CollabConstants.mjs +31 -0
  17. package/dist/esm/CollabControlOperationCodecs.mjs +106 -0
  18. package/dist/esm/CollabError.mjs +175 -0
  19. package/dist/esm/CollabMarkdownProse.mjs +54 -0
  20. package/dist/esm/CollabMemberMentionParser.mjs +48 -0
  21. package/dist/esm/CollabProjectBackupCheckpoint.mjs +2101 -0
  22. package/dist/esm/CollabProjectCheckpoint.mjs +1326 -0
  23. package/dist/esm/CollabProjectMembership.mjs +709 -0
  24. package/dist/esm/CollabProjectRetirement.mjs +119 -0
  25. package/dist/esm/CollabProtocol.mjs +64 -0
  26. package/dist/esm/CollabRequestTicketRequestCodecs.mjs +263 -0
  27. package/dist/esm/CollabRequestTicketResponseCodecs.mjs +353 -0
  28. package/dist/esm/CollabTicketReferenceParser.mjs +52 -0
  29. package/dist/esm/CollabValidation.mjs +19 -0
  30. package/dist/esm/DevelopmentBootstrap.mjs +537 -0
  31. package/dist/esm/index.mjs +17 -0
  32. package/dist/esm/types.mjs +8 -0
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.js +10 -3
  35. 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
+ }