@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,1326 @@
1
+ import { decodeCollabAuthorityRelinquishmentProof, decodeCollabAuthorityTransferLifecycleFence, } from './CollabAuthorityTransfer.mjs';
2
+ import { decodeCollabCloudProjectEventMessage, } from './CollabCloudProjectEvent.mjs';
3
+ import { COLLAB_MAIN_REF, COLLAB_MEMBER_REF_PREFIX, COLLAB_PROTOCOL_VERSION, } from './CollabConstants.mjs';
4
+ import { COLLAB_CONTROL_OPERATION_CODECS, collabControlOperationCodec, } from './CollabControlOperationCodecs.mjs';
5
+ import { CollabError } from './CollabError.mjs';
6
+ import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
7
+ export const COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION = 1;
8
+ export const COLLAB_PROJECT_COORDINATION_FORMAT_VERSION = 1;
9
+ export const COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION = 1;
10
+ export const COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS = Object.freeze({
11
+ maxCiphertextBytes: 4096,
12
+ nonceBytes: 24,
13
+ tagBytes: 16,
14
+ });
15
+ export const COLLAB_CHECKPOINT_PROFILES = Object.freeze([
16
+ 'authority-transfer',
17
+ 'backup',
18
+ 'export',
19
+ ]);
20
+ export const COLLAB_PROJECT_CHECKPOINT_ARTIFACTS = Object.freeze([
21
+ 'checkpoint.json',
22
+ 'coordination.ndjson',
23
+ 'repository.bundle',
24
+ ]);
25
+ export const COLLAB_CHECKPOINT_ARTIFACT_LIMITS = Object.freeze({
26
+ maxCoordinationBytes: 256 * 1024 * 1024,
27
+ maxManifestBytes: 64 * 1024,
28
+ maxRepositoryBundleBytes: 1024 * 1024 * 1024,
29
+ maxStagingBytes: 2 * 1024 * 1024 * 1024,
30
+ });
31
+ export const COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS = Object.freeze([
32
+ 'project',
33
+ 'member',
34
+ 'request',
35
+ 'request-comment',
36
+ 'ticket',
37
+ 'ticket-comment',
38
+ 'ticket-relation',
39
+ 'ticket-mention',
40
+ ]);
41
+ export const COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS = Object.freeze([
42
+ ...COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS,
43
+ 'cloud-event',
44
+ 'cloud-event-cursor',
45
+ 'idempotency-result',
46
+ 'principal-binding',
47
+ 'repository-placement',
48
+ 'lifecycle-state',
49
+ 'terminal-responder',
50
+ 'protected-claim-envelope',
51
+ 'tombstone',
52
+ 'schema-catalog',
53
+ 'server-compatibility',
54
+ 'authority-volume-pair',
55
+ ]);
56
+ const CHECKPOINT_PROFILE_SET = new Set(COLLAB_CHECKPOINT_PROFILES);
57
+ const PORTABLE_RECORD_KIND_SET = new Set(COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS);
58
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
59
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
60
+ const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
61
+ const PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET = new Set([
62
+ 'getTransferredMembershipClaim',
63
+ 'rotateTransferredMembershipClaims',
64
+ ]);
65
+ function gitOidMatchesFormat(oid, format) {
66
+ return oid.length === (format === 'sha1' ? 40 : 64);
67
+ }
68
+ function invalidPayload(field) {
69
+ return new CollabError({ code: 'protocol-payload-invalid', safeContext: { field } });
70
+ }
71
+ function record(value, field) {
72
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
73
+ throw invalidPayload(field);
74
+ }
75
+ return value;
76
+ }
77
+ function exactRecord(value, field, keys) {
78
+ const source = record(value, field);
79
+ const expected = new Set(keys);
80
+ if (!keys.every(key => Object.hasOwn(source, key))
81
+ || Object.keys(source).some(key => !expected.has(key)))
82
+ throw invalidPayload(field);
83
+ return source;
84
+ }
85
+ function token(source, field, validate = isCollabOpaqueId) {
86
+ const value = source[field];
87
+ if (typeof value !== 'string' || !validate(value))
88
+ throw invalidPayload(field);
89
+ return value;
90
+ }
91
+ function boundedString(source, field, maximumBytes, allowEmpty = false) {
92
+ const value = source[field];
93
+ if (typeof value !== 'string'
94
+ || (!allowEmpty && value.length === 0)
95
+ || !hasUtf8ByteLengthAtMost(value, maximumBytes))
96
+ throw invalidPayload(field);
97
+ return value;
98
+ }
99
+ function positiveInteger(source, field, maximum) {
100
+ const value = source[field];
101
+ if (typeof value !== 'number'
102
+ || !Number.isSafeInteger(value)
103
+ || value < 1
104
+ || (maximum !== undefined && value > maximum))
105
+ throw invalidPayload(field);
106
+ return value;
107
+ }
108
+ function nonNegativeInteger(source, field) {
109
+ const value = source[field];
110
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
111
+ throw invalidPayload(field);
112
+ }
113
+ return value;
114
+ }
115
+ function timestampValue(value, field) {
116
+ if (typeof value !== 'string'
117
+ || value.length > 64
118
+ || Number.isNaN(Date.parse(value))
119
+ || new Date(value).toISOString() !== value)
120
+ throw invalidPayload(field);
121
+ return value;
122
+ }
123
+ function timestamp(source, field) {
124
+ return timestampValue(source[field], field);
125
+ }
126
+ function nullableTimestamp(source, field) {
127
+ return source[field] === null ? null : timestampValue(source[field], field);
128
+ }
129
+ function canonicalBase64url(source, field, maximumDecodedBytes, exactDecodedBytes) {
130
+ const value = boundedString(source, field, Math.ceil(maximumDecodedBytes * 4 / 3));
131
+ if (!BASE64URL_PATTERN.test(value))
132
+ throw invalidPayload(field);
133
+ const remainder = value.length % 4;
134
+ const finalIndex = BASE64URL_ALPHABET.indexOf(value[value.length - 1]);
135
+ const decodedBytes = Math.floor(value.length * 6 / 8);
136
+ if (remainder === 1
137
+ || (remainder === 2 && (finalIndex & 15) !== 0)
138
+ || (remainder === 3 && (finalIndex & 3) !== 0)
139
+ || decodedBytes > maximumDecodedBytes
140
+ || (exactDecodedBytes !== undefined && decodedBytes !== exactDecodedBytes))
141
+ throw invalidPayload(field);
142
+ return value;
143
+ }
144
+ function sha256(source, field) {
145
+ const value = source[field];
146
+ if (typeof value !== 'string' || !SHA256_PATTERN.test(value))
147
+ throw invalidPayload(field);
148
+ return value;
149
+ }
150
+ function literal(source, field, values) {
151
+ const value = source[field];
152
+ if (typeof value !== 'string' || !values.includes(value))
153
+ throw invalidPayload(field);
154
+ return value;
155
+ }
156
+ function controlOperation(source, field) {
157
+ const value = source[field];
158
+ if (typeof value !== 'string' || !Object.hasOwn(COLLAB_CONTROL_OPERATION_CODECS, value)) {
159
+ throw invalidPayload(field);
160
+ }
161
+ return value;
162
+ }
163
+ function canonicalOperationResponseJson(source, field, operation) {
164
+ if (PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET.has(operation))
165
+ throw invalidPayload(field);
166
+ const value = boundedString(source, field, 512 * 1024, true);
167
+ let decoded;
168
+ try {
169
+ const parsed = JSON.parse(value);
170
+ const operationResponse = collabControlOperationCodec(operation).decodeResponse(parsed);
171
+ if (JSON.stringify(operationResponse) !== value)
172
+ throw invalidPayload(field);
173
+ decoded = record(operationResponse, field);
174
+ }
175
+ catch {
176
+ throw invalidPayload(field);
177
+ }
178
+ return { decoded, responseJson: value };
179
+ }
180
+ function authority(value, field) {
181
+ const source = exactRecord(value, field, ['generation', 'kind']);
182
+ return {
183
+ generation: positiveInteger(source, 'generation'),
184
+ kind: literal(source, 'kind', ['cloud', 'lan']),
185
+ };
186
+ }
187
+ function artifact(value) {
188
+ const source = exactRecord(value, 'artifact', ['byteCount', 'name', 'sha256']);
189
+ const name = literal(source, 'name', ['coordination.ndjson', 'repository.bundle']);
190
+ const maximum = name === 'coordination.ndjson'
191
+ ? COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxCoordinationBytes
192
+ : COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxRepositoryBundleBytes;
193
+ return {
194
+ byteCount: positiveInteger(source, 'byteCount', maximum),
195
+ name,
196
+ sha256: sha256(source, 'sha256'),
197
+ };
198
+ }
199
+ function artifacts(value) {
200
+ if (!Array.isArray(value) || value.length !== 2)
201
+ throw invalidPayload('artifacts');
202
+ const decoded = value.map(artifact);
203
+ if (decoded[0].name !== 'coordination.ndjson'
204
+ || decoded[1].name !== 'repository.bundle'
205
+ || decoded[0].byteCount + decoded[1].byteCount
206
+ > COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxStagingBytes)
207
+ throw invalidPayload('artifacts');
208
+ return Object.freeze(decoded);
209
+ }
210
+ function gitRef(value) {
211
+ const source = exactRecord(value, 'ref', ['name', 'oid']);
212
+ const name = boundedString(source, 'name', 512);
213
+ if (name !== COLLAB_MAIN_REF && !name.startsWith(COLLAB_MEMBER_REF_PREFIX)) {
214
+ throw invalidPayload('name');
215
+ }
216
+ const oid = token(source, 'oid', isCollabGitOid);
217
+ return { name, oid };
218
+ }
219
+ function gitRefs(value, expectedMainOid) {
220
+ if (!Array.isArray(value) || value.length === 0)
221
+ throw invalidPayload('refs');
222
+ const decoded = value.map(gitRef);
223
+ if (decoded[0].name !== COLLAB_MAIN_REF
224
+ || decoded[0].oid !== expectedMainOid
225
+ || decoded.some((item, index) => index > 0
226
+ && decoded[index - 1].name.localeCompare(item.name, 'en-US') >= 0))
227
+ throw invalidPayload('refs');
228
+ return Object.freeze(decoded);
229
+ }
230
+ function manifestObject(source, includeManifestSha256) {
231
+ const profile = literal(source, 'profile', COLLAB_CHECKPOINT_PROFILES);
232
+ const expectedMainOid = token(source, 'expectedMainOid', isCollabGitOid);
233
+ const gitObjectFormat = literal(source, 'gitObjectFormat', ['sha1', 'sha256']);
234
+ const refs = gitRefs(source.refs, expectedMainOid);
235
+ if (!gitOidMatchesFormat(expectedMainOid, gitObjectFormat)
236
+ || refs.some(ref => !gitOidMatchesFormat(ref.oid, gitObjectFormat)))
237
+ throw invalidPayload('gitObjectFormat');
238
+ const sourceAuthority = authority(source.sourceAuthority, 'sourceAuthority');
239
+ const targetAuthority = source.targetAuthority === null
240
+ ? null
241
+ : authority(source.targetAuthority, 'targetAuthority');
242
+ if (profile === 'authority-transfer') {
243
+ if (targetAuthority === null
244
+ || targetAuthority.kind === sourceAuthority.kind
245
+ || targetAuthority.generation !== sourceAuthority.generation + 1)
246
+ throw invalidPayload('targetAuthority');
247
+ }
248
+ else if (targetAuthority !== null) {
249
+ throw invalidPayload('targetAuthority');
250
+ }
251
+ const common = {
252
+ artifacts: artifacts(source.artifacts),
253
+ coordinationFormatVersion: source.coordinationFormatVersion,
254
+ createdAt: timestamp(source, 'createdAt'),
255
+ expectedMainOid,
256
+ gitObjectFormat,
257
+ manifestSchemaVersion: source.manifestSchemaVersion,
258
+ operationId: token(source, 'operationId'),
259
+ profile,
260
+ projectId: token(source, 'projectId', isCollabProjectId),
261
+ protocolVersion: source.protocolVersion,
262
+ refs,
263
+ sourceAuthority,
264
+ targetAuthority,
265
+ };
266
+ if (common.coordinationFormatVersion !== COLLAB_PROJECT_COORDINATION_FORMAT_VERSION
267
+ || common.manifestSchemaVersion !== COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION
268
+ || common.protocolVersion !== COLLAB_PROTOCOL_VERSION)
269
+ throw invalidPayload('manifest');
270
+ if (!includeManifestSha256)
271
+ return common;
272
+ return {
273
+ artifacts: common.artifacts,
274
+ coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
275
+ createdAt: common.createdAt,
276
+ expectedMainOid: common.expectedMainOid,
277
+ gitObjectFormat: common.gitObjectFormat,
278
+ manifestSchemaVersion: COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION,
279
+ manifestSha256: sha256(source, 'manifestSha256'),
280
+ operationId: common.operationId,
281
+ profile: common.profile,
282
+ projectId: common.projectId,
283
+ protocolVersion: COLLAB_PROTOCOL_VERSION,
284
+ refs: common.refs,
285
+ sourceAuthority: common.sourceAuthority,
286
+ targetAuthority: common.targetAuthority,
287
+ };
288
+ }
289
+ export function decodeCollabProjectCheckpointManifest(value) {
290
+ const source = exactRecord(value, 'manifest', [
291
+ 'artifacts',
292
+ 'coordinationFormatVersion',
293
+ 'createdAt',
294
+ 'expectedMainOid',
295
+ 'gitObjectFormat',
296
+ 'manifestSchemaVersion',
297
+ 'manifestSha256',
298
+ 'operationId',
299
+ 'profile',
300
+ 'projectId',
301
+ 'protocolVersion',
302
+ 'refs',
303
+ 'sourceAuthority',
304
+ 'targetAuthority',
305
+ ]);
306
+ const decoded = manifestObject(source, true);
307
+ const encoded = JSON.stringify(decoded);
308
+ if (!hasUtf8ByteLengthAtMost(encoded, COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxManifestBytes)) {
309
+ throw invalidPayload('manifest');
310
+ }
311
+ return decoded;
312
+ }
313
+ export function encodeCollabProjectCheckpointManifestCanonicalJson(manifest) {
314
+ return JSON.stringify(decodeCollabProjectCheckpointManifest(manifest));
315
+ }
316
+ export function encodeCollabProjectCheckpointManifestDigestInput(manifest) {
317
+ const decoded = decodeCollabProjectCheckpointManifest(manifest);
318
+ const source = {
319
+ artifacts: decoded.artifacts,
320
+ coordinationFormatVersion: decoded.coordinationFormatVersion,
321
+ createdAt: decoded.createdAt,
322
+ expectedMainOid: decoded.expectedMainOid,
323
+ gitObjectFormat: decoded.gitObjectFormat,
324
+ manifestSchemaVersion: decoded.manifestSchemaVersion,
325
+ operationId: decoded.operationId,
326
+ profile: decoded.profile,
327
+ projectId: decoded.projectId,
328
+ protocolVersion: decoded.protocolVersion,
329
+ refs: decoded.refs,
330
+ sourceAuthority: decoded.sourceAuthority,
331
+ targetAuthority: decoded.targetAuthority,
332
+ };
333
+ return JSON.stringify(source);
334
+ }
335
+ function recordEnvelope(value) {
336
+ const source = exactRecord(value, 'record', ['kind', 'recordId', 'revision', 'value']);
337
+ const kind = literal(source, 'kind', COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS);
338
+ const recordId = boundedString(source, 'recordId', 256);
339
+ const revision = positiveInteger(source, 'revision');
340
+ return { kind, recordId, revision, source };
341
+ }
342
+ function projectRecord(source, recordId, revision) {
343
+ const value = exactRecord(source.value, 'value', [
344
+ 'activatedAt',
345
+ 'authorityGeneration',
346
+ 'createdAt',
347
+ 'expectedMainOid',
348
+ 'managerSetGeneration',
349
+ 'name',
350
+ 'projectId',
351
+ ]);
352
+ const projectId = token(value, 'projectId', isCollabProjectId);
353
+ if (recordId !== projectId)
354
+ throw invalidPayload('recordId');
355
+ const createdAt = timestamp(value, 'createdAt');
356
+ const activatedAt = timestamp(value, 'activatedAt');
357
+ if (Date.parse(activatedAt) < Date.parse(createdAt))
358
+ throw invalidPayload('activatedAt');
359
+ return {
360
+ kind: 'project',
361
+ recordId,
362
+ revision,
363
+ value: {
364
+ activatedAt,
365
+ authorityGeneration: positiveInteger(value, 'authorityGeneration'),
366
+ createdAt,
367
+ expectedMainOid: token(value, 'expectedMainOid', isCollabGitOid),
368
+ managerSetGeneration: nonNegativeInteger(value, 'managerSetGeneration'),
369
+ name: boundedString(value, 'name', 1024),
370
+ projectId,
371
+ },
372
+ };
373
+ }
374
+ function memberRecord(source, recordId, revision) {
375
+ const value = exactRecord(source.value, 'value', [
376
+ 'activatedAt',
377
+ 'createdAt',
378
+ 'displayName',
379
+ 'memberId',
380
+ 'personalRef',
381
+ 'projectId',
382
+ 'role',
383
+ 'status',
384
+ 'revokedAt',
385
+ 'updatedAt',
386
+ ]);
387
+ const memberId = token(value, 'memberId', isCollabMemberId);
388
+ if (recordId !== memberId)
389
+ throw invalidPayload('recordId');
390
+ const personalRef = boundedString(value, 'personalRef', 512);
391
+ if (personalRef !== `${COLLAB_MEMBER_REF_PREFIX}${memberId}`)
392
+ throw invalidPayload('personalRef');
393
+ const activatedAt = nullableTimestamp(value, 'activatedAt');
394
+ const revokedAt = nullableTimestamp(value, 'revokedAt');
395
+ const createdAt = timestamp(value, 'createdAt');
396
+ const updatedAt = timestamp(value, 'updatedAt');
397
+ const status = literal(value, 'status', ['active', 'left', 'revoked']);
398
+ if ((status === 'active' && (activatedAt === null || revokedAt !== null))
399
+ || (status !== 'active' && revokedAt === null)
400
+ || Date.parse(updatedAt) < Date.parse(createdAt)
401
+ || (activatedAt !== null && Date.parse(activatedAt) < Date.parse(createdAt))
402
+ || (revokedAt !== null && (Date.parse(revokedAt) < Date.parse(createdAt)
403
+ || Date.parse(revokedAt) > Date.parse(updatedAt))))
404
+ throw invalidPayload('status');
405
+ return {
406
+ kind: 'member',
407
+ recordId,
408
+ revision,
409
+ value: {
410
+ activatedAt,
411
+ createdAt,
412
+ displayName: boundedString(value, 'displayName', 1024),
413
+ memberId,
414
+ personalRef,
415
+ projectId: token(value, 'projectId', isCollabProjectId),
416
+ role: literal(value, 'role', ['manager', 'member']),
417
+ status,
418
+ revokedAt,
419
+ updatedAt,
420
+ },
421
+ };
422
+ }
423
+ function requestRecord(source, recordId, revision) {
424
+ const value = exactRecord(source.value, 'value', [
425
+ 'createdAt',
426
+ 'description',
427
+ 'firstBaseOid',
428
+ 'latestHeadOid',
429
+ 'memberId',
430
+ 'mergedOid',
431
+ 'projectId',
432
+ 'requestId',
433
+ 'status',
434
+ 'updatedAt',
435
+ ]);
436
+ const requestId = token(value, 'requestId');
437
+ if (recordId !== requestId)
438
+ throw invalidPayload('recordId');
439
+ const status = literal(value, 'status', ['discarded', 'merged', 'open']);
440
+ const mergedOid = value.mergedOid === null
441
+ ? null
442
+ : token(value, 'mergedOid', isCollabGitOid);
443
+ const createdAt = timestamp(value, 'createdAt');
444
+ const updatedAt = timestamp(value, 'updatedAt');
445
+ if ((status === 'merged') !== (mergedOid !== null)
446
+ || Date.parse(updatedAt) < Date.parse(createdAt))
447
+ throw invalidPayload('status');
448
+ return {
449
+ kind: 'request',
450
+ recordId,
451
+ revision,
452
+ value: {
453
+ createdAt,
454
+ description: boundedString(value, 'description', 16 * 1024, true),
455
+ firstBaseOid: token(value, 'firstBaseOid', isCollabGitOid),
456
+ latestHeadOid: token(value, 'latestHeadOid', isCollabGitOid),
457
+ memberId: token(value, 'memberId', isCollabMemberId),
458
+ mergedOid,
459
+ projectId: token(value, 'projectId', isCollabProjectId),
460
+ requestId,
461
+ status,
462
+ updatedAt,
463
+ },
464
+ };
465
+ }
466
+ function requestCommentRecord(source, recordId, revision) {
467
+ const value = exactRecord(source.value, 'value', [
468
+ 'authorMemberId',
469
+ 'body',
470
+ 'commentId',
471
+ 'createdAt',
472
+ 'projectId',
473
+ 'requestId',
474
+ ]);
475
+ const commentId = token(value, 'commentId');
476
+ if (recordId !== commentId)
477
+ throw invalidPayload('recordId');
478
+ return {
479
+ kind: 'request-comment',
480
+ recordId,
481
+ revision,
482
+ value: {
483
+ authorMemberId: token(value, 'authorMemberId', isCollabMemberId),
484
+ body: boundedString(value, 'body', 16 * 1024, true),
485
+ commentId,
486
+ createdAt: timestamp(value, 'createdAt'),
487
+ projectId: token(value, 'projectId', isCollabProjectId),
488
+ requestId: token(value, 'requestId'),
489
+ },
490
+ };
491
+ }
492
+ function ticketRecord(source, recordId, revision) {
493
+ const value = exactRecord(source.value, 'value', [
494
+ 'authorMemberId',
495
+ 'body',
496
+ 'closedAt',
497
+ 'closedByMemberId',
498
+ 'createdAt',
499
+ 'number',
500
+ 'projectId',
501
+ 'status',
502
+ 'ticketId',
503
+ 'title',
504
+ 'updatedAt',
505
+ ]);
506
+ const ticketId = token(value, 'ticketId');
507
+ if (recordId !== ticketId)
508
+ throw invalidPayload('recordId');
509
+ const status = literal(value, 'status', ['closed', 'open']);
510
+ const closedAt = nullableTimestamp(value, 'closedAt');
511
+ const closedByMemberId = value.closedByMemberId === null
512
+ ? null
513
+ : token(value, 'closedByMemberId', isCollabMemberId);
514
+ const createdAt = timestamp(value, 'createdAt');
515
+ const updatedAt = timestamp(value, 'updatedAt');
516
+ if ((status === 'open' && (closedAt !== null || closedByMemberId !== null))
517
+ || (status === 'closed' && (closedAt === null || closedByMemberId === null))
518
+ || Date.parse(updatedAt) < Date.parse(createdAt)
519
+ || (closedAt !== null && Date.parse(closedAt) < Date.parse(createdAt)))
520
+ throw invalidPayload('status');
521
+ return {
522
+ kind: 'ticket',
523
+ recordId,
524
+ revision,
525
+ value: {
526
+ authorMemberId: token(value, 'authorMemberId', isCollabMemberId),
527
+ body: boundedString(value, 'body', 32 * 1024, true),
528
+ closedAt,
529
+ closedByMemberId,
530
+ createdAt,
531
+ number: positiveInteger(value, 'number'),
532
+ projectId: token(value, 'projectId', isCollabProjectId),
533
+ status,
534
+ ticketId,
535
+ title: boundedString(value, 'title', 1024),
536
+ updatedAt,
537
+ },
538
+ };
539
+ }
540
+ function ticketCommentRecord(source, recordId, revision) {
541
+ const value = exactRecord(source.value, 'value', [
542
+ 'authorMemberId',
543
+ 'body',
544
+ 'commentId',
545
+ 'createdAt',
546
+ 'projectId',
547
+ 'ticketId',
548
+ ]);
549
+ const commentId = token(value, 'commentId');
550
+ if (recordId !== commentId)
551
+ throw invalidPayload('recordId');
552
+ return {
553
+ kind: 'ticket-comment',
554
+ recordId,
555
+ revision,
556
+ value: {
557
+ authorMemberId: token(value, 'authorMemberId', isCollabMemberId),
558
+ body: boundedString(value, 'body', 16 * 1024, true),
559
+ commentId,
560
+ createdAt: timestamp(value, 'createdAt'),
561
+ projectId: token(value, 'projectId', isCollabProjectId),
562
+ ticketId: token(value, 'ticketId'),
563
+ },
564
+ };
565
+ }
566
+ function ticketRelationRecord(source, recordId, revision) {
567
+ const value = exactRecord(source.value, 'value', [
568
+ 'acceptedAt',
569
+ 'acceptedMergeOid',
570
+ 'commitOid',
571
+ 'createdAt',
572
+ 'createdByMemberId',
573
+ 'kind',
574
+ 'projectId',
575
+ 'relationId',
576
+ 'requestId',
577
+ 'state',
578
+ 'ticketId',
579
+ 'updatedAt',
580
+ ]);
581
+ const relationId = token(value, 'relationId');
582
+ if (recordId !== relationId)
583
+ throw invalidPayload('recordId');
584
+ const state = literal(value, 'state', ['accepted', 'pending']);
585
+ const acceptedAt = nullableTimestamp(value, 'acceptedAt');
586
+ const acceptedMergeOid = value.acceptedMergeOid === null
587
+ ? null
588
+ : token(value, 'acceptedMergeOid', isCollabGitOid);
589
+ const createdAt = timestamp(value, 'createdAt');
590
+ const updatedAt = timestamp(value, 'updatedAt');
591
+ if ((state === 'pending' && (acceptedAt !== null || acceptedMergeOid !== null))
592
+ || (state === 'accepted' && (acceptedAt === null || acceptedMergeOid === null))
593
+ || Date.parse(updatedAt) < Date.parse(createdAt)
594
+ || (acceptedAt !== null && Date.parse(acceptedAt) < Date.parse(createdAt)))
595
+ throw invalidPayload('state');
596
+ return {
597
+ kind: 'ticket-relation',
598
+ recordId,
599
+ revision,
600
+ value: {
601
+ acceptedAt,
602
+ acceptedMergeOid,
603
+ commitOid: token(value, 'commitOid', isCollabGitOid),
604
+ createdAt,
605
+ createdByMemberId: token(value, 'createdByMemberId', isCollabMemberId),
606
+ kind: literal(value, 'kind', ['references', 'resolves']),
607
+ projectId: token(value, 'projectId', isCollabProjectId),
608
+ relationId,
609
+ requestId: token(value, 'requestId'),
610
+ state,
611
+ ticketId: token(value, 'ticketId'),
612
+ updatedAt,
613
+ },
614
+ };
615
+ }
616
+ function ticketMentionRecord(source, recordId, revision) {
617
+ const value = exactRecord(source.value, 'value', [
618
+ 'createdAt',
619
+ 'mentionedMemberId',
620
+ 'projectId',
621
+ 'sourceId',
622
+ 'sourceKind',
623
+ 'ticketId',
624
+ ]);
625
+ return {
626
+ kind: 'ticket-mention',
627
+ recordId,
628
+ revision,
629
+ value: {
630
+ createdAt: timestamp(value, 'createdAt'),
631
+ mentionedMemberId: token(value, 'mentionedMemberId', isCollabMemberId),
632
+ projectId: token(value, 'projectId', isCollabProjectId),
633
+ sourceId: token(value, 'sourceId'),
634
+ sourceKind: literal(value, 'sourceKind', ['comment', 'description']),
635
+ ticketId: token(value, 'ticketId'),
636
+ },
637
+ };
638
+ }
639
+ function cloudEventRecord(source, recordId, revision) {
640
+ const value = exactRecord(source.value, 'value', ['event']);
641
+ const event = decodeCollabCloudProjectEventMessage(value.event);
642
+ if (!('projectId' in event) || recordId !== String(event.sequence).padStart(20, '0')) {
643
+ throw invalidPayload('event');
644
+ }
645
+ return { kind: 'cloud-event', recordId, revision, value: { event } };
646
+ }
647
+ function cloudEventCursorRecord(source, recordId, revision) {
648
+ const value = exactRecord(source.value, 'value', [
649
+ 'currentSequence',
650
+ 'projectId',
651
+ 'updatedAt',
652
+ ]);
653
+ const projectId = token(value, 'projectId', isCollabProjectId);
654
+ if (recordId !== projectId)
655
+ throw invalidPayload('recordId');
656
+ return {
657
+ kind: 'cloud-event-cursor',
658
+ recordId,
659
+ revision,
660
+ value: {
661
+ currentSequence: nonNegativeInteger(value, 'currentSequence'),
662
+ projectId,
663
+ updatedAt: timestamp(value, 'updatedAt'),
664
+ },
665
+ };
666
+ }
667
+ function idempotencyResultRecord(source, recordId, revision) {
668
+ const value = exactRecord(source.value, 'value', [
669
+ 'createdAt',
670
+ 'idempotencyKey',
671
+ 'memberId',
672
+ 'operation',
673
+ 'projectId',
674
+ 'requestFingerprint',
675
+ 'responseJson',
676
+ ]);
677
+ const operation = controlOperation(value, 'operation');
678
+ const idempotencyKey = token(value, 'idempotencyKey');
679
+ const projectId = token(value, 'projectId', isCollabProjectId);
680
+ const response = canonicalOperationResponseJson(value, 'responseJson', operation);
681
+ if (recordId !== idempotencyKey
682
+ || (Object.hasOwn(response.decoded, 'projectId')
683
+ && response.decoded.projectId !== projectId))
684
+ throw invalidPayload('responseJson');
685
+ return {
686
+ kind: 'idempotency-result',
687
+ recordId,
688
+ revision,
689
+ value: {
690
+ createdAt: timestamp(value, 'createdAt'),
691
+ idempotencyKey,
692
+ memberId: token(value, 'memberId', isCollabMemberId),
693
+ operation,
694
+ projectId,
695
+ requestFingerprint: sha256(value, 'requestFingerprint'),
696
+ responseJson: response.responseJson,
697
+ },
698
+ };
699
+ }
700
+ function principalBindingRecord(source, recordId, revision) {
701
+ const value = exactRecord(source.value, 'value', [
702
+ 'boundAt',
703
+ 'memberId',
704
+ 'principalId',
705
+ 'projectId',
706
+ ]);
707
+ const memberId = token(value, 'memberId', isCollabMemberId);
708
+ if (recordId !== memberId)
709
+ throw invalidPayload('recordId');
710
+ return {
711
+ kind: 'principal-binding',
712
+ recordId,
713
+ revision,
714
+ value: {
715
+ boundAt: timestamp(value, 'boundAt'),
716
+ memberId,
717
+ principalId: token(value, 'principalId'),
718
+ projectId: token(value, 'projectId', isCollabProjectId),
719
+ },
720
+ };
721
+ }
722
+ function repositoryPlacementRecord(source, recordId, revision) {
723
+ const value = exactRecord(source.value, 'value', [
724
+ 'nodeId',
725
+ 'placementGeneration',
726
+ 'projectId',
727
+ 'repositoryIdentity',
728
+ ]);
729
+ return {
730
+ kind: 'repository-placement',
731
+ recordId,
732
+ revision,
733
+ value: {
734
+ nodeId: token(value, 'nodeId'),
735
+ placementGeneration: positiveInteger(value, 'placementGeneration'),
736
+ projectId: token(value, 'projectId', isCollabProjectId),
737
+ repositoryIdentity: token(value, 'repositoryIdentity'),
738
+ },
739
+ };
740
+ }
741
+ function lifecycleStateRecord(source, recordId, revision) {
742
+ const value = exactRecord(source.value, 'value', [
743
+ 'batchRevision',
744
+ 'batchSha256',
745
+ 'checkpointSha256',
746
+ 'direction',
747
+ 'operationId',
748
+ 'operationKind',
749
+ 'phase',
750
+ 'projectId',
751
+ 'relinquishmentProof',
752
+ 'updatedAt',
753
+ ]);
754
+ const operationId = token(value, 'operationId');
755
+ if (recordId !== operationId)
756
+ throw invalidPayload('recordId');
757
+ const operationKind = literal(value, 'operationKind', [
758
+ 'authority-transfer',
759
+ 'backup',
760
+ 'delete',
761
+ 'retire',
762
+ ]);
763
+ const common = {
764
+ operationId,
765
+ projectId: token(value, 'projectId', isCollabProjectId),
766
+ updatedAt: timestamp(value, 'updatedAt'),
767
+ };
768
+ if (operationKind === 'authority-transfer') {
769
+ const fence = decodeCollabAuthorityTransferLifecycleFence({
770
+ batchRevision: value.batchRevision,
771
+ batchSha256: value.batchSha256,
772
+ checkpointSha256: value.checkpointSha256,
773
+ direction: value.direction,
774
+ phase: value.phase,
775
+ });
776
+ const relinquishmentProof = value.relinquishmentProof === null
777
+ ? null
778
+ : decodeCollabAuthorityRelinquishmentProof(value.relinquishmentProof);
779
+ if (fence.relinquishmentRequired !== (relinquishmentProof !== null)
780
+ || (relinquishmentProof !== null && (relinquishmentProof.batchRevision !== fence.batchRevision
781
+ || relinquishmentProof.batchSha256 !== fence.batchSha256
782
+ || relinquishmentProof.checkpointSha256 !== fence.checkpointSha256
783
+ || relinquishmentProof.projectId !== common.projectId
784
+ || relinquishmentProof.transferId !== operationId
785
+ || (fence.direction === 'lan-to-cloud'
786
+ ? relinquishmentProof.sourceAuthority.kind !== 'lan'
787
+ || relinquishmentProof.targetAuthority.kind !== 'cloud'
788
+ : relinquishmentProof.sourceAuthority.kind !== 'cloud'
789
+ || relinquishmentProof.targetAuthority.kind !== 'lan'))))
790
+ throw invalidPayload('relinquishmentProof');
791
+ return {
792
+ kind: 'lifecycle-state',
793
+ recordId,
794
+ revision,
795
+ value: {
796
+ batchRevision: fence.batchRevision,
797
+ batchSha256: fence.batchSha256,
798
+ checkpointSha256: fence.checkpointSha256,
799
+ direction: fence.direction,
800
+ operationId,
801
+ operationKind,
802
+ phase: fence.phase,
803
+ projectId: common.projectId,
804
+ relinquishmentProof,
805
+ updatedAt: common.updatedAt,
806
+ },
807
+ };
808
+ }
809
+ if (value.direction !== null
810
+ || value.batchRevision !== null
811
+ || value.batchSha256 !== null
812
+ || value.relinquishmentProof !== null
813
+ || (operationKind !== 'backup' && value.checkpointSha256 !== null))
814
+ throw invalidPayload('lifecycleState');
815
+ const checkpointSha256 = value.checkpointSha256 === null
816
+ ? null
817
+ : sha256(value, 'checkpointSha256');
818
+ const phase = boundedString(value, 'phase', 128);
819
+ const internalState = operationKind === 'backup'
820
+ ? {
821
+ batchRevision: null,
822
+ batchSha256: null,
823
+ checkpointSha256,
824
+ direction: null,
825
+ operationId,
826
+ operationKind,
827
+ phase,
828
+ projectId: common.projectId,
829
+ relinquishmentProof: null,
830
+ updatedAt: common.updatedAt,
831
+ }
832
+ : {
833
+ batchRevision: null,
834
+ batchSha256: null,
835
+ checkpointSha256: null,
836
+ direction: null,
837
+ operationId,
838
+ operationKind,
839
+ phase,
840
+ projectId: common.projectId,
841
+ relinquishmentProof: null,
842
+ updatedAt: common.updatedAt,
843
+ };
844
+ return {
845
+ kind: 'lifecycle-state',
846
+ recordId,
847
+ revision,
848
+ value: internalState,
849
+ };
850
+ }
851
+ function terminalResponderRecord(source, recordId, revision) {
852
+ const value = exactRecord(source.value, 'value', [
853
+ 'acknowledgements',
854
+ 'eligibleMemberIds',
855
+ 'expiresAt',
856
+ 'operation',
857
+ 'operationId',
858
+ 'projectId',
859
+ 'responseJson',
860
+ ]);
861
+ const operationId = token(value, 'operationId');
862
+ if (recordId !== operationId)
863
+ throw invalidPayload('recordId');
864
+ const operation = controlOperation(value, 'operation');
865
+ const projectId = token(value, 'projectId', isCollabProjectId);
866
+ const response = canonicalOperationResponseJson(value, 'responseJson', operation);
867
+ const responseOperationId = Object.hasOwn(response.decoded, 'transferId')
868
+ ? response.decoded.transferId
869
+ : Object.hasOwn(response.decoded, 'retirementId')
870
+ ? response.decoded.retirementId
871
+ : null;
872
+ if (response.decoded.projectId !== projectId
873
+ || responseOperationId !== operationId)
874
+ throw invalidPayload('responseJson');
875
+ const expiresAt = timestamp(value, 'expiresAt');
876
+ if (operation === 'retireProject') {
877
+ if (response.decoded.kind !== 'project-retired'
878
+ || response.decoded.terminalExpiresAt !== expiresAt)
879
+ throw invalidPayload('responseJson');
880
+ }
881
+ else if (operation === 'getProjectAuthorityTransfer') {
882
+ if (response.decoded.direction !== 'cloud-to-lan'
883
+ || response.decoded.phase !== 'completed'
884
+ || response.decoded.state !== 'completed'
885
+ || response.decoded.expiresAt !== expiresAt)
886
+ throw invalidPayload('responseJson');
887
+ }
888
+ else {
889
+ throw invalidPayload('operation');
890
+ }
891
+ if (!Array.isArray(value.eligibleMemberIds))
892
+ throw invalidPayload('eligibleMemberIds');
893
+ const eligibleMemberIds = value.eligibleMemberIds.map((item) => {
894
+ if (!isCollabMemberId(item))
895
+ throw invalidPayload('eligibleMemberIds');
896
+ return item;
897
+ });
898
+ if (eligibleMemberIds.some((item, index) => (index > 0 && eligibleMemberIds[index - 1].localeCompare(item, 'en-US') >= 0)))
899
+ throw invalidPayload('eligibleMemberIds');
900
+ if (!Array.isArray(value.acknowledgements))
901
+ throw invalidPayload('acknowledgements');
902
+ const acknowledgements = value.acknowledgements.map((item) => {
903
+ const acknowledgement = exactRecord(item, 'acknowledgement', [
904
+ 'acknowledgedAt',
905
+ 'memberId',
906
+ 'principalId',
907
+ ]);
908
+ return {
909
+ acknowledgedAt: timestamp(acknowledgement, 'acknowledgedAt'),
910
+ memberId: token(acknowledgement, 'memberId', isCollabMemberId),
911
+ principalId: token(acknowledgement, 'principalId'),
912
+ };
913
+ });
914
+ const acknowledgementPrincipals = new Set();
915
+ acknowledgements.forEach((item, index) => {
916
+ if (!eligibleMemberIds.includes(item.memberId)
917
+ || (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
918
+ || acknowledgementPrincipals.has(item.principalId))
919
+ throw invalidPayload('acknowledgements');
920
+ acknowledgementPrincipals.add(item.principalId);
921
+ });
922
+ return {
923
+ kind: 'terminal-responder',
924
+ recordId,
925
+ revision,
926
+ value: {
927
+ acknowledgements: Object.freeze(acknowledgements),
928
+ eligibleMemberIds: Object.freeze(eligibleMemberIds),
929
+ expiresAt,
930
+ operation,
931
+ operationId,
932
+ projectId,
933
+ responseJson: response.responseJson,
934
+ },
935
+ };
936
+ }
937
+ const PROTECTED_CLAIM_ASSOCIATED_DATA_KEYS = [
938
+ 'authorityGeneration',
939
+ 'checkpointSha256',
940
+ 'claimSha256',
941
+ 'envelopeVersion',
942
+ 'environmentIdentity',
943
+ 'memberId',
944
+ 'projectId',
945
+ 'transferId',
946
+ ];
947
+ function decodeProtectedClaimAssociatedData(value) {
948
+ const associatedData = exactRecord(value, 'associatedData', PROTECTED_CLAIM_ASSOCIATED_DATA_KEYS);
949
+ const envelopeVersion = associatedData.envelopeVersion;
950
+ if (envelopeVersion !== COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION) {
951
+ throw invalidPayload('envelopeVersion');
952
+ }
953
+ return {
954
+ authorityGeneration: positiveInteger(associatedData, 'authorityGeneration'),
955
+ checkpointSha256: sha256(associatedData, 'checkpointSha256'),
956
+ claimSha256: sha256(associatedData, 'claimSha256'),
957
+ envelopeVersion,
958
+ environmentIdentity: token(associatedData, 'environmentIdentity'),
959
+ memberId: token(associatedData, 'memberId', isCollabMemberId),
960
+ projectId: token(associatedData, 'projectId', isCollabProjectId),
961
+ transferId: token(associatedData, 'transferId'),
962
+ };
963
+ }
964
+ export function encodeCollabProtectedClaimAssociatedData(associatedData) {
965
+ return JSON.stringify({
966
+ domain: 'claudian-collab.protected-claim-envelope-associated-data.v1',
967
+ payload: decodeProtectedClaimAssociatedData(associatedData),
968
+ });
969
+ }
970
+ function protectedClaimEnvelopeRecord(source, recordId, revision) {
971
+ const value = exactRecord(source.value, 'value', [
972
+ 'associatedData',
973
+ 'associatedDataSha256',
974
+ 'ciphertext',
975
+ 'encryptionAlgorithm',
976
+ 'expiresAt',
977
+ 'keyId',
978
+ 'keyVersion',
979
+ 'memberId',
980
+ 'nonce',
981
+ 'receiptKeyId',
982
+ 'tag',
983
+ 'transferId',
984
+ ]);
985
+ const transferId = token(value, 'transferId');
986
+ const memberId = token(value, 'memberId', isCollabMemberId);
987
+ const decodedAssociatedData = decodeProtectedClaimAssociatedData(value.associatedData);
988
+ if (decodedAssociatedData.memberId !== memberId
989
+ || decodedAssociatedData.transferId !== transferId)
990
+ throw invalidPayload('associatedData');
991
+ return {
992
+ kind: 'protected-claim-envelope',
993
+ recordId,
994
+ revision,
995
+ value: {
996
+ associatedData: decodedAssociatedData,
997
+ associatedDataSha256: sha256(value, 'associatedDataSha256'),
998
+ ciphertext: canonicalBase64url(value, 'ciphertext', COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.maxCiphertextBytes),
999
+ encryptionAlgorithm: literal(value, 'encryptionAlgorithm', ['xchacha20-poly1305']),
1000
+ expiresAt: timestamp(value, 'expiresAt'),
1001
+ keyId: boundedString(value, 'keyId', 256),
1002
+ keyVersion: positiveInteger(value, 'keyVersion'),
1003
+ memberId,
1004
+ nonce: canonicalBase64url(value, 'nonce', COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.nonceBytes, COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.nonceBytes),
1005
+ receiptKeyId: boundedString(value, 'receiptKeyId', 256),
1006
+ tag: canonicalBase64url(value, 'tag', COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.tagBytes, COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.tagBytes),
1007
+ transferId,
1008
+ },
1009
+ };
1010
+ }
1011
+ function tombstoneRecord(source, recordId, revision) {
1012
+ const value = exactRecord(source.value, 'value', [
1013
+ 'authorityGeneration',
1014
+ 'projectId',
1015
+ 'retiredAt',
1016
+ 'terminalExpiresAt',
1017
+ ]);
1018
+ const projectId = token(value, 'projectId', isCollabProjectId);
1019
+ if (recordId !== projectId)
1020
+ throw invalidPayload('recordId');
1021
+ const retiredAt = timestamp(value, 'retiredAt');
1022
+ const terminalExpiresAt = timestamp(value, 'terminalExpiresAt');
1023
+ if (Date.parse(terminalExpiresAt) <= Date.parse(retiredAt)) {
1024
+ throw invalidPayload('terminalExpiresAt');
1025
+ }
1026
+ return {
1027
+ kind: 'tombstone',
1028
+ recordId,
1029
+ revision,
1030
+ value: {
1031
+ authorityGeneration: positiveInteger(value, 'authorityGeneration'),
1032
+ projectId,
1033
+ retiredAt,
1034
+ terminalExpiresAt,
1035
+ },
1036
+ };
1037
+ }
1038
+ function schemaCatalogRecord(source, recordId, revision) {
1039
+ const value = exactRecord(source.value, 'value', [
1040
+ 'coordinationSchemaVersion',
1041
+ 'projectId',
1042
+ 'repositoryFormatVersion',
1043
+ ]);
1044
+ const projectId = token(value, 'projectId', isCollabProjectId);
1045
+ if (recordId !== projectId)
1046
+ throw invalidPayload('recordId');
1047
+ return {
1048
+ kind: 'schema-catalog',
1049
+ recordId,
1050
+ revision,
1051
+ value: {
1052
+ coordinationSchemaVersion: positiveInteger(value, 'coordinationSchemaVersion'),
1053
+ projectId,
1054
+ repositoryFormatVersion: positiveInteger(value, 'repositoryFormatVersion'),
1055
+ },
1056
+ };
1057
+ }
1058
+ function serverCompatibilityRecord(source, recordId, revision) {
1059
+ const value = exactRecord(source.value, 'value', [
1060
+ 'maximumBuild',
1061
+ 'minimumBuild',
1062
+ 'projectId',
1063
+ ]);
1064
+ const projectId = token(value, 'projectId', isCollabProjectId);
1065
+ if (recordId !== projectId)
1066
+ throw invalidPayload('recordId');
1067
+ return {
1068
+ kind: 'server-compatibility',
1069
+ recordId,
1070
+ revision,
1071
+ value: {
1072
+ maximumBuild: boundedString(value, 'maximumBuild', 128),
1073
+ minimumBuild: boundedString(value, 'minimumBuild', 128),
1074
+ projectId,
1075
+ },
1076
+ };
1077
+ }
1078
+ function authorityVolumePairRecord(source, recordId, revision) {
1079
+ const value = exactRecord(source.value, 'value', [
1080
+ 'authorityId',
1081
+ 'authorityVolumeIdentity',
1082
+ 'projectId',
1083
+ 'restoreEpoch',
1084
+ ]);
1085
+ const projectId = token(value, 'projectId', isCollabProjectId);
1086
+ if (recordId !== projectId)
1087
+ throw invalidPayload('recordId');
1088
+ return {
1089
+ kind: 'authority-volume-pair',
1090
+ recordId,
1091
+ revision,
1092
+ value: {
1093
+ authorityId: token(value, 'authorityId'),
1094
+ authorityVolumeIdentity: token(value, 'authorityVolumeIdentity'),
1095
+ projectId,
1096
+ restoreEpoch: positiveInteger(value, 'restoreEpoch'),
1097
+ },
1098
+ };
1099
+ }
1100
+ function decodeCheckpointRecord(value) {
1101
+ const { kind, recordId, revision, source } = recordEnvelope(value);
1102
+ switch (kind) {
1103
+ case 'project': return projectRecord(source, recordId, revision);
1104
+ case 'member': return memberRecord(source, recordId, revision);
1105
+ case 'request': return requestRecord(source, recordId, revision);
1106
+ case 'request-comment': return requestCommentRecord(source, recordId, revision);
1107
+ case 'ticket': return ticketRecord(source, recordId, revision);
1108
+ case 'ticket-comment': return ticketCommentRecord(source, recordId, revision);
1109
+ case 'ticket-relation': return ticketRelationRecord(source, recordId, revision);
1110
+ case 'ticket-mention': return ticketMentionRecord(source, recordId, revision);
1111
+ case 'cloud-event': return cloudEventRecord(source, recordId, revision);
1112
+ case 'cloud-event-cursor': return cloudEventCursorRecord(source, recordId, revision);
1113
+ case 'idempotency-result': return idempotencyResultRecord(source, recordId, revision);
1114
+ case 'principal-binding': return principalBindingRecord(source, recordId, revision);
1115
+ case 'repository-placement': return repositoryPlacementRecord(source, recordId, revision);
1116
+ case 'lifecycle-state': return lifecycleStateRecord(source, recordId, revision);
1117
+ case 'terminal-responder': return terminalResponderRecord(source, recordId, revision);
1118
+ case 'protected-claim-envelope':
1119
+ return protectedClaimEnvelopeRecord(source, recordId, revision);
1120
+ case 'tombstone': return tombstoneRecord(source, recordId, revision);
1121
+ case 'schema-catalog': return schemaCatalogRecord(source, recordId, revision);
1122
+ case 'server-compatibility': return serverCompatibilityRecord(source, recordId, revision);
1123
+ case 'authority-volume-pair': return authorityVolumePairRecord(source, recordId, revision);
1124
+ }
1125
+ }
1126
+ function recordKindOrder(kind) {
1127
+ return COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS.indexOf(kind);
1128
+ }
1129
+ function compareRecords(left, right) {
1130
+ const kindOrder = recordKindOrder(left.kind) - recordKindOrder(right.kind);
1131
+ return kindOrder !== 0 ? kindOrder : left.recordId.localeCompare(right.recordId, 'en-US');
1132
+ }
1133
+ function validateRecordSequence(records, profile) {
1134
+ if (records.length === 0 || records[0].kind !== 'project')
1135
+ throw invalidPayload('records');
1136
+ const projectId = records[0].recordId;
1137
+ if (records.filter(item => item.kind === 'project').length !== 1) {
1138
+ throw invalidPayload('records');
1139
+ }
1140
+ if (records.some((item, index) => index > 0 && compareRecords(records[index - 1], item) >= 0)) {
1141
+ throw invalidPayload('records');
1142
+ }
1143
+ if (profile !== 'backup' && records.some(item => !PORTABLE_RECORD_KIND_SET.has(item.kind))) {
1144
+ throw invalidPayload('profile');
1145
+ }
1146
+ if (profile === 'backup') {
1147
+ const requiredKinds = [
1148
+ 'cloud-event-cursor',
1149
+ 'schema-catalog',
1150
+ 'server-compatibility',
1151
+ 'authority-volume-pair',
1152
+ ];
1153
+ if (requiredKinds.some(kind => records.filter(item => item.kind === kind).length !== 1)) {
1154
+ throw invalidPayload('profile');
1155
+ }
1156
+ }
1157
+ const projectRecordValue = records[0].value;
1158
+ if (projectRecordValue.projectId !== projectId)
1159
+ throw invalidPayload('records');
1160
+ for (const item of records) {
1161
+ let itemProjectId;
1162
+ if (item.kind === 'cloud-event')
1163
+ itemProjectId = item.value.event.projectId;
1164
+ else if (item.kind === 'protected-claim-envelope') {
1165
+ itemProjectId = item.value.associatedData.projectId;
1166
+ }
1167
+ else
1168
+ itemProjectId = item.value.projectId;
1169
+ if (itemProjectId !== projectId)
1170
+ throw invalidPayload('records');
1171
+ }
1172
+ const members = new Map(records
1173
+ .filter((item) => item.kind === 'member')
1174
+ .map(item => [item.value.memberId, item]));
1175
+ const requests = new Set(records
1176
+ .filter((item) => item.kind === 'request')
1177
+ .map(item => item.value.requestId));
1178
+ const tickets = new Set(records
1179
+ .filter((item) => item.kind === 'ticket')
1180
+ .map(item => item.value.ticketId));
1181
+ const ticketComments = new Map(records
1182
+ .filter((item) => item.kind === 'ticket-comment')
1183
+ .map(item => [item.value.commentId, item.value.ticketId]));
1184
+ const principalBindings = new Map(records
1185
+ .filter((item) => (item.kind === 'principal-binding'))
1186
+ .map(item => [item.value.memberId, item.value.principalId]));
1187
+ const terminalResponders = records.filter((item) => (item.kind === 'terminal-responder'));
1188
+ const tombstones = records.filter((item) => (item.kind === 'tombstone'));
1189
+ if (terminalResponders.length > 1
1190
+ || (terminalResponders.length === 1 && (tombstones.length !== 1
1191
+ || terminalResponders[0].value.expiresAt !== tombstones[0].value.terminalExpiresAt)))
1192
+ throw invalidPayload('records');
1193
+ const tombstone = tombstones[0];
1194
+ const retirementTerminal = terminalResponders.find(item => (item.value.operation === 'retireProject'));
1195
+ if (retirementTerminal !== undefined) {
1196
+ const response = canonicalOperationResponseJson({ responseJson: retirementTerminal.value.responseJson }, 'responseJson', 'retireProject').decoded;
1197
+ if (tombstone === undefined || response.retiredAt !== tombstone.value.retiredAt) {
1198
+ throw invalidPayload('records');
1199
+ }
1200
+ }
1201
+ const retirementEvents = records.filter((item) => (item.kind === 'cloud-event' && item.value.event.kind === 'project.retired'));
1202
+ if (tombstone !== undefined && retirementEvents.some(item => (item.value.event.kind === 'project.retired'
1203
+ && (item.value.event.payload.retiredAt !== tombstone.value.retiredAt
1204
+ || (retirementTerminal !== undefined
1205
+ && item.value.event.payload.retirementId !== retirementTerminal.value.operationId)))))
1206
+ throw invalidPayload('records');
1207
+ for (const item of records) {
1208
+ if (item.kind === 'request' && !members.has(item.value.memberId)) {
1209
+ throw invalidPayload('records');
1210
+ }
1211
+ if (item.kind === 'request-comment' && (!requests.has(item.value.requestId) || !members.has(item.value.authorMemberId)))
1212
+ throw invalidPayload('records');
1213
+ if (item.kind === 'ticket' && (!members.has(item.value.authorMemberId)
1214
+ || (item.value.closedByMemberId !== null && !members.has(item.value.closedByMemberId))))
1215
+ throw invalidPayload('records');
1216
+ if (item.kind === 'ticket-comment' && (!tickets.has(item.value.ticketId) || !members.has(item.value.authorMemberId)))
1217
+ throw invalidPayload('records');
1218
+ if (item.kind === 'ticket-relation' && (!tickets.has(item.value.ticketId)
1219
+ || !requests.has(item.value.requestId)
1220
+ || !members.has(item.value.createdByMemberId)))
1221
+ throw invalidPayload('records');
1222
+ if (item.kind === 'ticket-mention' && (!tickets.has(item.value.ticketId)
1223
+ || !members.has(item.value.mentionedMemberId)
1224
+ || (item.value.sourceKind === 'description' && item.value.sourceId !== item.value.ticketId)
1225
+ || (item.value.sourceKind === 'comment'
1226
+ && ticketComments.get(item.value.sourceId) !== item.value.ticketId)))
1227
+ throw invalidPayload('records');
1228
+ if ((item.kind === 'idempotency-result' || item.kind === 'principal-binding')
1229
+ && !members.has(item.value.memberId))
1230
+ throw invalidPayload('records');
1231
+ if (item.kind === 'protected-claim-envelope'
1232
+ && members.get(item.value.memberId)?.value.status !== 'active') {
1233
+ throw invalidPayload('records');
1234
+ }
1235
+ if (item.kind === 'terminal-responder' && (item.value.eligibleMemberIds.some(memberId => !members.has(memberId))
1236
+ || item.value.acknowledgements.some(acknowledgement => (principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId))))
1237
+ throw invalidPayload('records');
1238
+ }
1239
+ if (profile === 'backup') {
1240
+ const cursor = records.find((item) => (item.kind === 'cloud-event-cursor'));
1241
+ if (cursor === undefined || records.some(item => (item.kind === 'cloud-event' && item.value.event.sequence > cursor.value.currentSequence)))
1242
+ throw invalidPayload('records');
1243
+ }
1244
+ }
1245
+ export function decodeCollabProjectCheckpointCoordinationNdjson(value, profile) {
1246
+ if (typeof value !== 'string'
1247
+ || !CHECKPOINT_PROFILE_SET.has(profile)
1248
+ || !value.endsWith('\n')
1249
+ || !hasUtf8ByteLengthAtMost(value, COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxCoordinationBytes))
1250
+ throw invalidPayload('coordination');
1251
+ const lines = value.slice(0, -1).split('\n');
1252
+ if (lines.some(line => line.length === 0))
1253
+ throw invalidPayload('coordination');
1254
+ const decoded = lines.map((line) => {
1255
+ let parsed;
1256
+ try {
1257
+ parsed = JSON.parse(line);
1258
+ }
1259
+ catch {
1260
+ throw invalidPayload('coordination');
1261
+ }
1262
+ const result = decodeCheckpointRecord(parsed);
1263
+ if (JSON.stringify(result) !== line)
1264
+ throw invalidPayload('coordination');
1265
+ return result;
1266
+ });
1267
+ validateRecordSequence(decoded, profile);
1268
+ return Object.freeze(decoded);
1269
+ }
1270
+ export function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
1271
+ const encoded = records.map(record => JSON.stringify(record)).join('\n') + '\n';
1272
+ return decodeCollabProjectCheckpointCoordinationNdjson(encoded, profile)
1273
+ .map(record => JSON.stringify(record)).join('\n') + '\n';
1274
+ }
1275
+ function checkpointRecordGitOids(checkpointRecord) {
1276
+ switch (checkpointRecord.kind) {
1277
+ case 'project': return [checkpointRecord.value.expectedMainOid];
1278
+ case 'request': return [
1279
+ checkpointRecord.value.firstBaseOid,
1280
+ checkpointRecord.value.latestHeadOid,
1281
+ ...(checkpointRecord.value.mergedOid === null
1282
+ ? []
1283
+ : [checkpointRecord.value.mergedOid]),
1284
+ ];
1285
+ case 'ticket-relation': return [
1286
+ checkpointRecord.value.commitOid,
1287
+ ...(checkpointRecord.value.acceptedMergeOid === null
1288
+ ? []
1289
+ : [checkpointRecord.value.acceptedMergeOid]),
1290
+ ];
1291
+ case 'cloud-event': return checkpointRecord.value.event.kind === 'main.updated'
1292
+ ? [checkpointRecord.value.event.payload.mainOid]
1293
+ : [];
1294
+ default: return [];
1295
+ }
1296
+ }
1297
+ export function validateCollabProjectCheckpointConsistency(manifest, records) {
1298
+ const decodedManifest = decodeCollabProjectCheckpointManifest(manifest);
1299
+ validateRecordSequence(records, decodedManifest.profile);
1300
+ const project = records[0];
1301
+ if (project.kind !== 'project'
1302
+ || project.value.projectId !== decodedManifest.projectId
1303
+ || project.value.authorityGeneration !== decodedManifest.sourceAuthority.generation
1304
+ || project.value.expectedMainOid !== decodedManifest.expectedMainOid
1305
+ || records.some(record => checkpointRecordGitOids(record).some(oid => (!gitOidMatchesFormat(oid, decodedManifest.gitObjectFormat)))))
1306
+ throw invalidPayload('checkpoint');
1307
+ const activeMembers = records.filter((item) => (item.kind === 'member' && item.value.status === 'active'));
1308
+ const activeMemberIds = new Set(activeMembers.map(item => item.value.memberId));
1309
+ const activeMemberRefs = activeMembers
1310
+ .map(item => item.value.personalRef)
1311
+ .sort((left, right) => left.localeCompare(right, 'en-US'));
1312
+ const manifestMemberRefs = decodedManifest.refs.slice(1).map(item => item.name);
1313
+ if (activeMemberRefs.length !== manifestMemberRefs.length
1314
+ || activeMemberRefs.some((item, index) => item !== manifestMemberRefs[index]))
1315
+ throw invalidPayload('refs');
1316
+ const openRequestMemberIds = new Set();
1317
+ for (const record of records) {
1318
+ if (record.kind !== 'request' || record.value.status !== 'open')
1319
+ continue;
1320
+ if (!activeMemberIds.has(record.value.memberId)
1321
+ || openRequestMemberIds.has(record.value.memberId))
1322
+ throw invalidPayload('refs');
1323
+ openRequestMemberIds.add(record.value.memberId);
1324
+ }
1325
+ return records;
1326
+ }