@claudian-collab/protocol 4.1.3 → 4.2.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 +6 -5
- package/dist/CollabCloudBinding.d.ts +8 -2
- package/dist/CollabCloudBinding.js +25 -13
- package/dist/CollabConstants.d.ts +1 -1
- package/dist/CollabConstants.js +1 -1
- package/dist/CollabProjectBackupCheckpoint.js +46 -20
- package/dist/esm/CollabCloudBinding.mjs +25 -13
- package/dist/esm/CollabConstants.mjs +1 -1
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +46 -20
- package/package.json +1 -1
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
|
|
23
|
+
- Cloud binding v5 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
|
|
40
|
+
Cloud binding v5 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 v9 retains target-confirmed Cloud-to-LAN invalidation and cleanup to the existing authority-transfer contract. Its domain-separated Ed25519 proof binds the exact target Member, source and target generations, checkpoint, stage, current claim batch when present, and cleanup digest; Manager cancellation, timeout, disconnect, or target absence does not prove cleanup. 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. Portable profiles exclude engine storage, paths, credentials, tokens, private keys, and operational refs, and backups exclude plaintext invitation secrets, raw claims, and private keys. Existing LAN Join, invitation, endpoint, membership lifecycle, Host-transfer, snapshot, event, and HTTP bindings remain independently application-owned; the dedicated LAN authority-transfer transport binds 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,12 +64,13 @@ 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`): `4.
|
|
68
|
-
- **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `
|
|
69
|
-
- **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `
|
|
67
|
+
- **Package version** (this `package.json`): `4.2.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 `9`. The supported range is exactly `[9, 9]`. This is independent from Cloud binding version `5` and the application-owned LAN bindings. 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 `5`. 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 v5 and wire versions before v9 are unsupported rather than translated or dual-interpreted.
|
|
70
70
|
|
|
71
71
|
### Compatibility behavior
|
|
72
72
|
|
|
73
|
+
- A Cloud error envelope may carry `mutationOutcome: 'rejected'` only when the authority has checked the exact request's retained result and nonterminal journal under its Project write lane and durable state prevents that request from ever applying. A persisted rejection fence or a strictly advanced monotone expected-state value can establish that guarantee; a later snapshot, missing row, ordinary rejection code, timeout, or diagnostic reason cannot. The envelope's request ID binds the proof to the submitted operation and request. Without this property, clients must preserve ambiguous mutation recovery. The authority must preserve the facts establishing this guarantee in backup/restore and delayed-duplicate handling.
|
|
73
74
|
- Envelope decoders reject unknown fields (`protocol-payload-invalid`).
|
|
74
75
|
Operation request and response compatibility is
|
|
75
76
|
decoder-defined and pinned by package fixtures; decoded DTOs are reconstructed
|
|
@@ -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:
|
|
5
|
+
export declare const COLLAB_CLOUD_BINDING_VERSION: 5;
|
|
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];
|
|
@@ -122,6 +122,12 @@ export interface CollabCloudWireError {
|
|
|
122
122
|
}
|
|
123
123
|
export interface CollabCloudErrorEnvelope {
|
|
124
124
|
readonly error: CollabCloudWireError;
|
|
125
|
+
/**
|
|
126
|
+
* Exact request-correlated proof of no retained success or nonterminal journal,
|
|
127
|
+
* and durable authority state preventing this request from applying later.
|
|
128
|
+
* Omission leaves the mutation outcome unknown, regardless of the error code.
|
|
129
|
+
*/
|
|
130
|
+
readonly mutationOutcome?: 'rejected';
|
|
125
131
|
readonly protocolVersion: typeof COLLAB_PROTOCOL_VERSION;
|
|
126
132
|
readonly requestId: string;
|
|
127
133
|
}
|
|
@@ -142,5 +148,5 @@ export declare function collabCloudCapabilityDocument(capabilities: readonly Col
|
|
|
142
148
|
export declare function collabCloudCapabilitySupported(document: CollabCloudCapabilityDocument, capability: CollabCloudCapability): boolean;
|
|
143
149
|
export declare function collabCloudSuccessEnvelope<T>(requestId: string, data: T): CollabCloudSuccessEnvelope<T>;
|
|
144
150
|
export declare function decodeCollabCloudSuccessEnvelope<T = unknown>(value: unknown): CollabCloudSuccessEnvelope<T>;
|
|
145
|
-
export declare function collabCloudErrorEnvelope(requestId: string, error: CollabError): CollabCloudErrorEnvelope;
|
|
151
|
+
export declare function collabCloudErrorEnvelope(requestId: string, error: CollabError, mutationOutcome?: CollabCloudErrorEnvelope['mutationOutcome']): CollabCloudErrorEnvelope;
|
|
146
152
|
export declare function decodeCollabCloudErrorEnvelope(value: unknown): CollabCloudErrorEnvelope;
|
|
@@ -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 =
|
|
26
|
+
exports.COLLAB_CLOUD_BINDING_VERSION = 5;
|
|
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', `/
|
|
191
|
+
return route('POST', `/v5/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', `/
|
|
201
|
+
return route('GET', `/v5/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', `/
|
|
212
|
+
return route('GET', `/v5/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', `/
|
|
216
|
+
return route('POST', `/v5/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', `/
|
|
227
|
+
return route(direction === 'upload' ? 'PUT' : 'GET', `/v5/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', `/
|
|
240
|
+
return route('GET', `/v5/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', `/
|
|
252
|
+
return route(operation === 'begin' ? 'POST' : 'GET', `/v5/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 = '/
|
|
290
|
+
const base = '/v5/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] === '
|
|
349
|
+
if (segments[0] === 'v5'
|
|
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] !== '
|
|
428
|
+
if (segments[0] !== 'v5'
|
|
429
429
|
|| segments[1] !== 'development'
|
|
430
430
|
|| segments[2] !== 'bootstrap'
|
|
431
431
|
|| segments[3] !== 'attempts'
|
|
@@ -533,21 +533,32 @@ function decodeCollabCloudSuccessEnvelope(value) {
|
|
|
533
533
|
requestId: source.requestId,
|
|
534
534
|
});
|
|
535
535
|
}
|
|
536
|
-
function collabCloudErrorEnvelope(requestId, error) {
|
|
536
|
+
function collabCloudErrorEnvelope(requestId, error, mutationOutcome) {
|
|
537
537
|
if (!(0, CollabValidation_1.isCollabOpaqueId)(requestId))
|
|
538
538
|
throw invalidPayload('requestId');
|
|
539
|
+
if (mutationOutcome !== undefined && mutationOutcome !== 'rejected') {
|
|
540
|
+
throw invalidPayload('mutationOutcome');
|
|
541
|
+
}
|
|
539
542
|
return Object.freeze({
|
|
540
543
|
error: Object.freeze({
|
|
541
544
|
code: error.code,
|
|
542
545
|
recoveryActions: Object.freeze([...error.recoveryActions]),
|
|
543
546
|
safeContext: error.safeContext,
|
|
544
547
|
}),
|
|
548
|
+
...(mutationOutcome === undefined ? {} : { mutationOutcome }),
|
|
545
549
|
protocolVersion: CollabConstants_1.COLLAB_PROTOCOL_VERSION,
|
|
546
550
|
requestId,
|
|
547
551
|
});
|
|
548
552
|
}
|
|
549
553
|
function decodeCollabCloudErrorEnvelope(value) {
|
|
550
|
-
const source =
|
|
554
|
+
const source = record(value, 'envelope');
|
|
555
|
+
const hasOutcome = Object.hasOwn(source, 'mutationOutcome');
|
|
556
|
+
if (!hasExactKeys(source, hasOutcome
|
|
557
|
+
? ['error', 'mutationOutcome', 'protocolVersion', 'requestId']
|
|
558
|
+
: ['error', 'protocolVersion', 'requestId']))
|
|
559
|
+
throw invalidPayload('envelope');
|
|
560
|
+
if (hasOutcome && source.mutationOutcome !== 'rejected')
|
|
561
|
+
throw invalidPayload('mutationOutcome');
|
|
551
562
|
const wireError = exactRecord(source.error, 'error', [
|
|
552
563
|
'code',
|
|
553
564
|
'recoveryActions',
|
|
@@ -573,6 +584,7 @@ function decodeCollabCloudErrorEnvelope(value) {
|
|
|
573
584
|
]),
|
|
574
585
|
safeContext,
|
|
575
586
|
}),
|
|
587
|
+
...(hasOutcome ? { mutationOutcome: 'rejected' } : {}),
|
|
576
588
|
protocolVersion: CollabConstants_1.COLLAB_PROTOCOL_VERSION,
|
|
577
589
|
requestId: source.requestId,
|
|
578
590
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const COLLAB_PROTOCOL_VERSION:
|
|
1
|
+
export declare const COLLAB_PROTOCOL_VERSION: 9;
|
|
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<{
|
package/dist/CollabConstants.js
CHANGED
|
@@ -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 =
|
|
4
|
+
exports.COLLAB_PROTOCOL_VERSION = 9;
|
|
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({
|
|
@@ -1520,7 +1520,10 @@ function validateContinuity(records) {
|
|
|
1520
1520
|
const recoveryEvidenceInvalid = direction === null
|
|
1521
1521
|
|| (direction === 'lan-to-cloud' && recovery.value.sourceEvidence === null)
|
|
1522
1522
|
|| (targetEvidenceRequired && recovery.value.targetEvidence === null)
|
|
1523
|
-
|| (
|
|
1523
|
+
|| (isCancellation
|
|
1524
|
+
? (lifecycle.value.batchRevision !== null)
|
|
1525
|
+
!== (recovery.value.stageSha256 !== null)
|
|
1526
|
+
: stageRequired !== (recovery.value.stageSha256 !== null))
|
|
1524
1527
|
|| (direction === 'lan-to-cloud'
|
|
1525
1528
|
&& recovery.value.stageSha256 !== null
|
|
1526
1529
|
&& recovery.value.stageSha256 !== lifecycle.value.checkpointSha256)
|
|
@@ -1543,33 +1546,51 @@ function validateContinuity(records) {
|
|
|
1543
1546
|
if (projectAuthorityGeneration !== expectedProjectAuthorityGeneration) {
|
|
1544
1547
|
throw invalidPayload('records');
|
|
1545
1548
|
}
|
|
1549
|
+
const hostMemberId = direction === 'lan-to-cloud'
|
|
1550
|
+
? recovery.value.sourceHostMemberId
|
|
1551
|
+
: recovery.value.targetHostMemberId;
|
|
1552
|
+
const transferMemberIds = [...memberRecords.values()]
|
|
1553
|
+
.filter(item => (item.value.status === 'active'
|
|
1554
|
+
&& item.value.memberId !== hostMemberId
|
|
1555
|
+
&& Date.parse(item.value.createdAt) <= Date.parse(recovery.value.createdAt)))
|
|
1556
|
+
.map(item => item.value.memberId);
|
|
1546
1557
|
if (!isCancellation) {
|
|
1547
|
-
const hostMemberId = direction === 'lan-to-cloud'
|
|
1548
|
-
? recovery.value.sourceHostMemberId
|
|
1549
|
-
: recovery.value.targetHostMemberId;
|
|
1550
|
-
const eligibleMemberIds = [...memberRecords.values()]
|
|
1551
|
-
.filter(item => item.value.status === 'active' && item.value.memberId !== hostMemberId)
|
|
1552
|
-
.map(item => item.value.memberId);
|
|
1553
|
-
const memberCustodyRequired = normalPhaseIndex >= 3;
|
|
1554
1558
|
const batchReceiptRequired = direction === 'cloud-to-lan'
|
|
1555
1559
|
? normalPhaseIndex >= 3
|
|
1556
1560
|
: normalPhaseIndex >= 4;
|
|
1557
1561
|
if ((batchReceipt !== undefined) !== batchReceiptRequired) {
|
|
1558
1562
|
throw invalidPayload('records');
|
|
1559
1563
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1564
|
+
}
|
|
1565
|
+
const memberCustody = transferMemberIds.map((memberId) => {
|
|
1566
|
+
const identity = `${recovery.value.transferId}:${memberId}`;
|
|
1567
|
+
const claimPresent = claims.has(identity);
|
|
1568
|
+
const envelopePresent = protectedEnvelopes.has(identity);
|
|
1569
|
+
const redemptionPresent = redemptionReceipts.has(identity);
|
|
1570
|
+
return {
|
|
1571
|
+
absent: !claimPresent && !envelopePresent && !redemptionPresent,
|
|
1572
|
+
exact: direction === 'lan-to-cloud'
|
|
1566
1573
|
? claimPresent && !envelopePresent
|
|
1567
|
-
: !claimPresent && envelopePresent !== redemptionPresent
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1574
|
+
: !claimPresent && envelopePresent !== redemptionPresent,
|
|
1575
|
+
};
|
|
1576
|
+
});
|
|
1577
|
+
const cleanupInProgress = isCancellation
|
|
1578
|
+
&& cancellationIndex === 1
|
|
1579
|
+
&& lifecycle.value.batchRevision !== null;
|
|
1580
|
+
if (cleanupInProgress) {
|
|
1581
|
+
const allAbsent = memberCustody.every(item => item.absent);
|
|
1582
|
+
const allExact = memberCustody.every(item => item.exact);
|
|
1583
|
+
if (!allAbsent && !allExact) {
|
|
1584
|
+
throw invalidPayload('records');
|
|
1571
1585
|
}
|
|
1572
1586
|
}
|
|
1587
|
+
else {
|
|
1588
|
+
const memberCustodyRequired = isCancellation
|
|
1589
|
+
? !cleanupCompleted && lifecycle.value.batchRevision !== null
|
|
1590
|
+
: normalPhaseIndex >= 3;
|
|
1591
|
+
if (memberCustody.some(item => (memberCustodyRequired ? !item.exact : !item.absent)))
|
|
1592
|
+
throw invalidPayload('records');
|
|
1593
|
+
}
|
|
1573
1594
|
if (recovery.value.targetEvidence !== null && (recovery.value.targetHostMemberId === null
|
|
1574
1595
|
|| principalBindings.get(recovery.value.targetHostMemberId)
|
|
1575
1596
|
!== recovery.value.targetEvidence.principalId))
|
|
@@ -1585,6 +1606,7 @@ function validateContinuity(records) {
|
|
|
1585
1606
|
? 'cloud-to-lan'
|
|
1586
1607
|
: 'lan-to-cloud')
|
|
1587
1608
|
|| lifecycle.value.expectedAuthorityGeneration !== recovery.value.sourceAuthority.generation
|
|
1609
|
+
|| lifecycle.value.createdAt !== recovery.value.createdAt
|
|
1588
1610
|
|| lifecycle.value.scheduledAt !== recovery.value.expiresAt
|
|
1589
1611
|
|| (recovery.value.sourceHostMemberId !== null
|
|
1590
1612
|
&& !members.has(recovery.value.sourceHostMemberId))
|
|
@@ -1640,6 +1662,7 @@ function validateContinuity(records) {
|
|
|
1640
1662
|
for (const claim of claims.values()) {
|
|
1641
1663
|
const lifecycle = lifecycles.get(claim.value.transferId);
|
|
1642
1664
|
const recovery = recoveries.get(claim.value.transferId);
|
|
1665
|
+
const member = memberRecords.get(claim.value.memberId);
|
|
1643
1666
|
const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
|
|
1644
1667
|
const redeemedOverride = redemptionReceipt === undefined
|
|
1645
1668
|
? undefined
|
|
@@ -1648,8 +1671,9 @@ function validateContinuity(records) {
|
|
|
1648
1671
|
&& item.value.state === 'redeemed'
|
|
1649
1672
|
&& item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
|
|
1650
1673
|
&& item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
|
|
1651
|
-
if (
|
|
1674
|
+
if (member?.value.status !== 'active'
|
|
1652
1675
|
|| recovery === undefined
|
|
1676
|
+
|| Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
|
|
1653
1677
|
|| recovery.value.sourceAuthority.kind !== 'lan'
|
|
1654
1678
|
|| claim.value.memberId === recovery.value.sourceHostMemberId
|
|
1655
1679
|
|| lifecycle?.value.batchRevision !== claim.value.batchRevision
|
|
@@ -1832,10 +1856,12 @@ function validateContinuity(records) {
|
|
|
1832
1856
|
const identity = `${item.value.transferId}:${item.value.memberId}`;
|
|
1833
1857
|
const recovery = recoveries.get(item.value.transferId);
|
|
1834
1858
|
const lifecycle = lifecycles.get(item.value.transferId);
|
|
1859
|
+
const member = memberRecords.get(item.value.memberId);
|
|
1835
1860
|
if (item.recordId !== identity
|
|
1836
1861
|
|| recovery === undefined
|
|
1837
1862
|
|| lifecycle === undefined
|
|
1838
|
-
||
|
|
1863
|
+
|| member?.value.status !== 'active'
|
|
1864
|
+
|| Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
|
|
1839
1865
|
|| item.value.memberId === recovery.value.targetHostMemberId
|
|
1840
1866
|
|| item.value.associatedData.authorityGeneration
|
|
1841
1867
|
!== recovery.value.sourceAuthority.generation
|
|
@@ -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 =
|
|
6
|
+
export const COLLAB_CLOUD_BINDING_VERSION = 5;
|
|
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', `/
|
|
171
|
+
return route('POST', `/v5/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', `/
|
|
181
|
+
return route('GET', `/v5/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', `/
|
|
192
|
+
return route('GET', `/v5/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', `/
|
|
196
|
+
return route('POST', `/v5/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', `/
|
|
207
|
+
return route(direction === 'upload' ? 'PUT' : 'GET', `/v5/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', `/
|
|
220
|
+
return route('GET', `/v5/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', `/
|
|
232
|
+
return route(operation === 'begin' ? 'POST' : 'GET', `/v5/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 = '/
|
|
270
|
+
const base = '/v5/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] === '
|
|
329
|
+
if (segments[0] === 'v5'
|
|
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] !== '
|
|
408
|
+
if (segments[0] !== 'v5'
|
|
409
409
|
|| segments[1] !== 'development'
|
|
410
410
|
|| segments[2] !== 'bootstrap'
|
|
411
411
|
|| segments[3] !== 'attempts'
|
|
@@ -513,21 +513,32 @@ export function decodeCollabCloudSuccessEnvelope(value) {
|
|
|
513
513
|
requestId: source.requestId,
|
|
514
514
|
});
|
|
515
515
|
}
|
|
516
|
-
export function collabCloudErrorEnvelope(requestId, error) {
|
|
516
|
+
export function collabCloudErrorEnvelope(requestId, error, mutationOutcome) {
|
|
517
517
|
if (!isCollabOpaqueId(requestId))
|
|
518
518
|
throw invalidPayload('requestId');
|
|
519
|
+
if (mutationOutcome !== undefined && mutationOutcome !== 'rejected') {
|
|
520
|
+
throw invalidPayload('mutationOutcome');
|
|
521
|
+
}
|
|
519
522
|
return Object.freeze({
|
|
520
523
|
error: Object.freeze({
|
|
521
524
|
code: error.code,
|
|
522
525
|
recoveryActions: Object.freeze([...error.recoveryActions]),
|
|
523
526
|
safeContext: error.safeContext,
|
|
524
527
|
}),
|
|
528
|
+
...(mutationOutcome === undefined ? {} : { mutationOutcome }),
|
|
525
529
|
protocolVersion: COLLAB_PROTOCOL_VERSION,
|
|
526
530
|
requestId,
|
|
527
531
|
});
|
|
528
532
|
}
|
|
529
533
|
export function decodeCollabCloudErrorEnvelope(value) {
|
|
530
|
-
const source =
|
|
534
|
+
const source = record(value, 'envelope');
|
|
535
|
+
const hasOutcome = Object.hasOwn(source, 'mutationOutcome');
|
|
536
|
+
if (!hasExactKeys(source, hasOutcome
|
|
537
|
+
? ['error', 'mutationOutcome', 'protocolVersion', 'requestId']
|
|
538
|
+
: ['error', 'protocolVersion', 'requestId']))
|
|
539
|
+
throw invalidPayload('envelope');
|
|
540
|
+
if (hasOutcome && source.mutationOutcome !== 'rejected')
|
|
541
|
+
throw invalidPayload('mutationOutcome');
|
|
531
542
|
const wireError = exactRecord(source.error, 'error', [
|
|
532
543
|
'code',
|
|
533
544
|
'recoveryActions',
|
|
@@ -553,6 +564,7 @@ export function decodeCollabCloudErrorEnvelope(value) {
|
|
|
553
564
|
]),
|
|
554
565
|
safeContext,
|
|
555
566
|
}),
|
|
567
|
+
...(hasOutcome ? { mutationOutcome: 'rejected' } : {}),
|
|
556
568
|
protocolVersion: COLLAB_PROTOCOL_VERSION,
|
|
557
569
|
requestId: source.requestId,
|
|
558
570
|
});
|
|
@@ -1510,7 +1510,10 @@ function validateContinuity(records) {
|
|
|
1510
1510
|
const recoveryEvidenceInvalid = direction === null
|
|
1511
1511
|
|| (direction === 'lan-to-cloud' && recovery.value.sourceEvidence === null)
|
|
1512
1512
|
|| (targetEvidenceRequired && recovery.value.targetEvidence === null)
|
|
1513
|
-
|| (
|
|
1513
|
+
|| (isCancellation
|
|
1514
|
+
? (lifecycle.value.batchRevision !== null)
|
|
1515
|
+
!== (recovery.value.stageSha256 !== null)
|
|
1516
|
+
: stageRequired !== (recovery.value.stageSha256 !== null))
|
|
1514
1517
|
|| (direction === 'lan-to-cloud'
|
|
1515
1518
|
&& recovery.value.stageSha256 !== null
|
|
1516
1519
|
&& recovery.value.stageSha256 !== lifecycle.value.checkpointSha256)
|
|
@@ -1533,33 +1536,51 @@ function validateContinuity(records) {
|
|
|
1533
1536
|
if (projectAuthorityGeneration !== expectedProjectAuthorityGeneration) {
|
|
1534
1537
|
throw invalidPayload('records');
|
|
1535
1538
|
}
|
|
1539
|
+
const hostMemberId = direction === 'lan-to-cloud'
|
|
1540
|
+
? recovery.value.sourceHostMemberId
|
|
1541
|
+
: recovery.value.targetHostMemberId;
|
|
1542
|
+
const transferMemberIds = [...memberRecords.values()]
|
|
1543
|
+
.filter(item => (item.value.status === 'active'
|
|
1544
|
+
&& item.value.memberId !== hostMemberId
|
|
1545
|
+
&& Date.parse(item.value.createdAt) <= Date.parse(recovery.value.createdAt)))
|
|
1546
|
+
.map(item => item.value.memberId);
|
|
1536
1547
|
if (!isCancellation) {
|
|
1537
|
-
const hostMemberId = direction === 'lan-to-cloud'
|
|
1538
|
-
? recovery.value.sourceHostMemberId
|
|
1539
|
-
: recovery.value.targetHostMemberId;
|
|
1540
|
-
const eligibleMemberIds = [...memberRecords.values()]
|
|
1541
|
-
.filter(item => item.value.status === 'active' && item.value.memberId !== hostMemberId)
|
|
1542
|
-
.map(item => item.value.memberId);
|
|
1543
|
-
const memberCustodyRequired = normalPhaseIndex >= 3;
|
|
1544
1548
|
const batchReceiptRequired = direction === 'cloud-to-lan'
|
|
1545
1549
|
? normalPhaseIndex >= 3
|
|
1546
1550
|
: normalPhaseIndex >= 4;
|
|
1547
1551
|
if ((batchReceipt !== undefined) !== batchReceiptRequired) {
|
|
1548
1552
|
throw invalidPayload('records');
|
|
1549
1553
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1554
|
+
}
|
|
1555
|
+
const memberCustody = transferMemberIds.map((memberId) => {
|
|
1556
|
+
const identity = `${recovery.value.transferId}:${memberId}`;
|
|
1557
|
+
const claimPresent = claims.has(identity);
|
|
1558
|
+
const envelopePresent = protectedEnvelopes.has(identity);
|
|
1559
|
+
const redemptionPresent = redemptionReceipts.has(identity);
|
|
1560
|
+
return {
|
|
1561
|
+
absent: !claimPresent && !envelopePresent && !redemptionPresent,
|
|
1562
|
+
exact: direction === 'lan-to-cloud'
|
|
1556
1563
|
? claimPresent && !envelopePresent
|
|
1557
|
-
: !claimPresent && envelopePresent !== redemptionPresent
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1564
|
+
: !claimPresent && envelopePresent !== redemptionPresent,
|
|
1565
|
+
};
|
|
1566
|
+
});
|
|
1567
|
+
const cleanupInProgress = isCancellation
|
|
1568
|
+
&& cancellationIndex === 1
|
|
1569
|
+
&& lifecycle.value.batchRevision !== null;
|
|
1570
|
+
if (cleanupInProgress) {
|
|
1571
|
+
const allAbsent = memberCustody.every(item => item.absent);
|
|
1572
|
+
const allExact = memberCustody.every(item => item.exact);
|
|
1573
|
+
if (!allAbsent && !allExact) {
|
|
1574
|
+
throw invalidPayload('records');
|
|
1561
1575
|
}
|
|
1562
1576
|
}
|
|
1577
|
+
else {
|
|
1578
|
+
const memberCustodyRequired = isCancellation
|
|
1579
|
+
? !cleanupCompleted && lifecycle.value.batchRevision !== null
|
|
1580
|
+
: normalPhaseIndex >= 3;
|
|
1581
|
+
if (memberCustody.some(item => (memberCustodyRequired ? !item.exact : !item.absent)))
|
|
1582
|
+
throw invalidPayload('records');
|
|
1583
|
+
}
|
|
1563
1584
|
if (recovery.value.targetEvidence !== null && (recovery.value.targetHostMemberId === null
|
|
1564
1585
|
|| principalBindings.get(recovery.value.targetHostMemberId)
|
|
1565
1586
|
!== recovery.value.targetEvidence.principalId))
|
|
@@ -1575,6 +1596,7 @@ function validateContinuity(records) {
|
|
|
1575
1596
|
? 'cloud-to-lan'
|
|
1576
1597
|
: 'lan-to-cloud')
|
|
1577
1598
|
|| lifecycle.value.expectedAuthorityGeneration !== recovery.value.sourceAuthority.generation
|
|
1599
|
+
|| lifecycle.value.createdAt !== recovery.value.createdAt
|
|
1578
1600
|
|| lifecycle.value.scheduledAt !== recovery.value.expiresAt
|
|
1579
1601
|
|| (recovery.value.sourceHostMemberId !== null
|
|
1580
1602
|
&& !members.has(recovery.value.sourceHostMemberId))
|
|
@@ -1630,6 +1652,7 @@ function validateContinuity(records) {
|
|
|
1630
1652
|
for (const claim of claims.values()) {
|
|
1631
1653
|
const lifecycle = lifecycles.get(claim.value.transferId);
|
|
1632
1654
|
const recovery = recoveries.get(claim.value.transferId);
|
|
1655
|
+
const member = memberRecords.get(claim.value.memberId);
|
|
1633
1656
|
const redemptionReceipt = redemptionReceipts.get(`${claim.value.transferId}:${claim.value.memberId}`);
|
|
1634
1657
|
const redeemedOverride = redemptionReceipt === undefined
|
|
1635
1658
|
? undefined
|
|
@@ -1638,8 +1661,9 @@ function validateContinuity(records) {
|
|
|
1638
1661
|
&& item.value.state === 'redeemed'
|
|
1639
1662
|
&& item.value.claimSha256 === redemptionReceipt.value.receipt.claimSha256
|
|
1640
1663
|
&& item.value.redemptionReceiptId === redemptionReceipt.value.receipt.receiptId));
|
|
1641
|
-
if (
|
|
1664
|
+
if (member?.value.status !== 'active'
|
|
1642
1665
|
|| recovery === undefined
|
|
1666
|
+
|| Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
|
|
1643
1667
|
|| recovery.value.sourceAuthority.kind !== 'lan'
|
|
1644
1668
|
|| claim.value.memberId === recovery.value.sourceHostMemberId
|
|
1645
1669
|
|| lifecycle?.value.batchRevision !== claim.value.batchRevision
|
|
@@ -1822,10 +1846,12 @@ function validateContinuity(records) {
|
|
|
1822
1846
|
const identity = `${item.value.transferId}:${item.value.memberId}`;
|
|
1823
1847
|
const recovery = recoveries.get(item.value.transferId);
|
|
1824
1848
|
const lifecycle = lifecycles.get(item.value.transferId);
|
|
1849
|
+
const member = memberRecords.get(item.value.memberId);
|
|
1825
1850
|
if (item.recordId !== identity
|
|
1826
1851
|
|| recovery === undefined
|
|
1827
1852
|
|| lifecycle === undefined
|
|
1828
|
-
||
|
|
1853
|
+
|| member?.value.status !== 'active'
|
|
1854
|
+
|| Date.parse(member.value.createdAt) > Date.parse(recovery.value.createdAt)
|
|
1829
1855
|
|| item.value.memberId === recovery.value.targetHostMemberId
|
|
1830
1856
|
|| item.value.associatedData.authorityGeneration
|
|
1831
1857
|
!== recovery.value.sourceAuthority.generation
|