@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,537 @@
1
+ import { COLLAB_LIMITS, COLLAB_MAIN_REF, COLLAB_PROTOCOL_VERSION, } from './CollabConstants.mjs';
2
+ import { COLLAB_CLOUD_BINDING_LIMITS, } from './CollabCloudBinding.mjs';
3
+ import { CollabError } from './CollabError.mjs';
4
+ import { collabMemberRef } from './types.mjs';
5
+ import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
6
+ export const DEVELOPMENT_BOOTSTRAP_MANIFEST_SCHEMA_VERSION = 1;
7
+ export const DEVELOPMENT_BOOTSTRAP_ATTEMPT_STATES = Object.freeze([
8
+ 'collecting',
9
+ 'validating',
10
+ 'ready',
11
+ 'activating',
12
+ 'rejected',
13
+ 'cancelled',
14
+ 'recovery-required',
15
+ 'activated',
16
+ ]);
17
+ export const DEVELOPMENT_BOOTSTRAP_ACTIVATION_PHASES = Object.freeze([
18
+ 'publish-intent',
19
+ 'repository-published',
20
+ 'activated',
21
+ 'completed',
22
+ ]);
23
+ export const DEVELOPMENT_BOOTSTRAP_CANCELLATION_PHASES = Object.freeze([
24
+ 'cancel-intent',
25
+ 'cancelled',
26
+ 'recovery-required',
27
+ ]);
28
+ export const DEVELOPMENT_BOOTSTRAP_OPERATIONS = Object.freeze([
29
+ 'beginDevelopmentBootstrap',
30
+ 'submitDevelopmentBootstrapReport',
31
+ 'getDevelopmentBootstrap',
32
+ 'activateDevelopmentBootstrap',
33
+ 'cancelDevelopmentBootstrap',
34
+ 'putDevelopmentBootstrapGitBundle',
35
+ ]);
36
+ const ATTEMPT_STATE_SET = new Set(DEVELOPMENT_BOOTSTRAP_ATTEMPT_STATES);
37
+ const ACTIVATION_PHASE_SET = new Set(DEVELOPMENT_BOOTSTRAP_ACTIVATION_PHASES);
38
+ const CANCELLATION_PHASE_SET = new Set(DEVELOPMENT_BOOTSTRAP_CANCELLATION_PHASES);
39
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
40
+ function invalidPayload(field) {
41
+ return new CollabError({
42
+ code: 'protocol-payload-invalid',
43
+ safeContext: { field },
44
+ });
45
+ }
46
+ function record(value, field) {
47
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
48
+ throw invalidPayload(field);
49
+ }
50
+ return value;
51
+ }
52
+ function exactRecord(value, field, keys) {
53
+ const source = record(value, field);
54
+ const expected = new Set(keys);
55
+ if (!keys.every(key => Object.hasOwn(source, key))
56
+ || Object.keys(source).some(key => !expected.has(key)))
57
+ throw invalidPayload(field);
58
+ return source;
59
+ }
60
+ function exactRecordWithOptional(value, field, required, optional) {
61
+ const source = record(value, field);
62
+ const allowed = new Set([...required, ...optional]);
63
+ if (!required.every(key => Object.hasOwn(source, key))
64
+ || Object.keys(source).some(key => !allowed.has(key)))
65
+ throw invalidPayload(field);
66
+ return source;
67
+ }
68
+ function stringField(source, field, maximum, validate) {
69
+ const value = source[field];
70
+ if (typeof value !== 'string'
71
+ || value.length === 0
72
+ || value.length > maximum
73
+ || (validate && !validate(value)))
74
+ throw invalidPayload(field);
75
+ return value;
76
+ }
77
+ function timestamp(source, field) {
78
+ const value = stringField(source, field, 64);
79
+ if (Number.isNaN(Date.parse(value)) || new Date(value).toISOString() !== value) {
80
+ throw invalidPayload(field);
81
+ }
82
+ return value;
83
+ }
84
+ function nonNegativeInteger(source, field) {
85
+ const value = source[field];
86
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
87
+ throw invalidPayload(field);
88
+ }
89
+ return value;
90
+ }
91
+ function positiveInteger(source, field, maximum) {
92
+ const value = nonNegativeInteger(source, field);
93
+ if (value < 1 || (maximum !== undefined && value > maximum)) {
94
+ throw invalidPayload(field);
95
+ }
96
+ return value;
97
+ }
98
+ function exactTrue(source, field) {
99
+ if (source[field] !== true)
100
+ throw invalidPayload(field);
101
+ return true;
102
+ }
103
+ function sha256(source, field) {
104
+ return stringField(source, field, 64, value => SHA256_PATTERN.test(value));
105
+ }
106
+ function assertSerializedLimit(value, maximum, field) {
107
+ let serialized;
108
+ try {
109
+ serialized = JSON.stringify(value);
110
+ }
111
+ catch {
112
+ throw invalidPayload(field);
113
+ }
114
+ if (!hasUtf8ByteLengthAtMost(serialized, maximum))
115
+ throw invalidPayload(field);
116
+ }
117
+ function decodeComparisonMember(value) {
118
+ const source = exactRecord(value, 'comparison.members', [
119
+ 'activatedAt',
120
+ 'createdAt',
121
+ 'displayName',
122
+ 'memberId',
123
+ 'personalRef',
124
+ 'role',
125
+ 'status',
126
+ ]);
127
+ const memberId = stringField(source, 'memberId', 64, isCollabMemberId);
128
+ const role = source.role;
129
+ if (role !== 'manager' && role !== 'member')
130
+ throw invalidPayload('role');
131
+ if (source.status !== 'active')
132
+ throw invalidPayload('status');
133
+ const personalRef = stringField(source, 'personalRef', COLLAB_LIMITS.maxRepositoryPathUtf16);
134
+ if (personalRef !== collabMemberRef(memberId))
135
+ throw invalidPayload('personalRef');
136
+ return {
137
+ activatedAt: timestamp(source, 'activatedAt'),
138
+ createdAt: timestamp(source, 'createdAt'),
139
+ displayName: stringField(source, 'displayName', COLLAB_LIMITS.maxMemberDisplayNameUtf16),
140
+ memberId,
141
+ personalRef,
142
+ role,
143
+ status: 'active',
144
+ };
145
+ }
146
+ function decodeComparison(value) {
147
+ const source = exactRecord(value, 'comparison', [
148
+ 'mainOid',
149
+ 'mainRef',
150
+ 'managerSetGeneration',
151
+ 'members',
152
+ 'projectCreatedAt',
153
+ 'projectId',
154
+ 'projectName',
155
+ 'sourceCaFingerprint',
156
+ 'sourceEventSequence',
157
+ 'sourceHostMemberId',
158
+ ]);
159
+ if (!Array.isArray(source.members) || source.members.length !== 2) {
160
+ throw invalidPayload('members');
161
+ }
162
+ const members = source.members.map(decodeComparisonMember);
163
+ if (members[0].memberId.localeCompare(members[1].memberId, 'en-US') >= 0
164
+ || !members.some(member => member.role === 'manager'))
165
+ throw invalidPayload('members');
166
+ const sourceHostMemberId = stringField(source, 'sourceHostMemberId', 64, isCollabMemberId);
167
+ if (!members.some(member => member.memberId === sourceHostMemberId)) {
168
+ throw invalidPayload('sourceHostMemberId');
169
+ }
170
+ if (source.mainRef !== COLLAB_MAIN_REF)
171
+ throw invalidPayload('mainRef');
172
+ return {
173
+ mainOid: stringField(source, 'mainOid', 64, isCollabGitOid),
174
+ mainRef: COLLAB_MAIN_REF,
175
+ managerSetGeneration: nonNegativeInteger(source, 'managerSetGeneration'),
176
+ members,
177
+ projectCreatedAt: timestamp(source, 'projectCreatedAt'),
178
+ projectId: stringField(source, 'projectId', 64, isCollabProjectId),
179
+ projectName: stringField(source, 'projectName', COLLAB_LIMITS.maxProjectNameUtf16),
180
+ sourceCaFingerprint: stringField(source, 'sourceCaFingerprint', 64, candidate => SHA256_PATTERN.test(candidate)),
181
+ sourceEventSequence: nonNegativeInteger(source, 'sourceEventSequence'),
182
+ sourceHostMemberId,
183
+ };
184
+ }
185
+ function decodeSourceEligibility(value) {
186
+ const keys = [
187
+ 'liveInvitations',
188
+ 'nonActiveMemberships',
189
+ 'nonterminalAcceptOperations',
190
+ 'nonterminalHostTransfers',
191
+ 'nonterminalManagerOffers',
192
+ 'requestComments',
193
+ 'requests',
194
+ 'terminalProjectTransitions',
195
+ 'ticketComments',
196
+ 'ticketMentions',
197
+ 'ticketRelations',
198
+ 'tickets',
199
+ ];
200
+ const source = exactRecord(value, 'sourceEligibility', keys);
201
+ if (keys.some(key => source[key] !== 0))
202
+ throw invalidPayload('sourceEligibility');
203
+ return {
204
+ liveInvitations: 0,
205
+ nonActiveMemberships: 0,
206
+ nonterminalAcceptOperations: 0,
207
+ nonterminalHostTransfers: 0,
208
+ nonterminalManagerOffers: 0,
209
+ requestComments: 0,
210
+ requests: 0,
211
+ terminalProjectTransitions: 0,
212
+ ticketComments: 0,
213
+ ticketMentions: 0,
214
+ ticketRelations: 0,
215
+ tickets: 0,
216
+ };
217
+ }
218
+ function decodeGit(value, comparison) {
219
+ const source = exactRecord(value, 'git', ['bundle', 'objectFormat', 'refs']);
220
+ if (source.objectFormat !== 'sha1' && source.objectFormat !== 'sha256') {
221
+ throw invalidPayload('objectFormat');
222
+ }
223
+ const objectFormat = source.objectFormat;
224
+ const bundleSource = exactRecord(source.bundle, 'bundle', ['byteCount', 'sha256']);
225
+ if (!Array.isArray(source.refs) || source.refs.length !== 3)
226
+ throw invalidPayload('refs');
227
+ const refs = source.refs.map((item) => {
228
+ const ref = exactRecord(item, 'refs', ['name', 'oid']);
229
+ const oid = stringField(ref, 'oid', 64, isCollabGitOid);
230
+ if (oid.length !== (objectFormat === 'sha1' ? 40 : 64))
231
+ throw invalidPayload('oid');
232
+ return {
233
+ name: stringField(ref, 'name', COLLAB_LIMITS.maxRepositoryPathUtf16),
234
+ oid,
235
+ };
236
+ });
237
+ if (refs.some((item, index) => (index > 0 && refs[index - 1].name.localeCompare(item.name, 'en-US') >= 0)))
238
+ throw invalidPayload('refs');
239
+ const expectedNames = [
240
+ COLLAB_MAIN_REF,
241
+ ...comparison.members.map(member => member.personalRef),
242
+ ].sort((left, right) => left.localeCompare(right, 'en-US'));
243
+ if (refs.some((item, index) => item.name !== expectedNames[index])
244
+ || refs.find(item => item.name === COLLAB_MAIN_REF)?.oid !== comparison.mainOid)
245
+ throw invalidPayload('refs');
246
+ return {
247
+ bundle: {
248
+ byteCount: positiveInteger(bundleSource, 'byteCount', COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapGitBundleBytes),
249
+ sha256: sha256(bundleSource, 'sha256'),
250
+ },
251
+ objectFormat,
252
+ refs,
253
+ };
254
+ }
255
+ export function decodeDevelopmentBootstrapManifest(value) {
256
+ assertSerializedLimit(value, COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapManifestUtf8Bytes, 'manifest');
257
+ const source = exactRecord(value, 'manifest', [
258
+ 'attemptId',
259
+ 'comparison',
260
+ 'createdAt',
261
+ 'git',
262
+ 'manifestSchemaVersion',
263
+ 'protocolVersion',
264
+ 'sourceEligibility',
265
+ ]);
266
+ if (source.manifestSchemaVersion !== DEVELOPMENT_BOOTSTRAP_MANIFEST_SCHEMA_VERSION
267
+ || source.protocolVersion !== COLLAB_PROTOCOL_VERSION)
268
+ throw invalidPayload('manifestVersion');
269
+ const comparison = decodeComparison(source.comparison);
270
+ return {
271
+ attemptId: stringField(source, 'attemptId', 128, isCollabOpaqueId),
272
+ comparison,
273
+ createdAt: timestamp(source, 'createdAt'),
274
+ git: decodeGit(source.git, comparison),
275
+ manifestSchemaVersion: DEVELOPMENT_BOOTSTRAP_MANIFEST_SCHEMA_VERSION,
276
+ protocolVersion: COLLAB_PROTOCOL_VERSION,
277
+ sourceEligibility: decodeSourceEligibility(source.sourceEligibility),
278
+ };
279
+ }
280
+ export function encodeDevelopmentBootstrapManifestCanonicalJson(value) {
281
+ return JSON.stringify(decodeDevelopmentBootstrapManifest(value));
282
+ }
283
+ function decodeClientReadiness(value) {
284
+ const keys = [
285
+ 'cleanupSettled',
286
+ 'collabGitChildrenDrained',
287
+ 'conflictRecoverySettled',
288
+ 'hostTransferSettled',
289
+ 'joinSettled',
290
+ 'leaveSettled',
291
+ 'managerResponsibilitySettled',
292
+ 'projectOperationQueueDrained',
293
+ 'projectSetupSettled',
294
+ 'projectWorkSessionClosed',
295
+ 'publishSettled',
296
+ 'reconciliationSettled',
297
+ 'reconnectSettled',
298
+ 'repositoryIdentityExact',
299
+ 'retirementSettled',
300
+ ];
301
+ const source = exactRecord(value, 'clientReadiness', keys);
302
+ const result = Object.fromEntries(keys.map(key => [key, exactTrue(source, key)]));
303
+ return result;
304
+ }
305
+ function decodeHostStopAttestation(value, attemptId, projectId) {
306
+ const source = exactRecord(value, 'hostStopAttestation', [
307
+ 'attemptId',
308
+ 'autoStartDisabled',
309
+ 'fenceDurable',
310
+ 'fenceId',
311
+ 'hostStopped',
312
+ 'manifestSha256',
313
+ 'projectId',
314
+ 'resourcesDrained',
315
+ 'routeUnregistered',
316
+ 'stoppedAt',
317
+ ]);
318
+ const decodedAttemptId = stringField(source, 'attemptId', 128, isCollabOpaqueId);
319
+ const decodedProjectId = stringField(source, 'projectId', 64, isCollabProjectId);
320
+ if (decodedAttemptId !== attemptId || decodedProjectId !== projectId) {
321
+ throw invalidPayload('hostStopAttestation');
322
+ }
323
+ return {
324
+ attemptId: decodedAttemptId,
325
+ autoStartDisabled: exactTrue(source, 'autoStartDisabled'),
326
+ fenceDurable: exactTrue(source, 'fenceDurable'),
327
+ fenceId: stringField(source, 'fenceId', 128, isCollabOpaqueId),
328
+ hostStopped: exactTrue(source, 'hostStopped'),
329
+ manifestSha256: sha256(source, 'manifestSha256'),
330
+ projectId: decodedProjectId,
331
+ resourcesDrained: exactTrue(source, 'resourcesDrained'),
332
+ routeUnregistered: exactTrue(source, 'routeUnregistered'),
333
+ stoppedAt: timestamp(source, 'stoppedAt'),
334
+ };
335
+ }
336
+ export function decodeDevelopmentBootstrapReport(value) {
337
+ assertSerializedLimit(value, COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapReportUtf8Bytes, 'report');
338
+ const source = exactRecordWithOptional(value, 'report', [
339
+ 'attemptId',
340
+ 'capturedAt',
341
+ 'clientReadiness',
342
+ 'comparison',
343
+ 'observedPersonalRefOid',
344
+ 'reporterMemberId',
345
+ ], ['hostStopAttestation']);
346
+ const attemptId = stringField(source, 'attemptId', 128, isCollabOpaqueId);
347
+ const comparison = decodeComparison(source.comparison);
348
+ const reporterMemberId = stringField(source, 'reporterMemberId', 64, isCollabMemberId);
349
+ if (!comparison.members.some(member => member.memberId === reporterMemberId)) {
350
+ throw invalidPayload('reporterMemberId');
351
+ }
352
+ const isHost = reporterMemberId === comparison.sourceHostMemberId;
353
+ if (isHost !== Object.hasOwn(source, 'hostStopAttestation')) {
354
+ throw invalidPayload('hostStopAttestation');
355
+ }
356
+ const hostStopAttestation = isHost
357
+ ? decodeHostStopAttestation(source.hostStopAttestation, attemptId, comparison.projectId)
358
+ : undefined;
359
+ return {
360
+ attemptId,
361
+ capturedAt: timestamp(source, 'capturedAt'),
362
+ clientReadiness: decodeClientReadiness(source.clientReadiness),
363
+ comparison,
364
+ ...(hostStopAttestation ? { hostStopAttestation } : {}),
365
+ observedPersonalRefOid: stringField(source, 'observedPersonalRefOid', 64, isCollabGitOid),
366
+ reporterMemberId,
367
+ };
368
+ }
369
+ function decodeActivationResult(value) {
370
+ const source = exactRecord(value, 'activationResult', [
371
+ 'activatedAt',
372
+ 'activationOperationId',
373
+ 'placementGeneration',
374
+ 'projectId',
375
+ ]);
376
+ return {
377
+ activatedAt: timestamp(source, 'activatedAt'),
378
+ activationOperationId: stringField(source, 'activationOperationId', 128, isCollabOpaqueId),
379
+ placementGeneration: positiveInteger(source, 'placementGeneration'),
380
+ projectId: stringField(source, 'projectId', 64, isCollabProjectId),
381
+ };
382
+ }
383
+ function decodeAttemptStatus(value) {
384
+ const source = exactRecordWithOptional(value, 'attempt', [
385
+ 'attemptId',
386
+ 'bundleState',
387
+ 'createdAt',
388
+ 'expiresAt',
389
+ 'manifestSha256',
390
+ 'projectId',
391
+ 'reporterMemberIds',
392
+ 'state',
393
+ ], ['activationPhase', 'activationResult', 'cancellationPhase']);
394
+ if (typeof source.state !== 'string' || !ATTEMPT_STATE_SET.has(source.state)) {
395
+ throw invalidPayload('state');
396
+ }
397
+ if (source.bundleState !== 'missing'
398
+ && source.bundleState !== 'uploaded'
399
+ && source.bundleState !== 'validated')
400
+ throw invalidPayload('bundleState');
401
+ if (!Array.isArray(source.reporterMemberIds) || source.reporterMemberIds.length > 2) {
402
+ throw invalidPayload('reporterMemberIds');
403
+ }
404
+ const reporterMemberIds = source.reporterMemberIds.map((item) => {
405
+ if (!isCollabMemberId(item))
406
+ throw invalidPayload('reporterMemberIds');
407
+ return item;
408
+ });
409
+ if (reporterMemberIds.some((item, index) => (index > 0 && reporterMemberIds[index - 1].localeCompare(item, 'en-US') >= 0)))
410
+ throw invalidPayload('reporterMemberIds');
411
+ const activationPhase = source.activationPhase;
412
+ const cancellationPhase = source.cancellationPhase;
413
+ if (activationPhase !== undefined
414
+ && (typeof activationPhase !== 'string' || !ACTIVATION_PHASE_SET.has(activationPhase)))
415
+ throw invalidPayload('activationPhase');
416
+ if (cancellationPhase !== undefined
417
+ && (typeof cancellationPhase !== 'string' || !CANCELLATION_PHASE_SET.has(cancellationPhase)))
418
+ throw invalidPayload('cancellationPhase');
419
+ const activationResult = source.activationResult === undefined
420
+ ? undefined
421
+ : decodeActivationResult(source.activationResult);
422
+ const projectId = stringField(source, 'projectId', 64, isCollabProjectId);
423
+ if (source.state === 'activated'
424
+ && ((activationPhase !== 'activated' && activationPhase !== 'completed')
425
+ || activationResult === undefined))
426
+ throw invalidPayload('activationResult');
427
+ if (source.state !== 'activated'
428
+ && activationResult !== undefined)
429
+ throw invalidPayload('activationResult');
430
+ if (activationPhase !== undefined && cancellationPhase !== undefined) {
431
+ throw invalidPayload('attemptPhase');
432
+ }
433
+ if (activationResult !== undefined && activationResult.projectId !== projectId) {
434
+ throw invalidPayload('activationResult');
435
+ }
436
+ return {
437
+ ...(activationPhase
438
+ ? { activationPhase: activationPhase }
439
+ : {}),
440
+ ...(activationResult ? { activationResult } : {}),
441
+ attemptId: stringField(source, 'attemptId', 128, isCollabOpaqueId),
442
+ bundleState: source.bundleState,
443
+ ...(cancellationPhase
444
+ ? { cancellationPhase: cancellationPhase }
445
+ : {}),
446
+ createdAt: timestamp(source, 'createdAt'),
447
+ expiresAt: timestamp(source, 'expiresAt'),
448
+ manifestSha256: sha256(source, 'manifestSha256'),
449
+ projectId,
450
+ reporterMemberIds,
451
+ state: source.state,
452
+ };
453
+ }
454
+ function decodeAttemptOnlyRequest(value) {
455
+ const source = exactRecord(value, 'request', ['attemptId']);
456
+ return { attemptId: stringField(source, 'attemptId', 128, isCollabOpaqueId) };
457
+ }
458
+ function decodeOperationRequest(operation, value) {
459
+ switch (operation) {
460
+ case 'beginDevelopmentBootstrap': {
461
+ const source = exactRecord(value, 'request', ['manifest']);
462
+ return { manifest: decodeDevelopmentBootstrapManifest(source.manifest) };
463
+ }
464
+ case 'submitDevelopmentBootstrapReport': {
465
+ const source = exactRecord(value, 'request', ['attemptId', 'report']);
466
+ const attemptId = stringField(source, 'attemptId', 128, isCollabOpaqueId);
467
+ const report = decodeDevelopmentBootstrapReport(source.report);
468
+ if (report.attemptId !== attemptId)
469
+ throw invalidPayload('attemptId');
470
+ return { attemptId, report };
471
+ }
472
+ case 'getDevelopmentBootstrap':
473
+ case 'cancelDevelopmentBootstrap':
474
+ return decodeAttemptOnlyRequest(value);
475
+ case 'activateDevelopmentBootstrap': {
476
+ const source = exactRecord(value, 'request', ['attemptId', 'manifestSha256']);
477
+ return {
478
+ attemptId: stringField(source, 'attemptId', 128, isCollabOpaqueId),
479
+ manifestSha256: sha256(source, 'manifestSha256'),
480
+ };
481
+ }
482
+ case 'putDevelopmentBootstrapGitBundle': {
483
+ const source = exactRecord(value, 'request', [
484
+ 'attemptId',
485
+ 'byteCount',
486
+ 'contentEncoding',
487
+ 'contentType',
488
+ 'sha256',
489
+ ]);
490
+ if (source.contentEncoding !== 'identity'
491
+ || source.contentType !== 'application/x-git-bundle')
492
+ throw invalidPayload('contentType');
493
+ return {
494
+ attemptId: stringField(source, 'attemptId', 128, isCollabOpaqueId),
495
+ byteCount: positiveInteger(source, 'byteCount', COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapGitBundleBytes),
496
+ contentEncoding: 'identity',
497
+ contentType: 'application/x-git-bundle',
498
+ sha256: sha256(source, 'sha256'),
499
+ };
500
+ }
501
+ }
502
+ }
503
+ function decodeRequestResult(operation, value) {
504
+ try {
505
+ return { status: 'ok', value: decodeOperationRequest(operation, value) };
506
+ }
507
+ catch (error) {
508
+ return {
509
+ status: 'invalid',
510
+ error: error instanceof CollabError ? error : invalidPayload('request'),
511
+ };
512
+ }
513
+ }
514
+ function codec(operation) {
515
+ return Object.freeze({
516
+ decodeRequest: (value) => decodeRequestResult(operation, value),
517
+ decodeResponse: decodeAttemptStatus,
518
+ });
519
+ }
520
+ export const DEVELOPMENT_BOOTSTRAP_OPERATION_CODECS = Object.freeze({
521
+ beginDevelopmentBootstrap: codec('beginDevelopmentBootstrap'),
522
+ submitDevelopmentBootstrapReport: codec('submitDevelopmentBootstrapReport'),
523
+ getDevelopmentBootstrap: codec('getDevelopmentBootstrap'),
524
+ activateDevelopmentBootstrap: codec('activateDevelopmentBootstrap'),
525
+ cancelDevelopmentBootstrap: codec('cancelDevelopmentBootstrap'),
526
+ putDevelopmentBootstrapGitBundle: codec('putDevelopmentBootstrapGitBundle'),
527
+ });
528
+ export function developmentBootstrapOperationCodec(operation) {
529
+ const selected = DEVELOPMENT_BOOTSTRAP_OPERATION_CODECS[operation];
530
+ if (!selected) {
531
+ throw new CollabError({
532
+ code: 'operation-failed',
533
+ safeContext: { reason: 'bootstrap-operation-codec-missing' },
534
+ });
535
+ }
536
+ return selected;
537
+ }
@@ -0,0 +1,16 @@
1
+ export { COLLAB_LIMITS, COLLAB_MAIN_REF, COLLAB_MEMBER_REF_PREFIX, COLLAB_PROTOCOL_VERSION, } from './CollabConstants.mjs';
2
+ export { COLLAB_AUTHORITY_TRANSFER_CANCELLABLE_PHASES, COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES, COLLAB_AUTHORITY_TRANSFER_OPERATIONS, COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES, COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES, decodeCollabAuthorityRelinquishmentProof, decodeCollabAuthorityTransferOperationRequest, decodeCollabAuthorityTransferOperationResponse, decodeCollabAuthorityTransferProposal, decodeCollabAuthorityTransferStatus, decodeCollabTransferredMembershipClaim, decodeCollabTransferredMembershipClaimBatch, decodeCollabTransferredMembershipClaimCustodyReceipt, decodeCollabTransferredMembershipRedemptionReceipt, encodeCollabAuthorityRelinquishmentProofSigningInput, encodeCollabTransferredMembershipClaimBatchDigestInput, encodeCollabTransferredMembershipRedemptionReceiptSigningInput, } from './CollabAuthorityTransfer.mjs';
3
+ export { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS, COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS, COLLAB_CHECKPOINT_PROFILES, COLLAB_PROJECT_CHECKPOINT_ARTIFACTS, COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION, COLLAB_PROJECT_COORDINATION_FORMAT_VERSION, COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS, COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION, decodeCollabProjectCheckpointCoordinationNdjson, decodeCollabProjectCheckpointManifest, encodeCollabProjectCheckpointCoordinationNdjson, encodeCollabProjectCheckpointManifestCanonicalJson, encodeCollabProjectCheckpointManifestDigestInput, encodeCollabProtectedClaimAssociatedData, validateCollabProjectCheckpointConsistency, } from './CollabProjectCheckpoint.mjs';
4
+ export { COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION, COLLAB_PROJECT_BACKUP_RECORD_KINDS, collabProjectBackupIdempotencyRecordId, decodeCollabProjectBackupCheckpointCoordinationNdjson, decodeCollabProjectBackupCheckpointManifest, encodeCollabProjectBackupCheckpointCoordinationNdjson, encodeCollabProjectBackupCheckpointManifestCanonicalJson, encodeCollabProjectBackupCheckpointManifestDigestInput, validateCollabProjectBackupCheckpointConsistency, } from './CollabProjectBackupCheckpoint.mjs';
5
+ export { COLLAB_PROJECT_RETIREMENT_OPERATIONS, COLLAB_PROJECT_RETIREMENT_RESULT_KINDS, decodeCollabProjectRetirementAcknowledgement, decodeCollabProjectRetirementOperationRequest, decodeCollabProjectRetirementOperationResponse, decodeCollabProjectRetirementRequest, decodeCollabProjectRetirementResult, } from './CollabProjectRetirement.mjs';
6
+ export { COLLAB_CLOUD_BINDING_LIMITS, COLLAB_CLOUD_BINDING_VERSION, COLLAB_CLOUD_CAPABILITIES, COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION, COLLAB_CLOUD_JSON_OPERATIONS, collabCloudAuthorityTransferArtifactRoute, collabCloudCapabilityDocument, collabCloudCapabilitySupported, collabCloudCapabilitiesRoute, collabCloudErrorEnvelope, collabCloudGitRoute, collabCloudProjectCheckpointExportArtifactRoute, collabCloudProjectCheckpointExportRoute, collabCloudProjectEventsRoute, collabCloudProjectOperationRoute, collabCloudSuccessEnvelope, collabDevelopmentBootstrapRoute, decodeCollabCloudCapabilityDocument, decodeCollabCloudErrorEnvelope, decodeCollabCloudProjectCheckpointExportStatus, decodeCollabCloudSuccessEnvelope, matchCollabCloudRoute, } from './CollabCloudBinding.mjs';
7
+ export { DEVELOPMENT_BOOTSTRAP_ACTIVATION_PHASES, DEVELOPMENT_BOOTSTRAP_ATTEMPT_STATES, DEVELOPMENT_BOOTSTRAP_CANCELLATION_PHASES, DEVELOPMENT_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, DEVELOPMENT_BOOTSTRAP_OPERATIONS, DEVELOPMENT_BOOTSTRAP_OPERATION_CODECS, decodeDevelopmentBootstrapManifest, decodeDevelopmentBootstrapReport, developmentBootstrapOperationCodec, encodeDevelopmentBootstrapManifestCanonicalJson, } from './DevelopmentBootstrap.mjs';
8
+ export { COLLAB_CLOUD_PROJECT_SNAPSHOT_CODEC, decodeCollabCloudProjectSnapshot, } from './CollabCloudProjectSnapshot.mjs';
9
+ export { COLLAB_CLOUD_EVENT_KINDS, decodeCollabCloudProjectEventMessage, } from './CollabCloudProjectEvent.mjs';
10
+ export { COLLAB_CONTROL_OPERATION_CODECS, collabControlOperationCodec, } from './CollabControlOperationCodecs.mjs';
11
+ export { COLLAB_ERROR_CODES, CollabError, collabErrorGroup, sanitizeCollabDiagnosticContext, } from './CollabError.mjs';
12
+ export { parseCollabMemberMentions } from './CollabMemberMentionParser.mjs';
13
+ export { decodeCollabProtocolEnvelope } from './CollabProtocol.mjs';
14
+ export { parseCollabTicketReferences, scanCollabTicketReferences, } from './CollabTicketReferenceParser.mjs';
15
+ export { isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
16
+ export { collabMemberRef } from './types.mjs';
@@ -0,0 +1,8 @@
1
+ import { COLLAB_MEMBER_REF_PREFIX, } from './CollabConstants.mjs';
2
+ import { isCollabMemberId } from './CollabValidation.mjs';
3
+ export function collabMemberRef(memberId) {
4
+ if (!isCollabMemberId(memberId)) {
5
+ throw new RangeError('Invalid Collab member ID');
6
+ }
7
+ return `${COLLAB_MEMBER_REF_PREFIX}${memberId}`;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudian-collab/protocol",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,10 +21,13 @@
21
21
  "packageManager": "npm@11.13.0",
22
22
  "type": "commonjs",
23
23
  "main": "./dist/index.js",
24
+ "module": "./dist/esm/index.mjs",
24
25
  "types": "./dist/index.d.ts",
25
26
  "exports": {
26
27
  ".": {
27
28
  "types": "./dist/index.d.ts",
29
+ "import": "./dist/esm/index.mjs",
30
+ "require": "./dist/index.js",
28
31
  "default": "./dist/index.js"
29
32
  }
30
33
  },
@@ -34,7 +37,7 @@
34
37
  ],
35
38
  "sideEffects": false,
36
39
  "scripts": {
37
- "build": "npm run clean && tsc -p tsconfig.json",
40
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig.esm.json && node scripts/finalize-esm.mjs",
38
41
  "check:compatibility": "node scripts/check-compatibility.mjs",
39
42
  "check:migration-provenance": "node scripts/check-migration-provenance.mjs --verify-standalone",
40
43
  "clean": "node scripts/clean.mjs",