@claudian-collab/protocol 3.2.0 → 3.3.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 +8 -6
- package/dist/CollabAuthorityTransfer.d.ts +1 -1
- package/dist/CollabCloudBinding.d.ts +1 -1
- package/dist/CollabCloudBinding.js +7 -0
- package/dist/CollabControlOperationCodecs.d.ts +18 -0
- package/dist/CollabControlOperationCodecs.js +2 -0
- package/dist/CollabProjectBackupCheckpoint.d.ts +93 -4
- package/dist/CollabProjectBackupCheckpoint.js +640 -12
- package/dist/CollabProjectMembership.d.ts +318 -0
- package/dist/CollabProjectMembership.js +714 -0
- package/dist/CollabProtocol.d.ts +2 -1
- package/dist/esm/CollabAuthorityTransfer.mjs +884 -0
- package/dist/esm/CollabCloudBinding.mjs +559 -0
- package/dist/esm/CollabCloudProjectEvent.mjs +150 -0
- package/dist/esm/CollabCloudProjectSnapshot.mjs +277 -0
- package/dist/esm/CollabConstants.mjs +31 -0
- package/dist/esm/CollabControlOperationCodecs.mjs +106 -0
- package/dist/esm/CollabError.mjs +175 -0
- package/dist/esm/CollabMarkdownProse.mjs +54 -0
- package/dist/esm/CollabMemberMentionParser.mjs +48 -0
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +2101 -0
- package/dist/esm/CollabProjectCheckpoint.mjs +1326 -0
- package/dist/esm/CollabProjectMembership.mjs +709 -0
- package/dist/esm/CollabProjectRetirement.mjs +119 -0
- package/dist/esm/CollabProtocol.mjs +64 -0
- package/dist/esm/CollabRequestTicketRequestCodecs.mjs +263 -0
- package/dist/esm/CollabRequestTicketResponseCodecs.mjs +353 -0
- package/dist/esm/CollabTicketReferenceParser.mjs +52 -0
- package/dist/esm/CollabValidation.mjs +19 -0
- package/dist/esm/DevelopmentBootstrap.mjs +537 -0
- package/dist/esm/index.mjs +17 -0
- package/dist/esm/types.mjs +8 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +10 -3
- package/package.json +5 -2
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import { COLLAB_LIMITS, COLLAB_PROTOCOL_VERSION } from './CollabConstants.mjs';
|
|
2
|
+
import { COLLAB_CONTROL_OPERATION_CODECS, } from './CollabControlOperationCodecs.mjs';
|
|
3
|
+
import { COLLAB_ERROR_CODES, CollabError, sanitizeCollabDiagnosticContext, } from './CollabError.mjs';
|
|
4
|
+
import { isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
|
|
5
|
+
import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_PROJECT_CHECKPOINT_ARTIFACTS, } from './CollabProjectCheckpoint.mjs';
|
|
6
|
+
export const COLLAB_CLOUD_BINDING_VERSION = 2;
|
|
7
|
+
export const COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION = 2;
|
|
8
|
+
export const COLLAB_CLOUD_CAPABILITIES = Object.freeze([
|
|
9
|
+
'accept',
|
|
10
|
+
'authority-transfer',
|
|
11
|
+
'cloud-imported-membership-claims',
|
|
12
|
+
'cloud-project-create',
|
|
13
|
+
'cloud-project-invitations',
|
|
14
|
+
'cloud-project-join',
|
|
15
|
+
'cloud-project-leave',
|
|
16
|
+
'cloud-project-manager-responsibility',
|
|
17
|
+
'cloud-project-membership',
|
|
18
|
+
'development-bootstrap',
|
|
19
|
+
'git-receive-pack-personal-ref',
|
|
20
|
+
'git-upload-pack',
|
|
21
|
+
'project-checkpoint-export',
|
|
22
|
+
'project-events',
|
|
23
|
+
'project-retirement',
|
|
24
|
+
'project-snapshot',
|
|
25
|
+
'requests',
|
|
26
|
+
'tickets',
|
|
27
|
+
]);
|
|
28
|
+
export const COLLAB_CLOUD_JSON_OPERATIONS = Object.freeze([
|
|
29
|
+
'getProjectSnapshot',
|
|
30
|
+
...Object.keys(COLLAB_CONTROL_OPERATION_CODECS),
|
|
31
|
+
]);
|
|
32
|
+
export const COLLAB_CLOUD_BINDING_LIMITS = Object.freeze({
|
|
33
|
+
bootstrapAttemptTtlMs: 24 * 60 * 60 * 1_000,
|
|
34
|
+
defaultMaxConcurrentBootstrapUploads: 1,
|
|
35
|
+
eventHeartbeatMs: 30_000,
|
|
36
|
+
eventMissedHeartbeatLimit: 2,
|
|
37
|
+
maxCloudOpenRequests: 100,
|
|
38
|
+
maxCloudProjectMembers: 100,
|
|
39
|
+
maxCloudSnapshotUtf8Bytes: 448 * 1024,
|
|
40
|
+
maxCloudTicketHighlights: 5,
|
|
41
|
+
maxCheckpointCoordinationBytes: COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxCoordinationBytes,
|
|
42
|
+
maxCheckpointManifestUtf8Bytes: COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxManifestBytes,
|
|
43
|
+
maxCheckpointRepositoryBundleBytes: COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxRepositoryBundleBytes,
|
|
44
|
+
maxCheckpointStagingBytes: COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxStagingBytes,
|
|
45
|
+
maxDevelopmentBootstrapGitBundleBytes: 1024 * 1024 * 1024,
|
|
46
|
+
maxDevelopmentBootstrapManifestUtf8Bytes: 64 * 1024,
|
|
47
|
+
maxDevelopmentBootstrapReportUtf8Bytes: 64 * 1024,
|
|
48
|
+
maxDevelopmentBootstrapRepositoryBytes: 1024 * 1024 * 1024,
|
|
49
|
+
maxDevelopmentBootstrapStagingBytes: 2 * 1024 * 1024 * 1024,
|
|
50
|
+
maxEventReplay: 500,
|
|
51
|
+
maxGitReceivePackBytes: 256 * 1024 * 1024,
|
|
52
|
+
maxRepositoryBytes: 1024 * 1024 * 1024,
|
|
53
|
+
minRetainedEventCount: 10_000,
|
|
54
|
+
maxUploadsPerBootstrapAttempt: 1,
|
|
55
|
+
minEventRetentionDays: 30,
|
|
56
|
+
uploadDeadlineMs: 15 * 60 * 1_000,
|
|
57
|
+
uploadIdleTimeoutMs: 30_000,
|
|
58
|
+
});
|
|
59
|
+
const CLOUD_CAPABILITY_SET = new Set(COLLAB_CLOUD_CAPABILITIES);
|
|
60
|
+
const CLOUD_JSON_OPERATION_SET = new Set(COLLAB_CLOUD_JSON_OPERATIONS);
|
|
61
|
+
const COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET = new Set(COLLAB_PROJECT_CHECKPOINT_ARTIFACTS);
|
|
62
|
+
const COLLAB_ERROR_CODE_SET = new Set(COLLAB_ERROR_CODES);
|
|
63
|
+
const RECOVERY_ACTION_SET = new Set([
|
|
64
|
+
'retry',
|
|
65
|
+
'review-conflicts',
|
|
66
|
+
'request-access',
|
|
67
|
+
]);
|
|
68
|
+
const CAPABILITY_TOKEN_PATTERN = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
69
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
70
|
+
function invalidPayload(field) {
|
|
71
|
+
return new CollabError({
|
|
72
|
+
code: 'protocol-payload-invalid',
|
|
73
|
+
safeContext: { field },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function invalidRoute() {
|
|
77
|
+
throw new RangeError('Invalid Collab Cloud route input');
|
|
78
|
+
}
|
|
79
|
+
function record(value, field) {
|
|
80
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
81
|
+
throw invalidPayload(field);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function hasExactKeys(value, keys) {
|
|
86
|
+
const expected = new Set(keys);
|
|
87
|
+
return keys.every(key => Object.hasOwn(value, key))
|
|
88
|
+
&& Object.keys(value).every(key => expected.has(key));
|
|
89
|
+
}
|
|
90
|
+
function exactRecord(value, field, keys) {
|
|
91
|
+
const source = record(value, field);
|
|
92
|
+
if (!hasExactKeys(source, keys))
|
|
93
|
+
throw invalidPayload(field);
|
|
94
|
+
return source;
|
|
95
|
+
}
|
|
96
|
+
function positiveSafeInteger(value, field, maximum) {
|
|
97
|
+
if (typeof value !== 'number'
|
|
98
|
+
|| !Number.isSafeInteger(value)
|
|
99
|
+
|| value < 1
|
|
100
|
+
|| (maximum !== undefined && value > maximum)) {
|
|
101
|
+
throw invalidPayload(field);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
function sortedUniqueIntegers(value, field) {
|
|
106
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
107
|
+
throw invalidPayload(field);
|
|
108
|
+
const decoded = value.map(item => positiveSafeInteger(item, field));
|
|
109
|
+
if (decoded.some((item, index) => index > 0 && decoded[index - 1] >= item)) {
|
|
110
|
+
throw invalidPayload(field);
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze(decoded);
|
|
113
|
+
}
|
|
114
|
+
function sortedUniqueTokens(value, field) {
|
|
115
|
+
if (!Array.isArray(value))
|
|
116
|
+
throw invalidPayload(field);
|
|
117
|
+
const decoded = value.map((item) => {
|
|
118
|
+
if (typeof item !== 'string' || !CAPABILITY_TOKEN_PATTERN.test(item)) {
|
|
119
|
+
throw invalidPayload(field);
|
|
120
|
+
}
|
|
121
|
+
return item;
|
|
122
|
+
});
|
|
123
|
+
if (decoded.some((item, index) => index > 0 && decoded[index - 1].localeCompare(item, 'en-US') >= 0)) {
|
|
124
|
+
throw invalidPayload(field);
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze(decoded);
|
|
127
|
+
}
|
|
128
|
+
function requireSupportedVersion(versions, supportedVersion, kind) {
|
|
129
|
+
if (versions.length === 1 && versions[0] === supportedVersion)
|
|
130
|
+
return;
|
|
131
|
+
throw new CollabError({
|
|
132
|
+
code: 'protocol-version-unsupported',
|
|
133
|
+
safeContext: {
|
|
134
|
+
kind,
|
|
135
|
+
receivedVersion: versions.find(version => version !== supportedVersion)
|
|
136
|
+
?? versions[0]
|
|
137
|
+
?? 0,
|
|
138
|
+
supportedVersion,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function route(method, target, match) {
|
|
143
|
+
return Object.freeze({ match: Object.freeze(match), method, target });
|
|
144
|
+
}
|
|
145
|
+
function assertProjectId(projectId) {
|
|
146
|
+
if (!isCollabProjectId(projectId))
|
|
147
|
+
invalidRoute();
|
|
148
|
+
}
|
|
149
|
+
function assertAttemptId(attemptId) {
|
|
150
|
+
if (!isCollabOpaqueId(attemptId))
|
|
151
|
+
invalidRoute();
|
|
152
|
+
}
|
|
153
|
+
function isCloudJsonOperation(value) {
|
|
154
|
+
return CLOUD_JSON_OPERATION_SET.has(value);
|
|
155
|
+
}
|
|
156
|
+
function isoTimestamp(value, field) {
|
|
157
|
+
if (typeof value !== 'string'
|
|
158
|
+
|| value.length > 64
|
|
159
|
+
|| Number.isNaN(Date.parse(value))
|
|
160
|
+
|| new Date(value).toISOString() !== value)
|
|
161
|
+
throw invalidPayload(field);
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
export function collabCloudCapabilitiesRoute() {
|
|
165
|
+
return route('GET', '/collab/capabilities', { kind: 'capabilities' });
|
|
166
|
+
}
|
|
167
|
+
export function collabCloudProjectOperationRoute(projectId, operation) {
|
|
168
|
+
assertProjectId(projectId);
|
|
169
|
+
if (!isCloudJsonOperation(operation))
|
|
170
|
+
invalidRoute();
|
|
171
|
+
return route('POST', `/v2/projects/${projectId}/operations/${operation}`, {
|
|
172
|
+
kind: 'project-operation',
|
|
173
|
+
operation,
|
|
174
|
+
projectId,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export function collabCloudProjectEventsRoute(projectId, afterSequence) {
|
|
178
|
+
assertProjectId(projectId);
|
|
179
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0)
|
|
180
|
+
invalidRoute();
|
|
181
|
+
return route('GET', `/v2/projects/${projectId}/events?afterSequence=${afterSequence}`, {
|
|
182
|
+
afterSequence,
|
|
183
|
+
kind: 'project-events',
|
|
184
|
+
projectId,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export function collabCloudGitRoute(projectId, routeKind, service) {
|
|
188
|
+
assertProjectId(projectId);
|
|
189
|
+
if (routeKind === 'info-refs') {
|
|
190
|
+
if (service !== 'git-upload-pack' && service !== 'git-receive-pack')
|
|
191
|
+
invalidRoute();
|
|
192
|
+
return route('GET', `/v2/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
|
|
193
|
+
}
|
|
194
|
+
if (service !== undefined)
|
|
195
|
+
invalidRoute();
|
|
196
|
+
return route('POST', `/v2/projects/${projectId}/repository.git/${routeKind}`, {
|
|
197
|
+
kind: routeKind,
|
|
198
|
+
projectId,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export function collabCloudAuthorityTransferArtifactRoute(projectId, transferId, direction, artifact) {
|
|
202
|
+
assertProjectId(projectId);
|
|
203
|
+
assertAttemptId(transferId);
|
|
204
|
+
if (direction !== 'download' && direction !== 'upload'
|
|
205
|
+
|| !COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
|
|
206
|
+
invalidRoute();
|
|
207
|
+
return route(direction === 'upload' ? 'PUT' : 'GET', `/v2/projects/${projectId}/authority-transfers/${transferId}/checkpoint/${artifact}`, {
|
|
208
|
+
artifact,
|
|
209
|
+
direction,
|
|
210
|
+
kind: 'authority-transfer-artifact',
|
|
211
|
+
projectId,
|
|
212
|
+
transferId,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
export function collabCloudProjectCheckpointExportArtifactRoute(projectId, exportId, artifact) {
|
|
216
|
+
assertProjectId(projectId);
|
|
217
|
+
assertAttemptId(exportId);
|
|
218
|
+
if (!COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(artifact))
|
|
219
|
+
invalidRoute();
|
|
220
|
+
return route('GET', `/v2/projects/${projectId}/checkpoint-exports/${exportId}/checkpoint/${artifact}`, {
|
|
221
|
+
artifact,
|
|
222
|
+
exportId,
|
|
223
|
+
kind: 'project-checkpoint-export-artifact',
|
|
224
|
+
projectId,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
export function collabCloudProjectCheckpointExportRoute(projectId, exportId, operation) {
|
|
228
|
+
assertProjectId(projectId);
|
|
229
|
+
assertAttemptId(exportId);
|
|
230
|
+
if (operation !== 'begin' && operation !== 'status')
|
|
231
|
+
invalidRoute();
|
|
232
|
+
return route(operation === 'begin' ? 'POST' : 'GET', `/v2/projects/${projectId}/checkpoint-exports/${exportId}`, { exportId, kind: 'project-checkpoint-export', operation, projectId });
|
|
233
|
+
}
|
|
234
|
+
export function decodeCollabCloudProjectCheckpointExportStatus(value) {
|
|
235
|
+
const source = exactRecord(value, 'checkpointExport', [
|
|
236
|
+
'checkpointSha256',
|
|
237
|
+
'createdAt',
|
|
238
|
+
'expiresAt',
|
|
239
|
+
'exportId',
|
|
240
|
+
'projectId',
|
|
241
|
+
'state',
|
|
242
|
+
]);
|
|
243
|
+
if (!isCollabOpaqueId(source.exportId) || !isCollabProjectId(source.projectId)) {
|
|
244
|
+
throw invalidPayload('checkpointExport');
|
|
245
|
+
}
|
|
246
|
+
const state = source.state;
|
|
247
|
+
if (state !== 'preparing' && state !== 'ready')
|
|
248
|
+
throw invalidPayload('state');
|
|
249
|
+
if (source.checkpointSha256 !== null && typeof source.checkpointSha256 !== 'string') {
|
|
250
|
+
throw invalidPayload('checkpointSha256');
|
|
251
|
+
}
|
|
252
|
+
const checkpointSha256 = source.checkpointSha256;
|
|
253
|
+
if ((state === 'preparing' && checkpointSha256 !== null)
|
|
254
|
+
|| (state === 'ready' && (typeof checkpointSha256 !== 'string' || !SHA256_PATTERN.test(checkpointSha256))))
|
|
255
|
+
throw invalidPayload('checkpointSha256');
|
|
256
|
+
const createdAt = isoTimestamp(source.createdAt, 'createdAt');
|
|
257
|
+
const expiresAt = isoTimestamp(source.expiresAt, 'expiresAt');
|
|
258
|
+
if (Date.parse(expiresAt) <= Date.parse(createdAt))
|
|
259
|
+
throw invalidPayload('expiresAt');
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
checkpointSha256,
|
|
262
|
+
createdAt,
|
|
263
|
+
expiresAt,
|
|
264
|
+
exportId: source.exportId,
|
|
265
|
+
projectId: source.projectId,
|
|
266
|
+
state,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
export function collabDevelopmentBootstrapRoute(operation, attemptId) {
|
|
270
|
+
const base = '/v2/development/bootstrap/attempts';
|
|
271
|
+
if (operation === 'beginDevelopmentBootstrap') {
|
|
272
|
+
if (attemptId !== undefined)
|
|
273
|
+
invalidRoute();
|
|
274
|
+
return route('POST', base, { kind: 'development-bootstrap', operation });
|
|
275
|
+
}
|
|
276
|
+
if (attemptId === undefined)
|
|
277
|
+
invalidRoute();
|
|
278
|
+
assertAttemptId(attemptId);
|
|
279
|
+
const suffixes = {
|
|
280
|
+
activateDevelopmentBootstrap: ['POST', 'activate'],
|
|
281
|
+
cancelDevelopmentBootstrap: ['POST', 'cancel'],
|
|
282
|
+
getDevelopmentBootstrap: ['GET', ''],
|
|
283
|
+
putDevelopmentBootstrapGitBundle: ['PUT', 'git-bundle'],
|
|
284
|
+
submitDevelopmentBootstrapReport: ['POST', 'reports'],
|
|
285
|
+
};
|
|
286
|
+
const binding = suffixes[operation];
|
|
287
|
+
if (!binding)
|
|
288
|
+
invalidRoute();
|
|
289
|
+
const [method, suffix] = binding;
|
|
290
|
+
const target = suffix ? `${base}/${attemptId}/${suffix}` : `${base}/${attemptId}`;
|
|
291
|
+
return route(method, target, {
|
|
292
|
+
attemptId,
|
|
293
|
+
kind: 'development-bootstrap',
|
|
294
|
+
operation,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function exactQuery(url, expectedKeys) {
|
|
298
|
+
const keys = [...url.searchParams.keys()];
|
|
299
|
+
return keys.length === expectedKeys.length
|
|
300
|
+
&& expectedKeys.every(key => keys.filter(candidate => candidate === key).length === 1)
|
|
301
|
+
&& keys.every(key => expectedKeys.includes(key))
|
|
302
|
+
&& url.search === (expectedKeys.length === 0 ? '' : `?${url.searchParams.toString()}`);
|
|
303
|
+
}
|
|
304
|
+
function parseCanonicalNonNegativeInteger(value) {
|
|
305
|
+
if (value === null || !/^(?:0|[1-9][0-9]*)$/u.test(value))
|
|
306
|
+
return null;
|
|
307
|
+
const parsed = Number(value);
|
|
308
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
309
|
+
}
|
|
310
|
+
export function matchCollabCloudRoute(method, target) {
|
|
311
|
+
if (!target.startsWith('/') || target.startsWith('//') || target.includes('#'))
|
|
312
|
+
return null;
|
|
313
|
+
let url;
|
|
314
|
+
try {
|
|
315
|
+
url = new URL(target, 'http://collab.invalid');
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
if (`${url.pathname}${url.search}` !== target)
|
|
321
|
+
return null;
|
|
322
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
323
|
+
if (url.pathname !== `/${segments.join('/')}` || target.endsWith('?'))
|
|
324
|
+
return null;
|
|
325
|
+
if (method === 'GET'
|
|
326
|
+
&& url.pathname === '/collab/capabilities'
|
|
327
|
+
&& exactQuery(url, []))
|
|
328
|
+
return { kind: 'capabilities' };
|
|
329
|
+
if (segments[0] === 'v2'
|
|
330
|
+
&& segments[1] === 'projects'
|
|
331
|
+
&& isCollabProjectId(segments[2])) {
|
|
332
|
+
const projectId = segments[2];
|
|
333
|
+
if (method === 'GET'
|
|
334
|
+
&& segments.length === 7
|
|
335
|
+
&& segments[3] === 'checkpoint-exports'
|
|
336
|
+
&& isCollabOpaqueId(segments[4])
|
|
337
|
+
&& segments[5] === 'checkpoint'
|
|
338
|
+
&& COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(segments[6])
|
|
339
|
+
&& exactQuery(url, [])) {
|
|
340
|
+
return {
|
|
341
|
+
artifact: segments[6],
|
|
342
|
+
exportId: segments[4],
|
|
343
|
+
kind: 'project-checkpoint-export-artifact',
|
|
344
|
+
projectId,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if ((method === 'POST' || method === 'GET')
|
|
348
|
+
&& segments.length === 5
|
|
349
|
+
&& segments[3] === 'checkpoint-exports'
|
|
350
|
+
&& isCollabOpaqueId(segments[4])
|
|
351
|
+
&& exactQuery(url, [])) {
|
|
352
|
+
return {
|
|
353
|
+
exportId: segments[4],
|
|
354
|
+
kind: 'project-checkpoint-export',
|
|
355
|
+
operation: method === 'POST' ? 'begin' : 'status',
|
|
356
|
+
projectId,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if ((method === 'GET' || method === 'PUT')
|
|
360
|
+
&& segments.length === 7
|
|
361
|
+
&& segments[3] === 'authority-transfers'
|
|
362
|
+
&& isCollabOpaqueId(segments[4])
|
|
363
|
+
&& segments[5] === 'checkpoint'
|
|
364
|
+
&& COLLAB_PROJECT_CHECKPOINT_ARTIFACTS_SET.has(segments[6])
|
|
365
|
+
&& exactQuery(url, [])) {
|
|
366
|
+
return {
|
|
367
|
+
artifact: segments[6],
|
|
368
|
+
direction: method === 'PUT' ? 'upload' : 'download',
|
|
369
|
+
kind: 'authority-transfer-artifact',
|
|
370
|
+
projectId,
|
|
371
|
+
transferId: segments[4],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (method === 'POST'
|
|
375
|
+
&& segments.length === 5
|
|
376
|
+
&& segments[3] === 'operations'
|
|
377
|
+
&& isCloudJsonOperation(segments[4])
|
|
378
|
+
&& exactQuery(url, [])) {
|
|
379
|
+
return { kind: 'project-operation', operation: segments[4], projectId };
|
|
380
|
+
}
|
|
381
|
+
if (method === 'GET'
|
|
382
|
+
&& segments.length === 4
|
|
383
|
+
&& segments[3] === 'events'
|
|
384
|
+
&& exactQuery(url, ['afterSequence'])) {
|
|
385
|
+
const afterSequence = parseCanonicalNonNegativeInteger(url.searchParams.get('afterSequence'));
|
|
386
|
+
return afterSequence === null
|
|
387
|
+
? null
|
|
388
|
+
: { afterSequence, kind: 'project-events', projectId };
|
|
389
|
+
}
|
|
390
|
+
if (method === 'GET'
|
|
391
|
+
&& segments.length === 6
|
|
392
|
+
&& segments[3] === 'repository.git'
|
|
393
|
+
&& segments[4] === 'info'
|
|
394
|
+
&& segments[5] === 'refs'
|
|
395
|
+
&& exactQuery(url, ['service'])) {
|
|
396
|
+
const service = url.searchParams.get('service');
|
|
397
|
+
return service === 'git-upload-pack' || service === 'git-receive-pack'
|
|
398
|
+
? { kind: 'git-info-refs', projectId, service }
|
|
399
|
+
: null;
|
|
400
|
+
}
|
|
401
|
+
if (method === 'POST'
|
|
402
|
+
&& segments.length === 5
|
|
403
|
+
&& segments[3] === 'repository.git'
|
|
404
|
+
&& (segments[4] === 'git-upload-pack' || segments[4] === 'git-receive-pack')
|
|
405
|
+
&& exactQuery(url, []))
|
|
406
|
+
return { kind: segments[4], projectId };
|
|
407
|
+
}
|
|
408
|
+
if (segments[0] !== 'v2'
|
|
409
|
+
|| segments[1] !== 'development'
|
|
410
|
+
|| segments[2] !== 'bootstrap'
|
|
411
|
+
|| segments[3] !== 'attempts'
|
|
412
|
+
|| !exactQuery(url, []))
|
|
413
|
+
return null;
|
|
414
|
+
if (method === 'POST' && segments.length === 4) {
|
|
415
|
+
return { kind: 'development-bootstrap', operation: 'beginDevelopmentBootstrap' };
|
|
416
|
+
}
|
|
417
|
+
const attemptId = segments[4];
|
|
418
|
+
if (!isCollabOpaqueId(attemptId))
|
|
419
|
+
return null;
|
|
420
|
+
if (method === 'GET' && segments.length === 5) {
|
|
421
|
+
return { attemptId, kind: 'development-bootstrap', operation: 'getDevelopmentBootstrap' };
|
|
422
|
+
}
|
|
423
|
+
const operation = (segments.length === 6
|
|
424
|
+
? {
|
|
425
|
+
activate: method === 'POST' ? 'activateDevelopmentBootstrap' : null,
|
|
426
|
+
cancel: method === 'POST' ? 'cancelDevelopmentBootstrap' : null,
|
|
427
|
+
'git-bundle': method === 'PUT' ? 'putDevelopmentBootstrapGitBundle' : null,
|
|
428
|
+
reports: method === 'POST' ? 'submitDevelopmentBootstrapReport' : null,
|
|
429
|
+
}[segments[5]]
|
|
430
|
+
: null);
|
|
431
|
+
return operation
|
|
432
|
+
? { attemptId, kind: 'development-bootstrap', operation }
|
|
433
|
+
: null;
|
|
434
|
+
}
|
|
435
|
+
export function decodeCollabCloudCapabilityDocument(value) {
|
|
436
|
+
const source = exactRecord(value, 'capabilities', [
|
|
437
|
+
'bindingVersions',
|
|
438
|
+
'capabilities',
|
|
439
|
+
'limits',
|
|
440
|
+
'protocolVersions',
|
|
441
|
+
'schemaVersion',
|
|
442
|
+
]);
|
|
443
|
+
if (source.schemaVersion !== COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION) {
|
|
444
|
+
throw invalidPayload('schemaVersion');
|
|
445
|
+
}
|
|
446
|
+
const bindingVersions = sortedUniqueIntegers(source.bindingVersions, 'bindingVersions');
|
|
447
|
+
const protocolVersions = sortedUniqueIntegers(source.protocolVersions, 'protocolVersions');
|
|
448
|
+
requireSupportedVersion(bindingVersions, COLLAB_CLOUD_BINDING_VERSION, 'cloud-binding');
|
|
449
|
+
requireSupportedVersion(protocolVersions, COLLAB_PROTOCOL_VERSION, 'canonical-wire');
|
|
450
|
+
const capabilities = sortedUniqueTokens(source.capabilities, 'capabilities');
|
|
451
|
+
const limitsSource = exactRecord(source.limits, 'limits', [
|
|
452
|
+
'maxCheckpointCoordinationBytes',
|
|
453
|
+
'maxCheckpointManifestUtf8Bytes',
|
|
454
|
+
'maxCheckpointRepositoryBundleBytes',
|
|
455
|
+
'maxCheckpointStagingBytes',
|
|
456
|
+
'maxDevelopmentBootstrapGitBundleBytes',
|
|
457
|
+
'maxDevelopmentBootstrapManifestUtf8Bytes',
|
|
458
|
+
'maxDevelopmentBootstrapReportUtf8Bytes',
|
|
459
|
+
'maxEventReplay',
|
|
460
|
+
'maxGitReceivePackBytes',
|
|
461
|
+
'maxJsonPayloadUtf8Bytes',
|
|
462
|
+
'maxRepositoryBytes',
|
|
463
|
+
]);
|
|
464
|
+
const limits = Object.freeze({
|
|
465
|
+
maxCheckpointCoordinationBytes: positiveSafeInteger(limitsSource.maxCheckpointCoordinationBytes, 'maxCheckpointCoordinationBytes', COLLAB_CLOUD_BINDING_LIMITS.maxCheckpointCoordinationBytes),
|
|
466
|
+
maxCheckpointManifestUtf8Bytes: positiveSafeInteger(limitsSource.maxCheckpointManifestUtf8Bytes, 'maxCheckpointManifestUtf8Bytes', COLLAB_CLOUD_BINDING_LIMITS.maxCheckpointManifestUtf8Bytes),
|
|
467
|
+
maxCheckpointRepositoryBundleBytes: positiveSafeInteger(limitsSource.maxCheckpointRepositoryBundleBytes, 'maxCheckpointRepositoryBundleBytes', COLLAB_CLOUD_BINDING_LIMITS.maxCheckpointRepositoryBundleBytes),
|
|
468
|
+
maxCheckpointStagingBytes: positiveSafeInteger(limitsSource.maxCheckpointStagingBytes, 'maxCheckpointStagingBytes', COLLAB_CLOUD_BINDING_LIMITS.maxCheckpointStagingBytes),
|
|
469
|
+
maxDevelopmentBootstrapGitBundleBytes: positiveSafeInteger(limitsSource.maxDevelopmentBootstrapGitBundleBytes, 'maxDevelopmentBootstrapGitBundleBytes', COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapGitBundleBytes),
|
|
470
|
+
maxDevelopmentBootstrapManifestUtf8Bytes: positiveSafeInteger(limitsSource.maxDevelopmentBootstrapManifestUtf8Bytes, 'maxDevelopmentBootstrapManifestUtf8Bytes', COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapManifestUtf8Bytes),
|
|
471
|
+
maxDevelopmentBootstrapReportUtf8Bytes: positiveSafeInteger(limitsSource.maxDevelopmentBootstrapReportUtf8Bytes, 'maxDevelopmentBootstrapReportUtf8Bytes', COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapReportUtf8Bytes),
|
|
472
|
+
maxEventReplay: positiveSafeInteger(limitsSource.maxEventReplay, 'maxEventReplay', COLLAB_CLOUD_BINDING_LIMITS.maxEventReplay),
|
|
473
|
+
maxGitReceivePackBytes: positiveSafeInteger(limitsSource.maxGitReceivePackBytes, 'maxGitReceivePackBytes', COLLAB_CLOUD_BINDING_LIMITS.maxGitReceivePackBytes),
|
|
474
|
+
maxJsonPayloadUtf8Bytes: positiveSafeInteger(limitsSource.maxJsonPayloadUtf8Bytes, 'maxJsonPayloadUtf8Bytes', COLLAB_LIMITS.maxJsonPayloadUtf8Bytes),
|
|
475
|
+
maxRepositoryBytes: positiveSafeInteger(limitsSource.maxRepositoryBytes, 'maxRepositoryBytes', COLLAB_CLOUD_BINDING_LIMITS.maxRepositoryBytes),
|
|
476
|
+
});
|
|
477
|
+
return Object.freeze({
|
|
478
|
+
bindingVersions,
|
|
479
|
+
capabilities,
|
|
480
|
+
limits,
|
|
481
|
+
protocolVersions,
|
|
482
|
+
schemaVersion: COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
export function collabCloudCapabilityDocument(capabilities, limits) {
|
|
486
|
+
if (!capabilities.every(capability => CLOUD_CAPABILITY_SET.has(capability))) {
|
|
487
|
+
throw invalidPayload('capabilities');
|
|
488
|
+
}
|
|
489
|
+
return decodeCollabCloudCapabilityDocument({
|
|
490
|
+
bindingVersions: [COLLAB_CLOUD_BINDING_VERSION],
|
|
491
|
+
capabilities: [...capabilities].sort((left, right) => left.localeCompare(right, 'en-US')),
|
|
492
|
+
limits,
|
|
493
|
+
protocolVersions: [COLLAB_PROTOCOL_VERSION],
|
|
494
|
+
schemaVersion: COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
export function collabCloudCapabilitySupported(document, capability) {
|
|
498
|
+
return CLOUD_CAPABILITY_SET.has(capability) && document.capabilities.includes(capability);
|
|
499
|
+
}
|
|
500
|
+
export function collabCloudSuccessEnvelope(requestId, data) {
|
|
501
|
+
if (!isCollabOpaqueId(requestId))
|
|
502
|
+
throw invalidPayload('requestId');
|
|
503
|
+
return Object.freeze({ data, protocolVersion: COLLAB_PROTOCOL_VERSION, requestId });
|
|
504
|
+
}
|
|
505
|
+
export function decodeCollabCloudSuccessEnvelope(value) {
|
|
506
|
+
const source = exactRecord(value, 'envelope', ['data', 'protocolVersion', 'requestId']);
|
|
507
|
+
if (source.protocolVersion !== COLLAB_PROTOCOL_VERSION || !isCollabOpaqueId(source.requestId)) {
|
|
508
|
+
throw invalidPayload('envelope');
|
|
509
|
+
}
|
|
510
|
+
return Object.freeze({
|
|
511
|
+
data: source.data,
|
|
512
|
+
protocolVersion: COLLAB_PROTOCOL_VERSION,
|
|
513
|
+
requestId: source.requestId,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
export function collabCloudErrorEnvelope(requestId, error) {
|
|
517
|
+
if (!isCollabOpaqueId(requestId))
|
|
518
|
+
throw invalidPayload('requestId');
|
|
519
|
+
return Object.freeze({
|
|
520
|
+
error: Object.freeze({
|
|
521
|
+
code: error.code,
|
|
522
|
+
recoveryActions: Object.freeze([...error.recoveryActions]),
|
|
523
|
+
safeContext: error.safeContext,
|
|
524
|
+
}),
|
|
525
|
+
protocolVersion: COLLAB_PROTOCOL_VERSION,
|
|
526
|
+
requestId,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
export function decodeCollabCloudErrorEnvelope(value) {
|
|
530
|
+
const source = exactRecord(value, 'envelope', ['error', 'protocolVersion', 'requestId']);
|
|
531
|
+
const wireError = exactRecord(source.error, 'error', [
|
|
532
|
+
'code',
|
|
533
|
+
'recoveryActions',
|
|
534
|
+
'safeContext',
|
|
535
|
+
]);
|
|
536
|
+
if (source.protocolVersion !== COLLAB_PROTOCOL_VERSION
|
|
537
|
+
|| !isCollabOpaqueId(source.requestId)
|
|
538
|
+
|| typeof wireError.code !== 'string'
|
|
539
|
+
|| !COLLAB_ERROR_CODE_SET.has(wireError.code)
|
|
540
|
+
|| !Array.isArray(wireError.recoveryActions)
|
|
541
|
+
|| !wireError.recoveryActions.every(action => (typeof action === 'string' && RECOVERY_ACTION_SET.has(action))))
|
|
542
|
+
throw invalidPayload('error');
|
|
543
|
+
const safeContextSource = record(wireError.safeContext, 'safeContext');
|
|
544
|
+
const safeContext = sanitizeCollabDiagnosticContext(safeContextSource);
|
|
545
|
+
if (Object.keys(safeContextSource).length !== Object.keys(safeContext).length
|
|
546
|
+
|| Object.entries(safeContextSource).some(([key, item]) => safeContext[key] !== item))
|
|
547
|
+
throw invalidPayload('safeContext');
|
|
548
|
+
return Object.freeze({
|
|
549
|
+
error: Object.freeze({
|
|
550
|
+
code: wireError.code,
|
|
551
|
+
recoveryActions: Object.freeze([
|
|
552
|
+
...wireError.recoveryActions,
|
|
553
|
+
]),
|
|
554
|
+
safeContext,
|
|
555
|
+
}),
|
|
556
|
+
protocolVersion: COLLAB_PROTOCOL_VERSION,
|
|
557
|
+
requestId: source.requestId,
|
|
558
|
+
});
|
|
559
|
+
}
|