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