@claudian-collab/protocol 3.3.0 → 3.3.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
CHANGED
|
@@ -37,7 +37,7 @@ fixed part of the detail so the whole detail stays within
|
|
|
37
37
|
has one compatible final-serialization limit. Request comments and Ticket
|
|
38
38
|
accepted relations also have shared authority-enforced total limits; summary
|
|
39
39
|
counts let complete consumers reject partial or cross-snapshot assembly.
|
|
40
|
-
Cloud binding v2 adds the authority-neutral Project snapshot, nine redacted durable event kinds plus `snapshot.required`, eighteen capability tokens, the exact ordinary Project/Git route catalog, bounded authority-transfer artifact routes, and six private-development bootstrap bindings. Wire v6 includes the additive Cloud Project-membership operation family while preserving every existing operation contract. The shared checkpoint container is exactly `checkpoint.json`, `coordination.ndjson`, and `repository.bundle`; authority-transfer and export retain wire-visible coordination format v1, while the pre-production offline backup contract is directly replaced by format v3. Backup v3 adds Project invitations, protected invitation and transferred-claim override envelopes, manager-responsibility offers, membership recovery journals,
|
|
40
|
+
Cloud binding v2 adds the authority-neutral Project snapshot, nine redacted durable event kinds plus `snapshot.required`, eighteen capability tokens, the exact ordinary Project/Git route catalog, bounded authority-transfer artifact routes, and six private-development bootstrap bindings. Wire v6 includes the additive Cloud Project-membership operation family while preserving every existing operation contract. The shared checkpoint container is exactly `checkpoint.json`, `coordination.ndjson`, and `repository.bundle`; authority-transfer and export retain wire-visible coordination format v1, while the pre-production offline backup contract is directly replaced by format v3. Backup v3 adds Project invitations, protected invitation and transferred-claim override envelopes, manager-responsibility offers, membership recovery journals, secret-replay tombstones, and compacted Manager-responsibility idempotency tombstones to the existing continuity records. Portable profiles exclude engine storage, paths, credentials, tokens, private keys, and operational refs, and backups exclude plaintext invitation secrets, raw claims, and private keys. Domain-separated canonical UTF-8 inputs define protected-claim AEAD associated data and bind every declared field of transferred-membership redemption receipts and authority-relinquishment proofs before consumer-owned Ed25519 signing or verification. Existing LAN v9 Join, invitation, endpoint, membership lifecycle, Host-transfer, snapshot, event, and HTTP bindings remain independently application-owned; the dedicated LAN authority-transfer transport binds these shared payloads without creating another shared operation registry.
|
|
41
41
|
|
|
42
42
|
## Usage
|
|
43
43
|
|
|
@@ -62,7 +62,7 @@ part of the public surface.
|
|
|
62
62
|
|
|
63
63
|
Package SemVer, canonical wire version, and Cloud binding version are independent concepts.
|
|
64
64
|
|
|
65
|
-
- **Package version** (this `package.json`): `3.3.
|
|
65
|
+
- **Package version** (this `package.json`): `3.3.1`. Patch releases preserve
|
|
66
66
|
every existing public declaration and runtime behavior baseline. Minor
|
|
67
67
|
releases may add backward-compatible API. Removing or changing an existing
|
|
68
68
|
declaration, export, runtime behavior, codec, error, limit, ref rule,
|
|
@@ -74,7 +74,10 @@ Package SemVer, canonical wire version, and Cloud binding version are independen
|
|
|
74
74
|
operation family and capability tokens and directly replaces the pre-production
|
|
75
75
|
offline backup contract with coordination format v3, without changing
|
|
76
76
|
authority-transfer/export format v1, canonical wire v6, or Cloud binding v2.
|
|
77
|
-
Backup v2 has no supported decoder or migration path. The
|
|
77
|
+
Backup v2 has no supported decoder or migration path. The pre-production
|
|
78
|
+
`3.3.1` corrective patch adds only the missing backup-v3 tombstone needed to
|
|
79
|
+
compact terminal Manager-responsibility offers and exact responses without
|
|
80
|
+
allowing their idempotency identities to be reused. The package version never
|
|
78
81
|
signals wire compatibility by itself.
|
|
79
82
|
- **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `6`. The supported
|
|
80
83
|
range is exactly `[6, 6]`. This is independent from Cloud binding version `2` and the existing application
|
|
@@ -5,7 +5,7 @@ import { type CollabManagerResponsibilityOfferState, type CollabManagerResponsib
|
|
|
5
5
|
import type { CollabIsoTimestamp, CollabMemberId, CollabProjectId } from './types';
|
|
6
6
|
export declare const COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE: "pre-production-replaceable";
|
|
7
7
|
export declare const COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION: 3;
|
|
8
|
-
export declare const COLLAB_PROJECT_BACKUP_RECORD_KINDS: readonly ["project", "member", "request", "request-comment", "ticket", "ticket-comment", "ticket-relation", "ticket-mention", "cloud-event", "cloud-event-cursor", "idempotency-result", "principal-binding", "repository-placement", "lifecycle-journal", "authority-transfer-recovery", "transferred-membership-claim", "transfer-receipt-key", "transfer-claim-batch-receipt", "transfer-redemption-receipt", "terminal-principal", "terminal-responder-replay", "leave-former-principal-replay", "project-invitation", "protected-invitation-envelope", "transferred-membership-claim-override", "protected-claim-override-envelope", "manager-responsibility-offer", "project-membership-recovery", "secret-replay-tombstone", "terminal-responder", "protected-claim-envelope", "tombstone", "schema-catalog", "server-compatibility", "authority-volume-pair"];
|
|
8
|
+
export declare const COLLAB_PROJECT_BACKUP_RECORD_KINDS: readonly ["project", "member", "request", "request-comment", "ticket", "ticket-comment", "ticket-relation", "ticket-mention", "cloud-event", "cloud-event-cursor", "idempotency-result", "principal-binding", "repository-placement", "lifecycle-journal", "authority-transfer-recovery", "transferred-membership-claim", "transfer-receipt-key", "transfer-claim-batch-receipt", "transfer-redemption-receipt", "terminal-principal", "terminal-responder-replay", "leave-former-principal-replay", "project-invitation", "protected-invitation-envelope", "transferred-membership-claim-override", "protected-claim-override-envelope", "manager-responsibility-offer", "membership-idempotency-tombstone", "project-membership-recovery", "secret-replay-tombstone", "terminal-responder", "protected-claim-envelope", "tombstone", "schema-catalog", "server-compatibility", "authority-volume-pair"];
|
|
9
9
|
export type CollabProjectBackupRecordKind = typeof COLLAB_PROJECT_BACKUP_RECORD_KINDS[number];
|
|
10
10
|
interface BackupRecordBase<Kind extends string, Value> {
|
|
11
11
|
readonly kind: Kind;
|
|
@@ -208,6 +208,15 @@ export type CollabProjectBackupManagerResponsibilityOfferRecord = BackupRecordBa
|
|
|
208
208
|
readonly targetMembershipRevisionAtOffer: number;
|
|
209
209
|
readonly terminalAt: CollabIsoTimestamp | null;
|
|
210
210
|
}>;
|
|
211
|
+
type CollabProjectBackupCompactedMembershipOperation = Extract<CollabProjectMembershipOperation, 'createManagerResponsibilityOffer' | 'acknowledgeManagerResponsibility' | 'declineManagerResponsibility' | 'cancelManagerResponsibilityOffer' | 'promoteManager'>;
|
|
212
|
+
export type CollabProjectBackupMembershipIdempotencyTombstoneRecord = BackupRecordBase<'membership-idempotency-tombstone', {
|
|
213
|
+
readonly actorMemberId: CollabMemberId;
|
|
214
|
+
readonly compactedAt: CollabIsoTimestamp;
|
|
215
|
+
readonly idempotencyKey: string;
|
|
216
|
+
readonly operation: CollabProjectBackupCompactedMembershipOperation;
|
|
217
|
+
readonly projectId: CollabProjectId;
|
|
218
|
+
readonly requestFingerprint: string;
|
|
219
|
+
}>;
|
|
211
220
|
export type CollabProjectBackupMembershipRecoveryRecord = BackupRecordBase<'project-membership-recovery', {
|
|
212
221
|
readonly expectedMainOid: string;
|
|
213
222
|
readonly expectedPersonalRefOid: string;
|
|
@@ -229,7 +238,7 @@ export type CollabProjectBackupSecretReplayTombstoneRecord = BackupRecordBase<'s
|
|
|
229
238
|
readonly projectId: CollabProjectId;
|
|
230
239
|
readonly requestFingerprint: string;
|
|
231
240
|
}>;
|
|
232
|
-
export type CollabProjectBackupContinuityRecord = CollabProjectBackupLifecycleJournalRecord | CollabProjectBackupAuthorityTransferRecoveryRecord | CollabProjectBackupTransferredMembershipClaimRecord | CollabProjectBackupTransferReceiptKeyRecord | CollabProjectBackupTransferClaimBatchReceiptRecord | CollabProjectBackupTransferRedemptionReceiptRecord | CollabProjectBackupTerminalPrincipalRecord | CollabProjectBackupTerminalResponderReplayRecord | CollabProjectBackupLeaveFormerPrincipalReplayRecord | CollabProjectBackupInvitationRecord | CollabProjectBackupProtectedInvitationEnvelopeRecord | CollabProjectBackupTransferredMembershipClaimOverrideRecord | CollabProjectBackupProtectedClaimOverrideEnvelopeRecord | CollabProjectBackupManagerResponsibilityOfferRecord | CollabProjectBackupMembershipRecoveryRecord | CollabProjectBackupSecretReplayTombstoneRecord;
|
|
241
|
+
export type CollabProjectBackupContinuityRecord = CollabProjectBackupLifecycleJournalRecord | CollabProjectBackupAuthorityTransferRecoveryRecord | CollabProjectBackupTransferredMembershipClaimRecord | CollabProjectBackupTransferReceiptKeyRecord | CollabProjectBackupTransferClaimBatchReceiptRecord | CollabProjectBackupTransferRedemptionReceiptRecord | CollabProjectBackupTerminalPrincipalRecord | CollabProjectBackupTerminalResponderReplayRecord | CollabProjectBackupLeaveFormerPrincipalReplayRecord | CollabProjectBackupInvitationRecord | CollabProjectBackupProtectedInvitationEnvelopeRecord | CollabProjectBackupTransferredMembershipClaimOverrideRecord | CollabProjectBackupProtectedClaimOverrideEnvelopeRecord | CollabProjectBackupManagerResponsibilityOfferRecord | CollabProjectBackupMembershipIdempotencyTombstoneRecord | CollabProjectBackupMembershipRecoveryRecord | CollabProjectBackupSecretReplayTombstoneRecord;
|
|
233
242
|
type CollabProjectBackupBaseRecord = Exclude<CollabCheckpointBackupRecord, {
|
|
234
243
|
readonly kind: 'lifecycle-state';
|
|
235
244
|
}>;
|
|
@@ -32,6 +32,7 @@ const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
|
32
32
|
'transferred-membership-claim-override',
|
|
33
33
|
'protected-claim-override-envelope',
|
|
34
34
|
'manager-responsibility-offer',
|
|
35
|
+
'membership-idempotency-tombstone',
|
|
35
36
|
'project-membership-recovery',
|
|
36
37
|
'secret-replay-tombstone',
|
|
37
38
|
]);
|
|
@@ -1183,6 +1184,41 @@ function membershipRecoveryRecord(source, recordId, revision) {
|
|
|
1183
1184
|
},
|
|
1184
1185
|
};
|
|
1185
1186
|
}
|
|
1187
|
+
function membershipIdempotencyTombstoneRecord(source, recordId, revision) {
|
|
1188
|
+
const value = exactRecord(source.value, 'value', [
|
|
1189
|
+
'actorMemberId',
|
|
1190
|
+
'compactedAt',
|
|
1191
|
+
'idempotencyKey',
|
|
1192
|
+
'operation',
|
|
1193
|
+
'projectId',
|
|
1194
|
+
'requestFingerprint',
|
|
1195
|
+
]);
|
|
1196
|
+
const actorMemberId = token(value, 'actorMemberId', CollabValidation_1.isCollabMemberId);
|
|
1197
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1198
|
+
const operation = literal(value, 'operation', [
|
|
1199
|
+
'createManagerResponsibilityOffer',
|
|
1200
|
+
'acknowledgeManagerResponsibility',
|
|
1201
|
+
'declineManagerResponsibility',
|
|
1202
|
+
'cancelManagerResponsibilityOffer',
|
|
1203
|
+
'promoteManager',
|
|
1204
|
+
]);
|
|
1205
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1206
|
+
throw invalidPayload('recordId');
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
kind: 'membership-idempotency-tombstone',
|
|
1210
|
+
recordId,
|
|
1211
|
+
revision,
|
|
1212
|
+
value: {
|
|
1213
|
+
actorMemberId,
|
|
1214
|
+
compactedAt: timestamp(value, 'compactedAt'),
|
|
1215
|
+
idempotencyKey,
|
|
1216
|
+
operation,
|
|
1217
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1218
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1219
|
+
},
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1186
1222
|
function secretReplayTombstoneRecord(source, recordId, revision) {
|
|
1187
1223
|
const value = exactRecord(source.value, 'value', [
|
|
1188
1224
|
'actorMemberId',
|
|
@@ -1240,6 +1276,8 @@ function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
|
1240
1276
|
return protectedClaimOverrideEnvelopeRecord(source, recordId, revision);
|
|
1241
1277
|
case 'manager-responsibility-offer':
|
|
1242
1278
|
return managerResponsibilityOfferRecord(source, recordId, revision);
|
|
1279
|
+
case 'membership-idempotency-tombstone':
|
|
1280
|
+
return membershipIdempotencyTombstoneRecord(source, recordId, revision);
|
|
1243
1281
|
case 'project-membership-recovery':
|
|
1244
1282
|
return membershipRecoveryRecord(source, recordId, revision);
|
|
1245
1283
|
case 'secret-replay-tombstone':
|
|
@@ -1409,6 +1447,7 @@ function validateContinuity(records) {
|
|
|
1409
1447
|
item,
|
|
1410
1448
|
]));
|
|
1411
1449
|
const managerOffers = records.filter((item) => (item.kind === 'manager-responsibility-offer'));
|
|
1450
|
+
const membershipIdempotencyTombstones = records.filter((item) => (item.kind === 'membership-idempotency-tombstone'));
|
|
1412
1451
|
const membershipRecoveries = new Map(records
|
|
1413
1452
|
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1414
1453
|
.map(item => [item.value.operationId, item]));
|
|
@@ -1792,6 +1831,18 @@ function validateContinuity(records) {
|
|
|
1792
1831
|
throw invalidPayload('records');
|
|
1793
1832
|
}
|
|
1794
1833
|
}
|
|
1834
|
+
for (const tombstone of membershipIdempotencyTombstones) {
|
|
1835
|
+
const exactResult = records.some(item => (item.kind === 'idempotency-result'
|
|
1836
|
+
&& item.value.memberId === tombstone.value.actorMemberId
|
|
1837
|
+
&& item.value.operation === tombstone.value.operation
|
|
1838
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1839
|
+
const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
|
|
1840
|
+
&& managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
|
|
1841
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1842
|
+
if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
|
|
1843
|
+
throw invalidPayload('records');
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1795
1846
|
for (const item of records) {
|
|
1796
1847
|
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1797
1848
|
throw invalidPayload('records');
|
|
@@ -2069,6 +2120,9 @@ function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
|
2069
2120
|
if (item.kind === 'secret-replay-tombstone') {
|
|
2070
2121
|
return Date.parse(item.value.expiredAt) > captureTime;
|
|
2071
2122
|
}
|
|
2123
|
+
if (item.kind === 'membership-idempotency-tombstone') {
|
|
2124
|
+
return Date.parse(item.value.compactedAt) > captureTime;
|
|
2125
|
+
}
|
|
2072
2126
|
if (item.kind === 'project-invitation') {
|
|
2073
2127
|
return captureTime < Date.parse(item.value.createdAt)
|
|
2074
2128
|
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
@@ -22,6 +22,7 @@ const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
|
22
22
|
'transferred-membership-claim-override',
|
|
23
23
|
'protected-claim-override-envelope',
|
|
24
24
|
'manager-responsibility-offer',
|
|
25
|
+
'membership-idempotency-tombstone',
|
|
25
26
|
'project-membership-recovery',
|
|
26
27
|
'secret-replay-tombstone',
|
|
27
28
|
]);
|
|
@@ -1173,6 +1174,41 @@ function membershipRecoveryRecord(source, recordId, revision) {
|
|
|
1173
1174
|
},
|
|
1174
1175
|
};
|
|
1175
1176
|
}
|
|
1177
|
+
function membershipIdempotencyTombstoneRecord(source, recordId, revision) {
|
|
1178
|
+
const value = exactRecord(source.value, 'value', [
|
|
1179
|
+
'actorMemberId',
|
|
1180
|
+
'compactedAt',
|
|
1181
|
+
'idempotencyKey',
|
|
1182
|
+
'operation',
|
|
1183
|
+
'projectId',
|
|
1184
|
+
'requestFingerprint',
|
|
1185
|
+
]);
|
|
1186
|
+
const actorMemberId = token(value, 'actorMemberId', isCollabMemberId);
|
|
1187
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1188
|
+
const operation = literal(value, 'operation', [
|
|
1189
|
+
'createManagerResponsibilityOffer',
|
|
1190
|
+
'acknowledgeManagerResponsibility',
|
|
1191
|
+
'declineManagerResponsibility',
|
|
1192
|
+
'cancelManagerResponsibilityOffer',
|
|
1193
|
+
'promoteManager',
|
|
1194
|
+
]);
|
|
1195
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1196
|
+
throw invalidPayload('recordId');
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
kind: 'membership-idempotency-tombstone',
|
|
1200
|
+
recordId,
|
|
1201
|
+
revision,
|
|
1202
|
+
value: {
|
|
1203
|
+
actorMemberId,
|
|
1204
|
+
compactedAt: timestamp(value, 'compactedAt'),
|
|
1205
|
+
idempotencyKey,
|
|
1206
|
+
operation,
|
|
1207
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1208
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1209
|
+
},
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1176
1212
|
function secretReplayTombstoneRecord(source, recordId, revision) {
|
|
1177
1213
|
const value = exactRecord(source.value, 'value', [
|
|
1178
1214
|
'actorMemberId',
|
|
@@ -1230,6 +1266,8 @@ function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
|
1230
1266
|
return protectedClaimOverrideEnvelopeRecord(source, recordId, revision);
|
|
1231
1267
|
case 'manager-responsibility-offer':
|
|
1232
1268
|
return managerResponsibilityOfferRecord(source, recordId, revision);
|
|
1269
|
+
case 'membership-idempotency-tombstone':
|
|
1270
|
+
return membershipIdempotencyTombstoneRecord(source, recordId, revision);
|
|
1233
1271
|
case 'project-membership-recovery':
|
|
1234
1272
|
return membershipRecoveryRecord(source, recordId, revision);
|
|
1235
1273
|
case 'secret-replay-tombstone':
|
|
@@ -1399,6 +1437,7 @@ function validateContinuity(records) {
|
|
|
1399
1437
|
item,
|
|
1400
1438
|
]));
|
|
1401
1439
|
const managerOffers = records.filter((item) => (item.kind === 'manager-responsibility-offer'));
|
|
1440
|
+
const membershipIdempotencyTombstones = records.filter((item) => (item.kind === 'membership-idempotency-tombstone'));
|
|
1402
1441
|
const membershipRecoveries = new Map(records
|
|
1403
1442
|
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1404
1443
|
.map(item => [item.value.operationId, item]));
|
|
@@ -1782,6 +1821,18 @@ function validateContinuity(records) {
|
|
|
1782
1821
|
throw invalidPayload('records');
|
|
1783
1822
|
}
|
|
1784
1823
|
}
|
|
1824
|
+
for (const tombstone of membershipIdempotencyTombstones) {
|
|
1825
|
+
const exactResult = records.some(item => (item.kind === 'idempotency-result'
|
|
1826
|
+
&& item.value.memberId === tombstone.value.actorMemberId
|
|
1827
|
+
&& item.value.operation === tombstone.value.operation
|
|
1828
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1829
|
+
const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
|
|
1830
|
+
&& managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
|
|
1831
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1832
|
+
if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
|
|
1833
|
+
throw invalidPayload('records');
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1785
1836
|
for (const item of records) {
|
|
1786
1837
|
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1787
1838
|
throw invalidPayload('records');
|
|
@@ -2059,6 +2110,9 @@ export function validateCollabProjectBackupCheckpointConsistency(manifest, recor
|
|
|
2059
2110
|
if (item.kind === 'secret-replay-tombstone') {
|
|
2060
2111
|
return Date.parse(item.value.expiredAt) > captureTime;
|
|
2061
2112
|
}
|
|
2113
|
+
if (item.kind === 'membership-idempotency-tombstone') {
|
|
2114
|
+
return Date.parse(item.value.compactedAt) > captureTime;
|
|
2115
|
+
}
|
|
2062
2116
|
if (item.kind === 'project-invitation') {
|
|
2063
2117
|
return captureTime < Date.parse(item.value.createdAt)
|
|
2064
2118
|
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|