@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.
- package/README.md +12 -28
- package/dist/CollabCloudBinding.d.ts +1 -1
- package/dist/CollabCloudBinding.js +11 -11
- package/dist/CollabCloudProjectSnapshot.d.ts +1 -0
- package/dist/CollabCloudProjectSnapshot.js +2 -0
- package/dist/CollabConstants.d.ts +1 -1
- package/dist/CollabConstants.js +1 -1
- package/dist/CollabProjectBackupCheckpoint.js +42 -106
- package/dist/CollabProjectCheckpoint.d.ts +25 -0
- package/dist/CollabProjectCheckpoint.js +55 -25
- package/dist/CollabProjectMembership.d.ts +3 -5
- package/dist/CollabProjectMembership.js +27 -9
- package/dist/esm/CollabCloudBinding.mjs +11 -11
- package/dist/esm/CollabCloudProjectSnapshot.mjs +2 -0
- package/dist/esm/CollabConstants.mjs +1 -1
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +43 -107
- package/dist/esm/CollabProjectCheckpoint.mjs +50 -25
- package/dist/esm/CollabProjectMembership.mjs +27 -9
- package/package.json +2 -2
|
@@ -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,
|
|
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 !==
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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':
|
|
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':
|
|
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
|
-
|
|
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 => (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
+
"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
|
|
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": {
|