@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, and secret-replay 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.
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.0`. Patch releases preserve
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 package version never
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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudian-collab/protocol",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {