@claudian-collab/protocol 4.1.0 → 4.1.2

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 CHANGED
@@ -64,7 +64,7 @@ part of the public surface.
64
64
 
65
65
  Package SemVer, canonical wire version, and Cloud binding version are independent concepts.
66
66
 
67
- - **Package version** (this `package.json`): `4.1.0`. Behavior-preserving implementation refactors and reviewed compatible defect corrections may use a patch release when public declarations, runtime exports, and accepted wire/binding semantics remain unchanged. Minor releases may add backward-compatible API. Removing or incompatibly changing an existing declaration, export, runtime behavior, codec, error, limit, ref rule, operation, or compatibility rule requires a major release. Package API SemVer is classified independently from the wire and Cloud binding contracts, but a compatible package addition cannot exempt a changed wire contract from a wire-version increase or a changed Cloud binding contract from a binding-version increase. The package version never signals wire compatibility by itself.
67
+ - **Package version** (this `package.json`): `4.1.2`. Behavior-preserving implementation refactors and reviewed compatible defect corrections may use a patch release when public declarations, runtime exports, and accepted wire/binding semantics remain unchanged. Minor releases may add backward-compatible API. Removing or incompatibly changing an existing declaration, export, runtime behavior, codec, error, limit, ref rule, operation, or compatibility rule requires a major release. Package API SemVer is classified independently from the wire and Cloud binding contracts, but a compatible package addition cannot exempt a changed wire contract from a wire-version increase or a changed Cloud binding contract from a binding-version increase. The package version never signals wire compatibility by itself.
68
68
  - **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `8`. The supported range is exactly `[8, 8]`. This is independent from Cloud binding version `4` and the application-owned LAN bindings. Any change to an existing envelope, DTO, operation payload shape, or operation definition requires a new wire-protocol version. This includes additive operations: every wire-contract change increases the canonical wire version.
69
69
  - **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `4`. It versions Cloud routes, bounded transfer streams, and capabilities independently from package and wire versions. Every Cloud binding-contract change increases this version. Bindings before v4 and wire versions before v8 are unsupported rather than translated or dual-interpreted.
70
70
 
