@claudian-collab/protocol 3.3.1 → 3.3.2

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,9 @@ 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, 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.
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 Cloud Project-membership operation family. The shared checkpoint container is exactly `checkpoint.json`, `coordination.ndjson`, and `repository.bundle`; authority-transfer and export use wire-visible coordination format v1, and offline backup uses 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
+
42
+ Backup coordination artifacts are limited to 256 MiB of actual UTF-8 bytes, including the final newline. The decoder, encoder, and consistency checker apply this same inclusive limit without rewriting principals into an intermediate format. Valid current-format artifacts at the limit are accepted; an additional byte is rejected.
41
43
 
42
44
  ## Usage
43
45
 
@@ -62,31 +64,14 @@ part of the public surface.
62
64
 
63
65
  Package SemVer, canonical wire version, and Cloud binding version are independent concepts.
64
66
 
65
- - **Package version** (this `package.json`): `3.3.1`. Patch releases preserve
66
- every existing public declaration and runtime behavior baseline. Minor
67
- releases may add backward-compatible API. Removing or changing an existing
68
- declaration, export, runtime behavior, codec, error, limit, ref rule,
69
- operation, or compatibility rule requires a major release. An independently
70
- proven additive control operation may use a package minor release without a
71
- wire-version increase; existing operation declarations and fixtures must stay
72
- exact. Package 3 and wire 6 support exact transfer-claim batches and a
73
- source-pinned target receipt verifier. Package 3.3 adds the Cloud Project-membership
74
- operation family and capability tokens and directly replaces the pre-production
75
- offline backup contract with coordination format v3, without changing
76
- authority-transfer/export format v1, canonical wire v6, or Cloud binding v2.
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
81
- signals wire compatibility by itself.
67
+ - **Package version** (this `package.json`): `3.3.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.
82
68
  - **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `6`. The supported
83
69
  range is exactly `[6, 6]`. This is independent from Cloud binding version `2` and the existing application
84
70
  LAN control version `9`. Any change to an existing envelope, DTO, operation
85
- payload shape, or operation definition is wire-breaking and requires a new
86
- wire-protocol version. A strictly additive operation may remain on the
87
- current wire version when the compatibility classifier proves that every
88
- existing operation contract is unchanged.
89
- - **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `2`. It versions Cloud routes, bounded transfer streams, and capabilities independently from package and wire versions. Binding v1 and wire versions before v6 are unsupported rather than translated or dual-interpreted.
71
+ payload shape, or operation definition requires a new wire-protocol version.
72
+ This includes additive operations: every wire-contract change increases the
73
+ canonical wire version.
74
+ - **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `2`. It versions Cloud routes, bounded transfer streams, and capabilities independently from package and wire versions. Every Cloud binding-contract change increases this version. Binding v1 and wire versions before v6 are unsupported rather than translated or dual-interpreted.
90
75
 
91
76
  ### Compatibility behavior
92
77
 
@@ -105,14 +90,18 @@ Compatibility is tested with executable fixtures in this package
105
90
  (`tests/`), and cross-repository contract fixtures pin client and server to
106
91
  the same behavior.
107
92
 
93
+ The compatibility snapshot also fingerprints implementation tokens. Those fingerprints detect edits but cannot establish whether behavior changed. A reviewed implementation-only refactor or approved compatible defect correction uses `compatibility-review.json`, generated with `npm run check:compatibility -- --base <base-sha> --record-implementation-only-review "<reviewed reason>"` after updating the generated snapshot and characterizing the public seams. The reason distinguishes preserved behavior from any approved correction and its executable evidence. The record binds the exact base and candidate snapshots; the classifier independently rejects any changed public declaration, export, or wire/binding semantic fact. Missing or stale evidence cannot authorize an implementation change, and the record never applies to another version or later edit merely because an earlier change was accepted. This review requirement is general, not a prelaunch or one-release compatibility exception, and it is not an automated proof of behavioral equivalence.
94
+
108
95
  ## Development
109
96
 
110
97
  ```bash
111
98
  npm run build # compile src/ -> dist/ (CommonJS + declarations)
112
99
  npm test # package unit tests
113
100
  npm run check:compatibility
