@claudian-collab/protocol 3.3.2 → 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,7 @@ fixed part of the detail so the whole detail stays within
37
37
  has one compatible final-serialization limit. Request comments and Ticket
38
38
  accepted relations also have shared authority-enforced total limits; summary
39
39
  counts let complete consumers reject partial or cross-snapshot assembly.
40
- Cloud binding v2 adds the authority-neutral Project snapshot, nine redacted durable event kinds plus `snapshot.required`, eighteen capability tokens, the exact ordinary Project/Git route catalog, bounded authority-transfer artifact routes, and six private-development bootstrap bindings. Wire v6 includes the 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.
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
41
 
42
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.
43
43
 
@@ -64,14 +64,9 @@ part of the public surface.
64
64
 
65
65
  Package SemVer, canonical wire version, and Cloud binding version are independent concepts.
66
66
 
67
- - **Package version** (this `package.json`): `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.
68
- - **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `6`. The supported
69
- range is exactly `[6, 6]`. This is independent from Cloud binding version `2` and the existing application
70
- LAN control version `9`. Any change to an existing envelope, DTO, operation
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.
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.
75
70
 
76
71
  ### Compatibility behavior
77
72
 
@@ -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({
@@ -1,3 +1,4 @@
1
+ import type { CollabTransferredMembershipClaim } from './CollabAuthorityTransfer';
1
2
  import type { CollabDecodeResult } from './CollabProtocol';
2
3
  import type { CollabGitOid, CollabIdempotencyKey, CollabIsoTimestamp, CollabMemberId, CollabProjectId, CollabRequestId, CollabRole } from './types';
3
4
  export declare const COLLAB_PROJECT_MEMBERSHIP_LIMITS: Readonly<{
@@ -95,6 +96,7 @@ export interface JoinCloudProjectResponse {
95
96
  export interface CollabProjectMemberSummary {
96
97
  readonly bindingState: CollabProjectMemberBindingState;
97
98
  readonly displayName: string;
99
+ readonly importedClaimGeneration: number | null;
98
100
  readonly importedClaimState: CollabImportedClaimState;
99
101
  readonly memberId: CollabMemberId;
100
102
  readonly membershipRevision: number;
@@ -112,13 +114,9 @@ export interface ReissueTransferredMembershipClaimRequest extends CollabProjectM
112
114
  readonly expectedMembershipRevision: number;
113
115
  readonly memberId: CollabMemberId;
114
116
  }
115
- export interface ReissueTransferredMembershipClaimResponse {
116
- readonly claim: string;
117
+ export interface ReissueTransferredMembershipClaimResponse extends CollabTransferredMembershipClaim {
117
118
  readonly claimGeneration: number;
118
119
  readonly createdAt: CollabIsoTimestamp;
119
- readonly expiresAt: CollabIsoTimestamp;
120
- readonly memberId: CollabMemberId;
121
- readonly projectId: CollabProjectId;
122
120
  readonly secretReplayExpiresAt: CollabIsoTimestamp;
123
121
  }
124
122
  export type RevokeTransferredMembershipClaimRequest = ReissueTransferredMembershipClaimRequest;
@@ -338,23 +338,37 @@ function decodeMemberSummary(value) {
338
338
  const source = exactRecord(value, 'member', [
339
339
  'bindingState',
340
340
  'displayName',
341
+ 'importedClaimGeneration',
341
342
  'importedClaimState',
342
343
  'memberId',
343
344
  'membershipRevision',
344
345
  'role',
345
346
  ]);
347
+ const importedClaimState = literal(source, 'importedClaimState', [
348
+ 'not-applicable',
349
+ 'original-active',
350
+ 'override-active',
351
+ 'revoked',
352
+ 'expired',
353
+ 'redeemed',
354
+ 'hidden',
355
+ ]);
356
+ const importedClaimGeneration = source.importedClaimGeneration === null
357
+ ? null
358
+ : nonNegativeInteger(source, 'importedClaimGeneration');
359
+ if (importedClaimState === 'hidden' || importedClaimState === 'not-applicable') {
360
+ if (importedClaimGeneration !== null)
361
+ throw invalidPayload('importedClaimGeneration');
362
+ }
363
+ else if (importedClaimGeneration === null
364
+ || (importedClaimState === 'original-active' && importedClaimGeneration !== 0)
365
+ || (importedClaimState === 'override-active' && importedClaimGeneration === 0))
366
+ throw invalidPayload('importedClaimGeneration');
346
367
  return {
347
368
  bindingState: literal(source, 'bindingState', ['bound', 'unbound', 'hidden']),
348
369
  displayName: boundedText(source, 'displayName', exports.COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxDisplayNameUtf8Bytes),
349
- importedClaimState: literal(source, 'importedClaimState', [
350
- 'not-applicable',
351
- 'original-active',
352
- 'override-active',
353
- 'revoked',
354
- 'expired',
355
- 'redeemed',
356
- 'hidden',
357
- ]),
370
+ importedClaimGeneration,
371
+ importedClaimState,
358
372
  memberId: token(source, 'memberId', CollabValidation_1.isCollabMemberId),
359
373
  membershipRevision: positiveInteger(source, 'membershipRevision'),
360
374
  role: literal(source, 'role', ['manager', 'member']),
@@ -401,6 +415,8 @@ function decodeReissueClaimResponse(value) {
401
415
  'memberId',
402
416
  'projectId',
403
417
  'secretReplayExpiresAt',
418
+ 'targetAuthorityGeneration',
419
+ 'transferId',
404
420
  ]);
405
421
  const createdAt = timestamp(source, 'createdAt');
406
422
  const expiresAt = timestamp(source, 'expiresAt');
@@ -416,6 +432,8 @@ function decodeReissueClaimResponse(value) {
416
432
  memberId: token(source, 'memberId', CollabValidation_1.isCollabMemberId),
417
433
  projectId: token(source, 'projectId', CollabValidation_1.isCollabProjectId),
418
434
  secretReplayExpiresAt,
435
+ targetAuthorityGeneration: positiveInteger(source, 'targetAuthorityGeneration'),
436
+ transferId: token(source, 'transferId'),
419
437
  };
420
438
  }
421
439
  function decodeRevokeClaimResponse(value) {
@@ -3,7 +3,7 @@ import { COLLAB_CONTROL_OPERATION_CODECS, } from './CollabControlOperationCodecs
3
3
  import { COLLAB_ERROR_CODES, CollabError, sanitizeCollabDiagnosticContext, } from './CollabError.mjs';
4
4
  import { isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
5
5
  import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_PROJECT_CHECKPOINT_ARTIFACTS, } from './CollabProjectCheckpoint.mjs';
6
- export const COLLAB_CLOUD_BINDING_VERSION = 2;
6
+ export const COLLAB_CLOUD_BINDING_VERSION = 3;
7
7
  export const COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION = 2;
8
8
  export const COLLAB_CLOUD_CAPABILITIES = Object.freeze([
9
9
  'accept',
@@ -168,7 +168,7 @@ export function collabCloudProjectOperationRoute(projectId, operation) {
168
168
  assertProjectId(projectId);
169
169
  if (!isCloudJsonOperation(operation))
170
170
  invalidRoute();
171
- return route('POST', `/v2/projects/${projectId}/operations/${operation}`, {
171
+ return route('POST', `/v3/projects/${projectId}/operations/${operation}`, {
172
172
  kind: 'project-operation',
173
173
  operation,
174
174
  projectId,
@@ -178,7 +178,7 @@ export function collabCloudProjectEventsRoute(projectId, afterSequence) {
178
178
  assertProjectId(projectId);
179
179
  if (!Number.isSafeInteger(afterSequence) || afterSequence < 0)
180
180
  invalidRoute();
181
- return route('GET', `/v2/projects/${projectId}/events?afterSequence=${afterSequence}`, {
181
+ return route('GET', `/v3/projects/${projectId}/events?afterSequence=${afterSequence}`, {
182
182
  afterSequence,
183
183
  kind: 'project-events',
184
184
  projectId,
@@ -189,11 +189,11 @@ export function collabCloudGitRoute(projectId, routeKind, service) {
189
189
  if (routeKind === 'info-refs') {
190
190
  if (service !== 'git-upload-pack' && service !== 'git-receive-pack')
191
191
  invalidRoute();
192
- return route('GET', `/v2/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
192
+ return route('GET', `/v3/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
193
193
  }
194
194
  if (service !== undefined)
195
195
  invalidRoute();
196
- return route('POST', `/v2/projects/${projectId}/repository.git/${routeKind}`, {
196
+ return route('POST', `/v3/projects/${projectId}/repository.git/${routeKind}`, {
197
197
  kind: routeKind,
198
198
  projectId,
199
199
  });
@@ -204,7 +204,7 @@ export function collabCloudAuthorityTransferArtifactRoute(projectId, transferId,
204
204
  if (direction !== 'download' && direction !== 'upload'
205
205
  || !COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
206
206
  invalidRoute();
207
- return route(direction === 'upload' ? 'PUT' : 'GET', `/v2/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
207
+ return route(direction === 'upload' ? 'PUT' : 'GET', `/v3/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
208
208
  artifact,
209
209
  direction,
210
210
  kind: 'authority-transfer-artifact',
@@ -217,7 +217,7 @@ export function collabCloudProjectCheckpointExportArtifactRoute(projectId, expor
217
217
  assertAttemptId(exportId);
218
218
  if (!COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
219
219
  invalidRoute();
220
- return route('GET', `/v2/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
220
+ return route('GET', `/v3/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
221
221
  artifact,
222
222
  exportId,
223
223
  kind: 'project-checkpoint-export-artifact',
@@ -229,7 +229,7 @@ export function collabCloudProjectCheckpointExportRoute(projectId, exportId, ope
229
229
  assertAttemptId(exportId);
230
230
  if (operation !== 'begin' && operation !== 'status')
231
231
  invalidRoute();
232
- return route(operation === 'begin' ? 'POST' : 'GET', `/v2/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
232
+ return route(operation === 'begin' ? 'POST' : 'GET', `/v3/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
233
233
  }
234
234
  export function decodeCollabCloudProjectCheckpointExportStatus(value) {
235
235
  const source = exactRecord(value, 'checkpointExport', [
@@ -267,7 +267,7 @@ export function decodeCollabCloudProjectCheckpointExportStatus(value) {
267
267
  });
268
268
  }
269
269
  export function collabDevelopmentBootstrapRoute(operation, attemptId) {
270
- const base = '/v2/development/bootstrap/attempts';
270
+ const base = '/v3/development/bootstrap/attempts';
271
271
  if (operation === 'beginDevelopmentBootstrap') {
272
272
  if (attemptId !== undefined)
273
273
  invalidRoute();
@@ -326,7 +326,7 @@ export function matchCollabCloudRoute(method, target) {
326
326
  && url.pathname === '/collab/capabilities'
327
327
  && exactQuery(url, []))
328
328
  return { kind: 'capabilities' };
329
- if (segments[0] === 'v2'
329
+ if (segments[0] === 'v3'
330
330
  && segments[1] === 'projects'
331
331
  && isCollabProjectId(segments[2])) {
332
332
  const projectId = segments[2];
@@ -405,7 +405,7 @@ export function matchCollabCloudRoute(method, target) {
405
405
  && exactQuery(url, []))
406
406
  return { kind: segments[4], projectId };
407
407
  }
408
- if (segments[0] !== 'v2'
408
+ if (segments[0] !== 'v3'
409
409
  || segments[1] !== 'development'
410
410
  || segments[2] !== 'bootstrap'
411
411
  || segments[3] !== 'attempts'
@@ -63,6 +63,7 @@ function positiveInteger(source, field) {
63
63
  }
64
64
  function decodeProject(value) {
65
65
  const source = exactRecord(value, 'project', [
66
+ 'authorityGeneration',
66
67
  'createdAt',
67
68
  'expectedMainOid',
68
69
  'id',
@@ -72,6 +73,7 @@ function decodeProject(value) {
72
73
  if (source.mainRef !== COLLAB_MAIN_REF)
73
74
  throw invalidPayload('mainRef');
74
75
  return {
76
+ authorityGeneration: positiveInteger(source, 'authorityGeneration'),
75
77
  createdAt: timestamp(source, 'createdAt'),
76
78
  expectedMainOid: stringField(source, 'expectedMainOid', 64, isCollabGitOid),
77
79
  id: stringField(source, 'id', 64, isCollabProjectId),
@@ -1,4 +1,4 @@
1
- export const COLLAB_PROTOCOL_VERSION = 6;
1
+ export const COLLAB_PROTOCOL_VERSION = 7;
2
2
  export const COLLAB_MAIN_REF = 'refs/heads/main';
3
3
  export const COLLAB_MEMBER_REF_PREFIX = 'refs/heads/members/';
4
4
  export const COLLAB_LIMITS = Object.freeze({
@@ -333,23 +333,37 @@ function decodeMemberSummary(value) {
333
333
  const source = exactRecord(value, 'member', [
334
334
  'bindingState',
335
335
  'displayName',
336
+ 'importedClaimGeneration',
336
337
  'importedClaimState',
337
338
  'memberId',
338
339
  'membershipRevision',
339
340
  'role',
340
341
  ]);
342
+ const importedClaimState = literal(source, 'importedClaimState', [
343
+ 'not-applicable',
344
+ 'original-active',
345
+ 'override-active',
346
+ 'revoked',
347
+ 'expired',
348
+ 'redeemed',
349
+ 'hidden',
350
+ ]);
351
+ const importedClaimGeneration = source.importedClaimGeneration === null
352
+ ? null
353
+ : nonNegativeInteger(source, 'importedClaimGeneration');
354
+ if (importedClaimState === 'hidden' || importedClaimState === 'not-applicable') {
355
+ if (importedClaimGeneration !== null)
356
+ throw invalidPayload('importedClaimGeneration');
357
+ }
358
+ else if (importedClaimGeneration === null
359
+ || (importedClaimState === 'original-active' && importedClaimGeneration !== 0)
360
+ || (importedClaimState === 'override-active' && importedClaimGeneration === 0))
361
+ throw invalidPayload('importedClaimGeneration');
341
362
  return {
342
363
  bindingState: literal(source, 'bindingState', ['bound', 'unbound', 'hidden']),
343
364
  displayName: boundedText(source, 'displayName', COLLAB_PROJECT_MEMBERSHIP_LIMITS.maxDisplayNameUtf8Bytes),
344
- importedClaimState: literal(source, 'importedClaimState', [
345
- 'not-applicable',
346
- 'original-active',
347
- 'override-active',
348
- 'revoked',
349
- 'expired',
350
- 'redeemed',
351
- 'hidden',
352
- ]),
365
+ importedClaimGeneration,
366
+ importedClaimState,
353
367
  memberId: token(source, 'memberId', isCollabMemberId),
354
368
  membershipRevision: positiveInteger(source, 'membershipRevision'),
355
369
  role: literal(source, 'role', ['manager', 'member']),
@@ -396,6 +410,8 @@ function decodeReissueClaimResponse(value) {
396
410
  'memberId',
397
411
  'projectId',
398
412
  'secretReplayExpiresAt',
413
+ 'targetAuthorityGeneration',
414
+ 'transferId',
399
415
  ]);
400
416
  const createdAt = timestamp(source, 'createdAt');
401
417
  const expiresAt = timestamp(source, 'expiresAt');
@@ -411,6 +427,8 @@ function decodeReissueClaimResponse(value) {
411
427
  memberId: token(source, 'memberId', isCollabMemberId),
412
428
  projectId: token(source, 'projectId', isCollabProjectId),
413
429
  secretReplayExpiresAt,
430
+ targetAuthorityGeneration: positiveInteger(source, 'targetAuthorityGeneration'),
431
+ transferId: token(source, 'transferId'),
414
432
  };
415
433
  }
416
434
  function decodeRevokeClaimResponse(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudian-collab/protocol",
3
- "version": "3.3.2",
3
+ "version": "4.0.0",
4
4
  "description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
5
5
  "license": "MIT",
6
6
  "repository": {