@@ -1358,9 +1358,15 @@ function validateContinuity(records) {
1358
1358
  const recoveries = new Map(records
1359
1359
  .filter((item) => (item.kind === 'authority-transfer-recovery'))
1360
1360
  .map(item => [item.value.transferId, item]));
1361
- const keys = new Map(records
1362
- .filter((item) => (item.kind === 'transfer-receipt-key'))
1361
+ const keyRecords = records.filter((item) => (item.kind === 'transfer-receipt-key'));
1362
+ const keys = new Map(keyRecords
1363
1363
  .map(item => [`${item.value.transferId}:${item.value.receiptKeyId}`, item]));
1364
+ const keysByTransfer = new Map();
1365
+ for (const key of keyRecords) {
1366
+ const existing = keysByTransfer.get(key.value.transferId) ?? [];
1367
+ existing.push(key);
1368
+ keysByTransfer.set(key.value.transferId, existing);
1369
+ }
1364
1370
  const claimRecords = records
1365
1371
  .filter((item) => (item.kind === 'transferred-membership-claim'));
1366
1372
  const claims = new Map(claimRecords
@@ -1603,11 +1609,17 @@ function validateContinuity(records) {
1603
1609
  for (const key of keys.values()) {
1604
1610
  const recovery = recoveries.get(key.value.transferId);
1605
1611
  const evidence = recovery?.value.sourceEvidence ?? recovery?.value.targetEvidence;
1612
+ const isEvidenceKey = evidence !== null
1613
+ && evidence !== undefined
1614
+ && evidence.receiptKeyId === key.value.receiptKeyId
1615
+ && evidence.receiptPublicKey === key.value.receiptPublicKey;
1616
+ const isCloudSourceProofVerifier = recovery?.value.sourceAuthority.kind === 'cloud'
1617
+ && recovery.value.relinquishmentProof !== null
1618
+ && keysByTransfer.get(key.value.transferId)?.length === 2;
1606
1619
  if (recovery === undefined
1607
1620
  || evidence === null
1608
1621
  || evidence === undefined
1609
- || evidence.receiptKeyId !== key.value.receiptKeyId
1610
- || evidence.receiptPublicKey !== key.value.receiptPublicKey)
1622
+ || (!isEvidenceKey && !isCloudSourceProofVerifier))
1611
1623
  throw invalidPayload('records');
1612
1624
  }
1613
1625
  for (const claim of claims.values()) {
@@ -1912,13 +1924,16 @@ function validateContinuity(records) {
1912
1924
  if (!members.has(item.value.memberId)
1913
1925
  || lifecycle?.value.operationKind !== 'authority-transfer'
1914
1926
  || lifecycle.value.direction !== 'cloud-to-lan'
1915
- || lifecycle.value.state !== 'completed'
1927
+ || (lifecycle.value.state !== 'completed' && lifecycle.value.state !== 'cancelled')
1916
1928
  || terminal === undefined
1917
1929
  || recovery === undefined
1918
1930
  || item.value.memberId !== recovery.value.targetHostMemberId
1919
- || item.value.requestSha256 !== recovery.value.targetActivationRequestSha256
1920
1931
  || !terminal.value.eligibleMemberIds.includes(item.value.memberId)
1921
1932
  || response === undefined
1933
+ || response.phase !== lifecycle.value.phase
1934
+ || response.state !== lifecycle.value.state
1935
+ || (response.state === 'completed'
1936
+ && item.value.requestSha256 !== recovery.value.targetActivationRequestSha256)
1922
1937
  || response.batchRevision !== lifecycle.value.batchRevision
1923
1938
  || response.batchSha256 !== lifecycle.value.batchSha256
1924
1939
  || response.checkpointSha256 !== lifecycle.value.checkpointSha256
@@ -906,8 +906,8 @@ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
906
906
  }
907
907
  else if (operation === 'getProjectAuthorityTransfer') {
908
908
  if (response.decoded.direction !== 'cloud-to-lan'
909
- || response.decoded.phase !== 'completed'
910
- || response.decoded.state !== 'completed'
909
+ || !((response.decoded.phase === 'completed' && response.decoded.state === 'completed')
910
+ || (response.decoded.phase === 'cancelled' && response.decoded.state === 'cancelled'))
911
911
  || response.decoded.expiresAt !== expiresAt)
912
912
  throw invalidPayload('responseJson');
913
913
  }
@@ -1218,9 +1218,17 @@ function validateCheckpointRecordSequence(records, profile, acknowledgementBindi
1218
1218
  .map(item => [item.value.memberId, item.value.principalId]));
1219
1219
  const terminalResponders = records.filter((item) => (item.kind === 'terminal-responder'));
1220
1220
  const tombstones = records.filter((item) => (item.kind === 'tombstone'));
1221
+ const terminalResponder = terminalResponders[0];
1222
+ const transferTerminalResponse = terminalResponder?.value.operation
1223
+ === 'getProjectAuthorityTransfer'
1224
+ ? canonicalOperationResponseJson({ responseJson: terminalResponder.value.responseJson }, 'responseJson', 'getProjectAuthorityTransfer').decoded
1225
+ : undefined;
1226
+ const terminalRequiresTombstone = terminalResponder !== undefined && (terminalResponder.value.operation === 'retireProject'
1227
+ || transferTerminalResponse?.state === 'completed');
1221
1228
  if (terminalResponders.length > 1
1222
- || (terminalResponders.length === 1 && (tombstones.length !== 1
1223
- || terminalResponders[0].value.expiresAt !== tombstones[0].value.terminalExpiresAt)))
1229
+ || (terminalRequiresTombstone && (tombstones.length !== 1
1230
+ || terminalResponder.value.expiresAt !== tombstones[0].value.terminalExpiresAt))
1231
+ || (terminalResponder !== undefined && !terminalRequiresTombstone && tombstones.length !== 0))
1224
1232
  throw invalidPayload('records');
1225
1233
  const tombstone = tombstones[0];
1226
1234
  const retirementTerminal = terminalResponders.find(item => (item.value.operation === 'retireProject'));
@@ -1348,9 +1348,15 @@ function validateContinuity(records) {
1348
1348
  const recoveries = new Map(records
1349
1349
  .filter((item) => (item.kind === 'authority-transfer-recovery'))
1350
1350
  .map(item => [item.value.transferId, item]));
1351
- const keys = new Map(records
1352
- .filter((item) => (item.kind === 'transfer-receipt-key'))
1351
+ const keyRecords = records.filter((item) => (item.kind === 'transfer-receipt-key'));
1352
+ const keys = new Map(keyRecords
1353
1353
  .map(item => [`${item.value.transferId}:${item.value.receiptKeyId}`, item]));
1354
+ const keysByTransfer = new Map();
1355
+ for (const key of keyRecords) {
1356
+ const existing = keysByTransfer.get(key.value.transferId) ?? [];
1357
+ existing.push(key);
1358
+ keysByTransfer.set(key.value.transferId, existing);
1359
+ }
1354
1360
  const claimRecords = records
1355
1361
  .filter((item) => (item.kind === 'transferred-membership-claim'));
1356
1362
  const claims = new Map(claimRecords
@@ -1593,11 +1599,17 @@ function validateContinuity(records) {
1593
1599
  for (const key of keys.values()) {
1594
1600
  const recovery = recoveries.get(key.value.transferId);
1595
1601
  const evidence = recovery?.value.sourceEvidence ?? recovery?.value.targetEvidence;
1602
+ const isEvidenceKey = evidence !== null
1603
+ && evidence !== undefined
1604
+ && evidence.receiptKeyId === key.value.receiptKeyId
1605
+ && evidence.receiptPublicKey === key.value.receiptPublicKey;
1606
+ const isCloudSourceProofVerifier = recovery?.value.sourceAuthority.kind === 'cloud'
1607
+ && recovery.value.relinquishmentProof !== null
1608
+ && keysByTransfer.get(key.value.transferId)?.length === 2;
1596
1609
  if (recovery === undefined
1597
1610
  || evidence === null
1598
1611
  || evidence === undefined
1599
- || evidence.receiptKeyId !== key.value.receiptKeyId
1600
- || evidence.receiptPublicKey !== key.value.receiptPublicKey)
1612
+ || (!isEvidenceKey && !isCloudSourceProofVerifier))
1601
1613
  throw invalidPayload('records');
1602
1614
  }
1603
1615
  for (const claim of claims.values()) {
@@ -1902,13 +1914,16 @@ function validateContinuity(records) {
1902
1914
  if (!members.has(item.value.memberId)
1903
1915
  || lifecycle?.value.operationKind !== 'authority-transfer'
1904
1916
  || lifecycle.value.direction !== 'cloud-to-lan'
1905
- || lifecycle.value.state !== 'completed'
1917
+ || (lifecycle.value.state !== 'completed' && lifecycle.value.state !== 'cancelled')
1906
1918
  || terminal === undefined
1907
1919
  || recovery === undefined
1908
1920
  || item.value.memberId !== recovery.value.targetHostMemberId
1909
- || item.value.requestSha256 !== recovery.value.targetActivationRequestSha256
1910
1921
  || !terminal.value.eligibleMemberIds.includes(item.value.memberId)
1911
1922
  || response === undefined
1923
+ || response.phase !== lifecycle.value.phase
1924
+ || response.state !== lifecycle.value.state
1925
+ || (response.state === 'completed'
1926
+ && item.value.requestSha256 !== recovery.value.targetActivationRequestSha256)
1912
1927
  || response.batchRevision !== lifecycle.value.batchRevision
1913
1928
  || response.batchSha256 !== lifecycle.value.batchSha256
1914
1929
  || response.checkpointSha256 !== lifecycle.value.checkpointSha256
@@ -891,8 +891,8 @@ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
891
891
  }
892
892
  else if (operation === 'getProjectAuthorityTransfer') {
893
893
  if (response.decoded.direction !== 'cloud-to-lan'
894
- || response.decoded.phase !== 'completed'
895
- || response.decoded.state !== 'completed'
894
+ || !((response.decoded.phase === 'completed' && response.decoded.state === 'completed')
895
+ || (response.decoded.phase === 'cancelled' && response.decoded.state === 'cancelled'))
896
896
  || response.decoded.expiresAt !== expiresAt)
897
897
  throw invalidPayload('responseJson');
898
898
  }
@@ -1203,9 +1203,17 @@ export function validateCheckpointRecordSequence(records, profile, acknowledgeme
1203
1203
  .map(item => [item.value.memberId, item.value.principalId]));
1204
1204
  const terminalResponders = records.filter((item) => (item.kind === 'terminal-responder'));
1205
1205
  const tombstones = records.filter((item) => (item.kind === 'tombstone'));
1206
+ const terminalResponder = terminalResponders[0];
1207
+ const transferTerminalResponse = terminalResponder?.value.operation
1208
+ === 'getProjectAuthorityTransfer'
1209
+ ? canonicalOperationResponseJson({ responseJson: terminalResponder.value.responseJson }, 'responseJson', 'getProjectAuthorityTransfer').decoded
1210
+ : undefined;
1211
+ const terminalRequiresTombstone = terminalResponder !== undefined && (terminalResponder.value.operation === 'retireProject'
1212
+ || transferTerminalResponse?.state === 'completed');
1206
1213
  if (terminalResponders.length > 1
1207
- || (terminalResponders.length === 1 && (tombstones.length !== 1
1208
- || terminalResponders[0].value.expiresAt !== tombstones[0].value.terminalExpiresAt)))
1214
+ || (terminalRequiresTombstone && (tombstones.length !== 1
1215
+ || terminalResponder.value.expiresAt !== tombstones[0].value.terminalExpiresAt))
1216
+ || (terminalResponder !== undefined && !terminalRequiresTombstone && tombstones.length !== 0))
1209
1217
  throw invalidPayload('records');
1210
1218
  const tombstone = tombstones[0];
1211
1219
  const retirementTerminal = terminalResponders.find(item => (item.value.operation === 'retireProject'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudian-collab/protocol",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {