@claudian-collab/protocol 4.2.0 → 4.2.1
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 +1 -1
- package/dist/CollabMemberMentionParser.js +7 -5
- package/dist/CollabProjectBackupCheckpoint.js +50 -46
- package/dist/CollabProjectCheckpoint.js +4 -3
- package/dist/CollabTicketReferenceParser.js +18 -3
- package/dist/esm/CollabMemberMentionParser.mjs +7 -5
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +50 -46
- package/dist/esm/CollabProjectCheckpoint.mjs +4 -3
- package/dist/esm/CollabTicketReferenceParser.mjs +18 -3
- package/package.json +2 -3
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.2.
|
|
67
|
+
- **Package version** (this `package.json`): `4.2.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.
|
|
68
68
|
- **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `9`. The supported range is exactly `[9, 9]`. This is independent from Cloud binding version `5` 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 `5`. 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 v5 and wire versions before v9 are unsupported rather than translated or dual-interpreted.
|
|
70
70
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseCollabMemberMentions = parseCollabMemberMentions;
|
|
4
4
|
const CollabMarkdownProse_1 = require("./CollabMarkdownProse");
|
|
5
|
-
const
|
|
5
|
+
const MENTION_PRECEDING_CHARACTER = /[\p{L}\p{N}_@-]$/u;
|
|
6
|
+
const MENTION_FOLLOWING_CHARACTER = /^[\p{L}\p{N}_@-]/u;
|
|
6
7
|
function parseCollabMemberMentions(markdown, targets) {
|
|
7
8
|
const candidates = mentionCandidates(targets);
|
|
8
9
|
if (candidates.length === 0)
|
|
@@ -15,14 +16,15 @@ function parseCollabMemberMentions(markdown, targets) {
|
|
|
15
16
|
if (mentionStart < 0)
|
|
16
17
|
break;
|
|
17
18
|
searchFrom = mentionStart + 1;
|
|
18
|
-
const preceding = prose
|
|
19
|
-
if (
|
|
19
|
+
const preceding = prose.slice(Math.max(0, mentionStart - 2), mentionStart);
|
|
20
|
+
if (MENTION_PRECEDING_CHARACTER.test(preceding))
|
|
20
21
|
continue;
|
|
21
22
|
const candidate = candidates.find(entry => {
|
|
22
23
|
if (!prose.startsWith(entry.name, mentionStart + 1))
|
|
23
24
|
return false;
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const followingOffset = mentionStart + entry.name.length + 1;
|
|
26
|
+
const following = prose.slice(followingOffset, followingOffset + 2);
|
|
27
|
+
return !MENTION_FOLLOWING_CHARACTER.test(following);
|
|
26
28
|
});
|
|
27
29
|
if (!candidate)
|
|
28
30
|
continue;
|
|
@@ -1384,6 +1384,13 @@ function validateContinuity(records) {
|
|
|
1384
1384
|
const terminalResponders = new Map(records
|
|
1385
1385
|
.filter((item) => (item.kind === 'terminal-responder'))
|
|
1386
1386
|
.map(item => [item.value.operationId, item]));
|
|
1387
|
+
const terminalMembers = new Map([...terminalResponders].map(([operationId, terminal]) => [
|
|
1388
|
+
operationId,
|
|
1389
|
+
{
|
|
1390
|
+
acknowledgements: new Map(terminal.value.acknowledgements.map(item => [item.memberId, item])),
|
|
1391
|
+
eligibleMemberIds: new Set(terminal.value.eligibleMemberIds),
|
|
1392
|
+
},
|
|
1393
|
+
]));
|
|
1387
1394
|
const terminalPrincipals = new Map(records
|
|
1388
1395
|
.filter((item) => (item.kind === 'terminal-principal'))
|
|
1389
1396
|
.map(item => [`${item.value.operationId}:${item.value.memberId}`, item]));
|
|
@@ -1400,6 +1407,9 @@ function validateContinuity(records) {
|
|
|
1400
1407
|
.filter((item) => (item.kind === 'protected-invitation-envelope'))
|
|
1401
1408
|
.map(item => [item.value.invitationId, item]));
|
|
1402
1409
|
const claimOverrides = records.filter((item) => (item.kind === 'transferred-membership-claim-override'));
|
|
1410
|
+
const redeemedOverrideIdentities = new Set(claimOverrides
|
|
1411
|
+
.filter(item => item.value.state === 'redeemed')
|
|
1412
|
+
.map(item => (`${item.value.transferId}:${item.value.memberId}:${item.value.claimSha256}:${item.value.redemptionReceiptId}`)));
|
|
1403
1413
|
const claimOverrideEnvelopes = new Map(records
|
|
1404
1414
|
.filter((item) => (item.kind === 'protected-claim-override-envelope'))
|
|
1405
1415
|
.map(item => [
|
|
@@ -1412,6 +1422,28 @@ function validateContinuity(records) {
|
|
|
1412
1422
|
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1413
1423
|
.map(item => [item.value.operationId, item]));
|
|
1414
1424
|
const secretReplayTombstones = records.filter((item) => (item.kind === 'secret-replay-tombstone'));
|
|
1425
|
+
const secretTombstonesByIdentity = new Map(secretReplayTombstones.map(item => [
|
|
1426
|
+
`${item.recordId}:${item.value.requestFingerprint}`,
|
|
1427
|
+
item,
|
|
1428
|
+
]));
|
|
1429
|
+
const liveSecretIdentities = new Set();
|
|
1430
|
+
for (const invitation of invitations.values()) {
|
|
1431
|
+
if (invitationEnvelopes.has(invitation.value.invitationId)) {
|
|
1432
|
+
liveSecretIdentities.add(`createProjectInvitation:${invitation.value.issuedByMemberId}:${invitation.value.idempotencyKey}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
for (const override of claimOverrides) {
|
|
1436
|
+
if (claimOverrideEnvelopes.has(override.recordId)) {
|
|
1437
|
+
liveSecretIdentities.add(`reissueTransferredMembershipClaim:${override.value.managerMemberId}:${override.value.idempotencyKey}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const liveIdempotencyIdentities = new Set(records
|
|
1441
|
+
.filter(item => item.kind === 'idempotency-result')
|
|
1442
|
+
.map(item => `${item.value.operation}:${item.value.memberId}:${item.value.idempotencyKey}`));
|
|
1443
|
+
for (const offer of managerOffers) {
|
|
1444
|
+
liveIdempotencyIdentities.add(`createManagerResponsibilityOffer:${offer.value.sourceManagerMemberId}:${offer.value.idempotencyKey}`);
|
|
1445
|
+
}
|
|
1446
|
+
const claimOverrideRecordIds = new Set(claimOverrides.map(item => item.recordId));
|
|
1415
1447
|
const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
|
|
1416
1448
|
if (nonterminalLifecycles.length > 1
|
|
1417
1449
|
|| principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
|
|
@@ -1664,13 +1696,8 @@ function validateContinuity(records) {
|
|
|
1664
1696
|
const recovery = recoveries.get(claim.value.transferId);
|
|
1665
1697
|
const member = memberRecords.get(claim.value.memberId);
|
|
1666
1698
|
const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
: claimOverrides.find(item => (item.value.transferId === claim.value.transferId
|
|
1670
|
-
&& item.value.memberId === claim.value.memberId
|
|
1671
|
-
&& item.value.state === 'redeemed'
|
|
1672
|
-
&& item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
|
|
1673
|
-
&& item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
|
|
1699
|
+
const hasRedeemedOverride = redemptionReceipt !== undefined
|
|
1700
|
+
&& redeemedOverrideIdentities.has(`${claim.value.transferId}:${claim.value.memberId}:${redemptionReceipt.value.receipt.claimSha256}:${redemptionReceipt.value.receipt.receiptId}`);
|
|
1674
1701
|
if (member?.value.status !== 'active'
|
|
1675
1702
|
|| recovery === undefined
|
|
1676
1703
|
|| Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
|
|
@@ -1684,8 +1711,8 @@ function validateContinuity(records) {
|
|
|
1684
1711
|
|| (claim.value.state === 'redeemed' && redemptionReceipt === undefined)
|
|
1685
1712
|
|| (redemptionReceipt !== undefined
|
|
1686
1713
|
&& claim.value.state !== 'redeemed'
|
|
1687
|
-
&&
|
|
1688
|
-
|| (claim.value.state === 'redeemed' &&
|
|
1714
|
+
&& !hasRedeemedOverride)
|
|
1715
|
+
|| (claim.value.state === 'redeemed' && hasRedeemedOverride)) {
|
|
1689
1716
|
throw invalidPayload('records');
|
|
1690
1717
|
}
|
|
1691
1718
|
}
|
|
@@ -1728,10 +1755,7 @@ function validateContinuity(records) {
|
|
|
1728
1755
|
}
|
|
1729
1756
|
for (const invitation of invitations.values()) {
|
|
1730
1757
|
const envelope = invitationEnvelopes.get(invitation.value.invitationId);
|
|
1731
|
-
const tombstone =
|
|
1732
|
-
&& item.value.actorMemberId === invitation.value.issuedByMemberId
|
|
1733
|
-
&& item.value.idempotencyKey === invitation.value.idempotencyKey
|
|
1734
|
-
&& item.value.requestFingerprint === invitation.value.requestFingerprint));
|
|
1758
|
+
const tombstone = secretTombstonesByIdentity.get(`createProjectInvitation:${invitation.value.issuedByMemberId}:${invitation.value.idempotencyKey}:${invitation.value.requestFingerprint}`);
|
|
1735
1759
|
if (!members.has(invitation.value.issuedByMemberId)
|
|
1736
1760
|
|| (envelope === undefined) === (tombstone === undefined)
|
|
1737
1761
|
|| (tombstone !== undefined
|
|
@@ -1752,10 +1776,7 @@ function validateContinuity(records) {
|
|
|
1752
1776
|
values.push(override);
|
|
1753
1777
|
overridesByMember.set(identity, values);
|
|
1754
1778
|
const envelope = claimOverrideEnvelopes.get(`${identity}:${override.value.claimGeneration}`);
|
|
1755
|
-
const tombstone =
|
|
1756
|
-
&& item.value.actorMemberId === override.value.managerMemberId
|
|
1757
|
-
&& item.value.idempotencyKey === override.value.idempotencyKey
|
|
1758
|
-
&& item.value.requestFingerprint === override.value.requestFingerprint));
|
|
1779
|
+
const tombstone = secretTombstonesByIdentity.get(`reissueTransferredMembershipClaim:${override.value.managerMemberId}:${override.value.idempotencyKey}:${override.value.requestFingerprint}`);
|
|
1759
1780
|
if (!members.has(override.value.memberId)
|
|
1760
1781
|
|| !members.has(override.value.managerMemberId)
|
|
1761
1782
|
|| (envelope === undefined) === (tombstone === undefined)
|
|
@@ -1810,9 +1831,7 @@ function validateContinuity(records) {
|
|
|
1810
1831
|
throw invalidPayload('records');
|
|
1811
1832
|
}
|
|
1812
1833
|
for (const envelope of claimOverrideEnvelopes.values()) {
|
|
1813
|
-
if (!
|
|
1814
|
-
&& item.value.memberId === envelope.value.memberId
|
|
1815
|
-
&& item.value.claimGeneration === envelope.value.claimGeneration)))
|
|
1834
|
+
if (!claimOverrideRecordIds.has(envelope.recordId))
|
|
1816
1835
|
throw invalidPayload('records');
|
|
1817
1836
|
}
|
|
1818
1837
|
const currentOffers = managerOffers.filter(item => (item.value.state === 'offered' || item.value.state === 'acknowledged'));
|
|
@@ -1825,26 +1844,14 @@ function validateContinuity(records) {
|
|
|
1825
1844
|
!== currentOffers.length)
|
|
1826
1845
|
throw invalidPayload('records');
|
|
1827
1846
|
for (const tombstone of secretReplayTombstones) {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1831
|
-
&& invitationEnvelopes.has(item.value.invitationId)))
|
|
1832
|
-
: claimOverrides.some(item => (item.value.managerMemberId === tombstone.value.actorMemberId
|
|
1833
|
-
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1834
|
-
&& claimOverrideEnvelopes.has(`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`)));
|
|
1835
|
-
if (!members.has(tombstone.value.actorMemberId) || liveSecretRecord) {
|
|
1847
|
+
if (!members.has(tombstone.value.actorMemberId)
|
|
1848
|
+
|| liveSecretIdentities.has(tombstone.recordId)) {
|
|
1836
1849
|
throw invalidPayload('records');
|
|
1837
1850
|
}
|
|
1838
1851
|
}
|
|
1839
1852
|
for (const tombstone of membershipIdempotencyTombstones) {
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
&& item.value.operation === tombstone.value.operation
|
|
1843
|
-
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1844
|
-
const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
|
|
1845
|
-
&& managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
|
|
1846
|
-
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1847
|
-
if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
|
|
1853
|
+
if (!members.has(tombstone.value.actorMemberId)
|
|
1854
|
+
|| liveIdempotencyIdentities.has(tombstone.recordId)) {
|
|
1848
1855
|
throw invalidPayload('records');
|
|
1849
1856
|
}
|
|
1850
1857
|
}
|
|
@@ -1901,18 +1908,14 @@ function validateContinuity(records) {
|
|
|
1901
1908
|
const lifecycle = lifecycles.get(receipt.transferId);
|
|
1902
1909
|
const terminalPrincipal = terminalPrincipals.get(identity);
|
|
1903
1910
|
const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
|
|
1904
|
-
const
|
|
1905
|
-
&& item.value.memberId === receipt.memberId
|
|
1906
|
-
&& item.value.state === 'redeemed'
|
|
1907
|
-
&& item.value.claimSha256 === receipt.claimSha256
|
|
1908
|
-
&& item.value.redemptionReceiptId === receipt.receiptId));
|
|
1911
|
+
const hasRedeemedOverride = redeemedOverrideIdentities.has(`${receipt.transferId}:${receipt.memberId}:${receipt.claimSha256}:${receipt.receiptId}`);
|
|
1909
1912
|
const matchesSourceClaim = claim?.value.state === 'redeemed'
|
|
1910
1913
|
&& claim.value.claimSha256 === receipt.claimSha256
|
|
1911
1914
|
&& claim.value.checkpointSha256 === receipt.checkpointSha256
|
|
1912
1915
|
&& claim.value.operationIntentId === receipt.operationIntentId
|
|
1913
1916
|
&& claim.value.redemptionReceiptId === receipt.receiptId;
|
|
1914
1917
|
const matchesOverride = claim !== undefined
|
|
1915
|
-
&&
|
|
1918
|
+
&& hasRedeemedOverride
|
|
1916
1919
|
&& claim.value.checkpointSha256 === receipt.checkpointSha256;
|
|
1917
1920
|
if (recovery === undefined
|
|
1918
1921
|
|| lifecycle === undefined
|
|
@@ -1932,7 +1935,8 @@ function validateContinuity(records) {
|
|
|
1932
1935
|
}
|
|
1933
1936
|
if (item.kind === 'terminal-principal') {
|
|
1934
1937
|
const terminal = terminalResponders.get(item.value.operationId);
|
|
1935
|
-
const
|
|
1938
|
+
const terminalMemberIndex = terminalMembers.get(item.value.operationId);
|
|
1939
|
+
const acknowledgement = terminalMemberIndex?.acknowledgements.get(item.value.memberId);
|
|
1936
1940
|
const expectedOperationKind = terminal?.value.operation === 'getProjectAuthorityTransfer'
|
|
1937
1941
|
? 'authority-transfer'
|
|
1938
1942
|
: terminal?.value.operation === 'retireProject'
|
|
@@ -1941,7 +1945,7 @@ function validateContinuity(records) {
|
|
|
1941
1945
|
if (!members.has(item.value.memberId)
|
|
1942
1946
|
|| terminal === undefined
|
|
1943
1947
|
|| expectedOperationKind !== item.value.operationKind
|
|
1944
|
-
|| !
|
|
1948
|
+
|| !terminalMemberIndex?.eligibleMemberIds.has(item.value.memberId)
|
|
1945
1949
|
|| (item.value.acknowledgedAt === null) !== (acknowledgement === undefined)
|
|
1946
1950
|
|| (acknowledgement !== undefined && (acknowledgement.acknowledgedAt !== item.value.acknowledgedAt
|
|
1947
1951
|
|| acknowledgement.principalId !== item.value.principalId))
|
|
@@ -2097,8 +2101,8 @@ function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
|
2097
2101
|
}
|
|
2098
2102
|
function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
|
|
2099
2103
|
const value = records.map(item => JSON.stringify(item)).join('\n') + '\n';
|
|
2100
|
-
|
|
2101
|
-
|
|
2104
|
+
decodeCollabProjectBackupCheckpointCoordinationNdjson(value);
|
|
2105
|
+
return value;
|
|
2102
2106
|
}
|
|
2103
2107
|
function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
2104
2108
|
const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
|
|
@@ -939,9 +939,10 @@ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
|
|
|
939
939
|
: token(acknowledgement, 'principalId'),
|
|
940
940
|
};
|
|
941
941
|
});
|
|
942
|
+
const eligibleMemberIdSet = new Set(eligibleMemberIds);
|
|
942
943
|
const acknowledgementPrincipals = new Set();
|
|
943
944
|
acknowledgements.forEach((item, index) => {
|
|
944
|
-
if (!
|
|
945
|
+
if (!eligibleMemberIdSet.has(item.memberId)
|
|
945
946
|
|| (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
|
|
946
947
|
|| (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
|
|
947
948
|
throw invalidPayload('acknowledgements');
|
|
@@ -1313,8 +1314,8 @@ function decodeCollabProjectCheckpointCoordinationNdjson(value, profile) {
|
|
|
1313
1314
|
}
|
|
1314
1315
|
function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
|
|
1315
1316
|
const encoded = records.map(record => JSON.stringify(record)).join('\n') + '\n';
|
|
1316
|
-
|
|
1317
|
-
|
|
1317
|
+
decodeCollabProjectCheckpointCoordinationNdjson(encoded, profile);
|
|
1318
|
+
return encoded;
|
|
1318
1319
|
}
|
|
1319
1320
|
function checkpointRecordGitOids(checkpointRecord) {
|
|
1320
1321
|
switch (checkpointRecord.kind) {
|
|
@@ -4,11 +4,26 @@ exports.parseCollabTicketReferences = parseCollabTicketReferences;
|
|
|
4
4
|
exports.scanCollabTicketReferences = scanCollabTicketReferences;
|
|
5
5
|
const CollabConstants_1 = require("./CollabConstants");
|
|
6
6
|
const CollabMarkdownProse_1 = require("./CollabMarkdownProse");
|
|
7
|
-
const CLOSING_KEYWORD_PATTERN =
|
|
7
|
+
const CLOSING_KEYWORD_PATTERN = /^(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)$/i;
|
|
8
8
|
const TICKET_REFERENCE_PATTERN = /(^|[^#0-9A-Za-z_])#([1-9][0-9]*)(?![#0-9A-Za-z_])/gm;
|
|
9
9
|
function relationKindBefore(maskedDescription, referenceOffset) {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
let wordEnd = referenceOffset;
|
|
11
|
+
while (maskedDescription[wordEnd - 1] === ' ' || maskedDescription[wordEnd - 1] === '\t') {
|
|
12
|
+
wordEnd -= 1;
|
|
13
|
+
}
|
|
14
|
+
if (maskedDescription[wordEnd - 1] === ':') {
|
|
15
|
+
wordEnd -= 1;
|
|
16
|
+
while (maskedDescription[wordEnd - 1] === ' ' || maskedDescription[wordEnd - 1] === '\t') {
|
|
17
|
+
wordEnd -= 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
let wordStart = wordEnd;
|
|
21
|
+
while (wordStart > 0 && /[A-Za-z]/.test(maskedDescription[wordStart - 1])) {
|
|
22
|
+
wordStart -= 1;
|
|
23
|
+
}
|
|
24
|
+
return CLOSING_KEYWORD_PATTERN.test(maskedDescription.slice(wordStart, wordEnd))
|
|
25
|
+
? 'resolves'
|
|
26
|
+
: 'references';
|
|
12
27
|
}
|
|
13
28
|
function parseCollabTicketReferences(description) {
|
|
14
29
|
if (new TextEncoder().encode(description).byteLength >
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { maskCollabMarkdownProse } from './CollabMarkdownProse.mjs';
|
|
2
|
-
const
|
|
2
|
+
const MENTION_PRECEDING_CHARACTER = /[\p{L}\p{N}_@-]$/u;
|
|
3
|
+
const MENTION_FOLLOWING_CHARACTER = /^[\p{L}\p{N}_@-]/u;
|
|
3
4
|
export function parseCollabMemberMentions(markdown, targets) {
|
|
4
5
|
const candidates = mentionCandidates(targets);
|
|
5
6
|
if (candidates.length === 0)
|
|
@@ -12,14 +13,15 @@ export function parseCollabMemberMentions(markdown, targets) {
|
|
|
12
13
|
if (mentionStart < 0)
|
|
13
14
|
break;
|
|
14
15
|
searchFrom = mentionStart + 1;
|
|
15
|
-
const preceding = prose
|
|
16
|
-
if (
|
|
16
|
+
const preceding = prose.slice(Math.max(0, mentionStart - 2), mentionStart);
|
|
17
|
+
if (MENTION_PRECEDING_CHARACTER.test(preceding))
|
|
17
18
|
continue;
|
|
18
19
|
const candidate = candidates.find(entry => {
|
|
19
20
|
if (!prose.startsWith(entry.name, mentionStart + 1))
|
|
20
21
|
return false;
|
|
21
|
-
const
|
|
22
|
-
|
|
22
|
+
const followingOffset = mentionStart + entry.name.length + 1;
|
|
23
|
+
const following = prose.slice(followingOffset, followingOffset + 2);
|
|
24
|
+
return !MENTION_FOLLOWING_CHARACTER.test(following);
|
|
23
25
|
});
|
|
24
26
|
if (!candidate)
|
|
25
27
|
continue;
|
|
@@ -1374,6 +1374,13 @@ function validateContinuity(records) {
|
|
|
1374
1374
|
const terminalResponders = new Map(records
|
|
1375
1375
|
.filter((item) => (item.kind === 'terminal-responder'))
|
|
1376
1376
|
.map(item => [item.value.operationId, item]));
|
|
1377
|
+
const terminalMembers = new Map([...terminalResponders].map(([operationId, terminal]) => [
|
|
1378
|
+
operationId,
|
|
1379
|
+
{
|
|
1380
|
+
acknowledgements: new Map(terminal.value.acknowledgements.map(item => [item.memberId, item])),
|
|
1381
|
+
eligibleMemberIds: new Set(terminal.value.eligibleMemberIds),
|
|
1382
|
+
},
|
|
1383
|
+
]));
|
|
1377
1384
|
const terminalPrincipals = new Map(records
|
|
1378
1385
|
.filter((item) => (item.kind === 'terminal-principal'))
|
|
1379
1386
|
.map(item => [`${item.value.operationId}:${item.value.memberId}`, item]));
|
|
@@ -1390,6 +1397,9 @@ function validateContinuity(records) {
|
|
|
1390
1397
|
.filter((item) => (item.kind === 'protected-invitation-envelope'))
|
|
1391
1398
|
.map(item => [item.value.invitationId, item]));
|
|
1392
1399
|
const claimOverrides = records.filter((item) => (item.kind === 'transferred-membership-claim-override'));
|
|
1400
|
+
const redeemedOverrideIdentities = new Set(claimOverrides
|
|
1401
|
+
.filter(item => item.value.state === 'redeemed')
|
|
1402
|
+
.map(item => (`${item.value.transferId}:${item.value.memberId}:${item.value.claimSha256}:${item.value.redemptionReceiptId}`)));
|
|
1393
1403
|
const claimOverrideEnvelopes = new Map(records
|
|
1394
1404
|
.filter((item) => (item.kind === 'protected-claim-override-envelope'))
|
|
1395
1405
|
.map(item => [
|
|
@@ -1402,6 +1412,28 @@ function validateContinuity(records) {
|
|
|
1402
1412
|
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1403
1413
|
.map(item => [item.value.operationId, item]));
|
|
1404
1414
|
const secretReplayTombstones = records.filter((item) => (item.kind === 'secret-replay-tombstone'));
|
|
1415
|
+
const secretTombstonesByIdentity = new Map(secretReplayTombstones.map(item => [
|
|
1416
|
+
`${item.recordId}:${item.value.requestFingerprint}`,
|
|
1417
|
+
item,
|
|
1418
|
+
]));
|
|
1419
|
+
const liveSecretIdentities = new Set();
|
|
1420
|
+
for (const invitation of invitations.values()) {
|
|
1421
|
+
if (invitationEnvelopes.has(invitation.value.invitationId)) {
|
|
1422
|
+
liveSecretIdentities.add(`createProjectInvitation:${invitation.value.issuedByMemberId}:${invitation.value.idempotencyKey}`);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
for (const override of claimOverrides) {
|
|
1426
|
+
if (claimOverrideEnvelopes.has(override.recordId)) {
|
|
1427
|
+
liveSecretIdentities.add(`reissueTransferredMembershipClaim:${override.value.managerMemberId}:${override.value.idempotencyKey}`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
const liveIdempotencyIdentities = new Set(records
|
|
1431
|
+
.filter(item => item.kind === 'idempotency-result')
|
|
1432
|
+
.map(item => `${item.value.operation}:${item.value.memberId}:${item.value.idempotencyKey}`));
|
|
1433
|
+
for (const offer of managerOffers) {
|
|
1434
|
+
liveIdempotencyIdentities.add(`createManagerResponsibilityOffer:${offer.value.sourceManagerMemberId}:${offer.value.idempotencyKey}`);
|
|
1435
|
+
}
|
|
1436
|
+
const claimOverrideRecordIds = new Set(claimOverrides.map(item => item.recordId));
|
|
1405
1437
|
const nonterminalLifecycles = [...lifecycles.values()].filter(item => (item.value.state === 'active' || item.value.state === 'recovery-required'));
|
|
1406
1438
|
if (nonterminalLifecycles.length > 1
|
|
1407
1439
|
|| principalBindingRecords.some(item => (memberRecords.get(item.value.memberId)?.value.status !== 'active'))
|
|
@@ -1654,13 +1686,8 @@ function validateContinuity(records) {
|
|
|
1654
1686
|
const recovery = recoveries.get(claim.value.transferId);
|
|
1655
1687
|
const member = memberRecords.get(claim.value.memberId);
|
|
1656
1688
|
const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
: claimOverrides.find(item => (item.value.transferId === claim.value.transferId
|
|
1660
|
-
&& item.value.memberId === claim.value.memberId
|
|
1661
|
-
&& item.value.state === 'redeemed'
|
|
1662
|
-
&& item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
|
|
1663
|
-
&& item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
|
|
1689
|
+
const hasRedeemedOverride = redemptionReceipt !== undefined
|
|
1690
|
+
&& redeemedOverrideIdentities.has(`${claim.value.transferId}:${claim.value.memberId}:${redemptionReceipt.value.receipt.claimSha256}:${redemptionReceipt.value.receipt.receiptId}`);
|
|
1664
1691
|
if (member?.value.status !== 'active'
|
|
1665
1692
|
|| recovery === undefined
|
|
1666
1693
|
|| Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
|
|
@@ -1674,8 +1701,8 @@ function validateContinuity(records) {
|
|
|
1674
1701
|
|| (claim.value.state === 'redeemed' && redemptionReceipt === undefined)
|
|
1675
1702
|
|| (redemptionReceipt !== undefined
|
|
1676
1703
|
&& claim.value.state !== 'redeemed'
|
|
1677
|
-
&&
|
|
1678
|
-
|| (claim.value.state === 'redeemed' &&
|
|
1704
|
+
&& !hasRedeemedOverride)
|
|
1705
|
+
|| (claim.value.state === 'redeemed' && hasRedeemedOverride)) {
|
|
1679
1706
|
throw invalidPayload('records');
|
|
1680
1707
|
}
|
|
1681
1708
|
}
|
|
@@ -1718,10 +1745,7 @@ function validateContinuity(records) {
|
|
|
1718
1745
|
}
|
|
1719
1746
|
for (const invitation of invitations.values()) {
|
|
1720
1747
|
const envelope = invitationEnvelopes.get(invitation.value.invitationId);
|
|
1721
|
-
const tombstone =
|
|
1722
|
-
&& item.value.actorMemberId === invitation.value.issuedByMemberId
|
|
1723
|
-
&& item.value.idempotencyKey === invitation.value.idempotencyKey
|
|
1724
|
-
&& item.value.requestFingerprint === invitation.value.requestFingerprint));
|
|
1748
|
+
const tombstone = secretTombstonesByIdentity.get(`createProjectInvitation:${invitation.value.issuedByMemberId}:${invitation.value.idempotencyKey}:${invitation.value.requestFingerprint}`);
|
|
1725
1749
|
if (!members.has(invitation.value.issuedByMemberId)
|
|
1726
1750
|
|| (envelope === undefined) === (tombstone === undefined)
|
|
1727
1751
|
|| (tombstone !== undefined
|
|
@@ -1742,10 +1766,7 @@ function validateContinuity(records) {
|
|
|
1742
1766
|
values.push(override);
|
|
1743
1767
|
overridesByMember.set(identity, values);
|
|
1744
1768
|
const envelope = claimOverrideEnvelopes.get(`${identity}:${override.value.claimGeneration}`);
|
|
1745
|
-
const tombstone =
|
|
1746
|
-
&& item.value.actorMemberId === override.value.managerMemberId
|
|
1747
|
-
&& item.value.idempotencyKey === override.value.idempotencyKey
|
|
1748
|
-
&& item.value.requestFingerprint === override.value.requestFingerprint));
|
|
1769
|
+
const tombstone = secretTombstonesByIdentity.get(`reissueTransferredMembershipClaim:${override.value.managerMemberId}:${override.value.idempotencyKey}:${override.value.requestFingerprint}`);
|
|
1749
1770
|
if (!members.has(override.value.memberId)
|
|
1750
1771
|
|| !members.has(override.value.managerMemberId)
|
|
1751
1772
|
|| (envelope === undefined) === (tombstone === undefined)
|
|
@@ -1800,9 +1821,7 @@ function validateContinuity(records) {
|
|
|
1800
1821
|
throw invalidPayload('records');
|
|
1801
1822
|
}
|
|
1802
1823
|
for (const envelope of claimOverrideEnvelopes.values()) {
|
|
1803
|
-
if (!
|
|
1804
|
-
&& item.value.memberId === envelope.value.memberId
|
|
1805
|
-
&& item.value.claimGeneration === envelope.value.claimGeneration)))
|
|
1824
|
+
if (!claimOverrideRecordIds.has(envelope.recordId))
|
|
1806
1825
|
throw invalidPayload('records');
|
|
1807
1826
|
}
|
|
1808
1827
|
const currentOffers = managerOffers.filter(item => (item.value.state === 'offered' || item.value.state === 'acknowledged'));
|
|
@@ -1815,26 +1834,14 @@ function validateContinuity(records) {
|
|
|
1815
1834
|
!== currentOffers.length)
|
|
1816
1835
|
throw invalidPayload('records');
|
|
1817
1836
|
for (const tombstone of secretReplayTombstones) {
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1821
|
-
&& invitationEnvelopes.has(item.value.invitationId)))
|
|
1822
|
-
: claimOverrides.some(item => (item.value.managerMemberId === tombstone.value.actorMemberId
|
|
1823
|
-
&& item.value.idempotencyKey === tombstone.value.idempotencyKey
|
|
1824
|
-
&& claimOverrideEnvelopes.has(`${item.value.transferId}:${item.value.memberId}:${item.value.claimGeneration}`)));
|
|
1825
|
-
if (!members.has(tombstone.value.actorMemberId) || liveSecretRecord) {
|
|
1837
|
+
if (!members.has(tombstone.value.actorMemberId)
|
|
1838
|
+
|| liveSecretIdentities.has(tombstone.recordId)) {
|
|
1826
1839
|
throw invalidPayload('records');
|
|
1827
1840
|
}
|
|
1828
1841
|
}
|
|
1829
1842
|
for (const tombstone of membershipIdempotencyTombstones) {
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
&& item.value.operation === tombstone.value.operation
|
|
1833
|
-
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1834
|
-
const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
|
|
1835
|
-
&& managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
|
|
1836
|
-
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1837
|
-
if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
|
|
1843
|
+
if (!members.has(tombstone.value.actorMemberId)
|
|
1844
|
+
|| liveIdempotencyIdentities.has(tombstone.recordId)) {
|
|
1838
1845
|
throw invalidPayload('records');
|
|
1839
1846
|
}
|
|
1840
1847
|
}
|
|
@@ -1891,18 +1898,14 @@ function validateContinuity(records) {
|
|
|
1891
1898
|
const lifecycle = lifecycles.get(receipt.transferId);
|
|
1892
1899
|
const terminalPrincipal = terminalPrincipals.get(identity);
|
|
1893
1900
|
const lanToCloud = recovery?.value.sourceAuthority.kind === 'lan';
|
|
1894
|
-
const
|
|
1895
|
-
&& item.value.memberId === receipt.memberId
|
|
1896
|
-
&& item.value.state === 'redeemed'
|
|
1897
|
-
&& item.value.claimSha256 === receipt.claimSha256
|
|
1898
|
-
&& item.value.redemptionReceiptId === receipt.receiptId));
|
|
1901
|
+
const hasRedeemedOverride = redeemedOverrideIdentities.has(`${receipt.transferId}:${receipt.memberId}:${receipt.claimSha256}:${receipt.receiptId}`);
|
|
1899
1902
|
const matchesSourceClaim = claim?.value.state === 'redeemed'
|
|
1900
1903
|
&& claim.value.claimSha256 === receipt.claimSha256
|
|
1901
1904
|
&& claim.value.checkpointSha256 === receipt.checkpointSha256
|
|
1902
1905
|
&& claim.value.operationIntentId === receipt.operationIntentId
|
|
1903
1906
|
&& claim.value.redemptionReceiptId === receipt.receiptId;
|
|
1904
1907
|
const matchesOverride = claim !== undefined
|
|
1905
|
-
&&
|
|
1908
|
+
&& hasRedeemedOverride
|
|
1906
1909
|
&& claim.value.checkpointSha256 === receipt.checkpointSha256;
|
|
1907
1910
|
if (recovery === undefined
|
|
1908
1911
|
|| lifecycle === undefined
|
|
@@ -1922,7 +1925,8 @@ function validateContinuity(records) {
|
|
|
1922
1925
|
}
|
|
1923
1926
|
if (item.kind === 'terminal-principal') {
|
|
1924
1927
|
const terminal = terminalResponders.get(item.value.operationId);
|
|
1925
|
-
const
|
|
1928
|
+
const terminalMemberIndex = terminalMembers.get(item.value.operationId);
|
|
1929
|
+
const acknowledgement = terminalMemberIndex?.acknowledgements.get(item.value.memberId);
|
|
1926
1930
|
const expectedOperationKind = terminal?.value.operation === 'getProjectAuthorityTransfer'
|
|
1927
1931
|
? 'authority-transfer'
|
|
1928
1932
|
: terminal?.value.operation === 'retireProject'
|
|
@@ -1931,7 +1935,7 @@ function validateContinuity(records) {
|
|
|
1931
1935
|
if (!members.has(item.value.memberId)
|
|
1932
1936
|
|| terminal === undefined
|
|
1933
1937
|
|| expectedOperationKind !== item.value.operationKind
|
|
1934
|
-
|| !
|
|
1938
|
+
|| !terminalMemberIndex?.eligibleMemberIds.has(item.value.memberId)
|
|
1935
1939
|
|| (item.value.acknowledgedAt === null) !== (acknowledgement === undefined)
|
|
1936
1940
|
|| (acknowledgement !== undefined && (acknowledgement.acknowledgedAt !== item.value.acknowledgedAt
|
|
1937
1941
|
|| acknowledgement.principalId !== item.value.principalId))
|
|
@@ -2087,8 +2091,8 @@ export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
|
2087
2091
|
}
|
|
2088
2092
|
export function encodeCollabProjectBackupCheckpointCoordinationNdjson(records) {
|
|
2089
2093
|
const value = records.map(item => JSON.stringify(item)).join('\n') + '\n';
|
|
2090
|
-
|
|
2091
|
-
|
|
2094
|
+
decodeCollabProjectBackupCheckpointCoordinationNdjson(value);
|
|
2095
|
+
return value;
|
|
2092
2096
|
}
|
|
2093
2097
|
export function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
2094
2098
|
const decodedManifest = decodeCollabProjectBackupCheckpointManifest(manifest);
|
|
@@ -924,9 +924,10 @@ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
|
|
|
924
924
|
: token(acknowledgement, 'principalId'),
|
|
925
925
|
};
|
|
926
926
|
});
|
|
927
|
+
const eligibleMemberIdSet = new Set(eligibleMemberIds);
|
|
927
928
|
const acknowledgementPrincipals = new Set();
|
|
928
929
|
acknowledgements.forEach((item, index) => {
|
|
929
|
-
if (!
|
|
930
|
+
if (!eligibleMemberIdSet.has(item.memberId)
|
|
930
931
|
|| (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
|
|
931
932
|
|| (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
|
|
932
933
|
throw invalidPayload('acknowledgements');
|
|
@@ -1298,8 +1299,8 @@ export function decodeCollabProjectCheckpointCoordinationNdjson(value, profile)
|
|
|
1298
1299
|
}
|
|
1299
1300
|
export function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
|
|
1300
1301
|
const encoded = records.map(record => JSON.stringify(record)).join('\n') + '\n';
|
|
1301
|
-
|
|
1302
|
-
|
|
1302
|
+
decodeCollabProjectCheckpointCoordinationNdjson(encoded, profile);
|
|
1303
|
+
return encoded;
|
|
1303
1304
|
}
|
|
1304
1305
|
function checkpointRecordGitOids(checkpointRecord) {
|
|
1305
1306
|
switch (checkpointRecord.kind) {
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { COLLAB_LIMITS } from './CollabConstants.mjs';
|
|
2
2
|
import { maskCollabMarkdownProse } from './CollabMarkdownProse.mjs';
|
|
3
|
-
const CLOSING_KEYWORD_PATTERN =
|
|
3
|
+
const CLOSING_KEYWORD_PATTERN = /^(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)$/i;
|
|
4
4
|
const TICKET_REFERENCE_PATTERN = /(^|[^#0-9A-Za-z_])#([1-9][0-9]*)(?![#0-9A-Za-z_])/gm;
|
|
5
5
|
function relationKindBefore(maskedDescription, referenceOffset) {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
let wordEnd = referenceOffset;
|
|
7
|
+
while (maskedDescription[wordEnd - 1] === ' ' || maskedDescription[wordEnd - 1] === '\t') {
|
|
8
|
+
wordEnd -= 1;
|
|
9
|
+
}
|
|
10
|
+
if (maskedDescription[wordEnd - 1] === ':') {
|
|
11
|
+
wordEnd -= 1;
|
|
12
|
+
while (maskedDescription[wordEnd - 1] === ' ' || maskedDescription[wordEnd - 1] === '\t') {
|
|
13
|
+
wordEnd -= 1;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let wordStart = wordEnd;
|
|
17
|
+
while (wordStart > 0 && /[A-Za-z]/.test(maskedDescription[wordStart - 1])) {
|
|
18
|
+
wordStart -= 1;
|
|
19
|
+
}
|
|
20
|
+
return CLOSING_KEYWORD_PATTERN.test(maskedDescription.slice(wordStart, wordEnd))
|
|
21
|
+
? 'resolves'
|
|
22
|
+
: 'references';
|
|
8
23
|
}
|
|
9
24
|
export function parseCollabTicketReferences(description) {
|
|
10
25
|
if (new TextEncoder().encode(description).byteLength >
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudian-collab/protocol",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,14 +39,13 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig.esm.json && node scripts/finalize-esm.mjs",
|
|
41
41
|
"check:compatibility": "node scripts/run-compatibility-check.mjs",
|
|
42
|
-
"check:migration-provenance": "node scripts/check-migration-provenance.mjs --verify-standalone",
|
|
43
42
|
"clean": "node scripts/clean.mjs",
|
|
44
43
|
"lint": "eslint src tests scripts",
|
|
45
44
|
"prepack": "npm run build",
|
|
46
45
|
"release:candidate": "node scripts/release-candidate.mjs --output .context/release",
|
|
47
46
|
"test": "jest && node --test tests/*.test.mjs",
|
|
48
47
|
"typecheck": "tsc --noEmit -p tsconfig.jest.json",
|
|
49
|
-
"verify": "npm run lint && npm run typecheck && npm run release:candidate && npm run test && npm run check:compatibility && npm run
|
|
48
|
+
"verify": "npm run lint && npm run typecheck && npm run release:candidate && npm run test && npm run check:compatibility && npm run verify:pack",
|
|
50
49
|
"verify:pack": "node scripts/verify-pack.mjs"
|
|
51
50
|
},
|
|
52
51
|
"dependencies": {
|