@claudian-collab/protocol 3.3.1 → 4.0.0

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
@@ -20,7 +20,7 @@ or compatibility rules.
20
20
  - Shared limits that client and server must agree on (`COLLAB_LIMITS`)
21
21
  - Protocol-version negotiation (`COLLAB_PROTOCOL_VERSION`, envelope decoding)
22
22
  - Canonical Project checkpoint profiles, logical records, authority-generation transfer, exact membership claims, target-signed redemption receipts, offline backup operational continuity, and Project retirement contracts
23
- - Cloud binding v2 route construction/matching, bounded checkpoint streams, capability negotiation, strict success/error envelopes, private-development bootstrap, Project snapshot, and redacted event contracts
23
+ - Cloud binding v3 route construction/matching, bounded checkpoint streams, capability negotiation, strict success/error envelopes, private-development bootstrap, Project snapshot, and redacted event contracts
24
24
  - Markdown-derived Ticket-reference and Member-mention semantics shared by
25
25
  client rendering and authority derivation
26
26
 
@@ -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 v3 retains 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 v7 adds the required logical Project generation, state-consistent imported-claim generation, and complete reissued-claim transfer identity to the existing Cloud Project-membership read contracts. 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,9 @@ 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.
82
- - **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `6`. The supported
83
- range is exactly `[6, 6]`. This is independent from Cloud binding version `2` and the existing application
84
- 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.
67
+ - **Package version** (this `package.json`): `4.0.0`. Behavior-preserving implementation refactors and reviewed compatible defect corrections may use a patch release when public declarations, runtime exports, and accepted wire/binding semantics remain unchanged. Minor releases may add backward-compatible API. Removing or incompatibly changing an existing declaration, export, runtime behavior, codec, error, limit, ref rule, operation, or compatibility rule requires a major release. Package API SemVer is classified independently from the wire and Cloud binding contracts, but a compatible package addition cannot exempt a changed wire contract from a wire-version increase or a changed Cloud binding contract from a binding-version increase. The package version never signals wire compatibility by itself.
68
+ - **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `7`. The supported range is exactly `[7, 7]`. This is independent from Cloud binding version `3` and the existing application LAN control version `9`. Any change to an existing envelope, DTO, operation payload shape, or operation definition requires a new wire-protocol version. This includes additive operations: every wire-contract change increases the canonical wire version.
69
+ - **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `3`. It versions Cloud routes, bounded transfer streams, and capabilities independently from package and wire versions. Every Cloud binding-contract change increases this version. Bindings before v3 and wire versions before v7 are unsupported rather than translated or dual-interpreted.
90
70
 
91
71
  ### Compatibility behavior
92
72
 
@@ -105,14 +85,18 @@ Compatibility is tested with executable fixtures in this package
105
85
  (`tests/`), and cross-repository contract fixtures pin client and server to
106
86
  the same behavior.
107
87
 
88
+ 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.
89
+
108
90
  ## Development
109
91
 
110
92
  ```bash
111
93
  npm run build # compile src/ -> dist/ (CommonJS + declarations)
112
94
  npm test # package unit tests
113
95
  npm run check:compatibility
114
- npm run verify:pack # pack the artifact and smoke-test a clean consumer
96
+ npm run release:candidate # build and verify the reviewed release artifact
97
+ npm run verify:pack # smoke-test that exact artifact in a clean consumer
115
98
  ```
116
99
 
117
100
  `dist/` is generated output and is never committed. The packed artifact is
118
101
  reproducible from source: `npm pack` rebuilds via `prepack`.
102
+ The full verification gate constructs one release candidate and uses those same reviewed tarball bytes for clean-consumer smoke tests and publication.
@@ -2,7 +2,7 @@ import { COLLAB_PROTOCOL_VERSION } from './CollabConstants';
2
2
  import { CollabError, type CollabDiagnosticContext, type CollabErrorCode, type CollabRecoveryAction } from './CollabError';
3
3
  import { COLLAB_PROJECT_CHECKPOINT_ARTIFACTS } from './CollabProjectCheckpoint';
4
4
  import type { CollabIsoTimestamp, CollabProjectId } from './types';
5
- export declare const COLLAB_CLOUD_BINDING_VERSION: 2;
5
+ export declare const COLLAB_CLOUD_BINDING_VERSION: 3;
6
6
  export declare const COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION: 2;
