@claudian-collab/protocol 1.0.0 → 2.0.0

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