@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,353 @@
|
|
|
1
|
+
import { COLLAB_LIMITS } from './CollabConstants.mjs';
|
|
2
|
+
import { CollabError } from './CollabError.mjs';
|
|
3
|
+
import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, } from './CollabValidation.mjs';
|
|
4
|
+
function decodeError(field) {
|
|
5
|
+
return new CollabError({
|
|
6
|
+
code: 'protocol-payload-invalid',
|
|
7
|
+
recoveryActions: ['retry'],
|
|
8
|
+
safeContext: { field },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function record(value, field) {
|
|
15
|
+
if (!isRecord(value))
|
|
16
|
+
throw decodeError(field);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function assertJsonUtf8ByteLengthAtMost(value, maximum, field) {
|
|
20
|
+
let serialized;
|
|
21
|
+
try {
|
|
22
|
+
serialized = JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw decodeError(field);
|
|
26
|
+
}
|
|
27
|
+
if (serialized === undefined
|
|
28
|
+
|| !hasUtf8ByteLengthAtMost(serialized, maximum)) {
|
|
29
|
+
throw decodeError(field);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function string(value, field, maxLength, validate, unit = 'utf16') {
|
|
33
|
+
const candidate = value[field];
|
|
34
|
+
if (typeof candidate !== 'string'
|
|
35
|
+
|| candidate.length === 0
|
|
36
|
+
|| (unit === 'utf8'
|
|
37
|
+
? !hasUtf8ByteLengthAtMost(candidate, maxLength)
|
|
38
|
+
: candidate.length > maxLength)
|
|
39
|
+
|| (validate && !validate(candidate))) {
|
|
40
|
+
throw decodeError(field);
|
|
41
|
+
}
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
function text(value, field, maxLength, unit = 'utf16') {
|
|
45
|
+
const candidate = value[field];
|
|
46
|
+
if (typeof candidate !== 'string'
|
|
47
|
+
|| (unit === 'utf8'
|
|
48
|
+
? !hasUtf8ByteLengthAtMost(candidate, maxLength)
|
|
49
|
+
: candidate.length > maxLength))
|
|
50
|
+
throw decodeError(field);
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
function timestamp(value, field) {
|
|
54
|
+
const candidate = string(value, field, 64);
|
|
55
|
+
if (Number.isNaN(Date.parse(candidate)) || new Date(candidate).toISOString() !== candidate) {
|
|
56
|
+
throw decodeError(field);
|
|
57
|
+
}
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
function optionalTimestamp(value, field) {
|
|
61
|
+
return value[field] === undefined ? undefined : timestamp(value, field);
|
|
62
|
+
}
|
|
63
|
+
function nonNegativeInteger(value, field) {
|
|
64
|
+
const candidate = value[field];
|
|
65
|
+
if (typeof candidate !== 'number'
|
|
66
|
+
|| !Number.isSafeInteger(candidate)
|
|
67
|
+
|| candidate < 0) {
|
|
68
|
+
throw decodeError(field);
|
|
69
|
+
}
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
function boundedNonNegativeInteger(value, field, maximum) {
|
|
73
|
+
const candidate = nonNegativeInteger(value, field);
|
|
74
|
+
if (candidate > maximum)
|
|
75
|
+
throw decodeError(field);
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
function positiveInteger(value, field) {
|
|
79
|
+
const candidate = nonNegativeInteger(value, field);
|
|
80
|
+
if (candidate < 1)
|
|
81
|
+
throw decodeError(field);
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
function requestTicketRelation(value) {
|
|
85
|
+
const source = record(value, 'request.ticketRelations');
|
|
86
|
+
const kind = source.kind;
|
|
87
|
+
const state = source.state;
|
|
88
|
+
if ((kind !== 'references' && kind !== 'resolves')
|
|
89
|
+
|| (state !== 'pending' && state !== 'accepted')) {
|
|
90
|
+
throw decodeError('request.ticketRelations');
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
commitOid: string(source, 'commitOid', 64, isCollabGitOid),
|
|
94
|
+
id: string(source, 'id', 128, isCollabOpaqueId),
|
|
95
|
+
kind,
|
|
96
|
+
state,
|
|
97
|
+
ticketId: string(source, 'ticketId', 128, isCollabOpaqueId),
|
|
98
|
+
ticketNumber: positiveInteger(source, 'ticketNumber'),
|
|
99
|
+
ticketRevision: positiveInteger(source, 'ticketRevision'),
|
|
100
|
+
ticketTitle: string(source, 'ticketTitle', COLLAB_LIMITS.maxTicketTitleUtf16),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function changeRequest(value) {
|
|
104
|
+
const source = record(value, 'request');
|
|
105
|
+
const status = source.status;
|
|
106
|
+
if ((status !== 'open' && status !== 'merged' && status !== 'discarded')
|
|
107
|
+
|| !Array.isArray(source.ticketRelations)
|
|
108
|
+
|| source.ticketRelations.length > COLLAB_LIMITS.maxRequestTicketRelations) {
|
|
109
|
+
throw decodeError('request.status');
|
|
110
|
+
}
|
|
111
|
+
const mergedOid = source.mergedOid === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: string(source, 'mergedOid', 64, isCollabGitOid);
|
|
114
|
+
return {
|
|
115
|
+
commentCount: boundedNonNegativeInteger(source, 'commentCount', COLLAB_LIMITS.maxRequestComments),
|
|
116
|
+
createdAt: timestamp(source, 'createdAt'),
|
|
117
|
+
description: text(source, 'description', COLLAB_LIMITS.maxRequestDescriptionBytes, 'utf8'),
|
|
118
|
+
firstBaseOid: string(source, 'firstBaseOid', 64, isCollabGitOid),
|
|
119
|
+
id: string(source, 'id', 128, isCollabOpaqueId),
|
|
120
|
+
latestHeadOid: string(source, 'latestHeadOid', 64, isCollabGitOid),
|
|
121
|
+
memberId: string(source, 'memberId', 64, isCollabMemberId),
|
|
122
|
+
...(mergedOid ? { mergedOid } : {}),
|
|
123
|
+
revision: nonNegativeInteger(source, 'revision'),
|
|
124
|
+
status,
|
|
125
|
+
ticketRelations: source.ticketRelations.map(requestTicketRelation),
|
|
126
|
+
updatedAt: timestamp(source, 'updatedAt'),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function ticketSummary(value) {
|
|
130
|
+
const source = record(value, 'ticket');
|
|
131
|
+
const status = source.status;
|
|
132
|
+
const closedAt = optionalTimestamp(source, 'closedAt');
|
|
133
|
+
const closedByMemberId = source.closedByMemberId === undefined
|
|
134
|
+
? undefined
|
|
135
|
+
: string(source, 'closedByMemberId', 64, isCollabMemberId);
|
|
136
|
+
if ((status !== 'open' && status !== 'closed')
|
|
137
|
+
|| (status === 'open' && (closedAt !== undefined || closedByMemberId !== undefined))
|
|
138
|
+
|| (status === 'closed' && (closedAt === undefined || closedByMemberId === undefined))) {
|
|
139
|
+
throw decodeError('ticket.status');
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
acceptedRelationCount: boundedNonNegativeInteger(source, 'acceptedRelationCount', COLLAB_LIMITS.maxTicketAcceptedRelations),
|
|
143
|
+
authorMemberId: string(source, 'authorMemberId', 64, isCollabMemberId),
|
|
144
|
+
...(closedAt && closedByMemberId ? { closedAt, closedByMemberId } : {}),
|
|
145
|
+
commentCount: boundedNonNegativeInteger(source, 'commentCount', COLLAB_LIMITS.maxTicketComments),
|
|
146
|
+
createdAt: timestamp(source, 'createdAt'),
|
|
147
|
+
id: string(source, 'id', 128, isCollabOpaqueId),
|
|
148
|
+
number: positiveInteger(source, 'number'),
|
|
149
|
+
revision: positiveInteger(source, 'revision'),
|
|
150
|
+
status,
|
|
151
|
+
title: string(source, 'title', COLLAB_LIMITS.maxTicketTitleUtf16),
|
|
152
|
+
updatedAt: timestamp(source, 'updatedAt'),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function ticketComment(value) {
|
|
156
|
+
const source = record(value, 'ticket.comment');
|
|
157
|
+
return {
|
|
158
|
+
authorMemberId: string(source, 'authorMemberId', 64, isCollabMemberId),
|
|
159
|
+
body: string(source, 'body', COLLAB_LIMITS.maxTicketCommentBytes, undefined, 'utf8'),
|
|
160
|
+
createdAt: timestamp(source, 'createdAt'),
|
|
161
|
+
id: string(source, 'id', 128, isCollabOpaqueId),
|
|
162
|
+
ticketId: string(source, 'ticketId', 128, isCollabOpaqueId),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function acceptedTicketRelation(value) {
|
|
166
|
+
const source = record(value, 'ticket.acceptedRelation');
|
|
167
|
+
const kind = source.kind;
|
|
168
|
+
if (kind !== 'references' && kind !== 'resolves') {
|
|
169
|
+
throw decodeError('ticket.acceptedRelation.kind');
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
acceptedAt: timestamp(source, 'acceptedAt'),
|
|
173
|
+
acceptedMergeOid: string(source, 'acceptedMergeOid', 64, isCollabGitOid),
|
|
174
|
+
commitOid: string(source, 'commitOid', 64, isCollabGitOid),
|
|
175
|
+
id: string(source, 'id', 128, isCollabOpaqueId),
|
|
176
|
+
kind,
|
|
177
|
+
requestId: string(source, 'requestId', 128, isCollabOpaqueId),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function pageCursor(source) {
|
|
181
|
+
return source.nextCursor === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: string(source, 'nextCursor', COLLAB_LIMITS.maxPageCursorUtf16);
|
|
184
|
+
}
|
|
185
|
+
function commentPage(value) {
|
|
186
|
+
const source = record(value, 'commentPage');
|
|
187
|
+
assertJsonUtf8ByteLengthAtMost(source, COLLAB_LIMITS.commentPageMaxUtf8Bytes, 'commentPage.bytes');
|
|
188
|
+
if (!Array.isArray(source.comments)
|
|
189
|
+
|| source.comments.length > COLLAB_LIMITS.maxCommentPageSize) {
|
|
190
|
+
throw decodeError('commentPage');
|
|
191
|
+
}
|
|
192
|
+
const nextCursor = pageCursor(source);
|
|
193
|
+
return {
|
|
194
|
+
comments: source.comments.map(comment),
|
|
195
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function ticketCommentPage(value) {
|
|
199
|
+
const source = record(value, 'ticketCommentPage');
|
|
200
|
+
assertJsonUtf8ByteLengthAtMost(source, COLLAB_LIMITS.commentPageMaxUtf8Bytes, 'ticketCommentPage.bytes');
|
|
201
|
+
if (!Array.isArray(source.comments)
|
|
202
|
+
|| source.comments.length > COLLAB_LIMITS.maxCommentPageSize) {
|
|
203
|
+
throw decodeError('ticketCommentPage');
|
|
204
|
+
}
|
|
205
|
+
const nextCursor = pageCursor(source);
|
|
206
|
+
return {
|
|
207
|
+
comments: source.comments.map(ticketComment),
|
|
208
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function acceptedRelationPage(value) {
|
|
212
|
+
const source = record(value, 'ticketAcceptedRelationPage');
|
|
213
|
+
assertJsonUtf8ByteLengthAtMost(source, COLLAB_LIMITS.relationPageMaxUtf8Bytes, 'ticketAcceptedRelationPage.bytes');
|
|
214
|
+
if (!Array.isArray(source.acceptedRelations)
|
|
215
|
+
|| source.acceptedRelations.length > COLLAB_LIMITS.maxRelationsPerPage) {
|
|
216
|
+
throw decodeError('ticketAcceptedRelationPage');
|
|
217
|
+
}
|
|
218
|
+
const nextCursor = pageCursor(source);
|
|
219
|
+
return {
|
|
220
|
+
acceptedRelations: source.acceptedRelations.map(acceptedTicketRelation),
|
|
221
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function ticketDetail(value) {
|
|
225
|
+
const source = record(value, 'ticketDetail');
|
|
226
|
+
assertJsonUtf8ByteLengthAtMost(source, COLLAB_LIMITS.detailMaxUtf8Bytes, 'ticketDetail.bytes');
|
|
227
|
+
if (!isRecord(source.comments)
|
|
228
|
+
|| !isRecord(source.acceptedRelations)) {
|
|
229
|
+
throw decodeError('ticketDetail');
|
|
230
|
+
}
|
|
231
|
+
const decodedTicket = ticketSummary(source.ticket);
|
|
232
|
+
const comments = ticketCommentPage(source.comments);
|
|
233
|
+
if (comments.comments.some(commentValue => commentValue.ticketId !== decodedTicket.id)) {
|
|
234
|
+
throw decodeError('ticketDetail.comments');
|
|
235
|
+
}
|
|
236
|
+
const acceptedRelations = acceptedRelationPage(source.acceptedRelations);
|
|
237
|
+
return {
|
|
238
|
+
acceptedRelations,
|
|
239
|
+
body: string(source, 'body', COLLAB_LIMITS.maxTicketBodyBytes, undefined, 'utf8'),
|
|
240
|
+
comments,
|
|
241
|
+
ticket: decodedTicket,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function comment(value) {
|
|
245
|
+
const source = record(value, 'comment');
|
|
246
|
+
return {
|
|
247
|
+
authorMemberId: string(source, 'authorMemberId', 64, isCollabMemberId),
|
|
248
|
+
body: string(source, 'body', COLLAB_LIMITS.maxCommentBytes, undefined, 'utf8'),
|
|
249
|
+
createdAt: timestamp(source, 'createdAt'),
|
|
250
|
+
id: string(source, 'id', 128, isCollabOpaqueId),
|
|
251
|
+
requestId: string(source, 'requestId', 128, isCollabOpaqueId),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function envelopeData(value) {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
export function decodeEnsureMyRequestResponse(value) {
|
|
258
|
+
const data = record(envelopeData(value), 'data');
|
|
259
|
+
return {
|
|
260
|
+
mainOid: string(data, 'mainOid', 64, isCollabGitOid),
|
|
261
|
+
request: changeRequest(data.request),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export function decodeRequestDetailResponse(value) {
|
|
265
|
+
const data = record(envelopeData(value), 'data');
|
|
266
|
+
assertJsonUtf8ByteLengthAtMost(data, COLLAB_LIMITS.detailMaxUtf8Bytes, 'requestDetail.bytes');
|
|
267
|
+
if (!isRecord(data.comments)
|
|
268
|
+
|| data.changedFiles !== undefined) {
|
|
269
|
+
throw decodeError('requestDetail');
|
|
270
|
+
}
|
|
271
|
+
const decodedRequest = changeRequest(data.request);
|
|
272
|
+
const reviewedHeadOid = string(data, 'reviewedHeadOid', 64, isCollabGitOid);
|
|
273
|
+
const reviewCondition = data.reviewCondition;
|
|
274
|
+
const comments = commentPage(data.comments);
|
|
275
|
+
if (reviewedHeadOid !== decodedRequest.latestHeadOid
|
|
276
|
+
|| !['clean', 'conflicting', 'stale'].includes(String(reviewCondition))
|
|
277
|
+
|| comments.comments.some(item => item.requestId !== decodedRequest.id)) {
|
|
278
|
+
throw decodeError('requestDetail');
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
comments,
|
|
282
|
+
currentMainOid: string(data, 'currentMainOid', 64, isCollabGitOid),
|
|
283
|
+
request: decodedRequest,
|
|
284
|
+
reviewCondition: reviewCondition,
|
|
285
|
+
reviewedHeadOid,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export function decodeCommentPageResponse(value) {
|
|
289
|
+
return commentPage(record(envelopeData(value), 'data'));
|
|
290
|
+
}
|
|
291
|
+
export function decodeTicketCommentPageResponse(value) {
|
|
292
|
+
return ticketCommentPage(record(envelopeData(value), 'data'));
|
|
293
|
+
}
|
|
294
|
+
export function decodeTicketAcceptedRelationPageResponse(value) {
|
|
295
|
+
return acceptedRelationPage(record(envelopeData(value), 'data'));
|
|
296
|
+
}
|
|
297
|
+
export function decodeCreateCommentResponse(value) {
|
|
298
|
+
const data = record(envelopeData(value), 'data');
|
|
299
|
+
const decodedRequest = changeRequest(data.request);
|
|
300
|
+
const decodedComment = comment(data.comment);
|
|
301
|
+
if (decodedComment.requestId !== decodedRequest.id
|
|
302
|
+
|| decodedRequest.commentCount < 1) {
|
|
303
|
+
throw decodeError('commentResponse');
|
|
304
|
+
}
|
|
305
|
+
return { comment: decodedComment, request: decodedRequest };
|
|
306
|
+
}
|
|
307
|
+
export function decodeAcceptResponse(value) {
|
|
308
|
+
const data = record(envelopeData(value), 'data');
|
|
309
|
+
const mainOid = string(data, 'mainOid', 64, isCollabGitOid);
|
|
310
|
+
const mergeCommitOid = string(data, 'mergeCommitOid', 64, isCollabGitOid);
|
|
311
|
+
const decodedRequest = changeRequest(data.request);
|
|
312
|
+
if (mergeCommitOid !== mainOid
|
|
313
|
+
|| decodedRequest.status !== 'merged'
|
|
314
|
+
|| decodedRequest.mergedOid !== mainOid) {
|
|
315
|
+
throw decodeError('acceptResponse');
|
|
316
|
+
}
|
|
317
|
+
return { mainOid, mergeCommitOid, request: decodedRequest };
|
|
318
|
+
}
|
|
319
|
+
export function decodeTicketPageResponse(value) {
|
|
320
|
+
const data = record(envelopeData(value), 'data');
|
|
321
|
+
assertJsonUtf8ByteLengthAtMost(data, COLLAB_LIMITS.ticketPageMaxUtf8Bytes, 'ticketPage.bytes');
|
|
322
|
+
if (!Array.isArray(data.tickets)
|
|
323
|
+
|| data.tickets.length > COLLAB_LIMITS.maxTicketPageSize)
|
|
324
|
+
throw decodeError('ticketPage');
|
|
325
|
+
const nextCursor = data.nextCursor === undefined
|
|
326
|
+
? undefined
|
|
327
|
+
: string(data, 'nextCursor', 512);
|
|
328
|
+
return {
|
|
329
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
330
|
+
tickets: data.tickets.map(ticketSummary),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export function decodeTicketDetailResponse(value) {
|
|
334
|
+
return ticketDetail(envelopeData(value));
|
|
335
|
+
}
|
|
336
|
+
export function decodeCreateTicketResponse(value) {
|
|
337
|
+
return { ticket: ticketDetail(record(envelopeData(value), 'data').ticket) };
|
|
338
|
+
}
|
|
339
|
+
export function decodeTicketMutationResponse(value) {
|
|
340
|
+
return { ticket: ticketSummary(record(envelopeData(value), 'data').ticket) };
|
|
341
|
+
}
|
|
342
|
+
export function decodeTicketCommentResponse(value) {
|
|
343
|
+
const data = record(envelopeData(value), 'data');
|
|
344
|
+
const decodedTicket = ticketSummary(data.ticket);
|
|
345
|
+
const decodedComment = ticketComment(data.comment);
|
|
346
|
+
if (decodedComment.ticketId !== decodedTicket.id) {
|
|
347
|
+
throw decodeError('ticketCommentResponse');
|
|
348
|
+
}
|
|
349
|
+
return { comment: decodedComment, ticket: decodedTicket };
|
|
350
|
+
}
|
|
351
|
+
export function decodeUpdateRequestMetadataResponse(value) {
|
|
352
|
+
return { request: changeRequest(record(envelopeData(value), 'data').request) };
|
|
353
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { COLLAB_LIMITS } from './CollabConstants.mjs';
|
|
2
|
+
import { maskCollabMarkdownProse } from './CollabMarkdownProse.mjs';
|
|
3
|
+
const CLOSING_KEYWORD_PATTERN = /(?:^|[^A-Za-z])(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)[ \t]*:?[ \t]*$/i;
|
|
4
|
+
const TICKET_REFERENCE_PATTERN = /(^|[^#0-9A-Za-z_])#([1-9][0-9]*)(?![#0-9A-Za-z_])/gm;
|
|
5
|
+
function relationKindBefore(maskedDescription, referenceOffset) {
|
|
6
|
+
const prefix = maskedDescription.slice(0, referenceOffset);
|
|
7
|
+
return CLOSING_KEYWORD_PATTERN.test(prefix) ? 'resolves' : 'references';
|
|
8
|
+
}
|
|
9
|
+
export function parseCollabTicketReferences(description) {
|
|
10
|
+
if (new TextEncoder().encode(description).byteLength >
|
|
11
|
+
COLLAB_LIMITS.maxRequestDescriptionBytes) {
|
|
12
|
+
return { status: 'invalid', reason: 'description-too-large' };
|
|
13
|
+
}
|
|
14
|
+
const scanned = scanCollabTicketReferences(description);
|
|
15
|
+
if (scanned.status === 'invalid')
|
|
16
|
+
return scanned;
|
|
17
|
+
const references = new Map();
|
|
18
|
+
for (const token of scanned.tokens) {
|
|
19
|
+
const existing = references.get(token.ticketNumber);
|
|
20
|
+
if (existing !== 'resolves' || token.kind === 'resolves') {
|
|
21
|
+
references.set(token.ticketNumber, token.kind);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
status: 'ok',
|
|
26
|
+
references: [...references.entries()]
|
|
27
|
+
.sort(([left], [right]) => left - right)
|
|
28
|
+
.map(([ticketNumber, kind]) => ({ ticketNumber, kind })),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function scanCollabTicketReferences(description) {
|
|
32
|
+
const prose = maskCollabMarkdownProse(description);
|
|
33
|
+
const tokens = [];
|
|
34
|
+
for (const match of prose.matchAll(TICKET_REFERENCE_PATTERN)) {
|
|
35
|
+
const prefix = match[1] ?? '';
|
|
36
|
+
const numberToken = match[2];
|
|
37
|
+
if (!numberToken || match.index === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const ticketNumber = Number(numberToken);
|
|
40
|
+
if (!Number.isSafeInteger(ticketNumber)) {
|
|
41
|
+
return { status: 'invalid', reason: 'ticket-number-out-of-range' };
|
|
42
|
+
}
|
|
43
|
+
const referenceOffset = match.index + prefix.length;
|
|
44
|
+
tokens.push({
|
|
45
|
+
from: referenceOffset,
|
|
46
|
+
kind: relationKindBefore(prose, referenceOffset),
|
|
47
|
+
ticketNumber,
|
|
48
|
+
to: referenceOffset + numberToken.length + 1,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return { status: 'ok', tokens };
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const COLLAB_PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
2
|
+
const COLLAB_MEMBER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
3
|
+
const COLLAB_OPAQUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
4
|
+
const COLLAB_GIT_OID_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
5
|
+
export function isCollabProjectId(value) {
|
|
6
|
+
return typeof value === 'string' && COLLAB_PROJECT_ID_PATTERN.test(value);
|
|
7
|
+
}
|
|
8
|
+
export function isCollabMemberId(value) {
|
|
9
|
+
return typeof value === 'string' && COLLAB_MEMBER_ID_PATTERN.test(value);
|
|
10
|
+
}
|
|
11
|
+
export function isCollabOpaqueId(value) {
|
|
12
|
+
return typeof value === 'string' && COLLAB_OPAQUE_ID_PATTERN.test(value);
|
|
13
|
+
}
|
|
14
|
+
export function isCollabGitOid(value) {
|
|
15
|
+
return typeof value === 'string' && COLLAB_GIT_OID_PATTERN.test(value);
|
|
16
|
+
}
|
|
17
|
+
export function hasUtf8ByteLengthAtMost(value, maximum) {
|
|
18
|
+
return new TextEncoder().encode(value).byteLength <= maximum;
|
|
19
|
+
}
|