@claudian-collab/protocol 4.1.1 → 4.1.3

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.1`. 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.3`. 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
 
@@ -1608,18 +1608,33 @@ function validateContinuity(records) {
1608
1608
  }
1609
1609
  for (const key of keys.values()) {
1610
1610
  const recovery = recoveries.get(key.value.transferId);
1611
+ const lifecycle = lifecycles.get(key.value.transferId);
1611
1612
  const evidence = recovery?.value.sourceEvidence ?? recovery?.value.targetEvidence;
1612
1613
  const isEvidenceKey = evidence !== null
1613
1614
  && evidence !== undefined
1614
1615
  && evidence.receiptKeyId === key.value.receiptKeyId
1615
1616
  && 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;
1617
+ const effectivePhase = lifecycle?.value.state === 'recovery-required'
1618
+ ? lifecycle.value.recoveryFromPhase
1619
+ : lifecycle?.value.phase;
1620
+ const cancellationIndex = effectivePhase === null || effectivePhase === undefined
1621
+ ? undefined
1622
+ : CANCELLATION_PHASE_INDEX.get(effectivePhase);
1623
+ const normalPhaseIndex = lifecycle?.value.direction === 'cloud-to-lan'
1624
+ ? CollabAuthorityTransfer_1.COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.indexOf(effectivePhase)
1625
+ : -1;
1626
+ const isCloudSourceVerifier = recovery?.value.sourceAuthority.kind === 'cloud'
1627
+ && lifecycle?.value.direction === 'cloud-to-lan'
1628
+ && keysByTransfer.get(key.value.transferId)?.length === 2
1629
+ && (recovery.value.relinquishmentProof !== null
1630
+ || (cancellationIndex === undefined && normalPhaseIndex >= 2)
1631
+ || (cancellationIndex !== undefined
1632
+ && cancellationIndex <= 1
1633
+ && lifecycle.value.checkpointSha256 !== null));
1619
1634
  if (recovery === undefined
1620
1635
  || evidence === null
1621
1636
  || evidence === undefined
1622
- || (!isEvidenceKey && !isCloudSourceProofVerifier))
1637
+ || (!isEvidenceKey && !isCloudSourceVerifier))
1623
1638
  throw invalidPayload('records');
1624
1639
  }
1625
1640
  for (const claim of claims.values()) {
@@ -1924,13 +1939,16 @@ function validateContinuity(records) {
1924
1939
  if (!members.has(item.value.memberId)
1925
1940
  || lifecycle?.value.operationKind !== 'authority-transfer'
1926
1941
  || lifecycle.value.direction !== 'cloud-to-lan'
1927
- || lifecycle.value.state !== 'completed'
1942
+ || (lifecycle.value.state !== 'completed' && lifecycle.value.state !== 'cancelled')
1928
1943
  || terminal === undefined
1929
1944
  || recovery === undefined
1930
1945
  || item.value.memberId !== recovery.value.targetHostMemberId
1931
- || item.value.requestSha256 !== recovery.value.targetActivationRequestSha256
1932
1946
  || !terminal.value.eligibleMemberIds.includes(item.value.memberId)
1933
1947
  || response === undefined
1948
+ || response.phase !== lifecycle.value.phase
1949
+ || response.state !== lifecycle.value.state
1950
+ || (response.state === 'completed'
1951
+ && item.value.requestSha256 !== recovery.value.targetActivationRequestSha256)
1934
1952
  || response.batchRevision !== lifecycle.value.batchRevision
1935
1953
  || response.batchSha256 !== lifecycle.value.batchSha256
1936
1954
  || 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'));
@@ -1598,18 +1598,33 @@ function validateContinuity(records) {
1598
1598
  }
1599
1599
  for (const key of keys.values()) {
1600
1600
  const recovery = recoveries.get(key.value.transferId);
1601
+ const lifecycle = lifecycles.get(key.value.transferId);
1601
1602
  const evidence = recovery?.value.sourceEvidence ?? recovery?.value.targetEvidence;
1602
1603
  const isEvidenceKey = evidence !== null
1603
1604
  && evidence !== undefined
1604
1605
  && evidence.receiptKeyId === key.value.receiptKeyId
1605
1606
  && 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;
1607
+ const effectivePhase = lifecycle?.value.state === 'recovery-required'
1608
+ ? lifecycle.value.recoveryFromPhase
1609
+ : lifecycle?.value.phase;
1610
+ const cancellationIndex = effectivePhase === null || effectivePhase === undefined
1611
+ ? undefined
1612
+ : CANCELLATION_PHASE_INDEX.get(effectivePhase);
1613
+ const normalPhaseIndex = lifecycle?.value.direction === 'cloud-to-lan'
1614
+ ? COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.indexOf(effectivePhase)
1615
+ : -1;
1616
+ const isCloudSourceVerifier = recovery?.value.sourceAuthority.kind === 'cloud'
1617
+ && lifecycle?.value.direction === 'cloud-to-lan'
1618
+ && keysByTransfer.get(key.value.transferId)?.length === 2
1619
+ && (recovery.value.relinquishmentProof !== null
1620
+ || (cancellationIndex === undefined && normalPhaseIndex >= 2)
1621
+ || (cancellationIndex !== undefined
1622
+ && cancellationIndex <= 1
1623
+ && lifecycle.value.checkpointSha256 !== null));
1609
1624
  if (recovery === undefined
1610
1625
  || evidence === null
1611
1626
  || evidence === undefined
1612
- || (!isEvidenceKey && !isCloudSourceProofVerifier))
1627
+ || (!isEvidenceKey && !isCloudSourceVerifier))
1613
1628
  throw invalidPayload('records');
1614
1629
  }
1615
1630
  for (const claim of claims.values()) {
@@ -1914,13 +1929,16 @@ function validateContinuity(records) {
1914
1929
  if (!members.has(item.value.memberId)
1915
1930
  || lifecycle?.value.operationKind !== 'authority-transfer'
1916
1931
  || lifecycle.value.direction !== 'cloud-to-lan'
1917
- || lifecycle.value.state !== 'completed'
1932
+ || (lifecycle.value.state !== 'completed' && lifecycle.value.state !== 'cancelled')
1918
1933
  || terminal === undefined
1919
1934
  || recovery === undefined
1920
1935
  || item.value.memberId !== recovery.value.targetHostMemberId
1921
- || item.value.requestSha256 !== recovery.value.targetActivationRequestSha256
1922
1936
  || !terminal.value.eligibleMemberIds.includes(item.value.memberId)
1923
1937
  || response === undefined
1938
+ || response.phase !== lifecycle.value.phase
1939
+ || response.state !== lifecycle.value.state
1940
+ || (response.state === 'completed'
1941
+ && item.value.requestSha256 !== recovery.value.targetActivationRequestSha256)
1924
1942
  || response.batchRevision !== lifecycle.value.batchRevision
1925
1943
  || response.batchSha256 !== lifecycle.value.batchSha256
1926
1944
  || 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.1",
3
+ "version": "4.1.3",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {