@claudian-collab/protocol 4.1.2 → 4.1.4

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.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.
67
+ - **Package version** (this `package.json`): `4.1.4`. 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
 
@@ -1520,7 +1520,10 @@ function validateContinuity(records) {
1520
1520
  const recoveryEvidenceInvalid = direction === null
1521
1521
  || (direction === 'lan-to-cloud' && recovery.value.sourceEvidence === null)
1522
1522
  || (targetEvidenceRequired && recovery.value.targetEvidence === null)
1523
- || (!isCancellation && (stageRequired !== (recovery.value.stageSha256 !== null)))
1523
+ || (isCancellation
1524
+ ? (lifecycle.value.batchRevision !== null)
1525
+ !== (recovery.value.stageSha256 !== null)
1526
+ : stageRequired !== (recovery.value.stageSha256 !== null))
1524
1527
  || (direction === 'lan-to-cloud'
1525
1528
  && recovery.value.stageSha256 !== null
1526
1529
  && recovery.value.stageSha256 !== lifecycle.value.checkpointSha256)
@@ -1543,33 +1546,51 @@ function validateContinuity(records) {
1543
1546
  if (projectAuthorityGeneration !== expectedProjectAuthorityGeneration) {
1544
1547
  throw invalidPayload('records');
1545
1548
  }
1549
+ const hostMemberId = direction === 'lan-to-cloud'
1550
+ ? recovery.value.sourceHostMemberId
1551
+ : recovery.value.targetHostMemberId;
1552
+ const transferMemberIds = [...memberRecords.values()]
1553
+ .filter(item => (item.value.status === 'active'
1554
+ && item.value.memberId !== hostMemberId
1555
+ && Date.parse(item.value.createdAt) <= Date.parse(recovery.value.createdAt)))
1556
+ .map(item => item.value.memberId);
1546
1557
  if (!isCancellation) {
1547
- const hostMemberId = direction === 'lan-to-cloud'
1548
- ? recovery.value.sourceHostMemberId
1549
- : recovery.value.targetHostMemberId;
1550
- const eligibleMemberIds = [...memberRecords.values()]
1551
- .filter(item => item.value.status === 'active' && item.value.memberId !== hostMemberId)
1552
- .map(item => item.value.memberId);
1553
- const memberCustodyRequired = normalPhaseIndex >= 3;
1554
1558
  const batchReceiptRequired = direction === 'cloud-to-lan'
1555
1559
  ? normalPhaseIndex >= 3
1556
1560
  : normalPhaseIndex >= 4;
1557
1561
  if ((batchReceipt !== undefined) !== batchReceiptRequired) {
1558
1562
  throw invalidPayload('records');
1559
1563
  }
1560
- for (const memberId of eligibleMemberIds) {
1561
- const identity = `${recovery.value.transferId}:${memberId}`;
1562
- const claimPresent = claims.has(identity);
1563
- const envelopePresent = protectedEnvelopes.has(identity);
1564
- const redemptionPresent = redemptionReceipts.has(identity);
1565
- const exactMemberCustody = direction === 'lan-to-cloud'
1564
+ }
1565
+ const memberCustody = transferMemberIds.map((memberId) => {
1566
+ const identity = `${recovery.value.transferId}:${memberId}`;
1567
+ const claimPresent = claims.has(identity);
1568
+ const envelopePresent = protectedEnvelopes.has(identity);
1569
+ const redemptionPresent = redemptionReceipts.has(identity);
1570
+ return {
1571
+ absent: !claimPresent && !envelopePresent && !redemptionPresent,
1572
+ exact: direction === 'lan-to-cloud'
1566
1573
  ? claimPresent && !envelopePresent
1567
- : !claimPresent && envelopePresent !== redemptionPresent;
1568
- if (memberCustodyRequired !== exactMemberCustody) {
1569
- throw invalidPayload('records');
1570
- }
1574
+ : !claimPresent && envelopePresent !== redemptionPresent,
1575
+ };
1576
+ });
1577
+ const cleanupInProgress = isCancellation
1578
+ && cancellationIndex === 1
1579
+ && lifecycle.value.batchRevision !== null;
1580
+ if (cleanupInProgress) {
1581
+ const allAbsent = memberCustody.every(item => item.absent);
1582
+ const allExact = memberCustody.every(item => item.exact);
1583
+ if (!allAbsent && !allExact) {
1584
+ throw invalidPayload('records');
1571
1585
  }
1572
1586
  }
1587
+ else {
1588
+ const memberCustodyRequired = isCancellation
1589
+ ? !cleanupCompleted && lifecycle.value.batchRevision !== null
1590
+ : normalPhaseIndex >= 3;
1591
+ if (memberCustody.some(item => (memberCustodyRequired ? !item.exact : !item.absent)))
1592
+ throw invalidPayload('records');
1593
+ }
1573
1594
  if (recovery.value.targetEvidence !== null && (recovery.value.targetHostMemberId === null
1574
1595
  || principalBindings.get(recovery.value.targetHostMemberId)
1575
1596
  !== recovery.value.targetEvidence.principalId))
@@ -1585,6 +1606,7 @@ function validateContinuity(records) {
1585
1606
  ? 'cloud-to-lan'
1586
1607
  : 'lan-to-cloud')
1587
1608
  || lifecycle.value.expectedAuthorityGeneration !== recovery.value.sourceAuthority.generation
1609
+ || lifecycle.value.createdAt !== recovery.value.createdAt
1588
1610
  || lifecycle.value.scheduledAt !== recovery.value.expiresAt
1589
1611
  || (recovery.value.sourceHostMemberId !== null
1590
1612
  && !members.has(recovery.value.sourceHostMemberId))
@@ -1608,23 +1630,39 @@ function validateContinuity(records) {
1608
1630
  }
1609
1631
  for (const key of keys.values()) {
1610
1632
  const recovery = recoveries.get(key.value.transferId);
1633
+ const lifecycle = lifecycles.get(key.value.transferId);
1611
1634
  const evidence = recovery?.value.sourceEvidence ?? recovery?.value.targetEvidence;
1612
1635
  const isEvidenceKey = evidence !== null
1613
1636
  && evidence !== undefined
1614
1637
  && evidence.receiptKeyId === key.value.receiptKeyId
1615
1638
  && 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;
1639
+ const effectivePhase = lifecycle?.value.state === 'recovery-required'
1640
+ ? lifecycle.value.recoveryFromPhase
1641
+ : lifecycle?.value.phase;
1642
+ const cancellationIndex = effectivePhase === null || effectivePhase === undefined
1643
+ ? undefined
1644
+ : CANCELLATION_PHASE_INDEX.get(effectivePhase);
1645
+ const normalPhaseIndex = lifecycle?.value.direction === 'cloud-to-lan'
1646
+ ? CollabAuthorityTransfer_1.COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.indexOf(effectivePhase)
1647
+ : -1;
1648
+ const isCloudSourceVerifier = recovery?.value.sourceAuthority.kind === 'cloud'
1649
+ && lifecycle?.value.direction === 'cloud-to-lan'
1650
+ && keysByTransfer.get(key.value.transferId)?.length === 2
1651
+ && (recovery.value.relinquishmentProof !== null
1652
+ || (cancellationIndex === undefined && normalPhaseIndex >= 2)
1653
+ || (cancellationIndex !== undefined
1654
+ && cancellationIndex <= 1
1655
+ && lifecycle.value.checkpointSha256 !== null));
1619
1656
  if (recovery === undefined
1620
1657
  || evidence === null
1621
1658
  || evidence === undefined
1622
- || (!isEvidenceKey && !isCloudSourceProofVerifier))
1659
+ || (!isEvidenceKey && !isCloudSourceVerifier))
1623
1660
  throw invalidPayload('records');
1624
1661
  }
1625
1662
  for (const claim of claims.values()) {
1626
1663
  const lifecycle = lifecycles.get(claim.value.transferId);
1627
1664
  const recovery = recoveries.get(claim.value.transferId);
1665
+ const member = memberRecords.get(claim.value.memberId);
1628
1666
  const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
1629
1667
  const redeemedOverride = redemptionReceipt === undefined
1630
1668
  ? undefined
@@ -1633,8 +1671,9 @@ function validateContinuity(records) {
1633
1671
  && item.value.state === 'redeemed'
1634
1672
  && item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
1635
1673
  && item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
1636
- if (memberRecords.get(claim.value.memberId)?.value.status !== 'active'
1674
+ if (member?.value.status !== 'active'
1637
1675
  || recovery === undefined
1676
+ || Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
1638
1677
  || recovery.value.sourceAuthority.kind !== 'lan'
1639
1678
  || claim.value.memberId === recovery.value.sourceHostMemberId
1640
1679
  || lifecycle?.value.batchRevision !== claim.value.batchRevision
@@ -1817,10 +1856,12 @@ function validateContinuity(records) {
1817
1856
  const identity = `${item.value.transferId}:${item.value.memberId}`;
1818
1857
  const recovery = recoveries.get(item.value.transferId);
1819
1858
  const lifecycle = lifecycles.get(item.value.transferId);
1859
+ const member = memberRecords.get(item.value.memberId);
1820
1860
  if (item.recordId !== identity
1821
1861
  || recovery === undefined
1822
1862
  || lifecycle === undefined
1823
- || memberRecords.get(item.value.memberId)?.value.status !== 'active'
1863
+ || member?.value.status !== 'active'
1864
+ || Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
1824
1865
  || item.value.memberId === recovery.value.targetHostMemberId
1825
1866
  || item.value.associatedData.authorityGeneration
1826
1867
  !== recovery.value.sourceAuthority.generation
@@ -1510,7 +1510,10 @@ function validateContinuity(records) {
1510
1510
  const recoveryEvidenceInvalid = direction === null
1511
1511
  || (direction === 'lan-to-cloud' && recovery.value.sourceEvidence === null)
1512
1512
  || (targetEvidenceRequired && recovery.value.targetEvidence === null)
1513
- || (!isCancellation && (stageRequired !== (recovery.value.stageSha256 !== null)))
1513
+ || (isCancellation
1514
+ ? (lifecycle.value.batchRevision !== null)
1515
+ !== (recovery.value.stageSha256 !== null)
1516
+ : stageRequired !== (recovery.value.stageSha256 !== null))
1514
1517
  || (direction === 'lan-to-cloud'
1515
1518
  && recovery.value.stageSha256 !== null
1516
1519
  && recovery.value.stageSha256 !== lifecycle.value.checkpointSha256)
@@ -1533,33 +1536,51 @@ function validateContinuity(records) {
1533
1536
  if (projectAuthorityGeneration !== expectedProjectAuthorityGeneration) {
1534
1537
  throw invalidPayload('records');
1535
1538
  }
1539
+ const hostMemberId = direction === 'lan-to-cloud'
1540
+ ? recovery.value.sourceHostMemberId
1541
+ : recovery.value.targetHostMemberId;
1542
+ const transferMemberIds = [...memberRecords.values()]
1543
+ .filter(item => (item.value.status === 'active'
1544
+ && item.value.memberId !== hostMemberId
1545
+ && Date.parse(item.value.createdAt) <= Date.parse(recovery.value.createdAt)))
1546
+ .map(item => item.value.memberId);
1536
1547
  if (!isCancellation) {
1537
- const hostMemberId = direction === 'lan-to-cloud'
1538
- ? recovery.value.sourceHostMemberId
1539
- : recovery.value.targetHostMemberId;
1540
- const eligibleMemberIds = [...memberRecords.values()]
1541
- .filter(item => item.value.status === 'active' && item.value.memberId !== hostMemberId)
1542
- .map(item => item.value.memberId);
1543
- const memberCustodyRequired = normalPhaseIndex >= 3;
1544
1548
  const batchReceiptRequired = direction === 'cloud-to-lan'
1545
1549
  ? normalPhaseIndex >= 3
1546
1550
  : normalPhaseIndex >= 4;
1547
1551
  if ((batchReceipt !== undefined) !== batchReceiptRequired) {
1548
1552
  throw invalidPayload('records');
1549
1553
  }
1550
- for (const memberId of eligibleMemberIds) {
1551
- const identity = `${recovery.value.transferId}:${memberId}`;
1552
- const claimPresent = claims.has(identity);
1553
- const envelopePresent = protectedEnvelopes.has(identity);
1554
- const redemptionPresent = redemptionReceipts.has(identity);
1555
- const exactMemberCustody = direction === 'lan-to-cloud'
1554
+ }
1555
+ const memberCustody = transferMemberIds.map((memberId) => {
1556
+ const identity = `${recovery.value.transferId}:${memberId}`;
1557
+ const claimPresent = claims.has(identity);
1558
+ const envelopePresent = protectedEnvelopes.has(identity);
1559
+ const redemptionPresent = redemptionReceipts.has(identity);
1560
+ return {
1561
+ absent: !claimPresent && !envelopePresent && !redemptionPresent,
1562
+ exact: direction === 'lan-to-cloud'
1556
1563
  ? claimPresent && !envelopePresent
1557
- : !claimPresent && envelopePresent !== redemptionPresent;
1558
- if (memberCustodyRequired !== exactMemberCustody) {
1559
- throw invalidPayload('records');
1560
- }
1564
+ : !claimPresent && envelopePresent !== redemptionPresent,
1565
+ };
1566
+ });
1567
+ const cleanupInProgress = isCancellation
1568
+ && cancellationIndex === 1
1569
+ && lifecycle.value.batchRevision !== null;
1570
+ if (cleanupInProgress) {
1571
+ const allAbsent = memberCustody.every(item => item.absent);
1572
+ const allExact = memberCustody.every(item => item.exact);
1573
+ if (!allAbsent && !allExact) {
1574
+ throw invalidPayload('records');
1561
1575
  }
1562
1576
  }
1577
+ else {
1578
+ const memberCustodyRequired = isCancellation
1579
+ ? !cleanupCompleted && lifecycle.value.batchRevision !== null
1580
+ : normalPhaseIndex >= 3;
1581
+ if (memberCustody.some(item => (memberCustodyRequired ? !item.exact : !item.absent)))
1582
+ throw invalidPayload('records');
1583
+ }
1563
1584
  if (recovery.value.targetEvidence !== null && (recovery.value.targetHostMemberId === null
1564
1585
  || principalBindings.get(recovery.value.targetHostMemberId)
1565
1586
  !== recovery.value.targetEvidence.principalId))
@@ -1575,6 +1596,7 @@ function validateContinuity(records) {
1575
1596
  ? 'cloud-to-lan'
1576
1597
  : 'lan-to-cloud')
1577
1598
  || lifecycle.value.expectedAuthorityGeneration !== recovery.value.sourceAuthority.generation
1599
+ || lifecycle.value.createdAt !== recovery.value.createdAt
1578
1600
  || lifecycle.value.scheduledAt !== recovery.value.expiresAt
1579
1601
  || (recovery.value.sourceHostMemberId !== null
1580
1602
  && !members.has(recovery.value.sourceHostMemberId))
@@ -1598,23 +1620,39 @@ function validateContinuity(records) {
1598
1620
  }
1599
1621
  for (const key of keys.values()) {
1600
1622
  const recovery = recoveries.get(key.value.transferId);
1623
+ const lifecycle = lifecycles.get(key.value.transferId);
1601
1624
  const evidence = recovery?.value.sourceEvidence ?? recovery?.value.targetEvidence;
1602
1625
  const isEvidenceKey = evidence !== null
1603
1626
  && evidence !== undefined
1604
1627
  && evidence.receiptKeyId === key.value.receiptKeyId
1605
1628
  && 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;
1629
+ const effectivePhase = lifecycle?.value.state === 'recovery-required'
1630
+ ? lifecycle.value.recoveryFromPhase
1631
+ : lifecycle?.value.phase;
1632
+ const cancellationIndex = effectivePhase === null || effectivePhase === undefined
1633
+ ? undefined
1634
+ : CANCELLATION_PHASE_INDEX.get(effectivePhase);
1635
+ const normalPhaseIndex = lifecycle?.value.direction === 'cloud-to-lan'
1636
+ ? COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.indexOf(effectivePhase)
1637
+ : -1;
1638
+ const isCloudSourceVerifier = recovery?.value.sourceAuthority.kind === 'cloud'
1639
+ && lifecycle?.value.direction === 'cloud-to-lan'
1640
+ && keysByTransfer.get(key.value.transferId)?.length === 2
1641
+ && (recovery.value.relinquishmentProof !== null
1642
+ || (cancellationIndex === undefined && normalPhaseIndex >= 2)
1643
+ || (cancellationIndex !== undefined
1644
+ && cancellationIndex <= 1
1645
+ && lifecycle.value.checkpointSha256 !== null));
1609
1646
  if (recovery === undefined
1610
1647
  || evidence === null
1611
1648
  || evidence === undefined
1612
- || (!isEvidenceKey && !isCloudSourceProofVerifier))
1649
+ || (!isEvidenceKey && !isCloudSourceVerifier))
1613
1650
  throw invalidPayload('records');
1614
1651
  }
1615
1652
  for (const claim of claims.values()) {
1616
1653
  const lifecycle = lifecycles.get(claim.value.transferId);
1617
1654
  const recovery = recoveries.get(claim.value.transferId);
1655
+ const member = memberRecords.get(claim.value.memberId);
1618
1656
  const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
1619
1657
  const redeemedOverride = redemptionReceipt === undefined
1620
1658
  ? undefined
@@ -1623,8 +1661,9 @@ function validateContinuity(records) {
1623
1661
  && item.value.state === 'redeemed'
1624
1662
  && item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
1625
1663
  && item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
1626
- if (memberRecords.get(claim.value.memberId)?.value.status !== 'active'
1664
+ if (member?.value.status !== 'active'
1627
1665
  || recovery === undefined
1666
+ || Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
1628
1667
  || recovery.value.sourceAuthority.kind !== 'lan'
1629
1668
  || claim.value.memberId === recovery.value.sourceHostMemberId
1630
1669
  || lifecycle?.value.batchRevision !== claim.value.batchRevision
@@ -1807,10 +1846,12 @@ function validateContinuity(records) {
1807
1846
  const identity = `${item.value.transferId}:${item.value.memberId}`;
1808
1847
  const recovery = recoveries.get(item.value.transferId);
1809
1848
  const lifecycle = lifecycles.get(item.value.transferId);
1849
+ const member = memberRecords.get(item.value.memberId);
1810
1850
  if (item.recordId !== identity
1811
1851
  || recovery === undefined
1812
1852
  || lifecycle === undefined
1813
- || memberRecords.get(item.value.memberId)?.value.status !== 'active'
1853
+ || member?.value.status !== 'active'
1854
+ || Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
1814
1855
  || item.value.memberId === recovery.value.targetHostMemberId
1815
1856
  || item.value.associatedData.authorityGeneration
1816
1857
  !== recovery.value.sourceAuthority.generation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudian-collab/protocol",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {