@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,175 @@
|
|
|
1
|
+
export const COLLAB_ERROR_CODES = Object.freeze([
|
|
2
|
+
'protocol-version-unsupported',
|
|
3
|
+
'protocol-payload-invalid',
|
|
4
|
+
'project-not-found',
|
|
5
|
+
'quota-exceeded',
|
|
6
|
+
'authentication-failed',
|
|
7
|
+
'authorization-denied',
|
|
8
|
+
'membership-revoked',
|
|
9
|
+
'stale-main',
|
|
10
|
+
'stale-request-head',
|
|
11
|
+
'personal-ref-diverged',
|
|
12
|
+
'content-conflict',
|
|
13
|
+
'description-required',
|
|
14
|
+
'request-not-open',
|
|
15
|
+
'request-head-not-pushed',
|
|
16
|
+
'ticket-not-found',
|
|
17
|
+
'resolving-ticket-reference-not-found',
|
|
18
|
+
'ticket-not-open',
|
|
19
|
+
'stale-ticket',
|
|
20
|
+
'stale-request-metadata',
|
|
21
|
+
'authority-not-synchronized',
|
|
22
|
+
'authority-transfer-not-found',
|
|
23
|
+
'authority-transfer-stale',
|
|
24
|
+
'authority-transfer-cancellation-forbidden',
|
|
25
|
+
'membership-claim-invalid',
|
|
26
|
+
'membership-claim-expired',
|
|
27
|
+
'membership-claim-revoked',
|
|
28
|
+
'membership-claim-already-redeemed',
|
|
29
|
+
'project-retired',
|
|
30
|
+
'idempotency-conflict',
|
|
31
|
+
'acceptance-recovery-required',
|
|
32
|
+
'authority-integrity-error',
|
|
33
|
+
'operation-timeout',
|
|
34
|
+
'operation-failed',
|
|
35
|
+
]);
|
|
36
|
+
const COLLAB_RECOVERY_ACTIONS = Object.freeze([
|
|
37
|
+
'retry',
|
|
38
|
+
'review-conflicts',
|
|
39
|
+
'request-access',
|
|
40
|
+
]);
|
|
41
|
+
const COLLAB_ERROR_CODE_SET = new Set(COLLAB_ERROR_CODES);
|
|
42
|
+
const COLLAB_RECOVERY_ACTION_SET = new Set(COLLAB_RECOVERY_ACTIONS);
|
|
43
|
+
const SAFE_CONTEXT_RULES = Object.freeze({
|
|
44
|
+
endpoint: 'path',
|
|
45
|
+
exitCode: 'number',
|
|
46
|
+
field: 'token',
|
|
47
|
+
kind: 'token',
|
|
48
|
+
limit: 'number',
|
|
49
|
+
operation: 'token',
|
|
50
|
+
operationId: 'id',
|
|
51
|
+
path: 'path',
|
|
52
|
+
projectId: 'id',
|
|
53
|
+
quota: 'token',
|
|
54
|
+
reason: 'token',
|
|
55
|
+
receivedVersion: 'number',
|
|
56
|
+
recordKind: 'token',
|
|
57
|
+
retiredAt: 'timestamp',
|
|
58
|
+
side: 'token',
|
|
59
|
+
status: 'token',
|
|
60
|
+
supportedVersion: 'number',
|
|
61
|
+
ticketNumber: 'number',
|
|
62
|
+
});
|
|
63
|
+
const SAFE_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
64
|
+
const SAFE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
65
|
+
function sanitizeAllowedValue(rule, value) {
|
|
66
|
+
switch (rule) {
|
|
67
|
+
case 'number':
|
|
68
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
69
|
+
case 'path':
|
|
70
|
+
return typeof value === 'string' ? '[PATH]' : null;
|
|
71
|
+
case 'timestamp':
|
|
72
|
+
return typeof value === 'string'
|
|
73
|
+
&& !Number.isNaN(Date.parse(value))
|
|
74
|
+
&& new Date(value).toISOString() === value
|
|
75
|
+
? value
|
|
76
|
+
: null;
|
|
77
|
+
case 'id':
|
|
78
|
+
return typeof value === 'string' && SAFE_ID_PATTERN.test(value) ? value : null;
|
|
79
|
+
case 'token':
|
|
80
|
+
return typeof value === 'string' && SAFE_TOKEN_PATTERN.test(value) ? value : null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function sanitizeCollabDiagnosticContext(context = {}) {
|
|
84
|
+
const result = {};
|
|
85
|
+
for (const [key, value] of Object.entries(context)) {
|
|
86
|
+
const rule = SAFE_CONTEXT_RULES[key];
|
|
87
|
+
if (!rule)
|
|
88
|
+
continue;
|
|
89
|
+
const sanitized = sanitizeAllowedValue(rule, value);
|
|
90
|
+
if (sanitized !== null)
|
|
91
|
+
result[key] = sanitized;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
export function collabErrorGroup(code) {
|
|
96
|
+
switch (code) {
|
|
97
|
+
case 'protocol-version-unsupported':
|
|
98
|
+
case 'protocol-payload-invalid':
|
|
99
|
+
return 'setup';
|
|
100
|
+
case 'project-not-found':
|
|
101
|
+
case 'authority-transfer-not-found':
|
|
102
|
+
case 'quota-exceeded':
|
|
103
|
+
return 'path';
|
|
104
|
+
case 'authentication-failed':
|
|
105
|
+
case 'authorization-denied':
|
|
106
|
+
case 'membership-revoked':
|
|
107
|
+
case 'membership-claim-invalid':
|
|
108
|
+
case 'membership-claim-expired':
|
|
109
|
+
case 'membership-claim-revoked':
|
|
110
|
+
return 'authorization';
|
|
111
|
+
case 'stale-main':
|
|
112
|
+
case 'stale-request-head':
|
|
113
|
+
case 'personal-ref-diverged':
|
|
114
|
+
case 'content-conflict':
|
|
115
|
+
case 'description-required':
|
|
116
|
+
case 'request-not-open':
|
|
117
|
+
case 'request-head-not-pushed':
|
|
118
|
+
case 'ticket-not-found':
|
|
119
|
+
case 'resolving-ticket-reference-not-found':
|
|
120
|
+
case 'ticket-not-open':
|
|
121
|
+
case 'stale-ticket':
|
|
122
|
+
case 'stale-request-metadata':
|
|
123
|
+
case 'authority-not-synchronized':
|
|
124
|
+
case 'authority-transfer-stale':
|
|
125
|
+
case 'authority-transfer-cancellation-forbidden':
|
|
126
|
+
case 'membership-claim-already-redeemed':
|
|
127
|
+
case 'project-retired':
|
|
128
|
+
case 'idempotency-conflict':
|
|
129
|
+
return 'state';
|
|
130
|
+
case 'acceptance-recovery-required':
|
|
131
|
+
case 'authority-integrity-error':
|
|
132
|
+
return 'integrity';
|
|
133
|
+
case 'operation-timeout':
|
|
134
|
+
case 'operation-failed':
|
|
135
|
+
return 'operation';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export class CollabError extends Error {
|
|
139
|
+
code;
|
|
140
|
+
group;
|
|
141
|
+
safeContext;
|
|
142
|
+
recoveryActions;
|
|
143
|
+
constructor(options) {
|
|
144
|
+
if (!COLLAB_ERROR_CODE_SET.has(options.code)) {
|
|
145
|
+
throw new TypeError('Unsupported Collab error code');
|
|
146
|
+
}
|
|
147
|
+
if (!(options.recoveryActions ?? []).every(action => COLLAB_RECOVERY_ACTION_SET.has(action))) {
|
|
148
|
+
throw new TypeError('Unsupported Collab recovery action');
|
|
149
|
+
}
|
|
150
|
+
super(`collab.error.${options.code}`);
|
|
151
|
+
this.name = 'CollabError';
|
|
152
|
+
this.code = options.code;
|
|
153
|
+
this.group = collabErrorGroup(options.code);
|
|
154
|
+
this.safeContext = Object.freeze(sanitizeCollabDiagnosticContext(options.safeContext));
|
|
155
|
+
this.recoveryActions = Object.freeze([...(options.recoveryActions ?? [])]);
|
|
156
|
+
if (options.cause !== undefined) {
|
|
157
|
+
Object.defineProperty(this, 'cause', {
|
|
158
|
+
configurable: false,
|
|
159
|
+
enumerable: false,
|
|
160
|
+
value: options.cause,
|
|
161
|
+
writable: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
toJSON() {
|
|
166
|
+
return {
|
|
167
|
+
name: this.name,
|
|
168
|
+
code: this.code,
|
|
169
|
+
group: this.group,
|
|
170
|
+
message: this.message,
|
|
171
|
+
safeContext: this.safeContext,
|
|
172
|
+
recoveryActions: this.recoveryActions,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parser } from '@lezer/markdown';
|
|
2
|
+
const MASKED_MARKDOWN_NODES = new Set([
|
|
3
|
+
'CodeBlock',
|
|
4
|
+
'Comment',
|
|
5
|
+
'CommentBlock',
|
|
6
|
+
'FencedCode',
|
|
7
|
+
'HTMLBlock',
|
|
8
|
+
'HTMLTag',
|
|
9
|
+
'Image',
|
|
10
|
+
'InlineCode',
|
|
11
|
+
'LinkLabel',
|
|
12
|
+
'LinkReference',
|
|
13
|
+
'LinkTitle',
|
|
14
|
+
'ProcessingInstruction',
|
|
15
|
+
'ProcessingInstructionBlock',
|
|
16
|
+
'URL',
|
|
17
|
+
]);
|
|
18
|
+
const BOUNDARY_MASKED_MARKDOWN_NODES = new Set(['Entity']);
|
|
19
|
+
export function maskCollabMarkdownProse(markdown) {
|
|
20
|
+
const maskedRanges = [];
|
|
21
|
+
const escapes = [];
|
|
22
|
+
parser.parse(markdown).iterate({
|
|
23
|
+
enter(node) {
|
|
24
|
+
if (node.name === 'Escape') {
|
|
25
|
+
escapes.push({ from: node.from, to: node.to });
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (MASKED_MARKDOWN_NODES.has(node.name)
|
|
29
|
+
|| BOUNDARY_MASKED_MARKDOWN_NODES.has(node.name)) {
|
|
30
|
+
maskedRanges.push({
|
|
31
|
+
from: node.from,
|
|
32
|
+
preservesBoundary: BOUNDARY_MASKED_MARKDOWN_NODES.has(node.name),
|
|
33
|
+
to: node.to,
|
|
34
|
+
});
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const output = markdown.split('');
|
|
41
|
+
for (const range of maskedRanges) {
|
|
42
|
+
for (let offset = range.from; offset < range.to; offset += 1) {
|
|
43
|
+
if (markdown[offset] !== '\n' && markdown[offset] !== '\r') {
|
|
44
|
+
output[offset] = range.preservesBoundary ? '_' : ' ';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const escape of escapes) {
|
|
49
|
+
const escaped = markdown.slice(escape.from + 1, escape.to);
|
|
50
|
+
output[escape.from] = ' ';
|
|
51
|
+
output[escape.from + 1] = escaped === '@' || escaped === '#' ? '_' : escaped;
|
|
52
|
+
}
|
|
53
|
+
return output.join('');
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { maskCollabMarkdownProse } from './CollabMarkdownProse.mjs';
|
|
2
|
+
const MENTION_ADJACENT_CHARACTER = /[\p{L}\p{N}_@-]/u;
|
|
3
|
+
export function parseCollabMemberMentions(markdown, targets) {
|
|
4
|
+
const candidates = mentionCandidates(targets);
|
|
5
|
+
if (candidates.length === 0)
|
|
6
|
+
return [];
|
|
7
|
+
const prose = maskCollabMarkdownProse(markdown);
|
|
8
|
+
const memberIds = new Set();
|
|
9
|
+
let searchFrom = 0;
|
|
10
|
+
while (searchFrom < prose.length) {
|
|
11
|
+
const mentionStart = prose.indexOf('@', searchFrom);
|
|
12
|
+
if (mentionStart < 0)
|
|
13
|
+
break;
|
|
14
|
+
searchFrom = mentionStart + 1;
|
|
15
|
+
const preceding = prose[mentionStart - 1];
|
|
16
|
+
if (preceding !== undefined && MENTION_ADJACENT_CHARACTER.test(preceding))
|
|
17
|
+
continue;
|
|
18
|
+
const candidate = candidates.find(entry => {
|
|
19
|
+
if (!prose.startsWith(entry.name, mentionStart + 1))
|
|
20
|
+
return false;
|
|
21
|
+
const following = prose[mentionStart + entry.name.length + 1];
|
|
22
|
+
return following === undefined || !MENTION_ADJACENT_CHARACTER.test(following);
|
|
23
|
+
});
|
|
24
|
+
if (!candidate)
|
|
25
|
+
continue;
|
|
26
|
+
memberIds.add(candidate.memberId);
|
|
27
|
+
searchFrom = mentionStart + candidate.name.length + 1;
|
|
28
|
+
}
|
|
29
|
+
return [...memberIds];
|
|
30
|
+
}
|
|
31
|
+
function mentionCandidates(targets) {
|
|
32
|
+
const byName = new Map();
|
|
33
|
+
for (const target of targets) {
|
|
34
|
+
const name = target.displayName.trim();
|
|
35
|
+
if (name.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
const existing = byName.get(name);
|
|
38
|
+
if (existing === undefined) {
|
|
39
|
+
byName.set(name, { memberId: target.memberId, name });
|
|
40
|
+
}
|
|
41
|
+
else if (existing?.memberId !== target.memberId) {
|
|
42
|
+
byName.set(name, null);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...byName.values()]
|
|
46
|
+
.filter((candidate) => candidate !== null)
|
|
47
|
+
.sort((left, right) => right.name.length - left.name.length);
|
|
48
|
+
}
|