@claudian-collab/protocol 3.3.1 → 4.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.
@@ -227,7 +227,7 @@ function gitRefs(value, expectedMainOid) {
227
227
  throw invalidPayload('refs');
228
228
  return Object.freeze(decoded);
229
229
  }
230
- function manifestObject(source, includeManifestSha256) {
230
+ function manifestObject(source, coordinationFormatVersion) {
231
231
  const profile = literal(source, 'profile', COLLAB_CHECKPOINT_PROFILES);
232
232
  const expectedMainOid = token(source, 'expectedMainOid', isCollabGitOid);
233
233
  const gitObjectFormat = literal(source, 'gitObjectFormat', ['sha1', 'sha256']);
@@ -263,15 +263,13 @@ function manifestObject(source, includeManifestSha256) {
263
263
  sourceAuthority,
264
264
  targetAuthority,
265
265
  };
266
- if (common.coordinationFormatVersion !== COLLAB_PROJECT_COORDINATION_FORMAT_VERSION
266
+ if (common.coordinationFormatVersion !== coordinationFormatVersion
267
267
  || common.manifestSchemaVersion !== COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION
268
268
  || common.protocolVersion !== COLLAB_PROTOCOL_VERSION)
269
269
  throw invalidPayload('manifest');
270
- if (!includeManifestSha256)
271
- return common;
272
270
  return {
273
271
  artifacts: common.artifacts,
274
- coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
272
+ coordinationFormatVersion,
275
273
  createdAt: common.createdAt,
276
274
  expectedMainOid: common.expectedMainOid,
277
275
  gitObjectFormat: common.gitObjectFormat,
@@ -286,7 +284,8 @@ function manifestObject(source, includeManifestSha256) {
286
284
  targetAuthority: common.targetAuthority,
287
285
  };
288
286
  }
289
- export function decodeCollabProjectCheckpointManifest(value) {
287
+ /** @internal Shared field validation; public entry points select their own format. */
288
+ export function decodeCheckpointManifestFields(value, coordinationFormatVersion) {
290
289
  const source = exactRecord(value, 'manifest', [
291
290
  'artifacts',
292
291
  'coordinationFormatVersion',
@@ -303,18 +302,24 @@ export function decodeCollabProjectCheckpointManifest(value) {
303
302
  'sourceAuthority',
304
303
  'targetAuthority',
305
304
  ]);
306
- const decoded = manifestObject(source, true);
305
+ const decoded = manifestObject(source, coordinationFormatVersion);
307
306
  const encoded = JSON.stringify(decoded);
308
307
  if (!hasUtf8ByteLengthAtMost(encoded, COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxManifestBytes)) {
309
308
  throw invalidPayload('manifest');
310
309
  }
311
310
  return decoded;
312
311
  }
312
+ export function decodeCollabProjectCheckpointManifest(value) {
313
+ return decodeCheckpointManifestFields(value, COLLAB_PROJECT_COORDINATION_FORMAT_VERSION);
314
+ }
313
315
  export function encodeCollabProjectCheckpointManifestCanonicalJson(manifest) {
314
316
  return JSON.stringify(decodeCollabProjectCheckpointManifest(manifest));
315
317
  }
316
318
  export function encodeCollabProjectCheckpointManifestDigestInput(manifest) {
317
- const decoded = decodeCollabProjectCheckpointManifest(manifest);
319
+ return encodeCheckpointManifestDigestInput(decodeCollabProjectCheckpointManifest(manifest));
320
+ }
321
+ /** @internal Encode already validated fields without changing their format. */
322
+ export function encodeCheckpointManifestDigestInput(decoded) {
318
323
  const source = {
319
324
  artifacts: decoded.artifacts,
320
325
  coordinationFormatVersion: decoded.coordinationFormatVersion,
@@ -697,13 +702,13 @@ function idempotencyResultRecord(source, recordId, revision) {
697
702
  },
698
703
  };
699
704
  }
700
- function principalBindingRecord(source, recordId, revision) {
701
- const value = exactRecord(source.value, 'value', [
705
+ function principalBindingRecord(source, recordId, revision, deferPrincipals) {
706
+ const value = principalRecord(source.value, 'value', [
702
707
  'boundAt',
703
708
  'memberId',
704
709
  'principalId',
705
710
  'projectId',
706
- ]);
711
+ ], deferPrincipals);
707
712
  const memberId = token(value, 'memberId', isCollabMemberId);
708
713
  if (recordId !== memberId)
709
714
  throw invalidPayload('recordId');
@@ -714,7 +719,7 @@ function principalBindingRecord(source, recordId, revision) {
714
719
  value: {
715
720
  boundAt: timestamp(value, 'boundAt'),
716
721
  memberId,
717
- principalId: token(value, 'principalId'),
722
+ principalId: deferPrincipals ? value.principalId : token(value, 'principalId'),
718
723
  projectId: token(value, 'projectId', isCollabProjectId),
719
724
  },
720
725
  };
@@ -848,7 +853,13 @@ function lifecycleStateRecord(source, recordId, revision) {
848
853
  value: internalState,
849
854
  };
850
855
  }
851
- function terminalResponderRecord(source, recordId, revision) {
856
+ function principalRecord(value, field, keys, deferPrincipals) {
857
+ const source = record(value, field);
858
+ return exactRecord(source, field, deferPrincipals && !Object.hasOwn(source, 'principalId')
859
+ ? keys.filter(key => key !== 'principalId')
860
+ : keys);
861
+ }
862
+ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
852
863
  const value = exactRecord(source.value, 'value', [
853
864
  'acknowledgements',
854
865
  'eligibleMemberIds',
@@ -900,22 +911,24 @@ function terminalResponderRecord(source, recordId, revision) {
900
911
  if (!Array.isArray(value.acknowledgements))
901
912
  throw invalidPayload('acknowledgements');
902
913
  const acknowledgements = value.acknowledgements.map((item) => {
903
- const acknowledgement = exactRecord(item, 'acknowledgement', [
914
+ const acknowledgement = principalRecord(item, 'acknowledgement', [
904
915
  'acknowledgedAt',
905
916
  'memberId',
906
917
  'principalId',
907
- ]);
918
+ ], deferPrincipals);
908
919
  return {
909
920
  acknowledgedAt: timestamp(acknowledgement, 'acknowledgedAt'),
910
921
  memberId: token(acknowledgement, 'memberId', isCollabMemberId),
911
- principalId: token(acknowledgement, 'principalId'),
922
+ principalId: deferPrincipals
923
+ ? acknowledgement.principalId
924
+ : token(acknowledgement, 'principalId'),
912
925
  };
913
926
  });
914
927
  const acknowledgementPrincipals = new Set();
915
928
  acknowledgements.forEach((item, index) => {
916
929
  if (!eligibleMemberIds.includes(item.memberId)
917
930
  || (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
918
- || acknowledgementPrincipals.has(item.principalId))
931
+ || (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
919
932
  throw invalidPayload('acknowledgements');
920
933
  acknowledgementPrincipals.add(item.principalId);
921
934
  });
@@ -924,7 +937,7 @@ function terminalResponderRecord(source, recordId, revision) {
924
937
  recordId,
925
938
  revision,
926
939
  value: {
927
- acknowledgements: Object.freeze(acknowledgements),
940
+ acknowledgements,
928
941
  eligibleMemberIds: Object.freeze(eligibleMemberIds),
929
942
  expiresAt,
930
943
  operation,
@@ -1097,8 +1110,9 @@ function authorityVolumePairRecord(source, recordId, revision) {
1097
1110
  },
1098
1111
  };
1099
1112
  }
1100
- function decodeCheckpointRecord(value) {
1113
+ export function decodeCheckpointRecord(value, principalPhase) {
1101
1114
  const { kind, recordId, revision, source } = recordEnvelope(value);
1115
+ const deferPrincipals = principalPhase === 'deferred';
1102
1116
  switch (kind) {
1103
1117
  case 'project': return projectRecord(source, recordId, revision);
1104
1118
  case 'member': return memberRecord(source, recordId, revision);
@@ -1111,10 +1125,12 @@ function decodeCheckpointRecord(value) {
1111
1125
  case 'cloud-event': return cloudEventRecord(source, recordId, revision);
1112
1126
  case 'cloud-event-cursor': return cloudEventCursorRecord(source, recordId, revision);
1113
1127
  case 'idempotency-result': return idempotencyResultRecord(source, recordId, revision);
1114
- case 'principal-binding': return principalBindingRecord(source, recordId, revision);
1128
+ case 'principal-binding':
1129
+ return principalBindingRecord(source, recordId, revision, deferPrincipals);
1115
1130
  case 'repository-placement': return repositoryPlacementRecord(source, recordId, revision);
1116
1131
  case 'lifecycle-state': return lifecycleStateRecord(source, recordId, revision);
1117
- case 'terminal-responder': return terminalResponderRecord(source, recordId, revision);
1132
+ case 'terminal-responder':
1133
+ return terminalResponderRecord(source, recordId, revision, deferPrincipals);
1118
1134
  case 'protected-claim-envelope':
1119
1135
  return protectedClaimEnvelopeRecord(source, recordId, revision);
1120
1136
  case 'tombstone': return tombstoneRecord(source, recordId, revision);
@@ -1130,7 +1146,8 @@ function compareRecords(left, right) {
1130
1146
  const kindOrder = recordKindOrder(left.kind) - recordKindOrder(right.kind);
1131
1147
  return kindOrder !== 0 ? kindOrder : left.recordId.localeCompare(right.recordId, 'en-US');
1132
1148
  }
1133
- function validateRecordSequence(records, profile) {
1149
+ /** @internal V3 decoding checks terminal principals separately; consistency binds current principals. */
1150
+ export function validateCheckpointRecordSequence(records, profile, acknowledgementBinding = 'current-principal') {
1134
1151
  if (records.length === 0 || records[0].kind !== 'project')
1135
1152
  throw invalidPayload('records');
1136
1153
  const projectId = records[0].recordId;
@@ -1233,7 +1250,9 @@ function validateRecordSequence(records, profile) {
1233
1250
  throw invalidPayload('records');
1234
1251
  }
1235
1252
  if (item.kind === 'terminal-responder' && (item.value.eligibleMemberIds.some(memberId => !members.has(memberId))
1236
- || item.value.acknowledgements.some(acknowledgement => (principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId))))
1253
+ || item.value.acknowledgements.some(acknowledgement => (acknowledgementBinding === 'current-principal'
1254
+ ? principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId
1255
+ : !principalBindings.has(acknowledgement.memberId)))))
1237
1256
  throw invalidPayload('records');
1238
1257
  }
1239
1258
  if (profile === 'backup') {
@@ -1260,11 +1279,13 @@ export function decodeCollabProjectCheckpointCoordinationNdjson(value, profile)
1260
1279
  throw invalidPayload('coordination');
1261
1280
  }
1262
1281
  const result = decodeCheckpointRecord(parsed);
1282
+ if (result.kind === 'terminal-responder')
1283
+ Object.freeze(result.value.acknowledgements);
1263
1284
  if (JSON.stringify(result) !== line)
1264
1285
  throw invalidPayload('coordination');
1265
1286
  return result;
1266
1287
  });
1267
- validateRecordSequence(decoded, profile);
1288
+ validateCheckpointRecordSequence(decoded, profile);
1268
1289
  return Object.freeze(decoded);
1269
1290
  }
1270
1291
  export function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
@@ -1296,7 +1317,11 @@ function checkpointRecordGitOids(checkpointRecord) {
1296
1317
  }
1297
1318
  export function validateCollabProjectCheckpointConsistency(manifest, records) {
1298
1319
  const decodedManifest = decodeCollabProjectCheckpointManifest(manifest);
1299
- validateRecordSequence(records, decodedManifest.profile);
1320
+ validateCheckpointRecordSequence(records, decodedManifest.profile);
1321
+ return validateCheckpointManifestConsistency(decodedManifest, records);
1322
+ }
1323
+ /** @internal Cross-artifact checks operate on validated fields, not another format. */
1324
+ export function validateCheckpointManifestConsistency(decodedManifest, records) {
1300
1325
  const project = records[0];
1301
1326
  if (project.kind !== 'project'
1302
1327
  || project.value.projectId !== decodedManifest.projectId
@@ -333,23 +333,37 @@ function decodeMemberSummary(value) {
333
333
  const source = exactRecord(value, 'member', [
334
334
  'bindingState',
335
335
  'displayName',
336
+ 'importedClaimGeneration',
336
337
  'importedClaimState',
337
338
  'memberId',
338
339
  'membershipRevision',
339
340
  'role',
340
341
  ]);
342
+ const importedClaimState = literal(source, 'importedClaimState', [
343
+ 'not-applicable',
344
+ 'original-active',
345
+ 'override-active',
346
+ 'revoked',
347
+ 'expired',
348
+ 'redeemed',
349
+ 'hidden',
350
+ ]);
351
+ const importedClaimGeneration = source.importedClaimGeneration === null
352
+ ? null
353
+ : nonNegativeInteger(source, 'importedClaimGeneration');
354
+ if (importedClaimState === 'hidden' || importedClaimState === 'not-applicable') {
355
+ if (importedClaimGeneration !== null)
356
+ throw invalidPayload('importedClaimGeneration');
357
+ }
358
+ else if (importedClaimGeneration === null
359
+ || (importedClaimState === 'original-active' && importedClaimGeneration !== 0)
360
+ || (importedClaimState === 'override-active' && importedClaimGeneration === 0))
361
+ throw invalidPayload('importedClaimGeneration');
341
362
  return {
342
363
  bindingState: literal(source, 'bindingState', ['bound', 'unbound', 'hidden']),
343
364
  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
- ]),
365
+ importedClaimGeneration,
366
+ importedClaimState,
353
367
  memberId: token(source, 'memberId', isCollabMemberId),
354
368
  membershipRevision: positiveInteger(source, 'membershipRevision'),
355
369
  role: literal(source, 'role', ['manager', 'member']),
@@ -396,6 +410,8 @@ function decodeReissueClaimResponse(value) {
396
410
  'memberId',
397
411
  'projectId',
398
412
  'secretReplayExpiresAt',
413
+ 'targetAuthorityGeneration',
414
+ 'transferId',
399
415
  ]);
400
416
  const createdAt = timestamp(source, 'createdAt');
401
417
  const expiresAt = timestamp(source, 'expiresAt');
@@ -411,6 +427,8 @@ function decodeReissueClaimResponse(value) {
411
427
  memberId: token(source, 'memberId', isCollabMemberId),
412
428
  projectId: token(source, 'projectId', isCollabProjectId),
413
429
  secretReplayExpiresAt,
430
+ targetAuthorityGeneration: positiveInteger(source, 'targetAuthorityGeneration'),
431
+ transferId: token(source, 'transferId'),
414
432
  };
415
433
  }
416
434
  function decodeRevokeClaimResponse(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudian-collab/protocol",
3
- "version": "3.3.1",
3
+ "version": "4.0.0",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,7 +46,7 @@
46
46
  "release:candidate": "node scripts/release-candidate.mjs --output .context/release",
47
47
  "test": "jest && node --test tests/*.test.mjs",
48
48
  "typecheck": "tsc --noEmit -p tsconfig.jest.json",
49
- "verify": "npm run lint && npm run typecheck && npm run build && npm run test && npm run check:compatibility && npm run check:migration-provenance && npm run verify:pack && npm run release:candidate",
49
+ "verify": "npm run lint && npm run typecheck && npm run release:candidate && npm run test && npm run check:compatibility && npm run check:migration-provenance && npm run verify:pack",
50
50
  "verify:pack": "node scripts/verify-pack.mjs"
51
51
  },
52
52
  "dependencies": {