@claudian-collab/protocol 1.0.0 → 2.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 +8 -8
- package/dist/CollabAuthorityTransfer.d.ts +296 -0
- package/dist/CollabAuthorityTransfer.js +877 -0
- package/dist/CollabCloudBinding.d.ts +43 -3
- package/dist/CollabCloudBinding.js +151 -10
- package/dist/CollabCloudProjectEvent.d.ts +12 -1
- package/dist/CollabCloudProjectEvent.js +21 -0
- package/dist/CollabConstants.d.ts +1 -1
- package/dist/CollabConstants.js +1 -1
- package/dist/CollabControlOperationCodecs.d.ts +17 -0
- package/dist/CollabControlOperationCodecs.js +44 -0
- package/dist/CollabError.d.ts +1 -1
- package/dist/CollabError.js +16 -0
- package/dist/CollabProjectCheckpoint.d.ts +275 -0
- package/dist/CollabProjectCheckpoint.js +1336 -0
- package/dist/CollabProjectRetirement.d.ts +44 -0
- package/dist/CollabProjectRetirement.js +127 -0
- package/dist/CollabProtocol.d.ts +3 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +49 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS = exports.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS = exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS = exports.COLLAB_PROJECT_CHECKPOINT_ARTIFACTS = exports.COLLAB_CHECKPOINT_PROFILES = exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS = exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION = exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION = exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.decodeCollabProjectCheckpointManifest = decodeCollabProjectCheckpointManifest;
|
|
5
|
+
exports.encodeCollabProjectCheckpointManifestCanonicalJson = encodeCollabProjectCheckpointManifestCanonicalJson;
|
|
6
|
+
exports.encodeCollabProjectCheckpointManifestDigestInput = encodeCollabProjectCheckpointManifestDigestInput;
|
|
7
|
+
exports.encodeCollabProtectedClaimAssociatedData = encodeCollabProtectedClaimAssociatedData;
|
|
8
|
+
exports.decodeCollabProjectCheckpointCoordinationNdjson = decodeCollabProjectCheckpointCoordinationNdjson;
|
|
9
|
+
exports.encodeCollabProjectCheckpointCoordinationNdjson = encodeCollabProjectCheckpointCoordinationNdjson;
|
|
10
|
+
exports.validateCollabProjectCheckpointConsistency = validateCollabProjectCheckpointConsistency;
|
|
11
|
+
const CollabAuthorityTransfer_1 = require("./CollabAuthorityTransfer");
|
|
12
|
+
const CollabCloudProjectEvent_1 = require("./CollabCloudProjectEvent");
|
|
13
|
+
const CollabConstants_1 = require("./CollabConstants");
|
|
14
|
+
const CollabControlOperationCodecs_1 = require("./CollabControlOperationCodecs");
|
|
15
|
+
const CollabError_1 = require("./CollabError");
|
|
16
|
+
const CollabValidation_1 = require("./CollabValidation");
|
|
17
|
+
exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION = 1;
|
|
18
|
+
exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION = 1;
|
|
19
|
+
exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION = 1;
|
|
20
|
+
exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS = Object.freeze({
|
|
21
|
+
maxCiphertextBytes: 4096,
|
|
22
|
+
nonceBytes: 24,
|
|
23
|
+
tagBytes: 16,
|
|
24
|
+
});
|
|
25
|
+
exports.COLLAB_CHECKPOINT_PROFILES = Object.freeze([
|
|
26
|
+
'authority-transfer',
|
|
27
|
+
'backup',
|
|
28
|
+
'export',
|
|
29
|
+
]);
|
|
30
|
+
exports.COLLAB_PROJECT_CHECKPOINT_ARTIFACTS = Object.freeze([
|
|
31
|
+
'checkpoint.json',
|
|
32
|
+
'coordination.ndjson',
|
|
33
|
+
'repository.bundle',
|
|
34
|
+
]);
|
|
35
|
+
exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS = Object.freeze({
|
|
36
|
+
maxCoordinationBytes: 256 * 1024 * 1024,
|
|
37
|
+
maxManifestBytes: 64 * 1024,
|
|
38
|
+
maxRepositoryBundleBytes: 1024 * 1024 * 1024,
|
|
39
|
+
maxStagingBytes: 2 * 1024 * 1024 * 1024,
|
|
40
|
+
});
|
|
41
|
+
exports.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS = Object.freeze([
|
|
42
|
+
'project',
|
|
43
|
+
'member',
|
|
44
|
+
'request',
|
|
45
|
+
'request-comment',
|
|
46
|
+
'ticket',
|
|
47
|
+
'ticket-comment',
|
|
48
|
+
'ticket-relation',
|
|
49
|
+
'ticket-mention',
|
|
50
|
+
]);
|
|
51
|
+
exports.COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS = Object.freeze([
|
|
52
|
+
...exports.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS,
|
|
53
|
+
'cloud-event',
|
|
54
|
+
'cloud-event-cursor',
|
|
55
|
+
'idempotency-result',
|
|
56
|
+
'principal-binding',
|
|
57
|
+
'repository-placement',
|
|
58
|
+
'lifecycle-state',
|
|
59
|
+
'terminal-responder',
|
|
60
|
+
'protected-claim-envelope',
|
|
61
|
+
'tombstone',
|
|
62
|
+
'schema-catalog',
|
|
63
|
+
'server-compatibility',
|
|
64
|
+
'authority-volume-pair',
|
|
65
|
+
]);
|
|
66
|
+
const CHECKPOINT_PROFILE_SET = new Set(exports.COLLAB_CHECKPOINT_PROFILES);
|
|
67
|
+
const PORTABLE_RECORD_KIND_SET = new Set(exports.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS);
|
|
68
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
69
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
70
|
+
const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
71
|
+
const PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET = new Set([
|
|
72
|
+
'getTransferredMembershipClaim',
|
|
73
|
+
'rotateTransferredMembershipClaims',
|
|
74
|
+
]);
|
|
75
|
+
function gitOidMatchesFormat(oid, format) {
|
|
76
|
+
return oid.length === (format === 'sha1' ? 40 : 64);
|
|
77
|
+
}
|
|
78
|
+
function invalidPayload(field) {
|
|
79
|
+
return new CollabError_1.CollabError({ code: 'protocol-payload-invalid', safeContext: { field } });
|
|
80
|
+
}
|
|
81
|
+
function record(value, field) {
|
|
82
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
83
|
+
throw invalidPayload(field);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function exactRecord(value, field, keys) {
|
|
88
|
+
const source = record(value, field);
|
|
89
|
+
const expected = new Set(keys);
|
|
90
|
+
if (!keys.every(key => Object.hasOwn(source, key))
|
|
91
|
+
|| Object.keys(source).some(key => !expected.has(key)))
|
|
92
|
+
throw invalidPayload(field);
|
|
93
|
+
return source;
|
|
94
|
+
}
|
|
95
|
+
function token(source, field, validate = CollabValidation_1.isCollabOpaqueId) {
|
|
96
|
+
const value = source[field];
|
|
97
|
+
if (typeof value !== 'string' || !validate(value))
|
|
98
|
+
throw invalidPayload(field);
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
function boundedString(source, field, maximumBytes, allowEmpty = false) {
|
|
102
|
+
const value = source[field];
|
|
103
|
+
if (typeof value !== 'string'
|
|
104
|
+
|| (!allowEmpty && value.length === 0)
|
|
105
|
+
|| !(0, CollabValidation_1.hasUtf8ByteLengthAtMost)(value, maximumBytes))
|
|
106
|
+
throw invalidPayload(field);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function positiveInteger(source, field, maximum) {
|
|
110
|
+
const value = source[field];
|
|
111
|
+
if (typeof value !== 'number'
|
|
112
|
+
|| !Number.isSafeInteger(value)
|
|
113
|
+
|| value < 1
|
|
114
|
+
|| (maximum !== undefined && value > maximum))
|
|
115
|
+
throw invalidPayload(field);
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function nonNegativeInteger(source, field) {
|
|
119
|
+
const value = source[field];
|
|
120
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
121
|
+
throw invalidPayload(field);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function timestampValue(value, field) {
|
|
126
|
+
if (typeof value !== 'string'
|
|
127
|
+
|| value.length > 64
|
|
128
|
+
|| Number.isNaN(Date.parse(value))
|
|
129
|
+
|| new Date(value).toISOString() !== value)
|
|
130
|
+
throw invalidPayload(field);
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
function timestamp(source, field) {
|
|
134
|
+
return timestampValue(source[field], field);
|
|
135
|
+
}
|
|
136
|
+
function nullableTimestamp(source, field) {
|
|
137
|
+
return source[field] === null ? null : timestampValue(source[field], field);
|
|
138
|
+
}
|
|
139
|
+
function canonicalBase64url(source, field, maximumDecodedBytes, exactDecodedBytes) {
|
|
140
|
+
const value = boundedString(source, field, Math.ceil(maximumDecodedBytes * 4 / 3));
|
|
141
|
+
if (!BASE64URL_PATTERN.test(value))
|
|
142
|
+
throw invalidPayload(field);
|
|
143
|
+
const remainder = value.length % 4;
|
|
144
|
+
const finalIndex = BASE64URL_ALPHABET.indexOf(value[value.length - 1]);
|
|
145
|
+
const decodedBytes = Math.floor(value.length * 6 / 8);
|
|
146
|
+
if (remainder === 1
|
|
147
|
+
|| (remainder === 2 && (finalIndex & 15) !== 0)
|
|
148
|
+
|| (remainder === 3 && (finalIndex & 3) !== 0)
|
|
149
|
+
|| decodedBytes > maximumDecodedBytes
|
|
150
|
+
|| (exactDecodedBytes !== undefined && decodedBytes !== exactDecodedBytes))
|
|
151
|
+
throw invalidPayload(field);
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function sha256(source, field) {
|
|
155
|
+
const value = source[field];
|
|
156
|
+
if (typeof value !== 'string' || !SHA256_PATTERN.test(value))
|
|
157
|
+
throw invalidPayload(field);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function literal(source, field, values) {
|
|
161
|
+
const value = source[field];
|
|
162
|
+
if (typeof value !== 'string' || !values.includes(value))
|
|
163
|
+
throw invalidPayload(field);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function controlOperation(source, field) {
|
|
167
|
+
const value = source[field];
|
|
168
|
+
if (typeof value !== 'string' || !Object.hasOwn(CollabControlOperationCodecs_1.COLLAB_CONTROL_OPERATION_CODECS, value)) {
|
|
169
|
+
throw invalidPayload(field);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function canonicalOperationResponseJson(source, field, operation) {
|
|
174
|
+
if (PLAINTEXT_CLAIM_RESPONSE_OPERATION_SET.has(operation))
|
|
175
|
+
throw invalidPayload(field);
|
|
176
|
+
const value = boundedString(source, field, 512 * 1024, true);
|
|
177
|
+
let decoded;
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(value);
|
|
180
|
+
const operationResponse = (0, CollabControlOperationCodecs_1.collabControlOperationCodec)(operation).decodeResponse(parsed);
|
|
181
|
+
if (JSON.stringify(operationResponse) !== value)
|
|
182
|
+
throw invalidPayload(field);
|
|
183
|
+
decoded = record(operationResponse, field);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
throw invalidPayload(field);
|
|
187
|
+
}
|
|
188
|
+
return { decoded, responseJson: value };
|
|
189
|
+
}
|
|
190
|
+
function authority(value, field) {
|
|
191
|
+
const source = exactRecord(value, field, ['generation', 'kind']);
|
|
192
|
+
return {
|
|
193
|
+
generation: positiveInteger(source, 'generation'),
|
|
194
|
+
kind: literal(source, 'kind', ['cloud', 'lan']),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function artifact(value) {
|
|
198
|
+
const source = exactRecord(value, 'artifact', ['byteCount', 'name', 'sha256']);
|
|
199
|
+
const name = literal(source, 'name', ['coordination.ndjson', 'repository.bundle']);
|
|
200
|
+
const maximum = name === 'coordination.ndjson'
|
|
201
|
+
? exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxCoordinationBytes
|
|
202
|
+
: exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxRepositoryBundleBytes;
|
|
203
|
+
return {
|
|
204
|
+
byteCount: positiveInteger(source, 'byteCount', maximum),
|
|
205
|
+
name,
|
|
206
|
+
sha256: sha256(source, 'sha256'),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function artifacts(value) {
|
|
210
|
+
if (!Array.isArray(value) || value.length !== 2)
|
|
211
|
+
throw invalidPayload('artifacts');
|
|
212
|
+
const decoded = value.map(artifact);
|
|
213
|
+
if (decoded[0].name !== 'coordination.ndjson'
|
|
214
|
+
|| decoded[1].name !== 'repository.bundle'
|
|
215
|
+
|| decoded[0].byteCount + decoded[1].byteCount
|
|
216
|
+
> exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxStagingBytes)
|
|
217
|
+
throw invalidPayload('artifacts');
|
|
218
|
+
return Object.freeze(decoded);
|
|
219
|
+
}
|
|
220
|
+
function gitRef(value) {
|
|
221
|
+
const source = exactRecord(value, 'ref', ['name', 'oid']);
|
|
222
|
+
const name = boundedString(source, 'name', 512);
|
|
223
|
+
if (name !== CollabConstants_1.COLLAB_MAIN_REF && !name.startsWith(CollabConstants_1.COLLAB_MEMBER_REF_PREFIX)) {
|
|
224
|
+
throw invalidPayload('name');
|
|
225
|
+
}
|
|
226
|
+
const oid = token(source, 'oid', CollabValidation_1.isCollabGitOid);
|
|
227
|
+
return { name, oid };
|
|
228
|
+
}
|
|
229
|
+
function gitRefs(value, expectedMainOid) {
|
|
230
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
231
|
+
throw invalidPayload('refs');
|
|
232
|
+
const decoded = value.map(gitRef);
|
|
233
|
+
if (decoded[0].name !== CollabConstants_1.COLLAB_MAIN_REF
|
|
234
|
+
|| decoded[0].oid !== expectedMainOid
|
|
235
|
+
|| decoded.some((item, index) => index > 0
|
|
236
|
+
&& decoded[index - 1].name.localeCompare(item.name, 'en-US') >= 0))
|
|
237
|
+
throw invalidPayload('refs');
|
|
238
|
+
return Object.freeze(decoded);
|
|
239
|
+
}
|
|
240
|
+
function manifestObject(source, includeManifestSha256) {
|
|
241
|
+
const profile = literal(source, 'profile', exports.COLLAB_CHECKPOINT_PROFILES);
|
|
242
|
+
const expectedMainOid = token(source, 'expectedMainOid', CollabValidation_1.isCollabGitOid);
|
|
243
|
+
const gitObjectFormat = literal(source, 'gitObjectFormat', ['sha1', 'sha256']);
|
|
244
|
+
const refs = gitRefs(source.refs, expectedMainOid);
|
|
245
|
+
if (!gitOidMatchesFormat(expectedMainOid, gitObjectFormat)
|
|
246
|
+
|| refs.some(ref => !gitOidMatchesFormat(ref.oid, gitObjectFormat)))
|
|
247
|
+
throw invalidPayload('gitObjectFormat');
|
|
248
|
+
const sourceAuthority = authority(source.sourceAuthority, 'sourceAuthority');
|
|
249
|
+
const targetAuthority = source.targetAuthority === null
|
|
250
|
+
? null
|
|
251
|
+
: authority(source.targetAuthority, 'targetAuthority');
|
|
252
|
+
if (profile === 'authority-transfer') {
|
|
253
|
+
if (targetAuthority === null
|
|
254
|
+
|| targetAuthority.kind === sourceAuthority.kind
|
|
255
|
+
|| targetAuthority.generation !== sourceAuthority.generation + 1)
|
|
256
|
+
throw invalidPayload('targetAuthority');
|
|
257
|
+
}
|
|
258
|
+
else if (targetAuthority !== null) {
|
|
259
|
+
throw invalidPayload('targetAuthority');
|
|
260
|
+
}
|
|
261
|
+
const common = {
|
|
262
|
+
artifacts: artifacts(source.artifacts),
|
|
263
|
+
coordinationFormatVersion: source.coordinationFormatVersion,
|
|
264
|
+
createdAt: timestamp(source, 'createdAt'),
|
|
265
|
+
expectedMainOid,
|
|
266
|
+
gitObjectFormat,
|
|
267
|
+
manifestSchemaVersion: source.manifestSchemaVersion,
|
|
268
|
+
operationId: token(source, 'operationId'),
|
|
269
|
+
profile,
|
|
270
|
+
projectId: token(source, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
271
|
+
protocolVersion: source.protocolVersion,
|
|
272
|
+
refs,
|
|
273
|
+
sourceAuthority,
|
|
274
|
+
targetAuthority,
|
|
275
|
+
};
|
|
276
|
+
if (common.coordinationFormatVersion !== exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION
|
|
277
|
+
|| common.manifestSchemaVersion !== exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION
|
|
278
|
+
|| common.protocolVersion !== CollabConstants_1.COLLAB_PROTOCOL_VERSION)
|
|
279
|
+
throw invalidPayload('manifest');
|
|
280
|
+
if (!includeManifestSha256)
|
|
281
|
+
return common;
|
|
282
|
+
return {
|
|
283
|
+
artifacts: common.artifacts,
|
|
284
|
+
coordinationFormatVersion: exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
285
|
+
createdAt: common.createdAt,
|
|
286
|
+
expectedMainOid: common.expectedMainOid,
|
|
287
|
+
gitObjectFormat: common.gitObjectFormat,
|
|
288
|
+
manifestSchemaVersion: exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION,
|
|
289
|
+
manifestSha256: sha256(source, 'manifestSha256'),
|
|
290
|
+
operationId: common.operationId,
|
|
291
|
+
profile: common.profile,
|
|
292
|
+
projectId: common.projectId,
|
|
293
|
+
protocolVersion: CollabConstants_1.COLLAB_PROTOCOL_VERSION,
|
|
294
|
+
refs: common.refs,
|
|
295
|
+
sourceAuthority: common.sourceAuthority,
|
|
296
|
+
targetAuthority: common.targetAuthority,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function decodeCollabProjectCheckpointManifest(value) {
|
|
300
|
+
const source = exactRecord(value, 'manifest', [
|
|
301
|
+
'artifacts',
|
|
302
|
+
'coordinationFormatVersion',
|
|
303
|
+
'createdAt',
|
|
304
|
+
'expectedMainOid',
|
|
305
|
+
'gitObjectFormat',
|
|
306
|
+
'manifestSchemaVersion',
|
|
307
|
+
'manifestSha256',
|
|
308
|
+
'operationId',
|
|
309
|
+
'profile',
|
|
310
|
+
'projectId',
|
|
311
|
+
'protocolVersion',
|
|
312
|
+
'refs',
|
|
313
|
+
'sourceAuthority',
|
|
314
|
+
'targetAuthority',
|
|
315
|
+
]);
|
|
316
|
+
const decoded = manifestObject(source, true);
|
|
317
|
+
const encoded = JSON.stringify(decoded);
|
|
318
|
+
if (!(0, CollabValidation_1.hasUtf8ByteLengthAtMost)(encoded, exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxManifestBytes)) {
|
|
319
|
+
throw invalidPayload('manifest');
|
|
320
|
+
}
|
|
321
|
+
return decoded;
|
|
322
|
+
}
|
|
323
|
+
function encodeCollabProjectCheckpointManifestCanonicalJson(manifest) {
|
|
324
|
+
return JSON.stringify(decodeCollabProjectCheckpointManifest(manifest));
|
|
325
|
+
}
|
|
326
|
+
function encodeCollabProjectCheckpointManifestDigestInput(manifest) {
|
|
327
|
+
const decoded = decodeCollabProjectCheckpointManifest(manifest);
|
|
328
|
+
const source = {
|
|
329
|
+
artifacts: decoded.artifacts,
|
|
330
|
+
coordinationFormatVersion: decoded.coordinationFormatVersion,
|
|
331
|
+
createdAt: decoded.createdAt,
|
|
332
|
+
expectedMainOid: decoded.expectedMainOid,
|
|
333
|
+
gitObjectFormat: decoded.gitObjectFormat,
|
|
334
|
+
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
335
|
+
operationId: decoded.operationId,
|
|
336
|
+
profile: decoded.profile,
|
|
337
|
+
projectId: decoded.projectId,
|
|
338
|
+
protocolVersion: decoded.protocolVersion,
|
|
339
|
+
refs: decoded.refs,
|
|
340
|
+
sourceAuthority: decoded.sourceAuthority,
|
|
341
|
+
targetAuthority: decoded.targetAuthority,
|
|
342
|
+
};
|
|
343
|
+
return JSON.stringify(source);
|
|
344
|
+
}
|
|
345
|
+
function recordEnvelope(value) {
|
|
346
|
+
const source = exactRecord(value, 'record', ['kind', 'recordId', 'revision', 'value']);
|
|
347
|
+
const kind = literal(source, 'kind', exports.COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS);
|
|
348
|
+
const recordId = boundedString(source, 'recordId', 256);
|
|
349
|
+
const revision = positiveInteger(source, 'revision');
|
|
350
|
+
return { kind, recordId, revision, source };
|
|
351
|
+
}
|
|
352
|
+
function projectRecord(source, recordId, revision) {
|
|
353
|
+
const value = exactRecord(source.value, 'value', [
|
|
354
|
+
'activatedAt',
|
|
355
|
+
'authorityGeneration',
|
|
356
|
+
'createdAt',
|
|
357
|
+
'expectedMainOid',
|
|
358
|
+
'managerSetGeneration',
|
|
359
|
+
'name',
|
|
360
|
+
'projectId',
|
|
361
|
+
]);
|
|
362
|
+
const projectId = token(value, 'projectId', CollabValidation_1.isCollabProjectId);
|
|
363
|
+
if (recordId !== projectId)
|
|
364
|
+
throw invalidPayload('recordId');
|
|
365
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
366
|
+
const activatedAt = timestamp(value, 'activatedAt');
|
|
367
|
+
if (Date.parse(activatedAt) < Date.parse(createdAt))
|
|
368
|
+
throw invalidPayload('activatedAt');
|
|
369
|
+
return {
|
|
370
|
+
kind: 'project',
|
|
371
|
+
recordId,
|
|
372
|
+
revision,
|
|
373
|
+
value: {
|
|
374
|
+
activatedAt,
|
|
375
|
+
authorityGeneration: positiveInteger(value, 'authorityGeneration'),
|
|
376
|
+
createdAt,
|
|
377
|
+
expectedMainOid: token(value, 'expectedMainOid', CollabValidation_1.isCollabGitOid),
|
|
378
|
+
managerSetGeneration: nonNegativeInteger(value, 'managerSetGeneration'),
|
|
379
|
+
name: boundedString(value, 'name', 1024),
|
|
380
|
+
projectId,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function memberRecord(source, recordId, revision) {
|
|
385
|
+
const value = exactRecord(source.value, 'value', [
|
|
386
|
+
'activatedAt',
|
|
387
|
+
'createdAt',
|
|
388
|
+
'displayName',
|
|
389
|
+
'memberId',
|
|
390
|
+
'personalRef',
|
|
391
|
+
'projectId',
|
|
392
|
+
'role',
|
|
393
|
+
'status',
|
|
394
|
+
'revokedAt',
|
|
395
|
+
'updatedAt',
|
|
396
|
+
]);
|
|
397
|
+
const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
|
|
398
|
+
if (recordId !== memberId)
|
|
399
|
+
throw invalidPayload('recordId');
|
|
400
|
+
const personalRef = boundedString(value, 'personalRef', 512);
|
|
401
|
+
if (personalRef !== `${CollabConstants_1.COLLAB_MEMBER_REF_PREFIX}${memberId}`)
|
|
402
|
+
throw invalidPayload('personalRef');
|
|
403
|
+
const activatedAt = nullableTimestamp(value, 'activatedAt');
|
|
404
|
+
const revokedAt = nullableTimestamp(value, 'revokedAt');
|
|
405
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
406
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
407
|
+
const status = literal(value, 'status', ['active', 'left', 'revoked']);
|
|
408
|
+
if ((status === 'active' && (activatedAt === null || revokedAt !== null))
|
|
409
|
+
|| (status !== 'active' && revokedAt === null)
|
|
410
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt)
|
|
411
|
+
|| (activatedAt !== null && Date.parse(activatedAt) < Date.parse(createdAt))
|
|
412
|
+
|| (revokedAt !== null && (Date.parse(revokedAt) < Date.parse(createdAt)
|
|
413
|
+
|| Date.parse(revokedAt) > Date.parse(updatedAt))))
|
|
414
|
+
throw invalidPayload('status');
|
|
415
|
+
return {
|
|
416
|
+
kind: 'member',
|
|
417
|
+
recordId,
|
|
418
|
+
revision,
|
|
419
|
+
value: {
|
|
420
|
+
activatedAt,
|
|
421
|
+
createdAt,
|
|
422
|
+
displayName: boundedString(value, 'displayName', 1024),
|
|
423
|
+
memberId,
|
|
424
|
+
personalRef,
|
|
425
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
426
|
+
role: literal(value, 'role', ['manager', 'member']),
|
|
427
|
+
status,
|
|
428
|
+
revokedAt,
|
|
429
|
+
updatedAt,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function requestRecord(source, recordId, revision) {
|
|
434
|
+
const value = exactRecord(source.value, 'value', [
|
|
435
|
+
'createdAt',
|
|
436
|
+
'description',
|
|
437
|
+
'firstBaseOid',
|
|
438
|
+
'latestHeadOid',
|
|
439
|
+
'memberId',
|
|
440
|
+
'mergedOid',
|
|
441
|
+
'projectId',
|
|
442
|
+
'requestId',
|
|
443
|
+
'status',
|
|
444
|
+
'updatedAt',
|
|
445
|
+
]);
|
|
446
|
+
const requestId = token(value, 'requestId');
|
|
447
|
+
if (recordId !== requestId)
|
|
448
|
+
throw invalidPayload('recordId');
|
|
449
|
+
const status = literal(value, 'status', ['discarded', 'merged', 'open']);
|
|
450
|
+
const mergedOid = value.mergedOid === null
|
|
451
|
+
? null
|
|
452
|
+
: token(value, 'mergedOid', CollabValidation_1.isCollabGitOid);
|
|
453
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
454
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
455
|
+
if ((status === 'merged') !== (mergedOid !== null)
|
|
456
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt))
|
|
457
|
+
throw invalidPayload('status');
|
|
458
|
+
return {
|
|
459
|
+
kind: 'request',
|
|
460
|
+
recordId,
|
|
461
|
+
revision,
|
|
462
|
+
value: {
|
|
463
|
+
createdAt,
|
|
464
|
+
description: boundedString(value, 'description', 16 * 1024, true),
|
|
465
|
+
firstBaseOid: token(value, 'firstBaseOid', CollabValidation_1.isCollabGitOid),
|
|
466
|
+
latestHeadOid: token(value, 'latestHeadOid', CollabValidation_1.isCollabGitOid),
|
|
467
|
+
memberId: token(value, 'memberId', CollabValidation_1.isCollabMemberId),
|
|
468
|
+
mergedOid,
|
|
469
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
470
|
+
requestId,
|
|
471
|
+
status,
|
|
472
|
+
updatedAt,
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function requestCommentRecord(source, recordId, revision) {
|
|
477
|
+
const value = exactRecord(source.value, 'value', [
|
|
478
|
+
'authorMemberId',
|
|
479
|
+
'body',
|
|
480
|
+
'commentId',
|
|
481
|
+
'createdAt',
|
|
482
|
+
'projectId',
|
|
483
|
+
'requestId',
|
|
484
|
+
]);
|
|
485
|
+
const commentId = token(value, 'commentId');
|
|
486
|
+
if (recordId !== commentId)
|
|
487
|
+
throw invalidPayload('recordId');
|
|
488
|
+
return {
|
|
489
|
+
kind: 'request-comment',
|
|
490
|
+
recordId,
|
|
491
|
+
revision,
|
|
492
|
+
value: {
|
|
493
|
+
authorMemberId: token(value, 'authorMemberId', CollabValidation_1.isCollabMemberId),
|
|
494
|
+
body: boundedString(value, 'body', 16 * 1024, true),
|
|
495
|
+
commentId,
|
|
496
|
+
createdAt: timestamp(value, 'createdAt'),
|
|
497
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
498
|
+
requestId: token(value, 'requestId'),
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function ticketRecord(source, recordId, revision) {
|
|
503
|
+
const value = exactRecord(source.value, 'value', [
|
|
504
|
+
'authorMemberId',
|
|
505
|
+
'body',
|
|
506
|
+
'closedAt',
|
|
507
|
+
'closedByMemberId',
|
|
508
|
+
'createdAt',
|
|
509
|
+
'number',
|
|
510
|
+
'projectId',
|
|
511
|
+
'status',
|
|
512
|
+
'ticketId',
|
|
513
|
+
'title',
|
|
514
|
+
'updatedAt',
|
|
515
|
+
]);
|
|
516
|
+
const ticketId = token(value, 'ticketId');
|
|
517
|
+
if (recordId !== ticketId)
|
|
518
|
+
throw invalidPayload('recordId');
|
|
519
|
+
const status = literal(value, 'status', ['closed', 'open']);
|
|
520
|
+
const closedAt = nullableTimestamp(value, 'closedAt');
|
|
521
|
+
const closedByMemberId = value.closedByMemberId === null
|
|
522
|
+
? null
|
|
523
|
+
: token(value, 'closedByMemberId', CollabValidation_1.isCollabMemberId);
|
|
524
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
525
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
526
|
+
if ((status === 'open' && (closedAt !== null || closedByMemberId !== null))
|
|
527
|
+
|| (status === 'closed' && (closedAt === null || closedByMemberId === null))
|
|
528
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt)
|
|
529
|
+
|| (closedAt !== null && Date.parse(closedAt) < Date.parse(createdAt)))
|
|
530
|
+
throw invalidPayload('status');
|
|
531
|
+
return {
|
|
532
|
+
kind: 'ticket',
|
|
533
|
+
recordId,
|
|
534
|
+
revision,
|
|
535
|
+
value: {
|
|
536
|
+
authorMemberId: token(value, 'authorMemberId', CollabValidation_1.isCollabMemberId),
|
|
537
|
+
body: boundedString(value, 'body', 32 * 1024, true),
|
|
538
|
+
closedAt,
|
|
539
|
+
closedByMemberId,
|
|
540
|
+
createdAt,
|
|
541
|
+
number: positiveInteger(value, 'number'),
|
|
542
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
543
|
+
status,
|
|
544
|
+
ticketId,
|
|
545
|
+
title: boundedString(value, 'title', 1024),
|
|
546
|
+
updatedAt,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function ticketCommentRecord(source, recordId, revision) {
|
|
551
|
+
const value = exactRecord(source.value, 'value', [
|
|
552
|
+
'authorMemberId',
|
|
553
|
+
'body',
|
|
554
|
+
'commentId',
|
|
555
|
+
'createdAt',
|
|
556
|
+
'projectId',
|
|
557
|
+
'ticketId',
|
|
558
|
+
]);
|
|
559
|
+
const commentId = token(value, 'commentId');
|
|
560
|
+
if (recordId !== commentId)
|
|
561
|
+
throw invalidPayload('recordId');
|
|
562
|
+
return {
|
|
563
|
+
kind: 'ticket-comment',
|
|
564
|
+
recordId,
|
|
565
|
+
revision,
|
|
566
|
+
value: {
|
|
567
|
+
authorMemberId: token(value, 'authorMemberId', CollabValidation_1.isCollabMemberId),
|
|
568
|
+
body: boundedString(value, 'body', 16 * 1024, true),
|
|
569
|
+
commentId,
|
|
570
|
+
createdAt: timestamp(value, 'createdAt'),
|
|
571
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
572
|
+
ticketId: token(value, 'ticketId'),
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function ticketRelationRecord(source, recordId, revision) {
|
|
577
|
+
const value = exactRecord(source.value, 'value', [
|
|
578
|
+
'acceptedAt',
|
|
579
|
+
'acceptedMergeOid',
|
|
580
|
+
'commitOid',
|
|
581
|
+
'createdAt',
|
|
582
|
+
'createdByMemberId',
|
|
583
|
+
'kind',
|
|
584
|
+
'projectId',
|
|
585
|
+
'relationId',
|
|
586
|
+
'requestId',
|
|
587
|
+
'state',
|
|
588
|
+
'ticketId',
|
|
589
|
+
'updatedAt',
|
|
590
|
+
]);
|
|
591
|
+
const relationId = token(value, 'relationId');
|
|
592
|
+
if (recordId !== relationId)
|
|
593
|
+
throw invalidPayload('recordId');
|
|
594
|
+
const state = literal(value, 'state', ['accepted', 'pending']);
|
|
595
|
+
const acceptedAt = nullableTimestamp(value, 'acceptedAt');
|
|
596
|
+
const acceptedMergeOid = value.acceptedMergeOid === null
|
|
597
|
+
? null
|
|
598
|
+
: token(value, 'acceptedMergeOid', CollabValidation_1.isCollabGitOid);
|
|
599
|
+
const createdAt = timestamp(value, 'createdAt');
|
|
600
|
+
const updatedAt = timestamp(value, 'updatedAt');
|
|
601
|
+
if ((state === 'pending' && (acceptedAt !== null || acceptedMergeOid !== null))
|
|
602
|
+
|| (state === 'accepted' && (acceptedAt === null || acceptedMergeOid === null))
|
|
603
|
+
|| Date.parse(updatedAt) < Date.parse(createdAt)
|
|
604
|
+
|| (acceptedAt !== null && Date.parse(acceptedAt) < Date.parse(createdAt)))
|
|
605
|
+
throw invalidPayload('state');
|
|
606
|
+
return {
|
|
607
|
+
kind: 'ticket-relation',
|
|
608
|
+
recordId,
|
|
609
|
+
revision,
|
|
610
|
+
value: {
|
|
611
|
+
acceptedAt,
|
|
612
|
+
acceptedMergeOid,
|
|
613
|
+
commitOid: token(value, 'commitOid', CollabValidation_1.isCollabGitOid),
|
|
614
|
+
createdAt,
|
|
615
|
+
createdByMemberId: token(value, 'createdByMemberId', CollabValidation_1.isCollabMemberId),
|
|
616
|
+
kind: literal(value, 'kind', ['references', 'resolves']),
|
|
617
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
618
|
+
relationId,
|
|
619
|
+
requestId: token(value, 'requestId'),
|
|
620
|
+
state,
|
|
621
|
+
ticketId: token(value, 'ticketId'),
|
|
622
|
+
updatedAt,
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function ticketMentionRecord(source, recordId, revision) {
|
|
627
|
+
const value = exactRecord(source.value, 'value', [
|
|
628
|
+
'createdAt',
|
|
629
|
+
'mentionedMemberId',
|
|
630
|
+
'projectId',
|
|
631
|
+
'sourceId',
|
|
632
|
+
'sourceKind',
|
|
633
|
+
'ticketId',
|
|
634
|
+
]);
|
|
635
|
+
return {
|
|
636
|
+
kind: 'ticket-mention',
|
|
637
|
+
recordId,
|
|
638
|
+
revision,
|
|
639
|
+
value: {
|
|
640
|
+
createdAt: timestamp(value, 'createdAt'),
|
|
641
|
+
mentionedMemberId: token(value, 'mentionedMemberId', CollabValidation_1.isCollabMemberId),
|
|
642
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
643
|
+
sourceId: token(value, 'sourceId'),
|
|
644
|
+
sourceKind: literal(value, 'sourceKind', ['comment', 'description']),
|
|
645
|
+
ticketId: token(value, 'ticketId'),
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function cloudEventRecord(source, recordId, revision) {
|
|
650
|
+
const value = exactRecord(source.value, 'value', ['event']);
|
|
651
|
+
const event = (0, CollabCloudProjectEvent_1.decodeCollabCloudProjectEventMessage)(value.event);
|
|
652
|
+
if (!('projectId' in event) || recordId !== String(event.sequence).padStart(20, '0')) {
|
|
653
|
+
throw invalidPayload('event');
|
|
654
|
+
}
|
|
655
|
+
return { kind: 'cloud-event', recordId, revision, value: { event } };
|
|
656
|
+
}
|
|
657
|
+
function cloudEventCursorRecord(source, recordId, revision) {
|
|
658
|
+
const value = exactRecord(source.value, 'value', [
|
|
659
|
+
'currentSequence',
|
|
660
|
+
'projectId',
|
|
661
|
+
'updatedAt',
|
|
662
|
+
]);
|
|
663
|
+
const projectId = token(value, 'projectId', CollabValidation_1.isCollabProjectId);
|
|
664
|
+
if (recordId !== projectId)
|
|
665
|
+
throw invalidPayload('recordId');
|
|
666
|
+
return {
|
|
667
|
+
kind: 'cloud-event-cursor',
|
|
668
|
+
recordId,
|
|
669
|
+
revision,
|
|
670
|
+
value: {
|
|
671
|
+
currentSequence: nonNegativeInteger(value, 'currentSequence'),
|
|
672
|
+
projectId,
|
|
673
|
+
updatedAt: timestamp(value, 'updatedAt'),
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function idempotencyResultRecord(source, recordId, revision) {
|
|
678
|
+
const value = exactRecord(source.value, 'value', [
|
|
679
|
+
'createdAt',
|
|
680
|
+
'idempotencyKey',
|
|
681
|
+
'memberId',
|
|
682
|
+
'operation',
|
|
683
|
+
'projectId',
|
|
684
|
+
'requestFingerprint',
|
|
685
|
+
'responseJson',
|
|
686
|
+
]);
|
|
687
|
+
const operation = controlOperation(value, 'operation');
|
|
688
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
689
|
+
const projectId = token(value, 'projectId', CollabValidation_1.isCollabProjectId);
|
|
690
|
+
const response = canonicalOperationResponseJson(value, 'responseJson', operation);
|
|
691
|
+
if (recordId !== idempotencyKey
|
|
692
|
+
|| (Object.hasOwn(response.decoded, 'projectId')
|
|
693
|
+
&& response.decoded.projectId !== projectId))
|
|
694
|
+
throw invalidPayload('responseJson');
|
|
695
|
+
return {
|
|
696
|
+
kind: 'idempotency-result',
|
|
697
|
+
recordId,
|
|
698
|
+
revision,
|
|
699
|
+
value: {
|
|
700
|
+
createdAt: timestamp(value, 'createdAt'),
|
|
701
|
+
idempotencyKey,
|
|
702
|
+
memberId: token(value, 'memberId', CollabValidation_1.isCollabMemberId),
|
|
703
|
+
operation,
|
|
704
|
+
projectId,
|
|
705
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
706
|
+
responseJson: response.responseJson,
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function principalBindingRecord(source, recordId, revision) {
|
|
711
|
+
const value = exactRecord(source.value, 'value', [
|
|
712
|
+
'boundAt',
|
|
713
|
+
'memberId',
|
|
714
|
+
'principalId',
|
|
715
|
+
'projectId',
|
|
716
|
+
]);
|
|
717
|
+
const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
|
|
718
|
+
if (recordId !== memberId)
|
|
719
|
+
throw invalidPayload('recordId');
|
|
720
|
+
return {
|
|
721
|
+
kind: 'principal-binding',
|
|
722
|
+
recordId,
|
|
723
|
+
revision,
|
|
724
|
+
value: {
|
|
725
|
+
boundAt: timestamp(value, 'boundAt'),
|
|
726
|
+
memberId,
|
|
727
|
+
principalId: token(value, 'principalId'),
|
|
728
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function repositoryPlacementRecord(source, recordId, revision) {
|
|
733
|
+
const value = exactRecord(source.value, 'value', [
|
|
734
|
+
'nodeId',
|
|
735
|
+
'placementGeneration',
|
|
736
|
+
'projectId',
|
|
737
|
+
'repositoryIdentity',
|
|
738
|
+
]);
|
|
739
|
+
return {
|
|
740
|
+
kind: 'repository-placement',
|
|
741
|
+
recordId,
|
|
742
|
+
revision,
|
|
743
|
+
value: {
|
|
744
|
+
nodeId: token(value, 'nodeId'),
|
|
745
|
+
placementGeneration: positiveInteger(value, 'placementGeneration'),
|
|
746
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
747
|
+
repositoryIdentity: token(value, 'repositoryIdentity'),
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function lifecycleStateRecord(source, recordId, revision) {
|
|
752
|
+
const value = exactRecord(source.value, 'value', [
|
|
753
|
+
'batchRevision',
|
|
754
|
+
'batchSha256',
|
|
755
|
+
'checkpointSha256',
|
|
756
|
+
'direction',
|
|
757
|
+
'operationId',
|
|
758
|
+
'operationKind',
|
|
759
|
+
'phase',
|
|
760
|
+
'projectId',
|
|
761
|
+
'relinquishmentProof',
|
|
762
|
+
'updatedAt',
|
|
763
|
+
]);
|
|
764
|
+
const operationId = token(value, 'operationId');
|
|
765
|
+
if (recordId !== operationId)
|
|
766
|
+
throw invalidPayload('recordId');
|
|
767
|
+
const operationKind = literal(value, 'operationKind', [
|
|
768
|
+
'authority-transfer',
|
|
769
|
+
'backup',
|
|
770
|
+
'delete',
|
|
771
|
+
'retire',
|
|
772
|
+
]);
|
|
773
|
+
const common = {
|
|
774
|
+
operationId,
|
|
775
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
776
|
+
updatedAt: timestamp(value, 'updatedAt'),
|
|
777
|
+
};
|
|
778
|
+
if (operationKind === 'authority-transfer') {
|
|
779
|
+
const fence = (0, CollabAuthorityTransfer_1.decodeCollabAuthorityTransferLifecycleFence)({
|
|
780
|
+
batchRevision: value.batchRevision,
|
|
781
|
+
batchSha256: value.batchSha256,
|
|
782
|
+
checkpointSha256: value.checkpointSha256,
|
|
783
|
+
direction: value.direction,
|
|
784
|
+
phase: value.phase,
|
|
785
|
+
});
|
|
786
|
+
const relinquishmentProof = value.relinquishmentProof === null
|
|
787
|
+
? null
|
|
788
|
+
: (0, CollabAuthorityTransfer_1.decodeCollabAuthorityRelinquishmentProof)(value.relinquishmentProof);
|
|
789
|
+
if (fence.relinquishmentRequired !== (relinquishmentProof !== null)
|
|
790
|
+
|| (relinquishmentProof !== null && (relinquishmentProof.batchRevision !== fence.batchRevision
|
|
791
|
+
|| relinquishmentProof.batchSha256 !== fence.batchSha256
|
|
792
|
+
|| relinquishmentProof.checkpointSha256 !== fence.checkpointSha256
|
|
793
|
+
|| relinquishmentProof.projectId !== common.projectId
|
|
794
|
+
|| relinquishmentProof.transferId !== operationId
|
|
795
|
+
|| (fence.direction === 'lan-to-cloud'
|
|
796
|
+
? relinquishmentProof.sourceAuthority.kind !== 'lan'
|
|
797
|
+
|| relinquishmentProof.targetAuthority.kind !== 'cloud'
|
|
798
|
+
: relinquishmentProof.sourceAuthority.kind !== 'cloud'
|
|
799
|
+
|| relinquishmentProof.targetAuthority.kind !== 'lan'))))
|
|
800
|
+
throw invalidPayload('relinquishmentProof');
|
|
801
|
+
return {
|
|
802
|
+
kind: 'lifecycle-state',
|
|
803
|
+
recordId,
|
|
804
|
+
revision,
|
|
805
|
+
value: {
|
|
806
|
+
batchRevision: fence.batchRevision,
|
|
807
|
+
batchSha256: fence.batchSha256,
|
|
808
|
+
checkpointSha256: fence.checkpointSha256,
|
|
809
|
+
direction: fence.direction,
|
|
810
|
+
operationId,
|
|
811
|
+
operationKind,
|
|
812
|
+
phase: fence.phase,
|
|
813
|
+
projectId: common.projectId,
|
|
814
|
+
relinquishmentProof,
|
|
815
|
+
updatedAt: common.updatedAt,
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
if (value.direction !== null
|
|
820
|
+
|| value.batchRevision !== null
|
|
821
|
+
|| value.batchSha256 !== null
|
|
822
|
+
|| value.relinquishmentProof !== null
|
|
823
|
+
|| (operationKind !== 'backup' && value.checkpointSha256 !== null))
|
|
824
|
+
throw invalidPayload('lifecycleState');
|
|
825
|
+
const checkpointSha256 = value.checkpointSha256 === null
|
|
826
|
+
? null
|
|
827
|
+
: sha256(value, 'checkpointSha256');
|
|
828
|
+
const phase = boundedString(value, 'phase', 128);
|
|
829
|
+
const internalState = operationKind === 'backup'
|
|
830
|
+
? {
|
|
831
|
+
batchRevision: null,
|
|
832
|
+
batchSha256: null,
|
|
833
|
+
checkpointSha256,
|
|
834
|
+
direction: null,
|
|
835
|
+
operationId,
|
|
836
|
+
operationKind,
|
|
837
|
+
phase,
|
|
838
|
+
projectId: common.projectId,
|
|
839
|
+
relinquishmentProof: null,
|
|
840
|
+
updatedAt: common.updatedAt,
|
|
841
|
+
}
|
|
842
|
+
: {
|
|
843
|
+
batchRevision: null,
|
|
844
|
+
batchSha256: null,
|
|
845
|
+
checkpointSha256: null,
|
|
846
|
+
direction: null,
|
|
847
|
+
operationId,
|
|
848
|
+
operationKind,
|
|
849
|
+
phase,
|
|
850
|
+
projectId: common.projectId,
|
|
851
|
+
relinquishmentProof: null,
|
|
852
|
+
updatedAt: common.updatedAt,
|
|
853
|
+
};
|
|
854
|
+
return {
|
|
855
|
+
kind: 'lifecycle-state',
|
|
856
|
+
recordId,
|
|
857
|
+
revision,
|
|
858
|
+
value: internalState,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function terminalResponderRecord(source, recordId, revision) {
|
|
862
|
+
const value = exactRecord(source.value, 'value', [
|
|
863
|
+
'acknowledgements',
|
|
864
|
+
'eligibleMemberIds',
|
|
865
|
+
'expiresAt',
|
|
866
|
+
'operation',
|
|
867
|
+
'operationId',
|
|
868
|
+
'projectId',
|
|
869
|
+
'responseJson',
|
|
870
|
+
]);
|
|
871
|
+
const operationId = token(value, 'operationId');
|
|
872
|
+
if (recordId !== operationId)
|
|
873
|
+
throw invalidPayload('recordId');
|
|
874
|
+
const operation = controlOperation(value, 'operation');
|
|
875
|
+
const projectId = token(value, 'projectId', CollabValidation_1.isCollabProjectId);
|
|
876
|
+
const response = canonicalOperationResponseJson(value, 'responseJson', operation);
|
|
877
|
+
const responseOperationId = Object.hasOwn(response.decoded, 'transferId')
|
|
878
|
+
? response.decoded.transferId
|
|
879
|
+
: Object.hasOwn(response.decoded, 'retirementId')
|
|
880
|
+
? response.decoded.retirementId
|
|
881
|
+
: null;
|
|
882
|
+
if (response.decoded.projectId !== projectId
|
|
883
|
+
|| responseOperationId !== operationId)
|
|
884
|
+
throw invalidPayload('responseJson');
|
|
885
|
+
const expiresAt = timestamp(value, 'expiresAt');
|
|
886
|
+
if (operation === 'retireProject') {
|
|
887
|
+
if (response.decoded.kind !== 'project-retired'
|
|
888
|
+
|| response.decoded.terminalExpiresAt !== expiresAt)
|
|
889
|
+
throw invalidPayload('responseJson');
|
|
890
|
+
}
|
|
891
|
+
else if (operation === 'getProjectAuthorityTransfer') {
|
|
892
|
+
if (response.decoded.direction !== 'cloud-to-lan'
|
|
893
|
+
|| response.decoded.phase !== 'completed'
|
|
894
|
+
|| response.decoded.state !== 'completed'
|
|
895
|
+
|| response.decoded.expiresAt !== expiresAt)
|
|
896
|
+
throw invalidPayload('responseJson');
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
throw invalidPayload('operation');
|
|
900
|
+
}
|
|
901
|
+
if (!Array.isArray(value.eligibleMemberIds))
|
|
902
|
+
throw invalidPayload('eligibleMemberIds');
|
|
903
|
+
const eligibleMemberIds = value.eligibleMemberIds.map((item) => {
|
|
904
|
+
if (!(0, CollabValidation_1.isCollabMemberId)(item))
|
|
905
|
+
throw invalidPayload('eligibleMemberIds');
|
|
906
|
+
return item;
|
|
907
|
+
});
|
|
908
|
+
if (eligibleMemberIds.some((item, index) => (index > 0 && eligibleMemberIds[index - 1].localeCompare(item, 'en-US') >= 0)))
|
|
909
|
+
throw invalidPayload('eligibleMemberIds');
|
|
910
|
+
if (!Array.isArray(value.acknowledgements))
|
|
911
|
+
throw invalidPayload('acknowledgements');
|
|
912
|
+
const acknowledgements = value.acknowledgements.map((item) => {
|
|
913
|
+
const acknowledgement = exactRecord(item, 'acknowledgement', [
|
|
914
|
+
'acknowledgedAt',
|
|
915
|
+
'memberId',
|
|
916
|
+
'principalId',
|
|
917
|
+
]);
|
|
918
|
+
return {
|
|
919
|
+
acknowledgedAt: timestamp(acknowledgement, 'acknowledgedAt'),
|
|
920
|
+
memberId: token(acknowledgement, 'memberId', CollabValidation_1.isCollabMemberId),
|
|
921
|
+
principalId: token(acknowledgement, 'principalId'),
|
|
922
|
+
};
|
|
923
|
+
});
|
|
924
|
+
const acknowledgementPrincipals = new Set();
|
|
925
|
+
acknowledgements.forEach((item, index) => {
|
|
926
|
+
if (!eligibleMemberIds.includes(item.memberId)
|
|
927
|
+
|| (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
|
|
928
|
+
|| acknowledgementPrincipals.has(item.principalId))
|
|
929
|
+
throw invalidPayload('acknowledgements');
|
|
930
|
+
acknowledgementPrincipals.add(item.principalId);
|
|
931
|
+
});
|
|
932
|
+
return {
|
|
933
|
+
kind: 'terminal-responder',
|
|
934
|
+
recordId,
|
|
935
|
+
revision,
|
|
936
|
+
value: {
|
|
937
|
+
acknowledgements: Object.freeze(acknowledgements),
|
|
938
|
+
eligibleMemberIds: Object.freeze(eligibleMemberIds),
|
|
939
|
+
expiresAt,
|
|
940
|
+
operation,
|
|
941
|
+
operationId,
|
|
942
|
+
projectId,
|
|
943
|
+
responseJson: response.responseJson,
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
const PROTECTED_CLAIM_ASSOCIATED_DATA_KEYS = [
|
|
948
|
+
'authorityGeneration',
|
|
949
|
+
'checkpointSha256',
|
|
950
|
+
'claimSha256',
|
|
951
|
+
'envelopeVersion',
|
|
952
|
+
'environmentIdentity',
|
|
953
|
+
'memberId',
|
|
954
|
+
'projectId',
|
|
955
|
+
'transferId',
|
|
956
|
+
];
|
|
957
|
+
function decodeProtectedClaimAssociatedData(value) {
|
|
958
|
+
const associatedData = exactRecord(value, 'associatedData', PROTECTED_CLAIM_ASSOCIATED_DATA_KEYS);
|
|
959
|
+
const envelopeVersion = associatedData.envelopeVersion;
|
|
960
|
+
if (envelopeVersion !== exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION) {
|
|
961
|
+
throw invalidPayload('envelopeVersion');
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
authorityGeneration: positiveInteger(associatedData, 'authorityGeneration'),
|
|
965
|
+
checkpointSha256: sha256(associatedData, 'checkpointSha256'),
|
|
966
|
+
claimSha256: sha256(associatedData, 'claimSha256'),
|
|
967
|
+
envelopeVersion,
|
|
968
|
+
environmentIdentity: token(associatedData, 'environmentIdentity'),
|
|
969
|
+
memberId: token(associatedData, 'memberId', CollabValidation_1.isCollabMemberId),
|
|
970
|
+
projectId: token(associatedData, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
971
|
+
transferId: token(associatedData, 'transferId'),
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function encodeCollabProtectedClaimAssociatedData(associatedData) {
|
|
975
|
+
return JSON.stringify({
|
|
976
|
+
domain: 'claudian-collab.protected-claim-envelope-associated-data.v1',
|
|
977
|
+
payload: decodeProtectedClaimAssociatedData(associatedData),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
function protectedClaimEnvelopeRecord(source, recordId, revision) {
|
|
981
|
+
const value = exactRecord(source.value, 'value', [
|
|
982
|
+
'associatedData',
|
|
983
|
+
'associatedDataSha256',
|
|
984
|
+
'ciphertext',
|
|
985
|
+
'encryptionAlgorithm',
|
|
986
|
+
'expiresAt',
|
|
987
|
+
'keyId',
|
|
988
|
+
'keyVersion',
|
|
989
|
+
'memberId',
|
|
990
|
+
'nonce',
|
|
991
|
+
'receiptKeyId',
|
|
992
|
+
'tag',
|
|
993
|
+
'transferId',
|
|
994
|
+
]);
|
|
995
|
+
const transferId = token(value, 'transferId');
|
|
996
|
+
const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
|
|
997
|
+
const decodedAssociatedData = decodeProtectedClaimAssociatedData(value.associatedData);
|
|
998
|
+
if (decodedAssociatedData.memberId !== memberId
|
|
999
|
+
|| decodedAssociatedData.transferId !== transferId)
|
|
1000
|
+
throw invalidPayload('associatedData');
|
|
1001
|
+
return {
|
|
1002
|
+
kind: 'protected-claim-envelope',
|
|
1003
|
+
recordId,
|
|
1004
|
+
revision,
|
|
1005
|
+
value: {
|
|
1006
|
+
associatedData: decodedAssociatedData,
|
|
1007
|
+
associatedDataSha256: sha256(value, 'associatedDataSha256'),
|
|
1008
|
+
ciphertext: canonicalBase64url(value, 'ciphertext', exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.maxCiphertextBytes),
|
|
1009
|
+
encryptionAlgorithm: literal(value, 'encryptionAlgorithm', ['xchacha20-poly1305']),
|
|
1010
|
+
expiresAt: timestamp(value, 'expiresAt'),
|
|
1011
|
+
keyId: boundedString(value, 'keyId', 256),
|
|
1012
|
+
keyVersion: positiveInteger(value, 'keyVersion'),
|
|
1013
|
+
memberId,
|
|
1014
|
+
nonce: canonicalBase64url(value, 'nonce', exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.nonceBytes, exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.nonceBytes),
|
|
1015
|
+
receiptKeyId: boundedString(value, 'receiptKeyId', 256),
|
|
1016
|
+
tag: canonicalBase64url(value, 'tag', exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.tagBytes, exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS.tagBytes),
|
|
1017
|
+
transferId,
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
function tombstoneRecord(source, recordId, revision) {
|
|
1022
|
+
const value = exactRecord(source.value, 'value', [
|
|
1023
|
+
'authorityGeneration',
|
|
1024
|
+
'projectId',
|
|
1025
|
+
'retiredAt',
|
|
1026
|
+
'terminalExpiresAt',
|
|
1027
|
+
]);
|
|
1028
|
+
const projectId = token(value, 'projectId', CollabValidation_1.isCollabProjectId);
|
|
1029
|
+
if (recordId !== projectId)
|
|
1030
|
+
throw invalidPayload('recordId');
|
|
1031
|
+
const retiredAt = timestamp(value, 'retiredAt');
|
|
1032
|
+
const terminalExpiresAt = timestamp(value, 'terminalExpiresAt');
|
|
1033
|
+
if (Date.parse(terminalExpiresAt) <= Date.parse(retiredAt)) {
|
|
1034
|
+
throw invalidPayload('terminalExpiresAt');
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
kind: 'tombstone',
|
|
1038
|
+
recordId,
|
|
1039
|
+
revision,
|
|
1040
|
+
value: {
|
|
1041
|
+
authorityGeneration: positiveInteger(value, 'authorityGeneration'),
|
|
1042
|
+
projectId,
|
|
1043
|
+
retiredAt,
|
|
1044
|
+
terminalExpiresAt,
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function schemaCatalogRecord(source, recordId, revision) {
|
|
1049
|
+
const value = exactRecord(source.value, 'value', [
|
|
1050
|
+
'coordinationSchemaVersion',
|
|
1051
|
+
'projectId',
|
|
1052
|
+
'repositoryFormatVersion',
|
|
1053
|
+
]);
|
|
1054
|
+
const projectId = token(value, 'projectId', CollabValidation_1.isCollabProjectId);
|
|
1055
|
+
if (recordId !== projectId)
|
|
1056
|
+
throw invalidPayload('recordId');
|
|
1057
|
+
return {
|
|
1058
|
+
kind: 'schema-catalog',
|
|
1059
|
+
recordId,
|
|
1060
|
+
revision,
|
|
1061
|
+
value: {
|
|
1062
|
+
coordinationSchemaVersion: positiveInteger(value, 'coordinationSchemaVersion'),
|
|
1063
|
+
projectId,
|
|
1064
|
+
repositoryFormatVersion: positiveInteger(value, 'repositoryFormatVersion'),
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function serverCompatibilityRecord(source, recordId, revision) {
|
|
1069
|
+
const value = exactRecord(source.value, 'value', [
|
|
1070
|
+
'maximumBuild',
|
|
1071
|
+
'minimumBuild',
|
|
1072
|
+
'projectId',
|
|
1073
|
+
]);
|
|
1074
|
+
const projectId = token(value, 'projectId', CollabValidation_1.isCollabProjectId);
|
|
1075
|
+
if (recordId !== projectId)
|
|
1076
|
+
throw invalidPayload('recordId');
|
|
1077
|
+
return {
|
|
1078
|
+
kind: 'server-compatibility',
|
|
1079
|
+
recordId,
|
|
1080
|
+
revision,
|
|
1081
|
+
value: {
|
|
1082
|
+
maximumBuild: boundedString(value, 'maximumBuild', 128),
|
|
1083
|
+
minimumBuild: boundedString(value, 'minimumBuild', 128),
|
|
1084
|
+
projectId,
|
|
1085
|
+
},
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
function authorityVolumePairRecord(source, recordId, revision) {
|
|
1089
|
+
const value = exactRecord(source.value, 'value', [
|
|
1090
|
+
'authorityId',
|
|
1091
|
+
'authorityVolumeIdentity',
|
|
1092
|
+
'projectId',
|
|
1093
|
+
'restoreEpoch',
|
|
1094
|
+
]);
|
|
1095
|
+
const projectId = token(value, 'projectId', CollabValidation_1.isCollabProjectId);
|
|
1096
|
+
if (recordId !== projectId)
|
|
1097
|
+
throw invalidPayload('recordId');
|
|
1098
|
+
return {
|
|
1099
|
+
kind: 'authority-volume-pair',
|
|
1100
|
+
recordId,
|
|
1101
|
+
revision,
|
|
1102
|
+
value: {
|
|
1103
|
+
authorityId: token(value, 'authorityId'),
|
|
1104
|
+
authorityVolumeIdentity: token(value, 'authorityVolumeIdentity'),
|
|
1105
|
+
projectId,
|
|
1106
|
+
restoreEpoch: positiveInteger(value, 'restoreEpoch'),
|
|
1107
|
+
},
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function decodeCheckpointRecord(value) {
|
|
1111
|
+
const { kind, recordId, revision, source } = recordEnvelope(value);
|
|
1112
|
+
switch (kind) {
|
|
1113
|
+
case 'project': return projectRecord(source, recordId, revision);
|
|
1114
|
+
case 'member': return memberRecord(source, recordId, revision);
|
|
1115
|
+
case 'request': return requestRecord(source, recordId, revision);
|
|
1116
|
+
case 'request-comment': return requestCommentRecord(source, recordId, revision);
|
|
1117
|
+
case 'ticket': return ticketRecord(source, recordId, revision);
|
|
1118
|
+
case 'ticket-comment': return ticketCommentRecord(source, recordId, revision);
|
|
1119
|
+
case 'ticket-relation': return ticketRelationRecord(source, recordId, revision);
|
|
1120
|
+
case 'ticket-mention': return ticketMentionRecord(source, recordId, revision);
|
|
1121
|
+
case 'cloud-event': return cloudEventRecord(source, recordId, revision);
|
|
1122
|
+
case 'cloud-event-cursor': return cloudEventCursorRecord(source, recordId, revision);
|
|
1123
|
+
case 'idempotency-result': return idempotencyResultRecord(source, recordId, revision);
|
|
1124
|
+
case 'principal-binding': return principalBindingRecord(source, recordId, revision);
|
|
1125
|
+
case 'repository-placement': return repositoryPlacementRecord(source, recordId, revision);
|
|
1126
|
+
case 'lifecycle-state': return lifecycleStateRecord(source, recordId, revision);
|
|
1127
|
+
case 'terminal-responder': return terminalResponderRecord(source, recordId, revision);
|
|
1128
|
+
case 'protected-claim-envelope':
|
|
1129
|
+
return protectedClaimEnvelopeRecord(source, recordId, revision);
|
|
1130
|
+
case 'tombstone': return tombstoneRecord(source, recordId, revision);
|
|
1131
|
+
case 'schema-catalog': return schemaCatalogRecord(source, recordId, revision);
|
|
1132
|
+
case 'server-compatibility': return serverCompatibilityRecord(source, recordId, revision);
|
|
1133
|
+
case 'authority-volume-pair': return authorityVolumePairRecord(source, recordId, revision);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function recordKindOrder(kind) {
|
|
1137
|
+
return exports.COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS.indexOf(kind);
|
|
1138
|
+
}
|
|
1139
|
+
function compareRecords(left, right) {
|
|
1140
|
+
const kindOrder = recordKindOrder(left.kind) - recordKindOrder(right.kind);
|
|
1141
|
+
return kindOrder !== 0 ? kindOrder : left.recordId.localeCompare(right.recordId, 'en-US');
|
|
1142
|
+
}
|
|
1143
|
+
function validateRecordSequence(records, profile) {
|
|
1144
|
+
if (records.length === 0 || records[0].kind !== 'project')
|
|
1145
|
+
throw invalidPayload('records');
|
|
1146
|
+
const projectId = records[0].recordId;
|
|
1147
|
+
if (records.filter(item => item.kind === 'project').length !== 1) {
|
|
1148
|
+
throw invalidPayload('records');
|
|
1149
|
+
}
|
|
1150
|
+
if (records.some((item, index) => index > 0 && compareRecords(records[index - 1], item) >= 0)) {
|
|
1151
|
+
throw invalidPayload('records');
|
|
1152
|
+
}
|
|
1153
|
+
if (profile !== 'backup' && records.some(item => !PORTABLE_RECORD_KIND_SET.has(item.kind))) {
|
|
1154
|
+
throw invalidPayload('profile');
|
|
1155
|
+
}
|
|
1156
|
+
if (profile === 'backup') {
|
|
1157
|
+
const requiredKinds = [
|
|
1158
|
+
'cloud-event-cursor',
|
|
1159
|
+
'schema-catalog',
|
|
1160
|
+
'server-compatibility',
|
|
1161
|
+
'authority-volume-pair',
|
|
1162
|
+
];
|
|
1163
|
+
if (requiredKinds.some(kind => records.filter(item => item.kind === kind).length !== 1)) {
|
|
1164
|
+
throw invalidPayload('profile');
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const projectRecordValue = records[0].value;
|
|
1168
|
+
if (projectRecordValue.projectId !== projectId)
|
|
1169
|
+
throw invalidPayload('records');
|
|
1170
|
+
for (const item of records) {
|
|
1171
|
+
let itemProjectId;
|
|
1172
|
+
if (item.kind === 'cloud-event')
|
|
1173
|
+
itemProjectId = item.value.event.projectId;
|
|
1174
|
+
else if (item.kind === 'protected-claim-envelope') {
|
|
1175
|
+
itemProjectId = item.value.associatedData.projectId;
|
|
1176
|
+
}
|
|
1177
|
+
else
|
|
1178
|
+
itemProjectId = item.value.projectId;
|
|
1179
|
+
if (itemProjectId !== projectId)
|
|
1180
|
+
throw invalidPayload('records');
|
|
1181
|
+
}
|
|
1182
|
+
const members = new Map(records
|
|
1183
|
+
.filter((item) => item.kind === 'member')
|
|
1184
|
+
.map(item => [item.value.memberId, item]));
|
|
1185
|
+
const requests = new Set(records
|
|
1186
|
+
.filter((item) => item.kind === 'request')
|
|
1187
|
+
.map(item => item.value.requestId));
|
|
1188
|
+
const tickets = new Set(records
|
|
1189
|
+
.filter((item) => item.kind === 'ticket')
|
|
1190
|
+
.map(item => item.value.ticketId));
|
|
1191
|
+
const ticketComments = new Map(records
|
|
1192
|
+
.filter((item) => item.kind === 'ticket-comment')
|
|
1193
|
+
.map(item => [item.value.commentId, item.value.ticketId]));
|
|
1194
|
+
const principalBindings = new Map(records
|
|
1195
|
+
.filter((item) => (item.kind === 'principal-binding'))
|
|
1196
|
+
.map(item => [item.value.memberId, item.value.principalId]));
|
|
1197
|
+
const terminalResponders = records.filter((item) => (item.kind === 'terminal-responder'));
|
|
1198
|
+
const tombstones = records.filter((item) => (item.kind === 'tombstone'));
|
|
1199
|
+
if (terminalResponders.length > 1
|
|
1200
|
+
|| (terminalResponders.length === 1 && (tombstones.length !== 1
|
|
1201
|
+
|| terminalResponders[0].value.expiresAt !== tombstones[0].value.terminalExpiresAt)))
|
|
1202
|
+
throw invalidPayload('records');
|
|
1203
|
+
const tombstone = tombstones[0];
|
|
1204
|
+
const retirementTerminal = terminalResponders.find(item => (item.value.operation === 'retireProject'));
|
|
1205
|
+
if (retirementTerminal !== undefined) {
|
|
1206
|
+
const response = canonicalOperationResponseJson({ responseJson: retirementTerminal.value.responseJson }, 'responseJson', 'retireProject').decoded;
|
|
1207
|
+
if (tombstone === undefined || response.retiredAt !== tombstone.value.retiredAt) {
|
|
1208
|
+
throw invalidPayload('records');
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const retirementEvents = records.filter((item) => (item.kind === 'cloud-event' && item.value.event.kind === 'project.retired'));
|
|
1212
|
+
if (tombstone !== undefined && retirementEvents.some(item => (item.value.event.kind === 'project.retired'
|
|
1213
|
+
&& (item.value.event.payload.retiredAt !== tombstone.value.retiredAt
|
|
1214
|
+
|| (retirementTerminal !== undefined
|
|
1215
|
+
&& item.value.event.payload.retirementId !== retirementTerminal.value.operationId)))))
|
|
1216
|
+
throw invalidPayload('records');
|
|
1217
|
+
for (const item of records) {
|
|
1218
|
+
if (item.kind === 'request' && !members.has(item.value.memberId)) {
|
|
1219
|
+
throw invalidPayload('records');
|
|
1220
|
+
}
|
|
1221
|
+
if (item.kind === 'request-comment' && (!requests.has(item.value.requestId) || !members.has(item.value.authorMemberId)))
|
|
1222
|
+
throw invalidPayload('records');
|
|
1223
|
+
if (item.kind === 'ticket' && (!members.has(item.value.authorMemberId)
|
|
1224
|
+
|| (item.value.closedByMemberId !== null && !members.has(item.value.closedByMemberId))))
|
|
1225
|
+
throw invalidPayload('records');
|
|
1226
|
+
if (item.kind === 'ticket-comment' && (!tickets.has(item.value.ticketId) || !members.has(item.value.authorMemberId)))
|
|
1227
|
+
throw invalidPayload('records');
|
|
1228
|
+
if (item.kind === 'ticket-relation' && (!tickets.has(item.value.ticketId)
|
|
1229
|
+
|| !requests.has(item.value.requestId)
|
|
1230
|
+
|| !members.has(item.value.createdByMemberId)))
|
|
1231
|
+
throw invalidPayload('records');
|
|
1232
|
+
if (item.kind === 'ticket-mention' && (!tickets.has(item.value.ticketId)
|
|
1233
|
+
|| !members.has(item.value.mentionedMemberId)
|
|
1234
|
+
|| (item.value.sourceKind === 'description' && item.value.sourceId !== item.value.ticketId)
|
|
1235
|
+
|| (item.value.sourceKind === 'comment'
|
|
1236
|
+
&& ticketComments.get(item.value.sourceId) !== item.value.ticketId)))
|
|
1237
|
+
throw invalidPayload('records');
|
|
1238
|
+
if ((item.kind === 'idempotency-result' || item.kind === 'principal-binding')
|
|
1239
|
+
&& !members.has(item.value.memberId))
|
|
1240
|
+
throw invalidPayload('records');
|
|
1241
|
+
if (item.kind === 'protected-claim-envelope'
|
|
1242
|
+
&& members.get(item.value.memberId)?.value.status !== 'active') {
|
|
1243
|
+
throw invalidPayload('records');
|
|
1244
|
+
}
|
|
1245
|
+
if (item.kind === 'terminal-responder' && (item.value.eligibleMemberIds.some(memberId => !members.has(memberId))
|
|
1246
|
+
|| item.value.acknowledgements.some(acknowledgement => (principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId))))
|
|
1247
|
+
throw invalidPayload('records');
|
|
1248
|
+
}
|
|
1249
|
+
if (profile === 'backup') {
|
|
1250
|
+
const cursor = records.find((item) => (item.kind === 'cloud-event-cursor'));
|
|
1251
|
+
if (cursor === undefined || records.some(item => (item.kind === 'cloud-event' && item.value.event.sequence > cursor.value.currentSequence)))
|
|
1252
|
+
throw invalidPayload('records');
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function decodeCollabProjectCheckpointCoordinationNdjson(value, profile) {
|
|
1256
|
+
if (typeof value !== 'string'
|
|
1257
|
+
|| !CHECKPOINT_PROFILE_SET.has(profile)
|
|
1258
|
+
|| !value.endsWith('\n')
|
|
1259
|
+
|| !(0, CollabValidation_1.hasUtf8ByteLengthAtMost)(value, exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxCoordinationBytes))
|
|
1260
|
+
throw invalidPayload('coordination');
|
|
1261
|
+
const lines = value.slice(0, -1).split('\n');
|
|
1262
|
+
if (lines.some(line => line.length === 0))
|
|
1263
|
+
throw invalidPayload('coordination');
|
|
1264
|
+
const decoded = lines.map((line) => {
|
|
1265
|
+
let parsed;
|
|
1266
|
+
try {
|
|
1267
|
+
parsed = JSON.parse(line);
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
throw invalidPayload('coordination');
|
|
1271
|
+
}
|
|
1272
|
+
const result = decodeCheckpointRecord(parsed);
|
|
1273
|
+
if (JSON.stringify(result) !== line)
|
|
1274
|
+
throw invalidPayload('coordination');
|
|
1275
|
+
return result;
|
|
1276
|
+
});
|
|
1277
|
+
validateRecordSequence(decoded, profile);
|
|
1278
|
+
return Object.freeze(decoded);
|
|
1279
|
+
}
|
|
1280
|
+
function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
|
|
1281
|
+
const encoded = records.map(record => JSON.stringify(record)).join('\n') + '\n';
|
|
1282
|
+
return decodeCollabProjectCheckpointCoordinationNdjson(encoded, profile)
|
|
1283
|
+
.map(record => JSON.stringify(record)).join('\n') + '\n';
|
|
1284
|
+
}
|
|
1285
|
+
function checkpointRecordGitOids(checkpointRecord) {
|
|
1286
|
+
switch (checkpointRecord.kind) {
|
|
1287
|
+
case 'project': return [checkpointRecord.value.expectedMainOid];
|
|
1288
|
+
case 'request': return [
|
|
1289
|
+
checkpointRecord.value.firstBaseOid,
|
|
1290
|
+
checkpointRecord.value.latestHeadOid,
|
|
1291
|
+
...(checkpointRecord.value.mergedOid === null
|
|
1292
|
+
? []
|
|
1293
|
+
: [checkpointRecord.value.mergedOid]),
|
|
1294
|
+
];
|
|
1295
|
+
case 'ticket-relation': return [
|
|
1296
|
+
checkpointRecord.value.commitOid,
|
|
1297
|
+
...(checkpointRecord.value.acceptedMergeOid === null
|
|
1298
|
+
? []
|
|
1299
|
+
: [checkpointRecord.value.acceptedMergeOid]),
|
|
1300
|
+
];
|
|
1301
|
+
case 'cloud-event': return checkpointRecord.value.event.kind === 'main.updated'
|
|
1302
|
+
? [checkpointRecord.value.event.payload.mainOid]
|
|
1303
|
+
: [];
|
|
1304
|
+
default: return [];
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
function validateCollabProjectCheckpointConsistency(manifest, records) {
|
|
1308
|
+
const decodedManifest = decodeCollabProjectCheckpointManifest(manifest);
|
|
1309
|
+
validateRecordSequence(records, decodedManifest.profile);
|
|
1310
|
+
const project = records[0];
|
|
1311
|
+
if (project.kind !== 'project'
|
|
1312
|
+
|| project.value.projectId !== decodedManifest.projectId
|
|
1313
|
+
|| project.value.authorityGeneration !== decodedManifest.sourceAuthority.generation
|
|
1314
|
+
|| project.value.expectedMainOid !== decodedManifest.expectedMainOid
|
|
1315
|
+
|| records.some(record => checkpointRecordGitOids(record).some(oid => (!gitOidMatchesFormat(oid, decodedManifest.gitObjectFormat)))))
|
|
1316
|
+
throw invalidPayload('checkpoint');
|
|
1317
|
+
const activeMembers = records.filter((item) => (item.kind === 'member' && item.value.status === 'active'));
|
|
1318
|
+
const activeMemberIds = new Set(activeMembers.map(item => item.value.memberId));
|
|
1319
|
+
const activeMemberRefs = activeMembers
|
|
1320
|
+
.map(item => item.value.personalRef)
|
|
1321
|
+
.sort((left, right) => left.localeCompare(right, 'en-US'));
|
|
1322
|
+
const manifestMemberRefs = decodedManifest.refs.slice(1).map(item => item.name);
|
|
1323
|
+
if (activeMemberRefs.length !== manifestMemberRefs.length
|
|
1324
|
+
|| activeMemberRefs.some((item, index) => item !== manifestMemberRefs[index]))
|
|
1325
|
+
throw invalidPayload('refs');
|
|
1326
|
+
const openRequestMemberIds = new Set();
|
|
1327
|
+
for (const record of records) {
|
|
1328
|
+
if (record.kind !== 'request' || record.value.status !== 'open')
|
|
1329
|
+
continue;
|
|
1330
|
+
if (!activeMemberIds.has(record.value.memberId)
|
|
1331
|
+
|| openRequestMemberIds.has(record.value.memberId))
|
|
1332
|
+
throw invalidPayload('refs');
|
|
1333
|
+
openRequestMemberIds.add(record.value.memberId);
|
|
1334
|
+
}
|
|
1335
|
+
return records;
|
|
1336
|
+
}
|