7
7
  export declare const COLLAB_CLOUD_CAPABILITIES: readonly ["accept", "authority-transfer", "cloud-imported-membership-claims", "cloud-project-create", "cloud-project-invitations", "cloud-project-join", "cloud-project-leave", "cloud-project-manager-responsibility", "cloud-project-membership", "development-bootstrap", "git-receive-pack-personal-ref", "git-upload-pack", "project-checkpoint-export", "project-events", "project-retirement", "project-snapshot", "requests", "tickets"];
8
8
  export type CollabCloudCapability = typeof COLLAB_CLOUD_CAPABILITIES[number];
@@ -23,7 +23,7 @@ const CollabControlOperationCodecs_1 = require("./CollabControlOperationCodecs")
23
23
  const CollabError_1 = require("./CollabError");
24
24
  const CollabValidation_1 = require("./CollabValidation");
25
25
  const CollabProjectCheckpoint_1 = require("./CollabProjectCheckpoint");
26
- exports.COLLAB_CLOUD_BINDING_VERSION = 2;
26
+ exports.COLLAB_CLOUD_BINDING_VERSION = 3;
27
27
  exports.COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION = 2;
28
28
  exports.COLLAB_CLOUD_CAPABILITIES = Object.freeze([
29
29
  'accept',
@@ -188,7 +188,7 @@ function collabCloudProjectOperationRoute(projectId, operation) {
188
188
  assertProjectId(projectId);
189
189
  if (!isCloudJsonOperation(operation))
190
190
  invalidRoute();
191
- return route('POST', `/v2/projects/${projectId}/operations/${operation}`, {
191
+ return route('POST', `/v3/projects/${projectId}/operations/${operation}`, {
192
192
  kind: 'project-operation',
193
193
  operation,
194
194
  projectId,
@@ -198,7 +198,7 @@ function collabCloudProjectEventsRoute(projectId, afterSequence) {
198
198
  assertProjectId(projectId);
199
199
  if (!Number.isSafeInteger(afterSequence) || afterSequence < 0)
200
200
  invalidRoute();
201
- return route('GET', `/v2/projects/${projectId}/events?afterSequence=${afterSequence}`, {
201
+ return route('GET', `/v3/projects/${projectId}/events?afterSequence=${afterSequence}`, {
202
202
  afterSequence,
203
203
  kind: 'project-events',
204
204
  projectId,
@@ -209,11 +209,11 @@ function collabCloudGitRoute(projectId, routeKind, service) {
209
209
  if (routeKind === 'info-refs') {
210
210
  if (service !== 'git-upload-pack' && service !== 'git-receive-pack')
211
211
  invalidRoute();
212
- return route('GET', `/v2/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
212
+ return route('GET', `/v3/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
213
213
  }
214
214
  if (service !== undefined)
215
215
  invalidRoute();
216
- return route('POST', `/v2/projects/${projectId}/repository.git/${routeKind}`, {
216
+ return route('POST', `/v3/projects/${projectId}/repository.git/${routeKind}`, {
217
217
  kind: routeKind,
218
218
  projectId,
219
219
  });
@@ -224,7 +224,7 @@ function collabCloudAuthorityTransferArtifactRoute(projectId, transferId, direct
224
224
  if (direction !== 'download' && direction !== 'upload'
225
225
  || !COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
226
226
  invalidRoute();
227
- return route(direction === 'upload' ? 'PUT' : 'GET', `/v2/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
227
+ return route(direction === 'upload' ? 'PUT' : 'GET', `/v3/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
228
228
  artifact,
229
229
  direction,
230
230
  kind: 'authority-transfer-artifact',
@@ -237,7 +237,7 @@ function collabCloudProjectCheckpointExportArtifactRoute(projectId, exportId, ar
237
237
  assertAttemptId(exportId);
238
238
  if (!COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
239
239
  invalidRoute();
240
- return route('GET', `/v2/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
240
+ return route('GET', `/v3/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
241
241
  artifact,
242
242
  exportId,
243
243
  kind: 'project-checkpoint-export-artifact',
@@ -249,7 +249,7 @@ function collabCloudProjectCheckpointExportRoute(projectId, exportId, operation)
249
249
  assertAttemptId(exportId);
250
250
  if (operation !== 'begin' && operation !== 'status')
251
251
  invalidRoute();
252
- return route(operation === 'begin' ? 'POST' : 'GET', `/v2/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
252
+ return route(operation === 'begin' ? 'POST' : 'GET', `/v3/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
253
253
  }
254
254
  function decodeCollabCloudProjectCheckpointExportStatus(value) {
255
255
  const source = exactRecord(value, 'checkpointExport', [
@@ -287,7 +287,7 @@ function decodeCollabCloudProjectCheckpointExportStatus(value) {
287
287
  });
288
288
  }
289
289
  function collabDevelopmentBootstrapRoute(operation, attemptId) {
290
- const base = '/v2/development/bootstrap/attempts';
290
+ const base = '/v3/development/bootstrap/attempts';
291
291
  if (operation === 'beginDevelopmentBootstrap') {
292
292
  if (attemptId !== undefined)
293
293
  invalidRoute();
@@ -346,7 +346,7 @@ function matchCollabCloudRoute(method, target) {
346
346
  && url.pathname === '/collab/capabilities'
347
347
  && exactQuery(url, []))
348
348
  return { kind: 'capabilities' };
349
- if (segments[0] === 'v2'
349
+ if (segments[0] === 'v3'
350
350
  && segments[1] === 'projects'
351
351
  && (0, CollabValidation_1.isCollabProjectId)(segments[2])) {
352
352
  const projectId = segments[2];
@@ -425,7 +425,7 @@ function matchCollabCloudRoute(method, target) {
425
425
  && exactQuery(url, []))
426
426
  return { kind: segments[4], projectId };
427
427
  }
428
- if (segments[0] !== 'v2'
428
+ if (segments[0] !== 'v3'
429
429
  || segments[1] !== 'development'
430
430
  || segments[2] !== 'bootstrap'
431
431
  || segments[3] !== 'attempts'
@@ -2,6 +2,7 @@ import { COLLAB_MAIN_REF } from './CollabConstants';
2
2
  import type { CollabDecodeResult } from './CollabProtocol';
3
3
  import type { CollabChangeRequest, CollabGitOid, CollabIsoTimestamp, CollabMemberId, CollabProjectId, CollabRole, CollabTicketSummary } from './types';
4
4
  export interface CollabCloudProjectSummary {
5
+ readonly authorityGeneration: number;
5
6
  readonly createdAt: CollabIsoTimestamp;
6
7
  readonly expectedMainOid: CollabGitOid;
7
8
  readonly id: CollabProjectId;
@@ -67,6 +67,7 @@ function positiveInteger(source, field) {
67
67
  }
68
68
  function decodeProject(value) {
69
69
  const source = exactRecord(value, 'project', [
70
+ 'authorityGeneration',
70
71
  'createdAt',
71
72
  'expectedMainOid',
72
73
  'id',
@@ -76,6 +77,7 @@ function decodeProject(value) {
76
77
  if (source.mainRef !== CollabConstants_1.COLLAB_MAIN_REF)
77
78
  throw invalidPayload('mainRef');
78
79
  return {
80
+ authorityGeneration: positiveInteger(source, 'authorityGeneration'),
79
81
  createdAt: timestamp(source, 'createdAt'),
80
82
  expectedMainOid: stringField(source, 'expectedMainOid', 64, CollabValidation_1.isCollabGitOid),
81
83
  id: stringField(source, 'id', 64, CollabValidation_1.isCollabProjectId),
@@ -1,4 +1,4 @@
1
- export declare const COLLAB_PROTOCOL_VERSION: 6;
1
+ export declare const COLLAB_PROTOCOL_VERSION: 7;
2
2
  export declare const COLLAB_MAIN_REF: "refs/heads/main";
3
3
  export declare const COLLAB_MEMBER_REF_PREFIX: "refs/heads/members/";
4
4
  export declare const COLLAB_LIMITS: Readonly<{
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.COLLAB_LIMITS = exports.COLLAB_MEMBER_REF_PREFIX = exports.COLLAB_MAIN_REF = exports.COLLAB_PROTOCOL_VERSION = void 0;
4
- exports.COLLAB_PROTOCOL_VERSION = 6;
4
+ exports.COLLAB_PROTOCOL_VERSION = 7;
5
5
  exports.COLLAB_MAIN_REF = 'refs/heads/main';
6
6
  exports.COLLAB_MEMBER_REF_PREFIX = 'refs/heads/members/';
7
7
  exports.COLLAB_LIMITS = Object.freeze({
@@ -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 {};