@claudian-collab/protocol 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1473 @@
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_BACKUP_RECORD_KINDS, COLLAB_PROJECT_COORDINATION_FORMAT_VERSION, decodeCollabProjectCheckpointCoordinationNdjson, decodeCollabProjectCheckpointManifest, validateCollabProjectCheckpointConsistency, } from './CollabProjectCheckpoint.mjs';
6
+ import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
7
+ export const COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION = 2;
8
+ const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
9
+ 'lifecycle-journal',
10
+ 'authority-transfer-recovery',
11
+ 'transferred-membership-claim',
12
+ 'transfer-receipt-key',
13
+ 'transfer-claim-batch-receipt',
14
+ 'transfer-redemption-receipt',
15
+ 'terminal-principal',
16
+ 'terminal-responder-replay',
17
+ 'leave-former-principal-replay',
18
+ ]);
19
+ export const COLLAB_PROJECT_BACKUP_RECORD_KINDS = Object.freeze([
20
+ ...COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS.slice(0, 13),
21
+ ...BACKUP_CONTINUITY_RECORD_KINDS,
22
+ ...COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS.slice(14),
23
+ ]);
24
+ const CONTINUITY_KIND_SET = new Set(BACKUP_CONTINUITY_RECORD_KINDS);
25
+ const BACKUP_KIND_SET = new Set(COLLAB_PROJECT_BACKUP_RECORD_KINDS);
26
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
27
+ const PRINCIPAL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
28
+ const PHASE_PATTERN = /^[a-z][a-z0-9-]{0,63}$/u;
29
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
30
+ const STORAGE_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]{0,127}$/u;
31
+ const STORAGE_NODE_PATTERN = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
32
+ const CANCELLATION_PHASE_INDEX = new Map(COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES.map((phase, index) => [phase, index]));
33
+ const BACKUP_EXPORT_ACTIVE_PHASE_SET = new Set([
34
+ 'prepared',
35
+ 'coordination-captured',
36
+ 'repository-captured',
37
+ 'checkpoint-verified',
38
+ 'artifact-published',
39
+ 'cancel-intent',
40
+ ]);
41
+ const BACKUP_EXPORT_CHECKPOINT_REQUIRED_PHASE_SET = new Set([
42
+ 'checkpoint-verified', 'artifact-published', 'completed',
43
+ ]);
44
+ const DELETE_ACTIVE_PHASE_SET = new Set([
45
+ 'traffic-denied',
46
+ 'repository-delete-intent',
47
+ 'repository-removed',
48
+ 'coordination-removed',
49
+ 'tombstoned',
50
+ ]);
51
+ const PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET = new Set([
52
+ 'getTransferredMembershipClaim',
53
+ 'rotateTransferredMembershipClaims',
54
+ ]);
55
+ function invalidPayload(field) {
56
+ return new CollabError({ code: 'protocol-payload-invalid', safeContext: { field } });
57
+ }
58
+ function record(value, field) {
59
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
60
+ throw invalidPayload(field);
61
+ }
62
+ return value;
63
+ }
64
+ function exactRecord(value, field, keys) {
65
+ const source = record(value, field);
66
+ const expected = new Set(keys);
67
+ if (!keys.every(key => Object.hasOwn(source, key))
68
+ || Object.keys(source).some(key => !expected.has(key)))
69
+ throw invalidPayload(field);
70
+ return source;
71
+ }
72
+ function boundedString(source, field, maximumBytes, allowEmpty = false) {
73
+ const value = source[field];
74
+ if (typeof value !== 'string'
75
+ || (!allowEmpty && value.length === 0)
76
+ || !hasUtf8ByteLengthAtMost(value, maximumBytes))
77
+ throw invalidPayload(field);
78
+ return value;
79
+ }
80
+ function token(source, field, validate = isCollabOpaqueId) {
81
+ const value = source[field];
82
+ if (typeof value !== 'string' || !validate(value))
83
+ throw invalidPayload(field);
84
+ return value;
85
+ }
86
+ function nullableToken(source, field, validate = isCollabOpaqueId) {
87
+ return source[field] === null ? null : token(source, field, validate);
88
+ }
89
+ function positiveInteger(source, field) {
90
+ const value = source[field];
91
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
92
+ throw invalidPayload(field);
93
+ }
94
+ return value;
95
+ }
96
+ function nullablePositiveInteger(source, field) {
97
+ return source[field] === null ? null : positiveInteger(source, field);
98
+ }
99
+ function timestampValue(value, field) {
100
+ if (typeof value !== 'string'
101
+ || value.length > 64
102
+ || Number.isNaN(Date.parse(value))
103
+ || new Date(value).toISOString() !== value)
104
+ throw invalidPayload(field);
105
+ return value;
106
+ }
107
+ function timestamp(source, field) {
108
+ return timestampValue(source[field], field);
109
+ }
110
+ function nullableTimestamp(source, field) {
111
+ return source[field] === null ? null : timestampValue(source[field], field);
112
+ }
113
+ function sha256(source, field) {
114
+ const value = source[field];
115
+ if (typeof value !== 'string' || !SHA256_PATTERN.test(value))
116
+ throw invalidPayload(field);
117
+ return value;
118
+ }
119
+ function nullableSha256(source, field) {
120
+ return source[field] === null ? null : sha256(source, field);
121
+ }
122
+ function literal(source, field, values) {
123
+ const value = source[field];
124
+ if (typeof value !== 'string' || !values.includes(value))
125
+ throw invalidPayload(field);
126
+ return value;
127
+ }
128
+ function authority(value, field) {
129
+ const source = exactRecord(value, field, ['generation', 'kind']);
130
+ return {
131
+ generation: positiveInteger(source, 'generation'),
132
+ kind: literal(source, 'kind', ['cloud', 'lan']),
133
+ };
134
+ }
135
+ function canonicalBase64urlPublicKey(source, field) {
136
+ const value = boundedString(source, field, 128);
137
+ if (!BASE64URL_PATTERN.test(value))
138
+ throw invalidPayload(field);
139
+ const remainder = value.length % 4;
140
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
141
+ const finalIndex = alphabet.indexOf(value[value.length - 1]);
142
+ const decodedBytes = Math.floor(value.length * 6 / 8);
143
+ if (remainder === 1
144
+ || (remainder === 2 && (finalIndex & 15) !== 0)
145
+ || (remainder === 3 && (finalIndex & 3) !== 0)
146
+ || decodedBytes !== 32)
147
+ throw invalidPayload(field);
148
+ return value;
149
+ }
150
+ function proofValue(source, field) {
151
+ const value = boundedString(source, field, 4096);
152
+ if (!BASE64URL_PATTERN.test(value))
153
+ throw invalidPayload(field);
154
+ return value;
155
+ }
156
+ function absoluteTargetUrl(source) {
157
+ const targetUrl = boundedString(source, 'targetUrl', 2048);
158
+ let parsed;
159
+ try {
160
+ parsed = new URL(targetUrl);
161
+ }
162
+ catch {
163
+ throw invalidPayload('targetUrl');
164
+ }
165
+ if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
166
+ || parsed.username.length > 0
167
+ || parsed.password.length > 0
168
+ || targetUrl.includes('?')
169
+ || targetUrl.includes('#'))
170
+ throw invalidPayload('targetUrl');
171
+ return targetUrl;
172
+ }
173
+ function principal(source, field) {
174
+ return token(source, field, value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
175
+ }
176
+ function sanitizeBackupBasePrincipalRecord(item) {
177
+ const source = record(item, 'record');
178
+ const value = record(source.value, 'value');
179
+ if (source.kind === 'principal-binding') {
180
+ return {
181
+ ...source,
182
+ value: { ...value, principalId: `principal_${String(value.memberId)}` },
183
+ };
184
+ }
185
+ if (source.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
186
+ return {
187
+ ...source,
188
+ value: {
189
+ ...value,
190
+ acknowledgements: value.acknowledgements.map(item => ({
191
+ ...record(item, 'acknowledgement'),
192
+ principalId: `principal_${String(record(item, 'acknowledgement').memberId)}`,
193
+ })),
194
+ },
195
+ };
196
+ }
197
+ return item;
198
+ }
199
+ function restoreBackupBasePrincipals(source, base) {
200
+ if (base.kind === 'principal-binding') {
201
+ const value = exactRecord(source.value, 'value', [
202
+ 'boundAt', 'memberId', 'principalId', 'projectId',
203
+ ]);
204
+ return {
205
+ ...base,
206
+ value: { ...base.value, principalId: principal(value, 'principalId') },
207
+ };
208
+ }
209
+ if (base.kind === 'terminal-responder') {
210
+ const value = exactRecord(source.value, 'value', [
211
+ 'acknowledgements',
212
+ 'eligibleMemberIds',
213
+ 'expiresAt',
214
+ 'operation',
215
+ 'operationId',
216
+ 'projectId',
217
+ 'responseJson',
218
+ ]);
219
+ const sourceAcknowledgements = value.acknowledgements;
220
+ if (!Array.isArray(sourceAcknowledgements)
221
+ || sourceAcknowledgements.length !== base.value.acknowledgements.length)
222
+ throw invalidPayload('acknowledgements');
223
+ const acknowledgements = base.value.acknowledgements.map((item, index) => {
224
+ const original = exactRecord(sourceAcknowledgements[index], 'acknowledgement', [
225
+ 'acknowledgedAt', 'memberId', 'principalId',
226
+ ]);
227
+ return { ...item, principalId: principal(original, 'principalId') };
228
+ });
229
+ if (new Set(acknowledgements.map(item => item.principalId)).size !== acknowledgements.length) {
230
+ throw invalidPayload('acknowledgements');
231
+ }
232
+ return { ...base, value: { ...base.value, acknowledgements } };
233
+ }
234
+ return base;
235
+ }
236
+ function repositoryPublicationRef(value) {
237
+ const source = exactRecord(value, 'ref', ['name', 'oid']);
238
+ const name = boundedString(source, 'name', 512);
239
+ const memberId = name.startsWith(COLLAB_MEMBER_REF_PREFIX)
240
+ ? name.slice(COLLAB_MEMBER_REF_PREFIX.length)
241
+ : undefined;
242
+ if (name !== COLLAB_MAIN_REF
243
+ && (memberId === undefined || !isCollabMemberId(memberId))) {
244
+ throw invalidPayload('name');
245
+ }
246
+ return { name, oid: token(source, 'oid', isCollabGitOid) };
247
+ }
248
+ function inactiveRepositoryPublication(value) {
249
+ if (value === null)
250
+ return null;
251
+ const source = exactRecord(value, 'inactivePublication', [
252
+ 'artifactKey',
253
+ 'bundleByteCount',
254
+ 'bundleSha256',
255
+ 'objectFormat',
256
+ 'operationId',
257
+ 'placementGeneration',
258
+ 'projectId',
259
+ 'publicationMarkerSha256',
260
+ 'refs',
261
+ 'repositoryStorageKey',
262
+ 'status',
263
+ 'storageNodeId',
264
+ 'validationMarkerSha256',
265
+ ]);
266
+ if (!Array.isArray(source.refs) || source.refs.length < 2) {
267
+ throw invalidPayload('refs');
268
+ }
269
+ const refs = source.refs.map(repositoryPublicationRef);
270
+ const objectFormat = literal(source, 'objectFormat', ['sha1', 'sha256']);
271
+ const objectLength = objectFormat === 'sha1' ? 40 : 64;
272
+ const bundleByteCount = positiveInteger(source, 'bundleByteCount');
273
+ const repositoryStorageKey = boundedString(source, 'repositoryStorageKey', 128);
274
+ const storageNodeId = boundedString(source, 'storageNodeId', 64);
275
+ if (refs[0].name !== COLLAB_MAIN_REF
276
+ || refs.slice(1).some(item => !item.name.startsWith(COLLAB_MEMBER_REF_PREFIX))
277
+ || refs.some(item => item.oid.length !== objectLength)
278
+ || refs.some((item, index) => index > 0
279
+ && refs[index - 1].name.localeCompare(item.name, 'en-US') >= 0)
280
+ || bundleByteCount > COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxRepositoryBundleBytes
281
+ || !STORAGE_KEY_PATTERN.test(repositoryStorageKey)
282
+ || !STORAGE_NODE_PATTERN.test(storageNodeId))
283
+ throw invalidPayload('refs');
284
+ const artifactKey = boundedString(source, 'artifactKey', 64);
285
+ const placementGeneration = positiveInteger(source, 'placementGeneration');
286
+ if (!SHA256_PATTERN.test(artifactKey) || placementGeneration !== 1) {
287
+ throw invalidPayload('inactivePublication');
288
+ }
289
+ return {
290
+ artifactKey,
291
+ bundleByteCount,
292
+ bundleSha256: sha256(source, 'bundleSha256'),
293
+ objectFormat,
294
+ operationId: token(source, 'operationId'),
295
+ placementGeneration,
296
+ projectId: token(source, 'projectId', isCollabProjectId),
297
+ publicationMarkerSha256: sha256(source, 'publicationMarkerSha256'),
298
+ refs: Object.freeze(refs),
299
+ repositoryStorageKey,
300
+ status: literal(source, 'status', ['inactive']),
301
+ storageNodeId,
302
+ validationMarkerSha256: sha256(source, 'validationMarkerSha256'),
303
+ };
304
+ }
305
+ function lanToCloudSourceEvidence(value) {
306
+ if (value === null)
307
+ return null;
308
+ const source = exactRecord(value, 'sourceEvidence', [
309
+ 'checkpointManifestSha256',
310
+ 'principalId',
311
+ 'proof',
312
+ 'receiptKeyId',
313
+ 'receiptPublicKey',
314
+ 'schemaVersion',
315
+ ]);
316
+ if (source.schemaVersion !== 1)
317
+ throw invalidPayload('schemaVersion');
318
+ return {
319
+ checkpointManifestSha256: sha256(source, 'checkpointManifestSha256'),
320
+ principalId: principal(source, 'principalId'),
321
+ proof: proofValue(source, 'proof'),
322
+ receiptKeyId: token(source, 'receiptKeyId'),
323
+ receiptPublicKey: canonicalBase64urlPublicKey(source, 'receiptPublicKey'),
324
+ schemaVersion: 1,
325
+ };
326
+ }
327
+ function cloudToLanTargetEvidence(value) {
328
+ if (value === null)
329
+ return null;
330
+ const source = exactRecord(value, 'targetEvidence', [
331
+ 'acceptanceIntentId',
332
+ 'principalId',
333
+ 'proof',
334
+ 'receiptKeyId',
335
+ 'receiptPublicKey',
336
+ 'schemaVersion',
337
+ ]);
338
+ if (source.schemaVersion !== 1)
339
+ throw invalidPayload('schemaVersion');
340
+ return {
341
+ acceptanceIntentId: token(source, 'acceptanceIntentId'),
342
+ principalId: principal(source, 'principalId'),
343
+ proof: proofValue(source, 'proof'),
344
+ receiptKeyId: token(source, 'receiptKeyId'),
345
+ receiptPublicKey: canonicalBase64urlPublicKey(source, 'receiptPublicKey'),
346
+ schemaVersion: 1,
347
+ };
348
+ }
349
+ function operation(source, field) {
350
+ const value = source[field];
351
+ if (typeof value !== 'string')
352
+ throw invalidPayload(field);
353
+ try {
354
+ collabControlOperationCodec(value);
355
+ }
356
+ catch {
357
+ throw invalidPayload(field);
358
+ }
359
+ return value;
360
+ }
361
+ function recordEnvelope(value) {
362
+ const source = exactRecord(value, 'record', ['kind', 'recordId', 'revision', 'value']);
363
+ const kind = source.kind;
364
+ if (typeof kind !== 'string' || !BACKUP_KIND_SET.has(kind))
365
+ throw invalidPayload('kind');
366
+ return {
367
+ kind: kind,
368
+ recordId: boundedString(source, 'recordId', 512),
369
+ revision: positiveInteger(source, 'revision'),
370
+ source,
371
+ };
372
+ }
373
+ export function collabProjectBackupIdempotencyRecordId(input) {
374
+ if (!isCollabProjectId(input.projectId)
375
+ || !isCollabMemberId(input.memberId)
376
+ || !isCollabOpaqueId(input.idempotencyKey))
377
+ throw invalidPayload('idempotencyIdentity');
378
+ operation({ operation: input.operation }, 'operation');
379
+ return `${input.projectId}:${input.memberId}:${input.operation}:${input.idempotencyKey}`;
380
+ }
381
+ function idempotencyResultRecord(source, recordId, revision) {
382
+ const value = exactRecord(source.value, 'value', [
383
+ 'createdAt',
384
+ 'idempotencyKey',
385
+ 'memberId',
386
+ 'operation',
387
+ 'projectId',
388
+ 'requestFingerprint',
389
+ 'responseJson',
390
+ ]);
391
+ const decodedOperation = operation(value, 'operation');
392
+ if (PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET.has(decodedOperation)) {
393
+ throw invalidPayload('responseJson');
394
+ }
395
+ const projectId = token(value, 'projectId', isCollabProjectId);
396
+ const memberId = token(value, 'memberId', isCollabMemberId);
397
+ const idempotencyKey = token(value, 'idempotencyKey');
398
+ let responseJson;
399
+ try {
400
+ responseJson = boundedString(value, 'responseJson', 512 * 1024, true);
401
+ const decoded = collabControlOperationCodec(decodedOperation).decodeResponse(JSON.parse(responseJson));
402
+ if (JSON.stringify(decoded) !== responseJson
403
+ || (typeof decoded === 'object'
404
+ && decoded !== null
405
+ && 'projectId' in decoded
406
+ && decoded.projectId !== projectId))
407
+ throw invalidPayload('responseJson');
408
+ }
409
+ catch {
410
+ throw invalidPayload('responseJson');
411
+ }
412
+ if (recordId !== collabProjectBackupIdempotencyRecordId({
413
+ idempotencyKey,
414
+ memberId,
415
+ operation: decodedOperation,
416
+ projectId,
417
+ }))
418
+ throw invalidPayload('recordId');
419
+ return {
420
+ kind: 'idempotency-result',
421
+ recordId,
422
+ revision,
423
+ value: {
424
+ createdAt: timestamp(value, 'createdAt'),
425
+ idempotencyKey,
426
+ memberId,
427
+ operation: decodedOperation,
428
+ projectId,
429
+ requestFingerprint: sha256(value, 'requestFingerprint'),
430
+ responseJson,
431
+ },
432
+ };
433
+ }
434
+ function lifecycleJournalRecord(source, recordId, revision) {
435
+ const value = exactRecord(source.value, 'value', [
436
+ 'actorMemberId',
437
+ 'batchRevision',
438
+ 'batchSha256',
439
+ 'checkpointSha256',
440
+ 'createdAt',
441
+ 'direction',
442
+ 'expectedAuthorityGeneration',
443
+ 'expectedPersonalRefOid',
444
+ 'idempotencyKey',
445
+ 'operationId',
446
+ 'operationKind',
447
+ 'phase',
448
+ 'projectId',
449
+ 'recoveryFromPhase',
450
+ 'requestFingerprint',
451
+ 'resultSha256',
452
+ 'scheduledAt',
453
+ 'state',
454
+ 'updatedAt',
455
+ ]);
456
+ const operationId = token(value, 'operationId');
457
+ if (recordId !== operationId)
458
+ throw invalidPayload('recordId');
459
+ const operationKind = literal(value, 'operationKind', [
460
+ 'authority-transfer', 'backup', 'delete', 'export', 'leave', 'retire',
461
+ ]);
462
+ const direction = value.direction === null
463
+ ? null
464
+ : literal(value, 'direction', ['cloud-to-lan', 'lan-to-cloud']);
465
+ if ((operationKind === 'authority-transfer') !== (direction !== null)) {
466
+ throw invalidPayload('direction');
467
+ }
468
+ const expectedPersonalRefOid = value.expectedPersonalRefOid === null
469
+ ? null
470
+ : token(value, 'expectedPersonalRefOid', isCollabGitOid);
471
+ if ((operationKind === 'leave') !== (expectedPersonalRefOid !== null)) {
472
+ throw invalidPayload('expectedPersonalRefOid');
473
+ }
474
+ const batchRevision = nullablePositiveInteger(value, 'batchRevision');
475
+ const batchSha256 = nullableSha256(value, 'batchSha256');
476
+ const checkpointSha256 = nullableSha256(value, 'checkpointSha256');
477
+ if ((batchRevision === null) !== (batchSha256 === null)
478
+ || (batchRevision !== null && checkpointSha256 === null))
479
+ throw invalidPayload('batchRevision');
480
+ const state = literal(value, 'state', [
481
+ 'active', 'cancelled', 'completed', 'recovery-required',
482
+ ]);
483
+ const recoveryFromPhase = value.recoveryFromPhase === null
484
+ ? null
485
+ : boundedString(value, 'recoveryFromPhase', 64);
486
+ if ((state === 'recovery-required') !== (recoveryFromPhase !== null)
487
+ || (recoveryFromPhase !== null && !PHASE_PATTERN.test(recoveryFromPhase)))
488
+ throw invalidPayload('recoveryFromPhase');
489
+ const phase = boundedString(value, 'phase', 64);
490
+ if (!PHASE_PATTERN.test(phase))
491
+ throw invalidPayload('phase');
492
+ const createdAt = timestamp(value, 'createdAt');
493
+ const scheduledAt = timestamp(value, 'scheduledAt');
494
+ const updatedAt = timestamp(value, 'updatedAt');
495
+ if (Date.parse(scheduledAt) < Date.parse(createdAt)
496
+ || Date.parse(updatedAt) < Date.parse(createdAt))
497
+ throw invalidPayload('updatedAt');
498
+ return {
499
+ kind: 'lifecycle-journal',
500
+ recordId,
501
+ revision,
502
+ value: {
503
+ actorMemberId: nullableToken(value, 'actorMemberId', isCollabMemberId),
504
+ batchRevision,
505
+ batchSha256,
506
+ checkpointSha256,
507
+ createdAt,
508
+ direction,
509
+ expectedAuthorityGeneration: positiveInteger(value, 'expectedAuthorityGeneration'),
510
+ expectedPersonalRefOid,
511
+ idempotencyKey: token(value, 'idempotencyKey'),
512
+ operationId,
513
+ operationKind,
514
+ phase,
515
+ projectId: token(value, 'projectId', isCollabProjectId),
516
+ recoveryFromPhase,
517
+ requestFingerprint: sha256(value, 'requestFingerprint'),
518
+ resultSha256: nullableSha256(value, 'resultSha256'),
519
+ scheduledAt,
520
+ state,
521
+ updatedAt,
522
+ },
523
+ };
524
+ }
525
+ function nullableProof(source, field) {
526
+ return source[field] === null ? null : proofValue(source, field);
527
+ }
528
+ function authorityTransferRecoveryRecord(source, recordId, revision) {
529
+ const value = exactRecord(source.value, 'value', [
530
+ 'cancellationRequestSha256',
531
+ 'createdAt',
532
+ 'expiresAt',
533
+ 'inactivePublication',
534
+ 'projectId',
535
+ 'relinquishmentProof',
536
+ 'sourceAuthority',
537
+ 'sourceHostMemberId',
538
+ 'sourceEvidence',
539
+ 'sourceReopenSha256',
540
+ 'stageSha256',
541
+ 'targetActivationProof',
542
+ 'targetActivationRequestSha256',
543
+ 'targetAuthority',
544
+ 'targetHostMemberId',
545
+ 'targetEvidence',
546
+ 'targetUrl',
547
+ 'transferId',
548
+ 'updatedAt',
549
+ ]);
550
+ const transferId = token(value, 'transferId');
551
+ if (recordId !== transferId)
552
+ throw invalidPayload('recordId');
553
+ const projectId = token(value, 'projectId', isCollabProjectId);
554
+ const sourceAuthority = authority(value.sourceAuthority, 'sourceAuthority');
555
+ const targetAuthority = authority(value.targetAuthority, 'targetAuthority');
556
+ const sourceHostMemberId = nullableToken(value, 'sourceHostMemberId', isCollabMemberId);
557
+ const targetHostMemberId = nullableToken(value, 'targetHostMemberId', isCollabMemberId);
558
+ const inactivePublication = inactiveRepositoryPublication(value.inactivePublication);
559
+ const sourceEvidence = lanToCloudSourceEvidence(value.sourceEvidence);
560
+ const targetEvidence = cloudToLanTargetEvidence(value.targetEvidence);
561
+ if (sourceAuthority.kind === targetAuthority.kind
562
+ || targetAuthority.generation !== sourceAuthority.generation + 1
563
+ || (sourceAuthority.kind === 'lan'
564
+ ? sourceHostMemberId === null || targetHostMemberId !== null
565
+ : sourceHostMemberId !== null || targetHostMemberId === null)
566
+ || (sourceAuthority.kind === 'lan'
567
+ ? targetEvidence !== null
568
+ : sourceEvidence !== null || inactivePublication !== null))
569
+ throw invalidPayload('sourceAuthority');
570
+ if (inactivePublication !== null && (inactivePublication.projectId !== projectId
571
+ || inactivePublication.operationId !== transferId))
572
+ throw invalidPayload('inactivePublication');
573
+ const relinquishmentProof = value.relinquishmentProof === null
574
+ ? null
575
+ : decodeCollabAuthorityRelinquishmentProof(value.relinquishmentProof);
576
+ if (relinquishmentProof !== null && (relinquishmentProof.projectId !== projectId
577
+ || relinquishmentProof.transferId !== transferId
578
+ || relinquishmentProof.sourceAuthority.kind !== sourceAuthority.kind
579
+ || relinquishmentProof.sourceAuthority.generation !== sourceAuthority.generation
580
+ || relinquishmentProof.sourceHostMemberId !== sourceHostMemberId
581
+ || relinquishmentProof.targetAuthority.kind !== targetAuthority.kind
582
+ || relinquishmentProof.targetAuthority.generation !== targetAuthority.generation))
583
+ throw invalidPayload('relinquishmentProof');
584
+ const createdAt = timestamp(value, 'createdAt');
585
+ const expiresAt = timestamp(value, 'expiresAt');
586
+ const updatedAt = timestamp(value, 'updatedAt');
587
+ if (Date.parse(expiresAt) <= Date.parse(createdAt)
588
+ || Date.parse(updatedAt) < Date.parse(createdAt))
589
+ throw invalidPayload('updatedAt');
590
+ return {
591
+ kind: 'authority-transfer-recovery',
592
+ recordId,
593
+ revision,
594
+ value: {
595
+ cancellationRequestSha256: nullableSha256(value, 'cancellationRequestSha256'),
596
+ createdAt,
597
+ expiresAt,
598
+ inactivePublication,
599
+ projectId,
600
+ relinquishmentProof,
601
+ sourceAuthority,
602
+ sourceHostMemberId,
603
+ sourceEvidence,
604
+ sourceReopenSha256: nullableSha256(value, 'sourceReopenSha256'),
605
+ stageSha256: nullableSha256(value, 'stageSha256'),
606
+ targetActivationProof: nullableProof(value, 'targetActivationProof'),
607
+ targetActivationRequestSha256: nullableSha256(value, 'targetActivationRequestSha256'),
608
+ targetAuthority,
609
+ targetHostMemberId,
610
+ targetEvidence,
611
+ targetUrl: absoluteTargetUrl(value),
612
+ transferId,
613
+ updatedAt,
614
+ },
615
+ };
616
+ }
617
+ function transferredMembershipClaimRecord(source, recordId, revision) {
618
+ const value = exactRecord(source.value, 'value', [
619
+ 'batchRevision',
620
+ 'checkpointSha256',
621
+ 'claimSha256',
622
+ 'createdAt',
623
+ 'expiresAt',
624
+ 'memberId',
625
+ 'operationIntentId',
626
+ 'projectId',
627
+ 'redemptionReceiptId',
628
+ 'state',
629
+ 'targetPrincipalId',
630
+ 'transferId',
631
+ 'updatedAt',
632
+ ]);
633
+ const transferId = token(value, 'transferId');
634
+ const memberId = token(value, 'memberId', isCollabMemberId);
635
+ if (recordId !== `${transferId}:${memberId}`)
636
+ throw invalidPayload('recordId');
637
+ const state = literal(value, 'state', ['redeemed', 'revoked', 'unclaimed']);
638
+ const targetPrincipalId = nullableToken(value, 'targetPrincipalId', value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
639
+ const operationIntentId = nullableToken(value, 'operationIntentId');
640
+ const redemptionReceiptId = nullableToken(value, 'redemptionReceiptId');
641
+ if ((state !== 'redeemed'
642
+ && (targetPrincipalId !== null
643
+ || operationIntentId !== null
644
+ || redemptionReceiptId !== null))
645
+ || (state === 'redeemed'
646
+ && (targetPrincipalId === null
647
+ || operationIntentId === null
648
+ || redemptionReceiptId === null)))
649
+ throw invalidPayload('state');
650
+ const createdAt = timestamp(value, 'createdAt');
651
+ const expiresAt = timestamp(value, 'expiresAt');
652
+ const updatedAt = timestamp(value, 'updatedAt');
653
+ if (Date.parse(expiresAt) <= Date.parse(createdAt)
654
+ || Date.parse(updatedAt) < Date.parse(createdAt))
655
+ throw invalidPayload('updatedAt');
656
+ return {
657
+ kind: 'transferred-membership-claim',
658
+ recordId,
659
+ revision,
660
+ value: {
661
+ batchRevision: positiveInteger(value, 'batchRevision'),
662
+ checkpointSha256: sha256(value, 'checkpointSha256'),
663
+ claimSha256: sha256(value, 'claimSha256'),
664
+ createdAt,
665
+ expiresAt,
666
+ memberId,
667
+ operationIntentId,
668
+ projectId: token(value, 'projectId', isCollabProjectId),
669
+ redemptionReceiptId,
670
+ state,
671
+ targetPrincipalId,
672
+ transferId,
673
+ updatedAt,
674
+ },
675
+ };
676
+ }
677
+ function transferReceiptKeyRecord(source, recordId, revision) {
678
+ const value = exactRecord(source.value, 'value', [
679
+ 'createdAt',
680
+ 'projectId',
681
+ 'receiptKeyId',
682
+ 'receiptPublicKey',
683
+ 'receiptPublicKeyEncoding',
684
+ 'signatureAlgorithm',
685
+ 'transferId',
686
+ ]);
687
+ const transferId = token(value, 'transferId');
688
+ const receiptKeyId = token(value, 'receiptKeyId');
689
+ if (recordId !== `${transferId}:${receiptKeyId}`)
690
+ throw invalidPayload('recordId');
691
+ return {
692
+ kind: 'transfer-receipt-key',
693
+ recordId,
694
+ revision,
695
+ value: {
696
+ createdAt: timestamp(value, 'createdAt'),
697
+ projectId: token(value, 'projectId', isCollabProjectId),
698
+ receiptKeyId,
699
+ receiptPublicKey: canonicalBase64urlPublicKey(value, 'receiptPublicKey'),
700
+ receiptPublicKeyEncoding: literal(value, 'receiptPublicKeyEncoding', ['base64url-raw']),
701
+ signatureAlgorithm: literal(value, 'signatureAlgorithm', ['ed25519']),
702
+ transferId,
703
+ },
704
+ };
705
+ }
706
+ function transferClaimBatchReceiptRecord(source, recordId, revision) {
707
+ const value = exactRecord(source.value, 'value', ['receipt']);
708
+ const receipt = decodeCollabTransferredMembershipClaimCustodyReceipt(value.receipt);
709
+ if (recordId !== receipt.transferId)
710
+ throw invalidPayload('recordId');
711
+ return { kind: 'transfer-claim-batch-receipt', recordId, revision, value: { receipt } };
712
+ }
713
+ function transferRedemptionReceiptRecord(source, recordId, revision) {
714
+ const value = exactRecord(source.value, 'value', [
715
+ 'acknowledgedAt',
716
+ 'projectId',
717
+ 'receipt',
718
+ ]);
719
+ const receipt = decodeCollabTransferredMembershipRedemptionReceipt(value.receipt);
720
+ const projectId = token(value, 'projectId', isCollabProjectId);
721
+ if (projectId !== receipt.projectId
722
+ || recordId !== `${receipt.transferId}:${receipt.memberId}`)
723
+ throw invalidPayload('recordId');
724
+ const acknowledgedAt = nullableTimestamp(value, 'acknowledgedAt');
725
+ if (acknowledgedAt !== null
726
+ && Date.parse(acknowledgedAt) < Date.parse(receipt.redeemedAt))
727
+ throw invalidPayload('acknowledgedAt');
728
+ return {
729
+ kind: 'transfer-redemption-receipt',
730
+ recordId,
731
+ revision,
732
+ value: { acknowledgedAt, projectId, receipt },
733
+ };
734
+ }
735
+ function terminalPrincipalRecord(source, recordId, revision) {
736
+ const value = exactRecord(source.value, 'value', [
737
+ 'acknowledgedAt',
738
+ 'memberId',
739
+ 'operationId',
740
+ 'operationKind',
741
+ 'principalId',
742
+ 'projectId',
743
+ ]);
744
+ const operationId = token(value, 'operationId');
745
+ const memberId = token(value, 'memberId', isCollabMemberId);
746
+ if (recordId !== `${operationId}:${memberId}`)
747
+ throw invalidPayload('recordId');
748
+ return {
749
+ kind: 'terminal-principal',
750
+ recordId,
751
+ revision,
752
+ value: {
753
+ acknowledgedAt: nullableTimestamp(value, 'acknowledgedAt'),
754
+ memberId,
755
+ operationId,
756
+ operationKind: literal(value, 'operationKind', ['authority-transfer', 'retire']),
757
+ principalId: principal(value, 'principalId'),
758
+ projectId: token(value, 'projectId', isCollabProjectId),
759
+ },
760
+ };
761
+ }
762
+ function terminalResponderReplayRecord(source, recordId, revision) {
763
+ const value = exactRecord(source.value, 'value', [
764
+ 'memberId', 'operationId', 'projectId', 'requestSha256',
765
+ ]);
766
+ const operationId = token(value, 'operationId');
767
+ if (recordId !== operationId)
768
+ throw invalidPayload('recordId');
769
+ return {
770
+ kind: 'terminal-responder-replay',
771
+ recordId,
772
+ revision,
773
+ value: {
774
+ memberId: token(value, 'memberId', isCollabMemberId),
775
+ operationId,
776
+ projectId: token(value, 'projectId', isCollabProjectId),
777
+ requestSha256: sha256(value, 'requestSha256'),
778
+ },
779
+ };
780
+ }
781
+ function leaveFormerPrincipalReplayRecord(source, recordId, revision) {
782
+ const value = exactRecord(source.value, 'value', [
783
+ 'completedAt',
784
+ 'createdAt',
785
+ 'expectedPersonalRefOid',
786
+ 'expiresAt',
787
+ 'intentId',
788
+ 'memberId',
789
+ 'operationId',
790
+ 'principalSha256',
791
+ 'projectId',
792
+ 'requestFingerprint',
793
+ 'resultSha256',
794
+ 'state',
795
+ ]);
796
+ const operationId = token(value, 'operationId');
797
+ if (recordId !== operationId)
798
+ throw invalidPayload('recordId');
799
+ const state = literal(value, 'state', ['completed', 'recovering']);
800
+ const completedAt = nullableTimestamp(value, 'completedAt');
801
+ const resultSha256 = nullableSha256(value, 'resultSha256');
802
+ if ((state === 'completed' && (completedAt === null || resultSha256 === null))
803
+ || (state === 'recovering' && (completedAt !== null || resultSha256 !== null))) {
804
+ throw invalidPayload('state');
805
+ }
806
+ const createdAt = timestamp(value, 'createdAt');
807
+ const expiresAt = timestamp(value, 'expiresAt');
808
+ if (Date.parse(expiresAt) <= Date.parse(createdAt)
809
+ || (completedAt !== null && Date.parse(completedAt) < Date.parse(createdAt)))
810
+ throw invalidPayload('completedAt');
811
+ return {
812
+ kind: 'leave-former-principal-replay',
813
+ recordId,
814
+ revision,
815
+ value: {
816
+ completedAt,
817
+ createdAt,
818
+ expectedPersonalRefOid: token(value, 'expectedPersonalRefOid', isCollabGitOid),
819
+ expiresAt,
820
+ intentId: token(value, 'intentId'),
821
+ memberId: token(value, 'memberId', isCollabMemberId),
822
+ operationId,
823
+ principalSha256: sha256(value, 'principalSha256'),
824
+ projectId: token(value, 'projectId', isCollabProjectId),
825
+ requestFingerprint: sha256(value, 'requestFingerprint'),
826
+ resultSha256,
827
+ state,
828
+ },
829
+ };
830
+ }
831
+ function decodeContinuityRecord(kind, source, recordId, revision) {
832
+ switch (kind) {
833
+ case 'lifecycle-journal': return lifecycleJournalRecord(source, recordId, revision);
834
+ case 'authority-transfer-recovery':
835
+ return authorityTransferRecoveryRecord(source, recordId, revision);
836
+ case 'transferred-membership-claim':
837
+ return transferredMembershipClaimRecord(source, recordId, revision);
838
+ case 'transfer-receipt-key': return transferReceiptKeyRecord(source, recordId, revision);
839
+ case 'transfer-claim-batch-receipt':
840
+ return transferClaimBatchReceiptRecord(source, recordId, revision);
841
+ case 'transfer-redemption-receipt':
842
+ return transferRedemptionReceiptRecord(source, recordId, revision);
843
+ case 'terminal-principal': return terminalPrincipalRecord(source, recordId, revision);
844
+ case 'terminal-responder-replay':
845
+ return terminalResponderReplayRecord(source, recordId, revision);
846
+ case 'leave-former-principal-replay':
847
+ return leaveFormerPrincipalReplayRecord(source, recordId, revision);
848
+ }
849
+ }
850
+ function kindOrder(kind) {
851
+ return COLLAB_PROJECT_BACKUP_RECORD_KINDS.indexOf(kind);
852
+ }
853
+ function compareRecords(left, right) {
854
+ const difference = kindOrder(left.kind) - kindOrder(right.kind);
855
+ return difference === 0
856
+ ? left.recordId.localeCompare(right.recordId, 'en-US')
857
+ : difference;
858
+ }
859
+ function recordProjectId(item) {
860
+ if (item.kind === 'cloud-event')
861
+ return item.value.event.projectId;
862
+ if (item.kind === 'protected-claim-envelope')
863
+ return item.value.associatedData.projectId;
864
+ if (item.kind === 'transfer-claim-batch-receipt')
865
+ return item.value.receipt.projectId;
866
+ return item.value.projectId;
867
+ }
868
+ function lifecycleJournalHasInvalidSemantics(item) {
869
+ const value = item.value;
870
+ const effectivePhase = value.state === 'recovery-required'
871
+ ? value.recoveryFromPhase
872
+ : value.phase;
873
+ if (effectivePhase === null)
874
+ return true;
875
+ const hasResult = value.resultSha256 !== null;
876
+ const hasCheckpoint = value.checkpointSha256 !== null;
877
+ const hasTransferBatch = value.batchRevision !== null || value.batchSha256 !== null;
878
+ if (value.operationKind === 'authority-transfer') {
879
+ return value.state === 'recovery-required'
880
+ || hasResult !== (value.state === 'completed');
881
+ }
882
+ if (hasTransferBatch)
883
+ return true;
884
+ if (value.operationKind === 'backup' || value.operationKind === 'export') {
885
+ const statePhaseInvalid = value.state === 'active'
886
+ ? !BACKUP_EXPORT_ACTIVE_PHASE_SET.has(value.phase)
887
+ : value.state === 'recovery-required'
888
+ ? value.phase !== value.recoveryFromPhase
889
+ || !BACKUP_EXPORT_ACTIVE_PHASE_SET.has(effectivePhase)
890
+ : value.state === 'cancelled'
891
+ ? value.phase !== 'cancelled'
892
+ : value.phase !== 'completed';
893
+ const checkpointRequired = BACKUP_EXPORT_CHECKPOINT_REQUIRED_PHASE_SET.has(effectivePhase);
894
+ const checkpointOptional = effectivePhase === 'cancel-intent'
895
+ || effectivePhase === 'cancelled';
896
+ const resultRequired = effectivePhase === 'artifact-published'
897
+ || value.state === 'completed';
898
+ return statePhaseInvalid
899
+ || (!checkpointOptional && hasCheckpoint !== checkpointRequired)
900
+ || hasResult !== resultRequired;
901
+ }
902
+ if (hasCheckpoint)
903
+ return true;
904
+ if (value.operationKind === 'leave') {
905
+ const statePhaseValid = (value.state === 'active' && (value.phase === 'prepared'
906
+ || value.phase === 'membership-left'
907
+ || value.phase === 'personal-ref-removed')) || (value.state === 'recovery-required'
908
+ && value.phase === 'recovery-required'
909
+ && value.recoveryFromPhase === 'membership-left') || (value.state === 'cancelled'
910
+ && value.phase === 'manager-succession-required') || (value.state === 'completed' && value.phase === 'completed');
911
+ return !statePhaseValid || hasResult !== (value.state === 'completed');
912
+ }
913
+ if (value.operationKind === 'retire') {
914
+ return value.state !== 'completed' || value.phase !== 'completed' || !hasResult;
915
+ }
916
+ const deleteStatePhaseValid = (value.state === 'active'
917
+ && DELETE_ACTIVE_PHASE_SET.has(value.phase))
918
+ || (value.state === 'completed' && value.phase === 'completed');
919
+ return !deleteStatePhaseValid || hasResult !== (value.state === 'completed');
920
+ }
921
+ function validateContinuity(records) {
922
+ if (records.length === 0
923
+ || records[0].kind !== 'project'
924
+ || records.some((item, index) => index > 0 && compareRecords(records[index - 1], item) >= 0))
925
+ throw invalidPayload('records');
926
+ const projectId = records[0].value.projectId;
927
+ const projectAuthorityGeneration = records[0].value.authorityGeneration;
928
+ if (records.some(item => recordProjectId(item) !== projectId))
929
+ throw invalidPayload('records');
930
+ const memberRecords = new Map(records
931
+ .filter(item => item.kind === 'member')
932
+ .map(item => [item.value.memberId, item]));
933
+ const members = new Set(memberRecords.keys());
934
+ const principalBindingRecords = records.filter(item => item.kind === 'principal-binding');
935
+ const principalBindings = new Map(principalBindingRecords
936
+ .map(item => [item.value.memberId, item.value.principalId]));
937
+ const lifecycles = new Map(records
938
+ .filter((item) => (item.kind === 'lifecycle-journal'))
939
+ .map(item => [item.value.operationId, item]));
940
+ const recoveries = new Map(records
941
+ .filter((item) => (item.kind === 'authority-transfer-recovery'))
942
+ .map(item => [item.value.transferId, item]));
943
+ const keys = new Map(records
944
+ .filter((item) => (item.kind === 'transfer-receipt-key'))
945
+ .map(item => [`${item.value.transferId}:${item.value.receiptKeyId}`, item]));
946
+ const claimRecords = records
947
+ .filter((item) => (item.kind === 'transferred-membership-claim'));
948
+ const claims = new Map(claimRecords
949
+ .map(item => [`${item.value.transferId}:${item.value.memberId}`, item]));
950
+ const redemptionReceiptRecords = records
951
+ .filter((item) => (item.kind === 'transfer-redemption-receipt'));
952
+ const redemptionReceipts = new Map(redemptionReceiptRecords
953
+ .map(item => [`${item.value.receipt.transferId}:${item.value.receipt.memberId}`, item]));
954
+ const protectedEnvelopes = new Map(records
955
+ .filter(item => item.kind === 'protected-claim-envelope')
956
+ .map(item => [`${item.value.transferId}:${item.value.memberId}`, item]));
957
+ const batchReceipts = new Map(records
958
+ .filter((item) => (item.kind === 'transfer-claim-batch-receipt'))
959
+ .map(item => [item.value.receipt.transferId, item]));
960
+ const terminalResponders = new Map(records
961
+ .filter((item) => (item.kind === 'terminal-responder'))
962
+ .map(item => [item.value.operationId, item]));
963
+ const terminalPrincipals = new Map(records
964
+ .filter((item) => (item.kind === 'terminal-principal'))
965
+ .map(item => [`${item.value.operationId}:${item.value.memberId}`, item]));
966
+ const responderReplays = new Map(records
967
+ .filter((item) => (item.kind === 'terminal-responder-replay'))
968
+ .map(item => [item.value.operationId, item]));
969
+ const leaveReplays = new Map(records
970
+ .filter((item) => (item.kind === 'leave-former-principal-replay'))
971
+ .map(item => [item.value.operationId, item]));
972
+ const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
973
+ if (nonterminalLifecycles.length > 1
974
+ || principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
975
+ || new Set(principalBindingRecords.map(item => item.value.principalId)).size
976
+ !== principalBindingRecords.length
977
+ || new Set(claimRecords.map(item => (`${item.value.transferId}:${item.value.claimSha256}`))).size !== claimRecords.length
978
+ || new Set(redemptionReceiptRecords.map(item => (`${item.value.receipt.transferId}:${item.value.receipt.receiptId}`))).size !== redemptionReceiptRecords.length)
979
+ throw invalidPayload('records');
980
+ for (const lifecycle of lifecycles.values()) {
981
+ if (lifecycleJournalHasInvalidSemantics(lifecycle)
982
+ || (lifecycle.value.actorMemberId !== null
983
+ && !members.has(lifecycle.value.actorMemberId))) {
984
+ throw invalidPayload('records');
985
+ }
986
+ if (lifecycle.value.operationKind === 'authority-transfer'
987
+ && !recoveries.has(lifecycle.value.operationId))
988
+ throw invalidPayload('records');
989
+ if (lifecycle.value.operationKind === 'leave') {
990
+ const actorMemberId = lifecycle.value.actorMemberId;
991
+ const actor = actorMemberId === null ? undefined : memberRecords.get(actorMemberId);
992
+ const replay = leaveReplays.get(lifecycle.value.operationId);
993
+ const effectivePhase = lifecycle.value.state === 'recovery-required'
994
+ ? lifecycle.value.recoveryFromPhase
995
+ : lifecycle.value.phase;
996
+ const afterMembershipSettlement = effectivePhase === 'membership-left'
997
+ || effectivePhase === 'personal-ref-removed'
998
+ || effectivePhase === 'completed';
999
+ if (actor === undefined
1000
+ || (lifecycle.value.state === 'completed') !== (lifecycle.value.phase === 'completed')
1001
+ || (afterMembershipSettlement && (actor.value.status !== 'left'
1002
+ || principalBindings.has(actor.value.memberId)
1003
+ || replay === undefined))
1004
+ || (!afterMembershipSettlement && (actor.value.status !== 'active'
1005
+ || !principalBindings.has(actor.value.memberId)
1006
+ || replay !== undefined)))
1007
+ throw invalidPayload('records');
1008
+ }
1009
+ }
1010
+ for (const recovery of recoveries.values()) {
1011
+ const lifecycle = lifecycles.get(recovery.value.transferId);
1012
+ if (lifecycle === undefined)
1013
+ throw invalidPayload('records');
1014
+ const proof = recovery.value.relinquishmentProof;
1015
+ const batchReceipt = batchReceipts.get(recovery.value.transferId)?.value.receipt;
1016
+ const evidence = recovery.value.sourceEvidence ?? recovery.value.targetEvidence;
1017
+ const receiptKey = evidence === null
1018
+ ? undefined
1019
+ : keys.get(`${recovery.value.transferId}:${evidence.receiptKeyId}`);
1020
+ const effectivePhase = lifecycle.value.state === 'recovery-required'
1021
+ ? lifecycle.value.recoveryFromPhase
1022
+ : lifecycle.value.phase;
1023
+ let relinquishmentRequired;
1024
+ try {
1025
+ if (lifecycle.value.direction !== null && effectivePhase !== null) {
1026
+ relinquishmentRequired = decodeCollabAuthorityTransferLifecycleFence({
1027
+ batchRevision: lifecycle.value.batchRevision,
1028
+ batchSha256: lifecycle.value.batchSha256,
1029
+ checkpointSha256: lifecycle.value.checkpointSha256,
1030
+ direction: lifecycle.value.direction,
1031
+ phase: effectivePhase,
1032
+ }).relinquishmentRequired;
1033
+ }
1034
+ }
1035
+ catch {
1036
+ throw invalidPayload('records');
1037
+ }
1038
+ const direction = lifecycle.value.direction;
1039
+ const cancellationIndex = effectivePhase === null
1040
+ ? undefined
1041
+ : CANCELLATION_PHASE_INDEX.get(effectivePhase);
1042
+ const isCancellation = cancellationIndex !== undefined;
1043
+ const cleanupCompleted = isCancellation && cancellationIndex >= 2;
1044
+ const normalPhaseIndex = direction === 'cloud-to-lan'
1045
+ ? COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.indexOf(effectivePhase)
1046
+ : COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES.indexOf(effectivePhase);
1047
+ const stageRequired = !isCancellation && normalPhaseIndex >= 3;
1048
+ const targetEvidenceRequired = direction === 'cloud-to-lan'
1049
+ && !isCancellation
1050
+ && normalPhaseIndex >= 1;
1051
+ const activationEvidenceRequired = direction === 'cloud-to-lan'
1052
+ && !isCancellation
1053
+ && normalPhaseIndex >= 6;
1054
+ const publicationRequired = direction === 'lan-to-cloud'
1055
+ && !isCancellation
1056
+ && normalPhaseIndex >= 5;
1057
+ const publicationForbidden = direction === 'cloud-to-lan'
1058
+ || (!isCancellation && normalPhaseIndex < 4);
1059
+ const sourceReopenRequired = isCancellation && (direction === 'cloud-to-lan'
1060
+ ? cancellationIndex >= 1
1061
+ : cancellationIndex >= 3);
1062
+ const recoveryEvidenceInvalid = direction === null
1063
+ || (direction === 'lan-to-cloud' && recovery.value.sourceEvidence === null)
1064
+ || (targetEvidenceRequired && recovery.value.targetEvidence === null)
1065
+ || (!isCancellation && (stageRequired !== (recovery.value.stageSha256 !== null)))
1066
+ || (direction === 'lan-to-cloud'
1067
+ && recovery.value.stageSha256 !== null
1068
+ && recovery.value.stageSha256 !== lifecycle.value.checkpointSha256)
1069
+ || activationEvidenceRequired !== (recovery.value.targetActivationProof !== null
1070
+ && recovery.value.targetActivationRequestSha256 !== null)
1071
+ || ((recovery.value.targetActivationProof === null)
1072
+ !== (recovery.value.targetActivationRequestSha256 === null))
1073
+ || (publicationRequired && recovery.value.inactivePublication === null)
1074
+ || (publicationForbidden && recovery.value.inactivePublication !== null)
1075
+ || isCancellation !== (recovery.value.cancellationRequestSha256 !== null)
1076
+ || sourceReopenRequired !== (recovery.value.sourceReopenSha256 !== null);
1077
+ const expectedProjectAuthorityGeneration = relinquishmentRequired === true
1078
+ ? recovery.value.targetAuthority.generation
1079
+ : recovery.value.sourceAuthority.generation;
1080
+ if (recoveryEvidenceInvalid)
1081
+ throw invalidPayload('records');
1082
+ if (cleanupCompleted && ((direction === 'cloud-to-lan' && [...protectedEnvelopes.values()].some(item => (item.value.transferId === recovery.value.transferId)))
1083
+ || (direction === 'lan-to-cloud' && claimRecords.some(item => (item.value.transferId === recovery.value.transferId)))))
1084
+ throw invalidPayload('records');
1085
+ if (projectAuthorityGeneration !== expectedProjectAuthorityGeneration) {
1086
+ throw invalidPayload('records');
1087
+ }
1088
+ if (!isCancellation) {
1089
+ const hostMemberId = direction === 'lan-to-cloud'
1090
+ ? recovery.value.sourceHostMemberId
1091
+ : recovery.value.targetHostMemberId;
1092
+ const eligibleMemberIds = [...memberRecords.values()]
1093
+ .filter(item => item.value.status === 'active' && item.value.memberId !== hostMemberId)
1094
+ .map(item => item.value.memberId);
1095
+ const memberCustodyRequired = normalPhaseIndex >= 3;
1096
+ const batchReceiptRequired = direction === 'cloud-to-lan'
1097
+ ? normalPhaseIndex >= 3
1098
+ : normalPhaseIndex >= 4;
1099
+ if ((batchReceipt !== undefined) !== batchReceiptRequired) {
1100
+ throw invalidPayload('records');
1101
+ }
1102
+ for (const memberId of eligibleMemberIds) {
1103
+ const identity = `${recovery.value.transferId}:${memberId}`;
1104
+ const claimPresent = claims.has(identity);
1105
+ const envelopePresent = protectedEnvelopes.has(identity);
1106
+ const redemptionPresent = redemptionReceipts.has(identity);
1107
+ const exactMemberCustody = direction === 'lan-to-cloud'
1108
+ ? claimPresent && !envelopePresent
1109
+ : !claimPresent && envelopePresent !== redemptionPresent;
1110
+ if (memberCustodyRequired !== exactMemberCustody) {
1111
+ throw invalidPayload('records');
1112
+ }
1113
+ }
1114
+ }
1115
+ if (recovery.value.targetEvidence !== null && (recovery.value.targetHostMemberId === null
1116
+ || principalBindings.get(recovery.value.targetHostMemberId)
1117
+ !== recovery.value.targetEvidence.principalId))
1118
+ throw invalidPayload('records');
1119
+ if (lifecycle.value.operationKind !== 'authority-transfer'
1120
+ || relinquishmentRequired === undefined
1121
+ || relinquishmentRequired !== (proof !== null)
1122
+ || (lifecycle.value.state === 'cancelled') !== (lifecycle.value.phase === 'cancelled')
1123
+ || (lifecycle.value.state === 'completed') !== (lifecycle.value.phase === 'completed')
1124
+ || (lifecycle.value.state === 'active'
1125
+ && (lifecycle.value.phase === 'cancelled' || lifecycle.value.phase === 'completed'))
1126
+ || lifecycle.value.direction !== (recovery.value.sourceAuthority.kind === 'cloud'
1127
+ ? 'cloud-to-lan'
1128
+ : 'lan-to-cloud')
1129
+ || lifecycle.value.expectedAuthorityGeneration !== recovery.value.sourceAuthority.generation
1130
+ || lifecycle.value.scheduledAt !== recovery.value.expiresAt
1131
+ || (recovery.value.sourceHostMemberId !== null
1132
+ && !members.has(recovery.value.sourceHostMemberId))
1133
+ || (recovery.value.targetHostMemberId !== null
1134
+ && !members.has(recovery.value.targetHostMemberId))
1135
+ || (proof !== null && (proof.batchRevision !== lifecycle.value.batchRevision
1136
+ || proof.batchSha256 !== lifecycle.value.batchSha256
1137
+ || proof.checkpointSha256 !== lifecycle.value.checkpointSha256
1138
+ || (batchReceipt !== undefined && (proof.batchRevision !== batchReceipt.batchRevision
1139
+ || proof.batchSha256 !== batchReceipt.batchSha256
1140
+ || proof.checkpointSha256 !== batchReceipt.checkpointSha256))))
1141
+ || (recovery.value.sourceEvidence !== null
1142
+ && (recovery.value.sourceEvidence.checkpointManifestSha256
1143
+ !== lifecycle.value.checkpointSha256
1144
+ || recovery.value.sourceHostMemberId === null
1145
+ || principalBindings.get(recovery.value.sourceHostMemberId)
1146
+ !== recovery.value.sourceEvidence.principalId))
1147
+ || (evidence !== null && (receiptKey === undefined
1148
+ || receiptKey.value.receiptPublicKey !== evidence.receiptPublicKey)))
1149
+ throw invalidPayload('records');
1150
+ }
1151
+ for (const key of keys.values()) {
1152
+ const recovery = recoveries.get(key.value.transferId);
1153
+ const evidence = recovery?.value.sourceEvidence ?? recovery?.value.targetEvidence;
1154
+ if (recovery === undefined
1155
+ || evidence === null
1156
+ || evidence === undefined
1157
+ || evidence.receiptKeyId !== key.value.receiptKeyId
1158
+ || evidence.receiptPublicKey !== key.value.receiptPublicKey)
1159
+ throw invalidPayload('records');
1160
+ }
1161
+ for (const claim of claims.values()) {
1162
+ const lifecycle = lifecycles.get(claim.value.transferId);
1163
+ const recovery = recoveries.get(claim.value.transferId);
1164
+ const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
1165
+ if (memberRecords.get(claim.value.memberId)?.value.status !== 'active'
1166
+ || recovery === undefined
1167
+ || recovery.value.sourceAuthority.kind !== 'lan'
1168
+ || claim.value.memberId === recovery.value.sourceHostMemberId
1169
+ || lifecycle?.value.batchRevision !== claim.value.batchRevision
1170
+ || lifecycle.value.checkpointSha256 !== claim.value.checkpointSha256
1171
+ || claim.value.expiresAt !== recovery.value.expiresAt
1172
+ || (claim.value.targetPrincipalId !== null
1173
+ && principalBindings.get(claim.value.memberId) !== claim.value.targetPrincipalId)
1174
+ || (claim.value.state === 'redeemed') !== (redemptionReceipt !== undefined)) {
1175
+ throw invalidPayload('records');
1176
+ }
1177
+ }
1178
+ for (const terminal of terminalResponders.values()) {
1179
+ const operationKind = terminal.value.operation === 'getProjectAuthorityTransfer'
1180
+ ? 'authority-transfer'
1181
+ : 'retire';
1182
+ const principals = terminal.value.eligibleMemberIds.map(memberId => (terminalPrincipals.get(`${terminal.value.operationId}:${memberId}`)));
1183
+ if (principals.some(item => item === undefined)
1184
+ || principals.some(item => item?.value.operationKind !== operationKind)
1185
+ || new Set(principals.map(item => item?.value.principalId)).size !== principals.length
1186
+ || (operationKind === 'authority-transfer')
1187
+ !== responderReplays.has(terminal.value.operationId))
1188
+ throw invalidPayload('records');
1189
+ }
1190
+ for (const item of records) {
1191
+ if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
1192
+ throw invalidPayload('records');
1193
+ }
1194
+ if (item.kind === 'protected-claim-envelope') {
1195
+ const identity = `${item.value.transferId}:${item.value.memberId}`;
1196
+ const recovery = recoveries.get(item.value.transferId);
1197
+ const lifecycle = lifecycles.get(item.value.transferId);
1198
+ if (item.recordId !== identity
1199
+ || recovery === undefined
1200
+ || lifecycle === undefined
1201
+ || memberRecords.get(item.value.memberId)?.value.status !== 'active'
1202
+ || item.value.memberId === recovery.value.targetHostMemberId
1203
+ || item.value.associatedData.authorityGeneration
1204
+ !== recovery.value.sourceAuthority.generation
1205
+ || recovery.value.sourceAuthority.kind !== 'cloud'
1206
+ || item.value.associatedData.checkpointSha256 !== lifecycle.value.checkpointSha256
1207
+ || item.value.expiresAt !== recovery.value.expiresAt
1208
+ || !keys.has(`${item.value.transferId}:${item.value.receiptKeyId}`)
1209
+ || claims.has(identity)
1210
+ || redemptionReceipts.has(identity))
1211
+ throw invalidPayload('records');
1212
+ }
1213
+ if (item.kind === 'transfer-claim-batch-receipt') {
1214
+ const lifecycle = lifecycles.get(item.value.receipt.transferId);
1215
+ const recovery = recoveries.get(item.value.receipt.transferId);
1216
+ if (lifecycle === undefined
1217
+ || recovery === undefined
1218
+ || lifecycle.value.batchRevision !== item.value.receipt.batchRevision
1219
+ || lifecycle.value.batchSha256 !== item.value.receipt.batchSha256
1220
+ || lifecycle.value.checkpointSha256 !== item.value.receipt.checkpointSha256
1221
+ || item.value.receipt.custodyAuthority.kind
1222
+ !== recovery.value.sourceAuthority.kind
1223
+ || item.value.receipt.custodyAuthority.generation
1224
+ !== recovery.value.sourceAuthority.generation
1225
+ || item.value.receipt.targetAuthorityGeneration
1226
+ !== recovery.value.targetAuthority.generation
1227
+ || item.value.receipt.submittedByMemberId !== (recovery.value.sourceAuthority.kind === 'lan'
1228
+ ? recovery.value.sourceHostMemberId
1229
+ : recovery.value.targetHostMemberId)
1230
+ || !members.has(item.value.receipt.submittedByMemberId))
1231
+ throw invalidPayload('records');
1232
+ }
1233
+ if (item.kind === 'transfer-redemption-receipt') {
1234
+ const receipt = item.value.receipt;
1235
+ const identity = `${receipt.transferId}:${receipt.memberId}`;
1236
+ const claim = claims.get(identity);
1237
+ const recovery = recoveries.get(receipt.transferId);
1238
+ const lifecycle = lifecycles.get(receipt.transferId);
1239
+ const terminalPrincipal = terminalPrincipals.get(identity);
1240
+ const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
1241
+ if (recovery === undefined
1242
+ || lifecycle === undefined
1243
+ || memberRecords.get(receipt.memberId)?.value.status !== 'active'
1244
+ || receipt.targetAuthorityGeneration !== recovery.value.targetAuthority.generation
1245
+ || !keys.has(`${receipt.transferId}:${receipt.receiptKeyId}`)
1246
+ || Date.parse(receipt.redeemedAt) > Date.parse(recovery.value.expiresAt)
1247
+ || (lanToCloud && (item.value.acknowledgedAt !== null
1248
+ || claim === undefined
1249
+ || claim.value.claimSha256 !== receipt.claimSha256
1250
+ || claim.value.checkpointSha256 !== receipt.checkpointSha256
1251
+ || claim.value.operationIntentId !== receipt.operationIntentId
1252
+ || claim.value.redemptionReceiptId !== receipt.receiptId))
1253
+ || (!lanToCloud && (item.value.acknowledgedAt === null
1254
+ || receipt.memberId === recovery.value.targetHostMemberId
1255
+ || receipt.checkpointSha256 !== lifecycle.value.checkpointSha256
1256
+ || claim !== undefined
1257
+ || protectedEnvelopes.has(identity)
1258
+ || terminalPrincipal?.value.acknowledgedAt !== item.value.acknowledgedAt)))
1259
+ throw invalidPayload('records');
1260
+ }
1261
+ if (item.kind === 'terminal-principal') {
1262
+ const terminal = terminalResponders.get(item.value.operationId);
1263
+ const acknowledgement = terminal?.value.acknowledgements.find(value => (value.memberId === item.value.memberId));
1264
+ const expectedOperationKind = terminal?.value.operation === 'getProjectAuthorityTransfer'
1265
+ ? 'authority-transfer'
1266
+ : terminal?.value.operation === 'retireProject'
1267
+ ? 'retire'
1268
+ : undefined;
1269
+ if (!members.has(item.value.memberId)
1270
+ || terminal === undefined
1271
+ || expectedOperationKind !== item.value.operationKind
1272
+ || !terminal.value.eligibleMemberIds.includes(item.value.memberId)
1273
+ || (item.value.acknowledgedAt === null) !== (acknowledgement === undefined)
1274
+ || (acknowledgement !== undefined && (acknowledgement.acknowledgedAt !== item.value.acknowledgedAt
1275
+ || acknowledgement.principalId !== item.value.principalId))
1276
+ || (item.value.acknowledgedAt !== null
1277
+ && Date.parse(item.value.acknowledgedAt) > Date.parse(terminal.value.expiresAt)))
1278
+ throw invalidPayload('records');
1279
+ }
1280
+ if (item.kind === 'terminal-responder-replay') {
1281
+ const lifecycle = lifecycles.get(item.value.operationId);
1282
+ const terminal = terminalResponders.get(item.value.operationId);
1283
+ const recovery = recoveries.get(item.value.operationId);
1284
+ let response;
1285
+ try {
1286
+ response = terminal?.value.operation === 'getProjectAuthorityTransfer'
1287
+ ? collabControlOperationCodec('getProjectAuthorityTransfer').decodeResponse(JSON.parse(terminal.value.responseJson))
1288
+ : undefined;
1289
+ }
1290
+ catch {
1291
+ throw invalidPayload('records');
1292
+ }
1293
+ if (!members.has(item.value.memberId)
1294
+ || lifecycle?.value.operationKind !== 'authority-transfer'
1295
+ || lifecycle.value.direction !== 'cloud-to-lan'
1296
+ || lifecycle.value.state !== 'completed'
1297
+ || terminal === undefined
1298
+ || recovery === undefined
1299
+ || item.value.memberId !== recovery.value.targetHostMemberId
1300
+ || item.value.requestSha256 !== recovery.value.targetActivationRequestSha256
1301
+ || !terminal.value.eligibleMemberIds.includes(item.value.memberId)
1302
+ || response === undefined
1303
+ || response.batchRevision !== lifecycle.value.batchRevision
1304
+ || response.batchSha256 !== lifecycle.value.batchSha256
1305
+ || response.checkpointSha256 !== lifecycle.value.checkpointSha256
1306
+ || response.direction !== lifecycle.value.direction
1307
+ || response.expiresAt !== recovery.value.expiresAt
1308
+ || response.targetUrl !== recovery.value.targetUrl
1309
+ || JSON.stringify(response.relinquishmentProof)
1310
+ !== JSON.stringify(recovery.value.relinquishmentProof)
1311
+ || response.sourceAuthority.kind !== recovery.value.sourceAuthority.kind
1312
+ || response.sourceAuthority.generation !== recovery.value.sourceAuthority.generation
1313
+ || response.targetAuthority.kind !== recovery.value.targetAuthority.kind
1314
+ || response.targetAuthority.generation !== recovery.value.targetAuthority.generation)
1315
+ throw invalidPayload('records');
1316
+ }
1317
+ if (item.kind === 'leave-former-principal-replay') {
1318
+ const lifecycle = lifecycles.get(item.value.operationId);
1319
+ if (!members.has(item.value.memberId)
1320
+ || lifecycle?.value.operationKind !== 'leave'
1321
+ || lifecycle.value.actorMemberId !== item.value.memberId
1322
+ || lifecycle.value.expectedPersonalRefOid !== item.value.expectedPersonalRefOid
1323
+ || lifecycle.value.idempotencyKey !== item.value.intentId
1324
+ || lifecycle.value.requestFingerprint !== item.value.requestFingerprint
1325
+ || lifecycle.value.resultSha256 !== item.value.resultSha256
1326
+ || (item.value.state === 'completed' && (lifecycle.value.state !== 'completed'
1327
+ || item.value.completedAt !== lifecycle.value.updatedAt))
1328
+ || (item.value.state === 'recovering' && (lifecycle.value.state !== 'active'
1329
+ && lifecycle.value.state !== 'recovery-required')))
1330
+ throw invalidPayload('records');
1331
+ }
1332
+ if (item.kind === 'tombstone') {
1333
+ const transferTerminal = [...terminalResponders.values()].find(value => (value.value.operation === 'getProjectAuthorityTransfer'));
1334
+ const lifecycle = transferTerminal === undefined
1335
+ ? undefined
1336
+ : lifecycles.get(transferTerminal.value.operationId);
1337
+ const recovery = transferTerminal === undefined
1338
+ ? undefined
1339
+ : recoveries.get(transferTerminal.value.operationId);
1340
+ if (transferTerminal !== undefined && (lifecycle === undefined
1341
+ || recovery === undefined
1342
+ || item.value.authorityGeneration !== recovery.value.targetAuthority.generation
1343
+ || item.value.retiredAt !== lifecycle.value.updatedAt))
1344
+ throw invalidPayload('records');
1345
+ }
1346
+ }
1347
+ }
1348
+ function manifestWithFormatOne(value) {
1349
+ const source = record(value, 'manifest');
1350
+ if (source.profile !== 'backup'
1351
+ || source.coordinationFormatVersion !== COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION)
1352
+ throw invalidPayload('manifest');
1353
+ return decodeCollabProjectCheckpointManifest({
1354
+ ...source,
1355
+ coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
1356
+ });
1357
+ }
1358
+ export function decodeCollabProjectBackupCheckpointManifest(value) {
1359
+ const decoded = manifestWithFormatOne(value);
1360
+ return {
1361
+ artifacts: decoded.artifacts,
1362
+ coordinationFormatVersion: COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION,
1363
+ createdAt: decoded.createdAt,
1364
+ expectedMainOid: decoded.expectedMainOid,
1365
+ gitObjectFormat: decoded.gitObjectFormat,
1366
+ manifestSchemaVersion: decoded.manifestSchemaVersion,
1367
+ manifestSha256: decoded.manifestSha256,
1368
+ operationId: decoded.operationId,
1369
+ profile: 'backup',
1370
+ projectId: decoded.projectId,
1371
+ protocolVersion: decoded.protocolVersion,
1372
+ refs: decoded.refs,
1373
+ sourceAuthority: decoded.sourceAuthority,
1374
+ targetAuthority: decoded.targetAuthority,
1375
+ };
1376
+ }
1377
+ export function encodeCollabProjectBackupCheckpointManifestCanonicalJson(manifest) {
1378
+ return JSON.stringify(decodeCollabProjectBackupCheckpointManifest(manifest));
1379
+ }
1380
+ export function encodeCollabProjectBackupCheckpointManifestDigestInput(manifest) {
1381
+ const decoded = decodeCollabProjectBackupCheckpointManifest(manifest);
1382
+ return JSON.stringify({
1383
+ artifacts: decoded.artifacts,
1384
+ coordinationFormatVersion: decoded.coordinationFormatVersion,
1385
+ createdAt: decoded.createdAt,
1386
+ expectedMainOid: decoded.expectedMainOid,
1387
+ gitObjectFormat: decoded.gitObjectFormat,
1388
+ manifestSchemaVersion: decoded.manifestSchemaVersion,
1389
+ operationId: decoded.operationId,
1390
+ profile: decoded.profile,
1391
+ projectId: decoded.projectId,
1392
+ protocolVersion: decoded.protocolVersion,
1393
+ refs: decoded.refs,
1394
+ sourceAuthority: decoded.sourceAuthority,
1395
+ targetAuthority: decoded.targetAuthority,
1396
+ });
1397
+ }
1398
+ export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
1399
+ if (typeof value !== 'string'
1400
+ || !value.endsWith('\n')
1401
+ || !hasUtf8ByteLengthAtMost(value, COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxCoordinationBytes))
1402
+ throw invalidPayload('coordination');
1403
+ const lines = value.slice(0, -1).split('\n');
1404
+ if (lines.some(line => line.length === 0))
1405
+ throw invalidPayload('coordination');
1406
+ const parsed = lines.map((line) => {
1407
+ let item;
1408
+ try {
1409
+ item = JSON.parse(line);
1410
+ }
1411
+ catch {
1412
+ throw invalidPayload('coordination');
1413
+ }
1414
+ if (JSON.stringify(item) !== line)
1415
+ throw invalidPayload('coordination');
1416
+ return { envelope: recordEnvelope(item), item };
1417
+ });
1418
+ const baseItems = parsed.filter(({ envelope }) => (!CONTINUITY_KIND_SET.has(envelope.kind) && envelope.kind !== 'idempotency-result'));
1419
+ const baseDecoded = decodeCollabProjectCheckpointCoordinationNdjson(baseItems.map(({ item }) => JSON.stringify(sanitizeBackupBasePrincipalRecord(item))).join('\n') + '\n', 'backup');
1420
+ const baseByIdentity = new Map(baseDecoded.map(item => [`${item.kind}\0${item.recordId}`, item]));
1421
+ const decoded = parsed.map(({ envelope }) => {
1422
+ if (envelope.kind === 'idempotency-result') {
1423
+ return idempotencyResultRecord(envelope.source, envelope.recordId, envelope.revision);
1424
+ }
1425
+ if (CONTINUITY_KIND_SET.has(envelope.kind)) {
1426
+ return decodeContinuityRecord(envelope.kind, envelope.source, envelope.recordId, envelope.revision);
1427
+ }
1428
+ const base = baseByIdentity.get(`${envelope.kind}\0${envelope.recordId}`);
1429
+ if (base === undefined)
1430
+ throw invalidPayload('records');
1431
+ return restoreBackupBasePrincipals(envelope.source, base);
1432
+ });
1433
+ if (decoded.some((item, index) => JSON.stringify(item) !== lines[index])) {
1434
+ throw invalidPayload('coordination');
1435
+ }
1436
+ validateContinuity(decoded);
1437
+ return Object.freeze(decoded);
1438
+ }
1439
+ export function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
1440
+ const value = records.map(item => JSON.stringify(item)).join('\n') + '\n';
1441
+ return decodeCollabProjectBackupCheckpointCoordinationNdjson(value)
1442
+ .map(item => JSON.stringify(item)).join('\n') + '\n';
1443
+ }
1444
+ export function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
1445
+ const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
1446
+ const decodedRecords = decodeCollabProjectBackupCheckpointCoordinationNdjson(records.map(item => JSON.stringify(item)).join('\n') + '\n');
1447
+ if (decodedRecords.some(item => {
1448
+ switch (item.kind) {
1449
+ case 'lifecycle-journal': return item.value.operationId === decodedManifest.operationId;
1450
+ case 'authority-transfer-recovery':
1451
+ case 'transferred-membership-claim':
1452
+ case 'transfer-receipt-key': return item.value.transferId === decodedManifest.operationId;
1453
+ case 'transfer-claim-batch-receipt':
1454
+ return item.value.receipt.transferId === decodedManifest.operationId;
1455
+ case 'transfer-redemption-receipt':
1456
+ return item.value.receipt.transferId === decodedManifest.operationId;
1457
+ case 'terminal-principal':
1458
+ case 'terminal-responder-replay':
1459
+ case 'leave-former-principal-replay':
1460
+ case 'terminal-responder': return item.value.operationId === decodedManifest.operationId;
1461
+ case 'protected-claim-envelope':
1462
+ return item.value.transferId === decodedManifest.operationId;
1463
+ default: return false;
1464
+ }
1465
+ }))
1466
+ throw invalidPayload('records');
1467
+ const baseRecords = decodedRecords.filter(item => (!CONTINUITY_KIND_SET.has(item.kind) && item.kind !== 'idempotency-result'));
1468
+ validateCollabProjectCheckpointConsistency(decodeCollabProjectCheckpointManifest({
1469
+ ...decodedManifest,
1470
+ coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
1471
+ }), baseRecords);
1472
+ return records;
1473
+ }