@claudian-collab/protocol 3.2.1 → 3.3.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,709 @@
1
+ import { COLLAB_MEMBER_REF_PREFIX } from './CollabConstants.mjs';
2
+ import { CollabError } from './CollabError.mjs';
3
+ import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
4
+ export const COLLAB_PROJECT_MEMBERSHIP_LIMITS = Object.freeze({
5
+ invitationSecretLength: 43,
6
+ invitationTtlMs: 86_400_000,
7
+ managerResponsibilityOfferRetentionMs: 2_592_000_000,
8
+ managerResponsibilityOfferTtlMs: 86_400_000,
9
+ maxCurrentManagerOffers: 100,
10
+ maxDisplayNameUtf8Bytes: 128,
11
+ maxProjectInvitations: 100,
12
+ maxProjectMembers: 100,
13
+ maxProjectNameUtf8Bytes: 256,
14
+ secretReplayTtlMs: 2_592_000_000,
15
+ transferredClaimLength: 43,
16
+ transferredClaimTtlMs: 2_592_000_000,
17
+ });
18
+ export const COLLAB_PROJECT_MEMBERSHIP_OPERATIONS = Object.freeze([
19
+ 'createCloudProject',
20
+ 'createProjectInvitation',
21
+ 'listProjectInvitations',
22
+ 'revokeProjectInvitation',
23
+ 'joinCloudProject',
24
+ 'listProjectMembers',
25
+ 'reissueTransferredMembershipClaim',
26
+ 'revokeTransferredMembershipClaim',
27
+ 'createManagerResponsibilityOffer',
28
+ 'listCurrentManagerResponsibilityOffers',
29
+ 'getManagerResponsibilityOffer',
30
+ 'acknowledgeManagerResponsibility',
31
+ 'declineManagerResponsibility',
32
+ 'cancelManagerResponsibilityOffer',
33
+ 'promoteManager',
34
+ 'demoteManager',
35
+ 'removeMember',
36
+ 'leaveProject',
37
+ ]);
38
+ function invalidPayload(field) {
39
+ return new CollabError({ code: 'protocol-payload-invalid', safeContext: { field } });
40
+ }
41
+ function exactRecord(value, field, keys) {
42
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
43
+ throw invalidPayload(field);
44
+ }
45
+ const source = value;
46
+ const expected = new Set(keys);
47
+ if (!keys.every(key => Object.hasOwn(source, key))
48
+ || Object.keys(source).some(key => !expected.has(key)))
49
+ throw invalidPayload(field);
50
+ return source;
51
+ }
52
+ function token(source, field, validate = isCollabOpaqueId) {
53
+ const value = source[field];
54
+ if (typeof value !== 'string' || !validate(value))
55
+ throw invalidPayload(field);
56
+ return value;
57
+ }
58
+ function boundedText(source, field, maximum) {
59
+ const value = source[field];
60
+ if (typeof value !== 'string'
61
+ || value.length === 0
62
+ || value.includes('\u0000')
63
+ || !hasUtf8ByteLengthAtMost(value, maximum))
64
+ throw invalidPayload(field);
65
+ return value;
66
+ }
67
+ function positiveInteger(source, field) {
68
+ const value = source[field];
69
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
70
+ throw invalidPayload(field);
71
+ }
72
+ return value;
73
+ }
74
+ function nonNegativeInteger(source, field) {
75
+ const value = source[field];
76
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
77
+ throw invalidPayload(field);
78
+ }
79
+ return value;
80
+ }
81
+ function literal(source, field, values) {
82
+ const value = source[field];
83
+ if (typeof value !== 'string' || !values.includes(value)) {
84
+ throw invalidPayload(field);
85
+ }
86
+ return value;
87
+ }
88
+ function timestamp(source, field) {
89
+ const value = source[field];
90
+ if (typeof value !== 'string'
91
+ || value.length > 64
92
+ || Number.isNaN(Date.parse(value))
93
+ || new Date(value).toISOString() !== value)
94
+ throw invalidPayload(field);
95
+ return value;
96
+ }
97
+ function hasExactDuration(start, end, durationMs) {
98
+ return Date.parse(end) === Date.parse(start) + durationMs;
99
+ }
100
+ function nullableTimestamp(source, field) {
101
+ return source[field] === null ? null : timestamp(source, field);
102
+ }
103
+ function nullableToken(source, field, validate = isCollabOpaqueId) {
104
+ return source[field] === null ? null : token(source, field, validate);
105
+ }
106
+ function secret(source, field, exactLength) {
107
+ const value = source[field];
108
+ if (typeof value !== 'string'
109
+ || value.length !== exactLength
110
+ || !/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/u.test(value))
111
+ throw invalidPayload(field);
112
+ return value;
113
+ }
114
+ function projectFields(source) {
115
+ return { projectId: token(source, 'projectId', isCollabProjectId) };
116
+ }
117
+ function mutationFields(source) {
118
+ return {
119
+ idempotencyKey: token(source, 'idempotencyKey'),
120
+ ...projectFields(source),
121
+ };
122
+ }
123
+ function personalRef(source, memberId) {
124
+ const value = token(source, 'personalRef', value => typeof value === 'string');
125
+ if (value !== `${COLLAB_MEMBER_REF_PREFIX}${memberId}`)
126
+ throw invalidPayload('personalRef');
127
+ return value;
128
+ }
129
+ function decodeProjectRequest(value) {
130
+ return projectFields(exactRecord(value, 'request', ['projectId']));
131
+ }
132
+ function decodeCreateCloudProjectRequest(value) {
133
+ const source = exactRecord(value, 'request', [
134
+ 'idempotencyKey',
135
+ 'managerDisplayName',
136
+ 'projectId',
137
+ 'projectName',
138
+ ]);
139
+ return {
140
+ ...mutationFields(source),
141
+ managerDisplayName: boundedText(source, 'managerDisplayName', COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxDisplayNameUtf8Bytes),
142
+ projectName: boundedText(source, 'projectName', COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxProjectNameUtf8Bytes),
143
+ };
144
+ }
145
+ function decodeCreateCloudProjectResponse(value) {
146
+ const source = exactRecord(value, 'response', [
147
+ 'createdAt',
148
+ 'mainOid',
149
+ 'managerSetGeneration',
150
+ 'memberId',
151
+ 'membershipRevision',
152
+ 'personalRef',
153
+ 'projectId',
154
+ 'role',
155
+ ]);
156
+ const memberId = token(source, 'memberId', isCollabMemberId);
157
+ if (source.managerSetGeneration !== 1
158
+ || source.membershipRevision !== 2
159
+ || source.role !== 'manager')
160
+ throw invalidPayload('response');
161
+ return {
162
+ createdAt: timestamp(source, 'createdAt'),
163
+ mainOid: token(source, 'mainOid', isCollabGitOid),
164
+ managerSetGeneration: 1,
165
+ memberId,
166
+ membershipRevision: 2,
167
+ personalRef: personalRef(source, memberId),
168
+ projectId: token(source, 'projectId', isCollabProjectId),
169
+ role: 'manager',
170
+ };
171
+ }
172
+ function decodeCreateInvitationRequest(value) {
173
+ const source = exactRecord(value, 'request', [
174
+ 'expectedManagerSetGeneration',
175
+ 'idempotencyKey',
176
+ 'projectId',
177
+ ]);
178
+ return {
179
+ expectedManagerSetGeneration: positiveInteger(source, 'expectedManagerSetGeneration'),
180
+ ...mutationFields(source),
181
+ };
182
+ }
183
+ function decodeCreateInvitationResponse(value) {
184
+ const source = exactRecord(value, 'response', [
185
+ 'createdAt',
186
+ 'expiresAt',
187
+ 'invitationId',
188
+ 'issuedState',
189
+ 'projectId',
190
+ 'secret',
191
+ 'secretReplayExpiresAt',
192
+ ]);
193
+ const createdAt = timestamp(source, 'createdAt');
194
+ const expiresAt = timestamp(source, 'expiresAt');
195
+ const secretReplayExpiresAt = timestamp(source, 'secretReplayExpiresAt');
196
+ if (source.issuedState !== 'active'
197
+ || !hasExactDuration(createdAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.invitationTtlMs)
198
+ || !hasExactDuration(createdAt, secretReplayExpiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs))
199
+ throw invalidPayload('response');
200
+ return {
201
+ createdAt,
202
+ expiresAt,
203
+ invitationId: token(source, 'invitationId'),
204
+ issuedState: 'active',
205
+ projectId: token(source, 'projectId', isCollabProjectId),
206
+ secret: secret(source, 'secret', COLLAB_PROJECT_MEMBERSHIP_LIMITS.invitationSecretLength),
207
+ secretReplayExpiresAt,
208
+ };
209
+ }
210
+ function decodeInvitationSummary(value) {
211
+ const source = exactRecord(value, 'invitation', [
212
+ 'createdAt',
213
+ 'expiresAt',
214
+ 'invitationId',
215
+ 'revision',
216
+ 'state',
217
+ 'terminalAt',
218
+ ]);
219
+ const state = literal(source, 'state', [
220
+ 'active',
221
+ 'redeeming',
222
+ 'redeemed',
223
+ 'revoked',
224
+ 'expired',
225
+ ]);
226
+ const createdAt = timestamp(source, 'createdAt');
227
+ const expiresAt = timestamp(source, 'expiresAt');
228
+ const terminalAt = nullableTimestamp(source, 'terminalAt');
229
+ const terminal = state === 'redeemed' || state === 'revoked' || state === 'expired';
230
+ if (!hasExactDuration(createdAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.invitationTtlMs)
231
+ || terminal !== (terminalAt !== null)
232
+ || (terminalAt !== null && Date.parse(terminalAt) < Date.parse(createdAt)))
233
+ throw invalidPayload('invitation');
234
+ return {
235
+ createdAt,
236
+ expiresAt,
237
+ invitationId: token(source, 'invitationId'),
238
+ revision: positiveInteger(source, 'revision'),
239
+ state,
240
+ terminalAt,
241
+ };
242
+ }
243
+ function decodeListInvitationsResponse(value) {
244
+ const source = exactRecord(value, 'response', [
245
+ 'invitations',
246
+ 'managerSetGeneration',
247
+ 'projectId',
248
+ ]);
249
+ if (!Array.isArray(source.invitations)
250
+ || source.invitations.length > COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxProjectInvitations)
251
+ throw invalidPayload('invitations');
252
+ return {
253
+ invitations: source.invitations.map(decodeInvitationSummary),
254
+ managerSetGeneration: positiveInteger(source, 'managerSetGeneration'),
255
+ projectId: token(source, 'projectId', isCollabProjectId),
256
+ };
257
+ }
258
+ function decodeRevokeInvitationRequest(value) {
259
+ const source = exactRecord(value, 'request', [
260
+ 'expectedInvitationRevision',
261
+ 'expectedManagerSetGeneration',
262
+ 'idempotencyKey',
263
+ 'invitationId',
264
+ 'projectId',
265
+ ]);
266
+ return {
267
+ expectedInvitationRevision: positiveInteger(source, 'expectedInvitationRevision'),
268
+ expectedManagerSetGeneration: positiveInteger(source, 'expectedManagerSetGeneration'),
269
+ ...mutationFields(source),
270
+ invitationId: token(source, 'invitationId'),
271
+ };
272
+ }
273
+ function decodeRevokeInvitationResponse(value) {
274
+ const source = exactRecord(value, 'response', [
275
+ 'invitationId',
276
+ 'projectId',
277
+ 'revision',
278
+ 'revokedAt',
279
+ 'state',
280
+ ]);
281
+ if (source.state !== 'revoked')
282
+ throw invalidPayload('state');
283
+ return {
284
+ invitationId: token(source, 'invitationId'),
285
+ projectId: token(source, 'projectId', isCollabProjectId),
286
+ revision: positiveInteger(source, 'revision'),
287
+ revokedAt: timestamp(source, 'revokedAt'),
288
+ state: 'revoked',
289
+ };
290
+ }
291
+ function decodeJoinRequest(value) {
292
+ const source = exactRecord(value, 'request', [
293
+ 'displayName',
294
+ 'idempotencyKey',
295
+ 'invitationId',
296
+ 'projectId',
297
+ 'secret',
298
+ ]);
299
+ return {
300
+ displayName: boundedText(source, 'displayName', COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxDisplayNameUtf8Bytes),
301
+ ...mutationFields(source),
302
+ invitationId: token(source, 'invitationId'),
303
+ secret: secret(source, 'secret', COLLAB_PROJECT_MEMBERSHIP_LIMITS.invitationSecretLength),
304
+ };
305
+ }
306
+ function decodeJoinResponse(value) {
307
+ const source = exactRecord(value, 'response', [
308
+ 'joinedAt',
309
+ 'mainOid',
310
+ 'managerSetGeneration',
311
+ 'memberId',
312
+ 'membershipRevision',
313
+ 'personalRef',
314
+ 'projectId',
315
+ 'role',
316
+ ]);
317
+ const memberId = token(source, 'memberId', isCollabMemberId);
318
+ if (source.membershipRevision !== 2 || source.role !== 'member') {
319
+ throw invalidPayload('response');
320
+ }
321
+ return {
322
+ joinedAt: timestamp(source, 'joinedAt'),
323
+ mainOid: token(source, 'mainOid', isCollabGitOid),
324
+ managerSetGeneration: positiveInteger(source, 'managerSetGeneration'),
325
+ memberId,
326
+ membershipRevision: 2,
327
+ personalRef: personalRef(source, memberId),
328
+ projectId: token(source, 'projectId', isCollabProjectId),
329
+ role: 'member',
330
+ };
331
+ }
332
+ function decodeMemberSummary(value) {
333
+ const source = exactRecord(value, 'member', [
334
+ 'bindingState',
335
+ 'displayName',
336
+ 'importedClaimState',
337
+ 'memberId',
338
+ 'membershipRevision',
339
+ 'role',
340
+ ]);
341
+ return {
342
+ bindingState: literal(source, 'bindingState', ['bound', 'unbound', 'hidden']),
343
+ displayName: boundedText(source, 'displayName', COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxDisplayNameUtf8Bytes),
344
+ importedClaimState: literal(source, 'importedClaimState', [
345
+ 'not-applicable',
346
+ 'original-active',
347
+ 'override-active',
348
+ 'revoked',
349
+ 'expired',
350
+ 'redeemed',
351
+ 'hidden',
352
+ ]),
353
+ memberId: token(source, 'memberId', isCollabMemberId),
354
+ membershipRevision: positiveInteger(source, 'membershipRevision'),
355
+ role: literal(source, 'role', ['manager', 'member']),
356
+ };
357
+ }
358
+ function decodeListMembersResponse(value) {
359
+ const source = exactRecord(value, 'response', [
360
+ 'managerSetGeneration',
361
+ 'members',
362
+ 'projectId',
363
+ ]);
364
+ if (!Array.isArray(source.members)
365
+ || source.members.length > COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxProjectMembers)
366
+ throw invalidPayload('members');
367
+ return {
368
+ managerSetGeneration: positiveInteger(source, 'managerSetGeneration'),
369
+ members: source.members.map(decodeMemberSummary),
370
+ projectId: token(source, 'projectId', isCollabProjectId),
371
+ };
372
+ }
373
+ function decodeClaimMutationRequest(value) {
374
+ const source = exactRecord(value, 'request', [
375
+ 'expectedClaimGeneration',
376
+ 'expectedManagerSetGeneration',
377
+ 'expectedMembershipRevision',
378
+ 'idempotencyKey',
379
+ 'memberId',
380
+ 'projectId',
381
+ ]);
382
+ return {
383
+ expectedClaimGeneration: nonNegativeInteger(source, 'expectedClaimGeneration'),
384
+ expectedManagerSetGeneration: positiveInteger(source, 'expectedManagerSetGeneration'),
385
+ expectedMembershipRevision: positiveInteger(source, 'expectedMembershipRevision'),
386
+ ...mutationFields(source),
387
+ memberId: token(source, 'memberId', isCollabMemberId),
388
+ };
389
+ }
390
+ function decodeReissueClaimResponse(value) {
391
+ const source = exactRecord(value, 'response', [
392
+ 'claim',
393
+ 'claimGeneration',
394
+ 'createdAt',
395
+ 'expiresAt',
396
+ 'memberId',
397
+ 'projectId',
398
+ 'secretReplayExpiresAt',
399
+ ]);
400
+ const createdAt = timestamp(source, 'createdAt');
401
+ const expiresAt = timestamp(source, 'expiresAt');
402
+ const secretReplayExpiresAt = timestamp(source, 'secretReplayExpiresAt');
403
+ if (!hasExactDuration(createdAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.transferredClaimTtlMs)
404
+ || !hasExactDuration(createdAt, secretReplayExpiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.secretReplayTtlMs))
405
+ throw invalidPayload('response');
406
+ return {
407
+ claim: secret(source, 'claim', COLLAB_PROJECT_MEMBERSHIP_LIMITS.transferredClaimLength),
408
+ claimGeneration: positiveInteger(source, 'claimGeneration'),
409
+ createdAt,
410
+ expiresAt,
411
+ memberId: token(source, 'memberId', isCollabMemberId),
412
+ projectId: token(source, 'projectId', isCollabProjectId),
413
+ secretReplayExpiresAt,
414
+ };
415
+ }
416
+ function decodeRevokeClaimResponse(value) {
417
+ const source = exactRecord(value, 'response', [
418
+ 'claimGeneration',
419
+ 'memberId',
420
+ 'projectId',
421
+ 'revokedAt',
422
+ 'state',
423
+ ]);
424
+ if (source.state !== 'revoked')
425
+ throw invalidPayload('state');
426
+ return {
427
+ claimGeneration: nonNegativeInteger(source, 'claimGeneration'),
428
+ memberId: token(source, 'memberId', isCollabMemberId),
429
+ projectId: token(source, 'projectId', isCollabProjectId),
430
+ revokedAt: timestamp(source, 'revokedAt'),
431
+ state: 'revoked',
432
+ };
433
+ }
434
+ function decodeOffer(value) {
435
+ const source = exactRecord(value, 'offer', [
436
+ 'acknowledgedAt',
437
+ 'expiresAt',
438
+ 'managerSetGenerationAtOffer',
439
+ 'offeredAt',
440
+ 'offerId',
441
+ 'purpose',
442
+ 'revision',
443
+ 'sourceManagerMemberId',
444
+ 'state',
445
+ 'targetMemberId',
446
+ 'targetMembershipRevisionAtOffer',
447
+ 'terminalAt',
448
+ ]);
449
+ const state = literal(source, 'state', [
450
+ 'offered',
451
+ 'acknowledged',
452
+ 'declined',
453
+ 'cancelled',
454
+ 'consumed',
455
+ 'expired',
456
+ ]);
457
+ const offeredAt = timestamp(source, 'offeredAt');
458
+ const expiresAt = timestamp(source, 'expiresAt');
459
+ const acknowledgedAt = nullableTimestamp(source, 'acknowledgedAt');
460
+ const terminalAt = nullableTimestamp(source, 'terminalAt');
461
+ const isTerminal = ['declined', 'cancelled', 'consumed', 'expired'].includes(state);
462
+ if (!hasExactDuration(offeredAt, expiresAt, COLLAB_PROJECT_MEMBERSHIP_LIMITS.managerResponsibilityOfferTtlMs)
463
+ || (state === 'offered' && acknowledgedAt !== null)
464
+ || (state === 'acknowledged' && acknowledgedAt === null)
465
+ || isTerminal !== (terminalAt !== null)
466
+ || (acknowledgedAt !== null && Date.parse(acknowledgedAt) < Date.parse(offeredAt))
467
+ || (terminalAt !== null && Date.parse(terminalAt) < Date.parse(offeredAt)))
468
+ throw invalidPayload('offer');
469
+ return {
470
+ acknowledgedAt,
471
+ expiresAt,
472
+ managerSetGenerationAtOffer: positiveInteger(source, 'managerSetGenerationAtOffer'),
473
+ offeredAt,
474
+ offerId: token(source, 'offerId'),
475
+ purpose: literal(source, 'purpose', ['manager-promotion', 'manager-leave']),
476
+ revision: positiveInteger(source, 'revision'),
477
+ sourceManagerMemberId: token(source, 'sourceManagerMemberId', isCollabMemberId),
478
+ state,
479
+ targetMemberId: token(source, 'targetMemberId', isCollabMemberId),
480
+ targetMembershipRevisionAtOffer: positiveInteger(source, 'targetMembershipRevisionAtOffer'),
481
+ terminalAt,
482
+ };
483
+ }
484
+ function decodeOfferResponse(value) {
485
+ const source = exactRecord(value, 'response', ['offer']);
486
+ return { offer: decodeOffer(source.offer) };
487
+ }
488
+ function decodeCreateOfferRequest(value) {
489
+ const source = exactRecord(value, 'request', [
490
+ 'expectedManagerSetGeneration',
491
+ 'expectedTargetMembershipRevision',
492
+ 'idempotencyKey',
493
+ 'projectId',
494
+ 'purpose',
495
+ 'targetMemberId',
496
+ ]);
497
+ return {
498
+ expectedManagerSetGeneration: positiveInteger(source, 'expectedManagerSetGeneration'),
499
+ expectedTargetMembershipRevision: positiveInteger(source, 'expectedTargetMembershipRevision'),
500
+ ...mutationFields(source),
501
+ purpose: literal(source, 'purpose', ['manager-promotion', 'manager-leave']),
502
+ targetMemberId: token(source, 'targetMemberId', isCollabMemberId),
503
+ };
504
+ }
505
+ function decodeListOffersResponse(value) {
506
+ const source = exactRecord(value, 'response', ['offers', 'projectId']);
507
+ if (!Array.isArray(source.offers)
508
+ || source.offers.length > COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxCurrentManagerOffers)
509
+ throw invalidPayload('offers');
510
+ const offers = source.offers.map(decodeOffer);
511
+ if (offers.some(item => item.state !== 'offered' && item.state !== 'acknowledged')
512
+ || offers.some((item, index) => (index > 0 && offers[index - 1].offerId.localeCompare(item.offerId, 'en-US') >= 0)))
513
+ throw invalidPayload('offers');
514
+ return {
515
+ offers,
516
+ projectId: token(source, 'projectId', isCollabProjectId),
517
+ };
518
+ }
519
+ function decodeGetOfferRequest(value) {
520
+ const source = exactRecord(value, 'request', ['offerId', 'projectId']);
521
+ return { offerId: token(source, 'offerId'), ...projectFields(source) };
522
+ }
523
+ function decodeTransitionOfferRequest(value) {
524
+ const source = exactRecord(value, 'request', [
525
+ 'expectedOfferRevision',
526
+ 'idempotencyKey',
527
+ 'offerId',
528
+ 'projectId',
529
+ ]);
530
+ return {
531
+ expectedOfferRevision: positiveInteger(source, 'expectedOfferRevision'),
532
+ ...mutationFields(source),
533
+ offerId: token(source, 'offerId'),
534
+ };
535
+ }
536
+ function decodePromoteRequest(value) {
537
+ const source = exactRecord(value, 'request', [
538
+ 'expectedManagerSetGeneration',
539
+ 'expectedOfferRevision',
540
+ 'expectedTargetMembershipRevision',
541
+ 'idempotencyKey',
542
+ 'managerResponsibilityOfferId',
543
+ 'projectId',
544
+ 'targetMemberId',
545
+ ]);
546
+ return {
547
+ expectedManagerSetGeneration: positiveInteger(source, 'expectedManagerSetGeneration'),
548
+ expectedOfferRevision: positiveInteger(source, 'expectedOfferRevision'),
549
+ expectedTargetMembershipRevision: positiveInteger(source, 'expectedTargetMembershipRevision'),
550
+ ...mutationFields(source),
551
+ managerResponsibilityOfferId: token(source, 'managerResponsibilityOfferId'),
552
+ targetMemberId: token(source, 'targetMemberId', isCollabMemberId),
553
+ };
554
+ }
555
+ function decodePromoteResponse(value) {
556
+ const source = exactRecord(value, 'response', [
557
+ 'managerSetGeneration',
558
+ 'membershipRevision',
559
+ 'offerRevision',
560
+ 'projectId',
561
+ 'promotedMemberId',
562
+ ]);
563
+ return {
564
+ managerSetGeneration: positiveInteger(source, 'managerSetGeneration'),
565
+ membershipRevision: positiveInteger(source, 'membershipRevision'),
566
+ offerRevision: positiveInteger(source, 'offerRevision'),
567
+ projectId: token(source, 'projectId', isCollabProjectId),
568
+ promotedMemberId: token(source, 'promotedMemberId', isCollabMemberId),
569
+ };
570
+ }
571
+ function decodeTargetRoleRequest(value) {
572
+ const source = exactRecord(value, 'request', [
573
+ 'expectedManagerSetGeneration',
574
+ 'expectedTargetMembershipRevision',
575
+ 'idempotencyKey',
576
+ 'projectId',
577
+ 'targetMemberId',
578
+ ]);
579
+ return {
580
+ expectedManagerSetGeneration: positiveInteger(source, 'expectedManagerSetGeneration'),
581
+ expectedTargetMembershipRevision: positiveInteger(source, 'expectedTargetMembershipRevision'),
582
+ ...mutationFields(source),
583
+ targetMemberId: token(source, 'targetMemberId', isCollabMemberId),
584
+ };
585
+ }
586
+ function decodeDemoteResponse(value) {
587
+ const source = exactRecord(value, 'response', [
588
+ 'demotedMemberId',
589
+ 'managerSetGeneration',
590
+ 'membershipRevision',
591
+ 'projectId',
592
+ ]);
593
+ return {
594
+ demotedMemberId: token(source, 'demotedMemberId', isCollabMemberId),
595
+ managerSetGeneration: positiveInteger(source, 'managerSetGeneration'),
596
+ membershipRevision: positiveInteger(source, 'membershipRevision'),
597
+ projectId: token(source, 'projectId', isCollabProjectId),
598
+ };
599
+ }
600
+ function decodeRemoveResponse(value) {
601
+ const source = exactRecord(value, 'response', [
602
+ 'discardedRequestId',
603
+ 'managerSetGeneration',
604
+ 'memberId',
605
+ 'projectId',
606
+ 'removedAt',
607
+ 'status',
608
+ ]);
609
+ if (source.status !== 'revoked')
610
+ throw invalidPayload('status');
611
+ return {
612
+ discardedRequestId: nullableToken(source, 'discardedRequestId'),
613
+ managerSetGeneration: positiveInteger(source, 'managerSetGeneration'),
614
+ memberId: token(source, 'memberId', isCollabMemberId),
615
+ projectId: token(source, 'projectId', isCollabProjectId),
616
+ removedAt: timestamp(source, 'removedAt'),
617
+ status: 'revoked',
618
+ };
619
+ }
620
+ function decodeLeaveRequest(value) {
621
+ const source = exactRecord(value, 'request', [
622
+ 'expectedManagerSetGeneration',
623
+ 'expectedMembershipRevision',
624
+ 'expectedOfferRevision',
625
+ 'expectedPersonalRefOid',
626
+ 'idempotencyKey',
627
+ 'managerResponsibilityOfferId',
628
+ 'projectId',
629
+ ]);
630
+ const managerResponsibilityOfferId = nullableToken(source, 'managerResponsibilityOfferId');
631
+ const expectedOfferRevision = source.expectedOfferRevision === null
632
+ ? null
633
+ : positiveInteger(source, 'expectedOfferRevision');
634
+ if ((managerResponsibilityOfferId === null) !== (expectedOfferRevision === null)) {
635
+ throw invalidPayload('managerResponsibilityOfferId');
636
+ }
637
+ return {
638
+ expectedManagerSetGeneration: positiveInteger(source, 'expectedManagerSetGeneration'),
639
+ expectedMembershipRevision: positiveInteger(source, 'expectedMembershipRevision'),
640
+ expectedOfferRevision,
641
+ expectedPersonalRefOid: token(source, 'expectedPersonalRefOid', isCollabGitOid),
642
+ ...mutationFields(source),
643
+ managerResponsibilityOfferId,
644
+ };
645
+ }
646
+ function decodeLeaveResponse(value) {
647
+ const source = exactRecord(value, 'response', [
648
+ 'discardedRequestId',
649
+ 'leftAt',
650
+ 'managerSetGeneration',
651
+ 'memberId',
652
+ 'projectId',
653
+ 'promotedSuccessorMemberId',
654
+ 'status',
655
+ ]);
656
+ if (source.status !== 'left')
657
+ throw invalidPayload('status');
658
+ return {
659
+ discardedRequestId: nullableToken(source, 'discardedRequestId'),
660
+ leftAt: timestamp(source, 'leftAt'),
661
+ managerSetGeneration: positiveInteger(source, 'managerSetGeneration'),
662
+ memberId: token(source, 'memberId', isCollabMemberId),
663
+ projectId: token(source, 'projectId', isCollabProjectId),
664
+ promotedSuccessorMemberId: nullableToken(source, 'promotedSuccessorMemberId', isCollabMemberId),
665
+ status: 'left',
666
+ };
667
+ }
668
+ function codec(decodeRequestValue, decodeResponse) {
669
+ return Object.freeze({
670
+ decodeRequest: (input) => {
671
+ try {
672
+ return { status: 'ok', value: decodeRequestValue(input) };
673
+ }
674
+ catch (error) {
675
+ if (error instanceof CollabError && error.code === 'protocol-payload-invalid') {
676
+ return { error, status: 'invalid' };
677
+ }
678
+ throw error;
679
+ }
680
+ },
681
+ decodeResponse,
682
+ });
683
+ }
684
+ export const COLLAB_PROJECT_MEMBERSHIP_OPERATION_CODECS = Object.freeze({
685
+ createCloudProject: codec(decodeCreateCloudProjectRequest, decodeCreateCloudProjectResponse),
686
+ createProjectInvitation: codec(decodeCreateInvitationRequest, decodeCreateInvitationResponse),
687
+ listProjectInvitations: codec(decodeProjectRequest, decodeListInvitationsResponse),
688
+ revokeProjectInvitation: codec(decodeRevokeInvitationRequest, decodeRevokeInvitationResponse),
689
+ joinCloudProject: codec(decodeJoinRequest, decodeJoinResponse),
690
+ listProjectMembers: codec(decodeProjectRequest, decodeListMembersResponse),
691
+ reissueTransferredMembershipClaim: codec(decodeClaimMutationRequest, decodeReissueClaimResponse),
692
+ revokeTransferredMembershipClaim: codec(decodeClaimMutationRequest, decodeRevokeClaimResponse),
693
+ createManagerResponsibilityOffer: codec(decodeCreateOfferRequest, decodeOfferResponse),
694
+ listCurrentManagerResponsibilityOffers: codec(decodeProjectRequest, decodeListOffersResponse),
695
+ getManagerResponsibilityOffer: codec(decodeGetOfferRequest, decodeOfferResponse),
696
+ acknowledgeManagerResponsibility: codec(decodeTransitionOfferRequest, decodeOfferResponse),
697
+ declineManagerResponsibility: codec(decodeTransitionOfferRequest, decodeOfferResponse),
698
+ cancelManagerResponsibilityOffer: codec(decodeTransitionOfferRequest, decodeOfferResponse),
699
+ promoteManager: codec(decodePromoteRequest, decodePromoteResponse),
700
+ demoteManager: codec(decodeTargetRoleRequest, decodeDemoteResponse),
701
+ removeMember: codec(decodeTargetRoleRequest, decodeRemoveResponse),
702
+ leaveProject: codec(decodeLeaveRequest, decodeLeaveResponse),
703
+ });
704
+ export function decodeCollabProjectMembershipOperationRequest(operation, value) {
705
+ return COLLAB_PROJECT_MEMBERSHIP_OPERATION_CODECS[operation].decodeRequest(value);
706
+ }
707
+ export function decodeCollabProjectMembershipOperationResponse(operation, value) {
708
+ return COLLAB_PROJECT_MEMBERSHIP_OPERATION_CODECS[operation].decodeResponse(value);
709
+ }