114
- npm run verify:pack # pack the artifact and smoke-test a clean consumer
101
+ npm run release:candidate # build and verify the reviewed release artifact
102
+ npm run verify:pack # smoke-test that exact artifact in a clean consumer
115
103
  ```
116
104
 
117
105
  `dist/` is generated output and is never committed. The packed artifact is
118
106
  reproducible from source: `npm pack` rebuilds via `prepack`.
107
+ The full verification gate constructs one release candidate and uses those same reviewed tarball bytes for clean-consumer smoke tests and publication.
@@ -208,65 +208,19 @@ function absoluteTargetUrl(source) {
208
208
  function principal(source, field) {
209
209
  return token(source, field, value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
210
210
  }
211
- function sanitizeBackupBasePrincipalRecord(item) {
212
- const source = record(item, 'record');
213
- const value = record(source.value, 'value');
214
- if (source.kind === 'principal-binding') {
215
- return {
216
- ...source,
217
- value: { ...value, principalId: `principal_${String(value.memberId)}` },
218
- };
219
- }
220
- if (source.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
221
- return {
222
- ...source,
223
- value: {
224
- ...value,
225
- acknowledgements: value.acknowledgements.map(item => ({
226
- ...record(item, 'acknowledgement'),
227
- principalId: `principal_${String(record(item, 'acknowledgement').memberId)}`,
228
- })),
229
- },
230
- };
231
- }
232
- return item;
233
- }
234
- function restoreBackupBasePrincipals(source, base) {
235
- if (base.kind === 'principal-binding') {
236
- const value = exactRecord(source.value, 'value', [
237
- 'boundAt', 'memberId', 'principalId', 'projectId',
238
- ]);
239
- return {
240
- ...base,
241
- value: { ...base.value, principalId: principal(value, 'principalId') },
242
- };
243
- }
211
+ function validateBackupBasePrincipals(base) {
212
+ if (base.kind === 'principal-binding')
213
+ principal(base.value, 'principalId');
244
214
  if (base.kind === 'terminal-responder') {
245
- const value = exactRecord(source.value, 'value', [
246
- 'acknowledgements',
247
- 'eligibleMemberIds',
248
- 'expiresAt',
249
- 'operation',
250
- 'operationId',
251
- 'projectId',
252
- 'responseJson',
253
- ]);
254
- const sourceAcknowledgements = value.acknowledgements;
255
- if (!Array.isArray(sourceAcknowledgements)
256
- || sourceAcknowledgements.length !== base.value.acknowledgements.length)
257
- throw invalidPayload('acknowledgements');
258
- const acknowledgements = base.value.acknowledgements.map((item, index) => {
259
- const original = exactRecord(sourceAcknowledgements[index], 'acknowledgement', [
260
- 'acknowledgedAt', 'memberId', 'principalId',
261
- ]);
262
- return { ...item, principalId: principal(original, 'principalId') };
263
- });
264
- if (new Set(acknowledgements.map(item => item.principalId)).size !== acknowledgements.length) {
265
- throw invalidPayload('acknowledgements');
215
+ for (const acknowledgement of base.value.acknowledgements) {
216
+ if (acknowledgement.principalId === undefined)
217
+ throw invalidPayload('acknowledgement');
218
+ principal(acknowledgement, 'principalId');
266
219
  }
267
- return { ...base, value: { ...base.value, acknowledgements } };
220
+ if (new Set(base.value.acknowledgements.map(item => item.principalId)).size
221
+ !== base.value.acknowledgements.length)
222
+ throw invalidPayload('acknowledgements');
268
223
  }
269
- return base;
270
224
  }
271
225
  function repositoryPublicationRef(value) {
272
226
  const source = exactRecord(value, 'ref', ['name', 'oid']);
@@ -2010,55 +1964,18 @@ function validateContinuity(records) {
2010
1964
  }
2011
1965
  }
2012
1966
  }
2013
- function manifestWithFormatOne(value) {
1967
+ function decodeCollabProjectBackupCheckpointManifest(value) {
2014
1968
  const source = record(value, 'manifest');
2015
1969
  if (source.profile !== 'backup'
2016
1970
  || source.coordinationFormatVersion !== exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION)
2017
1971
  throw invalidPayload('manifest');
2018
- return (0, CollabProjectCheckpoint_1.decodeCollabProjectCheckpointManifest)({
2019
- ...source,
2020
- coordinationFormatVersion: CollabProjectCheckpoint_1.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
2021
- });
2022
- }
2023
- function decodeCollabProjectBackupCheckpointManifest(value) {
2024
- const decoded = manifestWithFormatOne(value);
2025
- return {
2026
- artifacts: decoded.artifacts,
2027
- coordinationFormatVersion: exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION,
2028
- createdAt: decoded.createdAt,
2029
- expectedMainOid: decoded.expectedMainOid,
2030
- gitObjectFormat: decoded.gitObjectFormat,
2031
- manifestSchemaVersion: decoded.manifestSchemaVersion,
2032
- manifestSha256: decoded.manifestSha256,
2033
- operationId: decoded.operationId,
2034
- profile: 'backup',
2035
- projectId: decoded.projectId,
2036
- protocolVersion: decoded.protocolVersion,
2037
- refs: decoded.refs,
2038
- sourceAuthority: decoded.sourceAuthority,
2039
- targetAuthority: decoded.targetAuthority,
2040
- };
1972
+ return (0, CollabProjectCheckpoint_1.decodeCheckpointManifestFields)(source, exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION);
2041
1973
  }
2042
1974
  function encodeCollabProjectBackupCheckpointManifestCanonicalJson(manifest) {
2043
1975
  return JSON.stringify(decodeCollabProjectBackupCheckpointManifest(manifest));
2044
1976
  }
2045
1977
  function encodeCollabProjectBackupCheckpointManifestDigestInput(manifest) {
2046
- const decoded = decodeCollabProjectBackupCheckpointManifest(manifest);
2047
- return JSON.stringify({
2048
- artifacts: decoded.artifacts,
2049
- coordinationFormatVersion: decoded.coordinationFormatVersion,
2050
- createdAt: decoded.createdAt,
2051
- expectedMainOid: decoded.expectedMainOid,
2052
- gitObjectFormat: decoded.gitObjectFormat,
2053
- manifestSchemaVersion: decoded.manifestSchemaVersion,
2054
- operationId: decoded.operationId,
2055
- profile: decoded.profile,
2056
- projectId: decoded.projectId,
2057
- protocolVersion: decoded.protocolVersion,
2058
- refs: decoded.refs,
2059
- sourceAuthority: decoded.sourceAuthority,
2060
- targetAuthority: decoded.targetAuthority,
2061
- });
1978
+ return (0, CollabProjectCheckpoint_1.encodeCheckpointManifestDigestInput)(decodeCollabProjectBackupCheckpointManifest(manifest));
2062
1979
  }
2063
1980
  function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
2064
1981
  if (typeof value !== 'string'
@@ -2078,22 +1995,43 @@ function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
2078
1995
  }
2079
1996
  if (JSON.stringify(item) !== line)
2080
1997
  throw invalidPayload('coordination');
2081
- return { envelope: recordEnvelope(item), item };
1998
+ return {
1999
+ envelope: recordEnvelope(item),
2000
+ item,
2001
+ base: undefined,
2002
+ };
2082
2003
  });
2083
2004
  const baseItems = parsed.filter(({ envelope }) => (!CONTINUITY_KIND_SET.has(envelope.kind) && envelope.kind !== 'idempotency-result'));
2084
- const baseDecoded = (0, CollabProjectCheckpoint_1.decodeCollabProjectCheckpointCoordinationNdjson)(baseItems.map(({ item }) => JSON.stringify(sanitizeBackupBasePrincipalRecord(item))).join('\n') + '\n', 'backup');
2085
- const baseByIdentity = new Map(baseDecoded.map(item => [`${item.kind}\0${item.recordId}`, item]));
2086
- const decoded = parsed.map(({ envelope }) => {
2005
+ for (const { envelope } of baseItems) {
2006
+ const value = record(envelope.source.value, 'value');
2007
+ if (envelope.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
2008
+ for (const acknowledgement of value.acknowledgements)
2009
+ record(acknowledgement, 'acknowledgement');
2010
+ }
2011
+ }
2012
+ const baseRecords = [];
2013
+ for (const entry of baseItems) {
2014
+ entry.base = (0, CollabProjectCheckpoint_1.decodeCheckpointRecord)(entry.item, 'deferred');
2015
+ if (JSON.stringify(entry.base) !== JSON.stringify(entry.item)
2016
+ || (entry.base.kind === 'principal-binding' && entry.base.value.principalId === undefined)) {
2017
+ throw invalidPayload('coordination');
2018
+ }
2019
+ baseRecords.push(entry.base);
2020
+ }
2021
+ if (baseRecords.length === 0)
2022
+ throw invalidPayload('coordination');
2023
+ (0, CollabProjectCheckpoint_1.validateCheckpointRecordSequence)(baseRecords, 'backup', 'member');
2024
+ const decoded = parsed.map(({ envelope, base }) => {
2087
2025
  if (envelope.kind === 'idempotency-result') {
2088
2026
  return idempotencyResultRecord(envelope.source, envelope.recordId, envelope.revision);
2089
2027
  }
2090
2028
  if (CONTINUITY_KIND_SET.has(envelope.kind)) {
2091
2029
  return decodeContinuityRecord(envelope.kind, envelope.source, envelope.recordId, envelope.revision);
2092
2030
  }
2093
- const base = baseByIdentity.get(`${envelope.kind}\0${envelope.recordId}`);
2094
2031
  if (base === undefined)
2095
2032
  throw invalidPayload('records');
2096
- return restoreBackupBasePrincipals(envelope.source, base);
2033
+ validateBackupBasePrincipals(base);
2034
+ return base;
2097
2035
  });
2098
2036
  if (decoded.some((item, index) => JSON.stringify(item) !== lines[index])) {
2099
2037
  throw invalidPayload('coordination');
@@ -2157,9 +2095,7 @@ function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
2157
2095
  }))
2158
2096
  throw invalidPayload('records');
2159
2097
  const baseRecords = decodedRecords.filter(item => (!CONTINUITY_KIND_SET.has(item.kind) && item.kind !== 'idempotency-result'));
2160
- (0, CollabProjectCheckpoint_1.validateCollabProjectCheckpointConsistency)((0, CollabProjectCheckpoint_1.decodeCollabProjectCheckpointManifest)({
2161
- ...decodedManifest,
2162
- coordinationFormatVersion: CollabProjectCheckpoint_1.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
2163
- }), baseRecords);
2098
+ (0, CollabProjectCheckpoint_1.validateCheckpointRecordSequence)(baseRecords, 'backup');
2099
+ (0, CollabProjectCheckpoint_1.validateCheckpointManifestConsistency)(decodedManifest, baseRecords);
2164
2100
  return records;
2165
2101
  }
@@ -265,11 +265,36 @@ export type CollabCheckpointAuthorityVolumePairRecord = CollabCheckpointRecordBa
265
265
  }>;
266
266
  export type CollabCheckpointPortableRecord = CollabCheckpointProjectRecord | CollabCheckpointMemberRecord | CollabCheckpointRequestRecord | CollabCheckpointRequestCommentRecord | CollabCheckpointTicketRecord | CollabCheckpointTicketCommentRecord | CollabCheckpointTicketRelationRecord | CollabCheckpointTicketMentionRecord;
267
267
  export type CollabCheckpointBackupRecord = CollabCheckpointPortableRecord | CollabCheckpointCloudEventRecord | CollabCheckpointCloudEventCursorRecord | CollabCheckpointIdempotencyResultRecord | CollabCheckpointPrincipalBindingRecord | CollabCheckpointRepositoryPlacementRecord | CollabCheckpointLifecycleStateRecord | CollabCheckpointTerminalResponderRecord | CollabCheckpointProtectedClaimEnvelopeRecord | CollabCheckpointTombstoneRecord | CollabCheckpointSchemaCatalogRecord | CollabCheckpointServerCompatibilityRecord | CollabCheckpointAuthorityVolumePairRecord;
268
+ type ParsedCheckpointPrincipalBindingRecord = CollabCheckpointRecordBase<'principal-binding', Omit<CollabCheckpointPrincipalBindingRecord['value'], 'principalId'> & {
269
+ readonly principalId: unknown;
270
+ }>;
271
+ type ParsedCheckpointTerminalResponderRecord = CollabCheckpointRecordBase<'terminal-responder', Omit<CollabCheckpointTerminalResponderRecord['value'], 'acknowledgements'> & {
272
+ readonly acknowledgements: readonly (Omit<CollabCheckpointTerminalAcknowledgement, 'principalId'> & {
273
+ readonly principalId: unknown;
274
+ })[];
275
+ }>;
276
+ /** @internal Format v3 validates principal fields after the shared base-record phase. */
277
+ export type ParsedCheckpointRecord = Exclude<CollabCheckpointBackupRecord, CollabCheckpointPrincipalBindingRecord | CollabCheckpointTerminalResponderRecord> | ParsedCheckpointPrincipalBindingRecord | ParsedCheckpointTerminalResponderRecord;
278
+ type CheckpointManifestFields<CoordinationVersion extends number> = Omit<CollabProjectCheckpointManifest, 'coordinationFormatVersion'> & {
279
+ readonly coordinationFormatVersion: CoordinationVersion;
280
+ };
281
+ /** @internal Shared field validation; public entry points select their own format. */
282
+ export declare function decodeCheckpointManifestFields<CoordinationVersion extends number>(value: unknown, coordinationFormatVersion: CoordinationVersion): CheckpointManifestFields<CoordinationVersion>;
268
283
  export declare function decodeCollabProjectCheckpointManifest(value: unknown): CollabProjectCheckpointManifest;
269
284
  export declare function encodeCollabProjectCheckpointManifestCanonicalJson(manifest: CollabProjectCheckpointManifest): string;
270
285
  export declare function encodeCollabProjectCheckpointManifestDigestInput(manifest: CollabProjectCheckpointManifest): string;
286
+ /** @internal Encode already validated fields without changing their format. */
287
+ export declare function encodeCheckpointManifestDigestInput(decoded: CheckpointManifestFields<number>): string;
271
288
  export declare function encodeCollabProtectedClaimAssociatedData(associatedData: CollabProtectedClaimAssociatedData): string;
289
+ /** @internal Public v1 decoding validates principals in each record. */
290
+ export declare function decodeCheckpointRecord(value: unknown): CollabCheckpointBackupRecord;
291
+ /** @internal Offline v3 decoding validates principals after the base-record sequence. */
292
+ export declare function decodeCheckpointRecord(value: unknown, principalPhase: 'deferred'): ParsedCheckpointRecord;
293
+ /** @internal V3 decoding checks terminal principals separately; consistency binds current principals. */
294
+ export declare function validateCheckpointRecordSequence(records: readonly ParsedCheckpointRecord[], profile: CollabCheckpointProfile, acknowledgementBinding?: 'current-principal' | 'member'): void;
272
295
  export declare function decodeCollabProjectCheckpointCoordinationNdjson(value: string, profile: CollabCheckpointProfile): readonly CollabCheckpointBackupRecord[];
273
296
  export declare function encodeCollabProjectCheckpointCoordinationNdjson(records: readonly CollabCheckpointBackupRecord[], profile: CollabCheckpointProfile): string;
274
297
  export declare function validateCollabProjectCheckpointConsistency(manifest: CollabProjectCheckpointManifest, records: readonly CollabCheckpointBackupRecord[]): readonly CollabCheckpointBackupRecord[];
298
+ /** @internal Cross-artifact checks operate on validated fields, not another format. */
299
+ export declare function validateCheckpointManifestConsistency(decodedManifest: CheckpointManifestFields<number>, records: readonly CollabCheckpointBackupRecord[]): readonly CollabCheckpointBackupRecord[];
275
300
  export {};
@@ -1,13 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS = exports.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS = exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS = exports.COLLAB_PROJECT_CHECKPOINT_ARTIFACTS = exports.COLLAB_CHECKPOINT_PROFILES = exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS = exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION = exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION = exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION = void 0;
4
+ exports.decodeCheckpointManifestFields = decodeCheckpointManifestFields;
4
5
  exports.decodeCollabProjectCheckpointManifest = decodeCollabProjectCheckpointManifest;
5
6
  exports.encodeCollabProjectCheckpointManifestCanonicalJson = encodeCollabProjectCheckpointManifestCanonicalJson;
6
7
  exports.encodeCollabProjectCheckpointManifestDigestInput = encodeCollabProjectCheckpointManifestDigestInput;
8
+ exports.encodeCheckpointManifestDigestInput = encodeCheckpointManifestDigestInput;
7
9
  exports.encodeCollabProtectedClaimAssociatedData = encodeCollabProtectedClaimAssociatedData;
10
+ exports.decodeCheckpointRecord = decodeCheckpointRecord;
11
+ exports.validateCheckpointRecordSequence = validateCheckpointRecordSequence;
8
12
  exports.decodeCollabProjectCheckpointCoordinationNdjson = decodeCollabProjectCheckpointCoordinationNdjson;
9
13
  exports.encodeCollabProjectCheckpointCoordinationNdjson = encodeCollabProjectCheckpointCoordinationNdjson;
10
14
  exports.validateCollabProjectCheckpointConsistency = validateCollabProjectCheckpointConsistency;
15
+ exports.validateCheckpointManifestConsistency = validateCheckpointManifestConsistency;
11
16
  const CollabAuthorityTransfer_1 = require("./CollabAuthorityTransfer");
12
17
  const CollabCloudProjectEvent_1 = require("./CollabCloudProjectEvent");
13
18
  const CollabConstants_1 = require("./CollabConstants");
@@ -237,7 +242,7 @@ function gitRefs(value, expectedMainOid) {
237
242
  throw invalidPayload('refs');
238
243
  return Object.freeze(decoded);
239
244
  }
240
- function manifestObject(source, includeManifestSha256) {
245
+ function manifestObject(source, coordinationFormatVersion) {
241
246
  const profile = literal(source, 'profile', exports.COLLAB_CHECKPOINT_PROFILES);
242
247
  const expectedMainOid = token(source, 'expectedMainOid', CollabValidation_1.isCollabGitOid);
243
248
  const gitObjectFormat = literal(source, 'gitObjectFormat', ['sha1', 'sha256']);
@@ -273,15 +278,13 @@ function manifestObject(source, includeManifestSha256) {
273
278
  sourceAuthority,
274
279
  targetAuthority,
275
280
  };
276
- if (common.coordinationFormatVersion !== exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION
281
+ if (common.coordinationFormatVersion !== coordinationFormatVersion
277
282
  || common.manifestSchemaVersion !== exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION
278
283
  || common.protocolVersion !== CollabConstants_1.COLLAB_PROTOCOL_VERSION)
279
284
  throw invalidPayload('manifest');
280
- if (!includeManifestSha256)
281
- return common;
282
285
  return {
283
286
  artifacts: common.artifacts,
284
- coordinationFormatVersion: exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
287
+ coordinationFormatVersion,
285
288
  createdAt: common.createdAt,
286
289
  expectedMainOid: common.expectedMainOid,
287
290
  gitObjectFormat: common.gitObjectFormat,
@@ -296,7 +299,8 @@ function manifestObject(source, includeManifestSha256) {
296
299
  targetAuthority: common.targetAuthority,
297
300
  };
298
301
  }
299
- function decodeCollabProjectCheckpointManifest(value) {
302
+ /** @internal Shared field validation; public entry points select their own format. */
303
+ function decodeCheckpointManifestFields(value, coordinationFormatVersion) {
300
304
  const source = exactRecord(value, 'manifest', [
301
305
  'artifacts',
302
306
  'coordinationFormatVersion',
@@ -313,18 +317,24 @@ function decodeCollabProjectCheckpointManifest(value) {
313
317
  'sourceAuthority',
314
318
  'targetAuthority',
315
319
  ]);
316
- const decoded = manifestObject(source, true);
320
+ const decoded = manifestObject(source, coordinationFormatVersion);
317
321
  const encoded = JSON.stringify(decoded);
318
322
  if (!(0, CollabValidation_1.hasUtf8ByteLengthAtMost)(encoded, exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxManifestBytes)) {
319
323
  throw invalidPayload('manifest');
320
324
  }
321
325
  return decoded;
322
326
  }
327
+ function decodeCollabProjectCheckpointManifest(value) {
328
+ return decodeCheckpointManifestFields(value, exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION);
329
+ }
323
330
  function encodeCollabProjectCheckpointManifestCanonicalJson(manifest) {
324
331
  return JSON.stringify(decodeCollabProjectCheckpointManifest(manifest));
325
332
  }
326
333
  function encodeCollabProjectCheckpointManifestDigestInput(manifest) {
327
- const decoded = decodeCollabProjectCheckpointManifest(manifest);
334
+ return encodeCheckpointManifestDigestInput(decodeCollabProjectCheckpointManifest(manifest));
335
+ }
336
+ /** @internal Encode already validated fields without changing their format. */
337
+ function encodeCheckpointManifestDigestInput(decoded) {
328
338
  const source = {
329
339
  artifacts: decoded.artifacts,
330
340
  coordinationFormatVersion: decoded.coordinationFormatVersion,
@@ -707,13 +717,13 @@ function idempotencyResultRecord(source, recordId, revision) {
707
717
  },
708
718
  };
709
719
  }
710
- function principalBindingRecord(source, recordId, revision) {
711
- const value = exactRecord(source.value, 'value', [
720
+ function principalBindingRecord(source, recordId, revision, deferPrincipals) {
721
+ const value = principalRecord(source.value, 'value', [
712
722
  'boundAt',
713
723
  'memberId',
714
724
  'principalId',
715
725
  'projectId',
716
- ]);
726
+ ], deferPrincipals);
717
727
  const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
718
728
  if (recordId !== memberId)
719
729
  throw invalidPayload('recordId');
@@ -724,7 +734,7 @@ function principalBindingRecord(source, recordId, revision) {
724
734
  value: {
725
735
  boundAt: timestamp(value, 'boundAt'),
726
736
  memberId,
727
- principalId: token(value, 'principalId'),
737
+ principalId: deferPrincipals ? value.principalId : token(value, 'principalId'),
728
738
  projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
729
739
  },
730
740
  };
@@ -858,7 +868,13 @@ function lifecycleStateRecord(source, recordId, revision) {
858
868
  value: internalState,
859
869
  };
860
870
  }
861
- function terminalResponderRecord(source, recordId, revision) {
871
+ function principalRecord(value, field, keys, deferPrincipals) {
872
+ const source = record(value, field);
873
+ return exactRecord(source, field, deferPrincipals && !Object.hasOwn(source, 'principalId')
874
+ ? keys.filter(key => key !== 'principalId')
875
+ : keys);
876
+ }
877
+ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
862
878
  const value = exactRecord(source.value, 'value', [
863
879
  'acknowledgements',
864
880
  'eligibleMemberIds',
@@ -910,22 +926,24 @@ function terminalResponderRecord(source, recordId, revision) {
910
926
  if (!Array.isArray(value.acknowledgements))
911
927
  throw invalidPayload('acknowledgements');
912
928
  const acknowledgements = value.acknowledgements.map((item) => {
913
- const acknowledgement = exactRecord(item, 'acknowledgement', [
929
+ const acknowledgement = principalRecord(item, 'acknowledgement', [
914
930
  'acknowledgedAt',
915
931
  'memberId',
916
932
  'principalId',
917
- ]);
933
+ ], deferPrincipals);
918
934
  return {
919
935
  acknowledgedAt: timestamp(acknowledgement, 'acknowledgedAt'),
920
936
  memberId: token(acknowledgement, 'memberId', CollabValidation_1.isCollabMemberId),
921
- principalId: token(acknowledgement, 'principalId'),
937
+ principalId: deferPrincipals
938
+ ? acknowledgement.principalId
939
+ : token(acknowledgement, 'principalId'),
922
940
  };
923
941
  });
924
942
  const acknowledgementPrincipals = new Set();
925
943
  acknowledgements.forEach((item, index) => {
926
944
  if (!eligibleMemberIds.includes(item.memberId)
927
945
  || (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
928
- || acknowledgementPrincipals.has(item.principalId))
946
+ || (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
929
947
  throw invalidPayload('acknowledgements');
930
948
  acknowledgementPrincipals.add(item.principalId);
931
949
  });
@@ -934,7 +952,7 @@ function terminalResponderRecord(source, recordId, revision) {
934
952
  recordId,
935
953
  revision,
936
954
  value: {
937
- acknowledgements: Object.freeze(acknowledgements),
955
+ acknowledgements,
938
956
  eligibleMemberIds: Object.freeze(eligibleMemberIds),
939
957
  expiresAt,
940
958
  operation,
@@ -1107,8 +1125,9 @@ function authorityVolumePairRecord(source, recordId, revision) {
1107
1125
  },
1108
1126
  };
1109
1127
  }
1110
- function decodeCheckpointRecord(value) {
1128
+ function decodeCheckpointRecord(value, principalPhase) {
1111
1129
  const { kind, recordId, revision, source } = recordEnvelope(value);
1130
+ const deferPrincipals = principalPhase === 'deferred';
1112
1131
  switch (kind) {
1113
1132
  case 'project': return projectRecord(source, recordId, revision);
1114
1133
  case 'member': return memberRecord(source, recordId, revision);
@@ -1121,10 +1140,12 @@ function decodeCheckpointRecord(value) {
1121
1140
  case 'cloud-event': return cloudEventRecord(source, recordId, revision);
1122
1141
  case 'cloud-event-cursor': return cloudEventCursorRecord(source, recordId, revision);
1123
1142
  case 'idempotency-result': return idempotencyResultRecord(source, recordId, revision);
1124
- case 'principal-binding': return principalBindingRecord(source, recordId, revision);
1143
+ case 'principal-binding':
1144
+ return principalBindingRecord(source, recordId, revision, deferPrincipals);
1125
1145
  case 'repository-placement': return repositoryPlacementRecord(source, recordId, revision);
1126
1146
  case 'lifecycle-state': return lifecycleStateRecord(source, recordId, revision);
1127
- case 'terminal-responder': return terminalResponderRecord(source, recordId, revision);
1147
+ case 'terminal-responder':
1148
+ return terminalResponderRecord(source, recordId, revision, deferPrincipals);
1128
1149
  case 'protected-claim-envelope':
1129
1150
  return protectedClaimEnvelopeRecord(source, recordId, revision);
1130
1151
  case 'tombstone': return tombstoneRecord(source, recordId, revision);
@@ -1140,7 +1161,8 @@ function compareRecords(left, right) {
1140
1161
  const kindOrder = recordKindOrder(left.kind) - recordKindOrder(right.kind);
1141
1162
  return kindOrder !== 0 ? kindOrder : left.recordId.localeCompare(right.recordId, 'en-US');
1142
1163
  }
1143
- function validateRecordSequence(records, profile) {
1164
+ /** @internal V3 decoding checks terminal principals separately; consistency binds current principals. */
1165
+ function validateCheckpointRecordSequence(records, profile, acknowledgementBinding = 'current-principal') {
1144
1166
  if (records.length === 0 || records[0].kind !== 'project')
1145
1167
  throw invalidPayload('records');
1146
1168
  const projectId = records[0].recordId;
@@ -1243,7 +1265,9 @@ function validateRecordSequence(records, profile) {
1243
1265
  throw invalidPayload('records');
1244
1266
  }
1245
1267
  if (item.kind === 'terminal-responder' && (item.value.eligibleMemberIds.some(memberId => !members.has(memberId))
1246
- || item.value.acknowledgements.some(acknowledgement => (principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId))))
1268
+ || item.value.acknowledgements.some(acknowledgement => (acknowledgementBinding === 'current-principal'
1269
+ ? principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId
1270
+ : !principalBindings.has(acknowledgement.memberId)))))
1247
1271
  throw invalidPayload('records');
1248
1272
  }
1249
1273
  if (profile === 'backup') {
@@ -1270,11 +1294,13 @@ function decodeCollabProjectCheckpointCoordinationNdjson(value, profile) {
1270
1294
  throw invalidPayload('coordination');
1271
1295
  }
1272
1296
  const result = decodeCheckpointRecord(parsed);
1297
+ if (result.kind === 'terminal-responder')
1298
+ Object.freeze(result.value.acknowledgements);
1273
1299
  if (JSON.stringify(result) !== line)
1274
1300
  throw invalidPayload('coordination');
1275
1301
  return result;
1276
1302
  });
1277
- validateRecordSequence(decoded, profile);
1303
+ validateCheckpointRecordSequence(decoded, profile);
1278
1304
  return Object.freeze(decoded);
1279
1305
  }
1280
1306
  function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
@@ -1306,7 +1332,11 @@ function checkpointRecordGitOids(checkpointRecord) {
1306
1332
  }
1307
1333
  function validateCollabProjectCheckpointConsistency(manifest, records) {
1308
1334
  const decodedManifest = decodeCollabProjectCheckpointManifest(manifest);
1309
- validateRecordSequence(records, decodedManifest.profile);
1335
+ validateCheckpointRecordSequence(records, decodedManifest.profile);
1336
+ return validateCheckpointManifestConsistency(decodedManifest, records);
1337
+ }
1338
+ /** @internal Cross-artifact checks operate on validated fields, not another format. */
1339
+ function validateCheckpointManifestConsistency(decodedManifest, records) {
1310
1340
  const project = records[0];
1311
1341
  if (project.kind !== 'project'
1312
1342
  || project.value.projectId !== decodedManifest.projectId
@@ -2,7 +2,7 @@ import { COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES, COLLAB_CLOUD_TO_LAN_TRAN
2
2
  import { COLLAB_MAIN_REF, COLLAB_MEMBER_REF_PREFIX, } from './CollabConstants.mjs';
3
3
  import { collabControlOperationCodec, } from './CollabControlOperationCodecs.mjs';
4
4
  import { CollabError } from './CollabError.mjs';
5
- import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS, COLLAB_PROJECT_COORDINATION_FORMAT_VERSION, decodeCollabProjectCheckpointCoordinationNdjson, decodeCollabProjectCheckpointManifest, validateCollabProjectCheckpointConsistency, } from './CollabProjectCheckpoint.mjs';
5
+ import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS, decodeCheckpointManifestFields, decodeCheckpointRecord, encodeCheckpointManifestDigestInput, validateCheckpointManifestConsistency, validateCheckpointRecordSequence, } from './CollabProjectCheckpoint.mjs';
6
6
  import { COLLAB_PROJECT_MEMBERSHIP_LIMITS, } from './CollabProjectMembership.mjs';
7
7
  import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
8
8
  export const COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE = 'pre-production-replaceable';
@@ -198,65 +198,19 @@ function absoluteTargetUrl(source) {
198
198
  function principal(source, field) {
199
199
  return token(source, field, value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
200
200
  }
201
- function sanitizeBackupBasePrincipalRecord(item) {
202
- const source = record(item, 'record');
203
- const value = record(source.value, 'value');
204
- if (source.kind === 'principal-binding') {
205
- return {
206
- ...source,
207
- value: { ...value, principalId: `principal_${String(value.memberId)}` },
208
- };
209
- }
210
- if (source.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
211
- return {
212
- ...source,
213
- value: {
214
- ...value,
215
- acknowledgements: value.acknowledgements.map(item => ({
216
- ...record(item, 'acknowledgement'),
217
- principalId: `principal_${String(record(item, 'acknowledgement').memberId)}`,
218
- })),
219
- },
220
- };
221
- }
222
- return item;
223
- }
224
- function restoreBackupBasePrincipals(source, base) {
225
- if (base.kind === 'principal-binding') {
226
- const value = exactRecord(source.value, 'value', [
227
- 'boundAt', 'memberId', 'principalId', 'projectId',
228
- ]);
229
- return {
230
- ...base,
231
- value: { ...base.value, principalId: principal(value, 'principalId') },
232
- };
233
- }
201
+ function validateBackupBasePrincipals(base) {
202
+ if (base.kind === 'principal-binding')
203
+ principal(base.value, 'principalId');
234
204
  if (base.kind === 'terminal-responder') {
235
- const value = exactRecord(source.value, 'value', [
236
- 'acknowledgements',
237
- 'eligibleMemberIds',
238
- 'expiresAt',
239
- 'operation',
240
- 'operationId',
241
- 'projectId',
242
- 'responseJson',
243
- ]);
244
- const sourceAcknowledgements = value.acknowledgements;
245
- if (!Array.isArray(sourceAcknowledgements)
246
- || sourceAcknowledgements.length !== base.value.acknowledgements.length)
247
- throw invalidPayload('acknowledgements');
248
- const acknowledgements = base.value.acknowledgements.map((item, index) => {
249
- const original = exactRecord(sourceAcknowledgements[index], 'acknowledgement', [
250
- 'acknowledgedAt', 'memberId', 'principalId',
251
- ]);
252
- return { ...item, principalId: principal(original, 'principalId') };
253
- });
254
- if (new Set(acknowledgements.map(item => item.principalId)).size !== acknowledgements.length) {
255
- throw invalidPayload('acknowledgements');
205
+ for (const acknowledgement of base.value.acknowledgements) {
206
+ if (acknowledgement.principalId === undefined)
207
+ throw invalidPayload('acknowledgement');
208
+ principal(acknowledgement, 'principalId');
256
209
  }
257
- return { ...base, value: { ...base.value, acknowledgements } };
210
+ if (new Set(base.value.acknowledgements.map(item => item.principalId)).size
211
+ !== base.value.acknowledgements.length)
212
+ throw invalidPayload('acknowledgements');
258
213
  }
259
- return base;
260
214
  }
261
215
  function repositoryPublicationRef(value) {
262
216
  const source = exactRecord(value, 'ref', ['name', 'oid']);
@@ -2000,55 +1954,18 @@ function validateContinuity(records) {
2000
1954
  }
2001
1955
  }
2002
1956
  }
2003
- function manifestWithFormatOne(value) {
1957
+ export function decodeCollabProjectBackupCheckpointManifest(value) {
2004
1958
  const source = record(value, 'manifest');
2005
1959
  if (source.profile !== 'backup'
2006
1960
  || source.coordinationFormatVersion !== COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION)
2007
1961
  throw invalidPayload('manifest');
2008
- return decodeCollabProjectCheckpointManifest({
2009
- ...source,
2010
- coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
2011
- });
2012
- }
2013
- export function decodeCollabProjectBackupCheckpointManifest(value) {
2014
- const decoded = manifestWithFormatOne(value);
2015
- return {
2016
- artifacts: decoded.artifacts,
2017
- coordinationFormatVersion: COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION,
2018
- createdAt: decoded.createdAt,
2019
- expectedMainOid: decoded.expectedMainOid,
2020
- gitObjectFormat: decoded.gitObjectFormat,
2021
- manifestSchemaVersion: decoded.manifestSchemaVersion,
2022
- manifestSha256: decoded.manifestSha256,
2023
- operationId: decoded.operationId,
2024
- profile: 'backup',
2025
- projectId: decoded.projectId,
2026
- protocolVersion: decoded.protocolVersion,
2027
- refs: decoded.refs,
2028
- sourceAuthority: decoded.sourceAuthority,
2029
- targetAuthority: decoded.targetAuthority,
2030
- };
1962
+ return decodeCheckpointManifestFields(source, COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION);
2031
1963
  }
2032
1964
  export function encodeCollabProjectBackupCheckpointManifestCanonicalJson(manifest) {
2033
1965
  return JSON.stringify(decodeCollabProjectBackupCheckpointManifest(manifest));
2034
1966
  }
2035
1967
  export function encodeCollabProjectBackupCheckpointManifestDigestInput(manifest) {
2036
- const decoded = decodeCollabProjectBackupCheckpointManifest(manifest);
2037
- return JSON.stringify({
2038
- artifacts: decoded.artifacts,
2039
- coordinationFormatVersion: decoded.coordinationFormatVersion,
2040
- createdAt: decoded.createdAt,
2041
- expectedMainOid: decoded.expectedMainOid,
2042
- gitObjectFormat: decoded.gitObjectFormat,
2043
- manifestSchemaVersion: decoded.manifestSchemaVersion,
2044
- operationId: decoded.operationId,
2045
- profile: decoded.profile,
2046
- projectId: decoded.projectId,
2047
- protocolVersion: decoded.protocolVersion,
2048
- refs: decoded.refs,
2049
- sourceAuthority: decoded.sourceAuthority,
2050
- targetAuthority: decoded.targetAuthority,
2051
- });
1968
+ return encodeCheckpointManifestDigestInput(decodeCollabProjectBackupCheckpointManifest(manifest));
2052
1969
  }
2053
1970
  export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
2054
1971
  if (typeof value !== 'string'
@@ -2068,22 +1985,43 @@ export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
2068
1985
  }
2069
1986
  if (JSON.stringify(item) !== line)
2070
1987
  throw invalidPayload('coordination');
2071
- return { envelope: recordEnvelope(item), item };
1988
+ return {
1989
+ envelope: recordEnvelope(item),
1990
+ item,
1991
+ base: undefined,
1992
+ };
2072
1993
  });
2073
1994
  const baseItems = parsed.filter(({ envelope }) => (!CONTINUITY_KIND_SET.has(envelope.kind) && envelope.kind !== 'idempotency-result'));
2074
- const baseDecoded = decodeCollabProjectCheckpointCoordinationNdjson(baseItems.map(({ item }) => JSON.stringify(sanitizeBackupBasePrincipalRecord(item))).join('\n') + '\n', 'backup');
2075
- const baseByIdentity = new Map(baseDecoded.map(item => [`${item.kind}\0${item.recordId}`, item]));
2076
- const decoded = parsed.map(({ envelope }) => {
1995
+ for (const { envelope } of baseItems) {
1996
+ const value = record(envelope.source.value, 'value');
1997
+ if (envelope.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
1998
+ for (const acknowledgement of value.acknowledgements)
1999
+ record(acknowledgement, 'acknowledgement');
2000
+ }
2001
+ }
2002
+ const baseRecords = [];
2003
+ for (const entry of baseItems) {
2004
+ entry.base = decodeCheckpointRecord(entry.item, 'deferred');
2005
+ if (JSON.stringify(entry.base) !== JSON.stringify(entry.item)
2006
+ || (entry.base.kind === 'principal-binding' && entry.base.value.principalId === undefined)) {
2007
+ throw invalidPayload('coordination');
2008
+ }
2009
+ baseRecords.push(entry.base);
2010
+ }
2011
+ if (baseRecords.length === 0)
2012
+ throw invalidPayload('coordination');
2013
+ validateCheckpointRecordSequence(baseRecords, 'backup', 'member');
2014
+ const decoded = parsed.map(({ envelope, base }) => {
2077
2015
  if (envelope.kind === 'idempotency-result') {
2078
2016
  return idempotencyResultRecord(envelope.source, envelope.recordId, envelope.revision);
2079
2017
  }
2080
2018
  if (CONTINUITY_KIND_SET.has(envelope.kind)) {
2081
2019
  return decodeContinuityRecord(envelope.kind, envelope.source, envelope.recordId, envelope.revision);
2082
2020
  }
2083
- const base = baseByIdentity.get(`${envelope.kind}\0${envelope.recordId}`);
2084
2021
  if (base === undefined)
2085
2022
  throw invalidPayload('records');
2086
- return restoreBackupBasePrincipals(envelope.source, base);
2023
+ validateBackupBasePrincipals(base);
2024
+ return base;
2087
2025
  });
2088
2026
  if (decoded.some((item, index) => JSON.stringify(item) !== lines[index])) {
2089
2027
  throw invalidPayload('coordination');
@@ -2147,9 +2085,7 @@ export function validateCollabProjectBackupCheckpointConsistency(manifest, recor
2147
2085
  }))
2148
2086
  throw invalidPayload('records');
2149
2087
  const baseRecords = decodedRecords.filter(item => (!CONTINUITY_KIND_SET.has(item.kind) && item.kind !== 'idempotency-result'));
2150
- validateCollabProjectCheckpointConsistency(decodeCollabProjectCheckpointManifest({
2151
- ...decodedManifest,
2152
- coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
2153
- }), baseRecords);
2088
+ validateCheckpointRecordSequence(baseRecords, 'backup');
2089
+ validateCheckpointManifestConsistency(decodedManifest, baseRecords);
2154
2090
  return records;
2155
2091
  }
@@ -227,7 +227,7 @@ function gitRefs(value, expectedMainOid) {
227
227
  throw invalidPayload('refs');
228
228
  return Object.freeze(decoded);
229
229
  }
230
- function manifestObject(source, includeManifestSha256) {
230
+ function manifestObject(source, coordinationFormatVersion) {
231
231
  const profile = literal(source, 'profile', COLLAB_CHECKPOINT_PROFILES);
232
232
  const expectedMainOid = token(source, 'expectedMainOid', isCollabGitOid);
233
233
  const gitObjectFormat = literal(source, 'gitObjectFormat', ['sha1', 'sha256']);
@@ -263,15 +263,13 @@ function manifestObject(source, includeManifestSha256) {
263
263
  sourceAuthority,
264
264
  targetAuthority,
265
265
  };
266
- if (common.coordinationFormatVersion !== COLLAB_PROJECT_COORDINATION_FORMAT_VERSION
266
+ if (common.coordinationFormatVersion !== coordinationFormatVersion
267
267
  || common.manifestSchemaVersion !== COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION
268
268
  || common.protocolVersion !== COLLAB_PROTOCOL_VERSION)
269
269
  throw invalidPayload('manifest');
270
- if (!includeManifestSha256)
271
- return common;
272
270
  return {
273
271
  artifacts: common.artifacts,
274
- coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
272
+ coordinationFormatVersion,
275
273
  createdAt: common.createdAt,
276
274
  expectedMainOid: common.expectedMainOid,
277
275
  gitObjectFormat: common.gitObjectFormat,
@@ -286,7 +284,8 @@ function manifestObject(source, includeManifestSha256) {
286
284
  targetAuthority: common.targetAuthority,
287
285
  };
288
286
  }
289
- export function decodeCollabProjectCheckpointManifest(value) {
287
+ /** @internal Shared field validation; public entry points select their own format. */
288
+ export function decodeCheckpointManifestFields(value, coordinationFormatVersion) {
290
289
  const source = exactRecord(value, 'manifest', [
291
290
  'artifacts',
292
291
  'coordinationFormatVersion',
@@ -303,18 +302,24 @@ export function decodeCollabProjectCheckpointManifest(value) {
303
302
  'sourceAuthority',
304
303
  'targetAuthority',
305
304
  ]);
306
- const decoded = manifestObject(source, true);
305
+ const decoded = manifestObject(source, coordinationFormatVersion);
307
306
  const encoded = JSON.stringify(decoded);
308
307
  if (!hasUtf8ByteLengthAtMost(encoded, COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxManifestBytes)) {
309
308
  throw invalidPayload('manifest');
310
309
  }
311
310
  return decoded;
312
311
  }
312
+ export function decodeCollabProjectCheckpointManifest(value) {
313
+ return decodeCheckpointManifestFields(value, COLLAB_PROJECT_COORDINATION_FORMAT_VERSION);
314
+ }
313
315
  export function encodeCollabProjectCheckpointManifestCanonicalJson(manifest) {
314
316
  return JSON.stringify(decodeCollabProjectCheckpointManifest(manifest));
315
317
  }
316
318
  export function encodeCollabProjectCheckpointManifestDigestInput(manifest) {
317
- const decoded = decodeCollabProjectCheckpointManifest(manifest);
319
+ return encodeCheckpointManifestDigestInput(decodeCollabProjectCheckpointManifest(manifest));
320
+ }
321
+ /** @internal Encode already validated fields without changing their format. */
322
+ export function encodeCheckpointManifestDigestInput(decoded) {
318
323
  const source = {
319
324
  artifacts: decoded.artifacts,
320
325
  coordinationFormatVersion: decoded.coordinationFormatVersion,
@@ -697,13 +702,13 @@ function idempotencyResultRecord(source, recordId, revision) {
697
702
  },
698
703
  };
699
704
  }
700
- function principalBindingRecord(source, recordId, revision) {
701
- const value = exactRecord(source.value, 'value', [
705
+ function principalBindingRecord(source, recordId, revision, deferPrincipals) {
706
+ const value = principalRecord(source.value, 'value', [
702
707
  'boundAt',
703
708
  'memberId',
704
709
  'principalId',
705
710
  'projectId',
706
- ]);
711
+ ], deferPrincipals);
707
712
  const memberId = token(value, 'memberId', isCollabMemberId);
708
713
  if (recordId !== memberId)
709
714
  throw invalidPayload('recordId');
@@ -714,7 +719,7 @@ function principalBindingRecord(source, recordId, revision) {
714
719
  value: {
715
720
  boundAt: timestamp(value, 'boundAt'),
716
721
  memberId,
717
- principalId: token(value, 'principalId'),
722
+ principalId: deferPrincipals ? value.principalId : token(value, 'principalId'),
718
723
  projectId: token(value, 'projectId', isCollabProjectId),
719
724
  },
720
725
  };
@@ -848,7 +853,13 @@ function lifecycleStateRecord(source, recordId, revision) {
848
853
  value: internalState,
849
854
  };
850
855
  }
851
- function terminalResponderRecord(source, recordId, revision) {
856
+ function principalRecord(value, field, keys, deferPrincipals) {
857
+ const source = record(value, field);
858
+ return exactRecord(source, field, deferPrincipals && !Object.hasOwn(source, 'principalId')
859
+ ? keys.filter(key => key !== 'principalId')
860
+ : keys);
861
+ }
862
+ function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
852
863
  const value = exactRecord(source.value, 'value', [
853
864
  'acknowledgements',
854
865
  'eligibleMemberIds',
@@ -900,22 +911,24 @@ function terminalResponderRecord(source, recordId, revision) {
900
911
  if (!Array.isArray(value.acknowledgements))
901
912
  throw invalidPayload('acknowledgements');
902
913
  const acknowledgements = value.acknowledgements.map((item) => {
903
- const acknowledgement = exactRecord(item, 'acknowledgement', [
914
+ const acknowledgement = principalRecord(item, 'acknowledgement', [
904
915
  'acknowledgedAt',
905
916
  'memberId',
906
917
  'principalId',
907
- ]);
918
+ ], deferPrincipals);
908
919
  return {
909
920
  acknowledgedAt: timestamp(acknowledgement, 'acknowledgedAt'),
910
921
  memberId: token(acknowledgement, 'memberId', isCollabMemberId),
911
- principalId: token(acknowledgement, 'principalId'),
922
+ principalId: deferPrincipals
923
+ ? acknowledgement.principalId
924
+ : token(acknowledgement, 'principalId'),
912
925
  };
913
926
  });
914
927
  const acknowledgementPrincipals = new Set();
915
928
  acknowledgements.forEach((item, index) => {
916
929
  if (!eligibleMemberIds.includes(item.memberId)
917
930
  || (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
918
- || acknowledgementPrincipals.has(item.principalId))
931
+ || (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
919
932
  throw invalidPayload('acknowledgements');
920
933
  acknowledgementPrincipals.add(item.principalId);
921
934
  });
@@ -924,7 +937,7 @@ function terminalResponderRecord(source, recordId, revision) {
924
937
  recordId,
925
938
  revision,
926
939
  value: {
927
- acknowledgements: Object.freeze(acknowledgements),
940
+ acknowledgements,
928
941
  eligibleMemberIds: Object.freeze(eligibleMemberIds),
929
942
  expiresAt,
930
943
  operation,
@@ -1097,8 +1110,9 @@ function authorityVolumePairRecord(source, recordId, revision) {
1097
1110
  },
1098
1111
  };
1099
1112
  }
1100
- function decodeCheckpointRecord(value) {
1113
+ export function decodeCheckpointRecord(value, principalPhase) {
1101
1114
  const { kind, recordId, revision, source } = recordEnvelope(value);
1115
+ const deferPrincipals = principalPhase === 'deferred';
1102
1116
  switch (kind) {
1103
1117
  case 'project': return projectRecord(source, recordId, revision);
1104
1118
  case 'member': return memberRecord(source, recordId, revision);
@@ -1111,10 +1125,12 @@ function decodeCheckpointRecord(value) {
1111
1125
  case 'cloud-event': return cloudEventRecord(source, recordId, revision);
1112
1126
  case 'cloud-event-cursor': return cloudEventCursorRecord(source, recordId, revision);
1113
1127
  case 'idempotency-result': return idempotencyResultRecord(source, recordId, revision);
1114
- case 'principal-binding': return principalBindingRecord(source, recordId, revision);
1128
+ case 'principal-binding':
1129
+ return principalBindingRecord(source, recordId, revision, deferPrincipals);
1115
1130
  case 'repository-placement': return repositoryPlacementRecord(source, recordId, revision);
1116
1131
  case 'lifecycle-state': return lifecycleStateRecord(source, recordId, revision);
1117
- case 'terminal-responder': return terminalResponderRecord(source, recordId, revision);
1132
+ case 'terminal-responder':
1133
+ return terminalResponderRecord(source, recordId, revision, deferPrincipals);
1118
1134
  case 'protected-claim-envelope':
1119
1135
  return protectedClaimEnvelopeRecord(source, recordId, revision);
1120
1136
  case 'tombstone': return tombstoneRecord(source, recordId, revision);
@@ -1130,7 +1146,8 @@ function compareRecords(left, right) {
1130
1146
  const kindOrder = recordKindOrder(left.kind) - recordKindOrder(right.kind);
1131
1147
  return kindOrder !== 0 ? kindOrder : left.recordId.localeCompare(right.recordId, 'en-US');
1132
1148
  }
1133
- function validateRecordSequence(records, profile) {
1149
+ /** @internal V3 decoding checks terminal principals separately; consistency binds current principals. */
1150
+ export function validateCheckpointRecordSequence(records, profile, acknowledgementBinding = 'current-principal') {
1134
1151
  if (records.length === 0 || records[0].kind !== 'project')
1135
1152
  throw invalidPayload('records');
1136
1153
  const projectId = records[0].recordId;
@@ -1233,7 +1250,9 @@ function validateRecordSequence(records, profile) {
1233
1250
  throw invalidPayload('records');
1234
1251
  }
1235
1252
  if (item.kind === 'terminal-responder' && (item.value.eligibleMemberIds.some(memberId => !members.has(memberId))
1236
- || item.value.acknowledgements.some(acknowledgement => (principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId))))
1253
+ || item.value.acknowledgements.some(acknowledgement => (acknowledgementBinding === 'current-principal'
1254
+ ? principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId
1255
+ : !principalBindings.has(acknowledgement.memberId)))))
1237
1256
  throw invalidPayload('records');
1238
1257
  }
1239
1258
  if (profile === 'backup') {
@@ -1260,11 +1279,13 @@ export function decodeCollabProjectCheckpointCoordinationNdjson(value, profile)
1260
1279
  throw invalidPayload('coordination');
1261
1280
  }
1262
1281
  const result = decodeCheckpointRecord(parsed);
1282
+ if (result.kind === 'terminal-responder')
1283
+ Object.freeze(result.value.acknowledgements);
1263
1284
  if (JSON.stringify(result) !== line)
1264
1285
  throw invalidPayload('coordination');
1265
1286
  return result;
1266
1287
  });
1267
- validateRecordSequence(decoded, profile);
1288
+ validateCheckpointRecordSequence(decoded, profile);
1268
1289
  return Object.freeze(decoded);
1269
1290
  }
1270
1291
  export function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
@@ -1296,7 +1317,11 @@ function checkpointRecordGitOids(checkpointRecord) {
1296
1317
  }
1297
1318
  export function validateCollabProjectCheckpointConsistency(manifest, records) {
1298
1319
  const decodedManifest = decodeCollabProjectCheckpointManifest(manifest);
1299
- validateRecordSequence(records, decodedManifest.profile);
1320
+ validateCheckpointRecordSequence(records, decodedManifest.profile);
1321
+ return validateCheckpointManifestConsistency(decodedManifest, records);
1322
+ }
1323
+ /** @internal Cross-artifact checks operate on validated fields, not another format. */
1324
+ export function validateCheckpointManifestConsistency(decodedManifest, records) {
1300
1325
  const project = records[0];
1301
1326
  if (project.kind !== 'project'
1302
1327
  || project.value.projectId !== decodedManifest.projectId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudian-collab/protocol",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,7 +46,7 @@
46
46
  "release:candidate": "node scripts/release-candidate.mjs --output .context/release",
47
47
  "test": "jest && node --test tests/*.test.mjs",
48
48
  "typecheck": "tsc --noEmit -p tsconfig.jest.json",
49
- "verify": "npm run lint && npm run typecheck && npm run build && npm run test && npm run check:compatibility && npm run check:migration-provenance && npm run verify:pack && npm run release:candidate",
49
+ "verify": "npm run lint && npm run typecheck && npm run release:candidate && npm run test && npm run check:compatibility && npm run check:migration-provenance && npm run verify:pack",
50
50
  "verify:pack": "node scripts/verify-pack.mjs"
51
51
  },
52
52
  "dependencies": {